Dashboards, Charts, and Polling
Dashboards, Charts, and Polling: a worked guide from route registry through to a production build.
Example
Poll through a cancellable query, retain the last successful rows while refreshing, and give the plot stable row identity and an accessible label.
import { createPlot } from '@askrjs/charts';
const Plot = createPlot<MetricRow>();
<Plot.Root data={metrics()} rowKey={(row) => row.timestamp} label="Request rate">
<Plot.Axis axis="x" />
<Plot.Axis axis="y" />
<Plot.Line x="timestamp" y="requests" />
</Plot.Root>Goal and architecture
Charts come from @askrjs/charts, a typed Canvas 2D plotting library with an immutable scene also available for SVG and data export — there's no separate headless/themed split here, createPlot<Row>() is the one entry point. A dashboard combines that with @askrjs/askr/data's query layer for live numbers and invalidateOnInterval() for polling, so the chart re-renders whenever its backing query refetches rather than on its own timer.
Implementation
Call createPlot<RevenueRow>() once at module scope to get a typed namespace, then compose marks inside <RevenuePlot.Root data={...} rowKey="id" label="..."> — Bar, Line, and Point marks reference row fields by name, and the type system rejects a numeric channel bound to a string field. For live data, wrap the query driving the chart with invalidateOnInterval(prefix, { intervalMs, ... }) — prefix is a required first argument naming which query keys to invalidate, not an options-object-only call — so it refetches on a schedule, and remember to import @askrjs/charts/styles once at the app boundary since the structural and theme-token CSS ships separately from the JS. Import @askrjs/charts/styles from a CSS file, not a .ts/.tsx module — it's a CSS export, not a typed module.
Failure states
A polling query that errors mid-interval shouldn't tear down the chart and replace it with an error state on every failed poll — treat a transient poll failure differently from the query's initial load failure, or a flaky network turns a dashboard into a strobe light. Mixing primitives from two different createPlot() factories inside one Root is caught at runtime with a thrown TypeError, not statically by TypeScript — primitives from different factories can look structurally identical to the type checker, so don't rely on the compiler to catch this one; the fix if you hit the runtime error is the same, consolidating on one factory per row shape. Empty data (zero rows) should render an explicit empty state inside Root's children, since an empty canvas with no rows looks identical to a still-loading one.
Verification
Watch a polling-driven chart across several interval ticks and confirm it actually updates when the underlying data changes, not just on first load — it's easy to wire invalidateOnInterval to the wrong query key and have the chart look live while it's actually static. Resize the container and confirm the plot redraws correctly rather than clipping, since Canvas-backed charts don't get free reflow the way DOM/SVG ones sometimes do. Finally check the page under a throttled connection to confirm polling backs off or queues sensibly rather than piling up overlapping requests.