Files
app/frontend/src/components/Dashboard/palette.check.ts
T
stroblme 6272876f8f Dashboard-level chart palette
A dashboard names the data colours its charts draw with: an ordered, distinct
subset of the `--chart-1…5` ramp, stored on the settings channel that already
carries `theme` and `locked`. No backend or client change — `SettingDef.value`
is free-form and a name this build does not wire up is left alone rather than
refused.

Naming none is the whole ramp, which resolves to the identical token per
series as the `--chart-${(index % 5) + 1}` charts drew with before, so every
existing dashboard is unaffected. `palette.check.ts` asserts that equivalence
rather than trusting it.

Distinct slots, not free assignment with repeats: only slot 1 against slot 5
clears 3:1 within the ramp, so two traces on one slot could not be told apart.
Status colour is deliberately outside the palette — a fault is `--destructive`
because of what it means, not because of where it sits.

The provider is mounted by the editor as well as the view, so picking a
palette repaints the charts beside the panel instead of describing them.
2026-08-23 08:45:31 +02:00

82 lines
2.5 KiB
TypeScript

/**
* The dashboard palette, checked.
*
* ponytail: a script rather than a suite, like `color.check.ts` beside it —
* the frontend's only runner is Playwright and picking a ramp slot does not
* need a browser:
*
* cd frontend && bun run src/components/Dashboard/palette.check.ts
*
* The first block is the one that matters. A palette changes how *every*
* existing dashboard is drawn, and the whole safety argument is that a
* dashboard which names none is drawn exactly as it was before palettes
* existed — `--chart-${(index % 5) + 1}`, the literal expression `seriesColor`
* used. That equivalence is asserted here rather than trusted.
*/
import assert from "node:assert/strict"
import {
CHART_SLOTS,
DEFAULT_PALETTE,
paletteOf,
slotFor,
} from "@/components/Common/UplotChart"
/** What a chart drew before a dashboard could name a palette. */
const before = (index: number) => String((index % 5) + 1)
// A document with no palette, and every shape a broken one can arrive in:
// all of them fall back to the ramp, and the ramp is the old expression.
for (const stored of [undefined, null, "", [], ["9", "nonsense"], { 0: "1" }]) {
const palette = paletteOf(stored)
assert.deepEqual(
palette,
DEFAULT_PALETTE,
`${JSON.stringify(stored)} is no palette`,
)
for (let index = 0; index < 23; index++) {
assert.equal(
slotFor(index, palette),
before(index),
`series ${index} of an unset palette draws as it always did`,
)
}
}
// The same, for a chart that was handed nothing at all — Health and Home draw
// through the very same component.
for (let index = 0; index < 23; index++) {
assert.equal(
slotFor(index),
before(index),
`series ${index} outside a dashboard`,
)
}
// A distinct subset in draw order: kept as picked, and cycled through.
assert.deepEqual(
paletteOf(["3", "1"]),
["3", "1"],
"the order picked is the order kept",
)
assert.deepEqual(
[0, 1, 2, 3, 4].map((index) => slotFor(index, ["3", "1"])),
["3", "1", "3", "1", "3"],
"a chart with more lines than colours starts over",
)
// Repeats are dropped rather than drawn: two lines on one slot could not be
// told apart, which is the whole reason the picker offers each slot once.
assert.deepEqual(paletteOf(["2", "2", "4"]), ["2", "4"], "a repeat is dropped")
assert.deepEqual(paletteOf([1, 2]), ["1", "2"], "numbers name slots too")
assert.deepEqual(paletteOf([...CHART_SLOTS].reverse()), [
"5",
"4",
"3",
"2",
"1",
])
console.log("palette: ok")