diff --git a/packages/app/src/components/evaluation/ui/ChartDisplay.tsx b/packages/app/src/components/evaluation/ui/ChartDisplay.tsx index 55b3bb6..3fa44b7 100644 --- a/packages/app/src/components/evaluation/ui/ChartDisplay.tsx +++ b/packages/app/src/components/evaluation/ui/ChartDisplay.tsx @@ -4,9 +4,8 @@ import { useCallback } from 'react'; import { useEvaluation } from '@/components/evaluation/EvaluationContext'; import { Card } from '@/components/ui/card'; +import { ChartShareActions } from '@/components/ui/chart-display-helpers'; import { ChartSection } from '@/components/ui/chart-section'; -import { ShareButton } from '@/components/ui/share-button'; -import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons'; import { getPrecisionLabel, isModelExperimental, Model, Precision } from '@/lib/data-mappings'; import { exportToCsv } from '@/lib/csv-export'; import { evaluationChartToCsv } from '@/lib/csv-export-helpers'; @@ -43,13 +42,7 @@ export default function EvaluationChartDisplay() { different GPUs, quantization levels, and inference configurations.

-
- -
- - -
-
+ diff --git a/packages/app/src/components/inference/ui/ChartDisplay.tsx b/packages/app/src/components/inference/ui/ChartDisplay.tsx index fb51267..92d8d2d 100644 --- a/packages/app/src/components/inference/ui/ChartDisplay.tsx +++ b/packages/app/src/components/inference/ui/ChartDisplay.tsx @@ -1,6 +1,5 @@ 'use client'; import { track } from '@/lib/analytics'; -import Link from 'next/link'; import { useMemo, useState } from 'react'; import { ChevronDown, X } from 'lucide-react'; @@ -8,9 +7,9 @@ import { useInference } from '@/components/inference/InferenceContext'; import { InferenceData, OverlayData, TrendDataPoint } from '@/components/inference/types'; import { processOverlayChartData } from '@/components/inference/utils'; import ScatterGraph from '@/components/inference/ui/ScatterGraph'; -import { Badge } from '@/components/ui/badge'; import { Card } from '@/components/ui/card'; import { ChartButtons } from '@/components/ui/chart-buttons'; +import { ChartShareActions, MetricAssumptionNotes } from '@/components/ui/chart-display-helpers'; import { exportToCsv } from '@/lib/csv-export'; import { inferenceChartToCsv } from '@/lib/csv-export-helpers'; import { @@ -20,13 +19,9 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ExternalLinkIcon } from '@/components/ui/external-link-icon'; -import { ShareButton } from '@/components/ui/share-button'; -import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Skeleton } from '@/components/ui/skeleton'; import { useUnofficialRun } from '@/components/unofficial-run-provider'; -import { GPU_SPECS } from '@/lib/constants'; import { getModelLabel, getPrecisionLabel, @@ -95,10 +90,8 @@ function E2eXAxisDropdown({ } /** - * Renders a display of scatter charts based on filtered graph data. - * It maps through the filtered graphs from the InferenceChartContext and renders a Card for each, - * containing a heading and a ScatterGraph component. - * @returns {JSX.Element[]} An array of JSX.Element representing the chart displays. + * Renders the inference chart cards, captions, overlay controls, and trend drill-down dialog for + * the current filtered benchmark data. */ export default function ChartDisplay() { const { @@ -380,213 +373,7 @@ export default function ChartDisplay() { )}

- {(selectedYAxisMetric === 'y_tpPerMw' || - selectedYAxisMetric === 'y_inputTputPerMw' || - selectedYAxisMetric === 'y_outputTputPerMw') && ( - <> -

- All in Power/GPU:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}: {specs.power}kW - - ))} -

-

- - Source:{' '} - - SemiAnalysis Datacenter Industry Model - - - -

- - )} - {(selectedYAxisMetric === 'y_costh' || - selectedYAxisMetric === 'y_costn' || - selectedYAxisMetric === 'y_costr') && ( - <> -

- TCO $/GPU/hr:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}:{' '} - {selectedYAxisMetric === 'y_costh' - ? specs.costh - : selectedYAxisMetric === 'y_costn' - ? specs.costn - : specs.costr} - - ))} -

-

- - Source:{' '} - - SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model - - - -

- - )} - {(selectedYAxisMetric === 'y_costhOutput' || - selectedYAxisMetric === 'y_costnOutput' || - selectedYAxisMetric === 'y_costrOutput') && ( - <> -

- TCO $/GPU/hr:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}:{' '} - {selectedYAxisMetric === 'y_costhOutput' - ? specs.costh - : selectedYAxisMetric === 'y_costnOutput' - ? specs.costn - : specs.costr} - - ))} -

-

- - Source:{' '} - - SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model - - - -

- - )} - {(selectedYAxisMetric === 'y_costhi' || - selectedYAxisMetric === 'y_costni' || - selectedYAxisMetric === 'y_costri') && ( - <> -

- TCO $/GPU/hr:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}:{' '} - {selectedYAxisMetric === 'y_costhi' - ? specs.costh - : selectedYAxisMetric === 'y_costni' - ? specs.costn - : specs.costr} - - ))} -

-

- - Source:{' '} - - SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model - - - -

- - )} -
-

- Note: Disaggregated inference configurations (e.g., MoRI - SGLang, Dynamo TRT) calculate cost per decode GPU or per prefill GPU, - rather than per total GPU count. This makes direct cost comparison with - aggregated configs not an apples-to-apples comparison. -

-
-
-

- Note: Disaggregated inference configurations (e.g., MoRI - SGLang, Dynamo TRT) calculate cost per decode GPU or per prefill GPU, - rather than per total GPU count. This makes direct cost comparison with - aggregated configs not an apples-to-apples comparison. -

-
-
-

- Note: Disaggregated inference configurations (e.g., MoRI - SGLang, Dynamo TRT) calculate power per decode GPU or per prefill GPU, - rather than per total GPU count. This makes direct power comparison with - aggregated configs not an apples-to-apples comparison. -

-
- {selectedYAxisMetric.startsWith('y_j') && ( - <> -

- All in Power/GPU:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}: {specs.power}kW - - ))} -

-

- - Source:{' '} - - SemiAnalysis Datacenter Industry Model - - - -

- - )} -
-

- Note: Disaggregated inference configurations (e.g., MoRI - SGLang, Dynamo TRT) calculate Joules per decode GPU or per prefill GPU, - rather than per total GPU count. This makes direct Joules per token - comparison with aggregated configs not an apples-to-apples comparison. -

-
+
-
- -
- - -
-
+ diff --git a/packages/app/src/components/reliability/ui/ChartDisplay.tsx b/packages/app/src/components/reliability/ui/ChartDisplay.tsx index 92af6c2..514eca0 100644 --- a/packages/app/src/components/reliability/ui/ChartDisplay.tsx +++ b/packages/app/src/components/reliability/ui/ChartDisplay.tsx @@ -4,9 +4,8 @@ import { useCallback } from 'react'; import { useReliabilityContext } from '@/components/reliability/ReliabilityContext'; import { Card } from '@/components/ui/card'; +import { ChartShareActions } from '@/components/ui/chart-display-helpers'; import { ChartSection } from '@/components/ui/chart-section'; -import { ShareButton } from '@/components/ui/share-button'; -import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons'; import { exportToCsv } from '@/lib/csv-export'; import { reliabilityChartToCsv } from '@/lib/csv-export-helpers'; @@ -35,13 +34,7 @@ export default function ReliabilityChartDisplay() { reliability for inference runs over time.

-
- -
- - -
-
+ diff --git a/packages/app/src/components/trends/HistoricalTrendsDisplay.tsx b/packages/app/src/components/trends/HistoricalTrendsDisplay.tsx index 4b1f220..95d194d 100644 --- a/packages/app/src/components/trends/HistoricalTrendsDisplay.tsx +++ b/packages/app/src/components/trends/HistoricalTrendsDisplay.tsx @@ -1,7 +1,6 @@ 'use client'; import { track } from '@/lib/analytics'; -import Link from 'next/link'; import React, { useCallback, useMemo, useState } from 'react'; import { useInference } from '@/components/inference/InferenceContext'; @@ -9,20 +8,17 @@ import { useInterpolatedTrendData } from '@/components/inference/hooks/useInterp import { TrendLineConfig } from '@/components/inference/types'; import ChartControls from '@/components/inference/ui/ChartControls'; import TrendChart from '@/components/inference/ui/TrendChart'; -import { Badge } from '@/components/ui/badge'; import { Card } from '@/components/ui/card'; import { ChartButtons } from '@/components/ui/chart-buttons'; +import { ChartShareActions, MetricAssumptionNotes } from '@/components/ui/chart-display-helpers'; import { exportToCsv } from '@/lib/csv-export'; import { historicalTrendToCsv } from '@/lib/csv-export-helpers'; import ChartLegend from '@/components/ui/chart-legend'; -import { ExternalLinkIcon } from '@/components/ui/external-link-icon'; import { Input } from '@/components/ui/input'; import { LabelWithTooltip } from '@/components/ui/label-with-tooltip'; -import { ShareButton } from '@/components/ui/share-button'; -import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons'; import { TooltipProvider } from '@/components/ui/tooltip'; import { Skeleton } from '@/components/ui/skeleton'; -import { getModelSortIndex, GPU_SPECS } from '@/lib/constants'; +import { getModelSortIndex } from '@/lib/constants'; import { getModelLabel, getPrecisionLabel, @@ -199,13 +195,7 @@ export default function HistoricalTrendsDisplay() { Interpolated performance metrics over time at a fixed interactivity operating point.

-
- -
- - -
-
+ @@ -316,195 +306,11 @@ export default function HistoricalTrendsDisplay() { <> • Updated: {workflowInfo[0].run_date.split(',')[0]} )}

- {selectedYAxisMetric === 'y_tpPerMw' && ( - <> -

- All in Power/GPU:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}: {specs.power}kW - - ))} -

-

- - Source:{' '} - - SemiAnalysis Datacenter Industry Model - - - -

- - )} - {(selectedYAxisMetric === 'y_costh' || - selectedYAxisMetric === 'y_costn' || - selectedYAxisMetric === 'y_costr') && ( - <> -

- TCO $/GPU/hr:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}:{' '} - {selectedYAxisMetric === 'y_costh' - ? specs.costh - : selectedYAxisMetric === 'y_costn' - ? specs.costn - : specs.costr} - - ))} -

-

- - Source:{' '} - - SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model - - - -

- - )} - {(selectedYAxisMetric === 'y_costhOutput' || - selectedYAxisMetric === 'y_costnOutput' || - selectedYAxisMetric === 'y_costrOutput') && ( - <> -

- TCO $/GPU/hr:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}:{' '} - {selectedYAxisMetric === 'y_costhOutput' - ? specs.costh - : selectedYAxisMetric === 'y_costnOutput' - ? specs.costn - : specs.costr} - - ))} -

-

- - Source:{' '} - - SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model - - - -

- - )} - {(selectedYAxisMetric === 'y_costhi' || - selectedYAxisMetric === 'y_costni' || - selectedYAxisMetric === 'y_costri') && ( - <> -

- TCO $/GPU/hr:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}:{' '} - {selectedYAxisMetric === 'y_costhi' - ? specs.costh - : selectedYAxisMetric === 'y_costni' - ? specs.costn - : specs.costr} - - ))} -

-

- - Source:{' '} - - SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model - - - -

- - )} -
-

- Note: Disaggregated inference configurations (e.g., MoRI - SGLang, Dynamo TRT) calculate cost per decode GPU or per prefill GPU, rather - than per total GPU count. This makes direct cost comparison with aggregated - configs not an apples-to-apples comparison. -

-
-
-

- Note: Disaggregated inference configurations (e.g., MoRI - SGLang, Dynamo TRT) calculate cost per decode GPU or per prefill GPU, rather - than per total GPU count. This makes direct cost comparison with aggregated - configs not an apples-to-apples comparison. -

-
- {selectedYAxisMetric.startsWith('y_j') && ( - <> -

- All in Power/GPU:{' '} - {Object.entries(GPU_SPECS).map(([base, specs]) => ( - - {base.toUpperCase()}: {specs.power}kW - - ))} -

-

- - Source:{' '} - - SemiAnalysis Datacenter Industry Model - - - -

- - )} -
-

- Note: Disaggregated inference configurations (e.g., MoRI - SGLang, Dynamo TRT) calculate Joules per decode GPU or per prefill GPU, - rather than per total GPU count. This makes direct Joules per token - comparison with aggregated configs not an apples-to-apples comparison. -

-
+
root.render(ui)); +} + +function getVisibleText() { + return container.textContent ?? ''; +} + +function getVisibleCaveatText() { + return Array.from(container.querySelectorAll('div.max-h-20 p')) + .map((element) => element.textContent ?? '') + .join(' '); +} + +beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); +}); + +describe('ChartShareActions', () => { + it('renders the shared copy-link, X, and LinkedIn buttons with stable test ids', () => { + renderUi(); + + expect(container.querySelector('[data-testid="share-button"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="share-twitter"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="share-linkedin"]')).not.toBeNull(); + }); +}); + +describe('MetricAssumptionNotes', () => { + it('shows power source badges and the per-MW disaggregation caveat for inference metrics', () => { + renderUi(); + + expect(getVisibleText()).toContain('All in Power/GPU:'); + expect(getVisibleText()).toContain('SemiAnalysis Datacenter Industry Model'); + expect(getVisibleCaveatText()).toContain('calculate power per decode GPU or per prefill GPU'); + }); + + it('preserves historical-trends semantics when both compatibility flags are disabled', () => { + renderUi( + , + ); + + expect(getVisibleText()).not.toContain('SemiAnalysis Datacenter Industry Model'); + expect(getVisibleCaveatText()).not.toContain( + 'calculate power per decode GPU or per prefill GPU', + ); + + renderUi( + , + ); + + expect(getVisibleText()).toContain('SemiAnalysis Datacenter Industry Model'); + expect(getVisibleCaveatText()).not.toContain( + 'calculate power per decode GPU or per prefill GPU', + ); + }); + + it('renders TCO notes, source attribution, and the cost disaggregation caveat', () => { + renderUi(); + + expect(getVisibleText()).toContain('TCO $/GPU/hr:'); + expect(getVisibleText()).toContain( + 'SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model', + ); + expect(getVisibleCaveatText()).toContain('calculate cost per decode GPU or per prefill GPU'); + }); + + it('renders metric-specific throughput caveats and preserves Joules wording semantics', () => { + renderUi(); + + expect(getVisibleCaveatText()).toContain( + 'calculate input throughput per decode GPU or per prefill GPU', + ); + expect(getVisibleCaveatText()).toContain('direct input throughput comparison'); + + renderUi(); + + expect(getVisibleText()).toContain('SemiAnalysis Datacenter Industry Model'); + expect(getVisibleCaveatText()).toContain('calculate Joules per decode GPU or per prefill GPU'); + expect(getVisibleCaveatText()).toContain('direct Joules per token comparison'); + }); +}); diff --git a/packages/app/src/components/ui/chart-display-helpers.tsx b/packages/app/src/components/ui/chart-display-helpers.tsx new file mode 100644 index 0000000..f08605b --- /dev/null +++ b/packages/app/src/components/ui/chart-display-helpers.tsx @@ -0,0 +1,177 @@ +'use client'; + +import Link from 'next/link'; +import type { ReactNode } from 'react'; + +import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons'; +import { Badge } from '@/components/ui/badge'; +import { ExternalLinkIcon } from '@/components/ui/external-link-icon'; +import { ShareButton } from '@/components/ui/share-button'; +import { GPU_SPECS } from '@/lib/constants'; + +// Keep these metric-key groups in sync with chart-utils/chart configs when new source-backed +// metrics are added; this helper owns which caption notes and caveats appear for each family. +const POWER_SOURCE_METRICS = new Set(['y_tpPerMw', 'y_inputTputPerMw', 'y_outputTputPerMw']); +const TOTAL_COST_METRICS = new Set(['y_costh', 'y_costn', 'y_costr']); +const OUTPUT_COST_METRICS = new Set(['y_costhOutput', 'y_costnOutput', 'y_costrOutput']); +const INPUT_COST_METRICS = new Set(['y_costhi', 'y_costni', 'y_costri']); +const POWER_VALUES = Object.fromEntries( + Object.entries(GPU_SPECS).map(([base, specs]) => [base, `${specs.power}kW`]), +); + +function MetricBadges({ + label, + values, +}: { + label: string; + values: Record; +}) { + return ( +

+ {label}{' '} + {Object.entries(values).map(([base, value]) => ( + + {base.toUpperCase()}: {value} + + ))} +

+ ); +} + +function SourceLink({ href, children }: { href: string; children: ReactNode }) { + return ( +

+ + Source:{' '} + + {children} + + + +

+ ); +} + +function DisaggCaveat({ + visible, + calculationNoun, + comparisonNoun = calculationNoun, +}: { + visible: boolean; + calculationNoun: string; + comparisonNoun?: string; +}) { + return ( +
+

+ Note: Disaggregated inference configurations (e.g., MoRI SGLang, Dynamo + TRT) calculate {calculationNoun} per decode GPU or per prefill GPU, rather than per total + GPU count. This makes direct {comparisonNoun} comparison with aggregated configs not an + apples-to-apples comparison. +

+
+ ); +} + +function getCostValues(selectedYAxisMetric: string) { + return Object.fromEntries( + Object.entries(GPU_SPECS).map(([base, specs]) => [ + base, + selectedYAxisMetric === 'y_costh' || + selectedYAxisMetric === 'y_costhOutput' || + selectedYAxisMetric === 'y_costhi' + ? specs.costh + : selectedYAxisMetric === 'y_costn' || + selectedYAxisMetric === 'y_costnOutput' || + selectedYAxisMetric === 'y_costni' + ? specs.costn + : specs.costr, + ]), + ); +} + +export function ChartShareActions() { + return ( +
+ +
+ + +
+
+ ); +} + +export function MetricAssumptionNotes({ + selectedYAxisMetric, + includeAllPowerThroughputMetrics = true, + includePowerThroughputCaveat = true, +}: { + selectedYAxisMetric: string; + // Historical trends only annotates y_tpPerMw and intentionally omits per-MW caveats to preserve + // the tab's existing caption contract while sharing the same helper as inference. + includeAllPowerThroughputMetrics?: boolean; + includePowerThroughputCaveat?: boolean; +}) { + const showPowerSource = includeAllPowerThroughputMetrics + ? POWER_SOURCE_METRICS.has(selectedYAxisMetric) + : selectedYAxisMetric === 'y_tpPerMw'; + const showTotalCostSource = TOTAL_COST_METRICS.has(selectedYAxisMetric); + const showOutputCostSource = OUTPUT_COST_METRICS.has(selectedYAxisMetric); + const showInputCostSource = INPUT_COST_METRICS.has(selectedYAxisMetric); + const showInputThroughputCaveat = selectedYAxisMetric === 'y_inputTputPerGpu'; + const showOutputThroughputCaveat = selectedYAxisMetric === 'y_outputTputPerGpu'; + const showJouleSource = selectedYAxisMetric.startsWith('y_j'); + + const costValues = + showTotalCostSource || showOutputCostSource || showInputCostSource + ? getCostValues(selectedYAxisMetric) + : null; + + return ( + <> + {showPowerSource && ( + <> + + + SemiAnalysis Datacenter Industry Model + + + )} + {costValues && ( + <> + + + SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model + + + )} + + + + {includePowerThroughputCaveat && ( + + )} + {showJouleSource && ( + <> + + + SemiAnalysis Datacenter Industry Model + + + )} + + + ); +}