fix: theme-aware chart colors — readable axes and tooltips in light mode
This commit is contained in:
@@ -121,9 +121,27 @@
|
|||||||
// Reset when switching away from a zone-capable metric or leaving histogram
|
// Reset when switching away from a zone-capable metric or leaving histogram
|
||||||
$: if (!canAlignZones) alignZones = false;
|
$: 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 ────────────────────────────────────────────────────────────
|
// ── Rendering ────────────────────────────────────────────────────────────
|
||||||
onMount(() => { renderChart(); });
|
let themeObserver: MutationObserver | null = null;
|
||||||
onDestroy(() => { chart?.remove(); chart = 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) {
|
$: if (chartEl) {
|
||||||
activeTab; xMode; chartType; histData; histThresholds; alignZones;
|
activeTab; xMode; chartType; histData; histThresholds; alignZones;
|
||||||
@@ -162,6 +180,7 @@
|
|||||||
|
|
||||||
function renderLine(w: number, h: number, yKey: string, yLabel: string, color: string) {
|
function renderLine(w: number, h: number, yKey: string, yLabel: string, color: string) {
|
||||||
const x = xMode === 'distance' ? 'dist_km' : 't';
|
const x = xMode === 'distance' ? 'dist_km' : 't';
|
||||||
|
const tc = getThemeColors();
|
||||||
const marks: any[] = [];
|
const marks: any[] = [];
|
||||||
|
|
||||||
if (activeTab === 'cadence') {
|
if (activeTab === 'cadence') {
|
||||||
@@ -174,12 +193,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
marks.push(
|
marks.push(
|
||||||
Plot.ruleX(data, Plot.pointerX({ x, stroke: 'rgba(255,255,255,0.3)', strokeWidth: 1, strokeDasharray: '4,4' })),
|
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: 'white', strokeWidth: 1.5 })),
|
Plot.dot(data, Plot.pointerX({ x, y: yKey, r: 4, fill: color, stroke: tc.tooltipBg, strokeWidth: 1.5 })),
|
||||||
Plot.text(data, Plot.pointerX({
|
Plot.text(data, Plot.pointerX({
|
||||||
x, y: yKey,
|
x, y: yKey,
|
||||||
text: (d: any) => d[yKey] != null ? `${Math.round(d[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({
|
return Plot.plot({
|
||||||
width: w, height: h, marginLeft: 48, marginBottom: 32,
|
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 },
|
x: { label: null, tickFormat: xTickFormat, grid: false, ticks: 6 },
|
||||||
y: { label: yLabel, grid: true, tickCount: 4 },
|
y: { label: yLabel, grid: true, tickCount: 4 },
|
||||||
marks,
|
marks,
|
||||||
@@ -204,6 +225,7 @@
|
|||||||
const yTickFormat = (v: number) => v >= 60 ? `${Math.round(v / 60)}m` : `${v}s`;
|
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 rawZones = activeTab === 'hr' ? athlete?.hr_zones : activeTab === 'power' ? athlete?.power_zones : null;
|
||||||
const zoneColors = activeTab === 'hr' ? HR_ZONE_COLORS : PWR_ZONE_COLORS;
|
const zoneColors = activeTab === 'hr' ? HR_ZONE_COLORS : PWR_ZONE_COLORS;
|
||||||
|
const tc = getThemeColors();
|
||||||
|
|
||||||
// ── Zone-aligned: one colored bar per zone ──────────────────────────────
|
// ── Zone-aligned: one colored bar per zone ──────────────────────────────
|
||||||
if (alignZones && rawZones?.length) {
|
if (alignZones && rawZones?.length) {
|
||||||
@@ -224,7 +246,7 @@
|
|||||||
|
|
||||||
return Plot.plot({
|
return Plot.plot({
|
||||||
width: w, height: h, marginLeft: 48, marginBottom: 32,
|
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]] },
|
x: { label: yLabel, grid: false, domain: [clampedZones[0][0], clampedZones[clampedZones.length - 1][1]] },
|
||||||
y: { label: 'Time', grid: true, tickCount: 4, tickFormat: yTickFormat },
|
y: { label: 'Time', grid: true, tickCount: 4, tickFormat: yTickFormat },
|
||||||
marks: [
|
marks: [
|
||||||
@@ -240,7 +262,7 @@
|
|||||||
fontSize: 10, fontWeight: '600',
|
fontSize: 10, fontWeight: '600',
|
||||||
dy: -8,
|
dy: -8,
|
||||||
}),
|
}),
|
||||||
Plot.ruleY([0], { stroke: '#52525b' }),
|
Plot.ruleY([0], { stroke: tc.ruleY }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -251,7 +273,7 @@
|
|||||||
{ y: 'count' },
|
{ y: 'count' },
|
||||||
{ x: yKey, fill: color, fillOpacity: 0.7, thresholds: histThresholds },
|
{ x: yKey, fill: color, fillOpacity: 0.7, thresholds: histThresholds },
|
||||||
)),
|
)),
|
||||||
Plot.ruleY([0], { stroke: '#52525b' }),
|
Plot.ruleY([0], { stroke: tc.ruleY }),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (rawZones?.length) {
|
if (rawZones?.length) {
|
||||||
@@ -282,7 +304,7 @@
|
|||||||
|
|
||||||
return Plot.plot({
|
return Plot.plot({
|
||||||
width: w, height: h, marginLeft: 48, marginBottom: 32,
|
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] },
|
x: { label: yLabel, grid: false, ticks: 6, domain: [trimMin, trimMax] },
|
||||||
y: { label: 'Time', grid: true, tickCount: 4, tickFormat: yTickFormat },
|
y: { label: 'Time', grid: true, tickCount: 4, tickFormat: yTickFormat },
|
||||||
marks,
|
marks,
|
||||||
|
|||||||
@@ -82,6 +82,10 @@
|
|||||||
|
|
||||||
$: colorMap = Object.fromEntries(selectedKeys.map((k, i) => [k, curveColor(k, i)]));
|
$: 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) {
|
function renderChart(data: typeof plotData, cmap: typeof colorMap) {
|
||||||
if (!chartEl) return;
|
if (!chartEl) return;
|
||||||
chartEl.innerHTML = '';
|
chartEl.innerHTML = '';
|
||||||
@@ -95,7 +99,7 @@
|
|||||||
height: 320,
|
height: 320,
|
||||||
marginLeft: 52,
|
marginLeft: 52,
|
||||||
marginBottom: 40,
|
marginBottom: 40,
|
||||||
style: { background: 'transparent', color: '#e4e4e7' },
|
style: { background: 'transparent', color: getAxisColor() },
|
||||||
x: {
|
x: {
|
||||||
type: 'log',
|
type: 'log',
|
||||||
label: 'Duration',
|
label: 'Duration',
|
||||||
@@ -160,7 +164,9 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
const ro = new ResizeObserver(() => renderChart(currentPlotData, currentColorMap));
|
const ro = new ResizeObserver(() => renderChart(currentPlotData, currentColorMap));
|
||||||
ro.observe(chartEl);
|
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 ─────────────────────────────────────────────────────────
|
// ── Toggle helpers ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user