@svgrid/grid 3.0.2 → 3.0.4

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 (281) hide show
  1. package/README.md +2 -1
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +110 -39
  8. package/dist/SvGrid.controller.svelte.js +655 -200
  9. package/dist/SvGrid.css +105 -2
  10. package/dist/SvGrid.svelte +386 -514
  11. package/dist/SvGrid.types.d.ts +210 -9
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +108 -22
  29. package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
  30. package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
  67. package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
  68. package/dist/cdn/svgrid.js +26 -15
  69. package/dist/cdn/svgrid.svelte-external.js +26 -15
  70. package/dist/chart-axes.d.ts +53 -0
  71. package/dist/chart-axes.js +351 -0
  72. package/dist/chart-cartesian.d.ts +88 -0
  73. package/dist/chart-cartesian.js +1862 -0
  74. package/dist/chart-decimate.d.ts +51 -0
  75. package/dist/chart-decimate.js +199 -0
  76. package/dist/chart-export-pdf.d.ts +47 -0
  77. package/dist/chart-export-pdf.js +187 -0
  78. package/dist/chart-export.d.ts +2 -1
  79. package/dist/chart-export.js +18 -3
  80. package/dist/chart-financial.d.ts +91 -0
  81. package/dist/chart-financial.js +175 -0
  82. package/dist/chart-flow.d.ts +10 -0
  83. package/dist/chart-flow.js +298 -0
  84. package/dist/chart-format.d.ts +18 -0
  85. package/dist/chart-format.js +65 -0
  86. package/dist/chart-grid.d.ts +8 -0
  87. package/dist/chart-grid.js +190 -0
  88. package/dist/chart-hierarchy.d.ts +17 -0
  89. package/dist/chart-hierarchy.js +146 -0
  90. package/dist/chart-indicators.d.ts +115 -0
  91. package/dist/chart-indicators.js +389 -0
  92. package/dist/chart-messages.d.ts +95 -0
  93. package/dist/chart-messages.js +92 -0
  94. package/dist/chart-motion.d.ts +16 -0
  95. package/dist/chart-motion.js +141 -0
  96. package/dist/chart-panel-messages.d.ts +265 -0
  97. package/dist/chart-panel-messages.js +285 -0
  98. package/dist/chart-pivot.d.ts +87 -0
  99. package/dist/chart-pivot.js +156 -0
  100. package/dist/chart-polar.d.ts +25 -0
  101. package/dist/chart-polar.js +700 -0
  102. package/dist/chart-samples.d.ts +16 -0
  103. package/dist/chart-samples.js +153 -0
  104. package/dist/chart-scale.d.ts +113 -0
  105. package/dist/chart-scale.js +405 -0
  106. package/dist/chart-stats.d.ts +111 -0
  107. package/dist/chart-stats.js +419 -0
  108. package/dist/chart-stream.d.ts +45 -0
  109. package/dist/chart-stream.js +91 -0
  110. package/dist/chart-summary.d.ts +27 -0
  111. package/dist/chart-summary.js +205 -0
  112. package/dist/chart-sync.svelte.d.ts +32 -0
  113. package/dist/chart-sync.svelte.js +23 -0
  114. package/dist/chart-table.d.ts +35 -0
  115. package/dist/chart-table.js +165 -0
  116. package/dist/chart-trend.d.ts +19 -0
  117. package/dist/chart-trend.js +61 -0
  118. package/dist/chart-types.d.ts +1456 -0
  119. package/dist/chart-types.js +6 -0
  120. package/dist/chart-validate.d.ts +43 -0
  121. package/dist/chart-validate.js +272 -0
  122. package/dist/chart-zoom.d.ts +40 -0
  123. package/dist/chart-zoom.js +145 -0
  124. package/dist/chart.d.ts +186 -865
  125. package/dist/chart.js +460 -2204
  126. package/dist/clipboard.d.ts +1 -0
  127. package/dist/clipboard.js +137 -18
  128. package/dist/column-resize.d.ts +3 -0
  129. package/dist/column-resize.js +4 -1
  130. package/dist/columns.js +3 -0
  131. package/dist/command-context.d.ts +19 -0
  132. package/dist/command-context.js +116 -0
  133. package/dist/core.d.ts +7 -0
  134. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  135. package/dist/editing.d.ts +2 -8
  136. package/dist/editing.js +262 -33
  137. package/dist/fill-patterns.d.ts +0 -5
  138. package/dist/fill-patterns.js +56 -2
  139. package/dist/grid-messages.d.ts +10 -3
  140. package/dist/grid-messages.js +5 -0
  141. package/dist/history.d.ts +83 -0
  142. package/dist/history.js +133 -0
  143. package/dist/index.d.ts +24 -6
  144. package/dist/index.js +20 -2
  145. package/dist/keyboard-handlers.js +88 -31
  146. package/dist/keyboard.d.ts +55 -0
  147. package/dist/keyboard.js +132 -0
  148. package/dist/menus.d.ts +3 -1
  149. package/dist/menus.js +7 -2
  150. package/dist/merges.d.ts +97 -0
  151. package/dist/merges.js +147 -0
  152. package/dist/row-resize.d.ts +3 -0
  153. package/dist/row-resize.js +14 -0
  154. package/dist/scroll-sync.d.ts +1 -0
  155. package/dist/scroll-sync.js +24 -0
  156. package/dist/selection.d.ts +16 -0
  157. package/dist/selection.js +108 -16
  158. package/dist/shortcut-registry.d.ts +145 -0
  159. package/dist/shortcut-registry.js +44 -0
  160. package/dist/svgrid-wrapper.types.d.ts +213 -22
  161. package/dist/virtualization/virtualizer.js +38 -17
  162. package/package.json +6 -1
  163. package/src/GridMenus.svelte +44 -2
  164. package/src/SvChartMenu.svelte +69 -0
  165. package/src/SvChartPanes.svelte +113 -0
  166. package/src/SvChartPanes.test.ts +66 -0
  167. package/src/SvGrid.controller.svelte.ts +629 -172
  168. package/src/SvGrid.css +105 -2
  169. package/src/SvGrid.svelte +386 -514
  170. package/src/SvGrid.types.ts +188 -8
  171. package/src/SvGridCellEditor.svelte +17 -0
  172. package/src/SvGridChart.svelte +2257 -329
  173. package/src/SvGridChart.test.ts +1419 -1
  174. package/src/SvGridChart.types.ts +336 -0
  175. package/src/SvGridChartBuilder.svelte +323 -0
  176. package/src/SvGridChartPanel.svelte +221 -275
  177. package/src/SvGridChartPickers.svelte +417 -0
  178. package/src/SvGridChartView.svelte +13 -14
  179. package/src/SvModal.svelte +7 -1
  180. package/src/SvModal.test.ts +9 -0
  181. package/src/ai.test.ts +57 -2
  182. package/src/ai.ts +166 -22
  183. package/src/aria.d.ts +11 -0
  184. package/src/build-api.coverage.test.ts +184 -0
  185. package/src/build-api.ts +107 -18
  186. package/src/chart-axes.test.ts +573 -0
  187. package/src/chart-axes.ts +342 -0
  188. package/src/chart-cartesian.ts +1863 -0
  189. package/src/chart-decimate.test.ts +249 -0
  190. package/src/chart-decimate.ts +186 -0
  191. package/src/chart-export-pdf.test.ts +91 -0
  192. package/src/chart-export-pdf.ts +207 -0
  193. package/src/chart-export.test.ts +14 -0
  194. package/src/chart-export.ts +13 -3
  195. package/src/chart-financial.ts +208 -0
  196. package/src/chart-flow.ts +286 -0
  197. package/src/chart-format.ts +67 -0
  198. package/src/chart-grid.ts +179 -0
  199. package/src/chart-hierarchy.ts +135 -0
  200. package/src/chart-indicators.test.ts +443 -0
  201. package/src/chart-indicators.ts +384 -0
  202. package/src/chart-messages.test.ts +21 -0
  203. package/src/chart-messages.ts +184 -0
  204. package/src/chart-motion.test.ts +71 -0
  205. package/src/chart-motion.ts +148 -0
  206. package/src/chart-panel-messages.test.ts +29 -0
  207. package/src/chart-panel-messages.ts +550 -0
  208. package/src/chart-pivot.test.ts +54 -0
  209. package/src/chart-pivot.ts +225 -0
  210. package/src/chart-polar.ts +674 -0
  211. package/src/chart-samples.ts +106 -0
  212. package/src/chart-scale.ts +394 -0
  213. package/src/chart-series-types.test.ts +714 -0
  214. package/src/chart-stats.ts +409 -0
  215. package/src/chart-stream.test.ts +75 -0
  216. package/src/chart-stream.ts +114 -0
  217. package/src/chart-summary.test.ts +90 -0
  218. package/src/chart-summary.ts +197 -0
  219. package/src/chart-sync.svelte.ts +41 -0
  220. package/src/chart-table.test.ts +76 -0
  221. package/src/chart-table.ts +178 -0
  222. package/src/chart-trend.ts +47 -0
  223. package/src/chart-types.ts +1309 -0
  224. package/src/chart-validate.test.ts +100 -0
  225. package/src/chart-validate.ts +247 -0
  226. package/src/chart-zoom.test.ts +137 -0
  227. package/src/chart-zoom.ts +124 -0
  228. package/src/chart.coverage.test.ts +81 -0
  229. package/src/chart.test.ts +56 -3
  230. package/src/chart.ts +557 -2784
  231. package/src/clipboard.test.ts +151 -0
  232. package/src/clipboard.ts +145 -16
  233. package/src/column-resize.ts +7 -1
  234. package/src/columns.ts +2 -0
  235. package/src/command-context.test.ts +200 -0
  236. package/src/command-context.ts +137 -0
  237. package/src/core.ts +7 -0
  238. package/src/editing.test.ts +281 -3
  239. package/src/editing.ts +256 -38
  240. package/src/fill-patterns.test.ts +35 -0
  241. package/src/fill-patterns.ts +56 -2
  242. package/src/grid-messages.ts +16 -3
  243. package/src/history.test.ts +196 -0
  244. package/src/history.ts +162 -0
  245. package/src/icon-seam.test.ts +3 -2
  246. package/src/index.ts +150 -2
  247. package/src/keyboard-handlers.coverage.test.ts +126 -6
  248. package/src/keyboard-handlers.ts +91 -30
  249. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  250. package/src/keyboard.test.ts +124 -1
  251. package/src/keyboard.ts +142 -0
  252. package/src/menus.test.ts +26 -0
  253. package/src/menus.ts +10 -3
  254. package/src/merges.test.ts +112 -0
  255. package/src/merges.ts +168 -0
  256. package/src/pivot.grid.test.ts +40 -5
  257. package/src/row-resize.test.ts +44 -0
  258. package/src/row-resize.ts +17 -0
  259. package/src/scroll-sync.test.ts +42 -1
  260. package/src/scroll-sync.ts +25 -0
  261. package/src/selection.multi-range.test.ts +34 -0
  262. package/src/selection.test.ts +30 -2
  263. package/src/selection.ts +111 -19
  264. package/src/shortcut-registry.test.ts +97 -0
  265. package/src/shortcut-registry.ts +181 -0
  266. package/src/svgrid-wrapper.types.ts +184 -13
  267. package/src/svgrid.behavior.test.ts +135 -0
  268. package/src/svgrid.charting.test.ts +513 -3
  269. package/src/svgrid.context-menu.test.ts +31 -0
  270. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  271. package/src/virtualization/virtualizer.ts +39 -17
  272. package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
  273. package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
  274. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  275. package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
  276. package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
  277. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
  278. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
  279. package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
  280. package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
  281. package/dist/cdn/chart-CKJRCPOk.js +0 -1652
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Pivot result -> chart spec.
3
+ *
4
+ * Turns the `{ rows, columns }` a pivot engine produces (the enterprise
5
+ * engine, through the `pivot` prop or `SvPivotDesigner`) into a {@link
6
+ * ChartSpec}, so one pivot layout drives both the table and a chart. The
7
+ * grid's own Chart panel runs this in pivot mode; enterprise's
8
+ * `pivotToChartSpec` is this function with its own types.
9
+ *
10
+ * Mapping:
11
+ * - categories = the row-axis LEAF rows (subtotals and the grand-total row
12
+ * are skipped). With nested rows the parents become a second axis tier
13
+ * (`categoryGroups`). With no row dims, the single total row.
14
+ * - series = the column-axis leaf value columns (one per col-path x
15
+ * measure). The grand-total column is excluded unless `includeTotals`.
16
+ * - values = each leaf row's aggregated cell for that series column; an
17
+ * empty cell (an average over no rows is null) is a gap, not a zero.
18
+ */
19
+ import type { CellFormatConfig } from './core'
20
+ import type { ChartSpec, ChartType } from './chart-types'
21
+
22
+ /** What {@link pivotResultToChartSpec} reads: the engine's rows and column tree, loosely typed so either package's result fits. */
23
+ export type PivotResultLike = {
24
+ rows: ReadonlyArray<Record<string, unknown>>
25
+ columns: ReadonlyArray<unknown>
26
+ }
27
+
28
+ /** Options for {@link pivotResultToChartSpec}. */
29
+ export type PivotResultChartOptions = {
30
+ /** Chart type. Default `'bar'`. A type that reads rows directly (scatter, gauge, box plot, histogram, ranges, bullet, candles) cannot be built from a pivot; see {@link pivotChartType}. */
31
+ type?: ChartType
32
+ /** Stack the series. Default false. */
33
+ stacked?: boolean
34
+ /** Stack to 100%. Implies `stacked`. */
35
+ stacked100?: boolean
36
+ /** Include the grand-total column / row as a series / category. Default false. */
37
+ includeTotals?: boolean
38
+ /** Cap the number of categories (row leaves) charted. */
39
+ maxCategories?: number
40
+ /**
41
+ * The measure's cell format, carried to the chart as `valueFormat` (with
42
+ * `currency` and `locale`), so a cell the table shows as "$469,662" charts
43
+ * as "$470k" on the axis and "$469,662" in the tooltip rather than "470k".
44
+ * With several measures in different formats, pass none.
45
+ */
46
+ format?: CellFormatConfig
47
+ /**
48
+ * The layout's measures, each with its cell `format`, when the caller has
49
+ * them rather than one format: used as `format` when they agree, ignored
50
+ * when a layout mixes a currency and a count (a "$" on the count would be
51
+ * wrong).
52
+ */
53
+ measures?: ReadonlyArray<{ format?: unknown }>
54
+ /** Name the value axis. Default: the measure's label when the layout has one measure, nothing otherwise; `null` for none. */
55
+ yAxisTitle?: string | null
56
+ /** Series colours, when the caller has a palette. */
57
+ palette?: string[]
58
+ }
59
+
60
+ /** Types that read rows one by one or need a shape a pivot does not have. */
61
+ const UNSHAPED: ReadonlySet<string> = new Set([
62
+ 'scatter', 'gauge', 'boxplot', 'histogram', 'range-bar', 'range-area', 'dumbbell', 'bullet', 'candlestick', 'ohlc',
63
+ ])
64
+
65
+ /** The type a pivot chart can draw: the one asked for, or a bar chart when that type cannot be built from a pivot. */
66
+ export function pivotChartType(type: ChartType | undefined): ChartType {
67
+ return !type || UNSHAPED.has(type) ? 'bar' : type
68
+ }
69
+
70
+ /**
71
+ * The grid column a click on a pivot chart filters: the innermost row
72
+ * dimension, when the grid has a column for that field. A category of the
73
+ * chart is one value of it, so a value filter there narrows the source rows
74
+ * and the pivot with them.
75
+ */
76
+ export function pivotFilterColumn(
77
+ rowFields: ReadonlyArray<string>,
78
+ columns: ReadonlyArray<{ id: string }>,
79
+ fieldOf: (id: string) => string | undefined,
80
+ ): string | null {
81
+ const field = rowFields[rowFields.length - 1]
82
+ return (field && columns.find((c) => fieldOf(c.id) === field)?.id) || null
83
+ }
84
+
85
+ /**
86
+ * Server aggregates as a spec: the grid panel's `charting.getAggregate` hands
87
+ * back `{ category, series?, value }` buckets and this lays them out as one
88
+ * series per distinct `series` (or a single one), categories in first-seen
89
+ * order, a missing cell as 0.
90
+ */
91
+ export function bucketsToChartSpec(
92
+ buckets: ReadonlyArray<{ category: string; series?: string; value: number }>,
93
+ type: ChartType,
94
+ hasSeries: boolean,
95
+ ): ChartSpec {
96
+ const cats: string[] = []
97
+ const catIdx = new Map<string, number>()
98
+ const seriesMap = new Map<string, number[]>()
99
+ for (const b of buckets) {
100
+ let ci = catIdx.get(b.category)
101
+ if (ci === undefined) {
102
+ ci = cats.length
103
+ catIdx.set(b.category, ci)
104
+ cats.push(b.category)
105
+ }
106
+ const key = hasSeries ? (b.series ?? '') : 'value'
107
+ let arr = seriesMap.get(key)
108
+ if (!arr) {
109
+ arr = []
110
+ seriesMap.set(key, arr)
111
+ }
112
+ arr[ci] = (arr[ci] ?? 0) + b.value
113
+ }
114
+ return {
115
+ type,
116
+ categories: cats,
117
+ series: [...seriesMap.entries()].map(([label, values]) => ({ label, values: cats.map((_, i) => values[i] ?? 0) })),
118
+ }
119
+ }
120
+
121
+ type LooseColumn = { id?: string; header?: unknown; columns?: LooseColumn[] }
122
+
123
+ /** The chart fields a cell format maps to. Dates have no place on a value axis. */
124
+ function chartFormat(f: CellFormatConfig | undefined): Pick<ChartSpec, 'valueFormat' | 'currency' | 'locale' | 'yAxis'> {
125
+ if (!f) return {}
126
+ const locale = Array.isArray(f.locales) ? f.locales[0] : f.locales
127
+ const base = locale ? { locale } : {}
128
+ if (f.type === 'number') return { ...base, valueFormat: 'number' }
129
+ if (f.type === 'currency') return { ...base, valueFormat: 'currency', currency: f.currency ?? 'USD' }
130
+ if (f.type === 'percent') {
131
+ // The chart's percent format multiplies by 100 the way Intl does; cells
132
+ // that hold percentage points already read "42", so they take a
133
+ // formatter that only appends the sign.
134
+ if (f.valueIsPercentPoints) return { ...base, yAxis: { formatter: (v) => `${v}%` } }
135
+ return { ...base, valueFormat: 'percent' }
136
+ }
137
+ return {}
138
+ }
139
+
140
+ /**
141
+ * A computed pivot as a chart spec. Row leaves become the categories (nested
142
+ * rows a grouped axis), column leaves the series, totals are left out unless
143
+ * asked for, the measure's format and name come along, and an empty cell is
144
+ * a gap.
145
+ */
146
+ export function pivotResultToChartSpec(result: PivotResultLike, opts: PivotResultChartOptions = {}): ChartSpec {
147
+ const type = pivotChartType(opts.type)
148
+
149
+ // Category rows: the row-axis leaves. Fall back to the grand-total (single
150
+ // "All") row when no row dims are configured, then to every row.
151
+ let catRows = result.rows.filter((r) => r.__pivotKind === 'leaf')
152
+ if (!catRows.length) catRows = result.rows.filter((r) => r.__pivotKind === 'grandTotal')
153
+ if (!catRows.length) catRows = result.rows.slice()
154
+ if (opts.maxCategories && catRows.length > opts.maxCategories) catRows = catRows.slice(0, opts.maxCategories)
155
+ // Categories are the leaf's OWN label; when rows are nested, emit a parent
156
+ // tier (categoryGroups) so the axis reads "Americas | Canada, USA ..." as a
157
+ // grouped axis rather than colliding on a bare repeated leaf label.
158
+ const byId = new Map(result.rows.map((r) => [r.__pivotId as string, r]))
159
+ const ownLabel = (r: Record<string, unknown>) => String(r.__pivotLabel ?? '').trim() || '(blank)'
160
+ const categories = catRows.map(ownLabel)
161
+ const parentOf = (r: Record<string, unknown>) => (r.__pivotParentId ? byId.get(r.__pivotParentId as string) : undefined)
162
+ const nested = catRows.length > 0 && catRows.every((r) => parentOf(r) !== undefined)
163
+ let categoryGroups: Array<{ label: string; span: number }> | undefined
164
+ if (nested) {
165
+ categoryGroups = []
166
+ let prevId: unknown
167
+ for (const r of catRows) {
168
+ const pid = r.__pivotParentId
169
+ const last = categoryGroups[categoryGroups.length - 1]
170
+ if (last && pid === prevId) last.span += 1
171
+ else {
172
+ categoryGroups.push({ label: ownLabel(parentOf(r)!), span: 1 })
173
+ prevId = pid
174
+ }
175
+ }
176
+ }
177
+
178
+ // Collect the leaf value columns, carrying the accumulated column-group path
179
+ // so a series can be labelled by its column path ("Q1", or "Q1 · Revenue").
180
+ type Leaf = { id: string; path: string[]; header: string }
181
+ const leaves: Leaf[] = []
182
+ const walk = (cols: LooseColumn[] | undefined, path: string[]): void => {
183
+ for (const c of cols ?? []) {
184
+ if (!c || c.id === '__pivotRowHeader') continue
185
+ // Skip the grand-total column group (and its leaves) unless asked for.
186
+ if (!opts.includeTotals && typeof c.id === 'string' && c.id.startsWith('pv_group__grand')) continue
187
+ if (Array.isArray(c.columns)) walk(c.columns, [...path, String(c.header ?? '')])
188
+ else if (typeof c.id === 'string') leaves.push({ id: c.id, path, header: String(c.header ?? '') })
189
+ }
190
+ }
191
+ walk(result.columns as LooseColumn[], [])
192
+
193
+ const filtered = opts.includeTotals ? leaves : leaves.filter((l) => !l.id.includes('__total'))
194
+ const uniqueHeaders = new Set(filtered.map((l) => l.header))
195
+ const series = filtered.map((l) => ({
196
+ label: l.path.length === 0 ? l.header : uniqueHeaders.size > 1 ? [...l.path, l.header].join(' · ') : l.path.join(' · '),
197
+ values: catRows.map((r) => {
198
+ const v = r[l.id]
199
+ // An empty cell (an average over no rows is null) is a gap in the
200
+ // chart, not a zero bar; a sum over no rows is the 0 the engine wrote.
201
+ if (v == null || v === '') return Number.NaN
202
+ return typeof v === 'number' ? v : Number(v)
203
+ }),
204
+ }))
205
+
206
+ // With one measure the value axis can carry its name; the series are then
207
+ // the column-axis values and say nothing about what is measured.
208
+ const measureLabel = uniqueHeaders.size === 1 && filtered.length ? filtered[0]!.header : ''
209
+ const yAxisTitle = opts.yAxisTitle === undefined ? measureLabel : opts.yAxisTitle
210
+ const formats = (opts.measures ?? []).map((m) => m.format as CellFormatConfig | undefined)
211
+ const agreed = formats[0] && formats.every((f) => f && JSON.stringify(f) === JSON.stringify(formats[0])) ? formats[0] : undefined
212
+ const fmt = chartFormat(opts.format ?? agreed)
213
+
214
+ return {
215
+ type,
216
+ categories,
217
+ series,
218
+ ...(categoryGroups && categoryGroups.length > 1 ? { categoryGroups } : {}),
219
+ ...(opts.stacked || opts.stacked100 ? { stacked: true } : {}),
220
+ ...(opts.stacked100 ? { stacked100: true } : {}),
221
+ ...(opts.palette ? { palette: opts.palette } : {}),
222
+ ...fmt,
223
+ ...(yAxisTitle ? { yAxis: { ...fmt.yAxis, title: yAxisTitle } } : {}),
224
+ }
225
+ }