@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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* The chart panel's pickers: type, the columns a chart reads, the
|
|
4
|
+
* aggregate, and the per-type switches (stacked, horizontal, donut, log
|
|
5
|
+
* scale, labels, bucket, date axis, bins, funnel shape, candle style,
|
|
6
|
+
* indicator chips), plus the sentence that says why the chosen type cannot
|
|
7
|
+
* draw yet. One component rendered twice: as a row under the panel head,
|
|
8
|
+
* and as a form on the builder's Data tab, so the two never disagree about
|
|
9
|
+
* what a type needs.
|
|
10
|
+
*
|
|
11
|
+
* Every label comes from `GridChartPanelMessages`, so the panel and the
|
|
12
|
+
* builder localize together through `localization.text`.
|
|
13
|
+
*/
|
|
14
|
+
import type { ChartReducer, ChartType } from "./chart-types";
|
|
15
|
+
import type { ChartPanelIndicator } from "./chart-financial";
|
|
16
|
+
import type { ChartTimeBucket } from "./chart-stats";
|
|
17
|
+
import type { GridChartPanelMessages } from "./chart-panel-messages";
|
|
18
|
+
|
|
19
|
+
/** What the type picker knows about a chart type. */
|
|
20
|
+
export type ChartPanelTypeDef = { value: ChartType; label: string; group: string };
|
|
21
|
+
type Shape = { dims: unknown[]; measures: unknown[]; dates: unknown[] };
|
|
22
|
+
type TypeSeed = { value: ChartType; label: keyof GridChartPanelMessages; group: keyof GridChartPanelMessages; needs?: (c: Shape) => boolean };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The chart types the picker offers, grouped by what they are FOR.
|
|
26
|
+
*
|
|
27
|
+
* `needs` gates on COLUMN SHAPE only, never on the data. Column shape is
|
|
28
|
+
* stable while the user filters; row counts are not, and a picker whose
|
|
29
|
+
* options vanish mid-session because a filter emptied a group is worse than
|
|
30
|
+
* one that explains itself. Data-level problems get a sentence instead, see
|
|
31
|
+
* `typeIssue` below.
|
|
32
|
+
*/
|
|
33
|
+
const TYPES: TypeSeed[] = [
|
|
34
|
+
{ value: "bar", label: "chartTypeBar", group: "chartGroupCompare" },
|
|
35
|
+
{ value: "line", label: "chartTypeLine", group: "chartGroupCompare" },
|
|
36
|
+
{ value: "area", label: "chartTypeArea", group: "chartGroupCompare" },
|
|
37
|
+
{ value: "lollipop", label: "chartTypeLollipop", group: "chartGroupCompare" },
|
|
38
|
+
{ value: "pareto", label: "chartTypePareto", group: "chartGroupCompare" },
|
|
39
|
+
{ value: "range-bar", label: "chartTypeRangeBar", group: "chartGroupCompare", needs: (c) => c.measures.length >= 2 },
|
|
40
|
+
{ value: "range-area", label: "chartTypeRangeArea", group: "chartGroupCompare", needs: (c) => c.measures.length >= 2 },
|
|
41
|
+
{ value: "dumbbell", label: "chartTypeDumbbell", group: "chartGroupCompare", needs: (c) => c.measures.length >= 2 },
|
|
42
|
+
{ value: "radial-column", label: "chartTypeRadialColumn", group: "chartGroupCompare" },
|
|
43
|
+
{ value: "radial-bar", label: "chartTypeRadialBar", group: "chartGroupCompare" },
|
|
44
|
+
{ value: "nightingale", label: "chartTypeNightingale", group: "chartGroupCompare" },
|
|
45
|
+
{ value: "pie", label: "chartTypePie", group: "chartGroupPartOfWhole" },
|
|
46
|
+
{ value: "treemap", label: "chartTypeTreemap", group: "chartGroupPartOfWhole" },
|
|
47
|
+
{ value: "sunburst", label: "chartTypeSunburst", group: "chartGroupPartOfWhole" },
|
|
48
|
+
{ value: "funnel", label: "chartTypeFunnel", group: "chartGroupFlow" },
|
|
49
|
+
{ value: "waterfall", label: "chartTypeWaterfall", group: "chartGroupFlow" },
|
|
50
|
+
{ value: "sankey", label: "chartTypeSankey", group: "chartGroupFlow", needs: (c) => c.dims.length >= 2 },
|
|
51
|
+
{ value: "chord", label: "chartTypeChord", group: "chartGroupFlow", needs: (c) => c.dims.length >= 2 },
|
|
52
|
+
{ value: "radar", label: "chartTypeRadar", group: "chartGroupDistribution" },
|
|
53
|
+
{ value: "heatmap", label: "chartTypeHeatmap", group: "chartGroupDistribution", needs: (c) => c.dims.length >= 2 },
|
|
54
|
+
{ value: "scatter", label: "chartTypeScatter", group: "chartGroupDistribution", needs: (c) => c.measures.length >= 2 },
|
|
55
|
+
{ value: "boxplot", label: "chartTypeBoxplot", group: "chartGroupDistribution" },
|
|
56
|
+
{ value: "histogram", label: "chartTypeHistogram", group: "chartGroupDistribution" },
|
|
57
|
+
{ value: "gauge", label: "chartTypeGauge", group: "chartGroupSingleValue" },
|
|
58
|
+
{ value: "bullet", label: "chartTypeBullet", group: "chartGroupSingleValue" },
|
|
59
|
+
{ value: "calendar", label: "chartTypeCalendar", group: "chartGroupOverTime", needs: (c) => c.dates.length >= 1 },
|
|
60
|
+
{ value: "stream", label: "chartTypeStream", group: "chartGroupOverTime", needs: (c) => c.dims.length >= 2 },
|
|
61
|
+
{ value: "candlestick", label: "chartTypeCandlestick", group: "chartGroupOverTime", needs: (c) => c.dates.length >= 1 && c.measures.length >= 4 },
|
|
62
|
+
{ value: "ohlc", label: "chartTypeOhlc", group: "chartGroupOverTime", needs: (c) => c.dates.length >= 1 && c.measures.length >= 4 },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** The types the panel offers for these columns, labelled in the given language. */
|
|
66
|
+
export function chartPanelTypes(columns: Shape, m: GridChartPanelMessages): ChartPanelTypeDef[] {
|
|
67
|
+
return TYPES.filter((t) => !t.needs || t.needs(columns)).map((t) => ({ value: t.value, label: m[t.label], group: m[t.group] }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The reducers the Aggregate picker offers, in its order. */
|
|
71
|
+
export function chartPanelReducers(m: GridChartPanelMessages): Array<{ value: ChartReducer; label: string }> {
|
|
72
|
+
return [
|
|
73
|
+
{ value: "sum", label: m.chartReduceSum },
|
|
74
|
+
{ value: "avg", label: m.chartReduceAvg },
|
|
75
|
+
{ value: "count", label: m.chartReduceCount },
|
|
76
|
+
{ value: "min", label: m.chartReduceMin },
|
|
77
|
+
{ value: "max", label: m.chartReduceMax },
|
|
78
|
+
{ value: "median", label: m.chartReduceMedian },
|
|
79
|
+
{ value: "p90", label: m.chartReduceP90 },
|
|
80
|
+
{ value: "first", label: m.chartReduceFirst },
|
|
81
|
+
{ value: "last", label: m.chartReduceLast },
|
|
82
|
+
{ value: "countDistinct", label: m.chartReduceCountDistinct },
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Named series palettes. "Theme" defers to --sg-chart-1..8 and the defaults. */
|
|
87
|
+
export const CHART_PANEL_PALETTES: Array<{ key: string; label: keyof GridChartPanelMessages; colors: string[] | null }> = [
|
|
88
|
+
{ key: "theme", label: "chartPaletteTheme", colors: null },
|
|
89
|
+
{ key: "ocean", label: "chartPaletteOcean", colors: ["#0ea5e9", "#14b8a6", "#6366f1", "#8b5cf6", "#06b6d4"] },
|
|
90
|
+
{ key: "sunset", label: "chartPaletteSunset", colors: ["#f59e0b", "#ef4444", "#ec4899", "#8b5cf6", "#f97316"] },
|
|
91
|
+
{ key: "forest", label: "chartPaletteForest", colors: ["#16a34a", "#65a30d", "#0d9488", "#4d7c0f", "#0891b2"] },
|
|
92
|
+
{ key: "slate", label: "chartPaletteSlate", colors: ["#475569", "#0ea5e9", "#64748b", "#38bdf8", "#94a3b8"] },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/** The indicator chips a price chart offers: panes first, then overlays. */
|
|
96
|
+
const INDICATORS: Array<{ value: ChartPanelIndicator; label: keyof GridChartPanelMessages; title: keyof GridChartPanelMessages }> = [
|
|
97
|
+
{ value: "volume", label: "chartIndVolume", title: "chartIndVolumeTitle" },
|
|
98
|
+
{ value: "rsi", label: "chartIndRsi", title: "chartIndRsiTitle" },
|
|
99
|
+
{ value: "macd", label: "chartIndMacd", title: "chartIndMacdTitle" },
|
|
100
|
+
{ value: "stochastic", label: "chartIndStochastic", title: "chartIndStochasticTitle" },
|
|
101
|
+
{ value: "atr", label: "chartIndAtr", title: "chartIndAtrTitle" },
|
|
102
|
+
{ value: "obv", label: "chartIndObv", title: "chartIndObvTitle" },
|
|
103
|
+
{ value: "sma", label: "chartIndSma", title: "chartIndSmaTitle" },
|
|
104
|
+
{ value: "ema", label: "chartIndEma", title: "chartIndEmaTitle" },
|
|
105
|
+
{ value: "bb", label: "chartIndBb", title: "chartIndBbTitle" },
|
|
106
|
+
{ value: "vwap", label: "chartIndVwap", title: "chartIndVwapTitle" },
|
|
107
|
+
];
|
|
108
|
+
const OVERLAY_KINDS: ChartPanelIndicator[] = ["sma", "ema", "bb", "vwap"];
|
|
109
|
+
/** The price columns a candlestick / OHLC chart reads, in picker order. */
|
|
110
|
+
const OHLC_FIELDS: Array<{ key: "open" | "high" | "low" | "close" | "volume"; label: keyof GridChartPanelMessages }> = [
|
|
111
|
+
{ key: "open", label: "chartOpen" },
|
|
112
|
+
{ key: "high", label: "chartHigh" },
|
|
113
|
+
{ key: "low", label: "chartLow" },
|
|
114
|
+
{ key: "close", label: "chartClosePrice" },
|
|
115
|
+
{ key: "volume", label: "chartVolume" },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// Which controls this type can actually use. Allow-lists rather than "not
|
|
119
|
+
// pie" guards: with thirty types, "everything except pie" hands a gauge a
|
|
120
|
+
// Stacked checkbox.
|
|
121
|
+
const CARTESIAN: ChartType[] = ["bar", "line", "area", "lollipop", "pareto", "range-bar", "range-area", "dumbbell", "stream"];
|
|
122
|
+
/** Types whose second measure is the HIGH end (or the target) rather than a Y. */
|
|
123
|
+
const TWO_MEASURE: ChartType[] = ["range-bar", "range-area", "dumbbell"];
|
|
124
|
+
const BUCKETS: Array<{ value: ChartTimeBucket | ""; label: keyof GridChartPanelMessages }> = [
|
|
125
|
+
{ value: "", label: "chartBucketExact" },
|
|
126
|
+
{ value: "day", label: "chartBucketDay" },
|
|
127
|
+
{ value: "week", label: "chartBucketWeek" },
|
|
128
|
+
{ value: "month", label: "chartBucketMonth" },
|
|
129
|
+
{ value: "quarter", label: "chartBucketQuarter" },
|
|
130
|
+
{ value: "year", label: "chartBucketYear" },
|
|
131
|
+
];
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<script lang="ts" generics="TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData">
|
|
135
|
+
import type { RowData, TableFeatures } from "./index";
|
|
136
|
+
import type { SvGridController } from "./SvGrid.controller.svelte";
|
|
137
|
+
import { pivotChartType } from "./chart-pivot";
|
|
138
|
+
|
|
139
|
+
let {
|
|
140
|
+
ctrl,
|
|
141
|
+
messages: m,
|
|
142
|
+
scope = "panel",
|
|
143
|
+
}: {
|
|
144
|
+
ctrl: SvGridController<TFeatures, TData>;
|
|
145
|
+
messages: GridChartPanelMessages;
|
|
146
|
+
/** `panel`: the row under the panel head, Type first. `builder`: the
|
|
147
|
+
* Data tab's form, without Type (the gallery picks it). */
|
|
148
|
+
scope?: "panel" | "builder";
|
|
149
|
+
} = $props();
|
|
150
|
+
|
|
151
|
+
const columns = $derived(ctrl.chartableColumns);
|
|
152
|
+
/** Pivot mode: the pivot on screen is the chart, so the data pickers go and the
|
|
153
|
+
* type list keeps the shapes a pivot can take (no scatter, gauge, ranges, candles). */
|
|
154
|
+
const pivot = $derived(ctrl.pivotResult != null);
|
|
155
|
+
const availableTypes = $derived(chartPanelTypes(columns, m).filter((t) => !pivot || pivotChartType(t.value) === t.value));
|
|
156
|
+
const typeGroups = $derived([...new Set(availableTypes.map((t) => t.group))]);
|
|
157
|
+
const reducers = $derived(chartPanelReducers(m));
|
|
158
|
+
|
|
159
|
+
function toggleIndicator(kind: ChartPanelIndicator) {
|
|
160
|
+
const on = ctrl.chartIndicators.includes(kind);
|
|
161
|
+
// One overlay at a time: a series carries a single `overlay`.
|
|
162
|
+
const base = OVERLAY_KINDS.includes(kind) ? ctrl.chartIndicators.filter((k) => !OVERLAY_KINDS.includes(k)) : ctrl.chartIndicators;
|
|
163
|
+
ctrl.chartIndicators = on ? base.filter((k) => k !== kind) : [...base, kind];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const activePalette = $derived(
|
|
167
|
+
CHART_PANEL_PALETTES.find((x) => x.colors && ctrl.chartPalette && x.colors.join() === ctrl.chartPalette.join())?.key ?? "theme",
|
|
168
|
+
);
|
|
169
|
+
const wantsHorizontal = $derived(ctrl.chartType === "bar");
|
|
170
|
+
const wantsDonut = $derived(ctrl.chartType === "pie" || ctrl.chartType === "sunburst");
|
|
171
|
+
const wantsGroupBy = $derived(!pivot && ctrl.chartType !== "gauge" && ctrl.chartType !== "histogram");
|
|
172
|
+
const wantsSplitBy = $derived(
|
|
173
|
+
!pivot && columns.dims.length > 1 &&
|
|
174
|
+
!["pie", "gauge", "waterfall", "funnel", "calendar", "bullet", "pareto", "radial-bar"].includes(ctrl.chartType),
|
|
175
|
+
);
|
|
176
|
+
const wantsStacked = $derived(["bar", "line", "area", "radial-column"].includes(ctrl.chartType));
|
|
177
|
+
const wantsLogScale = $derived(
|
|
178
|
+
CARTESIAN.includes(ctrl.chartType) || ctrl.chartType === "scatter" || ctrl.chartType === "boxplot" || ctrl.chartType === "histogram",
|
|
179
|
+
);
|
|
180
|
+
const wantsDataLabels = $derived(
|
|
181
|
+
!["gauge", "sankey", "calendar", "treemap", "scatter", "boxplot", "chord", "sunburst", "stream"].includes(ctrl.chartType),
|
|
182
|
+
);
|
|
183
|
+
const wantsSecondMeasure = $derived(!pivot && (ctrl.chartType === "scatter" || TWO_MEASURE.includes(ctrl.chartType) || ctrl.chartType === "bullet"));
|
|
184
|
+
/** What the second measure picker is labelled: Y for a scatter, High for a
|
|
185
|
+
* range, Target for a bullet. */
|
|
186
|
+
const secondMeasureLabel = $derived(ctrl.chartType === "scatter" ? m.chartY : ctrl.chartType === "bullet" ? m.chartTarget : m.chartHigh);
|
|
187
|
+
const wantsFunnelShape = $derived(ctrl.chartType === "funnel");
|
|
188
|
+
const wantsCandleStyle = $derived(ctrl.chartType === "candlestick");
|
|
189
|
+
const wantsOhlc = $derived(!pivot && ctrl.chartIsOhlc);
|
|
190
|
+
const wantsBins = $derived(!pivot && ctrl.chartType === "histogram");
|
|
191
|
+
const wantsValue = $derived(!pivot && !ctrl.chartIsOhlc);
|
|
192
|
+
/**
|
|
193
|
+
* A box plot is the one type that reduces its groups itself: the whole point
|
|
194
|
+
* is the spread of the sample, so `sum | avg | count` has nothing to say
|
|
195
|
+
* about it. Showing an Aggregate select that changes nothing is worse than
|
|
196
|
+
* not showing one.
|
|
197
|
+
*/
|
|
198
|
+
const wantsReduce = $derived(!pivot && ctrl.chartType !== "scatter" && ctrl.chartType !== "boxplot" && ctrl.chartType !== "histogram" && !ctrl.chartIsOhlc);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Why the current type cannot draw anything useful yet, as a sentence.
|
|
202
|
+
*
|
|
203
|
+
* The alternative - removing the option - means a chart can disappear from
|
|
204
|
+
* under the user when they filter. Saying what is missing keeps the picker
|
|
205
|
+
* honest without making it unstable.
|
|
206
|
+
*/
|
|
207
|
+
const typeIssue = $derived.by<string | null>(() => {
|
|
208
|
+
const t = ctrl.chartType;
|
|
209
|
+
const cats = ctrl.chartSpec?.categories.length ?? 0;
|
|
210
|
+
if (t === "heatmap" && !ctrl.chartSeriesId && columns.dims.length > 1) return m.chartIssueHeatmap;
|
|
211
|
+
if (t === "sankey" && !ctrl.chartSeriesId) return m.chartIssueSankey;
|
|
212
|
+
if (t === "radar" && cats > 0 && cats < 3) return m.chartIssueRadar;
|
|
213
|
+
if (t === "funnel" && cats === 1) return m.chartIssueFunnel;
|
|
214
|
+
if (t === "chord" && !ctrl.chartSeriesId) return m.chartIssueChord;
|
|
215
|
+
if (t === "stream" && !ctrl.chartSeriesId) return m.chartIssueStream;
|
|
216
|
+
if (TWO_MEASURE.includes(t) && !ctrl.effectiveChartMeasure2Id) return m.chartIssueSecondMeasure;
|
|
217
|
+
if (ctrl.chartIsOhlc) {
|
|
218
|
+
const o = ctrl.chartOhlc;
|
|
219
|
+
if (!ctrl.chartOhlcDateId) return m.chartIssueOhlcDate;
|
|
220
|
+
if (!o.open || !o.high || !o.low || !o.close) return m.chartIssueOhlcColumns;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
});
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<div class="sv-grid-chart-controls" class:is-form={scope === "builder"} data-scope={scope}>
|
|
227
|
+
{#if scope === "panel"}
|
|
228
|
+
<label class="sv-grid-chart-ctl">
|
|
229
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartType}</span>
|
|
230
|
+
<select value={ctrl.chartType} onchange={(e) => (ctrl.chartType = e.currentTarget.value as ChartType)}>
|
|
231
|
+
<!-- Grouped: a flat thirty-item list is a wall. optgroup is native,
|
|
232
|
+
so this costs no new component. -->
|
|
233
|
+
{#each typeGroups as g (g)}
|
|
234
|
+
<optgroup label={g}>
|
|
235
|
+
{#each availableTypes.filter((t) => t.group === g) as t (t.value)}
|
|
236
|
+
<option value={t.value}>{t.label}</option>
|
|
237
|
+
{/each}
|
|
238
|
+
</optgroup>
|
|
239
|
+
{/each}
|
|
240
|
+
</select>
|
|
241
|
+
</label>
|
|
242
|
+
{/if}
|
|
243
|
+
{#if wantsOhlc}
|
|
244
|
+
<label class="sv-grid-chart-ctl">
|
|
245
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartDate}</span>
|
|
246
|
+
<select value={ctrl.chartOhlcDateId ?? ""} onchange={(e) => (ctrl.chartDimensionId = e.currentTarget.value || null)}>
|
|
247
|
+
{#each columns.dates as d (d.id)}<option value={d.id}>{d.label}</option>{/each}
|
|
248
|
+
</select>
|
|
249
|
+
</label>
|
|
250
|
+
{#each OHLC_FIELDS as f (f.key)}
|
|
251
|
+
<label class="sv-grid-chart-ctl">
|
|
252
|
+
<span class="sv-grid-chart-ctl-lbl">{m[f.label]}</span>
|
|
253
|
+
<select value={ctrl.chartOhlc[f.key] ?? ""} onchange={(e) => (ctrl.chartOhlc = { ...ctrl.chartOhlc, [f.key]: e.currentTarget.value || null })}>
|
|
254
|
+
{#if f.key === "volume"}<option value="">{m.chartNone}</option>{/if}
|
|
255
|
+
{#each columns.measures as c (c.id)}<option value={c.id}>{c.label}</option>{/each}
|
|
256
|
+
</select>
|
|
257
|
+
</label>
|
|
258
|
+
{/each}
|
|
259
|
+
{:else if wantsGroupBy}
|
|
260
|
+
<label class="sv-grid-chart-ctl">
|
|
261
|
+
<span class="sv-grid-chart-ctl-lbl">{ctrl.chartType === "sankey" ? m.chartFrom : ctrl.chartType === "heatmap" ? m.chartColumns : m.chartGroupBy}</span>
|
|
262
|
+
<select value={ctrl.chartDimensionId ?? ""} onchange={(e) => (ctrl.chartDimensionId = e.currentTarget.value || null)}>
|
|
263
|
+
{#each columns.dims as d (d.id)}<option value={d.id}>{d.label}</option>{/each}
|
|
264
|
+
</select>
|
|
265
|
+
</label>
|
|
266
|
+
{/if}
|
|
267
|
+
{#if wantsSplitBy}
|
|
268
|
+
<label class="sv-grid-chart-ctl">
|
|
269
|
+
<span class="sv-grid-chart-ctl-lbl">{ctrl.chartType === "sankey" ? m.chartTo : ctrl.chartType === "heatmap" ? m.chartRows : m.chartSplitBy}</span>
|
|
270
|
+
<select value={ctrl.chartSeriesId ?? ""} onchange={(e) => (ctrl.chartSeriesId = e.currentTarget.value || null)}>
|
|
271
|
+
<option value="">{m.chartNone}</option>
|
|
272
|
+
{#each columns.dims as d (d.id)}<option value={d.id}>{d.label}</option>{/each}
|
|
273
|
+
</select>
|
|
274
|
+
</label>
|
|
275
|
+
{/if}
|
|
276
|
+
{#if wantsValue}
|
|
277
|
+
<label class="sv-grid-chart-ctl">
|
|
278
|
+
<span class="sv-grid-chart-ctl-lbl">{ctrl.chartType === "scatter" ? m.chartX : TWO_MEASURE.includes(ctrl.chartType) ? m.chartLow : ctrl.chartType === "gauge" ? m.chartMetric : m.chartValue}</span>
|
|
279
|
+
<select value={ctrl.chartMeasureId ?? ""} onchange={(e) => (ctrl.chartMeasureId = e.currentTarget.value || null)}>
|
|
280
|
+
{#each columns.measures as c (c.id)}<option value={c.id}>{c.label}</option>{/each}
|
|
281
|
+
</select>
|
|
282
|
+
</label>
|
|
283
|
+
{/if}
|
|
284
|
+
{#if wantsSecondMeasure}
|
|
285
|
+
<label class="sv-grid-chart-ctl">
|
|
286
|
+
<span class="sv-grid-chart-ctl-lbl">{secondMeasureLabel}</span>
|
|
287
|
+
<select value={ctrl.effectiveChartMeasure2Id ?? ""} onchange={(e) => (ctrl.chartMeasure2Id = e.currentTarget.value || null)}>
|
|
288
|
+
{#each columns.measures as c (c.id)}<option value={c.id}>{c.label}</option>{/each}
|
|
289
|
+
</select>
|
|
290
|
+
</label>
|
|
291
|
+
{/if}
|
|
292
|
+
{#if wantsReduce}
|
|
293
|
+
<label class="sv-grid-chart-ctl">
|
|
294
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartAggregate}</span>
|
|
295
|
+
<select value={ctrl.chartReduce} onchange={(e) => (ctrl.chartReduce = e.currentTarget.value as ChartReducer)}>
|
|
296
|
+
{#each reducers as r (r.value)}<option value={r.value}>{r.label}</option>{/each}
|
|
297
|
+
</select>
|
|
298
|
+
</label>
|
|
299
|
+
{/if}
|
|
300
|
+
{#if wantsBins}
|
|
301
|
+
<label class="sv-grid-chart-ctl" title={m.chartBinsTitle}>
|
|
302
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartBins}</span>
|
|
303
|
+
<input class="sv-grid-chart-num" type="number" min="1" max="200" value={ctrl.chartBins ?? ""} placeholder={m.chartBuilderAuto} onchange={(e) => (ctrl.chartBins = e.currentTarget.value ? Number(e.currentTarget.value) : null)} />
|
|
304
|
+
</label>
|
|
305
|
+
{/if}
|
|
306
|
+
{#if wantsFunnelShape}
|
|
307
|
+
<label class="sv-grid-chart-ctl">
|
|
308
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartShape}</span>
|
|
309
|
+
<select value={ctrl.chartFunnelShape ?? "trapezoid"} onchange={(e) => (ctrl.chartFunnelShape = e.currentTarget.value as "trapezoid" | "pyramid" | "cone")}>
|
|
310
|
+
<option value="trapezoid">{m.chartShapeFunnel}</option>
|
|
311
|
+
<option value="pyramid">{m.chartShapePyramid}</option>
|
|
312
|
+
<option value="cone">{m.chartShapeCone}</option>
|
|
313
|
+
</select>
|
|
314
|
+
</label>
|
|
315
|
+
{/if}
|
|
316
|
+
{#if wantsCandleStyle}
|
|
317
|
+
<label class="sv-grid-chart-ctl">
|
|
318
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartCandles}</span>
|
|
319
|
+
<select value={ctrl.chartCandleStyle ?? "classic"} onchange={(e) => (ctrl.chartCandleStyle = e.currentTarget.value as "classic" | "hollow" | "heikin-ashi")}>
|
|
320
|
+
<option value="classic">{m.chartCandlesClassic}</option>
|
|
321
|
+
<option value="hollow">{m.chartCandlesHollow}</option>
|
|
322
|
+
<option value="heikin-ashi">{m.chartCandlesHeikinAshi}</option>
|
|
323
|
+
</select>
|
|
324
|
+
</label>
|
|
325
|
+
{/if}
|
|
326
|
+
{#if wantsOhlc}
|
|
327
|
+
<span class="sv-grid-chart-ctl sv-grid-chart-indicators" role="group" aria-label={m.chartIndicators}>
|
|
328
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartIndicators}</span>
|
|
329
|
+
<span class="sv-grid-chart-chips">
|
|
330
|
+
{#each INDICATORS as ind (ind.value)}
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
class="sv-grid-chart-chip"
|
|
334
|
+
class:is-on={ctrl.chartIndicators.includes(ind.value)}
|
|
335
|
+
aria-pressed={ctrl.chartIndicators.includes(ind.value)}
|
|
336
|
+
title={m[ind.title]}
|
|
337
|
+
onclick={() => toggleIndicator(ind.value)}
|
|
338
|
+
>{m[ind.label]}</button>
|
|
339
|
+
{/each}
|
|
340
|
+
</span>
|
|
341
|
+
</span>
|
|
342
|
+
{/if}
|
|
343
|
+
<label class="sv-grid-chart-ctl">
|
|
344
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartFormat}</span>
|
|
345
|
+
<select value={ctrl.chartValueFormat} onchange={(e) => (ctrl.chartValueFormat = e.currentTarget.value as "number" | "currency" | "percent")}>
|
|
346
|
+
<option value="number">{m.chartFormatNumber}</option>
|
|
347
|
+
<option value="currency">{m.chartFormatCurrency}</option>
|
|
348
|
+
<option value="percent">{m.chartFormatPercent}</option>
|
|
349
|
+
</select>
|
|
350
|
+
</label>
|
|
351
|
+
{#if wantsStacked}
|
|
352
|
+
<label class="sv-grid-chart-toggle">
|
|
353
|
+
<input type="checkbox" checked={ctrl.chartStacked} onchange={(e) => (ctrl.chartStacked = e.currentTarget.checked)} />
|
|
354
|
+
{m.chartStacked}
|
|
355
|
+
</label>
|
|
356
|
+
<label class="sv-grid-chart-toggle" title={m.chartStacked100Title}>
|
|
357
|
+
<input type="checkbox" checked={ctrl.chartStacked100} onchange={(e) => (ctrl.chartStacked100 = e.currentTarget.checked)} />
|
|
358
|
+
{m.chartStacked100}
|
|
359
|
+
</label>
|
|
360
|
+
{/if}
|
|
361
|
+
{#if wantsHorizontal}
|
|
362
|
+
<label class="sv-grid-chart-toggle" title={m.chartHorizontalTitle}>
|
|
363
|
+
<input
|
|
364
|
+
type="checkbox"
|
|
365
|
+
checked={ctrl.chartOrientation === "horizontal"}
|
|
366
|
+
onchange={(e) => (ctrl.chartOrientation = e.currentTarget.checked ? "horizontal" : "vertical")}
|
|
367
|
+
/>
|
|
368
|
+
{m.chartHorizontal}
|
|
369
|
+
</label>
|
|
370
|
+
{/if}
|
|
371
|
+
{#if wantsDonut}
|
|
372
|
+
<label class="sv-grid-chart-toggle">
|
|
373
|
+
<input type="checkbox" checked={ctrl.chartDonut} onchange={(e) => (ctrl.chartDonut = e.currentTarget.checked)} />
|
|
374
|
+
{m.chartDonut}
|
|
375
|
+
</label>
|
|
376
|
+
{/if}
|
|
377
|
+
<label class="sv-grid-chart-ctl">
|
|
378
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartColours}</span>
|
|
379
|
+
<select
|
|
380
|
+
value={activePalette}
|
|
381
|
+
onchange={(e) => (ctrl.chartPalette = CHART_PANEL_PALETTES.find((x) => x.key === e.currentTarget.value)?.colors ?? null)}
|
|
382
|
+
>
|
|
383
|
+
{#each CHART_PANEL_PALETTES as pal (pal.key)}<option value={pal.key}>{m[pal.label]}</option>{/each}
|
|
384
|
+
</select>
|
|
385
|
+
</label>
|
|
386
|
+
{#if wantsDataLabels}
|
|
387
|
+
<label class="sv-grid-chart-toggle">
|
|
388
|
+
<input type="checkbox" checked={ctrl.chartDataLabels} onchange={(e) => (ctrl.chartDataLabels = e.currentTarget.checked)} />
|
|
389
|
+
{m.chartLabels}
|
|
390
|
+
</label>
|
|
391
|
+
{/if}
|
|
392
|
+
{#if wantsLogScale}
|
|
393
|
+
<label class="sv-grid-chart-toggle" title={m.chartLogScaleTitle}>
|
|
394
|
+
<input type="checkbox" checked={ctrl.chartLogScale} onchange={(e) => (ctrl.chartLogScale = e.currentTarget.checked)} />
|
|
395
|
+
{m.chartLogScale}
|
|
396
|
+
</label>
|
|
397
|
+
{/if}
|
|
398
|
+
{#if ctrl.chartDimensionIsDate && wantsGroupBy}
|
|
399
|
+
<label class="sv-grid-chart-ctl" title={m.chartBucketTitle}>
|
|
400
|
+
<span class="sv-grid-chart-ctl-lbl">{m.chartBucket}</span>
|
|
401
|
+
<select value={ctrl.chartBucket ?? ""} onchange={(e) => (ctrl.chartBucket = (e.currentTarget.value || null) as ChartTimeBucket | null)}>
|
|
402
|
+
{#each BUCKETS as b (b.value)}<option value={b.value}>{m[b.label]}</option>{/each}
|
|
403
|
+
</select>
|
|
404
|
+
</label>
|
|
405
|
+
{/if}
|
|
406
|
+
{#if ctrl.chartDimensionIsDate && CARTESIAN.includes(ctrl.chartType)}
|
|
407
|
+
<label class="sv-grid-chart-toggle" title={m.chartDateAxisTitle}>
|
|
408
|
+
<input type="checkbox" checked={ctrl.chartTimeAxis} onchange={(e) => (ctrl.chartTimeAxis = e.currentTarget.checked)} />
|
|
409
|
+
{m.chartDateAxis}
|
|
410
|
+
</label>
|
|
411
|
+
{/if}
|
|
412
|
+
{#if pivot}
|
|
413
|
+
<p class="sv-grid-chart-hint sv-grid-chart-pivot-hint" role="note">{m.chartPivotHint}</p>
|
|
414
|
+
{:else if typeIssue}
|
|
415
|
+
<p class="sv-grid-chart-hint" role="note">{typeIssue}</p>
|
|
416
|
+
{/if}
|
|
417
|
+
</div>
|
|
@@ -31,10 +31,9 @@
|
|
|
31
31
|
localization?: { locale?: string | ReadonlyArray<string> };
|
|
32
32
|
} = $props();
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
let h = $state(0);
|
|
34
|
+
// The chart fills the grid area and re-lays out on resize (the same
|
|
35
|
+
// responsive behaviour a table view has) through its own `autosize`, which
|
|
36
|
+
// also keeps its legend inside the box instead of below it.
|
|
38
37
|
|
|
39
38
|
const spec = $derived.by<ChartSpec>(() => {
|
|
40
39
|
const s = rowsToChartSpec((data ?? []) as ReadonlyArray<Record<string, unknown>>, {
|
|
@@ -57,20 +56,20 @@
|
|
|
57
56
|
});
|
|
58
57
|
</script>
|
|
59
58
|
|
|
60
|
-
<div class="sv-grid-chart-view"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
dataLabels={chart.dataLabels ?? false}
|
|
68
|
-
/>
|
|
69
|
-
{/if}
|
|
59
|
+
<div class="sv-grid-chart-view">
|
|
60
|
+
<SvGridChart
|
|
61
|
+
{spec}
|
|
62
|
+
autosize
|
|
63
|
+
legend={chart.legend ?? true}
|
|
64
|
+
dataLabels={chart.dataLabels ?? false}
|
|
65
|
+
/>
|
|
70
66
|
</div>
|
|
71
67
|
|
|
72
68
|
<style>
|
|
69
|
+
/* A grid, so the autosized chart is stretched to the full height rather
|
|
70
|
+
than asking for a percentage of it (which cannot be measured reliably). */
|
|
73
71
|
.sv-grid-chart-view {
|
|
72
|
+
display: grid;
|
|
74
73
|
width: 100%;
|
|
75
74
|
height: 100%;
|
|
76
75
|
min-height: 0;
|
package/src/SvModal.svelte
CHANGED
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
onClose?: () => void
|
|
23
23
|
title?: string
|
|
24
24
|
size?: 'sm' | 'md' | 'lg'
|
|
25
|
+
/**
|
|
26
|
+
* Panel width in px, when the three presets miss the content's natural
|
|
27
|
+
* width. Wins over `size`; the user's own resize wins over both.
|
|
28
|
+
*/
|
|
29
|
+
width?: number
|
|
25
30
|
draggable?: boolean
|
|
26
31
|
resizable?: boolean
|
|
27
32
|
closeOnBackdrop?: boolean
|
|
@@ -37,6 +42,7 @@
|
|
|
37
42
|
onClose,
|
|
38
43
|
title,
|
|
39
44
|
size = 'md',
|
|
45
|
+
width,
|
|
40
46
|
draggable = false,
|
|
41
47
|
resizable = false,
|
|
42
48
|
closeOnBackdrop = true,
|
|
@@ -118,7 +124,7 @@
|
|
|
118
124
|
class:is-resized={!!box}
|
|
119
125
|
{...overlay.dialogProps({ labelledBy: title ? titleId : undefined })}
|
|
120
126
|
style:transform={`translate(calc(-50% + ${drag.x}px), calc(-50% + ${drag.y}px))`}
|
|
121
|
-
style:width={box ? `${box.w}px` : undefined}
|
|
127
|
+
style:width={box ? `${box.w}px` : width ? `${width}px` : undefined}
|
|
122
128
|
style:height={box ? `${box.h}px` : undefined}
|
|
123
129
|
>
|
|
124
130
|
{#if title || !hideClose}
|
package/src/SvModal.test.ts
CHANGED
|
@@ -38,6 +38,15 @@ describe('SvModal', () => {
|
|
|
38
38
|
} finally { destroy() }
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
it('takes an exact width over the size preset', () => {
|
|
42
|
+
const { destroy } = mountModal({ open: true, size: 'sm', width: 440 })
|
|
43
|
+
try {
|
|
44
|
+
const el = dialog()!
|
|
45
|
+
expect(el.classList.contains('sv-modal--sm')).toBe(true)
|
|
46
|
+
expect(el.style.width).toBe('440px')
|
|
47
|
+
} finally { destroy() }
|
|
48
|
+
})
|
|
49
|
+
|
|
41
50
|
it('locks body scroll while open and restores it on close', () => {
|
|
42
51
|
document.body.style.overflow = 'auto'
|
|
43
52
|
const { destroy } = mountModal({ open: true })
|
package/src/ai.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
|
2
2
|
import {
|
|
3
3
|
setAIProvider, getAIProvider, hasAIProvider,
|
|
4
4
|
mockAIProvider,
|
|
5
|
-
aiFilter, aiSmartFill, aiSummarize, aiClassify, aiChart,
|
|
5
|
+
aiFilter, aiSmartFill, aiSummarize, aiClassify, aiChart, aiExplainChart, enableAiCharting, disableAiCharting,
|
|
6
6
|
type AIProvider, type AIRequest,
|
|
7
7
|
} from './ai'
|
|
8
8
|
|
|
@@ -552,10 +552,65 @@ describe('aiChart type vocabulary', () => {
|
|
|
552
552
|
expect(g.dimension).toBeNull()
|
|
553
553
|
})
|
|
554
554
|
|
|
555
|
+
it('plans a candlestick with indicators when the data has a date and four prices', async () => {
|
|
556
|
+
const prices = [
|
|
557
|
+
{ day: '2026-01-05', open: 10, high: 12, low: 9, close: 11, volume: 100 },
|
|
558
|
+
{ day: '2026-01-06', open: 11, high: 13, low: 10, close: 12, volume: 120 },
|
|
559
|
+
]
|
|
560
|
+
const p = await aiChart(fakeApi(prices).api as never, 'candlestick price chart with volume and RSI')
|
|
561
|
+
expect(p.type).toBe('candlestick')
|
|
562
|
+
expect(p.dimension).toBe('day')
|
|
563
|
+
expect(p.indicators).toEqual(['volume', 'rsi'])
|
|
564
|
+
// Without four numeric columns there is nothing to draw candles from.
|
|
565
|
+
expect((await plan('candlestick of revenue')).type).toBe('bar')
|
|
566
|
+
// Indicators only mean something on a candlestick, and unknown ones are dropped.
|
|
567
|
+
const loose: AIProvider = async () =>
|
|
568
|
+
JSON.stringify({ type: 'candlestick', dimension: 'day', measure: 'close', reduce: 'sum', indicators: ['rsi', 'bogus'] })
|
|
569
|
+
setAIProvider(loose)
|
|
570
|
+
const q = await aiChart(fakeApi(prices).api as never, 'anything')
|
|
571
|
+
expect(q.indicators).toEqual(['rsi'])
|
|
572
|
+
})
|
|
573
|
+
|
|
555
574
|
it('coerces an unknown type rather than passing it through', async () => {
|
|
556
575
|
const bogus: AIProvider = async () =>
|
|
557
|
-
JSON.stringify({ type: '
|
|
576
|
+
JSON.stringify({ type: 'hexbin', dimension: 'region', measure: 'revenue', reduce: 'sum' })
|
|
558
577
|
setAIProvider(bogus)
|
|
559
578
|
expect((await plan('anything')).type).toBe('bar')
|
|
560
579
|
})
|
|
561
580
|
})
|
|
581
|
+
|
|
582
|
+
describe('aiExplainChart', () => {
|
|
583
|
+
it('grounds the prompt on chartSummary and returns the summary with the insights', async () => {
|
|
584
|
+
const prompts: string[] = []
|
|
585
|
+
setAIProvider(async (req) => {
|
|
586
|
+
prompts.push(req.prompt)
|
|
587
|
+
expect(req.task).toBe('explain-chart')
|
|
588
|
+
return JSON.stringify({ insights: ['one', 'two', 'three', 'four'] })
|
|
589
|
+
})
|
|
590
|
+
const spec = { type: 'bar' as const, categories: ['Q1', 'Q2'], series: [{ label: 'Sales', values: [10, 15] }] }
|
|
591
|
+
const out = await aiExplainChart({ getChartSpec: () => spec } as never)
|
|
592
|
+
expect(out.summary).toBe('Sales rises 50% from 10 at Q1 to 15 at Q2.')
|
|
593
|
+
expect(out.insights).toEqual(['one', 'two', 'three'])
|
|
594
|
+
expect(prompts[0]).toContain('Reading of the chart: Sales rises 50%')
|
|
595
|
+
expect(prompts[0]).toContain('category\tSales\nQ1\t10\nQ2\t15')
|
|
596
|
+
// No chart: no call.
|
|
597
|
+
expect(await aiExplainChart({ getChartSpec: () => null } as never)).toEqual({ summary: 'No chart to explain.', insights: [] })
|
|
598
|
+
expect(prompts).toHaveLength(1)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('the mock provider answers with two insights, and enableAiCharting registers the explain handler', async () => {
|
|
602
|
+
setAIProvider(mockAIProvider)
|
|
603
|
+
const spec = { type: 'line' as const, categories: ['a', 'b'], series: [{ label: 'v', values: [1, 3] }] }
|
|
604
|
+
const out = await aiExplainChart({ getChartSpec: () => spec } as never)
|
|
605
|
+
expect(out.insights).toHaveLength(2)
|
|
606
|
+
expect(out.insights[0]).toContain('front-loaded')
|
|
607
|
+
const handlers: Record<string, unknown> = {}
|
|
608
|
+
const api = { getChartSpec: () => spec, setChartAiHandler: (fn: unknown) => { handlers.chart = fn }, setChartExplainHandler: (fn: unknown) => { handlers.explain = fn } }
|
|
609
|
+
enableAiCharting(api as never)
|
|
610
|
+
expect(typeof handlers.explain).toBe('function')
|
|
611
|
+
const res = await (handlers.explain as () => Promise<{ summary: string }>)()
|
|
612
|
+
expect(res.summary).toBe('v rises 200% from 1 at a to 3 at b.')
|
|
613
|
+
disableAiCharting(api as never)
|
|
614
|
+
expect(handlers.explain).toBeNull()
|
|
615
|
+
})
|
|
616
|
+
})
|