Chart a derived grid: source and derived data in one live chart
A revenue-by-region chart next to an orders table is two things to keep in step. Filter the table and the chart either follows or it doesn't, and the difference is usually a line of sync code nobody wrote.
The three usual fixes all leak:
- A second query. Two round trips doing the same arithmetic, two places for the business logic to drift apart, and a second failure mode on a panel that only summarises the first.
-
A copied array.
ordersreduced into a summary once, on load. Nothing tells the copy when the original changes. - A spreadsheet export. Correct for exactly the moment it was taken.
Lattice Grid has a different primitive: a derived grid.
What a derived grid is
A derived grid is a grid whose source is another grid rather than a dataset. You don't give it rows; you give it a recipe:
-
Which of the parent's rows to read (
follow):filtered(whatever the parent's filters leave),selected(only the ticked rows),grouped(one row per group of the user's own grouping) orall. -
What to do with them: group and aggregate (
groupBy+select), bucket dates (bucket), join a related grid, sort, keep the top N.
It holds no rows of its own. When the parent's rows change, whether from a filter, an edit or new data arriving, it re-runs the recipe over the rows the parent already has in memory.
Why that works
- There is only one copy of the data. The summary is a function of the source, not a second dataset, so it cannot drift.
- No second round trip. The rows already arrived once; the derivation is in-memory arithmetic over them.
- The rule lives in one place. "Revenue by region" is declared once, next to the grid it summarises.
- It is still a grid. Sorting, filtering, export, KPI tiles and charts all work on it unchanged, and they all inherit its liveness. A chart only ever reads a grid, so a chart over a derived grid is live by construction.
-
It is cheap by default. Re-derivation runs once per idle frame (
refresh: 'idle'), so a hundred edits in one burst cost one recalculation.
A chart that follows a filter
import { createGrid } from '@toclocoinc/lattice-grid';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
const book = createGrid(document.querySelector('#orders'), {
rowKey: 'id',
columns: [
{ field: 'id', title: 'Order' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'date', title: 'Date', type: 'dateString' },
{ field: 'amount', title: 'Amount', type: 'number' },
],
rows: orders, // sixteen orders across two months and three regions
});
// A second grid, built from the first.
const byRegion = createGrid(document.querySelector('#by-region'), {
columns: [
{ field: 'region', title: 'Region' },
{ field: 'revenue', title: 'Revenue', type: 'number' },
],
source: {
mode: 'derived',
from: book,
follow: 'filtered',
refresh: 'live',
groupBy: 'region',
select: { revenue: { of: 'amount', fn: 'sum' } },
sort: [{ col: 'revenue', dir: 'desc' }],
},
});
// The chart reads byRegion, not a copy of its rows.
createChart({ grid: byRegion, container: document.querySelector('#chart'), type: 'bar', x: 'region', y: 'revenue' });
follow: 'filtered' means the derived grid reads whatever the orders grid's filters leave. Filter the orders to EMEA and Americas, and byRegion drops to two rows: APAC is gone from the grid and from the chart in the same redraw, with no second call behind either.
The other follow modes: selected reads only the ticked rows, grouped reads one row per group of the user's own grouping, and all ignores the parent's filter (for an exceptions list that shouldn't shrink with the table).
Source and derived data in one chart
A chart reads one grid. To overlay the raw orders and a derived monthly total, build a third grid whose from is a union of the two. Each source gets a label and a map that reshapes its rows, and every combined row keeps a __source field saying where it came from.
const monthlyTotal = createGrid(document.querySelector('#monthly'), {
columns: [
{ field: 'date', title: 'Month', type: 'dateString' },
{ field: 'revenue', title: 'Revenue', type: 'number' },
],
source: {
mode: 'derived', from: book, follow: 'filtered', refresh: 'live',
groupBy: 'date', bucket: { of: 'date', by: 'month' },
select: { revenue: { of: 'amount', fn: 'sum' } },
},
});
const overlay = createGrid(document.querySelector('#overlay'), {
columns: [
{ field: '__source', title: 'Feed' },
{ field: 'date', title: 'Date' },
{ field: 'daily', title: 'Daily' },
{ field: 'monthly', title: 'Monthly' },
],
source: {
mode: 'derived', refresh: 'live',
from: [
{ grid: book, label: 'orders', map: (r) => ({ date: r.date, daily: r.amount }) },
{ grid: monthlyTotal, label: 'monthly total', map: (r) => ({ date: r.date, monthly: r.revenue }) },
],
sort: [{ col: 'date', dir: 'asc' }],
},
});
createChart({
grid: overlay,
container: document.querySelector('#overlay-chart'),
type: 'combo',
x: 'date',
measures: [
{ col: 'daily', fn: 'sum', type: 'bar', axis: 'left', title: 'Daily orders' },
{ col: 'monthly', fn: 'sum', type: 'line', axis: 'right', title: 'Monthly total' },
],
});
The map step is what makes the overlay work: the two sources don't share a column (one has amount, the other revenue), so each map renames its value into a column of its own. That lets one chart draw daily orders as bars and the monthly total as a line on its own right-hand axis, since a month of orders runs far larger than any single order.
With the sixteen sample orders, overlay holds eighteen rows: sixteen tagged orders and two tagged monthly total (January 3,725, February 3,090). Those two points are read from the same rows as the bars, not typed in separately. Filter the orders and monthlyTotal re-derives first, overlay next, and the chart redraws last, all on one frame.
When not to reach for one
A derived grid re-derives on an idle frame by default (refresh: 'idle'), so a hundred edits in one event cost one derivation. refresh: 'live' re-derives on every change, which suits a demo and wastes effort on a feed updating faster than anyone can read. refresh: 'manual' never re-derives on its own, for a panel someone refreshes with a button.
Both demos run live, with the full code, here: Chart a Derived Grid.
Top comments (0)