v1.6.0

<bmx-pivot-grid>

A multi-dimensional pivot grid: any number of fields down the side and across the top, nested as hierarchies that open and close, and any number of values totalled in every cell - with subtotals at every level, grand totals, and figures shown as shares, running totals, differences or ranks.

32 properties · 7 events · 27 methods · 20 parts

Example

Add a margin Show as % of total Top 3 reps Data bars Swap rows and columns Download Excel
Everything is live. Open and close regions and years with their arrows (or the arrow keys), open the field list with its tool to drag fields between Filters, Columns, Rows and Values, click a heading at the top left to sort and filter, and double-click a total to drill through to its records.
Show markup
<div class="row" role="group" aria-label="Example actions">
  <bmx-button id="ex-pg-margin" variant="soft">Add a margin</bmx-button>
  <bmx-button id="ex-pg-share" toggle variant="soft">Show as % of total</bmx-button>
  <bmx-button id="ex-pg-top" toggle variant="soft">Top 3 reps</bmx-button>
  <bmx-button id="ex-pg-bars" toggle variant="soft">Data bars</bmx-button>
  <bmx-button id="ex-pg-swap" variant="ghost">Swap rows and columns</bmx-button>
  <bmx-button id="ex-pg-excel" variant="ghost">Download Excel</bmx-button>
</div>

<bmx-pivot-grid id="ex-pg" label="Sales" field-list="closed" style="--bmx-pivot-grid-height: 30rem; margin-block-start: 1rem"></bmx-pivot-grid>

<div class="row" style="margin-block-start: 1rem">
  <span class="note" id="ex-pg-out">
    <strong>Everything is live.</strong> Open and close regions and years with their arrows (or the arrow keys), open
    the field list with its tool to drag fields between Filters, Columns, Rows and Values, click a heading at the top
    left to sort and filter, and double-click a total to drill through to its records.
  </span>
</div>

<script type="module">
  await customElements.whenDefined('bmx-pivot-grid');

  const pivot = document.getElementById('ex-pg');
  const out = document.getElementById('ex-pg-out');

  // Records are plain objects. Twenty thousand of them, made up here.
  const regions = ['North', 'South', 'East', 'West'];
  const countries = { North: ['Scotland', 'Norway'], South: ['Spain', 'Italy'], East: ['Poland', 'Greece'], West: ['Ireland', 'Portugal'] };
  const products = ['Laptops', 'Monitors', 'Docks', 'Keyboards'];
  let seed = 7;
  const random = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648);
  const records = Array.from({ length: 20000 }, () => {
    const region = regions[Math.floor(random() * 4)];
    return {
      region,
      country: countries[region][Math.floor(random() * 2)],
      product: products[Math.floor(random() * 4)],
      rep: `Rep ${1 + Math.floor(random() * 10)}`,
      ordered: `202${5 + Math.floor(random() * 2)}-${String(1 + Math.floor(random() * 12)).padStart(2, '0')}-${String(1 + Math.floor(random() * 28)).padStart(2, '0')}`,
      units: 1 + Math.floor(random() * 12),
      revenue: Math.round(random() * 250000) / 100,
      cost: Math.round(random() * 160000) / 100,
    };
  });

  // Fields only where the data cannot say it: formats, titles, a hierarchy.
  pivot.fields = [
    { field: 'revenue', format: { style: 'currency', currency: 'GBP' } },
    { field: 'cost', format: { style: 'currency', currency: 'GBP' } },
    { field: 'ordered', title: 'Order date' },
  ];
  pivot.hierarchies = [{ name: 'geography', title: 'Geography', levels: ['region', 'country'] }];
  const layout = {
    rows: ['region', 'country'],
    columns: ['ordered:year', 'ordered:quarter'],
    filters: ['product'],
    values: [{ field: 'revenue', title: 'Revenue' }],
  };
  pivot.layout = layout;
  pivot.data = records;

  pivot.addEventListener('bmxPivotUpdate', event => {
    const { records: count, took, worker } = event.detail;
    out.textContent = `${count.toLocaleString()} records totalled in ${took} ms${worker ? ' on a background thread' : ''}.`;
  });
  pivot.addEventListener('bmxPivotDrillThrough', event => {
    out.textContent = `${event.detail.count} records behind ${[...event.detail.rowPath, ...event.detail.columnPath].join(' / ') || 'the grand total'}.`;
  });

  document.getElementById('ex-pg-margin').addEventListener('click', async () => {
    const current = await pivot.getLayout();
    if (current.values.some(v => v.title === 'Margin')) return;
    await pivot.setLayout({ ...current, values: [...current.values, { title: 'Margin', formula: '([Revenue] - [cost]) / [Revenue]', format: { style: 'percent' } }] });
  });
  document.getElementById('ex-pg-share').addEventListener('bmxActivate', async event => {
    const current = await pivot.getLayout();
    await pivot.setLayout({ ...current, values: current.values.map(v => (v.title === 'Revenue' ? { ...v, showAs: event.detail.pressed ? 'percentOfGrandTotal' : undefined } : v)) });
  });
  document.getElementById('ex-pg-top').addEventListener('bmxActivate', async event => {
    const current = await pivot.getLayout();
    const rows = event.detail.pressed ? ['rep'] : layout.rows;
    const settings = event.detail.pressed ? { rep: { top: { count: 3, other: true } } } : {};
    await pivot.setLayout({ ...current, rows, settings });
  });
  document.getElementById('ex-pg-bars').addEventListener('bmxActivate', event => {
    // Rules are data; `value` names a value by its title or position.
    pivot.formatRules = event.detail.pressed
      ? [{ type: 'bars', value: 'Revenue' }, { type: 'highlight', value: 'Revenue', when: { op: 'top', value: 3 }, tone: 'success', bold: true }]
      : [];
  });
  document.getElementById('ex-pg-excel').addEventListener('click', () => pivot.download('xlsx'));
  document.getElementById('ex-pg-swap').addEventListener('click', async () => {
    const current = await pivot.getLayout();
    await pivot.setLayout({ ...current, rows: current.columns, columns: current.rows });
  });
</script>

DATA

data is an array of records - plain objects - or JSON; src or load() reads JSON, JSON lines, CSV or TSV from a URL, a file or text, and appendData() adds records a chunk at a time so a large set arrives without a pause. fields describes them where the data cannot: titles, types, formats, date levels, number ranges, orders of your own. Nothing needs a server: everything is worked out in the browser.

DIMENSIONS AND VALUES

layout says what goes where: rows, columns and filters hold levels

  • a field, or one part of a date (orderDate:year, orderDate:month) - and values the totals, each summed, counted, averaged, the smallest, the largest, a count of different values, a median, a variance or standard deviation, the first or last, a function of your own, or a formula over other totals ([Revenue] - [Cost], with Excel's functions too: ROUNDUP([Revenue] / [Units], 2)). Members sort by name, by a total, or in an order you give; keep the top ten and gather the rest as "Other"; filter any level by member or by name.

THE READER

The field list lets the reader drag fields between Filters, Columns, Rows and Values, change how each value is totalled and shown, add calculated values, filter and sort - or the page can lock the layout. Every change can be undone. getState() and setState() save and restore the whole workspace as JSON, and state-key keeps it in the browser by itself.

LARGE DATA

Records are read once into typed columns, a slice at a time so the page never freezes, and totalled in one pass on a background thread started from the library itself - nothing to host, nothing fetched. Both directions of the grid are virtualised: only the cells in view exist.

CHARTS

Every update is announced with the totals already arranged for a chart (bmxPivotUpdate), and getChartData() gives them on demand.

ACCESSIBLE

A treegrid with every line and column numbered for screen readers though only those in view exist. The keyboard reaches every cell and heading, opens and closes members, drills through and copies; the field list works without a mouse.

Properties

PropertyAttributeTypeDefaultDescription
appearance appearance 'modern' | 'striped' | 'bordered' | 'minimal' | 'glass' 'modern' The look: modern, striped, bordered, minimal or glass.
columnSubtotals column-subtotals boolean true A total column after each open column member's columns.
data data BmxPivotRecord[] | string [] The records: an array of objects, or JSON in the attribute.
dataProvider property only (request: BmxPivotServerRequest) => Promise<BmxPivotServerResponse> | BmxPivotServerResponse — Totals from your own server instead of from data: called with the layout (levels, values, filters), it answers every cell, subtotals and grand totals included. Declare the fields with fields. Set from script.
deferUpdates defer-updates boolean false Changes in the field list wait for its Update button.
density density 'compact' | 'standard' | 'comfortable' 'standard' Line height: compact, standard or comfortable.
drillPanel drill-panel boolean true Show the records behind a cell in a panel when it is drilled through, unless bmxPivotDrillThrough is cancelled.
drillProvider property only (request: BmxPivotServerDrillRequest) => Promise<BmxPivotRecord[]> | BmxPivotRecord[] — The records behind a cell from your server, for drill-through when totals come from dataProvider. Set from script.
emptyCell empty-cell string '' What an empty cell shows.
expandDepth expand-depth number — How many levels start open. Default: all of them.
fieldList field-list 'auto' | 'open' | 'closed' | 'none' 'auto' The field list: auto (open when the grid is wide enough), open, closed, or none for no field list at all.
fields fields BmxPivotFieldDef[] | string [] The fields: { field, title?, type?, format?, dateParts?, bin?, order?, folder?, hidden? }, or JSON. Fields not listed are read from the records.
fileName file-name string 'pivot' The name exported files are saved under.
formatRules format-rules BmxPivotFormatRule[] | string [] Conditional formatting, as data: [{ type: 'bars' | 'scale' | 'icons' | 'highlight', value?, scope?, when?, tone?, textTone?, bold?, tones?, icons?, reverse? }], or JSON. The reader can add them from a value's menu.
grandTotals grand-totals 'both' | 'rows' | 'columns' | 'none' 'both' Grand totals: a last row and a last column (both), the row only (rows), the column only (columns), or none.
hierarchies hierarchies BmxPivotHierarchy[] | string [] Groups of levels offered as one item in the field list: { name, title?, levels }, or JSON.
label label string — The accessible name. Default: "Pivot".
layout layout BmxPivotLayout | string {} What goes where: { rows, columns, filters, values, valuesOn, settings }, or JSON.
loading loading boolean false Show a loading bar: set while your own data is on its way.
locale locale string — A BCP 47 locale for numbers, dates, member names and sorting. Default: the page's lang, then the reader's.
lockLayout lock-layout boolean false The reader may sort, filter, open and close, but not move fields or change values.
memberProvider property only (level: string) => Promise<readonly { key: string; label?: string }[]> | readonly { key: string; label?: string }[] — A level's members from your server, for filters and slicers when totals come from dataProvider. Set from script.
requestInit property only RequestInit — Options for fetching src: headers, credentials.
rowLayout row-layout 'compact' | 'tabular' 'compact' Row headings in one indented column (compact) or one column per level (tabular).
src src string — A URL to load the records from: JSON, JSON lines, CSV or TSV.
stateKey state-key string — A name to keep the reader's workspace under in this browser: layout, open members, widths.
strings strings Partial<BmxPivotGridStrings> | string — Every word the grid shows, for another language: any of them, or JSON.
subtotals subtotals 'top' | 'bottom' | 'none' 'top' Where an open row member's total goes: on its own line (top), after its members (bottom), or nowhere.
timeZone time-zone 'local' | 'utc' 'local' How a moment with a time of day becomes a day: in the reader's zone (local) or utc. A bare date is always its own day.
toolbar toolbar boolean true Show the toolbar.
worker worker 'auto' | 'always' | 'never' 'auto' Total on a background thread: auto (from worker-threshold records), always, or never.
workerThreshold worker-threshold number 50000 Records from which auto uses a background thread.

Events

EventDetailDescription
bmxPivotCellClick BmxPivotCellDetail A cell was clicked.
bmxPivotDrillThrough BmxPivotDrillThroughDetail A cell was double-clicked or Enter pressed on it: the records behind it. Cancelable.
bmxPivotError BmxPivotErrorDetail The records could not be loaded or totalled.
bmxPivotLayoutChange BmxPivotLayoutChangeDetail The layout changed.
bmxPivotStateChange BmxPivotStateChangeDetail Something the reader can save changed: the layout, what is open, a width.
bmxPivotToggle BmxPivotToggleDetail A member was opened or closed.
bmxPivotUpdate BmxPivotUpdateDetail New totals are on show - with the numbers arranged for a chart.

Methods

MethodSignatureDescription
appendData appendData(records: BmxPivotRecord[]) => Promise<void> Adds records after those held, and totals again shortly after: call it with each chunk as a large set arrives. Columns already read are extended rather than read again.
collapseAll collapseAll(axis?: BmxPivotAxisName) => Promise<void> Closes every member, on one axis or both.
download download(format?: "xlsx" | "csv", options?: { records?: boolean; fileName?: string; }) => Promise<void> Saves what is on show as a file: xlsx or csv.
drillThrough drillThrough(rowPath: string[], columnPath: string[], limit?: number) => Promise<BmxPivotRecord[]> The records behind a total, by the member paths of its row and column. At most limit.
expandAll expandAll(axis?: BmxPivotAxisName) => Promise<void> Opens every member, on one axis or both.
expandToLevel expandToLevel(axis: BmxPivotAxisName | "both", depth: number) => Promise<void> Opens the levels above depth and closes the rest: 1 shows the outermost level's members, open.
exportData exportData(format?: "xlsx" | "csv" | "html", options?: { records?: boolean; }) => Promise<Blob> What is on show as a file: xlsx (the pivot as a styled sheet, headings merged, rows as an outline, conditional formats kept; with records: true the records behind it as a second sheet), csv, or html for printing.
getCellValue getCellValue(rowPath: string[], columnPath: string[], valueId?: string) => Promise<number | null> A total by the member paths of its row and column; the first value when none is named.
getChartData getChartData(options?: { maxCategories?: number; maxSeries?: number; withTotals?: boolean; }) => Promise<BmxPivotChartData> The totals on show arranged for a chart: a category per row, a series per column and value.
getLayout getLayout() => Promise<BmxPivotLayout> The layout, as JSON can carry it.
getLevelTitle getLevelTitle(level: string) => Promise<string> A level's name: "Region", or "Order date (Year)"; the key itself when there is no such level.
getMembers getMembers(level: string) => Promise<{ key: string; label: string; count: number | null; selected: boolean; }[]> A level's members in order, each with how many records it has under every other filter (null when totals come from a server) and whether the level's own filter keeps it. What bmx-pivot-slicer shows.
getPrintHtml getPrintHtml() => Promise<string> What is on show as a page to print: headings repeated on every page.
getState getState() => Promise<BmxPivotState> Everything the reader can change - layout, open members, widths, view - as JSON.
getText getText() => Promise<string> What is on show as text: tab-separated, headings first.
load load(source: string | URL | Blob) => Promise<void> Loads records from a URL, a File or Blob, or text: JSON, JSON lines, CSV or TSV.
print print() => Promise<void> Prints what is on show.
redo redo() => Promise<boolean> Steps the layout forward again.
refresh refresh() => Promise<void> Totals again from the records: after changing records in place.
setData setData(records: BmxPivotRecord[]) => Promise<void> Replaces the records.
setLayout setLayout(layout: BmxPivotLayout | string) => Promise<void> Replaces the layout. The reader can undo it.
setMemberFilter setMemberFilter(level: string, keys: string[] | null) => Promise<void> Keeps only these members of a level (their keys), whether or not the level is placed; null keeps them all.
setState setState(state: BmxPivotState | string) => Promise<boolean> Restores what getState gave. Returns false when it was not state at all.
settled settled() => Promise<void> Resolves once the totals for everything set so far are on show.
showFieldList showFieldList(open?: boolean) => Promise<void> Opens or closes the field list.
toggleMember toggleMember(axis: BmxPivotAxisName, path: string[], open?: boolean) => Promise<boolean> Opens or closes a member by its path of keys. Returns whether anything changed.
undo undo() => Promise<boolean> Steps the layout back.

Slots

SlotDescription
toolbar-end your own controls at the end of the toolbar.
toolbar-start your own controls at the start of the toolbar.

CSS shadow parts

PartDescription
add-calculated the button that adds a calculated value.
chip a field placed in an area.
drag-ghost the label that follows the pointer while a field is dragged.
drill-panel the panel of records behind a cell.
empty what is shown before any field is placed, or when no record matches.
error what is shown when the records could not be loaded or totalled.
field a field in the list.
field-list the field list.
field-search its search field.
grid the scrolling grid.
loading the bar shown while totals are worked out.
matrix the area holding the grid.
member-search the search in a member filter.
menu a pop-up: a field's menu, a value's menu, a filter, a formula.
menu-item one of its commands.
status the record count and timing in the toolbar.
tool-button a tool. The field list's tool is also fields-button.
toolbar the bar of tools above the grid.
update-button the field list's Update button, when updates are deferred.
zone one of the four areas: Filters, Columns, Rows, Values.

CSS custom properties

PropertyDescription
--bmx-pivot-grid-active The ring around the active cell.
--bmx-pivot-grid-background Behind the cells.
--bmx-pivot-grid-bar-opacity How strong a data bar is, 0-100%. Default 34%.
--bmx-pivot-grid-cell-padding Space either side of a cell's content.
--bmx-pivot-grid-chip-background A field placed in an area.
--bmx-pivot-grid-font-size The cells' text size.
--bmx-pivot-grid-grand-background A grand total.
--bmx-pivot-grid-header-background Behind the headings.
--bmx-pivot-grid-header-height Each heading row's height.
--bmx-pivot-grid-header-text The headings' text.
--bmx-pivot-grid-height How tall the grid is, toolbar included.
--bmx-pivot-grid-indent How far each row level is indented, in the compact form.
--bmx-pivot-grid-line The lines between cells.
--bmx-pivot-grid-negative The text of a negative total.
--bmx-pivot-grid-panel-background Behind the field list.
--bmx-pivot-grid-panel-width The field list's width.
--bmx-pivot-grid-radius The corners of the grid.
--bmx-pivot-grid-range The chosen cells.
--bmx-pivot-grid-row-alternate Every other line, in the striped look.
--bmx-pivot-grid-row-height Each line's height. Set it to override the density.
--bmx-pivot-grid-row-hover A line under the pointer.
--bmx-pivot-grid-scrollbar-thumb The scrollbars' thumbs.
--bmx-pivot-grid-scrollbar-track The scrollbars' tracks.
--bmx-pivot-grid-subtotal-background A subtotal.