<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
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) - andvaluesthe 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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
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
| Event | Detail | Description |
|---|---|---|
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
| Method | Signature | Description |
|---|---|---|
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
| Slot | Description |
|---|---|
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
| Part | Description |
|---|---|
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
| Property | Description |
|---|---|
--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. |