get default hr and power zones from config file
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { marked } from 'marked';
|
||||
import type { ActivitySummary, ActivityDetail } from '../lib/types';
|
||||
import type { ActivitySummary, ActivityDetail, AthleteZones } from '../lib/types';
|
||||
import { formatDistance, formatDuration, formatElevation, formatSpeed, formatDate, formatTime, sportIcon, sportLabel, sportColor } from '../lib/format';
|
||||
import ActivityMap from './ActivityMap.svelte';
|
||||
import ActivityCharts from './ActivityCharts.svelte';
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
export let activity: ActivitySummary;
|
||||
export let base: string = '/';
|
||||
export let athlete: AthleteZones | null = null;
|
||||
|
||||
const editUrl = import.meta.env.PUBLIC_EDIT_URL;
|
||||
|
||||
@@ -237,7 +238,7 @@
|
||||
<p class="text-red-400 text-sm mt-4">{error}</p>
|
||||
{:else if detail?.timeseries && detail.timeseries.t.length > 0}
|
||||
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
|
||||
<ActivityCharts timeseries={detail.timeseries} bind:hoveredIdx />
|
||||
<ActivityCharts timeseries={detail.timeseries} bind:hoveredIdx {athlete} />
|
||||
</div>
|
||||
{:else if !detail}
|
||||
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4 h-32 animate-pulse" />
|
||||
|
||||
@@ -28,9 +28,16 @@ export interface ActivitySummary {
|
||||
preview_coords: [number, number][] | null;
|
||||
}
|
||||
|
||||
export interface AthleteZones {
|
||||
max_hr?: number;
|
||||
ftp_w?: number;
|
||||
hr_zones?: [number, number][];
|
||||
power_zones?: [number, number][];
|
||||
}
|
||||
|
||||
export interface BASIndex {
|
||||
bas_version: string;
|
||||
owner: { handle: string; display_name: string; avatar_url: string | null };
|
||||
owner: { handle: string; display_name: string; avatar_url: string | null; athlete?: AthleteZones };
|
||||
generated_at: string;
|
||||
shards: Array<{ year: number; url: string; count: number }>;
|
||||
activities: ActivitySummary[];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import ActivityDetail from '../../components/ActivityDetail.svelte';
|
||||
import type { BASIndex, ActivitySummary } from '../../lib/types';
|
||||
import type { BASIndex, ActivitySummary, AthleteZones } from '../../lib/types';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const dataDir = process.env.BINCIO_DATA_DIR
|
||||
@@ -15,13 +15,13 @@ export async function getStaticPaths() {
|
||||
.filter(a => a.privacy !== 'private' && a.id)
|
||||
.map(a => ({
|
||||
params: { id: a.id },
|
||||
props: { activity: a },
|
||||
props: { activity: a, athlete: index.owner.athlete ?? null },
|
||||
}));
|
||||
}
|
||||
|
||||
const { activity } = Astro.props as { activity: ActivitySummary };
|
||||
const { activity, athlete } = Astro.props as { activity: ActivitySummary; athlete: AthleteZones | null };
|
||||
const base = import.meta.env.BASE_URL;
|
||||
---
|
||||
<Base title={`${activity.title} — BincioActivity`}>
|
||||
<ActivityDetail {activity} {base} client:only="svelte" />
|
||||
<ActivityDetail {activity} {base} {athlete} client:only="svelte" />
|
||||
</Base>
|
||||
|
||||
Reference in New Issue
Block a user