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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
This commit is contained in:
2026-08-20 08:36:36 +02:00
co-authored by Claude Opus 5
parent 49cb5be632
commit cd8fbbcc8c
2 changed files with 11 additions and 3 deletions
+10 -3
View File
@@ -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,
@@ -67,6 +67,7 @@ function presentation(cfg: Record<string, unknown>) {
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])