fix: theme-aware chart colors — readable axes and tooltips in light mode

This commit is contained in:
Davide Scaini
2026-04-15 22:18:06 +02:00
parent a95dd07e22
commit 5205a41224
2 changed files with 40 additions and 12 deletions
+32 -10
View File
@@ -121,9 +121,27 @@
// Reset when switching away from a zone-capable metric or leaving histogram
$: if (!canAlignZones) alignZones = false;
// ── Theme-aware colours ──────────────────────────────────────────────────
function getThemeColors() {
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
return {
axis: isDark ? '#71717a' : '#52525b', // zinc-500 / zinc-600
rule: isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)',
tooltipFg: isDark ? '#ffffff' : '#18181b',
tooltipBg: isDark ? '#09090b' : '#ffffff', // text outline backing
ruleY: isDark ? '#3f3f46' : '#d4d4d8', // baseline rule
};
}
// ── Rendering ────────────────────────────────────────────────────────────
onMount(() => { renderChart(); });
onDestroy(() => { chart?.remove(); chart = null; });
let themeObserver: MutationObserver | null = null;
onMount(() => {
renderChart();
themeObserver = new MutationObserver(() => renderChart());
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
});
onDestroy(() => { chart?.remove(); chart = null; themeObserver?.disconnect(); });
$: if (chartEl) {
activeTab; xMode; chartType; histData; histThresholds; alignZones;
@@ -162,6 +180,7 @@
function renderLine(w: number, h: number, yKey: string, yLabel: string, color: string) {
const x = xMode === 'distance' ? 'dist_km' : 't';
const tc = getThemeColors();
const marks: any[] = [];
if (activeTab === 'cadence') {
@@ -174,12 +193,14 @@
}
marks.push(
Plot.ruleX(data, Plot.pointerX({ x, stroke: 'rgba(255,255,255,0.3)', strokeWidth: 1, strokeDasharray: '4,4' })),
Plot.dot(data, Plot.pointerX({ x, y: yKey, r: 4, fill: color, stroke: 'white', strokeWidth: 1.5 })),
Plot.ruleX(data, Plot.pointerX({ x, stroke: tc.rule, strokeWidth: 1, strokeDasharray: '4,4' })),
Plot.dot(data, Plot.pointerX({ x, y: yKey, r: 4, fill: color, stroke: tc.tooltipBg, strokeWidth: 1.5 })),
Plot.text(data, Plot.pointerX({
x, y: yKey,
text: (d: any) => d[yKey] != null ? `${Math.round(d[yKey])}` : '',
dy: -12, fill: 'white', fontSize: 11, fontWeight: '600',
dy: -12,
fill: tc.tooltipFg, stroke: tc.tooltipBg, strokeWidth: 3,
fontSize: 11, fontWeight: '600',
})),
);
@@ -193,7 +214,7 @@
return Plot.plot({
width: w, height: h, marginLeft: 48, marginBottom: 32,
style: { background: 'transparent', color: 'var(--text-4, #a1a1aa)', fontSize: '11px' },
style: { background: 'transparent', color: tc.axis, fontSize: '11px' },
x: { label: null, tickFormat: xTickFormat, grid: false, ticks: 6 },
y: { label: yLabel, grid: true, tickCount: 4 },
marks,
@@ -204,6 +225,7 @@
const yTickFormat = (v: number) => v >= 60 ? `${Math.round(v / 60)}m` : `${v}s`;
const rawZones = activeTab === 'hr' ? athlete?.hr_zones : activeTab === 'power' ? athlete?.power_zones : null;
const zoneColors = activeTab === 'hr' ? HR_ZONE_COLORS : PWR_ZONE_COLORS;
const tc = getThemeColors();
// ── Zone-aligned: one colored bar per zone ──────────────────────────────
if (alignZones && rawZones?.length) {
@@ -224,7 +246,7 @@
return Plot.plot({
width: w, height: h, marginLeft: 48, marginBottom: 32,
style: { background: 'transparent', color: 'var(--text-4, #a1a1aa)', fontSize: '11px' },
style: { background: 'transparent', color: tc.axis, fontSize: '11px' },
x: { label: yLabel, grid: false, domain: [clampedZones[0][0], clampedZones[clampedZones.length - 1][1]] },
y: { label: 'Time', grid: true, tickCount: 4, tickFormat: yTickFormat },
marks: [
@@ -240,7 +262,7 @@
fontSize: 10, fontWeight: '600',
dy: -8,
}),
Plot.ruleY([0], { stroke: '#52525b' }),
Plot.ruleY([0], { stroke: tc.ruleY }),
],
});
}
@@ -251,7 +273,7 @@
{ y: 'count' },
{ x: yKey, fill: color, fillOpacity: 0.7, thresholds: histThresholds },
)),
Plot.ruleY([0], { stroke: '#52525b' }),
Plot.ruleY([0], { stroke: tc.ruleY }),
];
if (rawZones?.length) {
@@ -282,7 +304,7 @@
return Plot.plot({
width: w, height: h, marginLeft: 48, marginBottom: 32,
style: { background: 'transparent', color: 'var(--text-4, #a1a1aa)', fontSize: '11px' },
style: { background: 'transparent', color: tc.axis, fontSize: '11px' },
x: { label: yLabel, grid: false, ticks: 6, domain: [trimMin, trimMax] },
y: { label: 'Time', grid: true, tickCount: 4, tickFormat: yTickFormat },
marks,
+8 -2
View File
@@ -82,6 +82,10 @@
$: colorMap = Object.fromEntries(selectedKeys.map((k, i) => [k, curveColor(k, i)]));
function getAxisColor() {
return document.documentElement.getAttribute('data-theme') === 'light' ? '#52525b' : '#a1a1aa';
}
function renderChart(data: typeof plotData, cmap: typeof colorMap) {
if (!chartEl) return;
chartEl.innerHTML = '';
@@ -95,7 +99,7 @@
height: 320,
marginLeft: 52,
marginBottom: 40,
style: { background: 'transparent', color: '#e4e4e7' },
style: { background: 'transparent', color: getAxisColor() },
x: {
type: 'log',
label: 'Duration',
@@ -160,7 +164,9 @@
onMount(() => {
const ro = new ResizeObserver(() => renderChart(currentPlotData, currentColorMap));
ro.observe(chartEl);
return () => ro.disconnect();
const mo = new MutationObserver(() => renderChart(currentPlotData, currentColorMap));
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
return () => { ro.disconnect(); mo.disconnect(); };
});
// ── Toggle helpers ─────────────────────────────────────────────────────────