Chart cursor, range picker in the header, line smoothing

A dashboard canvas is CSS-scaled to fit its panel while uPlot maps the
pointer against its own unscaled plot width, so the cursor drifted further
right the further into a chart it went. A `cursor.move` refiner divides the
visual offset back into layout pixels; unscaled hosts get a no-op.

A querying chart's range picker moves onto the frame's title line through a
new `useHeaderSlot`, giving the plot back the row it spent. The editor's drag
handle is the header, so the picker is exempted from it.

Charts can be drawn as a monotone cubic spline — uPlot's own path builder,
monotone so a smoothed line never invents a reading between two real ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
This commit is contained in:
2026-08-23 06:27:48 +02:00
co-authored by Claude Opus 5
parent a225b48d0d
commit 680c6053b9
6 changed files with 153 additions and 19 deletions
@@ -647,6 +647,24 @@ export function WidgetPanel({
</div>
) : null}
{widget.type === "chart" ? (
<div className="grid gap-1.5">
<div className="flex items-center justify-between gap-2 text-sm">
Smoothing
<Switch
checked={Boolean(cfg.smooth)}
aria-label="Smoothing"
data-testid="chart-smooth"
onCheckedChange={(smooth) => set({ smooth })}
/>
</div>
<p className="text-xs text-muted-foreground">
Curves the lines between readings. It draws them softer; it does
not change what was measured.
</p>
</div>
) : null}
{widget.type === "stat" ||
widget.type === "gauge" ||
widget.type === "chart" ||