@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
package/src/ai.ts CHANGED
@@ -27,6 +27,9 @@
27
27
 
28
28
  import type { RowData, SvGridApi, TableFeatures } from './index'
29
29
  import { getExportProvider } from './export-provider'
30
+ import { validateChartSpec, type ChartDiagnostic } from './chart-validate'
31
+ import { chartSummary } from './chart-summary'
32
+ import type { ChartSpec } from './chart-types'
30
33
 
31
34
  // Export formats an AI export can plan. WRITING the enterprise formats (xlsx /
32
35
  // pdf / html / xml / md) needs the export engine from `@svgrid/enterprise`
@@ -68,7 +71,7 @@ export type AIRequest = {
68
71
  }
69
72
 
70
73
  /** Which helper produced a request - carried on {@link AIRequest} for routing and telemetry. */
71
- export type AITask = 'filter' | 'smart-fill' | 'summarize' | 'classify' | 'export' | 'anomaly' | 'chart'
74
+ export type AITask = 'filter' | 'smart-fill' | 'summarize' | 'classify' | 'export' | 'anomaly' | 'chart' | 'explain-chart'
72
75
 
73
76
  let provider: AIProvider | null = null
74
77
 
@@ -839,16 +842,21 @@ export async function aiFindAnomalies<
839
842
  * Chart shapes the model may choose from when planning a visualisation.
840
843
  *
841
844
  * These are exactly the types the built-in panel can build from a dimension, a
842
- * measure and an optional split - which is all a chart plan carries. Two
843
- * families are deliberately absent, because a plan cannot express them:
844
- * scatter needs a SECOND measure for its y axis, and candlestick / OHLC need
845
- * four under first/max/min/last. Asking for either would produce a plan the
846
- * panel renders as an empty frame, which is worse than not offering it.
845
+ * measure and an optional split - which is all a chart plan carries. Scatter
846
+ * is deliberately absent, because a plan cannot express its SECOND measure.
847
+ * A candlestick can be asked for: the panel finds the open / high / low /
848
+ * close / volume columns by name, so the plan only needs a date dimension.
847
849
  */
848
850
  export type AIChartType =
849
851
  | 'bar' | 'line' | 'area' | 'pie'
850
852
  | 'treemap' | 'funnel' | 'waterfall' | 'radar'
851
853
  | 'heatmap' | 'boxplot' | 'gauge' | 'calendar' | 'sankey'
854
+ | 'histogram' | 'lollipop' | 'pareto' | 'sunburst' | 'radial-bar' | 'radial-column' | 'nightingale' | 'bullet' | 'chord' | 'stream'
855
+ | 'candlestick'
856
+
857
+ /** The indicators a chart plan may ask for on a candlestick. */
858
+ export type AIChartIndicator = 'volume' | 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'sma' | 'ema' | 'bb' | 'vwap'
859
+ const AI_INDICATORS: AIChartIndicator[] = ['volume', 'rsi', 'macd', 'stochastic', 'atr', 'obv', 'sma', 'ema', 'bb', 'vwap']
852
860
 
853
861
  /** A chart the model proposed: its type, and the fields to plot. */
854
862
  export type AIChartPlan = {
@@ -859,14 +867,26 @@ export type AIChartPlan = {
859
867
  series: string | null
860
868
  /** Measure (value-axis) column field, or null. */
861
869
  measure: string | null
862
- reduce: 'sum' | 'avg' | 'count'
870
+ reduce: AIChartReduce
863
871
  stacked: boolean
864
872
  logScale: boolean
865
873
  timeAxis: boolean
866
874
  valueFormat: 'number' | 'currency' | 'percent'
875
+ /** Calendar unit to group a date dimension by, or null for exact values. */
876
+ bucket: 'day' | 'week' | 'month' | 'quarter' | 'year' | null
877
+ /** Candlestick only: indicator panes and overlays to add. */
878
+ indicators?: AIChartIndicator[]
867
879
  rationale: string
880
+ /** With `apply`: what `validateChartSpec` flagged as an error on the spec
881
+ * the panel built from the plan, empty when it drew cleanly. */
882
+ diagnostics?: ChartDiagnostic[]
868
883
  }
869
884
 
885
+ /** The reducers a chart plan may name: the panel's full list. */
886
+ export type AIChartReduce =
887
+ | 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'p90' | 'first' | 'last' | 'countDistinct'
888
+ const AI_REDUCERS: AIChartReduce[] = ['sum', 'avg', 'count', 'min', 'max', 'median', 'p90', 'first', 'last', 'countDistinct']
889
+
870
890
  /** Options for `aiChart` - preview the plan, or render it into the grid. */
871
891
  export type AIChartOptions = {
872
892
  /** Apply the plan to the grid's chart panel (open + configure). Default false. */
@@ -893,15 +913,17 @@ export async function aiChart<
893
913
  `into a strict-JSON chart plan the grid can render.\n\n` +
894
914
  `Columns:\n${schemaToPromptBlock(schema)}\n\n` +
895
915
  `Output JSON schema:\n` +
896
- `{ "type": "bar"|"line"|"area"|"pie"|"treemap"|"funnel"|"waterfall"|"radar"|"heatmap"|"boxplot"|"gauge"|"calendar"|"sankey", ` +
916
+ `{ "type": "bar"|"line"|"area"|"pie"|"treemap"|"funnel"|"waterfall"|"radar"|"heatmap"|"boxplot"|"gauge"|"calendar"|"sankey"|"histogram"|"lollipop"|"pareto"|"sunburst"|"radial-bar"|"radial-column"|"nightingale"|"bullet"|"chord"|"stream"|"candlestick", ` +
897
917
  `"dimension": "<a categorical column to group by>", ` +
898
918
  `"series": "<a second categorical column to split into series, or null>", ` +
899
919
  `"measure": "<a numeric column to aggregate>", ` +
900
- `"reduce": "sum"|"avg"|"count", ` +
920
+ `"reduce": "sum"|"avg"|"count"|"min"|"max"|"median"|"p90"|"first"|"last"|"countDistinct", ` +
921
+ `"bucket": "day"|"week"|"month"|"quarter"|"year"|null, ` +
901
922
  `"stacked": true|false, ` +
902
923
  `"logScale": true|false, ` +
903
924
  `"timeAxis": true|false, ` +
904
925
  `"valueFormat": "number"|"currency"|"percent", ` +
926
+ `"indicators": ["volume"|"rsi"|"macd"|"stochastic"|"atr"|"obv"|"sma"|"ema"|"bb"|"vwap", ...] (candlestick only, else omit), ` +
905
927
  `"rationale": "<one-sentence explanation>" }\n\n` +
906
928
  `Rules: pick "dimension" and "series" from text/date columns and "measure" ` +
907
929
  `from a numeric column; use only column names from the list above; ` +
@@ -914,9 +936,17 @@ export async function aiChart<
914
936
  `number vs a target -> gauge (leave "dimension" null); a year of daily ` +
915
937
  `activity -> calendar (dimension must be a date column); flow between two ` +
916
938
  `things -> sankey (needs "series": dimension is the source, series the ` +
917
- `target). ` +
939
+ `target); flows between groups round a circle -> chord (needs "series"); ` +
940
+ `frequency/how many fall in each range -> histogram (leave "dimension" null); ` +
941
+ `vital few/80-20/biggest contributors -> pareto; drilldown/rings/nested share -> sunburst; ` +
942
+ `progress rings/circular -> radial-bar; polar/rose -> nightingale or radial-column; ` +
943
+ `actual vs target per item -> bullet; layered over time/stream -> stream (needs "series"); ` +
944
+ `price/candles/OHLC/stock/ticker -> candlestick (dimension is the date column; the open/high/low/close/volume columns are found by name), ` +
945
+ `with "volume"/"RSI"/"MACD"/"Bollinger"/"moving average" -> indicators. ` +
918
946
  `"stacked"/"stack" -> stacked:true; "log"/"logarithmic" -> logScale:true; ` +
919
947
  `"over time"/"by date"/a date dimension -> timeAxis:true; ` +
948
+ `"monthly"/"per month"/"by month" -> bucket:"month" (likewise daily/weekly/quarterly/yearly), else bucket:null; ` +
949
+ `"median"/"typical" -> reduce:"median"; "highest"/"peak" -> "max"; "lowest" -> "min"; "unique"/"distinct" -> "countDistinct"; ` +
920
950
  `money/revenue/price/$ -> valueFormat:"currency"; rate/%/share -> valueFormat:"percent"; ` +
921
951
  `else valueFormat:"number". default reduce is "sum". ` +
922
952
  `Return JSON only, no prose.\n\n` +
@@ -935,6 +965,8 @@ export async function aiChart<
935
965
  'bar', 'line', 'area', 'pie',
936
966
  'treemap', 'funnel', 'waterfall', 'radar',
937
967
  'heatmap', 'boxplot', 'gauge', 'calendar', 'sankey',
968
+ 'histogram', 'lollipop', 'pareto', 'sunburst', 'radial-bar', 'radial-column', 'nightingale', 'bullet', 'chord', 'stream',
969
+ 'candlestick',
938
970
  ]
939
971
  plan.type = CHART_TYPES.includes(plan.type) ? plan.type : 'bar'
940
972
  plan.dimension = plan.dimension && valid.has(plan.dimension) ? plan.dimension : null
@@ -948,14 +980,28 @@ export async function aiChart<
948
980
  // a chart the data cannot make. Fall back to a bar, which any dimension and
949
981
  // measure can draw, rather than showing nothing and blaming the request.
950
982
  const dateFields = new Set(schema.filter((c) => c.type === 'date').map((c) => c.field))
951
- const needsSplit = plan.type === 'heatmap' || plan.type === 'sankey'
983
+ const needsSplit = plan.type === 'heatmap' || plan.type === 'sankey' || plan.type === 'chord' || plan.type === 'stream'
952
984
  if (needsSplit && !plan.series) plan.type = 'bar'
953
985
  if (plan.type === 'calendar' && !(plan.dimension && dateFields.has(plan.dimension))) plan.type = 'bar'
986
+ // A candlestick needs a date to run along and four prices to draw; the
987
+ // panel finds the price columns by name, so the shape check is the count.
988
+ if (plan.type === 'candlestick' && !(plan.dimension && dateFields.has(plan.dimension) && numericFields.size >= 4)) plan.type = 'bar'
989
+ plan.indicators = plan.type === 'candlestick' && Array.isArray(plan.indicators)
990
+ ? plan.indicators.filter((k): k is AIChartIndicator => AI_INDICATORS.includes(k))
991
+ : undefined
992
+ if (plan.indicators === undefined) delete plan.indicators
954
993
  // A gauge is one number: it has no category axis, so a dimension is noise.
955
- if (plan.type === 'gauge') plan.dimension = null
956
- if (plan.type !== 'gauge' && !plan.dimension) plan.type = 'bar'
957
-
958
- plan.reduce = ['sum', 'avg', 'count'].includes(plan.reduce) ? plan.reduce : 'sum'
994
+ // A histogram bins the measure itself, so a dimension is noise there too.
995
+ if (plan.type === 'gauge' || plan.type === 'histogram') plan.dimension = null
996
+ if (plan.type !== 'gauge' && plan.type !== 'histogram' && !plan.dimension) plan.type = 'bar'
997
+
998
+ plan.reduce = AI_REDUCERS.includes(plan.reduce) ? plan.reduce : 'sum'
999
+ // A bucket only means something on a date dimension.
1000
+ const BUCKETS = ['day', 'week', 'month', 'quarter', 'year'] as const
1001
+ plan.bucket =
1002
+ plan.bucket && (BUCKETS as ReadonlyArray<string>).includes(plan.bucket) && plan.dimension && dateFields.has(plan.dimension)
1003
+ ? plan.bucket
1004
+ : null
959
1005
  plan.stacked = plan.stacked === true
960
1006
  plan.logScale = plan.logScale === true
961
1007
  plan.timeAxis = plan.timeAxis === true
@@ -975,12 +1021,61 @@ export async function aiChart<
975
1021
  logScale: plan.logScale,
976
1022
  timeAxis: plan.timeAxis,
977
1023
  valueFormat: plan.valueFormat,
1024
+ ...(plan.indicators ? { indicators: plan.indicators } : {}),
1025
+ bucket: plan.bucket,
978
1026
  })
1027
+ // The panel built a spec from the plan; if it cannot draw as intended,
1028
+ // say so on the plan rather than showing an empty frame in silence.
1029
+ const spec = (api as unknown as { getChartSpec?: () => unknown }).getChartSpec?.()
1030
+ if (spec) plan.diagnostics = validateChartSpec(spec).filter((d) => d.severity === 'error')
979
1031
  }
980
1032
 
981
1033
  return plan
982
1034
  }
983
1035
 
1036
+ /** What `aiExplainChart` returns: the grounded summary and the model's insights. */
1037
+ export type AIChartExplanation = {
1038
+ /** The plain-language reading of the chart (`chartSummary`), as the model saw it. */
1039
+ summary: string
1040
+ /** Two to three observations the model added: comparisons, outliers, what to look at next. */
1041
+ insights: string[]
1042
+ }
1043
+
1044
+ /** Options for `aiExplainChart`. */
1045
+ export type AIExplainChartOptions = {
1046
+ /** The spec to explain. Default: the grid panel's active chart. */
1047
+ spec?: ChartSpec
1048
+ signal?: AbortSignal
1049
+ }
1050
+
1051
+ /**
1052
+ * Explain a chart in plain words. The prompt is grounded on `chartSummary`
1053
+ * (the same sentence a screen reader hears) plus a compact table of the
1054
+ * first 50 categories, so the model comments on numbers it was given rather
1055
+ * than on an image. Returns the summary and up to three insights.
1056
+ */
1057
+ export async function aiExplainChart<
1058
+ TFeatures extends TableFeatures,
1059
+ TData extends RowData,
1060
+ >(api: SvGridApi<TFeatures, TData>, opts: AIExplainChartOptions = {}): Promise<AIChartExplanation> {
1061
+ const spec = opts.spec ?? (api as unknown as { getChartSpec?: () => ChartSpec | null }).getChartSpec?.() ?? null
1062
+ if (!spec) return { summary: 'No chart to explain.', insights: [] }
1063
+ const summary = chartSummary(spec)
1064
+ const cats = spec.categories.slice(0, 50)
1065
+ const table = cats.length
1066
+ ? `${['category', ...spec.series.map((s) => s.label)].join('\t')}\n` +
1067
+ cats.map((c, i) => [c, ...spec.series.map((s) => String(s.values[i] ?? ''))].join('\t')).join('\n')
1068
+ : ''
1069
+ const prompt =
1070
+ `You are explaining a ${spec.type} chart to a busy reader.\n\n` +
1071
+ `Reading of the chart: ${summary}\n\n` +
1072
+ (table ? `Data (first ${cats.length} categories, tab separated):\n${table}\n\n` : '') +
1073
+ `Return strict JSON only: { "insights": ["<2-3 short observations a reader would miss at a glance: comparisons, outliers, what to look at next>"] }`
1074
+ const out = await callJSON<{ insights?: unknown }>({ prompt, task: 'explain-chart', signal: opts.signal, maxOutputTokens: 300 })
1075
+ const insights = Array.isArray(out.insights) ? out.insights.filter((x): x is string => typeof x === 'string').slice(0, 3) : []
1076
+ return { summary, insights }
1077
+ }
1078
+
984
1079
  /**
985
1080
  * Wire the grid's chart-panel AI button to the model: registers a handler (via
986
1081
  * `api.setChartAiHandler`) that runs `aiChart` and returns the plan for the
@@ -1008,9 +1103,13 @@ export function enableAiCharting<
1008
1103
  logScale: plan.logScale,
1009
1104
  timeAxis: plan.timeAxis,
1010
1105
  valueFormat: plan.valueFormat,
1106
+ bucket: plan.bucket,
1011
1107
  rationale: plan.rationale,
1012
1108
  }
1013
1109
  })
1110
+ // And the Explain button beside it.
1111
+ const explain = api as unknown as { setChartExplainHandler?: (fn: (() => Promise<AIChartExplanation | null>) | null) => void }
1112
+ explain.setChartExplainHandler?.(() => aiExplainChart(api))
1014
1113
  }
1015
1114
 
1016
1115
  /**
@@ -1021,8 +1120,9 @@ export function disableAiCharting<
1021
1120
  TFeatures extends TableFeatures,
1022
1121
  TData extends RowData,
1023
1122
  >(api: SvGridApi<TFeatures, TData>): void {
1024
- const hook = api as unknown as { setChartAiHandler?: (fn: null) => void }
1123
+ const hook = api as unknown as { setChartAiHandler?: (fn: null) => void; setChartExplainHandler?: (fn: null) => void }
1025
1124
  hook.setChartAiHandler?.(null)
1125
+ hook.setChartExplainHandler?.(null)
1026
1126
  }
1027
1127
 
1028
1128
  // ---------------------------------------------------------------------------
@@ -1060,6 +1160,17 @@ export const mockAIProvider: AIProvider = async (req) => {
1060
1160
  if (req.task === 'chart') {
1061
1161
  return JSON.stringify(buildMockChart(req.prompt))
1062
1162
  }
1163
+ if (req.task === 'explain-chart') {
1164
+ // Two canned observations round the reading the prompt was grounded on.
1165
+ const reading = /Reading of the chart: ([^\n]+)/.exec(req.prompt)?.[1] ?? ''
1166
+ const rises = /rises/.test(reading)
1167
+ return JSON.stringify({
1168
+ insights: [
1169
+ rises ? 'The growth is front-loaded: the early categories carry most of the change.' : 'The movement is gradual with no single category driving it.',
1170
+ 'Compare the peak against the same category a period earlier before reading it as a trend.',
1171
+ ],
1172
+ })
1173
+ }
1063
1174
  return '{}'
1064
1175
  }
1065
1176
 
@@ -1073,7 +1184,18 @@ function buildMockChart(prompt: string): AIChartPlan {
1073
1184
  // can reach the same charts a model can. Ordered most-specific first: "share
1074
1185
  // of the pipeline" is a funnel request, not a pie one.
1075
1186
  const type: AIChartType =
1076
- /\bfunnel\b|drop.?off|conversion|\bstages?\b|pipeline/.test(q) ? 'funnel'
1187
+ /\bcandle|\bohlc\b|stock price|price chart|\bticker\b/.test(q) ? 'candlestick'
1188
+ : /\bhistogram\b|frequenc|how many fall|bucket(?:ed)? by value/.test(q) ? 'histogram'
1189
+ : /\bpareto\b|80.?20|vital few|biggest contributors/.test(q) ? 'pareto'
1190
+ : /\bsunburst\b|\brings?\b|drill.?down/.test(q) ? 'sunburst'
1191
+ : /\bchord\b/.test(q) ? 'chord'
1192
+ : /\bstream\b|layered over time/.test(q) ? 'stream'
1193
+ : /\bbullet\b|actual vs target|against (?:a |the )?target per/.test(q) ? 'bullet'
1194
+ : /\bnightingale\b|\brose\b|\bpolar\b/.test(q) ? 'nightingale'
1195
+ : /radial (?:bar|progress)|progress rings?|circular progress/.test(q) ? 'radial-bar'
1196
+ : /radial column/.test(q) ? 'radial-column'
1197
+ : /\blollipop\b/.test(q) ? 'lollipop'
1198
+ : /\bfunnel\b|drop.?off|conversion|\bstages?\b|pipeline/.test(q) ? 'funnel'
1077
1199
  : /\bwaterfall\b|running total|contribution|\bbridge\b/.test(q) ? 'waterfall'
1078
1200
  : /\btree ?map\b|nested|hierarch/.test(q) ? 'treemap'
1079
1201
  : /\bradar\b|spider|multi.?measure/.test(q) ? 'radar'
@@ -1100,12 +1222,24 @@ function buildMockChart(prompt: string): AIChartPlan {
1100
1222
  // check downgrades them to a bar, so the mock would answer "heatmap" with a
1101
1223
  // bar chart and look broken rather than offline.
1102
1224
  const series =
1103
- /\b(split|stack(?:ed)?|by product|by category|per)\b/.test(q) || type === 'heatmap' || type === 'sankey'
1225
+ /\b(split|stack(?:ed)?|by product|by category|per)\b/.test(q) || type === 'heatmap' || type === 'sankey' || type === 'chord' || type === 'stream'
1104
1226
  ? (textFields.find((t) => t !== dimension) ?? null)
1105
1227
  : null
1106
1228
  const measure = numberFields.find((n) => q.includes(n.toLowerCase())) ?? numberFields[0] ?? null
1107
- const reduce: 'sum' | 'avg' | 'count' =
1108
- /\b(average|avg|mean)\b/.test(q) ? 'avg' : /\b(count|number of|how many)\b/.test(q) ? 'count' : 'sum'
1229
+ const reduce: AIChartReduce =
1230
+ /\b(average|avg|mean)\b/.test(q) ? 'avg'
1231
+ : /\b(median|typical)\b/.test(q) ? 'median'
1232
+ : /\b(highest|peak|max(?:imum)?)\b/.test(q) ? 'max'
1233
+ : /\b(lowest|min(?:imum)?)\b/.test(q) ? 'min'
1234
+ : /\b(unique|distinct)\b/.test(q) ? 'countDistinct'
1235
+ : /\b(count|number of|how many)\b/.test(q) ? 'count' : 'sum'
1236
+ const bucket: AIChartPlan['bucket'] =
1237
+ /\b(monthly|per month|by month|each month)\b/.test(q) ? 'month'
1238
+ : /\b(weekly|per week|by week|each week)\b/.test(q) ? 'week'
1239
+ : /\b(daily|per day|by day|each day)\b/.test(q) ? 'day'
1240
+ : /\b(quarterly|per quarter|by quarter)\b/.test(q) ? 'quarter'
1241
+ : /\b(yearly|annually|per year|by year)\b/.test(q) ? 'year'
1242
+ : null
1109
1243
  const stacked = /\bstack(ed)?\b/.test(q)
1110
1244
  const logScale = /\b(log|logarithmic)\b/.test(q)
1111
1245
  const timeAxis = /\b(over time|by date|by day|by month|by week|time series|timeline|trend)\b/.test(q)
@@ -1116,12 +1250,22 @@ function buildMockChart(prompt: string): AIChartPlan {
1116
1250
  ? 'percent'
1117
1251
  : 'number'
1118
1252
 
1253
+ // A candlestick runs along a DATE, and the indicators are named in the request.
1254
+ const dateField = typed.find((f) => f.type === 'date')?.name ?? null
1255
+ const indicators: AIChartIndicator[] | undefined = type === 'candlestick'
1256
+ ? ([
1257
+ /\bvolume\b/.test(q) && 'volume', /\brsi\b|relative strength/.test(q) && 'rsi', /\bmacd\b/.test(q) && 'macd',
1258
+ /\bstochastic\b/.test(q) && 'stochastic', /\batr\b|true range/.test(q) && 'atr', /\bobv\b|on.balance/.test(q) && 'obv',
1259
+ /bollinger|\bbands?\b/.test(q) && 'bb', /\bvwap\b/.test(q) && 'vwap', /\bema\b|exponential/.test(q) && 'ema', /\bsma\b|moving average/.test(q) && 'sma',
1260
+ ].filter(Boolean) as AIChartIndicator[])
1261
+ : undefined
1119
1262
  return {
1120
- type, dimension, series, measure, reduce, stacked, logScale, timeAxis, valueFormat,
1263
+ type, dimension: type === 'candlestick' ? (dateField ?? dimension) : dimension, series, measure, reduce, stacked, logScale, timeAxis, valueFormat, bucket,
1264
+ ...(indicators?.length ? { indicators } : {}),
1121
1265
  rationale:
1122
1266
  `${type} chart of ${reduce}(${measure ?? 'value'}) by ${dimension ?? 'category'}` +
1123
1267
  `${series ? `, split by ${series}` : ''}${stacked ? ', stacked' : ''}` +
1124
- `${logScale ? ', log scale' : ''}${timeAxis ? ', date axis' : ''} (mock - wire a real model for genuine parsing).`,
1268
+ `${logScale ? ', log scale' : ''}${timeAxis ? ', date axis' : ''}${bucket ? `, by ${bucket}` : ''} (mock - wire a real model for genuine parsing).`,
1125
1269
  }
1126
1270
  }
1127
1271
 
package/src/aria.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ // `aria-description` (ARIA 1.3) is what the chart uses to hand a screen
2
+ // reader its plain-language summary. Svelte's element typings predate it,
3
+ // so this adds the attribute to every element's ARIA set rather than casting
4
+ // at the one use site.
5
+ import 'svelte/elements'
6
+
7
+ declare module 'svelte/elements' {
8
+ interface AriaAttributes {
9
+ 'aria-description'?: string | null | undefined
10
+ }
11
+ }
@@ -177,6 +177,190 @@ describe("SvGridApi - cell selection", () => {
177
177
  });
178
178
  });
179
179
 
180
+ describe("SvGridApi - Handsontable's selection readers", () => {
181
+ it("getSelectedLast keeps the orientation the selection was made in", async () => {
182
+ const { api, destroy } = await mountGrid();
183
+ try {
184
+ // Programmatic ranges are normalised on the way in, so the orientation
185
+ // has to come from a gesture: anchor at (2,1), then extend up-left.
186
+ api.selectCells([[2, 1, 2, 1]]);
187
+ await flush();
188
+ api.getCommandContext().extendSelection(0, 0);
189
+ await flush();
190
+ expect(api.getSelected()).toEqual([[0, 0, 2, 1]]);
191
+ expect(api.getSelectedLast()).toEqual([2, 1, 0, 0]);
192
+ const last = api.getSelectedRangeLast();
193
+ expect(last?.from).toEqual({ row: 2, col: 1 });
194
+ expect(last?.to).toEqual({ row: 0, col: 0 });
195
+ } finally {
196
+ destroy();
197
+ }
198
+ });
199
+
200
+ it("getSelectedRange lists every rectangle oldest first, the active cell as its highlight", async () => {
201
+ const { api, destroy } = await mountGrid();
202
+ try {
203
+ api.selectCells([[0, 0, 1, 1], [3, 2, 4, 2]]);
204
+ await flush();
205
+ const ranges = api.getSelectedRange();
206
+ expect(ranges?.map((r) => [r.from, r.to])).toEqual([
207
+ [{ row: 0, col: 0 }, { row: 1, col: 1 }],
208
+ [{ row: 3, col: 2 }, { row: 4, col: 2 }],
209
+ ]);
210
+ // The active cell sits in the last range and highlights only that one.
211
+ expect(ranges?.[1]?.highlight).toEqual({ row: 3, col: 2 });
212
+ expect(ranges?.[0]?.highlight).toEqual({ row: 0, col: 0 });
213
+ } finally {
214
+ destroy();
215
+ }
216
+ });
217
+
218
+ it("returns undefined, as Handsontable does, when nothing is selected", async () => {
219
+ const { api, destroy } = await mountGrid();
220
+ try {
221
+ expect(api.getSelectedLast()).toBeUndefined();
222
+ expect(api.getSelectedRange()).toBeUndefined();
223
+ expect(api.getSelectedRangeLast()).toBeUndefined();
224
+ } finally {
225
+ destroy();
226
+ }
227
+ });
228
+
229
+ it("reports nothing selected when cell selection is off, whatever cell has focus", async () => {
230
+ // selectable={false} is observable through getSelected() staying [];
231
+ // the focused cell is getActiveCell()'s business, not a selection.
232
+ const { api, destroy } = await mountGrid({ enableCellSelection: false });
233
+ try {
234
+ api.getCommandContext().setActiveCell(1, 2);
235
+ await flush();
236
+ expect(api.getSelected()).toEqual([]);
237
+ expect(api.getSelectedLast()).toBeUndefined();
238
+ expect(api.getSelectedRange()).toBeUndefined();
239
+ expect(api.getSelectedRangeLast()).toBeUndefined();
240
+ } finally {
241
+ destroy();
242
+ }
243
+ });
244
+ });
245
+
246
+ describe("SvGridApi - row heights", () => {
247
+ it("getRowHeight reads the declared height until one is set", async () => {
248
+ const { api, destroy } = await mountGrid({ rowHeight: 24 });
249
+ try {
250
+ expect(api.getRowHeight(2)).toBe(24);
251
+ api.setRowHeight(2, 52);
252
+ await flush();
253
+ expect(api.getRowHeight(2)).toBe(52);
254
+ expect(api.getRowHeight(1)).toBe(24);
255
+ api.setRowHeight(2, null);
256
+ await flush();
257
+ expect(api.getRowHeight(2)).toBe(24);
258
+ } finally {
259
+ destroy();
260
+ }
261
+ });
262
+
263
+ it("a height belongs to its row: it follows a sort and survives new data", async () => {
264
+ // Keyed by index, a resized row snapped back on every data change and
265
+ // the height stayed in the slot when the rows were sorted.
266
+ const { api, destroy } = await mountGrid({ rowHeight: 24 });
267
+ try {
268
+ api.setRowHeight(0, 60); // Ada
269
+ await flush();
270
+ api.setSort("name", "asc");
271
+ await flush();
272
+ const rows = api.getDisplayedRows() as Row[];
273
+ const ada = rows.findIndex((r) => r.name === "Ada Lovelace");
274
+ expect(ada).toBeGreaterThan(-1);
275
+ expect(api.getRowHeight(ada)).toBe(60);
276
+ for (let i = 0; i < rows.length; i += 1) if (i !== ada) expect(api.getRowHeight(i)).toBe(24);
277
+
278
+ // A data change: a row added on top shifts every index by one.
279
+ api.addRow({ id: 9, name: "Aaron", team: "QA", salary: 1 } as Row, "top");
280
+ await flush();
281
+ const again = (api.getDisplayedRows() as Row[]).findIndex((r) => r.name === "Ada Lovelace");
282
+ expect(again).toBe(ada + 1);
283
+ expect(api.getRowHeight(again)).toBe(60);
284
+ expect(api.getRowHeight(ada)).toBe(24);
285
+ } finally {
286
+ destroy();
287
+ }
288
+ });
289
+ });
290
+
291
+ describe("SvGridApi - collapsed columns and rows", () => {
292
+ it("a collapsed column keeps its index and its width and takes no room", async () => {
293
+ const { api, target, destroy } = await mountGrid();
294
+ try {
295
+ expect(api.isColumnCollapsed("team")).toBe(false);
296
+ api.setColumnCollapsed("team", true);
297
+ await flush();
298
+ expect(api.isColumnCollapsed("team")).toBe(true);
299
+ // Still the second column: nothing after it shifted.
300
+ expect(api.getCommandContext().columnIdAt(1)).toBe("team");
301
+ expect(api.getCommandContext().columnIdAt(2)).toBe("salary");
302
+ expect(api.getColumnWidths().team).toBe(0);
303
+ const th = target.querySelector('[data-svgrid-header-col="team"]')!;
304
+ expect(th.classList.contains("sv-grid-column-collapsed")).toBe(true);
305
+ expect(target.querySelectorAll("td.sv-grid-cell-collapsed").length).toBe(baseRows.length);
306
+ api.setColumnCollapsed("team", false);
307
+ await flush();
308
+ expect(api.getColumnWidths().team).toBe(160);
309
+ expect(target.querySelectorAll("td.sv-grid-cell-collapsed").length).toBe(0);
310
+ } finally {
311
+ destroy();
312
+ }
313
+ });
314
+
315
+ it("a collapsed row is 0 high, leaves the flow, and follows its row through a sort", async () => {
316
+ const { api, target, destroy } = await mountGrid({ rowHeight: 24 });
317
+ try {
318
+ api.setRowCollapsed(0, true); // Ada
319
+ await flush();
320
+ expect(api.isRowCollapsed(0)).toBe(true);
321
+ expect(api.getRowHeight(0)).toBe(0);
322
+ expect(target.querySelectorAll("tr.sv-grid-row-collapsed").length).toBe(1);
323
+ api.setSort("name", "desc");
324
+ await flush();
325
+ const rows = api.getDisplayedRows() as Row[];
326
+ const ada = rows.findIndex((r) => r.name === "Ada Lovelace");
327
+ expect(api.isRowCollapsed(ada)).toBe(true);
328
+ expect(api.isRowCollapsed(0)).toBe(false);
329
+ api.setRowCollapsed(ada, false);
330
+ await flush();
331
+ expect(api.getRowHeight(ada)).toBe(24);
332
+ expect(target.querySelectorAll("tr.sv-grid-row-collapsed").length).toBe(0);
333
+ } finally {
334
+ destroy();
335
+ }
336
+ });
337
+
338
+ it("the arrow keys step over collapsed rows and columns", async () => {
339
+ const { api, target, destroy } = await mountGrid();
340
+ try {
341
+ api.setColumnCollapsed("team", true);
342
+ api.setRowCollapsed(1, true);
343
+ await flush();
344
+ const cmd = api.getCommandContext();
345
+ cmd.setActiveCell(0, 0);
346
+ await flush();
347
+ const root = target.querySelector<HTMLElement>("table.sv-grid-table")!;
348
+ root.focus();
349
+ root.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true, cancelable: true }));
350
+ await flush();
351
+ expect(api.getActiveCell()).toMatchObject({ rowIndex: 0, colIndex: 2 });
352
+ root.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }));
353
+ await flush();
354
+ expect(api.getActiveCell()).toMatchObject({ rowIndex: 2, colIndex: 2 });
355
+ root.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true, cancelable: true }));
356
+ await flush();
357
+ expect(api.getActiveCell()).toMatchObject({ rowIndex: 2, colIndex: 0 });
358
+ } finally {
359
+ destroy();
360
+ }
361
+ });
362
+ });
363
+
180
364
  describe("SvGridApi - row mutations", () => {
181
365
  it("addRow appends to the bottom by default", async () => {
182
366
  const { api, destroy } = await mountGrid();