From 2cfd863dbf12735e6ed2169fcf5eebdd84b79e2e Mon Sep 17 00:00:00 2001 From: artem Date: Sat, 12 Sep 2026 17:12:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/workouts/components/ChartGroup.ts | 31 ++++++++++++---- src/pages/workouts/components/GetWorkout.ts | 4 +-- .../workouts/components/LineWithLineChart.ts | 35 +++++++++++-------- src/pages/workouts/components/WorkoutItem.vue | 22 ++++++++++++ 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/pages/workouts/components/ChartGroup.ts b/src/pages/workouts/components/ChartGroup.ts index e9c233c..c0f82a4 100644 --- a/src/pages/workouts/components/ChartGroup.ts +++ b/src/pages/workouts/components/ChartGroup.ts @@ -50,14 +50,31 @@ export class ChartGroup { ) { continue; } + // Event position must be the real pixel coordinates of the data + // point: with a fake (0, 0) position the tooltip box on receiver + // charts is positioned from the corner and flips/clamps near the + // chart edges, so the hover line lagged and got stuck at borders. + const el = chart.getDatasetMeta(0).data[dataIndex] as + { x?: number; y?: number } | undefined; + const px = el?.x; + const py = el?.y; + const pos = + px !== undefined && + py !== undefined && + Number.isFinite(px) && + Number.isFinite(py) + ? { x: px, y: py } + : { x: 0, y: 0 }; chart.tooltip.setActiveElements( [{ datasetIndex: 0, index: dataIndex }], - { - x: 0, - y: 0, - }, + pos, ); - chart.draw(); + // update("none") = full recompute + render without animations; the + // animator does not start, so tooltip.opacity/position apply + // synchronously and the blue line is visible on "cold" charts + // immediately (with chart.draw() the opacity stayed 0 until the + // first animation tick). + chart.update("none"); } } finally { this.hovering = false; @@ -81,7 +98,9 @@ export class ChartGroup { continue; } chart.tooltip.setActiveElements([], { x: 0, y: 0 }); - chart.draw(); + // Same as in setHover: opacity -> 0 must apply synchronously, + // otherwise the lines would linger until the animator starts. + chart.update("none"); } } finally { this.hovering = false; diff --git a/src/pages/workouts/components/GetWorkout.ts b/src/pages/workouts/components/GetWorkout.ts index 178b3f9..fe72ba0 100644 --- a/src/pages/workouts/components/GetWorkout.ts +++ b/src/pages/workouts/components/GetWorkout.ts @@ -119,8 +119,8 @@ export const GetWorkout = (url: string) => { yAxisID: "logAxis", radius: 0, label: "Мощность", - borderColor: "#cccccc", - backgroundColor: "#cccccc", + borderColor: "#00bcd4", + backgroundColor: "#00bcd4", data: power, }, ], diff --git a/src/pages/workouts/components/LineWithLineChart.ts b/src/pages/workouts/components/LineWithLineChart.ts index 8816a55..03dfa6a 100644 --- a/src/pages/workouts/components/LineWithLineChart.ts +++ b/src/pages/workouts/components/LineWithLineChart.ts @@ -3,8 +3,6 @@ import { Ref } from "vue"; import { createTypedChart } from "vue-chartjs"; import { LineController } from "chart.js"; -const lineAlign = 8; - type GetMapPlugin = { mapX: Ref>; }; @@ -55,21 +53,28 @@ class LineWithLineController extends LineController { ctx.stroke(); ctx.restore(); } + // Draw the vertical hover line at the pixel X of the active tooltip + // element (the data point itself), not at the tooltip box position: + // the box has a fixed size and flips/clamps near the chart edges, so + // using tooltip.x would lag and get stuck at the borders. if (this.chart?.tooltip && this.chart.tooltip.opacity > 0) { - let x = this.chart.tooltip.x - lineAlign; - if (this.chart.tooltip.xAlign === "right") { - x = this.chart.tooltip.x + this.chart.tooltip.width + lineAlign; + const active = this.chart.tooltip.getActiveElements(); + const point = active[0] + ? (this.chart.getDatasetMeta(active[0].datasetIndex).data[ + active[0].index + ] as { x?: number } | undefined) + : undefined; + const x = point?.x ?? NaN; + if (Number.isFinite(x)) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(x, topY); + ctx.lineTo(x, bottomY); + ctx.lineWidth = 1; + ctx.strokeStyle = "#07C"; + ctx.stroke(); + ctx.restore(); } - - // draw line - ctx.save(); - ctx.beginPath(); - ctx.moveTo(x, topY); - ctx.lineTo(x, bottomY); - ctx.lineWidth = 1; - ctx.strokeStyle = "#07C"; - ctx.stroke(); - ctx.restore(); } } } diff --git a/src/pages/workouts/components/WorkoutItem.vue b/src/pages/workouts/components/WorkoutItem.vue index cc7baa1..69afbe9 100644 --- a/src/pages/workouts/components/WorkoutItem.vue +++ b/src/pages/workouts/components/WorkoutItem.vue @@ -129,6 +129,15 @@ {{ Math.floor(workoutItem.power) }} Вт +
+
Нормализованная мощность:
+
+ {{ Math.floor(workoutNP) }} Вт +
+
Максимальная мощность:
@@ -311,6 +320,7 @@ import { import LineWithLineChart from "./LineWithLineChart.js"; import ChartGroup from "./ChartGroup.js"; +import { normalizedPower } from "./NormalizedPower.js"; import { PowerZonesPlugin, ftpToZones, @@ -440,6 +450,13 @@ const computeAreaData = (start: number, end: number) => { Math.floor(sum / present.length).toString() + getUnit(config.data.datasets[0].label); } + if (data.power?.datasets[0].data && end - start >= 2) { + const np = normalizedPower(times, data.power.datasets[0].data, start, end); + if (np !== null) { + avgData["Нормализованная мощность"] = + Math.floor(np).toString() + getUnit("Мощность"); + } + } areaAvgData.value = avgData; }; /** Splits the points into sections: 10-minute steps in time mode, 5-km steps in distance mode. */ @@ -889,6 +906,11 @@ const times: Array = data.power?.labels ?? data.elevation?.labels ?? []; +// Normalized power for the whole workout (data is static after load, so a +// plain computed is memoized automatically). +const workoutNP = computed(() => + normalizedPower(times, data.power?.datasets[0].data ?? [], 0, times.length), +); // Rebuilt per x-axis mode: labels switch between times and distances and // options get a fresh x-scale, so every canvas must remount on mode change. const chartConfigs = computed>(() => {