Skip to content

Commit 032d3c2

Browse files
Nils Barsnbars
authored andcommitted
Fix scoreboard chart and ranking issues
- Disable dataZoom slider with fewer than 2 data points (ECharts bug) - Use notMerge on ChallengePlot setOption to match PointsOverTimeChart - Filter zero-score teams from rankings and challenge plots - Move time-window filtering to backend, simplify frontend ranking - Add assignment names to chart boundary markers
1 parent 6755a30 commit 032d3c2

4 files changed

Lines changed: 59 additions & 51 deletions

File tree

spa-frontend/src/components/scoreboard/ChallengePlot.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ function buildSeries() {
5353
const teams = (props.submissions && props.submissions[props.challengeName]) || {};
5454
const baseline = findBaseline();
5555
const mark = getMarkLineColors();
56-
return Object.entries(teams).map(([team, points], index) => {
56+
return Object.entries(teams).filter(([, points]) =>
57+
points.some((e) => Number(e.score) > 0),
58+
).map(([team, points], index) => {
5759
const parsed = points
5860
.map((entry) => {
5961
const d = parseApiDate(entry.ts);
@@ -132,8 +134,12 @@ function buildOption(): EChartsOption {
132134
const series = buildSeries();
133135
const xMin = earliestX(series);
134136
const common = buildCommonOptions(xMin);
137+
const totalPoints = series.reduce((n, s) => n + s.data.length, 0);
135138
return {
136139
...common,
140+
// ECharts renders a broken zoom slider when there is only one data
141+
// point — disable it entirely in that case.
142+
...(totalPoints < 2 ? { dataZoom: [] } : {}),
137143
tooltip: {
138144
...(common.tooltip ?? {}),
139145
trigger: 'item',
@@ -168,7 +174,7 @@ function buildOption(): EChartsOption {
168174
function render() {
169175
if (!root.value) return;
170176
if (!chart) chart = mountChart(root.value);
171-
chart.chart.setOption(buildOption());
177+
chart.chart.setOption(buildOption(), { notMerge: true });
172178
}
173179
174180
let offTheme: (() => void) | null = null;

spa-frontend/src/components/scoreboard/PointsOverTimeChart.vue

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import {
1313
type ManagedChart,
1414
} from './chartSetup';
1515
import type { ScoresOverTime } from '../../ranking/types';
16+
import type { AssignmentBoundary } from '../../ranking/util';
1617
1718
const props = defineProps<{
1819
scoresOverTime: ScoresOverTime;
19-
assignmentBoundaries: Date[];
20+
assignmentBoundaries: AssignmentBoundary[];
2021
}>();
2122
2223
const root = ref<HTMLDivElement | null>(null);
@@ -58,15 +59,15 @@ function buildSeries() {
5859
verticalAlign: 'middle' as const,
5960
color: mark.label,
6061
formatter: ({ dataIndex }: { dataIndex: number }) =>
61-
`Assignment ${dataIndex + 1}`,
62+
props.assignmentBoundaries[dataIndex]?.name ?? `Assignment ${dataIndex + 1}`,
6263
},
6364
lineStyle: {
6465
color: mark.line,
6566
type: 'dashed' as const,
6667
width: 1,
6768
},
68-
data: props.assignmentBoundaries.map((time) => ({
69-
xAxis: time.getTime(),
69+
data: props.assignmentBoundaries.map((b) => ({
70+
xAxis: b.date.getTime(),
7071
})),
7172
}
7273
: undefined,
@@ -95,15 +96,19 @@ function dataRange(series: Array<{ data: Array<{ value: [number, number] }> }>):
9596
function buildOption(): EChartsOption {
9697
const series = buildSeries();
9798
const { min: dataMin, max: dataMax } = dataRange(series);
98-
const boundaries = props.assignmentBoundaries.map((d) => d.getTime());
99+
const boundaries = props.assignmentBoundaries.map((b) => b.date.getTime());
99100
const allMins = [dataMin, ...boundaries].filter((n) => Number.isFinite(n));
100101
const allMaxs = [dataMax, ...boundaries].filter((n) => Number.isFinite(n));
101102
const xMin = allMins.length ? Math.min(...allMins) : 0;
102103
const xMax = allMaxs.length ? Math.max(...allMaxs) : 0;
103104
const pad = xMax > xMin ? (xMax - xMin) * 0.02 : 0;
104105
const common = buildCommonOptions(xMin - pad, xMax + pad);
106+
const totalPoints = series.reduce((n, s) => n + s.data.length, 0);
105107
return {
106108
...common,
109+
// ECharts renders a broken zoom slider when there is only one data
110+
// point — disable it entirely in that case.
111+
...(totalPoints < 2 ? { dataZoom: [] } : {}),
107112
tooltip: {
108113
...(common.tooltip ?? {}),
109114
trigger: 'axis',
@@ -131,7 +136,7 @@ function buildOption(): EChartsOption {
131136
function render() {
132137
if (!root.value) return;
133138
if (!chart) chart = mountChart(root.value);
134-
chart.chart.setOption(buildOption());
139+
chart.chart.setOption(buildOption(), { notMerge: true });
135140
}
136141
137142
let offTheme: (() => void) | null = null;

spa-frontend/src/ranking/best_sum.ts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,22 @@ function bestPerChallenge(
1717
assignments: Assignments,
1818
submissions: SubmissionsByChallenge,
1919
): Record<string, Record<string, number>> {
20-
const best: Record<string, Record<string, number>> = {};
20+
const knownChallenges = new Set<string>();
2121
for (const challenges of Object.values(assignments || {})) {
22-
for (const [name, cfg] of Object.entries(challenges || {})) {
23-
const cStart = parseApiDate(cfg.start);
24-
const cEnd = parseApiDate(cfg.end);
25-
if (!cStart || !cEnd) continue;
26-
const teams = (submissions && submissions[name]) || {};
27-
if (!best[name]) best[name] = {};
28-
for (const team of Object.keys(teams)) {
29-
for (const entry of teams[team] || []) {
30-
const ts = parseApiDate(entry.ts);
31-
if (!ts || ts < cStart || ts > cEnd) continue;
32-
const score = Number(entry.score);
33-
if (!Number.isFinite(score)) continue;
34-
if (!(team in best[name]) || score > best[name][team]) {
35-
best[name][team] = score;
36-
}
22+
for (const name of Object.keys(challenges || {})) {
23+
knownChallenges.add(name);
24+
}
25+
}
26+
const best: Record<string, Record<string, number>> = {};
27+
for (const [name, teams] of Object.entries(submissions || {})) {
28+
if (!knownChallenges.has(name)) continue;
29+
if (!best[name]) best[name] = {};
30+
for (const team of Object.keys(teams)) {
31+
for (const entry of teams[team] || []) {
32+
const score = Number(entry.score);
33+
if (!Number.isFinite(score)) continue;
34+
if (!(team in best[name]) || score > best[name][team]) {
35+
best[name][team] = score;
3736
}
3837
}
3938
}
@@ -52,7 +51,7 @@ export function getRanking(
5251
totals[team] = (totals[team] || 0) + score;
5352
}
5453
}
55-
return Object.entries(totals).sort((a, b) => b[1] - a[1]);
54+
return Object.entries(totals).filter(([, score]) => score > 0).sort((a, b) => b[1] - a[1]);
5655
}
5756

5857
export function computeChartScoresOverTime(
@@ -74,20 +73,21 @@ export function computeChartScoresOverTime(
7473
}
7574

7675
const events: Ev[] = [];
76+
const knownChallenges = new Set<string>();
7777
for (const challenges of Object.values(assignments || {})) {
78-
for (const [name, cfg] of Object.entries(challenges || {})) {
79-
const cStart = parseApiDate(cfg.start);
80-
const cEnd = parseApiDate(cfg.end);
81-
if (!cStart || !cEnd) continue;
82-
const teams = (submissions && submissions[name]) || {};
83-
for (const team of Object.keys(teams)) {
84-
for (const entry of teams[team] || []) {
85-
const ts = parseApiDate(entry.ts);
86-
if (!ts || ts < cStart || ts > cEnd) continue;
87-
const score = Number(entry.score);
88-
if (!Number.isFinite(score)) continue;
89-
events.push({ ts, team, challenge: name, score });
90-
}
78+
for (const name of Object.keys(challenges || {})) {
79+
knownChallenges.add(name);
80+
}
81+
}
82+
for (const [name, teams] of Object.entries(submissions || {})) {
83+
if (!knownChallenges.has(name)) continue;
84+
for (const team of Object.keys(teams)) {
85+
for (const entry of teams[team] || []) {
86+
const ts = parseApiDate(entry.ts);
87+
if (!ts) continue;
88+
const score = Number(entry.score);
89+
if (!Number.isFinite(score)) continue;
90+
events.push({ ts, team, challenge: name, score });
9191
}
9292
}
9393
}
@@ -105,8 +105,8 @@ export function computeChartScoresOverTime(
105105
if (ev.score > prev) {
106106
totals[ev.team] += ev.score - prev;
107107
bestPer[ev.team][ev.challenge] = ev.score;
108+
out[ev.team].push({ time: ev.ts.getTime(), score: totals[ev.team] });
108109
}
109-
out[ev.team].push({ time: ev.ts.getTime(), score: totals[ev.team] });
110110
}
111111

112112
for (const team of teamSet) {

spa-frontend/src/ranking/util.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,11 @@ export function getBadges(
6969
): Badges {
7070
const out: Badges = {};
7171
for (const challenges of Object.values(assignments || {})) {
72-
for (const [challenge, cfg] of Object.entries(challenges || {})) {
73-
const cStart = parseApiDate(cfg.start);
74-
const cEnd = parseApiDate(cfg.end);
75-
if (!cStart || !cEnd) continue;
72+
for (const [challenge] of Object.entries(challenges || {})) {
7673
const teams = (submissions && submissions[challenge]) || {};
7774
for (const team of Object.keys(teams)) {
7875
let earned = false;
7976
for (const entry of teams[team] || []) {
80-
const ts = parseApiDate(entry.ts);
81-
if (!ts || ts < cStart || ts > cEnd) continue;
8277
if (Number(entry.score) > 0) {
8378
earned = true;
8479
break;
@@ -138,18 +133,20 @@ export function getActiveAssignmentName(
138133
return activeName ?? upcomingName;
139134
}
140135

136+
export type AssignmentBoundary = { name: string; date: Date };
137+
141138
export function computeAssignmentStartTimes(
142139
assignments: Assignments,
143-
): Date[] {
144-
const times: Date[] = [];
145-
for (const challenges of Object.values(assignments || {})) {
140+
): AssignmentBoundary[] {
141+
const boundaries: AssignmentBoundary[] = [];
142+
for (const [name, challenges] of Object.entries(assignments || {})) {
146143
let earliest: Date | null = null;
147144
for (const ch of Object.values(challenges || {}) as ChallengeCfg[]) {
148145
const s = parseApiDate(ch.start);
149146
if (s && (!earliest || s < earliest)) earliest = s;
150147
}
151-
if (earliest) times.push(earliest);
148+
if (earliest) boundaries.push({ name, date: earliest });
152149
}
153-
times.sort((a, b) => a.getTime() - b.getTime());
154-
return times;
150+
boundaries.sort((a, b) => a.date.getTime() - b.date.getTime());
151+
return boundaries;
155152
}

0 commit comments

Comments
 (0)