Channels and Transforms
Channels and Transforms: anatomy, keyboard behavior, state, and theming in Askr.
Example
import { createPlot, movingAverage } from '@askrjs/charts';
const Plot = createPlot<ProjectRow>();
<Plot.Root data={rows} rowKey="id" label="Smoothed request rate">
<Plot.Line x="timestamp" y={movingAverage('requests', { window: 7 })} />
</Plot.Root>Channels
A channel on any mark accepts a bare row-field name, an accessor `(row, index) => value`, or a channel expression — nothing else. Bare strings are always interpreted as field names, so a literal string value (a fixed color, a fixed label) has to be wrapped in `constant("...")` or it will be read as a field lookup instead. Accessors can derive values inline, as in `y={(row) => row.value / 1_000}`.
Aggregation
`bin(input, options?)` groups a numeric or `Date` channel by thresholds, interval, or an explicit domain, and pairs naturally with `count()`, `sum(input)`, or `mean(input)` on the paired channel to build histograms and grouped summaries. `group(input)` declares the grouping key for a compound aggregate. `stack(input, options?)` and `normalize(input)` turn a numeric channel into stacked or proportional values, with `offset` (`zero`, `diverging`, `expand`) and `order` controlling how series accumulate.
Window transforms
`movingWindow(input, { window, operation?, partial? })` computes a rolling `sum`, `mean`, `min`, or `max` over a fixed row count, and `movingAverage(input, { window })` is the rolling-mean shorthand of the same helper. `regression(input, options?)` fits a linear trend either against row index or an explicit x channel. All three skip missing numeric values rather than treating them as zero, which keeps trend lines honest over sparse data.
Row lineage
Mark-level `transform` runs before layout and never mutates the source rows — `filterRows(predicate)`, `sortRows({ by, direction? })`, and `partition({ id, parentId?, children?, value, padding? })` each produce a fresh, frozen row set. That copy happens as soon as any transform is configured at all, not just when a transform actually changes something — a `filterRows` predicate that happens to keep every row still gets a newly allocated, frozen array back, not the original reference; only marks with no `transform` configured at all skip the copy and pass the original array through untouched. Because rendering is driven off this transformed output, stable row keys are what let selection, transitions, and live updates track a row across filtering, sorting, or re-partitioning.