fix activities' types

This commit is contained in:
Davide Scaini
2026-03-29 10:37:08 +02:00
parent 3441079913
commit 643d092acd
5 changed files with 60 additions and 19 deletions
+6
View File
@@ -127,6 +127,12 @@ Learnt the hard way during debugging (March 2026):
`addTo()`, which needs valid lngLat → same `'lng'` crash. Set a dummy `[0, 0]` `addTo()`, which needs valid lngLat → same `'lng'` crash. Set a dummy `[0, 0]`
if the real position arrives later (e.g. hover markers). if the real position arrives later (e.g. hover markers).
## Observable Plot — known gotchas
- **Curve names are hyphenated, not camelCase.**
Use `"monotone-x"`, not `"monotoneX"`. Plot uses its own curve name registry
(not raw d3 identifiers). Wrong names throw `unknown curve` at runtime.
The working `astro.config.mjs` Vite section: The working `astro.config.mjs` Vite section:
```js ```js
vite: { vite: {
+5 -1
View File
@@ -60,7 +60,7 @@ def _process_file(path: Path) -> dict:
if activity.source_hash in _known_hashes: if activity.source_hash in _known_hashes:
return {"status": "duplicate"} return {"status": "duplicate"}
# Enrich from Strava CSV # Enrich from Strava CSV (CSV is authoritative for sport on Strava activities)
row = _strava_lookup.get(activity.source_file) row = _strava_lookup.get(activity.source_file)
if row: if row:
if not activity.title: if not activity.title:
@@ -69,6 +69,10 @@ def _process_file(path: Path) -> dict:
activity.description = row.get("Activity Description", "").strip() or None activity.description = row.get("Activity Description", "").strip() or None
if not activity.strava_id: if not activity.strava_id:
activity.strava_id = row.get("Activity ID", "").strip() or None activity.strava_id = row.get("Activity ID", "").strip() or None
csv_type = row.get("Activity Type", "").strip()
if csv_type:
from bincio.extract.sport import normalise_sport
activity.sport = normalise_sport(csv_type)
try: try:
metrics = compute(activity) metrics = compute(activity)
+12 -2
View File
@@ -16,7 +16,7 @@ class FitParser:
points: list[DataPoint] = [] points: list[DataPoint] = []
laps: list[LapData] = [] laps: list[LapData] = []
sport: str = "cycling" sport: str = "other"
sub_sport: str | None = None sub_sport: str | None = None
device: str | None = None device: str | None = None
@@ -26,7 +26,17 @@ class FitParser:
continue continue
if frame.name == "sport": if frame.name == "sport":
sport = normalise_sport(_get(frame, "sport", "cycling")) sport = normalise_sport(_get(frame, "sport"))
sub_sport = _normalise_sub_sport(_get(frame, "sub_sport"))
elif frame.name == "session":
# Karoo and Strava-generated FIT files store sport here
# instead of (or in addition to) a separate 'sport' message.
# Only use session sport if no 'sport' frame was seen yet.
if sport == "other":
raw_sport = _get(frame, "sport")
if raw_sport is not None:
sport = normalise_sport(raw_sport)
sub_sport = _normalise_sub_sport(_get(frame, "sub_sport")) sub_sport = _normalise_sub_sport(_get(frame, "sub_sport"))
elif frame.name == "device_info": elif frame.name == "device_info":
+25 -3
View File
@@ -1,33 +1,53 @@
"""Sport name normalisation.""" """Sport name normalisation."""
import re
_MAPPING: dict[str, str] = { _MAPPING: dict[str, str] = {
# cycling variants # cycling variants (FIT enums, GPX types, Strava API/CSV types)
"cycling": "cycling", "cycling": "cycling",
"biking": "cycling", "biking": "cycling",
"bike": "cycling", "bike": "cycling",
"ride": "cycling",
"road_biking": "cycling", "road_biking": "cycling",
"road_cycling": "cycling",
"mountain_biking": "cycling", "mountain_biking": "cycling",
"mountain_bike_ride": "cycling",
"gravel_cycling": "cycling", "gravel_cycling": "cycling",
"gravel_ride": "cycling",
"cyclocross": "cycling", "cyclocross": "cycling",
"indoor_cycling": "cycling", "indoor_cycling": "cycling",
"indoor_ride": "cycling",
"virtual_ride": "cycling", "virtual_ride": "cycling",
"e-biking": "cycling", "e_biking": "cycling",
"ebikeride": "cycling",
"e_bike_ride": "cycling",
"ebike_ride": "cycling",
"handcycle": "cycling",
"velomobile": "cycling",
# running # running
"running": "running", "running": "running",
"run": "running", "run": "running",
"trail_running": "running", "trail_running": "running",
"trail_run": "running",
"treadmill_running": "running", "treadmill_running": "running",
"treadmill": "running",
"virtual_run": "running", "virtual_run": "running",
"outdoor_run": "running",
"indoor_run": "running",
"track_run": "running",
# hiking # hiking
"hiking": "hiking", "hiking": "hiking",
"hike": "hiking", "hike": "hiking",
"outdoor_hike": "hiking",
# walking # walking
"walking": "walking", "walking": "walking",
"walk": "walking", "walk": "walking",
"outdoor_walk": "walking",
# swimming # swimming
"swimming": "swimming", "swimming": "swimming",
"swim": "swimming", "swim": "swimming",
"open_water_swimming": "swimming", "open_water_swimming": "swimming",
"lap_swimming": "swimming",
} }
BAS_SPORTS = {"cycling", "running", "hiking", "walking", "swimming", "other"} BAS_SPORTS = {"cycling", "running", "hiking", "walking", "swimming", "other"}
@@ -36,5 +56,7 @@ BAS_SPORTS = {"cycling", "running", "hiking", "walking", "swimming", "other"}
def normalise_sport(raw: object) -> str: def normalise_sport(raw: object) -> str:
if raw is None: if raw is None:
return "other" return "other"
key = str(raw).lower().strip().replace(" ", "_") key = str(raw).lower().strip().replace(" ", "_").replace("-", "_")
# Strip leading date-like prefixes e.g. "20231117outdoor_run" → "outdoor_run"
key = re.sub(r"^\d+", "", key)
return _MAPPING.get(key, "other") return _MAPPING.get(key, "other")
+12 -13
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import * as Plot from '@observablehq/plot';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { Timeseries } from '../lib/types'; import type { Timeseries } from '../lib/types';
@@ -10,7 +11,6 @@
let activeTab: Tab = 'elevation'; let activeTab: Tab = 'elevation';
let chartEl: HTMLDivElement; let chartEl: HTMLDivElement;
let Plot: any;
let chart: SVGElement | null = null; let chart: SVGElement | null = null;
// Pre-build data array once // Pre-build data array once
@@ -34,18 +34,17 @@
cadence: 'Cadence', cadence: 'Cadence',
}; };
onMount(async () => { onMount(() => {
Plot = await import('@observablehq/plot');
renderChart(); renderChart();
}); });
$: if (Plot && chartEl) { $: if (chartEl) {
activeTab; // reactive dependency activeTab; // reactive dependency — re-render when tab changes
renderChart(); renderChart();
} }
function renderChart() { function renderChart() {
if (!Plot || !chartEl) return; if (!chartEl) return;
chart?.remove(); chart?.remove();
const w = chartEl.clientWidth || 800; const w = chartEl.clientWidth || 800;
@@ -59,25 +58,25 @@
if (activeTab === 'elevation' && hasElevation) { if (activeTab === 'elevation' && hasElevation) {
yKey = 'elevation'; yLabel = 'Elevation (m)'; color = '#00c8ff'; yKey = 'elevation'; yLabel = 'Elevation (m)'; color = '#00c8ff';
marks.push( marks.push(
Plot.areaY(data, { x: 't', y: yKey, fill: color, fillOpacity: 0.15, curve: 'monotoneX' }), Plot.areaY(data, { x: 't', y: yKey, fill: color, fillOpacity: 0.15, curve: 'monotone-x' }),
Plot.lineY(data, { x: 't', y: yKey, stroke: color, strokeWidth: 1.5, curve: 'monotoneX' }), Plot.lineY(data, { x: 't', y: yKey, stroke: color, strokeWidth: 1.5, curve: 'monotone-x' }),
); );
} else if (activeTab === 'speed' && hasSpeed) { } else if (activeTab === 'speed' && hasSpeed) {
yKey = 'speed'; yLabel = 'Speed (km/h)'; color = '#ff6b35'; yKey = 'speed'; yLabel = 'Speed (km/h)'; color = '#ff6b35';
marks.push( marks.push(
Plot.areaY(data, { x: 't', y: yKey, fill: color, fillOpacity: 0.15, curve: 'monotoneX' }), Plot.areaY(data, { x: 't', y: yKey, fill: color, fillOpacity: 0.15, curve: 'monotone-x' }),
Plot.lineY(data, { x: 't', y: yKey, stroke: color, strokeWidth: 1.5, curve: 'monotoneX' }), Plot.lineY(data, { x: 't', y: yKey, stroke: color, strokeWidth: 1.5, curve: 'monotone-x' }),
); );
} else if (activeTab === 'hr' && hasHR) { } else if (activeTab === 'hr' && hasHR) {
yKey = 'hr'; yLabel = 'Heart Rate (bpm)'; color = '#f87171'; yKey = 'hr'; yLabel = 'Heart Rate (bpm)'; color = '#f87171';
marks.push( marks.push(
Plot.areaY(data, { x: 't', y: yKey, fill: color, fillOpacity: 0.15, curve: 'monotoneX' }), Plot.areaY(data, { x: 't', y: yKey, fill: color, fillOpacity: 0.15, curve: 'monotone-x' }),
Plot.lineY(data, { x: 't', y: yKey, stroke: color, strokeWidth: 1.5, curve: 'monotoneX' }), Plot.lineY(data, { x: 't', y: yKey, stroke: color, strokeWidth: 1.5, curve: 'monotone-x' }),
); );
} else if (activeTab === 'cadence' && hasCadence) { } else if (activeTab === 'cadence' && hasCadence) {
yKey = 'cadence'; yLabel = 'Cadence (rpm)'; color = '#a78bfa'; yKey = 'cadence'; yLabel = 'Cadence (rpm)'; color = '#a78bfa';
marks.push( marks.push(
Plot.lineY(data, { x: 't', y: yKey, stroke: color, strokeWidth: 1.5, curve: 'monotoneX' }), Plot.lineY(data, { x: 't', y: yKey, stroke: color, strokeWidth: 1.5, curve: 'monotone-x' }),
); );
} }