@perspective-dev/viewer-charts 5.2.0 → 5.3.1
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/cdn/perspective-viewer-charts.js +1 -2
- package/dist/cdn/perspective-viewer-charts.js.map +4 -4
- package/dist/esm/axis/legend.d.ts +53 -5
- package/dist/esm/axis/map-ticks.d.ts +15 -0
- package/dist/esm/charts/candlestick/candlestick-interact.d.ts +2 -6
- package/dist/esm/charts/candlestick/candlestick-render.d.ts +3 -0
- package/dist/esm/charts/cartesian/cartesian.d.ts +7 -0
- package/dist/esm/charts/cartesian/glyph.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/density.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/lines.d.ts +2 -1
- package/dist/esm/charts/cartesian/glyphs/points.d.ts +2 -1
- package/dist/esm/charts/cartesian/tooltip-lines.d.ts +1 -1
- package/dist/esm/charts/chart-base.d.ts +24 -9
- package/dist/esm/charts/chart.d.ts +81 -6
- package/dist/esm/charts/common/category-axis-resolver.d.ts +1 -1
- package/dist/esm/charts/common/draw-tooltip-box.d.ts +2 -1
- package/dist/esm/charts/common/tree-chart.d.ts +1 -0
- package/dist/esm/charts/common/tree-interact.d.ts +1 -1
- package/dist/esm/charts/heatmap/heatmap.d.ts +1 -0
- package/dist/esm/charts/series/series-build.d.ts +7 -0
- package/dist/esm/charts/series/series-interact.d.ts +1 -1
- package/dist/esm/charts/series/series-type.d.ts +18 -0
- package/dist/esm/charts/series/series.d.ts +10 -1
- package/dist/esm/charts/sunburst/sunburst-render.d.ts +4 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/interaction/axis-indicators.d.ts +11 -0
- package/dist/esm/interaction/host-sink-dom.d.ts +3 -2
- package/dist/esm/interaction/host-sink-message.d.ts +3 -2
- package/dist/esm/interaction/lazy-tooltip.d.ts +2 -2
- package/dist/esm/interaction/legend-controller.d.ts +246 -0
- package/dist/esm/interaction/tooltip-controller.d.ts +20 -95
- package/dist/esm/interaction/tooltip-grid.d.ts +33 -0
- package/dist/esm/layout/facet-grid.d.ts +6 -0
- package/dist/esm/layout/ticks.d.ts +1 -0
- package/dist/esm/map/tile-source.d.ts +77 -8
- package/dist/esm/perspective-viewer-charts.js +1 -2
- package/dist/esm/perspective-viewer-charts.js.map +4 -4
- package/dist/esm/plugin/charts.d.ts +2 -0
- package/dist/esm/plugin/format-defaults.d.ts +9 -0
- package/dist/esm/plugin/plugin.d.ts +28 -1
- package/dist/esm/theme/gradient.d.ts +32 -0
- package/dist/esm/theme/overrides.d.ts +19 -0
- package/dist/esm/transport/protocol.d.ts +58 -5
- package/dist/esm/transport/renderer-transport.d.ts +3 -0
- package/dist/esm/utils/css.d.ts +4 -0
- package/dist/esm/worker/renderer.worker.d.ts +46 -1
- package/package.json +1 -1
- package/src/css/perspective-viewer-charts.css +0 -1
- package/src/ts/axis/bar-axis.ts +2 -7
- package/src/ts/axis/legend.ts +351 -24
- package/src/ts/axis/map-ticks.ts +94 -0
- package/src/ts/axis/numeric-axis.ts +2 -9
- package/src/ts/charts/candlestick/candlestick-build.ts +1 -0
- package/src/ts/charts/candlestick/candlestick-interact.ts +46 -40
- package/src/ts/charts/candlestick/candlestick-render.ts +81 -10
- package/src/ts/charts/candlestick/candlestick.ts +4 -3
- package/src/ts/charts/cartesian/cartesian-interact.ts +9 -3
- package/src/ts/charts/cartesian/cartesian-render.ts +374 -75
- package/src/ts/charts/cartesian/cartesian.ts +13 -0
- package/src/ts/charts/cartesian/glyph.ts +6 -2
- package/src/ts/charts/cartesian/glyphs/density.ts +2 -2
- package/src/ts/charts/cartesian/glyphs/lines.ts +15 -11
- package/src/ts/charts/cartesian/glyphs/points.ts +9 -5
- package/src/ts/charts/cartesian/tooltip-lines.ts +8 -8
- package/src/ts/charts/chart-base.ts +76 -42
- package/src/ts/charts/chart.ts +111 -7
- package/src/ts/charts/common/category-axis-resolver.ts +44 -5
- package/src/ts/charts/common/draw-tooltip-box.ts +13 -30
- package/src/ts/charts/common/tree-chart.ts +4 -0
- package/src/ts/charts/common/tree-chrome.ts +102 -23
- package/src/ts/charts/common/tree-interact.ts +18 -17
- package/src/ts/charts/heatmap/heatmap-build.ts +7 -1
- package/src/ts/charts/heatmap/heatmap-interact.ts +75 -8
- package/src/ts/charts/heatmap/heatmap-render.ts +105 -24
- package/src/ts/charts/heatmap/heatmap.ts +8 -3
- package/src/ts/charts/map/map.ts +3 -5
- package/src/ts/charts/series/series-build.ts +15 -2
- package/src/ts/charts/series/series-interact.ts +14 -24
- package/src/ts/charts/series/series-render.ts +310 -132
- package/src/ts/charts/series/series-type.ts +65 -0
- package/src/ts/charts/series/series.ts +31 -8
- package/src/ts/charts/sunburst/sunburst-render.ts +16 -1
- package/src/ts/charts/treemap/treemap-render.ts +12 -1
- package/src/ts/data/view-reader.ts +1 -4
- package/src/ts/index.ts +2 -0
- package/src/ts/interaction/axis-indicators.ts +137 -0
- package/src/ts/interaction/host-sink-dom.ts +66 -2
- package/src/ts/interaction/host-sink-message.ts +8 -2
- package/src/ts/interaction/lazy-tooltip.ts +5 -5
- package/src/ts/interaction/legend-controller.ts +1084 -0
- package/src/ts/interaction/tooltip-controller.ts +43 -131
- package/src/ts/interaction/tooltip-grid.ts +187 -0
- package/src/ts/layout/facet-grid.ts +8 -1
- package/src/ts/layout/ticks.ts +48 -12
- package/src/ts/map/tile-loader.ts +16 -1
- package/src/ts/map/tile-source.ts +199 -53
- package/src/ts/map/tile-sources.json +18 -0
- package/src/ts/plugin/charts.ts +40 -6
- package/src/ts/plugin/format-defaults.ts +37 -0
- package/src/ts/plugin/plugin.ts +330 -76
- package/src/ts/shaders/line.vert.glsl +5 -2
- package/src/ts/shaders/scatter.vert.glsl +0 -7
- package/src/ts/theme/gradient.ts +265 -1
- package/src/ts/theme/overrides.ts +76 -0
- package/src/ts/transport/protocol.ts +60 -4
- package/src/ts/transport/renderer-transport.ts +107 -14
- package/src/ts/utils/css.ts +12 -0
- package/src/ts/worker/dispatch.ts +6 -2
- package/src/ts/worker/renderer.worker.ts +191 -7
|
@@ -26,9 +26,14 @@ import {
|
|
|
26
26
|
withScissor,
|
|
27
27
|
} from "../../webgl/plot-frame";
|
|
28
28
|
import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
|
|
29
|
+
import { tooltipStyleOf } from "../../interaction/tooltip-grid";
|
|
30
|
+
import {
|
|
31
|
+
renderAxisHoverIndicators,
|
|
32
|
+
type AxisIndicator,
|
|
33
|
+
} from "../../interaction/axis-indicators";
|
|
29
34
|
import { drawBars, BAR_TYPE_BAR_VAL as BAR_TYPE_BAR } from "./glyphs/draw-bars";
|
|
30
|
-
import { getHoveredBar } from "./series-interact";
|
|
31
|
-
import { computeNiceTicks } from "../../layout/ticks";
|
|
35
|
+
import { getHoveredBar, formatBarCategoryPath } from "./series-interact";
|
|
36
|
+
import { computeNiceTicks, stepTickFormatter } from "../../layout/ticks";
|
|
32
37
|
import {
|
|
33
38
|
renderOuterXAxis,
|
|
34
39
|
renderOuterYAxis,
|
|
@@ -57,6 +62,19 @@ import { drawFacetTitle } from "../../axis/facet-chrome";
|
|
|
57
62
|
import { getScaledContext, initCanvas } from "../../axis/canvas";
|
|
58
63
|
import { drawGridlinesX, drawGridlinesY } from "../../axis/axis-primitives";
|
|
59
64
|
import { buildBarTooltipLines } from "./series-interact";
|
|
65
|
+
import {
|
|
66
|
+
LEGEND_LINE_HEIGHT,
|
|
67
|
+
legendAutoFit,
|
|
68
|
+
paintFloatingLegendFrame,
|
|
69
|
+
paintLegendScrollbar,
|
|
70
|
+
truncateText,
|
|
71
|
+
type LegendPaintView,
|
|
72
|
+
} from "../../axis/legend";
|
|
73
|
+
import {
|
|
74
|
+
legendRightGutter,
|
|
75
|
+
resolveLegendMode,
|
|
76
|
+
legendSidebarWidth,
|
|
77
|
+
} from "../../interaction/legend-controller";
|
|
60
78
|
|
|
61
79
|
/**
|
|
62
80
|
* Reusable scratch for bar instance uploads.
|
|
@@ -529,7 +547,12 @@ export function renderBarFrame(
|
|
|
529
547
|
? 55
|
|
530
548
|
: measureCategoricalAxisWidth(provisionalDomain);
|
|
531
549
|
const estLeft = leftExtra + (hasCatLabel ? 16 : 0);
|
|
532
|
-
const estRight =
|
|
550
|
+
const estRight = legendRightGutter(
|
|
551
|
+
chart._pluginConfig,
|
|
552
|
+
hasLegend,
|
|
553
|
+
80,
|
|
554
|
+
chart._series.length,
|
|
555
|
+
);
|
|
533
556
|
const estPlotWidthH = Math.max(1, cssWidth - estLeft - estRight);
|
|
534
557
|
const bottomExtra = valueCatActive
|
|
535
558
|
? measureCategoricalAxisHeight(valueCatDomain, estPlotWidthH)
|
|
@@ -540,6 +563,7 @@ export function renderBarFrame(
|
|
|
540
563
|
hasLegend,
|
|
541
564
|
leftExtra,
|
|
542
565
|
bottomExtra,
|
|
566
|
+
rightExtra: estRight,
|
|
543
567
|
});
|
|
544
568
|
} else if (numericCat) {
|
|
545
569
|
// Y Bar with numeric category axis on X. Value axis (Y, left)
|
|
@@ -553,6 +577,12 @@ export function renderBarFrame(
|
|
|
553
577
|
hasLegend,
|
|
554
578
|
bottomExtra: 24,
|
|
555
579
|
leftExtra,
|
|
580
|
+
rightExtra: legendRightGutter(
|
|
581
|
+
chart._pluginConfig,
|
|
582
|
+
hasLegend,
|
|
583
|
+
80,
|
|
584
|
+
chart._series.length,
|
|
585
|
+
),
|
|
556
586
|
});
|
|
557
587
|
} else {
|
|
558
588
|
// Y Bar with categorical X. Value axis on the left may be
|
|
@@ -561,7 +591,12 @@ export function renderBarFrame(
|
|
|
561
591
|
? measureCategoricalAxisWidth(valueCatDomain)
|
|
562
592
|
: 55;
|
|
563
593
|
const estLeft = leftExtraBase + 16;
|
|
564
|
-
const estRight =
|
|
594
|
+
const estRight = legendRightGutter(
|
|
595
|
+
chart._pluginConfig,
|
|
596
|
+
hasLegend,
|
|
597
|
+
80,
|
|
598
|
+
chart._series.length,
|
|
599
|
+
);
|
|
565
600
|
const estPlotWidth = Math.max(1, cssWidth - estLeft - estRight);
|
|
566
601
|
const bottomExtra = measureCategoricalAxisHeight(
|
|
567
602
|
provisionalDomain,
|
|
@@ -573,6 +608,7 @@ export function renderBarFrame(
|
|
|
573
608
|
hasLegend,
|
|
574
609
|
bottomExtra,
|
|
575
610
|
leftExtra: valueCatActive ? leftExtraBase : undefined,
|
|
611
|
+
rightExtra: estRight,
|
|
576
612
|
});
|
|
577
613
|
}
|
|
578
614
|
|
|
@@ -898,7 +934,11 @@ function renderFacetedBarFrame(
|
|
|
898
934
|
cssHeight,
|
|
899
935
|
xAxis: horizontal ? valAxisMode : catAxisMode,
|
|
900
936
|
yAxis: horizontal ? catAxisMode : valAxisMode,
|
|
901
|
-
hasLegend
|
|
937
|
+
hasLegend:
|
|
938
|
+
hasLegend &&
|
|
939
|
+
resolveLegendMode(chart._pluginConfig, chart._aggregates.length) ===
|
|
940
|
+
"sidebar",
|
|
941
|
+
legendWidth: legendSidebarWidth(chart._pluginConfig, 96),
|
|
902
942
|
hasXLabel: horizontal ? true : hasCatLabel,
|
|
903
943
|
hasYLabel: horizontal ? hasCatLabel : true,
|
|
904
944
|
gap: chart._facetConfig.facet_padding,
|
|
@@ -1088,6 +1128,11 @@ export function layoutForRecord(
|
|
|
1088
1128
|
* Draw axes chrome + legend + tooltip onto the overlay canvas.
|
|
1089
1129
|
*/
|
|
1090
1130
|
export function renderBarChromeOverlay(chart: SeriesChart): void {
|
|
1131
|
+
paintBarChromeOverlay(chart);
|
|
1132
|
+
chart.presentOverlay();
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function paintBarChromeOverlay(chart: SeriesChart): void {
|
|
1091
1136
|
if (
|
|
1092
1137
|
!chart._chromeCanvas ||
|
|
1093
1138
|
!chart._lastLayout ||
|
|
@@ -1346,66 +1391,89 @@ function renderFacetedBarChromeOverlay(chart: SeriesChart): void {
|
|
|
1346
1391
|
}
|
|
1347
1392
|
|
|
1348
1393
|
/**
|
|
1349
|
-
*
|
|
1350
|
-
*
|
|
1351
|
-
* the split dimension; every split of an aggregate shares its color —
|
|
1352
|
-
* see `ensurePalette`). A legend toggle targets the aggregate's full
|
|
1353
|
-
* seriesId set, so hiding "Sales" hides it in every facet at once; an
|
|
1354
|
-
* entry reads as hidden only when ALL of its series are hidden.
|
|
1394
|
+
* One toggleable legend row, resolved lazily — only rows inside the
|
|
1395
|
+
* visible scroll window are ever materialized.
|
|
1355
1396
|
*/
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
const M = chart._aggregates.length;
|
|
1363
|
-
if (M <= 1) {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1397
|
+
interface SeriesLegendEntry {
|
|
1398
|
+
label: string;
|
|
1399
|
+
color: [number, number, number];
|
|
1400
|
+
seriesIds: number[];
|
|
1401
|
+
hidden: boolean;
|
|
1402
|
+
}
|
|
1366
1403
|
|
|
1367
|
-
|
|
1404
|
+
/**
|
|
1405
|
+
* Shared swatch-list painter for both series legends (per-series and
|
|
1406
|
+
* per-aggregate). Paints only the rows inside the scroll window,
|
|
1407
|
+
* clipped to the content rect, rebuilds `chart._legendRects` with the
|
|
1408
|
+
* visible rows' canvas-space rects (so toggle clicks stay correct
|
|
1409
|
+
* while scrolled), and reports the painted geometry to the chart's
|
|
1410
|
+
* `LegendController`.
|
|
1411
|
+
*/
|
|
1412
|
+
function paintSeriesLegend(
|
|
1413
|
+
chart: SeriesChart,
|
|
1414
|
+
box: PlotRect,
|
|
1415
|
+
view: LegendPaintView,
|
|
1416
|
+
count: number,
|
|
1417
|
+
entryAt: (i: number) => SeriesLegendEntry,
|
|
1418
|
+
): void {
|
|
1419
|
+
const ctx = chart._chromeCanvas!.getContext("2d") as Context2D | null;
|
|
1368
1420
|
if (!ctx) {
|
|
1369
1421
|
return;
|
|
1370
1422
|
}
|
|
1371
1423
|
|
|
1372
1424
|
ctx.save();
|
|
1373
|
-
|
|
1374
1425
|
const theme = chart._resolveTheme();
|
|
1426
|
+
let content = box;
|
|
1427
|
+
if (view.mode === "floating") {
|
|
1428
|
+
content = paintFloatingLegendFrame(
|
|
1429
|
+
ctx,
|
|
1430
|
+
box,
|
|
1431
|
+
theme,
|
|
1432
|
+
view.title,
|
|
1433
|
+
view.opacity,
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1375
1437
|
const swatchSize = 10;
|
|
1376
|
-
const lineHeight =
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1438
|
+
const lineHeight = LEGEND_LINE_HEIGHT;
|
|
1439
|
+
const contentHeight = count * lineHeight;
|
|
1440
|
+
const scroll = chart._legend.clampScroll(content.height, contentHeight);
|
|
1441
|
+
const scrollable = contentHeight > content.height;
|
|
1442
|
+
const textMax = Math.max(
|
|
1443
|
+
0,
|
|
1444
|
+
content.width - swatchSize - 6 - (scrollable ? 10 : 0),
|
|
1445
|
+
);
|
|
1379
1446
|
|
|
1447
|
+
ctx.beginPath();
|
|
1448
|
+
ctx.rect(content.x, content.y, content.width, content.height);
|
|
1449
|
+
ctx.clip();
|
|
1380
1450
|
ctx.font = `11px ${theme.fontFamily}`;
|
|
1381
1451
|
ctx.textAlign = "left";
|
|
1382
1452
|
ctx.textBaseline = "middle";
|
|
1383
1453
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const
|
|
1395
|
-
const
|
|
1396
|
-
const r = Math.round(color[0] * 255);
|
|
1397
|
-
const g = Math.round(color[1] * 255);
|
|
1398
|
-
const b = Math.round(color[2] * 255);
|
|
1454
|
+
const x = content.x;
|
|
1455
|
+
const start = Math.floor(scroll / lineHeight);
|
|
1456
|
+
let y = content.y + lineHeight / 2 + start * lineHeight - scroll;
|
|
1457
|
+
for (
|
|
1458
|
+
let i = start;
|
|
1459
|
+
i < count && y - lineHeight / 2 < content.y + content.height;
|
|
1460
|
+
i++
|
|
1461
|
+
) {
|
|
1462
|
+
const e = entryAt(i);
|
|
1463
|
+
const r = Math.round(e.color[0] * 255);
|
|
1464
|
+
const g = Math.round(e.color[1] * 255);
|
|
1465
|
+
const b = Math.round(e.color[2] * 255);
|
|
1399
1466
|
|
|
1400
|
-
ctx.globalAlpha = hidden ? 0.3 : 1.0;
|
|
1467
|
+
ctx.globalAlpha = e.hidden ? 0.3 : 1.0;
|
|
1401
1468
|
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
1402
1469
|
ctx.fillRect(x, y - swatchSize / 2, swatchSize, swatchSize);
|
|
1403
1470
|
|
|
1471
|
+
const shown = truncateText(ctx, e.label, textMax);
|
|
1472
|
+
const textW = ctx.measureText(shown).width;
|
|
1404
1473
|
ctx.fillStyle = theme.legendText;
|
|
1405
|
-
ctx.fillText(
|
|
1474
|
+
ctx.fillText(shown, x + swatchSize + 6, y);
|
|
1406
1475
|
|
|
1407
|
-
|
|
1408
|
-
if (hidden) {
|
|
1476
|
+
if (e.hidden) {
|
|
1409
1477
|
ctx.strokeStyle = theme.legendText;
|
|
1410
1478
|
ctx.lineWidth = 1;
|
|
1411
1479
|
ctx.beginPath();
|
|
@@ -1417,11 +1485,14 @@ function renderFacetedBarLegend(chart: SeriesChart, grid: FacetGrid): void {
|
|
|
1417
1485
|
ctx.globalAlpha = 1.0;
|
|
1418
1486
|
|
|
1419
1487
|
chart._legendRects.push({
|
|
1420
|
-
seriesIds,
|
|
1488
|
+
seriesIds: e.seriesIds,
|
|
1421
1489
|
rect: {
|
|
1422
1490
|
x: x - 2,
|
|
1423
1491
|
y: y - lineHeight / 2,
|
|
1424
|
-
width:
|
|
1492
|
+
width: Math.min(
|
|
1493
|
+
swatchSize + 6 + textW + 4,
|
|
1494
|
+
Math.max(1, content.width),
|
|
1495
|
+
),
|
|
1425
1496
|
height: lineHeight,
|
|
1426
1497
|
},
|
|
1427
1498
|
});
|
|
@@ -1430,38 +1501,95 @@ function renderFacetedBarLegend(chart: SeriesChart, grid: FacetGrid): void {
|
|
|
1430
1501
|
}
|
|
1431
1502
|
|
|
1432
1503
|
ctx.restore();
|
|
1504
|
+
paintLegendScrollbar(ctx, content, scroll, contentHeight, theme);
|
|
1505
|
+
|
|
1506
|
+
chart._legend.setPainted({
|
|
1507
|
+
mode: view.mode,
|
|
1508
|
+
box,
|
|
1509
|
+
content,
|
|
1510
|
+
contentHeight,
|
|
1511
|
+
sidebarGutter: view.sidebarGutter,
|
|
1512
|
+
});
|
|
1433
1513
|
}
|
|
1434
1514
|
|
|
1435
1515
|
/**
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1516
|
+
* Aggregate-level legend for the faceted frame, painted into the
|
|
1517
|
+
* grid's shared right gutter (or as a floating panel). One entry per
|
|
1518
|
+
* aggregate (facets absorb the split dimension; every split of an
|
|
1519
|
+
* aggregate shares its color — see `ensurePalette`). A legend toggle
|
|
1520
|
+
* targets the aggregate's full seriesId set, so hiding "Sales" hides
|
|
1521
|
+
* it in every facet at once; an entry reads as hidden only when ALL of
|
|
1522
|
+
* its series are hidden.
|
|
1440
1523
|
*/
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
chart: SeriesChart,
|
|
1445
|
-
ctx: Context2D,
|
|
1446
|
-
fontFamily: string,
|
|
1447
|
-
): void {
|
|
1448
|
-
if (chart._legendCacheValid) {
|
|
1524
|
+
function renderFacetedBarLegend(chart: SeriesChart, grid: FacetGrid): void {
|
|
1525
|
+
chart._legendRects = [];
|
|
1526
|
+
if (!chart._chromeCanvas || !chart._lastLayout) {
|
|
1449
1527
|
return;
|
|
1450
1528
|
}
|
|
1451
1529
|
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1530
|
+
const cfg = chart._pluginConfig;
|
|
1531
|
+
const M = chart._aggregates.length;
|
|
1532
|
+
const mode = resolveLegendMode(cfg, M);
|
|
1533
|
+
const floating = mode === "floating";
|
|
1534
|
+
if (M <= 1 || mode === "none" || (!floating && !grid.legendRect)) {
|
|
1535
|
+
chart._legend.clearPainted();
|
|
1536
|
+
return;
|
|
1455
1537
|
}
|
|
1456
1538
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1539
|
+
// Pre-bucket series by aggregate once (O(series)) so the per-row
|
|
1540
|
+
// resolver is O(1) — the windowed painter may touch only a few of
|
|
1541
|
+
// potentially many rows.
|
|
1542
|
+
const idsByAgg: number[][] = Array.from({ length: M }, () => []);
|
|
1543
|
+
const colorByAgg: [number, number, number][] = Array.from(
|
|
1544
|
+
{ length: M },
|
|
1545
|
+
() => [0.5, 0.5, 0.5],
|
|
1546
|
+
);
|
|
1547
|
+
for (const s of chart._series) {
|
|
1548
|
+
if (s.aggIdx >= 0 && s.aggIdx < M) {
|
|
1549
|
+
idsByAgg[s.aggIdx].push(s.seriesId);
|
|
1550
|
+
colorByAgg[s.aggIdx] = s.color;
|
|
1551
|
+
}
|
|
1461
1552
|
}
|
|
1462
1553
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1554
|
+
const layout = chart._lastLayout;
|
|
1555
|
+
const box = floating
|
|
1556
|
+
? chart._legend.floatingBox(
|
|
1557
|
+
cfg,
|
|
1558
|
+
layout.cssWidth,
|
|
1559
|
+
layout.cssHeight,
|
|
1560
|
+
legendAutoFit(
|
|
1561
|
+
chart._chromeCanvas,
|
|
1562
|
+
chart._resolveTheme(),
|
|
1563
|
+
M,
|
|
1564
|
+
() => chart._aggregates.slice(0, M),
|
|
1565
|
+
{ title: "Legend" },
|
|
1566
|
+
),
|
|
1567
|
+
)
|
|
1568
|
+
: {
|
|
1569
|
+
x: grid.legendRect!.x + 12,
|
|
1570
|
+
y: grid.legendRect!.y + 10,
|
|
1571
|
+
width: Math.max(1, grid.legendRect!.width - 16),
|
|
1572
|
+
height: Math.max(1, grid.legendRect!.height - 20),
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
paintSeriesLegend(
|
|
1576
|
+
chart,
|
|
1577
|
+
box,
|
|
1578
|
+
{
|
|
1579
|
+
mode: floating ? "floating" : "sidebar",
|
|
1580
|
+
legend: chart._legend,
|
|
1581
|
+
title: "Legend",
|
|
1582
|
+
sidebarGutter: floating ? undefined : grid.legendRect!.width,
|
|
1583
|
+
opacity: cfg.legend_opacity,
|
|
1584
|
+
},
|
|
1585
|
+
M,
|
|
1586
|
+
(k) => ({
|
|
1587
|
+
label: chart._aggregates[k],
|
|
1588
|
+
color: colorByAgg[k],
|
|
1589
|
+
seriesIds: idsByAgg[k],
|
|
1590
|
+
hidden: idsByAgg[k].every((sid) => chart._hiddenSeries.has(sid)),
|
|
1591
|
+
}),
|
|
1592
|
+
);
|
|
1465
1593
|
}
|
|
1466
1594
|
|
|
1467
1595
|
function renderBarLegend(chart: SeriesChart): void {
|
|
@@ -1470,73 +1598,65 @@ function renderBarLegend(chart: SeriesChart): void {
|
|
|
1470
1598
|
return;
|
|
1471
1599
|
}
|
|
1472
1600
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
if (!ctx) {
|
|
1601
|
+
const cfg = chart._pluginConfig;
|
|
1602
|
+
const series = chart._series;
|
|
1603
|
+
const mode = resolveLegendMode(cfg, series.length);
|
|
1604
|
+
if (series.length <= 1 || mode === "none") {
|
|
1605
|
+
chart._legend.clearPainted();
|
|
1479
1606
|
return;
|
|
1480
1607
|
}
|
|
1481
1608
|
|
|
1482
|
-
ctx.save();
|
|
1483
|
-
|
|
1484
|
-
const theme = chart._resolveTheme();
|
|
1485
|
-
const textColor = theme.legendText;
|
|
1486
|
-
const fontFamily = theme.fontFamily;
|
|
1487
|
-
|
|
1488
|
-
ensureLegendLayout(chart, ctx, fontFamily);
|
|
1489
|
-
|
|
1490
1609
|
const layout = chart._lastLayout;
|
|
1491
|
-
const
|
|
1492
|
-
const
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1610
|
+
const floating = mode === "floating";
|
|
1611
|
+
const title = chart._splitBy.join(" / ") || "Legend";
|
|
1612
|
+
const box = floating
|
|
1613
|
+
? chart._legend.floatingBox(
|
|
1614
|
+
cfg,
|
|
1615
|
+
layout.cssWidth,
|
|
1616
|
+
layout.cssHeight,
|
|
1617
|
+
legendAutoFit(
|
|
1618
|
+
chart._chromeCanvas,
|
|
1619
|
+
chart._resolveTheme(),
|
|
1620
|
+
series.length,
|
|
1621
|
+
function* () {
|
|
1622
|
+
for (const s of series) {
|
|
1623
|
+
yield s.label;
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
{ title },
|
|
1627
|
+
),
|
|
1628
|
+
)
|
|
1629
|
+
: {
|
|
1630
|
+
x: layout.plotRect.x + layout.plotRect.width + 12,
|
|
1631
|
+
y: layout.margins.top + 10,
|
|
1632
|
+
width: Math.max(
|
|
1633
|
+
1,
|
|
1634
|
+
layout.cssWidth -
|
|
1635
|
+
layout.plotRect.x -
|
|
1636
|
+
layout.plotRect.width -
|
|
1637
|
+
16,
|
|
1638
|
+
),
|
|
1639
|
+
height: Math.max(1, layout.plotRect.height - 10),
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
paintSeriesLegend(
|
|
1643
|
+
chart,
|
|
1644
|
+
box,
|
|
1645
|
+
{
|
|
1646
|
+
mode: floating ? "floating" : "sidebar",
|
|
1647
|
+
legend: chart._legend,
|
|
1648
|
+
title,
|
|
1649
|
+
sidebarGutter: floating ? undefined : layout.margins.right,
|
|
1650
|
+
opacity: cfg.legend_opacity,
|
|
1651
|
+
},
|
|
1652
|
+
series.length,
|
|
1653
|
+
(i) => ({
|
|
1654
|
+
label: series[i].label,
|
|
1655
|
+
color: series[i].color,
|
|
1656
|
+
seriesIds: [series[i].seriesId],
|
|
1657
|
+
hidden: chart._hiddenSeries.has(series[i].seriesId),
|
|
1658
|
+
}),
|
|
1659
|
+
);
|
|
1540
1660
|
}
|
|
1541
1661
|
|
|
1542
1662
|
function renderBarTooltipCanvas(chart: SeriesChart): void {
|
|
@@ -1575,13 +1695,71 @@ function renderBarTooltipCanvas(chart: SeriesChart): void {
|
|
|
1575
1695
|
|
|
1576
1696
|
const lines = buildBarTooltipLines(chart, b);
|
|
1577
1697
|
const theme = chart._resolveTheme();
|
|
1698
|
+
const dpr = chart._glManager?.dpr ?? 1;
|
|
1699
|
+
|
|
1700
|
+
const datumPos =
|
|
1701
|
+
b.axis === 0
|
|
1702
|
+
? chart._isHorizontal
|
|
1703
|
+
? layout.dataToPixel(b.y1, b.xCenter)
|
|
1704
|
+
: layout.dataToPixel(b.xCenter, b.y1)
|
|
1705
|
+
: rightAxisDataToPixel(chart, b.xCenter, b.y1, layout);
|
|
1706
|
+
const indicators: AxisIndicator[] = [];
|
|
1707
|
+
|
|
1708
|
+
let categoryText: string;
|
|
1709
|
+
if (
|
|
1710
|
+
chart._categoryAxisMode === "numeric" &&
|
|
1711
|
+
chart._categoryPositions &&
|
|
1712
|
+
chart._categoryPositions[b.catIdx] != null
|
|
1713
|
+
) {
|
|
1714
|
+
const catFmt =
|
|
1715
|
+
chart.getColumnFormatter(chart._groupBy[0], "tick") ??
|
|
1716
|
+
stepTickFormatter(
|
|
1717
|
+
chart._numericCategoryDomain?.isDate,
|
|
1718
|
+
chart._lastCatTicks,
|
|
1719
|
+
);
|
|
1720
|
+
categoryText = catFmt(chart._categoryPositions[b.catIdx]);
|
|
1721
|
+
} else {
|
|
1722
|
+
categoryText = formatBarCategoryPath(chart, b.catIdx);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (categoryText !== "") {
|
|
1726
|
+
indicators.push({
|
|
1727
|
+
side: chart._isHorizontal ? "left" : "bottom",
|
|
1728
|
+
px: datumPos.px,
|
|
1729
|
+
py: datumPos.py,
|
|
1730
|
+
text: categoryText,
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const valueDomain =
|
|
1735
|
+
b.axis === 1 ? chart._lastAltYDomain : chart._lastYDomain;
|
|
1736
|
+
const valueTicks = b.axis === 1 ? chart._lastAltYTicks : chart._lastYTicks;
|
|
1737
|
+
const valueFmt =
|
|
1738
|
+
chart.getColumnFormatter(chart._series[b.seriesId]?.aggName, "tick") ??
|
|
1739
|
+
stepTickFormatter(valueDomain?.isDate, valueTicks);
|
|
1740
|
+
indicators.push({
|
|
1741
|
+
side: chart._isHorizontal ? "bottom" : b.axis === 1 ? "right" : "left",
|
|
1742
|
+
px: datumPos.px,
|
|
1743
|
+
py: datumPos.py,
|
|
1744
|
+
text: valueFmt(b.y1),
|
|
1745
|
+
});
|
|
1746
|
+
renderAxisHoverIndicators(
|
|
1747
|
+
chart._chromeCanvas,
|
|
1748
|
+
layout,
|
|
1749
|
+
theme,
|
|
1750
|
+
dpr,
|
|
1751
|
+
indicators,
|
|
1752
|
+
chart._pluginConfig.tooltip_opacity,
|
|
1753
|
+
);
|
|
1754
|
+
|
|
1578
1755
|
renderCanvasTooltip(
|
|
1579
1756
|
chart._chromeCanvas,
|
|
1580
1757
|
pos,
|
|
1581
1758
|
lines,
|
|
1582
1759
|
layout,
|
|
1583
1760
|
theme,
|
|
1584
|
-
|
|
1761
|
+
dpr,
|
|
1762
|
+
tooltipStyleOf(chart._pluginConfig),
|
|
1585
1763
|
);
|
|
1586
1764
|
}
|
|
1587
1765
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
+
import { parseCssColorList, parseCssColorStrict } from "../../theme/gradient";
|
|
14
|
+
|
|
13
15
|
export type ChartType = "bar" | "line" | "scatter" | "area";
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -45,6 +47,18 @@ export interface ColumnChartConfig {
|
|
|
45
47
|
* `"skip"`). Default `"skip"`. No effect on bar / scatter.
|
|
46
48
|
*/
|
|
47
49
|
interpolate?: InterpolateMode;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Single series-color override (`#rrggbb`) — the no-`split_by`
|
|
53
|
+
* shape.
|
|
54
|
+
*/
|
|
55
|
+
color?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Discrete color palette cycled over this column's split-generated
|
|
59
|
+
* series — the `split_by` shape.
|
|
60
|
+
*/
|
|
61
|
+
palette?: string;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/**
|
|
@@ -98,6 +112,57 @@ export function resolveAltAxis(
|
|
|
98
112
|
return cfg?.[aggName]?.alt_axis === true;
|
|
99
113
|
}
|
|
100
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a per-series color override from `columns_config`, or `null`
|
|
117
|
+
* when the theme palette applies.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveSeriesColorOverride(
|
|
120
|
+
series: { aggName: string; splitIdx: number },
|
|
121
|
+
facetActive: boolean,
|
|
122
|
+
cfg: Record<string, ColumnChartConfig> | undefined,
|
|
123
|
+
): [number, number, number] | null {
|
|
124
|
+
const entry = cfg?.[series.aggName];
|
|
125
|
+
if (!entry) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const palette =
|
|
130
|
+
typeof entry.palette === "string"
|
|
131
|
+
? parsePaletteCached(entry.palette)
|
|
132
|
+
: null;
|
|
133
|
+
if (palette && palette.length > 0) {
|
|
134
|
+
const idx = facetActive ? 0 : series.splitIdx % palette.length;
|
|
135
|
+
return palette[idx];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof entry.color === "string") {
|
|
139
|
+
return parseCssColorStrict(entry.color) ?? null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const PALETTE_PARSE_CACHE = new Map<
|
|
146
|
+
string,
|
|
147
|
+
Array<[number, number, number]> | null
|
|
148
|
+
>();
|
|
149
|
+
|
|
150
|
+
function parsePaletteCached(
|
|
151
|
+
src: string,
|
|
152
|
+
): Array<[number, number, number]> | null {
|
|
153
|
+
let parsed = PALETTE_PARSE_CACHE.get(src);
|
|
154
|
+
if (parsed === undefined) {
|
|
155
|
+
parsed = parseCssColorList(src);
|
|
156
|
+
if (PALETTE_PARSE_CACHE.size > 256) {
|
|
157
|
+
PALETTE_PARSE_CACHE.clear();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
PALETTE_PARSE_CACHE.set(src, parsed);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
|
|
101
166
|
/**
|
|
102
167
|
* Resolve the interpolation mode for this aggregate.
|
|
103
168
|
*/
|