@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
@@ -47,7 +47,17 @@ const HEATMAP_Y_AXIS_OPTS: CategoricalYAxisOptions = {
47
47
  skipLeafLevel: true,
48
48
  };
49
49
 
50
- import { renderLegend, renderLegendAt } from "../../axis/legend";
50
+ import {
51
+ gradientLegendAutoFit,
52
+ renderLegend,
53
+ renderLegendAt,
54
+ type LegendPaintView,
55
+ } from "../../axis/legend";
56
+ import {
57
+ legendRightGutter,
58
+ resolveLegendMode,
59
+ legendSidebarWidth,
60
+ } from "../../interaction/legend-controller";
51
61
  import heatmapVert from "../../shaders/heatmap.vert.glsl";
52
62
  import heatmapFrag from "../../shaders/heatmap.frag.glsl";
53
63
  import { colorValueToT } from "../../theme/gradient";
@@ -110,6 +120,7 @@ export function renderHeatmapFrame(
110
120
  // Measure both hierarchical axes *before* building the layout so the
111
121
  // plot rect accounts for their footprints. Numeric axes get fixed
112
122
  // gutters matching bar's branch (24px bottom, 55px left).
123
+ const rightExtra = legendRightGutter(chart._pluginConfig, true, 80, 0);
113
124
  const estLeft = yNumeric
114
125
  ? 55
115
126
  : measureCategoricalAxisWidth(yDomain, HEATMAP_Y_AXIS_OPTS);
@@ -117,7 +128,7 @@ export function renderHeatmapFrame(
117
128
  ? 24
118
129
  : measureCategoricalAxisHeight(
119
130
  xDomain,
120
- Math.max(1, cssWidth - estLeft - 110),
131
+ Math.max(1, cssWidth - estLeft - 30 - rightExtra),
121
132
  );
122
133
 
123
134
  const layout = new PlotLayout(cssWidth, cssHeight, {
@@ -126,6 +137,7 @@ export function renderHeatmapFrame(
126
137
  hasLegend: true,
127
138
  bottomExtra,
128
139
  leftExtra: estLeft,
140
+ rightExtra,
129
141
  });
130
142
  chart._lastLayout = layout;
131
143
  if (chart._zoomController) {
@@ -392,6 +404,11 @@ function drawCellsInstanced(
392
404
  * Chrome overlay: X axis + Y axis + color legend + (optional) tooltip.
393
405
  */
394
406
  export function renderHeatmapChromeOverlay(chart: HeatmapChart): void {
407
+ paintHeatmapChromeOverlay(chart);
408
+ chart.presentOverlay();
409
+ }
410
+
411
+ function paintHeatmapChromeOverlay(chart: HeatmapChart): void {
395
412
  if (!chart._chromeCanvas) {
396
413
  return;
397
414
  }
@@ -477,20 +494,58 @@ export function renderHeatmapChromeOverlay(chart: HeatmapChart): void {
477
494
  );
478
495
  }
479
496
 
480
- // Color legend on the right. The aggregate column name is in
481
- // `_columnSlots[0]` (heatmap's only data column slot is "Color").
482
- renderLegend(
483
- chart._chromeCanvas,
484
- layout,
485
- {
497
+ const legendMode = resolveLegendMode(chart._pluginConfig, 0);
498
+ if (legendMode === "none") {
499
+ chart._legend.clearPainted();
500
+ } else {
501
+ const colorDomain = {
486
502
  min: chart._colorMin,
487
503
  max: chart._colorMax,
488
504
  label: chart._aggName,
489
- },
490
- theme.gradientStops,
491
- theme,
492
- chart.getColumnFormatter(chart._columnSlots[0], "value"),
493
- );
505
+ };
506
+ const formatter = chart.getColumnFormatter(
507
+ chart._columnSlots[0],
508
+ "value",
509
+ );
510
+ const view: LegendPaintView = {
511
+ mode: legendMode === "floating" ? "floating" : "sidebar",
512
+ legend: chart._legend,
513
+ title: chart._aggName,
514
+ opacity: chart._pluginConfig.legend_opacity,
515
+ };
516
+ if (legendMode === "floating") {
517
+ renderLegendAt(
518
+ chart._chromeCanvas,
519
+ chart._legend.floatingBox(
520
+ chart._pluginConfig,
521
+ layout.cssWidth,
522
+ layout.cssHeight,
523
+ gradientLegendAutoFit(
524
+ chart._chromeCanvas,
525
+ theme,
526
+ colorDomain,
527
+ formatter,
528
+ chart._aggName,
529
+ ),
530
+ ),
531
+ colorDomain,
532
+ theme.gradientStops,
533
+ theme,
534
+ formatter,
535
+ view,
536
+ );
537
+ } else {
538
+ renderLegend(
539
+ chart._chromeCanvas,
540
+ layout,
541
+ colorDomain,
542
+ theme.gradientStops,
543
+ theme,
544
+ formatter,
545
+ view,
546
+ );
547
+ }
548
+ }
494
549
 
495
550
  if (chart._hoveredCell) {
496
551
  renderHeatmapTooltip(chart);
@@ -523,7 +578,8 @@ function renderFacetedHeatmap(
523
578
  cssHeight,
524
579
  xAxis: effectiveSharedX ? "outer" : "cell",
525
580
  yAxis: effectiveSharedY ? "outer" : "cell",
526
- hasLegend: true,
581
+ hasLegend: resolveLegendMode(chart._pluginConfig, 0) === "sidebar",
582
+ legendWidth: legendSidebarWidth(chart._pluginConfig, 96),
527
583
  hasXLabel: chart._groupBy.length > 0,
528
584
  hasYLabel: false,
529
585
  gap: 8,
@@ -788,18 +844,34 @@ function renderFacetedHeatmapChromeOverlay(chart: HeatmapChart): void {
788
844
  );
789
845
  }
790
846
 
791
- // Shared colorbar at `grid.legendRect`. No meaningful single label —
792
- // the facet titles already name each column, and a combined label
793
- // would be ambiguous when columns differ.
794
- if (grid.legendRect) {
847
+ const legendMode = resolveLegendMode(chart._pluginConfig, 0);
848
+ const floating = legendMode === "floating";
849
+ const facetLayout = chart._facets[0].layout;
850
+ const legendAnchor = floating
851
+ ? chart._legend.floatingBox(
852
+ chart._pluginConfig,
853
+ facetLayout.cssWidth,
854
+ facetLayout.cssHeight,
855
+ gradientLegendAutoFit(
856
+ chart._chromeCanvas,
857
+ theme,
858
+ { min: chart._colorMin, max: chart._colorMax },
859
+ chart.getColumnFormatter(chart._columnSlots[0], "value"),
860
+ chart._aggName,
861
+ ),
862
+ )
863
+ : grid.legendRect;
864
+ if (legendMode !== "none" && legendAnchor) {
795
865
  renderLegendAt(
796
866
  chart._chromeCanvas,
797
- {
798
- x: grid.legendRect.x,
799
- y: grid.legendRect.y + 20,
800
- width: grid.legendRect.width,
801
- height: Math.max(1, grid.legendRect.height - 20),
802
- },
867
+ floating
868
+ ? legendAnchor
869
+ : {
870
+ x: legendAnchor.x,
871
+ y: legendAnchor.y + 20,
872
+ width: legendAnchor.width,
873
+ height: Math.max(1, legendAnchor.height - 20),
874
+ },
803
875
  {
804
876
  min: chart._colorMin,
805
877
  max: chart._colorMax,
@@ -808,7 +880,16 @@ function renderFacetedHeatmapChromeOverlay(chart: HeatmapChart): void {
808
880
  theme.gradientStops,
809
881
  theme,
810
882
  chart.getColumnFormatter(chart._columnSlots[0], "value"),
883
+ {
884
+ mode: floating ? "floating" : "sidebar",
885
+ legend: chart._legend,
886
+ title: chart._aggName,
887
+ sidebarGutter: floating ? undefined : grid.legendRect?.width,
888
+ opacity: chart._pluginConfig.legend_opacity,
889
+ },
811
890
  );
891
+ } else {
892
+ chart._legend.clearPainted();
812
893
  }
813
894
 
814
895
  if (chart._hoveredCell) {
@@ -69,6 +69,10 @@ export interface HeatmapFacet {
69
69
  * scale and a single legend.
70
70
  */
71
71
  export class HeatmapChart extends AbstractChart {
72
+ protected override colorScaleColumn(): string | null {
73
+ return this._columnSlots[0] || null;
74
+ }
75
+
72
76
  _program: WebGLProgram | null = null;
73
77
  _locations: HeatmapLocations | null = null;
74
78
  _cornerBuffer: WebGLBuffer | null = null;
@@ -176,9 +180,10 @@ export class HeatmapChart extends AbstractChart {
176
180
  xIdx: number,
177
181
  yIdx: number,
178
182
  ): Promise<void> {
179
- const groupByValues: (string | null)[] = this._xLevels.map(
180
- (level) => level.labels[xIdx] ?? null,
181
- );
183
+ const groupByValues: (string | number | null)[] =
184
+ this._xAxisMode.mode === "numeric" && this._xPositions
185
+ ? [this._xPositions[xIdx] ?? null]
186
+ : this._xLevels.map((level) => level.labels[xIdx] ?? null);
182
187
  const splitByValues: (string | null)[] = this._yLevels
183
188
  .slice(0, this._splitBy.length)
184
189
  .map((level) => level.labels[yIdx] ?? null);
@@ -22,7 +22,7 @@ import type { Canvas2D, Context2D } from "../canvas-types";
22
22
  import type { ZoomConfig } from "../../interaction/zoom-controller";
23
23
  import type { PluginConfig } from "../chart";
24
24
  import { TileLayer } from "../../map/tile-layer";
25
- import { tileSourceFor, type TileProviderId } from "../../map/tile-source";
25
+ import { TILE_SOURCES } from "../../map/tile-source";
26
26
  import { lonLatToMercator } from "../../map/mercator";
27
27
  import { getScaledContext } from "../../axis/canvas";
28
28
 
@@ -76,7 +76,7 @@ export class MapChart extends CartesianChart {
76
76
  if (this._glManager) {
77
77
  this._tileLayer.setSource(
78
78
  this._glManager.gl,
79
- tileSourceFor(cfg.map_tile_provider as TileProviderId),
79
+ TILE_SOURCES.sourceFor(cfg.map_tile_provider),
80
80
  );
81
81
  }
82
82
 
@@ -97,9 +97,7 @@ export class MapChart extends CartesianChart {
97
97
  if (!this._tileLayer.source) {
98
98
  this._tileLayer.setSource(
99
99
  glManager.gl,
100
- tileSourceFor(
101
- this._pluginConfig.map_tile_provider as TileProviderId,
102
- ),
100
+ TILE_SOURCES.sourceFor(this._pluginConfig.map_tile_provider),
103
101
  );
104
102
  this._tileLayer.setAlpha(this._pluginConfig.map_tile_alpha);
105
103
  }
@@ -336,6 +336,14 @@ export interface SeriesPipelineResult {
336
336
  * is the position. Indexed by `catIdx` (0..numCategories-1).
337
337
  */
338
338
  categoryPositions: Float64Array | null;
339
+
340
+ /**
341
+ * One entry per (aggregate × split), in `k * P + p` order. INVARIANT:
342
+ * `series.length === aggregates.length * splitPrefixes.length` —
343
+ * consumers index `series[k * P]` directly (glyph runs, the
344
+ * `domain_mode: "expand"` axis signature), so an empty `series` MUST
345
+ * be paired with empty `aggregates` / `splitPrefixes`.
346
+ */
339
347
  series: SeriesInfo[];
340
348
 
341
349
  /**
@@ -473,13 +481,18 @@ export function buildSeriesPipeline(
473
481
  numRows,
474
482
  groupBy.length,
475
483
  levelTypes,
484
+ axisMode,
476
485
  );
477
486
 
478
487
  if (numCategories === 0) {
488
+ // NOT `aggregates` / `splitPrefixes`: `series` is empty here, and
489
+ // carrying the non-empty lists would break the `series.length ===
490
+ // M * P` invariant — `series[k * P]` consumers then read
491
+ // `undefined` (the `loadAndRender failed … reading 'axis'` crash).
492
+ // Same shape as the `aggregates.length === 0` return above, which
493
+ // every downstream path already renders as an empty chart.
479
494
  return {
480
495
  ...empty,
481
- aggregates,
482
- splitPrefixes,
483
496
  rowPaths,
484
497
  rowOffset,
485
498
  };
@@ -23,6 +23,7 @@ import {
23
23
  rightAxisDataToPixel,
24
24
  layoutForRecord,
25
25
  } from "./series-render";
26
+ import { tooltipStyleOf } from "../../interaction/tooltip-grid";
26
27
 
27
28
  const POINT_HIT_RADIUS_PX = 10;
28
29
 
@@ -598,26 +599,26 @@ export function handleBarLegendClick(
598
599
  export function buildBarTooltipLines(
599
600
  chart: SeriesChart,
600
601
  b: SeriesChartRecord,
601
- ): string[] {
602
- const lines: string[] = [];
602
+ ): string[][] {
603
+ const grid: string[][] = [];
603
604
  const s = chart._series[b.seriesId];
604
605
  const categoryPath = formatBarCategoryPath(chart, b.catIdx);
605
606
  if (categoryPath) {
606
- lines.push(categoryPath);
607
+ grid.push([categoryPath]);
607
608
  }
608
609
 
609
610
  const yFmt = chart.getColumnFormatter(s.aggName, "value");
610
- lines.push(`${s.aggName}: ${yFmt(b.value)}`);
611
+ grid.push([s.aggName, yFmt(b.value)]);
611
612
  if (s.splitKey) {
612
- lines.push(`Split: ${s.splitKey}`);
613
+ grid.push(["Split", s.splitKey]);
613
614
  }
614
615
 
615
616
  if (b.y0 !== 0) {
616
- lines.push(`Base: ${yFmt(b.y0)}`);
617
- lines.push(`Top: ${yFmt(b.y1)}`);
617
+ grid.push(["Base", yFmt(b.y0)]);
618
+ grid.push(["Top", yFmt(b.y1)]);
618
619
  }
619
620
 
620
- return lines;
621
+ return grid;
621
622
  }
622
623
 
623
624
  /**
@@ -628,19 +629,8 @@ export function formatBarCategoryPath(
628
629
  chart: SeriesChart,
629
630
  catIdx: number,
630
631
  ): string {
631
- // Numeric category mode: resolve from the bar's xCenter (real data
632
- // value) rather than the row-path label array, which is empty when
633
- // the single group_by level is non-string.
634
- if (chart._categoryAxisMode === "numeric" && chart._numericCategoryDomain) {
635
- const bars = chart._bars;
636
- let v: number | null = null;
637
- for (let i = 0; i < bars.count; i++) {
638
- if (bars.catIdx[i] === catIdx) {
639
- v = bars.xCenter[i];
640
- break;
641
- }
642
- }
643
-
632
+ if (chart._categoryAxisMode === "numeric" && chart._categoryPositions) {
633
+ const v = chart._categoryPositions[catIdx];
644
634
  if (v == null) {
645
635
  return "";
646
636
  }
@@ -716,12 +706,12 @@ function pinTooltip(chart: SeriesChart, b: SeriesChartRecord): void {
716
706
  : layout.dataToPixel(b.xCenter, anchorV)
717
707
  : rightAxisDataToPixel(chart, b.xCenter, anchorV, layout);
718
708
 
719
- const lines = buildBarTooltipLines(chart, b);
720
- if (lines.length === 0) {
709
+ const grid = buildBarTooltipLines(chart, b);
710
+ if (grid.length === 0) {
721
711
  return;
722
712
  }
723
713
 
724
- chart._tooltip.pin(lines, pos, layout);
714
+ chart._tooltip.pin(grid, pos, layout, tooltipStyleOf(chart._pluginConfig));
725
715
 
726
716
  chart._hoveredBarIdx = -1;
727
717
  chart._hoveredSample = null;