graphein 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/charts/matrix.d.ts +2 -1
  2. package/dist/charts/matrix.d.ts.map +1 -1
  3. package/dist/charts/matrix.js +481 -319
  4. package/dist/charts/matrix.js.map +3 -3
  5. package/dist/charts/table.d.ts +2 -1
  6. package/dist/charts/table.d.ts.map +1 -1
  7. package/dist/charts/table.js +214 -224
  8. package/dist/charts/table.js.map +3 -3
  9. package/dist/charts/tableView/canvas.d.ts +16 -1
  10. package/dist/charts/tableView/canvas.d.ts.map +1 -1
  11. package/dist/charts/tableView/canvas.js +361 -252
  12. package/dist/charts/tableView/canvas.js.map +3 -3
  13. package/dist/charts/tableView/conditional.d.ts +66 -40
  14. package/dist/charts/tableView/conditional.d.ts.map +1 -1
  15. package/dist/charts/tableView/conditional.js +163 -256
  16. package/dist/charts/tableView/conditional.js.map +3 -3
  17. package/dist/charts/tableView/dom.d.ts +26 -2
  18. package/dist/charts/tableView/dom.d.ts.map +1 -1
  19. package/dist/charts/tableView/dom.js +609 -269
  20. package/dist/charts/tableView/dom.js.map +3 -3
  21. package/dist/charts/tableView/layout.d.ts +22 -0
  22. package/dist/charts/tableView/layout.d.ts.map +1 -0
  23. package/dist/charts/tableView/layout.js +246 -0
  24. package/dist/charts/tableView/layout.js.map +7 -0
  25. package/dist/charts/tableView/sketch.d.ts +3 -2
  26. package/dist/charts/tableView/sketch.d.ts.map +1 -1
  27. package/dist/charts/tableView/sketch.js +2 -2
  28. package/dist/charts/tableView/sketch.js.map +2 -2
  29. package/dist/charts/tableView/sketchBar.d.ts +7 -0
  30. package/dist/charts/tableView/sketchBar.d.ts.map +1 -0
  31. package/dist/charts/tableView/sketchBar.js +32 -0
  32. package/dist/charts/tableView/sketchBar.js.map +7 -0
  33. package/dist/charts/tableView/sort.d.ts +9 -0
  34. package/dist/charts/tableView/sort.d.ts.map +1 -0
  35. package/dist/charts/tableView/sort.js +70 -0
  36. package/dist/charts/tableView/sort.js.map +7 -0
  37. package/dist/charts/tableView/sparkline.d.ts +30 -0
  38. package/dist/charts/tableView/sparkline.d.ts.map +1 -0
  39. package/dist/charts/tableView/sparkline.js +63 -0
  40. package/dist/charts/tableView/sparkline.js.map +7 -0
  41. package/dist/charts/tableView/style.d.ts +67 -0
  42. package/dist/charts/tableView/style.d.ts.map +1 -0
  43. package/dist/charts/tableView/style.js +131 -0
  44. package/dist/charts/tableView/style.js.map +7 -0
  45. package/dist/charts/tableView/types.d.ts +69 -46
  46. package/dist/charts/tableView/types.d.ts.map +1 -1
  47. package/dist/charts/tableView/types.js +22 -87
  48. package/dist/charts/tableView/types.js.map +2 -2
  49. package/dist/charts/tableView.d.ts +10 -5
  50. package/dist/charts/tableView.d.ts.map +1 -1
  51. package/dist/charts/tableView.js +12 -8
  52. package/dist/charts/tableView.js.map +2 -2
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/pivot/pivot.d.ts +18 -0
  57. package/dist/pivot/pivot.d.ts.map +1 -1
  58. package/dist/pivot/pivot.js +46 -34
  59. package/dist/pivot/pivot.js.map +2 -2
  60. package/dist/runtime/wire.d.ts.map +1 -1
  61. package/dist/runtime/wire.js +3 -1
  62. package/dist/runtime/wire.js.map +2 -2
  63. package/dist/spec/templates.js +2 -2
  64. package/dist/spec/templates.js.map +2 -2
  65. package/dist/spec/types.d.ts +58 -1
  66. package/dist/spec/types.d.ts.map +1 -1
  67. package/dist/spec/types.js.map +2 -2
  68. package/dist/spec/validate/charts.js +50 -14
  69. package/dist/spec/validate/charts.js.map +2 -2
  70. package/dist/spec/validate.data.d.ts.map +1 -1
  71. package/dist/spec/validate.data.js +3 -1
  72. package/dist/spec/validate.data.js.map +2 -2
  73. package/package.json +1 -1
@@ -1,72 +1,48 @@
1
1
  import { drawTitleBlock } from "./chrome.js";
2
2
  import {
3
- buildTable,
4
- createSketchGridCanvas,
5
- paintSketchGridOverlay,
3
+ buildColumnFormatter,
4
+ mountTable,
6
5
  paintTableCanvas,
7
6
  resolveConditionalDomain,
8
- densityMetrics
7
+ resolveTableStyle
9
8
  } from "./tableView.js";
10
- import { aggregateValues, pivot } from "../pivot/index.js";
9
+ import { aggregateValues, buildPivotCube } from "../pivot/index.js";
11
10
  import { resolveSketch } from "../spec/sketch.js";
12
- import { accessor } from "../util/data.js";
13
- const PATH_SEPARATOR = "\0";
14
- const BUFFER_ROWS = 8;
15
- function drawMatrix(surface, spec, tokens, size) {
11
+ import { accessor, toKey } from "../util/data.js";
12
+ import { formatValue } from "../format/index.js";
13
+ import { applyPick } from "../interaction/select.js";
14
+ const SEP = "\0";
15
+ const GRAND_TOTAL = "Grand Total";
16
+ const TOTAL = "Total";
17
+ const BLANK = "(Blank)";
18
+ const matrixStates = /* @__PURE__ */ new WeakMap();
19
+ const prepCache = /* @__PURE__ */ new WeakMap();
20
+ let collator = null;
21
+ function compareLabels(a, b) {
22
+ collator ??= new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
23
+ return collator.compare(a, b) || (a < b ? -1 : a > b ? 1 : 0);
24
+ }
25
+ function drawMatrix(surface, spec, tokens, size, context) {
16
26
  if (spec.type !== "matrix") return;
17
27
  surface.evidence?.begin("aggregate");
28
+ const rect = drawTitleBlock(surface, tokens, size, spec.title);
29
+ const prep = prepareMatrix(spec);
18
30
  if (surface.headless) {
19
- drawMatrixCanvas(surface, spec, tokens, size);
31
+ const rows = visibleRows(prep, spec, null, /* @__PURE__ */ new Map());
32
+ paintTableCanvas({
33
+ ...viewInput(prep, spec, tokens, rect, rows, null),
34
+ ctx: surface.marks.ctx,
35
+ evidence: surface.evidence,
36
+ labels: surface.labels
37
+ });
20
38
  return;
21
39
  }
22
- renderMatrix(surface, spec, tokens, size);
23
- }
24
- function drawMatrixCanvas(surface, spec, tokens, size) {
25
- const rect = drawTitleBlock(surface, tokens, size, spec.title);
26
- const prepared = prepareMatrix(spec);
27
- paintTableCanvas({
28
- ...matrixTableOptions(prepared, spec, tokens, rect),
29
- ctx: surface.marks.ctx,
30
- evidence: surface.evidence,
31
- labels: surface.labels,
32
- sketch: resolveSketch(spec)
33
- });
40
+ renderMatrix(surface, spec, tokens, size, rect, prep, context);
34
41
  }
35
- function renderMatrix(surface, spec, tokens, size) {
36
- const rect = drawTitleBlock(surface, tokens, size, spec.title);
37
- const prepared = prepareMatrix(spec);
38
- const sketch = resolveSketch(spec);
39
- const host = document.createElement("div");
40
- surface.overlay.appendChild(host);
41
- let buildOpts = null;
42
- let drawSketch = () => {
43
- };
44
- const rowHeight = densityMetrics(spec.density).rowHeight;
45
- const renderWindow = () => {
46
- surface.evidence?.reset(size, spec.data?.length ?? 0, spec.background ?? tokens.color.background);
47
- surface.evidence?.begin("aggregate");
48
- if (surface.labels) surface.labels.records.splice(
49
- 0,
50
- surface.labels.records.length,
51
- ...surface.labels.records.filter((l) => l.role === "title" || l.role === "subtitle")
52
- );
53
- const scrollTop = host.scrollTop || 0;
54
- const start = Math.max(0, Math.floor(scrollTop / rowHeight) - BUFFER_ROWS);
55
- const visible = Math.ceil(Math.max(rect.height, host.clientHeight || rect.height) / rowHeight) + BUFFER_ROWS * 2;
56
- const end = Math.min(prepared.rows.length, start + visible);
57
- buildOpts = {
58
- ...matrixTableOptions(prepared, spec, tokens, rect),
59
- container: host,
60
- evidence: surface.evidence,
61
- labels: surface.labels,
62
- stickyHeader: true,
63
- visibleRange: { start, end, rowHeight },
64
- sketch
65
- };
66
- buildTable(buildOpts);
67
- drawSketch();
68
- };
69
- surface.collectEvidence = () => {
42
+ function renderMatrix(surface, spec, tokens, size, rect, prep, context) {
43
+ const state = matrixStateFor(surface, spec);
44
+ let rows = visibleRows(prep, spec, state.sort, state.expanded);
45
+ const resetEvidence = () => {
70
46
  surface.evidence?.reset(size, spec.data?.length ?? 0, spec.background ?? tokens.color.background);
71
47
  surface.evidence?.begin("aggregate");
72
48
  if (surface.labels) surface.labels.records.splice(
@@ -74,148 +50,474 @@ function renderMatrix(surface, spec, tokens, size) {
74
50
  surface.labels.records.length,
75
51
  ...surface.labels.records.filter((l) => l.role === "title" || l.role === "subtitle")
76
52
  );
77
- buildOpts?.refreshEvidence?.();
78
53
  };
79
- if (sketch) {
80
- const overlay = createSketchGridCanvas(rect);
81
- if (overlay.ctx) {
82
- surface.overlay.appendChild(overlay.canvas);
83
- const ctx = overlay.ctx;
84
- drawSketch = () => {
85
- paintSketchGridOverlay(ctx, {
86
- sketch,
87
- tokens,
88
- rect,
89
- dpr: overlay.dpr,
90
- columns: prepared.columns,
91
- widths: buildOpts?.widths ?? [],
92
- rowCount: prepared.rows.length,
93
- headerRowCount: prepared.headerRows.length,
94
- hasFooter: false,
95
- rowHeaderSpan: 1,
96
- density: spec.density,
97
- scrollLeft: host.scrollLeft || 0,
98
- scrollTop: host.scrollTop || 0
99
- });
100
- };
101
- let frame2 = 0;
102
- host.addEventListener("scroll", () => {
103
- if (frame2 !== 0) return;
104
- frame2 = typeof requestAnimationFrame === "function" ? requestAnimationFrame(() => {
105
- frame2 = 0;
106
- renderWindow();
107
- }) : window.setTimeout(() => {
108
- frame2 = 0;
109
- renderWindow();
110
- }, 16);
111
- });
112
- renderWindow();
113
- return;
54
+ const select = context?.store && context.param ? { store: context.store, param: context.param, def: context.def ?? { type: "point" } } : null;
55
+ const refresh = (top) => {
56
+ if (top !== void 0) {
57
+ view.host.scrollTop = top;
58
+ state.scrollTop = top;
114
59
  }
115
- }
116
- let frame = 0;
117
- host.addEventListener("scroll", () => {
118
- if (frame !== 0) return;
119
- frame = typeof requestAnimationFrame === "function" ? requestAnimationFrame(() => {
120
- frame = 0;
121
- renderWindow();
122
- }) : window.setTimeout(() => {
123
- frame = 0;
124
- renderWindow();
125
- }, 16);
60
+ resetEvidence();
61
+ view.update(options());
62
+ };
63
+ const options = () => ({
64
+ ...viewInput(prep, spec, tokens, rect, rows, state.sort),
65
+ highlighted: selectedRows(spec, rows, context),
66
+ container: surface.overlay,
67
+ evidence: surface.evidence,
68
+ labels: surface.labels,
69
+ stickyHeader: true,
70
+ scrollTop: state.scrollTop,
71
+ scrollLeft: state.scrollLeft,
72
+ onScroll(top, left) {
73
+ state.scrollTop = top;
74
+ state.scrollLeft = left;
75
+ },
76
+ onToggle(row) {
77
+ const target = rows[row];
78
+ if (!target || target.kind !== "group") return;
79
+ state.expanded.set(target.node.id, !target.expanded);
80
+ rows = visibleRows(prep, spec, state.sort, state.expanded);
81
+ refresh();
82
+ },
83
+ onSort(colIndex) {
84
+ const id = colIndex === 0 ? "__row__" : prep.columns[colIndex]?.id;
85
+ if (!id) return;
86
+ const previous = state.sort;
87
+ state.sort = previous?.col === id ? { col: id, dir: previous.dir === "asc" ? "desc" : "asc" } : { col: id, dir: "asc" };
88
+ rows = visibleRows(prep, spec, state.sort, state.expanded);
89
+ refresh(0);
90
+ },
91
+ onRowClick: select ? (row, col) => {
92
+ const target = rows[row];
93
+ if (!target) return;
94
+ const fields = spec.rows.slice(0, target.node.depth + 1);
95
+ const tuple = [...target.node.values];
96
+ const leaf = col > 0 ? prep.valueColumns[col - 1]?.leaf : void 0;
97
+ if (leaf && leaf.path.length) {
98
+ fields.push(...(spec.columns ?? []).slice(0, leaf.path.length));
99
+ tuple.push(...leaf.values);
100
+ }
101
+ const pick = { kind: "point", fields, tuples: [tuple] };
102
+ applyPick(select, pick);
103
+ refresh();
104
+ } : void 0,
105
+ measure: measureSource(prep, spec)
126
106
  });
127
- renderWindow();
107
+ const view = mountTable(options());
108
+ surface.collectEvidence = () => {
109
+ resetEvidence();
110
+ view.recordEvidence();
111
+ };
128
112
  }
129
- function matrixTableOptions(prepared, spec, tokens, rect) {
113
+ function viewInput(prep, spec, tokens, rect, rows, sort) {
114
+ const style = resolveTableStyle(tokens, spec.density, spec.gridlines);
115
+ const sortCol = sort ? sort.col === "__row__" ? 0 : prep.columns.findIndex((column) => column.id === sort.col) : -1;
116
+ const byLevel = /* @__PURE__ */ new Map();
117
+ const formattersAt = (depth) => {
118
+ let formatters = byLevel.get(depth);
119
+ if (!formatters) {
120
+ const domains = prep.levelDomains[depth] ?? [];
121
+ formatters = prep.columns.map((column, i) => buildColumnFormatter(column.conditionalFormat, domains[i] ?? null, tokens, style.color.background));
122
+ byLevel.set(depth, formatters);
123
+ }
124
+ return formatters;
125
+ };
130
126
  return {
131
- dataRepresentation: "aggregate",
132
- sourceRepresentedRows: prepared.sourceRepresentedRows,
133
127
  tokens,
134
128
  rect,
135
- columns: prepared.columns,
136
- rowCount: prepared.rows.length,
137
- getCell(rowIndex, colIndex) {
138
- const row = prepared.rows[rowIndex];
139
- if (colIndex === 0) return { value: rowLabel(row), raw: null };
140
- const { leaf, valueIndex } = prepared.leafValueColumns[colIndex - 1];
141
- const value = row.cellsByColumnKey.get(leaf.path.join(PATH_SEPARATOR))?.values[valueIndex] ?? null;
142
- const display = displayMatrixValue(value, rowIndex, colIndex - 1, prepared.denominators, spec);
143
- return { value: display, raw: display };
144
- },
145
- rowClass(rowIndex) {
146
- const row = prepared.rows[rowIndex];
147
- return row.isGrandTotal === true ? "grandtotal" : row.isSubtotal === true ? "subtotal" : "normal";
148
- },
149
- cellIndent(rowIndex, colIndex) {
150
- return colIndex === 0 ? prepared.rows[rowIndex].depth * 16 : 0;
129
+ columns: prep.columns,
130
+ rowCount: rows.length,
131
+ getCell: (rowIndex, colIndex) => matrixCell(prep, rows[rowIndex], colIndex),
132
+ rowInfo: (rowIndex) => rowInfo(rows[rowIndex]),
133
+ frozenColumns: 1,
134
+ formatters: formattersAt(Math.max(0, spec.rows.length - 1)),
135
+ // Format the visible detail: leaves and collapsed groups; expanded group subtotals stay plain.
136
+ rowFormatters: (rowIndex) => {
137
+ const row = rows[rowIndex];
138
+ if (!row || row.kind === "subtotal" || row.kind === "group" && row.expanded) return null;
139
+ return formattersAt(row.node.depth);
151
140
  },
152
- rowHeaderSpan: 1,
153
- striped: false,
154
- conditionalDomains: prepared.bodyDomains,
155
- headerRows: prepared.headerRows,
156
- density: spec.density
141
+ extremes: prep.extremes,
142
+ headerRows: prep.headerRows,
143
+ footerRow: spec.grandTotals === true ? {
144
+ kind: "grandtotal",
145
+ cells: prep.columns.map((_, col) => {
146
+ if (col === 0) return { value: GRAND_TOTAL, raw: null, label: true };
147
+ const value = prep.display(0, prep.valueColumns[col - 1]);
148
+ return { value, raw: value };
149
+ })
150
+ } : void 0,
151
+ density: spec.density,
152
+ gridlines: spec.gridlines,
153
+ sortState: sortCol >= 0 && sort ? { col: sortCol, dir: sort.dir } : void 0,
154
+ sketch: resolveSketch(spec),
155
+ dataRepresentation: "aggregate",
156
+ sourceRepresentedRows: prep.sourceRepresentedRows
157
+ };
158
+ }
159
+ function measureSource(prep, spec) {
160
+ const all = visibleRows(prep, { ...spec, expandDepth: void 0 }, null, /* @__PURE__ */ new Map());
161
+ return {
162
+ rowCount: all.length,
163
+ getCell: (row, col) => matrixCell(prep, all[row], col),
164
+ rowInfo: (row) => rowInfo(all[row])
165
+ };
166
+ }
167
+ function selectedRows(spec, rows, context) {
168
+ const value = context?.param ? context.store?.get(context.param) : null;
169
+ if (!value || value.kind !== "point" || !value.tuples.length) return void 0;
170
+ const depth = value.fields.findIndex((field, i) => spec.rows[i] !== field);
171
+ const count = depth < 0 ? value.fields.length : depth;
172
+ if (count === 0) return void 0;
173
+ const keys = new Set(value.tuples.map((tuple) => tuple.slice(0, count).map(toKey).join(SEP)));
174
+ return (row) => {
175
+ const node = rows[row]?.node;
176
+ if (!node || node.values.length < count) return false;
177
+ return keys.has(node.values.slice(0, count).map(toKey).join(SEP));
178
+ };
179
+ }
180
+ function matrixCell(prep, row, col) {
181
+ if (!row) return { value: null, raw: null };
182
+ if (col === 0) {
183
+ const label = row.kind === "subtotal" ? `${row.node.label} ${TOTAL}` : row.node.label;
184
+ return { value: label, raw: null, label: true };
185
+ }
186
+ if (!row.showValues) return { value: null, raw: null };
187
+ const value = prep.display(row.node.pid, prep.valueColumns[col - 1]);
188
+ return { value, raw: value };
189
+ }
190
+ function rowInfo(row) {
191
+ if (!row) return { kind: "leaf", depth: 0 };
192
+ return {
193
+ kind: row.kind === "group" ? "group" : row.kind === "subtotal" ? "subtotal" : "leaf",
194
+ depth: row.node.depth,
195
+ expandable: row.kind === "group",
196
+ expanded: row.expanded,
197
+ label: row.node.label
198
+ };
199
+ }
200
+ function matrixStateFor(surface, spec) {
201
+ const signature = JSON.stringify([spec.rows, spec.columns ?? [], spec.values.map((v) => [v.field, v.op, v.showAs]), spec.expandDepth, spec.rowSort, spec.subtotals]);
202
+ const existing = matrixStates.get(surface);
203
+ if (existing && existing.signature === signature) return existing;
204
+ const state = { signature, expanded: /* @__PURE__ */ new Map(), sort: null, scrollTop: 0, scrollLeft: 0 };
205
+ matrixStates.set(surface, state);
206
+ return state;
207
+ }
208
+ function visibleRows(prep, spec, sort, expanded) {
209
+ const out = [];
210
+ const subtotals = spec.subtotals === true;
211
+ const bottom = spec.subtotalPosition === "bottom";
212
+ const depthLimit = spec.expandDepth ?? Infinity;
213
+ const ordered = siblingOrder(prep, spec, sort);
214
+ const walk = (nodes, parentId) => {
215
+ for (const node of ordered(nodes, parentId)) {
216
+ if (!node.children.length) {
217
+ out.push({ kind: "leaf", node, expanded: true, showValues: true });
218
+ continue;
219
+ }
220
+ const open = expanded.get(node.id) ?? node.depth < depthLimit;
221
+ out.push({ kind: "group", node, expanded: open, showValues: !open || subtotals && !bottom });
222
+ if (!open) continue;
223
+ walk(node.children, node.id);
224
+ if (subtotals && bottom) out.push({ kind: "subtotal", node, expanded: true, showValues: true });
225
+ }
157
226
  };
227
+ walk(prep.rowTree, "");
228
+ return out;
229
+ }
230
+ function siblingOrder(prep, spec, sort) {
231
+ const byLabel = !sort ? spec.rowSort?.by !== "value" : sort.col === "__row__";
232
+ const dir = (sort?.dir ?? spec.rowSort?.order ?? "asc") === "desc" ? -1 : 1;
233
+ if (byLabel && dir === 1) return (nodes) => nodes;
234
+ const signature = sort ? `${sort.col}|${sort.dir}` : `rowSort|${spec.rowSort?.by}|${spec.rowSort?.valueIndex ?? 0}|${spec.rowSort?.order}`;
235
+ let memo = prep.orders.get(signature);
236
+ if (!memo) {
237
+ memo = /* @__PURE__ */ new Map();
238
+ prep.orders.set(signature, memo);
239
+ }
240
+ const valueOf = byLabel ? null : sortValue(prep, spec, sort);
241
+ return (nodes, parentId) => {
242
+ const hit = memo.get(parentId);
243
+ if (hit) return hit;
244
+ const sorted = [...nodes];
245
+ if (valueOf) {
246
+ const keys = new Map(sorted.map((node) => [node, valueOf(node)]));
247
+ sorted.sort((a, b) => {
248
+ const va = keys.get(a);
249
+ const vb = keys.get(b);
250
+ if (va == null || vb == null) return va == null && vb == null ? compareLabels(a.label, b.label) : va == null ? 1 : -1;
251
+ return va === vb ? compareLabels(a.label, b.label) : (va - vb) * dir;
252
+ });
253
+ } else sorted.reverse();
254
+ memo.set(parentId, sorted);
255
+ return sorted;
256
+ };
257
+ }
258
+ function sortValue(prep, spec, sort) {
259
+ if (sort) {
260
+ const col = prep.columns.findIndex((column2) => column2.id === sort.col);
261
+ const column = prep.valueColumns[col - 1];
262
+ return (node) => column ? prep.display(node.pid, column) : null;
263
+ }
264
+ const valueIndex = Math.min(spec.rowSort?.valueIndex ?? 0, spec.values.length - 1);
265
+ return (node) => prep.cube.valueAtIds(node.pid, 0, valueIndex);
158
266
  }
159
267
  function prepareMatrix(spec) {
160
268
  const data = spec.data ?? [];
161
- const result = pivot(data, {
162
- rows: spec.rows,
163
- columns: spec.columns ?? [],
164
- values: spec.values.map((value) => ({ field: value.field, op: value.op, label: value.label })),
165
- includeRowSubtotals: spec.subtotals === true,
166
- includeColumnSubtotals: spec.subtotals === true,
167
- includeGrandTotals: spec.grandTotals === true,
168
- sort: "asc"
169
- });
170
- const sortedLeaves = sortColumnLeaves(result.columnLeaves, result.rows, spec);
171
- const denominators = computeShowAsDenominators(result.rows, sortedLeaves, spec);
172
- const leafValueColumns = flattenLeafValueColumns(sortedLeaves, spec);
173
- const columns = [
174
- {
175
- key: "__row__",
176
- title: spec.rows.join(" / ") || "Rows",
177
- align: "left",
178
- type: "nominal",
179
- width: Math.max(180, Math.min(280, 120 + spec.rows.length * 34))
180
- },
181
- ...leafValueColumns.map(({ leaf, valueIndex }) => {
182
- const valueDef = spec.values[valueIndex];
183
- const title = spec.values.length > 1 ? valueDef.label ?? valueDef.field : leaf.key;
269
+ const signature = JSON.stringify([spec.rows, spec.columns ?? [], spec.values, spec.subtotals, spec.grandTotals, spec.columnSort]);
270
+ let byConfig = prepCache.get(data);
271
+ const hit = byConfig?.get(signature);
272
+ if (hit) return { ...hit, spec };
273
+ const rowFields = spec.rows;
274
+ const columnFields = spec.columns ?? [];
275
+ const valueDefs = spec.values;
276
+ const cube = buildPivotCube(data, rowFields, columnFields, valueDefs.map((v) => ({ field: v.field, op: v.op, label: v.label })));
277
+ const rowTree = axisTree(cube.rowAxis);
278
+ const leafRows = [];
279
+ const collectLeaves = (nodes) => {
280
+ for (const node of nodes) {
281
+ if (node.children.length) collectLeaves(node.children);
282
+ else leafRows.push(node);
283
+ }
284
+ };
285
+ collectLeaves(rowTree);
286
+ const colTree = axisTree(cube.columnAxis);
287
+ sortColumnTree(colTree, cube, spec);
288
+ const leaves = columnLeaves(colTree, spec);
289
+ const dataLeaves = leaves.filter((leaf) => leaf.kind === "data");
290
+ const valueColumns = leaves.flatMap((leaf) => valueDefs.map((_, valueIndex) => ({ leaf, valueIndex })));
291
+ const rowDenominators = /* @__PURE__ */ new Map();
292
+ const columnDenominators = /* @__PURE__ */ new Map();
293
+ const totalDenominators = /* @__PURE__ */ new Map();
294
+ const sumLeafCells = (rows, cols, valueIndex) => {
295
+ let total = 0;
296
+ for (const row of rows) for (const col of cols) total += cube.valueAtIds(row, col.pid, valueIndex) ?? 0;
297
+ return total;
298
+ };
299
+ const leafRowIds = leafRows.map((row) => row.pid);
300
+ const display = (rowPid, column) => {
301
+ const value = cube.valueAtIds(rowPid, column.leaf.pid, column.valueIndex);
302
+ const showAs = valueDefs[column.valueIndex]?.showAs;
303
+ if (!showAs || showAs === "value") return value;
304
+ let denominator;
305
+ if (showAs === "percentOfRow") {
306
+ const key = `${rowPid}|${column.valueIndex}`;
307
+ denominator = rowDenominators.get(key);
308
+ if (denominator === void 0) {
309
+ denominator = sumLeafCells([rowPid], dataLeaves, column.valueIndex);
310
+ rowDenominators.set(key, denominator);
311
+ }
312
+ } else if (showAs === "percentOfColumn") {
313
+ const key = `${column.leaf.id}|${column.valueIndex}`;
314
+ denominator = columnDenominators.get(key);
315
+ if (denominator === void 0) {
316
+ denominator = sumLeafCells(leafRowIds, [column.leaf], column.valueIndex);
317
+ columnDenominators.set(key, denominator);
318
+ }
319
+ } else {
320
+ denominator = totalDenominators.get(column.valueIndex);
321
+ if (denominator === void 0) {
322
+ denominator = sumLeafCells(leafRowIds, dataLeaves, column.valueIndex);
323
+ totalDenominators.set(column.valueIndex, denominator);
324
+ }
325
+ }
326
+ return computeShowAsValue(value, showAs, denominator);
327
+ };
328
+ const columns = buildViewColumns(spec, valueColumns, leaves.length);
329
+ const levels = [];
330
+ const collectLevels = (nodes) => {
331
+ for (const node of nodes) {
332
+ (levels[node.depth] ??= []).push(node.pid);
333
+ if (node.children.length) collectLevels(node.children);
334
+ }
335
+ };
336
+ collectLevels(rowTree);
337
+ const levelExtents = levels.map((pids) => valueColumns.map((column) => {
338
+ let min = Infinity;
339
+ let max = -Infinity;
340
+ for (const pid of pids) {
341
+ const v = display(pid, column);
342
+ if (v == null || !Number.isFinite(v)) continue;
343
+ if (v < min) min = v;
344
+ if (v > max) max = v;
345
+ }
346
+ return min === Infinity ? null : [min, max];
347
+ }));
348
+ const levelDomains = levelExtents.map((extents) => [null, ...extents.map((extent, i) => {
349
+ const cf = columns[i + 1].conditionalFormat;
350
+ return cf ? resolveConditionalDomain(cf, extent ?? []) : null;
351
+ })]);
352
+ const extremes = [null, ...valueColumns.map((_, i) => {
353
+ const all = levelExtents.map((extents) => extents[i]).filter((e) => e != null);
354
+ return all.length ? [Math.min(...all.map((e) => e[0])), Math.max(...all.map((e) => e[1]))] : null;
355
+ })];
356
+ const prep = {
357
+ spec,
358
+ cube,
359
+ rowTree,
360
+ leafRows,
361
+ valueColumns,
362
+ columns,
363
+ headerRows: buildHeaderRows(spec, colTree, leaves, valueColumns),
364
+ levelDomains,
365
+ extremes,
366
+ sourceRepresentedRows: countRepresentedSourceRows(data, spec),
367
+ display,
368
+ orders: /* @__PURE__ */ new Map()
369
+ };
370
+ if (!byConfig) {
371
+ byConfig = /* @__PURE__ */ new Map();
372
+ prepCache.set(data, byConfig);
373
+ }
374
+ if (byConfig.size > 8) byConfig.clear();
375
+ byConfig.set(signature, prep);
376
+ return prep;
377
+ }
378
+ function axisTree(entries) {
379
+ const roots = [];
380
+ const byPid = /* @__PURE__ */ new Map();
381
+ for (const entry of entries) {
382
+ const parent = byPid.get(entry.parent);
383
+ const key = entry.key;
384
+ const node = {
385
+ key,
386
+ label: key === "" ? BLANK : entry.value instanceof Date ? formatValue(entry.value) : key,
387
+ path: parent ? [...parent.path, key] : [key],
388
+ values: parent ? [...parent.values, entry.value] : [entry.value],
389
+ id: parent ? `${parent.id}${SEP}${key}` : key,
390
+ pid: entry.id,
391
+ depth: entry.depth,
392
+ children: []
393
+ };
394
+ byPid.set(entry.id, node);
395
+ (parent ? parent.children : roots).push(node);
396
+ }
397
+ const sortTree = (nodes) => {
398
+ nodes.sort((a, b) => compareLabels(a.key, b.key));
399
+ for (const node of nodes) if (node.children.length) sortTree(node.children);
400
+ };
401
+ sortTree(roots);
402
+ return roots;
403
+ }
404
+ function sortColumnTree(nodes, cube, spec) {
405
+ const sort = spec.columnSort;
406
+ if (!sort) return;
407
+ const dir = sort.order === "desc" ? -1 : 1;
408
+ const valueIndex = Math.min(sort.valueIndex ?? 0, spec.values.length - 1);
409
+ const walk = (siblings) => {
410
+ if (sort.by === "label") siblings.sort((a, b) => compareLabels(a.key, b.key) * dir);
411
+ else {
412
+ const totals = new Map(siblings.map((node) => [node, cube.valueAtIds(0, node.pid, valueIndex) ?? -Infinity]));
413
+ siblings.sort((a, b) => {
414
+ const d = totals.get(a) - totals.get(b);
415
+ return d === 0 || !Number.isFinite(d) ? compareLabels(a.key, b.key) : d * dir;
416
+ });
417
+ }
418
+ for (const node of siblings) if (node.children.length) walk(node.children);
419
+ };
420
+ walk(nodes);
421
+ }
422
+ function columnLeaves(tree, spec) {
423
+ const fields = spec.columns ?? [];
424
+ if (!fields.length) {
425
+ return [{ id: "", pid: 0, path: [], values: [], label: spec.values.length === 1 ? valueLabel(spec, 0) : GRAND_TOTAL, kind: "data", group: 0 }];
426
+ }
427
+ const out = [];
428
+ const subtotals = spec.subtotals === true && fields.length > 1;
429
+ const walk = (nodes, group) => {
430
+ nodes.forEach((node, i) => {
431
+ const g = group ?? i;
432
+ if (node.children.length) {
433
+ walk(node.children, g);
434
+ if (subtotals) out.push({ id: `${node.id}${SEP}${TOTAL}`, pid: node.pid, path: node.path, values: node.values, label: TOTAL, kind: "subtotal", group: g });
435
+ } else {
436
+ out.push({ id: node.id, pid: node.pid, path: node.path, values: node.values, label: node.label, kind: "data", group: g });
437
+ }
438
+ });
439
+ };
440
+ walk(tree, null);
441
+ if (spec.grandTotals === true) out.push({ id: `${SEP}${GRAND_TOTAL}`, pid: 0, path: [], values: [], label: GRAND_TOTAL, kind: "grand", group: tree.length });
442
+ return out;
443
+ }
444
+ function valueLabel(spec, valueIndex) {
445
+ const def = spec.values[valueIndex];
446
+ return def.label ?? def.field;
447
+ }
448
+ function rowsTitle(spec) {
449
+ return spec.rows.join(" / ") || "Rows";
450
+ }
451
+ function buildViewColumns(spec, valueColumns, leafCount) {
452
+ const multi = spec.values.length > 1;
453
+ const nested = (spec.columns?.length ?? 0) > 1;
454
+ return [
455
+ { key: "__row__", id: "__row__", title: rowsTitle(spec), align: "left", type: "nominal", rowHeader: true },
456
+ ...valueColumns.map(({ leaf, valueIndex }, i) => {
457
+ const def = spec.values[valueIndex];
458
+ const percent = def.showAs !== void 0 && def.showAs !== "value";
459
+ const previous = valueColumns[i - 1];
460
+ const groupStart = i > 0 && valueIndex === 0 && leafCount > 1 && (multi || nested && previous.leaf.group !== leaf.group || leaf.kind === "grand");
184
461
  return {
185
- key: `${leaf.path.join(PATH_SEPARATOR)}:${valueIndex}`,
186
- title,
462
+ key: `${leaf.id}:${valueIndex}`,
463
+ id: `${leaf.id}:${valueIndex}`,
464
+ title: multi ? valueLabel(spec, valueIndex) : leaf.label,
187
465
  align: "right",
188
466
  type: "quantitative",
189
- format: valueDef.showAs && valueDef.showAs !== "value" ? ".1%" : valueDef.format,
190
- width: 118,
191
- conditionalFormat: valueDef.conditionalFormat,
467
+ format: percent ? def.format?.includes("%") ? def.format : ".1%" : def.format,
468
+ conditionalFormat: leaf.kind === "data" ? def.conditionalFormat : void 0,
192
469
  isMeasure: true,
193
- prefix: valueDef.showAs && valueDef.showAs !== "value" ? void 0 : valueDef.prefix,
194
- suffix: valueDef.showAs && valueDef.showAs !== "value" ? void 0 : valueDef.suffix,
195
- negativeStyle: valueDef.negativeStyle
470
+ prefix: percent ? void 0 : def.prefix,
471
+ suffix: percent ? void 0 : def.suffix,
472
+ negativeStyle: def.negativeStyle,
473
+ total: leaf.kind !== "data",
474
+ groupStart
196
475
  };
197
476
  })
198
477
  ];
199
- const bodyDomains = columns.map((column, colIndex) => {
200
- if (colIndex === 0) return null;
201
- const { leaf, valueIndex } = leafValueColumns[colIndex - 1];
202
- const colKey = leaf.path.join(PATH_SEPARATOR);
203
- return resolveConditionalDomain(
204
- column.conditionalFormat,
205
- result.rows.map(
206
- (row, rowIndex) => row.isSubtotal === true || row.isGrandTotal === true ? null : displayMatrixValue(row.cellsByColumnKey.get(colKey)?.values[valueIndex] ?? null, rowIndex, colIndex - 1, denominators, spec)
207
- )
208
- );
209
- });
210
- return {
211
- rows: result.rows,
212
- columns,
213
- leafValueColumns,
214
- denominators,
215
- bodyDomains,
216
- headerRows: buildMatrixHeaderRows(result.columnTree, sortedLeaves, spec, sortedLeaves !== result.columnLeaves),
217
- sourceRepresentedRows: countRepresentedSourceRows(data, spec)
478
+ }
479
+ function buildHeaderRows(spec, tree, leaves, valueColumns) {
480
+ const fields = spec.columns ?? [];
481
+ const valueCount = spec.values.length;
482
+ const rowHeader = { title: rowsTitle(spec), align: "left", colIndex: 0 };
483
+ if (!fields.length) {
484
+ return [[rowHeader, ...valueColumns.map((_, i) => ({ title: valueCount > 1 ? valueLabel(spec, i) : leaves[0].label, align: "right", colIndex: i + 1 }))]];
485
+ }
486
+ const levels = fields.length;
487
+ const total = levels + (valueCount > 1 ? 1 : 0);
488
+ const rows = Array.from({ length: total }, () => []);
489
+ rows[0].push({ ...rowHeader, rowSpan: total });
490
+ const leafIndex = new Map(leaves.map((leaf, i) => [leaf.id, i]));
491
+ const leafCell = (leaf, depth) => {
492
+ const index = leafIndex.get(leaf.id) ?? 0;
493
+ return {
494
+ title: leaf.label,
495
+ colSpan: valueCount,
496
+ rowSpan: levels - depth,
497
+ align: valueCount > 1 ? "center" : "right",
498
+ colIndex: valueCount > 1 ? void 0 : 1 + index
499
+ };
500
+ };
501
+ const span = (node) => node.children.length ? node.children.reduce((s, child) => s + span(child), 0) + (spec.subtotals === true && levels > 1 ? 1 : 0) : 1;
502
+ const walk = (nodes) => {
503
+ for (const node of nodes) {
504
+ if (!node.children.length) {
505
+ rows[node.depth].push(leafCell(leaves[leafIndex.get(node.id) ?? 0], node.depth));
506
+ continue;
507
+ }
508
+ rows[node.depth].push({ title: node.label, colSpan: span(node) * valueCount, align: "center" });
509
+ walk(node.children);
510
+ const subtotal = leaves.find((leaf) => leaf.kind === "subtotal" && leaf.id === `${node.id}${SEP}${TOTAL}`);
511
+ if (subtotal) rows[node.depth + 1].push(leafCell(subtotal, node.depth + 1));
512
+ }
218
513
  };
514
+ walk(tree);
515
+ const grand = leaves.find((leaf) => leaf.kind === "grand");
516
+ if (grand) rows[0].push(leafCell(grand, 0));
517
+ if (valueCount > 1) {
518
+ valueColumns.forEach(({ valueIndex }, i) => rows[total - 1].push({ title: valueLabel(spec, valueIndex), align: "right", colIndex: i + 1 }));
519
+ }
520
+ return rows;
219
521
  }
220
522
  function countRepresentedSourceRows(data, spec) {
221
523
  const reads = spec.values.map((value) => ({ value, read: accessor(value.field) }));
@@ -225,21 +527,6 @@ function countRepresentedSourceRows(data, spec) {
225
527
  }
226
528
  return count;
227
529
  }
228
- function rowLabel(row) {
229
- if (row.isGrandTotal === true) return row.label;
230
- if (row.isSubtotal === true) {
231
- const name = row.path[row.path.length - 1] ?? "";
232
- return name === "" ? row.label : `${name} ${row.label}`;
233
- }
234
- return row.label;
235
- }
236
- function flattenLeafValueColumns(columnLeaves, spec) {
237
- const out = [];
238
- for (const leaf of columnLeaves) {
239
- spec.values.forEach((_value, valueIndex) => out.push({ leaf, valueIndex }));
240
- }
241
- return out;
242
- }
243
530
  function computeShowAsValue(value, showAs, denominator) {
244
531
  if (value == null || !Number.isFinite(value)) return null;
245
532
  if (!showAs || showAs === "value") return value;
@@ -247,131 +534,6 @@ function computeShowAsValue(value, showAs, denominator) {
247
534
  const ratio = value / denominator;
248
535
  return Number.isFinite(ratio) ? ratio : null;
249
536
  }
250
- function computeShowAsDenominators(rows, leaves, spec) {
251
- const valueCount = spec.values.length;
252
- const rowTotals = rows.map((row) => {
253
- const totals = Array.from({ length: valueCount }, () => 0);
254
- leaves.forEach((leaf) => {
255
- const cell = row.cellsByColumnKey.get(leaf.path.join(PATH_SEPARATOR));
256
- for (let i = 0; i < valueCount; i += 1) totals[i] += cell?.values[i] ?? 0;
257
- });
258
- return totals;
259
- });
260
- const columnTotals = leaves.map((leaf) => {
261
- const totals = Array.from({ length: valueCount }, () => 0);
262
- rows.forEach((row) => {
263
- if (row.isSubtotal || row.isGrandTotal) return;
264
- const cell = row.cellsByColumnKey.get(leaf.path.join(PATH_SEPARATOR));
265
- for (let i = 0; i < valueCount; i += 1) totals[i] += cell?.values[i] ?? 0;
266
- });
267
- return totals;
268
- });
269
- const grandTotals = Array.from(
270
- { length: valueCount },
271
- (_, valueIndex) => columnTotals.reduce((sum, totals) => sum + totals[valueIndex], 0)
272
- );
273
- return { rowTotals, columnTotals, grandTotals };
274
- }
275
- function displayMatrixValue(value, rowIndex, leafValueIndex, denominators, spec) {
276
- const valueIndex = leafValueIndex % spec.values.length;
277
- const leafIndex = Math.floor(leafValueIndex / spec.values.length);
278
- const showAs = spec.values[valueIndex].showAs;
279
- if (!showAs || showAs === "value") return value;
280
- const denominator = showAs === "percentOfRow" ? denominators.rowTotals[rowIndex]?.[valueIndex] ?? 0 : showAs === "percentOfColumn" ? denominators.columnTotals[leafIndex]?.[valueIndex] ?? 0 : denominators.grandTotals[valueIndex] ?? 0;
281
- return computeShowAsValue(value, showAs, denominator);
282
- }
283
- function sortColumnLeaves(leaves, rows, spec) {
284
- const order = spec.columnSort?.order === "desc" ? -1 : 1;
285
- if (!spec.columnSort) return leaves;
286
- const sorted = [...leaves];
287
- if (spec.columnSort.by === "label") {
288
- return sorted.sort((left, right) => left.key.localeCompare(right.key, void 0, { numeric: true }) * order);
289
- }
290
- const valueIndex = spec.columnSort.valueIndex ?? 0;
291
- return sorted.sort((left, right) => {
292
- const leftTotal = columnTotal(rows, left, valueIndex);
293
- const rightTotal = columnTotal(rows, right, valueIndex);
294
- return (leftTotal === rightTotal ? left.key.localeCompare(right.key) : leftTotal - rightTotal) * order;
295
- });
296
- }
297
- function columnTotal(rows, leaf, valueIndex) {
298
- const key = leaf.path.join(PATH_SEPARATOR);
299
- return rows.reduce((sum, row) => {
300
- if (row.isSubtotal || row.isGrandTotal) return sum;
301
- return sum + (row.cellsByColumnKey.get(key)?.values[valueIndex] ?? 0);
302
- }, 0);
303
- }
304
- function buildMatrixHeaderRows(columnTree, columnLeaves, spec, flatLeaves = false) {
305
- if (flatLeaves) return buildFlatMatrixHeaderRows(columnLeaves, spec);
306
- const groupRows = Math.max(1, maxDepth(columnTree) + 1);
307
- const showMeasureRow = spec.values.length > 1;
308
- const totalRows = groupRows + (showMeasureRow ? 1 : 0);
309
- const rows = Array.from({ length: totalRows }, () => []);
310
- rows[0].push({
311
- title: spec.rows.join(" / ") || "Rows",
312
- rowSpan: totalRows,
313
- align: "left",
314
- colIndex: 0
315
- });
316
- for (const node of columnTree) appendHeaderNode(rows, node, 0, groupRows, spec.values.length);
317
- if (showMeasureRow) {
318
- for (let leafIndex = 0; leafIndex < columnLeaves.length; leafIndex += 1) {
319
- for (let valueIndex = 0; valueIndex < spec.values.length; valueIndex += 1) {
320
- rows[totalRows - 1].push({
321
- title: spec.values[valueIndex].label ?? spec.values[valueIndex].field,
322
- align: "right",
323
- colIndex: 1 + leafIndex * spec.values.length + valueIndex
324
- });
325
- }
326
- }
327
- }
328
- return rows;
329
- }
330
- function buildFlatMatrixHeaderRows(columnLeaves, spec) {
331
- const showMeasureRow = spec.values.length > 1;
332
- const rows = [[], ...showMeasureRow ? [[]] : []];
333
- rows[0].push({
334
- title: spec.rows.join(" / ") || "Rows",
335
- rowSpan: showMeasureRow ? 2 : 1,
336
- align: "left",
337
- colIndex: 0
338
- });
339
- columnLeaves.forEach((leaf, leafIndex) => {
340
- rows[0].push({
341
- title: leaf.path.join(" / ") || leaf.key,
342
- colSpan: spec.values.length,
343
- align: "center",
344
- colIndex: showMeasureRow ? void 0 : 1 + leafIndex
345
- });
346
- if (showMeasureRow) {
347
- spec.values.forEach(
348
- (value, valueIndex) => rows[1].push({
349
- title: value.label ?? value.field,
350
- align: "right",
351
- colIndex: 1 + leafIndex * spec.values.length + valueIndex
352
- })
353
- );
354
- }
355
- });
356
- return rows;
357
- }
358
- function appendHeaderNode(rows, node, depth, groupRows, valueCount) {
359
- const isLeaf = node.children.length === 0;
360
- rows[depth].push({
361
- title: node.key,
362
- colSpan: node.span * valueCount,
363
- rowSpan: isLeaf ? groupRows - depth : 1,
364
- align: "center"
365
- });
366
- for (const child of node.children) appendHeaderNode(rows, child, depth + 1, groupRows, valueCount);
367
- }
368
- function maxDepth(nodes) {
369
- let depth = 0;
370
- for (const node of nodes) {
371
- depth = Math.max(depth, node.depth, maxDepth(node.children));
372
- }
373
- return depth;
374
- }
375
537
  export {
376
538
  computeShowAsValue,
377
539
  drawMatrix