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,34 +1,50 @@
1
- import React, {useCallback, useEffect, useMemo, useRef} from 'react'
2
- import {NoTooltipMetadata, useChart} from "../hooks/useChart";
1
+ import {useCallback, useEffect, useEffectEvent, useMemo, useRef} from 'react'
2
+ import {useCanvasZoomPan} from "./useCanvasZoomPan";
3
+ import {type NoTooltipMetadata, useChart} from "../hooks/useChart";
3
4
  import * as d3 from "d3";
4
- import {CurveFactory, ZoomTransform} from "d3";
5
- import {setClipPathG} from "./plot";
6
- import {Datum} from "../series/timeSeries";
5
+ import {clipToArea} from "./plot";
7
6
  import {
8
7
  axesZoomHandler,
9
- BaseAxis,
10
- ContinuousNumericAxis,
8
+ AxisType,
9
+ type BaseAxis,
10
+ checkAxisType,
11
+ type ContinuousNumericAxis,
11
12
  defaultLineStyle,
12
13
  panHandler2D,
13
- SeriesLineStyle
14
+ type SeriesLineStyle
14
15
  } from "../axes/axes";
15
- import {GSelection} from "../d3types";
16
- import {Observable, Subscription} from "rxjs";
17
- import {formatTime, noop, textDimensions} from "../utils";
18
- import {Dimensions, Margin} from "../styling/margins";
19
- import {subscriptionIteratesFor} from "../subscriptions/subscriptions";
20
- import {useDataObservable} from "../hooks/useDataObservable";
21
- import {IterateChartData} from "../observables/iterates";
22
- import {IterateDatum, IterateSeries} from "../series/iterateSeries";
16
+ import type {CanvasContext} from "../d3types";
17
+ import {canvasLocalPoint, seriesAt, type SeriesGeometry} from "./hitTesting";
18
+ import {fontStringFor, formatTime, noop, textDimensions} from "../utils";
19
+ import type {Dimensions} from "../styling/margins";
20
+ import {iteratesViewDriverFor} from "../subscriptions/viewDrivers";
21
+ import {useDataSource, useDataSourceRunning, useInitialData, useStreamingSettings} from "../hooks/useDataSource";
22
+ import type {IterateChartData} from "../observables/iterates";
23
+ import type {IterateDatum, IterateSeries} from "../series/iterateSeries";
23
24
  import {usePlotDimensions} from "../hooks/usePlotDimensions";
24
- import {useInitialData} from "../hooks/useInitialData";
25
- import {TooltipData, useTooltip} from "../hooks/useTooltip";
26
- import {TimeSeriesChartData} from "../series/timeSeriesChartData";
25
+ import {type TooltipData, useTooltip} from "../hooks/useTooltip";
27
26
  import {ContinuousAxisRange} from "../axes/ContinuousAxisRange";
28
-
27
+ import {AxisInterval} from "../axes/AxisInterval";
28
+ import {warnAboutIgnoredAutoScale} from "../axes/autoScale";
29
+ import {FastShiftArray} from "fast-shift-array";
30
+ import {
31
+ DEFAULT_LINE_COLOR_STEPS,
32
+ DEFAULT_LINE_END_COLOR,
33
+ DEFAULT_LINE_START_COLOR,
34
+ pointColorScale,
35
+ strokeColorizedLine
36
+ } from "./colorizedLine";
37
+
38
+ // an iterate as the plot draws it: f[n](x), f[n+1](x), the iterate's time, and its index in its series
29
39
  type IteratePoint = { n: number, n_1: number, time: number, index: number }
30
- type IteratePointSeries = Array<IteratePoint>
40
+ type IteratePointSeries = FastShiftArray<IteratePoint>
31
41
 
42
+ /**
43
+ * Creates an axis range (with its original range the same as its current range) from each axis'
44
+ * current domain
45
+ * @param axes The axes (continuous numeric ones), by their IDs
46
+ * @return The axes' ranges, by their IDs
47
+ */
32
48
  function generateAxisRangeMap(axes: Map<string, BaseAxis>): Map<string, ContinuousAxisRange> {
33
49
  return new Map(
34
50
  Array.from(axes.entries()).map(([id, axis]) => {
@@ -38,10 +54,29 @@ function generateAxisRangeMap(axes: Map<string, BaseAxis>): Map<string, Continuo
38
54
  )
39
55
  }
40
56
 
41
- // A sentinel that represents that no curve factory is to be used, which means
42
- // that no line will be drawn between the iterates (kinda gross)
43
- export const NoCurveFactory: CurveFactory = undefined as unknown as CurveFactory;
57
+ /**
58
+ * A snapshot of the Poincaré plot's zoom/pan state, in a form that can be held outside the plot
59
+ * (e.g. in an application store) and handed back to a later instance of the plot (see the
60
+ * {@link Props.zoomState} and {@link Props.onZoomStateChange} props) -- so that zooming, navigating
61
+ * away, and navigating back doesn't reset the zoom.
62
+ */
63
+ export interface PoincarePlotZoomState {
64
+ /**
65
+ * d3-zoom's cumulative scale factor (`transform.k`) at the time of the snapshot. It must be
66
+ * restored onto the new canvas along with the ranges: d3-zoom's `scaleExtent` (see
67
+ * {@link Props.zoomMaxScaleFactor}) limits `k`, so a remounted canvas starting back at `k = 1`
68
+ * would otherwise mis-measure how much further the user may zoom in or out.
69
+ */
70
+ k: number
71
+ /**
72
+ * Maps each axis ID (x and y) to its current (zoomed, panned) domain
73
+ */
74
+ ranges: Record<string, [start: number, end: number]>
75
+ }
44
76
 
77
+ /**
78
+ * The properties of the {@link PoincarePlot}
79
+ */
45
80
  export interface Props {
46
81
  /**
47
82
  * The line interpolation curve factory. See the d3 documentation for curves at
@@ -49,14 +84,38 @@ export interface Props {
49
84
  */
50
85
  interpolation?: d3.CurveFactory
51
86
  /**
52
- * When set to `true` plots the data points as well as the line.
87
+ * When set to `true` (the default) plots the data points as well as the line.
53
88
  */
54
89
  showPoints?: boolean
55
90
  /**
56
- * The number of milliseconds of data to hold in memory before dropping it. Defaults to
57
- * infinity (i.e. no data is dropped)
91
+ * When `true`, the lines connecting the points change color along their length, one step per
92
+ * point: from `lineStartColor` at each series' first point to `lineEndColor` after
93
+ * `lineColorSteps` points, then back to `lineStartColor` over the next `lineColorSteps` points,
94
+ * and so on, sweeping the colors of the spectrum in between (the long way around the hue
95
+ * circle -- e.g. from blue, through cyan, green, and yellow, to red). A point keeps its color
96
+ * as old points are dropped (with an {@link IteratesDataSource}, which counts them), so that
97
+ * the path's direction, and its age, can be seen. The points are filled with the color of the
98
+ * line leaving them (except while hovered, when they're highlighted as usual). When `false`
99
+ * (the default), the lines and points are drawn in the series' own color.
100
+ */
101
+ colorizeLines?: boolean
102
+ /**
103
+ * The number of points (steps) it takes the colorized lines' color to sweep from
104
+ * `lineStartColor` to `lineEndColor` (and as many to sweep back) -- the larger, the slower the
105
+ * color changes. Defaults to {@link DEFAULT_LINE_COLOR_STEPS} (1,000). Only used when
106
+ * `colorizeLines` is `true`.
107
+ */
108
+ lineColorSteps?: number
109
+ /**
110
+ * The color the colorized lines' sweep starts at (each series' first point). Defaults to a blue
111
+ * ({@link DEFAULT_LINE_START_COLOR}). Only used when `colorizeLines` is `true`.
112
+ */
113
+ lineStartColor?: string
114
+ /**
115
+ * The color the colorized lines' sweep ends at (after `lineColorSteps` points). Defaults to a red
116
+ * ({@link DEFAULT_LINE_END_COLOR}). Only used when `colorizeLines` is `true`.
58
117
  */
59
- dropDataAfter?: number
118
+ lineEndColor?: string
60
119
  /**
61
120
  * Enables panning (default is false)
62
121
  */
@@ -83,6 +142,25 @@ export interface Props {
83
142
  * the larger this factor, the more the user can "zoom out". Default value is 1.0.
84
143
  */
85
144
  zoomMaxScaleFactor?: number
145
+ /**
146
+ * An (optional) zoom/pan state to restore when this plot mounts -- commonly the last value
147
+ * reported through {@link onZoomStateChange} and held by the application (e.g. in a store) so
148
+ * that the zoom survives the plot being unmounted and remounted (e.g. navigating away and
149
+ * back). Only read once, when the plot's axes are first available.
150
+ */
151
+ zoomState?: PoincarePlotZoomState
152
+ /**
153
+ * (Optional) callback that is handed the plot's zoom/pan state after each zoom event, at the
154
+ * end of each pan, and when the axes' bounds are changed from outside (which resets the zoom)
155
+ */
156
+ onZoomStateChange?: (zoomState: PoincarePlotZoomState) => void
157
+ /**
158
+ * Called whenever this plot (re)creates its zoom behavior, handing the caller a `resetZoom`
159
+ * function that clears d3-zoom's own accumulated scale back to identity and restores every axis
160
+ * to its original (un-zoomed, un-panned) domain -- e.g. for a "Clear" action. Only meaningful
161
+ * (called) while `zoomEnabled` is true.
162
+ */
163
+ onZoomReset?: (resetZoom: () => void) => void
86
164
  /**
87
165
  * When set, uses a cadence with the specified refresh period (in milliseconds). For plots
88
166
  * where the updates are slow (> 100 ms) using a cadence of 10 to 25 ms smooths out the
@@ -90,25 +168,39 @@ export interface Props {
90
168
  * then setting the cadence period too small will result in poor update performance. Generally
91
169
  * at high update speeds, the cadence is unnecessary. Finally, using cadence, sets the max time
92
170
  * to the current time.
171
+ *
172
+ * Note: the Poincaré plot accepts this, but doesn't (yet) use it -- its axes don't follow time.
93
173
  */
94
174
  withCadenceOf?: number
175
+ /**
176
+ * When true, hovering over a point also highlights the (single, shared) x- and y-axes it is
177
+ * plotted against, in the series' own highlight color/width. Defaults to `false`.
178
+ */
179
+ highlightAxesOnMouseOver?: boolean
95
180
  }
96
181
 
97
182
  /**
98
- * Renders a streaming Poincare (iterates) plot for the series in the initial data and those sourced by the
99
- * observable specified as a property in the {@link Chart}. This component uses the {@link useChart}
183
+ * Renders a streaming Poincare (iterates) plot for the series held by the {@link StreamingDataSource}
184
+ * handed to the {@link Chart} (its `dataSource` prop). This component uses the {@link useChart}
100
185
  * hook, and therefore must be a child of the {@link Chart} in order to be plugged in to the
101
186
  * chart ecosystem (axes, tracker, tooltip).
102
187
  *
103
- * @param props The properties associated with the scatter plot
188
+ * Internally, this no longer creates/updates SVG `<path>`/`<circle>`/`<text>` elements. Instead, it
189
+ * registers a single draw function with the chart's {@link CanvasContext} that redraws every
190
+ * series' line and points from scratch each time the canvas repaints. The old version's
191
+ * hover-a-point-and-annotate-its-neighbors behavior used direct `d3.select('#specific-id')` lookups
192
+ * into already-rendered SVG elements from a separate pass after the main render; since canvas has
193
+ * no persistent elements to reach back into, this is restructured so the single draw pass checks
194
+ * "is this point hovered, or a neighbor of the hovered point" and renders the highlight/label
195
+ * inline, driven by a `hoveredPointRef` updated on `mousemove`.
196
+ *
197
+ * @param props The properties associated with the Poincaré plot
198
+ * @return `null` (the plot draws onto the chart's canvas)
104
199
  * @example
105
200
  * ```typescript
106
- * <ScatterPlot
201
+ * <PoincarePlot
107
202
  * interpolation={interpolation}
108
- * axisAssignments={new Map([
109
- * ['test2', assignAxes("x-axis-2", "y-axis-2")],
110
- * ])}
111
- * dropDataAfter={10000}
203
+ * showPoints={true}
112
204
  * panEnabled={true}
113
205
  * zoomEnabled={true}
114
206
  * zoomKeyModifiersRequired={true}
@@ -118,8 +210,7 @@ export interface Props {
118
210
  export function PoincarePlot(props: Props): null {
119
211
  const {
120
212
  chartId,
121
- container,
122
- mainG,
213
+ canvasContext,
123
214
  axes,
124
215
  backgroundColor,
125
216
  seriesStyles,
@@ -138,6 +229,13 @@ export function PoincarePlot(props: Props): null {
138
229
  removeAxesRangesUpdateHandler,
139
230
  } = axes
140
231
 
232
+ // this plot doesn't auto-scale its y-axes, so warn about any that ask to be (see
233
+ // `ContinuousAxis`'s `autoScale` prop)
234
+ useEffect(
235
+ () => warnAboutIgnoredAutoScale("PoincarePlot", yAxesState.axes),
236
+ [yAxesState]
237
+ )
238
+
141
239
  const {
142
240
  mouseOverHandlerFor,
143
241
  mouseLeaveHandlerFor
@@ -149,46 +247,53 @@ export function PoincarePlot(props: Props): null {
149
247
  } = usePlotDimensions()
150
248
 
151
249
  const {
152
- seriesObservable,
153
250
  windowingTime = 100,
154
- shouldSubscribe,
155
-
156
- onSubscribe = noop,
157
- onUpdateData,
158
251
  onUpdateChartTime = noop,
159
- } = useDataObservable<IterateChartData, IterateDatum>()
252
+ } = useStreamingSettings()
160
253
 
161
- const {initialData} = useInitialData<TimeSeriesChartData, IterateDatum>()
254
+ // the application-owned source of the data, and whether it's running (streaming)
255
+ const dataSource = useDataSource<IterateChartData, IterateDatum, IterateSeries>()
256
+ const running = useDataSourceRunning(dataSource)
257
+
258
+ const {initialData} = useInitialData<IterateDatum>()
162
259
 
163
260
  const {visibilityState: tooltipVisible} = useTooltip()
164
261
 
165
262
  const {
166
263
  interpolation,
167
264
  showPoints = true,
168
- dropDataAfter = 1000,
169
265
  panEnabled = false,
170
266
  zoomEnabled = false,
171
267
  zoomKeyModifiersRequired = true,
172
268
  zoomMinScaleFactor = 0,
173
269
  zoomMaxScaleFactor = 1,
270
+ highlightAxesOnMouseOver = false,
271
+ colorizeLines = false,
272
+ lineStartColor = DEFAULT_LINE_START_COLOR,
273
+ lineEndColor = DEFAULT_LINE_END_COLOR,
274
+ lineColorSteps = DEFAULT_LINE_COLOR_STEPS,
275
+ zoomState = undefined,
276
+ onZoomStateChange = noop,
277
+ onZoomReset = noop,
174
278
  } = props
175
279
 
176
280
  // why do "dataRef" and "seriesRef" both hold on to the same underlying data? for performance.
177
281
  //
178
282
  // the "dataRef" and "seriesRef" both point to the same underlying data, a collection
179
- // of series. The series in "dataRef" are bound to the DOM elements through d3. The "seriesRef" series
180
- // are the ones that are updated as new data is streamed in.
283
+ // of series (held, and updated as new data is streamed in, by the data source). The series in
284
+ // "dataRef" are read by the canvas draw function, and the "seriesRef" map is for looking a
285
+ // series up by its name.
181
286
  //
182
287
  // the "dataRef" object holds on to a copy of the initial data (which is an array of
183
- // time-series, e.i. an array of BaseSeries<OrdinalDatum> objects). The slice just creates a copy of
288
+ // iterate series, e.i. an array of BaseSeries<IterateDatum> objects). The slice just creates a copy of
184
289
  // the array, but the references to the BaseSeries objects are the same and still point to the same
185
290
  // data as the "initialData" array.
186
291
  //
187
- // the "seriesRef" object is a reference to a map (series_name -> BaseSeries<OrdinalDatum>) which is
188
- // used to update the data in the series. When new data enters, it is appended to one or more series.
292
+ // the "seriesRef" object is a reference to a map (series_name -> BaseSeries<IterateDatum>), used
293
+ // to find the series under the mouse (for the tooltip).
189
294
  //
190
- // the series in the "dataRef" object are the ones bound to the DOM elements in d3, and so as these
191
- // are updated, d3 will update the DOM elements (the elements in this plot).
295
+ // the series in the "dataRef" object are the ones read by the next redraw, so as these are
296
+ // updated, the next canvas repaint picks up the new data.
192
297
  const dataRef = useRef<Array<IterateSeries>>(initialData.slice() as Array<IterateSeries>)
193
298
  const seriesRef = useRef<Map<string, IterateSeries>>(new Map(initialData.map(series => [series.name, series as IterateSeries])))
194
299
  // map(axis_id -> current_time) -- maps the axis ID to the current time for that axis
@@ -196,26 +301,323 @@ export function PoincarePlot(props: Props): null {
196
301
  const xAxisRangesRef = useRef<Map<string, ContinuousAxisRange>>(new Map());
197
302
  const yAxisRangesRef = useRef<Map<string, ContinuousAxisRange>>(new Map());
198
303
 
199
- const subscriptionRef = useRef<Subscription>(undefined)
200
- const isSubscriptionClosed = () => subscriptionRef.current === undefined || subscriptionRef.current.closed
304
+ // tooltips are only shown while the data source is paused (and not while panning)
305
+ const allowTooltip = useRef<boolean>(!(dataSource?.running ?? false))
201
306
 
202
- const allowTooltip = useRef<boolean>(isSubscriptionClosed())
203
307
 
204
- // so that we can reset the zoom when the axes-bounds change, we hold on to the zoom-behaviour
205
- // and the zoom-selection so that we can reset the transform to the identity
206
- const zoomRef = useRef<d3.ZoomBehavior<SVGSVGElement, Datum>>(undefined)
207
- const zoomSelectionRef = useRef<d3.Selection<SVGSVGElement, any, null, undefined>>(undefined)
308
+ // the last-drawn geometry for each series' points, in canvas coordinates, used for
309
+ // hit-testing mouse hover on `mousemove` (see the effect below that wires up the listener).
310
+ // Also keeps the corresponding `IteratePointSeries` so the hovered/neighbor points' data can
311
+ // be looked up by index.
312
+ const geometryRef = useRef<Map<string, SeriesGeometry>>(new Map())
313
+ const plotDataRef = useRef<Map<string, IteratePointSeries>>(new Map())
314
+ // which point (series + index) is currently hovered, so the draw pass can highlight it and
315
+ // its neighbors, and so we know when to fire a "leave"
316
+ const hoveredPointRef = useRef<{seriesName: string, index: number} | undefined>(undefined)
208
317
 
209
- // calculates the distinct axis IDs that cover all the series in the plot
210
- const xAxesForSeries = useMemo(
211
- (): Array<string> => xAxesState.axisIds(),
212
- [xAxesState]
318
+ const updatePlot = useCallback(
319
+ /**
320
+ * (Re-)registers this plot's draw function with the canvas context and requests a redraw.
321
+ *
322
+ * Pan/zoom behavior setup lives in a separate effect (see below), not here -- this
323
+ * function runs on every data tick, and recreating/reattaching a `d3.drag()`/`d3.zoom()`
324
+ * behavior that often is pure overhead unrelated to drawing the new data.
325
+ * @param cc The canvas context to register the draw function with
326
+ */
327
+ (cc: CanvasContext) => {
328
+ onUpdateChartTime(currentTimeRef.current)
329
+
330
+ // the colorized lines' color at a point, by the point's overall sequence number in its
331
+ // series (see `colorizeLines`)
332
+ const lineColorOf = pointColorScale(lineStartColor, lineEndColor, lineColorSteps)
333
+
334
+ const draw = (context: CanvasContext) => {
335
+ const {context2D} = context
336
+
337
+ // create a map associating series-names to their time-series.
338
+ const boundedSeries = new Map<string, IteratePointSeries>(dataRef.current.map(series => {
339
+ return [
340
+ series.name,
341
+ series.data
342
+ .filter(datum => !isNaN(datum.iterateN))
343
+ .map((datum, index) => ({
344
+ n: datum.iterateN,
345
+ n_1: datum.iterateN_1,
346
+ time: datum.time,
347
+ index: index
348
+ })
349
+ ) as IteratePointSeries
350
+ ]
351
+ }))
352
+
353
+ context2D.save()
354
+ clipToArea(context, plotDimensions, {x: margin.left, y: margin.top})
355
+ context2D.translate(margin.left, margin.top)
356
+
357
+ // ---
358
+ // todo only want to do this once, on the first plot, and then leave it,
359
+ // unless the axes are updated, also needs to be removed/added when the
360
+ // plot size changes
361
+ // (the same, checked axes as the series' below: the default x- and y-axes)
362
+ const [xAxis, yAxis] = axesFor(
363
+ axisId => xAxesState.axisFor(axisId).getOrUndefined(),
364
+ axisId => yAxesState.axisFor(axisId).getOrUndefined(),
365
+ )
366
+ if (xAxis === undefined || yAxis === undefined) throw new Error('No default axis found')
367
+ // ---
368
+
369
+ const [xStart, xEnd] = xAxesState.axisDefaultId()
370
+ .map(id => xAxisRangesRef.current.get(id)?.original.asTuple() || [0, 0])
371
+ .getOrElse([0, 0])
372
+ const [yStart, yEnd] = yAxesState.axisDefaultId()
373
+ .map(id => yAxisRangesRef.current.get(id)?.original.asTuple() || [0, 0])
374
+ .getOrElse([0, 0])
375
+
376
+ // the fn = fn+1 diagonal reference line (no hover behavior, matching the old
377
+ // version's explicitly no-op mouseenter/mouseleave handlers)
378
+ context2D.strokeStyle = "grey"
379
+ context2D.lineWidth = 1
380
+ context2D.beginPath()
381
+ context2D.moveTo(xAxis.scale(xStart), yAxis.scale(yStart))
382
+ context2D.lineTo(xAxis.scale(xEnd), yAxis.scale(yEnd))
383
+ context2D.stroke()
384
+
385
+ const newGeometry = new Map<string, SeriesGeometry>()
386
+ const newPlotDataBySeries = new Map<string, IteratePointSeries>()
387
+ const hovered = hoveredPointRef.current
388
+
389
+ boundedSeries.forEach((data, name) => {
390
+ // grab the x and y axes assigned to the series, and if either or both
391
+ // axes aren't found, then give up and return
392
+ const [xAxisLinear, yAxisLinear] = axesFor(
393
+ axisId => xAxesState.axisFor(axisId).getOrUndefined(),
394
+ axisId => yAxesState.axisFor(axisId).getOrUndefined(),
395
+ )
396
+ if (xAxisLinear === undefined || yAxisLinear === undefined) return
397
+
398
+ // grab the style for the series
399
+ const seriesLineStyle: SeriesLineStyle = seriesStyles.get(name) || {
400
+ ...defaultLineStyle(),
401
+ highlightColor: defaultLineStyle().color
402
+ }
403
+
404
+ // only show the data for which the filter matches
405
+ const plotData = (name.match(seriesFilter)) ? data : FastShiftArray.empty<IteratePoint>()
406
+ newPlotDataBySeries.set(name, plotData)
407
+
408
+ // create the time-series path (when no interpolation is given, don't draw a
409
+ // connecting line at all -- matches the old version's `mainGElem.selectAll(...).remove()`)
410
+ // when colorizing the lines, a point's overall sequence number in its series is
411
+ // the number of points dropped from the series so far (when the data source
412
+ // counts them) plus its index -- the line leaving a point, and the point itself,
413
+ // are colored by it
414
+ const dropped = colorizeLines ? droppedCountFor(dataSource, name) : 0
415
+
416
+ if (interpolation !== undefined) {
417
+ const pathGenerator = d3.line<IteratePoint>()
418
+ .x(d => xAxis.scale(d.n || 0))
419
+ .y(d => yAxis.scale(d.n_1 || 0))
420
+ .curve(interpolation)
421
+
422
+ context2D.lineWidth = seriesLineStyle.lineWidth
423
+ if (colorizeLines) {
424
+ // the line's color changes along it, one step per point
425
+ const linePoints = Array.from(plotData)
426
+ strokeColorizedLine(
427
+ context2D, linePoints,
428
+ d => xAxis.scale(d.n || 0), d => yAxis.scale(d.n_1 || 0),
429
+ interpolation, index => lineColorOf(dropped + linePoints[index].index)
430
+ )
431
+ } else {
432
+ context2D.strokeStyle = seriesLineStyle.color
433
+ context2D.stroke(new Path2D(pathGenerator(Array.from(plotData)) ?? ""))
434
+ }
435
+ }
436
+
437
+ // when specified, show a circle for the actual data point
438
+ const points: Array<[number, number]> = []
439
+ if (showPoints) {
440
+ const isThisSeriesHovered = hovered?.seriesName === name
441
+
442
+ plotData.forEach((d: IteratePoint) => {
443
+ const x = xAxisLinear.scale(d.n) || 0
444
+ const y = yAxisLinear.scale(d.n_1) || 0
445
+ points.push([x + margin.left, y + margin.top])
446
+
447
+ const isHoveredPoint = isThisSeriesHovered && hovered!.index === d.index
448
+ const isNeighborOfHovered = isThisSeriesHovered &&
449
+ (hovered!.index === d.index - 1 || hovered!.index === d.index + 1)
450
+
451
+ if (isHoveredPoint) {
452
+ // the hovered point itself: enlarged, highlight-colored, no label
453
+ // (matches the old version, which only labeled the *neighbors*)
454
+ context2D.fillStyle = seriesLineStyle.highlightColor
455
+ context2D.beginPath()
456
+ context2D.arc(x, y, 5, 0, 2 * Math.PI)
457
+ context2D.fill()
458
+ } else if (isNeighborOfHovered) {
459
+ // a neighbor of the hovered point: enlarged, brighter fill, stroked,
460
+ // with a floating "n = i; t = X ms" label above it
461
+ context2D.fillStyle = d3.rgb(seriesLineStyle.highlightColor).brighter(0.7).toString()
462
+ context2D.strokeStyle = seriesLineStyle.color
463
+ context2D.lineWidth = seriesLineStyle.lineWidth
464
+ context2D.beginPath()
465
+ context2D.arc(x, y, 5, 0, 2 * Math.PI)
466
+ context2D.fill()
467
+ context2D.stroke()
468
+
469
+ const label = `n = ${d.index}; t = ${formatTime(d.time)} ms`
470
+ context2D.font = fontStringFor(11, 'sans-serif', 700)
471
+ const {width, height} = textDimensions(context2D, label)
472
+ const padding = 4
473
+ const circleRadius = 5
474
+ const circleStroke = seriesLineStyle.lineWidth
475
+
476
+ context2D.fillStyle = backgroundColor
477
+ context2D.fillRect(
478
+ x - padding / 2 - 8,
479
+ y - padding / 2 - circleRadius - circleStroke - height,
480
+ width + padding,
481
+ height + padding / 2
482
+ )
483
+
484
+ context2D.fillStyle = seriesLineStyle.highlightColor
485
+ context2D.textAlign = 'left'
486
+ context2D.textBaseline = 'alphabetic'
487
+ context2D.fillText(label, x - 8, y - circleRadius - circleStroke - padding)
488
+ } else {
489
+ // normal, unhovered point (in the color of the line leaving it, when
490
+ // the lines are colorized)
491
+ context2D.fillStyle = colorizeLines ?
492
+ lineColorOf(dropped + d.index) :
493
+ seriesLineStyle.color
494
+ context2D.beginPath()
495
+ context2D.arc(x, y, 2, 0, 2 * Math.PI)
496
+ context2D.fill()
497
+ }
498
+ })
499
+ }
500
+ newGeometry.set(`${name}::points`, {points, hitRadius: 6})
501
+ })
502
+
503
+ geometryRef.current = newGeometry
504
+ plotDataRef.current = newPlotDataBySeries
505
+
506
+ context2D.restore()
507
+ }
508
+
509
+ cc.register(`poincare-plot-${chartId}`, draw, 10)
510
+ cc.requestRedraw()
511
+ },
512
+ [
513
+ chartId, onUpdateChartTime, plotDimensions, xAxesState, yAxesState, seriesStyles, seriesFilter,
514
+ showPoints, interpolation, backgroundColor, colorizeLines, lineStartColor, lineEndColor,
515
+ lineColorSteps, dataSource, margin.left, margin.top
516
+ ]
213
517
  )
214
- const yAxesForSeries = useMemo(
215
- (): Array<string> => yAxesState.axisIds(),
216
- [yAxesState]
518
+
519
+ // need to keep the function references for use by the subscription, which forms a closure
520
+ // on them. without the references, the closures become stale, and resizing during streaming
521
+ // doesn't work properly
522
+ const updatePlotRef = useRef<(cc: CanvasContext) => void>(updatePlot)
523
+ useEffect(
524
+ () => {
525
+ updatePlotRef.current = updatePlot
526
+ },
527
+ [updatePlot]
528
+ )
529
+
530
+ // hands the current zoom/pan state (d3-zoom's cumulative `k`, and every axis' current domain) to
531
+ // the `onZoomStateChange` callback
532
+ const onZoomStateChangeRef = useRef(onZoomStateChange)
533
+ useEffect(
534
+ () => {
535
+ onZoomStateChangeRef.current = onZoomStateChange
536
+ },
537
+ [onZoomStateChange]
538
+ )
539
+ const reportZoomState = useCallback(
540
+ (): void => {
541
+ const ranges: Record<string, [number, number]> = {}
542
+ xAxisRangesRef.current.forEach((range, axisId) => ranges[axisId] = range.current.asTuple())
543
+ yAxisRangesRef.current.forEach((range, axisId) => ranges[axisId] = range.current.asTuple())
544
+ const k = canvasContext !== null ? d3.zoomTransform(canvasContext.canvas).k : 1
545
+ onZoomStateChangeRef.current({k, ranges})
546
+ },
547
+ [canvasContext]
217
548
  )
218
549
 
550
+ // panning (dragging) and zooming (the mouse wheel) -- see `useCanvasZoomPan`. Fully zooming out
551
+ // (back to a scale of 1) snaps the axes back to their original ranges.
552
+ const {resetZoomTransform, setZoomScale} = useCanvasZoomPan(canvasContext, {
553
+ panEnabled,
554
+ zoomEnabled,
555
+ keyModifiersRequired: zoomKeyModifiersRequired,
556
+ scaleExtent: [zoomMinScaleFactor, zoomMaxScaleFactor],
557
+ translateExtent: [[margin.left, margin.top], [plotDimensions.width, plotDimensions.height]],
558
+ // (tooltips are suppressed while panning and zooming, so that one isn't left behind)
559
+ onPanStart: () => {
560
+ allowTooltip.current = false
561
+ },
562
+ onPan: (dx, dy) => {
563
+ if (canvasContext === null) return
564
+ onPan(dx, dy, plotDimensions, dataRef.current.map(series => series.name), xAxisRangesRef.current, yAxisRangesRef.current)
565
+ updatePlotRef.current(canvasContext)
566
+ },
567
+ onPanEnd: () => {
568
+ allowTooltip.current = !(dataSource?.running ?? false)
569
+ reportZoomState()
570
+ },
571
+ onZoom: ({factor, k, x, y}) => {
572
+ if (canvasContext === null) return
573
+ allowTooltip.current = false
574
+ if (k === 1) {
575
+ snapToOriginalRanges()
576
+ } else {
577
+ onZoom(factor, x - margin.left, y - margin.top, plotDimensions, xAxisRangesRef.current, yAxisRangesRef.current)
578
+ }
579
+ updatePlotRef.current(canvasContext)
580
+ reportZoomState()
581
+ allowTooltip.current = true
582
+ },
583
+ onZoomReset,
584
+ resetView: () => {
585
+ snapToOriginalRanges()
586
+ if (canvasContext !== null) updatePlotRef.current(canvasContext)
587
+ reportZoomState()
588
+ },
589
+ })
590
+
591
+ // the zoom state to restore, captured once at mount (see the `zoomState` prop); cleared once
592
+ // it has been applied (see `restoreZoomState` below), so it's only ever applied once
593
+ const pendingZoomStateRef = useRef<PoincarePlotZoomState | undefined>(zoomState)
594
+
595
+ // applies the pending zoom state (if any) to the axes' ranges and to d3-zoom's transform on the
596
+ // canvas (a remounted plot has a brand-new canvas, whose transform starts at identity). Each
597
+ // range keeps its `.original` -- the axis' real, un-zoomed domain -- since the zoom handler's
598
+ // constraint is measured against it. Waits until the axes' ranges and the canvas exist.
599
+ const restoreZoomState = useEffectEvent((): void => {
600
+ const pending = pendingZoomStateRef.current
601
+ if (pending === undefined || canvasContext === null) return
602
+ if (xAxisRangesRef.current.size === 0 || yAxisRangesRef.current.size === 0) return
603
+
604
+ Object.entries(pending.ranges).forEach(([axisId, [start, end]]) => {
605
+ const interval = AxisInterval.from(start, end)
606
+ const ranges = xAxisRangesRef.current.has(axisId) ? xAxisRangesRef.current : yAxisRangesRef.current
607
+ const range = ranges.get(axisId)
608
+ const axis = xAxesState.axisFor(axisId).getOrUndefined() ?? yAxesState.axisFor(axisId).getOrUndefined()
609
+ if (range === undefined || axis === undefined) return
610
+ ranges.set(axisId, range.update(start, end))
611
+ setAxisIntervalFor(axisId, interval)
612
+ axis.update(interval, plotDimensions, margin)
613
+ })
614
+ // `__zoom` is where d3-zoom keeps an element's transform; setting it directly (rather than
615
+ // through `zoom.transform`) avoids dispatching a synthetic "zoom" event
616
+ setZoomScale(pending.k)
617
+ pendingZoomStateRef.current = undefined
618
+ updatePlotRef.current(canvasContext)
619
+ })
620
+
219
621
  // when the axes are available, then set the reference, but only once
220
622
  useEffect(() => {
221
623
  if (xAxesState.axes.size > 0 && xAxisRangesRef.current.size === 0) {
@@ -224,21 +626,35 @@ export function PoincarePlot(props: Props): null {
224
626
  if (yAxesState.axes.size > 0 && yAxisRangesRef.current.size === 0) {
225
627
  yAxisRangesRef.current = generateAxisRangeMap(yAxesState.axes)
226
628
  }
227
- }, [xAxesState, yAxesState]);
629
+ // restore a zoom state handed in from a previous instance (only does anything the first
630
+ // time the axes and canvas are all available -- see `restoreZoomState`)
631
+ restoreZoomState()
632
+ }, [xAxesState, yAxesState, canvasContext]);
633
+
634
+ // calculates the distinct axis IDs that cover all the series in the plot
635
+ const xAxesForSeries = useMemo(
636
+ (): Array<string> => xAxesState.axisIds(),
637
+ [xAxesState]
638
+ )
639
+ const yAxesForSeries = useMemo(
640
+ (): Array<string> => yAxesState.axisIds(),
641
+ [yAxesState]
642
+ )
643
+
228
644
 
229
645
  // update the plot with the new axes bounds
230
646
  const updateRangesAndPlot = useCallback(
231
647
  (): void => {
232
- if (mainG !== null) {
233
- updatePlotRef.current(mainG)
648
+ if (canvasContext !== null) {
649
+ updatePlotRef.current(canvasContext)
234
650
  }
235
651
  },
236
- [mainG]
652
+ [canvasContext]
237
653
  )
238
654
 
239
655
  // todo find better way
240
656
  // when the initial data changes, then reset the plot. note that the initial data doesn't change
241
- // during the normal course of updates from the observable, only when the plot is restarted.
657
+ // during the normal course of updates from the data source, only when the data source is replaced.
242
658
  useEffect(
243
659
  () => {
244
660
  dataRef.current = initialData.slice()
@@ -251,25 +667,34 @@ export function PoincarePlot(props: Props): null {
251
667
  )
252
668
 
253
669
  /**
254
- * When the axes bounds have changed, we need to reset the range references so that
255
- * the new axis ranges are used
670
+ * When the axes bounds have changed (from outside, e.g. a different iterate function), resets
671
+ * the range references so that the new axis ranges are used -- as the axes' new original
672
+ * (un-zoomed) domains -- resets the zoom, and reports the (reset) zoom state
256
673
  * @param updates The updates to the axes
257
674
  */
258
675
  const updatedBoundsHandler = useCallback(
259
676
  (updates: Map<string, ContinuousAxisRange>): void => {
677
+ // the new bounds become the axes' new un-zoomed (original) domains -- the zoom is reset
678
+ // to identity below, so they're the baseline the zoom constraint and the fully-zoomed-out
679
+ // snap (see `snapToOriginalRanges`) must measure against. The update itself can't be
680
+ // used as-is: `ContinuousAxis` carries the *previous* `.original` forward on a domain
681
+ // change, which would leave the zoom clamped to (and snapping back to) the old domain,
682
+ // e.g. [0, 1] after switching to an iterate function whose range is [-1, 1].
260
683
  updates.forEach((update, axisId) => {
684
+ const [start, end] = update.current.asTuple()
261
685
  if (xAxisRangesRef.current.has(axisId)) {
262
- xAxisRangesRef.current.set(axisId, update)
686
+ xAxisRangesRef.current.set(axisId, ContinuousAxisRange.from(start, end))
263
687
  }
264
688
  if (yAxisRangesRef.current.has(axisId)) {
265
- yAxisRangesRef.current.set(axisId, update)
689
+ yAxisRangesRef.current.set(axisId, ContinuousAxisRange.from(start, end))
266
690
  }
267
691
  })
268
- if (zoomEnabled && zoomSelectionRef.current !== undefined && zoomRef.current !== undefined) {
269
- zoomSelectionRef.current.call(zoomRef.current.transform, d3.zoomIdentity)
270
- }
692
+ if (zoomEnabled) resetZoomTransform()
693
+ // the axes' bounds were changed from outside (e.g. a different iterate function), which
694
+ // resets the zoom, so a previously reported zoom state no longer applies
695
+ reportZoomState()
271
696
  },
272
- [zoomEnabled]
697
+ [zoomEnabled, resetZoomTransform, reportZoomState]
273
698
  )
274
699
 
275
700
  // strange construct so that we only add the update handler when the chart ID
@@ -294,11 +719,13 @@ export function PoincarePlot(props: Props): null {
294
719
  );
295
720
 
296
721
  /**
297
- * Adjusts the time-range and updates the plot when the plot is dragged to the left or right
298
- * @param x The amount that the plot is dragged
722
+ * Adjusts the x- and y-axes' ranges when the plot is dragged (the caller redraws the plot)
723
+ * @param x The amount that the plot is dragged in x
724
+ * @param y The amount that the plot is dragged in y
299
725
  * @param plotDimensions The dimensions of the plot
300
726
  * @param series An array of series names
301
- * @param ranges A map holding the axis ID and its associated time range
727
+ * @param xRanges A map holding the x-axis ID and its associated range
728
+ * @param yRanges A map holding the y-axis ID and its associated range
302
729
  */
303
730
  const onPan = useCallback(
304
731
  (
@@ -317,20 +744,46 @@ export function PoincarePlot(props: Props): null {
317
744
  [xAxesForSeries, yAxesForSeries, margin, setAxisIntervalFor, xAxesState, yAxesState]
318
745
  )
319
746
 
747
+ /**
748
+ * Restores every axis to its original (un-zoomed) domain. Used when d3-zoom's cumulative `k`
749
+ * returns to exactly 1 (fully zoomed out): composing pivot-preserving zooms at *different*
750
+ * pivots isn't position-preserving (zooming in at one spot and back out at another nets a
751
+ * translation, as in any pinch-zoom UI), and the zoom constraint clamps that translated window
752
+ * to the original bounds -- so without this, zooming in at one spot and out at another stops
753
+ * short of the full view on both axes, with `k` already at its maximum. d3-zoom's `scaleExtent`
754
+ * clamps `k` to *exactly* 1 at that limit, so the equality check reliably detects it. (This is
755
+ * the continuous-axis counterpart of `OrdinalAxisRange.scaledCumulative`'s `factor === 1` case.)
756
+ */
757
+ const snapToOriginalRanges = useCallback(
758
+ (): void => {
759
+ const snap = (ranges: Map<string, ContinuousAxisRange>, axesState: typeof xAxesState) =>
760
+ ranges.forEach((range, axisId) => {
761
+ ranges.set(axisId, range.update(range.original.start, range.original.end))
762
+ setAxisIntervalFor(axisId, range.original)
763
+ axesState.axisFor(axisId).ifPresent(axis => axis.update(range.original, plotDimensions, margin))
764
+ })
765
+ snap(xAxisRangesRef.current, xAxesState)
766
+ snap(yAxisRangesRef.current, yAxesState)
767
+ },
768
+ [setAxisIntervalFor, xAxesState, yAxesState, plotDimensions, margin]
769
+ )
770
+
320
771
  /**
321
772
  * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out. Zooms in/out
322
773
  * at the location of the mouse when the scroll wheel or gesture was applied. Unlike time-series
323
774
  * plots, the iterates plot zooms the x- and y-axis at the same rate.
324
- * @param transform The d3 zoom transformation information
775
+ * @param zoomFactor The *incremental* zoom scale factor for this event -- see
776
+ * `useCanvasZoomPan` for how d3-zoom's own cumulative `k` is converted to this incremental
777
+ * form before this is called.
325
778
  * @param x The x-position of the mouse when the scroll wheel or gesture is used
326
779
  * @param y The y-position of the mouse when the scroll wheel or gesture is used
327
780
  * @param plotDimensions The dimensions of the plot
328
- * @param series An array of series names
329
- * @param ranges A map holding the axis ID and its associated time-range
781
+ * @param xRanges A map holding the x-axis ID and its associated range
782
+ * @param yRanges A map holding the y-axis ID and its associated range
330
783
  */
331
784
  const onZoom = useCallback(
332
785
  (
333
- transform: ZoomTransform,
786
+ zoomFactor: number,
334
787
  x: number,
335
788
  y: number,
336
789
  plotDimensions: Dimensions,
@@ -338,494 +791,259 @@ export function PoincarePlot(props: Props): null {
338
791
  yRanges: Map<string, ContinuousAxisRange>
339
792
  ) => axesZoomHandler(
340
793
  xAxesForSeries, yAxesForSeries, margin, setAxisIntervalFor, xAxesState, yAxesState, [zoomMinScaleFactor, zoomMaxScaleFactor]
341
- )(transform, [x, y], plotDimensions, xRanges, yRanges),
794
+ )(zoomFactor, [x, y], plotDimensions, xRanges, yRanges),
342
795
  [xAxesForSeries, yAxesForSeries, margin, setAxisIntervalFor, xAxesState, yAxesState, zoomMinScaleFactor, zoomMaxScaleFactor]
343
796
  )
344
797
 
345
- const updatePlot = useCallback(
346
- /**
347
- * Updates the plot data for the specified time-range, which may have changed due to zoom or pan
348
- * @param mainGElem The main <g> element selection for that holds the plot
349
- */
350
- (mainGElem: GSelection) => {
351
- if (container) {
352
- onUpdateChartTime(currentTimeRef.current)
353
798
 
354
- // select the svg element bind the data to them
355
- const svg = d3.select<SVGSVGElement, any>(container)
799
+ const onUpdateAxesBoundsRef = useRef(updateAxisRanges)
800
+ useEffect(
801
+ () => {
802
+ onUpdateAxesBoundsRef.current = updateAxisRanges
803
+ },
804
+ [updateAxisRanges]
805
+ )
356
806
 
357
- // create a map associating series-names to their time-series.
358
- //
359
- // performance-related confusion: wondering where the dataRef is updated? well it isn't
360
- // directly. The dataRef holds on to an array of references to the Series. And so does the
361
- // seriesRef, though it uses a map(series_name -> series). The seriesRef is used to append
362
- // data to the underlying Series, and the dataRef is used so that we can just use
363
- // dataRef.current and don't have to do Array.from(seriesRef.current.values()) which
364
- // creates a temporary array
365
- // const offset = 1
366
- const boundedSeries = new Map<string, IteratePointSeries>(dataRef.current.map(series => {
367
- return [
368
- series.name,
369
- series.data
370
- .filter(datum => !isNaN(datum.iterateN))
371
- .map((datum, index) => ({
372
- n: datum.iterateN,
373
- n_1: datum.iterateN_1,
374
- time: datum.time,
375
- index: index
376
- })
377
- ) as IteratePointSeries
378
- ]
379
- }))
807
+ // updates the plot when the interpolation and filter change, because the updatePlot
808
+ // callback has changed.
809
+ useEffect(
810
+ () => {
811
+ if (canvasContext) {
812
+ updatePlot(canvasContext)
813
+ }
814
+ },
815
+ [axisRangeFor, canvasContext, updatePlot]
816
+ )
380
817
 
381
- // set up panning
382
- if (panEnabled) {
383
- const drag = d3.drag<SVGSVGElement, Datum>()
384
- .on("start", () => {
385
- d3.select(container).style("cursor", "move")
386
- // during panning, we need to disable viewing the tooltip to prevent
387
- // tooltips from rendering but not getting removed
388
- allowTooltip.current = false;
389
- })
390
- .on("drag", event => {
391
- onPan(
392
- event.dx,
393
- event.dy,
394
- plotDimensions,
395
- Array.from(boundedSeries.keys()),
396
- xAxisRangesRef.current,
397
- yAxisRangesRef.current
398
- )
399
- updatePlotRef.current(mainGElem)
400
- })
401
- .on("end", () => {
402
- d3.select(container).style("cursor", "auto")
403
- // during panning, we disabled viewing the tooltip to prevent
404
- // tooltips from rendering but not getting removed, now that panning
405
- // is over, allow tooltips to render again
406
- allowTooltip.current = isSubscriptionClosed();
407
- })
818
+ // wires up a single mousemove/mouseleave listener on the shared canvas to replace the old
819
+ // per-element SVG mouseenter/mouseleave handlers on the point circles. Hit-tests the mouse
820
+ // position against the last-drawn point geometry (see `geometryRef`, populated by the draw
821
+ // function above).
822
+ useEffect(
823
+ () => {
824
+ if (!canvasContext) return
825
+
826
+ const canvas = canvasContext.canvas
827
+
828
+ // highlights (or un-highlights) the shared x- and y-axes -- keyed on the series name
829
+ // alone (not the specific hovered point index), so moving the mouse between a
830
+ // series' own points doesn't flicker the highlight off and on
831
+ const highlightAxesFor = (seriesName: string, isHighlighted: boolean): void => {
832
+ if (!highlightAxesOnMouseOver) return
833
+ const [xAxis, yAxis] = axesFor(
834
+ axisId => xAxesState.axisFor(axisId).getOrUndefined(),
835
+ axisId => yAxesState.axisFor(axisId).getOrUndefined(),
836
+ )
837
+ const {highlightColor, highlightWidth} = seriesStyles.get(seriesName) || defaultLineStyle()
838
+ xAxis?.setHighlighted(isHighlighted, highlightColor, highlightWidth)
839
+ yAxis?.setHighlighted(isHighlighted, highlightColor, highlightWidth)
840
+ }
408
841
 
409
- svg.call(drag)
410
- }
842
+ const handleMove = (event: MouseEvent) => {
843
+ if (!allowTooltip.current || !tooltipVisible) return
411
844
 
412
- // set up for zooming
413
- if (zoomEnabled) {
414
- zoomRef.current = d3.zoom<SVGSVGElement, Datum>()
415
- .filter(event => !zoomKeyModifiersRequired || event.shiftKey || event.ctrlKey)
416
- .scaleExtent([zoomMinScaleFactor, zoomMaxScaleFactor])
417
- .translateExtent([[margin.left, margin.top], [plotDimensions.width, plotDimensions.height]])
418
- .on("zoom", event => {
419
- allowTooltip.current = false
420
- if (event.sourceEvent !== null) {
421
- onZoom(
422
- event.transform,
423
- event.sourceEvent.offsetX - margin.left,
424
- event.sourceEvent.offsetY - margin.top,
425
- plotDimensions,
426
- xAxisRangesRef.current,
427
- yAxisRangesRef.current
428
- )
429
- updatePlotRef.current(mainGElem)
430
- }
431
- allowTooltip.current = true
432
- }
433
- )
845
+ const [x, y] = canvasLocalPoint(event, canvas)
846
+ const hit = seriesAt(x, y, geometryRef.current)
847
+ const hitSeriesName = hit?.name.replace(/::points$/, '')
848
+
849
+ const previous = hoveredPointRef.current
434
850
 
435
- zoomSelectionRef.current = svg.call(zoomRef.current)
851
+ if (previous?.seriesName !== hitSeriesName) {
852
+ if (previous !== undefined) highlightAxesFor(previous.seriesName, false)
853
+ if (hitSeriesName !== undefined) highlightAxesFor(hitSeriesName, true)
436
854
  }
437
855
 
438
- // define the clip-path so that the series lines don't go beyond the plot area
439
- const clipPathId = setClipPathG(chartId, mainGElem, plotDimensions)
856
+ const sameAsBefore = previous !== undefined && hit !== undefined &&
857
+ previous.seriesName === hitSeriesName && previous.index === hit.index
440
858
 
441
- // ---
442
- // todo only want to do this once, on the first plot, and then leave it,
443
- // unless the axes are updated, also needs to be removed/added when the
444
- // plot size changes
445
- const xAxis = xAxesState.defaultAxis().getOrThrow(() => new Error('No default axis found')) as ContinuousNumericAxis
446
- const yAxis = yAxesState.defaultAxis().getOrThrow(() => new Error('No default axis found')) as ContinuousNumericAxis
859
+ if (sameAsBefore) return
447
860
 
448
- const lineGenerator = d3.line<[x: number, y: number]>()
449
- .x(d => xAxis.scale(d[0] || 0))
450
- .y(d => yAxis.scale(d[1] || 0))
451
- // ---
861
+ if (previous !== undefined) {
862
+ handleMouseLeavePoint(previous.seriesName, mouseLeaveHandlerFor(`tooltip-${chartId}`))
863
+ }
452
864
 
453
- const [xStart, xEnd] = xAxesState.axisDefaultId()
454
- .map(id => xAxisRangesRef.current.get(id)?.original.asTuple() || [0, 0])
455
- .getOrElse([0, 0])
456
- const [yStart, yEnd] = yAxesState.axisDefaultId()
457
- .map(id => yAxisRangesRef.current.get(id)?.original.asTuple() || [0, 0])
458
- .getOrElse([0, 0])
459
- // const [xStart, xEnd] = xAxisRangesRef.current.get(xAxesState.axisDefaultId())?.original.asTuple() || [0, 0]
460
- // const [yStart, yEnd] = yAxisRangesRef.current.get(yAxesState.axisDefaultId())?.original.asTuple() || [0, 0]
461
-
462
- mainGElem
463
- .selectAll(`#fn-equals-fn1-${chartId}-poincare`)
464
- .data([[[xStart, yStart], [xEnd, yEnd]] as Array<[x: number, y: number]>])
465
- .join(enter => enter
466
- .select("path")
467
- .style("stroke", "grey")
468
- .style("fill", "none")
469
- .style("stroke-width", "1px")
470
- .attr("class", `fn-equals-fn1-poincare`)
471
- .attr("id", `#fn-equals-fn1-${chartId}-poincare`)
472
- .attr('transform', `translate(${margin.left}, ${margin.top})`)
473
- .attr("d", lineGenerator),
474
- update => update.remove(),
475
- exit => exit.remove()
476
- )
477
- // clear out mouse-enter callbacks for the diagonal line
478
- .on("mouseenter", () => {
479
- })
480
- .on("mouseleave", () => {
481
- })
865
+ if (hit !== undefined && hitSeriesName !== undefined) {
866
+ const plotData = plotDataRef.current.get(hitSeriesName)
867
+ if (plotData !== undefined) {
868
+ handleMouseEnterPoint(
869
+ hitSeriesName,
870
+ plotData[hit.index],
871
+ plotData,
872
+ [x, y],
873
+ mouseOverHandlerFor(`tooltip-${chartId}`)
874
+ )
875
+ }
876
+ }
482
877
 
483
- boundedSeries.forEach((data, name) => {
484
- // grab the x and y axes assigned to the series, and if either or both
485
- // axes aren't found, then give up and return
486
- const [xAxisLinear, yAxisLinear] = axesFor(
487
- axisId => xAxesState.axisFor(axisId).getOrUndefined(),
488
- axisId => yAxesState.axisFor(axisId).getOrUndefined(),
489
- )
490
- if (xAxisLinear === undefined || yAxisLinear === undefined) return
878
+ hoveredPointRef.current = hit !== undefined && hitSeriesName !== undefined ?
879
+ {seriesName: hitSeriesName, index: hit.index} :
880
+ undefined
491
881
 
492
- // grab the style for the series
493
- const seriesLineStyle: SeriesLineStyle = seriesStyles.get(name) || {
494
- ...defaultLineStyle(),
495
- highlightColor: defaultLineStyle().color
496
- }
882
+ // hover state affects the highlight/label drawing, so request a redraw
883
+ canvasContext.requestRedraw()
884
+ }
497
885
 
498
- // only show the data for which the filter matches
499
- const plotData = (name.match(seriesFilter)) ? data : []
500
-
501
- const pathGenerator = d3.line<IteratePoint>()
502
- .x(d => xAxis.scale(d.n || 0))
503
- .y(d => yAxis.scale(d.n_1 || 0))
504
-
505
- if (interpolation === undefined) {
506
- mainGElem
507
- .selectAll(`#${name}-${chartId}-poincare`)
508
- .remove()
509
- } else {
510
- // create the time-series paths
511
- mainGElem
512
- .selectAll(`#${name}-${chartId}-poincare`)
513
- .data([[], plotData], () => `${name}`)
514
- .join(
515
- enter => enter
516
- .append("path")
517
- .attr("class", 'iterate-series-lines')
518
- .attr("id", `${name}-${chartId}-poincare`)
519
- .attr("d", pathGenerator.curve(interpolation))
520
- .style("fill", "none")
521
- .style("stroke", seriesLineStyle.color)
522
- .style("stroke-width", seriesLineStyle.lineWidth)
523
- .attr('transform', `translate(${margin.left}, ${margin.top})`)
524
- .attr("clip-path", `url(#${clipPathId})`)
525
- ,
526
- update => update,
527
- exit => exit.remove()
528
- )
529
- }
886
+ const handleLeaveCanvas = () => {
887
+ const previous = hoveredPointRef.current
888
+ if (previous !== undefined) {
889
+ handleMouseLeavePoint(previous.seriesName, mouseLeaveHandlerFor(`tooltip-${chartId}`))
890
+ highlightAxesFor(previous.seriesName, false)
891
+ hoveredPointRef.current = undefined
892
+ canvasContext.requestRedraw()
893
+ }
894
+ }
530
895
 
531
- // when specified, show a circle for the actual data point
532
- if (showPoints) {
533
- mainGElem
534
- .selectAll(`.${name}-${chartId}-poincare-points`)
535
- .data(plotData, () => `${name}`)
536
- .join(
537
- enter => enter
538
- .append("circle")
539
- .attr("class", `${name}-${chartId}-poincare-points`)
540
- .attr("id", (_, index) => `${name}-${chartId}-poincare-point-${index}`)
541
- .attr("fill", seriesLineStyle.color)
542
- .attr("stroke", "none")
543
- .attr("cx", (d: IteratePoint) => xAxisLinear.scale(d.n) || 0)
544
- .attr("cy", (d: IteratePoint) => yAxisLinear.scale(d.n_1) || 0)
545
- .attr("r", 2)
546
- .attr('transform', `translate(${margin.left}, ${margin.top})`)
547
- .attr("clip-path", `url(#${clipPathId})`)
548
- ,
549
- update => update
550
- .attr("cx", (d: IteratePoint) => xAxisLinear.scale(d.n) || 0)
551
- .attr("cy", (d: IteratePoint) => yAxisLinear.scale(d.n_1) || 0)
552
- ,
553
- exit => exit.remove()
554
- )
555
- .on("mouseenter",
556
- (event: React.MouseEvent<SVGCircleElement>, datum: IteratePoint) => {
557
- if (allowTooltip.current && tooltipVisible) {
558
- return handleMouseEnterPoint(
559
- chartId,
560
- name,
561
- container,
562
- event,
563
- datum,
564
- plotData,
565
- xAxisLinear,
566
- yAxisLinear,
567
- margin,
568
- seriesLineStyle,
569
- backgroundColor,
570
- allowTooltip.current,
571
- mouseOverHandlerFor(`tooltip-${chartId}`)
572
- )
573
- }
574
- return <></>
575
- }
576
- )
577
- .on("mouseleave", () => {
578
- handleMouseLeavePoint(
579
- chartId,
580
- name,
581
- seriesLineStyle.color,
582
- mouseLeaveHandlerFor(`tooltip-${chartId}`)
583
- )
584
- })
585
- }
586
- })
896
+ canvas.addEventListener('mousemove', handleMove)
897
+ canvas.addEventListener('mouseleave', handleLeaveCanvas)
898
+ return () => {
899
+ canvas.removeEventListener('mousemove', handleMove)
900
+ canvas.removeEventListener('mouseleave', handleLeaveCanvas)
587
901
  }
588
902
  },
589
903
  [
590
- container, onUpdateChartTime, panEnabled, zoomEnabled, chartId, plotDimensions, margin,
591
- xAxesState, yAxesState,
592
- onPan,
593
- zoomMinScaleFactor, zoomMaxScaleFactor, zoomKeyModifiersRequired, onZoom,
594
- seriesStyles, seriesFilter, showPoints,
595
- interpolation, backgroundColor,
596
- mouseOverHandlerFor, mouseLeaveHandlerFor,
597
- tooltipVisible,
904
+ canvasContext, chartId, mouseOverHandlerFor, mouseLeaveHandlerFor, tooltipVisible,
905
+ highlightAxesOnMouseOver, xAxesState, yAxesState, seriesStyles
598
906
  ]
599
907
  )
600
908
 
601
- // need to keep the function references for use by the subscription, which forms a closure
602
- // on them. without the references, the closures become stale, and resizing during streaming
603
- // doesn't work properly
604
- const updatePlotRef = useRef(updatePlot)
909
+ // the latest `updateRangesAndPlot`, for the view driver to call -- the driver is created once
910
+ // per run (see below), so it must not capture a stale one (e.g. bound to an older canvas)
911
+ const updateRangesAndPlotRef = useRef(updateRangesAndPlot)
605
912
  useEffect(
606
913
  () => {
607
- updatePlotRef.current = updatePlot
914
+ updateRangesAndPlotRef.current = updateRangesAndPlot
608
915
  },
609
- [updatePlot]
916
+ [updateRangesAndPlot]
610
917
  )
611
- const onUpdateAxesBoundsRef = useRef(updateAxisRanges)
612
- useEffect(
613
- () => {
614
- onUpdateAxesBoundsRef.current = updateAxisRanges
615
- },
616
- [updateAxisRanges]
918
+
919
+ // creates the view driver that keeps this plot's view in step with the data source, using the
920
+ // current settings -- see `useStreamingTimeAxes`'s identical effect event
921
+ const createViewDriver = useEffectEvent(
922
+ (source: NonNullable<typeof dataSource>) => iteratesViewDriverFor(
923
+ source,
924
+ windowingTime,
925
+ xAxesState,
926
+ yAxesState,
927
+ () => updateRangesAndPlotRef.current(),
928
+ end => currentTimeRef.current = end
929
+ )
617
930
  )
618
931
 
619
- // memoized function for subscribing to the chart-data observable
620
- const subscribe = useCallback(
932
+ // while the data source is running (and this plot has a canvas), drive the view from the
933
+ // data source's updates -- see `useStreamingTimeAxes`'s identical effect. The driver belongs to this plot
934
+ // instance alone and is always torn down with it.
935
+ useEffect(
621
936
  () => {
622
- if (seriesObservable === undefined || mainG === null) return undefined
623
- return subscriptionIteratesFor(
624
- seriesObservable as Observable<IterateChartData>,
625
- onSubscribe,
626
- windowingTime,
627
- xAxesState,
628
- yAxesState,
629
- onUpdateData,
630
- dropDataAfter,
631
- updateRangesAndPlot,
632
- // as new data flows into the subscription, the subscription
633
- // updates this map directly (for performance)
634
- seriesRef.current,
635
- end => currentTimeRef.current = end
636
- )
937
+ if (dataSource === undefined || !running || canvasContext === null) return
938
+ allowTooltip.current = false
939
+ const driver = createViewDriver(dataSource)
940
+ return () => {
941
+ driver.unsubscribe()
942
+ allowTooltip.current = true
943
+ }
637
944
  },
638
- [
639
- dropDataAfter, mainG,
640
- onSubscribe, onUpdateData,
641
- seriesObservable, updateRangesAndPlot, windowingTime,
642
- xAxesState, yAxesState,
643
- ]
945
+ [dataSource, running, canvasContext]
644
946
  )
645
947
 
646
- // updates the plot when the interpolation and filter change, because the updatePlot
647
- // callback has changed.
948
+ // this plot needs a data source (see the `Chart`'s `dataSource` prop)
648
949
  useEffect(
649
950
  () => {
650
- if (container && mainG) {
651
- updatePlot(mainG)
951
+ if (dataSource === undefined) {
952
+ console.error("PoincarePlot requires a data source; set the enclosing Chart's `dataSource` prop")
652
953
  }
653
954
  },
654
- [axisRangeFor, container, mainG, updatePlot]
955
+ [dataSource]
655
956
  )
656
957
 
657
- // subscribe/unsubscribe to the observable chart data. when the `shouldSubscribe`
658
- // is changed to `true` and we haven't subscribed yet, then subscribe. when the
659
- // `shouldSubscribe` is `false` and we had subscribed, then unsubscribe. otherwise,
660
- // do nothing.
958
+ // unregister this plot's draw function on unmount
661
959
  useEffect(
662
960
  () => {
663
- if (shouldSubscribe && subscriptionRef.current === undefined) {
664
- subscriptionRef.current = subscribe()
665
- allowTooltip.current = false
666
- } else if (!shouldSubscribe && subscriptionRef.current !== undefined) {
667
- subscriptionRef.current?.unsubscribe()
668
- subscriptionRef.current = undefined
669
- allowTooltip.current = true
961
+ return () => {
962
+ if (canvasContext) {
963
+ canvasContext.unregister(`poincare-plot-${chartId}`)
964
+ }
670
965
  }
671
966
  },
672
- [shouldSubscribe, subscribe]
967
+ [canvasContext, chartId]
673
968
  )
674
969
 
675
970
  return null
676
971
  }
677
972
 
678
973
  /**
679
- * Attempts to locate the x- and y-axes for the specified series. If no axis is found for the
680
- * series name, then uses the default returned by the useChart() hook
974
+ * @param dataSource The chart's data source
975
+ * @param seriesName The name of the series
976
+ * @return The number of points dropped from the series so far, when the data source counts them
977
+ * (an {@link IteratesDataSource} does); otherwise 0
978
+ */
979
+ function droppedCountFor(dataSource: unknown, seriesName: string): number {
980
+ return dataSource !== null && typeof dataSource === 'object' && 'droppedCountFor' in dataSource ?
981
+ (dataSource as {droppedCountFor: (name: string) => number}).droppedCountFor(seriesName) :
982
+ 0
983
+ }
984
+
985
+ /**
986
+ * Locates the plot's x- and y-axes: always the default (first) ones, since every series of a
987
+ * Poincaré plot shares the same axes
681
988
  * @param xAxisFor The function that accepts an axis ID and returns the corresponding x-axis
682
989
  * @param yAxisFor The function that accepts an axis ID and returns the corresponding y-axis
990
+ * @return The x- and y-axes (either is actually `undefined` when it isn't found, despite the type)
991
+ * @throws Error when either axis isn't a continuous numeric axis
683
992
  */
684
993
  function axesFor(
685
994
  xAxisFor: (id: string) => BaseAxis | undefined,
686
995
  yAxisFor: (id: string) => BaseAxis | undefined,
687
996
  ): [xAxis: ContinuousNumericAxis, yAxis: ContinuousNumericAxis] {
688
997
  const xAxis = xAxisFor("")
689
- const xAxisLinear = xAxis as ContinuousNumericAxis
690
998
  const yAxis = yAxisFor("")
999
+ checkAxisType("PoincarePlot", "x", xAxis, AxisType.ContinuousNumeric)
1000
+ checkAxisType("PoincarePlot", "y", yAxis, AxisType.ContinuousNumeric)
1001
+ const xAxisLinear = xAxis as ContinuousNumericAxis
691
1002
  const yAxisLinear = yAxis as ContinuousNumericAxis
692
- if (xAxis && !xAxisLinear) {
693
- throw Error("Poincare plot requires that x-axis be of type LinearAxis")
694
- }
695
- if (yAxis && !yAxisLinear) {
696
- throw Error("Poincare plot requires that y-axis be of type LinearAxis")
697
- }
698
1003
  return [xAxisLinear, yAxisLinear]
699
1004
  }
700
1005
 
701
1006
  /**
702
- * @param chartId The ID of the chart
1007
+ * Reports the tooltip for the hovered point. Replaces the old version, which also directly
1008
+ * mutated the hovered SVG `<circle>`'s radius/fill (and its neighbors') as a side effect; that
1009
+ * highlighting is now handled entirely by the draw function reading `hoveredPointRef` (see
1010
+ * `updatePlot`'s `draw`) rather than by touching elements here.
703
1011
  * @param seriesName The name of the series (i.e. the neuron ID)
704
- * @param container The SVG container holding the plot
705
- * @param event The mouse event that triggered this call
706
- * @param datum The datum over which the mouse has entered
1012
+ * @param datum The hovered point, or `undefined` if the index doesn't resolve to a point
707
1013
  * @param plotData The iterates series
708
- * @param xAxisLinear The x-axis (f[n](x))
709
- * @param yAxisLinear The y-axis (f[n+1](x))
710
- * @param margin The plot margin
711
- * @param seriesStyle The series style information (needed for (un)highlighting)
712
- * @param backgroundColor
713
- * @param allowTooltip When set to `false` won't show tooltip, even if it is visible (used by pan)
1014
+ * @param mouseCoords The `[x, y]` position of the mouse, in canvas coordinates
714
1015
  * @param mouseOverHandlerFor The handler for the mouse-over (registered by the <Tooltip/>)
715
1016
  */
716
1017
  function handleMouseEnterPoint(
717
- chartId: number,
718
1018
  seriesName: string,
719
- container: SVGSVGElement,
720
- event: React.MouseEvent<SVGPathElement>,
721
- datum: IteratePoint,
1019
+ datum: IteratePoint | undefined,
722
1020
  plotData: IteratePointSeries,
723
- xAxisLinear: ContinuousNumericAxis,
724
- yAxisLinear: ContinuousNumericAxis,
725
- margin: Margin,
726
- seriesStyle: SeriesLineStyle,
727
- backgroundColor: string,
728
- allowTooltip: boolean,
1021
+ mouseCoords: [x: number, y: number],
729
1022
  mouseOverHandlerFor: ((seriesName: string, time: number, tooltipData: TooltipData<IterateDatum, NoTooltipMetadata>, mouseCoords: [x: number, y: number]) => void) | undefined,
730
1023
  ): void {
731
- const {color, highlightColor, lineWidth} = seriesStyle
732
-
733
- const padding = 4
734
- const circleRadius = 5
735
- const circleStroke = lineWidth
736
-
737
- const circle = event.currentTarget as SVGCircleElement
738
-
739
- d3.select<SVGPathElement, Datum>(circle)
740
- .attr("r", circleRadius)
741
- .style("fill", highlightColor)
742
-
743
- const index = plotData
744
- .findIndex(point => point.n === datum.n && point.n_1 === datum.n_1)
745
-
746
- /**
747
- * Displays basic information about an iterate, generally its neighbors
748
- * @param index The index of the iterate
749
- */
750
- function showInfo(index: number): void {
751
- d3.select(`#${seriesName}-${chartId}-poincare-point-${index}`)
752
- .attr("r", circleRadius)
753
- .style("fill", d3.rgb(highlightColor).brighter(0.7).toString())
754
- .style("stroke-width", circleStroke)
755
- .style("stroke", color)
756
-
757
- // grab the (x, y)-coordinates for the plot
758
- const iterateN = xAxisLinear.scale(plotData[index].n) + margin.left
759
- const iterateN_1 = yAxisLinear.scale(plotData[index].n_1) + margin.top
760
-
761
- const svg = d3.select<SVGSVGElement, any>(container)
762
-
763
- // add a rectangle that serves as the background for the text (to make the
764
- // text readable when the chart is busy)
765
- const rect = svg
766
- .append("rect")
767
- .attr("class", `${seriesName}-${chartId}-poincare-point-text-background`)
768
-
769
- // add the information about the iterate as a text element
770
- const textElement = svg
771
- .append("text")
772
- .attr('class', `${seriesName}-${chartId}-poincare-point-text`)
773
- .attr('fill', highlightColor)
774
- .attr('font-family', 'sans-serif')
775
- .attr('font-size', 11)
776
- .attr('font-weight', 700)
777
- .text(`n = ${index}; t = ${formatTime(plotData[index].time)} ms`)
778
-
779
- // calculate the width and height of the text element
780
- const {width, height} = textDimensions(textElement)
781
-
782
- textElement
783
- .attr("transform", `translate(${iterateN - 8}, ${iterateN_1 - circleRadius - circleStroke - padding})`)
784
-
785
- rect
786
- .attr("x", iterateN - padding / 2 - 8)
787
- .attr("y", iterateN_1 - padding / 2 - circleRadius - circleStroke - height)
788
- .attr("width", width + padding)
789
- .attr("height", height + padding / 2)
790
- .style("fill", backgroundColor)
791
- }
792
-
793
- if (index > 0) {
794
- showInfo(index - 1)
795
- }
796
- if (index < plotData.length - 1) {
797
- showInfo(index + 1)
798
- }
799
-
800
- const [x, y] = d3.pointer(event, container)
801
- const currentDatum: IteratePoint = (index >= 0) ? plotData[index] : {time: 0, n: 0, n_1: 0, index}
1024
+ if (datum === undefined) return
802
1025
 
803
- if (mouseOverHandlerFor && allowTooltip) {
1026
+ if (mouseOverHandlerFor) {
804
1027
  mouseOverHandlerFor(
805
1028
  seriesName,
806
- currentDatum.time,
1029
+ datum.time,
807
1030
  {series: plotData.map(ip => ({iterateN: ip.n, iterateN_1: ip.n_1, time: ip.time})), metadata: {}},
808
- [x, y]
1031
+ mouseCoords
809
1032
  )
810
1033
  }
811
1034
  }
812
1035
 
1036
+ /**
1037
+ * Calls the mouse-leave-series handler registered for this series (the highlight is cleared by
1038
+ * the draw function, once the hovered point is cleared)
1039
+ * @param seriesName The name of the series
1040
+ * @param mouseLeaverHandlerFor Registered handler for the series when the mouse leaves
1041
+ */
813
1042
  function handleMouseLeavePoint(
814
- chartId: number,
815
1043
  seriesName: string,
816
- color: string,
817
1044
  mouseLeaverHandlerFor: ((seriesName: string) => void) | undefined,
818
1045
  ): void {
819
- d3.selectAll<SVGPathElement, Datum>(`.${seriesName}-${chartId}-poincare-points`)
820
- .attr("r", 2)
821
- .style("fill", color)
822
- .style("stroke", "none")
823
- d3.selectAll(`.${seriesName}-${chartId}-poincare-point-arrows`).remove()
824
- d3.selectAll(`.${seriesName}-${chartId}-poincare-point-text`).remove()
825
- d3.selectAll(`.${seriesName}-${chartId}-poincare-point-text-background`).remove()
826
-
827
1046
  if (mouseLeaverHandlerFor) {
828
1047
  mouseLeaverHandlerFor(seriesName)
829
1048
  }
830
-
831
1049
  }