From ad379e0f0779cb3039102fbce72703f06014d9ef Mon Sep 17 00:00:00 2001 From: AmanTahiliani Date: Sat, 4 Jul 2026 01:04:55 -0400 Subject: [PATCH 1/2] fix(compare): gap delta graph when reference lap is null Reference-null laps now emit null deltas for all drivers and exclude that lap window from aligned cumulative totals so later comparisons stay sane. Challenger-null behavior is unchanged. Compare tab meta documents the policy. Closes #46 Co-authored-by: Cursor --- frontend/src/components/CompareView.tsx | 4 +- frontend/src/lib/delta.ts | 35 ++++++++---- frontend/src/test/delta-time-graph.test.tsx | 60 ++++++++++++++++++++- 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/CompareView.tsx b/frontend/src/components/CompareView.tsx index 2328b09..eebe322 100644 --- a/frontend/src/components/CompareView.tsx +++ b/frontend/src/components/CompareView.tsx @@ -229,7 +229,9 @@ export function CompareView({ sessionKey, results, drivers }: Props) {
Race pace
- Cumulative lap-time delta vs {referenceLabel ?? 'reference'} + Cumulative lap-time delta vs {referenceLabel ?? 'reference'}. Deltas are plotted + only where the reference lap is valid; gaps appear when the reference has no lap + time.
): number[] { +/** + * Cumulative lap time aligned to reference-valid laps only. + * Laps where the reference is null are skipped for every series so later deltas + * do not compare against a frozen baseline while challengers keep accumulating. + */ +function buildAlignedCumulative( + lapTimes: ReadonlyArray, + referenceLapTimes: ReadonlyArray, +): number[] { const cumulative: number[] = [] let running = 0 - for (const lap of lapTimes) { + const length = Math.max(lapTimes.length, referenceLapTimes.length) + + for (let i = 0; i < length; i++) { + if (referenceLapTimes[i] === null) { + cumulative.push(running) + continue + } + const lap = lapTimes[i] if (lap !== null) { running += lap } cumulative.push(running) } + return cumulative } @@ -47,7 +63,8 @@ function resolveReference( /** * Compute per-lap cumulative time delta for each non-reference driver. * Positive = behind reference; negative = ahead. - * Null laps carry cumulative forward but emit null in deltas (skip when plotting). + * Deltas are only emitted where the reference lap is valid; reference-null laps + * gap every series. Challenger-null laps gap only that driver's line. */ export function computeCumulativeDeltas( series: ReadonlyArray, @@ -56,24 +73,22 @@ export function computeCumulativeDeltas( const reference = resolveReference(series, referenceLabel) if (!reference) return [] - const refCumulative = buildCumulative(reference.lapTimes) + const refLapTimes = reference.lapTimes + const refCumulative = buildAlignedCumulative(refLapTimes, refLapTimes) return series .filter((s) => s.label !== reference.label) .map((driver) => { - const driverCumulative = buildCumulative(driver.lapTimes) + const driverCumulative = buildAlignedCumulative(driver.lapTimes, refLapTimes) const lapCount = Math.max(driver.lapTimes.length, refCumulative.length) const deltas: (number | null)[] = [] for (let i = 0; i < lapCount; i++) { - if (driver.lapTimes[i] === null) { + if (refLapTimes[i] === null || driver.lapTimes[i] === null) { deltas.push(null) continue } - const refValue = refCumulative[i] ?? refCumulative[refCumulative.length - 1] ?? 0 - const driverValue = - driverCumulative[i] ?? driverCumulative[driverCumulative.length - 1] ?? 0 - deltas.push(driverValue - refValue) + deltas.push(driverCumulative[i] - refCumulative[i]) } return { diff --git a/frontend/src/test/delta-time-graph.test.tsx b/frontend/src/test/delta-time-graph.test.tsx index 291d136..65aa0e9 100644 --- a/frontend/src/test/delta-time-graph.test.tsx +++ b/frontend/src/test/delta-time-graph.test.tsx @@ -47,7 +47,7 @@ describe('computeCumulativeDeltas', () => { expect(result[0].deltas[2]).toBeCloseTo(2) }) - it('emits null for missing lap times while carrying cumulative forward', () => { + it('emits null for challenger missing lap times while carrying cumulative forward', () => { const withNull: DeltaSeries = { label: 'NOR', color: '#FF8000', @@ -60,6 +60,24 @@ describe('computeCumulativeDeltas', () => { expect(result[0].deltas[2]).toBeCloseTo(-92) }) + it('gaps all drivers when the reference lap is null and resumes without that window', () => { + const refWithNull: DeltaSeries = { + label: 'VER', + color: '#3671C6', + lapTimes: [90, null, 92], + } + const validChallenger: DeltaSeries = { + label: 'HAM', + color: '#E8002D', + lapTimes: [89, 91, 90], + } + const result = computeCumulativeDeltas([refWithNull, validChallenger]) + expect(result[0].deltas[0]).toBeCloseTo(-1) + expect(result[0].deltas[1]).toBeNull() + // Lap 3 excludes the reference-null window for both: (89+90) - (90+92) = -3 + expect(result[0].deltas[2]).toBeCloseTo(-3) + }) + it('returns an empty array when only one series is provided', () => { expect(computeCumulativeDeltas([reference])).toEqual([]) }) @@ -97,6 +115,19 @@ describe('DeltaTimeGraph', () => { expect(screen.queryByTestId('delta-line-VER')).not.toBeInTheDocument() }) + it('splits polylines at reference-null laps', () => { + const refWithNull: DeltaSeries = { + label: 'VER', + color: '#3671C6', + lapTimes: [90, null, 92], + } + const { container } = render( + , + ) + const lines = container.querySelectorAll('.delta-graph-driver-line') + expect(lines.length).toBeGreaterThan(1) + }) + it('shows a crosshair tooltip on hover', () => { vi.spyOn(SVGSVGElement.prototype, 'getBoundingClientRect').mockReturnValue({ x: 0, @@ -118,4 +149,31 @@ describe('DeltaTimeGraph', () => { expect(screen.getByText(/Lap 1/)).toBeInTheDocument() vi.restoreAllMocks() }) + + it('omits tooltip rows on reference-null laps', () => { + vi.spyOn(SVGSVGElement.prototype, 'getBoundingClientRect').mockReturnValue({ + x: 0, + y: 0, + left: 0, + top: 0, + width: 640, + height: 220, + right: 640, + bottom: 220, + toJSON: () => ({}), + }) + const refWithNull: DeltaSeries = { + label: 'VER', + color: '#3671C6', + lapTimes: [90, null, 92], + } + const { container } = render( + , + ) + const hoverLayer = container.querySelector('.delta-graph-hover-layer') + fireEvent.mouseMove(hoverLayer!, { clientX: 352, clientY: 100 }) + expect(screen.getByTestId('delta-crosshair')).toBeInTheDocument() + expect(screen.queryByTestId('delta-tooltip')).not.toBeInTheDocument() + vi.restoreAllMocks() + }) }) From 2d6fa531f2edcea0daa9045c44218d79867119a0 Mon Sep 17 00:00:00 2001 From: AmanTahiliani Date: Sat, 4 Jul 2026 01:16:01 -0400 Subject: [PATCH 2/2] fix(compare): gap deltas beyond reference laps --- frontend/src/lib/delta.ts | 6 +++--- frontend/src/test/delta-time-graph.test.tsx | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/delta.ts b/frontend/src/lib/delta.ts index a828393..c4738d4 100644 --- a/frontend/src/lib/delta.ts +++ b/frontend/src/lib/delta.ts @@ -35,12 +35,12 @@ function buildAlignedCumulative( const length = Math.max(lapTimes.length, referenceLapTimes.length) for (let i = 0; i < length; i++) { - if (referenceLapTimes[i] === null) { + if (referenceLapTimes[i] == null) { cumulative.push(running) continue } const lap = lapTimes[i] - if (lap !== null) { + if (lap != null) { running += lap } cumulative.push(running) @@ -84,7 +84,7 @@ export function computeCumulativeDeltas( const deltas: (number | null)[] = [] for (let i = 0; i < lapCount; i++) { - if (refLapTimes[i] === null || driver.lapTimes[i] === null) { + if (refLapTimes[i] == null || driver.lapTimes[i] == null) { deltas.push(null) continue } diff --git a/frontend/src/test/delta-time-graph.test.tsx b/frontend/src/test/delta-time-graph.test.tsx index 65aa0e9..7895dc4 100644 --- a/frontend/src/test/delta-time-graph.test.tsx +++ b/frontend/src/test/delta-time-graph.test.tsx @@ -78,6 +78,23 @@ describe('computeCumulativeDeltas', () => { expect(result[0].deltas[2]).toBeCloseTo(-3) }) + it('gaps challenger laps beyond a shorter reference series', () => { + const shortReference: DeltaSeries = { + label: 'VER', + color: '#3671C6', + lapTimes: [90, 91], + } + const longerChallenger: DeltaSeries = { + label: 'HAM', + color: '#E8002D', + lapTimes: [89, 92, 90], + } + const result = computeCumulativeDeltas([shortReference, longerChallenger]) + expect(result[0].deltas[0]).toBeCloseTo(-1) + expect(result[0].deltas[1]).toBeCloseTo(0) + expect(result[0].deltas[2]).toBeNull() + }) + it('returns an empty array when only one series is provided', () => { expect(computeCumulativeDeltas([reference])).toEqual([]) })