stream-charts 1.2.0 → 2.0.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 (463) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +809 -1022
  3. package/changes.md +40 -0
  4. package/dist/app/charts/Chart.d.ts +35 -139
  5. package/dist/app/charts/Chart.d.ts.map +1 -0
  6. package/dist/app/charts/Chart.js +74 -65
  7. package/dist/app/charts/Chart.js.map +1 -1
  8. package/dist/app/charts/axes/AxesState.d.ts +26 -18
  9. package/dist/app/charts/axes/AxesState.d.ts.map +1 -0
  10. package/dist/app/charts/axes/AxesState.js +26 -24
  11. package/dist/app/charts/axes/AxesState.js.map +1 -1
  12. package/dist/app/charts/axes/AxisInterval.d.ts +44 -26
  13. package/dist/app/charts/axes/AxisInterval.d.ts.map +1 -0
  14. package/dist/app/charts/axes/AxisInterval.js +48 -29
  15. package/dist/app/charts/axes/AxisInterval.js.map +1 -1
  16. package/dist/app/charts/axes/BaseAxisRange.d.ts +46 -22
  17. package/dist/app/charts/axes/BaseAxisRange.d.ts.map +1 -0
  18. package/dist/app/charts/axes/BaseAxisRange.js +17 -12
  19. package/dist/app/charts/axes/BaseAxisRange.js.map +1 -1
  20. package/dist/app/charts/axes/ContinuousAxis.d.ts +47 -6
  21. package/dist/app/charts/axes/ContinuousAxis.d.ts.map +1 -0
  22. package/dist/app/charts/axes/ContinuousAxis.js +160 -27
  23. package/dist/app/charts/axes/ContinuousAxis.js.map +1 -1
  24. package/dist/app/charts/axes/ContinuousAxisRange.d.ts +29 -10
  25. package/dist/app/charts/axes/ContinuousAxisRange.d.ts.map +1 -0
  26. package/dist/app/charts/axes/ContinuousAxisRange.js +29 -11
  27. package/dist/app/charts/axes/ContinuousAxisRange.js.map +1 -1
  28. package/dist/app/charts/axes/EmptyAxis.d.ts +5 -2
  29. package/dist/app/charts/axes/EmptyAxis.d.ts.map +1 -0
  30. package/dist/app/charts/axes/EmptyAxis.js +30 -9
  31. package/dist/app/charts/axes/EmptyAxis.js.map +1 -1
  32. package/dist/app/charts/axes/OrdinalAxis.d.ts +5 -3
  33. package/dist/app/charts/axes/OrdinalAxis.d.ts.map +1 -0
  34. package/dist/app/charts/axes/OrdinalAxis.js +39 -16
  35. package/dist/app/charts/axes/OrdinalAxis.js.map +1 -1
  36. package/dist/app/charts/axes/OrdinalAxisRange.d.ts +76 -13
  37. package/dist/app/charts/axes/OrdinalAxisRange.d.ts.map +1 -0
  38. package/dist/app/charts/axes/OrdinalAxisRange.js +95 -16
  39. package/dist/app/charts/axes/OrdinalAxisRange.js.map +1 -1
  40. package/dist/app/charts/axes/autoScale.d.ts +111 -0
  41. package/dist/app/charts/axes/autoScale.d.ts.map +1 -0
  42. package/dist/app/charts/axes/autoScale.js +188 -0
  43. package/dist/app/charts/axes/autoScale.js.map +1 -0
  44. package/dist/app/charts/axes/axes.d.ts +273 -150
  45. package/dist/app/charts/axes/axes.d.ts.map +1 -0
  46. package/dist/app/charts/axes/axes.js +808 -583
  47. package/dist/app/charts/axes/axes.js.map +1 -1
  48. package/dist/app/charts/d3types.d.ts +40 -11
  49. package/dist/app/charts/d3types.d.ts.map +1 -0
  50. package/dist/app/charts/d3types.js +3 -0
  51. package/dist/app/charts/d3types.js.map +1 -1
  52. package/dist/app/charts/datasources/StreamingDataSource.d.ts +149 -0
  53. package/dist/app/charts/datasources/StreamingDataSource.d.ts.map +1 -0
  54. package/dist/app/charts/datasources/StreamingDataSource.js +110 -0
  55. package/dist/app/charts/datasources/StreamingDataSource.js.map +1 -0
  56. package/dist/app/charts/datasources/iteratesDataSource.d.ts +73 -0
  57. package/dist/app/charts/datasources/iteratesDataSource.d.ts.map +1 -0
  58. package/dist/app/charts/datasources/iteratesDataSource.js +87 -0
  59. package/dist/app/charts/datasources/iteratesDataSource.js.map +1 -0
  60. package/dist/app/charts/datasources/ordinalDataSource.d.ts +75 -0
  61. package/dist/app/charts/datasources/ordinalDataSource.d.ts.map +1 -0
  62. package/dist/app/charts/datasources/ordinalDataSource.js +135 -0
  63. package/dist/app/charts/datasources/ordinalDataSource.js.map +1 -0
  64. package/dist/app/charts/datasources/outlierDataSource.d.ts +53 -0
  65. package/dist/app/charts/datasources/outlierDataSource.d.ts.map +1 -0
  66. package/dist/app/charts/datasources/outlierDataSource.js +57 -0
  67. package/dist/app/charts/datasources/outlierDataSource.js.map +1 -0
  68. package/dist/app/charts/datasources/timeSeriesDataSource.d.ts +77 -0
  69. package/dist/app/charts/datasources/timeSeriesDataSource.d.ts.map +1 -0
  70. package/dist/app/charts/datasources/timeSeriesDataSource.js +85 -0
  71. package/dist/app/charts/datasources/timeSeriesDataSource.js.map +1 -0
  72. package/dist/app/charts/filters/regexFilter.d.ts +31 -4
  73. package/dist/app/charts/filters/regexFilter.d.ts.map +1 -0
  74. package/dist/app/charts/filters/regexFilter.js +38 -4
  75. package/dist/app/charts/filters/regexFilter.js.map +1 -1
  76. package/dist/app/charts/hooks/AxesProvider.d.ts +21 -0
  77. package/dist/app/charts/hooks/AxesProvider.d.ts.map +1 -0
  78. package/dist/app/charts/hooks/AxesProvider.js +213 -0
  79. package/dist/app/charts/hooks/AxesProvider.js.map +1 -0
  80. package/dist/app/charts/hooks/CanvasSurfaceProvider.d.ts +25 -0
  81. package/dist/app/charts/hooks/CanvasSurfaceProvider.d.ts.map +1 -0
  82. package/dist/app/charts/hooks/CanvasSurfaceProvider.js +76 -0
  83. package/dist/app/charts/hooks/CanvasSurfaceProvider.js.map +1 -0
  84. package/dist/app/charts/hooks/ChartProvider.d.ts +40 -0
  85. package/dist/app/charts/hooks/ChartProvider.d.ts.map +1 -0
  86. package/dist/app/charts/hooks/ChartProvider.js +33 -0
  87. package/dist/app/charts/hooks/ChartProvider.js.map +1 -0
  88. package/dist/app/charts/hooks/InteractionProvider.d.ts +23 -0
  89. package/dist/app/charts/hooks/InteractionProvider.d.ts.map +1 -0
  90. package/dist/app/charts/hooks/InteractionProvider.js +60 -0
  91. package/dist/app/charts/hooks/InteractionProvider.js.map +1 -0
  92. package/dist/app/charts/hooks/PlotDimensionsProvider.d.ts +16 -0
  93. package/dist/app/charts/hooks/PlotDimensionsProvider.d.ts.map +1 -0
  94. package/dist/app/charts/hooks/PlotDimensionsProvider.js +76 -0
  95. package/dist/app/charts/hooks/PlotDimensionsProvider.js.map +1 -0
  96. package/dist/app/charts/hooks/defaultAxesValues.d.ts +11 -0
  97. package/dist/app/charts/hooks/defaultAxesValues.d.ts.map +1 -0
  98. package/dist/app/charts/hooks/defaultAxesValues.js +36 -0
  99. package/dist/app/charts/hooks/defaultAxesValues.js.map +1 -0
  100. package/dist/app/charts/hooks/defaultMouseValues.d.ts +9 -0
  101. package/dist/app/charts/hooks/defaultMouseValues.d.ts.map +1 -0
  102. package/dist/app/charts/hooks/defaultMouseValues.js +23 -0
  103. package/dist/app/charts/hooks/defaultMouseValues.js.map +1 -0
  104. package/dist/app/charts/hooks/defaultPlotDimensions.d.ts +13 -0
  105. package/dist/app/charts/hooks/defaultPlotDimensions.d.ts.map +1 -0
  106. package/dist/app/charts/hooks/defaultPlotDimensions.js +23 -0
  107. package/dist/app/charts/hooks/defaultPlotDimensions.js.map +1 -0
  108. package/dist/app/charts/hooks/defaultTooltipValues.d.ts +9 -0
  109. package/dist/app/charts/hooks/defaultTooltipValues.d.ts.map +1 -0
  110. package/dist/app/charts/hooks/defaultTooltipValues.js +21 -0
  111. package/dist/app/charts/hooks/defaultTooltipValues.js.map +1 -0
  112. package/dist/app/charts/hooks/defaultUseChartValues.d.ts +14 -0
  113. package/dist/app/charts/hooks/defaultUseChartValues.d.ts.map +1 -0
  114. package/dist/app/charts/hooks/defaultUseChartValues.js +30 -0
  115. package/dist/app/charts/hooks/defaultUseChartValues.js.map +1 -0
  116. package/dist/app/charts/hooks/useAxes.d.ts +49 -36
  117. package/dist/app/charts/hooks/useAxes.d.ts.map +1 -0
  118. package/dist/app/charts/hooks/useAxes.js +16 -176
  119. package/dist/app/charts/hooks/useAxes.js.map +1 -1
  120. package/dist/app/charts/hooks/useCanvasSurface.d.ts +25 -0
  121. package/dist/app/charts/hooks/useCanvasSurface.d.ts.map +1 -0
  122. package/dist/app/charts/hooks/useCanvasSurface.js +18 -0
  123. package/dist/app/charts/hooks/useCanvasSurface.js.map +1 -0
  124. package/dist/app/charts/hooks/useChart.d.ts +30 -44
  125. package/dist/app/charts/hooks/useChart.d.ts.map +1 -0
  126. package/dist/app/charts/hooks/useChart.js +26 -48
  127. package/dist/app/charts/hooks/useChart.js.map +1 -1
  128. package/dist/app/charts/hooks/useDataSource.d.ts +68 -0
  129. package/dist/app/charts/hooks/useDataSource.d.ts.map +1 -0
  130. package/dist/app/charts/hooks/useDataSource.js +63 -0
  131. package/dist/app/charts/hooks/useDataSource.js.map +1 -0
  132. package/dist/app/charts/hooks/useHoveredSeries.d.ts +25 -0
  133. package/dist/app/charts/hooks/useHoveredSeries.d.ts.map +1 -0
  134. package/dist/app/charts/hooks/useHoveredSeries.js +15 -0
  135. package/dist/app/charts/hooks/useHoveredSeries.js.map +1 -0
  136. package/dist/app/charts/hooks/useMouse.d.ts +46 -22
  137. package/dist/app/charts/hooks/useMouse.d.ts.map +1 -0
  138. package/dist/app/charts/hooks/useMouse.js +12 -56
  139. package/dist/app/charts/hooks/useMouse.js.map +1 -1
  140. package/dist/app/charts/hooks/usePlotDimensions.d.ts +25 -13
  141. package/dist/app/charts/hooks/usePlotDimensions.d.ts.map +1 -0
  142. package/dist/app/charts/hooks/usePlotDimensions.js +6 -62
  143. package/dist/app/charts/hooks/usePlotDimensions.js.map +1 -1
  144. package/dist/app/charts/hooks/useTooltip.d.ts +24 -26
  145. package/dist/app/charts/hooks/useTooltip.d.ts.map +1 -0
  146. package/dist/app/charts/hooks/useTooltip.js +12 -42
  147. package/dist/app/charts/hooks/useTooltip.js.map +1 -1
  148. package/dist/app/charts/index.d.ts +66 -0
  149. package/dist/app/charts/index.d.ts.map +1 -0
  150. package/dist/app/charts/index.js +87 -0
  151. package/dist/app/charts/index.js.map +1 -0
  152. package/dist/app/charts/legends/Legend.d.ts +33 -12
  153. package/dist/app/charts/legends/Legend.d.ts.map +1 -0
  154. package/dist/app/charts/legends/Legend.js +138 -379
  155. package/dist/app/charts/legends/Legend.js.map +1 -1
  156. package/dist/app/charts/legends/constants.d.ts +19 -0
  157. package/dist/app/charts/legends/constants.d.ts.map +1 -0
  158. package/dist/app/charts/legends/constants.js +34 -0
  159. package/dist/app/charts/legends/constants.js.map +1 -0
  160. package/dist/app/charts/observables/ChartData.d.ts +1 -0
  161. package/dist/app/charts/observables/ChartData.d.ts.map +1 -0
  162. package/dist/app/charts/observables/iterates.d.ts +13 -7
  163. package/dist/app/charts/observables/iterates.d.ts.map +1 -0
  164. package/dist/app/charts/observables/iterates.js +42 -9
  165. package/dist/app/charts/observables/iterates.js.map +1 -1
  166. package/dist/app/charts/observables/ordinals.d.ts +70 -12
  167. package/dist/app/charts/observables/ordinals.d.ts.map +1 -0
  168. package/dist/app/charts/observables/ordinals.js +127 -48
  169. package/dist/app/charts/observables/ordinals.js.map +1 -1
  170. package/dist/app/charts/observables/outliers.d.ts +18 -0
  171. package/dist/app/charts/observables/outliers.d.ts.map +1 -0
  172. package/dist/app/charts/observables/outliers.js +2 -0
  173. package/dist/app/charts/observables/outliers.js.map +1 -0
  174. package/dist/app/charts/plots/BarPlot.d.ts +87 -13
  175. package/dist/app/charts/plots/BarPlot.d.ts.map +1 -0
  176. package/dist/app/charts/plots/BarPlot.js +603 -356
  177. package/dist/app/charts/plots/BarPlot.js.map +1 -1
  178. package/dist/app/charts/plots/OutlierPlot.d.ts +123 -0
  179. package/dist/app/charts/plots/OutlierPlot.d.ts.map +1 -0
  180. package/dist/app/charts/plots/OutlierPlot.js +430 -0
  181. package/dist/app/charts/plots/OutlierPlot.js.map +1 -0
  182. package/dist/app/charts/plots/PoincarePlot.d.ts +92 -14
  183. package/dist/app/charts/plots/PoincarePlot.d.ts.map +1 -0
  184. package/dist/app/charts/plots/PoincarePlot.js +523 -364
  185. package/dist/app/charts/plots/PoincarePlot.js.map +1 -1
  186. package/dist/app/charts/plots/RasterPlot.d.ts +31 -10
  187. package/dist/app/charts/plots/RasterPlot.d.ts.map +1 -0
  188. package/dist/app/charts/plots/RasterPlot.js +198 -306
  189. package/dist/app/charts/plots/RasterPlot.js.map +1 -1
  190. package/dist/app/charts/plots/ScatterPlot.d.ts +77 -21
  191. package/dist/app/charts/plots/ScatterPlot.d.ts.map +1 -0
  192. package/dist/app/charts/plots/ScatterPlot.js +359 -302
  193. package/dist/app/charts/plots/ScatterPlot.js.map +1 -1
  194. package/dist/app/charts/plots/autoScaleYAxes.d.ts +43 -0
  195. package/dist/app/charts/plots/autoScaleYAxes.d.ts.map +1 -0
  196. package/dist/app/charts/plots/autoScaleYAxes.js +71 -0
  197. package/dist/app/charts/plots/autoScaleYAxes.js.map +1 -0
  198. package/dist/app/charts/plots/colorizedLine.d.ts +61 -0
  199. package/dist/app/charts/plots/colorizedLine.d.ts.map +1 -0
  200. package/dist/app/charts/plots/colorizedLine.js +157 -0
  201. package/dist/app/charts/plots/colorizedLine.js.map +1 -0
  202. package/dist/app/charts/plots/constants.d.ts +7 -0
  203. package/dist/app/charts/plots/constants.d.ts.map +1 -0
  204. package/dist/app/charts/plots/constants.js +11 -0
  205. package/dist/app/charts/plots/constants.js.map +1 -0
  206. package/dist/app/charts/plots/hitTesting.d.ts +88 -0
  207. package/dist/app/charts/plots/hitTesting.d.ts.map +1 -0
  208. package/dist/app/charts/plots/hitTesting.js +136 -0
  209. package/dist/app/charts/plots/hitTesting.js.map +1 -0
  210. package/dist/app/charts/plots/plot.d.ts +52 -16
  211. package/dist/app/charts/plots/plot.d.ts.map +1 -0
  212. package/dist/app/charts/plots/plot.js +108 -35
  213. package/dist/app/charts/plots/plot.js.map +1 -1
  214. package/dist/app/charts/plots/useCanvasZoomPan.d.ts +119 -0
  215. package/dist/app/charts/plots/useCanvasZoomPan.d.ts.map +1 -0
  216. package/dist/app/charts/plots/useCanvasZoomPan.js +113 -0
  217. package/dist/app/charts/plots/useCanvasZoomPan.js.map +1 -0
  218. package/dist/app/charts/plots/useStreamingTimeAxes.d.ts +103 -0
  219. package/dist/app/charts/plots/useStreamingTimeAxes.d.ts.map +1 -0
  220. package/dist/app/charts/plots/useStreamingTimeAxes.js +257 -0
  221. package/dist/app/charts/plots/useStreamingTimeAxes.js.map +1 -0
  222. package/dist/app/charts/series/baseSeries.d.ts +60 -8
  223. package/dist/app/charts/series/baseSeries.d.ts.map +1 -0
  224. package/dist/app/charts/series/baseSeries.js +53 -8
  225. package/dist/app/charts/series/baseSeries.js.map +1 -1
  226. package/dist/app/charts/series/iterateSeries.d.ts +9 -8
  227. package/dist/app/charts/series/iterateSeries.d.ts.map +1 -0
  228. package/dist/app/charts/series/iterateSeries.js +7 -7
  229. package/dist/app/charts/series/iterateSeries.js.map +1 -1
  230. package/dist/app/charts/series/ordinalSeries.d.ts +27 -13
  231. package/dist/app/charts/series/ordinalSeries.d.ts.map +1 -0
  232. package/dist/app/charts/series/ordinalSeries.js +29 -15
  233. package/dist/app/charts/series/ordinalSeries.js.map +1 -1
  234. package/dist/app/charts/series/outlierSeries.d.ts +142 -0
  235. package/dist/app/charts/series/outlierSeries.d.ts.map +1 -0
  236. package/dist/app/charts/series/outlierSeries.js +82 -0
  237. package/dist/app/charts/series/outlierSeries.js.map +1 -0
  238. package/dist/app/charts/series/timeSeries.d.ts +13 -9
  239. package/dist/app/charts/series/timeSeries.d.ts.map +1 -0
  240. package/dist/app/charts/series/timeSeries.js +6 -3
  241. package/dist/app/charts/series/timeSeries.js.map +1 -1
  242. package/dist/app/charts/series/timeSeriesChartData.d.ts +18 -10
  243. package/dist/app/charts/series/timeSeriesChartData.d.ts.map +1 -0
  244. package/dist/app/charts/series/timeSeriesChartData.js +28 -14
  245. package/dist/app/charts/series/timeSeriesChartData.js.map +1 -1
  246. package/dist/app/charts/styling/barPlotStyle.d.ts +46 -2
  247. package/dist/app/charts/styling/barPlotStyle.d.ts.map +1 -0
  248. package/dist/app/charts/styling/barPlotStyle.js +37 -3
  249. package/dist/app/charts/styling/barPlotStyle.js.map +1 -1
  250. package/dist/app/charts/styling/canvasStyle.d.ts +53 -0
  251. package/dist/app/charts/styling/canvasStyle.d.ts.map +1 -0
  252. package/dist/app/charts/styling/canvasStyle.js +71 -0
  253. package/dist/app/charts/styling/canvasStyle.js.map +1 -0
  254. package/dist/app/charts/styling/margins.d.ts +21 -2
  255. package/dist/app/charts/styling/margins.d.ts.map +1 -0
  256. package/dist/app/charts/styling/margins.js +25 -4
  257. package/dist/app/charts/styling/margins.js.map +1 -1
  258. package/dist/app/charts/styling/svgStyle.d.ts +10 -56
  259. package/dist/app/charts/styling/svgStyle.d.ts.map +1 -0
  260. package/dist/app/charts/styling/svgStyle.js +3 -78
  261. package/dist/app/charts/styling/svgStyle.js.map +1 -1
  262. package/dist/app/charts/subscriptions/viewDrivers.d.ts +124 -0
  263. package/dist/app/charts/subscriptions/viewDrivers.d.ts.map +1 -0
  264. package/dist/app/charts/subscriptions/viewDrivers.js +338 -0
  265. package/dist/app/charts/subscriptions/viewDrivers.js.map +1 -0
  266. package/dist/app/charts/tooltips/BarPlotTooltipContent.d.ts +12 -78
  267. package/dist/app/charts/tooltips/BarPlotTooltipContent.d.ts.map +1 -0
  268. package/dist/app/charts/tooltips/BarPlotTooltipContent.js +65 -257
  269. package/dist/app/charts/tooltips/BarPlotTooltipContent.js.map +1 -1
  270. package/dist/app/charts/tooltips/DefaultOutlierTooltipContent.d.ts +9 -0
  271. package/dist/app/charts/tooltips/DefaultOutlierTooltipContent.d.ts.map +1 -0
  272. package/dist/app/charts/tooltips/DefaultOutlierTooltipContent.js +16 -0
  273. package/dist/app/charts/tooltips/DefaultOutlierTooltipContent.js.map +1 -0
  274. package/dist/app/charts/tooltips/OutlierPlotTooltipContent.d.ts +30 -0
  275. package/dist/app/charts/tooltips/OutlierPlotTooltipContent.d.ts.map +1 -0
  276. package/dist/app/charts/tooltips/OutlierPlotTooltipContent.js +26 -0
  277. package/dist/app/charts/tooltips/OutlierPlotTooltipContent.js.map +1 -0
  278. package/dist/app/charts/tooltips/PoincarePlotTooltipContent.d.ts +22 -72
  279. package/dist/app/charts/tooltips/PoincarePlotTooltipContent.d.ts.map +1 -0
  280. package/dist/app/charts/tooltips/PoincarePlotTooltipContent.js +104 -171
  281. package/dist/app/charts/tooltips/PoincarePlotTooltipContent.js.map +1 -1
  282. package/dist/app/charts/tooltips/RasterPlotTooltipContent.d.ts +32 -8
  283. package/dist/app/charts/tooltips/RasterPlotTooltipContent.d.ts.map +1 -0
  284. package/dist/app/charts/tooltips/RasterPlotTooltipContent.js +62 -115
  285. package/dist/app/charts/tooltips/RasterPlotTooltipContent.js.map +1 -1
  286. package/dist/app/charts/tooltips/ScatterPlotTooltipContent.d.ts +39 -12
  287. package/dist/app/charts/tooltips/ScatterPlotTooltipContent.d.ts.map +1 -0
  288. package/dist/app/charts/tooltips/ScatterPlotTooltipContent.js +162 -134
  289. package/dist/app/charts/tooltips/ScatterPlotTooltipContent.js.map +1 -1
  290. package/dist/app/charts/tooltips/Tooltip.d.ts +27 -32
  291. package/dist/app/charts/tooltips/Tooltip.d.ts.map +1 -0
  292. package/dist/app/charts/tooltips/Tooltip.js +37 -81
  293. package/dist/app/charts/tooltips/Tooltip.js.map +1 -1
  294. package/dist/app/charts/tooltips/TooltipContent.d.ts +81 -0
  295. package/dist/app/charts/tooltips/TooltipContent.d.ts.map +1 -0
  296. package/dist/app/charts/tooltips/TooltipContent.js +99 -0
  297. package/dist/app/charts/tooltips/TooltipContent.js.map +1 -0
  298. package/dist/app/charts/tooltips/tooltipUtils.d.ts +186 -36
  299. package/dist/app/charts/tooltips/tooltipUtils.d.ts.map +1 -0
  300. package/dist/app/charts/tooltips/tooltipUtils.js +277 -48
  301. package/dist/app/charts/tooltips/tooltipUtils.js.map +1 -1
  302. package/dist/app/charts/tooltips/useOutlierTooltip.d.ts +59 -0
  303. package/dist/app/charts/tooltips/useOutlierTooltip.d.ts.map +1 -0
  304. package/dist/app/charts/tooltips/useOutlierTooltip.js +16 -0
  305. package/dist/app/charts/tooltips/useOutlierTooltip.js.map +1 -0
  306. package/dist/app/charts/tooltips/useTooltipPlacement.d.ts +51 -0
  307. package/dist/app/charts/tooltips/useTooltipPlacement.d.ts.map +1 -0
  308. package/dist/app/charts/tooltips/useTooltipPlacement.js +52 -0
  309. package/dist/app/charts/tooltips/useTooltipPlacement.js.map +1 -0
  310. package/dist/app/charts/tooltips/useTooltipStyle.d.ts +11 -0
  311. package/dist/app/charts/tooltips/useTooltipStyle.d.ts.map +1 -0
  312. package/dist/app/charts/tooltips/useTooltipStyle.js +14 -0
  313. package/dist/app/charts/tooltips/useTooltipStyle.js.map +1 -0
  314. package/dist/app/charts/trackers/Tracker.d.ts +22 -6
  315. package/dist/app/charts/trackers/Tracker.d.ts.map +1 -0
  316. package/dist/app/charts/trackers/Tracker.js +36 -22
  317. package/dist/app/charts/trackers/Tracker.js.map +1 -1
  318. package/dist/app/charts/trackers/trackerUtils.d.ts +40 -18
  319. package/dist/app/charts/trackers/trackerUtils.d.ts.map +1 -0
  320. package/dist/app/charts/trackers/trackerUtils.js +213 -300
  321. package/dist/app/charts/trackers/trackerUtils.js.map +1 -1
  322. package/dist/app/charts/utils.d.ts +72 -33
  323. package/dist/app/charts/utils.d.ts.map +1 -0
  324. package/dist/app/charts/utils.js +87 -42
  325. package/dist/app/charts/utils.js.map +1 -1
  326. package/dist/index.d.ts +2 -45
  327. package/dist/index.d.ts.map +1 -0
  328. package/dist/index.js +2 -72
  329. package/dist/index.js.map +1 -1
  330. package/package.json +87 -46
  331. package/src/app/charts/Chart.tsx +132 -168
  332. package/src/app/charts/axes/AxesState.ts +27 -29
  333. package/src/app/charts/axes/AxisInterval.ts +57 -32
  334. package/src/app/charts/axes/BaseAxisRange.ts +47 -27
  335. package/src/app/charts/axes/ContinuousAxis.tsx +235 -39
  336. package/src/app/charts/axes/ContinuousAxisRange.ts +29 -11
  337. package/src/app/charts/axes/EmptyAxis.tsx +52 -17
  338. package/src/app/charts/axes/OrdinalAxis.tsx +54 -28
  339. package/src/app/charts/axes/OrdinalAxisRange.ts +96 -16
  340. package/src/app/charts/axes/autoScale.ts +244 -0
  341. package/src/app/charts/axes/axes.ts +954 -735
  342. package/src/app/charts/d3types.ts +42 -13
  343. package/src/app/charts/datasources/StreamingDataSource.ts +214 -0
  344. package/src/app/charts/datasources/iteratesDataSource.ts +122 -0
  345. package/src/app/charts/datasources/ordinalDataSource.ts +205 -0
  346. package/src/app/charts/datasources/outlierDataSource.ts +90 -0
  347. package/src/app/charts/datasources/timeSeriesDataSource.ts +133 -0
  348. package/src/app/charts/filters/regexFilter.ts +45 -5
  349. package/src/app/charts/hooks/AxesProvider.tsx +301 -0
  350. package/src/app/charts/hooks/CanvasSurfaceProvider.tsx +117 -0
  351. package/src/app/charts/hooks/ChartProvider.tsx +79 -0
  352. package/src/app/charts/hooks/InteractionProvider.tsx +88 -0
  353. package/src/app/charts/hooks/PlotDimensionsProvider.tsx +109 -0
  354. package/src/app/charts/hooks/defaultAxesValues.ts +40 -0
  355. package/src/app/charts/hooks/defaultMouseValues.ts +24 -0
  356. package/src/app/charts/hooks/defaultPlotDimensions.ts +27 -0
  357. package/src/app/charts/hooks/defaultTooltipValues.tsx +23 -0
  358. package/src/app/charts/hooks/defaultUseChartValues.tsx +36 -0
  359. package/src/app/charts/hooks/useAxes.tsx +60 -228
  360. package/src/app/charts/hooks/useCanvasSurface.tsx +35 -0
  361. package/src/app/charts/hooks/useChart.tsx +47 -106
  362. package/src/app/charts/hooks/useDataSource.tsx +106 -0
  363. package/src/app/charts/hooks/useHoveredSeries.tsx +33 -0
  364. package/src/app/charts/hooks/useMouse.tsx +58 -82
  365. package/src/app/charts/hooks/usePlotDimensions.tsx +25 -89
  366. package/src/app/charts/hooks/useTooltip.tsx +32 -63
  367. package/src/app/charts/index.ts +213 -0
  368. package/src/app/charts/legends/Legend.tsx +198 -479
  369. package/src/app/charts/legends/constants.ts +37 -0
  370. package/src/app/charts/observables/iterates.ts +32 -12
  371. package/src/app/charts/observables/ordinals.ts +141 -67
  372. package/src/app/charts/observables/outliers.ts +20 -0
  373. package/src/app/charts/plots/BarPlot.tsx +784 -666
  374. package/src/app/charts/plots/OutlierPlot.tsx +685 -0
  375. package/src/app/charts/plots/PoincarePlot.tsx +707 -489
  376. package/src/app/charts/plots/RasterPlot.tsx +253 -445
  377. package/src/app/charts/plots/ScatterPlot.tsx +480 -463
  378. package/src/app/charts/plots/autoScaleYAxes.ts +102 -0
  379. package/src/app/charts/plots/colorizedLine.ts +185 -0
  380. package/src/app/charts/plots/constants.ts +16 -0
  381. package/src/app/charts/plots/hitTesting.ts +198 -0
  382. package/src/app/charts/plots/plot.ts +138 -44
  383. package/src/app/charts/plots/useCanvasZoomPan.ts +240 -0
  384. package/src/app/charts/plots/useStreamingTimeAxes.ts +384 -0
  385. package/src/app/charts/series/baseSeries.ts +81 -14
  386. package/src/app/charts/series/iterateSeries.ts +8 -8
  387. package/src/app/charts/series/ordinalSeries.ts +35 -20
  388. package/src/app/charts/series/outlierSeries.ts +188 -0
  389. package/src/app/charts/series/timeSeries.ts +14 -12
  390. package/src/app/charts/series/timeSeriesChartData.ts +31 -17
  391. package/src/app/charts/styling/barPlotStyle.ts +48 -5
  392. package/src/app/charts/styling/canvasStyle.ts +86 -0
  393. package/src/app/charts/styling/margins.ts +28 -4
  394. package/src/app/charts/styling/svgStyle.ts +10 -91
  395. package/src/app/charts/subscriptions/viewDrivers.ts +447 -0
  396. package/src/app/charts/tooltips/BarPlotTooltipContent.tsx +156 -380
  397. package/src/app/charts/tooltips/DefaultOutlierTooltipContent.tsx +40 -0
  398. package/src/app/charts/tooltips/OutlierPlotTooltipContent.tsx +67 -0
  399. package/src/app/charts/tooltips/PoincarePlotTooltipContent.tsx +229 -292
  400. package/src/app/charts/tooltips/RasterPlotTooltipContent.tsx +164 -217
  401. package/src/app/charts/tooltips/ScatterPlotTooltipContent.tsx +373 -236
  402. package/src/app/charts/tooltips/Tooltip.tsx +58 -124
  403. package/src/app/charts/tooltips/TooltipContent.tsx +177 -0
  404. package/src/app/charts/tooltips/tooltipUtils.ts +359 -61
  405. package/src/app/charts/tooltips/useOutlierTooltip.tsx +68 -0
  406. package/src/app/charts/tooltips/useTooltipPlacement.ts +86 -0
  407. package/src/app/charts/tooltips/useTooltipStyle.ts +15 -0
  408. package/src/app/charts/trackers/Tracker.tsx +68 -33
  409. package/src/app/charts/trackers/trackerUtils.ts +263 -376
  410. package/src/app/charts/utils.ts +90 -48
  411. package/src/index.ts +2 -322
  412. package/dist/app/charts/axes/AxesState.test.d.ts +0 -1
  413. package/dist/app/charts/axes/AxesState.test.js +0 -30
  414. package/dist/app/charts/axes/AxesState.test.js.map +0 -1
  415. package/dist/app/charts/axes/AxisInterval.test.d.ts +0 -1
  416. package/dist/app/charts/axes/AxisInterval.test.js +0 -120
  417. package/dist/app/charts/axes/AxisInterval.test.js.map +0 -1
  418. package/dist/app/charts/axes/ContinuousAxisRange.test.d.ts +0 -1
  419. package/dist/app/charts/axes/ContinuousAxisRange.test.js +0 -47
  420. package/dist/app/charts/axes/ContinuousAxisRange.test.js.map +0 -1
  421. package/dist/app/charts/axes/OrdinalAxisRange.test.d.ts +0 -1
  422. package/dist/app/charts/axes/OrdinalAxisRange.test.js +0 -48
  423. package/dist/app/charts/axes/OrdinalAxisRange.test.js.map +0 -1
  424. package/dist/app/charts/filters/regexFilter.test.d.ts +0 -1
  425. package/dist/app/charts/filters/regexFilter.test.js +0 -11
  426. package/dist/app/charts/filters/regexFilter.test.js.map +0 -1
  427. package/dist/app/charts/hooks/useDataObservable.d.ts +0 -72
  428. package/dist/app/charts/hooks/useDataObservable.js +0 -74
  429. package/dist/app/charts/hooks/useDataObservable.js.map +0 -1
  430. package/dist/app/charts/hooks/useInitialData.d.ts +0 -36
  431. package/dist/app/charts/hooks/useInitialData.js +0 -28
  432. package/dist/app/charts/hooks/useInitialData.js.map +0 -1
  433. package/dist/app/charts/observables/iterates.test.d.ts +0 -1
  434. package/dist/app/charts/observables/iterates.test.js +0 -180
  435. package/dist/app/charts/observables/iterates.test.js.map +0 -1
  436. package/dist/app/charts/observables/ordinals.test.d.ts +0 -1
  437. package/dist/app/charts/observables/ordinals.test.js +0 -102
  438. package/dist/app/charts/observables/ordinals.test.js.map +0 -1
  439. package/dist/app/charts/series/datumSeries.test.d.ts +0 -1
  440. package/dist/app/charts/series/datumSeries.test.js +0 -50
  441. package/dist/app/charts/series/datumSeries.test.js.map +0 -1
  442. package/dist/app/charts/styling/margins.test.d.ts +0 -1
  443. package/dist/app/charts/styling/margins.test.js +0 -19
  444. package/dist/app/charts/styling/margins.test.js.map +0 -1
  445. package/dist/app/charts/subscriptions/subscription.test.d.ts +0 -1
  446. package/dist/app/charts/subscriptions/subscription.test.js +0 -395
  447. package/dist/app/charts/subscriptions/subscription.test.js.map +0 -1
  448. package/dist/app/charts/subscriptions/subscriptions.d.ts +0 -100
  449. package/dist/app/charts/subscriptions/subscriptions.js +0 -449
  450. package/dist/app/charts/subscriptions/subscriptions.js.map +0 -1
  451. package/src/app/charts/axes/AxesState.test.ts +0 -34
  452. package/src/app/charts/axes/AxisInterval.test.ts +0 -143
  453. package/src/app/charts/axes/ContinuousAxisRange.test.ts +0 -55
  454. package/src/app/charts/axes/OrdinalAxisRange.test.ts +0 -55
  455. package/src/app/charts/filters/regexFilter.test.ts +0 -13
  456. package/src/app/charts/hooks/useDataObservable.tsx +0 -170
  457. package/src/app/charts/hooks/useInitialData.tsx +0 -64
  458. package/src/app/charts/observables/iterates.test.ts +0 -201
  459. package/src/app/charts/observables/ordinals.test.ts +0 -135
  460. package/src/app/charts/series/datumSeries.test.ts +0 -60
  461. package/src/app/charts/styling/margins.test.ts +0 -22
  462. package/src/app/charts/subscriptions/subscription.test.ts +0 -499
  463. package/src/app/charts/subscriptions/subscriptions.ts +0 -635
@@ -1,16 +1,23 @@
1
- import {Dimensions, Margin} from "../styling/margins";
1
+ import type {Dimensions, Margin} from "../styling/margins";
2
2
  import {ContinuousAxisRange} from "./ContinuousAxisRange";
3
+ import type {AutoScale} from "./autoScale";
3
4
  import * as d3 from "d3";
4
- import {Axis, ScaleBand, ScaleContinuousNumeric, ScaleLinear, ZoomTransform} from "d3";
5
- import {AxisElementSelection, SvgSelection} from "../d3types";
5
+ import {type ScaleBand, type ScaleContinuousNumeric, type ScaleLinear} from "d3";
6
+ import type {CanvasContext, DrawHandle} from "../d3types";
6
7
  import {AxesState} from "./AxesState";
7
- import {AxesAssignment} from "../plots/plot";
8
- import {BaseSeries} from "../series/baseSeries";
9
- import {noop} from "../utils";
8
+ import type {AxesAssignment} from "../plots/plot";
9
+ import {clipToArea} from "../plots/plot";
10
+ import type {BaseSeries} from "../series/baseSeries";
11
+ import {fontStringFor} from "../utils";
10
12
  import {OrdinalAxisRange} from "./OrdinalAxisRange";
11
13
  import {BaseAxisRange} from "./BaseAxisRange";
12
14
  import {AxisInterval} from "./AxisInterval";
13
15
 
16
+ /** Default tick length, in pixels, matching d3-axis's default `tickSize`. */
17
+ const TICK_SIZE = 6
18
+ /** Default gap between the end of a tick and its label, matching d3-axis's default `tickPadding`. */
19
+ const TICK_PADDING = 3
20
+
14
21
  /**
15
22
  * Holds the style for the axis tick labels
16
23
  */
@@ -24,7 +31,8 @@ export type AxisTickStyle = {
24
31
 
25
32
  /**
26
33
  * Factory function to create a default AxisTickStyle with the default font,
27
- * no rotation, and no auto-rotation.
34
+ * no rotation, and no autorotation.
35
+ * @return The default axis tick style
28
36
  */
29
37
  export function defaultAxisTickStyle(): AxisTickStyle {
30
38
  return {
@@ -47,6 +55,7 @@ export interface AxesFont {
47
55
  /**
48
56
  * Factory function to create a default AxesFont with the default font size, color,
49
57
  * family, and weight.
58
+ * @return The default axes font
50
59
  */
51
60
  export function defaultAxesFont(): AxesFont {
52
61
  return {
@@ -77,6 +86,7 @@ export interface SeriesLineStyle extends SeriesStyle {
77
86
  /**
78
87
  * Factory function to create a default SeriesLineStyle with the default color, line width,
79
88
  * highlight color, and highlight width.
89
+ * @return The default series line style
80
90
  */
81
91
  export function defaultLineStyle(): SeriesLineStyle {
82
92
  return {
@@ -88,17 +98,74 @@ export function defaultLineStyle(): SeriesLineStyle {
88
98
  }
89
99
 
90
100
  /**
91
- * The base interface that all Axes must implement. Each axis must have a unique ID,
92
- * a location (top, bottom, left, right), and d3 SVG selection for the axis.
101
+ * The concrete kind of an axis, set by every axis-creator function to its own unique value.
102
+ * Lets code that only has a `BaseAxis` (e.g. a plot's `axesFor` validating an `axisAssignments`
103
+ * entry) discriminate the concrete axis shape at runtime -- `BaseAxis`/`ContinuousNumericAxis`/
104
+ * `OrdinalStringAxis` are plain structural interfaces, so a type-level cast (`as ContinuousNumericAxis`)
105
+ * cannot do that; this field is what actually can.
106
+ * Note: replaces enums to support `erasableSyntaxOnly`
107
+ * TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
108
+ */
109
+ export const AxisType = {
110
+ ContinuousNumeric: "ContinuousNumeric",
111
+ OrdinalString: "OrdinalString",
112
+ } as const
113
+
114
+ export type AxisType = (typeof AxisType)[keyof typeof AxisType]
115
+
116
+ /**
117
+ * The axis component that creates each type of axis (with its article), for messages about axes
118
+ */
119
+ const AXIS_COMPONENTS: Record<AxisType, string> = {
120
+ [AxisType.ContinuousNumeric]: "a <ContinuousAxis> (or an <EmptyAxis>)",
121
+ [AxisType.OrdinalString]: "an <OrdinalAxis>",
122
+ }
123
+
124
+ /**
125
+ * Checks that a plot's axis is of the type the plot needs (e.g. a bar plot needs an ordinal x-axis).
126
+ * A missing axis passes, since the plots already skip the series whose axes aren't found.
127
+ * @param plotName The name of the plot's component (e.g. "RasterPlot")
128
+ * @param dimension Whether the axis is the plot's x-axis or its y-axis
129
+ * @param axis The axis (`undefined` when it isn't found)
130
+ * @param expected The axis type the plot needs
131
+ * @throws Error when the axis isn't of the expected type, naming the axis component it needs to be
132
+ */
133
+ export function checkAxisType(plotName: string, dimension: 'x' | 'y', axis: BaseAxis | undefined, expected: AxisType): void {
134
+ if (axis !== undefined && axis.axisType !== expected) {
135
+ throw Error(
136
+ `<${plotName}> needs its ${dimension}-axis to be ${AXIS_COMPONENTS[expected]}, but the ` +
137
+ `${dimension}-axis "${axis.axisId}" is ${AXIS_COMPONENTS[axis.axisType]}`
138
+ )
139
+ }
140
+ }
141
+
142
+ /**
143
+ * The base interface that all Axes must implement. Each axis must have a unique ID and a location
144
+ * (top, bottom, left, right). Under the old SVG-backed implementation this also carried a
145
+ * `selection`; that's gone now that axes draw onto a shared canvas instead of owning their own DOM
146
+ * elements.
93
147
  */
94
148
  export interface BaseAxis {
95
149
  axisId: string
96
150
  location: AxisLocation
97
- selection: AxisElementSelection
151
+ /**
152
+ * The concrete kind of this axis (see {@link AxisType}). Every axis-creator function sets this
153
+ * to its own unique value.
154
+ */
155
+ axisType: AxisType
156
+ /**
157
+ * True for a placeholder axis added via `addEmptyXAxis`/`addEmptyYAxis` (just a
158
+ * line, with no real data domain behind it). Consumers that report a value for the mouse
159
+ * position -- e.g., the tracker's axis labels -- should skip axes marked this way, since their
160
+ * scale doesn't reflect anything meaningful.
161
+ */
162
+ isEmpty?: boolean
98
163
  }
99
164
 
100
165
  /**
101
- * Represents a continuous numeric axis.
166
+ * Represents a continuous numeric axis. Draws itself by registering a draw function with the
167
+ * chart's {@link CanvasContext} (see `addContinuousNumericXAxis` and
168
+ * `addContinuousNumericYAxis`, below) rather than owning an SVG element.
102
169
  */
103
170
  export interface ContinuousNumericAxis extends BaseAxis {
104
171
  /**
@@ -106,135 +173,124 @@ export interface ContinuousNumericAxis extends BaseAxis {
106
173
  */
107
174
  scale: ScaleContinuousNumeric<number, number>
108
175
  /**
109
- * A d3 axis generator for a continuous numeric axis.
176
+ * When set, the axis' domain grows to fit the data plotted against it, within the settings'
177
+ * limits (see `ContinuousAxis`'s `autoScale` prop). Set by the axis component, and applied by
178
+ * the plots that support it (for y-axes).
110
179
  */
111
- generator: Axis<number | { valueOf(): number }>
180
+ autoScale?: AutoScale
112
181
  /**
113
- * Updates the axis based on the specified domain and plot dimensions.
182
+ * Updates the axis based on the specified domain and plot dimensions and requests the plot
183
+ * to be redrawn.
114
184
  * @param domain The interval representing the domain of the axis.
115
185
  * @param plotDimensions The dimensions of the plot (without the margins).
116
186
  * @param margin The margins for the plot
117
187
  */
118
188
  update: (domain: AxisInterval, plotDimensions: Dimensions, margin: Margin) => void
189
+ /**
190
+ * Updates the font (color, size, family, weight) used for ticks and the axis label, without
191
+ * recreating the axis or touching its domain. Used to keep the label color in sync with the
192
+ * chart's `color` prop (e.g., on a theme change) between full axis updates.
193
+ */
194
+ updateFont: (font: AxesFont) => void
195
+ /**
196
+ * Highlights (or un-highlights) this axis -- used, for example, to indicate which axis a
197
+ * currently-hovered series is plotted against. When `highlighted` is true, the axis' domain
198
+ * line, ticks, and labels are drawn in `color` at `lineWidth` instead of the normal font color
199
+ * and 1px line; `color`/`lineWidth` are remembered across calls, so a later `setHighlighted(true)`
200
+ * without them reuses whatever was last passed (or the built-in default on the first call).
201
+ * @param highlighted Whether the axis should be drawn highlighted
202
+ * @param color The highlight color
203
+ * @param lineWidth The highlight line width
204
+ */
205
+ setHighlighted: (highlighted: boolean, color?: string, lineWidth?: number) => void
119
206
  }
120
207
 
121
208
  /**
122
- * Represents an ordinal string axis.
209
+ * Represents an ordinal string axis. Draws itself by registering a draw function with the chart's
210
+ * {@link CanvasContext} rather than owning an SVG element.
123
211
  */
124
212
  export interface OrdinalStringAxis extends BaseAxis {
125
213
  /**
126
214
  * A d3 scale for an ordinal string axis.
127
215
  */
128
216
  scale: ScaleBand<string>
129
- /**
130
- * A d3 axis generator for an ordinal string axis.
131
- */
132
- generator: Axis<string>
133
217
  /**
134
218
  * The size of each category on the axis.
135
219
  */
136
220
  categorySize: number
137
221
  /**
138
- * Updates the axis based on the specified domain and plot dimensions.
222
+ * Updates the axis based on the specified categories, range, and plot dimensions, and requests
223
+ * the plot to be redrawn.
224
+ * @param categories The category names for the axis, in order -- replaces the scale's domain,
225
+ * so passing a different set of categories (e.g. because the number of series changed) is how
226
+ * an axis reflects a new category count. Pass `axis.scale.domain()` to leave the categories
227
+ * unchanged (e.g. from a pan/zoom call site that isn't varying the category set).
139
228
  * @param range An interval representing the range of the axis.
140
229
  * @param originalRange An interval representing the original range of the axis (before zooming or panning).
141
230
  * @param plotDimensions The dimensions of the plot (without the margins).
142
231
  * @param margin The margins for the plot
143
232
  * @return The number of pixels for each category on the axis.
144
233
  */
145
- update: (range: AxisInterval, originalRange: AxisInterval, plotDimensions: Dimensions, margin: Margin) => number
234
+ update: (categories: Array<string>, range: AxisInterval, originalRange: AxisInterval, plotDimensions: Dimensions, margin: Margin) => number
235
+ /**
236
+ * Updates the font used for the axis label without recreating the axis. The font's color is
237
+ * also the color of the axis line, the ticks, and the tick labels; the tick labels' size,
238
+ * family, and weight, though, stay fixed at whatever the tick style's font was set to when
239
+ * the axis was created.
240
+ */
241
+ updateFont: (font: AxesFont) => void
242
+ /**
243
+ * Highlights (or un-highlights) this axis -- see {@link ContinuousNumericAxis.setHighlighted}
244
+ * for the full explanation. `color`/`lineWidth` are remembered across calls, so a later
245
+ * `setHighlighted(true)` without them reuses whatever was last passed (or the built-in
246
+ * default on the first call).
247
+ * @param highlighted Whether the axis should be drawn highlighted
248
+ * @param color The highlight color
249
+ * @param lineWidth The highlight line width
250
+ */
251
+ setHighlighted: (highlighted: boolean, color?: string, lineWidth?: number) => void
146
252
  }
147
253
 
148
254
  /**
149
- * Enumerates the possible locations of an axis.
255
+ * The possible locations of an axis.
256
+ * Note: replaces enums to support `erasableSyntaxOnly`
257
+ * TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
150
258
  */
151
- export enum AxisLocation {
259
+ export const AxisLocation = {
152
260
  // y-axes
153
- Left,
154
- Right,
261
+ Left : "Left",
262
+ Right: "Right",
155
263
  // x-axes
156
- Bottom,
157
- Top
158
- }
159
-
160
- /*
161
- axis clip path definition
162
- */
163
-
164
- /**
165
- * Adds a clip area for the chart to the specified SVG element. The clip-area is given
166
- * an `id` of `clip-series-<chart_id>`, which because the chart ID should be unique, makes
167
- * this unique as well
168
- * @param chartId The ID of the chart to which the clip area is to be added
169
- * @param axisId The ID of the axis to which the clip area is to be added
170
- * @param svg The SVG element to which the clip area is to be added
171
- * @param location The location of the axis
172
- * @param plotDimensions The dimensions of the plot
173
- * @param margin The margins around the plot
174
- * @return The ID of the clip-path
175
- */
176
- export function setClipPath(
177
- chartId: number,
178
- axisId: string,
179
- svg: SvgSelection,
180
- location: AxisLocation,
181
- plotDimensions: Dimensions,
182
- margin: Margin
183
- ): string {
184
- const clipPathId = `chart-clip-path-${chartId}-${axisId}`
185
-
186
- // calculate the width and height of the clip-area for the axis
187
- const width = location === AxisLocation.Left ? margin.right :
188
- location === AxisLocation.Right ? margin.left :
189
- plotDimensions.width
190
-
191
- const height = location === AxisLocation.Top ? margin.top + 1 :
192
- location === AxisLocation.Bottom ? margin.bottom - 1:
193
- plotDimensions.height
194
-
195
- // remove the old clipping region and add a new one with the updated plot dimensions
196
- svg.select(`#${clipPathId}-defs`).remove();
197
- svg
198
- .append('defs')
199
- .attr('id', `${clipPathId}-defs`)
200
- .append("clipPath")
201
- .attr("id", clipPathId)
202
- .append("rect")
203
- .attr("x", 0)
204
- .attr("y", location === AxisLocation.Bottom ? -1 : -margin.top)
205
- .attr("width", width)
206
- .attr("height", height)
207
-
208
- return clipPathId
209
- }
264
+ Bottom: "Bottom",
265
+ Top: "Top"
266
+ } as const
210
267
 
268
+ export type AxisLocation = (typeof AxisLocation)[keyof typeof AxisLocation];
211
269
 
212
270
  /*
213
- category axes
271
+ category (ordinal) axes
214
272
  */
215
273
 
216
274
  /**
217
- * Adds a category axis to the specified location. When the location is top or bottom,
218
- * the category axis represents the x-axis. When the location is left or right, then the
219
- * category axis represents the y-axis.
220
- * @param chartId The unique ID of the chart to which this axis belongs
275
+ * Adds a category axis to the specified location, registering its draw function with the canvas
276
+ * context. When the location is top or bottom, the category axis represents the x-axis; when the
277
+ * location is left or right, it represents the y-axis.
278
+ * @param cc The canvas context to register the axis' draw function with
221
279
  * @param axisId A unique ID for the axis
222
- * @param svg The SVG selection (d3)
223
280
  * @param location The location of the axis
224
- * @param categories An array holding the category names
281
+ * @param categories The category names (the axis' `update` function can replace them later)
225
282
  * @param axisLabel The axis label
226
283
  * @param axesLabelFont The font for the axis label
227
- * @param axisTickStyle Styling information for the ticks (e.g. font, rotation, etc)
284
+ * @param axisTickStyle Styling information for the ticks (font, rotation, etc.)
228
285
  * @param plotDimensions The dimensions of the plot
229
286
  * @param margin The plot margin
230
- * @param setAxisRangeFor Lambda that sets the axis range for the specified axis
231
- * @param setOriginalAxisRangeFor Lambda that sets the original axis range for the specified axis
232
- * @return A category axis
287
+ * @param setAxisRangeFor Callback that sets the axis range for the specified axis
288
+ * @param setOriginalAxisRangeFor Callback that sets the original axis range for the specified axis
289
+ * @return An {@link OrdinalStringAxis}
233
290
  */
234
291
  export function addOrdinalStringAxis(
235
- chartId: number,
292
+ cc: CanvasContext,
236
293
  axisId: string,
237
- svg: SvgSelection,
238
294
  location: AxisLocation,
239
295
  categories: Array<string>,
240
296
  axisLabel: string,
@@ -249,54 +305,46 @@ export function addOrdinalStringAxis(
249
305
  case AxisLocation.Top:
250
306
  case AxisLocation.Bottom:
251
307
  return addOrdinalStringXAxis(
252
- chartId, axisId,
253
- svg,
254
- location,
255
- categories,
256
- axisLabel, axesLabelFont, axisTickStyle,
257
- plotDimensions,
258
- margin,
259
- setAxisRangeFor,
260
- setOriginalAxisRangeFor
308
+ cc, axisId, location, categories, axisLabel, axesLabelFont, axisTickStyle,
309
+ plotDimensions, margin, setAxisRangeFor, setOriginalAxisRangeFor
261
310
  )
262
311
  case AxisLocation.Left:
263
312
  case AxisLocation.Right:
264
313
  return addOrdinalStringYAxis(
265
- chartId, axisId,
266
- svg,
267
- location,
268
- categories,
269
- axisLabel, axesLabelFont, axisTickStyle,
270
- plotDimensions,
271
- margin,
272
- setAxisRangeFor,
273
- setOriginalAxisRangeFor
314
+ cc, axisId, location, categories, axisLabel, axesLabelFont, axisTickStyle,
315
+ plotDimensions, margin, setAxisRangeFor, setOriginalAxisRangeFor
274
316
  )
275
-
276
317
  }
277
318
  }
278
319
 
279
320
  /**
280
- * Adds a category axis representing the x-axis.
281
- * @param chartId The unique ID of the chart to which this axis belongs
321
+ * Adds a category x-axis (top or bottom), registering its draw function with the canvas context.
322
+ * See {@link addOrdinalStringAxis}.
323
+ *
324
+ * NOTE ON FIDELITY: the original SVG version rotated tick labels using each label's own
325
+ * `getBBox()` to compute a per-label rotation origin, which let long labels pivot around their own
326
+ * visual center/corner. Canvas `measureText` has no bbox-offset equivalent, so this version rotates
327
+ * every label about its anchor point at the tick instead. At `rotation: 0` (the common case) this
328
+ * produces the same centered layout as before; at non-zero rotations the labels will be close but
329
+ * not pixel-identical to the old SVG output -- worth a visual check if you use tick rotation.
330
+ * @param cc The canvas context to register the axis' draw function with
282
331
  * @param axisId A unique ID for the axis
283
- * @param svg The SVG selection (d3)
284
- * @param location The location of the axis
285
- * @param categories An array holding the category names
332
+ * @param location The location of the axis (top or bottom)
333
+ * @param categories The category names (the axis' `update` function can replace them later)
286
334
  * @param axisLabel The axis label
287
- * @param axesLabelFont The font for the axis label
288
- * @param axisTickStyle Styling information for the ticks (e.g. font, rotation, etc)
335
+ * @param axesLabelFont The font for the axis label (its color is also the color of the axis line,
336
+ * ticks, and tick labels)
337
+ * @param axisTickStyle Styling information for the ticks (font, rotation, etc.)
289
338
  * @param plotDimensions The dimensions of the plot
290
339
  * @param margin The plot margin
291
- * @param setAxisRangeFor Lambda that sets the axis range for the specified axis
292
- * @param setOriginalAxisRangeFor Lambda that sets the original axis range for the specified axis
293
- * @return A category axis
340
+ * @param setAxisRangeFor Callback that sets the axis range for the specified axis
341
+ * @param setOriginalAxisRangeFor Callback that sets the original axis range for the specified axis
342
+ * @return An {@link OrdinalStringAxis}
294
343
  */
295
344
  function addOrdinalStringXAxis(
296
- chartId: number,
345
+ cc: CanvasContext,
297
346
  axisId: string,
298
- svg: SvgSelection,
299
- location: AxisLocation.Bottom | AxisLocation.Top,
347
+ location: typeof AxisLocation.Bottom | typeof AxisLocation.Top,
300
348
  categories: Array<string>,
301
349
  axisLabel: string,
302
350
  axesLabelFont: AxesFont,
@@ -306,91 +354,135 @@ function addOrdinalStringXAxis(
306
354
  setAxisRangeFor: (axisId: string, range: AxisInterval) => void,
307
355
  setOriginalAxisRangeFor: (axisId: string, range: AxisInterval) => void,
308
356
  ): OrdinalStringAxis {
309
- const scale = d3.scaleBand()
310
- .domain(categories)
311
- .range([0, plotDimensions.width])
312
- const categorySize = scale.bandwidth()
313
-
314
- // create and add the axes
315
- const generator = location === AxisLocation.Bottom ? d3.axisBottom(scale) : d3.axisTop(scale)
316
-
317
- const clipPathId = setClipPath(chartId, axisId, svg, location, plotDimensions, margin)
318
- const selection = svg
319
- .append<SVGGElement>('g')
320
- .attr('id', `x-axis-selection-${chartId}-${axisId}`)
321
- .attr("clip-path", `url(#${clipPathId})`)
322
- .classed('x-axis', true)
323
- .attr('transform', `translate(${margin.left}, ${yTranslation(location, plotDimensions, margin)})`)
324
- .call(generator)
325
-
326
- // rotate the tick-labels by the specified amount (in degrees)
327
- let maxTickLabelHeight = 0
328
- const {font, rotation} = axisTickStyle
329
- selection
330
- .selectAll("text")
331
- .style("text-anchor", "end")
332
- .each(function () {
333
- const degrees = location === AxisLocation.Bottom ? -rotation : rotation
334
- const radians = degrees * Math.PI / 180
335
- const {width, height, x, y} = (this as SVGTextElement).getBBox()
336
- const xOrigin = x + width
337
- const yOrigin = location === AxisLocation.Bottom ? y + height / 2 : y + height / 2
338
-
339
- d3.select(this)
340
- .attr("transform", () => `translate(${width * Math.cos(radians) / 2}, 0) rotate(${degrees}, ${xOrigin}, ${yOrigin})`)
341
-
342
- const newHeight = Math.abs(width * Math.sin(radians))
343
- if (newHeight > maxTickLabelHeight) {
344
- maxTickLabelHeight = newHeight
345
- }
357
+ const scale = d3.scaleBand<string>().domain(categories).range([0, plotDimensions.width])
358
+
359
+ let currentDimensions = plotDimensions
360
+ let currentMargin = margin
361
+ let currentFont = axesLabelFont
362
+ const currentTickStyle = axisTickStyle
363
+ let highlighted = false
364
+ let highlightColor = defaultLineStyle().highlightColor
365
+ let highlightLineWidth = defaultLineStyle().highlightWidth
366
+
367
+ const drawHandle: DrawHandle = `x-axis-ordinal-${cc.chartId}-${axisId}`
368
+
369
+ const draw = (context: CanvasContext) => {
370
+ const {context2D} = context
371
+ context2D.save()
372
+ context2D.translate(currentMargin.left, yTranslation(location, currentDimensions, currentMargin))
373
+
374
+ context2D.save()
375
+ clipToArea(context, {width: currentDimensions.width, height: currentMargin.bottom}, {x: 0, y: location === AxisLocation.Bottom ? -1 : -currentMargin.top})
376
+
377
+ const axisColor = highlighted ? highlightColor : currentFont.color
378
+ context2D.strokeStyle = axisColor
379
+ context2D.fillStyle = axisColor
380
+ context2D.lineWidth = highlighted ? highlightLineWidth : 1
381
+ context2D.font = fontStringFor(currentTickStyle.font.size, currentTickStyle.font.family, currentTickStyle.font.weight)
382
+ context2D.textBaseline = 'middle'
383
+
384
+ // domain line
385
+ context2D.beginPath()
386
+ context2D.moveTo(0, 0)
387
+ context2D.lineTo(currentDimensions.width, 0)
388
+ context2D.stroke()
389
+
390
+ const tickDirection = location === AxisLocation.Bottom ? 1 : -1
391
+ const degrees = location === AxisLocation.Bottom ? -currentTickStyle.rotation : currentTickStyle.rotation
392
+ const radians = degrees * Math.PI / 180
393
+
394
+ categories.forEach(category => {
395
+ const x = (scale(category) ?? 0) + scale.bandwidth() / 2
396
+ context2D.beginPath()
397
+ context2D.moveTo(x, 0)
398
+ context2D.lineTo(x, TICK_SIZE * tickDirection)
399
+ context2D.stroke()
400
+
401
+ context2D.save()
402
+ context2D.translate(x, (TICK_SIZE + TICK_PADDING) * tickDirection)
403
+ context2D.rotate(radians)
404
+ context2D.textAlign = radians === 0 ? 'center' : 'end'
405
+ context2D.fillText(category, 0, 0)
406
+ context2D.restore()
346
407
  })
347
408
 
348
- svg
349
- .append<SVGTextElement>('text')
350
- .attr('id', labelIdFor(chartId, location))
351
- .style('text-anchor', 'middle')
352
- // when the axis is at the top, want the label to hang from the top (y=0) of the plot
353
- .attr('dominant-baseline', location === AxisLocation.Top ? "hanging" : "baseline")
354
- .attr('font-size', font.size)
355
- .attr('fill', font.color)
356
- .attr('font-family', font.family)
357
- .attr('font-weight', font.weight)
358
- .text(axisLabel)
409
+ context2D.restore() // pop the clip
410
+
411
+ // axis label (absolute position, not clipped, matches the old behavior)
412
+ context2D.fillStyle = axisColor
413
+ context2D.font = fontStringFor(currentFont.size, currentFont.family, currentFont.weight)
414
+ context2D.textAlign = 'center'
415
+ // (a bottom label sits on the canvas' bottom edge, so it's drawn above it, descenders and all)
416
+ context2D.textBaseline = location === AxisLocation.Top ? 'hanging' : 'bottom'
417
+ const labelX = ordinalLabelXTranslation(location, currentDimensions, currentMargin, currentFont) - currentMargin.left
418
+ const labelY = ordinalLabelYTranslation(location, currentDimensions, currentMargin) - yTranslation(location, currentDimensions, currentMargin)
419
+ context2D.fillText(axisLabel, labelX, labelY)
359
420
 
360
- const axis = {axisId, selection, location, scale, generator, categorySize, update: () => categorySize}
421
+ context2D.restore()
422
+ }
423
+
424
+ cc.register(drawHandle, draw, 0)
425
+
426
+ const categorySize = () => scale.bandwidth()
361
427
 
362
428
  return {
363
- ...axis,
364
- update: (range: AxisInterval, originalRange: AxisInterval, plotDimensions: Dimensions, margin: Margin) => {
365
- const categorySize = updateOrdinalStringXAxis(chartId, axis, svg, location, categories, range, axesLabelFont, plotDimensions, margin)
429
+ axisId,
430
+ location,
431
+ axisType: AxisType.OrdinalString,
432
+ scale,
433
+ categorySize: categorySize(),
434
+ update: (updatedCategories, range, originalRange, plotDimensions, margin) => {
435
+ // reassign the closed-over `categories` (not just a local variable) so `draw`'s own
436
+ // `categories.forEach(...)` picks up the change on the next redraw too
437
+ categories = updatedCategories
438
+ const updatedRange = AxisInterval.from(
439
+ Math.min(range.start, 0),
440
+ Math.max(range.end, plotDimensions.width)
441
+ )
442
+ scale.domain(categories).range(updatedRange.asTuple())
443
+ currentDimensions = plotDimensions
444
+ currentMargin = margin
366
445
  setAxisRangeFor(axisId, range)
367
446
  setOriginalAxisRangeFor(axisId, originalRange)
368
- return categorySize
447
+ cc.requestRedraw()
448
+ return categorySize()
449
+ },
450
+ setHighlighted: (isHighlighted, color, lineWidth) => {
451
+ highlighted = isHighlighted
452
+ if (color !== undefined) highlightColor = color
453
+ if (lineWidth !== undefined) highlightLineWidth = lineWidth
454
+ cc.requestRedraw()
455
+ },
456
+ updateFont: (font) => {
457
+ // the font's color is used for the label, the axis line, the ticks, and the tick
458
+ // labels; the tick labels' size, family, and weight stay at axisTickStyle.font's
459
+ currentFont = font
460
+ cc.requestRedraw()
369
461
  }
370
462
  }
371
463
  }
372
464
 
373
465
  /**
374
- * Adds a category axis representing the y-axis.
375
- * @param chartId The unique ID of the chart to which this axis belongs
466
+ * Adds a category y-axis (left or right), registering its draw function with the canvas context.
467
+ * See {@link addOrdinalStringAxis}.
468
+ * @param cc The canvas context to register the axis' draw function with
376
469
  * @param axisId A unique ID for the axis
377
- * @param svg The SVG selection (d3)
378
- * @param location The location of the axis
379
- * @param categories An array holding the category names
470
+ * @param location The location of the axis (left or right)
471
+ * @param categories The category names (the axis' `update` function can replace them later)
380
472
  * @param axisLabel The axis label
381
- * @param axesLabelFont The font for the axis label
382
- * @param axisTickStyle Styling information for the ticks (e.g. font, rotation, etc)
473
+ * @param axesLabelFont The font for the axis label (its color is also the color of the axis line,
474
+ * ticks, and tick labels)
475
+ * @param axisTickStyle Styling information for the ticks (font, rotation, etc.)
383
476
  * @param plotDimensions The dimensions of the plot
384
477
  * @param margin The plot margin
385
- * @param setAxisRangeFor Lambda that sets the axis range for the specified axis
386
- * @param setOriginalAxisRangeFor Lambda that sets the original axis range for the specified axis
387
- * @return A category axis
478
+ * @param setAxisRangeFor Callback that sets the axis range for the specified axis
479
+ * @param setOriginalAxisRangeFor Callback that sets the original axis range for the specified axis
480
+ * @return An {@link OrdinalStringAxis}
388
481
  */
389
482
  function addOrdinalStringYAxis(
390
- chartId: number,
483
+ cc: CanvasContext,
391
484
  axisId: string,
392
- svg: SvgSelection,
393
- location: AxisLocation.Left | AxisLocation.Right,
485
+ location: typeof AxisLocation.Left | typeof AxisLocation.Right,
394
486
  categories: Array<string>,
395
487
  axisLabel: string,
396
488
  axesLabelFont: AxesFont,
@@ -400,173 +492,154 @@ function addOrdinalStringYAxis(
400
492
  setAxisRangeFor: (axisId: string, range: AxisInterval) => void,
401
493
  setOriginalAxisRangeFor: (axisId: string, range: AxisInterval) => void,
402
494
  ): OrdinalStringAxis {
403
- const scale = d3.scaleBand()
404
- .domain(categories)
405
- .range([0, plotDimensions.height])
406
- const categorySize = scale.bandwidth()
407
-
408
- // create and add the axes
409
- const generator = location === AxisLocation.Left ? d3.axisLeft(scale) : d3.axisRight(scale)
410
-
411
- const selection = svg
412
- .append<SVGGElement>('g')
413
- .attr('id', `y-axis-selection-${chartId}`)
414
- .attr('class', 'y-axis')
415
- .attr('transform', `translate(${xTranslation(location, plotDimensions, margin)}, ${margin.top})`)
416
- .call(generator);
417
-
418
- // rotate the tick-labels by the specified amount (in degrees)
419
- const {font, rotation} = axisTickStyle
420
- selection
421
- .selectAll("text")
422
- .style("text-anchor", "end")
423
- .each(function () {
424
- const degrees = location === AxisLocation.Left ? -rotation : rotation
425
- const {width, height, x, y} = (this as SVGTextElement).getBBox()
426
- const xTranslation = location === AxisLocation.Left ? 0 : width
427
- const xOrigin = x + width
428
- const yOrigin = location === AxisLocation.Left ? y + height / 2 : y + height / 2
429
-
430
- d3.select(this)
431
- .attr("transform", () => `translate(${xTranslation}, 0) rotate(${degrees}, ${xOrigin}, ${yOrigin})`)
495
+ const scale = d3.scaleBand<string>().domain(categories).range([0, plotDimensions.height])
496
+
497
+ let currentDimensions = plotDimensions
498
+ let currentMargin = margin
499
+ let currentFont = axesLabelFont
500
+ const currentTickStyle = axisTickStyle
501
+ let highlighted = false
502
+ let highlightColor = defaultLineStyle().highlightColor
503
+ let highlightLineWidth = defaultLineStyle().highlightWidth
504
+
505
+ const drawHandle: DrawHandle = `y-axis-ordinal-${cc.chartId}-${axisId}`
506
+
507
+ const draw = (context: CanvasContext) => {
508
+ const {context2D} = context
509
+ context2D.save()
510
+ context2D.translate(xTranslation(location, currentDimensions, currentMargin), currentMargin.top)
511
+
512
+ context2D.save()
513
+ const clipWidth = location === AxisLocation.Left ? currentMargin.left : currentMargin.right
514
+ // the boundary that abuts the domain line (x=0) is nudged 1px past it (into the plot side),
515
+ // so the line's 1px-wide stroke -- centered on x=0, spanning -0.5 to +0.5 -- isn't clipped
516
+ // in half; mirrors the same -1 fudge used for the bottom x-axis's domain line
517
+ clipToArea(context, {width: clipWidth, height: currentDimensions.height}, {x: location === AxisLocation.Left ? -clipWidth + 1 : -1, y: 0})
518
+
519
+ const axisColor = highlighted ? highlightColor : currentFont.color
520
+ context2D.strokeStyle = axisColor
521
+ context2D.fillStyle = axisColor
522
+ context2D.lineWidth = highlighted ? highlightLineWidth : 1
523
+ context2D.font = fontStringFor(currentTickStyle.font.size, currentTickStyle.font.family, currentTickStyle.font.weight)
524
+ // matches the old SVG version, which forced text-anchor "end" for ordinal ticks
525
+ // regardless of Left/Right location
526
+ context2D.textAlign = 'end' as CanvasTextAlign
527
+ context2D.textBaseline = 'middle'
528
+
529
+ // domain line
530
+ context2D.beginPath()
531
+ context2D.moveTo(0, 0)
532
+ context2D.lineTo(0, currentDimensions.height)
533
+ context2D.stroke()
534
+
535
+ const degrees = location === AxisLocation.Left ? -currentTickStyle.rotation : currentTickStyle.rotation
536
+ const radians = degrees * Math.PI / 180
537
+ const tickDirection = location === AxisLocation.Left ? -1 : 1
538
+ // offset the label past the tick mark plus a little padding, rather than flush against
539
+ // the axis line -- this was previously just `0` (Left) / `scale.bandwidth()` (Right),
540
+ // which put the label's text-anchor edge right on top of the axis line with no gap
541
+ const xTick = (TICK_SIZE + TICK_PADDING) * tickDirection
542
+
543
+ categories.forEach(category => {
544
+ const y = (scale(category) ?? 0) + scale.bandwidth() / 2
545
+ context2D.beginPath()
546
+ context2D.moveTo(0, y)
547
+ context2D.lineTo(TICK_SIZE * tickDirection, y)
548
+ context2D.stroke()
549
+
550
+ context2D.save()
551
+ context2D.translate(xTick, y)
552
+ context2D.rotate(radians)
553
+ context2D.fillText(category, 0, 0)
554
+ context2D.restore()
432
555
  })
433
556
 
434
- svg
435
- .append<SVGTextElement>('text')
436
- .attr('id', labelIdFor(chartId, location))
437
- .attr('text-anchor', 'middle')
438
- .attr('font-size', font.size)
439
- .attr('fill', font.color)
440
- .attr('font-family', font.family)
441
- .attr('font-weight', font.weight)
442
- .attr('transform', `translate(${ordinalLabelXTranslation(location, plotDimensions, margin, axesLabelFont)}, ${ordinalLabelYTranslation(location, plotDimensions, margin)}) rotate(-90)`)
443
- .text(axisLabel)
557
+ context2D.restore() // pop the clip
558
+
559
+ // axis label, rotated -90deg, matching the old SVG version
560
+ context2D.save()
561
+ const labelX = ordinalLabelXTranslation(location, currentDimensions, currentMargin, currentFont) - xTranslation(location, currentDimensions, currentMargin)
562
+ const labelY = ordinalLabelYTranslation(location, currentDimensions, currentMargin) - currentMargin.top
563
+ context2D.translate(labelX, labelY)
564
+ context2D.rotate(-Math.PI / 2)
565
+ context2D.fillStyle = axisColor
566
+ context2D.font = fontStringFor(currentFont.size, currentFont.family, currentFont.weight)
567
+ context2D.textAlign = 'center'
568
+ context2D.textBaseline = 'alphabetic'
569
+ context2D.fillText(axisLabel, 0, 0)
570
+ context2D.restore()
571
+
572
+ context2D.restore()
573
+ }
574
+
575
+ cc.register(drawHandle, draw, 0)
444
576
 
445
- const axis = {axisId, selection, location, scale, generator, categorySize, update: () => categorySize}
577
+ const categorySize = () => scale.bandwidth()
446
578
 
447
579
  return {
448
- ...axis,
449
- update: (range: AxisInterval, originalRange: AxisInterval, plotDimensions: Dimensions, margin: Margin) => {
450
- const categorySize = updateOrdinalStringYAxis(chartId, axis, svg, location, categories, range, axesLabelFont, plotDimensions, margin)
580
+ axisId,
581
+ location,
582
+ axisType: AxisType.OrdinalString,
583
+ scale,
584
+ categorySize: categorySize(),
585
+ update: (updatedCategories, range, originalRange, plotDimensions, margin) => {
586
+ // reassign the closed-over `categories` (not just a local variable) so `draw`'s own
587
+ // `categories.forEach(...)` picks up the change on the next redraw too
588
+ categories = updatedCategories
589
+ const updatedRange = AxisInterval.from(
590
+ Math.min(range.start, 0),
591
+ Math.max(range.end, plotDimensions.height)
592
+ )
593
+ scale.domain(categories).range(updatedRange.asTuple())
594
+ currentDimensions = plotDimensions
595
+ currentMargin = margin
451
596
  setAxisRangeFor(axisId, range)
452
597
  setOriginalAxisRangeFor(axisId, originalRange)
453
- return categorySize
598
+ cc.requestRedraw()
599
+ return categorySize()
600
+ },
601
+ setHighlighted: (isHighlighted, color, lineWidth) => {
602
+ highlighted = isHighlighted
603
+ if (color !== undefined) highlightColor = color
604
+ if (lineWidth !== undefined) highlightLineWidth = lineWidth
605
+ cc.requestRedraw()
606
+ },
607
+ updateFont: (font) => {
608
+ // the font's color is used for the label, the axis line, the ticks, and the tick
609
+ // labels; the tick labels' size, family, and weight stay at axisTickStyle.font's
610
+ currentFont = font
611
+ cc.requestRedraw()
454
612
  }
455
613
  }
456
614
  }
457
615
 
458
616
  /**
459
- * Updates the category axis representing the x-axis, calculates and returns the
460
- * number of pixels for each category on the x-axis.
461
- * @param chartId The unique ID of the chart to which this axis belongs
462
- * @param axis The category axis to be updated
463
- * @param svg The SVG selection (d3)
464
- * @param location The location of the axis
465
- * @param names An array holding the category names
466
- * @param axesLabelFont The font for the axis label
467
- * @param plotDimensions The dimensions of the plot
468
- * @param margin The plot margin
469
- * @param range The range of the axis (e.g., start and end) in pixels
470
- * @param axesLabelFont The font for the axis label
471
- * @param plotDimensions The dimensions of the plot
472
- * @param margin The plot margin
473
- * adjust the axis label location when needed
474
- * @return The number of pixels for each category
617
+ * Removes a category x-axis' draw function from the canvas context (e.g. when the axis unmounts)
618
+ * @param cc The canvas context the axis' draw function is registered with
619
+ * @param axisId The ID of the axis
475
620
  */
476
- function updateOrdinalStringXAxis(
477
- chartId: number,
478
- axis: OrdinalStringAxis,
479
- svg: SvgSelection,
480
- location: AxisLocation.Bottom | AxisLocation.Top,
481
- names: Array<string>, // domain (categories)
482
- range: AxisInterval, // range (pixels)
483
- axesLabelFont: AxesFont,
484
- plotDimensions: Dimensions,
485
- margin: Margin
486
- ): number {
487
- const updatedRange = AxisInterval.from(
488
- Math.min(range.start, 0),
489
- Math.max(range.end, plotDimensions.width)
490
- )
491
-
492
- axis.scale = axis.scale
493
- .domain(names)
494
- .range(updatedRange.asTuple())
495
-
496
- axis.categorySize = axis.scale.bandwidth()
497
-
498
- const clipPathId = setClipPath(chartId, axis.axisId, svg, location, plotDimensions, margin)
499
- axis.selection = axis.selection
500
- .attr('transform', `translate(${margin.left}, ${yTranslation(location, plotDimensions, margin)})`)
501
- .attr("clip-path", `url(#${clipPathId})`)
502
- .call(axis.generator)
503
-
504
- const xLabelTranslation = ordinalLabelXTranslation(location, plotDimensions, margin, axesLabelFont)
505
- const yLabelTranslation = ordinalLabelYTranslation(location, plotDimensions, margin)
506
- svg
507
- .select(`#${labelIdFor(chartId, location)}`)
508
- .attr('transform', `translate(${xLabelTranslation}, ${yLabelTranslation})`)
509
-
510
- return axis.categorySize
621
+ export function removeOrdinalXAxis(cc: CanvasContext, axisId: string): void {
622
+ cc.unregister(`x-axis-ordinal-${cc.chartId}-${axisId}`)
511
623
  }
512
624
 
513
625
  /**
514
- * Updates the category axis representing the y-axis, calculates and returns the
515
- * number of pixels for each category on the y-axis.
516
- * @param chartId The unique ID of the chart to which this axis belongs
517
- * @param axis The category axis to be updated
518
- * @param svg The SVG selection (d3)
519
- * @param location The location of the axis
520
- * @param names An array holding the category names
521
- * @param range The range of the axis (e.g. start and end)
522
- * @param axesLabelFont The font for the axis label
523
- * @param plotDimensions The dimensions of the plot
524
- * @param margin The plot margin
525
- * @param axesLabelFont The font for the axis label
526
- * @param plotDimensions The dimensions of the plot
527
- * @param margin The plot margin
528
- * @return The number of pixels for each category
626
+ * Removes a category y-axis' draw function from the canvas context (e.g. when the axis unmounts)
627
+ * @param cc The canvas context the axis' draw function is registered with
628
+ * @param axisId The ID of the axis
529
629
  */
530
- function updateOrdinalStringYAxis(
531
- chartId: number,
532
- axis: OrdinalStringAxis,
533
- svg: SvgSelection,
534
- location: AxisLocation.Left | AxisLocation.Right,
535
- names: Array<string>, // domain
536
- range: AxisInterval, // range
537
- axesLabelFont: AxesFont,
538
- plotDimensions: Dimensions,
539
- margin: Margin,
540
- ): number {
541
- const updatedRange = AxisInterval.from(
542
- Math.min(range.start, 0),
543
- Math.max(range.end, plotDimensions.height)
544
- )
545
-
546
- axis.scale = axis.scale
547
- .domain(names)
548
- .range(updatedRange.asTuple())
549
- axis.categorySize = axis.scale.bandwidth()
550
- axis.selection = axis.selection
551
- .attr('transform', `translate(${xTranslation(location, plotDimensions, margin)}, ${margin.top})`)
552
- .call(axis.generator)
553
-
554
- svg
555
- .select(`#${labelIdFor(chartId, location)}`)
556
- .attr('transform', `translate(${ordinalLabelXTranslation(location, plotDimensions, margin, axesLabelFont)}, ${ordinalLabelYTranslation(location, plotDimensions, margin)}) rotate(-90)`)
557
-
558
- return axis.categorySize
630
+ export function removeOrdinalYAxis(cc: CanvasContext, axisId: string): void {
631
+ cc.unregister(`y-axis-ordinal-${cc.chartId}-${axisId}`)
559
632
  }
560
633
 
561
634
  /**
562
- * Calculates the number of pixels by which to translate the label in the x-direction for the axis.
563
- * The calculation uses the dimensions and margins of the plot, the location of the
564
- * axis, and the font for the label.
565
- * @param location The axis location (i.e. top, bottom, left, right)
635
+ * The x-coordinate (in canvas pixels, from the canvas' left edge) of a category axis' label: the
636
+ * label's font size in from the canvas' left or right edge for a y-axis, or the canvas' center for
637
+ * an x-axis
638
+ * @param location The location of the axis
566
639
  * @param plotDimensions The dimensions of the plot
567
- * @param margin The margins for the plot
640
+ * @param margin The plot margin
568
641
  * @param axesLabelFont The font for the axis label
569
- * @return The number of pixels to translate the label in the x-direction
642
+ * @return The x-coordinate of the axis label
570
643
  */
571
644
  function ordinalLabelXTranslation(
572
645
  location: AxisLocation,
@@ -587,13 +660,12 @@ function ordinalLabelXTranslation(
587
660
  }
588
661
 
589
662
  /**
590
- * Calculates the number of pixels by which to translate the label in the y-direction the axis.
591
- * The calculation uses the dimensions and margins of the plot, the location of the
592
- * axis, and the font for the label.
593
- * @param location The axis location (i.e. top, bottom, left, right)
663
+ * The y-coordinate (in canvas pixels, from the canvas' top edge) of a category axis' label: the
664
+ * canvas' middle for a y-axis, or the canvas' top or bottom edge for an x-axis
665
+ * @param location The location of the axis
594
666
  * @param plotDimensions The dimensions of the plot
595
- * @param margin The margins for the plot
596
- * @return The number of pixels to translate the label for the y-direction
667
+ * @param margin The plot margin
668
+ * @return The y-coordinate of the axis label
597
669
  */
598
670
  function ordinalLabelYTranslation(
599
671
  location: AxisLocation,
@@ -617,142 +689,190 @@ function ordinalLabelYTranslation(
617
689
  */
618
690
 
619
691
  /**
620
- * Adds a new empty x-axis to the SVG element at the specified location. An empty axis
692
+ * Adds a new, empty x-axis (a line with no ticks or label) to the canvas context. An empty axis
621
693
  * is just a line where the axis would be, without any ticks or labels.
694
+ * @param canvasContext The canvas context to register the axis' draw function with
622
695
  * @param axisId The ID of the axis
623
- * @param svg The SVG selection to which to add the axis
624
696
  * @param plotDimensions The dimensions of the plot
625
697
  * @param location The location of the axis
626
- * @param scaleGenerator The higher-order function that returns the axis d3 "scale" function
627
- * @param domain The axis range (start, end)
628
- * @param margin The plot margins for the border of main SVG group
629
- * @param setAxisRangeFor A callback used to set the axis range
630
- * @return A {@link ContinuousNumericAxis} based on the arguments to this function
698
+ * @param scaleGenerator The d3 scale to use for the axis (its domain and range get overwritten)
699
+ * @param margin The plot margins
700
+ * @param setAxisRangeFor A callback used to report the axis' domain when it's updated
701
+ * @param color The color of the axis line (the axis' `updateFont` updates it with the font's color)
702
+ * @param [domain=[0, 1]] The axis' initial domain (min, max)
703
+ * @return A {@link ContinuousNumericAxis} (marked `isEmpty`) based on the arguments to this function
631
704
  */
632
705
  export function addEmptyXAxis(
706
+ canvasContext: CanvasContext,
633
707
  axisId: string,
634
- svg: SvgSelection,
635
708
  plotDimensions: Dimensions,
636
- location: AxisLocation.Bottom | AxisLocation.Top,
709
+ location: typeof AxisLocation.Bottom | typeof AxisLocation.Top,
637
710
  scaleGenerator: ScaleContinuousNumeric<number, number>,
638
711
  margin: Margin,
639
712
  setAxisRangeFor: (axisId: string, timeRange: AxisInterval) => void,
713
+ color: string,
640
714
  domain: [minValue: number, maxValue: number] = [0, 1],
641
715
  ): ContinuousNumericAxis {
642
716
  const scale = scaleGenerator.domain(domain).range([0, plotDimensions.width])
643
717
 
644
- const selection = svg
645
- .append<SVGGElement>('g')
646
- .attr('transform', `translate(${margin.left}, ${yTranslation(location, plotDimensions, margin)})`)
718
+ let currentDimensions = plotDimensions
719
+ let currentMargin = margin
720
+ // NOTE: previously this read `ctx.fillStyle` directly, assuming it reflected "the chart's
721
+ // current color". But `ctx` is shared across every draw function registered on the canvas
722
+ // (all axes, all plots, the tracker). Whichever one last set `fillStyle` before this one
723
+ // ran left that value behind. It had nothing to do with this axis' actual color and never
724
+ // updated when the chart's `color`/theme changed, since nothing here ever assigned a new
725
+ // value in response to that. The `currentColor` is now explicit, passed in and refreshable via
726
+ // `updateFont` below.
727
+ let currentColor = color
728
+ let highlighted = false
729
+ let highlightColor = defaultLineStyle().highlightColor
730
+ let highlightLineWidth = defaultLineStyle().highlightWidth
731
+
732
+ const drawHandle: DrawHandle = `x-axis-empty-${canvasContext.chartId}-${axisId}`
733
+
734
+ const draw = (context: CanvasContext) => {
735
+ const {context2D} = context
736
+ context2D.save()
737
+ context2D.translate(currentMargin.left, yTranslation(location, currentDimensions, currentMargin))
738
+ context2D.strokeStyle = highlighted ? highlightColor : currentColor
739
+ context2D.lineWidth = highlighted ? highlightLineWidth : 1
740
+ context2D.beginPath()
741
+ context2D.moveTo(0, 0)
742
+ context2D.lineTo(currentDimensions.width, 0)
743
+ context2D.stroke()
744
+ context2D.restore()
745
+ }
647
746
 
648
- const line = selection.append('line')
649
- .attr('x1', 0)
650
- .attr('y1', 0)
651
- .attr('x2', plotDimensions.width)
652
- .attr('y2', 0)
653
- .attr('stroke', 'currentColor')
747
+ canvasContext.register(drawHandle, draw, 0)
654
748
 
655
- const axis: ContinuousNumericAxis = {
749
+ return {
656
750
  axisId,
657
751
  location,
658
- selection,
752
+ axisType: AxisType.ContinuousNumeric,
753
+ isEmpty: true,
659
754
  scale,
660
- generator: d3.axisBottom(scale).tickValues([]).tickSize(0),
661
- update: noop
662
- }
663
- return {
664
- ...axis,
665
- update: (domain: AxisInterval, plotDimensions: Dimensions, margin: Margin) => {
666
- axis.scale.domain(domain.asTuple()).range([0, plotDimensions.width])
667
- axis.selection
668
- .attr('transform', `translate(${margin.left}, ${yTranslation(location, plotDimensions, margin)})`)
669
-
670
- line.attr('x2', plotDimensions.width)
671
-
755
+ update: (domain, plotDimensions, margin) => {
756
+ scale.domain(domain.asTuple()).range([0, plotDimensions.width])
757
+ currentDimensions = plotDimensions
758
+ currentMargin = margin
672
759
  setAxisRangeFor(axisId, domain)
760
+ canvasContext.requestRedraw()
761
+ },
762
+ // an empty axis has no ticks or label text but does have a colored line -- reuse
763
+ // updateFont's color field to keep that line in sync with the chart's color (e.g., on a
764
+ // theme change), matching the pattern used by the other axis types
765
+ updateFont: (font) => {
766
+ currentColor = font.color
767
+ canvasContext.requestRedraw()
768
+ },
769
+ setHighlighted: (isHighlighted, color, lineWidth) => {
770
+ highlighted = isHighlighted
771
+ if (color !== undefined) highlightColor = color
772
+ if (lineWidth !== undefined) highlightLineWidth = lineWidth
773
+ canvasContext.requestRedraw()
673
774
  }
674
775
  }
675
776
  }
676
777
 
677
778
  /**
678
- * Adds a new empty y-axis to the SVG element at the specified location. An empty axis
679
- * is just a line where the axis would be, without any ticks or labels.
779
+ * Adds a new, empty y-axis (a line with no ticks or label) to the canvas context.
780
+ * @param cc The canvas context to register the axis' draw function with
680
781
  * @param axisId The ID of the axis
681
- * @param svg The SVG selection to which to add the axis
682
782
  * @param plotDimensions The dimensions of the plot
683
783
  * @param location The location of the axis
684
- * @param scaleGenerator The higher-order function that returns the axis d3 "scale" function
685
- * @param domain The axis range (start, end)
686
- * @param margin The plot margins for the border of main SVG group
687
- * @param setAxisRangeFor A callback used to set the axis range
688
- * @return A {@link ContinuousNumericAxis} based on the arguments to this function
784
+ * @param scaleGenerator The d3 scale to use for the axis (its domain and range get overwritten)
785
+ * @param margin The plot margins
786
+ * @param setAxisRangeFor A callback used to report the axis' domain when it's updated
787
+ * @param color The color of the axis line (the axis' `updateFont` updates it with the font's color)
788
+ * @param [domain=[0, 1]] The axis' initial domain (min, max)
789
+ * @return A {@link ContinuousNumericAxis} (marked `isEmpty`) based on the arguments to this function
689
790
  */
690
791
  export function addEmptyYAxis(
792
+ cc: CanvasContext,
691
793
  axisId: string,
692
- svg: SvgSelection,
693
794
  plotDimensions: Dimensions,
694
- location: AxisLocation.Left | AxisLocation.Right,
795
+ location: typeof AxisLocation.Left | typeof AxisLocation.Right,
695
796
  scaleGenerator: ScaleContinuousNumeric<number, number>,
696
797
  margin: Margin,
697
798
  setAxisRangeFor: (axisId: string, timeRange: AxisInterval) => void,
799
+ color: string,
698
800
  domain: [minValue: number, maxValue: number] = [0, 1],
699
801
  ): ContinuousNumericAxis {
700
802
  const scale = scaleGenerator.domain(domain).range([plotDimensions.height, 0])
701
803
 
702
- const selection = svg
703
- .append<SVGGElement>('g')
704
- .attr('transform', `translate(${xTranslation(location, plotDimensions, margin)}, ${margin.top})`)
804
+ let currentDimensions = plotDimensions
805
+ let currentMargin = margin
806
+ let currentColor = color
807
+ let highlighted = false
808
+ let highlightColor = defaultLineStyle().highlightColor
809
+ let highlightLineWidth = defaultLineStyle().highlightWidth
810
+
811
+ const drawHandle: DrawHandle = `y-axis-empty-${cc.chartId}-${axisId}`
812
+
813
+ const draw = (context: CanvasContext) => {
814
+ const {context2D} = context
815
+ context2D.save()
816
+ context2D.translate(xTranslation(location, currentDimensions, currentMargin), currentMargin.top)
817
+ context2D.strokeStyle = highlighted ? highlightColor : currentColor
818
+ context2D.lineWidth = highlighted ? highlightLineWidth : 1
819
+ context2D.beginPath()
820
+ context2D.moveTo(0, 0)
821
+ context2D.lineTo(0, currentDimensions.height)
822
+ context2D.stroke()
823
+ context2D.restore()
824
+ }
705
825
 
706
- const line = selection.append('line')
707
- .attr('x1', 0)
708
- .attr('y1', 0)
709
- .attr('x2', 0)
710
- .attr('y2', plotDimensions.height)
711
- .attr('stroke', 'currentColor')
826
+ cc.register(drawHandle, draw, 0)
712
827
 
713
- const axis: ContinuousNumericAxis = {
828
+ return {
714
829
  axisId,
715
830
  location,
716
- selection,
831
+ axisType: AxisType.ContinuousNumeric,
832
+ isEmpty: true,
717
833
  scale,
718
- generator: d3.axisLeft(scale).tickValues([]).tickSize(0),
719
- update: noop
720
- }
721
- return {
722
- ...axis,
723
- update: (domain: AxisInterval, plotDimensions: Dimensions, margin: Margin) => {
724
- axis.scale.domain(domain.asTuple()).range([plotDimensions.height, 0])
725
- axis.selection
726
- .attr('transform', `translate(${xTranslation(location, plotDimensions, margin)}, ${margin.top})`)
727
-
728
- line.attr('y2', plotDimensions.height)
729
-
834
+ update: (domain, plotDimensions, margin) => {
835
+ scale.domain(domain.asTuple()).range([plotDimensions.height, 0])
836
+ currentDimensions = plotDimensions
837
+ currentMargin = margin
730
838
  setAxisRangeFor(axisId, domain)
839
+ cc.requestRedraw()
840
+ },
841
+ // an empty axis has no ticks or label text but does have a colored line -- reuse
842
+ // updateFont's color field to keep that line in sync with the chart's color (e.g., on a
843
+ // theme change), matching the pattern used by the other axis types
844
+ updateFont: (font) => {
845
+ currentColor = font.color
846
+ cc.requestRedraw()
847
+ },
848
+ setHighlighted: (isHighlighted, color, lineWidth) => {
849
+ highlighted = isHighlighted
850
+ if (color !== undefined) highlightColor = color
851
+ if (lineWidth !== undefined) highlightLineWidth = lineWidth
852
+ cc.requestRedraw()
731
853
  }
732
854
  }
733
855
  }
734
856
 
735
857
  /**
736
- * Adds a new x-axis to the SVG element at the specified location
737
- * @param chartId The ID of the chart
738
- * @param axisId The ID of the axis
739
- * @param svg The SVG selection to which to add the axis
858
+ * Adds a new x-axis, registering its draw function with the canvas context.
859
+ * @param cc The canvas context to register the axis' draw function with
860
+ * @param axisId A unique ID for the axis
740
861
  * @param plotDimensions The dimensions of the plot
741
- * @param location The location of the axis
742
- * @param scaleGenerator The higher-order function that returns the axis d3 "scale" function
743
- * @param domain The axis range (start, end)
744
- * @param axesLabelFont The font for the axis labels
745
- * @param margin The plot margins for the border of main SVG group
862
+ * @param location The location of the axis (top or bottom)
863
+ * @param scaleGenerator The d3 scale to use for the axis (its domain/range get overwritten)
864
+ * @param domain The axis' initial domain (min, max)
865
+ * @param axesLabelFont The font for the axis label and ticks
866
+ * @param margin The plot margins
746
867
  * @param axisLabel The label for the axis
747
- * @param setAxisRangeFor A callback used to set the axis range
748
- * @return A {@link ContinuousNumericAxis} based on the arguments to this function
868
+ * @param setAxisRangeFor Callback used to report the axis' domain when it's updated
869
+ * @return A {@link ContinuousNumericAxis}
749
870
  */
750
871
  export function addContinuousNumericXAxis(
751
- chartId: number,
872
+ cc: CanvasContext,
752
873
  axisId: string,
753
- svg: SvgSelection,
754
874
  plotDimensions: Dimensions,
755
- location: AxisLocation.Bottom | AxisLocation.Top,
875
+ location: typeof AxisLocation.Bottom | typeof AxisLocation.Top,
756
876
  scaleGenerator: ScaleContinuousNumeric<number, number>,
757
877
  domain: [minValue: number, maxValue: number],
758
878
  axesLabelFont: AxesFont,
@@ -762,104 +882,112 @@ export function addContinuousNumericXAxis(
762
882
  ): ContinuousNumericAxis {
763
883
  const scale = scaleGenerator.domain(domain).range([0, plotDimensions.width])
764
884
 
765
- const selection = svg
766
- .append<SVGGElement>('g')
767
- .attr('transform', `translate(${margin.left}, ${yTranslation(location, plotDimensions, margin)})`)
768
-
769
- svg
770
- .append<SVGTextElement>('text')
771
- .attr('id', labelIdFor(chartId, location))
772
- .attr('text-anchor', 'middle')
773
- // when the axis is at the top, want the label to hang from the top (y=0) of the plot
774
- .attr('dominant-baseline', location === AxisLocation.Top ? "hanging" : "baseline")
775
- .attr('font-size', axesLabelFont.size)
776
- .attr('fill', axesLabelFont.color)
777
- .attr('font-family', axesLabelFont.family)
778
- .attr('font-weight', axesLabelFont.weight)
779
- .attr('transform', `translate(${margin.left + plotDimensions.width / 2}, ${continuousLabelYTranslation(location, plotDimensions, margin)})`)
780
- .text(axisLabel)
781
-
782
- const axis: ContinuousNumericAxis = {
885
+ let currentDimensions = plotDimensions
886
+ let currentMargin = margin
887
+ const currentLabel = axisLabel
888
+ let currentFont = axesLabelFont
889
+ let highlighted = false
890
+ let highlightColor = defaultLineStyle().highlightColor
891
+ let highlightLineWidth = defaultLineStyle().highlightWidth
892
+
893
+ const drawHandle: DrawHandle = `x-axis-${cc.chartId}-${axisId}`
894
+
895
+ const draw = (context: CanvasContext) => {
896
+ const {context2D} = context
897
+ context2D.save()
898
+ context2D.translate(currentMargin.left, yTranslation(location, currentDimensions, currentMargin))
899
+
900
+ // NOTE: the original SVG version never applied a clip-path to the continuous numeric
901
+ // axes (only the ordinal/category axes clip), which let the first/last tick labels
902
+ // overflow into the margin without being cut off. Clipping to exactly the plot width
903
+ // here would chop those end labels in half, so ticks/labels are intentionally unclipped.
904
+ const axisColor = highlighted ? highlightColor : currentFont.color
905
+ context2D.strokeStyle = axisColor
906
+ context2D.fillStyle = axisColor
907
+ context2D.lineWidth = highlighted ? highlightLineWidth : 1
908
+ context2D.font = fontStringFor(currentFont.size, currentFont.family, currentFont.weight)
909
+ context2D.textAlign = 'center'
910
+ context2D.textBaseline = location === AxisLocation.Bottom ? 'top' : 'bottom'
911
+
912
+ // domain line
913
+ context2D.beginPath()
914
+ context2D.moveTo(0, 0)
915
+ context2D.lineTo(currentDimensions.width, 0)
916
+ context2D.stroke()
917
+
918
+ // ticks
919
+ const tickDirection = location === AxisLocation.Bottom ? 1 : -1
920
+ scale.ticks().forEach(tickValue => {
921
+ const x = scale(tickValue)
922
+ context2D.beginPath()
923
+ context2D.moveTo(x, 0)
924
+ context2D.lineTo(x, TICK_SIZE * tickDirection)
925
+ context2D.stroke()
926
+ context2D.fillText(
927
+ scale.tickFormat()(tickValue),
928
+ x,
929
+ (TICK_SIZE + TICK_PADDING) * tickDirection
930
+ )
931
+ })
932
+
933
+ // axis label (not clipped, matches the old behavior)
934
+ context2D.fillStyle = axisColor
935
+ context2D.font = fontStringFor(currentFont.size, currentFont.family, currentFont.weight)
936
+ context2D.textAlign = 'center'
937
+ // (a bottom label sits on the canvas' bottom edge, so it's drawn above it, descenders and all)
938
+ context2D.textBaseline = location === AxisLocation.Top ? 'hanging' : 'bottom'
939
+ const labelY = continuousLabelYTranslation(location, currentDimensions, currentMargin) - yTranslation(location, currentDimensions, currentMargin)
940
+ context2D.fillText(currentLabel, currentDimensions.width / 2, labelY)
941
+
942
+ context2D.restore()
943
+ }
944
+
945
+ cc.register(drawHandle, draw, 0)
946
+
947
+ return {
783
948
  axisId,
784
949
  location,
785
- selection,
950
+ axisType: AxisType.ContinuousNumeric,
786
951
  scale,
787
- generator: location === AxisLocation.Bottom ? d3.axisBottom(scale) : d3.axisTop(scale),
788
- update: noop
789
- }
790
- return {
791
- ...axis,
792
- update: (domain: AxisInterval, plotDimensions: Dimensions, margin: Margin) => {
793
- updateLinearXAxis(domain, axisLabel, chartId, svg, axis, plotDimensions, margin, location)
952
+ update: (domain, plotDimensions, margin) => {
953
+ scale.domain(domain.asTuple()).range([0, plotDimensions.width])
954
+ currentDimensions = plotDimensions
955
+ currentMargin = margin
794
956
  setAxisRangeFor(axisId, domain)
957
+ cc.requestRedraw()
958
+ },
959
+ updateFont: (font) => {
960
+ currentFont = font
961
+ cc.requestRedraw()
962
+ },
963
+ setHighlighted: (isHighlighted, color, lineWidth) => {
964
+ highlighted = isHighlighted
965
+ if (color !== undefined) highlightColor = color
966
+ if (lineWidth !== undefined) highlightLineWidth = lineWidth
967
+ cc.requestRedraw()
795
968
  }
796
969
  }
797
970
  }
798
971
 
799
972
  /**
800
- * Updates the x-axis with the new domain and axis label
801
- * @param domain The new (start, end) range of the axis
802
- * @param label The new label for the axis
803
- * @param chartId The ID of the chart
804
- * @param svg The SVG selection of which the axis is a child node
805
- * @param axis The x-axis
806
- * @param plotDimensions The dimensions of the plot
807
- * @param margin The plot margins for the border of main SVG group
808
- * @param location The location of the axis (i.e. top, bottom, left, right)
809
- */
810
- function updateLinearXAxis(
811
- domain: AxisInterval,
812
- label: string,
813
- chartId: number,
814
- svg: SvgSelection,
815
- axis: ContinuousNumericAxis,
816
- plotDimensions: Dimensions,
817
- margin: Margin,
818
- location: AxisLocation.Bottom | AxisLocation.Top,
819
- ): void {
820
- axis.scale.domain(domain.asTuple()).range([0, plotDimensions.width])
821
-
822
- axis.selection
823
- .attr('transform', `translate(${margin.left}, ${yTranslation(location, plotDimensions, margin)})`)
824
- .call(axis.generator)
825
- svg
826
- .select(`#${labelIdFor(chartId, location)}`)
827
- .attr('transform', `translate(${margin.left + plotDimensions.width / 2}, ${continuousLabelYTranslation(location, plotDimensions, margin)})`)
828
- .text(label)
829
- }
830
-
831
- /**
832
- * The number of pixels to translate the x-axis label to the right
833
- * @param location The location of the x-axis
834
- * @param plotDimensions The dimensions of the plot
835
- * @param margin The plot margins for the border of main SVG group
836
- * @return The number of pixels to translate the x-axis label to the right
837
- */
838
- function continuousLabelYTranslation(location: AxisLocation.Bottom | AxisLocation.Top, plotDimensions: Dimensions, margin: Margin): number {
839
- return location === AxisLocation.Bottom ? plotDimensions.height + margin.top + margin.bottom : 0
840
- }
841
-
842
- /**
843
- * Adds a new y-axis to the SVG element at the specified location
844
- * @param chartId The ID of the chart
845
- * @param axisId The ID of the axis
846
- * @param svg The SVG selection to which to add the axis
973
+ * Adds a new y-axis, registering its draw function with the canvas context.
974
+ * @param cc The canvas context to register the axis' draw function with
975
+ * @param axisId A unique ID for the axis
847
976
  * @param plotDimensions The dimensions of the plot
848
- * @param location The location of the axis
849
- * @param scaleGenerator The higher-order function that returns the axis d3 "scale" function
850
- * @param domain The axis range (start, end)
851
- * @param axesLabelFont The font for the axis labels
852
- * @param margin The plot margins for the border of main SVG group
977
+ * @param location The location of the axis (left or right)
978
+ * @param scaleGenerator The d3 scale to use for the axis (its domain/range get overwritten)
979
+ * @param domain The axis' initial domain (min, max)
980
+ * @param axesLabelFont The font for the axis label and ticks
981
+ * @param margin The plot margins
853
982
  * @param axisLabel The label for the axis
854
- * @param setAxisRangeFor A callback used to set the axis range
855
- * @return A {@link ContinuousNumericAxis} based on the arguments to this function
983
+ * @param setAxisRangeFor Callback used to report the axis' domain when it's updated
984
+ * @return A {@link ContinuousNumericAxis}
856
985
  */
857
986
  export function addContinuousNumericYAxis(
858
- chartId: number,
987
+ cc: CanvasContext,
859
988
  axisId: string,
860
- svg: SvgSelection,
861
989
  plotDimensions: Dimensions,
862
- location: AxisLocation.Left | AxisLocation.Right,
990
+ location: typeof AxisLocation.Left | typeof AxisLocation.Right,
863
991
  scaleGenerator: ScaleContinuousNumeric<number, number>,
864
992
  domain: [minValue: number, maxValue: number],
865
993
  axesLabelFont: AxesFont,
@@ -871,85 +999,113 @@ export function addContinuousNumericYAxis(
871
999
  .domain(domain)
872
1000
  .range([Math.max(margin.bottom, plotDimensions.height), 0])
873
1001
 
874
- const selection = svg
875
- .append<SVGGElement>('g')
876
- .attr('class', 'y-axis')
877
- .attr('transform', `translate(${xTranslation(location, plotDimensions, margin)}, ${margin.top})`)
878
-
879
- svg
880
- .append<SVGTextElement>('text')
881
- .attr('id', labelIdFor(chartId, location))
882
- .attr('text-anchor', 'start')
883
- .attr('font-size', axesLabelFont.size)
884
- .attr('fill', axesLabelFont.color)
885
- .attr('font-family', axesLabelFont.family)
886
- .attr('font-weight', axesLabelFont.weight)
887
- .attr('transform', `translate(${continuousLabelXTranslation(location, plotDimensions, margin, axesLabelFont)}, ${margin.top + plotDimensions.height / 2}) rotate(-90)`)
888
- .text(axisLabel)
889
-
890
- const axis: ContinuousNumericAxis = {
1002
+ let currentDimensions = plotDimensions
1003
+ let currentMargin = margin
1004
+ const currentLabel = axisLabel
1005
+ let currentFont = axesLabelFont
1006
+ let highlighted = false
1007
+ let highlightColor = defaultLineStyle().highlightColor
1008
+ let highlightLineWidth = defaultLineStyle().highlightWidth
1009
+
1010
+ const drawHandle: DrawHandle = `y-axis-${cc.chartId}-${axisId}`
1011
+
1012
+ const draw = (context: CanvasContext) => {
1013
+ const {context2D} = context
1014
+ context2D.save()
1015
+ context2D.translate(xTranslation(location, currentDimensions, currentMargin), currentMargin.top)
1016
+
1017
+ // NOTE: unclipped, matching the old SVG version's continuous numeric axes (only the
1018
+ // ordinal/category axes clipped there) -- clipping to exactly the plot height would cut
1019
+ // off the top- and bottom-most tick labels, which straddle y=0 and y=height.
1020
+ const axisColor = highlighted ? highlightColor : currentFont.color
1021
+ context2D.strokeStyle = axisColor
1022
+ context2D.fillStyle = axisColor
1023
+ context2D.lineWidth = highlighted ? highlightLineWidth : 1
1024
+ context2D.font = fontStringFor(currentFont.size, currentFont.family, currentFont.weight)
1025
+ context2D.textBaseline = 'middle'
1026
+ context2D.textAlign = location === AxisLocation.Left ? 'right' : 'left'
1027
+
1028
+ // domain line
1029
+ context2D.beginPath()
1030
+ context2D.moveTo(0, 0)
1031
+ context2D.lineTo(0, currentDimensions.height)
1032
+ context2D.stroke()
1033
+
1034
+ // ticks
1035
+ const tickDirection = location === AxisLocation.Left ? -1 : 1
1036
+ scale.ticks().forEach(tickValue => {
1037
+ const y = scale(tickValue)
1038
+ context2D.beginPath()
1039
+ context2D.moveTo(0, y)
1040
+ context2D.lineTo(TICK_SIZE * tickDirection, y)
1041
+ context2D.stroke()
1042
+ context2D.fillText(
1043
+ scale.tickFormat()(tickValue),
1044
+ (TICK_SIZE + TICK_PADDING) * tickDirection,
1045
+ y
1046
+ )
1047
+ })
1048
+
1049
+ // axis label, rotated -90deg, matching the old SVG version
1050
+ context2D.save()
1051
+ const labelX = continuousLabelXTranslation(location, currentDimensions, currentMargin, currentFont) - xTranslation(location, currentDimensions, currentMargin)
1052
+ context2D.translate(labelX, currentDimensions.height / 2)
1053
+ context2D.rotate(-Math.PI / 2)
1054
+ context2D.fillStyle = axisColor
1055
+ context2D.font = fontStringFor(currentFont.size, currentFont.family, currentFont.weight)
1056
+ context2D.textAlign = 'center'
1057
+ context2D.textBaseline = 'alphabetic'
1058
+ context2D.fillText(currentLabel, 0, 0)
1059
+ context2D.restore()
1060
+
1061
+ context2D.restore()
1062
+ }
1063
+
1064
+ cc.register(drawHandle, draw, 0)
1065
+
1066
+ return {
891
1067
  axisId,
892
1068
  location,
893
- selection,
1069
+ axisType: AxisType.ContinuousNumeric,
894
1070
  scale,
895
- generator: location === AxisLocation.Left ? d3.axisLeft(scale) : d3.axisRight(scale),
896
- update: noop
897
- }
898
- return {
899
- ...axis,
900
- update: (domain: AxisInterval, plotDimensions: Dimensions, margin: Margin) => {
901
- updateLinearYAxis(domain, axisLabel, chartId, svg, axis, plotDimensions, margin, axesLabelFont, location)
1071
+ update: (domain, plotDimensions, margin) => {
1072
+ scale.domain(domain.asTuple()).range([Math.max(margin.bottom, plotDimensions.height), 0])
1073
+ currentDimensions = plotDimensions
1074
+ currentMargin = margin
902
1075
  setAxisRangeFor(axisId, domain)
1076
+ cc.requestRedraw()
1077
+ },
1078
+ updateFont: (font) => {
1079
+ currentFont = font
1080
+ cc.requestRedraw()
1081
+ },
1082
+ setHighlighted: (isHighlighted, color, lineWidth) => {
1083
+ highlighted = isHighlighted
1084
+ if (color !== undefined) highlightColor = color
1085
+ if (lineWidth !== undefined) highlightLineWidth = lineWidth
1086
+ cc.requestRedraw()
903
1087
  }
904
1088
  }
905
1089
  }
906
1090
 
907
1091
  /**
908
- * Updates the y-axis with the new domain and axis label
909
- * @param domain The new (start, end) range of the axis
910
- * @param label The new label for the axis
911
- * @param chartId The ID of the chart
912
- * @param svg The SVG selection of which the axis is a child node
913
- * @param axis The y-axis
914
- * @param plotDimensions The dimensions of the plot
915
- * @param margin The plot margins for the border of main SVG group
916
- * @param axesLabelFont The font for the axis label
917
- * @param location The location of the axis (i.e. top, bottom, left, right)
1092
+ * Removes a continuous x-axis' draw function from the canvas context (called on "unmount" to avoid
1093
+ * leaking a draw registration for an axis that's no longer part of the chart).
1094
+ * @param cc The canvas context the axis' draw function is registered with
1095
+ * @param axisId The ID of the axis
918
1096
  */
919
- function updateLinearYAxis(
920
- domain: AxisInterval,
921
- label: string,
922
- chartId: number,
923
- svg: SvgSelection,
924
- axis: ContinuousNumericAxis,
925
- plotDimensions: Dimensions,
926
- margin: Margin,
927
- axesLabelFont: AxesFont,
928
- location: AxisLocation.Left | AxisLocation.Right,
929
- ): void {
930
- axis.scale.domain(domain.asTuple()).range([Math.max(margin.bottom, plotDimensions.height), 0])
931
- axis.selection
932
- .attr('transform', `translate(${xTranslation(location, plotDimensions, margin)}, ${margin.top})`)
933
- .call(axis.generator)
934
-
935
- svg
936
- .select(`#${labelIdFor(chartId, location)}`)
937
- .attr('transform', `translate(${continuousLabelXTranslation(location, plotDimensions, margin, axesLabelFont)}, ${margin.top + plotDimensions.height / 2}) rotate(-90)`)
938
- .text(label)
1097
+ export function removeContinuousXAxis(cc: CanvasContext, axisId: string): void {
1098
+ cc.unregister(`x-axis-${cc.chartId}-${axisId}`)
939
1099
  }
940
1100
 
941
1101
  /**
942
- * The number of pixels to translate the y-axis label down
943
- * @param location The location of the y-axis
944
- * @param plotDimensions The dimensions of the plot
945
- * @param margin The plot margins for the border of main SVG group
946
- * @param axesLabelFont The font for the axis label
947
- * @return The number of pixels to translate the y-axis label down
1102
+ * Removes a continuous y-axis' draw function from the canvas context (called on "unmount" to avoid
1103
+ * leaking a draw registration for an axis that's no longer part of the chart).
1104
+ * @param cc The canvas context the axis' draw function is registered with
1105
+ * @param axisId The ID of the axis
948
1106
  */
949
- function continuousLabelXTranslation(location: AxisLocation.Left | AxisLocation.Right, plotDimensions: Dimensions, margin: Margin, axesLabelFont: AxesFont): number {
950
- return location === AxisLocation.Left ?
951
- axesLabelFont.size :
952
- margin.left + plotDimensions.width + margin.right - axesLabelFont.size
1107
+ export function removeContinuousYAxis(cc: CanvasContext, axisId: string): void {
1108
+ cc.unregister(`y-axis-${cc.chartId}-${axisId}`)
953
1109
  }
954
1110
 
955
1111
  /*
@@ -957,46 +1113,57 @@ function continuousLabelXTranslation(location: AxisLocation.Left | AxisLocation.
957
1113
  */
958
1114
 
959
1115
  /**
960
- * Calculates the number of pixels by which to translate the x-axis.
961
- * @param location The axis location (i.e. top, bottom, left, right)
1116
+ * The x-coordinate (in canvas pixels) of a y-axis' line: the plot's left edge for a left axis, or
1117
+ * its right edge for a right axis
1118
+ * @param location The location of the axis
962
1119
  * @param plotDimensions The dimensions of the plot
963
- * @param margin The margins for the plot
964
- * @return The number of pixels to translate the x-axis
1120
+ * @param margin The plot margin
1121
+ * @return The x-coordinate of the axis line
965
1122
  */
966
- function xTranslation(location: AxisLocation.Left | AxisLocation.Right, plotDimensions: Dimensions, margin: Margin): number {
1123
+ function xTranslation(location: typeof AxisLocation.Left | typeof AxisLocation.Right, plotDimensions: Dimensions, margin: Margin): number {
967
1124
  return location === AxisLocation.Left ? margin.left : margin.left + plotDimensions.width
968
1125
  }
969
1126
 
970
1127
  /**
971
- * The number of pixels to translate the x-axis to the right
972
- * @param location The location of the x-axis
1128
+ * The y-coordinate (in canvas pixels) of an x-axis' line: the plot's bottom edge for a bottom
1129
+ * axis, or its top edge for a top axis
1130
+ * @param location The location of the axis
973
1131
  * @param plotDimensions The dimensions of the plot
974
- * @param margin The plot margins for the border of main SVG group
975
- * @return The number of pixels to translate the y-axis to the down
1132
+ * @param margin The plot margin
1133
+ * @return The y-coordinate of the axis line
976
1134
  */
977
- function yTranslation(location: AxisLocation.Bottom | AxisLocation.Top, plotDimensions: Dimensions, margin: Margin): number {
1135
+ function yTranslation(location: typeof AxisLocation.Bottom | typeof AxisLocation.Top, plotDimensions: Dimensions, margin: Margin): number {
978
1136
  return location === AxisLocation.Bottom ?
979
1137
  plotDimensions.height + margin.top :
980
1138
  margin.top
981
1139
  }
982
1140
 
983
1141
  /**
984
- * Calculates a unique ID for the axis based on the location and chart ID
985
- * @param chartId The unique ID of the chart
986
- * @param location The axis location (i.e. top, bottom, left, right)
987
- * @return A unique ID for the axis
1142
+ * The y-coordinate (in canvas pixels) of a continuous x-axis' label: the canvas' bottom edge for
1143
+ * a bottom axis, or its top edge for a top axis
1144
+ * @param location The location of the axis
1145
+ * @param plotDimensions The dimensions of the plot
1146
+ * @param margin The plot margin
1147
+ * @return The y-coordinate of the axis label
988
1148
  */
989
- export function labelIdFor(chartId: number, location: AxisLocation): string {
990
- switch (location) {
991
- case AxisLocation.Left:
992
- case AxisLocation.Right:
993
- return `stream-chart-y-axis-${location}-label-${chartId}`
994
- case AxisLocation.Top:
995
- case AxisLocation.Bottom:
996
- return `stream-chart-x-axis-${location}-label-${chartId}`
997
- }
1149
+ function continuousLabelYTranslation(location: typeof AxisLocation.Bottom | typeof AxisLocation.Top, plotDimensions: Dimensions, margin: Margin): number {
1150
+ return location === AxisLocation.Bottom ? plotDimensions.height + margin.top + margin.bottom : 0
998
1151
  }
999
1152
 
1153
+ /**
1154
+ * The x-coordinate (in canvas pixels) of a continuous y-axis' label: the label's font size in from
1155
+ * the canvas' left edge for a left axis, or from its right edge for a right axis
1156
+ * @param location The location of the axis
1157
+ * @param plotDimensions The dimensions of the plot
1158
+ * @param margin The plot margin
1159
+ * @param axesLabelFont The font for the axis label
1160
+ * @return The x-coordinate of the axis label
1161
+ */
1162
+ function continuousLabelXTranslation(location: typeof AxisLocation.Left | typeof AxisLocation.Right, plotDimensions: Dimensions, margin: Margin, axesLabelFont: AxesFont): number {
1163
+ return location === AxisLocation.Left ?
1164
+ axesLabelFont.size :
1165
+ margin.left + plotDimensions.width + margin.right - axesLabelFont.size
1166
+ }
1000
1167
 
1001
1168
  /*
1002
1169
  zooming
@@ -1007,79 +1174,60 @@ export function labelIdFor(chartId: number, location: AxisLocation): string {
1007
1174
  */
1008
1175
  export interface ZoomResult<AR extends BaseAxisRange> {
1009
1176
  range: AR
1010
- // range: ContinuousAxisRange
1011
- zoomFactor: number
1012
1177
  }
1013
1178
 
1014
- // /**
1015
- // * todo this can be generalized to any continuous numeric axis, replace x with value
1016
- // * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out. Zooms in/out
1017
- // * at the location of the mouse when the scroll wheel or gesture was applied.
1018
- // * @param transform The d3 zoom transformation information
1019
- // * @param x The x-position of the mouse when the scroll wheel or gesture is used
1020
- // * @param axis The axis being zoomed
1021
- // * @param range The current range for the axis being zoomed
1022
- // * @return The updated range and the new zoom factor
1023
- // */
1024
- // // export function calculateZoomFor<AR extends BaseAxisRange>(
1025
- // export function calculateZoomFor(
1026
- // transform: ZoomTransform,
1027
- // x: number,
1028
- // axis: ContinuousNumericAxis,
1029
- // range: ContinuousAxisRange,
1030
- // ): ZoomResult<ContinuousAxisRange> {
1031
- // const time = axis.generator.scale<ScaleLinear<number, number>>().invert(x);
1032
- // return {
1033
- // range: range.scale(transform.k, time),
1034
- // zoomFactor: transform.k
1035
- // } as ZoomResult<ContinuousAxisRange>
1036
- // }
1037
-
1038
1179
  /**
1039
1180
  * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out. Zooms in/out
1040
- * at the location of the mouse when the scroll wheel or gesture was applied, while ensure that
1041
- * the range (start, end) is contained within the constraint (min, max).
1042
- * @param transform The d3 zoom transformation information
1043
- * @param x The x-position of the mouse when the scroll wheel or gesture is used
1044
- * @param axis The axis being zoomed
1181
+ * around the specified domain value (the pivot), while ensuring that the range (start, end) is
1182
+ * contained within the constraint (min, max). The pivot is a domain value rather than a pixel
1183
+ * position -- while streaming, callers pin it to the axis's current "now" rather than to wherever
1184
+ * the mouse happens to be (see `calcZoomAndUpdate`'s `pivotDomainValueFor`), which is what keeps
1185
+ * "now" fixed on screen as the zoom widens/narrows the window around it.
1186
+ *
1187
+ * `zoomFactor` is *incremental* (relative to the range's `.current`, not its `.original`) -- see
1188
+ * {@link BaseAxisRange.scaledRange}. d3-zoom's own `transform.k` is cumulative from wherever
1189
+ * `__zoom` was last reset to identity, so callers must convert it to an incremental factor (this
1190
+ * event's `k` divided by the previously-applied `k`) before calling this -- see
1191
+ * `useCanvasZoomPan`.
1192
+ * @param zoomFactor The incremental zoom scale factor for this event
1193
+ * @param domainValue The domain value to zoom around (the pivot)
1045
1194
  * @param range The current range for the axis being zoomed
1046
1195
  * @param constraint The minimum and maximum value the scaled range can have
1047
- * @return The updated range and the new zoom factor
1196
+ * @return The updated range
1048
1197
  */
1049
1198
  export function calculateConstrainedZoomFor(
1050
- transform: ZoomTransform,
1051
- x: number,
1052
- axis: ContinuousNumericAxis,
1199
+ zoomFactor: number,
1200
+ domainValue: number,
1053
1201
  range: ContinuousAxisRange,
1054
1202
  constraint: [min: number, max: number],
1055
1203
  ): ZoomResult<ContinuousAxisRange> {
1056
- const domainValue = axis.generator.scale<ScaleLinear<number, number>>().invert(x);
1057
1204
  return {
1058
- range: range.constrainedScale(transform.k, domainValue, constraint),
1059
- zoomFactor: transform.k
1060
- } as ZoomResult<ContinuousAxisRange>
1205
+ range: range.constrainedScale(zoomFactor, domainValue, constraint),
1206
+ }
1061
1207
  }
1062
1208
 
1063
1209
  /**
1064
- * Calculates the zoom for an ordinal axis.
1065
- * @param transform The d3 zoom transformation information
1210
+ * Calculates the zoom for an ordinal axis. Unlike {@link calculateConstrainedZoomFor}'s continuous
1211
+ * axis, `zoomFactor` here is *cumulative* (d3-zoom's own `event.transform.k`, passed through
1212
+ * unmodified) -- see {@link OrdinalAxisRange.scaledCumulative} for why: the range is scaled from
1213
+ * `.current` by the incremental step the cumulative factor implies, except that a factor of
1214
+ * exactly 1 snaps it back to `.original`, so that zooming all the way out always shows every
1215
+ * category, whatever pivots were used along the way.
1216
+ * @param zoomFactor The cumulative zoom scale factor for this event
1066
1217
  * @param x The x-position of the mouse when the scroll wheel or gesture is used
1067
1218
  * @param range The current range for the axis being zoomed
1068
1219
  * @param constraint The minimum and maximum value the scaled range can have
1069
- * @return A ZoomResult holding the updated range and the new zoom factor
1220
+ * @return A ZoomResult holding the updated range
1070
1221
  */
1071
1222
  export function calculateOrdinalConstrainedZoomFor(
1072
- transform: ZoomTransform,
1223
+ zoomFactor: number,
1073
1224
  x: number,
1074
1225
  range: OrdinalAxisRange,
1075
1226
  constraint: [min: number, max: number],
1076
1227
  ): ZoomResult<OrdinalAxisRange> {
1077
- const updatedRange = range.constrainedScale(transform.k, x, constraint) as OrdinalAxisRange
1078
- const k = range.original.equals(updatedRange.original) ? 1 : transform.k
1079
1228
  return {
1080
- range: updatedRange,
1081
- zoomFactor: k
1082
- } as ZoomResult<OrdinalAxisRange>
1229
+ range: range.constrainedScale(zoomFactor, x, constraint) as OrdinalAxisRange,
1230
+ }
1083
1231
  }
1084
1232
 
1085
1233
  /*
@@ -1087,12 +1235,14 @@ export function calculateOrdinalConstrainedZoomFor(
1087
1235
  */
1088
1236
 
1089
1237
  /**
1090
- * Adjusts the range and updates the plot when the plot is dragged to the left or right
1091
- * @param delta The amount that the plot is dragged
1092
- * @param axis The axis being zoomed
1093
- * @param range The current range for the axis being zoomed
1238
+ * Calculates the axis range after the plot is dragged to the left or right (x-axis) or up or down
1239
+ * (y-axis).
1240
+ * @param delta The amount (in pixels) that the plot is dragged
1241
+ * @param axis The axis being panned
1242
+ * @param range The current range for the axis being panned
1094
1243
  * @param [constrainToOriginalRange=false] When set to `true` then the pan requires that the axis
1095
- * range remains a subset of the origin axis range; when `false` the pan allows and range
1244
+ * range remains a subset of the original axis range (a pan that would leave it isn't made); when
1245
+ * `false` the pan allows any range
1096
1246
  * @return The updated range
1097
1247
  */
1098
1248
  export function calculatePanFor(
@@ -1101,7 +1251,7 @@ export function calculatePanFor(
1101
1251
  range: ContinuousAxisRange,
1102
1252
  constrainToOriginalRange: boolean = false
1103
1253
  ): ContinuousAxisRange {
1104
- const scale = axis.generator.scale<ScaleLinear<number, number>>()
1254
+ const scale = axis.scale as ScaleLinear<number, number>
1105
1255
  const value = scale(range.current.start)
1106
1256
  if (value !== undefined) {
1107
1257
  const deltaValue = scale.invert(value + delta) - range.current.start
@@ -1113,35 +1263,48 @@ export function calculatePanFor(
1113
1263
  return range
1114
1264
  }
1115
1265
 
1266
+ /**
1267
+ * Calculates the ordinal axis range after the plot is dragged to the left or right (x-axis) or up
1268
+ * or down (y-axis). A pan that would leave part of the plot without categories (i.e. take the
1269
+ * range's start past 0, or its end short of the plot's width or height) isn't made.
1270
+ * @param delta The amount (in pixels) to pan the axis by.
1271
+ * @param range The current axis range.
1272
+ * @param plotDimensions The dimensions of the plot.
1273
+ * @param location The location of the axis.
1274
+ * @param constrainToOriginalRange Whether to constrain the pan to the original range.
1275
+ * @returns The new axis range.
1276
+ */
1116
1277
  export function calculateOrdinalPanFor(
1117
1278
  delta: number,
1118
1279
  range: OrdinalAxisRange,
1119
1280
  plotDimensions: Dimensions,
1281
+ location: AxisLocation,
1120
1282
  constrainToOriginalRange: boolean = false
1121
1283
  ): OrdinalAxisRange {
1122
1284
  const constraint: [start: number, end: number] = constrainToOriginalRange ?
1123
1285
  range.original.asTuple() :
1124
1286
  [-Infinity, Infinity]
1125
- // only allow panning if the plot dimensions (i.e. [0, width]) is in the current axis range
1126
- // todo deal with the fact that this could be a pan of the y-axis
1127
- if (range.current.start + delta > 0 || range.current.end + delta < plotDimensions.width) {
1287
+ // the axis's pixel extent depends on its location: a top/bottom (x) axis spans the plot's
1288
+ // width, while a left/right (y) axis spans the plot's height
1289
+ const extent = location === AxisLocation.Top || location === AxisLocation.Bottom ?
1290
+ plotDimensions.width :
1291
+ plotDimensions.height
1292
+ // only allow panning if the plot dimensions (i.e. [0, extent]) is in the current axis range
1293
+ if (range.current.start + delta > 0 || range.current.end + delta < extent) {
1128
1294
  return range
1129
1295
  }
1130
1296
  return range.translate(delta, constraint) as OrdinalAxisRange
1131
1297
  }
1132
1298
 
1133
- /*
1134
-
1135
- */
1136
-
1137
1299
  /**
1138
1300
  * Accepts the series, the assignment of the series to axes, and the current x-axes state, and
1139
- * returns an array of the distinct axis IDs that cover all the series in the plot.
1301
+ * returns an array of the distinct x-axis IDs that cover all the series in the plot (a series
1302
+ * without an x-axis assignment uses the default x-axis).
1140
1303
  *
1141
1304
  * @param series The array of series
1142
1305
  * @param axisAssignments A map association a series name with its axis assignments
1143
- * @param axesState The current axis state
1144
- * @return an array of the distinct axes that cover all the series in the plot
1306
+ * @param axesState The current x-axes state
1307
+ * @return an array of the distinct x-axis IDs that cover all the series in the plot
1145
1308
  */
1146
1309
  export function axesForSeriesGen<D, A extends BaseAxis>(
1147
1310
  series: Array<BaseSeries<D>>,
@@ -1149,7 +1312,7 @@ export function axesForSeriesGen<D, A extends BaseAxis>(
1149
1312
  axesState: AxesState<A>
1150
1313
  ): Array<string> {
1151
1314
  return series.map(srs => srs.name)
1152
- // grab the x-axis assigned to the series, or use the default x-axis if not
1315
+ // grab the x-axis assigned to the series or use the default x-axis if not
1153
1316
  // assignment has been made
1154
1317
  .map(name => axisAssignments.get(name)?.xAxis || axesState.axisDefaultId().getOrElse(""))
1155
1318
  // de-dup the array of axis IDs so that we don't end up applying the pan or zoom
@@ -1230,15 +1393,16 @@ function ordinalPanAxes(
1230
1393
  const currentRange = ranges.get(axisId)
1231
1394
  if (currentRange && axis) {
1232
1395
  // calculate the change in the axis-range based on the pixel change from the drag event
1233
- const range = calculateOrdinalPanFor(delta, currentRange, plotDimensions, constrainToOriginalRange)
1396
+ const range = calculateOrdinalPanFor(delta, currentRange, plotDimensions, axis.location, constrainToOriginalRange)
1234
1397
 
1235
1398
  // update the time-range for the axis
1236
1399
  ranges.set(axisId, range)
1237
1400
 
1238
1401
  setAxisRange(axisId, range.current)
1239
1402
 
1240
- // update the axis' time-range
1241
- axis.update(range.current, range.original, plotDimensions, margin)
1403
+ // update the axis' time-range -- panning never changes the set of categories, so
1404
+ // pass the axis's own current domain back unchanged
1405
+ axis.update(axis.scale.domain(), range.current, range.original, plotDimensions, margin)
1242
1406
  }
1243
1407
  })
1244
1408
  })
@@ -1251,7 +1415,7 @@ function ordinalPanAxes(
1251
1415
  * to the left or right. After calling the handler function, the plot needs to be updated as well, and this is
1252
1416
  * left for the caller.
1253
1417
  *
1254
- * Please note that the function generated by this function has side effects -- it updates the axes ranges.
1418
+ * Please note that the function generated by this function has side effects -- it updates the axis ranges.
1255
1419
  *
1256
1420
  * @param axesForSeries The distinct axes that cover all the series
1257
1421
  * @param margin The plot margin
@@ -1270,18 +1434,15 @@ export function panHandler(
1270
1434
  ): (
1271
1435
  x: number,
1272
1436
  plotDimensions: Dimensions,
1273
- // series: Array<string>,
1274
1437
  ranges: Map<string, ContinuousAxisRange>,
1275
1438
  ) => void {
1276
1439
  /**
1277
1440
  * Adjusts the time-range and updates the plot when the plot is dragged to the left or right
1278
- * @param deltaX The amount that the plot is dragged
1441
+ * @param delta The amount that the plot is dragged
1279
1442
  * @param plotDimensions The dimensions of the plot
1280
- * @param series An array of series names
1281
1443
  * @param ranges A map holding the axis ID and its associated time range
1282
1444
  */
1283
1445
  return (delta: number, plotDimensions: Dimensions, ranges: Map<string, ContinuousAxisRange>) => {
1284
- // return (delta: number, plotDimensions: Dimensions, series: Array<string>, ranges: Map<string, ContinuousAxisRange>) => {
1285
1446
  // run through the axis IDs, adjust their domain, and update the time-range set for that axis
1286
1447
  panAxes(delta, axesForSeries, axesState, ranges, setAxisRangeFor, plotDimensions, margin, constrainToOriginalRange)
1287
1448
  // hey, don't forget to update the plot with the new time-ranges in the code calling this... :)
@@ -1295,7 +1456,7 @@ export function panHandler(
1295
1456
  * to the left or right. After calling the handler function, the plot needs to be updated as well, and this is
1296
1457
  * left for the caller.
1297
1458
  *
1298
- * Please note that the function generated by this function has side effects -- it updates the axes ranges.
1459
+ * Please note that the function generated by this function has side effects -- it updates the axes' ranges.
1299
1460
  *
1300
1461
  * @param axesForSeries The distinct axes that cover all the series
1301
1462
  * @param margin The plot margin
@@ -1319,12 +1480,12 @@ export function ordinalPanHandler(
1319
1480
  ) => void {
1320
1481
  /**
1321
1482
  * Adjusts the time-range and updates the plot when the plot is dragged to the left or right
1322
- * @param deltaX The amount that the plot is dragged
1483
+ * @param delta The amount that the plot is dragged
1323
1484
  * @param plotDimensions The dimensions of the plot
1324
- * @param series An array of series names
1485
+ * @param _series An array of series names
1325
1486
  * @param ranges A map holding the axis ID and its associated time range
1326
1487
  */
1327
- return (delta: number, plotDimensions: Dimensions, series: Array<string>, ranges: Map<string, OrdinalAxisRange>) => {
1488
+ return (delta: number, plotDimensions: Dimensions, _series: Array<string>, ranges: Map<string, OrdinalAxisRange>) => {
1328
1489
  // run through the axis IDs, adjust their domain, and update the time-range set for that axis
1329
1490
  ordinalPanAxes(delta, axesForSeries, axesState, ranges, setAxisRangeFor, plotDimensions, margin, constrainToOriginalRange)
1330
1491
  // hey, don't forget to update the plot with the new ranges in the code calling this... :)
@@ -1338,7 +1499,7 @@ export function ordinalPanHandler(
1338
1499
  * to the left or right. After calling the handler function, the plot needs to be updated as well, and this is
1339
1500
  * left for the caller.
1340
1501
  *
1341
- * Please note that the function generated by this function has side effects -- it updates the axes ranges.
1502
+ * Please note that the function generated by this function has side effects -- it updates the axes' ranges.
1342
1503
  *
1343
1504
  * @param xAxesForSeries The distinct x-axes that cover all the series
1344
1505
  * @param yAxesForSeries The distinct y-axes that cover all the series
@@ -1368,12 +1529,14 @@ export function panHandler2D(
1368
1529
  ) => void {
1369
1530
  /**
1370
1531
  * Adjusts the time-range and updates the plot when the plot is dragged to the left or right
1371
- * @param deltaX The amount that the plot is dragged
1532
+ * @param deltaX The amount that the plot is dragged in x
1533
+ * @param deltaY The amount that the plot is dragged in y
1372
1534
  * @param plotDimensions The dimensions of the plot
1373
- * @param series An array of series names
1374
- * @param ranges A map holding the axis ID and its associated time range
1535
+ * @param _series An array of series names
1536
+ * @param xRanges A map holding the x-axis ID and its associated time range
1537
+ * @param yRanges A map holding the y-axis ID and its associated time range
1375
1538
  */
1376
- return (deltaX, deltaY, plotDimensions, series, xRanges, yRanges) => {
1539
+ return (deltaX, deltaY, plotDimensions, _series, xRanges, yRanges) => {
1377
1540
  // run through the x- and y-axes and update them by delta, within the original bounds
1378
1541
  panAxes(deltaX, xAxesForSeries, xAxesState, xRanges, setAxisRange, plotDimensions, margin, constrainToOriginalRange)
1379
1542
  panAxes(deltaY, yAxesForSeries, yAxesState, yRanges, setAxisRange, plotDimensions, margin, constrainToOriginalRange)
@@ -1383,25 +1546,30 @@ export function panHandler2D(
1383
1546
 
1384
1547
  /**
1385
1548
  * Calculates the zoom for the specified axis and updates the axis and the axis ranges
1386
- * @param value The x- or y-coordinate of the mouse
1387
- * @param axisId
1549
+ * @param pivotDomainValueFor Given the axis being zoomed, returns the domain value to pivot the
1550
+ * zoom around. Callers pass a function (rather than a fixed value) because the right pivot
1551
+ * depends on the axis's *own* domain/scale (e.g. inverting a shared mouse pixel position through
1552
+ * each axis's own scale, or reading each axis's own "now" tracking) -- there's no single value
1553
+ * that's correct for every axis when a plot has more than one (e.g. Scatter's dual x-axes).
1554
+ * @param axisId The id of the axis to zoom
1388
1555
  * @param margin The plot margin
1389
- * @param setRangeFor Function for setting the new time-range for a specific axis * @param scaleExtent The smallest and largest scale factors allowed
1390
- * @param axesState
1556
+ * @param setRangeFor Function for setting the new time-range for a specific axis
1557
+ * @param axesState The current state of the x- or y-axes
1391
1558
  * @param ranges A map associating axis IDs with axis ranges
1392
1559
  * @param scaleExtent The smallest and largest scale factors allowed
1393
- * @param transform The d3 zoom transformation information
1560
+ * @param zoomFactor The incremental zoom scale factor for this event -- see
1561
+ * {@link calculateConstrainedZoomFor} for what "incremental" means here
1394
1562
  * @param plotDimensions The dimensions of the plot
1395
1563
  */
1396
1564
  function calcZoomAndUpdate(
1397
- value: number,
1565
+ pivotDomainValueFor: (axis: ContinuousNumericAxis) => number,
1398
1566
  axisId: string,
1399
1567
  margin: Margin,
1400
1568
  setRangeFor: (axisId: string, range: AxisInterval) => void,
1401
1569
  axesState: AxesState<ContinuousNumericAxis>,
1402
1570
  ranges: Map<string, ContinuousAxisRange>,
1403
1571
  scaleExtent: [min: number, max: number],
1404
- transform: ZoomTransform,
1572
+ zoomFactor: number,
1405
1573
  plotDimensions: Dimensions,
1406
1574
  ): void {
1407
1575
  const [, zoomMax] = scaleExtent
@@ -1415,7 +1583,7 @@ function calcZoomAndUpdate(
1415
1583
  [range.original.start * zoomMax, range.original.end * zoomMax] :
1416
1584
  [0, Infinity]
1417
1585
 
1418
- const zoom = calculateConstrainedZoomFor(transform, value, axis, range, constraint)
1586
+ const zoom = calculateConstrainedZoomFor(zoomFactor, pivotDomainValueFor(axis), range, constraint)
1419
1587
 
1420
1588
  // update the axis range
1421
1589
  ranges.set(axisId, zoom.range)
@@ -1428,6 +1596,21 @@ function calcZoomAndUpdate(
1428
1596
  })
1429
1597
  }
1430
1598
 
1599
+ /**
1600
+ * Calculates the zoom for the specified ordinal axis and updates the axis and the axis ranges (the
1601
+ * original range is reset to the plot's width or height)
1602
+ * @param value The pixel position to pivot the zoom around (e.g. the mouse position)
1603
+ * @param axisId The id of the axis to zoom
1604
+ * @param margin The plot margin
1605
+ * @param setRangeFor Function for setting the new range for a specific axis
1606
+ * @param setOriginalRangeFor Function for setting the new original range for a specific axis
1607
+ * @param axesState The current state of the x- or y-axes
1608
+ * @param ranges A map associating axis IDs with axis ranges (updated with the zoomed range)
1609
+ * @param scaleExtent The smallest and largest scale factors allowed
1610
+ * @param zoomFactor The *cumulative* zoom scale factor for this event -- see
1611
+ * {@link calculateOrdinalConstrainedZoomFor}
1612
+ * @param plotDimensions The dimensions of the plot
1613
+ */
1431
1614
  function calcOrdinalZoomAndUpdate(
1432
1615
  value: number,
1433
1616
  axisId: string,
@@ -1437,7 +1620,7 @@ function calcOrdinalZoomAndUpdate(
1437
1620
  axesState: AxesState<OrdinalStringAxis>,
1438
1621
  ranges: Map<string, OrdinalAxisRange>,
1439
1622
  scaleExtent: [min: number, max: number],
1440
- transform: ZoomTransform,
1623
+ zoomFactor: number,
1441
1624
  plotDimensions: Dimensions,
1442
1625
  ): void {
1443
1626
  const [, zoomMax] = scaleExtent
@@ -1446,22 +1629,27 @@ function calcOrdinalZoomAndUpdate(
1446
1629
  const range = ranges.get(axisId)
1447
1630
  if (range && axis) {
1448
1631
  // calculate the constraint for the zoom
1449
- // const [originalStart, originalEnd] = range.original
1450
1632
  const constraint: [number, number] = isFinite(zoomMax) ?
1451
1633
  [range.original.start * zoomMax, range.original.end * zoomMax] :
1452
1634
  [range.original.start, range.original.end]
1453
1635
 
1454
- const zoom = calculateOrdinalConstrainedZoomFor(transform, value, range, constraint)
1636
+ const zoom = calculateOrdinalConstrainedZoomFor(zoomFactor, value, range, constraint)
1455
1637
 
1456
1638
  // update the axis range
1457
1639
  ranges.set(axisId, zoom.range)
1458
1640
 
1459
1641
  setRangeFor(axisId, zoom.range.current)
1460
- const origRange = AxisInterval.from(0, plotDimensions.width)
1642
+ // the axis's pixel extent depends on its location: a top/bottom (x) axis spans the
1643
+ // plot's width, while a left/right (y) axis spans the plot's height
1644
+ const extent = axis.location === AxisLocation.Top || axis.location === AxisLocation.Bottom ?
1645
+ plotDimensions.width :
1646
+ plotDimensions.height
1647
+ const origRange = AxisInterval.from(0, extent)
1461
1648
  setOriginalRangeFor(axisId, origRange)
1462
1649
 
1463
- // update the axis' range
1464
- axis.update(zoom.range.current, origRange, plotDimensions, margin)
1650
+ // update the axis' range -- zooming never changes the set of categories, so pass the
1651
+ // axis's own current domain back unchanged
1652
+ axis.update(axis.scale.domain(), zoom.range.current, origRange, plotDimensions, margin)
1465
1653
  }
1466
1654
  })
1467
1655
  }
@@ -1472,14 +1660,14 @@ function calcOrdinalZoomAndUpdate(
1472
1660
  * function returns a handler function. And this handler function adjusts the range when the plot is zoomed.
1473
1661
  * After calling the handler function, the plot needs to be updated as well, and this is left for the caller.
1474
1662
  *
1475
- * Please note that the function generated by this function has side effects -- it updates the axes ranges.
1663
+ * Please note that the function generated by this function has side effects -- it updates the axes' ranges.
1476
1664
  *
1477
1665
  * @param axesForSeries The distinct axes that cover all the series
1478
1666
  * @param margin The plot margin
1479
1667
  * @param setRangeFor Function for setting the new time-range for a specific axis
1480
1668
  * @param axesState The current state of the x- or y-axes
1481
- * @param scaleExtent The minimum and maximum allowed scale factors
1482
- * @return A handler function for pan events
1669
+ * @param [scaleExtent=[0, Infinity]] The minimum and maximum allowed scale factors
1670
+ * @return A handler function for zoom events
1483
1671
  */
1484
1672
  export function continuousAxisZoomHandler(
1485
1673
  axesForSeries: Array<string>,
@@ -1488,24 +1676,32 @@ export function continuousAxisZoomHandler(
1488
1676
  axesState: AxesState<ContinuousNumericAxis>,
1489
1677
  scaleExtent: [min: number, max: number] = [0, Infinity],
1490
1678
  ): (
1491
- transform: ZoomTransform,
1492
- x: number,
1679
+ zoomFactor: number,
1680
+ pivotDomainValueFor: (axisId: string, axis: ContinuousNumericAxis) => number,
1493
1681
  plotDimensions: Dimensions,
1494
1682
  ranges: Map<string, ContinuousAxisRange>,
1495
1683
  ) => void {
1496
1684
 
1497
1685
  /**
1498
- * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out. Zooms in/out
1499
- * at the location of the mouse when the scroll wheel or gesture was applied.
1500
- * @param transform The d3 zoom transformation information
1501
- * @param x The x-position of the mouse when the scroll wheel or gesture is used
1686
+ * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out.
1687
+ * @param zoomFactor The *incremental* zoom scale factor for this event -- relative to each
1688
+ * axis's `.current`, not its `.original` (see {@link calculateConstrainedZoomFor}). Callers
1689
+ * must convert d3-zoom's cumulative `event.transform.k` to this incremental form themselves
1690
+ * (this event's `k` divided by the previously-applied `k`) before calling this handler.
1691
+ * @param pivotDomainValueFor Given an axis ID and its axis, returns the domain value to pivot
1692
+ * that axis's zoom around -- e.g. the mouse position inverted through that axis's own scale,
1693
+ * or (while streaming) that axis's own "now" tracking, so "now" stays fixed on screen as the
1694
+ * zoom widens/narrows the window around it instead of drifting with wherever the mouse is.
1502
1695
  * @param plotDimensions The dimensions of the plot
1503
1696
  * @param ranges A map holding the axis ID and its associated time-range
1504
1697
  */
1505
- return (transform, x, plotDimensions, ranges) => {
1698
+ return (zoomFactor, pivotDomainValueFor, plotDimensions, ranges) => {
1506
1699
  // run through the axis IDs, adjust their domain, and update the time-range set for that axis
1507
1700
  axesForSeries.forEach(axisId =>
1508
- calcZoomAndUpdate(x, axisId, margin, setRangeFor, axesState, ranges, scaleExtent, transform, plotDimensions)
1701
+ calcZoomAndUpdate(
1702
+ axis => pivotDomainValueFor(axisId, axis),
1703
+ axisId, margin, setRangeFor, axesState, ranges, scaleExtent, zoomFactor, plotDimensions
1704
+ )
1509
1705
  )
1510
1706
  // hey, don't forget to update the plot with the new time-ranges in the code calling this... :)
1511
1707
  }
@@ -1517,15 +1713,15 @@ export function continuousAxisZoomHandler(
1517
1713
  * function returns a handler function. And this handler function adjusts the range when the plot is zoomed.
1518
1714
  * After calling the handler function, the plot needs to be updated as well, and this is left for the caller.
1519
1715
  *
1520
- * Please note that the function generated by this function has side effects -- it updates the axes ranges.
1716
+ * Please note that the function generated by this function has side effects -- it updates the axes' ranges.
1521
1717
  *
1522
1718
  * @param axesForSeries The distinct axes that cover all the series
1523
1719
  * @param margin The plot margin
1524
1720
  * @param setRangeFor Function for setting the new time-range for a specific axis
1525
1721
  * @param setOriginalRangeFor Function for setting the new original range for a specific axis
1526
1722
  * @param axesState The current state of the x- or y-axes
1527
- * @param scaleExtent The minimum and maximum allowed scale factors
1528
- * @return A handler function for pan events
1723
+ * @param [scaleExtent=[0, Infinity]] The minimum and maximum allowed scale factors
1724
+ * @return A handler function for zoom events
1529
1725
  */
1530
1726
  export function ordinalAxisZoomHandler(
1531
1727
  axesForSeries: Array<string>,
@@ -1535,7 +1731,7 @@ export function ordinalAxisZoomHandler(
1535
1731
  axesState: AxesState<OrdinalStringAxis>,
1536
1732
  scaleExtent: [min: number, max: number] = [0, Infinity],
1537
1733
  ): (
1538
- transform: ZoomTransform,
1734
+ zoomFactor: number,
1539
1735
  x: number,
1540
1736
  plotDimensions: Dimensions,
1541
1737
  ranges: Map<string, OrdinalAxisRange>,
@@ -1544,15 +1740,19 @@ export function ordinalAxisZoomHandler(
1544
1740
  /**
1545
1741
  * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out. Zooms in/out
1546
1742
  * at the location of the mouse when the scroll wheel or gesture was applied.
1547
- * @param transform The d3 zoom transformation information
1743
+ * @param zoomFactor The *cumulative* zoom scale factor for this event -- d3-zoom's own
1744
+ * `event.transform.k`, passed through unmodified. Unlike {@link continuousAxisZoomHandler},
1745
+ * this must NOT be converted to an incremental factor -- see
1746
+ * {@link OrdinalAxisRange.scaledCumulative} for why an ordinal axis's bounded domain needs
1747
+ * the raw cumulative factor instead.
1548
1748
  * @param x The x-position of the mouse when the scroll wheel or gesture is used
1549
1749
  * @param plotDimensions The dimensions of the plot
1550
1750
  * @param ranges A map holding the axis ID and its associated time-range
1551
1751
  */
1552
- return (transform, x, plotDimensions, ranges) => {
1752
+ return (zoomFactor, x, plotDimensions, ranges) => {
1553
1753
  // run through the axis IDs, adjust their domain, and update the time-range set for that axis
1554
1754
  axesForSeries.forEach(axisId =>
1555
- calcOrdinalZoomAndUpdate(x, axisId, margin, setRangeFor, setOriginalRangeFor, axesState, ranges, scaleExtent, transform, plotDimensions)
1755
+ calcOrdinalZoomAndUpdate(x, axisId, margin, setRangeFor, setOriginalRangeFor, axesState, ranges, scaleExtent, zoomFactor, plotDimensions)
1556
1756
  )
1557
1757
  // hey, don't forget to update the plot with the new time-ranges in the code calling this... :)
1558
1758
  }
@@ -1564,7 +1764,7 @@ export function ordinalAxisZoomHandler(
1564
1764
  * function returns a handler function. And this handler function adjusts the time-range when the plot is zoomed.
1565
1765
  * After calling the handler function, the plot needs to be updated as well, and this is left for the caller.
1566
1766
  *
1567
- * Please note that the function generated by this function has side effects -- it updates the axes ranges.
1767
+ * Please note that the function generated by this function has side effects -- it updates the axes' ranges.
1568
1768
  *
1569
1769
  * @param xAxesForSeries The distinct x-axes that cover all the series
1570
1770
  * @param yAxesForSeries The distinct y-axes that cover all the series
@@ -1573,7 +1773,7 @@ export function ordinalAxisZoomHandler(
1573
1773
  * @param xAxesState The current state of the x-axes
1574
1774
  * @param yAxesState The current state of the y-axes
1575
1775
  * @param scaleExtent The smallest and largest scale factors allowed
1576
- * @return A handler function for pan events
1776
+ * @return A handler function for zoom events
1577
1777
  */
1578
1778
  export function axesZoomHandler(
1579
1779
  xAxesForSeries: Array<string>,
@@ -1584,7 +1784,7 @@ export function axesZoomHandler(
1584
1784
  yAxesState: AxesState<ContinuousNumericAxis>,
1585
1785
  scaleExtent: [min: number, max: number],
1586
1786
  ): (
1587
- transform: ZoomTransform,
1787
+ zoomFactor: number,
1588
1788
  mousePosition: [x: number, y: number],
1589
1789
  plotDimensions: Dimensions,
1590
1790
  xRanges: Map<string, ContinuousAxisRange>,
@@ -1594,47 +1794,57 @@ export function axesZoomHandler(
1594
1794
  /**
1595
1795
  * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out. Zooms in/out
1596
1796
  * at the location of the mouse when the scroll wheel or gesture was applied.
1597
- * @param transform The d3 zoom transformation information
1598
- * @param mousePosistion The position of the mouse when the scroll wheel or gesture is used
1797
+ * @param zoomFactor The *incremental* zoom scale factor for this event -- see
1798
+ * {@link continuousAxisZoomHandler} for the full explanation of why this must already be
1799
+ * incremental (not d3-zoom's raw, cumulative `event.transform.k`)
1800
+ * @param mousePosition The position of the mouse when the scroll wheel or gesture is used
1599
1801
  * @param plotDimensions The dimensions of the plot
1600
- * @param ranges A map holding the axis ID and its associated time-range
1802
+ * @param xRanges A map holding the x-axis ID and its associated time-range
1803
+ * @param yRanges A map holding the y-axis ID and its associated time-range
1601
1804
  */
1602
- return (transform, mousePosition, plotDimensions, xRanges, yRanges) => {
1805
+ return (zoomFactor, mousePosition, plotDimensions, xRanges, yRanges) => {
1603
1806
  // run through the axis IDs, adjust their domain, and update the time-range set for that axis
1604
1807
  const [x, y] = mousePosition
1605
1808
  xAxesForSeries.forEach(id =>
1606
- calcZoomAndUpdate(x, id, margin, setRangeFor, xAxesState, xRanges, scaleExtent, transform, plotDimensions)
1809
+ calcZoomAndUpdate(
1810
+ axis => (axis.scale as ScaleLinear<number, number>).invert(x),
1811
+ id, margin, setRangeFor, xAxesState, xRanges, scaleExtent, zoomFactor, plotDimensions
1812
+ )
1607
1813
  )
1608
1814
  yAxesForSeries.forEach(id =>
1609
- calcZoomAndUpdate(y, id, margin, setRangeFor, yAxesState, yRanges, scaleExtent, transform, plotDimensions)
1815
+ calcZoomAndUpdate(
1816
+ axis => (axis.scale as ScaleLinear<number, number>).invert(y),
1817
+ id, margin, setRangeFor, yAxesState, yRanges, scaleExtent, zoomFactor, plotDimensions
1818
+ )
1610
1819
  )
1611
1820
  // hey, don't forget to update the plot with the new time-ranges in the code calling this... :)
1612
1821
  }
1613
1822
  }
1614
1823
 
1615
1824
  /**
1616
- * Calculates the axis-ranges for each of the continuous numeric axes in the map
1825
+ * Calculates the axis-ranges for each of the continuous numeric axes in the map, from each axis'
1826
+ * current domain (see {@link continuousRange})
1617
1827
  * @param axes The map containing the axes and their associated IDs
1618
- * @return a map associating the axis IDs to their continuous axis-range
1828
+ * @return a map associating the axis IDs with their continuous axis-range
1619
1829
  */
1620
1830
  export function continuousAxisRanges(axes: Map<string, ContinuousNumericAxis>): Map<string, ContinuousAxisRange> {
1621
1831
  return continuousRange(axes)
1622
1832
  }
1623
1833
 
1624
1834
  /**
1625
- * Calculates the axis-ranges for each of the ordinal axes in the map
1835
+ * Calculates the axis-ranges for each of the ordinal axes in the map, read from each axis's own
1836
+ * scale range.
1626
1837
  * @param axes The map containing the axes and their associated IDs
1627
- * @param originalRange The original range of the axis
1628
- * @return a map associating the axis IDs to their ordinal axis-range
1838
+ * @return a map associating the axis IDs with their ordinal axis-range
1629
1839
  */
1630
- export function ordinalAxisRanges(axes: Map<string, OrdinalStringAxis>, originalRange: AxisInterval): Map<string, OrdinalAxisRange> {
1631
- return ordinalRange(axes, originalRange)
1840
+ export function ordinalAxisRanges(axes: Map<string, OrdinalStringAxis>): Map<string, OrdinalAxisRange> {
1841
+ return ordinalRange(axes)
1632
1842
  }
1633
1843
 
1634
1844
  /**
1635
- * Calculates the axis interval (start, end) for each of the axis
1636
- * @param axes The axes representing the time
1637
- * @return A map associating each axis with a (start, end) interval
1845
+ * Calculates the axis interval (start, end) of each axis' current domain
1846
+ * @param axes The axes, by their IDs
1847
+ * @return A map associating each axis ID with its domain's (start, end) interval
1638
1848
  */
1639
1849
  export function continuousAxisIntervals(axes: Map<string, ContinuousNumericAxis>): Map<string, AxisInterval> {
1640
1850
  return new Map(Array.from(axes.entries())
@@ -1645,9 +1855,10 @@ export function continuousAxisIntervals(axes: Map<string, ContinuousNumericAxis>
1645
1855
  }
1646
1856
 
1647
1857
  /**
1648
- * Calculates the axis interval (start, end) for each of the axis
1649
- * @param axes The axes representing the time
1650
- * @return A map associating each axis with a (start, end) interval
1858
+ * Calculates the pixel interval (start, end) of each ordinal axis' current range, along with its
1859
+ * categories
1860
+ * @param axes The axes, by their IDs
1861
+ * @return A map associating each axis ID with its range's (start, end) interval and its categories
1651
1862
  */
1652
1863
  export function ordinalAxisIntervals(axes: Map<string, OrdinalStringAxis>): Map<string, {interval: AxisInterval, categories: Array<string>}> {
1653
1864
  return new Map(Array.from(axes.entries())
@@ -1658,7 +1869,8 @@ export function ordinalAxisIntervals(axes: Map<string, OrdinalStringAxis>): Map<
1658
1869
  }
1659
1870
 
1660
1871
  /**
1661
- * Returns the bounds on the specified continuous numeric axes
1872
+ * Returns the bounds on the specified continuous numeric axes: a new axis range (with its original
1873
+ * range the same as its current range) from each axis' current domain
1662
1874
  * @param axes A map associating an axis ID with a {@link ContinuousNumericAxis}
1663
1875
  * @return A map associating each specified axis ID with the interval covered (bounds) by the axis
1664
1876
  */
@@ -1671,15 +1883,22 @@ export function continuousRange(axes: Map<string, ContinuousNumericAxis>): Map<s
1671
1883
  }
1672
1884
 
1673
1885
  /**
1674
- * Returns the bounds on the specified continuous numeric axes
1886
+ * Returns the bounds on the specified ordinal axes, read from each axis's own scale range.
1887
+ *
1888
+ * Previously mapped every axis ID to one shared, caller-supplied `originalRange`, discarding the
1889
+ * axis object entirely (`[id, ]`) -- harmless today only because there's no per-axis margin/offset
1890
+ * mechanism anywhere in this codebase, so every ordinal axis in a chart is architecturally
1891
+ * guaranteed to span the same pixel range already. Reading each axis's own `.scale.range()`
1892
+ * instead (mirroring {@link ordinalAxisIntervals}, which already does this correctly) removes that
1893
+ * latent footgun for a chart with multiple category axes whose ranges genuinely differ.
1675
1894
  * @param axes A map associating an axis ID with a {@link OrdinalStringAxis}
1676
- * @param originalRange The original range of the axis
1677
1895
  * @return A map associating each specified axis ID with the interval covered (bounds) by the axis
1678
1896
  */
1679
- export function ordinalRange(axes: Map<string, OrdinalStringAxis>, originalRange: AxisInterval): Map<string, OrdinalAxisRange> {
1897
+ export function ordinalRange(axes: Map<string, OrdinalStringAxis>): Map<string, OrdinalAxisRange> {
1680
1898
  return new Map(Array.from(axes.entries())
1681
- .map(([id, _]) =>
1682
- [id, OrdinalAxisRange.from(originalRange.start, originalRange.end)]
1683
- )
1899
+ .map(([id, axis]) => {
1900
+ const [start, end] = axis.scale.range()
1901
+ return [id, OrdinalAxisRange.from(start, end)]
1902
+ })
1684
1903
  )
1685
- }
1904
+ }