fix activities' types
This commit is contained in:
@@ -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: {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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' }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user