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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 47 additions & 178 deletions packages/app/src/components/calculator/ThroughputCalculatorDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import { track } from '@/lib/analytics';
import Link from 'next/link';
import { BarChart3, Download, FileSpreadsheet, Image, RotateCcw, Table2 } from 'lucide-react';
import { BarChart3, Table2 } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { useGlobalFilters } from '@/components/GlobalFilterContext';
import { Badge } from '@/components/ui/badge';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { ChartButtons } from '@/components/ui/chart-buttons';
import ChartLegend from '@/components/ui/chart-legend';
import {
ModelSelector,
Expand All @@ -28,6 +28,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
import { Skeleton } from '@/components/ui/skeleton';
import {
getModelLabel,
Expand All @@ -40,8 +41,6 @@ import {
import { getModelSortIndex, GPU_SPECS, HARDWARE_CONFIG } from '@/lib/constants';
import { useThemeColors } from '@/hooks/useThemeColors';

import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useChartExport } from '@/hooks/useChartExport';
import { getDisplayLabel } from '@/lib/utils';
import { exportToCsv } from '@/lib/csv-export';
import { calculatorChartToCsv } from '@/lib/csv-export-helpers';
Expand Down Expand Up @@ -72,133 +71,25 @@ const BAR_METRIC_OPTIONS: { value: BarMetric; label: string }[] = [
{ value: 'cost', label: 'Cost' },
];

function CalculatorChartButtons({
viewMode,
setViewMode,
chartId,
onExportCsv,
setIsLegendExpanded,
exportFileName,
}: {
viewMode: 'chart' | 'table';
setViewMode: (v: 'chart' | 'table') => void;
chartId: string;
onExportCsv: () => void;
setIsLegendExpanded?: (expanded: boolean) => void;
exportFileName?: string;
}) {
const { isExporting, exportToImage } = useChartExport({
chartId,
setIsLegendExpanded,
exportFileName,
});
const [exportPopoverOpen, setExportPopoverOpen] = useState(false);
type CalculatorViewMode = 'chart' | 'table';

const CALCULATOR_VIEW_MODE_OPTIONS: SegmentedToggleOption<CalculatorViewMode>[] = [
{
value: 'chart',
label: 'Chart',
icon: <BarChart3 className="size-3.5" />,
testId: 'calculator-chart-view-btn',
},
{
value: 'table',
label: 'Table',
icon: <Table2 className="size-3.5" />,
testId: 'calculator-table-view-btn',
},
];

const handleExportPng = () => {
setExportPopoverOpen(false);
track('calculator_chart_exported');
exportToImage();
};

const handleExportCsv = () => {
setExportPopoverOpen(false);
track('calculator_csv_exported');
onExportCsv();
window.dispatchEvent(new CustomEvent('inferencex:action'));
};

return (
<div className="hidden md:flex absolute top-6 right-6 md:top-8 md:right-8 no-export export-buttons gap-1 z-10">
<div
className="inline-flex items-center rounded-lg border border-border p-0.5 gap-0.5 shrink-0"
role="tablist"
aria-label="View mode"
data-testid="calculator-view-toggle"
>
<button
type="button"
role="tab"
aria-selected={viewMode === 'chart'}
className={`inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
viewMode === 'chart'
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
onClick={() => {
setViewMode('chart');
track('calculator_view_changed', { view: 'chart' });
}}
data-testid="calculator-chart-view-btn"
>
<BarChart3 className="size-3.5" />
Chart
</button>
<button
type="button"
role="tab"
aria-selected={viewMode === 'table'}
className={`inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
viewMode === 'table'
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
onClick={() => {
setViewMode('table');
track('calculator_view_changed', { view: 'table' });
}}
data-testid="calculator-table-view-btn"
>
<Table2 className="size-3.5" />
Table
</button>
</div>
<Popover open={exportPopoverOpen} onOpenChange={setExportPopoverOpen}>
<PopoverTrigger asChild>
<Button
data-testid="export-button"
variant="outline"
size={isExporting ? 'default' : 'icon'}
className={`h-7 shrink-0 ${isExporting ? '' : 'w-7'}`}
disabled={isExporting}
>
<Download className={isExporting ? 'mr-2' : ''} size={16} />
{isExporting && 'Exporting...'}
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-44 p-1">
<button
data-testid="export-png-button"
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer"
onClick={handleExportPng}
>
<Image size={14} />
Download PNG
</button>
<button
data-testid="export-csv-button"
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer"
onClick={handleExportCsv}
>
<FileSpreadsheet size={14} />
Download CSV
</button>
</PopoverContent>
</Popover>
<Button
data-testid="zoom-reset-button"
variant="outline"
size="icon"
className="h-7 w-7"
onClick={() => {
track('calculator_zoom_reset_button');
window.dispatchEvent(new CustomEvent(`d3chart_zoom_reset_${chartId}`));
}}
>
<RotateCcw size={16} />
</Button>
</div>
);
}
const CALCULATOR_MOBILE_VIEW_MODE_OPTIONS: SegmentedToggleOption<CalculatorViewMode>[] =
CALCULATOR_VIEW_MODE_OPTIONS.map(({ testId: _testId, ...option }) => option);

export default function ThroughputCalculatorDisplay() {
const {
Expand All @@ -225,7 +116,7 @@ export default function ThroughputCalculatorDisplay() {
const [selectedBars, setSelectedBars] = useState<Set<string>>(new Set());
const [isLegendExpanded, setIsLegendExpanded] = useState(true);
const [highContrast, setHighContrast] = useState(false);
const [viewMode, setViewMode] = useState<'chart' | 'table'>('chart');
const [viewMode, setViewMode] = useState<CalculatorViewMode>('chart');

const { hardwareConfig, ranges, getResults, loading, error, hasData, availableHwKeys } =
useThroughputData(selectedModel, selectedSequence, selectedPrecisions, selectedRunDate);
Expand Down Expand Up @@ -383,6 +274,11 @@ export default function ThroughputCalculatorDisplay() {
exportToCsv(`InferenceX_calculator_${selectedModel}`, headers, rows);
}, [results, targetValue, hardwareConfig]);

const handleViewModeChange = useCallback((value: CalculatorViewMode) => {
setViewMode(value);
track('calculator_view_changed', { view: value });
}, []);

const handleResetGpus = useCallback(() => {
setVisibleHwKeys(new Set(availableHwKeys));
track('calculator_gpu_reset', { gpuCount: availableHwKeys.length });
Expand Down Expand Up @@ -696,70 +592,43 @@ export default function ThroughputCalculatorDisplay() {
{/* Chart / Table */}
<section data-testid="calculator-chart-section">
<figure data-testid="calculator-figure" className="relative rounded-lg">
<CalculatorChartButtons
viewMode={viewMode}
setViewMode={setViewMode}
<ChartButtons
chartId="calculator-chart"
analyticsPrefix="calculator"
zoomResetEvent="d3chart_zoom_reset_calculator-chart"
onExportCsv={handleExportCsv}
setIsLegendExpanded={setIsLegendExpanded}
exportFileName={`InferenceX_calculator_${selectedModel}`}
leadingControls={
<SegmentedToggle
value={viewMode}
options={CALCULATOR_VIEW_MODE_OPTIONS}
onValueChange={handleViewModeChange}
ariaLabel="View mode"
testId="calculator-view-toggle"
className="shrink-0"
/>
}
/>
<Card>
{loading ? (
<Skeleton className="h-125 w-full" />
) : (
<>
{(() => {
const mobileToggle = (
<div
className="md:hidden inline-flex items-center rounded-lg border border-border p-0.5 gap-0.5 shrink-0"
role="tablist"
aria-label="View mode"
>
<button
type="button"
role="tab"
aria-selected={viewMode === 'chart'}
className={`inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
viewMode === 'chart'
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
onClick={() => {
setViewMode('chart');
track('calculator_view_changed', { view: 'chart' });
}}
>
<BarChart3 className="size-3.5" />
Chart
</button>
<button
type="button"
role="tab"
aria-selected={viewMode === 'table'}
className={`inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
viewMode === 'table'
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
onClick={() => {
setViewMode('table');
track('calculator_view_changed', { view: 'table' });
}}
>
<Table2 className="size-3.5" />
Table
</button>
</div>
);

const captionContent = (
<>
<div className="flex items-start justify-between gap-4">
<h2 className="text-lg font-semibold">
{getChartTitle(barMetric, mode, targetValue, costType, costProvider)}
</h2>
{mobileToggle}
<SegmentedToggle
value={viewMode}
options={CALCULATOR_MOBILE_VIEW_MODE_OPTIONS}
onValueChange={handleViewModeChange}
ariaLabel="View mode"
className="md:hidden shrink-0"
/>
</div>
<p className="text-sm text-muted-foreground mb-2">
{getModelLabel(selectedModel)} •{' '}
Expand Down
62 changes: 34 additions & 28 deletions packages/app/src/components/gpu-power/GpuPowerDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Card } from '@/components/ui/card';
import ChartLegend from '@/components/ui/chart-legend';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
import { ShareButton } from '@/components/ui/share-button';
import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import {
Expand All @@ -35,6 +36,23 @@ import { ALL_METRIC_OPTIONS, getAvailableMetrics } from './types';
const GPU_COLORS = d3.schemeTableau10;
const FEATURE_GATE_KEY = 'inferencex-feature-gate';

type GpuMetricsView = 'chart' | 'correlation';

const GPU_METRICS_VIEW_OPTIONS: SegmentedToggleOption<GpuMetricsView>[] = [
{
value: 'chart',
icon: <BarChart3 className="h-3.5 w-3.5" />,
ariaLabel: 'Line chart',
title: 'Line chart',
},
{
value: 'correlation',
icon: <ScatterChart className="h-3.5 w-3.5" />,
ariaLabel: 'Correlation scatter',
title: 'Correlation scatter',
},
];

export default function GpuMetricsDisplay() {
const router = useRouter();
const [runIdInput, setRunIdInput] = useState('22806827144');
Expand All @@ -49,7 +67,7 @@ export default function GpuMetricsDisplay() {
const [isLegendExpanded, setIsLegendExpanded] = useState(true);
const [downsample, setDownsample] = useState(true);
// View toggle + correlation
const [chartView, setChartView] = useState<'chart' | 'correlation'>('chart');
const [chartView, setChartView] = useState<GpuMetricsView>('chart');
const [corrXMetric, setCorrXMetric] = useState<GpuMetricKey>('power');
const [corrYMetric, setCorrYMetric] = useState<GpuMetricKey>('temperature');
// URL state
Expand Down Expand Up @@ -205,6 +223,11 @@ export default function GpuMetricsDisplay() {
track('gpu_metrics_gpu_reset_filter');
}, [allGpuIndices]);

const handleChartViewChange = useCallback((value: GpuMetricsView) => {
setChartView(value);
track('gpu_metrics_view_changed', { view: value });
}, []);

return (
<section data-testid="gpu-metrics-display">
<Card className="mb-4">
Expand Down Expand Up @@ -360,33 +383,16 @@ export default function GpuMetricsDisplay() {
>
<div className="flex items-center justify-end mb-2">
<div className="flex items-center gap-1.5 no-export">
{/* View toggle */}
<div className="flex items-center border rounded-md" role="tablist">
<button
role="tab"
aria-selected={chartView === 'chart'}
className={`p-1.5 ${chartView === 'chart' ? 'bg-muted' : 'hover:bg-muted/50'} rounded-l-md`}
onClick={() => {
setChartView('chart');
track('gpu_metrics_view_changed', { view: 'chart' });
}}
title="Line chart"
>
<BarChart3 className="h-3.5 w-3.5" />
</button>
<button
role="tab"
aria-selected={chartView === 'correlation'}
className={`p-1.5 ${chartView === 'correlation' ? 'bg-muted' : 'hover:bg-muted/50'} rounded-r-md`}
onClick={() => {
setChartView('correlation');
track('gpu_metrics_view_changed', { view: 'correlation' });
}}
title="Correlation scatter"
>
<ScatterChart className="h-3.5 w-3.5" />
</button>
</div>
<SegmentedToggle
value={chartView}
options={GPU_METRICS_VIEW_OPTIONS}
onValueChange={handleChartViewChange}
ariaLabel="View mode"
className="rounded-md border p-0 gap-0"
buttonClassName="p-1.5 rounded-none first:rounded-l-md last:rounded-r-md"
activeButtonClassName="bg-muted text-foreground"
inactiveButtonClassName="text-muted-foreground hover:bg-muted/50 hover:text-foreground"
/>
<Button
variant="outline"
size="sm"
Expand Down
Loading
Loading