правки по графикам
Gitea Actions Demo / build_and_push (push) Failing after 23s Details

This commit is contained in:
artem 2026-09-12 17:12:27 +03:00
parent 43354ecaed
commit 2cfd863dbf
4 changed files with 69 additions and 23 deletions

View File

@ -50,14 +50,31 @@ export class ChartGroup {
) { ) {
continue; 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( chart.tooltip.setActiveElements(
[{ datasetIndex: 0, index: dataIndex }], [{ datasetIndex: 0, index: dataIndex }],
{ pos,
x: 0,
y: 0,
},
); );
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 { } finally {
this.hovering = false; this.hovering = false;
@ -81,7 +98,9 @@ export class ChartGroup {
continue; continue;
} }
chart.tooltip.setActiveElements([], { x: 0, y: 0 }); 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 { } finally {
this.hovering = false; this.hovering = false;

View File

@ -119,8 +119,8 @@ export const GetWorkout = (url: string) => {
yAxisID: "logAxis", yAxisID: "logAxis",
radius: 0, radius: 0,
label: "Мощность", label: "Мощность",
borderColor: "#cccccc", borderColor: "#00bcd4",
backgroundColor: "#cccccc", backgroundColor: "#00bcd4",
data: power, data: power,
}, },
], ],

View File

@ -3,8 +3,6 @@ import { Ref } from "vue";
import { createTypedChart } from "vue-chartjs"; import { createTypedChart } from "vue-chartjs";
import { LineController } from "chart.js"; import { LineController } from "chart.js";
const lineAlign = 8;
type GetMapPlugin = { type GetMapPlugin = {
mapX: Ref<Array<number>>; mapX: Ref<Array<number>>;
}; };
@ -55,21 +53,28 @@ class LineWithLineController extends LineController {
ctx.stroke(); ctx.stroke();
ctx.restore(); 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) { if (this.chart?.tooltip && this.chart.tooltip.opacity > 0) {
let x = this.chart.tooltip.x - lineAlign; const active = this.chart.tooltip.getActiveElements();
if (this.chart.tooltip.xAlign === "right") { const point = active[0]
x = this.chart.tooltip.x + this.chart.tooltip.width + lineAlign; ? (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();
} }
} }
} }

View File

@ -129,6 +129,15 @@
{{ Math.floor(workoutItem.power) }} Вт {{ Math.floor(workoutItem.power) }} Вт
</div> </div>
</div> </div>
<div
class="workout-item-params"
v-if="workoutItem.power && workoutNP !== null"
>
<div class="workout-item-params-name">Нормализованная мощность:</div>
<div class="workout-item-params-value">
{{ Math.floor(workoutNP) }} Вт
</div>
</div>
<div class="workout-item-params" v-if="workoutItem.max_power"> <div class="workout-item-params" v-if="workoutItem.max_power">
<div class="workout-item-params-name">Максимальная мощность:</div> <div class="workout-item-params-name">Максимальная мощность:</div>
<div class="workout-item-params-value"> <div class="workout-item-params-value">
@ -311,6 +320,7 @@ import {
import LineWithLineChart from "./LineWithLineChart.js"; import LineWithLineChart from "./LineWithLineChart.js";
import ChartGroup from "./ChartGroup.js"; import ChartGroup from "./ChartGroup.js";
import { normalizedPower } from "./NormalizedPower.js";
import { import {
PowerZonesPlugin, PowerZonesPlugin,
ftpToZones, ftpToZones,
@ -440,6 +450,13 @@ const computeAreaData = (start: number, end: number) => {
Math.floor(sum / present.length).toString() + Math.floor(sum / present.length).toString() +
getUnit(config.data.datasets[0].label); 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; areaAvgData.value = avgData;
}; };
/** Splits the points into sections: 10-minute steps in time mode, 5-km steps in distance mode. */ /** Splits the points into sections: 10-minute steps in time mode, 5-km steps in distance mode. */
@ -889,6 +906,11 @@ const times: Array<string | number> =
data.power?.labels ?? data.power?.labels ??
data.elevation?.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<number | null>(() =>
normalizedPower(times, data.power?.datasets[0].data ?? [], 0, times.length),
);
// Rebuilt per x-axis mode: labels switch between times and distances and // 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. // options get a fresh x-scale, so every canvas must remount on mode change.
const chartConfigs = computed<Array<ChartConfig>>(() => { const chartConfigs = computed<Array<ChartConfig>>(() => {