From ba4bc1983296cf42f76ab4c1e4bfebf5bc66fda9 Mon Sep 17 00:00:00 2001 From: Perplexity Computer Date: Sun, 17 May 2026 20:54:21 +0000 Subject: [PATCH] fix(inference): lock y-axis during scatter & GPU-graph pan/zoom The Inference scatter chart (Token Throughput per GPU vs. Interactivity / TTFT) and the GPU time-series chart on the same page were configured with `axes: 'both'` for d3-zoom. When a user dragged to pan horizontally to 'slide around' along the x-axis, the d3 zoom transform was applied to both axes \u2014 the y-axis ticks drifted as a side effect of the y-translation component, even when the intended interaction was purely horizontal. This matched Jordan's report: 'y axis is wrong on some interactivities when sliding around'. Fix: switch both charts to `axes: 'x'`, matching the working TrendChart configuration. Y-scale is now fixed during pan/zoom; only the x-axis rescales. The framework still re-renders the y-axis on each zoom with the configured tickFormat, so we keep the log-scale tickFormat override in both onZoom callbacks (the linear formatter is already set on yAxisConfig and applied automatically). Also simplified ScatterGraph's `constrain` to drop the y-translation clamping (no longer needed) and pin ty to 0. --- .../src/components/inference/ui/GPUGraph.tsx | 13 ++++++++--- .../components/inference/ui/ScatterGraph.tsx | 22 +++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/app/src/components/inference/ui/GPUGraph.tsx b/packages/app/src/components/inference/ui/GPUGraph.tsx index 407e7256..b0fb2e06 100644 --- a/packages/app/src/components/inference/ui/GPUGraph.tsx +++ b/packages/app/src/components/inference/ui/GPUGraph.tsx @@ -652,17 +652,24 @@ const GPUGraph = React.memo( ]} zoom={{ enabled: true, - axes: 'both', + // X-only: GPU time-series is a date-axis exploration. Vertical pan + // caused the y-axis to drift on drag, making tick labels look wrong + // relative to the visible data. + axes: 'x', scaleExtent: [1, 20], resetEventName: `gpu_timeseries_zoom_reset_${chartId}`, onReset: () => { track('interactivity_zoom_reset'); }, onZoom: (_event, ctx: ZoomContext) => { + // y-axis is locked with axes: 'x' (newYScale === yScale), but the + // framework re-renders axes on every zoom with the configured + // tickFormat. For log scale the tickFormat is `undefined` in this + // component, so re-apply `logTickFormat` here to preserve labels. if (logScale) { - const newYScale = ctx.newYScale as d3.ScaleLogarithmic; + const yScale = ctx.yScale as d3.ScaleLogarithmic; ctx.layout.yAxisGroup.call( - d3.axisLeft(newYScale).ticks(10).tickFormat(logTickFormat(newYScale)) as any, + d3.axisLeft(yScale).ticks(10).tickFormat(logTickFormat(yScale)) as any, ); } }, diff --git a/packages/app/src/components/inference/ui/ScatterGraph.tsx b/packages/app/src/components/inference/ui/ScatterGraph.tsx index f9a73aa8..b95c3614 100644 --- a/packages/app/src/components/inference/ui/ScatterGraph.tsx +++ b/packages/app/src/components/inference/ui/ScatterGraph.tsx @@ -592,7 +592,11 @@ const ScatterGraph = React.memo( const zoomConfig = useMemo( () => ({ enabled: true, - axes: 'both' as const, + // X-only: the scatter chart is fundamentally an X-axis exploration + // (Interactivity / TTFT). Allowing vertical pan caused the y-axis tick + // labels to drift on drag ("sliding around"), making them look wrong + // relative to the visible data. Matches the TrendChart zoom behavior. + axes: 'x' as const, scaleExtent: [0.7, 20] as [number, number], resetEventName: zoomResetEventName, onReset: () => { @@ -600,17 +604,13 @@ const ScatterGraph = React.memo( }, constrain: (transform: d3.ZoomTransform, extent: [[number, number], [number, number]]) => { const width = extent[1][0]; - const height = extent[1][1]; let tx = transform.x; - let ty = transform.y; const k = transform.k; const maxTx = 0; const minTx = Math.min(0, width - width * k); - const minTy = height * (1 - k); - const maxTy = Math.max(minTy, 0); tx = Math.max(minTx, Math.min(maxTx, tx)); - ty = Math.max(minTy, Math.min(maxTy, ty)); - return d3.zoomIdentity.translate(tx, ty).scale(k); + // Lock y-translation to 0 — y-axis stays fixed during pan/zoom. + return d3.zoomIdentity.translate(tx, 0).scale(k); }, onZoom: (_event: d3.D3ZoomEvent, ctx: ZoomContext) => { if (xScaleConfig._isLog) { @@ -619,10 +619,14 @@ const ScatterGraph = React.memo( d3.axisBottom(newXS).ticks(10).tickFormat(logTickFormat(newXS)) as any, ); } + // With axes: 'x' the y-scale doesn't change, but the framework still + // re-renders the y-axis on each zoom with the configured tickFormat. + // For log scale, tickFormat is `undefined` (see yAxisConfig above), + // so re-apply `logTickFormat` here to preserve the labels. if (yScaleConfig.type === 'log') { - const newYS = ctx.newYScale as d3.ScaleLogarithmic; + const yScale = ctx.yScale as d3.ScaleLogarithmic; ctx.layout.yAxisGroup.call( - d3.axisLeft(newYS).ticks(10).tickFormat(logTickFormat(newYS)) as any, + d3.axisLeft(yScale).ticks(10).tickFormat(logTickFormat(yScale)) as any, ); } },