datalean core — $, render, HTML, SVGthe original sin of reactive UI
Almost every reactive system re-runs the whole computation when a single
value changes. data re-runs only the path that changed — work scales
with the edit, not the dataset. Wrap a value in $(), chain
operators, bind to the DOM. No virtual DOM, no diffing.
Like crossfilter's incremental aggregation meets Solid-style fine-grained DOM — for UIs over large, fast-changing data: dashboards, analytics, grids, live feeds.
≈29 kB gzip · 0 dependencies · no virtual DOM · MIT
The same engine you pick above, doing two jobs. Top: one realistic order-book tick stream through the same depth-chart-and-ladder viz — data moves a couple of bucket counters per tick (O(1)); every peer re-walks all 150,000 orders once per frame (O(N)), and the green baseline is data against the 16ms budget. Bottom: the same library filtering 231,083 flight rows live as you brush, with its real per-interaction latency. Every implementation is the real library doing the idiomatic thing.
A change is small. You set one field on one row. Yet most reactive stacks respond by re-running everything downstream — re-filtering the whole list, re-summing every number, diffing an entire tree — and then trusting a memoiser to paper over the waste.
data takes the change literally. A mutation carries its exact path, and each operator in the chain does work scoped to that path alone:
orders[42].bid = 99.85
No diff pass. No list re-render. No selector re-run over 200,000 rows. The cost of an update is the length of the path it travels — which is why the race above holds at any rate, and why it scales to datasets that make recompute-everything systems stall.
One source, many derived views. Wrap a collection in $() and
chain operators; each returns a reactive view that updates incrementally
as the source changes. Every row below runs live off one shared
$(trades) feed of rate trades — pick any tenor, watch the
top-K reshuffle, see the groups recount as bid/ask/pnl tick.
trades.filter('tenor', '5Y')1671×trades.between('pnl', [-500, 500])547×trades.gt('pnl', 0)351×trades.za('pnl', 5)420×trades.length(d => d.tenor)447×trades.avg('pnl')723×trades.some(r => r.pnl > 1000)723×fiveY.intersect(gainers)441×fiveY.union(gainers)250×trades.group(d => d.tenor)2310×trades.distinct(r => r.tenor)1241×trades.map(r => ({ id, spread }))1902×trades.reduce(add, remove, 0)29556×trades.tap(() => n++)1875×trades.keys()7621××-figures are the peak speedup — the widest gap to a peer — across eight measured peer libraries; click any to see the full fastest→slowest range in its reproducible run (npm run bench:ops regenerates them). some/every reuse the scalar-aggregate path, so they carry its figure. Summary table →
datalean core — $, render, HTML, SVGdata/fullcore + every operator + JSX helpersdata/renderjust the DOM render layerdata/devtoolsopt-in reactive-graph inspectorimport { $ } from 'data/full'
const trades = $(rows)
const top = trades
.map(t => ({ ...t, spread: t.ask - t.bid }))
.filter(t => t.spread > 0.10)
.za('spread', 10)
trades[42].bid = 99.5 // O(1) + O(1) + O(log N)
import { $, render, HTML } from 'data'
const { ul, li } = HTML
const todos = $([{ task: 'foo' }, { task: 'bar' }])
render(document.body,
ul(todos, (node, item) => node.text(item.task))
)
todos.insert({ task: 'baz' }) // a new <li> appears
JSX works too (jsxImportSource: "data"). render() does per-key surgical
updates — element identity and focus survive across changes. See it running in the
gallery ↓.
Import data/devtools and a graph-first overlay auto-mounts: a
right-edge dock with a Tree/DAG view of the reactive graph and a slide-in
inspector (Inspect / Events / Profile). Hold Alt for inline
badges, click ◎ to pick any DOM element back to the view that
bound it. Lazy-loaded — the console API alone
($.inspect, $.trace, $.profile)
doesn't pay the panel's bytes.
Or try it on a worked example: todo-jsx ?devtools → · crossfilter ?devtools →
Each is the same library under a different load — runnable from
npm run serve, source in examples/.
An eight-section essay on the duality at the core of the library: a table and its stream of changes are the same thing in two forms, so a view — a filter, a count, a grouping — stays live for the price of the change, not the table — O(Δ), not O(N) — flowing only through the views that read it, down to a single minimal DOM instruction. The readability of recompute, the cost of hand-tuned deltas, and none of the bookkeeping.
Brushable charts over flight records: between → intersect → length(group) → za → limit, every brush incremental.
A messaging app in JSX: the open channel is filter('channel', c).az('ts'), channel counts a per-channel length histogram, and reactions a <For> over a nested object that mutates in place. A bot streams messages and blast 200 inserts a batch via patch — new rows append while existing rows keep DOM identity.
Each lives under examples/ — npm run serve and open any of them.