@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
@@ -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>
@@ -1,9 +1,17 @@
1
1
  import { type RowData, type TableFeatures } from "./index.js";
2
2
  import "./sv-grid-scrollbar.js";
3
+ import type { Snippet } from "svelte";
4
+ import type { GridIconName } from "./grid-icons.js";
3
5
  import type { SvGridController } from "./SvGrid.controller.svelte.js";
4
6
  declare function $$render<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(): {
5
7
  props: {
6
8
  ctrl: SvGridController<TFeatures, TData>;
9
+ /**
10
+ * The grid's icon resolver, so the pager arrows honour the `icons` prop.
11
+ * Required rather than optional: SvGrid is the only thing that mounts this
12
+ * component, so a missing icon would be a bug, not a supported call.
13
+ */
14
+ icon: Snippet<[GridIconName]>;
7
15
  /** Render the pagination controls in this instance. */
8
16
  pager?: boolean;
9
17
  /** Render the status bar in this instance. */
@@ -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>
@@ -1,11 +1,12 @@
1
1
  import { type RowData, type TableFeatures } from "./index.js";
2
2
  import "./sv-grid-scrollbar.js";
3
3
  import type { Snippet } from "svelte";
4
+ import type { GridIconName } from "./grid-icons.js";
4
5
  import type { SvGridController } from "./SvGrid.controller.svelte.js";
5
6
  declare function $$render<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(): {
6
7
  props: {
7
8
  ctrl: SvGridController<TFeatures, TData>;
8
- icon: Snippet<[string]>;
9
+ icon: Snippet<[GridIconName]>;
9
10
  };
10
11
  exports: {};
11
12
  bindings: "";
@@ -371,6 +371,11 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
371
371
  field: string;
372
372
  label: string;
373
373
  }[];
374
+ dates: {
375
+ id: string;
376
+ field: string;
377
+ label: string;
378
+ }[];
374
379
  };
375
380
  readonly chartSpec: ChartSpec | null;
376
381
  applyChartCrossFilter: (category: string) => void;
@@ -389,6 +394,11 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
389
394
  dimension: string | null;
390
395
  series: string | null;
391
396
  measure: string | null;
397
+ measure2: string | null;
398
+ orientation: "horizontal" | "vertical" | null;
399
+ stacked100: boolean | null;
400
+ donut: boolean | null;
401
+ palette: string[] | null;
392
402
  reduce: "sum" | "avg" | "count";
393
403
  stacked: boolean | null;
394
404
  dataLabels: boolean | null;
@@ -397,6 +407,13 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
397
407
  valueFormat: ChartValueFormat | null;
398
408
  }[];
399
409
  applyChartsState(list: ReadonlyArray<Record<string, unknown>>, active?: number): void;
410
+ readonly chartCrossFilterable: boolean;
411
+ chartOrientation: "horizontal" | "vertical";
412
+ chartStacked100: boolean;
413
+ chartDonut: boolean;
414
+ chartPalette: string[] | null;
415
+ chartMeasure2Id: string | null;
416
+ readonly effectiveChartMeasure2Id: string | null;
400
417
  chartAiHandler: ((prompt: string) => Promise<Record<string, unknown> | null>) | null;
401
418
  applyChartConfig(config: {
402
419
  open?: boolean;
@@ -1661,6 +1661,11 @@ export function createSvGridController(rawProps, domIdBase) {
1661
1661
  dimensionId: null,
1662
1662
  measureId: null,
1663
1663
  seriesId: undefined,
1664
+ measure2Id: null,
1665
+ orientation: null,
1666
+ stacked100: null,
1667
+ donut: null,
1668
+ palette: null,
1664
1669
  stacked: null,
1665
1670
  dataLabels: null,
1666
1671
  logScale: null,
@@ -1703,6 +1708,11 @@ export function createSvGridController(rawProps, domIdBase) {
1703
1708
  const chartableColumns = $derived.by(function chartableColumns_d() {
1704
1709
  const dims = [];
1705
1710
  const measures = [];
1711
+ // Dates are a subset of dims, tracked separately so the panel can tell
1712
+ // whether a calendar chart is possible at all before the user picks
1713
+ // anything. Offering a type that can only render blank is worse than not
1714
+ // offering it.
1715
+ const dates = [];
1706
1716
  for (const col of allColumns) {
1707
1717
  const field = col.columnDef.field;
1708
1718
  if (!field)
@@ -1710,10 +1720,13 @@ export function createSvGridController(rawProps, domIdBase) {
1710
1720
  const entry = { id: col.id, field, label: columnLabel(col) };
1711
1721
  if (isNumericColumn(col))
1712
1722
  measures.push(entry);
1713
- else
1723
+ else {
1714
1724
  dims.push(entry);
1725
+ if (columnIsDate(col))
1726
+ dates.push(entry);
1727
+ }
1715
1728
  }
1716
- return { dims, measures };
1729
+ return { dims, measures, dates };
1717
1730
  });
1718
1731
  const chartColumnDefaults = $derived.by(function chartColumnDefaults_d() {
1719
1732
  const { dims, measures } = chartableColumns;
@@ -1759,6 +1772,15 @@ export function createSvGridController(rawProps, domIdBase) {
1759
1772
  return configured;
1760
1773
  return chartColumnDefaults.measureIds;
1761
1774
  });
1775
+ /** Scatter's Y measure. Defaults to the second numeric column, so picking
1776
+ * scatter plots something immediately rather than nothing. */
1777
+ const effectiveChartMeasure2Id = $derived(activeChart.measure2Id ??
1778
+ chartableColumns.measures.find((m) => m.id !== effectiveChartMeasureId)?.id ??
1779
+ null);
1780
+ const effectiveChartOrientation = $derived(activeChart.orientation ?? chartCfg?.orientation ?? "vertical");
1781
+ const effectiveChartStacked100 = $derived(activeChart.stacked100 ?? chartCfg?.stacked100 === true);
1782
+ const effectiveChartDonut = $derived(activeChart.donut ?? !!chartCfg?.donut);
1783
+ const effectiveChartPalette = $derived(activeChart.palette ?? chartCfg?.palette ?? null);
1762
1784
  const effectiveChartStacked = $derived(chartStacked ?? chartCfg?.stacked ?? false);
1763
1785
  const effectiveChartDataLabels = $derived(chartDataLabels ?? chartCfg?.dataLabels ?? false);
1764
1786
  const effectiveChartLogScale = $derived(chartLogScale ?? chartCfg?.yScale === "log");
@@ -1784,8 +1806,13 @@ export function createSvGridController(rawProps, domIdBase) {
1784
1806
  ? "Count"
1785
1807
  : `${chartReduce === "avg" ? "Average" : "Sum"} of ${label}`;
1786
1808
  });
1787
- const chartDimensionIsDate = $derived.by(() => {
1788
- const col = allColumns.find((c) => c.id === effectiveChartDimensionId);
1809
+ /**
1810
+ * Does this column hold dates? Declared type first, then a probe of the
1811
+ * first non-empty value. Lifted out of `chartDimensionIsDate` so the chart
1812
+ * panel can also ask it of columns the user has NOT picked yet, which is
1813
+ * what decides whether a calendar chart is offerable at all.
1814
+ */
1815
+ function columnIsDate(col) {
1789
1816
  if (!col)
1790
1817
  return false;
1791
1818
  if (col.columnDef.cellDataType === "date")
@@ -1801,7 +1828,8 @@ export function createSvGridController(rawProps, domIdBase) {
1801
1828
  return typeof v === "string" && /^\d{4}-\d{2}(-\d{2})?/.test(v);
1802
1829
  }
1803
1830
  return false;
1804
- });
1831
+ }
1832
+ const chartDimensionIsDate = $derived.by(() => columnIsDate(allColumns.find((c) => c.id === effectiveChartDimensionId)));
1805
1833
  const chartRows = $derived.by(() => {
1806
1834
  const rects = getSelectionRects();
1807
1835
  const pushLeaf = (out, row) => {
@@ -1893,11 +1921,27 @@ export function createSvGridController(rawProps, domIdBase) {
1893
1921
  // null until it resolves, a tick after the panel itself starts loading (which it
1894
1922
  // already does). The `getAggregate` server path uses the local bucketsToSpec and
1895
1923
  // does not need the engine.
1924
+ // The whole module, not just `rowsToChartSpec`: the panel can now reach
1925
+ // every chart type, and the ones that do not read a categories-by-series
1926
+ // grid (scatter, gauge, treemap, calendar, sankey) need their own builders
1927
+ // from the same lazy chunk. Same `import()`, same boundary.
1896
1928
  let chartEngine = $state(null);
1897
1929
  $effect(() => {
1898
1930
  if (chartingEnabled && !chartEngine)
1899
- import("./chart.js").then((m) => (chartEngine = m.rowsToChartSpec));
1900
- });
1931
+ import("./chart.js").then((m) => (chartEngine = m));
1932
+ });
1933
+ /** Put the chart's numbers in the same locale as the grid's. A grid that set
1934
+ * `localization.locale` has already said which locale its data is in, and a
1935
+ * chart of that data reading in a different one is a bug nobody would think
1936
+ * to look for; `charting.locale` overrides when they really should differ.
1937
+ * Currency has no such default - only the app knows what the numbers are. */
1938
+ const applyChartLocale = (spec) => {
1939
+ const locale = chartCfg?.locale ?? props.localization?.locale;
1940
+ if (locale)
1941
+ spec.locale = locale;
1942
+ if (chartCfg?.currency)
1943
+ spec.currency = chartCfg.currency;
1944
+ };
1901
1945
  const chartSpec = $derived.by(() => {
1902
1946
  if (!chartingEnabled || !chartCfg)
1903
1947
  return null;
@@ -1917,6 +1961,7 @@ export function createSvGridController(rawProps, domIdBase) {
1917
1961
  spec.yScale = "log";
1918
1962
  if (effectiveChartValueFormat)
1919
1963
  spec.valueFormat = effectiveChartValueFormat;
1964
+ applyChartLocale(spec);
1920
1965
  return spec;
1921
1966
  }
1922
1967
  if (chartCfg.buildSpec)
@@ -1929,33 +1974,69 @@ export function createSvGridController(rawProps, domIdBase) {
1929
1974
  if (!chartEngine)
1930
1975
  return null;
1931
1976
  const seriesField = fieldOf(effectiveChartSeriesId);
1932
- const spec = chartEngine(chartRows, {
1977
+ const rowsForChart = chartRows;
1978
+ // Some types read the rows directly rather than a grouped grid: a scatter
1979
+ // point is one row, a gauge has no category axis at all, and a box plot
1980
+ // needs the whole sample per group rather than one reduced number. The
1981
+ // engine owns that dispatch - naming each type here would put them in the
1982
+ // base bundle, which every grid pays for whether or not it ever charts.
1983
+ // Everything else falls through to the aggregation below and is reshaped
1984
+ // afterwards, so reduce / sort / topN / "Other" keep working for all of it.
1985
+ const direct = chartEngine.rowsToDirectSpec(chartType, rowsForChart, {
1986
+ category: category,
1987
+ ...(values[0] ? { value: values[0] } : {}),
1988
+ ...(fieldOf(effectiveChartMeasure2Id) ? { value2: fieldOf(effectiveChartMeasure2Id) } : {}),
1989
+ ...(seriesField ? { series: seriesField } : {}),
1990
+ reduce: chartReduce,
1991
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
1992
+ });
1993
+ if (direct)
1994
+ return direct;
1995
+ // A direct type that could not build (scatter with no Y picked yet, for
1996
+ // instance) must not fall through to the bar-chart path and draw the wrong
1997
+ // chart - it renders the panel's empty state instead.
1998
+ if (chartType === "scatter" || chartType === "gauge" || chartType === "boxplot")
1999
+ return null;
2000
+ const spec = chartEngine.rowsToChartSpec(rowsForChart, {
1933
2001
  type: chartType,
1934
2002
  category: category,
1935
2003
  value: values.length === 1 ? values[0] : values,
1936
2004
  ...(seriesField ? { series: seriesField } : {}),
1937
2005
  reduce: chartReduce,
1938
- stacked: effectiveChartStacked || chartCfg.stacked100 === true,
1939
- stacked100: chartCfg.stacked100 === true,
1940
- ...(chartCfg.palette ? { palette: chartCfg.palette } : {}),
2006
+ stacked: effectiveChartStacked || effectiveChartStacked100,
2007
+ stacked100: effectiveChartStacked100,
2008
+ ...(effectiveChartPalette ? { palette: effectiveChartPalette } : {}),
1941
2009
  ...(chartCfg.topN ? { topN: chartCfg.topN } : {}),
1942
2010
  ...(chartCfg.otherLabel ? { otherLabel: chartCfg.otherLabel } : {}),
1943
2011
  ...(chartCfg.sort ? { sort: chartCfg.sort } : {}),
1944
2012
  });
1945
- if (chartCfg.orientation)
1946
- spec.orientation = chartCfg.orientation;
2013
+ // Reshape the aggregated grid into whatever this type actually reads.
2014
+ // These run AFTER the aggregation so grouping, reduce, sort, topN and the
2015
+ // "Other" bucket apply to them exactly as they do to a bar chart.
2016
+ if (chartType === "treemap")
2017
+ spec.treemap = chartEngine.specToTreemap(spec);
2018
+ else if (chartType === "calendar")
2019
+ spec.calendarValues = chartEngine.specToCalendar(spec);
2020
+ else if (chartType === "sankey") {
2021
+ const flow = chartEngine.specToSankey(spec);
2022
+ spec.sankeyNodes = flow.nodes;
2023
+ spec.sankeyLinks = flow.links;
2024
+ }
2025
+ if (effectiveChartOrientation === "horizontal")
2026
+ spec.orientation = "horizontal";
1947
2027
  if (effectiveChartTimeAxis)
1948
2028
  spec.xType = "time";
1949
2029
  if (effectiveChartLogScale)
1950
2030
  spec.yScale = "log";
1951
2031
  if (effectiveChartValueFormat)
1952
2032
  spec.valueFormat = effectiveChartValueFormat;
2033
+ applyChartLocale(spec);
1953
2034
  if (chartType !== "pie" && spec.orientation !== "horizontal" && !spec.yAxisTitle && chartAutoYAxisTitle) {
1954
2035
  spec.yAxisTitle = chartAutoYAxisTitle;
1955
2036
  }
1956
2037
  if (effectiveChartValueFormat && activeChart.valueFormat === null) { /* inherited default */ }
1957
2038
  if (chartType === "pie") {
1958
- if (chartCfg.donut)
2039
+ if (effectiveChartDonut)
1959
2040
  spec.innerRadius = typeof chartCfg.donut === "number" ? chartCfg.donut : 0.6;
1960
2041
  }
1961
2042
  if (chartCfg.annotations)
@@ -1982,7 +2063,18 @@ export function createSvGridController(rawProps, domIdBase) {
1982
2063
  }
1983
2064
  return spec;
1984
2065
  });
2066
+ /**
2067
+ * Can a click on this chart be turned back into a grid filter?
2068
+ *
2069
+ * Only where a clicked mark maps to exactly one value of the charted
2070
+ * dimension. It does not for a sankey (clicking a TARGET would filter the
2071
+ * SOURCE column by a target value and empty the grid), a gauge (no category
2072
+ * at all), or a scatter (the mark carries a point label, not a group).
2073
+ */
2074
+ const chartCrossFilterable = $derived(chartType !== "sankey" && chartType !== "gauge" && chartType !== "scatter");
1985
2075
  function applyChartCrossFilter(category) {
2076
+ if (!chartCrossFilterable)
2077
+ return;
1986
2078
  const dimId = effectiveChartDimensionId;
1987
2079
  if (!dimId)
1988
2080
  return;
@@ -3418,6 +3510,11 @@ export function createSvGridController(rawProps, domIdBase) {
3418
3510
  dimension: c.dimensionId,
3419
3511
  series: c.seriesId ?? null,
3420
3512
  measure: c.measureId,
3513
+ measure2: c.measure2Id,
3514
+ orientation: c.orientation,
3515
+ stacked100: c.stacked100,
3516
+ donut: c.donut,
3517
+ palette: c.palette,
3421
3518
  reduce: c.reduce,
3422
3519
  stacked: c.stacked,
3423
3520
  dataLabels: c.dataLabels,
@@ -3435,6 +3532,11 @@ export function createSvGridController(rawProps, domIdBase) {
3435
3532
  dimensionId: c.dimension ?? null,
3436
3533
  measureId: c.measure ?? null,
3437
3534
  seriesId: c.series,
3535
+ measure2Id: c.measure2 ?? null,
3536
+ orientation: c.orientation ?? null,
3537
+ stacked100: c.stacked100 ?? null,
3538
+ donut: c.donut ?? null,
3539
+ palette: c.palette ?? null,
3438
3540
  stacked: c.stacked ?? null,
3439
3541
  dataLabels: c.dataLabels ?? null,
3440
3542
  logScale: c.logScale ?? null,
@@ -3444,6 +3546,18 @@ export function createSvGridController(rawProps, domIdBase) {
3444
3546
  charts = restored.length ? restored : [makeChart("Chart 1")];
3445
3547
  activeChartIndex = Math.max(0, Math.min(charts.length - 1, active ?? 0));
3446
3548
  },
3549
+ get chartCrossFilterable() { return chartCrossFilterable; },
3550
+ get chartOrientation() { return effectiveChartOrientation; },
3551
+ set chartOrientation(v) { activeChart.orientation = v; },
3552
+ get chartStacked100() { return effectiveChartStacked100; },
3553
+ set chartStacked100(v) { activeChart.stacked100 = v; },
3554
+ get chartDonut() { return effectiveChartDonut; },
3555
+ set chartDonut(v) { activeChart.donut = v; },
3556
+ get chartPalette() { return effectiveChartPalette; },
3557
+ set chartPalette(v) { activeChart.palette = v; },
3558
+ get chartMeasure2Id() { return activeChart.measure2Id; },
3559
+ set chartMeasure2Id(v) { activeChart.measure2Id = v; },
3560
+ get effectiveChartMeasure2Id() { return effectiveChartMeasure2Id; },
3447
3561
  get chartAiHandler() { return chartAiHandler; },
3448
3562
  set chartAiHandler(v) { chartAiHandler = v; },
3449
3563
  applyChartConfig(config) {
package/dist/SvGrid.css CHANGED
@@ -153,6 +153,10 @@
153
153
  /* Advanced-filter indicator. Reads as state rather than as another action, so
154
154
  it uses the accent fill the toolbar buttons reserve for `is-active` instead
155
155
  of the neutral button shell. */
156
+ .sv-grid-advf-chip > .sv-grid-icon {
157
+ width: 13px;
158
+ height: 13px;
159
+ }
156
160
  .sv-grid-advf-chip {
157
161
  display: inline-flex;
158
162
  align-items: center;
@@ -261,6 +265,12 @@
261
265
  flex-shrink: 0;
262
266
  }
263
267
  .sv-grid-tool-panel-btn {
268
+ /* Centred as flex because these draw an icon now, not a bare character: a
269
+ glyph default still centres, and an SVG override no longer sits on the
270
+ text baseline. */
271
+ display: inline-flex;
272
+ align-items: center;
273
+ justify-content: center;
264
274
  width: 22px;
265
275
  height: 22px;
266
276
  border: 0;
@@ -807,9 +817,18 @@
807
817
  color: var(--sg-accent, #2563eb);
808
818
  }
809
819
  .sv-grid-group-caret {
820
+ display: inline-flex;
821
+ align-items: center;
822
+ justify-content: center;
823
+ width: 10px;
824
+ height: 10px;
810
825
  flex-shrink: 0;
811
826
  transition: transform 140ms ease;
812
827
  }
828
+ .sv-grid-group-caret > .sv-grid-icon {
829
+ width: 100%;
830
+ height: 100%;
831
+ }
813
832
  /* Collapsed: point the caret right (like a closed disclosure). */
814
833
  .sv-grid-group-toggle.is-collapsed .sv-grid-group-caret {
815
834
  transform: rotate(-90deg);
@@ -1855,6 +1874,23 @@ select.sv-grid-fr-editor {
1855
1874
  display: block;
1856
1875
  flex: none;
1857
1876
  }
1877
+ /* A consumer icon from the `icons` prop. SvGrid wraps the override in this box
1878
+ rather than asking the consumer to add `.sv-grid-icon` themselves, so it
1879
+ inherits every per-site size rule below and the expander's rotate-on-open
1880
+ without needing to know those rules exist. Must stay AFTER .sv-grid-icon:
1881
+ equal specificity, and this display has to win. */
1882
+ .sv-grid-icon-custom {
1883
+ display: inline-flex;
1884
+ align-items: center;
1885
+ justify-content: center;
1886
+ line-height: 0;
1887
+ }
1888
+ .sv-grid-icon-custom > svg,
1889
+ .sv-grid-icon-custom > img {
1890
+ width: 100%;
1891
+ height: 100%;
1892
+ display: block;
1893
+ }
1858
1894
 
1859
1895
  .sv-grid-header-cell {
1860
1896
  display: flex;
@@ -2453,14 +2489,19 @@ select.sv-grid-fr-editor {
2453
2489
  group banners and auto-group columns, SvGroupCell and SvTree all draw the
2454
2490
  same `chevron-right` and rotate it open, rather than swapping between two
2455
2491
  different glyphs. */
2456
- .sv-grid-tree-toggle .sv-grid-icon,
2457
- .sv-grid-group-toggle .sv-grid-icon {
2492
+ /* Direct child, not descendant. The column-group header button also carries
2493
+ `sv-grid-group-toggle` + `aria-expanded`, and its caret is wrapped one level
2494
+ deeper - a descendant selector would rotate that caret 90deg too, so an
2495
+ expanded down-caret would point left. The three row/tree expanders render
2496
+ their icon as an immediate child, so `>` changes nothing for them. */
2497
+ .sv-grid-tree-toggle > .sv-grid-icon,
2498
+ .sv-grid-group-toggle > .sv-grid-icon {
2458
2499
  width: 12px;
2459
2500
  height: 12px;
2460
2501
  transition: transform 0.15s ease;
2461
2502
  }
2462
- .sv-grid-tree-toggle[aria-expanded="true"] .sv-grid-icon,
2463
- .sv-grid-group-toggle[aria-expanded="true"] .sv-grid-icon {
2503
+ .sv-grid-tree-toggle[aria-expanded="true"] > .sv-grid-icon,
2504
+ .sv-grid-group-toggle[aria-expanded="true"] > .sv-grid-icon {
2464
2505
  transform: rotate(90deg);
2465
2506
  }
2466
2507
  .sv-grid-tree-toggle:focus-visible {
@@ -2624,7 +2665,7 @@ select.sv-grid-fr-editor {
2624
2665
  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.18);
2625
2666
  min-width: 280px;
2626
2667
  }
2627
- .sv-grid-find-icon {
2668
+ .sv-grid-find > .sv-grid-icon {
2628
2669
  width: 14px; height: 14px;
2629
2670
  color: var(--sg-muted, #94a3b8);
2630
2671
  flex: none;
@@ -2868,13 +2909,26 @@ select.sv-grid-fr-editor {
2868
2909
  opacity: 1;
2869
2910
  }
2870
2911
 
2871
- /* Magnifier prefix on the menu's search input. */
2912
+ /* Magnifier prefix on the menu's search input. Real markup rather than a
2913
+ background-image, so it follows the theme and the `icons` prop can replace
2914
+ it; the input keeps its own class because focusFilterPanel() and a test both
2915
+ resolve `.sv-grid-menu-search`. */
2872
2916
  .sv-grid-menu-search {
2873
2917
  padding-left: 28px;
2874
- background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='6'/%3E%3Cpath d='M20 20l-4.5-4.5'/%3E%3C/svg%3E");
2875
- background-position: 8px center;
2876
- background-repeat: no-repeat;
2877
- background-size: 14px 14px;
2918
+ }
2919
+ .sv-grid-menu-search-wrap {
2920
+ position: relative;
2921
+ display: block;
2922
+ }
2923
+ .sv-grid-menu-search-wrap > .sv-grid-icon {
2924
+ position: absolute;
2925
+ left: 8px;
2926
+ top: 50%;
2927
+ transform: translateY(-50%);
2928
+ width: 14px;
2929
+ height: 14px;
2930
+ color: var(--sg-muted, #94a3b8);
2931
+ pointer-events: none;
2878
2932
  }
2879
2933
 
2880
2934
  /* Filter menu vertical rhythm. */
@@ -2910,6 +2964,11 @@ select.sv-grid-fr-editor {
2910
2964
  opacity: 1;
2911
2965
  }
2912
2966
 
2967
+ .sv-grid-board-search > .sv-grid-icon {
2968
+ width: 14px;
2969
+ height: 14px;
2970
+ }
2971
+
2913
2972
  /* Kanban board mode search box (board.searchable) */
2914
2973
  .sv-grid-board-search {
2915
2974
  display: flex;
@@ -3048,6 +3107,16 @@ select.sv-grid-fr-editor {
3048
3107
  .sv-grid-chart-actions { display: inline-flex; align-items: center; gap: 6px; }
3049
3108
  .sv-grid-chart-controls { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
3050
3109
  .sv-grid-chart-controls:empty { display: none; }
3110
+ /* Says why the chosen chart type cannot draw yet - a missing Split by, too few
3111
+ groups. The picker deliberately does NOT hide such a type: options that come
3112
+ and go as the user filters are worse than one sentence of explanation. */
3113
+ .sv-grid-chart-hint {
3114
+ flex-basis: 100%;
3115
+ margin: 2px 0 0;
3116
+ font-size: 11.5px;
3117
+ font-weight: 400;
3118
+ color: var(--sg-muted, #64748b);
3119
+ }
3051
3120
  .sv-grid-chart-ctl { display: flex; flex-direction: column; gap: 2px; font-size: 11px; font-weight: 600; color: var(--sg-muted, #64748b); }
3052
3121
  .sv-grid-chart-ctl select {
3053
3122
  font: inherit; font-size: 12.5px; font-weight: 400; color: var(--sg-fg, #0f172a);