diff --git a/frontend/src/components/charts/ExpectedVsActualChart.tsx b/frontend/src/components/charts/ExpectedVsActualChart.tsx index 94647cd..be1cd31 100644 --- a/frontend/src/components/charts/ExpectedVsActualChart.tsx +++ b/frontend/src/components/charts/ExpectedVsActualChart.tsx @@ -147,10 +147,20 @@ interface Point { function buildLapWindows(laps: Lap[]): LapWindow[] { const windows: LapWindow[] = []; let elapsedMin = 0; - for (const l of laps) { + laps.forEach((l, i) => { const start = elapsedMin; elapsedMin += l.DurationSeconds / 60; + // A lap whose type merely repeats the previous lap's (e.g. a second + // "cool-down" lap right after the first) means the recording continued + // past the end of that step -- confirmed against Garmin Connect's own + // workout view for a real activity where this happened: the run kept + // going well past the prescribed cool-down, and Garmin logged the extra + // time as a further same-type lap that was never actually part of the + // workout. Only the first lap of such a run carries a real prescribed + // target; the continuation's target/HR fields are suppressed. + const isContinuation = i > 0 && laps[i - 1].IntensityType === l.IntensityType; + // Pace (sec/km) is inverted vs speed (m/s): the *faster* (higher) speed // bound is the *lower* (faster) pace bound. const paceLow = l.TargetPaceHighMps != null ? paceSecPerKm(l.TargetPaceHighMps) : null; @@ -160,13 +170,13 @@ function buildLapWindows(laps: Lap[]): LapWindow[] { start, end: elapsedMin, intensityType: l.IntensityType, - targetPaceRange: paceLow != null && paceHigh != null ? [paceLow, paceHigh] : null, - targetHRRange: l.TargetHRLowBpm != null && l.TargetHRHighBpm != null ? [l.TargetHRLowBpm, l.TargetHRHighBpm] : null, + targetPaceRange: !isContinuation && paceLow != null && paceHigh != null ? [paceLow, paceHigh] : null, + targetHRRange: !isContinuation && l.TargetHRLowBpm != null && l.TargetHRHighBpm != null ? [l.TargetHRLowBpm, l.TargetHRHighBpm] : null, avgPace: paceSecPerKm(l.AvgSpeedMps), avgHR: l.AvgHR, color: PHASE_COLORS[l.IntensityType], }); - } + }); return windows; }