@perspective-dev/viewer-charts 4.3.0

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 (258) hide show
  1. package/LICENSE.md +193 -0
  2. package/dist/cdn/perspective-viewer-charts.js +3 -0
  3. package/dist/cdn/perspective-viewer-charts.js.map +7 -0
  4. package/dist/esm/axis/axis-primitives.d.ts +24 -0
  5. package/dist/esm/axis/bar-axis.d.ts +51 -0
  6. package/dist/esm/axis/canvas.d.ts +24 -0
  7. package/dist/esm/axis/categorical-axis-core.d.ts +42 -0
  8. package/dist/esm/axis/categorical-axis.d.ts +27 -0
  9. package/dist/esm/axis/facet-chrome.d.ts +13 -0
  10. package/dist/esm/axis/label-geometry.d.ts +41 -0
  11. package/dist/esm/axis/legend.d.ts +44 -0
  12. package/dist/esm/axis/numeric-axis.d.ts +20 -0
  13. package/dist/esm/charts/candlestick/candlestick-build.d.ts +129 -0
  14. package/dist/esm/charts/candlestick/candlestick-interact.d.ts +10 -0
  15. package/dist/esm/charts/candlestick/candlestick-render.d.ts +24 -0
  16. package/dist/esm/charts/candlestick/candlestick.d.ts +144 -0
  17. package/dist/esm/charts/candlestick/glyphs/draw-candlesticks.d.ts +36 -0
  18. package/dist/esm/charts/candlestick/glyphs/draw-ohlc.d.ts +33 -0
  19. package/dist/esm/charts/canvas-types.d.ts +15 -0
  20. package/dist/esm/charts/cartesian/cartesian-build.d.ts +14 -0
  21. package/dist/esm/charts/cartesian/cartesian-interact.d.ts +20 -0
  22. package/dist/esm/charts/cartesian/cartesian-render.d.ts +26 -0
  23. package/dist/esm/charts/cartesian/cartesian.d.ts +239 -0
  24. package/dist/esm/charts/cartesian/glyph.d.ts +53 -0
  25. package/dist/esm/charts/cartesian/glyphs/density.d.ts +142 -0
  26. package/dist/esm/charts/cartesian/glyphs/lines.d.ts +23 -0
  27. package/dist/esm/charts/cartesian/glyphs/points.d.ts +24 -0
  28. package/dist/esm/charts/cartesian/label-interner.d.ts +21 -0
  29. package/dist/esm/charts/cartesian/tooltip-lines.d.ts +11 -0
  30. package/dist/esm/charts/chart-base.d.ts +402 -0
  31. package/dist/esm/charts/chart.d.ts +338 -0
  32. package/dist/esm/charts/common/band-layout.d.ts +32 -0
  33. package/dist/esm/charts/common/categorical-y-chart.d.ts +53 -0
  34. package/dist/esm/charts/common/category-axis-resolver.d.ts +90 -0
  35. package/dist/esm/charts/common/chrome-cache.d.ts +18 -0
  36. package/dist/esm/charts/common/draw-tooltip-box.d.ts +9 -0
  37. package/dist/esm/charts/common/leaf-color.d.ts +33 -0
  38. package/dist/esm/charts/common/node-store.d.ts +81 -0
  39. package/dist/esm/charts/common/tree-chart.d.ts +48 -0
  40. package/dist/esm/charts/common/tree-chrome.d.ts +31 -0
  41. package/dist/esm/charts/common/tree-data.d.ts +54 -0
  42. package/dist/esm/charts/common/visible-extent.d.ts +51 -0
  43. package/dist/esm/charts/heatmap/heatmap-build.d.ts +86 -0
  44. package/dist/esm/charts/heatmap/heatmap-interact.d.ts +19 -0
  45. package/dist/esm/charts/heatmap/heatmap-render.d.ts +19 -0
  46. package/dist/esm/charts/heatmap/heatmap-y-axis.d.ts +46 -0
  47. package/dist/esm/charts/heatmap/heatmap.d.ts +117 -0
  48. package/dist/esm/charts/map/map.d.ts +67 -0
  49. package/dist/esm/charts/registry.d.ts +14 -0
  50. package/dist/esm/charts/series/glyphs/draw-areas.d.ts +30 -0
  51. package/dist/esm/charts/series/glyphs/draw-bars.d.ts +15 -0
  52. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +34 -0
  53. package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +33 -0
  54. package/dist/esm/charts/series/series-build.d.ts +228 -0
  55. package/dist/esm/charts/series/series-interact.d.ts +35 -0
  56. package/dist/esm/charts/series/series-render.d.ts +41 -0
  57. package/dist/esm/charts/series/series-type.d.ts +49 -0
  58. package/dist/esm/charts/series/series.d.ts +317 -0
  59. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +7 -0
  60. package/dist/esm/charts/sunburst/sunburst-layout.d.ts +33 -0
  61. package/dist/esm/charts/sunburst/sunburst-render.d.ts +22 -0
  62. package/dist/esm/charts/sunburst/sunburst.d.ts +85 -0
  63. package/dist/esm/charts/treemap/treemap-interact.d.ts +12 -0
  64. package/dist/esm/charts/treemap/treemap-layout.d.ts +28 -0
  65. package/dist/esm/charts/treemap/treemap-render.d.ts +18 -0
  66. package/dist/esm/charts/treemap/treemap.d.ts +74 -0
  67. package/dist/esm/config.d.ts +27 -0
  68. package/dist/esm/data/lazy-row.d.ts +32 -0
  69. package/dist/esm/data/split-groups.d.ts +20 -0
  70. package/dist/esm/data/view-reader.d.ts +35 -0
  71. package/dist/esm/event-detail.d.ts +28 -0
  72. package/dist/esm/index.d.ts +3 -0
  73. package/dist/esm/interaction/hit-test.d.ts +30 -0
  74. package/dist/esm/interaction/host-sink-dom.d.ts +19 -0
  75. package/dist/esm/interaction/host-sink-message.d.ts +46 -0
  76. package/dist/esm/interaction/lazy-tooltip.d.ts +61 -0
  77. package/dist/esm/interaction/raw-event-forwarder.d.ts +27 -0
  78. package/dist/esm/interaction/spatial-grid.d.ts +15 -0
  79. package/dist/esm/interaction/tooltip-controller.d.ts +193 -0
  80. package/dist/esm/interaction/zoom-controller.d.ts +106 -0
  81. package/dist/esm/interaction/zoom-router.d.ts +48 -0
  82. package/dist/esm/layout/facet-grid.d.ts +126 -0
  83. package/dist/esm/layout/plot-layout.d.ts +104 -0
  84. package/dist/esm/layout/ticks.d.ts +17 -0
  85. package/dist/esm/map/mercator.d.ts +102 -0
  86. package/dist/esm/map/tile-cache.d.ts +38 -0
  87. package/dist/esm/map/tile-layer.d.ts +66 -0
  88. package/dist/esm/map/tile-loader.d.ts +52 -0
  89. package/dist/esm/map/tile-source.d.ts +66 -0
  90. package/dist/esm/perspective-viewer-charts.js +3 -0
  91. package/dist/esm/perspective-viewer-charts.js.map +7 -0
  92. package/dist/esm/plugin/charts.d.ts +40 -0
  93. package/dist/esm/plugin/plugin.d.ts +95 -0
  94. package/dist/esm/render/scheduler.d.ts +41 -0
  95. package/dist/esm/theme/gradient.d.ts +48 -0
  96. package/dist/esm/theme/palette.d.ts +13 -0
  97. package/dist/esm/theme/theme-snapshot.d.ts +7 -0
  98. package/dist/esm/theme/theme.d.ts +53 -0
  99. package/dist/esm/transport/protocol.d.ts +430 -0
  100. package/dist/esm/transport/renderer-transport.d.ts +201 -0
  101. package/dist/esm/utils/css.d.ts +1 -0
  102. package/dist/esm/utils/font-snapshot.d.ts +50 -0
  103. package/dist/esm/webgl/buffer-pool.d.ts +62 -0
  104. package/dist/esm/webgl/context-manager.d.ts +184 -0
  105. package/dist/esm/webgl/gradient-texture.d.ts +17 -0
  106. package/dist/esm/webgl/instanced-attrs.d.ts +44 -0
  107. package/dist/esm/webgl/plot-frame.d.ts +39 -0
  108. package/dist/esm/webgl/program-cache.d.ts +13 -0
  109. package/dist/esm/webgl/shader-manifest.d.ts +53 -0
  110. package/dist/esm/webgl/shader-registry.d.ts +22 -0
  111. package/dist/esm/worker/boot.d.ts +0 -0
  112. package/dist/esm/worker/dispatch.d.ts +9 -0
  113. package/dist/esm/worker/font-loader.d.ts +2 -0
  114. package/dist/esm/worker/renderer.worker.d.ts +115 -0
  115. package/dist/esm/worker/session-host.d.ts +26 -0
  116. package/package.json +47 -0
  117. package/src/css/perspective-viewer-charts.css +95 -0
  118. package/src/ts/axis/axis-primitives.ts +125 -0
  119. package/src/ts/axis/bar-axis.ts +345 -0
  120. package/src/ts/axis/canvas.ts +64 -0
  121. package/src/ts/axis/categorical-axis-core.ts +125 -0
  122. package/src/ts/axis/categorical-axis.ts +716 -0
  123. package/src/ts/axis/facet-chrome.ts +42 -0
  124. package/src/ts/axis/label-geometry.ts +188 -0
  125. package/src/ts/axis/legend.ts +218 -0
  126. package/src/ts/axis/numeric-axis.ts +353 -0
  127. package/src/ts/charts/candlestick/candlestick-build.ts +516 -0
  128. package/src/ts/charts/candlestick/candlestick-interact.ts +256 -0
  129. package/src/ts/charts/candlestick/candlestick-render.ts +387 -0
  130. package/src/ts/charts/candlestick/candlestick.ts +367 -0
  131. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +432 -0
  132. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +317 -0
  133. package/src/ts/charts/canvas-types.ts +30 -0
  134. package/src/ts/charts/cartesian/cartesian-build.ts +616 -0
  135. package/src/ts/charts/cartesian/cartesian-interact.ts +355 -0
  136. package/src/ts/charts/cartesian/cartesian-render.ts +948 -0
  137. package/src/ts/charts/cartesian/cartesian.ts +469 -0
  138. package/src/ts/charts/cartesian/glyph.ts +81 -0
  139. package/src/ts/charts/cartesian/glyphs/density.ts +1263 -0
  140. package/src/ts/charts/cartesian/glyphs/lines.ts +320 -0
  141. package/src/ts/charts/cartesian/glyphs/points.ts +239 -0
  142. package/src/ts/charts/cartesian/label-interner.ts +56 -0
  143. package/src/ts/charts/cartesian/tooltip-lines.ts +80 -0
  144. package/src/ts/charts/chart-base.ts +840 -0
  145. package/src/ts/charts/chart.ts +427 -0
  146. package/src/ts/charts/common/band-layout.ts +63 -0
  147. package/src/ts/charts/common/categorical-y-chart.ts +81 -0
  148. package/src/ts/charts/common/category-axis-resolver.ts +314 -0
  149. package/src/ts/charts/common/chrome-cache.ts +79 -0
  150. package/src/ts/charts/common/draw-tooltip-box.ts +84 -0
  151. package/src/ts/charts/common/leaf-color.ts +92 -0
  152. package/src/ts/charts/common/node-store.ts +235 -0
  153. package/src/ts/charts/common/tree-chart.ts +76 -0
  154. package/src/ts/charts/common/tree-chrome.ts +123 -0
  155. package/src/ts/charts/common/tree-data.ts +623 -0
  156. package/src/ts/charts/common/visible-extent.ts +112 -0
  157. package/src/ts/charts/heatmap/heatmap-build.ts +426 -0
  158. package/src/ts/charts/heatmap/heatmap-interact.ts +274 -0
  159. package/src/ts/charts/heatmap/heatmap-render.ts +815 -0
  160. package/src/ts/charts/heatmap/heatmap-y-axis.ts +351 -0
  161. package/src/ts/charts/heatmap/heatmap.ts +368 -0
  162. package/src/ts/charts/map/map.ts +201 -0
  163. package/src/ts/charts/registry.ts +65 -0
  164. package/src/ts/charts/series/glyphs/draw-areas.ts +331 -0
  165. package/src/ts/charts/series/glyphs/draw-bars.ts +113 -0
  166. package/src/ts/charts/series/glyphs/draw-lines.ts +320 -0
  167. package/src/ts/charts/series/glyphs/draw-scatter.ts +328 -0
  168. package/src/ts/charts/series/series-build.ts +848 -0
  169. package/src/ts/charts/series/series-interact.ts +604 -0
  170. package/src/ts/charts/series/series-render.ts +1109 -0
  171. package/src/ts/charts/series/series-type.ts +99 -0
  172. package/src/ts/charts/series/series.ts +794 -0
  173. package/src/ts/charts/sunburst/sunburst-interact.ts +460 -0
  174. package/src/ts/charts/sunburst/sunburst-layout.ts +238 -0
  175. package/src/ts/charts/sunburst/sunburst-render.ts +887 -0
  176. package/src/ts/charts/sunburst/sunburst.ts +248 -0
  177. package/src/ts/charts/treemap/treemap-interact.ts +445 -0
  178. package/src/ts/charts/treemap/treemap-layout.ts +328 -0
  179. package/src/ts/charts/treemap/treemap-render.ts +886 -0
  180. package/src/ts/charts/treemap/treemap.ts +247 -0
  181. package/src/ts/config.ts +41 -0
  182. package/src/ts/data/lazy-row.ts +140 -0
  183. package/src/ts/data/split-groups.ts +97 -0
  184. package/src/ts/data/view-reader.ts +107 -0
  185. package/src/ts/event-detail.ts +44 -0
  186. package/src/ts/index.ts +53 -0
  187. package/src/ts/interaction/hit-test.ts +106 -0
  188. package/src/ts/interaction/host-sink-dom.ts +85 -0
  189. package/src/ts/interaction/host-sink-message.ts +75 -0
  190. package/src/ts/interaction/lazy-tooltip.ts +102 -0
  191. package/src/ts/interaction/raw-event-forwarder.ts +175 -0
  192. package/src/ts/interaction/spatial-grid.ts +100 -0
  193. package/src/ts/interaction/tooltip-controller.ts +407 -0
  194. package/src/ts/interaction/zoom-controller.ts +468 -0
  195. package/src/ts/interaction/zoom-router.ts +230 -0
  196. package/src/ts/layout/facet-grid.ts +346 -0
  197. package/src/ts/layout/plot-layout.ts +277 -0
  198. package/src/ts/layout/ticks.ts +168 -0
  199. package/src/ts/map/mercator.ts +204 -0
  200. package/src/ts/map/tile-cache.ts +96 -0
  201. package/src/ts/map/tile-layer.ts +382 -0
  202. package/src/ts/map/tile-loader.ts +143 -0
  203. package/src/ts/map/tile-source.ts +156 -0
  204. package/src/ts/plugin/charts.ts +286 -0
  205. package/src/ts/plugin/plugin.ts +668 -0
  206. package/src/ts/render/scheduler.ts +339 -0
  207. package/src/ts/shaders/area.frag.glsl +20 -0
  208. package/src/ts/shaders/area.vert.glsl +19 -0
  209. package/src/ts/shaders/bar.frag.glsl +25 -0
  210. package/src/ts/shaders/bar.vert.glsl +60 -0
  211. package/src/ts/shaders/candlestick-body.frag.glsl +19 -0
  212. package/src/ts/shaders/candlestick-body.vert.glsl +34 -0
  213. package/src/ts/shaders/density-extreme.frag.glsl +30 -0
  214. package/src/ts/shaders/density-mrt.frag.glsl +44 -0
  215. package/src/ts/shaders/density-mrt.vert.glsl +48 -0
  216. package/src/ts/shaders/density-resolve.frag.glsl +89 -0
  217. package/src/ts/shaders/density-resolve.vert.glsl +23 -0
  218. package/src/ts/shaders/density-splat.frag.glsl +34 -0
  219. package/src/ts/shaders/density-splat.vert.glsl +52 -0
  220. package/src/ts/shaders/gridline.frag.glsl +18 -0
  221. package/src/ts/shaders/gridline.vert.glsl +18 -0
  222. package/src/ts/shaders/heatmap.frag.glsl +23 -0
  223. package/src/ts/shaders/heatmap.vert.glsl +42 -0
  224. package/src/ts/shaders/line-uniform.frag.glsl +26 -0
  225. package/src/ts/shaders/line-uniform.vert.glsl +54 -0
  226. package/src/ts/shaders/line.frag.glsl +28 -0
  227. package/src/ts/shaders/line.vert.glsl +87 -0
  228. package/src/ts/shaders/scatter.frag.glsl +39 -0
  229. package/src/ts/shaders/scatter.vert.glsl +67 -0
  230. package/src/ts/shaders/sunburst-arc.frag.glsl +19 -0
  231. package/src/ts/shaders/sunburst-arc.vert.glsl +79 -0
  232. package/src/ts/shaders/tile.frag.glsl +27 -0
  233. package/src/ts/shaders/tile.vert.glsl +35 -0
  234. package/src/ts/shaders/treemap.frag.glsl +19 -0
  235. package/src/ts/shaders/treemap.vert.glsl +25 -0
  236. package/src/ts/shaders/y-scatter.frag.glsl +30 -0
  237. package/src/ts/shaders/y-scatter.vert.glsl +31 -0
  238. package/src/ts/theme/gradient.ts +312 -0
  239. package/src/ts/theme/palette.ts +64 -0
  240. package/src/ts/theme/theme-snapshot.ts +66 -0
  241. package/src/ts/theme/theme.ts +166 -0
  242. package/src/ts/transport/protocol.ts +497 -0
  243. package/src/ts/transport/renderer-transport.ts +788 -0
  244. package/src/ts/utils/css.ts +36 -0
  245. package/src/ts/utils/font-snapshot.ts +159 -0
  246. package/src/ts/webgl/buffer-pool.ts +163 -0
  247. package/src/ts/webgl/context-manager.ts +414 -0
  248. package/src/ts/webgl/gradient-texture.ts +84 -0
  249. package/src/ts/webgl/instanced-attrs.ts +139 -0
  250. package/src/ts/webgl/plot-frame.ts +91 -0
  251. package/src/ts/webgl/program-cache.ts +46 -0
  252. package/src/ts/webgl/shader-manifest.ts +148 -0
  253. package/src/ts/webgl/shader-registry.ts +97 -0
  254. package/src/ts/worker/boot.ts +22 -0
  255. package/src/ts/worker/dispatch.ts +99 -0
  256. package/src/ts/worker/font-loader.ts +89 -0
  257. package/src/ts/worker/renderer.worker.ts +734 -0
  258. package/src/ts/worker/session-host.ts +118 -0
@@ -0,0 +1,248 @@
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 { ColumnDataMap } from "../../data/view-reader";
14
+ import type { WebGLContextManager } from "../../webgl/context-manager";
15
+ import { TreeChartBase } from "../common/tree-chart";
16
+ import { NULL_NODE } from "../common/node-store";
17
+ import {
18
+ processTreeChunk,
19
+ finalizeTree,
20
+ resetTreeState,
21
+ } from "../common/tree-data";
22
+ import {
23
+ renderSunburstFrame,
24
+ renderSunburstChromeOverlay,
25
+ } from "./sunburst-render";
26
+ import {
27
+ handleSunburstHover,
28
+ handleSunburstClick,
29
+ dismissSunburstPinnedTooltip,
30
+ type SunburstBreadcrumbRegion,
31
+ } from "./sunburst-interact";
32
+
33
+ export interface SunburstLocations {
34
+ u_center: WebGLUniformLocation | null;
35
+ u_resolution: WebGLUniformLocation | null;
36
+ u_border_px: WebGLUniformLocation | null;
37
+ a_strip_t: number;
38
+ a_side: number;
39
+ a_angles: number;
40
+ a_radii: number;
41
+ a_color: number;
42
+ }
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
+ /**
59
+ * Sunburst chart. Shares tree storage + streaming pipeline + color
60
+ * mode with `TreeChartBase`; adds polar layout + instanced-arc WebGL
61
+ * rendering + drill / tooltip interactions.
62
+ *
63
+ * Internal option: `_labelRotation` — `"upright"` keeps labels on the
64
+ * left half flipped 180° so they read upright (d3fc behavior);
65
+ * `"radial"` leaves them purely tangent to the arc. Defaults to
66
+ * `"upright"`; toggle here if a call site wants flat radial labels.
67
+ */
68
+ export class SunburstChart extends TreeChartBase {
69
+ _program: WebGLProgram | null = null;
70
+ _locations: SunburstLocations | null = null;
71
+ _stripBuffer: WebGLBuffer | null = null;
72
+ _instanceBuffer: WebGLBuffer | null = null;
73
+ _instanceCount = 0;
74
+
75
+ /**
76
+ * Label orientation mode — see class docstring.
77
+ */
78
+ _labelRotation: "upright" | "radial" = "upright";
79
+
80
+ // Center / radius state resolved per frame.
81
+ _centerX = 0;
82
+ _centerY = 0;
83
+ _maxRadius = 0;
84
+
85
+ // Interaction
86
+ _hoveredNodeId: number = NULL_NODE;
87
+ _pinnedNodeId: number = NULL_NODE;
88
+ _breadcrumbRegions: SunburstBreadcrumbRegion[] = [];
89
+
90
+ _chromeCache: ImageBitmap | null = null;
91
+ _chromeCacheDirty = true;
92
+
93
+ /**
94
+ * See `TreemapChart._chromeCacheGen` — same race, same fix.
95
+ */
96
+ _chromeCacheGen = 0;
97
+
98
+ // Faceted state
99
+ _facetGrid: import("../../layout/facet-grid").FacetGrid | null = null;
100
+
101
+ /**
102
+ * Per-facet drill roots — mirrors `TreemapChart._facetDrillRoots`.
103
+ */
104
+ _facetDrillRoots: Map<string, number> = new Map();
105
+
106
+ /**
107
+ * Per-facet rendering state. `index` matches the facet grid cell;
108
+ * `centerX`, `centerY`, `maxRadius` are used for layout + hit test;
109
+ * `drillRoot` is the sub-root the facet is currently showing;
110
+ * `instanceStart`, `instanceCount` index into the shared GPU
111
+ * instance buffer for draw dispatch (these are post-skip values,
112
+ * rewritten by `uploadArcInstances` after zero-width arcs and the
113
+ * drill root are filtered out). `nodeStart`, `nodeCount` are the
114
+ * pre-skip range over `_visibleNodeIds` and are *not* rewritten —
115
+ * canvas chrome (arc-label translate origin) walks this range so
116
+ * each label can be placed around its own facet's center instead
117
+ * of the chart-wide `_centerX/_centerY` (which always point at the
118
+ * first facet).
119
+ */
120
+ _facets: {
121
+ label: string;
122
+ centerX: number;
123
+ centerY: number;
124
+ maxRadius: number;
125
+ drillRoot: number;
126
+ instanceStart: number;
127
+ instanceCount: number;
128
+ nodeStart: number;
129
+ nodeCount: number;
130
+ }[] = [];
131
+
132
+ protected override tooltipCallbacks() {
133
+ return {
134
+ onHover: (mx: number, my: number) =>
135
+ handleSunburstHover(this, mx, my),
136
+ onLeave: () => {
137
+ if (
138
+ this._hoveredNodeId !== NULL_NODE &&
139
+ this._pinnedNodeId === NULL_NODE
140
+ ) {
141
+ this._hoveredNodeId = NULL_NODE;
142
+ renderSunburstChromeOverlay(this);
143
+ }
144
+ },
145
+ onClickPre: (mx: number, my: number) => {
146
+ handleSunburstClick(this, mx, my);
147
+ return true;
148
+ },
149
+ };
150
+ }
151
+
152
+ async uploadAndRender(
153
+ glManager: WebGLContextManager,
154
+ columns: ColumnDataMap,
155
+ startRow: number,
156
+ _endRow: number,
157
+ ): Promise<void> {
158
+ this._glManager = glManager;
159
+
160
+ if (startRow === 0) {
161
+ const slots = this._columnSlots;
162
+ this._sizeName = slots[0] || firstNonMetadataColumn(columns) || "";
163
+ this._colorName = slots[1] || "";
164
+ if (!this._colorName) {
165
+ this._colorMode = "empty";
166
+ } else {
167
+ const t = this._columnTypes[this._colorName];
168
+ const isNumeric =
169
+ t === "float" ||
170
+ t === "integer" ||
171
+ t === "date" ||
172
+ t === "datetime";
173
+ this._colorMode = isNumeric ? "numeric" : "series";
174
+ }
175
+
176
+ // Clear per-draw state tied to the old tree — see
177
+ // `TreemapChart.uploadAndRender` for the same pattern and
178
+ // rationale.
179
+ this._hoveredNodeId = NULL_NODE;
180
+ this._pinnedNodeId = NULL_NODE;
181
+ this._breadcrumbRegions = [];
182
+ this._facetDrillRoots.clear();
183
+ this._facetGrid = null;
184
+ this._facets = [];
185
+
186
+ // Invalidate the instance buffer so a render that fires
187
+ // before the fresh upload draws zero arcs.
188
+ this._instanceCount = 0;
189
+
190
+ // Drop any in-flight hover tooltip promise (see treemap).
191
+ this._lazyTooltip.clearHover();
192
+ this._lazyTooltip.invalidatePin();
193
+ dismissSunburstPinnedTooltip(this);
194
+ this._chromeCache?.close();
195
+ this._chromeCache = null;
196
+ this._chromeCacheDirty = true;
197
+ this._chromeCacheGen++;
198
+
199
+ resetTreeState(this);
200
+ }
201
+
202
+ processTreeChunk(this, columns);
203
+ finalizeTree(this);
204
+ if (this._rootId !== NULL_NODE) {
205
+ await this.requestRender(glManager);
206
+ }
207
+ }
208
+
209
+ _fullRender(glManager: WebGLContextManager): void {
210
+ if (this._rootId === NULL_NODE) {
211
+ return;
212
+ }
213
+
214
+ this._glManager = glManager;
215
+ renderSunburstFrame(this, glManager);
216
+ }
217
+
218
+ protected destroyInternal(): void {
219
+ dismissSunburstPinnedTooltip(this);
220
+ this._chromeCache?.close();
221
+ this._chromeCache = null;
222
+ const gl = this._glManager?.gl;
223
+ if (gl) {
224
+ if (this._stripBuffer) {
225
+ gl.deleteBuffer(this._stripBuffer);
226
+ }
227
+
228
+ if (this._instanceBuffer) {
229
+ gl.deleteBuffer(this._instanceBuffer);
230
+ }
231
+ }
232
+
233
+ this._stripBuffer = null;
234
+ this._instanceBuffer = null;
235
+ this._program = null;
236
+ this._locations = null;
237
+ this._rootId = NULL_NODE;
238
+ this._currentRootId = NULL_NODE;
239
+ this._breadcrumbIds = [];
240
+ this._childLookup.clear();
241
+ this._visibleNodeIds = null;
242
+ this._visibleNodeCount = 0;
243
+ this._breadcrumbRegions = [];
244
+ this._facetGrid = null;
245
+ this._facetDrillRoots.clear();
246
+ this._facets = [];
247
+ }
248
+ }
@@ -0,0 +1,445 @@
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 { TreemapChart } from "./treemap";
14
+ import { NULL_NODE, ancestorNames } from "../common/node-store";
15
+ import { PADDING_LABEL, rebuildBreadcrumbs } from "./treemap-layout";
16
+ import {
17
+ renderTreemapFrame,
18
+ renderTreemapChromeOverlay,
19
+ } from "./treemap-render";
20
+
21
+ interface HitResult {
22
+ leafId: number;
23
+ branchId: number;
24
+ inHeader: boolean;
25
+ }
26
+
27
+ /**
28
+ * Find the smallest leaf AND deepest branch at `(mx, my)`. Walks the
29
+ * (already LOD-filtered) `_visibleNodeIds` — at 2M total nodes this is
30
+ * still a small linear scan because LOD keeps visible count bounded.
31
+ *
32
+ * In faceted mode `chart._visibleRootIds[i]` names the drill root that
33
+ * owns node `i`, so the "skip the root itself" check works regardless
34
+ * of which facet the node belongs to.
35
+ */
36
+ function hitTest(chart: TreemapChart, mx: number, my: number): HitResult {
37
+ const store = chart._nodeStore;
38
+ const x0 = store.x0;
39
+ const y0 = store.y0;
40
+ const x1 = store.x1;
41
+ const y1 = store.y1;
42
+ const depth = store.depth;
43
+ const firstChild = store.firstChild;
44
+ const ids = chart._visibleNodeIds;
45
+ const n = chart._visibleNodeCount;
46
+ const baseArr = chart._visibleBaseDepths;
47
+ const rootArr = chart._visibleRootIds;
48
+
49
+ let bestLeafId = NULL_NODE;
50
+ let bestLeafArea = Infinity;
51
+ let bestBranchId = NULL_NODE;
52
+ let bestBranchArea = Infinity;
53
+ let labelBranchId = NULL_NODE;
54
+
55
+ if (!ids) {
56
+ return { leafId: NULL_NODE, branchId: NULL_NODE, inHeader: false };
57
+ }
58
+
59
+ for (let i = 0; i < n; i++) {
60
+ const id = ids[i];
61
+ const rootId = rootArr ? rootArr[i] : chart._currentRootId;
62
+ if (id === rootId) {
63
+ continue;
64
+ }
65
+
66
+ if (!(mx >= x0[id] && mx <= x1[id] && my >= y0[id] && my <= y1[id])) {
67
+ continue;
68
+ }
69
+
70
+ const area = (x1[id] - x0[id]) * (y1[id] - y0[id]);
71
+ if (firstChild[id] !== NULL_NODE) {
72
+ if (area < bestBranchArea) {
73
+ bestBranchArea = area;
74
+ bestBranchId = id;
75
+ }
76
+
77
+ const baseDepth = baseArr
78
+ ? baseArr[i]
79
+ : depth[chart._currentRootId];
80
+ const relDepth = depth[id] - baseDepth;
81
+ if (relDepth === 1 && my <= y0[id] + PADDING_LABEL) {
82
+ labelBranchId = id;
83
+ }
84
+
85
+ if (relDepth === 2) {
86
+ const nw = x1[id] - x0[id];
87
+ const nh = y1[id] - y0[id];
88
+ if (nw >= 60 && nh >= 30) {
89
+ const cy = y0[id] + nh / 2;
90
+ const cx = x0[id] + nw / 2;
91
+ if (
92
+ Math.abs(my - cy) < 10 &&
93
+ Math.abs(mx - cx) < nw * 0.4
94
+ ) {
95
+ labelBranchId = id;
96
+ }
97
+ }
98
+ }
99
+ } else {
100
+ if (area < bestLeafArea) {
101
+ bestLeafArea = area;
102
+ bestLeafId = id;
103
+ }
104
+ }
105
+ }
106
+
107
+ if (labelBranchId !== NULL_NODE) {
108
+ return { leafId: NULL_NODE, branchId: labelBranchId, inHeader: true };
109
+ }
110
+
111
+ return {
112
+ leafId: bestLeafId,
113
+ branchId: bestBranchId,
114
+ inHeader: false,
115
+ };
116
+ }
117
+
118
+ export function handleTreemapHover(
119
+ chart: TreemapChart,
120
+ mx: number,
121
+ my: number,
122
+ ): void {
123
+ if (chart._pinnedNodeId !== NULL_NODE) {
124
+ return;
125
+ }
126
+
127
+ for (const region of chart._breadcrumbRegions) {
128
+ if (
129
+ mx >= region.x0 &&
130
+ mx <= region.x1 &&
131
+ my >= region.y0 &&
132
+ my <= region.y1
133
+ ) {
134
+ chart._tooltip.setCursor("pointer");
135
+ chart._hoveredNodeId = NULL_NODE;
136
+ renderTreemapChromeOverlay(chart);
137
+ return;
138
+ }
139
+ }
140
+
141
+ const { leafId, branchId, inHeader } = hitTest(chart, mx, my);
142
+ const best = inHeader ? branchId : leafId !== NULL_NODE ? leafId : branchId;
143
+
144
+ if (best !== chart._hoveredNodeId) {
145
+ chart._hoveredNodeId = best;
146
+ chart._tooltip.setCursor(
147
+ branchId !== NULL_NODE ? "pointer" : "default",
148
+ );
149
+ if (best !== NULL_NODE) {
150
+ // Kick off the lazy tooltip build for hover; re-render
151
+ // the chrome overlay once lines resolve. Stale results
152
+ // (mouse moved elsewhere, new view) are dropped by the
153
+ // controller's serial gate.
154
+ const serial = chart._lazyTooltip.beginHover(best);
155
+ buildTreemapTooltipLines(chart, best).then((lines) => {
156
+ if (chart._lazyTooltip.commitHover(serial, lines)) {
157
+ renderTreemapChromeOverlay(chart);
158
+ }
159
+ });
160
+ } else {
161
+ chart._lazyTooltip.clearHover();
162
+ }
163
+
164
+ renderTreemapChromeOverlay(chart);
165
+ }
166
+ }
167
+
168
+ export function handleTreemapClick(
169
+ chart: TreemapChart,
170
+ mx: number,
171
+ my: number,
172
+ ): void {
173
+ if (chart._pinnedNodeId !== NULL_NODE) {
174
+ dismissTreemapPinnedTooltip(chart);
175
+ chart.emitUnselect();
176
+ return;
177
+ }
178
+
179
+ for (const region of chart._breadcrumbRegions) {
180
+ if (
181
+ mx >= region.x0 &&
182
+ mx <= region.x1 &&
183
+ my >= region.y0 &&
184
+ my <= region.y1
185
+ ) {
186
+ if (region.nodeId !== chart._currentRootId) {
187
+ drillTo(chart, region.nodeId);
188
+ // Breadcrumb is chrome — no `perspective-click`. The
189
+ // drill-up pops one or more levels off the host's
190
+ // cached filter stack via `selected: false`.
191
+ chart.emitUnselect();
192
+ }
193
+
194
+ return;
195
+ }
196
+ }
197
+
198
+ const { leafId, branchId, inHeader } = hitTest(chart, mx, my);
199
+
200
+ if (branchId !== NULL_NODE && inHeader) {
201
+ drillTo(chart, branchId);
202
+ void emitTreemapNodeEvent(chart, branchId, "branch");
203
+ } else if (leafId !== NULL_NODE) {
204
+ showTreemapPinnedTooltip(chart, leafId);
205
+ void emitTreemapNodeEvent(chart, leafId, "leaf");
206
+ } else if (branchId !== NULL_NODE) {
207
+ drillTo(chart, branchId);
208
+ void emitTreemapNodeEvent(chart, branchId, "branch");
209
+ }
210
+ }
211
+
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
+ export function handleTreemapDblClick(
256
+ chart: TreemapChart,
257
+ mx: number,
258
+ my: number,
259
+ ): void {
260
+ const wasPinned = chart._pinnedNodeId !== NULL_NODE;
261
+ dismissTreemapPinnedTooltip(chart);
262
+ if (wasPinned) {
263
+ chart.emitUnselect();
264
+ }
265
+
266
+ const { leafId, branchId } = hitTest(chart, mx, my);
267
+ const store = chart._nodeStore;
268
+ let target = branchId;
269
+ if (target === NULL_NODE && leafId !== NULL_NODE) {
270
+ const parent = store.parent[leafId];
271
+ if (parent !== chart._currentRootId && parent !== NULL_NODE) {
272
+ target = parent;
273
+ }
274
+ }
275
+
276
+ if (
277
+ target !== NULL_NODE &&
278
+ target !== chart._currentRootId &&
279
+ store.firstChild[target] !== NULL_NODE
280
+ ) {
281
+ drillTo(chart, target);
282
+ void emitTreemapNodeEvent(chart, target, "branch");
283
+ if (leafId !== NULL_NODE && store.firstChild[leafId] === NULL_NODE) {
284
+ showTreemapPinnedTooltip(chart, leafId);
285
+ void emitTreemapNodeEvent(chart, leafId, "leaf");
286
+ }
287
+ }
288
+ }
289
+
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
+ 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;
316
+ if (chart._glManager) {
317
+ renderTreemapFrame(chart, chart._glManager);
318
+ }
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
+ }
329
+ }
330
+
331
+ export function showTreemapPinnedTooltip(
332
+ chart: TreemapChart,
333
+ nodeId: number,
334
+ ): void {
335
+ chart._tooltip.dismiss();
336
+ chart._pinnedNodeId = nodeId;
337
+
338
+ const store = chart._nodeStore;
339
+ const cx = (store.x0[nodeId] + store.x1[nodeId]) / 2;
340
+ 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);
365
+ }
366
+
367
+ export function dismissTreemapPinnedTooltip(chart: TreemapChart): void {
368
+ chart._tooltip.dismiss();
369
+ chart._pinnedNodeId = NULL_NODE;
370
+ }
371
+
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
+ }