diff --git a/packages/app/cypress/e2e/line-labels.cy.ts b/packages/app/cypress/e2e/line-labels.cy.ts index a3fa4b68..822cad2d 100644 --- a/packages/app/cypress/e2e/line-labels.cy.ts +++ b/packages/app/cypress/e2e/line-labels.cy.ts @@ -39,6 +39,30 @@ describe('Line Labels Toggle', () => { ); }); + it('line labels render in the foreground, after the scatter points', () => { + // Labels were toggled on in the test above and remain on here. + cy.get('[data-testid="scatter-graph"] svg g.line-label').should('have.length.greaterThan', 0); + + cy.get('[data-testid="scatter-graph"] svg').then(($svg) => { + const svg = $svg[0]; + const dots = svg.querySelectorAll('.dot-group'); + const labels = svg.querySelectorAll('g.line-label'); + expect(dots.length, 'scatter dot groups exist').to.be.greaterThan(0); + expect(labels.length, 'line labels exist').to.be.greaterThan(0); + + // Every label must paint after every dot group. Comparing the *last* dot + // group against the *first* label is sufficient: if the earliest label + // follows the latest dot in document order, all labels are in front. + const lastDot = dots.item(dots.length - 1)!; + const firstLabel = labels.item(0)!; + const relation = lastDot.compareDocumentPosition(firstLabel); + expect( + relation & Node.DOCUMENT_POSITION_FOLLOWING, + 'line label follows the scatter points in DOM order (foreground)', + ).to.be.greaterThan(0); + }); + }); + it('toggling Line Labels off removes label elements', () => { cy.get('#scatter-line-labels').click(); cy.get('#scatter-line-labels').should('have.attr', 'data-state', 'unchecked'); diff --git a/packages/app/src/components/inference/replay/ReplayPanel.tsx b/packages/app/src/components/inference/replay/ReplayPanel.tsx index 4ff3a499..7273ed24 100644 --- a/packages/app/src/components/inference/replay/ReplayPanel.tsx +++ b/packages/app/src/components/inference/replay/ReplayPanel.tsx @@ -10,6 +10,8 @@ import { useInference } from '@/components/inference/InferenceContext'; import ScatterGraph from '@/components/inference/ui/ScatterGraph'; import type { ChartDefinition } from '@/components/inference/types'; import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, @@ -21,7 +23,7 @@ import { useBenchmarkHistory } from '@/hooks/api/use-benchmark-history'; import { track } from '@/lib/analytics'; import { cn } from '@/lib/utils'; -import { buildReplayTimeline } from './buildReplayTimeline'; +import { buildReplayTimeline, computeFullRunDomain } from './buildReplayTimeline'; import type { Mp4ExportError, Mp4ExportStage } from './exportMp4'; import { buildFrameData, dateAtFraction, shouldCommitFraction, spanMs } from './replayFrameData'; import { useReducedMotion } from './useReducedMotion'; @@ -63,7 +65,7 @@ export default function ReplayPanel({ xLabel, }: ReplayPanelProps) { const inference = useInference(); - const { selectedModel, selectedSequence } = inference; + const { selectedModel, selectedSequence, activeHwTypes } = inference; const { isl = 0, osl = 0 } = sequenceToIslOsl(selectedSequence) ?? {}; const history = useBenchmarkHistory(selectedModel, isl, osl); @@ -90,6 +92,15 @@ export default function ReplayPanel({ inference.selectedPrecisions, ]); + // Fixed axes for the whole run: take the extent across every step (not just + // the current frame) for the active hardware, so the axes stay put and the + // frontier visibly expands toward them over time instead of the chart + // refitting each frame. Recomputed when the legend's hw filter changes. + const fixedExtent = useMemo( + () => (timeline ? computeFullRunDomain(timeline, (hw) => activeHwTypes.has(hw)) : null), + [timeline, activeHwTypes], + ); + // Track the SVG's position inside our relative wrapper so the date overlay // can anchor its bottom-right to the chart plot's top-right (the wrapper // also contains the legend, so we can't anchor to the wrapper edge). @@ -161,6 +172,10 @@ export default function ReplayPanel({ const [fraction, setFraction] = useState(0); const [playing, setPlaying] = useState(false); const [speed, setSpeed] = useState(1); + // Fixed axes (default) freeze the coordinate space to the whole run so the + // frontier visibly expands over time; turning this off lets the axes refit to + // each frame's points. + const [fixedAxes, setFixedAxes] = useState(true); const [isExporting, setIsExporting] = useState(false); const [exportProgress, setExportProgress] = useState(null); const [exportError, setExportError] = useState(null); @@ -496,6 +511,9 @@ export default function ReplayPanel({ chartDefinition={chartDefinition} transitionDuration={0} niceAxes={false} + pinLineLabels + xExtentOverride={fixedAxes ? fixedExtent?.x : undefined} + yExtentOverride={fixedAxes ? fixedExtent?.y : undefined} />
+
+ { + setFixedAxes(checked); + track('inference_replay_fixed_axes_toggled', { enabled: checked }); + }} + /> + +