alexshah commited on
Commit
4f0b1aa
·
1 Parent(s): 9d8c5be
Files changed (1) hide show
  1. app.js +1354 -251
app.js CHANGED
@@ -19,18 +19,25 @@ const state = {
19
  colorScores: false,
20
  availability: new Set(["Open", "Closed"]),
21
  availabilityMenuOpen: false,
 
 
 
22
  };
23
 
24
  function setTheme(theme, persist = true) {
25
  document.documentElement.dataset.theme = theme;
26
  const toggle = document.querySelector("#theme-toggle");
 
27
  if (toggle) {
28
  const nextTheme = theme === "dark" ? "light" : "dark";
29
  toggle.setAttribute("aria-label", `Switch to ${nextTheme} mode`);
30
  toggle.title = `Switch to ${nextTheme} mode`;
31
  }
 
32
  if (persist) {
33
- try { localStorage.setItem("arm-asr-theme", theme); } catch (_) {}
 
 
34
  }
35
  }
36
 
@@ -42,6 +49,7 @@ function parseCsv(text) {
42
 
43
  for (let index = 0; index < text.length; index += 1) {
44
  const char = text[index];
 
45
  if (quoted) {
46
  if (char === '"' && text[index + 1] === '"') {
47
  field += '"';
@@ -58,7 +66,11 @@ function parseCsv(text) {
58
  field = "";
59
  } else if (char === "\n") {
60
  row.push(field.replace(/\r$/, ""));
61
- if (row.some((value) => value !== "")) rows.push(row);
 
 
 
 
62
  row = [];
63
  field = "";
64
  } else {
@@ -70,30 +82,47 @@ function parseCsv(text) {
70
  row.push(field.replace(/\r$/, ""));
71
  rows.push(row);
72
  }
 
73
  return rows;
74
  }
75
 
76
  function ingest(rows) {
77
  const [sections, datasets, metrics] = rows.slice(0, 3);
 
78
  const modelIndex = sections.indexOf("model");
79
  const urlIndex = sections.indexOf("model_url");
80
  const availabilityIndex = sections.indexOf("availability");
81
  const standardIndex = sections.indexOf("standard");
 
82
  let currentDataset = "";
83
 
84
  for (let index = standardIndex; index < sections.length; index += 1) {
85
- if (datasets[index]) currentDataset = datasets[index];
 
 
 
86
  if (currentDataset && METRICS.includes(metrics[index])) {
87
- state.columns.set(`${currentDataset}|${metrics[index]}`, index);
88
- if (!state.datasets.includes(currentDataset)) state.datasets.push(currentDataset);
 
 
 
 
 
 
89
  }
90
  }
91
 
92
  state.records = rows.slice(3).map((row) => {
93
  const values = {};
 
94
  for (const [key, index] of state.columns) {
95
- values[key] = row[index] === "" || row[index] === undefined ? null : Number(row[index]);
 
 
 
96
  }
 
97
  return {
98
  model: row[modelIndex],
99
  url: row[urlIndex],
@@ -104,325 +133,1399 @@ function ingest(rows) {
104
  }
105
 
106
  function currentDataset() {
107
- return document.querySelector('input[name="dataset"]:checked')?.value || state.datasets[0];
 
 
 
108
  }
109
 
110
  function currentMatrixMetric() {
111
- return document.querySelector('input[name="matrix-metric"]:checked')?.value || "WER";
 
 
 
112
  }
113
 
114
  function rankRecords(records, dataset, metric = "WER") {
115
  const rankKey = `${dataset}|${metric}`;
 
116
  const ranked = [...records].sort((left, right) => {
117
  const a = left.values[rankKey];
118
  const b = right.values[rankKey];
 
119
  if (a === null) return 1;
120
  if (b === null) return -1;
 
121
  return a - b || left.model.localeCompare(right.model);
122
  });
123
- return new Map(ranked.map((record, index) => [record.model, index + 1]));
 
 
 
 
 
 
124
  }
125
 
126
  function makeCell(tag, text, className = "") {
127
  const cell = document.createElement(tag);
128
  cell.textContent = text;
129
- if (className) cell.className = className;
 
 
 
 
130
  return cell;
131
  }
132
 
133
  function matrixRanges(columns) {
134
- const familyValues = { wer: [], cer: [] };
 
 
 
 
135
  columns.forEach((column) => {
136
  const metric = column.key.split("|")[1];
137
  const family = metric.startsWith("CER") ? "cer" : "wer";
 
138
  state.records.forEach((record) => {
139
  const value = record.values[column.key];
140
- if (value !== null) familyValues[family].push(value);
 
 
 
141
  });
142
  });
143
 
144
- const familyRanges = Object.fromEntries(Object.entries(familyValues).map(([family, values]) => [
145
- family,
146
- values.length ? { min: Math.min(...values), max: Math.max(...values) } : null,
147
- ]));
 
 
 
 
 
 
 
148
 
149
- return new Map(columns.map((column) => {
150
- const metric = column.key.split("|")[1];
151
- const family = metric.startsWith("CER") ? "cer" : "wer";
152
- return [column.key, familyRanges[family]];
153
- }));
 
 
 
 
 
 
154
  }
155
 
156
  function heatClass(value, range) {
157
- if (value === null || !range || range.max === range.min) return "";
158
- const ratio = (value - range.min) / (range.max - range.min);
159
- return ` heat-score-${Math.min(5, Math.floor(ratio * 5) + 1)}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
 
162
- function render() {
163
- const dataset = currentDataset();
164
- const matrixMetric = currentMatrixMetric();
165
- const isMatrix = state.view === "matrix";
166
- const columns = isMatrix
167
- ? state.datasets.map((item) => ({ label: DATASET_LABELS[item] || item, key: `${item}|${matrixMetric}` }))
168
- : METRICS.map((metric) => ({ label: metric, key: `${dataset}|${metric}` }));
169
- const table = document.querySelector("#leaderboard-table");
170
- const head = table.querySelector("thead");
171
- const body = table.querySelector("tbody");
172
- const rankDataset = isMatrix ? "combined" : dataset;
173
- const rankMetric = isMatrix ? matrixMetric : "WER";
174
- const ranks = rankRecords(state.records, rankDataset, rankMetric);
175
- const ranges = state.colorScores ? matrixRanges(columns) : new Map();
176
-
177
- let records = state.records.filter((record) => state.availability.has(record.availability));
178
-
179
- if (columns.some((column) => column.key === state.sortKey)) {
180
- const direction = state.sortDirection === "asc" ? 1 : -1;
181
- records = [...records].sort((left, right) => {
182
- const a = left.values[state.sortKey];
183
- const b = right.values[state.sortKey];
184
- if (a === null) return 1;
185
- if (b === null) return -1;
186
- return (a - b) * direction || left.model.localeCompare(right.model);
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  });
 
 
 
 
 
 
188
  }
189
 
190
- const headerRow = document.createElement("tr");
191
- const rankHeader = makeCell("th", "Rank");
192
- rankHeader.title = `Ranked by ${rankMetric} for ${DATASET_LABELS[rankDataset] || rankDataset}`;
193
- const availabilityHeader = document.createElement("th");
194
- const availabilityControl = document.createElement("span");
195
- availabilityControl.className = "availability-control";
196
- const availabilityLabel = document.createElement("button");
197
- availabilityLabel.type = "button";
198
- availabilityLabel.className = "availability-label";
199
- availabilityLabel.setAttribute("aria-expanded", String(state.availabilityMenuOpen));
200
- availabilityLabel.setAttribute("aria-label", "Filter models by availability");
201
- const availabilitySummary = state.availability.size === 2
202
- ? "All"
203
- : [...state.availability][0];
204
- availabilityLabel.innerHTML = `Availability <span>${availabilitySummary}</span><b aria-hidden="true">⌄</b>`;
205
- availabilityLabel.addEventListener("click", (event) => {
206
- event.stopPropagation();
207
- state.availabilityMenuOpen = !state.availabilityMenuOpen;
208
- render();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  });
210
- const availabilityFilters = document.createElement("span");
211
- availabilityFilters.className = "availability-filters";
212
- availabilityFilters.hidden = !state.availabilityMenuOpen;
213
- availabilityFilters.addEventListener("click", (event) => event.stopPropagation());
214
- ["Open", "Closed"].forEach((value) => {
215
- const button = document.createElement("button");
216
- button.type = "button";
217
- button.className = `availability-filter ${value.toLowerCase()}`;
218
- button.classList.toggle("is-active", state.availability.has(value));
219
- button.setAttribute("aria-pressed", String(state.availability.has(value)));
220
- button.setAttribute("aria-label", `Toggle ${value.toLowerCase()} models`);
221
- button.textContent = value;
222
- button.addEventListener("click", () => {
223
- if (state.availability.has(value) && state.availability.size > 1) state.availability.delete(value);
224
- else state.availability.add(value);
225
- render();
226
- });
227
- availabilityFilters.append(button);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  });
229
- availabilityControl.append(availabilityLabel, availabilityFilters);
230
- availabilityHeader.append(availabilityControl);
231
- headerRow.append(rankHeader, makeCell("th", "Model"), availabilityHeader);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  columns.forEach((column) => {
233
- const header = document.createElement("th");
234
- if (isMatrix && column.key.startsWith("combined|")) header.classList.add("combined-score");
235
- const button = document.createElement("button");
236
- button.type = "button";
237
- button.className = "sort-button";
238
- if (state.sortKey === column.key) button.classList.add(`is-${state.sortDirection}`);
239
- button.textContent = column.label;
240
- button.setAttribute("aria-label", `Sort by ${column.label}`);
241
- button.addEventListener("click", () => {
242
- if (state.sortKey === column.key) {
243
- state.sortDirection = state.sortDirection === "asc" ? "desc" : "asc";
244
- } else {
245
- state.sortKey = column.key;
246
- state.sortDirection = "asc";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
- render();
249
- });
250
  header.append(button);
251
  headerRow.append(header);
252
  });
253
- head.replaceChildren(headerRow);
254
 
255
- const rows = records.map((record) => {
256
- const row = document.createElement("tr");
257
- const modelCell = document.createElement("td");
258
- const modelWrap = document.createElement("div");
259
- modelWrap.className = "model-cell";
260
- const model = document.createElement("span");
261
- model.className = "model-name";
262
- model.textContent = record.model;
263
- modelWrap.append(model);
264
- if (record.url) {
265
- const link = document.createElement("a");
266
- link.className = "model-link";
267
- link.href = record.url;
268
- link.target = "_blank";
269
- link.rel = "noreferrer";
270
- link.title = `Open ${record.model}`;
271
- link.setAttribute("aria-label", `Open ${record.model} model page`);
272
- link.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5m0-5-8 8M19 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h4"/></svg>';
273
- modelWrap.append(link);
274
- }
275
- modelCell.append(modelWrap);
276
 
277
- const rank = ranks.get(record.model);
278
- const rankCell = makeCell("td", rank ? `#${rank}` : "—", `rank${rank <= 3 ? " rank-top" : ""}`);
 
 
279
 
280
- const availabilityCell = document.createElement("td");
281
- const badge = document.createElement("span");
282
- badge.className = `availability-badge ${record.availability.toLowerCase()}`;
283
- badge.textContent = record.availability;
284
- availabilityCell.append(badge);
285
- row.append(rankCell, modelCell, availabilityCell);
286
 
287
- columns.forEach((column) => {
288
- const value = record.values[column.key];
289
- const heat = state.colorScores ? `heat-cell${heatClass(value, ranges.get(column.key))}` : "";
290
- const cell = makeCell("td", value === null ? "—" : value.toFixed(2), heat);
291
- if (isMatrix && column.key.startsWith("combined|")) cell.classList.add("combined-score");
292
- row.append(cell);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  });
294
- return row;
295
- });
296
- body.replaceChildren(...rows);
297
-
298
- document.querySelector("#table-title").textContent = isMatrix
299
- ? `${matrixMetric} across all datasets`
300
- : `${DATASET_LABELS[dataset] || dataset} leaderboard`;
301
- const colorNote = state.colorScores
302
- ? " Colors are scaled separately within the WER and CER metric families: green is better, red is worse."
303
- : "";
304
- document.querySelector("#table-description").textContent = (isMatrix
305
- ? `Rank is based on Overall ${matrixMetric}. Scores are percentages; lower is better.`
306
- : "Rank is based on WER. Scores are percentages; lower is better.") + colorNote;
307
- document.querySelector("#result-count").textContent = `${records.length} ${records.length === 1 ? "model" : "models"}`;
308
- document.querySelector("#empty-state").hidden = records.length !== 0;
309
- table.hidden = records.length === 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  }
311
 
312
  function setView(view) {
313
- state.view = view;
314
- const isMatrix = view === "matrix";
315
- document.querySelector("#leaderboards").classList.toggle("matrix-view", isMatrix);
316
- document.querySelector(".matrix-metric-field").hidden = !isMatrix;
317
- document.querySelectorAll(".view-button").forEach((button) => {
318
- const active = button.dataset.view === view;
319
- button.classList.toggle("is-active", active);
320
- button.setAttribute("aria-pressed", String(active));
321
- });
322
- state.sortKey = isMatrix
323
- ? `combined|${currentMatrixMetric()}`
324
- : `${currentDataset()}|WER`;
325
- state.sortDirection = "asc";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  render();
327
  }
328
 
329
- function activateTab(name, updateHash = true) {
330
- const validName = ["leaderboards", "datasets", "about"].includes(name) ? name : "leaderboards";
331
- document.querySelectorAll(".tab").forEach((tab) => {
332
- const active = tab.dataset.tab === validName;
333
- tab.classList.toggle("is-active", active);
334
- tab.setAttribute("aria-selected", String(active));
335
- });
336
- document.querySelectorAll(".panel").forEach((panel) => {
337
- const active = panel.id === validName;
338
- panel.classList.toggle("is-active", active);
339
- panel.hidden = !active;
340
- });
341
- if (updateHash) history.replaceState(null, "", `#${validName}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  }
343
 
344
  function buildControls() {
345
- const datasetOptions = document.querySelector("#dataset-options");
346
- state.datasets.forEach((dataset, index) => {
347
- const label = document.createElement("label");
348
- label.className = "choice-pill";
349
- if (dataset === "combined") label.classList.add("combined-choice");
350
- const input = document.createElement("input");
351
- input.type = "radio";
352
- input.name = "dataset";
353
- input.value = dataset;
354
- input.checked = index === 0;
355
- input.addEventListener("change", () => {
356
- state.sortKey = `${dataset}|WER`;
357
- state.sortDirection = "asc";
358
- render();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  });
360
- label.append(input, document.createTextNode(DATASET_LABELS[dataset] || dataset));
361
- datasetOptions.append(label);
362
- });
363
 
364
- const matrixOptions = document.querySelector("#matrix-metric-options");
365
- METRICS.forEach((metric, index) => {
366
- const matrixLabel = document.createElement("label");
367
- matrixLabel.className = "choice-pill";
368
- const matrixInput = document.createElement("input");
369
- matrixInput.type = "radio";
370
- matrixInput.name = "matrix-metric";
371
- matrixInput.value = metric;
372
- matrixInput.checked = index === 0;
373
- matrixInput.addEventListener("change", () => {
374
- state.sortKey = `combined|${metric}`;
375
- state.sortDirection = "asc";
376
- render();
377
  });
378
- matrixLabel.append(matrixInput, document.createTextNode(metric));
379
- matrixOptions.append(matrixLabel);
380
- });
381
 
382
- document.querySelector("#color-toggle").addEventListener("click", (event) => {
383
- state.colorScores = !state.colorScores;
384
- event.currentTarget.classList.toggle("is-active", state.colorScores);
385
- event.currentTarget.setAttribute("aria-pressed", String(state.colorScores));
386
- render();
387
- });
388
- document.querySelectorAll(".view-button").forEach((button) => button.addEventListener("click", () => setView(button.dataset.view)));
389
- document.querySelectorAll(".tab").forEach((tab) => tab.addEventListener("click", () => activateTab(tab.dataset.tab)));
390
- document.querySelector("#theme-toggle").addEventListener("click", () => {
391
- setTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
392
- });
393
- document.addEventListener("click", () => {
394
- if (!state.availabilityMenuOpen) return;
395
- state.availabilityMenuOpen = false;
396
- render();
397
- });
398
- document.addEventListener("keydown", (event) => {
399
- if (event.key !== "Escape" || !state.availabilityMenuOpen) return;
400
- state.availabilityMenuOpen = false;
401
- render();
402
- });
403
- setTheme(document.documentElement.dataset.theme, false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  }
405
 
406
  async function start() {
407
  try {
408
- const response = await fetch("results.csv");
409
- if (!response.ok) throw new Error(`Could not load results.csv (${response.status})`);
410
- ingest(parseCsv(await response.text()));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  buildControls();
412
- activateTab(location.hash.slice(1), false);
 
 
 
 
 
 
413
  render();
 
414
  } catch (error) {
415
- document.querySelector("#table-title").textContent = "Leaderboard unavailable";
416
- document.querySelector("#leaderboard-table").hidden = true;
417
- const empty = document.querySelector("#empty-state");
418
- empty.hidden = false;
419
- empty.querySelector("h3").textContent = "Could not load results";
420
- empty.querySelector("p").textContent = error.message;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  }
422
  }
423
 
424
- if (typeof module !== "undefined") {
425
- module.exports = { heatClass, ingest, matrixRanges, parseCsv, rankRecords, setTheme, state };
 
 
 
 
 
 
 
 
 
 
 
426
  }
427
 
428
- if (typeof document !== "undefined") start();
 
 
 
 
 
 
19
  colorScores: false,
20
  availability: new Set(["Open", "Closed"]),
21
  availabilityMenuOpen: false,
22
+ chartDataset: "combined",
23
+ chartMetric: "WER",
24
+ chartAvailability: "All",
25
  };
26
 
27
  function setTheme(theme, persist = true) {
28
  document.documentElement.dataset.theme = theme;
29
  const toggle = document.querySelector("#theme-toggle");
30
+
31
  if (toggle) {
32
  const nextTheme = theme === "dark" ? "light" : "dark";
33
  toggle.setAttribute("aria-label", `Switch to ${nextTheme} mode`);
34
  toggle.title = `Switch to ${nextTheme} mode`;
35
  }
36
+
37
  if (persist) {
38
+ try {
39
+ localStorage.setItem("arm-asr-theme", theme);
40
+ } catch (_) {}
41
  }
42
  }
43
 
 
49
 
50
  for (let index = 0; index < text.length; index += 1) {
51
  const char = text[index];
52
+
53
  if (quoted) {
54
  if (char === '"' && text[index + 1] === '"') {
55
  field += '"';
 
66
  field = "";
67
  } else if (char === "\n") {
68
  row.push(field.replace(/\r$/, ""));
69
+
70
+ if (row.some((value) => value !== "")) {
71
+ rows.push(row);
72
+ }
73
+
74
  row = [];
75
  field = "";
76
  } else {
 
82
  row.push(field.replace(/\r$/, ""));
83
  rows.push(row);
84
  }
85
+
86
  return rows;
87
  }
88
 
89
  function ingest(rows) {
90
  const [sections, datasets, metrics] = rows.slice(0, 3);
91
+
92
  const modelIndex = sections.indexOf("model");
93
  const urlIndex = sections.indexOf("model_url");
94
  const availabilityIndex = sections.indexOf("availability");
95
  const standardIndex = sections.indexOf("standard");
96
+
97
  let currentDataset = "";
98
 
99
  for (let index = standardIndex; index < sections.length; index += 1) {
100
+ if (datasets[index]) {
101
+ currentDataset = datasets[index];
102
+ }
103
+
104
  if (currentDataset && METRICS.includes(metrics[index])) {
105
+ state.columns.set(
106
+ `${currentDataset}|${metrics[index]}`,
107
+ index
108
+ );
109
+
110
+ if (!state.datasets.includes(currentDataset)) {
111
+ state.datasets.push(currentDataset);
112
+ }
113
  }
114
  }
115
 
116
  state.records = rows.slice(3).map((row) => {
117
  const values = {};
118
+
119
  for (const [key, index] of state.columns) {
120
+ values[key] =
121
+ row[index] === "" || row[index] === undefined
122
+ ? null
123
+ : Number(row[index]);
124
  }
125
+
126
  return {
127
  model: row[modelIndex],
128
  url: row[urlIndex],
 
133
  }
134
 
135
  function currentDataset() {
136
+ return (
137
+ document.querySelector('input[name="dataset"]:checked')?.value ||
138
+ state.datasets[0]
139
+ );
140
  }
141
 
142
  function currentMatrixMetric() {
143
+ return (
144
+ document.querySelector('input[name="matrix-metric"]:checked')?.value ||
145
+ "WER"
146
+ );
147
  }
148
 
149
  function rankRecords(records, dataset, metric = "WER") {
150
  const rankKey = `${dataset}|${metric}`;
151
+
152
  const ranked = [...records].sort((left, right) => {
153
  const a = left.values[rankKey];
154
  const b = right.values[rankKey];
155
+
156
  if (a === null) return 1;
157
  if (b === null) return -1;
158
+
159
  return a - b || left.model.localeCompare(right.model);
160
  });
161
+
162
+ return new Map(
163
+ ranked.map((record, index) => [
164
+ record.model,
165
+ index + 1,
166
+ ])
167
+ );
168
  }
169
 
170
  function makeCell(tag, text, className = "") {
171
  const cell = document.createElement(tag);
172
  cell.textContent = text;
173
+
174
+ if (className) {
175
+ cell.className = className;
176
+ }
177
+
178
  return cell;
179
  }
180
 
181
  function matrixRanges(columns) {
182
+ const familyValues = {
183
+ wer: [],
184
+ cer: [],
185
+ };
186
+
187
  columns.forEach((column) => {
188
  const metric = column.key.split("|")[1];
189
  const family = metric.startsWith("CER") ? "cer" : "wer";
190
+
191
  state.records.forEach((record) => {
192
  const value = record.values[column.key];
193
+
194
+ if (value !== null) {
195
+ familyValues[family].push(value);
196
+ }
197
  });
198
  });
199
 
200
+ const familyRanges = Object.fromEntries(
201
+ Object.entries(familyValues).map(([family, values]) => [
202
+ family,
203
+ values.length
204
+ ? {
205
+ min: Math.min(...values),
206
+ max: Math.max(...values),
207
+ }
208
+ : null,
209
+ ])
210
+ );
211
 
212
+ return new Map(
213
+ columns.map((column) => {
214
+ const metric = column.key.split("|")[1];
215
+ const family = metric.startsWith("CER") ? "cer" : "wer";
216
+
217
+ return [
218
+ column.key,
219
+ familyRanges[family],
220
+ ];
221
+ })
222
+ );
223
  }
224
 
225
  function heatClass(value, range) {
226
+ if (
227
+ value === null ||
228
+ !range ||
229
+ range.max === range.min
230
+ ) {
231
+ return "";
232
+ }
233
+
234
+ const ratio =
235
+ (value - range.min) /
236
+ (range.max - range.min);
237
+
238
+ return ` heat-score-${Math.min(
239
+ 5,
240
+ Math.floor(ratio * 5) + 1
241
+ )}`;
242
  }
243
 
244
+ function renderComparisonChart() {
245
+ const container =
246
+ document.querySelector("#comparison-bars");
247
+
248
+ const empty =
249
+ document.querySelector("#comparison-chart-empty");
250
+
251
+ if (!container || !empty) {
252
+ return;
253
+ }
254
+
255
+ const key =
256
+ `${state.chartDataset}|${state.chartMetric}`;
257
+
258
+ const records = state.records
259
+ .filter((record) => {
260
+ return (
261
+ state.chartAvailability === "All" ||
262
+ record.availability === state.chartAvailability
263
+ );
264
+ })
265
+ .map((record) => ({
266
+ model: record.model,
267
+ url: record.url,
268
+ availability: record.availability,
269
+ value: record.values[key],
270
+ }))
271
+ .filter((record) => {
272
+ return (
273
+ record.value !== null &&
274
+ Number.isFinite(record.value)
275
+ );
276
+ })
277
+ .sort((left, right) => {
278
+ return (
279
+ left.value - right.value ||
280
+ left.model.localeCompare(right.model)
281
+ );
282
  });
283
+
284
+ empty.hidden = records.length !== 0;
285
+
286
+ if (!records.length) {
287
+ container.replaceChildren();
288
+ return;
289
  }
290
 
291
+ const maxValue = Math.max(
292
+ ...records.map((record) => record.value)
293
+ );
294
+
295
+ const rows = records.map((record) => {
296
+ const row =
297
+ document.createElement("div");
298
+
299
+ row.className =
300
+ "comparison-bar-row";
301
+
302
+ const modelWrap =
303
+ document.createElement("div");
304
+
305
+ modelWrap.className =
306
+ "comparison-model";
307
+
308
+ const availabilityDot =
309
+ document.createElement("span");
310
+
311
+ availabilityDot.className =
312
+ `comparison-availability-dot ${record.availability.toLowerCase()}`;
313
+
314
+ availabilityDot.title =
315
+ record.availability === "Open"
316
+ ? "Open model"
317
+ : "Closed model";
318
+
319
+ availabilityDot.setAttribute(
320
+ "aria-label",
321
+ record.availability === "Open"
322
+ ? "Open model"
323
+ : "Closed model"
324
+ );
325
+
326
+ modelWrap.append(
327
+ availabilityDot
328
+ );
329
+
330
+ if (record.url) {
331
+ const modelLink =
332
+ document.createElement("a");
333
+
334
+ modelLink.className =
335
+ "comparison-model-link";
336
+
337
+ modelLink.href =
338
+ record.url;
339
+
340
+ modelLink.target =
341
+ "_blank";
342
+
343
+ modelLink.rel =
344
+ "noreferrer";
345
+
346
+ modelLink.title =
347
+ `Open ${record.model}`;
348
+
349
+ modelLink.setAttribute(
350
+ "aria-label",
351
+ `Open ${record.model} model page`
352
+ );
353
+
354
+ const modelName =
355
+ document.createElement("span");
356
+
357
+ modelName.textContent =
358
+ record.model;
359
+
360
+ const icon =
361
+ document.createElement("svg");
362
+
363
+ icon.setAttribute(
364
+ "viewBox",
365
+ "0 0 24 24"
366
+ );
367
+
368
+ icon.setAttribute(
369
+ "aria-hidden",
370
+ "true"
371
+ );
372
+
373
+ icon.innerHTML =
374
+ '<path d="M14 5h5v5m0-5-8 8M19 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h4"/>';
375
+
376
+ modelLink.append(
377
+ modelName,
378
+ icon
379
+ );
380
+
381
+ modelWrap.append(
382
+ modelLink
383
+ );
384
+ } else {
385
+ const modelName =
386
+ document.createElement("span");
387
+
388
+ modelName.className =
389
+ "comparison-model-name";
390
+
391
+ modelName.textContent =
392
+ record.model;
393
+
394
+ modelName.title =
395
+ record.model;
396
+
397
+ modelWrap.append(
398
+ modelName
399
+ );
400
+ }
401
+
402
+ const track =
403
+ document.createElement("div");
404
+
405
+ track.className =
406
+ "comparison-track";
407
+
408
+ track.setAttribute(
409
+ "aria-hidden",
410
+ "true"
411
+ );
412
+
413
+ const fill =
414
+ document.createElement("div");
415
+
416
+ fill.className =
417
+ "comparison-fill";
418
+
419
+ fill.style.width = `${
420
+ maxValue
421
+ ? (record.value / maxValue) * 100
422
+ : 0
423
+ }%`;
424
+
425
+ track.append(fill);
426
+
427
+ const value =
428
+ document.createElement("div");
429
+
430
+ value.className =
431
+ "comparison-value";
432
+
433
+ value.textContent =
434
+ `${record.value.toFixed(2)}%`;
435
+
436
+ row.setAttribute(
437
+ "aria-label",
438
+ `${record.model}: ${record.value.toFixed(
439
+ 2
440
+ )} percent ${state.chartMetric}, ${record.availability}`
441
+ );
442
+
443
+ row.append(
444
+ modelWrap,
445
+ track,
446
+ value
447
+ );
448
+
449
+ return row;
450
  });
451
+
452
+ container.replaceChildren(
453
+ ...rows
454
+ );
455
+ }
456
+
457
+ function buildChartControls() {
458
+ const datasetSelect =
459
+ document.querySelector("#chart-dataset");
460
+
461
+ const metricSelect =
462
+ document.querySelector("#chart-metric");
463
+
464
+ const availabilitySelect =
465
+ document.querySelector("#chart-availability");
466
+
467
+ if (
468
+ !datasetSelect ||
469
+ !metricSelect ||
470
+ !availabilitySelect
471
+ ) {
472
+ return;
473
+ }
474
+
475
+ state.datasets.forEach((dataset) => {
476
+ const option =
477
+ document.createElement("option");
478
+
479
+ option.value =
480
+ dataset;
481
+
482
+ option.textContent =
483
+ DATASET_LABELS[dataset] ||
484
+ dataset;
485
+
486
+ datasetSelect.append(
487
+ option
488
+ );
489
+ });
490
+
491
+ METRICS.forEach((metric) => {
492
+ const option =
493
+ document.createElement("option");
494
+
495
+ option.value =
496
+ metric;
497
+
498
+ option.textContent =
499
+ metric;
500
+
501
+ metricSelect.append(
502
+ option
503
+ );
504
  });
505
+
506
+ state.chartDataset =
507
+ state.datasets.includes("combined")
508
+ ? "combined"
509
+ : state.datasets[0];
510
+
511
+ state.chartMetric =
512
+ "WER";
513
+
514
+ state.chartAvailability =
515
+ "All";
516
+
517
+ datasetSelect.value =
518
+ state.chartDataset;
519
+
520
+ metricSelect.value =
521
+ state.chartMetric;
522
+
523
+ availabilitySelect.value =
524
+ state.chartAvailability;
525
+
526
+ datasetSelect.addEventListener(
527
+ "change",
528
+ () => {
529
+ state.chartDataset =
530
+ datasetSelect.value;
531
+
532
+ renderComparisonChart();
533
+ }
534
+ );
535
+
536
+ metricSelect.addEventListener(
537
+ "change",
538
+ () => {
539
+ state.chartMetric =
540
+ metricSelect.value;
541
+
542
+ renderComparisonChart();
543
+ }
544
+ );
545
+
546
+ availabilitySelect.addEventListener(
547
+ "change",
548
+ () => {
549
+ state.chartAvailability =
550
+ availabilitySelect.value;
551
+
552
+ renderComparisonChart();
553
+ }
554
+ );
555
+ }
556
+
557
+ function render() {
558
+ const dataset =
559
+ currentDataset();
560
+
561
+ const matrixMetric =
562
+ currentMatrixMetric();
563
+
564
+ const isMatrix =
565
+ state.view === "matrix";
566
+
567
+ const columns = isMatrix
568
+ ? state.datasets.map((item) => ({
569
+ label:
570
+ DATASET_LABELS[item] ||
571
+ item,
572
+ key:
573
+ `${item}|${matrixMetric}`,
574
+ }))
575
+ : METRICS.map((metric) => ({
576
+ label:
577
+ metric,
578
+ key:
579
+ `${dataset}|${metric}`,
580
+ }));
581
+
582
+ const table =
583
+ document.querySelector(
584
+ "#leaderboard-table"
585
+ );
586
+
587
+ const head =
588
+ table.querySelector("thead");
589
+
590
+ const body =
591
+ table.querySelector("tbody");
592
+
593
+ const rankDataset =
594
+ isMatrix
595
+ ? "combined"
596
+ : dataset;
597
+
598
+ const rankMetric =
599
+ isMatrix
600
+ ? matrixMetric
601
+ : "WER";
602
+
603
+ const ranks =
604
+ rankRecords(
605
+ state.records,
606
+ rankDataset,
607
+ rankMetric
608
+ );
609
+
610
+ const ranges =
611
+ state.colorScores
612
+ ? matrixRanges(columns)
613
+ : new Map();
614
+
615
+ let records =
616
+ state.records.filter(
617
+ (record) =>
618
+ state.availability.has(
619
+ record.availability
620
+ )
621
+ );
622
+
623
+ if (
624
+ columns.some(
625
+ (column) =>
626
+ column.key === state.sortKey
627
+ )
628
+ ) {
629
+ const direction =
630
+ state.sortDirection === "asc"
631
+ ? 1
632
+ : -1;
633
+
634
+ records =
635
+ [...records].sort(
636
+ (left, right) => {
637
+ const a =
638
+ left.values[
639
+ state.sortKey
640
+ ];
641
+
642
+ const b =
643
+ right.values[
644
+ state.sortKey
645
+ ];
646
+
647
+ if (a === null) return 1;
648
+ if (b === null) return -1;
649
+
650
+ return (
651
+ (a - b) * direction ||
652
+ left.model.localeCompare(
653
+ right.model
654
+ )
655
+ );
656
+ }
657
+ );
658
+ }
659
+
660
+ const headerRow =
661
+ document.createElement("tr");
662
+
663
+ const rankHeader =
664
+ makeCell("th", "Rank");
665
+
666
+ rankHeader.title =
667
+ `Ranked by ${rankMetric} for ${
668
+ DATASET_LABELS[rankDataset] ||
669
+ rankDataset
670
+ }`;
671
+
672
+ const availabilityHeader =
673
+ document.createElement("th");
674
+
675
+ const availabilityControl =
676
+ document.createElement("span");
677
+
678
+ availabilityControl.className =
679
+ "availability-control";
680
+
681
+ const availabilityLabel =
682
+ document.createElement("button");
683
+
684
+ availabilityLabel.type =
685
+ "button";
686
+
687
+ availabilityLabel.className =
688
+ "availability-label";
689
+
690
+ availabilityLabel.setAttribute(
691
+ "aria-expanded",
692
+ String(
693
+ state.availabilityMenuOpen
694
+ )
695
+ );
696
+
697
+ availabilityLabel.setAttribute(
698
+ "aria-label",
699
+ "Filter models by availability"
700
+ );
701
+
702
+ const availabilitySummary =
703
+ state.availability.size === 2
704
+ ? "All"
705
+ : [...state.availability][0];
706
+
707
+ availabilityLabel.innerHTML =
708
+ `Availability <span>${availabilitySummary}</span><b aria-hidden="true">⌄</b>`;
709
+
710
+ availabilityLabel.addEventListener(
711
+ "click",
712
+ (event) => {
713
+ event.stopPropagation();
714
+
715
+ state.availabilityMenuOpen =
716
+ !state.availabilityMenuOpen;
717
+
718
+ render();
719
+ }
720
+ );
721
+
722
+ const availabilityFilters =
723
+ document.createElement("span");
724
+
725
+ availabilityFilters.className =
726
+ "availability-filters";
727
+
728
+ availabilityFilters.hidden =
729
+ !state.availabilityMenuOpen;
730
+
731
+ availabilityFilters.addEventListener(
732
+ "click",
733
+ (event) =>
734
+ event.stopPropagation()
735
+ );
736
+
737
+ ["Open", "Closed"].forEach(
738
+ (value) => {
739
+ const button =
740
+ document.createElement(
741
+ "button"
742
+ );
743
+
744
+ button.type =
745
+ "button";
746
+
747
+ button.className =
748
+ `availability-filter ${value.toLowerCase()}`;
749
+
750
+ button.classList.toggle(
751
+ "is-active",
752
+ state.availability.has(
753
+ value
754
+ )
755
+ );
756
+
757
+ button.setAttribute(
758
+ "aria-pressed",
759
+ String(
760
+ state.availability.has(
761
+ value
762
+ )
763
+ )
764
+ );
765
+
766
+ button.setAttribute(
767
+ "aria-label",
768
+ `Toggle ${value.toLowerCase()} models`
769
+ );
770
+
771
+ button.textContent =
772
+ value;
773
+
774
+ button.addEventListener(
775
+ "click",
776
+ () => {
777
+ if (
778
+ state.availability.has(
779
+ value
780
+ ) &&
781
+ state.availability.size > 1
782
+ ) {
783
+ state.availability.delete(
784
+ value
785
+ );
786
+ } else {
787
+ state.availability.add(
788
+ value
789
+ );
790
+ }
791
+
792
+ render();
793
+ }
794
+ );
795
+
796
+ availabilityFilters.append(
797
+ button
798
+ );
799
+ }
800
+ );
801
+
802
+ availabilityControl.append(
803
+ availabilityLabel,
804
+ availabilityFilters
805
+ );
806
+
807
+ availabilityHeader.append(
808
+ availabilityControl
809
+ );
810
+
811
+ headerRow.append(
812
+ rankHeader,
813
+ makeCell("th", "Model"),
814
+ availabilityHeader
815
+ );
816
+
817
  columns.forEach((column) => {
818
+ const header =
819
+ document.createElement("th");
820
+
821
+ if (
822
+ isMatrix &&
823
+ column.key.startsWith(
824
+ "combined|"
825
+ )
826
+ ) {
827
+ header.classList.add(
828
+ "combined-score"
829
+ );
830
+ }
831
+
832
+ const button =
833
+ document.createElement(
834
+ "button"
835
+ );
836
+
837
+ button.type =
838
+ "button";
839
+
840
+ button.className =
841
+ "sort-button";
842
+
843
+ if (
844
+ state.sortKey ===
845
+ column.key
846
+ ) {
847
+ button.classList.add(
848
+ `is-${state.sortDirection}`
849
+ );
850
+ }
851
+
852
+ button.textContent =
853
+ column.label;
854
+
855
+ button.setAttribute(
856
+ "aria-label",
857
+ `Sort by ${column.label}`
858
+ );
859
+
860
+ button.addEventListener(
861
+ "click",
862
+ () => {
863
+ if (
864
+ state.sortKey ===
865
+ column.key
866
+ ) {
867
+ state.sortDirection =
868
+ state.sortDirection ===
869
+ "asc"
870
+ ? "desc"
871
+ : "asc";
872
+ } else {
873
+ state.sortKey =
874
+ column.key;
875
+
876
+ state.sortDirection =
877
+ "asc";
878
+ }
879
+
880
+ render();
881
  }
882
+ );
883
+
884
  header.append(button);
885
  headerRow.append(header);
886
  });
 
887
 
888
+ head.replaceChildren(
889
+ headerRow
890
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
 
892
+ const rows =
893
+ records.map((record) => {
894
+ const row =
895
+ document.createElement("tr");
896
 
897
+ const modelCell =
898
+ document.createElement(
899
+ "td"
900
+ );
 
 
901
 
902
+ const modelWrap =
903
+ document.createElement(
904
+ "div"
905
+ );
906
+
907
+ modelWrap.className =
908
+ "model-cell";
909
+
910
+ const model =
911
+ document.createElement(
912
+ "span"
913
+ );
914
+
915
+ model.className =
916
+ "model-name";
917
+
918
+ model.textContent =
919
+ record.model;
920
+
921
+ modelWrap.append(
922
+ model
923
+ );
924
+
925
+ if (record.url) {
926
+ const link =
927
+ document.createElement(
928
+ "a"
929
+ );
930
+
931
+ link.className =
932
+ "model-link";
933
+
934
+ link.href =
935
+ record.url;
936
+
937
+ link.target =
938
+ "_blank";
939
+
940
+ link.rel =
941
+ "noreferrer";
942
+
943
+ link.title =
944
+ `Open ${record.model}`;
945
+
946
+ link.setAttribute(
947
+ "aria-label",
948
+ `Open ${record.model} model page`
949
+ );
950
+
951
+ link.innerHTML =
952
+ '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5m0-5-8 8M19 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h4"/></svg>';
953
+
954
+ modelWrap.append(
955
+ link
956
+ );
957
+ }
958
+
959
+ modelCell.append(
960
+ modelWrap
961
+ );
962
+
963
+ const rank =
964
+ ranks.get(
965
+ record.model
966
+ );
967
+
968
+ const rankCell =
969
+ makeCell(
970
+ "td",
971
+ rank
972
+ ? `#${rank}`
973
+ : "—",
974
+ `rank${
975
+ rank <= 3
976
+ ? " rank-top"
977
+ : ""
978
+ }`
979
+ );
980
+
981
+ const availabilityCell =
982
+ document.createElement(
983
+ "td"
984
+ );
985
+
986
+ const badge =
987
+ document.createElement(
988
+ "span"
989
+ );
990
+
991
+ badge.className =
992
+ `availability-badge ${record.availability.toLowerCase()}`;
993
+
994
+ badge.textContent =
995
+ record.availability;
996
+
997
+ availabilityCell.append(
998
+ badge
999
+ );
1000
+
1001
+ row.append(
1002
+ rankCell,
1003
+ modelCell,
1004
+ availabilityCell
1005
+ );
1006
+
1007
+ columns.forEach(
1008
+ (column) => {
1009
+ const value =
1010
+ record.values[
1011
+ column.key
1012
+ ];
1013
+
1014
+ const heat =
1015
+ state.colorScores
1016
+ ? `heat-cell${heatClass(
1017
+ value,
1018
+ ranges.get(
1019
+ column.key
1020
+ )
1021
+ )}`
1022
+ : "";
1023
+
1024
+ const cell =
1025
+ makeCell(
1026
+ "td",
1027
+ value === null
1028
+ ? "—"
1029
+ : value.toFixed(2),
1030
+ heat
1031
+ );
1032
+
1033
+ if (
1034
+ isMatrix &&
1035
+ column.key.startsWith(
1036
+ "combined|"
1037
+ )
1038
+ ) {
1039
+ cell.classList.add(
1040
+ "combined-score"
1041
+ );
1042
+ }
1043
+
1044
+ row.append(
1045
+ cell
1046
+ );
1047
+ }
1048
+ );
1049
+
1050
+ return row;
1051
  });
1052
+
1053
+ body.replaceChildren(
1054
+ ...rows
1055
+ );
1056
+
1057
+ document.querySelector(
1058
+ "#table-title"
1059
+ ).textContent =
1060
+ isMatrix
1061
+ ? `${matrixMetric} across all datasets`
1062
+ : `${
1063
+ DATASET_LABELS[dataset] ||
1064
+ dataset
1065
+ } leaderboard`;
1066
+
1067
+ const colorNote =
1068
+ state.colorScores
1069
+ ? " Colors are scaled separately within the WER and CER metric families: green is better, red is worse."
1070
+ : "";
1071
+
1072
+ document.querySelector(
1073
+ "#table-description"
1074
+ ).textContent =
1075
+ (
1076
+ isMatrix
1077
+ ? `Rank is based on Overall ${matrixMetric}. Scores are percentages; lower is better.`
1078
+ : "Rank is based on WER. Scores are percentages; lower is better."
1079
+ ) + colorNote;
1080
+
1081
+ document.querySelector(
1082
+ "#result-count"
1083
+ ).textContent =
1084
+ `${records.length} ${
1085
+ records.length === 1
1086
+ ? "model"
1087
+ : "models"
1088
+ }`;
1089
+
1090
+ document.querySelector(
1091
+ "#empty-state"
1092
+ ).hidden =
1093
+ records.length !== 0;
1094
+
1095
+ table.hidden =
1096
+ records.length === 0;
1097
  }
1098
 
1099
  function setView(view) {
1100
+ state.view =
1101
+ view;
1102
+
1103
+ const isMatrix =
1104
+ view === "matrix";
1105
+
1106
+ document
1107
+ .querySelector(
1108
+ "#leaderboards"
1109
+ )
1110
+ .classList.toggle(
1111
+ "matrix-view",
1112
+ isMatrix
1113
+ );
1114
+
1115
+ document.querySelector(
1116
+ ".matrix-metric-field"
1117
+ ).hidden =
1118
+ !isMatrix;
1119
+
1120
+ document
1121
+ .querySelectorAll(
1122
+ ".view-button"
1123
+ )
1124
+ .forEach((button) => {
1125
+ const active =
1126
+ button.dataset.view ===
1127
+ view;
1128
+
1129
+ button.classList.toggle(
1130
+ "is-active",
1131
+ active
1132
+ );
1133
+
1134
+ button.setAttribute(
1135
+ "aria-pressed",
1136
+ String(active)
1137
+ );
1138
+ });
1139
+
1140
+ state.sortKey =
1141
+ isMatrix
1142
+ ? `combined|${currentMatrixMetric()}`
1143
+ : `${currentDataset()}|WER`;
1144
+
1145
+ state.sortDirection =
1146
+ "asc";
1147
+
1148
  render();
1149
  }
1150
 
1151
+ function activateTab(
1152
+ name,
1153
+ updateHash = true
1154
+ ) {
1155
+ const validName =
1156
+ [
1157
+ "leaderboards",
1158
+ "datasets",
1159
+ "about",
1160
+ ].includes(name)
1161
+ ? name
1162
+ : "leaderboards";
1163
+
1164
+ document
1165
+ .querySelectorAll(
1166
+ ".tab"
1167
+ )
1168
+ .forEach((tab) => {
1169
+ const active =
1170
+ tab.dataset.tab ===
1171
+ validName;
1172
+
1173
+ tab.classList.toggle(
1174
+ "is-active",
1175
+ active
1176
+ );
1177
+
1178
+ tab.setAttribute(
1179
+ "aria-selected",
1180
+ String(active)
1181
+ );
1182
+ });
1183
+
1184
+ document
1185
+ .querySelectorAll(
1186
+ ".panel"
1187
+ )
1188
+ .forEach((panel) => {
1189
+ const active =
1190
+ panel.id ===
1191
+ validName;
1192
+
1193
+ panel.classList.toggle(
1194
+ "is-active",
1195
+ active
1196
+ );
1197
+
1198
+ panel.hidden =
1199
+ !active;
1200
+ });
1201
+
1202
+ if (updateHash) {
1203
+ history.replaceState(
1204
+ null,
1205
+ "",
1206
+ `#${validName}`
1207
+ );
1208
+ }
1209
  }
1210
 
1211
  function buildControls() {
1212
+ const datasetOptions =
1213
+ document.querySelector(
1214
+ "#dataset-options"
1215
+ );
1216
+
1217
+ state.datasets.forEach(
1218
+ (dataset, index) => {
1219
+ const label =
1220
+ document.createElement(
1221
+ "label"
1222
+ );
1223
+
1224
+ label.className =
1225
+ "choice-pill";
1226
+
1227
+ if (
1228
+ dataset === "combined"
1229
+ ) {
1230
+ label.classList.add(
1231
+ "combined-choice"
1232
+ );
1233
+ }
1234
+
1235
+ const input =
1236
+ document.createElement(
1237
+ "input"
1238
+ );
1239
+
1240
+ input.type =
1241
+ "radio";
1242
+
1243
+ input.name =
1244
+ "dataset";
1245
+
1246
+ input.value =
1247
+ dataset;
1248
+
1249
+ input.checked =
1250
+ index === 0;
1251
+
1252
+ input.addEventListener(
1253
+ "change",
1254
+ () => {
1255
+ state.sortKey =
1256
+ `${dataset}|WER`;
1257
+
1258
+ state.sortDirection =
1259
+ "asc";
1260
+
1261
+ render();
1262
+ }
1263
+ );
1264
+
1265
+ label.append(
1266
+ input,
1267
+ document.createTextNode(
1268
+ DATASET_LABELS[
1269
+ dataset
1270
+ ] || dataset
1271
+ )
1272
+ );
1273
+
1274
+ datasetOptions.append(
1275
+ label
1276
+ );
1277
+ }
1278
+ );
1279
+
1280
+ const matrixOptions =
1281
+ document.querySelector(
1282
+ "#matrix-metric-options"
1283
+ );
1284
+
1285
+ METRICS.forEach(
1286
+ (metric, index) => {
1287
+ const matrixLabel =
1288
+ document.createElement(
1289
+ "label"
1290
+ );
1291
+
1292
+ matrixLabel.className =
1293
+ "choice-pill";
1294
+
1295
+ const matrixInput =
1296
+ document.createElement(
1297
+ "input"
1298
+ );
1299
+
1300
+ matrixInput.type =
1301
+ "radio";
1302
+
1303
+ matrixInput.name =
1304
+ "matrix-metric";
1305
+
1306
+ matrixInput.value =
1307
+ metric;
1308
+
1309
+ matrixInput.checked =
1310
+ index === 0;
1311
+
1312
+ matrixInput.addEventListener(
1313
+ "change",
1314
+ () => {
1315
+ state.sortKey =
1316
+ `combined|${metric}`;
1317
+
1318
+ state.sortDirection =
1319
+ "asc";
1320
+
1321
+ render();
1322
+ }
1323
+ );
1324
+
1325
+ matrixLabel.append(
1326
+ matrixInput,
1327
+ document.createTextNode(
1328
+ metric
1329
+ )
1330
+ );
1331
+
1332
+ matrixOptions.append(
1333
+ matrixLabel
1334
+ );
1335
+ }
1336
+ );
1337
+
1338
+ document
1339
+ .querySelector(
1340
+ "#color-toggle"
1341
+ )
1342
+ .addEventListener(
1343
+ "click",
1344
+ (event) => {
1345
+ state.colorScores =
1346
+ !state.colorScores;
1347
+
1348
+ event.currentTarget.classList.toggle(
1349
+ "is-active",
1350
+ state.colorScores
1351
+ );
1352
+
1353
+ event.currentTarget.setAttribute(
1354
+ "aria-pressed",
1355
+ String(
1356
+ state.colorScores
1357
+ )
1358
+ );
1359
+
1360
+ render();
1361
+ }
1362
+ );
1363
+
1364
+ document
1365
+ .querySelectorAll(
1366
+ ".view-button"
1367
+ )
1368
+ .forEach((button) => {
1369
+ button.addEventListener(
1370
+ "click",
1371
+ () => {
1372
+ setView(
1373
+ button.dataset.view
1374
+ );
1375
+ }
1376
+ );
1377
  });
 
 
 
1378
 
1379
+ document
1380
+ .querySelectorAll(
1381
+ ".tab"
1382
+ )
1383
+ .forEach((tab) => {
1384
+ tab.addEventListener(
1385
+ "click",
1386
+ () => {
1387
+ activateTab(
1388
+ tab.dataset.tab
1389
+ );
1390
+ }
1391
+ );
1392
  });
 
 
 
1393
 
1394
+ document
1395
+ .querySelector(
1396
+ "#theme-toggle"
1397
+ )
1398
+ .addEventListener(
1399
+ "click",
1400
+ () => {
1401
+ setTheme(
1402
+ document.documentElement
1403
+ .dataset.theme ===
1404
+ "dark"
1405
+ ? "light"
1406
+ : "dark"
1407
+ );
1408
+ }
1409
+ );
1410
+
1411
+ document.addEventListener(
1412
+ "click",
1413
+ () => {
1414
+ if (
1415
+ !state.availabilityMenuOpen
1416
+ ) {
1417
+ return;
1418
+ }
1419
+
1420
+ state.availabilityMenuOpen =
1421
+ false;
1422
+
1423
+ render();
1424
+ }
1425
+ );
1426
+
1427
+ document.addEventListener(
1428
+ "keydown",
1429
+ (event) => {
1430
+ if (
1431
+ event.key !== "Escape" ||
1432
+ !state.availabilityMenuOpen
1433
+ ) {
1434
+ return;
1435
+ }
1436
+
1437
+ state.availabilityMenuOpen =
1438
+ false;
1439
+
1440
+ render();
1441
+ }
1442
+ );
1443
+
1444
+ setTheme(
1445
+ document.documentElement
1446
+ .dataset.theme,
1447
+ false
1448
+ );
1449
  }
1450
 
1451
  async function start() {
1452
  try {
1453
+ const response =
1454
+ await fetch(
1455
+ "results.csv"
1456
+ );
1457
+
1458
+ if (!response.ok) {
1459
+ throw new Error(
1460
+ `Could not load results.csv (${response.status})`
1461
+ );
1462
+ }
1463
+
1464
+ ingest(
1465
+ parseCsv(
1466
+ await response.text()
1467
+ )
1468
+ );
1469
+
1470
  buildControls();
1471
+ buildChartControls();
1472
+
1473
+ activateTab(
1474
+ location.hash.slice(1),
1475
+ false
1476
+ );
1477
+
1478
  render();
1479
+ renderComparisonChart();
1480
  } catch (error) {
1481
+ document.querySelector(
1482
+ "#table-title"
1483
+ ).textContent =
1484
+ "Leaderboard unavailable";
1485
+
1486
+ document.querySelector(
1487
+ "#leaderboard-table"
1488
+ ).hidden =
1489
+ true;
1490
+
1491
+ const empty =
1492
+ document.querySelector(
1493
+ "#empty-state"
1494
+ );
1495
+
1496
+ empty.hidden =
1497
+ false;
1498
+
1499
+ empty.querySelector(
1500
+ "h3"
1501
+ ).textContent =
1502
+ "Could not load results";
1503
+
1504
+ empty.querySelector(
1505
+ "p"
1506
+ ).textContent =
1507
+ error.message;
1508
  }
1509
  }
1510
 
1511
+ if (
1512
+ typeof module !==
1513
+ "undefined"
1514
+ ) {
1515
+ module.exports = {
1516
+ heatClass,
1517
+ ingest,
1518
+ matrixRanges,
1519
+ parseCsv,
1520
+ rankRecords,
1521
+ setTheme,
1522
+ state,
1523
+ };
1524
  }
1525
 
1526
+ if (
1527
+ typeof document !==
1528
+ "undefined"
1529
+ ) {
1530
+ start();
1531
+ }