Mark Duppenthaler commited on
Commit
f76d503
·
1 Parent(s): ed37070

Subgroups and stats

Browse files
frontend/src/components/AudioPlayer.tsx CHANGED
@@ -1,6 +1,7 @@
1
- import { useEffect, useRef, useState } from 'react'
2
  import WaveSurfer from 'wavesurfer.js'
3
  // @ts-ignore: No types for timeline.esm.js
 
4
  import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'
5
  import API from '../API' // Correct import for the API class
6
 
@@ -8,6 +9,17 @@ const AudioPlayer = ({ src }: { src: string }) => {
8
  const containerRef = useRef<HTMLDivElement>(null)
9
  const wavesurferRef = useRef<WaveSurfer | null>(null)
10
  const [isPlaying, setIsPlaying] = useState(false)
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  // Initialize WaveSurfer when component mounts
13
  useEffect(() => {
@@ -23,7 +35,12 @@ const AudioPlayer = ({ src }: { src: string }) => {
23
  progressColor: 'rgb(100, 0, 100)',
24
  url: proxiedUrl, // Use the proxied URL
25
  minPxPerSec: 100,
26
- plugins: [TimelinePlugin.create()],
 
 
 
 
 
27
  })
28
 
29
  // Play on click
@@ -53,9 +70,9 @@ const AudioPlayer = ({ src }: { src: string }) => {
53
  }
54
 
55
  return (
56
- <div>
57
  <div ref={containerRef} />
58
- <button onClick={handlePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button>
59
  </div>
60
  )
61
  }
 
1
+ import { useEffect, useMemo, useRef, useState } from 'react'
2
  import WaveSurfer from 'wavesurfer.js'
3
  // @ts-ignore: No types for timeline.esm.js
4
+ // import Timeline from 'wavesurfer.js/dist/plugins/timeline.esm.js'
5
  import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'
6
  import API from '../API' // Correct import for the API class
7
 
 
9
  const containerRef = useRef<HTMLDivElement>(null)
10
  const wavesurferRef = useRef<WaveSurfer | null>(null)
11
  const [isPlaying, setIsPlaying] = useState(false)
12
+ // const plugins = useMemo(() => [TimelinePlugin.create()], [])
13
+
14
+ const bottomTimeline = TimelinePlugin.create({
15
+ height: 16,
16
+ timeInterval: 0.1,
17
+ primaryLabelInterval: 1,
18
+ style: {
19
+ fontSize: '10px',
20
+ // color: '#6A3274',
21
+ },
22
+ })
23
 
24
  // Initialize WaveSurfer when component mounts
25
  useEffect(() => {
 
35
  progressColor: 'rgb(100, 0, 100)',
36
  url: proxiedUrl, // Use the proxied URL
37
  minPxPerSec: 100,
38
+ barWidth: 10,
39
+ barRadius: 10,
40
+ barGap: 2,
41
+ mediaControls: true,
42
+
43
+ // plugins: [bottomTimeline],
44
  })
45
 
46
  // Play on click
 
70
  }
71
 
72
  return (
73
+ <div className="">
74
  <div ref={containerRef} />
75
+ {/* <button onClick={handlePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button> */}
76
  </div>
77
  )
78
  }
frontend/src/components/LeaderboardTable.tsx CHANGED
@@ -15,16 +15,28 @@ interface Groups {
15
  [group: string]: { [subgroup: string]: string[] }
16
  }
17
 
 
 
 
 
 
18
  const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
19
  const [tableRows, setTableRows] = useState<Row[]>([])
20
  const [tableHeader, setTableHeader] = useState<string[]>([])
21
  const [loading, setLoading] = useState(true)
22
  const [error, setError] = useState<string | null>(null)
23
  const [groups, setGroups] = useState<Groups>({})
 
 
 
 
24
 
25
  const [selectedMetrics, setSelectedMetrics] = useState<Set<string>>(new Set())
26
  const [defaultSelectedMetrics, setDefaultSelectedMetrics] = useState<string[]>([])
27
 
 
 
 
28
  useEffect(() => {
29
  API.fetchStaticFile(`data/${file}_benchmark.csv`)
30
  .then((response) => {
@@ -32,6 +44,20 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
32
  const rows: Row[] = data['rows']
33
  const groups = data['groups'] as { [key: string]: string[] }
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  // Each value of groups is a list of metrics, group them by the first part of the metric before the first _
36
  const groupsData = Object.entries(groups)
37
  .sort(([groupA], [groupB]) => {
@@ -69,11 +95,28 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
69
  )
70
 
71
  const allKeys: string[] = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  setSelectedMetrics(new Set(data['default_selected_metrics']))
73
  setDefaultSelectedMetrics(data['default_selected_metrics'])
74
- setTableHeader(allKeys)
75
  setTableRows(rows)
76
  setGroups(groupsData)
 
 
77
  setLoading(false)
78
  })
79
  .catch((err) => {
@@ -86,6 +129,85 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
86
  setSelectedMetrics(new Set(defaultSelectedMetrics))
87
  }
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  return (
90
  <div className="rounded shadow overflow-auto">
91
  <h3 className="font-bold mb-2">{file}</h3>
@@ -100,32 +222,293 @@ const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
100
  setSelectedMetrics={setSelectedMetrics}
101
  defaultSelectedMetrics={defaultSelectedMetrics}
102
  />
103
- <table className="table">
104
  <thead>
105
  <tr>
106
- {tableHeader.map((col, idx) => (
107
- <th key={idx}>{col}</th>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  ))}
109
  </tr>
110
  </thead>
111
  <tbody>
112
- {tableRows
113
- .filter((row) => selectedMetrics.has(row['metric'] as string))
114
- .map((row, i) => (
115
- <tr key={i}>
116
- {Object.keys(row).map((column, j) => {
117
- const cell = row[column]
118
-
119
- return (
120
- <td key={j}>
121
- <div className="">
122
- {isNaN(Number(cell)) ? cell : Number(Number(cell).toFixed(3))}
123
- </div>
124
- </td>
125
- )
126
- })}
127
- </tr>
128
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  </tbody>
130
  </table>
131
  </div>
 
15
  [group: string]: { [subgroup: string]: string[] }
16
  }
17
 
18
+ interface GroupStats {
19
+ average: { [key: string]: number }
20
+ stdDev: { [key: string]: number }
21
+ }
22
+
23
  const LeaderboardTable: React.FC<LeaderboardTableProps> = ({ file }) => {
24
  const [tableRows, setTableRows] = useState<Row[]>([])
25
  const [tableHeader, setTableHeader] = useState<string[]>([])
26
  const [loading, setLoading] = useState(true)
27
  const [error, setError] = useState<string | null>(null)
28
  const [groups, setGroups] = useState<Groups>({})
29
+ const [openGroups, setOpenGroups] = useState<{ [key: string]: boolean }>({})
30
+ const [openSubGroups, setOpenSubGroups] = useState<{ [key: string]: { [key: string]: boolean } }>(
31
+ {}
32
+ )
33
 
34
  const [selectedMetrics, setSelectedMetrics] = useState<Set<string>>(new Set())
35
  const [defaultSelectedMetrics, setDefaultSelectedMetrics] = useState<string[]>([])
36
 
37
+ // To store the unique metrics from the Overall group
38
+ const [overallMetrics, setOverallMetrics] = useState<string[]>([])
39
+
40
  useEffect(() => {
41
  API.fetchStaticFile(`data/${file}_benchmark.csv`)
42
  .then((response) => {
 
44
  const rows: Row[] = data['rows']
45
  const groups = data['groups'] as { [key: string]: string[] }
46
 
47
+ // Extract unique metrics from the Overall group (after the underscore)
48
+ const overallGroup = groups['Overall'] || []
49
+ const uniqueMetrics = new Set<string>()
50
+
51
+ overallGroup.forEach((metric) => {
52
+ if (metric.includes('_')) {
53
+ // Extract the part after the first underscore
54
+ const metricName = metric.split('_').slice(1).join('_')
55
+ uniqueMetrics.add(metricName)
56
+ }
57
+ })
58
+
59
+ setOverallMetrics(Array.from(uniqueMetrics).sort())
60
+
61
  // Each value of groups is a list of metrics, group them by the first part of the metric before the first _
62
  const groupsData = Object.entries(groups)
63
  .sort(([groupA], [groupB]) => {
 
95
  )
96
 
97
  const allKeys: string[] = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
98
+ // Remove 'metric' from headers if it exists
99
+ const headers = allKeys.filter((key) => key !== 'metric')
100
+
101
+ // Initialize open states for groups and subgroups
102
+ const initialOpenGroups: { [key: string]: boolean } = {}
103
+ const initialOpenSubGroups: { [key: string]: { [key: string]: boolean } } = {}
104
+
105
+ Object.keys(groupsData).forEach((group) => {
106
+ initialOpenGroups[group] = false
107
+ initialOpenSubGroups[group] = {}
108
+ Object.keys(groupsData[group]).forEach((subGroup) => {
109
+ initialOpenSubGroups[group][subGroup] = false
110
+ })
111
+ })
112
+
113
  setSelectedMetrics(new Set(data['default_selected_metrics']))
114
  setDefaultSelectedMetrics(data['default_selected_metrics'])
115
+ setTableHeader(headers)
116
  setTableRows(rows)
117
  setGroups(groupsData)
118
+ setOpenGroups(initialOpenGroups)
119
+ setOpenSubGroups(initialOpenSubGroups)
120
  setLoading(false)
121
  })
122
  .catch((err) => {
 
129
  setSelectedMetrics(new Set(defaultSelectedMetrics))
130
  }
131
 
132
+ const toggleGroup = (group: string) => {
133
+ setOpenGroups((prev) => ({ ...prev, [group]: !prev[group] }))
134
+ }
135
+
136
+ const toggleSubGroup = (group: string, subGroup: string) => {
137
+ setOpenSubGroups((prev) => ({
138
+ ...prev,
139
+ [group]: {
140
+ ...(prev[group] || {}),
141
+ [subGroup]: !prev[group]?.[subGroup],
142
+ },
143
+ }))
144
+ }
145
+
146
+ // Find all metrics matching a particular extracted metric name (like "log10_p_value")
147
+ const findAllMetricsForName = (metricName: string): string[] => {
148
+ return tableRows
149
+ .filter((row) => {
150
+ const metric = row.metric as string
151
+ if (metric.includes('_')) {
152
+ const extractedName = metric.split('_').slice(1).join('_')
153
+ return extractedName.endsWith(metricName)
154
+ }
155
+ return false
156
+ })
157
+ .map((row) => row.metric as string)
158
+ }
159
+
160
+ // Calculate average and standard deviation for a set of metrics for a specific column
161
+ const calculateStats = (
162
+ metricNames: string[],
163
+ columnKey: string
164
+ ): { avg: number; stdDev: number } => {
165
+ const values = metricNames
166
+ .map((metricName) => {
167
+ const row = tableRows.find((row) => row.metric === metricName)
168
+ return row ? Number(row[columnKey]) : NaN
169
+ })
170
+ .filter((value) => !isNaN(value))
171
+
172
+ if (values.length === 0) return { avg: NaN, stdDev: NaN }
173
+
174
+ const avg = values.reduce((sum, val) => sum + val, 0) / values.length
175
+
176
+ const squareDiffs = values.map((value) => {
177
+ const diff = value - avg
178
+ return diff * diff
179
+ })
180
+ const variance = squareDiffs.reduce((sum, sqrDiff) => sum + sqrDiff, 0) / values.length
181
+ const stdDev = Math.sqrt(variance)
182
+
183
+ return { avg, stdDev }
184
+ }
185
+
186
+ // Filter metrics by group and/or subgroup
187
+ const filterMetricsByGroupAndSubgroup = (
188
+ metricNames: string[],
189
+ group: string | null = null,
190
+ subgroup: string | null = null
191
+ ): string[] => {
192
+ // If no group specified, return all metrics
193
+ if (!group) return metricNames
194
+
195
+ // Get all metrics for the specified group
196
+ const groupMetrics = Object.values(groups[group] || {}).flat()
197
+
198
+ // If subgroup is specified, further filter to that subgroup
199
+ if (subgroup && groups[group]?.[subgroup]) {
200
+ return metricNames.filter(
201
+ (metric) => groups[group][subgroup].includes(metric) && selectedMetrics.has(metric)
202
+ )
203
+ }
204
+
205
+ // Otherwise return all metrics in the group
206
+ return metricNames.filter(
207
+ (metric) => groupMetrics.includes(metric) && selectedMetrics.has(metric)
208
+ )
209
+ }
210
+
211
  return (
212
  <div className="rounded shadow overflow-auto">
213
  <h3 className="font-bold mb-2">{file}</h3>
 
222
  setSelectedMetrics={setSelectedMetrics}
223
  defaultSelectedMetrics={defaultSelectedMetrics}
224
  />
225
+ <table className="table w-full">
226
  <thead>
227
  <tr>
228
+ <th>Group / Subgroup</th>
229
+ {overallMetrics.map((metric) => (
230
+ <th key={metric} colSpan={tableHeader.length} className="text-center border-x">
231
+ {metric}
232
+ </th>
233
+ ))}
234
+ </tr>
235
+ <tr>
236
+ <th></th>
237
+ {overallMetrics.map((metric) => (
238
+ <React.Fragment key={`header-models-${metric}`}>
239
+ {tableHeader.map((model) => (
240
+ <th key={`${metric}-${model}`} className="text-center text-xs">
241
+ {model}
242
+ </th>
243
+ ))}
244
+ </React.Fragment>
245
  ))}
246
  </tr>
247
  </thead>
248
  <tbody>
249
+ {/* First render each group */}
250
+ {Object.entries(groups).map(([group, subGroups]) => {
251
+ // Get all metrics for this group
252
+ const allGroupMetrics = Object.values(subGroups).flat()
253
+ // Filter to only include selected metrics
254
+ const visibleGroupMetrics = filterMetricsByGroupAndSubgroup(allGroupMetrics, group)
255
+
256
+ // Skip this group if no metrics are selected
257
+ if (visibleGroupMetrics.length === 0) return null
258
+
259
+ return (
260
+ <React.Fragment key={group}>
261
+ {/* Group row with average stats for the entire group */}
262
+ <tr
263
+ className="bg-base-200 cursor-pointer hover:bg-base-300"
264
+ onClick={() => toggleGroup(group)}
265
+ >
266
+ <td className="font-medium">
267
+ {openGroups[group] ? '▼ ' : '▶ '}
268
+ {group}
269
+ </td>
270
+ {/* For each metric column */}
271
+ {overallMetrics.map((metric) => (
272
+ // Render sub-columns for each model
273
+ <React.Fragment key={`${group}-${metric}`}>
274
+ {tableHeader.map((col) => {
275
+ // Find all metrics in this group that match the current metric name
276
+ const allMetricsWithName = findAllMetricsForName(metric)
277
+ const metricsInGroupForThisMetric = visibleGroupMetrics.filter((m) =>
278
+ allMetricsWithName.includes(m)
279
+ )
280
+ const stats = calculateStats(metricsInGroupForThisMetric, col)
281
+
282
+ return (
283
+ <td
284
+ key={`${group}-${metric}-${col}`}
285
+ className="font-medium text-center"
286
+ >
287
+ {!isNaN(stats.avg)
288
+ ? `${stats.avg.toFixed(3)} ± ${stats.stdDev.toFixed(3)}`
289
+ : 'N/A'}
290
+ </td>
291
+ )
292
+ })}
293
+ </React.Fragment>
294
+ ))}
295
+ </tr>
296
+
297
+ {/* Only render subgroups if group is open */}
298
+ {openGroups[group] &&
299
+ Object.entries(subGroups).map(([subGroup, metrics]) => {
300
+ // Filter to only include selected metrics in this subgroup
301
+ const visibleSubgroupMetrics = filterMetricsByGroupAndSubgroup(
302
+ metrics,
303
+ group,
304
+ subGroup
305
+ )
306
+
307
+ // Skip this subgroup if no metrics are selected
308
+ if (visibleSubgroupMetrics.length === 0) return null
309
+
310
+ return (
311
+ <React.Fragment key={`${group}-${subGroup}`}>
312
+ {/* Subgroup row with average stats for the subgroup */}
313
+ <tr
314
+ className="bg-base-100 cursor-pointer hover:bg-base-200"
315
+ onClick={() => toggleSubGroup(group, subGroup)}
316
+ >
317
+ <td className="pl-6 font-medium">
318
+ {openSubGroups[group]?.[subGroup] ? '▼ ' : '▶ '}
319
+ {subGroup}
320
+ </td>
321
+ {/* For each metric column */}
322
+ {overallMetrics.map((metric) => (
323
+ // Render sub-columns for each model
324
+ <React.Fragment key={`${group}-${subGroup}-${metric}`}>
325
+ {tableHeader.map((col) => {
326
+ // Find all metrics in this subgroup that match the current metric name
327
+ const allMetricsWithName = findAllMetricsForName(metric)
328
+ const metricsInSubgroupForThisMetric =
329
+ visibleSubgroupMetrics.filter((m) =>
330
+ allMetricsWithName.includes(m)
331
+ )
332
+ const stats = calculateStats(
333
+ metricsInSubgroupForThisMetric,
334
+ col
335
+ )
336
+
337
+ return (
338
+ <td
339
+ key={`${group}-${subGroup}-${metric}-${col}`}
340
+ className="font-medium text-center"
341
+ >
342
+ {!isNaN(stats.avg)
343
+ ? `${stats.avg.toFixed(3)} ± ${stats.stdDev.toFixed(3)}`
344
+ : 'N/A'}
345
+ </td>
346
+ )
347
+ })}
348
+ </React.Fragment>
349
+ ))}
350
+ </tr>
351
+
352
+ {/* Individual metric rows */}
353
+ {openSubGroups[group]?.[subGroup] &&
354
+ // Sort visibleSubgroupMetrics alphabetically by the clean metric name
355
+ [...visibleSubgroupMetrics]
356
+ .sort((a, b) => {
357
+ // Extract clean metric names (after the underscore)
358
+ console.log({ a })
359
+
360
+ // For metrics with format {category}_{strength}_{overall_metric_name},
361
+ // First sort by category, then by overall_metric_name, then by strength
362
+
363
+ // First extract the overall metric group
364
+ const getOverallMetricGroup = (metric: string) => {
365
+ for (const overall of overallMetrics) {
366
+ if (metric.endsWith(`_${overall}`) || metric === overall) {
367
+ return overall
368
+ }
369
+ }
370
+ return ''
371
+ }
372
+
373
+ const overallA = getOverallMetricGroup(a)
374
+ const overallB = getOverallMetricGroup(b)
375
+
376
+ // Extract the strength (last part before the overall metric)
377
+ const stripOverall = (metric: string, overall: string) => {
378
+ if (metric.endsWith(`_${overall}`)) {
379
+ // Remove the overall metric group and any preceding underscore
380
+ const stripped = metric.slice(
381
+ 0,
382
+ metric.length - overall.length - 1
383
+ )
384
+ const parts = stripped.split('_')
385
+ return parts.length > 0 ? parts[parts.length - 1] : ''
386
+ }
387
+ return metric
388
+ }
389
+
390
+ // Extract the category (what remains after removing strength and overall_metric_name)
391
+ const getCategory = (metric: string, overall: string) => {
392
+ if (metric.endsWith(`_${overall}`)) {
393
+ const stripped = metric.slice(
394
+ 0,
395
+ metric.length - overall.length - 1
396
+ )
397
+ const parts = stripped.split('_')
398
+ // Remove the last part (strength) and join the rest (category)
399
+ return parts.length > 1
400
+ ? parts.slice(0, parts.length - 1).join('_')
401
+ : ''
402
+ }
403
+ return metric
404
+ }
405
+
406
+ const categoryA = getCategory(a, overallA)
407
+ const categoryB = getCategory(b, overallB)
408
+
409
+ // First sort by category
410
+ if (categoryA !== categoryB) {
411
+ return categoryA.localeCompare(categoryB)
412
+ }
413
+
414
+ // Then sort by overall metric name
415
+ if (overallA !== overallB) {
416
+ return overallA.localeCompare(overallB)
417
+ }
418
+
419
+ // Finally sort by strength
420
+ const subA = stripOverall(a, overallA)
421
+ const subB = stripOverall(b, overallB)
422
+
423
+ // Try to parse subA and subB as numbers, handling k/m/b suffixes
424
+ const parseNumber = (str: string) => {
425
+ const match = str.match(/^(\d+(?:\.\d+)?)([kKmMbB]?)$/)
426
+ if (!match) return NaN
427
+ let [_, num, suffix] = match
428
+ let value = parseFloat(num)
429
+ switch (suffix.toLowerCase()) {
430
+ case 'k':
431
+ value *= 1e3
432
+ break
433
+ case 'm':
434
+ value *= 1e6
435
+ break
436
+ case 'b':
437
+ value *= 1e9
438
+ break
439
+ }
440
+ return value
441
+ }
442
+
443
+ const numA = parseNumber(subA)
444
+ const numB = parseNumber(subB)
445
+
446
+ if (!isNaN(numA) && !isNaN(numB)) {
447
+ return numA - numB
448
+ }
449
+ // Fallback to string comparison if not both numbers
450
+ return subA.localeCompare(subB)
451
+ })
452
+ .map((metric) => {
453
+ const row = tableRows.find((r) => r.metric === metric)
454
+ if (!row) return null
455
+
456
+ // Extract the metric name (after the underscore)
457
+ const metricName = metric.includes('_')
458
+ ? metric.split('_').slice(1).join('_')
459
+ : metric
460
+
461
+ return (
462
+ <tr key={metric} className="hover:bg-base-100">
463
+ <td className="pl-10">{metric}</td>
464
+ {/* For each metric column */}
465
+ {overallMetrics.map((oMetric) => {
466
+ // Only show values for the matching metric
467
+ const isMatchingMetric =
468
+ findAllMetricsForName(oMetric).includes(metric)
469
+
470
+ if (!isMatchingMetric) {
471
+ // Fill empty cells for non-matching metrics
472
+ return (
473
+ <React.Fragment key={`${metric}-${oMetric}`}>
474
+ {tableHeader.map((col) => (
475
+ <td
476
+ key={`${metric}-${oMetric}-${col}`}
477
+ className="text-center"
478
+ ></td>
479
+ ))}
480
+ </React.Fragment>
481
+ )
482
+ }
483
+
484
+ // Show values for the matching metric
485
+ return (
486
+ <React.Fragment key={`${metric}-${oMetric}`}>
487
+ {tableHeader.map((col) => {
488
+ const cell = row[col]
489
+ return (
490
+ <td
491
+ key={`${metric}-${oMetric}-${col}`}
492
+ className="text-center"
493
+ >
494
+ {!isNaN(Number(cell))
495
+ ? Number(Number(cell).toFixed(3))
496
+ : cell}
497
+ </td>
498
+ )
499
+ })}
500
+ </React.Fragment>
501
+ )
502
+ })}
503
+ </tr>
504
+ )
505
+ })}
506
+ </React.Fragment>
507
+ )
508
+ })}
509
+ </React.Fragment>
510
+ )
511
+ })}
512
  </tbody>
513
  </table>
514
  </div>