@perspective-dev/viewer-charts 4.5.0 → 4.5.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 (73) hide show
  1. package/LICENSE.md +193 -0
  2. package/dist/cdn/perspective-viewer-charts.js +2 -2
  3. package/dist/cdn/perspective-viewer-charts.js.map +3 -3
  4. package/dist/esm/axis/bar-axis.d.ts +9 -1
  5. package/dist/esm/axis/categorical-axis.d.ts +0 -2
  6. package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
  7. package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
  8. package/dist/esm/charts/common/expand-domain.d.ts +20 -0
  9. package/dist/esm/charts/common/tree-chart.d.ts +7 -0
  10. package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
  11. package/dist/esm/charts/common/tree-interact.d.ts +46 -0
  12. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
  13. package/dist/esm/charts/series/series-build.d.ts +38 -2
  14. package/dist/esm/charts/series/series-render.d.ts +1 -4
  15. package/dist/esm/charts/series/series-type.d.ts +19 -17
  16. package/dist/esm/charts/series/series.d.ts +16 -0
  17. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
  18. package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
  19. package/dist/esm/interaction/host-sink-message.d.ts +10 -28
  20. package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
  21. package/dist/esm/interaction/zoom-controller.d.ts +31 -20
  22. package/dist/esm/interaction/zoom-router.d.ts +3 -26
  23. package/dist/esm/perspective-viewer-charts.js +2 -2
  24. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  25. package/dist/esm/plugin/plugin.d.ts +0 -1
  26. package/dist/esm/theme/palette.d.ts +0 -5
  27. package/dist/esm/transport/protocol.d.ts +2 -7
  28. package/dist/esm/worker/renderer.worker.d.ts +2 -4
  29. package/package.json +45 -45
  30. package/src/ts/axis/bar-axis.ts +74 -45
  31. package/src/ts/axis/categorical-axis.ts +0 -2
  32. package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
  33. package/src/ts/charts/candlestick/candlestick.ts +10 -29
  34. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
  35. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
  36. package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
  37. package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
  38. package/src/ts/charts/cartesian/cartesian.ts +43 -4
  39. package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
  40. package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
  41. package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
  42. package/src/ts/charts/chart-base.ts +20 -6
  43. package/src/ts/charts/chart.ts +1 -1
  44. package/src/ts/charts/common/category-axis-resolver.ts +135 -1
  45. package/src/ts/charts/common/expand-domain.ts +40 -0
  46. package/src/ts/charts/common/tree-chart.ts +16 -0
  47. package/src/ts/charts/common/tree-chrome.ts +86 -1
  48. package/src/ts/charts/common/tree-interact.ts +209 -0
  49. package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
  50. package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
  51. package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
  52. package/src/ts/charts/series/series-build.ts +394 -21
  53. package/src/ts/charts/series/series-render.ts +159 -38
  54. package/src/ts/charts/series/series-type.ts +37 -17
  55. package/src/ts/charts/series/series.ts +63 -68
  56. package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
  57. package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
  58. package/src/ts/charts/sunburst/sunburst.ts +1 -15
  59. package/src/ts/charts/treemap/treemap-interact.ts +22 -189
  60. package/src/ts/charts/treemap/treemap-render.ts +19 -46
  61. package/src/ts/charts/treemap/treemap.ts +1 -16
  62. package/src/ts/interaction/host-sink-message.ts +33 -22
  63. package/src/ts/interaction/raw-event-forwarder.ts +10 -12
  64. package/src/ts/interaction/zoom-controller.ts +120 -83
  65. package/src/ts/interaction/zoom-router.ts +3 -126
  66. package/src/ts/map/tile-layer.ts +13 -13
  67. package/src/ts/plugin/plugin.ts +100 -184
  68. package/src/ts/shaders/line-uniform.frag.glsl +2 -1
  69. package/src/ts/shaders/line-uniform.vert.glsl +19 -0
  70. package/src/ts/theme/palette.ts +1 -4
  71. package/src/ts/transport/protocol.ts +3 -8
  72. package/src/ts/worker/dispatch.ts +0 -1
  73. package/src/ts/worker/renderer.worker.ts +10 -46
@@ -11,13 +11,19 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  import type { SunburstChart } from "./sunburst";
14
- import { NULL_NODE, ancestorNames } from "../common/node-store";
15
- import { rebuildBreadcrumbs } from "../common/tree-data";
14
+ import { NULL_NODE } from "../common/node-store";
16
15
  import {
17
16
  renderSunburstFrame,
18
17
  renderSunburstChromeOverlay,
19
18
  facetCenterForNode,
20
19
  } from "./sunburst-render";
20
+ import {
21
+ buildTreeTooltipLines,
22
+ dismissTreePinnedTooltip,
23
+ emitTreeNodeEvent,
24
+ showTreePinnedTooltip,
25
+ treeDrillTo,
26
+ } from "../common/tree-interact";
21
27
 
22
28
  export type { BreadcrumbRegion as SunburstBreadcrumbRegion } from "../common/tree-chrome";
23
29
 
@@ -194,7 +200,7 @@ export function handleSunburstHover(
194
200
  chart._hoveredNodeId = hit;
195
201
  if (hit !== NULL_NODE) {
196
202
  const serial = chart._lazyTooltip.beginHover(hit);
197
- buildSunburstTooltipLines(chart, hit).then((lines) => {
203
+ buildTreeTooltipLines(chart, hit).then((lines) => {
198
204
  if (chart._lazyTooltip.commitHover(serial, lines)) {
199
205
  renderSunburstChromeOverlay(chart);
200
206
  }
@@ -273,188 +279,38 @@ export function handleSunburstClick(
273
279
 
274
280
  if (store.firstChild[hit] !== NULL_NODE) {
275
281
  drillTo(chart, hit);
276
- void emitSunburstNodeEvent(chart, hit, "branch");
282
+ void emitTreeNodeEvent(chart, hit, "branch");
277
283
  } else {
278
284
  showSunburstPinnedTooltip(chart, hit);
279
- void emitSunburstNodeEvent(chart, hit, "leaf");
285
+ void emitTreeNodeEvent(chart, hit, "leaf");
280
286
  }
281
287
  }
282
288
 
283
- /**
284
- * Counterpart to `emitTreemapNodeEvent` for sunburst. Same path-walk
285
- * semantics — split-by prefix in faceted mode, group-by levels
286
- * afterward, leaf row idx from `_nodeStore.leafRowIdx`.
287
- */
288
- async function emitSunburstNodeEvent(
289
- chart: SunburstChart,
290
- nodeId: number,
291
- kind: "leaf" | "branch",
292
- ): Promise<void> {
293
- const store = chart._nodeStore;
294
- const path = ancestorNames(store, nodeId);
295
- const isFaceted =
296
- chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid";
297
- const splitByValues: (string | null)[] = isFaceted
298
- ? path.slice(0, chart._splitBy.length)
299
- : [];
300
- const groupByValues: (string | null)[] = isFaceted
301
- ? path.slice(
302
- chart._splitBy.length,
303
- chart._splitBy.length + chart._groupBy.length,
304
- )
305
- : path.slice(0, chart._groupBy.length);
306
-
307
- const rowIdx = kind === "leaf" ? (store.leafRowIdx[nodeId] ?? null) : null;
308
-
309
- await chart.emitClickAndSelect({
310
- rowIdx: rowIdx != null && rowIdx >= 0 ? rowIdx : null,
311
- columnName: chart._sizeName,
312
- groupByValues,
313
- splitByValues,
314
- });
315
- }
316
-
317
- /**
318
- * Drill the clicked facet (or the whole chart in non-facet mode).
319
- * Faceted drill walks up to the facet root (top-level child of
320
- * `_rootId`), records the new drill node under that facet's label,
321
- * and re-renders.
322
- */
323
289
  function drillTo(chart: SunburstChart, nodeId: number): void {
324
- const store = chart._nodeStore;
325
- if (chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid") {
326
- let p = nodeId;
327
- while (p !== NULL_NODE && store.parent[p] !== chart._rootId) {
328
- p = store.parent[p];
329
- }
330
-
331
- if (p !== NULL_NODE) {
332
- chart._facetDrillRoots.set(store.name[p], nodeId);
333
- }
334
-
335
- chart._hoveredNodeId = NULL_NODE;
290
+ treeDrillTo(chart, nodeId, () => {
336
291
  if (chart._glManager) {
337
292
  renderSunburstFrame(chart, chart._glManager);
338
293
  }
339
-
340
- return;
341
- }
342
-
343
- chart._currentRootId = nodeId;
344
- rebuildBreadcrumbs(chart, nodeId);
345
- chart._hoveredNodeId = NULL_NODE;
346
- if (chart._glManager) {
347
- renderSunburstFrame(chart, chart._glManager);
348
- }
294
+ });
349
295
  }
350
296
 
351
297
  export function showSunburstPinnedTooltip(
352
298
  chart: SunburstChart,
353
299
  nodeId: number,
354
300
  ): void {
355
- chart._tooltip.dismiss();
356
- chart._pinnedNodeId = nodeId;
357
-
358
301
  const store = chart._nodeStore;
359
302
  const midA = (store.a0[nodeId] + store.a1[nodeId]) / 2;
360
303
  const midR = (store.r0[nodeId] + store.r1[nodeId]) / 2;
361
304
  const { centerX, centerY } = facetCenterForNode(chart, nodeId);
362
305
  const cx = centerX + Math.cos(midA) * midR;
363
306
  const cy = centerY + Math.sin(midA) * midR;
364
-
365
- // CSS bounds: prefer `glManager` (works in both local and worker
366
- // modes, since the worker constructs its own context manager).
367
- const cssWidth = chart._glManager?.cssWidth ?? 0;
368
- const cssHeight = chart._glManager?.cssHeight ?? 0;
369
-
370
- // Tooltip columns are fetched lazily from the view — the tree
371
- // itself only retains ancestor names + aggregated value + color.
372
- // Stale resolutions are discarded via the `_pinnedNodeId` check.
373
- buildSunburstTooltipLines(chart, nodeId).then((lines) => {
374
- if (chart._pinnedNodeId !== nodeId) {
375
- return;
376
- }
377
-
378
- if (lines.length === 0) {
379
- return;
380
- }
381
-
382
- chart._tooltip.pin(lines, { px: cx, py: cy }, { cssWidth, cssHeight });
383
- });
384
-
385
- chart._hoveredNodeId = NULL_NODE;
386
- renderSunburstChromeOverlay(chart);
307
+ showTreePinnedTooltip(chart, nodeId, { cx, cy }, () =>
308
+ renderSunburstChromeOverlay(chart),
309
+ );
387
310
  }
388
311
 
389
312
  export function dismissSunburstPinnedTooltip(chart: SunburstChart): void {
390
- chart._tooltip.dismiss();
391
- chart._pinnedNodeId = NULL_NODE;
313
+ dismissTreePinnedTooltip(chart);
392
314
  }
393
315
 
394
- export async function buildSunburstTooltipLines(
395
- chart: SunburstChart,
396
- nodeId: number,
397
- ): Promise<string[]> {
398
- const store = chart._nodeStore;
399
- const lines: string[] = [];
400
-
401
- // Ancestor path.
402
- const pathNames: string[] = [];
403
- let p = nodeId;
404
- while (store.parent[p] !== NULL_NODE) {
405
- pathNames.push(store.name[p]);
406
- p = store.parent[p];
407
- }
408
-
409
- pathNames.reverse();
410
- if (pathNames.length > 0) {
411
- lines.push(pathNames.join(" › "));
412
- } else {
413
- lines.push(store.name[nodeId]);
414
- }
415
-
416
- const sizeFmt = chart.getColumnFormatter(chart._sizeName, "value");
417
- lines.push(`Value: ${sizeFmt(store.value[nodeId])}`);
418
-
419
- // Color value (numeric branch): stored on the node at insert
420
- // time, so it's always available without a view fetch.
421
- if (chart._colorName && !isNaN(store.colorValue[nodeId])) {
422
- const colorFmt = chart.getColumnFormatter(chart._colorName, "value");
423
- lines.push(
424
- `${chart._colorName}: ${colorFmt(store.colorValue[nodeId])}`,
425
- );
426
- }
427
-
428
- const rowIdx = store.leafRowIdx[nodeId];
429
- const isLeaf =
430
- store.firstChild[nodeId] === NULL_NODE && rowIdx !== NULL_NODE;
431
-
432
- // Extra tooltip columns fetched on demand — see the treemap
433
- // counterpart for the same pattern.
434
- if (isLeaf && chart._lazyRows) {
435
- const row = await chart._lazyRows.fetchRow(rowIdx);
436
- for (const [name, value] of row) {
437
- if (value === null || value === undefined) {
438
- continue;
439
- }
440
-
441
- if (name === chart._colorName && !isNaN(store.colorValue[nodeId])) {
442
- continue;
443
- }
444
-
445
- if (typeof value === "number") {
446
- lines.push(
447
- `${name}: ${chart.getColumnFormatter(name, "value")(value)}`,
448
- );
449
- } else {
450
- lines.push(`${name}: ${value}`);
451
- }
452
- }
453
- }
454
-
455
- if (store.firstChild[nodeId] !== NULL_NODE) {
456
- lines.push(`Children: ${store.childCount[nodeId]}`);
457
- }
458
-
459
- return lines;
460
- }
316
+ export { buildTreeTooltipLines as buildSunburstTooltipLines } from "../common/tree-interact";
@@ -16,12 +16,11 @@ import type { SunburstChart } from "./sunburst";
16
16
  import { NULL_NODE } from "../common/node-store";
17
17
  import { resolvePalette, type Vec3 } from "../../theme/palette";
18
18
  import { type GradientStop } from "../../theme/gradient";
19
- import { renderLegend, renderCategoricalLegend } from "../../axis/legend";
20
- import { PlotLayout } from "../../layout/plot-layout";
21
19
  import { leafColor, leafRGBA, luminance } from "../common/leaf-color";
22
20
  import arcVert from "../../shaders/sunburst-arc.vert.glsl";
23
21
  import arcFrag from "../../shaders/sunburst-arc.frag.glsl";
24
22
  import { getInstancing } from "../../webgl/instanced-attrs";
23
+ import { compileProgram } from "../../webgl/program-cache";
25
24
  import {
26
25
  partitionSunburst,
27
26
  collectVisibleArcs,
@@ -29,10 +28,10 @@ import {
29
28
  INNER_RING_PX,
30
29
  } from "./sunburst-layout";
31
30
  import { buildFacetGrid } from "../../layout/facet-grid";
32
- import { renderCategoricalLegendAt } from "../../axis/legend";
33
31
  import { withChromeCache } from "../common/chrome-cache";
34
32
  import {
35
33
  renderBreadcrumbs as renderTreeBreadcrumbs,
34
+ renderTreeColorLegend,
36
35
  renderTreeTooltip,
37
36
  } from "../common/tree-chrome";
38
37
 
@@ -294,22 +293,18 @@ function ensureProgram(
294
293
  }
295
294
 
296
295
  const gl = glManager.gl;
297
- const prog = glManager.shaders.getOrCreate(
296
+ const compiled = compileProgram<
297
+ { program: WebGLProgram } & NonNullable<SunburstChart["_locations"]>
298
+ >(
299
+ glManager,
298
300
  "sunburst-arc",
299
301
  arcVert,
300
302
  arcFrag,
303
+ ["u_center", "u_resolution", "u_border_px"],
304
+ ["a_strip_t", "a_side", "a_angles", "a_radii", "a_color"],
301
305
  );
302
- chart._program = prog;
303
- chart._locations = {
304
- u_center: gl.getUniformLocation(prog, "u_center"),
305
- u_resolution: gl.getUniformLocation(prog, "u_resolution"),
306
- u_border_px: gl.getUniformLocation(prog, "u_border_px"),
307
- a_strip_t: gl.getAttribLocation(prog, "a_strip_t"),
308
- a_side: gl.getAttribLocation(prog, "a_side"),
309
- a_angles: gl.getAttribLocation(prog, "a_angles"),
310
- a_radii: gl.getAttribLocation(prog, "a_radii"),
311
- a_color: gl.getAttribLocation(prog, "a_color"),
312
- };
306
+ chart._program = compiled.program;
307
+ chart._locations = compiled;
313
308
 
314
309
  // Build the static triangle-strip template once. Layout:
315
310
  // pairs of (strip_t, side) for each of the 2*(N_STEPS+1) vertices.
@@ -669,80 +664,20 @@ function drawStaticChrome(
669
664
  renderTreeBreadcrumbs(chart, ctx, cssWidth, fontFamily, textColor);
670
665
  }
671
666
 
672
- // Legend. In faceted mode use the grid's explicit rect; otherwise
673
- // derive from a synthetic single-plot layout.
674
- if (faceted && chart._facetGrid?.legendRect) {
675
- if (
676
- chart._colorMode === "series" &&
677
- chart._uniqueColorLabels.size > 1
678
- ) {
679
- renderCategoricalLegendAt(
680
- canvas,
681
- chart._facetGrid.legendRect,
682
- chart._uniqueColorLabels,
683
- palette,
684
- theme,
685
- );
686
- } else if (
687
- chart._colorMode === "numeric" &&
688
- chart._colorMin < chart._colorMax
689
- ) {
690
- const legendLayout = new PlotLayout(cssWidth, cssHeight, {
691
- hasXLabel: false,
692
- hasYLabel: false,
693
- hasLegend: true,
694
- });
695
- renderLegend(
696
- canvas,
697
- legendLayout,
698
- {
699
- min: chart._colorMin,
700
- max: chart._colorMax,
701
- label: chart._colorName,
702
- },
703
- stops,
704
- theme,
705
- chart.getColumnFormatter(chart._colorName, "value"),
706
- );
707
- }
708
- } else if (
709
- chart._colorMode === "series" &&
710
- chart._uniqueColorLabels.size > 1
711
- ) {
712
- const legendLayout = new PlotLayout(cssWidth, cssHeight, {
713
- hasXLabel: false,
714
- hasYLabel: false,
715
- hasLegend: true,
716
- });
717
- renderCategoricalLegend(
718
- canvas,
719
- legendLayout,
720
- chart._uniqueColorLabels,
721
- palette,
722
- theme,
723
- );
724
- } else if (
725
- chart._colorMode === "numeric" &&
726
- chart._colorMin < chart._colorMax
727
- ) {
728
- const legendLayout = new PlotLayout(cssWidth, cssHeight, {
729
- hasXLabel: false,
730
- hasYLabel: false,
731
- hasLegend: true,
732
- });
733
- renderLegend(
734
- canvas,
735
- legendLayout,
736
- {
737
- min: chart._colorMin,
738
- max: chart._colorMax,
739
- label: chart._colorName,
740
- },
741
- stops,
742
- theme,
743
- chart.getColumnFormatter(chart._colorName, "value"),
744
- );
745
- }
667
+ // Legend. Faceted mode passes `FacetGrid.legendRect` so the
668
+ // categorical-swatch variant lands in the dedicated grid slot;
669
+ // numeric gradient always derives from a synthetic single-plot
670
+ // layout (its vertical bar doesn't fit the compact rect).
671
+ renderTreeColorLegend(
672
+ chart,
673
+ canvas,
674
+ palette,
675
+ stops,
676
+ theme,
677
+ cssWidth,
678
+ cssHeight,
679
+ faceted ? (chart._facetGrid?.legendRect ?? null) : null,
680
+ );
746
681
 
747
682
  ctx.restore();
748
683
  }
@@ -12,7 +12,7 @@
12
12
 
13
13
  import type { ColumnDataMap } from "../../data/view-reader";
14
14
  import type { WebGLContextManager } from "../../webgl/context-manager";
15
- import { TreeChartBase } from "../common/tree-chart";
15
+ import { TreeChartBase, firstNonMetadataColumn } from "../common/tree-chart";
16
16
  import { NULL_NODE } from "../common/node-store";
17
17
  import {
18
18
  processTreeChunk,
@@ -41,20 +41,6 @@ export interface SunburstLocations {
41
41
  a_color: number;
42
42
  }
43
43
 
44
- /**
45
- * Sentinel fallback for the Size slot when the user hasn't picked one:
46
- * use the first non-metadata column in the incoming view.
47
- */
48
- function firstNonMetadataColumn(columns: ColumnDataMap): string {
49
- for (const k of columns.keys()) {
50
- if (!k.startsWith("__")) {
51
- return k;
52
- }
53
- }
54
-
55
- return "";
56
- }
57
-
58
44
  /**
59
45
  * Sunburst chart. Shares tree storage + streaming pipeline + color
60
46
  * mode with `TreeChartBase`; adds polar layout + instanced-arc WebGL
@@ -11,12 +11,19 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  import type { TreemapChart } from "./treemap";
14
- import { NULL_NODE, ancestorNames } from "../common/node-store";
15
- import { PADDING_LABEL, rebuildBreadcrumbs } from "./treemap-layout";
14
+ import { NULL_NODE } from "../common/node-store";
15
+ import { PADDING_LABEL } from "./treemap-layout";
16
16
  import {
17
17
  renderTreemapFrame,
18
18
  renderTreemapChromeOverlay,
19
19
  } from "./treemap-render";
20
+ import {
21
+ buildTreeTooltipLines,
22
+ dismissTreePinnedTooltip,
23
+ emitTreeNodeEvent,
24
+ showTreePinnedTooltip,
25
+ treeDrillTo,
26
+ } from "../common/tree-interact";
20
27
 
21
28
  interface HitResult {
22
29
  leafId: number;
@@ -152,7 +159,7 @@ export function handleTreemapHover(
152
159
  // (mouse moved elsewhere, new view) are dropped by the
153
160
  // controller's serial gate.
154
161
  const serial = chart._lazyTooltip.beginHover(best);
155
- buildTreemapTooltipLines(chart, best).then((lines) => {
162
+ buildTreeTooltipLines(chart, best).then((lines) => {
156
163
  if (chart._lazyTooltip.commitHover(serial, lines)) {
157
164
  renderTreemapChromeOverlay(chart);
158
165
  }
@@ -199,59 +206,16 @@ export function handleTreemapClick(
199
206
 
200
207
  if (branchId !== NULL_NODE && inHeader) {
201
208
  drillTo(chart, branchId);
202
- void emitTreemapNodeEvent(chart, branchId, "branch");
209
+ void emitTreeNodeEvent(chart, branchId, "branch");
203
210
  } else if (leafId !== NULL_NODE) {
204
211
  showTreemapPinnedTooltip(chart, leafId);
205
- void emitTreemapNodeEvent(chart, leafId, "leaf");
212
+ void emitTreeNodeEvent(chart, leafId, "leaf");
206
213
  } else if (branchId !== NULL_NODE) {
207
214
  drillTo(chart, branchId);
208
- void emitTreemapNodeEvent(chart, branchId, "branch");
215
+ void emitTreeNodeEvent(chart, branchId, "branch");
209
216
  }
210
217
  }
211
218
 
212
- /**
213
- * Build a click detail from a treemap node id and emit both
214
- * `perspective-click` and `perspective-global-filter selected:true`.
215
- *
216
- * For leaves, the source-view row index is `store.leafRowIdx[id]` and
217
- * the row payload is populated via `_lazyRows`. For branches, no
218
- * source row exists (the branch is a rollup), so `rowIdx: null` and
219
- * the row payload is `{}` — only the filter path is meaningful.
220
- *
221
- * The path is walked via `ancestorNames` and split into split-by
222
- * prefix + group-by levels using `_splitBy.length` as the boundary.
223
- * Faceted mode (`facet_mode === "grid"` with non-empty `_splitBy`)
224
- * keeps the depth-0 ancestor name as the split prefix.
225
- */
226
- async function emitTreemapNodeEvent(
227
- chart: TreemapChart,
228
- nodeId: number,
229
- kind: "leaf" | "branch",
230
- ): Promise<void> {
231
- const store = chart._nodeStore;
232
- const path = ancestorNames(store, nodeId);
233
- const isFaceted =
234
- chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid";
235
- const splitByValues: (string | null)[] = isFaceted
236
- ? path.slice(0, chart._splitBy.length)
237
- : [];
238
- const groupByValues: (string | null)[] = isFaceted
239
- ? path.slice(
240
- chart._splitBy.length,
241
- chart._splitBy.length + chart._groupBy.length,
242
- )
243
- : path.slice(0, chart._groupBy.length);
244
-
245
- const rowIdx = kind === "leaf" ? (store.leafRowIdx[nodeId] ?? null) : null;
246
-
247
- await chart.emitClickAndSelect({
248
- rowIdx: rowIdx != null && rowIdx >= 0 ? rowIdx : null,
249
- columnName: chart._sizeName,
250
- groupByValues,
251
- splitByValues,
252
- });
253
- }
254
-
255
219
  export function handleTreemapDblClick(
256
220
  chart: TreemapChart,
257
221
  mx: number,
@@ -279,167 +243,36 @@ export function handleTreemapDblClick(
279
243
  store.firstChild[target] !== NULL_NODE
280
244
  ) {
281
245
  drillTo(chart, target);
282
- void emitTreemapNodeEvent(chart, target, "branch");
246
+ void emitTreeNodeEvent(chart, target, "branch");
283
247
  if (leafId !== NULL_NODE && store.firstChild[leafId] === NULL_NODE) {
284
248
  showTreemapPinnedTooltip(chart, leafId);
285
- void emitTreemapNodeEvent(chart, leafId, "leaf");
249
+ void emitTreeNodeEvent(chart, leafId, "leaf");
286
250
  }
287
251
  }
288
252
  }
289
253
 
290
- /**
291
- * Drill the current facet (or the whole chart in non-facet mode).
292
- *
293
- * In faceted mode, walks up the ancestor chain of `nodeId` until the
294
- * facet root (a top-level child of `_rootId`) is found, then sets
295
- * `_facetDrillRoots[facetLabel] = nodeId` so only that facet's
296
- * subtree re-layouts. Non-facet mode keeps the existing single-
297
- * `_currentRootId` behavior and rebuilds the breadcrumb trail.
298
- */
299
254
  function drillTo(chart: TreemapChart, nodeId: number): void {
300
- const store = chart._nodeStore;
301
- if (chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid") {
302
- // Walk up to find the facet-root ancestor (top-level child of
303
- // `_rootId`). Guard against drills that target the synthetic
304
- // root or a facet root itself — those would un-drill the facet.
305
- let p = nodeId;
306
- while (p !== NULL_NODE && store.parent[p] !== chart._rootId) {
307
- p = store.parent[p];
308
- }
309
-
310
- if (p !== NULL_NODE) {
311
- const label = store.name[p];
312
- chart._facetDrillRoots.set(label, nodeId);
313
- }
314
-
315
- chart._hoveredNodeId = NULL_NODE;
255
+ treeDrillTo(chart, nodeId, () => {
316
256
  if (chart._glManager) {
317
257
  renderTreemapFrame(chart, chart._glManager);
318
258
  }
319
-
320
- return;
321
- }
322
-
323
- chart._currentRootId = nodeId;
324
- rebuildBreadcrumbs(chart, nodeId);
325
- chart._hoveredNodeId = NULL_NODE;
326
- if (chart._glManager) {
327
- renderTreemapFrame(chart, chart._glManager);
328
- }
259
+ });
329
260
  }
330
261
 
331
262
  export function showTreemapPinnedTooltip(
332
263
  chart: TreemapChart,
333
264
  nodeId: number,
334
265
  ): void {
335
- chart._tooltip.dismiss();
336
- chart._pinnedNodeId = nodeId;
337
-
338
266
  const store = chart._nodeStore;
339
267
  const cx = (store.x0[nodeId] + store.x1[nodeId]) / 2;
340
268
  const cy = (store.y0[nodeId] + store.y1[nodeId]) / 2;
341
-
342
- // CSS bounds: prefer `glManager` (works in both local and worker
343
- // modes, since the worker constructs its own context manager).
344
- const cssWidth = chart._glManager?.cssWidth ?? 0;
345
- const cssHeight = chart._glManager?.cssHeight ?? 0;
346
-
347
- // Tooltip columns are fetched lazily from the view — the tree
348
- // itself only retains ancestor names + aggregated value + color.
349
- // If the user dismisses or re-pins between click and resolve, the
350
- // `_pinnedNodeId` check discards the stale result.
351
- buildTreemapTooltipLines(chart, nodeId).then((lines) => {
352
- if (chart._pinnedNodeId !== nodeId) {
353
- return;
354
- }
355
-
356
- if (lines.length === 0) {
357
- return;
358
- }
359
-
360
- chart._tooltip.pin(lines, { px: cx, py: cy }, { cssWidth, cssHeight });
361
- });
362
-
363
- chart._hoveredNodeId = NULL_NODE;
364
- renderTreemapChromeOverlay(chart);
269
+ showTreePinnedTooltip(chart, nodeId, { cx, cy }, () =>
270
+ renderTreemapChromeOverlay(chart),
271
+ );
365
272
  }
366
273
 
367
274
  export function dismissTreemapPinnedTooltip(chart: TreemapChart): void {
368
- chart._tooltip.dismiss();
369
- chart._pinnedNodeId = NULL_NODE;
275
+ dismissTreePinnedTooltip(chart);
370
276
  }
371
277
 
372
- /**
373
- * Build the tooltip for `nodeId`. The node's own name path + aggregate
374
- * value are derived from the tree; per-row tooltip columns come from
375
- * the `leafRowIdx` → column-buffer lookup (no per-node `Map`).
376
- */
377
- export async function buildTreemapTooltipLines(
378
- chart: TreemapChart,
379
- nodeId: number,
380
- ): Promise<string[]> {
381
- const store = chart._nodeStore;
382
- const lines: string[] = [];
383
-
384
- // Name path (ancestors, topmost first, excluding synthetic root).
385
- const pathNames: string[] = [];
386
- let p = nodeId;
387
- while (store.parent[p] !== NULL_NODE) {
388
- pathNames.push(store.name[p]);
389
- p = store.parent[p];
390
- }
391
-
392
- pathNames.reverse();
393
- if (pathNames.length > 0) {
394
- lines.push(pathNames.join(" \u203A "));
395
- } else {
396
- lines.push(store.name[nodeId]);
397
- }
398
-
399
- const sizeFmt = chart.getColumnFormatter(chart._sizeName, "value");
400
- lines.push(`Value: ${sizeFmt(store.value[nodeId])}`);
401
-
402
- // Color value (numeric branch): stored on the node at insert
403
- // time, so it's always available without a view fetch.
404
- if (chart._colorName && !isNaN(store.colorValue[nodeId])) {
405
- const colorFmt = chart.getColumnFormatter(chart._colorName, "value");
406
- lines.push(
407
- `${chart._colorName}: ${colorFmt(store.colorValue[nodeId])}`,
408
- );
409
- }
410
-
411
- const rowIdx = store.leafRowIdx[nodeId];
412
- const isLeaf =
413
- store.firstChild[nodeId] === NULL_NODE && rowIdx !== NULL_NODE;
414
-
415
- // Extra tooltip columns come from the source view row, fetched on
416
- // demand via `_lazyRows`. Only leaves correspond to a single view
417
- // row; branch nodes aggregate rows and don't carry extra columns.
418
- if (isLeaf && chart._lazyRows) {
419
- const row = await chart._lazyRows.fetchRow(rowIdx);
420
- for (const [name, value] of row) {
421
- if (value === null || value === undefined) {
422
- continue;
423
- }
424
-
425
- if (name === chart._colorName && !isNaN(store.colorValue[nodeId])) {
426
- // Already emitted from the retained tree state above.
427
- continue;
428
- }
429
-
430
- if (typeof value === "number") {
431
- lines.push(
432
- `${name}: ${chart.getColumnFormatter(name, "value")(value)}`,
433
- );
434
- } else {
435
- lines.push(`${name}: ${value}`);
436
- }
437
- }
438
- }
439
-
440
- if (store.firstChild[nodeId] !== NULL_NODE) {
441
- lines.push(`Children: ${store.childCount[nodeId]}`);
442
- }
443
-
444
- return lines;
445
- }
278
+ export { buildTreeTooltipLines as buildTreemapTooltipLines } from "../common/tree-interact";