stream-charts 1.0.1 → 1.1.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 (271) hide show
  1. package/README.md +756 -343
  2. package/changes.md +7 -0
  3. package/dist/src/app/charts/Chart.d.ts +116 -87
  4. package/dist/src/app/charts/Chart.js +96 -101
  5. package/dist/src/app/charts/Chart.js.map +1 -1
  6. package/dist/src/app/charts/axes/AxesState.d.ts +64 -0
  7. package/dist/src/app/charts/axes/AxesState.js +97 -0
  8. package/dist/src/app/charts/axes/AxesState.js.map +1 -0
  9. package/dist/src/app/charts/axes/AxesState.test.js +30 -0
  10. package/dist/src/app/charts/axes/AxesState.test.js.map +1 -0
  11. package/dist/src/app/charts/axes/AxisInterval.d.ts +92 -0
  12. package/dist/src/app/charts/axes/AxisInterval.js +126 -0
  13. package/dist/src/app/charts/axes/AxisInterval.js.map +1 -0
  14. package/dist/src/app/charts/axes/AxisInterval.test.js +120 -0
  15. package/dist/src/app/charts/axes/AxisInterval.test.js.map +1 -0
  16. package/dist/src/app/charts/axes/BaseAxisRange.d.ts +83 -0
  17. package/dist/src/app/charts/axes/BaseAxisRange.js +58 -0
  18. package/dist/src/app/charts/axes/BaseAxisRange.js.map +1 -0
  19. package/dist/src/app/charts/axes/ContinuousAxis.d.ts +22 -0
  20. package/dist/src/app/charts/axes/ContinuousAxis.js +96 -0
  21. package/dist/src/app/charts/axes/ContinuousAxis.js.map +1 -0
  22. package/dist/src/app/charts/axes/ContinuousAxisRange.d.ts +59 -0
  23. package/dist/src/app/charts/axes/ContinuousAxisRange.js +84 -0
  24. package/dist/src/app/charts/axes/ContinuousAxisRange.js.map +1 -0
  25. package/dist/src/app/charts/axes/ContinuousAxisRange.test.js +47 -0
  26. package/dist/src/app/charts/axes/ContinuousAxisRange.test.js.map +1 -0
  27. package/dist/src/app/charts/axes/OrdinalAxis.d.ts +23 -0
  28. package/dist/src/app/charts/axes/OrdinalAxis.js +124 -0
  29. package/dist/src/app/charts/axes/OrdinalAxis.js.map +1 -0
  30. package/dist/src/app/charts/axes/OrdinalAxisRange.d.ts +69 -0
  31. package/dist/src/app/charts/axes/OrdinalAxisRange.js +100 -0
  32. package/dist/src/app/charts/axes/OrdinalAxisRange.js.map +1 -0
  33. package/dist/src/app/charts/axes/OrdinalAxisRange.test.js +48 -0
  34. package/dist/src/app/charts/axes/OrdinalAxisRange.test.js.map +1 -0
  35. package/dist/src/app/charts/axes/axes.d.ts +391 -0
  36. package/dist/src/app/charts/axes/axes.js +1046 -0
  37. package/dist/src/app/charts/axes/axes.js.map +1 -0
  38. package/dist/src/app/charts/d3types.d.ts +10 -10
  39. package/dist/src/app/charts/filters/regexFilter.d.ts +9 -0
  40. package/dist/src/app/charts/filters/regexFilter.js +18 -0
  41. package/dist/src/app/charts/filters/regexFilter.js.map +1 -0
  42. package/dist/src/app/charts/filters/regexFilter.test.d.ts +1 -0
  43. package/dist/src/app/charts/filters/regexFilter.test.js +11 -0
  44. package/dist/src/app/charts/filters/regexFilter.test.js.map +1 -0
  45. package/dist/src/app/charts/hooks/useAxes.d.ts +144 -0
  46. package/dist/src/app/charts/hooks/useAxes.js +187 -0
  47. package/dist/src/app/charts/hooks/useAxes.js.map +1 -0
  48. package/dist/src/app/charts/hooks/useChart.d.ts +30 -201
  49. package/dist/src/app/charts/hooks/useChart.js +33 -134
  50. package/dist/src/app/charts/hooks/useChart.js.map +1 -1
  51. package/dist/src/app/charts/hooks/useDataObservable.d.ts +74 -0
  52. package/dist/src/app/charts/hooks/useDataObservable.js +76 -0
  53. package/dist/src/app/charts/hooks/useDataObservable.js.map +1 -0
  54. package/dist/src/app/charts/hooks/useInitialData.d.ts +38 -0
  55. package/dist/src/app/charts/hooks/useInitialData.js +29 -0
  56. package/dist/src/app/charts/hooks/useInitialData.js.map +1 -0
  57. package/dist/src/app/charts/hooks/useMouse.d.ts +53 -0
  58. package/dist/src/app/charts/hooks/useMouse.js +65 -0
  59. package/dist/src/app/charts/hooks/useMouse.js.map +1 -0
  60. package/dist/src/app/charts/hooks/usePlotDimensions.d.ts +34 -0
  61. package/dist/src/app/charts/hooks/usePlotDimensions.js +73 -0
  62. package/dist/src/app/charts/hooks/usePlotDimensions.js.map +1 -0
  63. package/dist/src/app/charts/hooks/useTooltip.d.ts +64 -0
  64. package/dist/src/app/charts/hooks/useTooltip.js +52 -0
  65. package/dist/src/app/charts/hooks/useTooltip.js.map +1 -0
  66. package/dist/src/app/charts/observables/ChartData.d.ts +23 -0
  67. package/dist/src/app/charts/observables/ChartData.js +20 -0
  68. package/dist/src/app/charts/observables/ChartData.js.map +1 -0
  69. package/dist/src/app/charts/observables/iterates.d.ts +52 -0
  70. package/dist/src/app/charts/observables/iterates.js +121 -0
  71. package/dist/src/app/charts/observables/iterates.js.map +1 -0
  72. package/dist/src/app/charts/observables/iterates.test.d.ts +1 -0
  73. package/dist/src/app/charts/observables/iterates.test.js +180 -0
  74. package/dist/src/app/charts/observables/iterates.test.js.map +1 -0
  75. package/dist/src/app/charts/observables/ordinals.d.ts +75 -0
  76. package/dist/src/app/charts/observables/ordinals.js +141 -0
  77. package/dist/src/app/charts/observables/ordinals.js.map +1 -0
  78. package/dist/src/app/charts/observables/ordinals.test.d.ts +1 -0
  79. package/dist/src/app/charts/observables/ordinals.test.js +102 -0
  80. package/dist/src/app/charts/observables/ordinals.test.js.map +1 -0
  81. package/dist/src/app/charts/plots/BarPlot.d.ts +81 -0
  82. package/dist/src/app/charts/plots/BarPlot.js +557 -0
  83. package/dist/src/app/charts/plots/BarPlot.js.map +1 -0
  84. package/dist/src/app/charts/plots/PoincarePlot.d.ts +78 -0
  85. package/dist/src/app/charts/plots/PoincarePlot.js +504 -0
  86. package/dist/src/app/charts/plots/PoincarePlot.js.map +1 -0
  87. package/dist/src/app/charts/{RasterPlot.d.ts → plots/RasterPlot.d.ts} +1 -1
  88. package/dist/src/app/charts/{RasterPlot.js → plots/RasterPlot.js} +152 -133
  89. package/dist/src/app/charts/plots/RasterPlot.js.map +1 -0
  90. package/dist/src/app/charts/{ScatterPlot.d.ts → plots/ScatterPlot.d.ts} +10 -8
  91. package/dist/src/app/charts/plots/ScatterPlot.js +379 -0
  92. package/dist/src/app/charts/plots/ScatterPlot.js.map +1 -0
  93. package/dist/src/app/charts/{plot.d.ts → plots/plot.d.ts} +12 -14
  94. package/dist/src/app/charts/{plot.js → plots/plot.js} +19 -17
  95. package/dist/src/app/charts/plots/plot.js.map +1 -0
  96. package/dist/src/app/charts/series/baseSeries.d.ts +38 -0
  97. package/dist/src/app/charts/series/baseSeries.js +36 -0
  98. package/dist/src/app/charts/series/baseSeries.js.map +1 -0
  99. package/dist/src/app/charts/series/datumSeries.test.d.ts +1 -0
  100. package/dist/src/app/charts/series/datumSeries.test.js +50 -0
  101. package/dist/src/app/charts/series/datumSeries.test.js.map +1 -0
  102. package/dist/src/app/charts/series/iterateSeries.d.ts +42 -0
  103. package/dist/src/app/charts/series/iterateSeries.js +34 -0
  104. package/dist/src/app/charts/series/iterateSeries.js.map +1 -0
  105. package/dist/src/app/charts/series/ordinalSeries.d.ts +54 -0
  106. package/dist/src/app/charts/series/ordinalSeries.js +98 -0
  107. package/dist/src/app/charts/series/ordinalSeries.js.map +1 -0
  108. package/dist/src/app/charts/series/timeSeries.d.ts +30 -0
  109. package/dist/src/app/charts/series/timeSeries.js +19 -0
  110. package/dist/src/app/charts/series/timeSeries.js.map +1 -0
  111. package/dist/src/app/charts/{chartData.d.ts → series/timeSeriesChartData.d.ts} +11 -6
  112. package/dist/src/app/charts/series/timeSeriesChartData.js +32 -0
  113. package/dist/src/app/charts/series/timeSeriesChartData.js.map +1 -0
  114. package/dist/src/app/charts/styling/barPlotStyle.d.ts +41 -0
  115. package/dist/src/app/charts/styling/barPlotStyle.js +84 -0
  116. package/dist/src/app/charts/styling/barPlotStyle.js.map +1 -0
  117. package/dist/src/app/charts/{margins.d.ts → styling/margins.d.ts} +7 -5
  118. package/dist/src/app/charts/{margins.js → styling/margins.js} +19 -9
  119. package/dist/src/app/charts/styling/margins.js.map +1 -0
  120. package/dist/src/app/charts/styling/margins.test.d.ts +1 -0
  121. package/dist/src/app/charts/styling/margins.test.js +19 -0
  122. package/dist/src/app/charts/styling/margins.test.js.map +1 -0
  123. package/dist/src/app/charts/styling/svgStyle.d.ts +76 -0
  124. package/dist/src/app/charts/styling/svgStyle.js +84 -0
  125. package/dist/src/app/charts/styling/svgStyle.js.map +1 -0
  126. package/dist/src/app/charts/subscriptions/subscription.test.d.ts +1 -0
  127. package/dist/src/app/charts/subscriptions/subscription.test.js +395 -0
  128. package/dist/src/app/charts/subscriptions/subscription.test.js.map +1 -0
  129. package/dist/src/app/charts/subscriptions/subscriptions.d.ts +100 -0
  130. package/dist/src/app/charts/subscriptions/subscriptions.js +449 -0
  131. package/dist/src/app/charts/subscriptions/subscriptions.js.map +1 -0
  132. package/dist/src/app/charts/tooltips/BarPlotTooltipContent.d.ts +90 -0
  133. package/dist/src/app/charts/tooltips/BarPlotTooltipContent.js +302 -0
  134. package/dist/src/app/charts/tooltips/BarPlotTooltipContent.js.map +1 -0
  135. package/dist/src/app/charts/tooltips/PoincarePlotTooltipContent.d.ts +100 -0
  136. package/dist/src/app/charts/tooltips/PoincarePlotTooltipContent.js +190 -0
  137. package/dist/src/app/charts/tooltips/PoincarePlotTooltipContent.js.map +1 -0
  138. package/dist/src/app/charts/{RasterPlotTooltipContent.js → tooltips/RasterPlotTooltipContent.js} +40 -44
  139. package/dist/src/app/charts/tooltips/RasterPlotTooltipContent.js.map +1 -0
  140. package/dist/src/app/charts/tooltips/ScatterPlotTooltipContent.js +154 -0
  141. package/dist/src/app/charts/tooltips/ScatterPlotTooltipContent.js.map +1 -0
  142. package/dist/src/app/charts/{Tooltip.d.ts → tooltips/Tooltip.d.ts} +4 -2
  143. package/dist/src/app/charts/{Tooltip.js → tooltips/Tooltip.js} +23 -28
  144. package/dist/src/app/charts/tooltips/Tooltip.js.map +1 -0
  145. package/dist/src/app/charts/{tooltipUtils.d.ts → tooltips/tooltipUtils.d.ts} +11 -6
  146. package/dist/src/app/charts/{tooltipUtils.js → tooltips/tooltipUtils.js} +33 -17
  147. package/dist/src/app/charts/tooltips/tooltipUtils.js.map +1 -0
  148. package/dist/src/app/charts/{Tracker.d.ts → trackers/Tracker.d.ts} +6 -5
  149. package/dist/src/app/charts/trackers/Tracker.js +60 -0
  150. package/dist/src/app/charts/trackers/Tracker.js.map +1 -0
  151. package/dist/src/app/charts/{trackerUtils.d.ts → trackers/trackerUtils.d.ts} +13 -8
  152. package/dist/src/app/charts/trackers/trackerUtils.js +359 -0
  153. package/dist/src/app/charts/trackers/trackerUtils.js.map +1 -0
  154. package/dist/src/app/charts/utils.d.ts +22 -32
  155. package/dist/src/app/charts/utils.js +36 -37
  156. package/dist/src/app/charts/utils.js.map +1 -1
  157. package/dist/src/index.d.ts +48 -15
  158. package/dist/src/index.js +71 -15
  159. package/dist/src/index.js.map +1 -1
  160. package/package.json +30 -20
  161. package/src/app/charts/Chart.tsx +334 -0
  162. package/src/app/charts/axes/AxesState.test.ts +34 -0
  163. package/src/app/charts/axes/AxesState.ts +112 -0
  164. package/src/app/charts/axes/AxisInterval.test.ts +143 -0
  165. package/src/app/charts/axes/AxisInterval.ts +139 -0
  166. package/src/app/charts/axes/BaseAxisRange.ts +113 -0
  167. package/src/app/charts/axes/ContinuousAxis.tsx +192 -0
  168. package/src/app/charts/axes/ContinuousAxisRange.test.ts +55 -0
  169. package/src/app/charts/axes/ContinuousAxisRange.ts +96 -0
  170. package/src/app/charts/axes/OrdinalAxis.tsx +223 -0
  171. package/src/app/charts/axes/OrdinalAxisRange.test.ts +55 -0
  172. package/src/app/charts/axes/OrdinalAxisRange.ts +112 -0
  173. package/src/app/charts/axes/axes.ts +1569 -0
  174. package/src/app/charts/d3types.ts +16 -0
  175. package/src/app/charts/filters/regexFilter.test.ts +13 -0
  176. package/src/app/charts/filters/regexFilter.ts +17 -0
  177. package/src/app/charts/hooks/useAxes.tsx +341 -0
  178. package/src/app/charts/hooks/useChart.tsx +159 -0
  179. package/src/app/charts/hooks/useDataObservable.tsx +175 -0
  180. package/src/app/charts/hooks/useInitialData.tsx +65 -0
  181. package/src/app/charts/hooks/useMouse.tsx +125 -0
  182. package/src/app/charts/hooks/usePlotDimensions.tsx +120 -0
  183. package/src/app/charts/hooks/useTooltip.tsx +112 -0
  184. package/src/app/charts/observables/ChartData.ts +31 -0
  185. package/src/app/charts/observables/iterates.test.ts +201 -0
  186. package/src/app/charts/observables/iterates.ts +203 -0
  187. package/src/app/charts/observables/ordinals.test.ts +135 -0
  188. package/src/app/charts/observables/ordinals.ts +248 -0
  189. package/src/app/charts/plots/BarPlot.tsx +1088 -0
  190. package/src/app/charts/plots/PoincarePlot.tsx +832 -0
  191. package/src/app/charts/plots/RasterPlot.tsx +676 -0
  192. package/src/app/charts/plots/ScatterPlot.tsx +653 -0
  193. package/src/app/charts/plots/plot.ts +75 -0
  194. package/src/app/charts/series/baseSeries.ts +52 -0
  195. package/src/app/charts/series/datumSeries.test.ts +60 -0
  196. package/src/app/charts/series/iterateSeries.ts +56 -0
  197. package/src/app/charts/series/ordinalSeries.ts +164 -0
  198. package/src/app/charts/series/timeSeries.ts +40 -0
  199. package/src/app/charts/series/timeSeriesChartData.ts +68 -0
  200. package/src/app/charts/styling/barPlotStyle.ts +126 -0
  201. package/src/app/charts/styling/margins.test.ts +22 -0
  202. package/src/app/charts/styling/margins.ts +58 -0
  203. package/src/app/charts/styling/svgStyle.ts +118 -0
  204. package/src/app/charts/subscriptions/subscription.test.ts +499 -0
  205. package/src/app/charts/subscriptions/subscriptions.ts +635 -0
  206. package/src/app/charts/tooltips/BarPlotTooltipContent.tsx +440 -0
  207. package/src/app/charts/tooltips/PoincarePlotTooltipContent.tsx +352 -0
  208. package/src/app/charts/tooltips/RasterPlotTooltipContent.tsx +274 -0
  209. package/src/app/charts/tooltips/ScatterPlotTooltipContent.tsx +319 -0
  210. package/src/app/charts/tooltips/Tooltip.tsx +148 -0
  211. package/src/app/charts/tooltips/tooltipUtils.ts +216 -0
  212. package/src/app/charts/trackers/Tracker.tsx +129 -0
  213. package/src/app/charts/trackers/trackerUtils.ts +494 -0
  214. package/src/app/charts/utils.ts +109 -0
  215. package/dist/src/app/charts/CategoryAxis.d.ts +0 -20
  216. package/dist/src/app/charts/CategoryAxis.js +0 -112
  217. package/dist/src/app/charts/CategoryAxis.js.map +0 -1
  218. package/dist/src/app/charts/ContinuousAxis.d.ts +0 -16
  219. package/dist/src/app/charts/ContinuousAxis.js +0 -186
  220. package/dist/src/app/charts/ContinuousAxis.js.map +0 -1
  221. package/dist/src/app/charts/RasterPlot.js.map +0 -1
  222. package/dist/src/app/charts/RasterPlotTooltipContent.js.map +0 -1
  223. package/dist/src/app/charts/ScatterPlot.js +0 -351
  224. package/dist/src/app/charts/ScatterPlot.js.map +0 -1
  225. package/dist/src/app/charts/ScatterPlotTooltipContent.js +0 -163
  226. package/dist/src/app/charts/ScatterPlotTooltipContent.js.map +0 -1
  227. package/dist/src/app/charts/Tooltip.js.map +0 -1
  228. package/dist/src/app/charts/Tracker.js +0 -65
  229. package/dist/src/app/charts/Tracker.js.map +0 -1
  230. package/dist/src/app/charts/axes.d.ts +0 -132
  231. package/dist/src/app/charts/axes.js +0 -265
  232. package/dist/src/app/charts/axes.js.map +0 -1
  233. package/dist/src/app/charts/chartData.js +0 -32
  234. package/dist/src/app/charts/chartData.js.map +0 -1
  235. package/dist/src/app/charts/continuousAxisRangeFor.d.ts +0 -20
  236. package/dist/src/app/charts/continuousAxisRangeFor.js +0 -68
  237. package/dist/src/app/charts/continuousAxisRangeFor.js.map +0 -1
  238. package/dist/src/app/charts/datumSeries.d.ts +0 -59
  239. package/dist/src/app/charts/datumSeries.js +0 -55
  240. package/dist/src/app/charts/datumSeries.js.map +0 -1
  241. package/dist/src/app/charts/datumSeries.test.js +0 -50
  242. package/dist/src/app/charts/datumSeries.test.js.map +0 -1
  243. package/dist/src/app/charts/hooks/AxesState.d.ts +0 -37
  244. package/dist/src/app/charts/hooks/AxesState.js +0 -44
  245. package/dist/src/app/charts/hooks/AxesState.js.map +0 -1
  246. package/dist/src/app/charts/margins.js.map +0 -1
  247. package/dist/src/app/charts/margins.test.js +0 -14
  248. package/dist/src/app/charts/margins.test.js.map +0 -1
  249. package/dist/src/app/charts/plot.js.map +0 -1
  250. package/dist/src/app/charts/regexFilter.d.ts +0 -9
  251. package/dist/src/app/charts/regexFilter.js +0 -17
  252. package/dist/src/app/charts/regexFilter.js.map +0 -1
  253. package/dist/src/app/charts/regexFilter.test.js +0 -11
  254. package/dist/src/app/charts/regexFilter.test.js.map +0 -1
  255. package/dist/src/app/charts/subscriptions.d.ts +0 -43
  256. package/dist/src/app/charts/subscriptions.js +0 -213
  257. package/dist/src/app/charts/subscriptions.js.map +0 -1
  258. package/dist/src/app/charts/svgStyle.d.ts +0 -27
  259. package/dist/src/app/charts/svgStyle.js +0 -32
  260. package/dist/src/app/charts/svgStyle.js.map +0 -1
  261. package/dist/src/app/charts/timeRange.test.js +0 -54
  262. package/dist/src/app/charts/timeRange.test.js.map +0 -1
  263. package/dist/src/app/charts/tooltipUtils.js.map +0 -1
  264. package/dist/src/app/charts/trackerUtils.js +0 -130
  265. package/dist/src/app/charts/trackerUtils.js.map +0 -1
  266. /package/dist/src/app/charts/{datumSeries.test.d.ts → axes/AxesState.test.d.ts} +0 -0
  267. /package/dist/src/app/charts/{margins.test.d.ts → axes/AxisInterval.test.d.ts} +0 -0
  268. /package/dist/src/app/charts/{regexFilter.test.d.ts → axes/ContinuousAxisRange.test.d.ts} +0 -0
  269. /package/dist/src/app/charts/{timeRange.test.d.ts → axes/OrdinalAxisRange.test.d.ts} +0 -0
  270. /package/dist/src/app/charts/{RasterPlotTooltipContent.d.ts → tooltips/RasterPlotTooltipContent.d.ts} +0 -0
  271. /package/dist/src/app/charts/{ScatterPlotTooltipContent.d.ts → tooltips/ScatterPlotTooltipContent.d.ts} +0 -0
@@ -0,0 +1,832 @@
1
+ import React, {useCallback, useEffect, useMemo, useRef} from 'react'
2
+ import {NoTooltipMetadata, useChart} from "../hooks/useChart";
3
+ import * as d3 from "d3";
4
+ import {CurveFactory, ZoomTransform} from "d3";
5
+ import {setClipPathG} from "./plot";
6
+ import {Datum} from "../series/timeSeries";
7
+ import {
8
+ axesZoomHandler,
9
+ BaseAxis,
10
+ ContinuousNumericAxis,
11
+ defaultLineStyle,
12
+ panHandler2D,
13
+ SeriesLineStyle
14
+ } 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";
23
+ import {usePlotDimensions} from "../hooks/usePlotDimensions";
24
+ import {useInitialData} from "../hooks/useInitialData";
25
+ import {TooltipData, useTooltip} from "../hooks/useTooltip";
26
+ import {TimeSeriesChartData} from "../series/timeSeriesChartData";
27
+ import {ContinuousAxisRange} from "../axes/ContinuousAxisRange";
28
+
29
+ type IteratePoint = { n: number, n_1: number, time: number, index: number }
30
+ type IteratePointSeries = Array<IteratePoint>
31
+
32
+ function generateAxisRangeMap(axes: Map<string, BaseAxis>): Map<string, ContinuousAxisRange> {
33
+ return new Map(
34
+ Array.from(axes.entries()).map(([id, axis]) => {
35
+ const [start, end] = (axis as ContinuousNumericAxis).scale.domain()
36
+ return [id, ContinuousAxisRange.from(start, end)]
37
+ })
38
+ )
39
+ }
40
+
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;
44
+
45
+ interface Props {
46
+ /**
47
+ * The line interpolation curve factory. See the d3 documentation for curves at
48
+ * {@link https://github.com/d3/d3-shape#curves} for information on available interpolations
49
+ */
50
+ interpolation?: d3.CurveFactory
51
+ /**
52
+ * When set to `true` plots the data points as well as the line.
53
+ */
54
+ showPoints?: boolean
55
+ /**
56
+ * The number of milliseconds of data to hold in memory before dropping it. Defaults to
57
+ * infinity (i.e. no data is dropped)
58
+ */
59
+ dropDataAfter?: number
60
+ /**
61
+ * Enables panning (default is false)
62
+ */
63
+ panEnabled?: boolean
64
+ /**
65
+ * Enables zooming (default is false)
66
+ */
67
+ zoomEnabled?: boolean
68
+ /**
69
+ * When true, requires that the shift or control key be pressed while scrolling
70
+ * in order to activate the zoom
71
+ */
72
+ zoomKeyModifiersRequired?: boolean
73
+ /**
74
+ * The smallest scale factor allowed for zooming (in). For example, a setting of 0.5 means
75
+ * that the largest zoom amount is 2 times the current size, or put another way, an interval
76
+ * of length 1 unit covers twice as may pixels after the zoom. Effectively, the smaller this
77
+ * factor, the more the user can "zoom in". Default value is 0.0.
78
+ */
79
+ zoomMinScaleFactor?: number
80
+ /**
81
+ * The largest scale factor allowed for zooming (out). For example, a setting of 2.0 means
82
+ * that at this value, the length of 1 unit covers 1/2 the number of pixels. Effectively,
83
+ * the larger this factor, the more the user can "zoom out". Default value is 1.0.
84
+ */
85
+ zoomMaxScaleFactor?: number
86
+ /**
87
+ * When set, uses a cadence with the specified refresh period (in milliseconds). For plots
88
+ * where the updates are slow (> 100 ms) using a cadence of 10 to 25 ms smooths out the
89
+ * updates and makes the plot updates look cleaner. When updates are around 25 ms or less,
90
+ * then setting the cadence period too small will result in poor update performance. Generally
91
+ * at high update speeds, the cadence is unnecessary. Finally, using cadence, sets the max time
92
+ * to the current time.
93
+ */
94
+ withCadenceOf?: number
95
+ }
96
+
97
+ /**
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}
100
+ * hook, and therefore must be a child of the {@link Chart} in order to be plugged in to the
101
+ * chart ecosystem (axes, tracker, tooltip).
102
+ *
103
+ * @param props The properties associated with the scatter plot
104
+ * @constructor
105
+ * @example
106
+ * ```typescript
107
+ * <ScatterPlot
108
+ * interpolation={interpolation}
109
+ * axisAssignments={new Map([
110
+ * ['test2', assignAxes("x-axis-2", "y-axis-2")],
111
+ * ])}
112
+ * dropDataAfter={10000}
113
+ * panEnabled={true}
114
+ * zoomEnabled={true}
115
+ * zoomKeyModifiersRequired={true}
116
+ * />
117
+ * ```
118
+ */
119
+ export function PoincarePlot(props: Props): null {
120
+ const {
121
+ chartId,
122
+ container,
123
+ mainG,
124
+ axes,
125
+ backgroundColor,
126
+ seriesStyles,
127
+ seriesFilter,
128
+
129
+ mouse
130
+ } = useChart<IterateDatum, SeriesLineStyle, NoTooltipMetadata, ContinuousAxisRange, ContinuousNumericAxis>()
131
+
132
+ const {
133
+ xAxesState,
134
+ yAxesState,
135
+ setAxisIntervalFor,
136
+ updateAxisRanges = noop,
137
+ axisRangeFor,
138
+ addAxesRangesUpdateHandler,
139
+ removeAxesRangesUpdateHandler,
140
+ } = axes
141
+
142
+ const {
143
+ mouseOverHandlerFor,
144
+ mouseLeaveHandlerFor
145
+ } = mouse
146
+
147
+ const {
148
+ plotDimensions,
149
+ margin,
150
+ } = usePlotDimensions()
151
+
152
+ const {
153
+ seriesObservable,
154
+ windowingTime = 100,
155
+ shouldSubscribe,
156
+
157
+ onSubscribe = noop,
158
+ onUpdateData,
159
+ onUpdateChartTime = noop,
160
+ } = useDataObservable<IterateChartData, IterateDatum>()
161
+
162
+ const {initialData} = useInitialData<TimeSeriesChartData, IterateDatum>()
163
+
164
+ const {visibilityState: tooltipVisible} = useTooltip()
165
+
166
+ const {
167
+ interpolation,
168
+ showPoints = true,
169
+ dropDataAfter = 1000,
170
+ panEnabled = false,
171
+ zoomEnabled = false,
172
+ zoomKeyModifiersRequired = true,
173
+ zoomMinScaleFactor = 0,
174
+ zoomMaxScaleFactor = 1,
175
+ } = props
176
+
177
+ // why do "dataRef" and "seriesRef" both hold on to the same underlying data? for performance.
178
+ //
179
+ // the "dataRef" and "seriesRef" both point to the same underlying data, a collection
180
+ // of series. The series in "dataRef" are bound to the DOM elements through d3. The "seriesRef" series
181
+ // are the ones that are updated as new data is streamed in.
182
+ //
183
+ // the "dataRef" object holds on to a copy of the initial data (which is an array of
184
+ // time-series, e.i. an array of BaseSeries<OrdinalDatum> objects). The slice just creates a copy of
185
+ // the array, but the references to the BaseSeries objects are the same and still point to the same
186
+ // data as the "initialData" array.
187
+ //
188
+ // the "seriesRef" object is a reference to a map (series_name -> BaseSeries<OrdinalDatum>) which is
189
+ // used to update the data in the series. When new data enters, it is appended to one or more series.
190
+ //
191
+ // the series in the "dataRef" object are the ones bound to the DOM elements in d3, and so as these
192
+ // are updated, d3 will update the DOM elements (the elements in this plot).
193
+ const dataRef = useRef<Array<IterateSeries>>(initialData.slice() as Array<IterateSeries>)
194
+ const seriesRef = useRef<Map<string, IterateSeries>>(new Map(initialData.map(series => [series.name, series as IterateSeries])))
195
+ // map(axis_id -> current_time) -- maps the axis ID to the current time for that axis
196
+ const currentTimeRef = useRef<number>(0)
197
+ const xAxisRangesRef = useRef<Map<string, ContinuousAxisRange>>(new Map());
198
+ const yAxisRangesRef = useRef<Map<string, ContinuousAxisRange>>(new Map());
199
+
200
+ const subscriptionRef = useRef<Subscription>(undefined)
201
+ const isSubscriptionClosed = () => subscriptionRef.current === undefined || subscriptionRef.current.closed
202
+
203
+ const allowTooltip = useRef<boolean>(isSubscriptionClosed())
204
+
205
+ // so that we can reset the zoom when the axes-bounds change, we hold on to the zoom-behaviour
206
+ // and the zoom-selection so that we can reset the transform to the identity
207
+ const zoomRef = useRef<d3.ZoomBehavior<SVGSVGElement, Datum>>(undefined)
208
+ const zoomSelectionRef = useRef<d3.Selection<SVGSVGElement, any, null, undefined>>(undefined)
209
+
210
+ // calculates the distinct axis IDs that cover all the series in the plot
211
+ const xAxesForSeries = useMemo(
212
+ (): Array<string> => xAxesState.axisIds(),
213
+ [xAxesState]
214
+ )
215
+ const yAxesForSeries = useMemo(
216
+ (): Array<string> => yAxesState.axisIds(),
217
+ [yAxesState]
218
+ )
219
+
220
+ // when the axes are available, then set the reference, but only once
221
+ useEffect(() => {
222
+ if (xAxesState.axes.size > 0 && xAxisRangesRef.current.size === 0) {
223
+ xAxisRangesRef.current = generateAxisRangeMap(xAxesState.axes)
224
+ }
225
+ if (yAxesState.axes.size > 0 && yAxisRangesRef.current.size === 0) {
226
+ yAxisRangesRef.current = generateAxisRangeMap(yAxesState.axes)
227
+ }
228
+ }, [xAxesState, yAxesState]);
229
+
230
+ // update the plot with the new axes bounds
231
+ const updateRangesAndPlot = useCallback(
232
+ (): void => {
233
+ if (mainG !== null) {
234
+ updatePlotRef.current(mainG)
235
+ }
236
+ },
237
+ [mainG]
238
+ )
239
+
240
+ // todo find better way
241
+ // when the initial data changes, then reset the plot. note that the initial data doesn't change
242
+ // during the normal course of updates from the observable, only when the plot is restarted.
243
+ useEffect(
244
+ () => {
245
+ dataRef.current = initialData.slice()
246
+ seriesRef.current = new Map(initialData.map(series => [series.name, series]))
247
+ currentTimeRef.current = 0
248
+
249
+ updateRangesAndPlot()
250
+ },
251
+ [initialData, updateRangesAndPlot]
252
+ )
253
+
254
+ /**
255
+ * When the axes bounds have changed, we need to reset the range references so that
256
+ * the new axis ranges are used
257
+ * @param updates The updates to the axes
258
+ */
259
+ const updatedBoundsHandler = useCallback(
260
+ (updates: Map<string, ContinuousAxisRange>): void => {
261
+ updates.forEach((update, axisId) => {
262
+ if (xAxisRangesRef.current.has(axisId)) {
263
+ xAxisRangesRef.current.set(axisId, update)
264
+ }
265
+ if (yAxisRangesRef.current.has(axisId)) {
266
+ yAxisRangesRef.current.set(axisId, update)
267
+ }
268
+ })
269
+ if (zoomEnabled && zoomSelectionRef.current !== undefined && zoomRef.current !== undefined) {
270
+ zoomSelectionRef.current.call(zoomRef.current.transform, d3.zoomIdentity)
271
+ }
272
+ },
273
+ [zoomEnabled]
274
+ )
275
+
276
+ // strange construct so that we only add the update handler when the chart ID
277
+ // changes, and not when the addAxesBoundsUpdateHandler or removeAxesBoundsUpdateHandler
278
+ // which they do, and that breaks the updates...someone, please teach me react
279
+ //
280
+ // the update handler is needed so that when the axis bounds are changed (say to accommodate a
281
+ // different iterate function's domain/range), then the handler needs to update the x and y
282
+ // axes range refs
283
+ const addAxesBoundsUpdateHandlerRef = useRef(addAxesRangesUpdateHandler)
284
+ const removeAxesBoundsUpdateHandlerRef = useRef(removeAxesRangesUpdateHandler)
285
+ useEffect(
286
+ () => {
287
+ addAxesBoundsUpdateHandlerRef.current(`handler-${chartId}`, updatedBoundsHandler)
288
+ const removeHandler = removeAxesBoundsUpdateHandlerRef.current
289
+ return () => {
290
+ // closure on the function to remove the handler from this chart
291
+ removeHandler(`handler-${chartId}`)
292
+ }
293
+ },
294
+ [chartId, updatedBoundsHandler]
295
+ );
296
+
297
+ /**
298
+ * Adjusts the time-range and updates the plot when the plot is dragged to the left or right
299
+ * @param x The amount that the plot is dragged
300
+ * @param plotDimensions The dimensions of the plot
301
+ * @param series An array of series names
302
+ * @param ranges A map holding the axis ID and its associated time range
303
+ */
304
+ const onPan = useCallback(
305
+ (
306
+ x: number,
307
+ y: number,
308
+ plotDimensions: Dimensions,
309
+ series: Array<string>,
310
+ xRanges: Map<string, ContinuousAxisRange>,
311
+ yRanges: Map<string, ContinuousAxisRange>,
312
+ ) => panHandler2D(
313
+ xAxesForSeries, yAxesForSeries,
314
+ margin,
315
+ setAxisIntervalFor,
316
+ xAxesState, yAxesState
317
+ )(x, y, plotDimensions, series, xRanges, yRanges),
318
+ [xAxesForSeries, yAxesForSeries, margin, setAxisIntervalFor, xAxesState, yAxesState]
319
+ )
320
+
321
+ /**
322
+ * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out. Zooms in/out
323
+ * at the location of the mouse when the scroll wheel or gesture was applied. Unlike time-series
324
+ * plots, the iterates plot zooms the x- and y-axis at the same rate.
325
+ * @param transform The d3 zoom transformation information
326
+ * @param x The x-position of the mouse when the scroll wheel or gesture is used
327
+ * @param y The y-position of the mouse when the scroll wheel or gesture is used
328
+ * @param plotDimensions The dimensions of the plot
329
+ * @param series An array of series names
330
+ * @param ranges A map holding the axis ID and its associated time-range
331
+ */
332
+ const onZoom = useCallback(
333
+ (
334
+ transform: ZoomTransform,
335
+ x: number,
336
+ y: number,
337
+ plotDimensions: Dimensions,
338
+ xRanges: Map<string, ContinuousAxisRange>,
339
+ yRanges: Map<string, ContinuousAxisRange>
340
+ ) => axesZoomHandler(
341
+ xAxesForSeries, yAxesForSeries, margin, setAxisIntervalFor, xAxesState, yAxesState, [zoomMinScaleFactor, zoomMaxScaleFactor]
342
+ )(transform, [x, y], plotDimensions, xRanges, yRanges),
343
+ [xAxesForSeries, yAxesForSeries, margin, setAxisIntervalFor, xAxesState, yAxesState, zoomMinScaleFactor, zoomMaxScaleFactor]
344
+ )
345
+
346
+ const updatePlot = useCallback(
347
+ /**
348
+ * Updates the plot data for the specified time-range, which may have changed due to zoom or pan
349
+ * @param mainGElem The main <g> element selection for that holds the plot
350
+ */
351
+ (mainGElem: GSelection) => {
352
+ if (container) {
353
+ onUpdateChartTime(currentTimeRef.current)
354
+
355
+ // select the svg element bind the data to them
356
+ const svg = d3.select<SVGSVGElement, any>(container)
357
+
358
+ // create a map associating series-names to their time-series.
359
+ //
360
+ // performance-related confusion: wondering where the dataRef is updated? well it isn't
361
+ // directly. The dataRef holds on to an array of references to the Series. And so does the
362
+ // seriesRef, though it uses a map(series_name -> series). The seriesRef is used to append
363
+ // data to the underlying Series, and the dataRef is used so that we can just use
364
+ // dataRef.current and don't have to do Array.from(seriesRef.current.values()) which
365
+ // creates a temporary array
366
+ // const offset = 1
367
+ const boundedSeries = new Map<string, IteratePointSeries>(dataRef.current.map(series => {
368
+ return [
369
+ series.name,
370
+ series.data
371
+ .filter(datum => !isNaN(datum.iterateN))
372
+ .map((datum, index) => ({
373
+ n: datum.iterateN,
374
+ n_1: datum.iterateN_1,
375
+ time: datum.time,
376
+ index: index
377
+ })
378
+ ) as IteratePointSeries
379
+ ]
380
+ }))
381
+
382
+ // set up panning
383
+ if (panEnabled) {
384
+ const drag = d3.drag<SVGSVGElement, Datum>()
385
+ .on("start", () => {
386
+ d3.select(container).style("cursor", "move")
387
+ // during panning, we need to disable viewing the tooltip to prevent
388
+ // tooltips from rendering but not getting removed
389
+ allowTooltip.current = false;
390
+ })
391
+ .on("drag", event => {
392
+ onPan(
393
+ event.dx,
394
+ event.dy,
395
+ plotDimensions,
396
+ Array.from(boundedSeries.keys()),
397
+ xAxisRangesRef.current,
398
+ yAxisRangesRef.current
399
+ )
400
+ updatePlotRef.current(mainGElem)
401
+ })
402
+ .on("end", () => {
403
+ d3.select(container).style("cursor", "auto")
404
+ // during panning, we disabled viewing the tooltip to prevent
405
+ // tooltips from rendering but not getting removed, now that panning
406
+ // is over, allow tooltips to render again
407
+ allowTooltip.current = isSubscriptionClosed();
408
+ })
409
+
410
+ svg.call(drag)
411
+ }
412
+
413
+ // set up for zooming
414
+ if (zoomEnabled) {
415
+ zoomRef.current = d3.zoom<SVGSVGElement, Datum>()
416
+ .filter(event => !zoomKeyModifiersRequired || event.shiftKey || event.ctrlKey)
417
+ .scaleExtent([zoomMinScaleFactor, zoomMaxScaleFactor])
418
+ .translateExtent([[margin.left, margin.top], [plotDimensions.width, plotDimensions.height]])
419
+ .on("zoom", event => {
420
+ allowTooltip.current = false
421
+ if (event.sourceEvent !== null) {
422
+ onZoom(
423
+ event.transform,
424
+ event.sourceEvent.offsetX - margin.left,
425
+ event.sourceEvent.offsetY - margin.top,
426
+ plotDimensions,
427
+ xAxisRangesRef.current,
428
+ yAxisRangesRef.current
429
+ )
430
+ updatePlotRef.current(mainGElem)
431
+ }
432
+ allowTooltip.current = true
433
+ }
434
+ )
435
+
436
+ zoomSelectionRef.current = svg.call(zoomRef.current)
437
+ }
438
+
439
+ // define the clip-path so that the series lines don't go beyond the plot area
440
+ const clipPathId = setClipPathG(chartId, mainGElem, plotDimensions)
441
+
442
+ // ---
443
+ // todo only want to do this once, on the first plot, and then leave it,
444
+ // unless the axes are updated, also needs to be removed/added when the
445
+ // plot size changes
446
+ const xAxis = xAxesState.defaultAxis().getOrThrow(() => new Error('No default axis found')) as ContinuousNumericAxis
447
+ const yAxis = yAxesState.defaultAxis().getOrThrow(() => new Error('No default axis found')) as ContinuousNumericAxis
448
+
449
+ const lineGenerator = d3.line<[x: number, y: number]>()
450
+ .x(d => xAxis.scale(d[0] || 0))
451
+ .y(d => yAxis.scale(d[1] || 0))
452
+ // ---
453
+
454
+ const [xStart, xEnd] = xAxesState.axisDefaultId()
455
+ .map(id => xAxisRangesRef.current.get(id)?.original.asTuple() || [0, 0])
456
+ .getOrElse([0, 0])
457
+ const [yStart, yEnd] = yAxesState.axisDefaultId()
458
+ .map(id => yAxisRangesRef.current.get(id)?.original.asTuple() || [0, 0])
459
+ .getOrElse([0, 0])
460
+ // const [xStart, xEnd] = xAxisRangesRef.current.get(xAxesState.axisDefaultId())?.original.asTuple() || [0, 0]
461
+ // const [yStart, yEnd] = yAxisRangesRef.current.get(yAxesState.axisDefaultId())?.original.asTuple() || [0, 0]
462
+
463
+ mainGElem
464
+ .selectAll(`#fn-equals-fn1-${chartId}-poincare`)
465
+ .data([[[xStart, yStart], [xEnd, yEnd]] as Array<[x: number, y: number]>])
466
+ .join(enter => enter
467
+ .select("path")
468
+ .style("stroke", "grey")
469
+ .style("fill", "none")
470
+ .style("stroke-width", "1px")
471
+ .attr("class", `fn-equals-fn1-poincare`)
472
+ .attr("id", `#fn-equals-fn1-${chartId}-poincare`)
473
+ .attr('transform', `translate(${margin.left}, ${margin.top})`)
474
+ .attr("d", lineGenerator),
475
+ update => update.remove(),
476
+ exit => exit.remove()
477
+ )
478
+ // clear out mouse-enter callbacks for the diagonal line
479
+ .on("mouseenter", () => {
480
+ })
481
+ .on("mouseleave", () => {
482
+ })
483
+
484
+ boundedSeries.forEach((data, name) => {
485
+ // grab the x and y axes assigned to the series, and if either or both
486
+ // axes aren't found, then give up and return
487
+ const [xAxisLinear, yAxisLinear] = axesFor(
488
+ axisId => xAxesState.axisFor(axisId).getOrUndefined(),
489
+ axisId => yAxesState.axisFor(axisId).getOrUndefined(),
490
+ )
491
+ if (xAxisLinear === undefined || yAxisLinear === undefined) return
492
+
493
+ // grab the style for the series
494
+ const seriesLineStyle: SeriesLineStyle = seriesStyles.get(name) || {
495
+ ...defaultLineStyle(),
496
+ highlightColor: defaultLineStyle().color
497
+ }
498
+
499
+ // only show the data for which the filter matches
500
+ const plotData = (name.match(seriesFilter)) ? data : []
501
+
502
+ const pathGenerator = d3.line<IteratePoint>()
503
+ .x(d => xAxis.scale(d.n || 0))
504
+ .y(d => yAxis.scale(d.n_1 || 0))
505
+
506
+ if (interpolation === undefined) {
507
+ mainGElem
508
+ .selectAll(`#${name}-${chartId}-poincare`)
509
+ .remove()
510
+ } else {
511
+ // create the time-series paths
512
+ mainGElem
513
+ .selectAll(`#${name}-${chartId}-poincare`)
514
+ .data([[], plotData], () => `${name}`)
515
+ .join(
516
+ enter => enter
517
+ .append("path")
518
+ .attr("class", 'iterate-series-lines')
519
+ .attr("id", `${name}-${chartId}-poincare`)
520
+ .attr("d", pathGenerator.curve(interpolation))
521
+ .style("fill", "none")
522
+ .style("stroke", seriesLineStyle.color)
523
+ .style("stroke-width", seriesLineStyle.lineWidth)
524
+ .attr('transform', `translate(${margin.left}, ${margin.top})`)
525
+ .attr("clip-path", `url(#${clipPathId})`)
526
+ ,
527
+ update => update,
528
+ exit => exit.remove()
529
+ )
530
+ }
531
+
532
+ // when specified, show a circle for the actual data point
533
+ if (showPoints) {
534
+ mainGElem
535
+ .selectAll(`.${name}-${chartId}-poincare-points`)
536
+ .data(plotData, () => `${name}`)
537
+ .join(
538
+ enter => enter
539
+ .append("circle")
540
+ .attr("class", `${name}-${chartId}-poincare-points`)
541
+ .attr("id", (_, index) => `${name}-${chartId}-poincare-point-${index}`)
542
+ .attr("fill", seriesLineStyle.color)
543
+ .attr("stroke", "none")
544
+ .attr("cx", (d: IteratePoint) => xAxisLinear.scale(d.n) || 0)
545
+ .attr("cy", (d: IteratePoint) => yAxisLinear.scale(d.n_1) || 0)
546
+ .attr("r", 2)
547
+ .attr('transform', `translate(${margin.left}, ${margin.top})`)
548
+ .attr("clip-path", `url(#${clipPathId})`)
549
+ ,
550
+ update => update
551
+ .attr("cx", (d: IteratePoint) => xAxisLinear.scale(d.n) || 0)
552
+ .attr("cy", (d: IteratePoint) => yAxisLinear.scale(d.n_1) || 0)
553
+ ,
554
+ exit => exit.remove()
555
+ )
556
+ .on("mouseenter",
557
+ (event: React.MouseEvent<SVGCircleElement>, datum: IteratePoint) => {
558
+ if (allowTooltip.current && tooltipVisible) {
559
+ return handleMouseEnterPoint(
560
+ chartId,
561
+ name,
562
+ container,
563
+ event,
564
+ datum,
565
+ plotData,
566
+ xAxisLinear,
567
+ yAxisLinear,
568
+ margin,
569
+ seriesLineStyle,
570
+ backgroundColor,
571
+ allowTooltip.current,
572
+ mouseOverHandlerFor(`tooltip-${chartId}`)
573
+ )
574
+ }
575
+ return <></>
576
+ }
577
+ )
578
+ .on("mouseleave", () => {
579
+ handleMouseLeavePoint(
580
+ chartId,
581
+ name,
582
+ seriesLineStyle.color,
583
+ mouseLeaveHandlerFor(`tooltip-${chartId}`)
584
+ )
585
+ })
586
+ }
587
+ })
588
+ }
589
+ },
590
+ [
591
+ container, onUpdateChartTime, panEnabled, zoomEnabled, chartId, plotDimensions, margin,
592
+ xAxesState, yAxesState,
593
+ onPan,
594
+ zoomMinScaleFactor, zoomMaxScaleFactor, zoomKeyModifiersRequired, onZoom,
595
+ seriesStyles, seriesFilter, showPoints,
596
+ interpolation, backgroundColor,
597
+ mouseOverHandlerFor, mouseLeaveHandlerFor,
598
+ tooltipVisible,
599
+ ]
600
+ )
601
+
602
+ // need to keep the function references for use by the subscription, which forms a closure
603
+ // on them. without the references, the closures become stale, and resizing during streaming
604
+ // doesn't work properly
605
+ const updatePlotRef = useRef(updatePlot)
606
+ useEffect(
607
+ () => {
608
+ updatePlotRef.current = updatePlot
609
+ },
610
+ [updatePlot]
611
+ )
612
+ const onUpdateAxesBoundsRef = useRef(updateAxisRanges)
613
+ useEffect(
614
+ () => {
615
+ onUpdateAxesBoundsRef.current = updateAxisRanges
616
+ },
617
+ [updateAxisRanges]
618
+ )
619
+
620
+ // memoized function for subscribing to the chart-data observable
621
+ const subscribe = useCallback(
622
+ () => {
623
+ if (seriesObservable === undefined || mainG === null) return undefined
624
+ return subscriptionIteratesFor(
625
+ seriesObservable as Observable<IterateChartData>,
626
+ onSubscribe,
627
+ windowingTime,
628
+ xAxesState,
629
+ yAxesState,
630
+ onUpdateData,
631
+ dropDataAfter,
632
+ updateRangesAndPlot,
633
+ // as new data flows into the subscription, the subscription
634
+ // updates this map directly (for performance)
635
+ seriesRef.current,
636
+ end => currentTimeRef.current = end
637
+ )
638
+ },
639
+ [
640
+ dropDataAfter, mainG,
641
+ onSubscribe, onUpdateData,
642
+ seriesObservable, updateRangesAndPlot, windowingTime,
643
+ xAxesState, yAxesState,
644
+ ]
645
+ )
646
+
647
+ // updates the plot when the interpolation and filter change, because the updatePlot
648
+ // callback has changed.
649
+ useEffect(
650
+ () => {
651
+ if (container && mainG) {
652
+ updatePlot(mainG)
653
+ }
654
+ },
655
+ [axisRangeFor, container, mainG, updatePlot]
656
+ )
657
+
658
+ // subscribe/unsubscribe to the observable chart data. when the `shouldSubscribe`
659
+ // is changed to `true` and we haven't subscribed yet, then subscribe. when the
660
+ // `shouldSubscribe` is `false` and we had subscribed, then unsubscribe. otherwise,
661
+ // do nothing.
662
+ useEffect(
663
+ () => {
664
+ if (shouldSubscribe && subscriptionRef.current === undefined) {
665
+ subscriptionRef.current = subscribe()
666
+ allowTooltip.current = false
667
+ } else if (!shouldSubscribe && subscriptionRef.current !== undefined) {
668
+ subscriptionRef.current?.unsubscribe()
669
+ subscriptionRef.current = undefined
670
+ allowTooltip.current = true
671
+ }
672
+ },
673
+ [shouldSubscribe, subscribe]
674
+ )
675
+
676
+ return null
677
+ }
678
+
679
+ /**
680
+ * Attempts to locate the x- and y-axes for the specified series. If no axis is found for the
681
+ * series name, then uses the default returned by the useChart() hook
682
+ * @param xAxisFor The function that accepts an axis ID and returns the corresponding x-axis
683
+ * @param yAxisFor The function that accepts an axis ID and returns the corresponding y-axis
684
+ */
685
+ function axesFor(
686
+ xAxisFor: (id: string) => BaseAxis | undefined,
687
+ yAxisFor: (id: string) => BaseAxis | undefined,
688
+ ): [xAxis: ContinuousNumericAxis, yAxis: ContinuousNumericAxis] {
689
+ const xAxis = xAxisFor("")
690
+ const xAxisLinear = xAxis as ContinuousNumericAxis
691
+ const yAxis = yAxisFor("")
692
+ const yAxisLinear = yAxis as ContinuousNumericAxis
693
+ if (xAxis && !xAxisLinear) {
694
+ throw Error("Poincare plot requires that x-axis be of type LinearAxis")
695
+ }
696
+ if (yAxis && !yAxisLinear) {
697
+ throw Error("Poincare plot requires that y-axis be of type LinearAxis")
698
+ }
699
+ return [xAxisLinear, yAxisLinear]
700
+ }
701
+
702
+ /**
703
+ * @param chartId The ID of the chart
704
+ * @param seriesName The name of the series (i.e. the neuron ID)
705
+ * @param container The SVG container holding the plot
706
+ * @param event The mouse event that triggered this call
707
+ * @param datum The datum over which the mouse has entered
708
+ * @param plotData The iterates series
709
+ * @param xAxisLinear The x-axis (f[n](x))
710
+ * @param yAxisLinear The y-axis (f[n+1](x))
711
+ * @param margin The plot margin
712
+ * @param seriesStyle The series style information (needed for (un)highlighting)
713
+ * @param backgroundColor
714
+ * @param allowTooltip When set to `false` won't show tooltip, even if it is visible (used by pan)
715
+ * @param mouseOverHandlerFor The handler for the mouse-over (registered by the <Tooltip/>)
716
+ */
717
+ function handleMouseEnterPoint(
718
+ chartId: number,
719
+ seriesName: string,
720
+ container: SVGSVGElement,
721
+ event: React.MouseEvent<SVGPathElement>,
722
+ datum: IteratePoint,
723
+ plotData: IteratePointSeries,
724
+ xAxisLinear: ContinuousNumericAxis,
725
+ yAxisLinear: ContinuousNumericAxis,
726
+ margin: Margin,
727
+ seriesStyle: SeriesLineStyle,
728
+ backgroundColor: string,
729
+ allowTooltip: boolean,
730
+ mouseOverHandlerFor: ((seriesName: string, time: number, tooltipData: TooltipData<IterateDatum, NoTooltipMetadata>, mouseCoords: [x: number, y: number]) => void) | undefined,
731
+ ): void {
732
+ const {color, highlightColor, lineWidth} = seriesStyle
733
+
734
+ const padding = 4
735
+ const circleRadius = 5
736
+ const circleStroke = lineWidth
737
+
738
+ const circle = event.currentTarget as SVGCircleElement
739
+
740
+ d3.select<SVGPathElement, Datum>(circle)
741
+ .attr("r", circleRadius)
742
+ .style("fill", highlightColor)
743
+
744
+ const index = plotData
745
+ .findIndex(point => point.n === datum.n && point.n_1 === datum.n_1)
746
+
747
+ /**
748
+ * Displays basic information about an iterate, generally its neighbors
749
+ * @param index The index of the iterate
750
+ */
751
+ function showInfo(index: number): void {
752
+ d3.select(`#${seriesName}-${chartId}-poincare-point-${index}`)
753
+ .attr("r", circleRadius)
754
+ .style("fill", d3.rgb(highlightColor).brighter(0.7).toString())
755
+ .style("stroke-width", circleStroke)
756
+ .style("stroke", color)
757
+
758
+ // grab the (x, y)-coordinates for the plot
759
+ const iterateN = xAxisLinear.scale(plotData[index].n) + margin.left
760
+ const iterateN_1 = yAxisLinear.scale(plotData[index].n_1) + margin.top
761
+
762
+ const svg = d3.select<SVGSVGElement, any>(container)
763
+
764
+ // add a rectangle that serves as the background for the text (to make the
765
+ // text readable when the chart is busy)
766
+ const rect = svg
767
+ .append("rect")
768
+ .attr("class", `${seriesName}-${chartId}-poincare-point-text-background`)
769
+
770
+ // add the information about the iterate as a text element
771
+ const textElement = svg
772
+ .append("text")
773
+ .attr('class', `${seriesName}-${chartId}-poincare-point-text`)
774
+ .attr('fill', highlightColor)
775
+ .attr('font-family', 'sans-serif')
776
+ .attr('font-size', 11)
777
+ .attr('font-weight', 700)
778
+ .text(`n = ${index}; t = ${formatTime(plotData[index].time)} ms`)
779
+
780
+ // calculate the width and height of the text element
781
+ const {width, height} = textDimensions(textElement)
782
+
783
+ textElement
784
+ .attr("transform", `translate(${iterateN - 8}, ${iterateN_1 - circleRadius - circleStroke - padding})`)
785
+
786
+ rect
787
+ .attr("x", iterateN - padding / 2 - 8)
788
+ .attr("y", iterateN_1 - padding / 2 - circleRadius - circleStroke - height)
789
+ .attr("width", width + padding)
790
+ .attr("height", height + padding / 2)
791
+ .style("fill", backgroundColor)
792
+ }
793
+
794
+ if (index > 0) {
795
+ showInfo(index - 1)
796
+ }
797
+ if (index < plotData.length - 1) {
798
+ showInfo(index + 1)
799
+ }
800
+
801
+ const [x, y] = d3.pointer(event, container)
802
+ const currentDatum: IteratePoint = (index >= 0) ? plotData[index] : {time: 0, n: 0, n_1: 0, index}
803
+
804
+ if (mouseOverHandlerFor && allowTooltip) {
805
+ mouseOverHandlerFor(
806
+ seriesName,
807
+ currentDatum.time,
808
+ {series: plotData.map(ip => ({iterateN: ip.n, iterateN_1: ip.n_1, time: ip.time})), metadata: {}},
809
+ [x, y]
810
+ )
811
+ }
812
+ }
813
+
814
+ function handleMouseLeavePoint(
815
+ chartId: number,
816
+ seriesName: string,
817
+ color: string,
818
+ mouseLeaverHandlerFor: ((seriesName: string) => void) | undefined,
819
+ ): void {
820
+ d3.selectAll<SVGPathElement, Datum>(`.${seriesName}-${chartId}-poincare-points`)
821
+ .attr("r", 2)
822
+ .style("fill", color)
823
+ .style("stroke", "none")
824
+ d3.selectAll(`.${seriesName}-${chartId}-poincare-point-arrows`).remove()
825
+ d3.selectAll(`.${seriesName}-${chartId}-poincare-point-text`).remove()
826
+ d3.selectAll(`.${seriesName}-${chartId}-poincare-point-text-background`).remove()
827
+
828
+ if (mouseLeaverHandlerFor) {
829
+ mouseLeaverHandlerFor(seriesName)
830
+ }
831
+
832
+ }