@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
@@ -30,6 +30,7 @@ import {
30
30
  } from "./series-build";
31
31
  import {
32
32
  renderBarFrame,
33
+ renderBarChromeOverlay,
33
34
  uploadBarInstances,
34
35
  invalidateGlyphBuffers,
35
36
  rebuildGlyphBuffers,
@@ -41,6 +42,7 @@ import {
41
42
  showBarPinnedTooltipForSample,
42
43
  } from "./series-interact";
43
44
  import { resolvePalette } from "../../theme/palette";
45
+ import { resolveSeriesColorOverride } from "./series-type";
44
46
  import { LineGlyph } from "./glyphs/draw-lines";
45
47
  import { ScatterGlyph } from "./glyphs/draw-scatter";
46
48
  import { AreaGlyph } from "./glyphs/draw-areas";
@@ -125,6 +127,15 @@ export class SeriesChart extends CategoricalYChart {
125
127
  return { lockAxis: this._isHorizontal ? "x" : "y" };
126
128
  }
127
129
 
130
+ /**
131
+ * Chrome-only repaint for legend scroll / floating-legend drag —
132
+ * the same lightweight path hover updates use. No GL pass; the
133
+ * composite re-presents over the retained plot bitmap.
134
+ */
135
+ repaintChrome(): void {
136
+ renderBarChromeOverlay(this);
137
+ }
138
+
128
139
  _locations: CachedLocations | null = null;
129
140
 
130
141
  // Series-specific categorical-axis bookkeeping. `_rowPaths`,
@@ -250,13 +261,16 @@ export class SeriesChart extends CategoricalYChart {
250
261
  /**
251
262
  * Cached palette + identity-keys for short-circuiting per-frame
252
263
  * resolution. Inputs (`seriesPalette` ref, `gradientStops` ref,
253
- * `series.length`) only change on data load or `restyle()`.
264
+ * `series.length`, `_columnsConfig` ref — per-series color
265
+ * overrides) only change on data load, `restyle()`, or
266
+ * `setColumnsConfig`.
254
267
  */
255
268
  _paletteCache: [number, number, number][] | null = null;
256
269
  _paletteCacheKey: {
257
270
  seriesPalette: [number, number, number][] | null;
258
271
  gradientStops: unknown;
259
272
  seriesLength: number;
273
+ columnsConfig: unknown;
260
274
  } | null = null;
261
275
 
262
276
  /**
@@ -514,9 +528,10 @@ export class SeriesChart extends CategoricalYChart {
514
528
  return;
515
529
  }
516
530
 
517
- const groupByValues: (string | null)[] = this._rowPaths.map(
518
- (level) => level.labels[b.catIdx] ?? null,
519
- );
531
+ const groupByValues: (string | number | null)[] =
532
+ this._categoryAxisMode === "numeric" && this._categoryPositions
533
+ ? [this._categoryPositions[b.catIdx] ?? null]
534
+ : this._rowPaths.map((level) => level.labels[b.catIdx] ?? null);
520
535
  const splitKey = this._splitPrefixes[b.splitIdx] ?? "";
521
536
  const splitByValues =
522
537
  this._splitBy.length > 0 && splitKey !== ""
@@ -903,6 +918,7 @@ export function ensurePalette(chart: SeriesChart): boolean {
903
918
  const theme = chart._resolveTheme();
904
919
  const seriesPalette = theme.seriesPalette;
905
920
  const gradientStops = theme.gradientStops;
921
+ const columnsConfig = chart._columnsConfig;
906
922
  const paletteCount = chart._facetActive
907
923
  ? Math.max(1, chart._aggregates.length)
908
924
  : chart._series.length;
@@ -913,7 +929,8 @@ export function ensurePalette(chart: SeriesChart): boolean {
913
929
  key &&
914
930
  key.seriesPalette === seriesPalette &&
915
931
  key.gradientStops === gradientStops &&
916
- key.seriesLength === paletteCount
932
+ key.seriesLength === paletteCount &&
933
+ key.columnsConfig === columnsConfig
917
934
  ) {
918
935
  return false;
919
936
  }
@@ -928,12 +945,18 @@ export function ensurePalette(chart: SeriesChart): boolean {
928
945
  seriesPalette,
929
946
  gradientStops,
930
947
  seriesLength: paletteCount,
948
+ columnsConfig,
931
949
  };
932
950
 
933
951
  for (let i = 0; i < chart._series.length; i++) {
934
- chart._series[i].color = chart._facetActive
935
- ? palette[chart._series[i].aggIdx]
936
- : palette[i];
952
+ const series = chart._series[i];
953
+ const themed = chart._facetActive ? palette[series.aggIdx] : palette[i];
954
+ series.color =
955
+ resolveSeriesColorOverride(
956
+ series,
957
+ chart._facetActive,
958
+ columnsConfig,
959
+ ) ?? themed;
937
960
  }
938
961
 
939
962
  return true;
@@ -28,6 +28,7 @@ import {
28
28
  INNER_RING_PX,
29
29
  } from "./sunburst-layout";
30
30
  import { buildFacetGrid } from "../../layout/facet-grid";
31
+ import { legendTreeGutter } from "../../interaction/legend-controller";
31
32
  import { withChromeCache } from "../common/chrome-cache";
32
33
  import {
33
34
  renderBreadcrumbs as renderTreeBreadcrumbs,
@@ -106,7 +107,12 @@ export function renderSunburstFrame(
106
107
  chart._colorMin < chart._colorMax;
107
108
  const breadcrumbH =
108
109
  !hasSplits && chart._breadcrumbIds.length > 1 ? BREADCRUMB_H : 0;
109
- const legendW = hasLegend ? LEGEND_W : 0;
110
+ const legendW = legendTreeGutter(
111
+ chart._pluginConfig,
112
+ hasLegend,
113
+ LEGEND_W,
114
+ chart._colorMode === "series" ? chart._uniqueColorLabels.size : 0,
115
+ );
110
116
 
111
117
  if (hasSplits) {
112
118
  layoutFacetedSunburst(chart, cssWidth, cssHeight, legendW);
@@ -501,7 +507,16 @@ function drawArcs(
501
507
 
502
508
  // Chrome overlay (Canvas2D)
503
509
 
510
+ /**
511
+ * Render the chrome overlay (labels, center text, hover highlight +
512
+ * tooltip).
513
+ */
504
514
  export function renderSunburstChromeOverlay(chart: SunburstChart): void {
515
+ paintSunburstChromeOverlay(chart);
516
+ chart.presentOverlay();
517
+ }
518
+
519
+ function paintSunburstChromeOverlay(chart: SunburstChart): void {
505
520
  if (!chart._chromeCanvas || chart._currentRootId === NULL_NODE) {
506
521
  return;
507
522
  }
@@ -23,6 +23,7 @@ import { Theme } from "../../theme/theme";
23
23
  import { resolvePalette, type Vec3 } from "../../theme/palette";
24
24
  import { type GradientStop } from "../../theme/gradient";
25
25
  import { buildFacetGrid } from "../../layout/facet-grid";
26
+ import { legendTreeGutter } from "../../interaction/legend-controller";
26
27
  import { leafColor, leafRGBA, luminance } from "../common/leaf-color";
27
28
  import treemapVert from "../../shaders/treemap.vert.glsl";
28
29
  import treemapFrag from "../../shaders/treemap.frag.glsl";
@@ -70,7 +71,12 @@ export function renderTreemapFrame(
70
71
  ? chart._uniqueColorLabels.size > 1
71
72
  : chart._colorMode === "numeric" &&
72
73
  chart._colorMin < chart._colorMax;
73
- const legendW = hasLegend ? 90 : 0;
74
+ const legendW = legendTreeGutter(
75
+ chart._pluginConfig,
76
+ hasLegend,
77
+ 90,
78
+ chart._colorMode === "series" ? chart._uniqueColorLabels.size : 0,
79
+ );
74
80
 
75
81
  // Scratch buffer for the ordered-layout child ids. Worst case:
76
82
  // active children at every level = store.count. Reuse the chart's
@@ -467,6 +473,11 @@ function emitRect(
467
473
  * tooltip + highlight on top.
468
474
  */
469
475
  export function renderTreemapChromeOverlay(chart: TreemapChart): void {
476
+ paintTreemapChromeOverlay(chart);
477
+ chart.presentOverlay();
478
+ }
479
+
480
+ function paintTreemapChromeOverlay(chart: TreemapChart): void {
470
481
  if (!chart._chromeCanvas || chart._currentRootId === NULL_NODE) {
471
482
  return;
472
483
  }
@@ -95,12 +95,9 @@ export async function viewToColumnDataMap(
95
95
  } else if (vals instanceof Float64Array) {
96
96
  // Datetime/Date columns are emitted as Float64 to keep
97
97
  // millisecond precision; numeric Float64 also lands here
98
- // when `float32` mode is off. Keep them as f64 — the
99
- // chart's CPU mirrors and extents will rebase to f32 at
100
- // upload time.
98
+ // when `float32` mode is off.
101
99
  result.set(name, { type: "float64", values: vals, valid });
102
100
  } else {
103
- // Fallback: treat as float32
104
101
  // TODO: Instance check if this needs a copy?
105
102
  result.set(name, {
106
103
  type: "float32",
package/src/ts/index.ts CHANGED
@@ -15,6 +15,8 @@ import { HTMLPerspectiveViewerWebGLPluginElement } from "./plugin/plugin";
15
15
 
16
16
  export type { PerspectiveClickDetail } from "./event-detail";
17
17
  export { PerspectiveSelectDetail } from "./event-detail";
18
+ export { registerTileSource } from "./plugin/plugin";
19
+ export { tileSources, type TileSourceSpec } from "./map/tile-source";
18
20
 
19
21
  export function register(...plugin_names: string[]) {
20
22
  const plugins = new Set(
@@ -0,0 +1,137 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import type { Canvas2D, Context2D } from "../charts/canvas-types";
14
+ import type { PlotLayout } from "../layout/plot-layout";
15
+ import type { Theme } from "../theme/theme";
16
+
17
+ export type AxisSide = "bottom" | "left" | "right";
18
+
19
+ export interface AxisIndicator {
20
+ side: AxisSide;
21
+
22
+ px: number;
23
+ py: number;
24
+
25
+ text: string;
26
+ }
27
+
28
+ const BADGE_FONT_PX = 11;
29
+ const BADGE_PAD_H = 4;
30
+ const BADGE_PAD_V = 2;
31
+ const BADGE_GAP = 4;
32
+
33
+ export function renderAxisHoverIndicators(
34
+ canvas: Canvas2D | null,
35
+ layout: PlotLayout,
36
+ theme: Theme,
37
+ dpr: number,
38
+ indicators: AxisIndicator[],
39
+ opacity: number = 1.0,
40
+ ): void {
41
+ if (!canvas || indicators.length === 0) {
42
+ return;
43
+ }
44
+
45
+ const ctx = canvas.getContext("2d") as Context2D | null;
46
+ if (!ctx) {
47
+ return;
48
+ }
49
+
50
+ const plot = layout.plotRect;
51
+ ctx.save();
52
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
53
+ ctx.scale(dpr, dpr);
54
+ ctx.font = `${BADGE_FONT_PX}px ${theme.fontFamily}`;
55
+
56
+ for (const ind of indicators) {
57
+ if (
58
+ ind.px < plot.x ||
59
+ ind.px > plot.x + plot.width ||
60
+ ind.py < plot.y ||
61
+ ind.py > plot.y + plot.height
62
+ ) {
63
+ continue;
64
+ }
65
+
66
+ drawTraceLine(ctx, ind, plot, theme);
67
+ drawBadge(ctx, ind, layout, theme, opacity);
68
+ }
69
+
70
+ ctx.restore();
71
+ }
72
+
73
+ function drawTraceLine(
74
+ ctx: Context2D,
75
+ ind: AxisIndicator,
76
+ plot: { x: number; y: number; width: number; height: number },
77
+ theme: Theme,
78
+ ): void {
79
+ ctx.strokeStyle = theme.tickColor;
80
+ ctx.lineWidth = 1;
81
+ ctx.setLineDash([4, 4]);
82
+ ctx.beginPath();
83
+ ctx.moveTo(ind.px, ind.py);
84
+ if (ind.side === "bottom") {
85
+ ctx.lineTo(ind.px, plot.y + plot.height);
86
+ } else if (ind.side === "left") {
87
+ ctx.lineTo(plot.x, ind.py);
88
+ } else {
89
+ ctx.lineTo(plot.x + plot.width, ind.py);
90
+ }
91
+
92
+ ctx.stroke();
93
+ ctx.setLineDash([]);
94
+ }
95
+
96
+ function drawBadge(
97
+ ctx: Context2D,
98
+ ind: AxisIndicator,
99
+ layout: PlotLayout,
100
+ theme: Theme,
101
+ opacity: number,
102
+ ): void {
103
+ const plot = layout.plotRect;
104
+ const boxW = ctx.measureText(ind.text).width + BADGE_PAD_H * 2;
105
+ const boxH = BADGE_FONT_PX + BADGE_PAD_V * 2 + 2;
106
+
107
+ let bx: number;
108
+ let by: number;
109
+ if (ind.side === "bottom") {
110
+ bx = ind.px - boxW / 2;
111
+ by = plot.y + plot.height + BADGE_GAP;
112
+ } else if (ind.side === "left") {
113
+ bx = plot.x - BADGE_GAP - boxW;
114
+ by = ind.py - boxH / 2;
115
+ } else {
116
+ bx = plot.x + plot.width + BADGE_GAP;
117
+ by = ind.py - boxH / 2;
118
+ }
119
+
120
+ bx = Math.min(Math.max(bx, 0), Math.max(0, layout.cssWidth - boxW));
121
+ by = Math.min(Math.max(by, 0), Math.max(0, layout.cssHeight - boxH));
122
+
123
+ ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
124
+ ctx.fillStyle = theme.tooltipBg;
125
+ ctx.strokeStyle = theme.tooltipBorder;
126
+ ctx.lineWidth = 1;
127
+ ctx.beginPath();
128
+ ctx.rect(bx, by, boxW, boxH);
129
+ ctx.fill();
130
+ ctx.stroke();
131
+ ctx.globalAlpha = 1.0;
132
+
133
+ ctx.fillStyle = theme.tooltipText;
134
+ ctx.textAlign = "left";
135
+ ctx.textBaseline = "middle";
136
+ ctx.fillText(ind.text, bx + BADGE_PAD_H, by + boxH / 2 + 1);
137
+ }
@@ -11,6 +11,13 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  import type { CssBounds, HostSink } from "./tooltip-controller";
14
+ import {
15
+ DEFAULT_TOOLTIP_STYLE,
16
+ NUMERIC_RE,
17
+ TOOLTIP_COL_GAP,
18
+ type TooltipContent,
19
+ type TooltipStyle,
20
+ } from "./tooltip-grid";
14
21
 
15
22
  /**
16
23
  * Host-side `HostSink` that materializes pinned tooltips as a `<div>`
@@ -29,16 +36,17 @@ export class DomHostSink implements HostSink {
29
36
  }
30
37
 
31
38
  pin(
32
- lines: string[],
39
+ grid: TooltipContent,
33
40
  pos: { px: number; py: number },
34
41
  bounds: CssBounds,
42
+ style: TooltipStyle = DEFAULT_TOOLTIP_STYLE,
35
43
  ): void {
36
44
  this.dismiss();
37
45
  const div = document.createElement("div");
38
46
 
39
47
  div.className = "webgl-tooltip";
40
48
  div.style.maxHeight = `${Math.round(bounds.cssHeight * 0.6)}px`;
41
- div.textContent = lines.join("\n");
49
+ renderTooltipGridDom(div, grid, style);
42
50
 
43
51
  if (getComputedStyle(this._parent).position === "static") {
44
52
  this._parent.style.position = "relative";
@@ -83,3 +91,59 @@ export class DomHostSink implements HostSink {
83
91
  this._glCanvas.style.cursor = cursor;
84
92
  }
85
93
  }
94
+
95
+ function renderTooltipGridDom(
96
+ root: HTMLDivElement,
97
+ grid: TooltipContent,
98
+ style: TooltipStyle,
99
+ ): void {
100
+ const ncols = grid.reduce((n, row) => Math.max(n, row.length), 0);
101
+ root.style.display = "grid";
102
+ root.style.gridTemplateColumns = `repeat(${Math.max(1, ncols)}, minmax(0, max-content))`;
103
+ root.style.columnGap = `${TOOLTIP_COL_GAP}px`;
104
+ if (style.opacity < 1) {
105
+ const pct = Math.max(0, Math.round(style.opacity * 100));
106
+ root.style.background = `color-mix(in srgb, var(--psp-charts--tooltip--background) ${pct}%, transparent)`;
107
+ }
108
+
109
+ const rightAlign: boolean[] = [];
110
+ for (const row of grid) {
111
+ if (row.length <= 1) {
112
+ continue;
113
+ }
114
+
115
+ for (let j = 1; j < row.length; j++) {
116
+ if (row[j] === "") {
117
+ continue;
118
+ }
119
+
120
+ const numeric = NUMERIC_RE.test(row[j]);
121
+ rightAlign[j] =
122
+ rightAlign[j] === undefined
123
+ ? numeric
124
+ : rightAlign[j] && numeric;
125
+ }
126
+ }
127
+
128
+ for (const row of grid) {
129
+ const span = row.length <= 1;
130
+ for (let j = 0; j < Math.max(1, row.length); j++) {
131
+ const cell = document.createElement("div");
132
+ cell.textContent = row[j] ?? "";
133
+ cell.style.maxWidth = `${style.maxColumnPx * (span ? 2 : 1)}px`;
134
+ cell.style.overflow = "hidden";
135
+ cell.style.textOverflow = "ellipsis";
136
+ cell.style.whiteSpace = "nowrap";
137
+ if (span) {
138
+ cell.style.gridColumn = "1 / -1";
139
+ } else if (rightAlign[j]) {
140
+ cell.style.textAlign = "right";
141
+ }
142
+
143
+ root.appendChild(cell);
144
+ if (span) {
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
@@ -23,6 +23,11 @@ import type {
23
23
  UserClickPayload,
24
24
  UserSelectPayload,
25
25
  } from "./tooltip-controller";
26
+ import {
27
+ DEFAULT_TOOLTIP_STYLE,
28
+ type TooltipContent,
29
+ type TooltipStyle,
30
+ } from "./tooltip-grid";
26
31
 
27
32
  /**
28
33
  * The subset of `WorkerMsg`s that flow chart → host through a
@@ -52,11 +57,12 @@ export class MessageHostSink implements HostSink {
52
57
  }
53
58
 
54
59
  pin(
55
- lines: string[],
60
+ grid: TooltipContent,
56
61
  pos: { px: number; py: number },
57
62
  bounds: CssBounds,
63
+ style: TooltipStyle = DEFAULT_TOOLTIP_STYLE,
58
64
  ): void {
59
- this._send({ kind: "pinTooltip", lines, pos, bounds });
65
+ this._send({ kind: "pinTooltip", grid, style, pos, bounds });
60
66
  }
61
67
 
62
68
  dismiss(): void {
@@ -32,7 +32,7 @@ export class LazyTooltip<Target> {
32
32
  /**
33
33
  * Cached lines for the latest committed hover, or `null`.
34
34
  */
35
- lines: string[] | null = null;
35
+ grid: string[][] | null = null;
36
36
 
37
37
  /**
38
38
  * Identity of the entity `lines` describe. `null` when cleared.
@@ -50,7 +50,7 @@ export class LazyTooltip<Target> {
50
50
  * the write.
51
51
  */
52
52
  beginHover(target: Target): number {
53
- this.lines = null;
53
+ this.grid = null;
54
54
  this.hoveredTarget = target;
55
55
  return ++this._hoverSerial;
56
56
  }
@@ -60,12 +60,12 @@ export class LazyTooltip<Target> {
60
60
  * when the write happened (caller should repaint), false when the
61
61
  * serial was stale.
62
62
  */
63
- commitHover(serial: number, lines: string[]): boolean {
63
+ commitHover(serial: number, grid: string[][]): boolean {
64
64
  if (serial !== this._hoverSerial) {
65
65
  return false;
66
66
  }
67
67
 
68
- this.lines = lines;
68
+ this.grid = grid;
69
69
  return true;
70
70
  }
71
71
 
@@ -73,7 +73,7 @@ export class LazyTooltip<Target> {
73
73
  * Clear hover state (mouse left, view changed, etc.).
74
74
  */
75
75
  clearHover(): void {
76
- this.lines = null;
76
+ this.grid = null;
77
77
  this.hoveredTarget = null;
78
78
  this._hoverSerial++;
79
79
  }