From cd8fbbcc8c36a3207a77f8838cd1e33216e48919 Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 20 Aug 2026 08:36:36 +0200 Subject: [PATCH] Chart: an optional y axis title The unit stays on the ticks; `yLabel` names the axis. It is part of the chart's rebuild identity, so editing the title takes effect at once. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA --- frontend/src/components/Common/UplotChart.tsx | 13 ++++++++++--- frontend/src/components/Dashboard/ChartWidget.tsx | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Common/UplotChart.tsx b/frontend/src/components/Common/UplotChart.tsx index b488ee7..7e43511 100644 --- a/frontend/src/components/Common/UplotChart.tsx +++ b/frontend/src/components/Common/UplotChart.tsx @@ -76,6 +76,7 @@ export function UplotChart({ empty = "Nothing has come through yet.", unit, yRange, + yLabel, onCursor, onSelect, }: { @@ -88,6 +89,8 @@ export function UplotChart({ unit?: string /** A y axis fixed to these bounds; unset lets it follow the data. */ yRange?: [number, number] + /** A named y axis; the unit stays on the ticks. */ + yLabel?: string /** The x value under the pointer, and null once it leaves the plot. */ onCursor?: (ts: number | null) => void /** The x value clicked, or null for a click that landed on no point. */ @@ -107,9 +110,9 @@ export function UplotChart({ const points = plots.reduce((total, plot) => total + plot.length, 0) // The identity of the series set: the chart is rebuilt when it changes, - // while a new reading only sets its data. The unit and the fixed range are - // part of it — both are baked into the axes when the chart is built. - const key = `${labels.join(" ")}|${unit ?? ""}|${yRange?.join(",") ?? ""}` + // while a new reading only sets its data. The unit, the fixed range and the + // axis title are part of it — all are baked into the axes at build time. + const key = `${labels.join(" ")}|${unit ?? ""}|${yRange?.join(",") ?? ""}|${yLabel ?? ""}` // uPlot leaves its axes half-initialised while the scales have no range, and // a resize in that window (a card still settling, say) draws them anyway and // throws. Waiting for the first reading avoids the state altogether. @@ -170,6 +173,10 @@ export function UplotChart({ { ...axis, size: 28 }, { ...axis, + label: yLabel, + // uPlot's default label font is a hardcoded bold sans-serif, which + // would ignore the app's face. + labelFont: axis.font, // The unit is written after every tick, so the gutter widens to // hold it rather than clipping the number in front of it. size: unit ? 62 : 46, diff --git a/frontend/src/components/Dashboard/ChartWidget.tsx b/frontend/src/components/Dashboard/ChartWidget.tsx index 9a5b603..e660672 100644 --- a/frontend/src/components/Dashboard/ChartWidget.tsx +++ b/frontend/src/components/Dashboard/ChartWidget.tsx @@ -67,6 +67,7 @@ function presentation(cfg: Record) { const high = Number(cfg.y_max) return { unit: cfg.unit ? String(cfg.unit) : undefined, + yLabel: cfg.y_label ? String(cfg.y_label) : undefined, yRange: Number.isFinite(low) && Number.isFinite(high) && low < high ? ([low, high] as [number, number])