@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.
Files changed (109) hide show
  1. package/dist/cdn/perspective-viewer-charts.js +1 -2
  2. package/dist/cdn/perspective-viewer-charts.js.map +4 -4
  3. package/dist/esm/axis/legend.d.ts +53 -5
  4. package/dist/esm/axis/map-ticks.d.ts +15 -0
  5. package/dist/esm/charts/candlestick/candlestick-interact.d.ts +2 -6
  6. package/dist/esm/charts/candlestick/candlestick-render.d.ts +3 -0
  7. package/dist/esm/charts/cartesian/cartesian.d.ts +7 -0
  8. package/dist/esm/charts/cartesian/glyph.d.ts +2 -1
  9. package/dist/esm/charts/cartesian/glyphs/density.d.ts +2 -1
  10. package/dist/esm/charts/cartesian/glyphs/lines.d.ts +2 -1
  11. package/dist/esm/charts/cartesian/glyphs/points.d.ts +2 -1
  12. package/dist/esm/charts/cartesian/tooltip-lines.d.ts +1 -1
  13. package/dist/esm/charts/chart-base.d.ts +24 -9
  14. package/dist/esm/charts/chart.d.ts +81 -6
  15. package/dist/esm/charts/common/category-axis-resolver.d.ts +1 -1
  16. package/dist/esm/charts/common/draw-tooltip-box.d.ts +2 -1
  17. package/dist/esm/charts/common/tree-chart.d.ts +1 -0
  18. package/dist/esm/charts/common/tree-interact.d.ts +1 -1
  19. package/dist/esm/charts/heatmap/heatmap.d.ts +1 -0
  20. package/dist/esm/charts/series/series-build.d.ts +7 -0
  21. package/dist/esm/charts/series/series-interact.d.ts +1 -1
  22. package/dist/esm/charts/series/series-type.d.ts +18 -0
  23. package/dist/esm/charts/series/series.d.ts +10 -1
  24. package/dist/esm/charts/sunburst/sunburst-render.d.ts +4 -0
  25. package/dist/esm/index.d.ts +2 -0
  26. package/dist/esm/interaction/axis-indicators.d.ts +11 -0
  27. package/dist/esm/interaction/host-sink-dom.d.ts +3 -2
  28. package/dist/esm/interaction/host-sink-message.d.ts +3 -2
  29. package/dist/esm/interaction/lazy-tooltip.d.ts +2 -2
  30. package/dist/esm/interaction/legend-controller.d.ts +246 -0
  31. package/dist/esm/interaction/tooltip-controller.d.ts +20 -95
  32. package/dist/esm/interaction/tooltip-grid.d.ts +33 -0
  33. package/dist/esm/layout/facet-grid.d.ts +6 -0
  34. package/dist/esm/layout/ticks.d.ts +1 -0
  35. package/dist/esm/map/tile-source.d.ts +77 -8
  36. package/dist/esm/perspective-viewer-charts.js +1 -2
  37. package/dist/esm/perspective-viewer-charts.js.map +4 -4
  38. package/dist/esm/plugin/charts.d.ts +2 -0
  39. package/dist/esm/plugin/format-defaults.d.ts +9 -0
  40. package/dist/esm/plugin/plugin.d.ts +28 -1
  41. package/dist/esm/theme/gradient.d.ts +32 -0
  42. package/dist/esm/theme/overrides.d.ts +19 -0
  43. package/dist/esm/transport/protocol.d.ts +58 -5
  44. package/dist/esm/transport/renderer-transport.d.ts +3 -0
  45. package/dist/esm/utils/css.d.ts +4 -0
  46. package/dist/esm/worker/renderer.worker.d.ts +46 -1
  47. package/package.json +1 -1
  48. package/src/css/perspective-viewer-charts.css +0 -1
  49. package/src/ts/axis/bar-axis.ts +2 -7
  50. package/src/ts/axis/legend.ts +351 -24
  51. package/src/ts/axis/map-ticks.ts +94 -0
  52. package/src/ts/axis/numeric-axis.ts +2 -9
  53. package/src/ts/charts/candlestick/candlestick-build.ts +1 -0
  54. package/src/ts/charts/candlestick/candlestick-interact.ts +46 -40
  55. package/src/ts/charts/candlestick/candlestick-render.ts +81 -10
  56. package/src/ts/charts/candlestick/candlestick.ts +4 -3
  57. package/src/ts/charts/cartesian/cartesian-interact.ts +9 -3
  58. package/src/ts/charts/cartesian/cartesian-render.ts +374 -75
  59. package/src/ts/charts/cartesian/cartesian.ts +13 -0
  60. package/src/ts/charts/cartesian/glyph.ts +6 -2
  61. package/src/ts/charts/cartesian/glyphs/density.ts +2 -2
  62. package/src/ts/charts/cartesian/glyphs/lines.ts +15 -11
  63. package/src/ts/charts/cartesian/glyphs/points.ts +9 -5
  64. package/src/ts/charts/cartesian/tooltip-lines.ts +8 -8
  65. package/src/ts/charts/chart-base.ts +76 -42
  66. package/src/ts/charts/chart.ts +111 -7
  67. package/src/ts/charts/common/category-axis-resolver.ts +44 -5
  68. package/src/ts/charts/common/draw-tooltip-box.ts +13 -30
  69. package/src/ts/charts/common/tree-chart.ts +4 -0
  70. package/src/ts/charts/common/tree-chrome.ts +102 -23
  71. package/src/ts/charts/common/tree-interact.ts +18 -17
  72. package/src/ts/charts/heatmap/heatmap-build.ts +7 -1
  73. package/src/ts/charts/heatmap/heatmap-interact.ts +75 -8
  74. package/src/ts/charts/heatmap/heatmap-render.ts +105 -24
  75. package/src/ts/charts/heatmap/heatmap.ts +8 -3
  76. package/src/ts/charts/map/map.ts +3 -5
  77. package/src/ts/charts/series/series-build.ts +15 -2
  78. package/src/ts/charts/series/series-interact.ts +14 -24
  79. package/src/ts/charts/series/series-render.ts +310 -132
  80. package/src/ts/charts/series/series-type.ts +65 -0
  81. package/src/ts/charts/series/series.ts +31 -8
  82. package/src/ts/charts/sunburst/sunburst-render.ts +16 -1
  83. package/src/ts/charts/treemap/treemap-render.ts +12 -1
  84. package/src/ts/data/view-reader.ts +1 -4
  85. package/src/ts/index.ts +2 -0
  86. package/src/ts/interaction/axis-indicators.ts +137 -0
  87. package/src/ts/interaction/host-sink-dom.ts +66 -2
  88. package/src/ts/interaction/host-sink-message.ts +8 -2
  89. package/src/ts/interaction/lazy-tooltip.ts +5 -5
  90. package/src/ts/interaction/legend-controller.ts +1084 -0
  91. package/src/ts/interaction/tooltip-controller.ts +43 -131
  92. package/src/ts/interaction/tooltip-grid.ts +187 -0
  93. package/src/ts/layout/facet-grid.ts +8 -1
  94. package/src/ts/layout/ticks.ts +48 -12
  95. package/src/ts/map/tile-loader.ts +16 -1
  96. package/src/ts/map/tile-source.ts +199 -53
  97. package/src/ts/map/tile-sources.json +18 -0
  98. package/src/ts/plugin/charts.ts +40 -6
  99. package/src/ts/plugin/format-defaults.ts +37 -0
  100. package/src/ts/plugin/plugin.ts +330 -76
  101. package/src/ts/shaders/line.vert.glsl +5 -2
  102. package/src/ts/shaders/scatter.vert.glsl +0 -7
  103. package/src/ts/theme/gradient.ts +265 -1
  104. package/src/ts/theme/overrides.ts +76 -0
  105. package/src/ts/transport/protocol.ts +60 -4
  106. package/src/ts/transport/renderer-transport.ts +107 -14
  107. package/src/ts/utils/css.ts +12 -0
  108. package/src/ts/worker/dispatch.ts +6 -2
  109. package/src/ts/worker/renderer.worker.ts +191 -7
@@ -31,6 +31,11 @@ import {
31
31
  } from "../../webgl/plot-frame";
32
32
  import { ensureGradientTexture } from "../../webgl/gradient-texture";
33
33
  import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
34
+ import { tooltipStyleOf } from "../../interaction/tooltip-grid";
35
+ import {
36
+ renderAxisHoverIndicators,
37
+ type AxisIndicator,
38
+ } from "../../interaction/axis-indicators";
34
39
  import {
35
40
  computeTicks,
36
41
  renderGridlines,
@@ -41,6 +46,9 @@ import {
41
46
  type AxisDomain,
42
47
  } from "../../axis/numeric-axis";
43
48
  import { initCanvas, getScaledContext } from "../../axis/canvas";
49
+ import { computeMapDegreeTicks } from "../../axis/map-ticks";
50
+ import { mercatorToLonLat } from "../../map/mercator";
51
+ import { stepTickFormatter } from "../../layout/ticks";
44
52
  import {
45
53
  type CategoricalDomain,
46
54
  type CategoricalLevel,
@@ -54,7 +62,16 @@ import {
54
62
  renderLegendAt,
55
63
  renderCategoricalLegend,
56
64
  renderCategoricalLegendAt,
65
+ legendAutoFit,
66
+ gradientLegendAutoFit,
67
+ type LegendPaintView,
57
68
  } from "../../axis/legend";
69
+ import {
70
+ legendRightGutter,
71
+ legendSidebarWidth,
72
+ resolveLegendMode,
73
+ type LegendAutoFit,
74
+ } from "../../interaction/legend-controller";
58
75
 
59
76
  /**
60
77
  * NaN guard: `_xOrigin`/`_yOrigin` start as NaN before the first valid sample.
@@ -63,6 +80,43 @@ function rebaseOrigin(o: number): number {
63
80
  return isNaN(o) ? 0 : o;
64
81
  }
65
82
 
83
+ /**
84
+ * Legend entry count for `legend_mode: "auto"` resolution: the
85
+ * categorical swatch count when a string color column is wired, `0`
86
+ * for continuous gradient legends (no entry list — always compact).
87
+ */
88
+ function legendEntryCount(chart: CartesianChart): number {
89
+ return chart._colorIsString ? chart._uniqueColorLabels.size : 0;
90
+ }
91
+
92
+ /**
93
+ * Content measurements for a `legend_size_mode: "auto"` floating panel.
94
+ * Categorical legends hug their label list; gradient legends have no
95
+ * rows and hug their tick labels instead.
96
+ */
97
+ function legendFit(chart: CartesianChart, theme: Theme): LegendAutoFit {
98
+ const title = chart._colorName ?? undefined;
99
+ if (chart._colorIsString) {
100
+ return legendAutoFit(
101
+ chart._chromeCanvas,
102
+ theme,
103
+ chart._uniqueColorLabels.size,
104
+ () => chart._uniqueColorLabels.keys(),
105
+ { title },
106
+ );
107
+ }
108
+
109
+ return gradientLegendAutoFit(
110
+ chart._chromeCanvas,
111
+ theme,
112
+ { min: chart._colorMin, max: chart._colorMax },
113
+ chart._colorName
114
+ ? chart.getColumnFormatter(chart._colorName, "value")
115
+ : undefined,
116
+ title,
117
+ );
118
+ }
119
+
66
120
  /**
67
121
  * Full-frame render: gridlines → glyph draw inside the plot-frame
68
122
  * scissor → chrome overlay (axes + legend + tooltip).
@@ -368,7 +422,12 @@ function renderSinglePlotFrame(
368
422
 
369
423
  // One-pass plot-width / plot-height estimate to size the
370
424
  // categorical gutter overrides; same approach as series-render.
371
- const estRight = hasColorCol ? 80 : 16;
425
+ const estRight = legendRightGutter(
426
+ chart._pluginConfig,
427
+ hasColorCol,
428
+ 80,
429
+ legendEntryCount(chart),
430
+ );
372
431
  const estLeftPlain = 55 + (chart._yLabel ? 16 : 0);
373
432
  const estPlotWidth = Math.max(1, cssWidth - estLeftPlain - estRight);
374
433
  const leftExtra = chart._yCategoryDomain
@@ -378,13 +437,36 @@ function renderSinglePlotFrame(
378
437
  ? measureCategoricalAxisHeight(chart._xCategoryDomain, estPlotWidth)
379
438
  : undefined;
380
439
 
381
- const layout = new PlotLayout(cssWidth, cssHeight, {
382
- hasXLabel: !!chart._xLabel,
383
- hasYLabel: !!chart._yLabel,
384
- hasLegend: hasColorCol,
385
- leftExtra,
386
- bottomExtra,
387
- });
440
+ const isMap = chart._renderMode === "map";
441
+ const bareMap = isMap && !chart._pluginConfig.numeric_axes;
442
+ const layout = new PlotLayout(
443
+ cssWidth,
444
+ cssHeight,
445
+ bareMap
446
+ ? {
447
+ hasXLabel: false,
448
+ hasYLabel: false,
449
+ hasLegend: hasColorCol,
450
+ leftExtra: 0,
451
+ bottomExtra: 0,
452
+ rightExtra:
453
+ hasColorCol &&
454
+ resolveLegendMode(
455
+ chart._pluginConfig,
456
+ legendEntryCount(chart),
457
+ ) === "sidebar"
458
+ ? legendSidebarWidth(chart._pluginConfig, 80)
459
+ : 0,
460
+ }
461
+ : {
462
+ hasXLabel: !!chart._xLabel,
463
+ hasYLabel: !!chart._yLabel,
464
+ hasLegend: hasColorCol,
465
+ leftExtra,
466
+ bottomExtra,
467
+ rightExtra: estRight,
468
+ },
469
+ );
388
470
  chart._lastLayout = layout;
389
471
  if (chart._zoomController) {
390
472
  chart._zoomController.updateLayout(layout);
@@ -408,8 +490,6 @@ function renderSinglePlotFrame(
408
490
  const xTicks = chart._xIsString ? [] : numericTicks.xTicks;
409
491
  const yTicks = chart._yIsString ? [] : numericTicks.yTicks;
410
492
 
411
- const isMap = chart._renderMode === "map";
412
-
413
493
  // Defer the gridline draw past the GPU fence (see `_defer2D`) so the
414
494
  // gridline canvas doesn't present ahead of the GL glyphs on resize.
415
495
  // The closure captures this frame's `layout` / ticks / `theme`.
@@ -516,14 +596,28 @@ function renderFacetedFrame(
516
596
  // charts always have both axes, so the false branch maps to
517
597
  // per-cell mode (never to "none", which is reserved for tree
518
598
  // charts).
599
+ const isMap = chart._renderMode === "map";
600
+ const bareMap = isMap && !chart._pluginConfig.numeric_axes;
519
601
  const grid: FacetGrid = buildFacetGrid(labels, {
520
602
  cssWidth,
521
603
  cssHeight,
522
- xAxis: chart._lastEffectiveSharedX ? "outer" : "cell",
523
- yAxis: chart._lastEffectiveSharedY ? "outer" : "cell",
524
- hasLegend,
525
- hasXLabel: !!chart._xLabel,
526
- hasYLabel: !!chart._yLabel,
604
+ xAxis: bareMap
605
+ ? "none"
606
+ : chart._lastEffectiveSharedX
607
+ ? "outer"
608
+ : "cell",
609
+ yAxis: bareMap
610
+ ? "none"
611
+ : chart._lastEffectiveSharedY
612
+ ? "outer"
613
+ : "cell",
614
+ hasLegend:
615
+ hasLegend &&
616
+ resolveLegendMode(chart._pluginConfig, legendEntryCount(chart)) ===
617
+ "sidebar",
618
+ legendWidth: legendSidebarWidth(chart._pluginConfig, 96),
619
+ hasXLabel: !bareMap && !!chart._xLabel,
620
+ hasYLabel: !bareMap && !!chart._yLabel,
527
621
  gap: chart._facetConfig.facet_padding,
528
622
  });
529
623
  chart._facetGrid = grid;
@@ -618,7 +712,6 @@ function renderFacetedFrame(
618
712
  // own domain). Map mode skips gridlines entirely; the
619
713
  // basemap layer is rendered into the GL canvas inside the
620
714
  // facet's scissor below.
621
- const isMap = chart._renderMode === "map";
622
715
  if (gridlineCanvas && !isMap) {
623
716
  // Deferred to the post-fence 2D flush. The closure captures
624
717
  // this facet's `cell` (whose `cell.layout` already carries
@@ -699,6 +792,11 @@ function renderFacetedFrame(
699
792
  * Redraw the chrome canvas only. Used for lightweight hover updates.
700
793
  */
701
794
  export function renderCartesianChromeOverlay(chart: CartesianChart): void {
795
+ paintCartesianChromeOverlay(chart);
796
+ chart.presentOverlay();
797
+ }
798
+
799
+ function paintCartesianChromeOverlay(chart: CartesianChart): void {
702
800
  if (
703
801
  !chart._chromeCanvas ||
704
802
  !chart._lastLayout ||
@@ -728,6 +826,30 @@ function renderSinglePlotChromeOverlay(chart: CartesianChart): void {
728
826
  const isMap = chart._renderMode === "map";
729
827
 
730
828
  if (isMap) {
829
+ if (chart._pluginConfig.numeric_axes) {
830
+ const mt = computeMapDegreeTicks(layout);
831
+ renderCellXAxis(
832
+ chart._chromeCanvas!,
833
+ chart._lastXDomain!,
834
+ layout,
835
+ mt.xTicks,
836
+ theme,
837
+ !!chart._xLabel,
838
+ dpr,
839
+ mt.formatX,
840
+ );
841
+ renderCellYAxis(
842
+ chart._chromeCanvas!,
843
+ chart._lastYDomain!,
844
+ layout,
845
+ mt.yTicks,
846
+ theme,
847
+ !!chart._yLabel,
848
+ dpr,
849
+ mt.formatY,
850
+ );
851
+ }
852
+
731
853
  chart.renderMapChrome(chart._chromeCanvas!, layout, theme, dpr);
732
854
  } else {
733
855
  renderCartesianCellAxes(
@@ -743,37 +865,95 @@ function renderSinglePlotChromeOverlay(chart: CartesianChart): void {
743
865
  );
744
866
  }
745
867
 
746
- if (chart._lastHasColorCol) {
868
+ const legendMode = resolveLegendMode(
869
+ chart._pluginConfig,
870
+ legendEntryCount(chart),
871
+ );
872
+ let legendPainted = false;
873
+ if (chart._lastHasColorCol && legendMode !== "none") {
747
874
  const stops = chart._lastGradientStops ?? theme.gradientStops;
875
+ const floating = legendMode === "floating";
876
+ const view: LegendPaintView = {
877
+ mode: floating ? "floating" : "sidebar",
878
+ legend: chart._legend,
879
+ title: chart._colorName ?? undefined,
880
+ opacity: chart._pluginConfig.legend_opacity,
881
+ };
882
+ const floatBox = floating
883
+ ? chart._legend.floatingBox(
884
+ chart._pluginConfig,
885
+ layout.cssWidth,
886
+ layout.cssHeight,
887
+ legendFit(chart, theme),
888
+ )
889
+ : null;
748
890
  if (chart._colorIsString && chart._uniqueColorLabels.size > 0) {
749
891
  const palette = resolvePalette(
750
892
  theme.seriesPalette,
751
893
  stops,
752
894
  chart._uniqueColorLabels.size,
753
895
  );
754
- renderCategoricalLegend(
755
- chart._chromeCanvas!,
756
- layout,
757
- chart._uniqueColorLabels,
758
- palette,
759
- theme,
760
- );
896
+ if (floatBox) {
897
+ renderCategoricalLegendAt(
898
+ chart._chromeCanvas!,
899
+ floatBox,
900
+ chart._uniqueColorLabels,
901
+ palette,
902
+ theme,
903
+ view,
904
+ );
905
+ } else {
906
+ renderCategoricalLegend(
907
+ chart._chromeCanvas!,
908
+ layout,
909
+ chart._uniqueColorLabels,
910
+ palette,
911
+ theme,
912
+ view,
913
+ );
914
+ }
915
+
916
+ legendPainted = true;
761
917
  } else if (chart._colorName) {
762
- renderLegend(
763
- chart._chromeCanvas!,
764
- layout,
765
- {
766
- min: chart._colorMin,
767
- max: chart._colorMax,
768
- label: chart._colorName,
769
- },
770
- stops,
771
- theme,
772
- chart.getColumnFormatter(chart._colorName, "value"),
918
+ const colorDomain = {
919
+ min: chart._colorMin,
920
+ max: chart._colorMax,
921
+ label: chart._colorName,
922
+ };
923
+ const formatter = chart.getColumnFormatter(
924
+ chart._colorName,
925
+ "value",
773
926
  );
927
+ if (floatBox) {
928
+ renderLegendAt(
929
+ chart._chromeCanvas!,
930
+ floatBox,
931
+ colorDomain,
932
+ stops,
933
+ theme,
934
+ formatter,
935
+ view,
936
+ );
937
+ } else {
938
+ renderLegend(
939
+ chart._chromeCanvas!,
940
+ layout,
941
+ colorDomain,
942
+ stops,
943
+ theme,
944
+ formatter,
945
+ view,
946
+ );
947
+ }
948
+
949
+ legendPainted = true;
774
950
  }
775
951
  }
776
952
 
953
+ if (!legendPainted) {
954
+ chart._legend.clearPainted();
955
+ }
956
+
777
957
  renderScatterLabels(chart, chart._chromeCanvas!, layout, 0, 1);
778
958
 
779
959
  if (chart._hoveredIndex >= 0 && chart._xData && chart._yData) {
@@ -811,35 +991,44 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
811
991
  // (one pass per leftmost-column cell). Map mode replaces both
812
992
  // with `renderMapChrome` (attribution + scale bar), painted once
813
993
  // over the whole facet grid.
994
+ const mapAxes = isMap && chart._pluginConfig.numeric_axes;
995
+ const sharedMapTicks =
996
+ mapAxes && grid.cells.length > 0
997
+ ? computeMapDegreeTicks(grid.cells[0].layout)
998
+ : null;
814
999
  if (isMap) {
815
1000
  chart.renderMapChrome(canvas, chart._lastLayout!, theme, dpr);
816
1001
  }
817
1002
 
818
- if (!isMap && sharedX && grid.outerXAxisRect) {
1003
+ if ((!isMap || sharedMapTicks) && sharedX && grid.outerXAxisRect) {
819
1004
  renderOuterXAxis(
820
1005
  canvas,
821
1006
  grid.outerXAxisRect,
822
1007
  xDomain,
823
- sharedXTicks,
1008
+ sharedMapTicks ? sharedMapTicks.xTicks : sharedXTicks,
824
1009
  bottomRowLayouts(grid),
825
1010
  theme,
826
1011
  !!chart._xLabel,
827
1012
  dpr,
828
- chart.getColumnFormatter(chart._xName, "tick"),
1013
+ sharedMapTicks
1014
+ ? sharedMapTicks.formatX
1015
+ : chart.getColumnFormatter(chart._xName, "tick"),
829
1016
  );
830
1017
  }
831
1018
 
832
- if (!isMap && sharedY && grid.outerYAxisRect) {
1019
+ if ((!isMap || sharedMapTicks) && sharedY && grid.outerYAxisRect) {
833
1020
  renderOuterYAxis(
834
1021
  canvas,
835
1022
  grid.outerYAxisRect,
836
1023
  yDomain,
837
- sharedYTicks,
1024
+ sharedMapTicks ? sharedMapTicks.yTicks : sharedYTicks,
838
1025
  leftColumnLayouts(grid),
839
1026
  theme,
840
1027
  !!chart._yLabel,
841
1028
  dpr,
842
- chart.getColumnFormatter(chart._yName, "tick"),
1029
+ sharedMapTicks
1030
+ ? sharedMapTicks.formatY
1031
+ : chart.getColumnFormatter(chart._yName, "tick"),
843
1032
  );
844
1033
  }
845
1034
 
@@ -852,11 +1041,16 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
852
1041
  const d = zc ? zc.getVisibleDomain() : null;
853
1042
  const localX = d ? { ...xDomain, min: d.xMin, max: d.xMax } : xDomain;
854
1043
  const localY = d ? { ...yDomain, min: d.yMin, max: d.yMax } : yDomain;
855
- const ticks = independent
856
- ? computeTicks(localX, localY, cell.layout)
857
- : { xTicks: sharedXTicks, yTicks: sharedYTicks };
1044
+ const cellMapTicks = mapAxes
1045
+ ? computeMapDegreeTicks(cell.layout)
1046
+ : null;
1047
+ const ticks = cellMapTicks
1048
+ ? cellMapTicks
1049
+ : independent
1050
+ ? computeTicks(localX, localY, cell.layout)
1051
+ : { xTicks: sharedXTicks, yTicks: sharedYTicks };
858
1052
 
859
- if (!isMap && !sharedX) {
1053
+ if ((!isMap || cellMapTicks) && !sharedX) {
860
1054
  if (chart._xIsString && chart._xCategoryDomain) {
861
1055
  const cellCtx = getScaledContext(canvas, dpr);
862
1056
  if (cellCtx) {
@@ -876,12 +1070,14 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
876
1070
  theme,
877
1071
  !!chart._xLabel,
878
1072
  dpr,
879
- chart.getColumnFormatter(chart._xName, "tick"),
1073
+ cellMapTicks
1074
+ ? cellMapTicks.formatX
1075
+ : chart.getColumnFormatter(chart._xName, "tick"),
880
1076
  );
881
1077
  }
882
1078
  }
883
1079
 
884
- if (!isMap && !sharedY) {
1080
+ if ((!isMap || cellMapTicks) && !sharedY) {
885
1081
  if (chart._yIsString && chart._yCategoryDomain) {
886
1082
  const cellCtx = getScaledContext(canvas, dpr);
887
1083
  if (cellCtx) {
@@ -901,7 +1097,9 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
901
1097
  theme,
902
1098
  !!chart._yLabel,
903
1099
  dpr,
904
- chart.getColumnFormatter(chart._yName, "tick"),
1100
+ cellMapTicks
1101
+ ? cellMapTicks.formatY
1102
+ : chart.getColumnFormatter(chart._yName, "tick"),
905
1103
  );
906
1104
  }
907
1105
  }
@@ -914,10 +1112,29 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
914
1112
  }
915
1113
 
916
1114
  // Shared legend: categorical (string color) or gradient
917
- // (numeric color). Position derives from `grid.legendRect`
918
- // which `buildFacetGrid` populates when `hasLegend` was set.
919
- if (chart._lastHasColorCol && grid.legendRect) {
1115
+ const legendMode = resolveLegendMode(
1116
+ chart._pluginConfig,
1117
+ legendEntryCount(chart),
1118
+ );
1119
+ const floating = legendMode === "floating";
1120
+ const legendAnchor = floating
1121
+ ? chart._legend.floatingBox(
1122
+ chart._pluginConfig,
1123
+ chart._lastLayout!.cssWidth,
1124
+ chart._lastLayout!.cssHeight,
1125
+ legendFit(chart, theme),
1126
+ )
1127
+ : grid.legendRect;
1128
+ let legendPainted = false;
1129
+ if (chart._lastHasColorCol && legendMode !== "none" && legendAnchor) {
920
1130
  const stops = chart._lastGradientStops ?? theme.gradientStops;
1131
+ const view: LegendPaintView = {
1132
+ mode: floating ? "floating" : "sidebar",
1133
+ legend: chart._legend,
1134
+ title: chart._colorName ?? undefined,
1135
+ sidebarGutter: floating ? undefined : grid.legendRect?.width,
1136
+ opacity: chart._pluginConfig.legend_opacity,
1137
+ };
921
1138
  if (chart._colorIsString && chart._uniqueColorLabels.size > 0) {
922
1139
  const palette = resolvePalette(
923
1140
  theme.seriesPalette,
@@ -926,23 +1143,27 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
926
1143
  );
927
1144
  renderCategoricalLegendAt(
928
1145
  canvas,
929
- grid.legendRect,
1146
+ legendAnchor,
930
1147
  chart._uniqueColorLabels,
931
1148
  palette,
932
1149
  theme,
1150
+ view,
933
1151
  );
1152
+ legendPainted = true;
934
1153
  } else if (chart._colorName) {
935
1154
  // Numeric gradient legend in the shared outer rect. The
936
1155
  // label sits above the bar, so inset the rect's top by
937
1156
  // the usual 20 px that `renderLegend` reserves.
938
1157
  renderLegendAt(
939
1158
  canvas,
940
- {
941
- x: grid.legendRect.x,
942
- y: grid.legendRect.y + 20,
943
- width: grid.legendRect.width,
944
- height: grid.legendRect.height - 20,
945
- },
1159
+ floating
1160
+ ? legendAnchor
1161
+ : {
1162
+ x: legendAnchor.x,
1163
+ y: legendAnchor.y + 20,
1164
+ width: legendAnchor.width,
1165
+ height: legendAnchor.height - 20,
1166
+ },
946
1167
  {
947
1168
  min: chart._colorMin,
948
1169
  max: chart._colorMax,
@@ -951,10 +1172,16 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
951
1172
  stops,
952
1173
  theme,
953
1174
  chart.getColumnFormatter(chart._colorName, "value"),
1175
+ view,
954
1176
  );
1177
+ legendPainted = true;
955
1178
  }
956
1179
  }
957
1180
 
1181
+ if (!legendPainted) {
1182
+ chart._legend.clearPainted();
1183
+ }
1184
+
958
1185
  // Coordinated hover / click indicators across facets. The tooltip
959
1186
  // lines are whatever the last resolved lazy fetch produced (or
960
1187
  // null while a fetch is still in flight); `renderCanvasTooltip`
@@ -970,7 +1197,9 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
970
1197
  const dataY = chart._yData[chart._hoveredIndex] + yOrigin;
971
1198
  const sourceFacet = seriesFromIndex(chart, chart._hoveredIndex);
972
1199
  const opts = chart.glyph.tooltipOptions();
973
- const tooltipLines = chart._lazyTooltip.lines ?? [];
1200
+ const axisInd = axisIndicatorsEnabled(chart, opts);
1201
+ const style = tooltipStyleOf(chart._pluginConfig);
1202
+ const tooltipGrid = chart._lazyTooltip.grid ?? [];
974
1203
 
975
1204
  for (let i = 0; i < grid.cells.length; i++) {
976
1205
  const cell = grid.cells[i];
@@ -989,10 +1218,28 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
989
1218
  continue;
990
1219
  }
991
1220
 
1221
+ if (isSource && axisInd) {
1222
+ renderAxisHoverIndicators(
1223
+ canvas,
1224
+ cell.layout,
1225
+ theme,
1226
+ dpr,
1227
+ cartesianAxisIndicators(
1228
+ chart,
1229
+ cell.layout,
1230
+ pos,
1231
+ dataX,
1232
+ dataY,
1233
+ ),
1234
+ style.opacity,
1235
+ );
1236
+ }
1237
+
992
1238
  const coordinated = chart._facetConfig.coordinated_tooltip;
993
- const lines = isSource || coordinated ? tooltipLines : [];
994
- renderCanvasTooltip(canvas, pos, lines, cell.layout, theme, dpr, {
995
- crosshair: opts.crosshair,
1239
+ const content = isSource || coordinated ? tooltipGrid : [];
1240
+ renderCanvasTooltip(canvas, pos, content, cell.layout, theme, dpr, {
1241
+ ...style,
1242
+ crosshair: opts.crosshair && (!isSource || !axisInd),
996
1243
  highlightRadius: isSource ? opts.highlightRadius : 0,
997
1244
  });
998
1245
  }
@@ -1117,24 +1364,76 @@ function renderTooltip(
1117
1364
 
1118
1365
  const xOrigin = isNaN(chart._xOrigin) ? 0 : chart._xOrigin;
1119
1366
  const yOrigin = isNaN(chart._yOrigin) ? 0 : chart._yOrigin;
1120
- const pos = layout.dataToPixel(
1121
- chart._xData[idx] + xOrigin,
1122
- chart._yData[idx] + yOrigin,
1123
- );
1367
+ const dataX = chart._xData[idx] + xOrigin;
1368
+ const dataY = chart._yData[idx] + yOrigin;
1369
+ const pos = layout.dataToPixel(dataX, dataY);
1124
1370
 
1125
1371
  // Lines come from the async lazy tooltip fetch kicked off in
1126
1372
  // `handleCartesianHover`. While a fetch is in flight this is
1127
1373
  // `null`; the canvas tooltip helper still paints the crosshair /
1128
1374
  // highlight ring but skips the text box.
1129
- const lines = chart._lazyTooltip.lines ?? [];
1375
+ const grid = chart._lazyTooltip.grid ?? [];
1130
1376
  const theme = chart._resolveTheme();
1131
- renderCanvasTooltip(
1132
- canvas,
1133
- pos,
1134
- lines,
1135
- layout,
1136
- theme,
1137
- chart._glManager?.dpr ?? 1,
1138
- chart.glyph.tooltipOptions(),
1377
+ const dpr = chart._glManager?.dpr ?? 1;
1378
+ const opts = chart.glyph.tooltipOptions();
1379
+ const axisInd = axisIndicatorsEnabled(chart, opts);
1380
+ if (axisInd) {
1381
+ renderAxisHoverIndicators(
1382
+ canvas,
1383
+ layout,
1384
+ theme,
1385
+ dpr,
1386
+ cartesianAxisIndicators(chart, layout, pos, dataX, dataY),
1387
+ chart._pluginConfig.tooltip_opacity,
1388
+ );
1389
+ }
1390
+
1391
+ renderCanvasTooltip(canvas, pos, grid, layout, theme, dpr, {
1392
+ ...tooltipStyleOf(chart._pluginConfig),
1393
+ crosshair: opts.crosshair && !axisInd,
1394
+ highlightRadius: opts.highlightRadius,
1395
+ });
1396
+ }
1397
+
1398
+ function axisIndicatorsEnabled(
1399
+ chart: CartesianChart,
1400
+ opts: { axisIndicators: boolean },
1401
+ ): boolean {
1402
+ return (
1403
+ opts.axisIndicators &&
1404
+ (chart._renderMode !== "map" || !!chart._pluginConfig.numeric_axes)
1139
1405
  );
1140
1406
  }
1407
+
1408
+ function cartesianAxisIndicators(
1409
+ chart: CartesianChart,
1410
+ layout: PlotLayout,
1411
+ pos: { px: number; py: number },
1412
+ dataX: number,
1413
+ dataY: number,
1414
+ ): AxisIndicator[] {
1415
+ let xText: string;
1416
+ let yText: string;
1417
+ const xOverride = chart.getColumnFormatter(chart._xName, "tick");
1418
+ const yOverride = chart.getColumnFormatter(chart._yName, "tick");
1419
+ if (chart._renderMode === "map") {
1420
+ const mt = computeMapDegreeTicks(layout);
1421
+ const [lon, lat] = mercatorToLonLat(dataX, dataY);
1422
+ xText = xOverride ? xOverride(lon) : mt.formatX(dataX);
1423
+ yText = yOverride ? yOverride(lat) : mt.formatY(dataY);
1424
+ } else {
1425
+ const xFmt =
1426
+ xOverride ??
1427
+ stepTickFormatter(chart._lastXDomain?.isDate, chart._lastXTicks);
1428
+ const yFmt =
1429
+ yOverride ??
1430
+ stepTickFormatter(chart._lastYDomain?.isDate, chart._lastYTicks);
1431
+ xText = xFmt(dataX);
1432
+ yText = yFmt(dataY);
1433
+ }
1434
+
1435
+ return [
1436
+ { side: "bottom", px: pos.px, py: pos.py, text: xText },
1437
+ { side: "left", px: pos.px, py: pos.py, text: yText },
1438
+ ];
1439
+ }
@@ -61,6 +61,19 @@ export class CartesianChart extends AbstractChart {
61
61
  this.glyph = glyph;
62
62
  }
63
63
 
64
+ protected override colorScaleColumn(): string | null {
65
+ return this._colorName || null;
66
+ }
67
+
68
+ /**
69
+ * Chrome-only repaint for legend scroll / floating-legend drag —
70
+ * the same lightweight path hover updates use. No GL pass; the
71
+ * composite re-presents over the retained plot bitmap.
72
+ */
73
+ repaintChrome(): void {
74
+ renderCartesianChromeOverlay(this);
75
+ }
76
+
64
77
  /**
65
78
  * Rendering pipeline selector. `"cartesian"` is the default —
66
79
  * draws axes, gridlines, and ticks via the chrome canvas.