From b92af7e58a1b512fdf0527b4f04e5c94e442d954 Mon Sep 17 00:00:00 2001 From: artem Date: Sat, 5 Sep 2026 22:55:16 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=D0=B0=20=D1=82=D1=80=D0=B5=D0=BD=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/workouts/components/WorkoutItem.vue | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/pages/workouts/components/WorkoutItem.vue b/src/pages/workouts/components/WorkoutItem.vue index 8806bd9..bc531c3 100644 --- a/src/pages/workouts/components/WorkoutItem.vue +++ b/src/pages/workouts/components/WorkoutItem.vue @@ -213,6 +213,23 @@ {{ readoutValue[config.key] }} + + + + {{ zone.label }} + + (null); +// Power zones keyed by FTP; FTP is not a separate field, so the average +// power is used as the FTP heuristic (Strava-style). Null -> no heatmap. +// Absolute watt bounds do not depend on the x-axis mode, so the result is +// memoized per FTP value (computed cache + no re-allocation). +let powerZonesFtp: number | undefined | null = NaN; +let powerZonesMemo: Array | null = null; +const powerZones = computed | null>(() => { + const ftp = workoutItem?.power; + if (ftp !== powerZonesFtp) { + powerZonesFtp = ftp; + powerZonesMemo = ftpToZones(ftp); + } + return powerZonesMemo; +}); +// Expose the memoized zones to the shared plugin instance (read at draw time). +PowerZonesPlugin.zones = powerZones; // Inner section boundaries (point indexes, never 0 or the last point). const sectionBoundaries = computed>(() => sectionBounds.value @@ -558,6 +596,10 @@ const chartPlugins = [ } }, }, + // Power-zone heatmap: shared instance for all charts; the hook no-ops on + // every canvas except the power one (dataset label check). The zone list + // is exposed through the shared object, same pattern as the mapX ref. + PowerZonesPlugin, ]; /** X scale per mode: time scale for "time", plain meters for "distance". */ const xScaleOptions = (mode: XAxisMode): Record => { @@ -1101,4 +1143,24 @@ h3 { font-weight: normal; color: #07c; } +.power-zones-legend { + display: inline-flex; + flex-wrap: wrap; + gap: 4px 8px; + margin-left: 10px; + font-size: 11px; + font-weight: normal; + color: #555; +} +.power-zone-chip { + display: inline-flex; + align-items: center; + gap: 3px; +} +.power-zone-color { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 2px; +}