dashboard: pace a querying chart by its window, and an example to evaluate it

A chart is drawn in buckets, and nothing it can show changes until the
bucket it is drawing closes — so the resolution sets the refresh rather
than a flat five-second floor. A week at quarter-hour buckets now asks
four times an hour instead of sixty, for the same picture. Leaving the
field empty follows the window; a slower rate is still honoured.

`make seed-example` builds the thing to evaluate it with: a flow that
logs a temperature to InfluxDB, a flow that answers a chart's request by
turning the window into Flux and the rows back into a series, and a
dashboard holding the chart. The reading flow declares the request as an
input with a starting value, which is how a flow says a value reaches it
from a panel rather than from a node upstream.

Axis labels keep enough decimals to stay distinct — `si` rounds to three
figures, so every tick of a chart living inside one degree read "19".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:34:32 +02:00
co-authored by Claude Opus 5
parent 61bfd68f26
commit 1c09b8209d
5 changed files with 61 additions and 16 deletions
+23 -3
View File
@@ -29,6 +29,22 @@ function token(name: string): string {
const seriesColor = (index: number) => token(`--chart-${(index % 5) + 1}`)
/**
* Tick labels that stay distinct.
*
* `si` shortens to three significant figures, which is what a curve spanning
* decades wants and the opposite of what one wobbling inside a degree does —
* 18.9 and 19.1 both read "19", and the axis says nothing at all. When the
* short labels would repeat, the tick spacing decides the decimals instead.
*/
function tickLabels(ticks: number[]): string[] {
const short = ticks.map((value) => si(value))
if (new Set(short).size === short.length) return short
const step = Math.abs(ticks[1] - ticks[0]) || 1
const decimals = Math.min(6, Math.max(0, Math.ceil(-Math.log10(step))))
return ticks.map((value) => value.toFixed(decimals))
}
/** The series joined onto one x axis, which is what uPlot draws. */
function table(plots: HistoryPoint[][]): uPlot.AlignedData {
return uPlot.join(
@@ -160,7 +176,9 @@ export function UplotChart({
// The gutter has a fixed width, so a grouped "15,000" would be
// clipped to something that reads as a different number entirely.
values: (_self: uPlot, ticks: number[]) =>
ticks.map((value) => (unit ? `${si(value)} ${unit}` : si(value))),
tickLabels(ticks).map((label) =>
unit ? `${label} ${unit}` : label,
),
},
],
series: [
@@ -172,9 +190,11 @@ export function UplotChart({
// rebuilt chart.
stroke: () => seriesColor(index),
// The cursor readout is what decides how wide the legend gets, so
// it is shortened here; a named unit is short enough to keep.
// it is shortened here; a named unit is short enough to keep. Four
// figures rather than three: this is the number someone is pointing
// at to read, and 18.97 rounded to "19" is not an answer.
value: (_self: uPlot, raw: number) =>
unit ? `${si(raw)} ${unit}` : si(raw),
unit ? `${si(raw, 4)} ${unit}` : si(raw, 4),
// Series arrive on their own clocks; a joined table is mostly
// holes, and a line with a hole per point is not a line.
spanGaps: true,