Drill-down and charts
Both of these are headless. The grid gives you data; you render the
surface. EnterprisePivotGrid ships the components but wires neither — so a
first integration that "does nothing" is usually this, not a bug.
Drill-down: the rows behind a cell
Clicking a cell resolves the source records that produced it and hands them to
onCellClick.
import { useState } from 'react';
import { EnterprisePivotGrid, PivotDrillDownModal } from '@kanunilabs/pivotgrid-react-enterprise';
const [drill, setDrill] = useState<{ records: unknown[]; rowPath: string[]; colPath: string[] } | null>(null);
<EnterprisePivotGrid
licenseKey={key}
data={data}
initialFields={fields}
locale="de"
onCellClick={(e) => {
// Guard the cell type — headers and totals resolve too.
if (e.cellType !== 'data' || !e.records?.length) return;
setDrill({ records: e.records, rowPath: e.rowPath, colPath: e.colPath });
}}
/>
{drill && (
<PivotDrillDownModal
records={drill.records}
rowPath={drill.rowPath}
colPath={drill.colPath}
locale="de" // the window sits outside the grid, so give it the grid's language
fields={fields} // columns titled by your captions instead of the record keys
onClose={() => setDrill(null)}
/>
)}
e.records is found when you first read it, by scanning the dataset — the grid
does not keep the rows behind each cell. A handler that never reads it costs
nothing. See the limits.
The window is yours to place, usually outside the grid, so it cannot see the
grid's language: pass the same locale (and dictionary, if you reword any
caption). Its title, record count, search box and buttons then follow it in
all twelve languages, numbers and dates are written for that locale, and
Arabic turns it right-to-left. Rendered inside a PivotConfigProvider, it
follows the provider when locale is left out.
Charts
Three pieces: a transformer turns the engine result into chart-shaped data, a hook keeps it in sync, and a factory renders it.
import { useMemo } from 'react';
import { EnterprisePivotGrid, ChartDataTransformer, ChartFactory } from '@kanunilabs/pivotgrid-react-enterprise';
// MEMOISE THIS. An inline literal recomputes the transform on every render.
const chartOptions = useMemo(
() => ({
transformer: ChartDataTransformer.transform, // required — there is no default
maxPoints: 100,
hideTotals: true,
inverted: false,
}),
[],
);
const [chart, setChart] = useState({ chartData: [], chartSeries: [] });
<EnterprisePivotGrid
licenseKey={key}
data={data}
initialFields={fields}
chartIntegrationOptions={chartOptions}
onChartSync={setChart}
/>
<div style={{ height: 320 }}>
<ChartFactory type="column" data={chart.chartData} series={chart.chartSeries} />
</div>
| Option | Type | Default |
|---|---|---|
transformer | (engineResult, …) => ChartNormalizedData | none — required in practice |
maxPoints | number | 100 |
hideTotals | boolean | true |
inverted | boolean | false |
Chart types: column, bar, line, area, pie, donut, scatter, xy,
bubble, polar, combination, waterfall, funnel, treemap, heatmap,
rangeBar, sunburst, histogram. The last seven are drawn as plain SVG rather
than through recharts.
A histogram counts how many categories fall in each band of the first visible
measure: with products on the rows and revenue as the value, "how many products
sold between 1,000 and 2,000". The bands share one round width — 1, 2 or 5 times
a power of ten — so the edges read as 0, 500, 1,000; bins asks for about that
many (default 10), and the last band includes its upper edge. countLabel names
the value axis (default 'Count'; pass the grid's word). Blank values are left
out. A bar stands for many categories, so it reports no onCategoryClick and the
keyboard does not stop on it. A sunburst draws the row fields as rings: the outermost field in the
middle, each arc its share of the whole, each inner ring the sum of the rings
outside it. Expand a row group to add its ring. It sizes the arcs by the first
visible measure, and a parent's arc is the sum of its children, which is right
for a sum or a count but not for an average. Zero and negative values take no
arc.
A range bar floats between the first two measures in each category, from the
smaller to the larger (say "Min of price" and "Max of price"), each end in its
measure's colour. A pivot's
heatmap is its own matrix: row categories across, column paths and measures
down. Pass totalLabel to caption the waterfall's closing bar in your
language. scatter is a dot plot: dots over the category axis.
xy is the true scatter chart. It puts one point per category at
(first measure, second measure) for each column path. bubble sizes each
point by a third measure, so put the measures in the data area in that order.
With too few measures the chart says what is missing (xyNeedsMeasuresText
sets that sentence in your language; rangeNeedsTwoText does the same for a
rangeBar, which needs a low and a high measure). The renderer is recharts, a peer dependency — install it
yourself. ResponsiveContainer fills 100% of its parent, so the parent needs a
height.
ChartFactory also takes:
| Prop | Type | Default |
|---|---|---|
stacking | 'grouped' | 'stacked' | 'normalized' | 'grouped' |
width, height | number (px) | fill the parent |
animate | boolean | true |
format | ChartFormat: title, categoryAxisTitle, valueAxisTitle, legend, dataLabels, valueScale, font, labelAngle, gridLines, gridDashed, lineWidth, lineDashed, fillOpacity, palette, colors | none |
fields | the grid's PivotField[] | none |
palette | string[]; 'theme' for a ramp from --pivot-primary; or a named set — 'classic', 'colorblind', 'vivid', 'monochrome' — kept at 3:1 contrast with --pivot-surface. format.palette and format.colors win over it. | the transformer's colours |
hiddenSeries, onHiddenSeriesChange | string[] of series dataKeys, and the legend clicks | kept inside |
defaultHiddenSeries | string[] | none |
stacking lays the series of a column, bar, line or area chart on top of one
another, or as shares of 100% with a percent axis. It needs two or more series
and leaves combination charts alone. Negative values stack below zero, a
missing value leaves a gap, and the tooltip shows the real value with the share
beside it.
A filling chart is sized by a ResizeObserver, which never fires while the
chart is hidden (an inactive tab, a collapsed panel), so it draws nothing until
shown. Pass both width and height to draw at a fixed size straight away.
animate={false} turns off the entry animation, for dashboards that redraw
often and for images taken right after mount.
format takes the same shape the chart panels edit: a title above the chart,
axis titles, the legend at the top, bottom, left or right (or 'none'), and
data labels, and valueScale: 'log' for a logarithmic value axis (zero and
negatives are left out; stacked charts stay linear). It also takes the text
(font: { family, size, color, bold, italic }), the category-label angle
(labelAngle: 'auto' | 0 | 45 | 90), the grid (gridLines: 'value' | 'both' | 'none', gridDashed) and the series lines (lineWidth, lineDashed,
fillOpacity), and the series colours: palette names a set — 'theme',
'classic', 'colorblind' (the Okabe–Ito hues), 'vivid' or 'monochrome' —
whose colours are kept at 3:1 contrast with the chart background, and colors
gives your own list, used exactly as given. A field left out keeps the chart's
own look. The JavaScript grid's chart panel edits all of them (its Format
dialog has a Colours picker) except the font family, bold or italic text and
colors. Pass fields (the grid's current fields) and tooltips and labels
print each series the way its measure prints in the grid, using the field's
format. Without fields they use the locale's number format.
A click on a category — a bar, a slice, a dot-plot point or a shape cell —
calls onCategoryClick with the category and the row values behind it, ready
to filter the pivot by:
<ChartFactory
type="column"
data={chart.chartData}
series={chart.chartSeries}
onCategoryClick={({ path }) =>
path.forEach((p) => controller.applyFilter(p.fieldId, new Set([p.value]), 'include'))
}
/>
path has one entry per row field, so a click on "EMEA - Q1" names both the
region and the quarter. It is empty for the grand total.
With onCategoryClick set, the chart is also one tab stop: the arrow keys
(and Home / End) move a focused category, a chip at its
top names it with its values and position ("APAC: Revenue 30 — 2 of 3"), its
bars or slice are outlined, and Enter or Space calls
onCategoryClick exactly as a click would. The chip is a live region, so a
screen reader says the same; it speaks the locale you pass, in the twelve
languages. Without onCategoryClick the chart is not wrapped and takes no
keys.
Everything about the chart is already your state — the type, stacking and
format are props — except which series the reader switched off in the legend.
Pass hiddenSeries with onHiddenSeriesChange to own that too and save it with
the rest; the keys are the series' dataKeys, the ones the JavaScript panel's
getChartState() uses.
A chart in a window
ChartWindow puts a chart in a window over the pivot: moved by its title bar,
resized from its corner, both from the keyboard too (arrow keys on its two
grips), and always kept inside its parent. Several windows are several charts —
the last one touched comes to the front.
import { ChartWindow, ChartFactory } from '@kanunilabs/pivotgrid-react-enterprise';
<div style={{ position: 'relative', height: 600 }}>
<EnterprisePivotGrid /* … */ onChartSync={setChart} />
{windows.map((w) => (
<ChartWindow
key={w.id}
title={w.title}
defaultRect={w.rect} // { x, y, width, height }; centred without it
onRectChange={(rect) => save(w.id, rect)}
onClose={() => remove(w.id)}
toolbar={
<select value={w.type} onChange={(e) => setType(w.id, e.target.value as ChartType)}>
<option value="column">Column</option>
<option value="pie">Pie</option>
</select>
}
>
<ChartFactory type={w.type} data={chart.chartData} series={chart.chartSeries} />
</ChartWindow>
))}
</div>
The window is positioned inside its parent, and makes the parent a
positioning box while it is mounted if it is not one already. defaultRect is
read once; after that the window is the user's to move, and onRectChange
reports each finished move or resize so you can open it there next time.
labels names the grips and the close button in your language
({ move, resize, close }; English by default).
In the JavaScript package there is no charting peer: @kanunilabs/pivotgrid-enterprise
draws the fifteen types as its own SVG and exports them as PNG or SVG — see
Charts without a charting library.
Row sparkline
A pivot row already holds a series: one value per column, month after month.
rowSparkline draws it small in the row's grand-total cell, in front of the
total, so you can see at a glance which rows rise and which fall:
<EnterprisePivotGrid licenseKey={key} data={rows} initialFields={fields} rowSparkline />
// or pick the measure, bars instead of a line, and the size
<EnterprisePivotGrid rowSparkline={{ field: 'units', type: 'bar', width: 72, height: 18 }} … />
createEnterprisePivotGrid(host, { rowSparkline: true, … }) takes the same
option. The trend runs over the columns on screen, in their on-screen order: a
collapsed column group is one step, an expanded one is its children, and totals
are left out. Each row has its own scale, because the line shows the shape and
the total beside it shows the size. A month with no data leaves a gap in the
line. A negative bar takes the theme's danger colour.
It needs the grand-total column (showColumnGrandTotals, on by default) and at
least two columns. The grand-total column keeps its width, so give it more
room (setColumnWidths) if the totals are long. It is drawn through the
cellRender plugin socket: pass your own registry and the sparkline
registers into it. A measure with its own cellTemplate keeps that template.
What these do not do
Both
- There is no server-side data source.
data: TRow[]in memory is the only input — to the grid, to drill-down and to charts. PivotGrid has no remote row model at all. Size your dataset accordingly. - Object identity is load-bearing.
chartIntegrationOptionsis in the memo's dependency list, so an inline literal recomputes the transform every render and re-firesonChartSync— which typically sets state, which re-renders. Memoise it. - No keyboard or screen-reader path into either. The drill-down modal has
no
role="dialog", no focus trap and no focus restore; the chart emits norole, noaria-labeland no tabular fallback, so its numbers exist only as SVG geometry. - Neither inherits the grid's theme, and in fullscreen the drill-down modal
portals to
document.body— outside the fullscreen element, so it is not visible. Wrap them in your ownPivotThemeProvider. - Everything is hardcoded English — the whole drill-down modal and the chart type selector. Neither goes through the dictionary.
Drill-down
- Clicking a grand-total cell hands you the entire filtered dataset. The
row path is empty, so both match loops run zero times and nothing narrows.
The modal then renders it unpaginated — one
<tr>per source row, with no cap and no warning. Guard oncellTypeand onrecords.length. - Reading
recordsscans the whole dataset once, on the main thread. The rows are found rather than remembered: the cell's path is matched first and the filters run only on what matched. About 30–60 ms per read on a million rows on a recent laptop; the same event's second read is free. - Columns are inferred from the first record only —
Object.keys(records[0])— so heterogeneous records lose the keys the first one lacks. Headers are produced by capitalising the key. - An empty record set renders nothing and says nothing. The modal returns
null, so your state says a dialog is open and the screen shows none.
Charts
- No transformer means an empty chart.
usePivotChartSyncneedschartIntegrationOptions.transformer; without one it returns no data and warns once in the console. - Series are capped.
maxPointscaps the categories (default 100) andmaxSeriesthe series (default 20). The hook's result reports both (isSampledoftotalArgs,isSeriesSampledoftotalSeries), so say so when the chart shows part of the pivot. - Colours follow the theme only when asked. Without
palettethe transformer's eight colours are used, wrapping, so series nine repeats series one's.palette="theme"derives them from the theme instead. - Cross-filter is yours to apply.
onCategoryClickreports the category and the row values behind it; the factory filters nothing itself (the JavaScript panel does, withchartCrossFilter). Lines and areas have no mark to click. ChartFactoryis headless. It draws a chart; maximising, a type picker and export buttons are the app's (ChartWindowis the window to put it in). The exporters are exported (exportChartPng,exportChartJpeg,exportChartSvg,chartImageDataUrlfor the image as adata:URL at a size you choose, andfindChartSvgfor the container).
Without a licence key both run watermarked.