get default hr and power zones from config file

This commit is contained in:
Davide Scaini
2026-03-29 22:06:22 +02:00
parent 3fcc8bc089
commit 4537273de9
11 changed files with 224 additions and 16 deletions
+52 -8
View File
@@ -1,11 +1,15 @@
<script lang="ts">
import * as Plot from '@observablehq/plot';
import { onMount } from 'svelte';
import type { Timeseries } from '../lib/types';
import type { Timeseries, AthleteZones } from '../lib/types';
export let timeseries: Timeseries;
// Linked hover: emit/receive index into timeseries arrays
export let hoveredIdx: number | null = null;
export let athlete: AthleteZones | null = null;
const HR_ZONE_COLORS = ['#60a5fa', '#4ade80', '#facc15', '#fb923c', '#f87171'];
const PWR_ZONE_COLORS = ['#60a5fa', '#34d399', '#facc15', '#fb923c', '#f87171', '#c084fc', '#f43f5e'];
type Tab = 'elevation' | 'speed' | 'hr' | 'cadence' | 'power';
type XMode = 'time' | 'distance';
@@ -181,18 +185,58 @@
function renderHistogram(w: number, h: number, yKey: string, yLabel: string, color: string) {
const yTickFormat = (v: number) => v >= 60 ? `${Math.round(v / 60)}m` : `${v}s`;
const marks: any[] = [
Plot.rectY(histData, Plot.binX(
{ y: 'count' },
{ x: yKey, fill: color, fillOpacity: 0.7, thresholds: histThresholds },
)),
Plot.ruleY([0], { stroke: '#52525b' }),
];
const rawZones = activeTab === 'hr' ? athlete?.hr_zones : activeTab === 'power' ? athlete?.power_zones : null;
const zoneColors = activeTab === 'hr' ? HR_ZONE_COLORS : PWR_ZONE_COLORS;
if (rawZones?.length) {
// Boundary vertical lines (interior boundaries only, skip first lo and last hi)
const boundaries = rawZones.slice(0, -1).map((z, i) => ({
x: z[1], // the upper bound of each zone = lower bound of the next
color: zoneColors[i + 1] ?? zoneColors[zoneColors.length - 1],
})).filter(b => b.x > trimMin && b.x < trimMax);
// Zone midpoints for labels
const labels = rawZones.map((z, i) => ({
mid: (Math.max(z[0], trimMin) + Math.min(z[1], trimMax)) / 2,
label: `Z${i + 1}`,
color: zoneColors[i] ?? zoneColors[zoneColors.length - 1],
visible: z[1] > trimMin && z[0] < trimMax,
})).filter(l => l.visible && l.mid >= trimMin && l.mid <= trimMax);
marks.push(
Plot.ruleX(boundaries, {
x: 'x',
stroke: (d: any) => d.color,
strokeWidth: 1,
strokeOpacity: 0.5,
strokeDasharray: '4,3',
}),
Plot.text(labels, {
x: 'mid',
text: 'label',
fill: (d: any) => d.color,
fontSize: 9,
fontWeight: '600',
frameAnchor: 'top',
dy: 6,
}),
);
}
return Plot.plot({
width: w, height: h, marginLeft: 48, marginBottom: 32,
style: { background: 'transparent', color: '#a1a1aa', fontSize: '11px' },
x: { label: yLabel, grid: false, ticks: 6, domain: [trimMin, trimMax] },
y: { label: 'Time', grid: true, tickCount: 4, tickFormat: yTickFormat },
marks: [
Plot.rectY(histData, Plot.binX(
{ y: 'count' },
{ x: yKey, fill: color, fillOpacity: 0.7, thresholds: histThresholds },
)),
Plot.ruleY([0], { stroke: '#52525b' }),
],
marks,
});
}
</script>