@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.
- package/README.md +2 -1
- package/dist/GridMenus.svelte +44 -2
- package/dist/SvChartMenu.svelte +69 -0
- package/dist/SvChartMenu.svelte.d.ts +11 -0
- package/dist/SvChartPanes.svelte +113 -0
- package/dist/SvChartPanes.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.d.ts +110 -39
- package/dist/SvGrid.controller.svelte.js +655 -200
- package/dist/SvGrid.css +105 -2
- package/dist/SvGrid.svelte +386 -514
- package/dist/SvGrid.types.d.ts +210 -9
- package/dist/SvGridCellEditor.svelte +17 -0
- package/dist/SvGridChart.svelte +2257 -329
- package/dist/SvGridChart.svelte.d.ts +6 -106
- package/dist/SvGridChart.types.d.ts +338 -0
- package/dist/SvGridChart.types.js +1 -0
- package/dist/SvGridChartBuilder.svelte +323 -0
- package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
- package/dist/SvGridChartPanel.svelte +221 -275
- package/dist/SvGridChartPickers.svelte +417 -0
- package/dist/SvGridChartPickers.svelte.d.ts +70 -0
- package/dist/SvGridChartView.svelte +13 -14
- package/dist/SvModal.svelte +7 -1
- package/dist/SvModal.svelte.d.ts +5 -0
- package/dist/ai.d.ts +40 -8
- package/dist/ai.js +135 -24
- package/dist/aria.d.ts +11 -0
- package/dist/build-api.js +108 -22
- package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
- package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
- package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
- package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
- package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
- package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
- package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
- package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
- package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
- package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
- package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
- package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
- package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
- package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
- package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
- package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
- package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
- package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
- package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
- package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
- package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
- package/dist/cdn/SvModal-CIZWCcad.js +397 -0
- package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
- package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
- package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
- package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
- package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
- package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
- package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
- package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
- package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
- package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
- package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
- package/dist/cdn/dismissable-DAHetSNk.js +44 -0
- package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
- package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
- package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
- package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
- package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
- package/dist/cdn/svgrid.js +26 -15
- package/dist/cdn/svgrid.svelte-external.js +26 -15
- package/dist/chart-axes.d.ts +53 -0
- package/dist/chart-axes.js +351 -0
- package/dist/chart-cartesian.d.ts +88 -0
- package/dist/chart-cartesian.js +1862 -0
- package/dist/chart-decimate.d.ts +51 -0
- package/dist/chart-decimate.js +199 -0
- package/dist/chart-export-pdf.d.ts +47 -0
- package/dist/chart-export-pdf.js +187 -0
- package/dist/chart-export.d.ts +2 -1
- package/dist/chart-export.js +18 -3
- package/dist/chart-financial.d.ts +91 -0
- package/dist/chart-financial.js +175 -0
- package/dist/chart-flow.d.ts +10 -0
- package/dist/chart-flow.js +298 -0
- package/dist/chart-format.d.ts +18 -0
- package/dist/chart-format.js +65 -0
- package/dist/chart-grid.d.ts +8 -0
- package/dist/chart-grid.js +190 -0
- package/dist/chart-hierarchy.d.ts +17 -0
- package/dist/chart-hierarchy.js +146 -0
- package/dist/chart-indicators.d.ts +115 -0
- package/dist/chart-indicators.js +389 -0
- package/dist/chart-messages.d.ts +95 -0
- package/dist/chart-messages.js +92 -0
- package/dist/chart-motion.d.ts +16 -0
- package/dist/chart-motion.js +141 -0
- package/dist/chart-panel-messages.d.ts +265 -0
- package/dist/chart-panel-messages.js +285 -0
- package/dist/chart-pivot.d.ts +87 -0
- package/dist/chart-pivot.js +156 -0
- package/dist/chart-polar.d.ts +25 -0
- package/dist/chart-polar.js +700 -0
- package/dist/chart-samples.d.ts +16 -0
- package/dist/chart-samples.js +153 -0
- package/dist/chart-scale.d.ts +113 -0
- package/dist/chart-scale.js +405 -0
- package/dist/chart-stats.d.ts +111 -0
- package/dist/chart-stats.js +419 -0
- package/dist/chart-stream.d.ts +45 -0
- package/dist/chart-stream.js +91 -0
- package/dist/chart-summary.d.ts +27 -0
- package/dist/chart-summary.js +205 -0
- package/dist/chart-sync.svelte.d.ts +32 -0
- package/dist/chart-sync.svelte.js +23 -0
- package/dist/chart-table.d.ts +35 -0
- package/dist/chart-table.js +165 -0
- package/dist/chart-trend.d.ts +19 -0
- package/dist/chart-trend.js +61 -0
- package/dist/chart-types.d.ts +1456 -0
- package/dist/chart-types.js +6 -0
- package/dist/chart-validate.d.ts +43 -0
- package/dist/chart-validate.js +272 -0
- package/dist/chart-zoom.d.ts +40 -0
- package/dist/chart-zoom.js +145 -0
- package/dist/chart.d.ts +186 -865
- package/dist/chart.js +460 -2204
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +137 -18
- package/dist/column-resize.d.ts +3 -0
- package/dist/column-resize.js +4 -1
- package/dist/columns.js +3 -0
- package/dist/command-context.d.ts +19 -0
- package/dist/command-context.js +116 -0
- package/dist/core.d.ts +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.d.ts +2 -8
- package/dist/editing.js +262 -33
- package/dist/fill-patterns.d.ts +0 -5
- package/dist/fill-patterns.js +56 -2
- package/dist/grid-messages.d.ts +10 -3
- package/dist/grid-messages.js +5 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +133 -0
- package/dist/index.d.ts +24 -6
- package/dist/index.js +20 -2
- package/dist/keyboard-handlers.js +88 -31
- package/dist/keyboard.d.ts +55 -0
- package/dist/keyboard.js +132 -0
- package/dist/menus.d.ts +3 -1
- package/dist/menus.js +7 -2
- package/dist/merges.d.ts +97 -0
- package/dist/merges.js +147 -0
- package/dist/row-resize.d.ts +3 -0
- package/dist/row-resize.js +14 -0
- package/dist/scroll-sync.d.ts +1 -0
- package/dist/scroll-sync.js +24 -0
- package/dist/selection.d.ts +16 -0
- package/dist/selection.js +108 -16
- package/dist/shortcut-registry.d.ts +145 -0
- package/dist/shortcut-registry.js +44 -0
- package/dist/svgrid-wrapper.types.d.ts +213 -22
- package/dist/virtualization/virtualizer.js +38 -17
- package/package.json +6 -1
- package/src/GridMenus.svelte +44 -2
- package/src/SvChartMenu.svelte +69 -0
- package/src/SvChartPanes.svelte +113 -0
- package/src/SvChartPanes.test.ts +66 -0
- package/src/SvGrid.controller.svelte.ts +629 -172
- package/src/SvGrid.css +105 -2
- package/src/SvGrid.svelte +386 -514
- package/src/SvGrid.types.ts +188 -8
- package/src/SvGridCellEditor.svelte +17 -0
- package/src/SvGridChart.svelte +2257 -329
- package/src/SvGridChart.test.ts +1419 -1
- package/src/SvGridChart.types.ts +336 -0
- package/src/SvGridChartBuilder.svelte +323 -0
- package/src/SvGridChartPanel.svelte +221 -275
- package/src/SvGridChartPickers.svelte +417 -0
- package/src/SvGridChartView.svelte +13 -14
- package/src/SvModal.svelte +7 -1
- package/src/SvModal.test.ts +9 -0
- package/src/ai.test.ts +57 -2
- package/src/ai.ts +166 -22
- package/src/aria.d.ts +11 -0
- package/src/build-api.coverage.test.ts +184 -0
- package/src/build-api.ts +107 -18
- package/src/chart-axes.test.ts +573 -0
- package/src/chart-axes.ts +342 -0
- package/src/chart-cartesian.ts +1863 -0
- package/src/chart-decimate.test.ts +249 -0
- package/src/chart-decimate.ts +186 -0
- package/src/chart-export-pdf.test.ts +91 -0
- package/src/chart-export-pdf.ts +207 -0
- package/src/chart-export.test.ts +14 -0
- package/src/chart-export.ts +13 -3
- package/src/chart-financial.ts +208 -0
- package/src/chart-flow.ts +286 -0
- package/src/chart-format.ts +67 -0
- package/src/chart-grid.ts +179 -0
- package/src/chart-hierarchy.ts +135 -0
- package/src/chart-indicators.test.ts +443 -0
- package/src/chart-indicators.ts +384 -0
- package/src/chart-messages.test.ts +21 -0
- package/src/chart-messages.ts +184 -0
- package/src/chart-motion.test.ts +71 -0
- package/src/chart-motion.ts +148 -0
- package/src/chart-panel-messages.test.ts +29 -0
- package/src/chart-panel-messages.ts +550 -0
- package/src/chart-pivot.test.ts +54 -0
- package/src/chart-pivot.ts +225 -0
- package/src/chart-polar.ts +674 -0
- package/src/chart-samples.ts +106 -0
- package/src/chart-scale.ts +394 -0
- package/src/chart-series-types.test.ts +714 -0
- package/src/chart-stats.ts +409 -0
- package/src/chart-stream.test.ts +75 -0
- package/src/chart-stream.ts +114 -0
- package/src/chart-summary.test.ts +90 -0
- package/src/chart-summary.ts +197 -0
- package/src/chart-sync.svelte.ts +41 -0
- package/src/chart-table.test.ts +76 -0
- package/src/chart-table.ts +178 -0
- package/src/chart-trend.ts +47 -0
- package/src/chart-types.ts +1309 -0
- package/src/chart-validate.test.ts +100 -0
- package/src/chart-validate.ts +247 -0
- package/src/chart-zoom.test.ts +137 -0
- package/src/chart-zoom.ts +124 -0
- package/src/chart.coverage.test.ts +81 -0
- package/src/chart.test.ts +56 -3
- package/src/chart.ts +557 -2784
- package/src/clipboard.test.ts +151 -0
- package/src/clipboard.ts +145 -16
- package/src/column-resize.ts +7 -1
- package/src/columns.ts +2 -0
- package/src/command-context.test.ts +200 -0
- package/src/command-context.ts +137 -0
- package/src/core.ts +7 -0
- package/src/editing.test.ts +281 -3
- package/src/editing.ts +256 -38
- package/src/fill-patterns.test.ts +35 -0
- package/src/fill-patterns.ts +56 -2
- package/src/grid-messages.ts +16 -3
- package/src/history.test.ts +196 -0
- package/src/history.ts +162 -0
- package/src/icon-seam.test.ts +3 -2
- package/src/index.ts +150 -2
- package/src/keyboard-handlers.coverage.test.ts +126 -6
- package/src/keyboard-handlers.ts +91 -30
- package/src/keyboard-shortcuts.seam.test.ts +200 -0
- package/src/keyboard.test.ts +124 -1
- package/src/keyboard.ts +142 -0
- package/src/menus.test.ts +26 -0
- package/src/menus.ts +10 -3
- package/src/merges.test.ts +112 -0
- package/src/merges.ts +168 -0
- package/src/pivot.grid.test.ts +40 -5
- package/src/row-resize.test.ts +44 -0
- package/src/row-resize.ts +17 -0
- package/src/scroll-sync.test.ts +42 -1
- package/src/scroll-sync.ts +25 -0
- package/src/selection.multi-range.test.ts +34 -0
- package/src/selection.test.ts +30 -2
- package/src/selection.ts +111 -19
- package/src/shortcut-registry.test.ts +97 -0
- package/src/shortcut-registry.ts +181 -0
- package/src/svgrid-wrapper.types.ts +184 -13
- package/src/svgrid.behavior.test.ts +135 -0
- package/src/svgrid.charting.test.ts +513 -3
- package/src/svgrid.context-menu.test.ts +31 -0
- package/src/svgrid.new-features.wrapper.test.ts +10 -3
- package/src/virtualization/virtualizer.ts +39 -17
- package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
- package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
- package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
- package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
- package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
- package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
- package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
- package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
- 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.
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
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:
|
|
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
|
-
|
|
956
|
-
if (plan.type
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
/\
|
|
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:
|
|
1108
|
-
/\b(average|avg|mean)\b/.test(q) ? 'avg'
|
|
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();
|