Documentation

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>
OptionTypeDefault
transformer(engineResult, …) => ChartNormalizedDatanone — required in practice
maxPointsnumber100
hideTotalsbooleantrue
invertedbooleanfalse

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:

PropTypeDefault
stacking'grouped' | 'stacked' | 'normalized''grouped'
width, heightnumber (px)fill the parent
animatebooleantrue
formatChartFormat: title, categoryAxisTitle, valueAxisTitle, legend, dataLabels, valueScale, font, labelAngle, gridLines, gridDashed, lineWidth, lineDashed, fillOpacity, palette, colorsnone
fieldsthe grid's PivotField[]none
palettestring[]; '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, onHiddenSeriesChangestring[] of series dataKeys, and the legend clickskept inside
defaultHiddenSeriesstring[]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. chartIntegrationOptions is in the memo's dependency list, so an inline literal recomputes the transform every render and re-fires onChartSync — 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 no role, no aria-label and 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 own PivotThemeProvider.
  • 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 on cellType and on records.length.
  • Reading records scans 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. usePivotChartSync needs chartIntegrationOptions.transformer; without one it returns no data and warns once in the console.
  • Series are capped. maxPoints caps the categories (default 100) and maxSeries the series (default 20). The hook's result reports both (isSampled of totalArgs, isSeriesSampled of totalSeries), so say so when the chart shows part of the pivot.
  • Colours follow the theme only when asked. Without palette the 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. onCategoryClick reports the category and the row values behind it; the factory filters nothing itself (the JavaScript panel does, with chartCrossFilter). Lines and areas have no mark to click.
  • ChartFactory is headless. It draws a chart; maximising, a type picker and export buttons are the app's (ChartWindow is the window to put it in). The exporters are exported (exportChartPng, exportChartJpeg, exportChartSvg, chartImageDataUrl for the image as a data: URL at a size you choose, and findChartSvg for the container).

Without a licence key both run watermarked.