@svgrid/grid 2.1.6 → 2.1.8
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/dist/SvCarousel.svelte +35 -6
- package/dist/SvCarousel.svelte.d.ts +3 -0
- package/dist/SvColorInput.svelte +0 -1
- package/dist/SvContextMenu.svelte +26 -6
- package/dist/SvDrawer.svelte +5 -3
- package/dist/SvGrid.controller.svelte.d.ts +85 -2
- package/dist/SvGrid.controller.svelte.js +494 -1
- package/dist/SvGrid.css +142 -0
- package/dist/SvGrid.svelte +49 -27
- package/dist/SvGrid.types.d.ts +78 -0
- package/dist/SvGridBoard.svelte +71 -13
- package/dist/SvGridChart.svelte +41 -7
- package/dist/SvGridChart.svelte.d.ts +4 -0
- package/dist/SvGridChartPanel.svelte +485 -0
- package/dist/SvGridChartPanel.svelte.d.ts +28 -0
- package/dist/SvGridDropdown.svelte +18 -2
- package/dist/SvGroupCell.svelte +5 -5
- package/dist/SvMenu.svelte +25 -2
- package/dist/SvMenu.svelte.d.ts +3 -0
- package/dist/SvMenuList.svelte +31 -9
- package/dist/SvMenuList.svelte.d.ts +9 -1
- package/dist/SvNumberInput.svelte +1 -1
- package/dist/SvOtpInput.svelte +3 -2
- package/dist/SvPopover.svelte +13 -1
- package/dist/SvRowGroupPanel.svelte +23 -0
- package/dist/SvScrollArea.svelte +13 -2
- package/dist/SvScrollArea.svelte.d.ts +2 -0
- package/dist/SvTooltip.svelte +27 -2
- package/dist/SvTooltip.svelte.d.ts +3 -2
- package/dist/SvTour.svelte +30 -5
- package/dist/SvTour.svelte.d.ts +3 -0
- package/dist/SvTree.svelte +7 -3
- package/dist/SvTreeSelect.svelte +16 -9
- package/dist/build-api.js +51 -0
- package/dist/cdn/svgrid.js +17394 -16052
- package/dist/cdn/svgrid.svelte-external.js +16784 -15644
- package/dist/chart-export.d.ts +21 -0
- package/dist/chart-export.js +52 -11
- package/dist/chart.d.ts +30 -0
- package/dist/chart.js +53 -13
- package/dist/createAutocomplete.svelte.d.ts +6 -0
- package/dist/createAutocomplete.svelte.js +10 -0
- package/dist/createCombobox.svelte.d.ts +1 -0
- package/dist/createCombobox.svelte.js +1 -0
- package/dist/createCountryInput.svelte.d.ts +5 -0
- package/dist/createCountryInput.svelte.js +11 -0
- package/dist/createDropdownList.svelte.d.ts +4 -0
- package/dist/createDropdownList.svelte.js +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +5 -3
- package/dist/createPopoverSelect.svelte.js +5 -3
- package/dist/createTree.svelte.d.ts +1 -0
- package/dist/createTree.svelte.js +15 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/svgrid-wrapper.types.d.ts +58 -0
- package/package.json +1 -1
- package/src/SvCarousel.svelte +35 -6
- package/src/SvColorInput.svelte +0 -1
- package/src/SvContextMenu.svelte +26 -6
- package/src/SvDrawer.svelte +5 -3
- package/src/SvGrid.controller.svelte.ts +481 -0
- package/src/SvGrid.css +142 -0
- package/src/SvGrid.svelte +49 -27
- package/src/SvGrid.types.ts +83 -0
- package/src/SvGridBoard.svelte +71 -13
- package/src/SvGridChart.svelte +41 -7
- package/src/SvGridChartPanel.svelte +485 -0
- package/src/SvGridDropdown.svelte +18 -2
- package/src/SvGroupCell.svelte +5 -5
- package/src/SvMenu.svelte +25 -2
- package/src/SvMenuList.svelte +31 -9
- package/src/SvNumberInput.svelte +1 -1
- package/src/SvOtpInput.svelte +3 -2
- package/src/SvPopover.svelte +13 -1
- package/src/SvRowGroupPanel.svelte +23 -0
- package/src/SvScrollArea.svelte +13 -2
- package/src/SvTooltip.svelte +27 -2
- package/src/SvTour.svelte +30 -5
- package/src/SvTree.svelte +7 -3
- package/src/SvTreeSelect.svelte +16 -9
- package/src/build-api.ts +49 -0
- package/src/chart-export.ts +57 -0
- package/src/chart.ts +70 -13
- package/src/createAutocomplete.svelte.ts +11 -0
- package/src/createCombobox.svelte.ts +1 -0
- package/src/createCountryInput.svelte.ts +12 -0
- package/src/createDropdownList.svelte.ts +8 -0
- package/src/createPopoverSelect.svelte.ts +5 -3
- package/src/createTree.svelte.ts +15 -2
- package/src/index.ts +6 -0
- package/src/overlays.test.ts +5 -0
- package/src/svgrid-wrapper.types.ts +62 -0
- package/src/svgrid.charting.test.ts +527 -0
- package/src/test-setup.ts +22 -0
package/src/SvGrid.svelte
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
} from "./SvGrid.helpers";
|
|
55
55
|
import { createSvGridController } from "./SvGrid.controller.svelte";
|
|
56
56
|
import GridMenus from "./GridMenus.svelte";
|
|
57
|
+
import SvGridChartPanel from "./SvGridChartPanel.svelte";
|
|
57
58
|
import GridFooter from "./GridFooter.svelte";
|
|
58
59
|
import SvGridBoard from "./SvGridBoard.svelte";
|
|
59
60
|
let props: Props<TFeatures, TData> = $props();
|
|
@@ -1303,35 +1304,52 @@
|
|
|
1303
1304
|
</label>
|
|
1304
1305
|
{/if}
|
|
1305
1306
|
|
|
1306
|
-
{#if toolPanelEnabled}
|
|
1307
|
+
{#if toolPanelEnabled || ctrl.chartingEnabled}
|
|
1307
1308
|
<div class="sv-grid-toolbar">
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1309
|
+
{#if toolPanelEnabled}
|
|
1310
|
+
<button
|
|
1311
|
+
type="button"
|
|
1312
|
+
class="sv-grid-toolbar-btn"
|
|
1313
|
+
class:is-active={ctrl.toolPanelOpen}
|
|
1314
|
+
aria-label={ctrl.toolPanelOpen
|
|
1315
|
+
? "Close tool panel"
|
|
1316
|
+
: "Open tool panel (columns & filters)"}
|
|
1317
|
+
aria-expanded={ctrl.toolPanelOpen}
|
|
1318
|
+
onclick={() => (ctrl.toolPanelOpen = !ctrl.toolPanelOpen)}
|
|
1319
|
+
>
|
|
1320
|
+
<svg
|
|
1321
|
+
viewBox="0 0 24 24"
|
|
1322
|
+
width="15"
|
|
1323
|
+
height="15"
|
|
1324
|
+
fill="none"
|
|
1325
|
+
stroke="currentColor"
|
|
1326
|
+
stroke-width="2"
|
|
1327
|
+
stroke-linecap="round"
|
|
1328
|
+
stroke-linejoin="round"
|
|
1329
|
+
aria-hidden="true"
|
|
1330
|
+
>
|
|
1331
|
+
<rect x="3" y="4" width="6" height="16" rx="1" />
|
|
1332
|
+
<rect x="11" y="4" width="4" height="16" rx="1" />
|
|
1333
|
+
<rect x="17" y="4" width="4" height="16" rx="1" />
|
|
1334
|
+
</svg>
|
|
1335
|
+
Columns & Filters
|
|
1336
|
+
</button>
|
|
1337
|
+
{/if}
|
|
1338
|
+
{#if ctrl.chartingEnabled}
|
|
1339
|
+
<button
|
|
1340
|
+
type="button"
|
|
1341
|
+
class="sv-grid-toolbar-btn sv-grid-chart-toggle"
|
|
1342
|
+
class:is-active={ctrl.chartPanelOpen}
|
|
1343
|
+
aria-label={ctrl.chartPanelOpen ? "Close chart" : "Open chart"}
|
|
1344
|
+
aria-expanded={ctrl.chartPanelOpen}
|
|
1345
|
+
onclick={() => (ctrl.chartPanelOpen = !ctrl.chartPanelOpen)}
|
|
1328
1346
|
>
|
|
1329
|
-
<
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1347
|
+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1348
|
+
<line x1="12" y1="20" x2="12" y2="10" /><line x1="18" y1="20" x2="18" y2="4" /><line x1="6" y1="20" x2="6" y2="16" />
|
|
1349
|
+
</svg>
|
|
1350
|
+
Chart
|
|
1351
|
+
</button>
|
|
1352
|
+
{/if}
|
|
1335
1353
|
</div>
|
|
1336
1354
|
{/if}
|
|
1337
1355
|
|
|
@@ -2757,6 +2775,10 @@
|
|
|
2757
2775
|
</aside>
|
|
2758
2776
|
{/if}
|
|
2759
2777
|
{/if}
|
|
2778
|
+
|
|
2779
|
+
{#if ctrl.chartingEnabled && ctrl.chartPanelOpen}
|
|
2780
|
+
<SvGridChartPanel {ctrl} />
|
|
2781
|
+
{/if}
|
|
2760
2782
|
</div>
|
|
2761
2783
|
<!-- /.sv-grid-root -->
|
|
2762
2784
|
|
package/src/SvGrid.types.ts
CHANGED
|
@@ -10,6 +10,84 @@ import type {
|
|
|
10
10
|
} from "./index";
|
|
11
11
|
import type { ConditionalFormat } from "./conditional-formatting";
|
|
12
12
|
import type { MenuItem } from "./SvMenuList.svelte";
|
|
13
|
+
import type {
|
|
14
|
+
ChartType,
|
|
15
|
+
ChartSpec,
|
|
16
|
+
ChartAnnotation,
|
|
17
|
+
ChartReferenceLine,
|
|
18
|
+
} from "./chart";
|
|
19
|
+
|
|
20
|
+
/** One aggregated bucket returned by a server-side `getAggregate`. */
|
|
21
|
+
export type ChartAggregateBucket = { category: string; series?: string; value: number };
|
|
22
|
+
/** The request a server-side `getAggregate` receives (whole-dataset charting). */
|
|
23
|
+
export type ChartAggregateRequest = {
|
|
24
|
+
dimension: string | undefined;
|
|
25
|
+
measure: string | null;
|
|
26
|
+
series?: string;
|
|
27
|
+
reduce: "sum" | "avg" | "count";
|
|
28
|
+
filterModel: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Config for the built-in `charting` prop. All fields optional and additive;
|
|
33
|
+
* `charting={true}` is the zero-config form. Column references are column
|
|
34
|
+
* field names / ids.
|
|
35
|
+
*/
|
|
36
|
+
export type ChartingConfig<TData extends RowData = RowData> = {
|
|
37
|
+
/** Right-side drawer (default) or bottom dock. */
|
|
38
|
+
position?: "bottom" | "right";
|
|
39
|
+
/** Open the panel on first render. */
|
|
40
|
+
defaultOpen?: boolean;
|
|
41
|
+
/** Initial chart type. */
|
|
42
|
+
defaultType?: ChartType;
|
|
43
|
+
/** Docked-bottom height (px). */
|
|
44
|
+
height?: number;
|
|
45
|
+
/** Drawer width (px). */
|
|
46
|
+
width?: number;
|
|
47
|
+
/** Clicking a chart category filters the grid on the charted dimension. */
|
|
48
|
+
crossFilter?: boolean;
|
|
49
|
+
/** Group-by column. */
|
|
50
|
+
dimension?: string;
|
|
51
|
+
/** Split-by column: one series per distinct value. */
|
|
52
|
+
series?: string;
|
|
53
|
+
/** Measure column, or several for a multi-series chart. */
|
|
54
|
+
measures?: string | string[];
|
|
55
|
+
reduce?: "sum" | "avg" | "count";
|
|
56
|
+
stacked?: boolean;
|
|
57
|
+
stacked100?: boolean;
|
|
58
|
+
/** Per-series-label type override (combo). */
|
|
59
|
+
seriesTypes?: Record<string, "bar" | "line" | "area">;
|
|
60
|
+
/** Per-series-label axis override (dual axis). */
|
|
61
|
+
seriesAxes?: Record<string, "left" | "right">;
|
|
62
|
+
referenceLines?: ChartReferenceLine[];
|
|
63
|
+
averageLine?: boolean;
|
|
64
|
+
trend?: "sma" | "ema" | "linear";
|
|
65
|
+
annotations?: ChartAnnotation[];
|
|
66
|
+
yScale?: "linear" | "log";
|
|
67
|
+
timeAxis?: boolean;
|
|
68
|
+
valueFormat?: "number" | "currency" | "percent" | "compact";
|
|
69
|
+
smooth?: boolean;
|
|
70
|
+
orientation?: "vertical" | "horizontal";
|
|
71
|
+
donut?: number | boolean;
|
|
72
|
+
palette?: string[];
|
|
73
|
+
patternFallback?: boolean;
|
|
74
|
+
topN?: number;
|
|
75
|
+
otherLabel?: string;
|
|
76
|
+
sort?: "value-desc" | "value-asc" | "category" | "none";
|
|
77
|
+
dataLabels?: boolean;
|
|
78
|
+
zoom?: boolean;
|
|
79
|
+
brush?: boolean;
|
|
80
|
+
/** Show the chart's PNG/SVG export toolbar. */
|
|
81
|
+
export?: boolean;
|
|
82
|
+
/** Escape hatch: build a fully custom ChartSpec from the (scoped) rows. */
|
|
83
|
+
buildSpec?: (rows: TData[]) => ChartSpec | null;
|
|
84
|
+
/** Server-side aggregation: chart the whole dataset, not just loaded rows. */
|
|
85
|
+
getAggregate?: (
|
|
86
|
+
request: ChartAggregateRequest,
|
|
87
|
+
) => Promise<ReadonlyArray<ChartAggregateBucket>>;
|
|
88
|
+
/** Bump to force a server-side refetch. */
|
|
89
|
+
refreshKey?: unknown;
|
|
90
|
+
};
|
|
13
91
|
|
|
14
92
|
/**
|
|
15
93
|
* The built-in card detail drawer. Set `board.drawer` (`true` for all fields,
|
|
@@ -578,6 +656,11 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
578
656
|
* grid; the panel docks on the right edge.
|
|
579
657
|
*/
|
|
580
658
|
toolPanel?: boolean;
|
|
659
|
+
/**
|
|
660
|
+
* Integrated charting: `true` for the zero-config docked chart panel, or a
|
|
661
|
+
* {@link ChartingConfig} object to author defaults + advanced options.
|
|
662
|
+
*/
|
|
663
|
+
charting?: boolean | ChartingConfig<TData>;
|
|
581
664
|
/**
|
|
582
665
|
* Render the header column menu (⋮) as a tabbed popover - **General**,
|
|
583
666
|
* **Filter**, and **Columns** tabs (the AG-Grid layout). Defaults to `false`,
|
package/src/SvGridBoard.svelte
CHANGED
|
@@ -656,13 +656,8 @@
|
|
|
656
656
|
if (dragLaneId == null) return;
|
|
657
657
|
e.preventDefault();
|
|
658
658
|
const ids = lanes.map((l) => l.id);
|
|
659
|
-
const from = ids.indexOf(dragLaneId);
|
|
660
659
|
const to = ids.indexOf(laneId);
|
|
661
|
-
if (
|
|
662
|
-
ids.splice(to, 0, ids.splice(from, 1)[0]!);
|
|
663
|
-
laneOrder = ids;
|
|
664
|
-
board.onLaneReorder?.(ids);
|
|
665
|
-
}
|
|
660
|
+
if (to > -1) reorderLaneTo(dragLaneId, to);
|
|
666
661
|
dragLaneId = null;
|
|
667
662
|
overLaneId = null;
|
|
668
663
|
}
|
|
@@ -671,6 +666,35 @@
|
|
|
671
666
|
overLaneId = null;
|
|
672
667
|
}
|
|
673
668
|
|
|
669
|
+
// Shared by mouse drag-drop (above) and keyboard reorder (below): moves
|
|
670
|
+
// `fromId` to `toIndex` in the lane order, persists it, and notifies.
|
|
671
|
+
// Returns the new ordered ids, or null if the move was a no-op / invalid.
|
|
672
|
+
function reorderLaneTo(fromId: string, toIndex: number): string[] | null {
|
|
673
|
+
const ids = lanes.map((l) => l.id);
|
|
674
|
+
const from = ids.indexOf(fromId);
|
|
675
|
+
if (from < 0 || toIndex < 0 || toIndex >= ids.length || from === toIndex) return null;
|
|
676
|
+
ids.splice(toIndex, 0, ids.splice(from, 1)[0]!);
|
|
677
|
+
laneOrder = ids;
|
|
678
|
+
board.onLaneReorder?.(ids);
|
|
679
|
+
return ids;
|
|
680
|
+
}
|
|
681
|
+
// Keyboard lane reorder: Ctrl/Cmd+ArrowLeft/Right shifts the focused lane
|
|
682
|
+
// header by one position, mirroring the drag-reorder above.
|
|
683
|
+
function onLaneHeaderKeydown(e: KeyboardEvent, laneId: string) {
|
|
684
|
+
if (!board.reorderableLanes) return;
|
|
685
|
+
if ((e.key !== "ArrowLeft" && e.key !== "ArrowRight") || !(e.ctrlKey || e.metaKey)) return;
|
|
686
|
+
e.preventDefault();
|
|
687
|
+
const ids = lanes.map((l) => l.id);
|
|
688
|
+
const from = ids.indexOf(laneId);
|
|
689
|
+
if (from < 0) return;
|
|
690
|
+
const to = from + (e.key === "ArrowRight" ? 1 : -1);
|
|
691
|
+
const moved = reorderLaneTo(laneId, to);
|
|
692
|
+
if (moved) {
|
|
693
|
+
const pos = moved.indexOf(laneId) + 1;
|
|
694
|
+
announce(`Moved ${laneTitleById(laneId)} to position ${pos} of ${moved.length}.`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
674
698
|
// Runtime group-by change: the position overlay (laneOf/orderOf/laneOrder) is
|
|
675
699
|
// keyed to the old field, so reset it when `groupBy` changes. Edits and
|
|
676
700
|
// sub-tasks (keyed by row) are kept. Skips the initial run so a restored
|
|
@@ -874,6 +898,19 @@
|
|
|
874
898
|
openEditor(row);
|
|
875
899
|
return;
|
|
876
900
|
}
|
|
901
|
+
// Keyboard multi-select: Ctrl/Cmd+Space toggles this card's selection,
|
|
902
|
+
// mirroring onCardClick's ctrl/meta-click branch. Kept on a modifier so it
|
|
903
|
+
// never collides with plain Space, which grabs the card for a move.
|
|
904
|
+
if (board.selectable && e.key === " " && (e.ctrlKey || e.metaKey) && grabbed == null) {
|
|
905
|
+
e.preventDefault();
|
|
906
|
+
const k = key(row);
|
|
907
|
+
selected[k] = !selected[k];
|
|
908
|
+
emitSelection();
|
|
909
|
+
announce(
|
|
910
|
+
`${selected[k] ? "Selected" : "Deselected"} ${cardLabel(row)}. ${selectedRows().length} selected.`,
|
|
911
|
+
);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
877
914
|
if (e.key === " " || e.key === "Enter") {
|
|
878
915
|
e.preventDefault();
|
|
879
916
|
if (grabbed === row) {
|
|
@@ -1214,10 +1251,15 @@
|
|
|
1214
1251
|
class:is-reorderable={board.reorderableLanes}
|
|
1215
1252
|
class:is-lane-dropover={overLaneId === lane.id && dragLaneId !== lane.id}
|
|
1216
1253
|
draggable={board.reorderableLanes ? true : undefined}
|
|
1254
|
+
tabindex={board.reorderableLanes ? 0 : undefined}
|
|
1255
|
+
role={board.reorderableLanes ? "group" : undefined}
|
|
1256
|
+
aria-roledescription={board.reorderableLanes ? "Reorderable lane" : undefined}
|
|
1257
|
+
title={board.reorderableLanes ? "Ctrl+Left/Right arrow to reorder this lane" : undefined}
|
|
1217
1258
|
ondragstart={board.reorderableLanes ? (e) => onLaneHeaderDragStart(e, lane.id) : undefined}
|
|
1218
1259
|
ondragover={board.reorderableLanes ? (e) => onLaneHeaderDragOver(e, lane.id) : undefined}
|
|
1219
1260
|
ondrop={board.reorderableLanes ? (e) => onLaneHeaderDrop(e, lane.id) : undefined}
|
|
1220
1261
|
ondragend={board.reorderableLanes ? onLaneHeaderDragEnd : undefined}
|
|
1262
|
+
onkeydown={board.reorderableLanes ? (e) => onLaneHeaderKeydown(e, lane.id) : undefined}
|
|
1221
1263
|
oncontextmenu={board.laneMenu ? (e) => openLaneMenu(e, lane.id, cards) : undefined}
|
|
1222
1264
|
>
|
|
1223
1265
|
{#if board.collapsibleLanes}
|
|
@@ -1241,14 +1283,21 @@
|
|
|
1241
1283
|
}}
|
|
1242
1284
|
onblur={() => commitRename(lane.id)}
|
|
1243
1285
|
/>
|
|
1286
|
+
{:else if board.onLaneRename}
|
|
1287
|
+
<button
|
|
1288
|
+
type="button"
|
|
1289
|
+
class="sv-board-lane-title is-renamable"
|
|
1290
|
+
ondblclick={() => startRename(lane)}
|
|
1291
|
+
onkeydown={(e) => {
|
|
1292
|
+
if (e.key === "Enter" || e.key === "F2") {
|
|
1293
|
+
e.preventDefault();
|
|
1294
|
+
startRename(lane);
|
|
1295
|
+
}
|
|
1296
|
+
}}
|
|
1297
|
+
title="Double-click, Enter, or F2 to rename"
|
|
1298
|
+
>{laneTitle(lane)}</button>
|
|
1244
1299
|
{:else}
|
|
1245
|
-
|
|
1246
|
-
<span
|
|
1247
|
-
class="sv-board-lane-title"
|
|
1248
|
-
class:is-renamable={board.onLaneRename}
|
|
1249
|
-
ondblclick={board.onLaneRename ? () => startRename(lane) : undefined}
|
|
1250
|
-
title={board.onLaneRename ? "Double-click to rename" : undefined}
|
|
1251
|
-
>{laneTitle(lane)}</span>
|
|
1300
|
+
<span class="sv-board-lane-title">{laneTitle(lane)}</span>
|
|
1252
1301
|
{/if}
|
|
1253
1302
|
<span
|
|
1254
1303
|
class="sv-board-lane-count"
|
|
@@ -1630,7 +1679,16 @@
|
|
|
1630
1679
|
color: color-mix(in srgb, var(--sg-fg, #101828) 60%, transparent);
|
|
1631
1680
|
}
|
|
1632
1681
|
.sv-board-lane-title.is-renamable {
|
|
1682
|
+
all: unset;
|
|
1633
1683
|
cursor: text;
|
|
1684
|
+
font-weight: 600;
|
|
1685
|
+
font-size: 0.82rem;
|
|
1686
|
+
flex: 1 1 auto;
|
|
1687
|
+
min-width: 0;
|
|
1688
|
+
white-space: nowrap;
|
|
1689
|
+
overflow: hidden;
|
|
1690
|
+
text-overflow: ellipsis;
|
|
1691
|
+
text-align: left;
|
|
1634
1692
|
}
|
|
1635
1693
|
.sv-board-lane-rename {
|
|
1636
1694
|
flex: 1 1 auto;
|
package/src/SvGridChart.svelte
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* focus tooltips, a clickable legend that toggles series, optional data
|
|
8
8
|
* labels, and an `onSelect` drill hook. No external charting dependency.
|
|
9
9
|
*/
|
|
10
|
-
import { buildChart, DEFAULT_PALETTE, type ChartSpec, type ChartSelection } from './chart'
|
|
10
|
+
import { buildChart, DEFAULT_PALETTE, formatChartValue, type ChartSpec, type ChartSelection } from './chart'
|
|
11
11
|
|
|
12
12
|
type Props = {
|
|
13
13
|
spec: ChartSpec
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
* export (PNG / SVG / copy). Default true when zoomable or onDrill is
|
|
39
39
|
* set, otherwise false. Set to `false` to hide explicitly. */
|
|
40
40
|
toolbar?: boolean
|
|
41
|
+
/** Explicit chart pixel size (viewBox). When set, the chart lays out to
|
|
42
|
+
* fit exactly - used by the docked panel to size the chart to its body. */
|
|
43
|
+
width?: number
|
|
44
|
+
height?: number
|
|
41
45
|
}
|
|
42
46
|
let {
|
|
43
47
|
spec,
|
|
@@ -51,15 +55,19 @@
|
|
|
51
55
|
brush = false,
|
|
52
56
|
brushHeight = 88,
|
|
53
57
|
toolbar,
|
|
58
|
+
width,
|
|
59
|
+
height,
|
|
54
60
|
}: Props = $props()
|
|
55
61
|
const showToolbar = $derived(toolbar ?? (zoomable || !!onDrill))
|
|
56
62
|
|
|
57
63
|
const fmt = (v: number) =>
|
|
58
64
|
formatValue
|
|
59
65
|
? formatValue(v)
|
|
60
|
-
:
|
|
61
|
-
? v
|
|
62
|
-
:
|
|
66
|
+
: spec.valueFormat
|
|
67
|
+
? formatChartValue(v, spec.valueFormat)
|
|
68
|
+
: Number.isFinite(v)
|
|
69
|
+
? v.toLocaleString(undefined, { maximumFractionDigits: 2 })
|
|
70
|
+
: String(v)
|
|
63
71
|
|
|
64
72
|
let hidden = $state(new Set<string>())
|
|
65
73
|
// Isolate: double-clicking a legend chip shows ONLY that series/slice.
|
|
@@ -290,7 +298,9 @@
|
|
|
290
298
|
// chartEl) and hand buildChart a light/dark hint so low-value cells render as
|
|
291
299
|
// "cold" rather than glaring white rectangles on a dark grid.
|
|
292
300
|
let isDark = $state(false)
|
|
293
|
-
const geo = $derived(
|
|
301
|
+
const geo = $derived(
|
|
302
|
+
buildChart(width || height ? { ...visibleSpec, width, height } : visibleSpec, isDark ? 'dark' : 'light'),
|
|
303
|
+
)
|
|
294
304
|
const isCartesian = $derived(
|
|
295
305
|
spec.type !== 'pie' && spec.type !== 'heatmap' &&
|
|
296
306
|
spec.type !== 'funnel' && spec.type !== 'radar' &&
|
|
@@ -727,6 +737,15 @@
|
|
|
727
737
|
<text class="sv-grid-chart-axis" x={t.x} y={geo.plot.y + geo.plot.h + 16} text-anchor="middle">{truncate(t.label, 16)}</text>
|
|
728
738
|
{/if}
|
|
729
739
|
{/each}
|
|
740
|
+
<!-- Grouped (nested) category axis: parent tier under the leaf labels. -->
|
|
741
|
+
{#each geo.categoryGroupTicks as g (g.label + g.x0)}
|
|
742
|
+
{@const gy = geo.plot.y + geo.plot.h + (geo.xLabelRotated ? 46 : 32)}
|
|
743
|
+
<line class="sv-grid-chart-gridline" x1={g.x0 + 3} y1={gy - 9} x2={g.x1 - 3} y2={gy - 9} />
|
|
744
|
+
{#if g.x0 > geo.plot.x + 1}
|
|
745
|
+
<line class="sv-grid-chart-gridline" x1={g.x0} y1={geo.plot.y + geo.plot.h} x2={g.x0} y2={gy - 9} opacity="0.5" />
|
|
746
|
+
{/if}
|
|
747
|
+
<text class="sv-grid-chart-axis sv-grid-chart-group-label" x={g.xCenter} y={gy} text-anchor="middle">{truncate(g.label, Math.max(4, Math.floor((g.x1 - g.x0) / 7)))}</text>
|
|
748
|
+
{/each}
|
|
730
749
|
{#if spec.yAxisTitle}
|
|
731
750
|
<text class="sv-grid-chart-axis-title" x={13} y={geo.plot.y + geo.plot.h / 2} text-anchor="middle" transform={`rotate(-90 13 ${geo.plot.y + geo.plot.h / 2})`}>{spec.yAxisTitle}</text>
|
|
732
751
|
{/if}
|
|
@@ -850,6 +869,7 @@
|
|
|
850
869
|
onmousemove={(e) => hoverCell(e.clientX, e.clientY, cell)}
|
|
851
870
|
onmouseleave={clearActive}
|
|
852
871
|
onclick={() => select(cell.colLabel, cell.rowLabel, cell.value)}
|
|
872
|
+
onkeydown={(e) => { if (onSelect && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); select(cell.colLabel, cell.rowLabel, cell.value) } }}
|
|
853
873
|
/>
|
|
854
874
|
{#if dataLabels && cell.w > 22 && cell.h > 14}
|
|
855
875
|
<text class="sv-grid-chart-heatlabel" x={cell.x + cell.w / 2} y={cell.y + cell.h / 2 + 3} text-anchor="middle" fill={cell.textColor}>{fmt(cell.value)}</text>
|
|
@@ -887,6 +907,7 @@
|
|
|
887
907
|
onmousemove={(e) => hoverFunnel(e.clientX, e.clientY, seg)}
|
|
888
908
|
onmouseleave={clearActive}
|
|
889
909
|
onclick={() => select(seg.label, seg.label, seg.value, fi)}
|
|
910
|
+
onkeydown={(e) => { if (onSelect && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); select(seg.label, seg.label, seg.value, fi) } }}
|
|
890
911
|
/>
|
|
891
912
|
<text class="sv-grid-chart-funnel-label" x={seg.cx} y={seg.cy - 3} text-anchor="middle" fill={seg.textColor}>{truncate(seg.label, 24)}</text>
|
|
892
913
|
<text class="sv-grid-chart-funnel-value" x={seg.cx} y={seg.cy + 11} text-anchor="middle" fill={seg.textColor}>
|
|
@@ -997,15 +1018,18 @@
|
|
|
997
1018
|
|
|
998
1019
|
{#if isTreemap}
|
|
999
1020
|
{#each geo.treemapCells as cell, ti (ti)}
|
|
1000
|
-
<!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events -->
|
|
1021
|
+
<!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events a11y_no_noninteractive_tabindex -->
|
|
1001
1022
|
<rect
|
|
1002
1023
|
class="sv-grid-chart-treemap-cell"
|
|
1003
1024
|
x={cell.x} y={cell.y} width={cell.w} height={cell.h}
|
|
1004
1025
|
fill={cell.color}
|
|
1026
|
+
role={onSelect ? 'button' : 'presentation'}
|
|
1027
|
+
tabindex={onSelect ? 0 : undefined}
|
|
1005
1028
|
aria-label={`${cell.name}: ${fmt(cell.value)}`}
|
|
1006
1029
|
onmousemove={(e) => showTip(e.clientX, e.clientY, cell.name, [{ color: cell.color, value: fmt(cell.value) }])}
|
|
1007
1030
|
onmouseleave={clearActive}
|
|
1008
1031
|
onclick={() => select(cell.name, '', cell.value)}
|
|
1032
|
+
onkeydown={(e) => { if (onSelect && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); select(cell.name, '', cell.value) } }}
|
|
1009
1033
|
/>
|
|
1010
1034
|
{#if cell.w > 36 && cell.h > 18}
|
|
1011
1035
|
<text class="sv-grid-chart-treemap-label" x={cell.x + 4} y={cell.y + 12} fill={cell.textColor}>{truncate(cell.name, Math.max(3, Math.floor(cell.w / 7)))}</text>
|
|
@@ -1034,17 +1058,23 @@
|
|
|
1034
1058
|
{/each}
|
|
1035
1059
|
<!-- Node rectangles + labels. -->
|
|
1036
1060
|
{#each geo.sankeyNodes as node, ni (ni)}
|
|
1037
|
-
<!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events -->
|
|
1061
|
+
<!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events a11y_no_noninteractive_tabindex -->
|
|
1038
1062
|
<rect
|
|
1039
1063
|
class="sv-grid-chart-sankey-node"
|
|
1040
1064
|
x={node.x} y={node.y} width={node.w} height={node.h}
|
|
1041
1065
|
fill={node.color}
|
|
1066
|
+
tabindex={0}
|
|
1042
1067
|
aria-label={`${node.label}: in ${fmt(node.totalIn)}, out ${fmt(node.totalOut)}`}
|
|
1043
1068
|
onmousemove={(e) => showTip(e.clientX, e.clientY, node.label, [
|
|
1044
1069
|
{ label: 'in', value: fmt(node.totalIn) },
|
|
1045
1070
|
{ label: 'out', value: fmt(node.totalOut) },
|
|
1046
1071
|
])}
|
|
1047
1072
|
onmouseleave={clearActive}
|
|
1073
|
+
onfocus={(e) => { const b = e.currentTarget.getBoundingClientRect(); showTip(b.left + b.width / 2, b.top, node.label, [
|
|
1074
|
+
{ label: 'in', value: fmt(node.totalIn) },
|
|
1075
|
+
{ label: 'out', value: fmt(node.totalOut) },
|
|
1076
|
+
]) }}
|
|
1077
|
+
onblur={clearActive}
|
|
1048
1078
|
/>
|
|
1049
1079
|
<text class="sv-grid-chart-sankey-label"
|
|
1050
1080
|
x={node.column === 0 ? node.x + node.w + 4 : node.x - 4}
|
|
@@ -1270,6 +1300,10 @@
|
|
|
1270
1300
|
font-weight: 600;
|
|
1271
1301
|
font-family: inherit;
|
|
1272
1302
|
}
|
|
1303
|
+
.sv-grid-chart-group-label {
|
|
1304
|
+
fill: var(--sg-fg, #0f172a);
|
|
1305
|
+
font-weight: 600;
|
|
1306
|
+
}
|
|
1273
1307
|
.sv-grid-chart-crosshair {
|
|
1274
1308
|
stroke: var(--sg-accent, #2563eb);
|
|
1275
1309
|
stroke-width: 1;
|