Suppress the target band for laps that are just an unplanned continuation

Confirmed against Garmin Connect's own workout view: a lap whose
IntensityType merely repeats the immediately preceding lap's (e.g. a second
"cool-down" lap right after the first) means the recording continued past
the end of that step, not that the workout prescribed a second target for
it. Only the first lap of such a run keeps its target/HR fields; the
continuation shows the actual trace with no expectation overlay. Doesn't
affect normal interval structure, since Effort/Recovery always alternate
and never appear back-to-back.
This commit is contained in:
2026-07-19 17:17:19 +02:00
parent 1fb00f5bc6
commit 619277b0ee

View File

@@ -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;
}