@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,716 @@
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 { Context2D } from "../charts/canvas-types";
14
+ import { PlotLayout } from "../layout/plot-layout";
15
+ import {
16
+ labelRect,
17
+ rectContained,
18
+ rectsOverlap,
19
+ rotatedLabelsOverlap,
20
+ truncateLabel,
21
+ } from "./label-geometry";
22
+ import { type GroupRun, runsInRange } from "./categorical-axis-core";
23
+ import type { Theme } from "../theme/theme";
24
+
25
+ export interface CategoricalLevel {
26
+ labels: string[];
27
+ runs: GroupRun[];
28
+ maxLabelChars: number;
29
+ }
30
+
31
+ export interface CategoricalDomain {
32
+ levels: CategoricalLevel[];
33
+ numRows: number;
34
+ levelLabels: string[];
35
+ }
36
+
37
+ interface LevelTickLayout {
38
+ size: number;
39
+ rotation: 0 | 45 | 90;
40
+ }
41
+
42
+ const LEAF_LEVEL_HEIGHT = 25;
43
+ const OUTER_LEVEL_HEIGHT = 22;
44
+ const TICK_SIZE = 5;
45
+ const LABEL_FONT_PX = 11;
46
+ const LABEL_LINE_HEIGHT = 14;
47
+ const LEAF_LEVEL_WIDTH_MIN = 55;
48
+ const OUTER_LEVEL_WIDTH = 60;
49
+ const LEAF_LABEL_PADDING = 10;
50
+
51
+ function categoryIndexToPixelX(layout: PlotLayout, index: number): number {
52
+ return layout.dataToPixel(index, 0).px;
53
+ }
54
+
55
+ function categoryIndexToPixelY(layout: PlotLayout, index: number): number {
56
+ return layout.dataToPixel(0, index).py;
57
+ }
58
+
59
+ export const categoryIndexToPixel = categoryIndexToPixelX;
60
+
61
+ function leafLevelLayout(
62
+ numRows: number,
63
+ longestCharCount: number,
64
+ plotWidth: number,
65
+ ): LevelTickLayout {
66
+ const budget = Math.max(0, plotWidth - 100);
67
+ if (numRows * 16 > budget) {
68
+ return { size: longestCharCount * 6.62 + 10, rotation: 90 };
69
+ }
70
+
71
+ if (numRows * (longestCharCount * 6 + 10) > budget) {
72
+ return { size: longestCharCount * 4 + 20, rotation: 45 };
73
+ }
74
+
75
+ return { size: LEAF_LEVEL_HEIGHT, rotation: 0 };
76
+ }
77
+
78
+ export function measureCategoricalLevels(
79
+ domain: CategoricalDomain,
80
+ plotWidth: number,
81
+ ): LevelTickLayout[] {
82
+ const L = domain.levels.length;
83
+ const result: LevelTickLayout[] = [];
84
+ for (let l = 0; l < L; l++) {
85
+ const lev = domain.levels[l];
86
+ if (l === L - 1) {
87
+ result.push(
88
+ leafLevelLayout(domain.numRows, lev.maxLabelChars, plotWidth),
89
+ );
90
+ } else {
91
+ result.push({ size: OUTER_LEVEL_HEIGHT, rotation: 0 });
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ export function measureCategoricalLevelWidths(
99
+ domain: CategoricalDomain,
100
+ ): number[] {
101
+ const L = domain.levels.length;
102
+ const widths: number[] = [];
103
+ const charPx = 6.2;
104
+ for (let l = 0; l < L; l++) {
105
+ if (l === L - 1) {
106
+ const longest = domain.levels[l].maxLabelChars;
107
+ widths.push(
108
+ Math.max(
109
+ LEAF_LEVEL_WIDTH_MIN,
110
+ longest * charPx + LEAF_LABEL_PADDING,
111
+ ),
112
+ );
113
+ } else {
114
+ widths.push(OUTER_LEVEL_WIDTH);
115
+ }
116
+ }
117
+
118
+ return widths;
119
+ }
120
+
121
+ function sumNumeric(arr: number[]): number {
122
+ let t = 0;
123
+ for (const v of arr) {
124
+ t += v;
125
+ }
126
+
127
+ return t;
128
+ }
129
+
130
+ export function measureCategoricalAxisHeight(
131
+ domain: CategoricalDomain,
132
+ plotWidth: number,
133
+ ): number {
134
+ if (domain.numRows === 0 || domain.levels.length === 0) {
135
+ return 24;
136
+ }
137
+
138
+ return sumNumeric(
139
+ measureCategoricalLevels(domain, plotWidth).map((l) => l.size),
140
+ );
141
+ }
142
+
143
+ export function measureCategoricalAxisWidth(domain: CategoricalDomain): number {
144
+ if (domain.numRows === 0 || domain.levels.length === 0) {
145
+ return 55;
146
+ }
147
+
148
+ return sumNumeric(measureCategoricalLevelWidths(domain));
149
+ }
150
+
151
+ function selectLeafTickIndices(
152
+ visMin: number,
153
+ visMax: number,
154
+ plotWidth: number,
155
+ avgLabelPx: number,
156
+ ): number[] {
157
+ const count = visMax - visMin + 1;
158
+ if (count <= 0) {
159
+ return [];
160
+ }
161
+
162
+ const maxLabels = Math.max(1, Math.floor(plotWidth / avgLabelPx));
163
+ if (count <= maxLabels) {
164
+ const out: number[] = [];
165
+ for (let i = visMin; i <= visMax; i++) {
166
+ out.push(i);
167
+ }
168
+
169
+ return out;
170
+ }
171
+
172
+ const step = Math.ceil(count / maxLabels);
173
+ const out: number[] = [];
174
+ for (let i = visMin; i <= visMax; i += step) {
175
+ out.push(i);
176
+ }
177
+
178
+ return out;
179
+ }
180
+
181
+ function getLeafText(level: CategoricalLevel, row: number): string {
182
+ return level.labels[row] ?? "";
183
+ }
184
+
185
+ /**
186
+ * Visible row window from a (possibly zoomed) padded data range. `flip`
187
+ * accounts for the categorical Y-axis storing the domain inverted so
188
+ * that catIdx=0 renders at the top.
189
+ */
190
+ function visibleRowWindow(
191
+ numRows: number,
192
+ a: number,
193
+ b: number,
194
+ flip: boolean,
195
+ ): [number, number] | null {
196
+ const lo = flip ? Math.min(a, b) : a;
197
+ const hi = flip ? Math.max(a, b) : b;
198
+ const visMin = Math.max(0, Math.ceil(lo));
199
+ const visMax = Math.min(numRows - 1, Math.floor(hi));
200
+ return visMax < visMin ? null : [visMin, visMax];
201
+ }
202
+
203
+ /**
204
+ * Compute clipped main-axis spans for each visible run. Used by both
205
+ * outer-level renderers; the caller supplies the projection from
206
+ * row-index to the relevant pixel coordinate (X or Y) and the plot
207
+ * extent along the main axis.
208
+ */
209
+ function clippedRuns(
210
+ runs: GroupRun[],
211
+ pixelOf: (idx: number) => number,
212
+ mainStart: number,
213
+ mainEnd: number,
214
+ ): Array<{
215
+ run: GroupRun;
216
+ nearEdge: number;
217
+ farEdge: number;
218
+ nearClip: number;
219
+ farClip: number;
220
+ }> {
221
+ const out = [];
222
+ for (const run of runs) {
223
+ const nearEdge = pixelOf(run.startIdx - 0.5);
224
+ const farEdge = pixelOf(run.endIdx + 0.5);
225
+ const nearClip = Math.max(mainStart, Math.min(nearEdge, farEdge));
226
+ const farClip = Math.min(mainEnd, Math.max(nearEdge, farEdge));
227
+ if (farClip > nearClip) {
228
+ out.push({ run, nearEdge, farEdge, nearClip, farClip });
229
+ }
230
+ }
231
+
232
+ return out;
233
+ }
234
+
235
+ export function renderCategoricalXTicks(
236
+ ctx: Context2D,
237
+ layout: PlotLayout,
238
+ domain: CategoricalDomain,
239
+ theme: Theme,
240
+ ): void {
241
+ if (domain.numRows === 0 || domain.levels.length === 0) {
242
+ return;
243
+ }
244
+
245
+ const { tickColor, labelColor, fontFamily } = theme;
246
+ const { plotRect: plot } = layout;
247
+ const baselineY = plot.y + plot.height;
248
+
249
+ ctx.strokeStyle = tickColor;
250
+ ctx.fillStyle = tickColor;
251
+ ctx.lineWidth = 1;
252
+ ctx.font = `${LABEL_FONT_PX}px ${fontFamily}`;
253
+
254
+ const levelLayouts = measureCategoricalLevels(domain, plot.width);
255
+ const win = visibleRowWindow(
256
+ domain.numRows,
257
+ layout.paddedXMin,
258
+ layout.paddedXMax,
259
+ false,
260
+ );
261
+ if (!win) {
262
+ return;
263
+ }
264
+
265
+ const [visMin, visMax] = win;
266
+
267
+ const L = domain.levels.length;
268
+ let yCursor = baselineY;
269
+ for (let l = L - 1; l >= 0; l--) {
270
+ const level = domain.levels[l];
271
+ const lay = levelLayouts[l];
272
+ const rowTop = yCursor;
273
+ yCursor += lay.size;
274
+
275
+ if (l === L - 1) {
276
+ renderLeafLevel(
277
+ ctx,
278
+ layout,
279
+ level,
280
+ visMin,
281
+ visMax,
282
+ rowTop,
283
+ lay,
284
+ fontFamily,
285
+ tickColor,
286
+ );
287
+ } else {
288
+ renderOuterLevel(
289
+ ctx,
290
+ layout,
291
+ level,
292
+ visMin,
293
+ visMax,
294
+ rowTop,
295
+ fontFamily,
296
+ tickColor,
297
+ );
298
+ }
299
+ }
300
+
301
+ const axisLabel = domain.levelLabels.filter((s) => !!s).join(" / ");
302
+ if (axisLabel) {
303
+ ctx.fillStyle = labelColor;
304
+ ctx.font = `13px ${fontFamily}`;
305
+ ctx.textAlign = "center";
306
+ ctx.textBaseline = "bottom";
307
+ ctx.fillText(axisLabel, plot.x + plot.width / 2, layout.cssHeight - 2);
308
+ }
309
+ }
310
+
311
+ function renderLeafLevel(
312
+ ctx: Context2D,
313
+ layout: PlotLayout,
314
+ level: CategoricalLevel,
315
+ visMin: number,
316
+ visMax: number,
317
+ rowTop: number,
318
+ lay: LevelTickLayout,
319
+ fontFamily: string,
320
+ tickColor: string,
321
+ ): void {
322
+ const { plotRect: plot } = layout;
323
+
324
+ const avgCharWidth = 6.2;
325
+ const avgLabelPx = Math.max(
326
+ 40,
327
+ Math.min(level.maxLabelChars * avgCharWidth + 8, plot.width / 2),
328
+ );
329
+
330
+ const tickRows =
331
+ lay.rotation === 0
332
+ ? selectLeafTickIndices(visMin, visMax, plot.width, avgLabelPx)
333
+ : leafRowsForRotated(visMin, visMax);
334
+
335
+ ctx.strokeStyle = tickColor;
336
+ ctx.fillStyle = tickColor;
337
+ ctx.beginPath();
338
+ for (const r of tickRows) {
339
+ const px = categoryIndexToPixelX(layout, r);
340
+ if (px < plot.x - 1 || px > plot.x + plot.width + 1) {
341
+ continue;
342
+ }
343
+
344
+ ctx.moveTo(px, rowTop);
345
+ ctx.lineTo(px, rowTop + TICK_SIZE);
346
+ }
347
+
348
+ ctx.stroke();
349
+
350
+ ctx.font = `${LABEL_FONT_PX}px ${fontFamily}`;
351
+ const labelY = rowTop + TICK_SIZE + 3;
352
+ const boundsRect = {
353
+ x: plot.x,
354
+ width: plot.width,
355
+ y: rowTop,
356
+ height: 9999,
357
+ };
358
+ const kept: {
359
+ x: number;
360
+ y: number;
361
+ width: number;
362
+ height: number;
363
+ }[] = [];
364
+ for (const r of tickRows) {
365
+ const px = categoryIndexToPixelX(layout, r);
366
+ if (px < plot.x - 1 || px > plot.x + plot.width + 1) {
367
+ continue;
368
+ }
369
+
370
+ const text = getLeafText(level, r);
371
+ if (!text) {
372
+ continue;
373
+ }
374
+
375
+ const textWidth = ctx.measureText(text).width;
376
+ const rect = labelRect(
377
+ px,
378
+ labelY,
379
+ textWidth,
380
+ LABEL_LINE_HEIGHT,
381
+ lay.rotation,
382
+ );
383
+ if (!rectContained(rect, boundsRect)) {
384
+ continue;
385
+ }
386
+
387
+ if (lay.rotation === 0) {
388
+ if (kept.some((r) => rectsOverlap(r, rect))) {
389
+ continue;
390
+ }
391
+ } else {
392
+ if (kept.some((r) => rotatedLabelsOverlap(r, rect))) {
393
+ continue;
394
+ }
395
+ }
396
+
397
+ kept.push(rect);
398
+
399
+ drawLabel(ctx, text, px, labelY, lay.rotation, "center");
400
+ }
401
+ }
402
+
403
+ function renderOuterLevel(
404
+ ctx: Context2D,
405
+ layout: PlotLayout,
406
+ level: CategoricalLevel,
407
+ visMin: number,
408
+ visMax: number,
409
+ rowTop: number,
410
+ fontFamily: string,
411
+ tickColor: string,
412
+ ): void {
413
+ const { plotRect: plot } = layout;
414
+ const runs = runsInRange(level.runs, visMin, visMax);
415
+ if (runs.length === 0) {
416
+ return;
417
+ }
418
+
419
+ const clipped = clippedRuns(
420
+ runs,
421
+ (idx) => categoryIndexToPixelX(layout, idx),
422
+ plot.x,
423
+ plot.x + plot.width,
424
+ );
425
+ if (clipped.length === 0) {
426
+ return;
427
+ }
428
+
429
+ ctx.strokeStyle = tickColor;
430
+ ctx.fillStyle = tickColor;
431
+
432
+ ctx.beginPath();
433
+ for (const c of clipped) {
434
+ ctx.moveTo(c.nearClip, rowTop + 3);
435
+ ctx.lineTo(c.farClip, rowTop + 3);
436
+ ctx.moveTo(c.nearEdge, rowTop);
437
+ ctx.lineTo(c.nearEdge, rowTop + 3);
438
+ ctx.moveTo(c.farEdge, rowTop);
439
+ ctx.lineTo(c.farEdge, rowTop + 3);
440
+ }
441
+
442
+ ctx.stroke();
443
+
444
+ ctx.font = `${LABEL_FONT_PX}px ${fontFamily}`;
445
+ const labelY = rowTop + 3 + 4;
446
+ const kept: {
447
+ x: number;
448
+ y: number;
449
+ width: number;
450
+ height: number;
451
+ }[] = [];
452
+ const boundsRect = {
453
+ x: plot.x,
454
+ width: plot.width,
455
+ y: rowTop,
456
+ height: 9999,
457
+ };
458
+
459
+ for (const c of clipped) {
460
+ const cx = (c.nearClip + c.farClip) / 2;
461
+
462
+ const text = c.run.label;
463
+ if (!text) {
464
+ continue;
465
+ }
466
+
467
+ const available = c.farClip - c.nearClip - 4;
468
+ const display = truncateLabel(ctx, text, available);
469
+ if (!display) {
470
+ continue;
471
+ }
472
+
473
+ const textWidth = ctx.measureText(display).width;
474
+ const rect = labelRect(cx, labelY, textWidth, LABEL_LINE_HEIGHT, 0);
475
+ if (!rectContained(rect, boundsRect)) {
476
+ continue;
477
+ }
478
+
479
+ if (kept.some((r) => rectsOverlap(r, rect))) {
480
+ continue;
481
+ }
482
+
483
+ kept.push(rect);
484
+
485
+ drawLabel(ctx, display, cx, labelY, 0, "center");
486
+ }
487
+ }
488
+
489
+ function leafRowsForRotated(visMin: number, visMax: number): number[] {
490
+ const out: number[] = [];
491
+ for (let i = visMin; i <= visMax; i++) {
492
+ out.push(i);
493
+ }
494
+
495
+ return out;
496
+ }
497
+
498
+ export function renderCategoricalYTicks(
499
+ ctx: Context2D,
500
+ layout: PlotLayout,
501
+ domain: CategoricalDomain,
502
+ theme: Theme,
503
+ ): void {
504
+ if (domain.numRows === 0 || domain.levels.length === 0) {
505
+ return;
506
+ }
507
+
508
+ const { tickColor, labelColor, fontFamily } = theme;
509
+ const { plotRect: plot } = layout;
510
+ const axisX = plot.x;
511
+
512
+ ctx.strokeStyle = tickColor;
513
+ ctx.fillStyle = tickColor;
514
+ ctx.lineWidth = 1;
515
+ ctx.font = `${LABEL_FONT_PX}px ${fontFamily}`;
516
+
517
+ const widths = measureCategoricalLevelWidths(domain);
518
+ const win = visibleRowWindow(
519
+ domain.numRows,
520
+ layout.paddedYMin,
521
+ layout.paddedYMax,
522
+ true,
523
+ );
524
+ if (!win) {
525
+ return;
526
+ }
527
+
528
+ const [visMin, visMax] = win;
529
+
530
+ const L = domain.levels.length;
531
+ let xCursor = axisX;
532
+ for (let l = L - 1; l >= 0; l--) {
533
+ const level = domain.levels[l];
534
+ const w = widths[l];
535
+ const colRight = xCursor;
536
+ xCursor -= w;
537
+
538
+ if (l === L - 1) {
539
+ renderLeafLevelY(
540
+ ctx,
541
+ layout,
542
+ level,
543
+ visMin,
544
+ visMax,
545
+ colRight,
546
+ fontFamily,
547
+ tickColor,
548
+ );
549
+ } else {
550
+ renderOuterLevelY(
551
+ ctx,
552
+ layout,
553
+ level,
554
+ visMin,
555
+ visMax,
556
+ colRight,
557
+ w,
558
+ fontFamily,
559
+ tickColor,
560
+ );
561
+ }
562
+ }
563
+
564
+ const axisLabel = domain.levelLabels.filter((s) => !!s).join(" / ");
565
+ if (axisLabel) {
566
+ ctx.fillStyle = labelColor;
567
+ ctx.font = `13px ${fontFamily}`;
568
+ ctx.save();
569
+ ctx.translate(14, plot.y + plot.height / 2);
570
+ ctx.rotate(-Math.PI / 2);
571
+ ctx.textAlign = "center";
572
+ ctx.textBaseline = "bottom";
573
+ ctx.fillText(axisLabel, 0, 0);
574
+ ctx.restore();
575
+ }
576
+ }
577
+
578
+ function renderLeafLevelY(
579
+ ctx: Context2D,
580
+ layout: PlotLayout,
581
+ level: CategoricalLevel,
582
+ visMin: number,
583
+ visMax: number,
584
+ colRight: number,
585
+ fontFamily: string,
586
+ tickColor: string,
587
+ ): void {
588
+ const { plotRect: plot } = layout;
589
+
590
+ const avgLabelHeight = LABEL_LINE_HEIGHT + 4;
591
+ const count = visMax - visMin + 1;
592
+ const maxLabels = Math.max(1, Math.floor(plot.height / avgLabelHeight));
593
+ const step = count <= maxLabels ? 1 : Math.ceil(count / maxLabels);
594
+
595
+ ctx.strokeStyle = tickColor;
596
+ ctx.fillStyle = tickColor;
597
+ ctx.beginPath();
598
+ for (let r = visMin; r <= visMax; r += step) {
599
+ const py = categoryIndexToPixelY(layout, r);
600
+ if (py < plot.y - 1 || py > plot.y + plot.height + 1) {
601
+ continue;
602
+ }
603
+
604
+ ctx.moveTo(colRight - TICK_SIZE, py);
605
+ ctx.lineTo(colRight, py);
606
+ }
607
+
608
+ ctx.stroke();
609
+
610
+ ctx.font = `${LABEL_FONT_PX}px ${fontFamily}`;
611
+ ctx.textAlign = "right";
612
+ ctx.textBaseline = "middle";
613
+ for (let r = visMin; r <= visMax; r += step) {
614
+ const py = categoryIndexToPixelY(layout, r);
615
+ if (py < plot.y - 1 || py > plot.y + plot.height + 1) {
616
+ continue;
617
+ }
618
+
619
+ const text = getLeafText(level, r);
620
+ if (!text) {
621
+ continue;
622
+ }
623
+
624
+ ctx.fillText(text, colRight - TICK_SIZE - 3, py);
625
+ }
626
+ }
627
+
628
+ function renderOuterLevelY(
629
+ ctx: Context2D,
630
+ layout: PlotLayout,
631
+ level: CategoricalLevel,
632
+ visMin: number,
633
+ visMax: number,
634
+ colRight: number,
635
+ colWidth: number,
636
+ fontFamily: string,
637
+ tickColor: string,
638
+ ): void {
639
+ const { plotRect: plot } = layout;
640
+ const runs = runsInRange(level.runs, visMin, visMax);
641
+ if (runs.length === 0) {
642
+ return;
643
+ }
644
+
645
+ const clipped = clippedRuns(
646
+ runs,
647
+ (idx) => categoryIndexToPixelY(layout, idx),
648
+ plot.y,
649
+ plot.y + plot.height,
650
+ );
651
+ if (clipped.length === 0) {
652
+ return;
653
+ }
654
+
655
+ ctx.strokeStyle = tickColor;
656
+ ctx.fillStyle = tickColor;
657
+
658
+ const bracketX = colRight - 3;
659
+ ctx.beginPath();
660
+ for (const c of clipped) {
661
+ ctx.moveTo(bracketX, c.nearClip);
662
+ ctx.lineTo(bracketX, c.farClip);
663
+ ctx.moveTo(bracketX, c.nearEdge);
664
+ ctx.lineTo(colRight, c.nearEdge);
665
+ ctx.moveTo(bracketX, c.farEdge);
666
+ ctx.lineTo(colRight, c.farEdge);
667
+ }
668
+
669
+ ctx.stroke();
670
+
671
+ ctx.font = `${LABEL_FONT_PX}px ${fontFamily}`;
672
+ ctx.textAlign = "right";
673
+ ctx.textBaseline = "middle";
674
+
675
+ for (const c of clipped) {
676
+ const cy = (c.nearClip + c.farClip) / 2;
677
+
678
+ const text = c.run.label;
679
+ if (!text) {
680
+ continue;
681
+ }
682
+
683
+ const available = colWidth - 6;
684
+ const display = truncateLabel(ctx, text, available);
685
+ if (!display) {
686
+ continue;
687
+ }
688
+
689
+ ctx.fillText(display, bracketX - 3, cy);
690
+ }
691
+ }
692
+
693
+ function drawLabel(
694
+ ctx: Context2D,
695
+ text: string,
696
+ px: number,
697
+ py: number,
698
+ rotation: 0 | 45 | 90,
699
+ anchor: "center" | "end",
700
+ ): void {
701
+ if (rotation === 0) {
702
+ ctx.textAlign = anchor === "end" ? "right" : "center";
703
+ ctx.textBaseline = "top";
704
+ ctx.fillText(text, px, py);
705
+ return;
706
+ }
707
+
708
+ ctx.save();
709
+ ctx.translate(px, py);
710
+ ctx.rotate((-rotation * Math.PI) / 180);
711
+ ctx.textAlign = "right";
712
+ ctx.textBaseline = "middle";
713
+
714
+ ctx.fillText(text, -2, 0);
715
+ ctx.restore();
716
+ }