Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ For development, besides the JDK, addtitionally [NPM](https://www.npmjs.com/) an

We kindly ask you to refer to the following paper in publications mentioning or employing DRES:

> Loris Sauter, Ralph Gasser, Heiko Schuldt, Abraham Bernstein, and Luca Rossetto. 2024. Performance Evaluation in Multimedia Retrieval. ACM Transactions on Multimedia Computing, Communications and Applications. https://doi.org/10.1145/3678881

**Link:** https://dl.acm.org/doi/10.1145/3678881

**Bibtex:**

```
@article{10.1145/3678881,
author = {Sauter, Loris and Gasser, Ralph and Schuldt, Heiko and Bernstein, Abraham and Rossetto, Luca},
title = {Performance Evaluation in Multimedia Retrieval},
year = {2024},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
issn = {1551-6857},
url = {https://doi.org/10.1145/3678881},
doi = {10.1145/3678881},
journal = {ACM Transactions on Multimedia Computing, Communications and Applications},
month = oct,
keywords = {Interactive Multimedia Retrieval, Retrieval Evaluation, Interactive Evaluation, Evaluation System}
}
```

We also have a demo publication:

> Rossetto L., Gasser R., Sauter L., Bernstein A., Schuldt H. (2021) A System for Interactive Multimedia Retrieval Evaluations. In: Lokoč J. et al. (eds) MultiMedia Modeling. MMM 2021. Lecture Notes in Computer Science, vol 12573. Springer, Cham.

**Link:** https://doi.org/10.1007/978-3-030-67835-7_33
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/app/error/error-dialog.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { MatDialog } from "@angular/material/dialog";
import { ErrorDialogComponent } from "./error-dialog/error-dialog.component";
import { environment } from "../../environments/environment";

@Injectable()
export class ErrorDialogService {
Expand All @@ -12,6 +13,11 @@ export class ErrorDialogService {
}

openDialog(message: string):void {
if (!environment.showErrorPopups) {
console.warn('[Silent Error]:', message);
return;
}

if(!this.opened){
this.opened = true;
const dialogRef = this.dialog.open(ErrorDialogComponent, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="graph-wrapper" *ngIf="graphData$ | async as data">

<div class="legend-container">
<div class="legend-item" *ngFor="let group of data.legend">
<div class="color-swatch" [style.backgroundColor]="group.color"></div>
<span class="group-name">{{ group.name }}</span>
</div>
</div>

<div class="chart-scroll-area">
<div class="chart-row" *ngFor="let team of data.teams">

<div class="row-label">
<span class="name" [matTooltip]="team.name">{{ team.name }}</span>
</div>

<div class="track-container">
<div class="track">
<div
class="segment"
*ngFor="let seg of team.segments"
[style.width.%]="seg.widthInPercent"
[style.backgroundColor]="seg.color"
[matTooltip]="seg.name + ': ' + seg.value">
</div>
</div>
</div>

</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
:host {
display: block;
position: relative;
height: 100%;
width: 100%;
}

.graph-wrapper {
position: absolute;
top: 0; bottom: 0; left: 0; right: 0;
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.1);
padding: 10px;
box-sizing: border-box;
}

/* --- LEGEND --- */
.legend-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
padding-bottom: 8px;
border-bottom: 1px solid #444;
margin-bottom: 8px;
flex-shrink: 0;
}

.legend-item {
display: flex;
align-items: center;
gap: 6px;

.color-swatch { width: 10px; height: 10px; border-radius: 2px; }
.group-name { color: #ccc; font-size: 11px; text-transform: uppercase; font-weight: 600; }
}

/* --- SCROLLABLE CHART --- */
.chart-scroll-area {
flex: 1;
overflow-y: auto;
padding-right: 5px;
display: flex;
flex-direction: column;
gap: 2px;
}

.chart-row {
display: flex;
align-items: center;
gap: 6px;
height: 12px;

&:hover .track { filter: brightness(1.2); }
}

.row-label {
width: 55px;
flex-shrink: 0;
display: flex;
align-items: center;

.name {
flex: 1;
font-size: 11px;
color: #bbb;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

.track-container {
flex: 1;
}

.track {
width: 100%;
height: 8px;
background: rgba(255, 255, 255, 0.05);
border-radius: 1px;
display: flex;
overflow: hidden;
}

.segment {
height: 100%;
transition: width 0.4s ease-in-out;
cursor: pointer;

&:hover { filter: brightness(1.3); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable, combineLatest, of } from 'rxjs';
import { catchError, map, switchMap, filter } from 'rxjs/operators';
import { ApiEvaluationInfo, ApiEvaluationState, EvaluationScoresService } from '../../../../openapi';

@Component({
selector: 'app-compact-score-graph',
templateUrl: './compact-score-graph.component.html',
styleUrls: ['./compact-score-graph.component.scss']
})
export class CompactScoreGraphComponent implements OnInit {
@Input() info: Observable<ApiEvaluationInfo>;
@Input() state: Observable<ApiEvaluationState>;

graphData$: Observable<any>;

colorPalette = [
'#9b59b6', '#3498db', '#e67e22', '#2ecc71', '#e74c3c',
'#1abc9c', '#f1c40f', '#34495e', '#ff9ff3', '#feca57',
'#ff6b6b', '#48dbfb', '#1dd1a1', '#5f27cd', '#c8d6e5',
'#22a6b3', '#badc58', '#eb4d4b', '#686de0', '#30336b'
];

constructor(private scoreService: EvaluationScoresService) {}

ngOnInit(): void {
const rawScores$ = this.state.pipe(
filter(s => !!s && !!s.evaluationId),
switchMap(s => this.scoreService.getApiV2ScoreEvaluationByEvaluationId(s.evaluationId).pipe(
catchError(() => of([]))
))
);

this.graphData$ = combineLatest([this.info, rawScores$]).pipe(
map(([info, scores]) => {
if (!info || !info.teams || !scores) return null;

const validGroups = scores.filter(s => s.name !== 'sum' && s.name !== 'average');
const legend = validGroups.map((g, i) => ({
name: g.name,
color: this.colorPalette[i % this.colorPalette.length]
}));

let globalMaxScore = 0;

const teamsData = info.teams.map(team => {
let totalScore = 0;
const segments: any[] = [];

validGroups.forEach((group, index) => {
const teamScoreObj = group.scores?.find(ss => ss.teamId === team.id);
const value = teamScoreObj ? Math.round(teamScoreObj.score) : 0;

totalScore += value;
segments.push({
name: group.name,
value: value,
color: this.colorPalette[index % this.colorPalette.length]
});
});

if (totalScore > globalMaxScore) globalMaxScore = totalScore;

return { name: team.name, total: totalScore, segments };
});

teamsData.sort((a, b) => b.total - a.total);
const chartScaleMax = Math.max(globalMaxScore, 1000);

teamsData.forEach(team => {
team.segments.forEach(seg => {
seg.widthInPercent = (seg.value / chartScaleMax) * 100;
});
});

return { teams: teamsData, legend: legend };
})
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="compact-teams-container">
<div class="teams-grid" *ngIf="teamsData$ | async as teams">

<div
*ngFor="let team of teams"
class="tile"
[class.status-correct]="team.correct > 0"
[class.status-wrong]="team.wrong > 0 && team.correct === 0"
[class.gold]="team.rank === 1 && team.score > 0"
[class.silver]="team.rank === 2 && team.score > 0"
[class.bronze]="team.rank === 3 && team.score > 0"
>
<div class="tile-header">
<span class="rank-badge">#{{ team.rank }}</span>
<h3 [matTooltip]="team.name">{{ team.name }}</h3>
</div>

<p class="score">
{{ team.score | number: '1.0-0' }}
</p>

<p class="counter">
<span class="CORRECT">{{ team.correct }}</span>
<span class="WRONG">{{ team.wrong }}</span>
<span class="INDETERMINATE">{{ team.indeterminate }}</span>
</p>
</div>

</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
:host {
display: block;
width: 100%;
height: 100%;
overflow: hidden;
}

.compact-teams-container {
height: 100%;
padding: 8px;
box-sizing: border-box;
overflow-y: auto; /* Scroll if 50 teams don't fit vertically */
}

.teams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 6px;
align-content: start;
}

.tile {
min-height: 70px;
padding: 4px;
border-radius: 6px;
border: 2px solid #333;
background: #2c2c2e;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;

.tile-header {
display: flex;
align-items: center;
gap: 6px;
background: rgba(0, 0, 0, 0.2);
padding: 2px 4px;
border-radius: 4px;

.rank-badge {
background: #444;
color: #fff;
font-size: 10px;
font-weight: bold;
padding: 2px 4px;
border-radius: 4px;
flex-shrink: 0;
}

h3 {
font-size: clamp(10px, 1.1vw, 13px);
margin: 0;
font-weight: 600;
color: #eee;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

p.score {
font-size: clamp(16px, 1.8vw, 20px);
font-weight: bold;
color: #fff;
margin: 4px 0;
text-align: center;
&.dres-fix-correct { color: #00ff9d; }
}

p.counter {
font-size: 11px;
font-weight: 600;
margin: 0;
display: flex;
justify-content: center;
gap: 8px;

span.CORRECT { color: #2ecc71; }
span.WRONG { color: #e74c3c; }
span.INDETERMINATE { color: #95a5a6; }
}

&.status-correct {
background: radial-gradient(circle at top left, rgba(46, 204, 113, 0.45) 0%, rgba(46, 204, 113, 0) 100%), #2c2c2e;
border-color: rgba(46, 204, 113, 0.8);
}
&.status-wrong {
background: radial-gradient(circle at top left, rgba(231, 76, 60, 0.45) 0%, rgba(231, 76, 60, 0) 100%), #2c2c2e;
border-color: rgba(231, 76, 60, 0.8);
}
&.gold { border-color: #FFD700; .tile-header .rank-badge { background: #FFD700; color: #000; } }
&.silver { border-color: #C0C0C0; .tile-header .rank-badge { background: #C0C0C0; color: #000; } }
&.bronze { border-color: #CD7F32; .tile-header .rank-badge { background: #CD7F32; color: #000; } }
}
Loading