@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
@@ -6,6 +6,8 @@ import "./sv-grid-scrollbar.js";
6
6
  import { computeColumnStat, formatsNeedingStats, formatNeedsStats, } from "./conditional-formatting.js";
7
7
  import { rawToNumber, } from "./SvGrid.helpers.js";
8
8
  import { createFeatures } from "./features.js";
9
+ import { pushHistory } from "./history.js";
10
+ import { buildMergeIndex } from "./merges.js";
9
11
  import { createScrollSync, } from "./scroll-sync.js";
10
12
  import { createKeyboard, } from "./keyboard-handlers.js";
11
13
  import { createSummaries, } from "./summaries.js";
@@ -215,7 +217,12 @@ export function createSvGridController(rawProps, domIdBase) {
215
217
  let pendingScrollTop = 0;
216
218
  let pendingScrollLeft = 0;
217
219
  let scrollSyncRaf = null;
218
- let selectionRange = $state({ anchor: null, focus: null });
220
+ // `$state.raw`: every writer replaces the whole object, and every rendered
221
+ // cell's selection derived reads it. Under a deep proxy each of those ~300
222
+ // deriveds tracked the nested anchor/focus row/col signals separately (six
223
+ // dependencies per cell to mark and re-check on every arrow key) where one
224
+ // will do.
225
+ let selectionRange = $state.raw({ anchor: null, focus: null });
219
226
  // Extra committed ranges for multi-range (Ctrl+drag) selection. The
220
227
  // `selectionRange` above is always the ACTIVE range being manipulated; these
221
228
  // are the finished ones. Full selection = these + the active range.
@@ -243,7 +250,12 @@ export function createSvGridController(rawProps, domIdBase) {
243
250
  // Full-row editing: the row currently in whole-row edit + its per-column
244
251
  // draft (keyed by column id). Null when not in full-row mode.
245
252
  let fullRowEdit = $state(null);
246
- let editedCellValues = $state({});
253
+ // `$state.raw`, not `$state`: every writer replaces the whole object, and
254
+ // the render path asks `key in editedCellValues` once per rendered cell. On
255
+ // a deep proxy that `in` check, run inside an effect, creates and keeps a
256
+ // per-key signal for every cell that ever scrolls into view - a leak that
257
+ // grows with rows x columns and a Map lookup per cell per render.
258
+ let editedCellValues = $state.raw({});
247
259
  const UNDO_LIMIT = 200;
248
260
  let history = $state([]);
249
261
  /** Index in `history` of the LAST applied step. -1 means "nothing applied".
@@ -253,6 +265,11 @@ export function createSvGridController(rawProps, domIdBase) {
253
265
  /** Bumps on every undo / redo / record so $derived consumers can
254
266
  * observe via the api without subscribing to history directly. */
255
267
  let historyVersion = $state(0);
268
+ /** Set while a `runHistoryGroup` call is on the stack, so every step pushed
269
+ * inside one action shares a group id and undoes together. Deliberately not
270
+ * $state: it is written and read synchronously inside a single call and
271
+ * nothing renders from it. */
272
+ let historyGroupId = undefined;
256
273
  let tooltip = $state(null);
257
274
  let tooltipTimer = null;
258
275
  // ---- Find-in-grid ----------------------------------------------------
@@ -288,6 +305,26 @@ export function createSvGridController(rawProps, domIdBase) {
288
305
  let editorSelectAll = true;
289
306
  /** Per-column width overrides set by the resize handles. */
290
307
  let columnWidths = $state({});
308
+ /**
309
+ * Columns folded to nothing, the way a sheet hides a column: the column
310
+ * keeps its index, its cells and its width for later, and takes no room.
311
+ * Distinct from `hiddenColumns`, which takes a column out of the model
312
+ * and shifts every index after it; a spreadsheet's formulas and formats
313
+ * are keyed by index and could not follow that.
314
+ */
315
+ let collapsedColumns = $state({});
316
+ function setColumnCollapsed(columnId, collapsed) {
317
+ if (!!collapsedColumns[columnId] === collapsed)
318
+ return;
319
+ if (collapsed) {
320
+ collapsedColumns = { ...collapsedColumns, [columnId]: true };
321
+ }
322
+ else {
323
+ const next = { ...collapsedColumns };
324
+ delete next[columnId];
325
+ collapsedColumns = next;
326
+ }
327
+ }
291
328
  const MIN_COLUMN_WIDTH = 40;
292
329
  /** Columns pinned to the left or right edge of the grid (sticky positioning).
293
330
  * Seeded from `props.initialColumnPinning` so demos / tests can show the
@@ -373,26 +410,53 @@ export function createSvGridController(rawProps, domIdBase) {
373
410
  const hb = column.columnDef?.hideBelow;
374
411
  return hb != null && viewportWidth > 0 && viewportWidth < hb;
375
412
  }
376
- const scrollMetrics = $derived.by(function scrollMetrics_d() {
413
+ /**
414
+ * Live scroll geometry of the scroll container (feeds the custom
415
+ * scrollbars and `hasVerticalOverflow`).
416
+ *
417
+ * Measured from a plain `$effect`, NOT inside a `$derived`, on purpose.
418
+ * `scrollTop` / `scrollHeight` / `scrollWidth` reads force a synchronous
419
+ * layout whenever the tree is dirty. As a derived this was pulled by the
420
+ * shell's `class:sv-grid-has-vscroll` at the very start of every scroll
421
+ * flush - before the recycled rows had been rewritten - so it forced a
422
+ * full table layout that the row writes then invalidated again, and the
423
+ * browser laid the table out a second time before painting. Two table
424
+ * layouts per arrow key on a large grid. An effect runs after the flush
425
+ * has written the DOM, so the reads below land on a tree the browser has
426
+ * to lay out anyway, and the pre-paint pass is incremental.
427
+ *
428
+ * The version reads keep the same triggers the derived had: a scroll
429
+ * tick, a shell resize, and either virtualizer's window/size change (data
430
+ * load, row / column count change) - without those the scrollbar kept a
431
+ * stale `content-size` ≈ 0, its hidden-check tripped, it set
432
+ * `pointer-events: none`, and the user could not drag it.
433
+ */
434
+ let scrollMetrics = $state.raw({
435
+ scrollTop: 0,
436
+ scrollLeft: 0,
437
+ scrollHeight: 0,
438
+ scrollWidth: 0,
439
+ });
440
+ $effect(function scrollMetrics_e() {
377
441
  scrollVersion;
378
442
  viewportVersion;
379
- // Track the virtualizers' versions too so when data loads or row /
380
- // column counts change, scrollMetrics re-reads the DOM's grown
381
- // scrollHeight / scrollWidth. Without these deps the scrollbar
382
- // receives a stale `content-size` ≈ 0, its hidden-check trips, it
383
- // sets `pointer-events: none`, and the user can't drag it. The
384
- // identifiers below are declared further down - derived callbacks
385
- // run lazily, so by the time this fires they're in scope.
386
443
  virtualizer.version;
387
444
  columnVirtualizerVersion;
388
- return {
389
- scrollTop: scrollContainer?.scrollTop ?? 0,
390
- scrollLeft: scrollContainer?.scrollLeft ?? 0,
391
- clientHeight: scrollContainer?.clientHeight ?? 0,
392
- clientWidth: scrollContainer?.clientWidth ?? 0,
393
- scrollHeight: scrollContainer?.scrollHeight ?? 0,
394
- scrollWidth: scrollContainer?.scrollWidth ?? 0,
395
- };
445
+ const el = scrollContainer;
446
+ if (!el)
447
+ return;
448
+ const scrollTop = el.scrollTop;
449
+ const scrollLeft = el.scrollLeft;
450
+ const scrollHeight = el.scrollHeight;
451
+ const scrollWidth = el.scrollWidth;
452
+ const prev = untrack(() => scrollMetrics);
453
+ if (prev.scrollTop === scrollTop &&
454
+ prev.scrollLeft === scrollLeft &&
455
+ prev.scrollHeight === scrollHeight &&
456
+ prev.scrollWidth === scrollWidth) {
457
+ return;
458
+ }
459
+ scrollMetrics = { scrollTop, scrollLeft, scrollHeight, scrollWidth };
396
460
  });
397
461
  /** Vertical overflow from the virtualizer's authoritative total size,
398
462
  * NOT from `scrollMetrics.scrollHeight` alone. Reading DOM dimensions
@@ -1355,8 +1419,14 @@ export function createSvGridController(rawProps, domIdBase) {
1355
1419
  const paginationTotalRows = $derived(externalPaginationEnabled ? (props.rowCount ?? 0) : paginatedRowTotal);
1356
1420
  const paginationPageIndex = $derived(externalPaginationEnabled ? (props.pageIndex ?? 0) : paginationState.pageIndex);
1357
1421
  const paginationPageSize = $derived(externalPaginationEnabled ? (props.pageSize ?? 10) : paginationState.pageSize);
1422
+ // Tracks `dataStateVersion`, which the store subscription bumps when the
1423
+ // rowSelection slice (among others) changes - NOT the catch-all
1424
+ // `gridStateVersion`, which also bumps on every active-cell move. Every
1425
+ // rendered row and cell reads this (`sv-grid-row-selected`, `aria-selected`),
1426
+ // so on the catch-all each arrow key re-marked and re-checked ~400 effects
1427
+ // to find the same object.
1358
1428
  const rowSelectionState = $derived.by(function rowSelectionState_d() {
1359
- gridStateVersion;
1429
+ dataStateVersion;
1360
1430
  return grid.getState().rowSelection ?? {};
1361
1431
  });
1362
1432
  // ---- Selection bar ----------------------------------------------------
@@ -1652,7 +1722,9 @@ export function createSvGridController(rawProps, domIdBase) {
1652
1722
  let chartMaximized = $state(false);
1653
1723
  let chartFloatRect = $state.raw(null);
1654
1724
  let chartAiHandler = $state(null);
1725
+ let chartExplainHandler = $state(null);
1655
1726
  let chartSeq = 0;
1727
+ const isZoomWindow = (v) => !!v && typeof v === "object" && Number.isInteger(v.i0) && Number.isInteger(v.i1);
1656
1728
  const makeChart = (title) => ({
1657
1729
  id: `chart-${chartSeq++}`,
1658
1730
  title,
@@ -1671,10 +1743,50 @@ export function createSvGridController(rawProps, domIdBase) {
1671
1743
  logScale: null,
1672
1744
  timeAxis: null,
1673
1745
  valueFormat: null,
1746
+ bucket: null,
1747
+ bins: null,
1748
+ funnelShape: null,
1749
+ candleStyle: null,
1750
+ zoom: null,
1751
+ ohlc: null,
1752
+ indicators: [],
1753
+ format: null,
1754
+ frozen: null,
1674
1755
  });
1756
+ // The saved view (`GridState.charts[i]`) names four fields differently
1757
+ // from the tab; everything else is the tab's own key.
1758
+ const TAB_ALIASES = { dimensionId: "dimension", measureId: "measure", seriesId: "series", measure2Id: "measure2" };
1759
+ /** A tab as the saved view carries it, without the runtime id. */
1760
+ const tabToState = (c) => {
1761
+ const out = {};
1762
+ for (const k in c)
1763
+ if (k !== "id")
1764
+ out[TAB_ALIASES[k] ?? k] = k === "seriesId" ? (c.seriesId ?? null) : c[k];
1765
+ return out;
1766
+ };
1767
+ /** A tab from a saved-view snapshot: a fresh tab with every known field
1768
+ * the snapshot carries, and the shape-checked ones dropped when malformed. */
1769
+ const tabFromState = (c, i) => {
1770
+ const t = makeChart(typeof c.title === "string" ? c.title : `Chart ${i + 1}`);
1771
+ for (const k in t) {
1772
+ const v = c[TAB_ALIASES[k] ?? k];
1773
+ if (k !== "id" && k !== "title" && v !== undefined)
1774
+ t[k] = v;
1775
+ }
1776
+ if (!isZoomWindow(t.zoom))
1777
+ t.zoom = null;
1778
+ if (!Array.isArray(t.indicators))
1779
+ t.indicators = [];
1780
+ if (typeof t.bins !== "number")
1781
+ t.bins = null;
1782
+ if (!t.frozen || typeof t.frozen !== "object" || !t.frozen.spec)
1783
+ t.frozen = null;
1784
+ return t;
1785
+ };
1675
1786
  // svelte-ignore state_referenced_locally
1676
1787
  let charts = $state([makeChart("Chart 1")]);
1677
1788
  let activeChartIndex = $state(0);
1789
+ let savedCharts = $state([]);
1678
1790
  const activeChart = $derived((charts[Math.min(activeChartIndex, charts.length - 1)] ?? charts[0]));
1679
1791
  const chartType = $derived(activeChart.type);
1680
1792
  const chartReduce = $derived(activeChart.reduce);
@@ -1685,6 +1797,10 @@ export function createSvGridController(rawProps, domIdBase) {
1685
1797
  const chartDataLabels = $derived(activeChart.dataLabels);
1686
1798
  const chartLogScale = $derived(activeChart.logScale);
1687
1799
  const chartTimeAxis = $derived(activeChart.timeAxis);
1800
+ const chartBucket = $derived(activeChart.bucket);
1801
+ const chartBins = $derived(activeChart.bins);
1802
+ const chartFunnelShape = $derived(activeChart.funnelShape);
1803
+ const chartCandleStyle = $derived(activeChart.candleStyle);
1688
1804
  const chartValueFormat = $derived(activeChart.valueFormat);
1689
1805
  const asMeasureList = (m) => m == null ? [] : Array.isArray(m) ? m : [m];
1690
1806
  const isNumericColumn = (col) => {
@@ -1802,9 +1918,14 @@ export function createSvGridController(rawProps, domIdBase) {
1802
1918
  const label = col ? columnLabel(col) : "";
1803
1919
  if (!label)
1804
1920
  return undefined;
1805
- return chartReduce === "count"
1806
- ? "Count"
1807
- : `${chartReduce === "avg" ? "Average" : "Sum"} of ${label}`;
1921
+ const word = {
1922
+ sum: "Sum", avg: "Average", min: "Min", max: "Max", median: "Median",
1923
+ first: "First", last: "Last", countDistinct: "Distinct",
1924
+ };
1925
+ if (chartReduce === "count")
1926
+ return "Count";
1927
+ const w = word[chartReduce] ?? (chartReduce.startsWith("p") ? `P${chartReduce.slice(1)}` : "Sum");
1928
+ return `${w} of ${label}`;
1808
1929
  });
1809
1930
  /**
1810
1931
  * Does this column hold dates? Declared type first, then a probe of the
@@ -1830,6 +1951,34 @@ export function createSvGridController(rawProps, domIdBase) {
1830
1951
  return false;
1831
1952
  }
1832
1953
  const chartDimensionIsDate = $derived.by(() => columnIsDate(allColumns.find((c) => c.id === effectiveChartDimensionId)));
1954
+ // The chart derivation engine (rowsToChartSpec) is loaded lazily the first time
1955
+ // charting is enabled, keeping chart.ts (~11 KB gzip) out of the base <SvGrid>
1956
+ // bundle - it rides in the same lazy chunk as the chart panel. `chartSpec` stays
1957
+ // null until it resolves, a tick after the panel itself starts loading (which it
1958
+ // already does). The `getAggregate` server path uses the local bucketsToSpec and
1959
+ // does not need the engine.
1960
+ // The whole module, not just `rowsToChartSpec`: the panel can now reach
1961
+ // every chart type, and the ones that do not read a categories-by-series
1962
+ // grid (scatter, gauge, treemap, calendar, sankey) need their own builders
1963
+ // from the same lazy chunk. Same `import()`, same boundary.
1964
+ let chartEngine = $state(null);
1965
+ $effect(() => {
1966
+ if (chartingEnabled && !chartEngine)
1967
+ import("./chart.js").then((m) => (chartEngine = m));
1968
+ });
1969
+ const chartIsOhlc = $derived(chartType === "candlestick" || chartType === "ohlc");
1970
+ /** The price columns: what the reader picked, else the engine's guess
1971
+ * from the column names (nothing until the engine chunk has loaded). */
1972
+ const effectiveChartOhlc = $derived(chartEngine
1973
+ ? chartEngine.guessOhlcColumns(chartableColumns.measures, activeChart.ohlc)
1974
+ : { open: null, high: null, low: null, close: null, volume: null, ...activeChart.ohlc });
1975
+ /** The date column a candlestick reads: the picked dimension when it is a
1976
+ * date, else the first date column. */
1977
+ const effectiveChartOhlcDateId = $derived(chartDimensionIsDate ? effectiveChartDimensionId : (chartableColumns.dates[0]?.id ?? null));
1978
+ const chartIndicators = $derived(activeChart.indicators);
1979
+ /** The panes under a price chart and the overlay on it, split by the engine. */
1980
+ const chartIndicatorSplit = $derived(chartIsOhlc && chartEngine ? chartEngine.splitPanelIndicators(chartIndicators) : { panes: [], overlay: null });
1981
+ const chartIndicatorPanes = $derived(chartIndicatorSplit.panes);
1833
1982
  const chartRows = $derived.by(() => {
1834
1983
  const rects = getSelectionRects();
1835
1984
  const pushLeaf = (out, row) => {
@@ -1883,53 +2032,6 @@ export function createSvGridController(rawProps, domIdBase) {
1883
2032
  chartServerBuckets = [];
1884
2033
  });
1885
2034
  });
1886
- const bucketsToSpec = (buckets, type, hasSeries) => {
1887
- const cats = [];
1888
- const catIdx = new Map();
1889
- const ensureCat = (c) => {
1890
- let i = catIdx.get(c);
1891
- if (i === undefined) {
1892
- i = cats.length;
1893
- catIdx.set(c, i);
1894
- cats.push(c);
1895
- }
1896
- return i;
1897
- };
1898
- const seriesMap = new Map();
1899
- for (const b of buckets) {
1900
- const ci = ensureCat(b.category);
1901
- const key = hasSeries ? (b.series ?? "") : "value";
1902
- let arr = seriesMap.get(key);
1903
- if (!arr) {
1904
- arr = [];
1905
- seriesMap.set(key, arr);
1906
- }
1907
- arr[ci] = (arr[ci] ?? 0) + b.value;
1908
- }
1909
- return {
1910
- type,
1911
- categories: cats,
1912
- series: [...seriesMap.entries()].map(([label, values]) => ({
1913
- label,
1914
- values: cats.map((_, i) => values[i] ?? 0),
1915
- })),
1916
- };
1917
- };
1918
- // The chart derivation engine (rowsToChartSpec) is loaded lazily the first time
1919
- // charting is enabled, keeping chart.ts (~11 KB gzip) out of the base <SvGrid>
1920
- // bundle - it rides in the same lazy chunk as the chart panel. `chartSpec` stays
1921
- // null until it resolves, a tick after the panel itself starts loading (which it
1922
- // already does). The `getAggregate` server path uses the local bucketsToSpec and
1923
- // does not need the engine.
1924
- // The whole module, not just `rowsToChartSpec`: the panel can now reach
1925
- // every chart type, and the ones that do not read a categories-by-series
1926
- // grid (scatter, gauge, treemap, calendar, sankey) need their own builders
1927
- // from the same lazy chunk. Same `import()`, same boundary.
1928
- let chartEngine = $state(null);
1929
- $effect(() => {
1930
- if (chartingEnabled && !chartEngine)
1931
- import("./chart.js").then((m) => (chartEngine = m));
1932
- });
1933
2035
  /** Put the chart's numbers in the same locale as the grid's. A grid that set
1934
2036
  * `localization.locale` has already said which locale its data is in, and a
1935
2037
  * chart of that data reading in a different one is a bug nobody would think
@@ -1942,13 +2044,13 @@ export function createSvGridController(rawProps, domIdBase) {
1942
2044
  if (chartCfg?.currency)
1943
2045
  spec.currency = chartCfg.currency;
1944
2046
  };
1945
- const chartSpec = $derived.by(() => {
2047
+ const chartSpecLive = $derived.by(() => {
1946
2048
  if (!chartingEnabled || !chartCfg)
1947
2049
  return null;
1948
2050
  if (chartCfg.getAggregate) {
1949
- if (!chartServerBuckets)
2051
+ if (!chartServerBuckets || !chartEngine)
1950
2052
  return null;
1951
- const spec = bucketsToSpec(chartServerBuckets, chartType, !!effectiveChartSeriesId);
2053
+ const spec = chartEngine.bucketsToChartSpec(chartServerBuckets, chartType, !!effectiveChartSeriesId);
1952
2054
  if (effectiveChartStacked || chartCfg.stacked100)
1953
2055
  spec.stacked = true;
1954
2056
  if (chartCfg.stacked100)
@@ -1957,6 +2059,8 @@ export function createSvGridController(rawProps, domIdBase) {
1957
2059
  spec.palette = chartCfg.palette;
1958
2060
  if (effectiveChartTimeAxis)
1959
2061
  spec.xType = "time";
2062
+ else if (chartBucket && chartDimensionIsDate)
2063
+ spec.xType = "ordinal-time";
1960
2064
  if (effectiveChartLogScale)
1961
2065
  spec.yScale = "log";
1962
2066
  if (effectiveChartValueFormat)
@@ -1966,55 +2070,110 @@ export function createSvGridController(rawProps, domIdBase) {
1966
2070
  }
1967
2071
  if (chartCfg.buildSpec)
1968
2072
  return chartCfg.buildSpec(chartRows) ?? null;
1969
- const category = fieldOf(effectiveChartDimensionId);
1970
- const values = effectiveChartMeasureIds.map((id) => fieldOf(id)).filter((f) => !!f);
1971
- if (!category || !values.length)
1972
- return null;
1973
2073
  // Engine not loaded yet (lazy): render nothing until it resolves.
1974
2074
  if (!chartEngine)
1975
2075
  return null;
1976
- const seriesField = fieldOf(effectiveChartSeriesId);
1977
- const rowsForChart = chartRows;
1978
- // Some types read the rows directly rather than a grouped grid: a scatter
1979
- // point is one row, a gauge has no category axis at all, and a box plot
1980
- // needs the whole sample per group rather than one reduced number. The
1981
- // engine owns that dispatch - naming each type here would put them in the
1982
- // base bundle, which every grid pays for whether or not it ever charts.
1983
- // Everything else falls through to the aggregation below and is reshaped
1984
- // afterwards, so reduce / sort / topN / "Other" keep working for all of it.
1985
- const direct = chartEngine.rowsToDirectSpec(chartType, rowsForChart, {
1986
- category: category,
1987
- ...(values[0] ? { value: values[0] } : {}),
1988
- ...(fieldOf(effectiveChartMeasure2Id) ? { value2: fieldOf(effectiveChartMeasure2Id) } : {}),
1989
- ...(seriesField ? { series: seriesField } : {}),
1990
- reduce: chartReduce,
1991
- ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
1992
- });
1993
- if (direct)
1994
- return direct;
1995
- // A direct type that could not build (scatter with no Y picked yet, for
1996
- // instance) must not fall through to the bar-chart path and draw the wrong
1997
- // chart - it renders the panel's empty state instead.
1998
- if (chartType === "scatter" || chartType === "gauge" || chartType === "boxplot")
1999
- return null;
2000
- const spec = chartEngine.rowsToChartSpec(rowsForChart, {
2001
- type: chartType,
2002
- category: category,
2003
- value: values.length === 1 ? values[0] : values,
2004
- ...(seriesField ? { series: seriesField } : {}),
2005
- reduce: chartReduce,
2006
- stacked: effectiveChartStacked || effectiveChartStacked100,
2007
- stacked100: effectiveChartStacked100,
2008
- ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2009
- ...(chartCfg.topN ? { topN: chartCfg.topN } : {}),
2010
- ...(chartCfg.otherLabel ? { otherLabel: chartCfg.otherLabel } : {}),
2011
- ...(chartCfg.sort ? { sort: chartCfg.sort } : {}),
2012
- });
2076
+ // Shared by the direct and aggregated paths so the two cannot drift apart
2077
+ // again - which is how these came to be skipped for scatter, gauge and
2078
+ // boxplot in the first place. Declared here, at the derived's own scope:
2079
+ // a function declaration inside the `else` block below is block-scoped,
2080
+ // and the aggregated path calls it after that block has closed.
2081
+ function applyChartValueSettings(spec) {
2082
+ if (effectiveChartTimeAxis)
2083
+ spec.xType = "time";
2084
+ if (effectiveChartLogScale)
2085
+ spec.yScale = "log";
2086
+ if (effectiveChartValueFormat)
2087
+ spec.valueFormat = effectiveChartValueFormat;
2088
+ applyChartLocale(spec);
2089
+ return spec;
2090
+ }
2091
+ // Pivot mode: the pivot on screen is the chart, rows as categories and
2092
+ // columns as series; the pickers have nothing to choose.
2093
+ let spec;
2094
+ if (pivotResult) {
2095
+ spec = chartEngine.pivotResultToChartSpec(pivotResult, {
2096
+ type: chartType, stacked: effectiveChartStacked, stacked100: effectiveChartStacked100,
2097
+ measures: pivotConfig.values,
2098
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2099
+ });
2100
+ }
2101
+ else {
2102
+ const category = fieldOf(effectiveChartDimensionId);
2103
+ const values = effectiveChartMeasureIds.map((id) => fieldOf(id)).filter((f) => !!f);
2104
+ if (!chartIsOhlc && (!category || !values.length))
2105
+ return null;
2106
+ const seriesField = fieldOf(effectiveChartSeriesId);
2107
+ const rowsForChart = chartRows;
2108
+ // Some types read the rows directly rather than a grouped grid: a scatter
2109
+ // point is one row, a gauge has no category axis at all, and a box plot
2110
+ // needs the whole sample per group rather than one reduced number. The
2111
+ // engine owns that dispatch - naming each type here would put them in the
2112
+ // base bundle, which every grid pays for whether or not it ever charts.
2113
+ // Everything else falls through to the aggregation below and is reshaped
2114
+ // afterwards, so reduce / sort / topN / "Other" keep working for all of it.
2115
+ const ohlcOpts = chartIsOhlc
2116
+ ? chartEngine.ohlcDirectOptions(fieldOf, effectiveChartOhlcDateId, effectiveChartOhlc, { bucket: chartBucket, candleStyle: chartCandleStyle })
2117
+ : null;
2118
+ const direct = chartEngine.rowsToDirectSpec(chartType, rowsForChart, {
2119
+ ...(ohlcOpts ?? {}),
2120
+ category: ohlcOpts?.category ?? category,
2121
+ ...(values[0] ? { value: values[0] } : {}),
2122
+ ...(fieldOf(effectiveChartMeasure2Id) ? { value2: fieldOf(effectiveChartMeasure2Id) } : {}),
2123
+ ...(seriesField ? { series: seriesField } : {}),
2124
+ reduce: chartReduce,
2125
+ ...(chartBins ? { bins: chartBins } : {}),
2126
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2127
+ });
2128
+ // Number formatting and the scale/axis-type settings are about how values
2129
+ // READ, so they apply to a scatter or a gauge exactly as they do to a bar
2130
+ // chart. They used to live only on the aggregated path below, so setting
2131
+ // `valueFormat` or `logScale` on a direct type silently did nothing.
2132
+ //
2133
+ // The series-shaped settings (trend, averageLine, seriesTypes) stay on the
2134
+ // aggregated path: a direct spec does not have the series shape they read.
2135
+ if (direct) {
2136
+ if (chartIsOhlc) {
2137
+ // Overlays ride on the price series; the panes are composed by the
2138
+ // panel body from `chartIndicatorPanes`.
2139
+ const overlay = chartIndicatorSplit.overlay;
2140
+ if (overlay && direct.series[0])
2141
+ direct.series[0].overlay = overlay;
2142
+ }
2143
+ return applyChartValueSettings(direct);
2144
+ }
2145
+ // A direct type that could not build (scatter with no Y picked yet, for
2146
+ // instance) must not fall through to the bar-chart path and draw the wrong
2147
+ // chart - it renders the panel's empty state instead.
2148
+ if (chartType === "scatter" || chartType === "gauge" || chartType === "boxplot" ||
2149
+ chartType === "histogram" || chartType === "range-bar" || chartType === "range-area" ||
2150
+ chartType === "dumbbell" || chartType === "bullet" || chartIsOhlc)
2151
+ return null;
2152
+ spec = chartEngine.rowsToChartSpec(rowsForChart, {
2153
+ type: chartType,
2154
+ category: category,
2155
+ value: values.length === 1 ? values[0] : values,
2156
+ ...(seriesField ? { series: seriesField } : {}),
2157
+ reduce: chartReduce,
2158
+ ...(chartBucket && chartDimensionIsDate ? { bucket: chartBucket } : {}),
2159
+ stacked: effectiveChartStacked || effectiveChartStacked100,
2160
+ stacked100: effectiveChartStacked100,
2161
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2162
+ ...(chartCfg.topN ? { topN: chartCfg.topN } : {}),
2163
+ ...(chartCfg.otherLabel ? { otherLabel: chartCfg.otherLabel } : {}),
2164
+ ...(chartCfg.sort ? { sort: chartCfg.sort } : {}),
2165
+ });
2166
+ }
2013
2167
  // Reshape the aggregated grid into whatever this type actually reads.
2014
2168
  // These run AFTER the aggregation so grouping, reduce, sort, topN and the
2015
2169
  // "Other" bucket apply to them exactly as they do to a bar chart.
2016
- if (chartType === "treemap")
2170
+ if (chartType === "treemap" || chartType === "sunburst")
2017
2171
  spec.treemap = chartEngine.specToTreemap(spec);
2172
+ else if (chartType === "chord") {
2173
+ const flow = chartEngine.specToSankey(spec);
2174
+ spec.sankeyNodes = flow.nodes;
2175
+ spec.sankeyLinks = flow.links;
2176
+ }
2018
2177
  else if (chartType === "calendar")
2019
2178
  spec.calendarValues = chartEngine.specToCalendar(spec);
2020
2179
  else if (chartType === "sankey") {
@@ -2024,13 +2183,7 @@ export function createSvGridController(rawProps, domIdBase) {
2024
2183
  }
2025
2184
  if (effectiveChartOrientation === "horizontal")
2026
2185
  spec.orientation = "horizontal";
2027
- if (effectiveChartTimeAxis)
2028
- spec.xType = "time";
2029
- if (effectiveChartLogScale)
2030
- spec.yScale = "log";
2031
- if (effectiveChartValueFormat)
2032
- spec.valueFormat = effectiveChartValueFormat;
2033
- applyChartLocale(spec);
2186
+ applyChartValueSettings(spec);
2034
2187
  if (chartType !== "pie" && spec.orientation !== "horizontal" && !spec.yAxisTitle && chartAutoYAxisTitle) {
2035
2188
  spec.yAxisTitle = chartAutoYAxisTitle;
2036
2189
  }
@@ -2043,6 +2196,16 @@ export function createSvGridController(rawProps, domIdBase) {
2043
2196
  spec.annotations = chartCfg.annotations;
2044
2197
  if (chartCfg.patternFallback)
2045
2198
  spec.patternFallback = true;
2199
+ if (chartFunnelShape && chartType === "funnel")
2200
+ spec.funnelShape = chartFunnelShape;
2201
+ if (chartCandleStyle && (chartType === "candlestick" || chartType === "ohlc"))
2202
+ spec.candleStyle = chartCandleStyle;
2203
+ // A stream is a stacked area on a wiggle baseline; the picker's Stacked
2204
+ // box has nothing to add, so the offset is applied here outright.
2205
+ if (chartType === "stream") {
2206
+ spec.stacked = true;
2207
+ spec.stackOffset = "wiggle";
2208
+ }
2046
2209
  const refLines = [...(chartCfg.referenceLines ?? [])];
2047
2210
  if (chartCfg.averageLine && spec.series[0]?.values.length) {
2048
2211
  const vals = spec.series[0].values;
@@ -2052,8 +2215,10 @@ export function createSvGridController(rawProps, domIdBase) {
2052
2215
  if (refLines.length)
2053
2216
  spec.referenceLines = refLines;
2054
2217
  for (const s of spec.series) {
2055
- if (chartCfg.trend)
2056
- s.overlay = chartCfg.trend === "linear" ? "linear" : `${chartCfg.trend}:7`;
2218
+ if (chartCfg.trend) {
2219
+ const t = chartCfg.trend;
2220
+ s.overlay = t === "sma" || t === "ema" ? `${t}:7` : t === "poly" ? "poly:2" : t;
2221
+ }
2057
2222
  if (chartCfg.smooth)
2058
2223
  s.smooth = true;
2059
2224
  if (chartCfg.seriesTypes?.[s.label])
@@ -2063,6 +2228,51 @@ export function createSvGridController(rawProps, domIdBase) {
2063
2228
  }
2064
2229
  return spec;
2065
2230
  });
2231
+ /**
2232
+ * What the panel renders: an unlinked chart's frozen snapshot, else the live
2233
+ * derivation with the builder's format applied. The format runs through
2234
+ * the lazy engine so its code stays out of the base bundle.
2235
+ */
2236
+ const chartSpec = $derived.by(() => {
2237
+ if (activeChart.frozen)
2238
+ return activeChart.frozen.spec;
2239
+ const live = chartSpecLive;
2240
+ if (!live || !activeChart.format || !chartEngine)
2241
+ return live;
2242
+ return chartEngine.applyChartFormat(live, activeChart.format);
2243
+ });
2244
+ let announcedFirstChart = false;
2245
+ $effect(() => {
2246
+ if (!chartPanelOpen || announcedFirstChart || !chartCfg?.onChartCreated)
2247
+ return;
2248
+ announcedFirstChart = true;
2249
+ const c = charts[0];
2250
+ untrack(() => chartCfg?.onChartCreated?.({ index: 0, title: c.title, type: c.type, spec: null }));
2251
+ });
2252
+ // Lifecycle: one `onChartChanged` per burst of spec changes.
2253
+ let chartChangeTimer = null;
2254
+ $effect(() => {
2255
+ const cb = chartCfg?.onChartChanged;
2256
+ const spec = chartSpec;
2257
+ const index = activeChartIndex;
2258
+ if (!cb)
2259
+ return;
2260
+ untrack(() => {
2261
+ if (chartChangeTimer)
2262
+ clearTimeout(chartChangeTimer);
2263
+ chartChangeTimer = setTimeout(() => {
2264
+ chartChangeTimer = null;
2265
+ const c = charts[index] ?? activeChart;
2266
+ cb({ index, title: c.title, type: c.type, spec });
2267
+ }, 150);
2268
+ });
2269
+ return () => {
2270
+ if (chartChangeTimer) {
2271
+ clearTimeout(chartChangeTimer);
2272
+ chartChangeTimer = null;
2273
+ }
2274
+ };
2275
+ });
2066
2276
  /**
2067
2277
  * Can a click on this chart be turned back into a grid filter?
2068
2278
  *
@@ -2071,11 +2281,16 @@ export function createSvGridController(rawProps, domIdBase) {
2071
2281
  * SOURCE column by a target value and empty the grid), a gauge (no category
2072
2282
  * at all), or a scatter (the mark carries a point label, not a group).
2073
2283
  */
2074
- const chartCrossFilterable = $derived(chartType !== "sankey" && chartType !== "gauge" && chartType !== "scatter");
2284
+ const chartCrossFilterable = $derived(chartType !== "sankey" && chartType !== "gauge" && chartType !== "scatter" &&
2285
+ // A chord's arcs are both ends of a flow, a sunburst's are a hierarchy
2286
+ // and a histogram's are bins: none maps a click back to one dimension value.
2287
+ chartType !== "chord" && chartType !== "sunburst" && chartType !== "histogram");
2288
+ /** The column a chart click filters: the picked dimension, or in pivot mode the innermost row dimension when it is a grid column. */
2289
+ const chartFilterDimId = $derived(pivotResult ? (chartEngine?.pivotFilterColumn(pivotConfig.rows, chartableColumns.dims, fieldOf) ?? null) : effectiveChartDimensionId);
2075
2290
  function applyChartCrossFilter(category) {
2076
2291
  if (!chartCrossFilterable)
2077
2292
  return;
2078
- const dimId = effectiveChartDimensionId;
2293
+ const dimId = chartFilterDimId;
2079
2294
  if (!dimId)
2080
2295
  return;
2081
2296
  const existing = valueFilters[dimId];
@@ -2087,7 +2302,7 @@ export function createSvGridController(rawProps, domIdBase) {
2087
2302
  valueFilters = { ...valueFilters, [dimId]: next };
2088
2303
  }
2089
2304
  function clearChartCrossFilter() {
2090
- const dimId = effectiveChartDimensionId;
2305
+ const dimId = chartFilterDimId;
2091
2306
  if (!dimId)
2092
2307
  return;
2093
2308
  const next = { ...valueFilters };
@@ -2246,9 +2461,50 @@ export function createSvGridController(rawProps, domIdBase) {
2246
2461
  const logical = virtualizer.getState().scrollOffset;
2247
2462
  return logical - rowScrollScaling.logicalToDom(logical);
2248
2463
  });
2464
+ /**
2465
+ * The first `frozenRows` rows are rendered ahead of the virtual window
2466
+ * and stick under the header. The virtualizer still counts them, so its
2467
+ * offsets stay in row-index space and nothing downstream has to translate;
2468
+ * the window skips them, and the top spacer gives back the height they
2469
+ * already occupy in the flow.
2470
+ */
2471
+ const frozenRowCount = $derived(Math.max(0, Math.min(Math.floor(props.frozenRows ?? 0), allRows.length)));
2472
+ const frozenRowList = $derived(frozenRowCount ? allRows.slice(0, frozenRowCount) : []);
2473
+ /** The merged cells, indexed; null (the usual case) costs nothing. */
2474
+ const mergeIndex = $derived(buildMergeIndex(props.mergedCells));
2475
+ const frozenBandHeight = $derived.by(() => {
2476
+ if (!frozenRowCount)
2477
+ return 0;
2478
+ rowResizeVersion;
2479
+ autoRowHeightVersion;
2480
+ let total = 0;
2481
+ for (let i = 0; i < frozenRowCount; i += 1)
2482
+ total += rowSizePxOf(i);
2483
+ return total;
2484
+ });
2485
+ /** The declared or dragged height of one row; measured under auto height. */
2486
+ function rowSizePxOf(i) {
2487
+ if (isRowCollapsed(i))
2488
+ return 0;
2489
+ if (autoRowHeightOn)
2490
+ return measuredRowHeights.get(i) ?? autoRowHeightFallback;
2491
+ const own = rowResizeHeightPx(i);
2492
+ if (own != null)
2493
+ return own;
2494
+ const rh = props.rowHeight;
2495
+ return typeof rh === "function" ? rh(i) : (rh ?? 30);
2496
+ }
2497
+ /** Where frozen row i sticks: under the header and the frozen rows above it. */
2498
+ function frozenRowTop(i) {
2499
+ let top = headerHeight;
2500
+ for (let k = 0; k < i; k += 1)
2501
+ top += rowSizePxOf(k);
2502
+ return top;
2503
+ }
2249
2504
  // Spacer heights in DOM space. With scaling inert these equal the original
2250
- // virtualRowStart / virtualRowBottomSpacer.
2251
- const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment, 0));
2505
+ // virtualRowStart / virtualRowBottomSpacer. The frozen band is in the flow
2506
+ // before the spacer whatever the window shows, so it comes off the spacer.
2507
+ const rowTopSpacer = $derived(Math.max(virtualRowStart - rowOffsetAdjustment - frozenBandHeight, 0));
2252
2508
  const rowBottomSpacer = $derived(Math.max(rowDomTotalSize - (virtualRowEnd - rowOffsetAdjustment), 0));
2253
2509
  const virtualColumns = $derived.by(function virtualColumns_d() {
2254
2510
  columnVirtualizerVersion;
@@ -2585,35 +2841,117 @@ export function createSvGridController(rawProps, domIdBase) {
2585
2841
  let autoRowHeightVersion = $state(0);
2586
2842
  const autoRowHeightFallback = $derived(typeof props.rowHeight === "number" ? props.rowHeight : 30);
2587
2843
  // ----- Interactive row resize (`rowResize` prop) -----
2588
- // Heights the user has dragged, by row index. A plain Map plus a version
2589
- // counter, exactly like `measuredRowHeights` above and for the same reason:
2590
- // the reset effect below has to read this collection to know whether there is
2591
- // anything to clear, and if the collection were reactive that read would make
2592
- // every resize re-trigger the reset and wipe the height it just stored.
2844
+ // Heights the user has dragged, or set through `api.setRowHeight`, keyed
2845
+ // by ROW ID so they belong to the row rather than to a slot: the height
2846
+ // follows its row through a sort or a filter and survives the data being
2847
+ // replaced. They used to be keyed by index and wiped on every data change,
2848
+ // which in a spreadsheet meant every keystroke, so a row dragged taller
2849
+ // snapped back the moment the next cell was typed. A plain Map plus a
2850
+ // version counter, like `measuredRowHeights` above: the readers touch the
2851
+ // version, and the Map itself stays out of the reactive graph.
2593
2852
  const rowResizeHeights = new Map();
2594
2853
  let rowResizeVersion = $state(0);
2595
- function setRowResizeHeight(index, height) {
2596
- if (!rowResizeOn || !Number.isFinite(index) || height <= 0)
2854
+ /**
2855
+ * Rows folded to nothing, the row-side twin of `collapsedColumns`: the
2856
+ * row keeps its index and its cells and takes no height. Keyed by row id
2857
+ * like the heights and announced through the same version counter, so
2858
+ * every reader of a row's size sees the change.
2859
+ */
2860
+ const collapsedRows = new Set();
2861
+ function isRowCollapsed(index) {
2862
+ rowResizeVersion;
2863
+ const id = allRows[index]?.id;
2864
+ return id != null && collapsedRows.has(id);
2865
+ }
2866
+ function setRowCollapsed(index, collapsed) {
2867
+ const id = allRows[index]?.id;
2868
+ if (id == null)
2597
2869
  return;
2598
- rowResizeHeights.set(index, Math.round(height));
2870
+ if (collapsed === collapsedRows.has(id))
2871
+ return;
2872
+ if (collapsed)
2873
+ collapsedRows.add(id);
2874
+ else
2875
+ collapsedRows.delete(id);
2599
2876
  rowResizeVersion += 1;
2600
2877
  }
2601
- /** A user-dragged height for this row, or undefined when it has not been
2602
- * resized. Read by the view AND by the virtualizer's `estimateSize`. */
2603
- function rowResizeHeightPx(index) {
2604
- return rowResizeOn ? rowResizeHeights.get(index) : undefined;
2878
+ /**
2879
+ * A size the user changed by hand is one undo step, as it is in a
2880
+ * spreadsheet: Ctrl+Z after a column drag puts the width back. Recorded
2881
+ * only for the user's gestures; the api's own setters record nothing, so
2882
+ * a consumer restoring saved sizes does not fill the history.
2883
+ */
2884
+ function recordSizeUndo(undo, redo) {
2885
+ pushHistory(ctx, [
2886
+ { rowId: "", columnId: "", field: "", before: undefined, after: undefined, custom: { undo, redo } },
2887
+ ]);
2605
2888
  }
2606
- // A row index means a different row once the data changes, so keeping the
2607
- // heights would size new rows by the old ones - the same reasoning as the
2608
- // measured-height reset below.
2609
- $effect(() => {
2610
- void allRows.length;
2611
- void internalData;
2612
- if (rowResizeHeights.size === 0)
2889
+ // The undo and redo of a size step report through the same callbacks as
2890
+ // the gesture did: a consumer keeping the sizes (a sheet saving its
2891
+ // document) would otherwise hold the width the user dragged to while the
2892
+ // grid showed the one Ctrl+Z put back.
2893
+ function resizeColumnByUser(columnId, width, before) {
2894
+ const apply = (w) => {
2895
+ columnWidths = { ...columnWidths, [columnId]: w };
2896
+ props.onColumnResize?.({ columnId, width: w });
2897
+ };
2898
+ apply(width);
2899
+ if (before !== width)
2900
+ recordSizeUndo(() => apply(before), () => apply(width));
2901
+ }
2902
+ function autosizeColumnByUser(columnId) {
2903
+ const before = getColumnWidth(columnId);
2904
+ autosizeColumn(columnId);
2905
+ const width = getColumnWidth(columnId);
2906
+ const apply = (w) => {
2907
+ columnWidths = { ...columnWidths, [columnId]: w };
2908
+ props.onColumnResize?.({ columnId, width: w });
2909
+ };
2910
+ if (before !== width)
2911
+ recordSizeUndo(() => apply(before), () => apply(width));
2912
+ else
2913
+ props.onColumnResize?.({ columnId, width });
2914
+ }
2915
+ function resizeRowByUser(index, height) {
2916
+ const before = rowResizeHeightPx(index) ?? null;
2917
+ const apply = (h) => {
2918
+ setRowResizeHeight(index, h);
2919
+ props.onRowResize?.({ rowIndex: index, height: h });
2920
+ };
2921
+ apply(height);
2922
+ if (before !== height)
2923
+ recordSizeUndo(() => apply(before), () => apply(height));
2924
+ }
2925
+ function setRowResizeHeight(index, height) {
2926
+ const id = allRows[index]?.id;
2927
+ if (id == null)
2613
2928
  return;
2614
- rowResizeHeights.clear();
2929
+ if (height == null) {
2930
+ if (!rowResizeHeights.delete(id))
2931
+ return;
2932
+ }
2933
+ else {
2934
+ if (!Number.isFinite(height) || height <= 0)
2935
+ return;
2936
+ rowResizeHeights.set(id, Math.round(height));
2937
+ }
2615
2938
  rowResizeVersion += 1;
2616
- });
2939
+ }
2940
+ /** A row's own height, or undefined when it has none. Read by the view AND
2941
+ * by the virtualizer's `estimateSize`. Auto height wins over it: the
2942
+ * content decides the height there, and a manual one would be overwritten
2943
+ * by the next measurement. */
2944
+ function rowResizeHeightPx(index) {
2945
+ const id = allRows[index]?.id;
2946
+ if (id == null)
2947
+ return undefined;
2948
+ // A collapsed row is 0 whatever else it has, under auto height too.
2949
+ if (collapsedRows.has(id))
2950
+ return 0;
2951
+ if (autoRowHeightOn)
2952
+ return undefined;
2953
+ return rowResizeHeights.get(id);
2954
+ }
2617
2955
  /** Report a row's measured height. Called by the view's measuring action.
2618
2956
  * Sub-pixel churn is ignored so a fractional layout can't loop. */
2619
2957
  function reportRowHeight(index, height) {
@@ -2638,12 +2976,20 @@ export function createSvGridController(rawProps, domIdBase) {
2638
2976
  function measureRowHeight(node, index) {
2639
2977
  let current = index;
2640
2978
  let stop = null;
2641
- const measure = () => reportRowHeight(current, node.getBoundingClientRect().height);
2979
+ // Read the rect only under autoRowHeight. `getBoundingClientRect` forces a
2980
+ // synchronous layout, and the virtualizer's slot keys hand every rendered
2981
+ // <tr> a new index on each one-row scroll, so measuring unconditionally
2982
+ // meant ~30 forced layouts per arrow key on a fixed-height grid whose
2983
+ // measurement `reportRowHeight` then threw away.
2984
+ const measure = () => {
2985
+ if (!autoRowHeightOn)
2986
+ return;
2987
+ reportRowHeight(current, node.getBoundingClientRect().height);
2988
+ };
2642
2989
  const attach = () => {
2643
2990
  stop?.();
2644
2991
  stop = autoRowHeightOn ? observeSizeRaf(node, measure) : null;
2645
- if (autoRowHeightOn)
2646
- measure();
2992
+ measure();
2647
2993
  };
2648
2994
  attach();
2649
2995
  return {
@@ -2694,8 +3040,8 @@ export function createSvGridController(rawProps, domIdBase) {
2694
3040
  : typeof rh === "function"
2695
3041
  ? rh
2696
3042
  : (rh ?? 30);
2697
- const estimateSize = rowResizeOn
2698
- ? (index) => rowResizeHeights.get(index) ??
3043
+ const estimateSize = rowResizeHeights.size > 0 || collapsedRows.size > 0
3044
+ ? (index) => rowResizeHeightPx(index) ??
2699
3045
  (typeof base === "function" ? base(index) : base)
2700
3046
  : base;
2701
3047
  virtualizer.setOptions({
@@ -2755,6 +3101,7 @@ export function createSvGridController(rawProps, domIdBase) {
2755
3101
  // closure each run; the virtualizer sees a new function reference and
2756
3102
  // re-derives its layout from the current per-column widths.
2757
3103
  columnWidths;
3104
+ collapsedColumns;
2758
3105
  columnVirtualizer.setOptions({
2759
3106
  count: allColumns.length,
2760
3107
  estimateSize: (index) => {
@@ -2795,8 +3142,11 @@ export function createSvGridController(rawProps, domIdBase) {
2795
3142
  // Lets the resolver answer from cache without re-invoking the source, so an
2796
3143
  // async source fires one request per row instead of one per render.
2797
3144
  const asyncEditorColumns = new Set();
3145
+ // Depends on `allRows` and `rowSelectionState` only. It used to read the
3146
+ // catch-all `gridStateVersion` too, which made every arrow key walk all the
3147
+ // rows (10k iterations on the large-dataset demo, 100k on its top tier) to
3148
+ // recount a selection that had not changed.
2798
3149
  const headerSelectionState = $derived.by(function headerSelectionState_d() {
2799
- gridStateVersion;
2800
3150
  const selectable = allRows.filter((row) => !isGroupRow(row));
2801
3151
  if (!selectable.length)
2802
3152
  return "none";
@@ -2815,6 +3165,10 @@ export function createSvGridController(rawProps, domIdBase) {
2815
3165
  * from a user-focused (0,0). The fill handle keys off this flag so it
2816
3166
  * stays hidden until the user actually selects something. */
2817
3167
  let userHasActivatedCell = $state(false);
3168
+ /** The column a run of Tabs began in, so the Enter that ends the run goes
3169
+ * down from there (`getEntryStep`). Plain, not state: nothing renders
3170
+ * it. `setActiveCell` clears it; the Tab and Enter paths set it back. */
3171
+ let tabRunOrigin = null;
2818
3172
  /**
2819
3173
  * Per-column fitted widths when `fitColumns` is on. Computed in one pass
2820
3174
  * so the LAST auto-sized column can absorb the rounding residue and make
@@ -2837,7 +3191,7 @@ export function createSvGridController(rawProps, domIdBase) {
2837
3191
  return null;
2838
3192
  // Exclude columns hidden by a collapsed group too (#57) - otherwise their
2839
3193
  // widths stay in the fit distribution and leave a blank gap at the edge.
2840
- const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id]);
3194
+ const cols = grid.getAllColumns().filter((c) => !hiddenColumns[c.id] && !hiddenByGroupCollapse[c.id] && !collapsedColumns[c.id]);
2841
3195
  if (!cols.length)
2842
3196
  return null;
2843
3197
  const rowNumberWidth = showRowNumbersEffective ? rowNumberColumnWidth : 0;
@@ -3170,6 +3524,17 @@ export function createSvGridController(rawProps, domIdBase) {
3170
3524
  // state mutation inside the callback (e.g. `api.setGroupBy(...)`) created
3171
3525
  // an infinite update loop. Now it's a true mount-once notification.
3172
3526
  let apiNotified = false;
3527
+ // Memoized: `buildApi()` builds a fresh object each call, and a feature pack
3528
+ // that keys per-grid state on the api (a WeakMap in @svgrid/enterprise) needs
3529
+ // the object the consumer received and the object a command handler sees to
3530
+ // be the same one. Every method reads through `ctx`, so one instance stays
3531
+ // correct for the life of the grid.
3532
+ let apiInstance = null;
3533
+ function api() {
3534
+ if (!apiInstance)
3535
+ apiInstance = buildApi();
3536
+ return apiInstance;
3537
+ }
3173
3538
  $effect(() => {
3174
3539
  if (apiNotified)
3175
3540
  return;
@@ -3177,7 +3542,7 @@ export function createSvGridController(rawProps, domIdBase) {
3177
3542
  if (!cb)
3178
3543
  return;
3179
3544
  apiNotified = true;
3180
- cb(buildApi());
3545
+ cb(api());
3181
3546
  });
3182
3547
  const ctx = {
3183
3548
  get props() { return props; },
@@ -3239,6 +3604,8 @@ export function createSvGridController(rawProps, domIdBase) {
3239
3604
  set history(v) { history = v; },
3240
3605
  get historyPtr() { return historyPtr; },
3241
3606
  set historyPtr(v) { historyPtr = v; },
3607
+ get historyGroupId() { return historyGroupId; },
3608
+ set historyGroupId(v) { historyGroupId = v; },
3242
3609
  get historyVersion() { return historyVersion; },
3243
3610
  set historyVersion(v) { historyVersion = v; },
3244
3611
  get tooltip() { return tooltip; },
@@ -3261,6 +3628,9 @@ export function createSvGridController(rawProps, domIdBase) {
3261
3628
  get editorSelectAll() { return editorSelectAll; },
3262
3629
  set editorSelectAll(v) { editorSelectAll = v; },
3263
3630
  get columnWidths() { return columnWidths; },
3631
+ get collapsedColumns() { return collapsedColumns; },
3632
+ get mergeIndex() { return mergeIndex; },
3633
+ get setColumnCollapsed() { return setColumnCollapsed; },
3264
3634
  set columnWidths(v) { columnWidths = v; },
3265
3635
  get MIN_COLUMN_WIDTH() { return MIN_COLUMN_WIDTH; },
3266
3636
  get columnPinning() { return columnPinning; },
@@ -3332,6 +3702,7 @@ export function createSvGridController(rawProps, domIdBase) {
3332
3702
  get moveCellsEffective() { return moveCellsEffective; },
3333
3703
  get flushScheduledScrollSync() { return flushScheduledScrollSync; },
3334
3704
  get scheduleScrollSync() { return scheduleScrollSync; },
3705
+ get syncScrollNow() { return syncScrollNow; },
3335
3706
  get internalData() { return internalData; },
3336
3707
  set internalData(v) { internalData = v; },
3337
3708
  get internalColumns() { return internalColumns; },
@@ -3435,6 +3806,8 @@ export function createSvGridController(rawProps, domIdBase) {
3435
3806
  get moveColumnInPanel() { return moveColumnInPanel; },
3436
3807
  // ---- Localized chrome strings ----
3437
3808
  get messages() { return gridMessages; },
3809
+ /** The raw `localization.text` map, for the lazy chart panel's own strings. */
3810
+ get localizationText() { return props.localization?.text; },
3438
3811
  // ---- In-grid pivot mode ----
3439
3812
  get pivotConfig() { return pivotConfig; },
3440
3813
  get pivotActive() { return pivotActive; },
@@ -3467,6 +3840,35 @@ export function createSvGridController(rawProps, domIdBase) {
3467
3840
  set chartLogScale(v) { activeChart.logScale = v; },
3468
3841
  get chartTimeAxis() { return effectiveChartTimeAxis; },
3469
3842
  set chartTimeAxis(v) { activeChart.timeAxis = v; },
3843
+ get chartBucket() { return chartBucket; },
3844
+ set chartBucket(v) { activeChart.bucket = v; },
3845
+ get chartBins() { return chartBins; },
3846
+ set chartBins(v) { activeChart.bins = v; },
3847
+ get chartFunnelShape() { return chartFunnelShape; },
3848
+ set chartFunnelShape(v) { activeChart.funnelShape = v; },
3849
+ get chartCandleStyle() { return chartCandleStyle; },
3850
+ set chartCandleStyle(v) { activeChart.candleStyle = v; },
3851
+ get chartFormat() { return activeChart.format; },
3852
+ set chartFormat(v) { activeChart.format = v; },
3853
+ get chartFrozen() { return activeChart.frozen; },
3854
+ get chartIsOhlc() { return chartIsOhlc; },
3855
+ get chartOhlc() { return effectiveChartOhlc; },
3856
+ set chartOhlc(v) { activeChart.ohlc = v; },
3857
+ get chartOhlcDateId() { return effectiveChartOhlcDateId; },
3858
+ get chartIndicators() { return chartIndicators; },
3859
+ set chartIndicators(v) { activeChart.indicators = v; },
3860
+ get chartIndicatorPanes() { return chartIndicatorPanes; },
3861
+ get chartZoom() { return activeChart.zoom; },
3862
+ // The panel keeps one chart component across tabs, so "tabs share a
3863
+ // window" is simply every tab carrying the same window: a zoom on one is
3864
+ // written to all, and switching tabs hands the component the same value.
3865
+ set chartZoom(v) {
3866
+ if (chartCfg?.syncTabs)
3867
+ for (const c of charts)
3868
+ c.zoom = v;
3869
+ else
3870
+ activeChart.zoom = v;
3871
+ },
3470
3872
  get chartValueFormat() { return effectiveChartValueFormat ?? "number"; },
3471
3873
  set chartValueFormat(v) { activeChart.valueFormat = v; },
3472
3874
  get chartDimensionIsDate() { return chartDimensionIsDate; },
@@ -3487,8 +3889,23 @@ export function createSvGridController(rawProps, domIdBase) {
3487
3889
  get activeChartIndex() { return Math.min(activeChartIndex, charts.length - 1); },
3488
3890
  set activeChartIndex(v) { activeChartIndex = Math.max(0, Math.min(charts.length - 1, v)); },
3489
3891
  addChart() {
3490
- charts = [...charts, makeChart(`Chart ${charts.length + 1}`)];
3892
+ const next = makeChart(`Chart ${charts.length + 1}`);
3893
+ if (chartCfg?.syncTabs)
3894
+ next.zoom = activeChart.zoom;
3895
+ charts = [...charts, next];
3491
3896
  activeChartIndex = charts.length - 1;
3897
+ chartCfg?.onChartCreated?.({ index: activeChartIndex, title: next.title, type: next.type, spec: null });
3898
+ },
3899
+ /** Unlink the active chart from the grid: it keeps the spec it has now. */
3900
+ freezeChart() {
3901
+ const spec = chartSpec;
3902
+ if (!spec)
3903
+ return;
3904
+ activeChart.frozen = { spec: JSON.parse(JSON.stringify(spec)), at: new Date().toISOString() };
3905
+ },
3906
+ /** Link the active chart back to the grid's rows. */
3907
+ unfreezeChart() {
3908
+ activeChart.frozen = null;
3492
3909
  },
3493
3910
  removeChart(index) {
3494
3911
  if (charts.length <= 1)
@@ -3504,48 +3921,40 @@ export function createSvGridController(rawProps, domIdBase) {
3504
3921
  c.title = title;
3505
3922
  },
3506
3923
  getChartsState() {
3507
- return charts.map((c) => ({
3508
- title: c.title,
3509
- type: c.type,
3510
- dimension: c.dimensionId,
3511
- series: c.seriesId ?? null,
3512
- measure: c.measureId,
3513
- measure2: c.measure2Id,
3514
- orientation: c.orientation,
3515
- stacked100: c.stacked100,
3516
- donut: c.donut,
3517
- palette: c.palette,
3518
- reduce: c.reduce,
3519
- stacked: c.stacked,
3520
- dataLabels: c.dataLabels,
3521
- logScale: c.logScale,
3522
- timeAxis: c.timeAxis,
3523
- valueFormat: c.valueFormat,
3524
- }));
3924
+ return charts.map(tabToState);
3525
3925
  },
3526
3926
  applyChartsState(list, active) {
3527
- const restored = list.map((c, i) => ({
3528
- id: `chart-${chartSeq++}`,
3529
- title: typeof c.title === "string" ? c.title : `Chart ${i + 1}`,
3530
- type: c.type ?? "bar",
3531
- reduce: c.reduce ?? "sum",
3532
- dimensionId: c.dimension ?? null,
3533
- measureId: c.measure ?? null,
3534
- seriesId: c.series,
3535
- measure2Id: c.measure2 ?? null,
3536
- orientation: c.orientation ?? null,
3537
- stacked100: c.stacked100 ?? null,
3538
- donut: c.donut ?? null,
3539
- palette: c.palette ?? null,
3540
- stacked: c.stacked ?? null,
3541
- dataLabels: c.dataLabels ?? null,
3542
- logScale: c.logScale ?? null,
3543
- timeAxis: c.timeAxis ?? null,
3544
- valueFormat: c.valueFormat ?? null,
3545
- }));
3927
+ const restored = list.map(tabFromState);
3546
3928
  charts = restored.length ? restored : [makeChart("Chart 1")];
3547
3929
  activeChartIndex = Math.max(0, Math.min(charts.length - 1, active ?? 0));
3548
3930
  },
3931
+ // ---- Saved charts ----
3932
+ get savedCharts() { return savedCharts; },
3933
+ /** Keep the active tab's configuration under a name (replacing a same-named one). */
3934
+ saveChart(name) {
3935
+ const entry = { name, savedAt: new Date().toISOString(), tab: tabToState(activeChart) };
3936
+ savedCharts = [...savedCharts.filter((s) => s.name !== name), entry];
3937
+ },
3938
+ /** Apply a saved chart to the active tab; the tab keeps its id and title. */
3939
+ applySavedChart(name) {
3940
+ const entry = savedCharts.find((s) => s.name === name);
3941
+ if (!entry)
3942
+ return false;
3943
+ const { id: _id, title: _title, ...rest } = tabFromState(entry.tab, activeChartIndex);
3944
+ Object.assign(activeChart, rest);
3945
+ return true;
3946
+ },
3947
+ removeSavedChart(name) {
3948
+ savedCharts = savedCharts.filter((s) => s.name !== name);
3949
+ },
3950
+ getSavedCharts() {
3951
+ return savedCharts.map((s) => ({ ...s, tab: { ...s.tab } }));
3952
+ },
3953
+ applySavedCharts(list) {
3954
+ savedCharts = list
3955
+ .filter((s) => typeof s?.name === "string" && s.tab && typeof s.tab === "object")
3956
+ .map((s) => ({ name: s.name, savedAt: String(s.savedAt ?? ""), tab: s.tab }));
3957
+ },
3549
3958
  get chartCrossFilterable() { return chartCrossFilterable; },
3550
3959
  get chartOrientation() { return effectiveChartOrientation; },
3551
3960
  set chartOrientation(v) { activeChart.orientation = v; },
@@ -3560,7 +3969,11 @@ export function createSvGridController(rawProps, domIdBase) {
3560
3969
  get effectiveChartMeasure2Id() { return effectiveChartMeasure2Id; },
3561
3970
  get chartAiHandler() { return chartAiHandler; },
3562
3971
  set chartAiHandler(v) { chartAiHandler = v; },
3972
+ get chartExplainHandler() { return chartExplainHandler; },
3973
+ set chartExplainHandler(v) { chartExplainHandler = v; },
3563
3974
  applyChartConfig(config) {
3975
+ if (config.saved)
3976
+ this.applySavedChart(config.saved);
3564
3977
  const cols = [...chartableColumns.dims, ...chartableColumns.measures];
3565
3978
  const resolve = (v) => {
3566
3979
  if (v == null)
@@ -3593,6 +4006,33 @@ export function createSvGridController(rawProps, domIdBase) {
3593
4006
  activeChart.timeAxis = config.timeAxis;
3594
4007
  if (config.valueFormat)
3595
4008
  activeChart.valueFormat = config.valueFormat;
4009
+ if ("bucket" in config)
4010
+ activeChart.bucket = config.bucket ?? null;
4011
+ if ("bins" in config)
4012
+ activeChart.bins = config.bins ?? null;
4013
+ if ("funnelShape" in config)
4014
+ activeChart.funnelShape = config.funnelShape ?? null;
4015
+ if ("candleStyle" in config)
4016
+ activeChart.candleStyle = config.candleStyle ?? null;
4017
+ if ("measure2" in config)
4018
+ activeChart.measure2Id = resolve(config.measure2);
4019
+ if ("ohlc" in config) {
4020
+ const o = config.ohlc;
4021
+ activeChart.ohlc = o
4022
+ ? { open: resolve(o.open), high: resolve(o.high), low: resolve(o.low), close: resolve(o.close), volume: resolve(o.volume) }
4023
+ : null;
4024
+ }
4025
+ if (config.indicators)
4026
+ activeChart.indicators = [...config.indicators];
4027
+ if ("format" in config)
4028
+ activeChart.format = config.format ?? null;
4029
+ if (config.frozen === false)
4030
+ activeChart.frozen = null;
4031
+ else if (config.frozen === true && !activeChart.frozen) {
4032
+ const spec = chartSpec;
4033
+ if (spec)
4034
+ activeChart.frozen = { spec: JSON.parse(JSON.stringify(spec)), at: new Date().toISOString() };
4035
+ }
3596
4036
  },
3597
4037
  get toggleGroupInPanel() { return toggleGroupInPanel; },
3598
4038
  get lastSortingSerialized() { return lastSortingSerialized; },
@@ -3615,8 +4055,17 @@ export function createSvGridController(rawProps, domIdBase) {
3615
4055
  },
3616
4056
  /** True when the `rowResize` prop is on and nothing overrides it. */
3617
4057
  get rowResizeOn() { return rowResizeOn; },
4058
+ get frozenRowCount() { return frozenRowCount; },
4059
+ get frozenRowList() { return frozenRowList; },
4060
+ get frozenBandHeight() { return frozenBandHeight; },
4061
+ get frozenRowTop() { return frozenRowTop; },
3618
4062
  /** Record the height the user dragged a row to. */
3619
4063
  get setRowResizeHeight() { return setRowResizeHeight; },
4064
+ get isRowCollapsed() { return isRowCollapsed; },
4065
+ get setRowCollapsed() { return setRowCollapsed; },
4066
+ get resizeRowByUser() { return resizeRowByUser; },
4067
+ get resizeColumnByUser() { return resizeColumnByUser; },
4068
+ get autosizeColumnByUser() { return autosizeColumnByUser; },
3620
4069
  /** A row's dragged height, or undefined when it has not been resized. */
3621
4070
  get rowResizeHeightPx() {
3622
4071
  rowResizeVersion;
@@ -3679,6 +4128,8 @@ export function createSvGridController(rawProps, domIdBase) {
3679
4128
  get toggleSelectAllRows() { return toggleSelectAllRows; },
3680
4129
  get userHasActivatedCell() { return userHasActivatedCell; },
3681
4130
  set userHasActivatedCell(v) { userHasActivatedCell = v; },
4131
+ get tabRunOrigin() { return tabRunOrigin; },
4132
+ set tabRunOrigin(v) { tabRunOrigin = v; },
3682
4133
  get setActiveCell() { return setActiveCell; },
3683
4134
  get scrollActiveCellIntoView() { return scrollActiveCellIntoView; },
3684
4135
  get getColumnBaseWidth() { return getColumnBaseWidth; },
@@ -3688,6 +4139,8 @@ export function createSvGridController(rawProps, domIdBase) {
3688
4139
  get extendSelection() { return extendSelection; },
3689
4140
  get isCellInSelectedRange() { return isCellInSelectedRange; },
3690
4141
  get getCellRangeEdges() { return getCellRangeEdges; },
4142
+ get activeRangeRect() { return activeRangeRect; },
4143
+ get cellSelectionState() { return cellSelectionState; },
3691
4144
  get getSelectionRects() { return getSelectionRects; },
3692
4145
  get fillHandleCell() { return fillHandleCell; },
3693
4146
  get isInFillPreview() { return isInFillPreview; },
@@ -3710,6 +4163,7 @@ export function createSvGridController(rawProps, domIdBase) {
3710
4163
  get startFillDrag() { return startFillDrag; },
3711
4164
  get onFillPointerMove() { return onFillPointerMove; },
3712
4165
  get onFillPointerUp() { return onFillPointerUp; },
4166
+ get fillDownToNeighbour() { return fillDownToNeighbour; },
3713
4167
  get isOnMoveGrabStrip() { return isOnMoveGrabStrip; },
3714
4168
  get startMoveDrag() { return startMoveDrag; },
3715
4169
  get onMovePointerMove() { return onMovePointerMove; },
@@ -3729,6 +4183,7 @@ export function createSvGridController(rawProps, domIdBase) {
3729
4183
  get cutSelectionToClipboard() { return cutSelectionToClipboard; },
3730
4184
  get pasteFromClipboard() { return pasteFromClipboard; },
3731
4185
  get onGridPaste() { return onGridPaste; },
4186
+ get armPasteFallback() { return armPasteFallback; },
3732
4187
  get clearSelectedCells() { return clearSelectedCells; },
3733
4188
  get onCellDoubleClick() { return onCellDoubleClick; },
3734
4189
  get startEditingWithChar() { return startEditingWithChar; },
@@ -3805,12 +4260,12 @@ export function createSvGridController(rawProps, domIdBase) {
3805
4260
  get clearColumnFilter() { return clearColumnFilter; },
3806
4261
  get onWindowKeydown() { return onWindowKeydown; },
3807
4262
  get columnDefMatchesId() { return columnDefMatchesId; },
3808
- get buildApi() { return buildApi; },
4263
+ get buildApi() { return api; },
3809
4264
  get apiNotified() { return apiNotified; },
3810
4265
  set apiNotified(v) { apiNotified = v; },
3811
4266
  };
3812
4267
  const { resolveEffectiveFeatures } = createFeatures(ctx);
3813
- const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, onBodyScroll } = createScrollSync(ctx);
4268
+ const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, syncScrollNow, onBodyScroll } = createScrollSync(ctx);
3814
4269
  const { onGridKeyDown, onWindowKeydown, onHeaderSortClick } = createKeyboard(ctx);
3815
4270
  const { computeSummaries, hasRenderedColumn } = createSummaries(ctx);
3816
4271
  // Hand the aggregator to `eagerSummaries` (declared above `ctx`, which this
@@ -3818,13 +4273,13 @@ export function createSvGridController(rawProps, domIdBase) {
3818
4273
  summarize = computeSummaries;
3819
4274
  const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, setFacetSelection, setFilterTokens, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus(ctx);
3820
4275
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, areEditorOptionsLoading, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender(ctx);
3821
- const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing(ctx);
3822
- const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
4276
+ const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste, armPasteFallback } = createEditing(ctx);
4277
+ const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, activeRangeRect, cellSelectionState, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
3823
4278
  const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns(ctx);
3824
4279
  const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd, onRowPointerDown, destroyRowDrag } = createRowDrag(ctx);
3825
4280
  const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids(ctx);
3826
4281
  const { buildApi } = createGridApi(ctx);
3827
- const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, isOnMoveGrabStrip, startMoveDrag, onMovePointerMove, onMovePointerUp, applyMoveRange, moveDestRect, trackEdgeScroll, stopEdgeScroll, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
4282
+ const { readCellRaw, writeCellRaw, applyFillPattern, clearSelectedCellValues, startFillDrag, onFillPointerMove, onFillPointerUp, fillDownToNeighbour, isOnMoveGrabStrip, startMoveDrag, onMovePointerMove, onMovePointerUp, applyMoveRange, moveDestRect, trackEdgeScroll, stopEdgeScroll, toggleBooleanCell, copySelectionToClipboard, clearSelectedCells, cutSelectionToClipboard } = createClipboard(ctx);
3828
4283
  // Dev-time configuration checks. Silent misconfiguration was the grid's
3829
4284
  // biggest usability gap - a misspelled `field` rendered a column of blank
3830
4285
  // cells and printed nothing, and an inert `pageSize` was simply ignored.