@svgrid/grid 3.0.1 → 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.
Files changed (77) hide show
  1. package/dist/GridFooter.svelte +14 -5
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/GridMenus.svelte +18 -10
  4. package/dist/GridMenus.svelte.d.ts +2 -1
  5. package/dist/SvGrid.controller.svelte.d.ts +17 -0
  6. package/dist/SvGrid.controller.svelte.js +128 -14
  7. package/dist/SvGrid.css +79 -10
  8. package/dist/SvGrid.svelte +191 -186
  9. package/dist/SvGrid.types.d.ts +42 -1
  10. package/dist/SvGridChart.svelte +571 -41
  11. package/dist/SvGridChart.svelte.d.ts +63 -1
  12. package/dist/SvGridChartPanel.svelte +169 -21
  13. package/dist/SvGridChartView.svelte +9 -0
  14. package/dist/SvGridChartView.svelte.d.ts +7 -0
  15. package/dist/SvGroupCell.svelte +28 -3
  16. package/dist/SvGroupCell.svelte.d.ts +8 -0
  17. package/dist/SvRowGroupPanel.svelte +20 -4
  18. package/dist/SvRowGroupPanel.svelte.d.ts +8 -0
  19. package/dist/ai.d.ts +11 -2
  20. package/dist/ai.js +51 -7
  21. package/dist/cdn/GridMenus-DyFA-QxA.js +602 -0
  22. package/dist/cdn/{GridMenus-DFB6iwF8.js → GridMenus-M2XP9t0_.js} +152 -150
  23. package/dist/cdn/SvGridChart-DAb9RbRs.js +1480 -0
  24. package/dist/cdn/SvGridChart-Yl-IXZFO.js +1481 -0
  25. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +834 -0
  26. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +810 -0
  27. package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-CxfakxR0.js} +5 -3
  28. package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-Dm_YEx9L.js} +5 -3
  29. package/dist/cdn/cell-formatting-C2Hf5gqd.js +86 -0
  30. package/dist/cdn/chart-CKJRCPOk.js +1652 -0
  31. package/dist/cdn/{export-format-Cv1Dll6k.js → export-format-CUDLy2yn.js} +84 -168
  32. package/dist/cdn/{src-C8QjSaq-.js → src-BJ2opd0j.js} +3608 -3414
  33. package/dist/cdn/{src-D0hO2eYN.js → src-DNbz94Ct.js} +8617 -8423
  34. package/dist/cdn/svgrid.js +16 -15
  35. package/dist/cdn/svgrid.svelte-external.js +16 -15
  36. package/dist/cell-formatting.d.ts +7 -0
  37. package/dist/cell-formatting.js +7 -1
  38. package/dist/chart-export.d.ts +23 -2
  39. package/dist/chart-export.js +80 -6
  40. package/dist/chart.d.ts +410 -18
  41. package/dist/chart.js +824 -24
  42. package/dist/grid-icons.d.ts +49 -0
  43. package/dist/grid-icons.js +86 -0
  44. package/dist/index.d.ts +3 -2
  45. package/dist/index.js +3 -2
  46. package/package.json +11 -11
  47. package/src/GridFooter.svelte +14 -5
  48. package/src/GridMenus.svelte +18 -10
  49. package/src/SvGrid.controller.svelte.ts +146 -13
  50. package/src/SvGrid.css +79 -10
  51. package/src/SvGrid.svelte +191 -186
  52. package/src/SvGrid.types.ts +42 -1
  53. package/src/SvGridChart.svelte +571 -41
  54. package/src/SvGridChart.test.ts +243 -1
  55. package/src/SvGridChartPanel.svelte +169 -21
  56. package/src/SvGridChartView.svelte +9 -0
  57. package/src/SvGroupCell.svelte +28 -3
  58. package/src/SvRowGroupPanel.svelte +20 -4
  59. package/src/ai.test.ts +60 -1
  60. package/src/ai.ts +59 -6
  61. package/src/cell-formatting.ts +7 -1
  62. package/src/chart-export.test.ts +112 -0
  63. package/src/chart-export.ts +76 -8
  64. package/src/chart.test.ts +830 -1
  65. package/src/chart.ts +1053 -32
  66. package/src/grid-icons.test.ts +46 -0
  67. package/src/grid-icons.ts +184 -0
  68. package/src/icon-overrides.grid.test.ts +188 -0
  69. package/src/icon-seam.test.ts +100 -0
  70. package/src/index.ts +19 -0
  71. package/src/svgrid.charting.test.ts +120 -0
  72. package/dist/cdn/GridMenus-CYzOvmNQ.js +0 -600
  73. package/dist/cdn/SvGridChart-BEJmNNx9.js +0 -1203
  74. package/dist/cdn/SvGridChart-BwVos976.js +0 -1202
  75. package/dist/cdn/SvGridChartPanel-BOknOkrP.js +0 -561
  76. package/dist/cdn/SvGridChartPanel-D2PjII4O.js +0 -585
  77. package/dist/cdn/chart-i4XcXyHJ.js +0 -1214
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Grid chrome icons. Every glyph `<SvGrid>` draws for its own UI - sort
3
+ * indicators, the filter funnel, expanders, the pager arrows, tool-panel
4
+ * buttons - has a name here, and the `icons` prop replaces any subset of them.
5
+ *
6
+ * This mirrors `grid-messages.ts`: a name-keyed map with built-in defaults,
7
+ * where an unset name keeps the default. A partial map is all a consumer ever
8
+ * needs, so `icons` is safe to pass without knowing the full catalogue. That
9
+ * matters more than it looks - a single catch-all snippet would make the
10
+ * consumer responsible for every name the grid can ask for, and missing one
11
+ * renders an empty box. The `op-between` comment in SvGrid.svelte records what
12
+ * that looks like when it ships.
13
+ *
14
+ * Deliberately NOT in here:
15
+ * - the scrollbar stepper arrows. `sv-grid-scrollbar.ts` is a custom element
16
+ * that builds its SVG with `createElementNS` inside a shadow root, on
17
+ * purpose, so it stays compatible with strict CSP / Trusted Types. A Svelte
18
+ * snippet cannot render there. Theme it with the `--sg-scrollbar-*` tokens.
19
+ * - the checkbox tick and its indeterminate dash. Both are `::after`
20
+ * pseudo-elements drawn from borders (SvGrid.css), a form-control
21
+ * affordance rather than an icon, and they render once per visible row.
22
+ * - conditional formatting's `iconSet`. Those are value-driven data glyphs a
23
+ * consumer already chooses; a different concept that shares the word.
24
+ */
25
+ import type { Snippet } from 'svelte';
26
+ /**
27
+ * Every icon `<SvGrid>` draws for its own chrome. Pass any subset of these to
28
+ * the `icons` prop to swap the built-in glyph for your own markup.
29
+ */
30
+ export type GridIconName = 'sort' | 'sort-asc' | 'sort-desc' | 'filter' | 'menu' | 'group' | 'x' | 'chevron-down' | 'chevron-right' | 'autosize' | 'columns' | 'reset' | 'op-contains' | 'op-notContains' | 'op-equals' | 'op-notEquals' | 'op-startsWith' | 'op-endsWith' | 'op-regex' | 'op-in' | 'op-notIn' | 'op-greaterThan' | 'op-lessThan' | 'op-between' | 'op-isBlank' | 'op-isNotBlank' | 'pin-left' | 'pin-right' | 'unpin' | 'search' | 'tool-panel' | 'chart' | 'advanced-filter' | 'column-group-caret' | 'close' | 'clear' | 'remove' | 'row-number' | 'pinned-row-top' | 'pinned-row-bottom' | 'find-prev' | 'find-next' | 'move-up' | 'move-down' | 'group-add' | 'page-first' | 'page-prev' | 'page-next' | 'page-last' | 'page-size-caret' | 'drag-handle' | 'breadcrumb-separator';
31
+ /**
32
+ * Consumer icon overrides, keyed by name. Any subset; names you leave out keep
33
+ * their built-in glyph, so `icons` never has to be exhaustive.
34
+ */
35
+ export type GridIcons = Partial<Record<GridIconName, Snippet>>;
36
+ /**
37
+ * Icons whose built-in form is a character rather than an SVG path. Kept as
38
+ * data so `<SvGrid>`, its footer and the standalone `SvRowGroupPanel` render
39
+ * the same defaults instead of three copies drifting apart.
40
+ *
41
+ * Each is the exact character that shipped inline before, so a grid that sets
42
+ * no `icons` renders byte-for-byte what it always did.
43
+ */
44
+ export declare const GRID_ICON_GLYPHS: Partial<Record<GridIconName, string>>;
45
+ /**
46
+ * Every icon name, in catalogue order. Exported for tests and docs generation;
47
+ * the grid itself never reads it, so it costs a consumer's bundle nothing.
48
+ */
49
+ export declare const GRID_ICON_NAMES: ReadonlyArray<GridIconName>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Icons whose built-in form is a character rather than an SVG path. Kept as
3
+ * data so `<SvGrid>`, its footer and the standalone `SvRowGroupPanel` render
4
+ * the same defaults instead of three copies drifting apart.
5
+ *
6
+ * Each is the exact character that shipped inline before, so a grid that sets
7
+ * no `icons` renders byte-for-byte what it always did.
8
+ */
9
+ export const GRID_ICON_GLYPHS = {
10
+ close: '✕', // multiplication X
11
+ clear: '✕',
12
+ remove: '×', // multiplication sign
13
+ 'row-number': '#',
14
+ 'pinned-row-top': '↑', // upwards arrow
15
+ 'pinned-row-bottom': '↓', // downwards arrow
16
+ 'find-prev': '↑',
17
+ 'find-next': '↓',
18
+ 'move-up': '↑',
19
+ 'move-down': '↓',
20
+ 'group-add': '⊞', // squared plus
21
+ 'page-first': '⇤', // leftwards arrow to bar
22
+ 'page-prev': '‹', // single left-pointing angle quote
23
+ 'page-next': '›', // single right-pointing angle quote
24
+ 'page-last': '⇥', // rightwards arrow to bar
25
+ 'page-size-caret': '▾', // black down-pointing small triangle
26
+ 'drag-handle': '⠿', // braille pattern dots-123456, the grip
27
+ 'breadcrumb-separator': '›',
28
+ };
29
+ /**
30
+ * Every icon name, in catalogue order. Exported for tests and docs generation;
31
+ * the grid itself never reads it, so it costs a consumer's bundle nothing.
32
+ */
33
+ export const GRID_ICON_NAMES = [
34
+ 'sort',
35
+ 'sort-asc',
36
+ 'sort-desc',
37
+ 'filter',
38
+ 'menu',
39
+ 'group',
40
+ 'x',
41
+ 'chevron-down',
42
+ 'chevron-right',
43
+ 'autosize',
44
+ 'columns',
45
+ 'reset',
46
+ 'op-contains',
47
+ 'op-notContains',
48
+ 'op-equals',
49
+ 'op-notEquals',
50
+ 'op-startsWith',
51
+ 'op-endsWith',
52
+ 'op-regex',
53
+ 'op-in',
54
+ 'op-notIn',
55
+ 'op-greaterThan',
56
+ 'op-lessThan',
57
+ 'op-between',
58
+ 'op-isBlank',
59
+ 'op-isNotBlank',
60
+ 'pin-left',
61
+ 'pin-right',
62
+ 'unpin',
63
+ 'search',
64
+ 'tool-panel',
65
+ 'chart',
66
+ 'advanced-filter',
67
+ 'column-group-caret',
68
+ 'close',
69
+ 'clear',
70
+ 'remove',
71
+ 'row-number',
72
+ 'pinned-row-top',
73
+ 'pinned-row-bottom',
74
+ 'find-prev',
75
+ 'find-next',
76
+ 'move-up',
77
+ 'move-down',
78
+ 'group-add',
79
+ 'page-first',
80
+ 'page-prev',
81
+ 'page-next',
82
+ 'page-last',
83
+ 'page-size-caret',
84
+ 'drag-handle',
85
+ 'breadcrumb-separator',
86
+ ];
package/dist/index.d.ts CHANGED
@@ -193,8 +193,8 @@ export { default as SvChart } from './SvGridChart.svelte';
193
193
  export { default as SvChartPanel } from './SvGridChartPanel.svelte';
194
194
  export type { ChartingConfig, ChartAggregateRequest, ChartAggregateBucket } from './SvGrid.types.js';
195
195
  export type { GridLocalization } from './SvGrid.types.js';
196
- export { buildChart, formatChartValue, rowsToChartSpec, niceScale, niceLogScale, linearTrend, simpleMovingAverage, exponentialMovingAverage, computeOverlay, buildLinePath, sampleGradient, pickContrastText, DEFAULT_PALETTE, type ChartType, type ChartSpec, type ChartSeries, type ChartGeometry, type ChartBar, type ChartLine, type ChartLinePoint, type ChartPieSlice, type ChartSelection, type ChartReferenceLine, type ChartRefLineGeo, type ChartRefLineGeoV, type ChartScatterDot, type ScatterPoint, type NiceScale, type SeriesOverlay, type SeriesPattern, type ChartAnnotation, type ChartHeatmapCell, type ChartFunnelSegment, type ChartRadarSeries, type ChartRadarAxis, type ChartTreemapCell, type ChartCalendarCell, type ChartGaugeLayout, type ChartSankeyNode, type ChartSankeyLink, type ChartValueFormat, type TreeNode, } from './chart.js';
197
- export { chartToSvgString, downloadChartSvg, chartToPngBlob, downloadChartPng, chartSpecToCsv, downloadChartCsv, type ChartExportOptions, } from './chart-export.js';
196
+ export { buildChart, formatChartValue, rowsToChartSpec, rowsToBoxSpec, rowsToDirectSpec, boxStats, sliceChartWindow, chartScales, ordinalDateTicks, niceScale, niceLogScale, linearTrend, simpleMovingAverage, exponentialMovingAverage, computeOverlay, buildLinePath, sampleGradient, pickContrastText, DEFAULT_PALETTE, type ChartType, type ChartSpec, type ChartSeries, type ChartGeometry, type ChartScales, type ChartBar, type ChartCandle, type ChartBox, type ChartErrorBar, type BoxStats, type OhlcBar, type ChartLine, type ChartLinePoint, type ChartPieSlice, type ChartSelection, type ChartReferenceLine, type ChartRefLineGeo, type ChartRefLineGeoV, type ChartScatterDot, type ScatterPoint, type NiceScale, type SeriesOverlay, type SeriesPattern, type ChartAnnotation, type ChartHeatmapCell, type ChartFunnelSegment, type ChartRadarSeries, type ChartRadarAxis, type ChartTreemapCell, type ChartCalendarCell, type ChartGaugeLayout, type ChartSankeyNode, type ChartSankeyLink, type ChartValueFormat, type TreeNode, } from './chart.js';
197
+ export { chartToSvgString, downloadChartSvg, chartToPngBlob, downloadChartPng, chartCsvExportable, chartSpecToCsv, downloadChartCsv, type ChartExportOptions, } from './chart-export.js';
198
198
  export { buildSparkline, toSparklineValues, type SparklineConfig, type SparklineType, type SparklineGeometry, } from './sparkline.js';
199
199
  export { spreadsheetLayout, spansToMerges, type SpanColumn, type SpreadsheetActionOptions, type MergeSpec, type CellBorderSpec, type BorderSpec, } from './spreadsheet.js';
200
200
  export { rowResize, type RowResizeOptions } from './row-resize.js';
@@ -211,6 +211,7 @@ export { default as SvRowGroupPanel } from './SvRowGroupPanel.svelte';
211
211
  export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, type SavedView, type ViewStorage, type NamedViews, type AutoSavedViewOptions, } from './named-views.js';
212
212
  export { resolveCellFormat, computeColumnStat, lerpColor, rgba, contrastText, formatNeedsStats, formatsNeedingStats, type ConditionalFormat, type ConditionalFormatSpec, type ColorScaleFormat, type ColorScaleStop, type ScaleBounds, type DataBarFormat, type IconSetFormat, type RuleFormat, type ResolvedCellFormat, } from './conditional-formatting.js';
213
213
  export { defaultGridMessages, resolveGridMessages, type GridMessages, } from './grid-messages.js';
214
+ export { GRID_ICON_GLYPHS, GRID_ICON_NAMES, type GridIconName, type GridIcons, } from './grid-icons.js';
214
215
  export { insertGroupFooters, buildAutoGroupColumns, paginateGroupedRows, type GroupDisplayType, type GroupFooterOptions, type AutoGroupColumnSpec, type AutoGroupResult, type GroupPaginationOptions, type GroupPaginationResult, } from './group-display.js';
215
216
  export { getKeyboardIntent, getNextActiveCell, type GridKeyboardIntent } from './keyboard.js';
216
217
  export { createVirtualizer } from './virtualization/virtualizer.js';
package/dist/index.js CHANGED
@@ -230,8 +230,8 @@ export { default as SvGridChartPanel } from './SvGridChartPanel.svelte';
230
230
  // as aliases so existing imports keep working.
231
231
  export { default as SvChart } from './SvGridChart.svelte';
232
232
  export { default as SvChartPanel } from './SvGridChartPanel.svelte';
233
- export { buildChart, formatChartValue, rowsToChartSpec, niceScale, niceLogScale, linearTrend, simpleMovingAverage, exponentialMovingAverage, computeOverlay, buildLinePath, sampleGradient, pickContrastText, DEFAULT_PALETTE, } from './chart.js';
234
- export { chartToSvgString, downloadChartSvg, chartToPngBlob, downloadChartPng, chartSpecToCsv, downloadChartCsv, } from './chart-export.js';
233
+ export { buildChart, formatChartValue, rowsToChartSpec, rowsToBoxSpec, rowsToDirectSpec, boxStats, sliceChartWindow, chartScales, ordinalDateTicks, niceScale, niceLogScale, linearTrend, simpleMovingAverage, exponentialMovingAverage, computeOverlay, buildLinePath, sampleGradient, pickContrastText, DEFAULT_PALETTE, } from './chart.js';
234
+ export { chartToSvgString, downloadChartSvg, chartToPngBlob, downloadChartPng, chartCsvExportable, chartSpecToCsv, downloadChartCsv, } from './chart-export.js';
235
235
  export { buildSparkline, toSparklineValues, } from './sparkline.js';
236
236
  export { spreadsheetLayout, spansToMerges, } from './spreadsheet.js';
237
237
  // The two resize actions, exported as a matched pair. `<SvGrid>` drives both
@@ -253,6 +253,7 @@ export { default as SvRowGroupPanel } from './SvRowGroupPanel.svelte';
253
253
  export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, } from './named-views.js';
254
254
  export { resolveCellFormat, computeColumnStat, lerpColor, rgba, contrastText, formatNeedsStats, formatsNeedingStats, } from './conditional-formatting.js';
255
255
  export { defaultGridMessages, resolveGridMessages, } from './grid-messages.js';
256
+ export { GRID_ICON_GLYPHS, GRID_ICON_NAMES, } from './grid-icons.js';
256
257
  export { insertGroupFooters, buildAutoGroupColumns, paginateGroupedRows, } from './group-display.js';
257
258
  export { getKeyboardIntent, getNextActiveCell } from './keyboard.js';
258
259
  export { createVirtualizer } from './virtualization/virtualizer.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",
@@ -116,15 +116,6 @@
116
116
  "themes",
117
117
  "CHANGELOG.md"
118
118
  ],
119
- "scripts": {
120
- "clean": "rimraf ./build && rimraf ./dist",
121
- "test:eslint": "eslint ./src",
122
- "test:types": "svelte-check --tsconfig ./tsconfig.json",
123
- "test:lib": "vitest run --coverage",
124
- "test:build": "publint --strict",
125
- "build": "svelte-package --input ./src --output ./dist && node ../../tools/strip-dist-tests.mjs ./dist && node ../../tools/add-dist-extensions.mjs ./dist && node ./scripts/build-cdn.mjs && node ./scripts/build-themes.mjs",
126
- "lint": "eslint ./src"
127
- },
128
119
  "devDependencies": {
129
120
  "@sveltejs/package": "^2.5.7",
130
121
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
@@ -141,5 +132,14 @@
141
132
  },
142
133
  "dependencies": {
143
134
  "esm-env": "^1.2.2"
135
+ },
136
+ "scripts": {
137
+ "clean": "rimraf ./build && rimraf ./dist",
138
+ "test:eslint": "eslint ./src",
139
+ "test:types": "svelte-check --tsconfig ./tsconfig.json",
140
+ "test:lib": "vitest run --coverage",
141
+ "test:build": "publint --strict",
142
+ "build": "svelte-package --input ./src --output ./dist && node ../../tools/strip-dist-tests.mjs ./dist && node ../../tools/add-dist-extensions.mjs ./dist && node ./scripts/build-cdn.mjs && node ./scripts/build-themes.mjs",
143
+ "lint": "eslint ./src"
144
144
  }
145
- }
145
+ }
@@ -7,16 +7,25 @@
7
7
  import {
8
8
  fmtStat,
9
9
  } from "./SvGrid.helpers";
10
+ import type { Snippet } from "svelte";
11
+ import type { GridIconName } from "./grid-icons";
10
12
  import type { SvGridController } from "./SvGrid.controller.svelte";
11
13
 
12
14
  let {
13
15
  ctrl,
16
+ icon,
14
17
  pager = true,
15
18
  showStatus = true,
16
19
  top = false,
17
20
  pageSizeOptions,
18
21
  }: {
19
22
  ctrl: SvGridController<TFeatures, TData>;
23
+ /**
24
+ * The grid's icon resolver, so the pager arrows honour the `icons` prop.
25
+ * Required rather than optional: SvGrid is the only thing that mounts this
26
+ * component, so a missing icon would be a bug, not a supported call.
27
+ */
28
+ icon: Snippet<[GridIconName]>;
20
29
  /** Render the pagination controls in this instance. */
21
30
  pager?: boolean;
22
31
  /** Render the status bar in this instance. */
@@ -132,7 +141,7 @@
132
141
  onclick={openPageSize}
133
142
  >
134
143
  <span class="sv-grid-dropdown-label">{pageSize}</span>
135
- <span class="sv-grid-dropdown-caret" aria-hidden="true">▾</span>
144
+ <span class="sv-grid-dropdown-caret" aria-hidden="true">{@render icon("page-size-caret")}</span>
136
145
  </button>
137
146
  </div>
138
147
  {/if}
@@ -149,14 +158,14 @@
149
158
  class="sv-grid-pagination-btn"
150
159
  disabled={onFirst}
151
160
  onclick={() => goToPage(0)}
152
- aria-label={messages.firstPage}>⇤</button
161
+ aria-label={messages.firstPage}>{@render icon("page-first")}</button
153
162
  >
154
163
  <button
155
164
  type="button"
156
165
  class="sv-grid-pagination-btn"
157
166
  disabled={onFirst}
158
167
  onclick={() => changePage(-1)}
159
- aria-label={messages.prevPage}>‹</button
168
+ aria-label={messages.prevPage}>{@render icon("page-prev")}</button
160
169
  >
161
170
  <span class="sv-grid-pagination-label">
162
171
  {messages.page} <strong>{currentPage.toLocaleString()}</strong> {messages.of}
@@ -167,14 +176,14 @@
167
176
  class="sv-grid-pagination-btn"
168
177
  disabled={onLast}
169
178
  onclick={() => changePage(1)}
170
- aria-label={messages.nextPage}>›</button
179
+ aria-label={messages.nextPage}>{@render icon("page-next")}</button
171
180
  >
172
181
  <button
173
182
  type="button"
174
183
  class="sv-grid-pagination-btn"
175
184
  disabled={onLast}
176
185
  onclick={() => goToPage(pageCount - 1)}
177
- aria-label={messages.lastPage}>⇥</button
186
+ aria-label={messages.lastPage}>{@render icon("page-last")}</button
178
187
  >
179
188
  </div>
180
189
  </div>
@@ -10,6 +10,7 @@
10
10
  import "./sv-grid-scrollbar";
11
11
  import { onScrollOutside } from "./a11y/dismissable";
12
12
  import type { Snippet } from "svelte";
13
+ import type { GridIconName } from "./grid-icons";
13
14
  import SvListBox from "./SvListBox.svelte";
14
15
  import type {
15
16
  FilterOperator,
@@ -23,7 +24,7 @@
23
24
 
24
25
  import type { SvGridController } from "./SvGrid.controller.svelte";
25
26
 
26
- let { ctrl, icon }: { ctrl: SvGridController<TFeatures, TData>; icon: Snippet<[string]> } = $props();
27
+ let { ctrl, icon }: { ctrl: SvGridController<TFeatures, TData>; icon: Snippet<[GridIconName]> } = $props();
27
28
 
28
29
  // View facade: re-bind the controller's reactive members so the markup
29
30
  // (moved verbatim from SvGrid.svelte) stays identical.
@@ -403,7 +404,7 @@
403
404
  onclick={() => patchFilter(colId, { join: "OR" })}>OR</button>
404
405
  <button type="button" class="sv-grid-menu-join-x" title="Remove second condition"
405
406
  aria-label="Remove second condition"
406
- onclick={() => patchFilter(colId, { operator2: undefined, value2: undefined, valueTo2: undefined, join: undefined })}>×</button>
407
+ onclick={() => patchFilter(colId, { operator2: undefined, value2: undefined, valueTo2: undefined, join: undefined })}>{@render icon("remove")}</button>
407
408
  </div>
408
409
  <select
409
410
  class="sv-grid-menu-operator-select"
@@ -452,11 +453,18 @@
452
453
  <span class="sv-grid-menu-head-note">{pickedCount} of {totalCount}</span>
453
454
  {/if}
454
455
  </div>
455
- <input
456
- class="sv-grid-menu-search"
457
- placeholder="Search values..."
458
- bind:value={ctrl.columnMenuSearch}
459
- />
456
+ <!-- The magnifier used to be a background-image data URI in SvGrid.css
457
+ with a hard-coded stroke colour, so it ignored every theme and could
458
+ not be overridden. It is markup now, and the input keeps its own
459
+ class: focusFilterPanel() and a test both resolve `.sv-grid-menu-search`. -->
460
+ <span class="sv-grid-menu-search-wrap">
461
+ {@render icon("search")}
462
+ <input
463
+ class="sv-grid-menu-search"
464
+ placeholder="Search values..."
465
+ bind:value={ctrl.columnMenuSearch}
466
+ />
467
+ </span>
460
468
  <label class="sv-grid-facet sv-grid-facet-all">
461
469
  <input type="checkbox" checked={isAllFacetsChecked(colId)} onchange={() => toggleAllFacets(colId)} />
462
470
  <!-- Says what it will actually do: with a query typed it acts on the
@@ -593,7 +601,7 @@
593
601
  onclick={() => pinColumnLeft(menuColumnId)}
594
602
  >
595
603
  <span class="sv-grid-header-icon"
596
- >{@render icon("op-startsWith")}</span
604
+ >{@render icon("pin-left")}</span
597
605
  > Pin to left
598
606
  </button>
599
607
  <button
@@ -604,7 +612,7 @@
604
612
  onclick={() => pinColumnRight(menuColumnId)}
605
613
  >
606
614
  <span class="sv-grid-header-icon"
607
- >{@render icon("op-greaterThan")}</span
615
+ >{@render icon("pin-right")}</span
608
616
  > Pin to right
609
617
  </button>
610
618
  <button
@@ -614,7 +622,7 @@
614
622
  disabled={!menuPinSide}
615
623
  onclick={() => unpinColumn(menuColumnId)}
616
624
  >
617
- <span class="sv-grid-header-icon">{@render icon("x")}</span> Unpin column
625
+ <span class="sv-grid-header-icon">{@render icon("unpin")}</span> Unpin column
618
626
  </button>
619
627
  {/if}
620
628
  <div class="sv-grid-menu-sep"></div>
@@ -2028,6 +2028,18 @@ export function createSvGridController<
2028
2028
  dimensionId: string | null;
2029
2029
  measureId: string | null;
2030
2030
  seriesId: string | null | undefined;
2031
+ /** Scatter only: the Y measure. The X measure reuses `measureId`. */
2032
+ measure2Id: string | null;
2033
+ // These four were config-only until the panel grew controls for them, so
2034
+ // an author could set them and a user could not. Null inherits the config.
2035
+ /** Bar orientation. */
2036
+ orientation: "vertical" | "horizontal" | null;
2037
+ /** Normalise each category to 100 percent. Implies stacked. */
2038
+ stacked100: boolean | null;
2039
+ /** Pie only: draw as a donut. */
2040
+ donut: boolean | null;
2041
+ /** Series colours chosen in the panel. */
2042
+ palette: string[] | null;
2031
2043
  stacked: boolean | null;
2032
2044
  dataLabels: boolean | null;
2033
2045
  logScale: boolean | null;
@@ -2045,6 +2057,11 @@ export function createSvGridController<
2045
2057
  dimensionId: null,
2046
2058
  measureId: null,
2047
2059
  seriesId: undefined,
2060
+ measure2Id: null,
2061
+ orientation: null,
2062
+ stacked100: null,
2063
+ donut: null,
2064
+ palette: null,
2048
2065
  stacked: null,
2049
2066
  dataLabels: null,
2050
2067
  logScale: null,
@@ -2088,14 +2105,22 @@ export function createSvGridController<
2088
2105
  const chartableColumns = $derived.by(function chartableColumns_d() {
2089
2106
  const dims: Array<{ id: string; field: string; label: string }> = [];
2090
2107
  const measures: Array<{ id: string; field: string; label: string }> = [];
2108
+ // Dates are a subset of dims, tracked separately so the panel can tell
2109
+ // whether a calendar chart is possible at all before the user picks
2110
+ // anything. Offering a type that can only render blank is worse than not
2111
+ // offering it.
2112
+ const dates: Array<{ id: string; field: string; label: string }> = [];
2091
2113
  for (const col of allColumns) {
2092
2114
  const field = col.columnDef.field;
2093
2115
  if (!field) continue;
2094
2116
  const entry = { id: col.id, field, label: columnLabel(col) };
2095
2117
  if (isNumericColumn(col)) measures.push(entry);
2096
- else dims.push(entry);
2118
+ else {
2119
+ dims.push(entry);
2120
+ if (columnIsDate(col)) dates.push(entry);
2121
+ }
2097
2122
  }
2098
- return { dims, measures };
2123
+ return { dims, measures, dates };
2099
2124
  });
2100
2125
  const chartColumnDefaults = $derived.by(function chartColumnDefaults_d() {
2101
2126
  const { dims, measures } = chartableColumns;
@@ -2139,6 +2164,21 @@ export function createSvGridController<
2139
2164
  if (configured.length) return configured;
2140
2165
  return chartColumnDefaults.measureIds;
2141
2166
  });
2167
+ /** Scatter's Y measure. Defaults to the second numeric column, so picking
2168
+ * scatter plots something immediately rather than nothing. */
2169
+ const effectiveChartMeasure2Id = $derived(
2170
+ activeChart.measure2Id ??
2171
+ chartableColumns.measures.find((m) => m.id !== effectiveChartMeasureId)?.id ??
2172
+ null,
2173
+ );
2174
+ const effectiveChartOrientation = $derived(
2175
+ activeChart.orientation ?? chartCfg?.orientation ?? "vertical",
2176
+ );
2177
+ const effectiveChartStacked100 = $derived(
2178
+ activeChart.stacked100 ?? chartCfg?.stacked100 === true,
2179
+ );
2180
+ const effectiveChartDonut = $derived(activeChart.donut ?? !!chartCfg?.donut);
2181
+ const effectiveChartPalette = $derived(activeChart.palette ?? chartCfg?.palette ?? null);
2142
2182
  const effectiveChartStacked = $derived(chartStacked ?? chartCfg?.stacked ?? false);
2143
2183
  const effectiveChartDataLabels = $derived(chartDataLabels ?? chartCfg?.dataLabels ?? false);
2144
2184
  const effectiveChartLogScale = $derived(chartLogScale ?? chartCfg?.yScale === "log");
@@ -2162,8 +2202,13 @@ export function createSvGridController<
2162
2202
  ? "Count"
2163
2203
  : `${chartReduce === "avg" ? "Average" : "Sum"} of ${label}`;
2164
2204
  });
2165
- const chartDimensionIsDate = $derived.by<boolean>(() => {
2166
- const col = allColumns.find((c) => c.id === effectiveChartDimensionId);
2205
+ /**
2206
+ * Does this column hold dates? Declared type first, then a probe of the
2207
+ * first non-empty value. Lifted out of `chartDimensionIsDate` so the chart
2208
+ * panel can also ask it of columns the user has NOT picked yet, which is
2209
+ * what decides whether a calendar chart is offerable at all.
2210
+ */
2211
+ function columnIsDate(col: Column<TData> | undefined): boolean {
2167
2212
  if (!col) return false;
2168
2213
  if (col.columnDef.cellDataType === "date") return true;
2169
2214
  if (col.columnDef.cellDataType) return false;
@@ -2174,7 +2219,10 @@ export function createSvGridController<
2174
2219
  return typeof v === "string" && /^\d{4}-\d{2}(-\d{2})?/.test(v);
2175
2220
  }
2176
2221
  return false;
2177
- });
2222
+ }
2223
+ const chartDimensionIsDate = $derived.by<boolean>(() =>
2224
+ columnIsDate(allColumns.find((c) => c.id === effectiveChartDimensionId)),
2225
+ );
2178
2226
  const chartRows = $derived.by<TData[]>(() => {
2179
2227
  const rects = getSelectionRects();
2180
2228
  const pushLeaf = (out: TData[], row: Row<TData> | undefined) => {
@@ -2260,12 +2308,27 @@ export function createSvGridController<
2260
2308
  // null until it resolves, a tick after the panel itself starts loading (which it
2261
2309
  // already does). The `getAggregate` server path uses the local bucketsToSpec and
2262
2310
  // does not need the engine.
2263
- let chartEngine = $state<typeof import("./chart").rowsToChartSpec | null>(null);
2311
+ // The whole module, not just `rowsToChartSpec`: the panel can now reach
2312
+ // every chart type, and the ones that do not read a categories-by-series
2313
+ // grid (scatter, gauge, treemap, calendar, sankey) need their own builders
2314
+ // from the same lazy chunk. Same `import()`, same boundary.
2315
+ let chartEngine = $state<typeof import("./chart") | null>(null);
2264
2316
  $effect(() => {
2265
2317
  if (chartingEnabled && !chartEngine)
2266
- import("./chart").then((m) => (chartEngine = m.rowsToChartSpec));
2318
+ import("./chart").then((m) => (chartEngine = m));
2267
2319
  });
2268
2320
 
2321
+ /** Put the chart's numbers in the same locale as the grid's. A grid that set
2322
+ * `localization.locale` has already said which locale its data is in, and a
2323
+ * chart of that data reading in a different one is a bug nobody would think
2324
+ * to look for; `charting.locale` overrides when they really should differ.
2325
+ * Currency has no such default - only the app knows what the numbers are. */
2326
+ const applyChartLocale = (spec: ChartSpec) => {
2327
+ const locale = chartCfg?.locale ?? props.localization?.locale;
2328
+ if (locale) spec.locale = locale;
2329
+ if (chartCfg?.currency) spec.currency = chartCfg.currency;
2330
+ };
2331
+
2269
2332
  const chartSpec = $derived.by<ChartSpec | null>(() => {
2270
2333
  if (!chartingEnabled || !chartCfg) return null;
2271
2334
  if (chartCfg.getAggregate) {
@@ -2277,6 +2340,7 @@ export function createSvGridController<
2277
2340
  if (effectiveChartTimeAxis) spec.xType = "time";
2278
2341
  if (effectiveChartLogScale) spec.yScale = "log";
2279
2342
  if (effectiveChartValueFormat) spec.valueFormat = effectiveChartValueFormat;
2343
+ applyChartLocale(spec);
2280
2344
  return spec;
2281
2345
  }
2282
2346
  if (chartCfg.buildSpec) return chartCfg.buildSpec(chartRows) ?? null;
@@ -2287,31 +2351,65 @@ export function createSvGridController<
2287
2351
  // Engine not loaded yet (lazy): render nothing until it resolves.
2288
2352
  if (!chartEngine) return null;
2289
2353
  const seriesField = fieldOf(effectiveChartSeriesId);
2354
+ const rowsForChart = chartRows as Array<Record<string, unknown>>;
2355
+
2356
+ // Some types read the rows directly rather than a grouped grid: a scatter
2357
+ // point is one row, a gauge has no category axis at all, and a box plot
2358
+ // needs the whole sample per group rather than one reduced number. The
2359
+ // engine owns that dispatch - naming each type here would put them in the
2360
+ // base bundle, which every grid pays for whether or not it ever charts.
2361
+ // Everything else falls through to the aggregation below and is reshaped
2362
+ // afterwards, so reduce / sort / topN / "Other" keep working for all of it.
2363
+ const direct = chartEngine.rowsToDirectSpec(chartType, rowsForChart, {
2364
+ category: category as string,
2365
+ ...(values[0] ? { value: values[0] } : {}),
2366
+ ...(fieldOf(effectiveChartMeasure2Id) ? { value2: fieldOf(effectiveChartMeasure2Id)! } : {}),
2367
+ ...(seriesField ? { series: seriesField } : {}),
2368
+ reduce: chartReduce,
2369
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2370
+ });
2371
+ if (direct) return direct;
2372
+ // A direct type that could not build (scatter with no Y picked yet, for
2373
+ // instance) must not fall through to the bar-chart path and draw the wrong
2374
+ // chart - it renders the panel's empty state instead.
2375
+ if (chartType === "scatter" || chartType === "gauge" || chartType === "boxplot") return null;
2290
2376
 
2291
- const spec = chartEngine<Record<string, unknown>>(chartRows as Array<Record<string, unknown>>, {
2377
+ const spec = chartEngine.rowsToChartSpec<Record<string, unknown>>(rowsForChart, {
2292
2378
  type: chartType,
2293
2379
  category: category as string,
2294
2380
  value: values.length === 1 ? values[0]! : values,
2295
2381
  ...(seriesField ? { series: seriesField } : {}),
2296
2382
  reduce: chartReduce,
2297
- stacked: effectiveChartStacked || chartCfg.stacked100 === true,
2298
- stacked100: chartCfg.stacked100 === true,
2299
- ...(chartCfg.palette ? { palette: chartCfg.palette } : {}),
2383
+ stacked: effectiveChartStacked || effectiveChartStacked100,
2384
+ stacked100: effectiveChartStacked100,
2385
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
2300
2386
  ...(chartCfg.topN ? { topN: chartCfg.topN } : {}),
2301
2387
  ...(chartCfg.otherLabel ? { otherLabel: chartCfg.otherLabel } : {}),
2302
2388
  ...(chartCfg.sort ? { sort: chartCfg.sort } : {}),
2303
2389
  });
2304
2390
 
2305
- if (chartCfg.orientation) spec.orientation = chartCfg.orientation;
2391
+ // Reshape the aggregated grid into whatever this type actually reads.
2392
+ // These run AFTER the aggregation so grouping, reduce, sort, topN and the
2393
+ // "Other" bucket apply to them exactly as they do to a bar chart.
2394
+ if (chartType === "treemap") spec.treemap = chartEngine.specToTreemap(spec);
2395
+ else if (chartType === "calendar") spec.calendarValues = chartEngine.specToCalendar(spec);
2396
+ else if (chartType === "sankey") {
2397
+ const flow = chartEngine.specToSankey(spec);
2398
+ spec.sankeyNodes = flow.nodes;
2399
+ spec.sankeyLinks = flow.links;
2400
+ }
2401
+
2402
+ if (effectiveChartOrientation === "horizontal") spec.orientation = "horizontal";
2306
2403
  if (effectiveChartTimeAxis) spec.xType = "time";
2307
2404
  if (effectiveChartLogScale) spec.yScale = "log";
2308
2405
  if (effectiveChartValueFormat) spec.valueFormat = effectiveChartValueFormat;
2406
+ applyChartLocale(spec);
2309
2407
  if (chartType !== "pie" && spec.orientation !== "horizontal" && !spec.yAxisTitle && chartAutoYAxisTitle) {
2310
2408
  spec.yAxisTitle = chartAutoYAxisTitle;
2311
2409
  }
2312
2410
  if (effectiveChartValueFormat && activeChart.valueFormat === null) { /* inherited default */ }
2313
2411
  if (chartType === "pie") {
2314
- if (chartCfg.donut) spec.innerRadius = typeof chartCfg.donut === "number" ? chartCfg.donut : 0.6;
2412
+ if (effectiveChartDonut) spec.innerRadius = typeof chartCfg.donut === "number" ? chartCfg.donut : 0.6;
2315
2413
  }
2316
2414
  if (chartCfg.annotations) spec.annotations = chartCfg.annotations;
2317
2415
  if (chartCfg.patternFallback) spec.patternFallback = true;
@@ -2331,7 +2429,20 @@ export function createSvGridController<
2331
2429
  return spec;
2332
2430
  });
2333
2431
 
2432
+ /**
2433
+ * Can a click on this chart be turned back into a grid filter?
2434
+ *
2435
+ * Only where a clicked mark maps to exactly one value of the charted
2436
+ * dimension. It does not for a sankey (clicking a TARGET would filter the
2437
+ * SOURCE column by a target value and empty the grid), a gauge (no category
2438
+ * at all), or a scatter (the mark carries a point label, not a group).
2439
+ */
2440
+ const chartCrossFilterable = $derived(
2441
+ chartType !== "sankey" && chartType !== "gauge" && chartType !== "scatter",
2442
+ );
2443
+
2334
2444
  function applyChartCrossFilter(category: string) {
2445
+ if (!chartCrossFilterable) return;
2335
2446
  const dimId = effectiveChartDimensionId;
2336
2447
  if (!dimId) return;
2337
2448
  const existing = valueFilters[dimId];
@@ -3937,6 +4048,11 @@ export function createSvGridController<
3937
4048
  dimension: c.dimensionId,
3938
4049
  series: c.seriesId ?? null,
3939
4050
  measure: c.measureId,
4051
+ measure2: c.measure2Id,
4052
+ orientation: c.orientation,
4053
+ stacked100: c.stacked100,
4054
+ donut: c.donut,
4055
+ palette: c.palette,
3940
4056
  reduce: c.reduce,
3941
4057
  stacked: c.stacked,
3942
4058
  dataLabels: c.dataLabels,
@@ -3954,6 +4070,11 @@ export function createSvGridController<
3954
4070
  dimensionId: (c.dimension as string | null) ?? null,
3955
4071
  measureId: (c.measure as string | null) ?? null,
3956
4072
  seriesId: c.series as string | null | undefined,
4073
+ measure2Id: (c.measure2 as string | null) ?? null,
4074
+ orientation: (c.orientation as "vertical" | "horizontal" | null) ?? null,
4075
+ stacked100: (c.stacked100 as boolean | null) ?? null,
4076
+ donut: (c.donut as boolean | null) ?? null,
4077
+ palette: (c.palette as string[] | null) ?? null,
3957
4078
  stacked: (c.stacked as boolean | null) ?? null,
3958
4079
  dataLabels: (c.dataLabels as boolean | null) ?? null,
3959
4080
  logScale: (c.logScale as boolean | null) ?? null,
@@ -3963,6 +4084,18 @@ export function createSvGridController<
3963
4084
  charts = restored.length ? restored : [makeChart("Chart 1")];
3964
4085
  activeChartIndex = Math.max(0, Math.min(charts.length - 1, active ?? 0));
3965
4086
  },
4087
+ get chartCrossFilterable() { return chartCrossFilterable; },
4088
+ get chartOrientation() { return effectiveChartOrientation; },
4089
+ set chartOrientation(v) { activeChart.orientation = v as never; },
4090
+ get chartStacked100() { return effectiveChartStacked100; },
4091
+ set chartStacked100(v) { activeChart.stacked100 = v as never; },
4092
+ get chartDonut() { return effectiveChartDonut; },
4093
+ set chartDonut(v) { activeChart.donut = v as never; },
4094
+ get chartPalette() { return effectiveChartPalette; },
4095
+ set chartPalette(v) { activeChart.palette = v as never; },
4096
+ get chartMeasure2Id() { return activeChart.measure2Id; },
4097
+ set chartMeasure2Id(v) { activeChart.measure2Id = v as never; },
4098
+ get effectiveChartMeasure2Id() { return effectiveChartMeasure2Id; },
3966
4099
  get chartAiHandler() { return chartAiHandler; },
3967
4100
  set chartAiHandler(v) { chartAiHandler = v as never; },
3968
4101
  applyChartConfig(config: {