@svgrid/grid 3.0.0 → 3.0.2
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/CHANGELOG.md +116 -88
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +190 -181
- package/dist/GridFooter.svelte.d.ts +8 -0
- package/dist/GridMenus.svelte +18 -10
- package/dist/GridMenus.svelte.d.ts +2 -1
- package/dist/SvAutoComplete.svelte +169 -169
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +279 -279
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +163 -163
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +378 -378
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.controller.svelte.d.ts +17 -0
- package/dist/SvGrid.controller.svelte.js +128 -14
- package/dist/SvGrid.css +79 -10
- package/dist/SvGrid.svelte +191 -186
- package/dist/SvGrid.types.d.ts +42 -1
- package/dist/SvGridCellEditor.svelte +748 -744
- package/dist/SvGridChart.svelte +2246 -1716
- package/dist/SvGridChart.svelte.d.ts +63 -1
- package/dist/SvGridChartPanel.svelte +633 -485
- package/dist/SvGridChartView.svelte +79 -70
- package/dist/SvGridChartView.svelte.d.ts +7 -0
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +270 -270
- package/dist/SvGroupCell.svelte +141 -116
- package/dist/SvGroupCell.svelte.d.ts +8 -0
- package/dist/SvListBox.svelte +334 -334
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +293 -293
- package/dist/SvNumberInput.svelte +172 -172
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +186 -170
- package/dist/SvRowGroupPanel.svelte.d.ts +8 -0
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +203 -203
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +115 -115
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +239 -239
- package/dist/ai.d.ts +11 -2
- package/dist/ai.js +51 -7
- package/dist/cdn/GridMenus-DyFA-QxA.js +602 -0
- package/dist/cdn/GridMenus-M2XP9t0_.js +603 -0
- package/dist/cdn/{SvDateRangeInput-DflbiP7N.js → SvDateRangeInput-CaOuMs8O.js} +1 -1
- package/dist/cdn/{SvDateRangeInput-Bh0A0JkF.js → SvDateRangeInput-DMLKmGEc.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-Bivn8dAP.js → SvDateTimePicker-CCbDZNZB.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-BWwpfB_o.js → SvDateTimePicker-sonaH0oh.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-Ba7rY3eu.js → SvGridCellEditor-BWnTi2N7.js} +2 -2
- package/dist/cdn/{SvGridCellEditor-AXL8cHGO.js → SvGridCellEditor-CjEJMJKc.js} +2 -2
- package/dist/cdn/SvGridChart-DAb9RbRs.js +1480 -0
- package/dist/cdn/SvGridChart-Yl-IXZFO.js +1481 -0
- package/dist/cdn/SvGridChartPanel-DayQpDsR.js +834 -0
- package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +810 -0
- package/dist/cdn/{SvGridChartView-eSPuJE6g.js → SvGridChartView-CxfakxR0.js} +5 -3
- package/dist/cdn/{SvGridChartView-SmPW10dI.js → SvGridChartView-Dm_YEx9L.js} +5 -3
- package/dist/cdn/{SvGridDropdown-B1ZcLpgs.js → SvGridDropdown-D0VdjeR8.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CtdbHcIS.js → SvGridDropdown-D13MtJ2j.js} +1 -1
- package/dist/cdn/cell-formatting-C2Hf5gqd.js +86 -0
- package/dist/cdn/chart-CKJRCPOk.js +1652 -0
- package/dist/cdn/{date-format-DZT7T1wf.js → date-format-BNii4zeD.js} +2 -2
- package/dist/cdn/{date-format-CcP1tafP.js → date-format-BnnHlqGw.js} +2 -2
- package/dist/cdn/{export-format-Cv1Dll6k.js → export-format-CUDLy2yn.js} +84 -168
- package/dist/cdn/{src-B_YS5AOc.js → src-BJ2opd0j.js} +3642 -3450
- package/dist/cdn/{src-DYXXpuSk.js → src-DNbz94Ct.js} +8649 -8457
- package/dist/cdn/svgrid.js +16 -15
- package/dist/cdn/svgrid.svelte-external.js +16 -15
- package/dist/cell-formatting.d.ts +7 -0
- package/dist/cell-formatting.js +7 -1
- package/dist/chart-export.d.ts +23 -2
- package/dist/chart-export.js +88 -14
- package/dist/chart.d.ts +410 -18
- package/dist/chart.js +824 -24
- package/dist/editing.js +22 -13
- package/dist/grid-icons.d.ts +49 -0
- package/dist/grid-icons.js +86 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/package.json +1 -1
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +190 -181
- package/src/GridMenus.svelte +18 -10
- package/src/SvAutoComplete.svelte +169 -169
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +279 -279
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +163 -163
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +378 -378
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +146 -13
- package/src/SvGrid.css +79 -10
- package/src/SvGrid.svelte +191 -186
- package/src/SvGrid.types.ts +2133 -2092
- package/src/SvGridCellEditor.svelte +748 -744
- package/src/SvGridChart.svelte +2246 -1716
- package/src/SvGridChart.test.ts +243 -1
- package/src/SvGridChartPanel.svelte +633 -485
- package/src/SvGridChartView.svelte +79 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +270 -270
- package/src/SvGroupCell.svelte +141 -116
- package/src/SvListBox.svelte +334 -334
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +293 -293
- package/src/SvNumberInput.svelte +172 -172
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +186 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +203 -203
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +115 -115
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +239 -239
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +561 -502
- package/src/ai.ts +1472 -1419
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +846 -846
- package/src/builtin-editors.grid.test.ts +83 -83
- package/src/cell-formatting.ts +177 -171
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +148 -148
- package/src/chart-export.test.ts +112 -0
- package/src/chart-export.ts +270 -202
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.test.ts +830 -1
- package/src/chart.ts +3342 -2321
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.ts +78 -78
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1865 -1865
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +250 -250
- package/src/createMenu.svelte.ts +224 -224
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +322 -322
- package/src/datetime/date-core.ts +206 -206
- package/src/datetime/date-restrict.ts +61 -61
- package/src/datetime/timezone.ts +135 -135
- package/src/dock-manager-model.ts +596 -596
- package/src/dock-model.ts +374 -374
- package/src/editing.test.ts +974 -974
- package/src/editing.ts +618 -609
- package/src/editor-contract.ts +171 -171
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +160 -160
- package/src/filtering/excel-filters.ts +325 -325
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/grid-icons.test.ts +46 -0
- package/src/grid-icons.ts +184 -0
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/headless.ts +87 -87
- package/src/icon-overrides.grid.test.ts +188 -0
- package/src/icon-seam.test.ts +100 -0
- package/src/index.ts +19 -0
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +179 -179
- package/src/menus.ts +597 -597
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/positioning.ts +268 -268
- package/src/render-component.ts +28 -28
- package/src/row-drag.test.ts +401 -401
- package/src/row-drag.ts +419 -419
- package/src/row-resize.test.ts +524 -524
- package/src/row-resize.ts +228 -228
- package/src/scheduler-ical.ts +181 -181
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +873 -873
- package/src/selection.test.ts +885 -885
- package/src/server-data-source.test.ts +383 -383
- package/src/server-data-source.ts +469 -469
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +113 -113
- package/src/svgrid-wrapper.types.ts +563 -563
- package/src/svgrid.async-editor-options.test.ts +273 -273
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +910 -910
- package/src/svgrid.charting.test.ts +654 -534
- package/src/svgrid.comments-autocomplete.test.ts +127 -127
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +382 -377
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.wrapper.test.ts +63 -63
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +288 -288
- package/src/toast-store.svelte.ts +250 -250
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +322 -322
- package/dist/cdn/GridMenus-DMihUtma.js +0 -600
- package/dist/cdn/GridMenus-nmNDj1a3.js +0 -601
- package/dist/cdn/SvGridChart-Bs2GIR2Q.js +0 -1202
- package/dist/cdn/SvGridChart-CwhFz7GV.js +0 -1203
- package/dist/cdn/SvGridChartPanel-1WXuStzM.js +0 -561
- package/dist/cdn/SvGridChartPanel-e5_nqNP9.js +0 -585
- package/dist/cdn/chart-i4XcXyHJ.js +0 -1214
package/dist/SvTreeSelect.svelte
CHANGED
|
@@ -1,239 +1,239 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
export type { TreeSelectNode } from './ui-app.types'
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<script lang="ts">
|
|
6
|
-
/**
|
|
7
|
-
* SvTreeSelect - a single-select dropdown that shows an indented, collapsible
|
|
8
|
-
* tree in its panel (pick a node from a hierarchy). The open / position /
|
|
9
|
-
* dismiss / roving / keyboard / ARIA mechanics come from the shared
|
|
10
|
-
* `createPopoverSelect` core (with ArrowLeft/Right handled via onExtraKey for
|
|
11
|
-
* expand/collapse); this component adds the tree flattening + rendering. Covers
|
|
12
|
-
* the "cascader / tree select" pattern without depending on SvTree.
|
|
13
|
-
*/
|
|
14
|
-
import type { TreeSelectNode } from './ui-app.types'
|
|
15
|
-
import SvField from './SvField.svelte'
|
|
16
|
-
import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
|
|
17
|
-
import { portalToBody, popIn } from './popover'
|
|
18
|
-
import { createPopoverSelect } from './createPopoverSelect.svelte'
|
|
19
|
-
import { flattenVisible, findNodePath } from './tree-select'
|
|
20
|
-
|
|
21
|
-
type Props = Pick<
|
|
22
|
-
SvEditorProps,
|
|
23
|
-
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'block' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
24
|
-
> & {
|
|
25
|
-
nodes: ReadonlyArray<TreeSelectNode>
|
|
26
|
-
value?: string | number | null
|
|
27
|
-
onChange?: (value: string | number) => void
|
|
28
|
-
onCommit?: (value: string | number) => void
|
|
29
|
-
onCancel?: () => void
|
|
30
|
-
placeholder?: string
|
|
31
|
-
showPath?: boolean
|
|
32
|
-
expandedIds?: ReadonlyArray<string | number>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let {
|
|
36
|
-
nodes,
|
|
37
|
-
value = $bindable<string | number | null>(null),
|
|
38
|
-
onChange,
|
|
39
|
-
placeholder = 'Select…',
|
|
40
|
-
showPath = false,
|
|
41
|
-
expandedIds = [],
|
|
42
|
-
disabled = false,
|
|
43
|
-
readonly = false,
|
|
44
|
-
label,
|
|
45
|
-
hint,
|
|
46
|
-
error,
|
|
47
|
-
required = false,
|
|
48
|
-
invalid = false,
|
|
49
|
-
block = false,
|
|
50
|
-
size = 'md',
|
|
51
|
-
id,
|
|
52
|
-
name,
|
|
53
|
-
dir,
|
|
54
|
-
ariaLabel,
|
|
55
|
-
loading = false,
|
|
56
|
-
}: Props = $props()
|
|
57
|
-
|
|
58
|
-
const autoId = nextEditorId('sv-ts')
|
|
59
|
-
const uid = $derived(id ?? autoId)
|
|
60
|
-
|
|
61
|
-
let expanded = $state(new Set<string | number>(expandedIds))
|
|
62
|
-
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
63
|
-
let panelEl = $state<HTMLDivElement | null>(null)
|
|
64
|
-
let treeEl = $state<HTMLUListElement | null>(null)
|
|
65
|
-
|
|
66
|
-
const rows = $derived(flattenVisible(nodes, expanded))
|
|
67
|
-
const selectedPath = $derived(value == null ? null : findNodePath(nodes, value))
|
|
68
|
-
const triggerText = $derived(
|
|
69
|
-
!selectedPath
|
|
70
|
-
? ''
|
|
71
|
-
: showPath
|
|
72
|
-
? selectedPath.map((n) => n.label).join(' / ')
|
|
73
|
-
: selectedPath[selectedPath.length - 1]!.label,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
function setExpanded(v: string | number, on: boolean) {
|
|
77
|
-
const next = new Set(expanded)
|
|
78
|
-
on ? next.add(v) : next.delete(v)
|
|
79
|
-
expanded = next
|
|
80
|
-
}
|
|
81
|
-
function toggleExpand(v: string | number) {
|
|
82
|
-
setExpanded(v, !expanded.has(v))
|
|
83
|
-
}
|
|
84
|
-
function pick(node: TreeSelectNode) {
|
|
85
|
-
if (node.disabled) return
|
|
86
|
-
value = node.value // enables bind:value
|
|
87
|
-
onChange?.(node.value)
|
|
88
|
-
ts.closePanel()
|
|
89
|
-
}
|
|
90
|
-
function onRowClick(row: { node: TreeSelectNode; hasChildren: boolean }) {
|
|
91
|
-
if (row.hasChildren) toggleExpand(row.node.value)
|
|
92
|
-
else pick(row.node)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const ts = createPopoverSelect({
|
|
96
|
-
itemCount: () => rows.length,
|
|
97
|
-
disabled: (i) => !!rows[i]?.node.disabled,
|
|
98
|
-
readonly: () => readonly,
|
|
99
|
-
onSelect: (i) => { const row = rows[i]; if (row) onRowClick(row) },
|
|
100
|
-
getTrigger: () => triggerEl,
|
|
101
|
-
getPanel: () => panelEl,
|
|
102
|
-
getInitialFocus: () => treeEl,
|
|
103
|
-
closeOnSelect: () => false, // onRowClick handles pick + close (branches stay open)
|
|
104
|
-
initialActive: () => Math.max(0, rows.findIndex((r) => r.node.value === value)),
|
|
105
|
-
minWidth: () => 220,
|
|
106
|
-
estimatedHeight: () => 300,
|
|
107
|
-
onExtraKey: (e, act) => {
|
|
108
|
-
const row = rows[act]
|
|
109
|
-
if (!row) return false
|
|
110
|
-
// "expand" opens the node or steps into the first child; "collapse" closes
|
|
111
|
-
// it or jumps to the parent - same fallback as createTree's core.
|
|
112
|
-
const expand = () => {
|
|
113
|
-
if (row.hasChildren && !row.expanded) setExpanded(row.node.value, true)
|
|
114
|
-
else if (row.hasChildren && row.expanded) ts.active = Math.min(act + 1, rows.length - 1)
|
|
115
|
-
}
|
|
116
|
-
const collapse = () => {
|
|
117
|
-
if (row.hasChildren && row.expanded) setExpanded(row.node.value, false)
|
|
118
|
-
else {
|
|
119
|
-
let pi = act - 1
|
|
120
|
-
while (pi >= 0 && rows[pi]!.depth >= row.depth) pi--
|
|
121
|
-
if (pi >= 0) ts.active = pi
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (e.key === 'ArrowRight') { e.preventDefault(); expand(); return true }
|
|
125
|
-
if (e.key === 'ArrowLeft') { e.preventDefault(); collapse(); return true }
|
|
126
|
-
return false
|
|
127
|
-
},
|
|
128
|
-
idPrefix: 'sv-ts',
|
|
129
|
-
})
|
|
130
|
-
</script>
|
|
131
|
-
|
|
132
|
-
<SvField id={uid} {label} {hint} {error} {required} {dir} {loading}>
|
|
133
|
-
<div class="sv-ts sv-ts--{size}"
|
|
134
|
-
class:is-block={block} class:is-disabled={disabled}>
|
|
135
|
-
<button
|
|
136
|
-
bind:this={triggerEl}
|
|
137
|
-
type="button"
|
|
138
|
-
class="sv-ts__trigger"
|
|
139
|
-
class:is-invalid={invalid}
|
|
140
|
-
{disabled}
|
|
141
|
-
onclick={() => ts.toggle()}
|
|
142
|
-
{...ts.triggerProps('tree')}
|
|
143
|
-
{...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
|
|
144
|
-
>
|
|
145
|
-
<span class="sv-ts__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
|
|
146
|
-
<span class="sv-ts__arrow" aria-hidden="true">▾</span>
|
|
147
|
-
</button>
|
|
148
|
-
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
149
|
-
</div>
|
|
150
|
-
</SvField>
|
|
151
|
-
|
|
152
|
-
{#if ts.open}
|
|
153
|
-
<div
|
|
154
|
-
bind:this={panelEl}
|
|
155
|
-
class="sv-ts__panel"
|
|
156
|
-
dir={dir}
|
|
157
|
-
use:portalToBody
|
|
158
|
-
use:popIn={{ up: ts.rect.openUpward }}
|
|
159
|
-
style:position="fixed"
|
|
160
|
-
style:top={`${ts.rect.top}px`}
|
|
161
|
-
style:left={`${ts.rect.left}px`}
|
|
162
|
-
style:min-width={`${ts.rect.width}px`}
|
|
163
|
-
>
|
|
164
|
-
<ul
|
|
165
|
-
bind:this={treeEl}
|
|
166
|
-
id={ts.panelId}
|
|
167
|
-
class="sv-ts__tree"
|
|
168
|
-
role="tree"
|
|
169
|
-
aria-label={label ?? ariaLabel ?? 'Tree'}
|
|
170
|
-
aria-activedescendant={ts.activeDescendant()}
|
|
171
|
-
tabindex="0"
|
|
172
|
-
style:max-height={`${ts.rect.maxHeight}px`}
|
|
173
|
-
onkeydown={ts.onPanelKeydown}
|
|
174
|
-
>
|
|
175
|
-
{#each rows as row, i (row.node.value)}
|
|
176
|
-
<li
|
|
177
|
-
class="sv-ts__row"
|
|
178
|
-
class:is-active={ts.isActive(i)}
|
|
179
|
-
class:is-selected={row.node.value === value}
|
|
180
|
-
class:is-disabled={row.node.disabled}
|
|
181
|
-
role="treeitem"
|
|
182
|
-
aria-selected={row.node.value === value}
|
|
183
|
-
aria-expanded={row.hasChildren ? row.expanded : undefined}
|
|
184
|
-
style:padding-inline-start={`${8 + row.depth * 16}px`}
|
|
185
|
-
onclick={() => onRowClick(row)}
|
|
186
|
-
{...ts.itemProps(i)}
|
|
187
|
-
>
|
|
188
|
-
{#if row.hasChildren}
|
|
189
|
-
<!-- Same chevron as SvTree and the grid's expanders. -->
|
|
190
|
-
<span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">
|
|
191
|
-
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
|
|
192
|
-
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
|
193
|
-
</span>
|
|
194
|
-
{:else}
|
|
195
|
-
<span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
|
|
196
|
-
{/if}
|
|
197
|
-
<span class="sv-ts__label">{row.node.label}</span>
|
|
198
|
-
</li>
|
|
199
|
-
{/each}
|
|
200
|
-
</ul>
|
|
201
|
-
</div>
|
|
202
|
-
{/if}
|
|
203
|
-
|
|
204
|
-
<style>
|
|
205
|
-
.sv-ts { --_accent: var(--sg-accent, #2563eb); display: inline-flex; width: 240px; }
|
|
206
|
-
.sv-ts.is-disabled { opacity: 0.6; }
|
|
207
|
-
.sv-ts__trigger {
|
|
208
|
-
flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
|
|
209
|
-
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
210
|
-
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
211
|
-
font: inherit; cursor: pointer; padding: 0 9px;
|
|
212
|
-
}
|
|
213
|
-
.sv-ts--sm .sv-ts__trigger { height: 28px; font-size: 12px; }
|
|
214
|
-
.sv-ts--md .sv-ts__trigger { height: 34px; font-size: 13px; }
|
|
215
|
-
.sv-ts--lg .sv-ts__trigger { height: 40px; font-size: 15px; }
|
|
216
|
-
.sv-ts__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
217
|
-
.sv-ts__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
218
|
-
.sv-ts__value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
219
|
-
.sv-ts__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
|
|
220
|
-
.sv-ts__arrow { color: var(--sg-muted, #64748b); font-size: 10px; }
|
|
221
|
-
|
|
222
|
-
:global(.sv-ts__panel) {
|
|
223
|
-
z-index: 2147483646; max-height: 320px; max-width: 92vw; overflow: hidden;
|
|
224
|
-
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
225
|
-
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
226
|
-
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
227
|
-
}
|
|
228
|
-
:global(.sv-ts__tree) { list-style: none; margin: 0; padding: 4px; overflow: auto; max-height: 320px; outline: none; }
|
|
229
|
-
:global(.sv-ts__row) { display: flex; align-items: center; gap: 4px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
|
230
|
-
:global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
231
|
-
:global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
232
|
-
:global(.sv-ts__row.is-disabled) { opacity: 0.5; cursor: not-allowed; }
|
|
233
|
-
:global(.sv-ts__chev) { width: 14px; flex: none; display: inline-flex; align-items: center; justify-content: center; color: var(--sg-muted, #64748b); transition: transform 0.12s; }
|
|
234
|
-
:global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
|
|
235
|
-
:global(.sv-ts__chev--leaf) { visibility: hidden; }
|
|
236
|
-
:global(.sv-ts__label) { flex: 1; white-space: nowrap; }
|
|
237
|
-
/* Fill the container - see `block` in SvEditorProps. */
|
|
238
|
-
.sv-ts.is-block { width: 100%; max-width: 100%; }
|
|
239
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type { TreeSelectNode } from './ui-app.types'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* SvTreeSelect - a single-select dropdown that shows an indented, collapsible
|
|
8
|
+
* tree in its panel (pick a node from a hierarchy). The open / position /
|
|
9
|
+
* dismiss / roving / keyboard / ARIA mechanics come from the shared
|
|
10
|
+
* `createPopoverSelect` core (with ArrowLeft/Right handled via onExtraKey for
|
|
11
|
+
* expand/collapse); this component adds the tree flattening + rendering. Covers
|
|
12
|
+
* the "cascader / tree select" pattern without depending on SvTree.
|
|
13
|
+
*/
|
|
14
|
+
import type { TreeSelectNode } from './ui-app.types'
|
|
15
|
+
import SvField from './SvField.svelte'
|
|
16
|
+
import { editorAria, nextEditorId, type SvEditorProps } from './editor-contract'
|
|
17
|
+
import { portalToBody, popIn } from './popover'
|
|
18
|
+
import { createPopoverSelect } from './createPopoverSelect.svelte'
|
|
19
|
+
import { flattenVisible, findNodePath } from './tree-select'
|
|
20
|
+
|
|
21
|
+
type Props = Pick<
|
|
22
|
+
SvEditorProps,
|
|
23
|
+
'disabled' | 'readonly' | 'label' | 'hint' | 'error' | 'required' | 'invalid' | 'block' | 'size' | 'id' | 'name' | 'dir' | 'ariaLabel' | 'loading'
|
|
24
|
+
> & {
|
|
25
|
+
nodes: ReadonlyArray<TreeSelectNode>
|
|
26
|
+
value?: string | number | null
|
|
27
|
+
onChange?: (value: string | number) => void
|
|
28
|
+
onCommit?: (value: string | number) => void
|
|
29
|
+
onCancel?: () => void
|
|
30
|
+
placeholder?: string
|
|
31
|
+
showPath?: boolean
|
|
32
|
+
expandedIds?: ReadonlyArray<string | number>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
nodes,
|
|
37
|
+
value = $bindable<string | number | null>(null),
|
|
38
|
+
onChange,
|
|
39
|
+
placeholder = 'Select…',
|
|
40
|
+
showPath = false,
|
|
41
|
+
expandedIds = [],
|
|
42
|
+
disabled = false,
|
|
43
|
+
readonly = false,
|
|
44
|
+
label,
|
|
45
|
+
hint,
|
|
46
|
+
error,
|
|
47
|
+
required = false,
|
|
48
|
+
invalid = false,
|
|
49
|
+
block = false,
|
|
50
|
+
size = 'md',
|
|
51
|
+
id,
|
|
52
|
+
name,
|
|
53
|
+
dir,
|
|
54
|
+
ariaLabel,
|
|
55
|
+
loading = false,
|
|
56
|
+
}: Props = $props()
|
|
57
|
+
|
|
58
|
+
const autoId = nextEditorId('sv-ts')
|
|
59
|
+
const uid = $derived(id ?? autoId)
|
|
60
|
+
|
|
61
|
+
let expanded = $state(new Set<string | number>(expandedIds))
|
|
62
|
+
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
63
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
64
|
+
let treeEl = $state<HTMLUListElement | null>(null)
|
|
65
|
+
|
|
66
|
+
const rows = $derived(flattenVisible(nodes, expanded))
|
|
67
|
+
const selectedPath = $derived(value == null ? null : findNodePath(nodes, value))
|
|
68
|
+
const triggerText = $derived(
|
|
69
|
+
!selectedPath
|
|
70
|
+
? ''
|
|
71
|
+
: showPath
|
|
72
|
+
? selectedPath.map((n) => n.label).join(' / ')
|
|
73
|
+
: selectedPath[selectedPath.length - 1]!.label,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
function setExpanded(v: string | number, on: boolean) {
|
|
77
|
+
const next = new Set(expanded)
|
|
78
|
+
on ? next.add(v) : next.delete(v)
|
|
79
|
+
expanded = next
|
|
80
|
+
}
|
|
81
|
+
function toggleExpand(v: string | number) {
|
|
82
|
+
setExpanded(v, !expanded.has(v))
|
|
83
|
+
}
|
|
84
|
+
function pick(node: TreeSelectNode) {
|
|
85
|
+
if (node.disabled) return
|
|
86
|
+
value = node.value // enables bind:value
|
|
87
|
+
onChange?.(node.value)
|
|
88
|
+
ts.closePanel()
|
|
89
|
+
}
|
|
90
|
+
function onRowClick(row: { node: TreeSelectNode; hasChildren: boolean }) {
|
|
91
|
+
if (row.hasChildren) toggleExpand(row.node.value)
|
|
92
|
+
else pick(row.node)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ts = createPopoverSelect({
|
|
96
|
+
itemCount: () => rows.length,
|
|
97
|
+
disabled: (i) => !!rows[i]?.node.disabled,
|
|
98
|
+
readonly: () => readonly,
|
|
99
|
+
onSelect: (i) => { const row = rows[i]; if (row) onRowClick(row) },
|
|
100
|
+
getTrigger: () => triggerEl,
|
|
101
|
+
getPanel: () => panelEl,
|
|
102
|
+
getInitialFocus: () => treeEl,
|
|
103
|
+
closeOnSelect: () => false, // onRowClick handles pick + close (branches stay open)
|
|
104
|
+
initialActive: () => Math.max(0, rows.findIndex((r) => r.node.value === value)),
|
|
105
|
+
minWidth: () => 220,
|
|
106
|
+
estimatedHeight: () => 300,
|
|
107
|
+
onExtraKey: (e, act) => {
|
|
108
|
+
const row = rows[act]
|
|
109
|
+
if (!row) return false
|
|
110
|
+
// "expand" opens the node or steps into the first child; "collapse" closes
|
|
111
|
+
// it or jumps to the parent - same fallback as createTree's core.
|
|
112
|
+
const expand = () => {
|
|
113
|
+
if (row.hasChildren && !row.expanded) setExpanded(row.node.value, true)
|
|
114
|
+
else if (row.hasChildren && row.expanded) ts.active = Math.min(act + 1, rows.length - 1)
|
|
115
|
+
}
|
|
116
|
+
const collapse = () => {
|
|
117
|
+
if (row.hasChildren && row.expanded) setExpanded(row.node.value, false)
|
|
118
|
+
else {
|
|
119
|
+
let pi = act - 1
|
|
120
|
+
while (pi >= 0 && rows[pi]!.depth >= row.depth) pi--
|
|
121
|
+
if (pi >= 0) ts.active = pi
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (e.key === 'ArrowRight') { e.preventDefault(); expand(); return true }
|
|
125
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); collapse(); return true }
|
|
126
|
+
return false
|
|
127
|
+
},
|
|
128
|
+
idPrefix: 'sv-ts',
|
|
129
|
+
})
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir} {loading}>
|
|
133
|
+
<div class="sv-ts sv-ts--{size}"
|
|
134
|
+
class:is-block={block} class:is-disabled={disabled}>
|
|
135
|
+
<button
|
|
136
|
+
bind:this={triggerEl}
|
|
137
|
+
type="button"
|
|
138
|
+
class="sv-ts__trigger"
|
|
139
|
+
class:is-invalid={invalid}
|
|
140
|
+
{disabled}
|
|
141
|
+
onclick={() => ts.toggle()}
|
|
142
|
+
{...ts.triggerProps('tree')}
|
|
143
|
+
{...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
|
|
144
|
+
>
|
|
145
|
+
<span class="sv-ts__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
|
|
146
|
+
<span class="sv-ts__arrow" aria-hidden="true">▾</span>
|
|
147
|
+
</button>
|
|
148
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
149
|
+
</div>
|
|
150
|
+
</SvField>
|
|
151
|
+
|
|
152
|
+
{#if ts.open}
|
|
153
|
+
<div
|
|
154
|
+
bind:this={panelEl}
|
|
155
|
+
class="sv-ts__panel"
|
|
156
|
+
dir={dir}
|
|
157
|
+
use:portalToBody
|
|
158
|
+
use:popIn={{ up: ts.rect.openUpward }}
|
|
159
|
+
style:position="fixed"
|
|
160
|
+
style:top={`${ts.rect.top}px`}
|
|
161
|
+
style:left={`${ts.rect.left}px`}
|
|
162
|
+
style:min-width={`${ts.rect.width}px`}
|
|
163
|
+
>
|
|
164
|
+
<ul
|
|
165
|
+
bind:this={treeEl}
|
|
166
|
+
id={ts.panelId}
|
|
167
|
+
class="sv-ts__tree"
|
|
168
|
+
role="tree"
|
|
169
|
+
aria-label={label ?? ariaLabel ?? 'Tree'}
|
|
170
|
+
aria-activedescendant={ts.activeDescendant()}
|
|
171
|
+
tabindex="0"
|
|
172
|
+
style:max-height={`${ts.rect.maxHeight}px`}
|
|
173
|
+
onkeydown={ts.onPanelKeydown}
|
|
174
|
+
>
|
|
175
|
+
{#each rows as row, i (row.node.value)}
|
|
176
|
+
<li
|
|
177
|
+
class="sv-ts__row"
|
|
178
|
+
class:is-active={ts.isActive(i)}
|
|
179
|
+
class:is-selected={row.node.value === value}
|
|
180
|
+
class:is-disabled={row.node.disabled}
|
|
181
|
+
role="treeitem"
|
|
182
|
+
aria-selected={row.node.value === value}
|
|
183
|
+
aria-expanded={row.hasChildren ? row.expanded : undefined}
|
|
184
|
+
style:padding-inline-start={`${8 + row.depth * 16}px`}
|
|
185
|
+
onclick={() => onRowClick(row)}
|
|
186
|
+
{...ts.itemProps(i)}
|
|
187
|
+
>
|
|
188
|
+
{#if row.hasChildren}
|
|
189
|
+
<!-- Same chevron as SvTree and the grid's expanders. -->
|
|
190
|
+
<span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">
|
|
191
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
|
|
192
|
+
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
|
193
|
+
</span>
|
|
194
|
+
{:else}
|
|
195
|
+
<span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
|
|
196
|
+
{/if}
|
|
197
|
+
<span class="sv-ts__label">{row.node.label}</span>
|
|
198
|
+
</li>
|
|
199
|
+
{/each}
|
|
200
|
+
</ul>
|
|
201
|
+
</div>
|
|
202
|
+
{/if}
|
|
203
|
+
|
|
204
|
+
<style>
|
|
205
|
+
.sv-ts { --_accent: var(--sg-accent, #2563eb); display: inline-flex; width: 240px; }
|
|
206
|
+
.sv-ts.is-disabled { opacity: 0.6; }
|
|
207
|
+
.sv-ts__trigger {
|
|
208
|
+
flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; text-align: start;
|
|
209
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
210
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
211
|
+
font: inherit; cursor: pointer; padding: 0 9px;
|
|
212
|
+
}
|
|
213
|
+
.sv-ts--sm .sv-ts__trigger { height: 28px; font-size: 12px; }
|
|
214
|
+
.sv-ts--md .sv-ts__trigger { height: 34px; font-size: 13px; }
|
|
215
|
+
.sv-ts--lg .sv-ts__trigger { height: 40px; font-size: 15px; }
|
|
216
|
+
.sv-ts__trigger:focus-visible { outline: none; border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
217
|
+
.sv-ts__trigger.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
218
|
+
.sv-ts__value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
219
|
+
.sv-ts__value.is-placeholder { color: var(--sg-muted, #94a3b8); }
|
|
220
|
+
.sv-ts__arrow { color: var(--sg-muted, #64748b); font-size: 10px; }
|
|
221
|
+
|
|
222
|
+
:global(.sv-ts__panel) {
|
|
223
|
+
z-index: 2147483646; max-height: 320px; max-width: 92vw; overflow: hidden;
|
|
224
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
225
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
226
|
+
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
227
|
+
}
|
|
228
|
+
:global(.sv-ts__tree) { list-style: none; margin: 0; padding: 4px; overflow: auto; max-height: 320px; outline: none; }
|
|
229
|
+
:global(.sv-ts__row) { display: flex; align-items: center; gap: 4px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
|
230
|
+
:global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
231
|
+
:global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
|
|
232
|
+
:global(.sv-ts__row.is-disabled) { opacity: 0.5; cursor: not-allowed; }
|
|
233
|
+
:global(.sv-ts__chev) { width: 14px; flex: none; display: inline-flex; align-items: center; justify-content: center; color: var(--sg-muted, #64748b); transition: transform 0.12s; }
|
|
234
|
+
:global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
|
|
235
|
+
:global(.sv-ts__chev--leaf) { visibility: hidden; }
|
|
236
|
+
:global(.sv-ts__label) { flex: 1; white-space: nowrap; }
|
|
237
|
+
/* Fill the container - see `block` in SvEditorProps. */
|
|
238
|
+
.sv-ts.is-block { width: 100%; max-width: 100%; }
|
|
239
|
+
</style>
|
package/dist/ai.d.ts
CHANGED
|
@@ -266,8 +266,17 @@ export type AIAnomalyOptions = {
|
|
|
266
266
|
* export just those".
|
|
267
267
|
*/
|
|
268
268
|
export declare function aiFindAnomalies<TFeatures extends TableFeatures, TData extends RowData>(api: SvGridApi<TFeatures, TData>, opts?: AIAnomalyOptions): Promise<AIAnomalyResult>;
|
|
269
|
-
/**
|
|
270
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Chart shapes the model may choose from when planning a visualisation.
|
|
271
|
+
*
|
|
272
|
+
* These are exactly the types the built-in panel can build from a dimension, a
|
|
273
|
+
* measure and an optional split - which is all a chart plan carries. Two
|
|
274
|
+
* families are deliberately absent, because a plan cannot express them:
|
|
275
|
+
* scatter needs a SECOND measure for its y axis, and candlestick / OHLC need
|
|
276
|
+
* four under first/max/min/last. Asking for either would produce a plan the
|
|
277
|
+
* panel renders as an empty frame, which is worse than not offering it.
|
|
278
|
+
*/
|
|
279
|
+
export type AIChartType = 'bar' | 'line' | 'area' | 'pie' | 'treemap' | 'funnel' | 'waterfall' | 'radar' | 'heatmap' | 'boxplot' | 'gauge' | 'calendar' | 'sankey';
|
|
271
280
|
/** A chart the model proposed: its type, and the fields to plot. */
|
|
272
281
|
export type AIChartPlan = {
|
|
273
282
|
type: AIChartType;
|
package/dist/ai.js
CHANGED
|
@@ -511,7 +511,7 @@ export async function aiChart(api, query, opts = {}) {
|
|
|
511
511
|
`into a strict-JSON chart plan the grid can render.\n\n` +
|
|
512
512
|
`Columns:\n${schemaToPromptBlock(schema)}\n\n` +
|
|
513
513
|
`Output JSON schema:\n` +
|
|
514
|
-
`{ "type": "bar"|"line"|"area"|"pie", ` +
|
|
514
|
+
`{ "type": "bar"|"line"|"area"|"pie"|"treemap"|"funnel"|"waterfall"|"radar"|"heatmap"|"boxplot"|"gauge"|"calendar"|"sankey", ` +
|
|
515
515
|
`"dimension": "<a categorical column to group by>", ` +
|
|
516
516
|
`"series": "<a second categorical column to split into series, or null>", ` +
|
|
517
517
|
`"measure": "<a numeric column to aggregate>", ` +
|
|
@@ -523,6 +523,16 @@ export async function aiChart(api, query, opts = {}) {
|
|
|
523
523
|
`"rationale": "<one-sentence explanation>" }\n\n` +
|
|
524
524
|
`Rules: pick "dimension" and "series" from text/date columns and "measure" ` +
|
|
525
525
|
`from a numeric column; use only column names from the list above; ` +
|
|
526
|
+
`default to "bar" unless the request clearly wants another type. ` +
|
|
527
|
+
`Type hints: share/proportion/breakdown -> pie; nested/hierarchy/"what makes ` +
|
|
528
|
+
`up" -> treemap; drop-off/conversion/stages/pipeline -> funnel; ` +
|
|
529
|
+
`running total/contribution/bridge -> waterfall; comparing several measures ` +
|
|
530
|
+
`per item -> radar; a grid of two dimensions -> heatmap (needs "series"); ` +
|
|
531
|
+
`spread/distribution/variance/outliers/percentiles -> boxplot; a single ` +
|
|
532
|
+
`number vs a target -> gauge (leave "dimension" null); a year of daily ` +
|
|
533
|
+
`activity -> calendar (dimension must be a date column); flow between two ` +
|
|
534
|
+
`things -> sankey (needs "series": dimension is the source, series the ` +
|
|
535
|
+
`target). ` +
|
|
526
536
|
`"stacked"/"stack" -> stacked:true; "log"/"logarithmic" -> logScale:true; ` +
|
|
527
537
|
`"over time"/"by date"/a date dimension -> timeAxis:true; ` +
|
|
528
538
|
`money/revenue/price/$ -> valueFormat:"currency"; rate/%/share -> valueFormat:"percent"; ` +
|
|
@@ -537,7 +547,11 @@ export async function aiChart(api, query, opts = {}) {
|
|
|
537
547
|
});
|
|
538
548
|
const valid = new Set(schema.map((c) => c.field));
|
|
539
549
|
const numericFields = new Set(schema.filter((c) => c.type === 'number').map((c) => c.field));
|
|
540
|
-
const CHART_TYPES = [
|
|
550
|
+
const CHART_TYPES = [
|
|
551
|
+
'bar', 'line', 'area', 'pie',
|
|
552
|
+
'treemap', 'funnel', 'waterfall', 'radar',
|
|
553
|
+
'heatmap', 'boxplot', 'gauge', 'calendar', 'sankey',
|
|
554
|
+
];
|
|
541
555
|
plan.type = CHART_TYPES.includes(plan.type) ? plan.type : 'bar';
|
|
542
556
|
plan.dimension = plan.dimension && valid.has(plan.dimension) ? plan.dimension : null;
|
|
543
557
|
plan.series = plan.series && valid.has(plan.series) && plan.series !== plan.dimension ? plan.series : null;
|
|
@@ -545,6 +559,21 @@ export async function aiChart(api, query, opts = {}) {
|
|
|
545
559
|
plan.measure && numericFields.has(plan.measure)
|
|
546
560
|
? plan.measure
|
|
547
561
|
: (schema.find((c) => c.type === 'number')?.field ?? null);
|
|
562
|
+
// Shape check. Some types need more than a dimension and a measure, and a
|
|
563
|
+
// plan that does not satisfy one renders an empty frame - the model asked for
|
|
564
|
+
// a chart the data cannot make. Fall back to a bar, which any dimension and
|
|
565
|
+
// measure can draw, rather than showing nothing and blaming the request.
|
|
566
|
+
const dateFields = new Set(schema.filter((c) => c.type === 'date').map((c) => c.field));
|
|
567
|
+
const needsSplit = plan.type === 'heatmap' || plan.type === 'sankey';
|
|
568
|
+
if (needsSplit && !plan.series)
|
|
569
|
+
plan.type = 'bar';
|
|
570
|
+
if (plan.type === 'calendar' && !(plan.dimension && dateFields.has(plan.dimension)))
|
|
571
|
+
plan.type = 'bar';
|
|
572
|
+
// A gauge is one number: it has no category axis, so a dimension is noise.
|
|
573
|
+
if (plan.type === 'gauge')
|
|
574
|
+
plan.dimension = null;
|
|
575
|
+
if (plan.type !== 'gauge' && !plan.dimension)
|
|
576
|
+
plan.type = 'bar';
|
|
548
577
|
plan.reduce = ['sum', 'avg', 'count'].includes(plan.reduce) ? plan.reduce : 'sum';
|
|
549
578
|
plan.stacked = plan.stacked === true;
|
|
550
579
|
plan.logScale = plan.logScale === true;
|
|
@@ -640,10 +669,22 @@ function buildMockChart(prompt) {
|
|
|
640
669
|
const typed = extractFieldTypes(prompt);
|
|
641
670
|
const textFields = typed.filter((f) => f.type === 'string' || f.type === 'date').map((f) => f.name);
|
|
642
671
|
const numberFields = typed.filter((f) => f.type === 'number').map((f) => f.name);
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
672
|
+
// Mirrors the type hints in the real prompt, so the bundled offline provider
|
|
673
|
+
// can reach the same charts a model can. Ordered most-specific first: "share
|
|
674
|
+
// of the pipeline" is a funnel request, not a pie one.
|
|
675
|
+
const type = /\bfunnel\b|drop.?off|conversion|\bstages?\b|pipeline/.test(q) ? 'funnel'
|
|
676
|
+
: /\bwaterfall\b|running total|contribution|\bbridge\b/.test(q) ? 'waterfall'
|
|
677
|
+
: /\btree ?map\b|nested|hierarch/.test(q) ? 'treemap'
|
|
678
|
+
: /\bradar\b|spider|multi.?measure/.test(q) ? 'radar'
|
|
679
|
+
: /\bheat ?map\b/.test(q) ? 'heatmap'
|
|
680
|
+
: /\bbox ?plot\b|spread|distribution|variance|outlier|percentile|quartile/.test(q) ? 'boxplot'
|
|
681
|
+
: /\bgauge\b|\bdial\b|vs target|against target/.test(q) ? 'gauge'
|
|
682
|
+
: /\bcalendar\b|daily activity|year of/.test(q) ? 'calendar'
|
|
683
|
+
: /\bsankey\b|\bflow\b/.test(q) ? 'sankey'
|
|
684
|
+
: /\bline\b|\btrend\b|over time/.test(q) ? 'line'
|
|
685
|
+
: /\barea\b/.test(q) ? 'area'
|
|
686
|
+
: /\bpie\b|share|proportion|breakdown/.test(q) ? 'pie'
|
|
687
|
+
: 'bar';
|
|
647
688
|
const byMatch = q.match(/\bby\s+([a-z0-9_ ]+?)(?:\s+(?:split|stacked|grouped|by|as|,|$))/);
|
|
648
689
|
const pickText = (hint) => {
|
|
649
690
|
if (hint) {
|
|
@@ -654,7 +695,10 @@ function buildMockChart(prompt) {
|
|
|
654
695
|
return null;
|
|
655
696
|
};
|
|
656
697
|
const dimension = pickText(byMatch?.[1]) ?? textFields[0] ?? null;
|
|
657
|
-
|
|
698
|
+
// Heatmap and sankey are two-dimension charts: without a split the shape
|
|
699
|
+
// check downgrades them to a bar, so the mock would answer "heatmap" with a
|
|
700
|
+
// bar chart and look broken rather than offline.
|
|
701
|
+
const series = /\b(split|stack(?:ed)?|by product|by category|per)\b/.test(q) || type === 'heatmap' || type === 'sankey'
|
|
658
702
|
? (textFields.find((t) => t !== dimension) ?? null)
|
|
659
703
|
: null;
|
|
660
704
|
const measure = numberFields.find((n) => q.includes(n.toLowerCase())) ?? numberFields[0] ?? null;
|