import {
appendPlotRows,
bin,
constant,
count,
createPlot,
filterRows,
group,
mean,
movingAverage,
movingWindow,
normalize,
partition,
regression,
removePlotRows,
sortRows,
stack,
sum,
trimPlotRows,
upsertPlotRows,
} from "@askrjs/charts";Load styles once in application CSS:
@import "@askrjs/charts/styles";Do not create the plot factory during render:
type RequestRow = {
id: string;
timestamp: Date;
latencyMs: number | null;
outcome: "ok" | "error";
};
const RequestPlot = createPlot<RequestRow>();Typed field names are the shortest channel form:
<RequestPlot.Point x="timestamp" y="latencyMs" fill="outcome" />Accessors are useful for units or derived labels. Return null or undefined to mark a value missing:
<RequestPlot.Point
x={(row) => row.timestamp}
y={(row) => (row.latencyMs == null ? null : row.latencyMs / 1_000)}
title={(row) => `${row.outcome}: ${row.latencyMs ?? "missing"} ms`}
/>A bare string names a field. Use constant(...) for literal strings:
<RequestPlot.Line x="timestamp" y="latencyMs" stroke={constant("#2563eb")} />This distinction prevents a field name such as red from being confused with a CSS color.
Expressions are immutable and can be kept near the plot definition:
const latencyBin = bin("latencyMs", { thresholds: 20 });
const rowCount = count();
const totalLatency = sum("latencyMs");
const meanLatency = mean("latencyMs");
const outcomeGroup = group("outcome");
const divergingLatency = stack("latencyMs", { offset: "diverging" });
const normalizedLatency = normalize("latencyMs");
const rollingMaximum = movingWindow("latencyMs", {
window: 12,
operation: "max",
partial: false,
});
const rollingMean = movingAverage("latencyMs", { window: 7 });
const latencyTrend = regression("latencyMs", { x: "timestamp" });Use them as typed channels:
<RequestPlot.Root data={rows} rowKey="id" label="Request latency distribution">
<RequestPlot.Bar x={latencyBin} y={rowCount} fill="outcome" stack="outcome" />
<RequestPlot.Line x="timestamp" y={rollingMean} />
<RequestPlot.Line x="timestamp" y={latencyTrend} />
</RequestPlot.Root>Aggregation expressions operate within the active bin/group. group(...) declares a grouping channel; count, sum, and mean produce its numeric result. stack(...) accepts offset: "zero" | "diverging" | "expand" and a stable series order. normalize(...) produces proportional values.
movingWindow supports sum, mean, min, and max. Set partial: false to leave the leading incomplete window missing. movingAverage fixes the operation to mean. Regression is linear in 0.1 and uses row index unless x is supplied.
Filter and sort one mark without rewriting the root's source rows:
<RequestPlot.Line
transform={[
filterRows<RequestRow>((row) => row.outcome === "ok"),
sortRows<RequestRow>({ by: "timestamp", direction: "ascending" }),
]}
x="timestamp"
y="latencyMs"
/>Partition accepts either flat id/parentId rows or nested children rows. Rect can consume its generated layout without explicit x/y channels:
type FrameRow = {
id: string;
parentId: string | null;
label: string;
service: string;
durationMs: number;
};
const TracePlot = createPlot<FrameRow>();
<TracePlot.Root data={frames} rowKey="id" label="Request trace">
<TracePlot.Rect
transform={partition<FrameRow>({
id: "id",
parentId: "parentId",
value: "durationMs",
padding: 0.004,
})}
fill="service"
title="label"
/>
<TracePlot.Legend interactive label="Service" />
<TracePlot.Tooltip channels={["label", "durationMs"]} />
</TracePlot.Root>;For a single Cartesian composition, inferred scales, axes, and tooltips are usually enough:
<RequestPlot.Root data={rows} rowKey="id" label="P95 latency">
<RequestPlot.Line x="timestamp" y="latencyMs" />
<RequestPlot.Point x="timestamp" y="latencyMs" />
</RequestPlot.Root>Use explicit named scales when domains, time zones, or units differ:
type ServiceRow = {
id: string;
timestamp: Date;
requests: number;
p95: number;
};
const ServicePlot = createPlot<ServiceRow>();
<ServicePlot.Root data={serviceRows} rowKey="id" label="Requests and latency">
<ServicePlot.Scale name="time" channel="x" type="utc" />
<ServicePlot.Scale name="requests" channel="y" type="linear" nice />
<ServicePlot.Scale name="latency" channel="y" type="symlog" constant={10} nice />
<ServicePlot.Bar x="timestamp" y="requests" xScale="time" yScale="requests" />
<ServicePlot.Line x="timestamp" y="p95" xScale="time" yScale="latency" />
<ServicePlot.Point x="timestamp" y="p95" xScale="time" yScale="latency" />
<ServicePlot.Axis scale="time" orient="bottom" label="UTC time" />
<ServicePlot.Axis scale="requests" orient="left" label="Requests" />
<ServicePlot.Axis scale="latency" orient="right" label="P95 (ms)" />
<ServicePlot.Grid scale="latency" axis="y" />
</ServicePlot.Root>;Use power with exponent, log with base, and symlog with its linear-region constant. A log scale accepts strictly positive values only. time uses local calendar ticks; utc uses UTC calendar ticks.
Color scales can be named too:
<RequestPlot.Scale
name="outcomes"
channel="color"
type="ordinal-color"
domain={["ok", "error"]}
/>
<RequestPlot.Bar
x={bin("latencyMs", { interval: 50 })}
y={count()}
fill="outcome"
colorScale="outcomes"
/>
<RequestPlot.Legend scale="outcomes" interactive /><Plot.Bar x="category" y="value" orientation="vertical" radius={4} />
<Plot.Line
x="timestamp"
y="value"
defined={(row) => row.value != null}
curve="monotone"
strokeWidth={2}
/>
<Plot.Area x="timestamp" y="high" y2="low" curve="monotone" />
<Plot.Point x="timestamp" y="value" r="weight" shape="diamond" />When stroke or fill creates multiple data-driven series, Plot.Line cycles
dash patterns and Plot.Point cycles shapes by default, so hue is not the only
series encoding. Set dash={[]} or an explicit point shape when a deliberate
single visual treatment is more appropriate.
<SharePlot.Arc value="value" category="category" innerRadius={0} />
<SharePlot.Arc
value="value"
category="category"
innerRadius={48}
padAngle={0.02}
cornerRadius={6}
/>
<GaugePlot.Root
data={gaugeRows}
rowKey="id"
label="Storage used"
meter={{ role: "meter", min: 0, max: 100, value: 72, valueText: "72%" }}
>
<GaugePlot.Arc value="value" min={0} max={100} startAngle={-Math.PI} endAngle={0} />
</GaugePlot.Root>Line.defined starts a new path run after every false row, so Canvas painting, hit regions,
downsampling, and SVG export all preserve the same discontinuities. Arc.padAngle separates
adjacent sectors and Arc.cornerRadius rounds their radial corners.
<HeatPlot.Cell x="day" y="hour" value="requests" inset={1} /><TimelinePlot.Rule x="startedAt" x2="finishedAt" y="lane" strokeWidth={3} />
<TimelinePlot.Point x="startedAt" y="lane" />
<TimelinePlot.Text x="startedAt" y="lane" text="label" align="left" /><ProgressPlot.Root
data={progressRows}
rowKey="id"
label="Migration progress"
meter={{ role: "meter", min: 0, max: 100, value: 64, valueText: "64%" }}
>
<ProgressPlot.Bar x="label" y="value" min={0} max={100} orientation="horizontal" />
</ProgressPlot.Root>See mark-families.tsx for one typed source file containing every mark.
<RequestPlot.Tooltip
channels={["timestamp", "latencyMs", "outcome"]}
format={(record) => `${record.outcome}: ${record.latencyMs} ms`}
/>
<RequestPlot.Crosshair axes="xy" />
<RequestPlot.Select mode="toggle" />
<RequestPlot.Legend scale="outcomes" interactive position="bottom" />
<RequestPlot.Zoom axes="xy" wheel pinch pan min={1} max={64} />
<RequestPlot.Brush axis="x" modifier="shift" />Wheel and pinch zoom the enabled axes. Primary drag pans when enabled. Shift-drag brushing avoids taking ordinary drag gestures from the page. Arrow keys inspect marks; Enter or Space activates the focused mark; plus and minus zoom; Home resets the view; and Shift plus an arrow pans. With brushing enabled, Shift+Space toggles the focused row in the selection. Keyboard inspection reads the same hit records as pointer inspection.
Select makes pointer and keyboard activation update selection before onActivate. Aggregated marks select all contributing source keys. Tooltip mode="x" inspects the nearest aligned Cartesian cohort; mode="mark" stays on one mark; auto chooses by family.
Use onActivate for product drill-down:
<RequestPlot.Root
data={rows}
rowKey="id"
label="Request latency"
onActivate={(row, key) => openRequest(row.id, key)}
>
<RequestPlot.Point x="timestamp" y="latencyMs" />
</RequestPlot.Root>Use defaultView and defaultSelection for local plot state. Use controlled props when a route, URL, or shared owner must persist the state:
const [view, setView] = state<PlotView>({});
const [selection, setSelection] = state<PlotSelection>({ keys: [] });
<RequestPlot.Root
data={rows}
rowKey="id"
label="Request latency"
view={view()}
onViewChange={(next) => setView(next)}
selection={selection()}
onSelectionChange={(next) => setSelection(next)}
>
<RequestPlot.Line x="timestamp" y="latencyMs" />
<RequestPlot.Zoom axes="xy" />
<RequestPlot.Brush axis="x" modifier="shift" />
</RequestPlot.Root>;Import state from @askrjs/askr and PlotView/PlotSelection as types from @askrjs/charts in a real component.
Update a readonly array through Askr state:
setRows((current) =>
trimPlotRows(appendPlotRows(current, nextRow), {
durationMs: 5 * 60_000,
field: "timestamp",
}),
);Other immutable update forms:
setRows((current) => upsertPlotRows(current, changedRows, "id"));
setRows((current) => removePlotRows(current, removedIds, "id"));
setRows((current) => trimPlotRows(current, { rows: 1_000 }));Pass the Askr state getter directly as plot data and configure a matching follow window:
<RequestPlot.Root
data={rows}
rowKey="id"
label="Live requests"
followLatest={{ durationMs: 5 * 60_000, field: "timestamp" }}
onApiChange={(next) => {
api = next;
}}
>
<RequestPlot.Line x="timestamp" y="latencyMs" />
<RequestPlot.Zoom axes="x" />
</RequestPlot.Root>Pan or zoom pauses following. Resume only in response to explicit operator intent:
<button type="button" onClick={() => api?.resumeLive()}>
Resume live
</button>Capture the mounted API with onApiChange:
let api: PlotApi<RequestRow> | null = null;
<RequestPlot.Root
data={rows}
rowKey="id"
label="Request latency"
onApiChange={(next) => {
api = next;
}}
>
<RequestPlot.Line x="timestamp" y="latencyMs" />
</RequestPlot.Root>;Then export the same scene:
const png = await api?.exportPng({ view: "current", pixelRatio: 2 });
const svg = api?.exportSvg({ view: "full", background: null });
const csv = api?.exportData({
view: "current",
rows: "transformed",
scope: "selected",
format: "csv",
});PNG and SVG require mounted dimensions. Current/full view and optional backgrounds are supported; pass background: null to preserve transparency. Transient overlays are excluded unless includeOverlays is true. Data export can choose current/full view, source/transformed rows, and all/visible/selected scope. CSV string cells that look like spreadsheet formulas are neutralized automatically.
<RequestPlot.Root
data={rows}
rowKey="id"
label="Request latency"
empty="No request latency is available for this range."
diagnostics
summary={({ sourceRowCount, visibleRowCount, omittedRowCount }) =>
`${visibleRowCount} of ${sourceRowCount} rows visible; ${omittedRowCount} omitted.`
}
>
<RequestPlot.Scale name="latency" channel="y" type="log" />
<RequestPlot.Point x="timestamp" y="latencyMs" yScale="latency" />
</RequestPlot.Root>Negative finite values are preserved. Missing and non-finite values are not converted to zero. Log scales omit zero and negative inputs and report the omission; choose symlog when those values are meaningful.
Always supply a useful label, and use title, description, or summary to explain the product question. SSR includes the reserved region and semantic content but not graphical marks. After hydration, canvas marks appear and the full transformed table is created only when “View data” is opened.
Tooltips must not carry the only copy of an important value. Use root meter semantics for bounded progress and gauges. Keep route-owned loading and errors outside the plot; use empty only for a successfully loaded empty dataset.
Use width as the SSR/initial fallback and height for deterministic structural height. Mounted width always observes its container. Override chart tokens at an app boundary:
.operations-plot {
--ak-chart-height: 24rem;
--ak-chart-series-1: var(--ak-color-accent);
--ak-chart-grid: color-mix(in srgb, currentcolor 14%, transparent);
}Prefer --ak-chart-* tokens and stable data-slot="plot-*" hooks. Do not style generated scene IDs or assume canvas geometry is DOM.