@svgrid/grid 2.1.5 → 2.1.7
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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createSortedRowModel,
|
|
11
11
|
createSvGrid,
|
|
12
12
|
getGridCellDomId,
|
|
13
|
+
rowsToChartSpec,
|
|
13
14
|
sortFns,
|
|
14
15
|
tableFeatures,
|
|
15
16
|
rowSortingFeature,
|
|
@@ -21,6 +22,9 @@ import {
|
|
|
21
22
|
type Row,
|
|
22
23
|
type RowData,
|
|
23
24
|
type TableFeatures,
|
|
25
|
+
type ChartType,
|
|
26
|
+
type ChartSpec,
|
|
27
|
+
type ChartValueFormat,
|
|
24
28
|
} from "./index";
|
|
25
29
|
import {
|
|
26
30
|
createRowScrollScaling,
|
|
@@ -40,6 +44,7 @@ import type {
|
|
|
40
44
|
FilterOperator,
|
|
41
45
|
MenuPosition,
|
|
42
46
|
ContextMenuTarget,
|
|
47
|
+
ChartingConfig,
|
|
43
48
|
} from "./SvGrid.types";
|
|
44
49
|
import {
|
|
45
50
|
rawToNumber,
|
|
@@ -1302,6 +1307,356 @@ export function createSvGridController<
|
|
|
1302
1307
|
return ordered;
|
|
1303
1308
|
});
|
|
1304
1309
|
|
|
1310
|
+
// ===================================================================
|
|
1311
|
+
// Integrated charting (the `charting` prop)
|
|
1312
|
+
// ===================================================================
|
|
1313
|
+
const chartCfg = $derived.by<ChartingConfig<TData> | null>(() => {
|
|
1314
|
+
const c = props.charting;
|
|
1315
|
+
if (!c) return null;
|
|
1316
|
+
return c === true ? ({} as ChartingConfig<TData>) : (c as ChartingConfig<TData>);
|
|
1317
|
+
});
|
|
1318
|
+
const chartingEnabled = $derived(!!chartCfg);
|
|
1319
|
+
const chartIsCustom = $derived(typeof chartCfg?.buildSpec === "function");
|
|
1320
|
+
const chartingConfig = $derived(
|
|
1321
|
+
chartCfg
|
|
1322
|
+
? {
|
|
1323
|
+
position: chartCfg.position ?? "right",
|
|
1324
|
+
defaultOpen: chartCfg.defaultOpen === true,
|
|
1325
|
+
defaultType: chartCfg.defaultType ?? "bar",
|
|
1326
|
+
height: chartCfg.height ?? 300,
|
|
1327
|
+
width: chartCfg.width ?? 460,
|
|
1328
|
+
crossFilter: chartCfg.crossFilter !== false,
|
|
1329
|
+
}
|
|
1330
|
+
: null,
|
|
1331
|
+
);
|
|
1332
|
+
|
|
1333
|
+
let chartPanelOpen = $state(
|
|
1334
|
+
!!props.charting && props.charting !== true && (props.charting as ChartingConfig).defaultOpen === true,
|
|
1335
|
+
);
|
|
1336
|
+
let chartSize = $state<number | null>(null);
|
|
1337
|
+
let chartFloating = $state(false);
|
|
1338
|
+
let chartMaximized = $state(false);
|
|
1339
|
+
let chartFloatRect = $state.raw<{ x: number; y: number; w: number; h: number } | null>(null);
|
|
1340
|
+
let chartAiHandler = $state<
|
|
1341
|
+
((prompt: string) => Promise<Record<string, unknown> | null>) | null
|
|
1342
|
+
>(null);
|
|
1343
|
+
|
|
1344
|
+
// Multiple charts, each an independently-configured working set switched by a
|
|
1345
|
+
// tab strip. Pickers / spec read the ACTIVE chart.
|
|
1346
|
+
type ChartTabState = {
|
|
1347
|
+
id: string;
|
|
1348
|
+
title: string;
|
|
1349
|
+
type: ChartType;
|
|
1350
|
+
reduce: "sum" | "avg" | "count";
|
|
1351
|
+
dimensionId: string | null;
|
|
1352
|
+
measureId: string | null;
|
|
1353
|
+
seriesId: string | null | undefined;
|
|
1354
|
+
stacked: boolean | null;
|
|
1355
|
+
dataLabels: boolean | null;
|
|
1356
|
+
logScale: boolean | null;
|
|
1357
|
+
timeAxis: boolean | null;
|
|
1358
|
+
valueFormat: ChartValueFormat | null;
|
|
1359
|
+
};
|
|
1360
|
+
let chartSeq = 0;
|
|
1361
|
+
const makeChart = (title: string): ChartTabState => ({
|
|
1362
|
+
id: `chart-${chartSeq++}`,
|
|
1363
|
+
title,
|
|
1364
|
+
type:
|
|
1365
|
+
(props.charting && props.charting !== true && (props.charting as ChartingConfig).defaultType) || "bar",
|
|
1366
|
+
reduce:
|
|
1367
|
+
(props.charting && props.charting !== true && (props.charting as ChartingConfig).reduce) || "sum",
|
|
1368
|
+
dimensionId: null,
|
|
1369
|
+
measureId: null,
|
|
1370
|
+
seriesId: undefined,
|
|
1371
|
+
stacked: null,
|
|
1372
|
+
dataLabels: null,
|
|
1373
|
+
logScale: null,
|
|
1374
|
+
timeAxis: null,
|
|
1375
|
+
valueFormat: null,
|
|
1376
|
+
});
|
|
1377
|
+
// svelte-ignore state_referenced_locally
|
|
1378
|
+
let charts = $state<ChartTabState[]>([makeChart("Chart 1")]);
|
|
1379
|
+
let activeChartIndex = $state(0);
|
|
1380
|
+
const activeChart = $derived(
|
|
1381
|
+
(charts[Math.min(activeChartIndex, charts.length - 1)] ?? charts[0])!,
|
|
1382
|
+
);
|
|
1383
|
+
const chartType = $derived(activeChart.type);
|
|
1384
|
+
const chartReduce = $derived(activeChart.reduce);
|
|
1385
|
+
const chartDimensionId = $derived(activeChart.dimensionId);
|
|
1386
|
+
const chartMeasureId = $derived(activeChart.measureId);
|
|
1387
|
+
const chartSeriesId = $derived(activeChart.seriesId);
|
|
1388
|
+
const chartStacked = $derived(activeChart.stacked);
|
|
1389
|
+
const chartDataLabels = $derived(activeChart.dataLabels);
|
|
1390
|
+
const chartLogScale = $derived(activeChart.logScale);
|
|
1391
|
+
const chartTimeAxis = $derived(activeChart.timeAxis);
|
|
1392
|
+
const chartValueFormat = $derived(activeChart.valueFormat);
|
|
1393
|
+
|
|
1394
|
+
const asMeasureList = (m: string | string[] | undefined): string[] =>
|
|
1395
|
+
m == null ? [] : Array.isArray(m) ? m : [m];
|
|
1396
|
+
const isNumericColumn = (col: Column<TData>): boolean => {
|
|
1397
|
+
const dt = col.columnDef.cellDataType;
|
|
1398
|
+
if (dt === "number") return true;
|
|
1399
|
+
if (dt) return false;
|
|
1400
|
+
for (const row of allRows) {
|
|
1401
|
+
const v = row.getCellValueByColumnId(col.id);
|
|
1402
|
+
if (v == null || v === "") continue;
|
|
1403
|
+
return typeof v === "number" || (typeof v === "string" && v.trim() !== "" && !Number.isNaN(Number(v)));
|
|
1404
|
+
}
|
|
1405
|
+
return false;
|
|
1406
|
+
};
|
|
1407
|
+
const columnLabel = (col: Column<TData>): string => {
|
|
1408
|
+
const h = col.columnDef.header;
|
|
1409
|
+
return typeof h === "string" && h ? h : (col.columnDef.field ?? col.id);
|
|
1410
|
+
};
|
|
1411
|
+
const chartableColumns = $derived.by(() => {
|
|
1412
|
+
const dims: Array<{ id: string; field: string; label: string }> = [];
|
|
1413
|
+
const measures: Array<{ id: string; field: string; label: string }> = [];
|
|
1414
|
+
for (const col of allColumns) {
|
|
1415
|
+
const field = col.columnDef.field;
|
|
1416
|
+
if (!field) continue;
|
|
1417
|
+
const entry = { id: col.id, field, label: columnLabel(col) };
|
|
1418
|
+
if (isNumericColumn(col)) measures.push(entry);
|
|
1419
|
+
else dims.push(entry);
|
|
1420
|
+
}
|
|
1421
|
+
return { dims, measures };
|
|
1422
|
+
});
|
|
1423
|
+
const chartColumnDefaults = $derived.by(() => {
|
|
1424
|
+
const { dims, measures } = chartableColumns;
|
|
1425
|
+
let dimId = dims[0]?.id ?? null;
|
|
1426
|
+
let seriesId: string | null = null;
|
|
1427
|
+
let measureIds = measures[0] ? [measures[0].id] : [];
|
|
1428
|
+
const rects = getSelectionRects();
|
|
1429
|
+
if (rects.length) {
|
|
1430
|
+
let minCol = Infinity;
|
|
1431
|
+
let maxCol = -Infinity;
|
|
1432
|
+
for (const r of rects) {
|
|
1433
|
+
minCol = Math.min(minCol, r.minCol);
|
|
1434
|
+
maxCol = Math.max(maxCol, r.maxCol);
|
|
1435
|
+
}
|
|
1436
|
+
const span = allColumns.slice(minCol, maxCol + 1);
|
|
1437
|
+
const spanDims = span.filter((c) => dims.some((d) => d.id === c.id));
|
|
1438
|
+
const spanMeasures = span.filter((c) => measures.some((m) => m.id === c.id));
|
|
1439
|
+
if (maxCol > minCol) {
|
|
1440
|
+
if (spanDims[0]) dimId = spanDims[0].id;
|
|
1441
|
+
if (spanDims[1]) seriesId = spanDims[1].id;
|
|
1442
|
+
if (spanMeasures.length) measureIds = spanMeasures.map((c) => c.id);
|
|
1443
|
+
} else {
|
|
1444
|
+
if (spanDims[0]) dimId = spanDims[0].id;
|
|
1445
|
+
if (spanMeasures[0]) measureIds = [spanMeasures[0].id];
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return { dimId, seriesId, measureIds, measureId: measureIds[0] ?? null };
|
|
1449
|
+
});
|
|
1450
|
+
const effectiveChartDimensionId = $derived(
|
|
1451
|
+
chartDimensionId ?? chartCfg?.dimension ?? chartColumnDefaults.dimId,
|
|
1452
|
+
);
|
|
1453
|
+
const effectiveChartMeasureId = $derived(
|
|
1454
|
+
chartMeasureId ?? asMeasureList(chartCfg?.measures)[0] ?? chartColumnDefaults.measureId,
|
|
1455
|
+
);
|
|
1456
|
+
const effectiveChartSeriesId = $derived(
|
|
1457
|
+
chartSeriesId !== undefined ? chartSeriesId : (chartCfg?.series ?? chartColumnDefaults.seriesId),
|
|
1458
|
+
);
|
|
1459
|
+
const effectiveChartMeasureIds = $derived.by<string[]>(() => {
|
|
1460
|
+
if (chartMeasureId) return [chartMeasureId];
|
|
1461
|
+
const configured = asMeasureList(chartCfg?.measures);
|
|
1462
|
+
if (configured.length) return configured;
|
|
1463
|
+
return chartColumnDefaults.measureIds;
|
|
1464
|
+
});
|
|
1465
|
+
const effectiveChartStacked = $derived(chartStacked ?? chartCfg?.stacked ?? false);
|
|
1466
|
+
const effectiveChartDataLabels = $derived(chartDataLabels ?? chartCfg?.dataLabels ?? false);
|
|
1467
|
+
const effectiveChartLogScale = $derived(chartLogScale ?? chartCfg?.yScale === "log");
|
|
1468
|
+
const effectiveChartTimeAxis = $derived(chartTimeAxis ?? chartCfg?.timeAxis === true);
|
|
1469
|
+
const chartMeasureFormatDefault = $derived.by<ChartValueFormat | undefined>(() => {
|
|
1470
|
+
const col = allColumns.find((c) => c.id === effectiveChartMeasureId);
|
|
1471
|
+
const t = (col?.columnDef.format as { type?: string } | undefined)?.type;
|
|
1472
|
+
if (t === "currency") return "currency";
|
|
1473
|
+
if (t === "percent") return "percent";
|
|
1474
|
+
return undefined;
|
|
1475
|
+
});
|
|
1476
|
+
const effectiveChartValueFormat = $derived<ChartValueFormat | undefined>(
|
|
1477
|
+
chartValueFormat ?? chartCfg?.valueFormat ?? chartMeasureFormatDefault,
|
|
1478
|
+
);
|
|
1479
|
+
const chartAutoYAxisTitle = $derived.by<string | undefined>(() => {
|
|
1480
|
+
if (effectiveChartMeasureIds.length !== 1) return undefined;
|
|
1481
|
+
const col = allColumns.find((c) => c.id === effectiveChartMeasureId);
|
|
1482
|
+
const label = col ? columnLabel(col) : "";
|
|
1483
|
+
if (!label) return undefined;
|
|
1484
|
+
return chartReduce === "count"
|
|
1485
|
+
? "Count"
|
|
1486
|
+
: `${chartReduce === "avg" ? "Average" : "Sum"} of ${label}`;
|
|
1487
|
+
});
|
|
1488
|
+
const chartDimensionIsDate = $derived.by<boolean>(() => {
|
|
1489
|
+
const col = allColumns.find((c) => c.id === effectiveChartDimensionId);
|
|
1490
|
+
if (!col) return false;
|
|
1491
|
+
if (col.columnDef.cellDataType === "date") return true;
|
|
1492
|
+
if (col.columnDef.cellDataType) return false;
|
|
1493
|
+
for (const row of allRows) {
|
|
1494
|
+
const v = row.getCellValueByColumnId(col.id);
|
|
1495
|
+
if (v == null || v === "") continue;
|
|
1496
|
+
if (v instanceof Date) return true;
|
|
1497
|
+
return typeof v === "string" && /^\d{4}-\d{2}(-\d{2})?/.test(v);
|
|
1498
|
+
}
|
|
1499
|
+
return false;
|
|
1500
|
+
});
|
|
1501
|
+
const chartRows = $derived.by<TData[]>(() => {
|
|
1502
|
+
const rects = getSelectionRects();
|
|
1503
|
+
const pushLeaf = (out: TData[], row: Row<TData> | undefined) => {
|
|
1504
|
+
if (!row || (row.subRows && row.subRows.length > 0)) return;
|
|
1505
|
+
out.push(row.original as TData);
|
|
1506
|
+
};
|
|
1507
|
+
if (!rects.length) {
|
|
1508
|
+
const out: TData[] = [];
|
|
1509
|
+
for (const row of allRows) pushLeaf(out, row);
|
|
1510
|
+
return out;
|
|
1511
|
+
}
|
|
1512
|
+
const idx = new Set<number>();
|
|
1513
|
+
for (const r of rects) for (let i = r.minRow; i <= r.maxRow; i += 1) idx.add(i);
|
|
1514
|
+
const out: TData[] = [];
|
|
1515
|
+
for (const i of Array.from(idx).sort((a, b) => a - b)) pushLeaf(out, allRows[i]);
|
|
1516
|
+
return out;
|
|
1517
|
+
});
|
|
1518
|
+
const fieldOf = (id: string | null | undefined): string | undefined =>
|
|
1519
|
+
id ? allColumns.find((c) => c.id === id)?.columnDef.field : undefined;
|
|
1520
|
+
|
|
1521
|
+
// ---- Server-side aggregation (charting.getAggregate) ----
|
|
1522
|
+
let chartServerBuckets = $state.raw<ReadonlyArray<{ category: string; series?: string; value: number }> | null>(null);
|
|
1523
|
+
let chartReqSeq = 0;
|
|
1524
|
+
$effect(() => {
|
|
1525
|
+
const fn = chartCfg?.getAggregate;
|
|
1526
|
+
if (!fn) return;
|
|
1527
|
+
const category = fieldOf(effectiveChartDimensionId);
|
|
1528
|
+
const measure = fieldOf(effectiveChartMeasureIds[0]) ?? null;
|
|
1529
|
+
const series = fieldOf(effectiveChartSeriesId);
|
|
1530
|
+
// Read valueFilters + filterMenuValues so the fetch re-runs when either
|
|
1531
|
+
// changes; hand the model the facet checklists + operator filters + global.
|
|
1532
|
+
const facets = Object.fromEntries(
|
|
1533
|
+
Object.entries(valueFilters).map(([k, set]) => [k, Array.from(set)]),
|
|
1534
|
+
);
|
|
1535
|
+
const filterModel = {
|
|
1536
|
+
facets,
|
|
1537
|
+
columns: { ...filterMenuValues },
|
|
1538
|
+
global: globalFilter,
|
|
1539
|
+
} as Record<string, unknown>;
|
|
1540
|
+
void chartCfg?.refreshKey;
|
|
1541
|
+
const seq = ++chartReqSeq;
|
|
1542
|
+
Promise.resolve(fn({ dimension: category, measure, series, reduce: chartReduce, filterModel }))
|
|
1543
|
+
.then((buckets) => {
|
|
1544
|
+
if (seq === chartReqSeq) chartServerBuckets = buckets;
|
|
1545
|
+
})
|
|
1546
|
+
.catch(() => {
|
|
1547
|
+
if (seq === chartReqSeq) chartServerBuckets = [];
|
|
1548
|
+
});
|
|
1549
|
+
});
|
|
1550
|
+
const bucketsToSpec = (
|
|
1551
|
+
buckets: ReadonlyArray<{ category: string; series?: string; value: number }>,
|
|
1552
|
+
type: ChartType,
|
|
1553
|
+
hasSeries: boolean,
|
|
1554
|
+
): ChartSpec => {
|
|
1555
|
+
const cats: string[] = [];
|
|
1556
|
+
const catIdx = new Map<string, number>();
|
|
1557
|
+
const ensureCat = (c: string) => {
|
|
1558
|
+
let i = catIdx.get(c);
|
|
1559
|
+
if (i === undefined) { i = cats.length; catIdx.set(c, i); cats.push(c); }
|
|
1560
|
+
return i;
|
|
1561
|
+
};
|
|
1562
|
+
const seriesMap = new Map<string, number[]>();
|
|
1563
|
+
for (const b of buckets) {
|
|
1564
|
+
const ci = ensureCat(b.category);
|
|
1565
|
+
const key = hasSeries ? (b.series ?? "") : "value";
|
|
1566
|
+
let arr = seriesMap.get(key);
|
|
1567
|
+
if (!arr) { arr = []; seriesMap.set(key, arr); }
|
|
1568
|
+
arr[ci] = (arr[ci] ?? 0) + b.value;
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
type,
|
|
1572
|
+
categories: cats,
|
|
1573
|
+
series: [...seriesMap.entries()].map(([label, values]) => ({
|
|
1574
|
+
label,
|
|
1575
|
+
values: cats.map((_, i) => values[i] ?? 0),
|
|
1576
|
+
})),
|
|
1577
|
+
};
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
const chartSpec = $derived.by<ChartSpec | null>(() => {
|
|
1581
|
+
if (!chartingEnabled || !chartCfg) return null;
|
|
1582
|
+
if (chartCfg.getAggregate) {
|
|
1583
|
+
if (!chartServerBuckets) return null;
|
|
1584
|
+
const spec = bucketsToSpec(chartServerBuckets, chartType, !!effectiveChartSeriesId);
|
|
1585
|
+
if (effectiveChartStacked || chartCfg.stacked100) spec.stacked = true;
|
|
1586
|
+
if (chartCfg.stacked100) spec.stacked100 = true;
|
|
1587
|
+
if (chartCfg.palette) spec.palette = chartCfg.palette;
|
|
1588
|
+
if (effectiveChartTimeAxis) spec.xType = "time";
|
|
1589
|
+
if (effectiveChartLogScale) spec.yScale = "log";
|
|
1590
|
+
if (effectiveChartValueFormat) spec.valueFormat = effectiveChartValueFormat;
|
|
1591
|
+
return spec;
|
|
1592
|
+
}
|
|
1593
|
+
if (chartCfg.buildSpec) return chartCfg.buildSpec(chartRows) ?? null;
|
|
1594
|
+
|
|
1595
|
+
const category = fieldOf(effectiveChartDimensionId);
|
|
1596
|
+
const values = effectiveChartMeasureIds.map((id) => fieldOf(id)).filter((f): f is string => !!f);
|
|
1597
|
+
if (!category || !values.length) return null;
|
|
1598
|
+
const seriesField = fieldOf(effectiveChartSeriesId);
|
|
1599
|
+
|
|
1600
|
+
const spec = rowsToChartSpec<Record<string, unknown>>(chartRows as Array<Record<string, unknown>>, {
|
|
1601
|
+
type: chartType,
|
|
1602
|
+
category: category as string,
|
|
1603
|
+
value: values.length === 1 ? values[0]! : values,
|
|
1604
|
+
...(seriesField ? { series: seriesField } : {}),
|
|
1605
|
+
reduce: chartReduce,
|
|
1606
|
+
stacked: effectiveChartStacked || chartCfg.stacked100 === true,
|
|
1607
|
+
stacked100: chartCfg.stacked100 === true,
|
|
1608
|
+
...(chartCfg.palette ? { palette: chartCfg.palette } : {}),
|
|
1609
|
+
...(chartCfg.topN ? { topN: chartCfg.topN } : {}),
|
|
1610
|
+
...(chartCfg.otherLabel ? { otherLabel: chartCfg.otherLabel } : {}),
|
|
1611
|
+
...(chartCfg.sort ? { sort: chartCfg.sort } : {}),
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
if (chartCfg.orientation) spec.orientation = chartCfg.orientation;
|
|
1615
|
+
if (effectiveChartTimeAxis) spec.xType = "time";
|
|
1616
|
+
if (effectiveChartLogScale) spec.yScale = "log";
|
|
1617
|
+
if (effectiveChartValueFormat) spec.valueFormat = effectiveChartValueFormat;
|
|
1618
|
+
if (chartType !== "pie" && spec.orientation !== "horizontal" && !spec.yAxisTitle && chartAutoYAxisTitle) {
|
|
1619
|
+
spec.yAxisTitle = chartAutoYAxisTitle;
|
|
1620
|
+
}
|
|
1621
|
+
if (effectiveChartValueFormat && activeChart.valueFormat === null) { /* inherited default */ }
|
|
1622
|
+
if (chartType === "pie") {
|
|
1623
|
+
if (chartCfg.donut) spec.innerRadius = typeof chartCfg.donut === "number" ? chartCfg.donut : 0.6;
|
|
1624
|
+
}
|
|
1625
|
+
if (chartCfg.annotations) spec.annotations = chartCfg.annotations;
|
|
1626
|
+
if (chartCfg.patternFallback) spec.patternFallback = true;
|
|
1627
|
+
const refLines = [...(chartCfg.referenceLines ?? [])];
|
|
1628
|
+
if (chartCfg.averageLine && spec.series[0]?.values.length) {
|
|
1629
|
+
const vals = spec.series[0].values;
|
|
1630
|
+
const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
1631
|
+
refLines.push({ value: avg, label: "Average", dashed: true });
|
|
1632
|
+
}
|
|
1633
|
+
if (refLines.length) spec.referenceLines = refLines;
|
|
1634
|
+
for (const s of spec.series) {
|
|
1635
|
+
if (chartCfg.trend) s.overlay = chartCfg.trend === "linear" ? "linear" : `${chartCfg.trend}:7`;
|
|
1636
|
+
if (chartCfg.smooth) s.smooth = true;
|
|
1637
|
+
if (chartCfg.seriesTypes?.[s.label]) s.type = chartCfg.seriesTypes[s.label];
|
|
1638
|
+
if (chartCfg.seriesAxes?.[s.label]) s.axis = chartCfg.seriesAxes[s.label];
|
|
1639
|
+
}
|
|
1640
|
+
return spec;
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
function applyChartCrossFilter(category: string) {
|
|
1644
|
+
const dimId = effectiveChartDimensionId;
|
|
1645
|
+
if (!dimId) return;
|
|
1646
|
+
const existing = valueFilters[dimId];
|
|
1647
|
+
const next = new Set(existing && existing.has(category) ? existing : []);
|
|
1648
|
+
if (next.has(category)) next.delete(category);
|
|
1649
|
+
else next.add(category);
|
|
1650
|
+
valueFilters = { ...valueFilters, [dimId]: next };
|
|
1651
|
+
}
|
|
1652
|
+
function clearChartCrossFilter() {
|
|
1653
|
+
const dimId = effectiveChartDimensionId;
|
|
1654
|
+
if (!dimId) return;
|
|
1655
|
+
const next = { ...valueFilters };
|
|
1656
|
+
delete next[dimId];
|
|
1657
|
+
valueFilters = next;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1305
1660
|
|
|
1306
1661
|
// Forward sort-clause changes to the consumer. Same dedupe pattern as the
|
|
1307
1662
|
// selection callback above - fires only when the serialized clauses change.
|
|
@@ -2351,6 +2706,132 @@ export function createSvGridController<
|
|
|
2351
2706
|
get toolPanelHeaderLabel() { return toolPanelHeaderLabel; },
|
|
2352
2707
|
get toggleColumnVisibleInPanel() { return toggleColumnVisibleInPanel; },
|
|
2353
2708
|
get moveColumnInPanel() { return moveColumnInPanel; },
|
|
2709
|
+
// ---- Integrated charting ----
|
|
2710
|
+
get chartingEnabled() { return chartingEnabled; },
|
|
2711
|
+
get chartingConfig() { return chartingConfig; },
|
|
2712
|
+
get chartCfg() { return chartCfg; },
|
|
2713
|
+
get chartIsCustom() { return chartIsCustom; },
|
|
2714
|
+
get chartPanelOpen() { return chartPanelOpen; },
|
|
2715
|
+
set chartPanelOpen(v) { chartPanelOpen = v as never; },
|
|
2716
|
+
get chartType() { return chartType; },
|
|
2717
|
+
set chartType(v) { activeChart.type = v as never; },
|
|
2718
|
+
get chartReduce() { return chartReduce; },
|
|
2719
|
+
set chartReduce(v) { activeChart.reduce = v as never; },
|
|
2720
|
+
get chartDimensionId() { return effectiveChartDimensionId; },
|
|
2721
|
+
set chartDimensionId(v) { activeChart.dimensionId = v as never; },
|
|
2722
|
+
get chartMeasureId() { return effectiveChartMeasureId; },
|
|
2723
|
+
set chartMeasureId(v) { activeChart.measureId = v as never; },
|
|
2724
|
+
get chartSeriesId() { return effectiveChartSeriesId; },
|
|
2725
|
+
set chartSeriesId(v) { activeChart.seriesId = v as never; },
|
|
2726
|
+
get chartStacked() { return effectiveChartStacked; },
|
|
2727
|
+
set chartStacked(v) { activeChart.stacked = v as never; },
|
|
2728
|
+
get chartDataLabels() { return effectiveChartDataLabels; },
|
|
2729
|
+
set chartDataLabels(v) { activeChart.dataLabels = v as never; },
|
|
2730
|
+
get chartLogScale() { return effectiveChartLogScale; },
|
|
2731
|
+
set chartLogScale(v) { activeChart.logScale = v as never; },
|
|
2732
|
+
get chartTimeAxis() { return effectiveChartTimeAxis; },
|
|
2733
|
+
set chartTimeAxis(v) { activeChart.timeAxis = v as never; },
|
|
2734
|
+
get chartValueFormat() { return effectiveChartValueFormat ?? "number"; },
|
|
2735
|
+
set chartValueFormat(v) { activeChart.valueFormat = v as never; },
|
|
2736
|
+
get chartDimensionIsDate() { return chartDimensionIsDate; },
|
|
2737
|
+
get chartSize() { return chartSize; },
|
|
2738
|
+
set chartSize(v) { chartSize = v as never; },
|
|
2739
|
+
get chartFloating() { return chartFloating; },
|
|
2740
|
+
set chartFloating(v) { chartFloating = v as never; },
|
|
2741
|
+
get chartMaximized() { return chartMaximized; },
|
|
2742
|
+
set chartMaximized(v) { chartMaximized = v as never; },
|
|
2743
|
+
get chartFloatRect() { return chartFloatRect; },
|
|
2744
|
+
set chartFloatRect(v) { chartFloatRect = v as never; },
|
|
2745
|
+
get chartableColumns() { return chartableColumns; },
|
|
2746
|
+
get chartSpec() { return chartSpec; },
|
|
2747
|
+
applyChartCrossFilter,
|
|
2748
|
+
clearChartCrossFilter,
|
|
2749
|
+
// Multiple charts (tab strip)
|
|
2750
|
+
get charts() { return charts.map((c) => ({ id: c.id, title: c.title })); },
|
|
2751
|
+
get activeChartIndex() { return Math.min(activeChartIndex, charts.length - 1); },
|
|
2752
|
+
set activeChartIndex(v) { activeChartIndex = Math.max(0, Math.min(charts.length - 1, v as number)); },
|
|
2753
|
+
addChart() {
|
|
2754
|
+
charts = [...charts, makeChart(`Chart ${charts.length + 1}`)];
|
|
2755
|
+
activeChartIndex = charts.length - 1;
|
|
2756
|
+
},
|
|
2757
|
+
removeChart(index?: number) {
|
|
2758
|
+
if (charts.length <= 1) return;
|
|
2759
|
+
const idx = index ?? activeChartIndex;
|
|
2760
|
+
charts = charts.filter((_, k) => k !== idx);
|
|
2761
|
+
if (activeChartIndex >= charts.length) activeChartIndex = charts.length - 1;
|
|
2762
|
+
},
|
|
2763
|
+
renameChart(index: number, title: string) {
|
|
2764
|
+
const c = charts[index];
|
|
2765
|
+
if (c) c.title = title;
|
|
2766
|
+
},
|
|
2767
|
+
getChartsState() {
|
|
2768
|
+
return charts.map((c) => ({
|
|
2769
|
+
title: c.title,
|
|
2770
|
+
type: c.type,
|
|
2771
|
+
dimension: c.dimensionId,
|
|
2772
|
+
series: c.seriesId ?? null,
|
|
2773
|
+
measure: c.measureId,
|
|
2774
|
+
reduce: c.reduce,
|
|
2775
|
+
stacked: c.stacked,
|
|
2776
|
+
dataLabels: c.dataLabels,
|
|
2777
|
+
logScale: c.logScale,
|
|
2778
|
+
timeAxis: c.timeAxis,
|
|
2779
|
+
valueFormat: c.valueFormat,
|
|
2780
|
+
}));
|
|
2781
|
+
},
|
|
2782
|
+
applyChartsState(list: ReadonlyArray<Record<string, unknown>>, active?: number) {
|
|
2783
|
+
const restored = list.map((c, i) => ({
|
|
2784
|
+
id: `chart-${chartSeq++}`,
|
|
2785
|
+
title: typeof c.title === "string" ? c.title : `Chart ${i + 1}`,
|
|
2786
|
+
type: (c.type as ChartType) ?? "bar",
|
|
2787
|
+
reduce: (c.reduce as "sum" | "avg" | "count") ?? "sum",
|
|
2788
|
+
dimensionId: (c.dimension as string | null) ?? null,
|
|
2789
|
+
measureId: (c.measure as string | null) ?? null,
|
|
2790
|
+
seriesId: c.series as string | null | undefined,
|
|
2791
|
+
stacked: (c.stacked as boolean | null) ?? null,
|
|
2792
|
+
dataLabels: (c.dataLabels as boolean | null) ?? null,
|
|
2793
|
+
logScale: (c.logScale as boolean | null) ?? null,
|
|
2794
|
+
timeAxis: (c.timeAxis as boolean | null) ?? null,
|
|
2795
|
+
valueFormat: (c.valueFormat as ChartValueFormat | null) ?? null,
|
|
2796
|
+
}));
|
|
2797
|
+
charts = restored.length ? restored : [makeChart("Chart 1")];
|
|
2798
|
+
activeChartIndex = Math.max(0, Math.min(charts.length - 1, active ?? 0));
|
|
2799
|
+
},
|
|
2800
|
+
get chartAiHandler() { return chartAiHandler; },
|
|
2801
|
+
set chartAiHandler(v) { chartAiHandler = v as never; },
|
|
2802
|
+
applyChartConfig(config: {
|
|
2803
|
+
open?: boolean;
|
|
2804
|
+
type?: ChartType;
|
|
2805
|
+
dimension?: string | null;
|
|
2806
|
+
series?: string | null;
|
|
2807
|
+
measure?: string | null;
|
|
2808
|
+
reduce?: "sum" | "avg" | "count";
|
|
2809
|
+
stacked?: boolean;
|
|
2810
|
+
dataLabels?: boolean;
|
|
2811
|
+
logScale?: boolean;
|
|
2812
|
+
timeAxis?: boolean;
|
|
2813
|
+
valueFormat?: ChartValueFormat;
|
|
2814
|
+
}) {
|
|
2815
|
+
const cols = [...chartableColumns.dims, ...chartableColumns.measures];
|
|
2816
|
+
const resolve = (v: string | null | undefined): string | null => {
|
|
2817
|
+
if (v == null) return null;
|
|
2818
|
+
const byId = cols.find((c) => c.id === v);
|
|
2819
|
+
if (byId) return byId.id;
|
|
2820
|
+
const byField = cols.find((c) => c.field === v);
|
|
2821
|
+
return byField ? byField.id : v;
|
|
2822
|
+
};
|
|
2823
|
+
if (config.open !== false) chartPanelOpen = true;
|
|
2824
|
+
if (config.type) activeChart.type = config.type;
|
|
2825
|
+
if ("dimension" in config) activeChart.dimensionId = resolve(config.dimension);
|
|
2826
|
+
if ("series" in config) activeChart.seriesId = config.series == null ? null : resolve(config.series);
|
|
2827
|
+
if ("measure" in config) activeChart.measureId = resolve(config.measure);
|
|
2828
|
+
if (config.reduce) activeChart.reduce = config.reduce;
|
|
2829
|
+
if (typeof config.stacked === "boolean") activeChart.stacked = config.stacked;
|
|
2830
|
+
if (typeof config.dataLabels === "boolean") activeChart.dataLabels = config.dataLabels;
|
|
2831
|
+
if (typeof config.logScale === "boolean") activeChart.logScale = config.logScale;
|
|
2832
|
+
if (typeof config.timeAxis === "boolean") activeChart.timeAxis = config.timeAxis;
|
|
2833
|
+
if (config.valueFormat) activeChart.valueFormat = config.valueFormat;
|
|
2834
|
+
},
|
|
2354
2835
|
get toggleGroupInPanel() { return toggleGroupInPanel; },
|
|
2355
2836
|
get lastSortingSerialized() { return lastSortingSerialized; },
|
|
2356
2837
|
set lastSortingSerialized(v) { lastSortingSerialized = v as never; },
|
package/src/SvGrid.css
CHANGED
|
@@ -2375,3 +2375,145 @@ select.sv-grid-fr-editor {
|
|
|
2375
2375
|
font-size: 0.82rem;
|
|
2376
2376
|
color: var(--sg-fg, #101828);
|
|
2377
2377
|
}
|
|
2378
|
+
|
|
2379
|
+
/* ===================================================================
|
|
2380
|
+
Integrated chart panel (the `charting` prop)
|
|
2381
|
+
=================================================================== */
|
|
2382
|
+
.sv-grid-chart-toggle svg { flex: none; }
|
|
2383
|
+
.sv-grid-chart-panel {
|
|
2384
|
+
position: absolute;
|
|
2385
|
+
z-index: 900;
|
|
2386
|
+
display: flex;
|
|
2387
|
+
flex-direction: column;
|
|
2388
|
+
background: var(--sg-bg, #fff);
|
|
2389
|
+
color: var(--sg-fg, #0f172a);
|
|
2390
|
+
}
|
|
2391
|
+
.sv-grid-chart-panel.is-bottom {
|
|
2392
|
+
left: 0;
|
|
2393
|
+
right: 0;
|
|
2394
|
+
bottom: 0;
|
|
2395
|
+
border-top: 1px solid var(--sg-border, #e2e8f0);
|
|
2396
|
+
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.12);
|
|
2397
|
+
}
|
|
2398
|
+
.sv-grid-chart-panel.is-right {
|
|
2399
|
+
top: 0;
|
|
2400
|
+
right: 0;
|
|
2401
|
+
bottom: 0;
|
|
2402
|
+
border-left: 1px solid var(--sg-border, #e2e8f0);
|
|
2403
|
+
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.12);
|
|
2404
|
+
}
|
|
2405
|
+
.sv-grid-chart-panel.is-floating {
|
|
2406
|
+
top: auto;
|
|
2407
|
+
bottom: auto;
|
|
2408
|
+
left: auto;
|
|
2409
|
+
right: auto;
|
|
2410
|
+
border: 1px solid var(--sg-border, #e2e8f0);
|
|
2411
|
+
border-radius: calc(var(--sg-radius, 6px) + 2px);
|
|
2412
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.28);
|
|
2413
|
+
overflow: hidden;
|
|
2414
|
+
}
|
|
2415
|
+
.sv-grid-chart-resizer {
|
|
2416
|
+
position: absolute;
|
|
2417
|
+
z-index: 1;
|
|
2418
|
+
background: transparent;
|
|
2419
|
+
touch-action: none;
|
|
2420
|
+
}
|
|
2421
|
+
.sv-grid-chart-panel.is-bottom > .sv-grid-chart-resizer { top: -3px; left: 0; right: 0; height: 7px; cursor: ns-resize; }
|
|
2422
|
+
.sv-grid-chart-panel.is-right > .sv-grid-chart-resizer { left: -3px; top: 0; bottom: 0; width: 7px; cursor: ew-resize; }
|
|
2423
|
+
.sv-grid-chart-resizer:hover,
|
|
2424
|
+
.sv-grid-chart-resizer.is-dragging { background: color-mix(in srgb, var(--sg-accent, #2563eb) 30%, transparent); }
|
|
2425
|
+
.sv-grid-chart-float-resizer {
|
|
2426
|
+
position: absolute;
|
|
2427
|
+
right: 0;
|
|
2428
|
+
bottom: 0;
|
|
2429
|
+
width: 16px;
|
|
2430
|
+
height: 16px;
|
|
2431
|
+
cursor: nwse-resize;
|
|
2432
|
+
touch-action: none;
|
|
2433
|
+
background: linear-gradient(135deg, transparent 50%, color-mix(in srgb, var(--sg-muted, #64748b) 55%, transparent) 50%);
|
|
2434
|
+
border-bottom-right-radius: calc(var(--sg-radius, 6px) + 2px);
|
|
2435
|
+
}
|
|
2436
|
+
.sv-grid-chart-panel-head {
|
|
2437
|
+
display: flex;
|
|
2438
|
+
flex-direction: column;
|
|
2439
|
+
align-items: stretch;
|
|
2440
|
+
gap: 8px;
|
|
2441
|
+
padding: 8px 12px 10px;
|
|
2442
|
+
border-bottom: 1px solid var(--sg-border, #e2e8f0);
|
|
2443
|
+
background: var(--sg-header-bg, #f8fafc);
|
|
2444
|
+
}
|
|
2445
|
+
.sv-grid-chart-drawer-title { display: flex; align-items: center; gap: 7px; }
|
|
2446
|
+
.sv-grid-chart-drawer-title.is-drag-handle { cursor: move; user-select: none; }
|
|
2447
|
+
.sv-grid-chart-title-icon { color: var(--sg-accent, #2563eb); flex: none; }
|
|
2448
|
+
.sv-grid-chart-title { font-size: 13px; font-weight: 700; letter-spacing: 0.01em; color: var(--sg-fg, #0f172a); }
|
|
2449
|
+
.sv-grid-chart-drawer-title .sv-grid-chart-actions { margin-left: auto; }
|
|
2450
|
+
.sv-grid-chart-actions { display: inline-flex; align-items: center; gap: 6px; }
|
|
2451
|
+
.sv-grid-chart-controls { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
|
|
2452
|
+
.sv-grid-chart-controls:empty { display: none; }
|
|
2453
|
+
.sv-grid-chart-ctl { display: flex; flex-direction: column; gap: 2px; font-size: 11px; font-weight: 600; color: var(--sg-muted, #64748b); }
|
|
2454
|
+
.sv-grid-chart-ctl select {
|
|
2455
|
+
font: inherit; font-size: 12.5px; font-weight: 400; color: var(--sg-fg, #0f172a);
|
|
2456
|
+
background: var(--sg-input-bg, var(--sg-bg, #fff));
|
|
2457
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #e2e8f0));
|
|
2458
|
+
border-radius: var(--sg-radius, 6px); padding: 4px 8px;
|
|
2459
|
+
}
|
|
2460
|
+
.sv-grid-chart-toggle {
|
|
2461
|
+
display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600;
|
|
2462
|
+
color: var(--sg-muted, #64748b); cursor: pointer; white-space: nowrap;
|
|
2463
|
+
}
|
|
2464
|
+
.sv-grid-chart-btn {
|
|
2465
|
+
font: inherit; font-size: 12px; font-weight: 550; padding: 5px 10px;
|
|
2466
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 6px);
|
|
2467
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); cursor: pointer;
|
|
2468
|
+
}
|
|
2469
|
+
.sv-grid-chart-btn:hover:not(:disabled) { border-color: var(--sg-accent, #2563eb); color: var(--sg-accent, #2563eb); }
|
|
2470
|
+
.sv-grid-chart-btn:disabled { opacity: 0.5; cursor: default; }
|
|
2471
|
+
.sv-grid-chart-icon-btn {
|
|
2472
|
+
display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px;
|
|
2473
|
+
border: 1px solid transparent; border-radius: var(--sg-radius, 6px); background: transparent;
|
|
2474
|
+
color: var(--sg-muted, #64748b); cursor: pointer;
|
|
2475
|
+
}
|
|
2476
|
+
.sv-grid-chart-icon-btn:hover { border-color: var(--sg-border, #e2e8f0); color: var(--sg-accent, #2563eb); background: var(--sg-bg, #fff); }
|
|
2477
|
+
.sv-grid-chart-close { border: 0; background: transparent; color: var(--sg-muted, #64748b); font-size: 18px; line-height: 1; cursor: pointer; padding: 0 4px; }
|
|
2478
|
+
.sv-grid-chart-close:hover { color: var(--sg-fg, #0f172a); }
|
|
2479
|
+
.sv-grid-chart-panel-body {
|
|
2480
|
+
flex: 1; min-height: 0; padding: 4px 10px 8px; overflow: hidden;
|
|
2481
|
+
display: flex; align-items: stretch; justify-content: center;
|
|
2482
|
+
}
|
|
2483
|
+
.sv-grid-chart-panel-body > .sv-grid-chart { width: 100%; justify-content: center; }
|
|
2484
|
+
.sv-grid-chart-empty { margin: 0; font-size: 13px; color: var(--sg-muted, #64748b); text-align: center; max-width: 42ch; }
|
|
2485
|
+
/* Export menu */
|
|
2486
|
+
.sv-grid-chart-export { position: relative; display: inline-flex; }
|
|
2487
|
+
.sv-grid-chart-export-menu {
|
|
2488
|
+
position: absolute; top: calc(100% + 4px); right: 0; z-index: 5; min-width: 150px; padding: 4px;
|
|
2489
|
+
display: flex; flex-direction: column; background: var(--sg-bg, #fff);
|
|
2490
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 6px);
|
|
2491
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
|
|
2492
|
+
}
|
|
2493
|
+
.sv-grid-chart-export-menu button {
|
|
2494
|
+
font: inherit; font-size: 12.5px; text-align: left; padding: 6px 10px; border: 0;
|
|
2495
|
+
border-radius: calc(var(--sg-radius, 6px) - 2px); background: transparent; color: var(--sg-fg, #0f172a); cursor: pointer; white-space: nowrap;
|
|
2496
|
+
}
|
|
2497
|
+
.sv-grid-chart-export-menu button:hover:not(:disabled) { background: var(--sg-hover-bg, color-mix(in srgb, var(--sg-accent, #2563eb) 10%, transparent)); }
|
|
2498
|
+
.sv-grid-chart-export-menu button:disabled { opacity: 0.45; cursor: default; }
|
|
2499
|
+
/* AI "chart this" */
|
|
2500
|
+
.sv-grid-chart-ai-btn { display: inline-flex; align-items: center; gap: 4px; }
|
|
2501
|
+
.sv-grid-chart-ai-btn.is-active { border-color: var(--sg-accent, #2563eb); color: var(--sg-accent, #2563eb); }
|
|
2502
|
+
.sv-grid-chart-ai { display: flex; flex-direction: column; gap: 4px; }
|
|
2503
|
+
.sv-grid-chart-ai-row { display: flex; gap: 6px; align-items: center; }
|
|
2504
|
+
.sv-grid-chart-ai-input {
|
|
2505
|
+
flex: 1; min-width: 0; font: inherit; font-size: 12.5px; color: var(--sg-fg, #0f172a);
|
|
2506
|
+
background: var(--sg-input-bg, var(--sg-bg, #fff)); border: 1px solid var(--sg-input-border, var(--sg-border, #e2e8f0));
|
|
2507
|
+
border-radius: var(--sg-radius, 6px); padding: 5px 9px;
|
|
2508
|
+
}
|
|
2509
|
+
.sv-grid-chart-ai-input:focus { outline: none; border-color: var(--sg-accent, #2563eb); }
|
|
2510
|
+
.sv-grid-chart-ai-msg { margin: 0; font-size: 12px; color: var(--sg-muted, #64748b); }
|
|
2511
|
+
.sv-grid-chart-ai-msg.is-error { color: var(--sg-danger, #dc2626); }
|
|
2512
|
+
/* Multi-chart tab strip */
|
|
2513
|
+
.sv-grid-chart-tabs { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
|
|
2514
|
+
.sv-grid-chart-tab { display: inline-flex; align-items: center; border: 1px solid var(--sg-border, #e2e8f0); border-radius: var(--sg-radius, 6px); background: var(--sg-bg, #fff); overflow: hidden; }
|
|
2515
|
+
.sv-grid-chart-tab.is-active { border-color: var(--sg-accent, #2563eb); background: color-mix(in srgb, var(--sg-accent, #2563eb) 10%, transparent); }
|
|
2516
|
+
.sv-grid-chart-tab-label { font: inherit; font-size: 12px; font-weight: 600; color: var(--sg-fg, #0f172a); background: transparent; border: 0; padding: 3px 8px; cursor: pointer; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2517
|
+
.sv-grid-chart-tab.is-active .sv-grid-chart-tab-label { color: var(--sg-accent, #2563eb); }
|
|
2518
|
+
.sv-grid-chart-tab-x { border: 0; background: transparent; color: var(--sg-muted, #64748b); font-size: 14px; line-height: 1; padding: 0 6px 0 2px; cursor: pointer; }
|
|
2519
|
+
.sv-grid-chart-tab-x:hover { color: var(--sg-fg, #0f172a); }
|