Skip to content

Commit 02548ff

Browse files
authored
feat: add table view for all charts (#177)
* feat: add chart/table toggle to inference charts * test: add unit tests for InferenceTable sorting and value resolution * fix: use per-chart view mode state instead of shared * feat: add pagination to inference table (50/100/250/500 per page) * fix: default table page size to 25 * fix: use shadcn Select for page size dropdown * fix: show only number in dropdown, 'per page' label outside * fix: remove Pareto star column from inference table * refactor: extract generic DataTable component, InferenceTable passes columns * feat: add column header sorting to DataTable (desc → asc → none) * fix: add keyboard sorting (Enter/Space) and null-safe sort comparisons * feat: add search box, column visibility toggle, and sticky header to DataTable * fix: remove column visibility toggle, fix z-[1] to z-1 * fix: hide PNG export and zoom reset in table view, show CSV-only button * fix: disable PNG export and zoom reset in table view instead of hiding * feat: add chart/table toggle to evaluation (accuracy evals) tab * refactor: replace inline TCO calculator table with DataTable component * fix: add top margin to DataTable above search box * fix: thicker border below table column headers * fix: remove stale border-t from DataTable wrapper
1 parent 5bcc299 commit 02548ff

10 files changed

Lines changed: 878 additions & 128 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use client';
2+
3+
import { useMemo } from 'react';
4+
5+
import type { InterpolatedResult, CostType } from '@/components/calculator/types';
6+
import {
7+
getThroughputForType,
8+
getTpPerMwForType,
9+
} from '@/components/calculator/ThroughputBarChart';
10+
import { type DataTableColumn, DataTable } from '@/components/ui/data-table';
11+
import type { HardwareConfig } from '@/components/inference/types';
12+
import { getDisplayLabel } from '@/lib/utils';
13+
14+
interface CalculatorTableProps {
15+
results: InterpolatedResult[];
16+
costType: CostType;
17+
hardwareConfig: HardwareConfig;
18+
}
19+
20+
function getLabel(r: InterpolatedResult, hardwareConfig: HardwareConfig): string {
21+
const config = hardwareConfig[r.hwKey];
22+
const baseName = config ? getDisplayLabel(config) : r.hwKey;
23+
return r.precision ? `${baseName} (${r.precision.toUpperCase()})` : baseName;
24+
}
25+
26+
function getCost(r: InterpolatedResult, costType: CostType): number {
27+
if (costType === 'input') return r.costInput;
28+
if (costType === 'output') return r.costOutput;
29+
return r.cost;
30+
}
31+
32+
export default function CalculatorTable({
33+
results,
34+
costType,
35+
hardwareConfig,
36+
}: CalculatorTableProps) {
37+
const throughputLabel =
38+
costType === 'input' ? 'Input' : costType === 'output' ? 'Output' : 'Total';
39+
const costLabel = `$/M ${costType === 'input' ? 'input ' : costType === 'output' ? 'output ' : ''}tok`;
40+
const mwLabel =
41+
costType === 'input'
42+
? 'Input tok/s/MW'
43+
: costType === 'output'
44+
? 'Output tok/s/MW'
45+
: 'tok/s/MW';
46+
47+
const columns = useMemo<DataTableColumn<InterpolatedResult>[]>(
48+
() => [
49+
{
50+
header: 'GPU',
51+
cell: (r) => getLabel(r, hardwareConfig),
52+
sortValue: (r) => getLabel(r, hardwareConfig),
53+
className: 'font-medium whitespace-nowrap',
54+
},
55+
{
56+
header: `${throughputLabel} Throughput (tok/s/gpu)`,
57+
align: 'right',
58+
cell: (r) => getThroughputForType(r, costType).toFixed(1),
59+
sortValue: (r) => getThroughputForType(r, costType),
60+
className: 'tabular-nums',
61+
},
62+
{
63+
header: `Cost (${costLabel})`,
64+
align: 'right',
65+
cell: (r) => `$${getCost(r, costType).toFixed(3)}`,
66+
sortValue: (r) => getCost(r, costType),
67+
className: 'tabular-nums',
68+
},
69+
{
70+
header: mwLabel,
71+
align: 'right',
72+
cell: (r) => getTpPerMwForType(r, costType).toFixed(0),
73+
sortValue: (r) => getTpPerMwForType(r, costType),
74+
className: 'tabular-nums',
75+
},
76+
{
77+
header: 'Concurrency',
78+
align: 'right',
79+
cell: (r) => `~${r.concurrency}`,
80+
sortValue: (r) => r.concurrency,
81+
className: 'tabular-nums',
82+
},
83+
],
84+
[costType, hardwareConfig, throughputLabel, costLabel, mwLabel],
85+
);
86+
87+
return (
88+
<>
89+
<DataTable
90+
data={results}
91+
columns={columns}
92+
testId="calculator-results-table"
93+
analyticsPrefix="calculator_table"
94+
/>
95+
<p className="text-xs text-muted-foreground mt-3">
96+
Values are interpolated from real InferenceMAX benchmark data points. Only GPUs with data in
97+
the measured range are shown.
98+
</p>
99+
</>
100+
);
101+
}

packages/app/src/components/calculator/ThroughputCalculatorDisplay.tsx

Lines changed: 6 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Link from 'next/link';
55
import { BarChart3, Table2 } from 'lucide-react';
66
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
77

8+
import CalculatorTable from '@/components/calculator/CalculatorTable';
89
import { useGlobalFilters } from '@/components/GlobalFilterContext';
910
import { Badge } from '@/components/ui/badge';
1011
import { Card } from '@/components/ui/card';
@@ -780,93 +781,11 @@ export default function ThroughputCalculatorDisplay() {
780781
) : (
781782
<>
782783
<figcaption>{captionContent}</figcaption>
783-
<div data-testid="calculator-results-table">
784-
<div className="overflow-x-auto relative">
785-
{/* Watermark */}
786-
<div
787-
className="absolute inset-0 pointer-events-none flex items-center justify-center"
788-
aria-hidden="true"
789-
>
790-
<img src="/brand/logo-color.webp" alt="" className="w-48 opacity-10" />
791-
</div>
792-
<table className="w-full text-sm relative">
793-
<thead>
794-
<tr className="border-b border-border">
795-
<th className="text-left py-2 px-3 font-medium text-muted-foreground">
796-
GPU
797-
</th>
798-
<th className="text-right py-2 px-3 font-medium text-muted-foreground">
799-
{costType === 'input'
800-
? 'Input'
801-
: costType === 'output'
802-
? 'Output'
803-
: 'Total'}{' '}
804-
Throughput (tok/s/gpu)
805-
</th>
806-
<th className="text-right py-2 px-3 font-medium text-muted-foreground">
807-
Cost ($/M{' '}
808-
{costType === 'input'
809-
? 'input'
810-
: costType === 'output'
811-
? 'output'
812-
: ''}{' '}
813-
tok)
814-
</th>
815-
<th className="text-right py-2 px-3 font-medium text-muted-foreground">
816-
{costType === 'input'
817-
? 'Input'
818-
: costType === 'output'
819-
? 'Output'
820-
: ''}{' '}
821-
tok/s/MW
822-
</th>
823-
<th className="text-right py-2 px-3 font-medium text-muted-foreground">
824-
Concurrency
825-
</th>
826-
</tr>
827-
</thead>
828-
<tbody>
829-
{results.map((r) => {
830-
const config = hardwareConfig[r.hwKey];
831-
const baseName = config ? getDisplayLabel(config) : r.hwKey;
832-
const label = r.precision
833-
? `${baseName} (${r.precision.toUpperCase()})`
834-
: baseName;
835-
return (
836-
<tr
837-
key={r.resultKey}
838-
className="border-b border-border/50 hover:bg-muted/30"
839-
>
840-
<td className="py-2 px-3 font-medium">{label}</td>
841-
<td className="text-right py-2 px-3 tabular-nums">
842-
{getThroughputForType(r, costType).toFixed(1)}
843-
</td>
844-
<td className="text-right py-2 px-3 tabular-nums">
845-
$
846-
{(costType === 'input'
847-
? r.costInput
848-
: costType === 'output'
849-
? r.costOutput
850-
: r.cost
851-
).toFixed(3)}
852-
</td>
853-
<td className="text-right py-2 px-3 tabular-nums">
854-
{getTpPerMwForType(r, costType).toFixed(0)}
855-
</td>
856-
<td className="text-right py-2 px-3 tabular-nums">
857-
~{r.concurrency}
858-
</td>
859-
</tr>
860-
);
861-
})}
862-
</tbody>
863-
</table>
864-
</div>
865-
<p className="text-xs text-muted-foreground mt-3">
866-
Values are interpolated from real InferenceMAX benchmark data points. Only
867-
GPUs with data in the measured range are shown.
868-
</p>
869-
</div>
784+
<CalculatorTable
785+
results={results}
786+
costType={costType}
787+
hardwareConfig={hardwareConfig}
788+
/>
870789
</>
871790
);
872791
})()}

packages/app/src/components/evaluation/ui/ChartDisplay.tsx

Lines changed: 83 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
'use client';
22

3-
import { useCallback } from 'react';
3+
import { useCallback, useState } from 'react';
4+
import { BarChart3, Table2 } from 'lucide-react';
45

6+
import { track } from '@/lib/analytics';
57
import { useEvaluation } from '@/components/evaluation/EvaluationContext';
8+
import EvaluationTable from '@/components/evaluation/ui/EvaluationTable';
69
import { Card } from '@/components/ui/card';
710
import { ChartShareActions } from '@/components/ui/chart-display-helpers';
811
import { ChartSection } from '@/components/ui/chart-section';
12+
import { type SegmentedToggleOption, SegmentedToggle } from '@/components/ui/segmented-toggle';
913
import { useUnofficialRun } from '@/components/unofficial-run-provider';
1014
import {
1115
type Model,
@@ -19,6 +23,23 @@ import { evaluationChartToCsv } from '@/lib/csv-export-helpers';
1923
import EvaluationChartControls from './ChartControls';
2024
import EvalBarChartD3 from './BarChartD3';
2125

26+
type EvalViewMode = 'chart' | 'table';
27+
28+
const VIEW_MODE_OPTIONS: SegmentedToggleOption<EvalViewMode>[] = [
29+
{
30+
value: 'chart',
31+
label: 'Chart',
32+
icon: <BarChart3 className="size-3.5" />,
33+
testId: 'evaluation-chart-view-btn',
34+
},
35+
{
36+
value: 'table',
37+
label: 'Table',
38+
icon: <Table2 className="size-3.5" />,
39+
testId: 'evaluation-table-view-btn',
40+
},
41+
];
42+
2243
export default function EvaluationChartDisplay() {
2344
const CHART_ID = 'evaluation-chart';
2445
const {
@@ -31,11 +52,54 @@ export default function EvaluationChartDisplay() {
3152
} = useEvaluation();
3253
const { isUnofficialRun } = useUnofficialRun();
3354

55+
const [viewMode, setViewMode] = useState<EvalViewMode>('chart');
56+
const handleViewModeChange = (value: EvalViewMode) => {
57+
setViewMode(value);
58+
track('evaluation_view_changed', { view: value });
59+
};
60+
3461
const handleExportCsv = useCallback(() => {
3562
const { headers, rows } = evaluationChartToCsv(chartData);
3663
exportToCsv(`InferenceX_evaluation_${selectedModel}_${selectedBenchmark}`, headers, rows);
3764
}, [chartData]);
3865

66+
const caption = (
67+
<>
68+
<h3 className="text-lg font-semibold">Evaluation Score by Hardware Configuration</h3>
69+
<p className="text-sm text-muted-foreground mb-2">
70+
{selectedModel}{' '}
71+
{selectedPrecisions.map((p) => getPrecisionLabel(p as Precision)).join(', ')}{' '}
72+
{selectedBenchmark}{' '}
73+
{isUnofficialRun ? 'Source: UNOFFICIAL' : 'Source: SemiAnalysis InferenceX™'}
74+
{selectedRunDate && (
75+
<>
76+
{' '}
77+
• Updated:{' '}
78+
{new Date(`${selectedRunDate}T00:00:00Z`).toLocaleDateString('en-US', {
79+
year: 'numeric',
80+
month: '2-digit',
81+
day: '2-digit',
82+
timeZone: 'UTC',
83+
})}
84+
</>
85+
)}
86+
</p>
87+
<div
88+
className={`overflow-hidden transition-all duration-200 ease-in-out ${
89+
selectedModel && isModelExperimental(selectedModel as Model)
90+
? 'max-h-20 opacity-100'
91+
: 'max-h-0 opacity-0'
92+
}`}
93+
>
94+
<p className="text-muted-foreground text-xs mt-2 border-l-2 border-amber-500 pl-2 bg-amber-500/5 py-1">
95+
<strong>Note:</strong> We at SemiAnalysis InferenceX™ are still in very early stages of
96+
adding support for this model. Please keep that in mind that these InferenceX numbers are
97+
experimental.
98+
</p>
99+
</div>
100+
</>
101+
);
102+
39103
return (
40104
<div data-testid="evaluation-chart-display" className="flex flex-col gap-4">
41105
<section className="relative z-10">
@@ -62,45 +126,25 @@ export default function EvaluationChartDisplay() {
62126
setIsLegendExpanded={setIsLegendExpanded}
63127
onExportCsv={handleExportCsv}
64128
exportFileName={`InferenceX_evaluation_${selectedModel}_${selectedBenchmark}`}
129+
hideImageExport={viewMode === 'table'}
130+
leadingControls={
131+
<SegmentedToggle
132+
value={viewMode}
133+
options={VIEW_MODE_OPTIONS}
134+
onValueChange={handleViewModeChange}
135+
ariaLabel="View mode"
136+
testId="evaluation-view-toggle"
137+
/>
138+
}
65139
>
66-
<EvalBarChartD3
67-
caption={
68-
<>
69-
<h3 className="text-lg font-semibold">Evaluation Score by Hardware Configuration</h3>
70-
<p className="text-sm text-muted-foreground mb-2">
71-
{selectedModel}{' '}
72-
{selectedPrecisions.map((p) => getPrecisionLabel(p as Precision)).join(', ')}{' '}
73-
{selectedBenchmark}{' '}
74-
{isUnofficialRun ? 'Source: UNOFFICIAL' : 'Source: SemiAnalysis InferenceX™'}
75-
{selectedRunDate && (
76-
<>
77-
{' '}
78-
• Updated:{' '}
79-
{new Date(`${selectedRunDate}T00:00:00Z`).toLocaleDateString('en-US', {
80-
year: 'numeric',
81-
month: '2-digit',
82-
day: '2-digit',
83-
timeZone: 'UTC',
84-
})}
85-
</>
86-
)}
87-
</p>
88-
<div
89-
className={`overflow-hidden transition-all duration-200 ease-in-out ${
90-
selectedModel && isModelExperimental(selectedModel as Model)
91-
? 'max-h-20 opacity-100'
92-
: 'max-h-0 opacity-0'
93-
}`}
94-
>
95-
<p className="text-muted-foreground text-xs mt-2 border-l-2 border-amber-500 pl-2 bg-amber-500/5 py-1">
96-
<strong>Note:</strong> We at SemiAnalysis InferenceX™ are still in very early
97-
stages of adding support for this model. Please keep that in mind that these
98-
InferenceX numbers are experimental.
99-
</p>
100-
</div>
101-
</>
102-
}
103-
/>
140+
{viewMode === 'table' ? (
141+
<>
142+
{caption}
143+
<EvaluationTable data={chartData} />
144+
</>
145+
) : (
146+
<EvalBarChartD3 caption={caption} />
147+
)}
104148
</ChartSection>
105149
</div>
106150
);

0 commit comments

Comments
 (0)