stream-charts 1.0.0 → 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 +758 -345
  2. package/changes.md +10 -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} +17 -11
  146. package/dist/src/app/charts/{tooltipUtils.js → tooltips/tooltipUtils.js} +52 -25
  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,1088 @@
1
+ import {AxesAssignment, setClipPathG} from "./plot";
2
+ import * as d3 from "d3";
3
+ import {ZoomTransform} from "d3";
4
+ import {noop} from "../utils";
5
+ import {useChart} from "../hooks/useChart";
6
+ import React, {useCallback, useEffect, useMemo, useRef} from "react";
7
+ import {Datum, TimeSeries} from "../series/timeSeries";
8
+ import {GSelection, SvgSelection} from "../d3types";
9
+ import {
10
+ axesForSeriesGen,
11
+ BaseAxis,
12
+ ContinuousNumericAxis,
13
+ ordinalAxisIntervals,
14
+ ordinalAxisRanges,
15
+ ordinalAxisZoomHandler,
16
+ ordinalPanHandler,
17
+ OrdinalStringAxis
18
+ } from "../axes/axes";
19
+ import {Subscription} from "rxjs";
20
+ import {Dimensions, Margin} from "../styling/margins";
21
+ import {subscriptionOrdinalXFor, WindowedOrdinalStats} from "../subscriptions/subscriptions";
22
+ import {useDataObservable} from "../hooks/useDataObservable";
23
+ import {usePlotDimensions} from "../hooks/usePlotDimensions";
24
+ import {useInitialData} from "../hooks/useInitialData";
25
+ import {copyValueStatsForSeries, OrdinalChartData, OrdinalStats} from "../observables/ordinals";
26
+ import {BaseSeries} from "../series/baseSeries";
27
+ import {calculateOrdinalStats, OrdinalDatum, OrdinalSeries} from "../series/ordinalSeries";
28
+ import {
29
+ applyFillStylesTo,
30
+ applyStrokeStylesTo,
31
+ STROKE_COLOR,
32
+ STROKE_OPACITY,
33
+ STROKE_WIDTH,
34
+ SvgFillStyle,
35
+ SvgStrokeStyle
36
+ } from "../styling/svgStyle";
37
+ import {BarSeriesStyle, BarStyle, defaultBarSeriesStyle, LineStyle} from "../styling/barPlotStyle";
38
+ import {TooltipData} from "../hooks/useTooltip";
39
+ import {OrdinalAxisRange} from "../axes/OrdinalAxisRange";
40
+ import {AxisInterval} from "../axes/AxisInterval";
41
+ import {Optional} from "result-fn";
42
+
43
+ // typescript doesn't support enums with computed string values, even though they are all constants...
44
+ export type BarChartElementId = {
45
+ readonly currentValue: string
46
+ readonly meanValue: string
47
+ readonly minMax: string
48
+ readonly windowedMeanValue: string
49
+ readonly windowedMinMax: string
50
+ }
51
+
52
+ const STREAM_CHARTS_BAR_CHART_ID = 'stream-charts-bar-chart'
53
+
54
+ // elements of the bar-chart
55
+ const BAR_CHART_CLASS_IDS: BarChartElementId = {
56
+ currentValue: STREAM_CHARTS_BAR_CHART_ID + '-value-lines',
57
+ meanValue: STREAM_CHARTS_BAR_CHART_ID + '-mean-value-lines',
58
+ minMax: STREAM_CHARTS_BAR_CHART_ID + '-min-max-bars',
59
+ windowedMeanValue: STREAM_CHARTS_BAR_CHART_ID + '-windowed-mean-value-lines',
60
+ windowedMinMax: STREAM_CHARTS_BAR_CHART_ID + '-windowed-mean-bars'
61
+ }
62
+
63
+ const classIdFor = (id: string) => '.' + id
64
+
65
+ // constants identifying the bar-chart elements for which mouse-over/mouse-leave events are defined
66
+ const TOOLTIP_PROVIDER_ID = STREAM_CHARTS_BAR_CHART_ID + '-tooltip-provider'
67
+
68
+ export const BAR_CHART_TOOLTIP_PROVIDER_IDS: BarChartElementId = {
69
+ currentValue: TOOLTIP_PROVIDER_ID + '-current-value',
70
+ meanValue: TOOLTIP_PROVIDER_ID + '-mean-value',
71
+ minMax: TOOLTIP_PROVIDER_ID + '-min-max',
72
+ windowedMeanValue: TOOLTIP_PROVIDER_ID + '-windowed-min-max',
73
+ windowedMinMax: TOOLTIP_PROVIDER_ID + '-windowed-mean'
74
+ }
75
+
76
+ interface Props {
77
+ /**
78
+ * Holds the mapping between a series and the axis it uses (is assigned). The
79
+ * map's key holds the series name, and the value is an {@link AxesAssignment}
80
+ * object holding the ID of the assigned x-axis and y-axis.
81
+ */
82
+ axisAssignments?: Map<string, AxesAssignment>
83
+ showMinMaxBars?: boolean
84
+ showWindowedMinMaxBars?: boolean
85
+ showValueLines?: boolean
86
+ showMeanValueLines?: boolean
87
+ showWindowedMeanValueLines?: boolean
88
+ /**
89
+ * The number of milliseconds worth of data to hold in memory before dropping it. Defaults to
90
+ * infinity (i.e. no data is dropped)
91
+ */
92
+ dropDataAfter?: number
93
+ /**
94
+ * Enables panning (default is false)
95
+ */
96
+ panEnabled?: boolean
97
+ /**
98
+ * Enables zooming (default is false)
99
+ */
100
+ zoomEnabled?: boolean
101
+ /**
102
+ * When true, requires that the shift or control key be pressed while scrolling
103
+ * in order to activate the zoom
104
+ */
105
+ zoomKeyModifiersRequired?: boolean
106
+ /**
107
+ * The (optional, default = 2 pixels) top and bottom margin (in pixels) for the spike lines in the plot.
108
+ * Margins on individual series can also be set through the {@link Chart.seriesStyles} property.
109
+ */
110
+ barMargin?: number
111
+ /**
112
+ * The (optional) default style for the bar series that are used if no other styles are specified
113
+ */
114
+ barSeriesStyle?: BarSeriesStyle
115
+ }
116
+
117
+ /**
118
+ * Renders a streaming bar plot for the series in the initial data and those sourced by the
119
+ * observable specified as a property in the {@link Chart}. This component uses the {@link useChart}
120
+ * hook, and therefore must be a child of the {@link Chart} to be plugged in to the
121
+ * chart ecosystem (axes, tracker, tooltip).
122
+ *
123
+ * For a relatively complete example of how to use this plot component, see the
124
+ * <a href="https://github.com/robphilipp/stream-charts-examples">`StreamingBarChart` example</a>
125
+ *
126
+ * @param props The properties associated with the bar plot
127
+ * @constructor
128
+ * @example
129
+ * ```typescript
130
+ * <BarPlot
131
+ * barMargin={1}
132
+ * dropDataAfter={5000}
133
+ * // panEnabled={true}
134
+ * // zoomEnabled={true}
135
+ * // zoomKeyModifiersRequired={true}
136
+ * // withCadenceOf={50}
137
+ *
138
+ * showMinMaxBars={showMinMax}
139
+ * showValueLines={showValue}
140
+ * showMeanValueLines={showMean}
141
+ * showWindowedMinMaxBars={showWinMinMax}
142
+ * showWindowedMeanValueLines={showWinMean}
143
+ * />
144
+ * ```
145
+ */
146
+ export function BarPlot(props: Props): null {
147
+ const {
148
+ chartId,
149
+ container,
150
+ mainG,
151
+ axes,
152
+ seriesStyles,
153
+ seriesFilter,
154
+ mouse
155
+ } = useChart<OrdinalDatum, BarSeriesStyle, WindowedOrdinalStats, OrdinalAxisRange, OrdinalStringAxis>()
156
+
157
+ const {
158
+ xAxesState,
159
+ yAxesState,
160
+ setAxisAssignments,
161
+ setAxisIntervalFor,
162
+ setOriginalAxisIntervalFor,
163
+ axesRanges
164
+ } = axes
165
+
166
+ const {mouseOverHandlerFor, mouseLeaveHandlerFor} = mouse
167
+
168
+ const {plotDimensions, margin} = usePlotDimensions()
169
+
170
+ const {
171
+ seriesObservable,
172
+ windowingTime = 100,
173
+ shouldSubscribe,
174
+
175
+ onSubscribe = noop,
176
+ onUpdateData,
177
+ onUpdateChartTime = noop
178
+ } = useDataObservable<OrdinalChartData, OrdinalDatum>()
179
+
180
+ const {initialData} = useInitialData<OrdinalChartData, OrdinalDatum>()
181
+
182
+ const {
183
+ axisAssignments = new Map<string, AxesAssignment>(),
184
+ dropDataAfter = Infinity,
185
+ panEnabled = false,
186
+ zoomEnabled = false,
187
+ zoomKeyModifiersRequired = true,
188
+ showMinMaxBars = true,
189
+ showWindowedMinMaxBars = true,
190
+ showValueLines = true,
191
+ showMeanValueLines = true,
192
+ showWindowedMeanValueLines = true,
193
+ barMargin = 2,
194
+ barSeriesStyle = defaultBarSeriesStyle()
195
+ } = props
196
+
197
+ // why do "dataRef" and "seriesRef" both hold on to the same underlying data? for performance.
198
+ //
199
+ // the "dataRef" and "seriesRef" both point to the same underlying data, a collection
200
+ // of series. The series in "dataRef" are bound to the DOM elements through d3. The "seriesRef" series
201
+ // are the ones that are updated as new data is streamed in.
202
+ //
203
+ // the "dataRef" object holds on to a copy of the initial data (which is an array of
204
+ // time-series, e.i. an array of BaseSeries<OrdinalDatum> objects). The slice just creates a copy of
205
+ // the array, but the references to the BaseSeries objects are the same and still point to the same
206
+ // data as the "initialData" array.
207
+ //
208
+ // the "seriesRef" object is a reference to a map (series_name -> BaseSeries<OrdinalDatum>) which is
209
+ // used to update the data in the series. When new data enters, it is appended to one or more series.
210
+ //
211
+ // the series in the "dataRef" object are the ones bound to the DOM elements in d3, and so as these
212
+ // are updated, d3 will update the DOM elements (the elements in this plot).
213
+ const dataRef = useRef<Array<BaseSeries<OrdinalDatum>>>(initialData.slice())
214
+ const seriesRef = useRef<Map<string, BaseSeries<OrdinalDatum>>>(
215
+ new Map(initialData.map(series => [series.name, series]))
216
+ )
217
+ const statsRef = useRef<WindowedOrdinalStats>(initialOrdinalStats(dataRef.current))
218
+
219
+ // map(axis_id -> current_time) -- maps the axis ID to the current time for that axis
220
+ const currentTimeRef = useRef<number>(0)
221
+ const subscriptionRef = useRef<Subscription>(undefined)
222
+
223
+ const isSubscriptionClosed = () => subscriptionRef.current === undefined || subscriptionRef.current.closed
224
+
225
+ const allowTooltipRef = useRef<boolean>(isSubscriptionClosed())
226
+
227
+ useEffect(
228
+ () => {
229
+ currentTimeRef.current = 0
230
+ },
231
+ [xAxesState]
232
+ )
233
+
234
+ // set the axis assignments needed if a tooltip is being used
235
+ useEffect(
236
+ () => {
237
+ setAxisAssignments(axisAssignments)
238
+ },
239
+ [axisAssignments, setAxisAssignments]
240
+ )
241
+
242
+ // calculates the distinct series IDs that cover all the series in the plot
243
+ const axesForSeries = useMemo(
244
+ () => axesForSeriesGen<OrdinalDatum, OrdinalStringAxis>(initialData, axisAssignments, xAxesState),
245
+ [initialData, axisAssignments, xAxesState]
246
+ )
247
+
248
+ // updates the timing using the onUpdateTime and updatePlot references. This and the references
249
+ // defined above allow the axes' times to be updated properly by avoid stale reference to these
250
+ // functions.
251
+ const updateTimingAndPlot = useCallback(
252
+ (ranges: Map<string, OrdinalAxisRange>): void => {
253
+ if (mainG !== null) {
254
+ updatePlotRef.current(ranges, mainG)
255
+ onUpdateChartTime(currentTimeRef.current)
256
+ updatePlotRef.current(ranges, mainG)
257
+ }
258
+ },
259
+ [mainG, onUpdateChartTime]
260
+ )
261
+
262
+ // todo find better way
263
+ // when the initial data changes, then reset the plot. note that the initial data doesn't change
264
+ // during the normal course of updates from the observable, only when the plot is restarted.
265
+ useEffect(
266
+ () => {
267
+ dataRef.current = initialData.slice()
268
+ seriesRef.current = new Map(initialData.map(series => [series.name, series]))
269
+ currentTimeRef.current = 0
270
+ statsRef.current = initialOrdinalStats(dataRef.current)
271
+ },
272
+ // ** not happy about this **
273
+ // only want this effect to run when the initial data is changed, which mean all the
274
+ // other dependencies are recalculated anyway.
275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
276
+ [initialData]
277
+ )
278
+
279
+ /**
280
+ * Adjusts the time-range and updates the plot when the plot is dragged to the left or right
281
+ * @param deltaX The amount that the plot is dragged
282
+ * @param plotDimensions The dimensions of the plot
283
+ * @param series An array of series names
284
+ * @param ranges A map holding the axis ID and its associated time range
285
+ */
286
+ const onPan = useCallback(
287
+ (x: number,
288
+ plotDimensions: Dimensions,
289
+ series: Array<string>,
290
+ ranges: Map<string, OrdinalAxisRange>
291
+ ) => ordinalPanHandler(axesForSeries, margin, setAxisIntervalFor, xAxesState)(x, plotDimensions, series, ranges),
292
+ [axesForSeries, margin, setAxisIntervalFor, xAxesState]
293
+ )
294
+
295
+ /**
296
+ * Called when the user uses the scroll wheel (or scroll gesture) to zoom in or out. Zooms in/out
297
+ * at the location of the mouse when the scroll wheel or gesture was applied.
298
+ * @param transform The d3 zoom transformation information
299
+ * @param x The x-position of the mouse when the scroll wheel or gesture is used
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 onZoom = useCallback(
305
+ (
306
+ transform: ZoomTransform,
307
+ x: number,
308
+ plotDimensions: Dimensions,
309
+ ranges: Map<string, OrdinalAxisRange>,
310
+ ) => ordinalAxisZoomHandler(axesForSeries, margin, setAxisIntervalFor, setOriginalAxisIntervalFor, xAxesState)(transform, x, plotDimensions, ranges),
311
+ [axesForSeries, margin, setAxisIntervalFor, setOriginalAxisIntervalFor, xAxesState]
312
+ )
313
+
314
+ /**
315
+ * @param ordinalRanges
316
+ * @param mainGElem
317
+ */
318
+ const updatePlot = useCallback(
319
+ (ordinalRanges: Map<string, OrdinalAxisRange>, mainGElem: GSelection) => {
320
+ if (container) {
321
+ // select the svg element bind the data to them
322
+ const svg: SvgSelection = d3.select<SVGSVGElement, any>(container)
323
+
324
+ // set up panning
325
+ if (panEnabled) {
326
+ const drag = d3.drag<SVGSVGElement, Datum>()
327
+ .on("start", () => {
328
+ d3.select(container).style("cursor", "move")
329
+ allowTooltipRef.current = false
330
+ })
331
+ .on("drag", (event: any) => {
332
+ const names = dataRef.current.map(series => series.name)
333
+ onPan(event.dx, plotDimensions, names, ordinalRanges)
334
+ // need to update the plot with the new time-ranges
335
+ updatePlotRef.current(ordinalRanges, mainGElem)
336
+ })
337
+ .on("end", () => {
338
+ d3.select(container).style("cursor", "auto")
339
+ allowTooltipRef.current = isSubscriptionClosed()
340
+ })
341
+
342
+ svg.call(drag)
343
+ }
344
+
345
+ // set up for zooming
346
+ if (zoomEnabled) {
347
+ const zoom = d3.zoom<SVGSVGElement, Datum>()
348
+ .filter((event: any) => !zoomKeyModifiersRequired || event.shiftKey || event.ctrlKey)
349
+ .scaleExtent([1, 10])
350
+ .translateExtent([[margin.left, margin.top], [plotDimensions.width, plotDimensions.height]])
351
+ .on("zoom", (event: any) => {
352
+ onZoom(
353
+ event.transform,
354
+ event.sourceEvent.offsetX - margin.left,
355
+ plotDimensions,
356
+ ordinalRanges,
357
+ )
358
+ updatePlotRef.current(ordinalRanges, mainGElem)
359
+ }
360
+ )
361
+ svg.call(zoom)
362
+ }
363
+
364
+ // enter, update, delete the bar data
365
+ dataRef.current.forEach(series => {
366
+ const [xAxis, yAxis] = axesFor(
367
+ series.name,
368
+ axisAssignments,
369
+ axisId => xAxesState.axisFor(axisId).getOrUndefined(),
370
+ axisId => yAxesState.axisFor(axisId).getOrUndefined()
371
+ )
372
+
373
+ // grab the series styles, or the defaults if none exist
374
+ const {
375
+ margin: categoryMargin = barMargin,
376
+ valueLine: valueLineStyle,
377
+ meanValueLine: meanValueLineStyle,
378
+ windowedMeanValueLine: windowedMeanLineStyle,
379
+ minMaxBar: minMaxBarStyle,
380
+ windowedMinMaxBar: windowedBarStyle
381
+ } = seriesStyles.get(series.name) || {
382
+ ...barSeriesStyle,
383
+ highlightColor: barSeriesStyle.color
384
+ }
385
+
386
+ // only show the data for which the regex filter matches
387
+ const plotData = series.name.match(seriesFilter) ? [series.data[series.data.length - 1]] : []
388
+
389
+ // grab the functions for determining the lower and upper bounds of the category
390
+ const {
391
+ lower,
392
+ upper
393
+ } = xAxisCategoryBoundsFn(xAxis.scale.bandwidth(), valueLineStyle.regular.width, categoryMargin)
394
+
395
+ // grab the value (index) associated with the series name (this is a category axis)
396
+ const x = xAxis.scale(series.name) || 0
397
+
398
+ //
399
+ // min/max bar rectangle
400
+ const totalBar = barDimensions(
401
+ minMaxBarStyle.widthFraction,
402
+ lower(x), upper(x),
403
+ statsRef.current.valueStatsForSeries.get(series.name)?.min.value || 0,
404
+ statsRef.current.valueStatsForSeries.get(series.name)?.max.value || 0,
405
+ yAxis
406
+ )
407
+
408
+ svg
409
+ .select<SVGGElement>(`#${series.name}-${chartId}-bar`)
410
+ .selectAll<SVGRectElement, PlotData>(classIdFor(BAR_CHART_CLASS_IDS.minMax))
411
+ .data(plotData)
412
+ .join(
413
+ enter => barFor(
414
+ enter.append<SVGRectElement>('rect').attr('class', BAR_CHART_CLASS_IDS.minMax),
415
+ totalBar,
416
+ barStyleFor(showMinMaxBars, minMaxBarStyle)
417
+ ),
418
+ update => barFor(
419
+ update,
420
+ totalBar,
421
+ barStyleFor(showMinMaxBars, minMaxBarStyle)
422
+ ),
423
+ exit => exit.remove()
424
+ )
425
+ .on(
426
+ "mouseover",
427
+ (event,) =>
428
+ handleMouseOverBar(
429
+ container,
430
+ yAxis,
431
+ series,
432
+ statsRef.current,
433
+ event,
434
+ margin,
435
+ seriesStyles,
436
+ barSeriesStyle,
437
+ allowTooltipRef.current,
438
+ mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.minMax),
439
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.minMax
440
+ )
441
+ )
442
+ .on(
443
+ "mouseleave",
444
+ event => handleMouseLeaveSeries(
445
+ series.name,
446
+ event.currentTarget,
447
+ seriesStyles,
448
+ barSeriesStyle,
449
+ mouseLeaveHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.minMax),
450
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.minMax
451
+ )
452
+ )
453
+
454
+ //
455
+ // windowed-mean line when the windowed stats are defined for the series
456
+ const seriesWindowedStats = statsRef.current.windowedValueStatsForSeries.get(series.name)
457
+ if (seriesWindowedStats !== undefined) {
458
+ const windowedBar = barDimensions(
459
+ windowedBarStyle.widthFraction,
460
+ lower(x), upper(x),
461
+ seriesWindowedStats.min.value, seriesWindowedStats.max.value,
462
+ yAxis
463
+ )
464
+
465
+ svg
466
+ .select<SVGGElement>(`#${series.name}-${chartId}-bar`)
467
+ .selectAll<SVGRectElement, PlotData>(classIdFor(BAR_CHART_CLASS_IDS.windowedMinMax))
468
+ .data(plotData)
469
+ .join(
470
+ enter => barFor(
471
+ enter.append<SVGRectElement>('rect').attr('class', BAR_CHART_CLASS_IDS.windowedMinMax),
472
+ windowedBar,
473
+ barStyleFor(showWindowedMinMaxBars, windowedBarStyle)
474
+ ),
475
+ update => barFor(
476
+ update,
477
+ windowedBar,
478
+ barStyleFor(showWindowedMinMaxBars, windowedBarStyle)),
479
+ exit => exit.remove()
480
+ )
481
+ .on(
482
+ "mouseover",
483
+ (event,) =>
484
+ handleMouseOverBar(
485
+ container,
486
+ yAxis,
487
+ series,
488
+ statsRef.current,
489
+ event,
490
+ margin,
491
+ seriesStyles,
492
+ barSeriesStyle,
493
+ allowTooltipRef.current,
494
+ mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMinMax),
495
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMinMax
496
+ )
497
+ )
498
+ .on(
499
+ "mouseleave",
500
+ event => handleMouseLeaveSeries(
501
+ series.name,
502
+ event.currentTarget,
503
+ seriesStyles,
504
+ barSeriesStyle,
505
+ mouseLeaveHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMinMax),
506
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMinMax
507
+ )
508
+ )
509
+
510
+ //
511
+ // mean line
512
+ const meanLineY = yAxis.scale(statsRef.current.valueStatsForSeries.get(series.name)?.mean || 0)
513
+ svg
514
+ .select<SVGGElement>(`#${series.name}-${chartId}-bar`)
515
+ .selectAll<SVGLineElement, PlotData>(classIdFor(BAR_CHART_CLASS_IDS.meanValue))
516
+ .data(showMeanValueLines ? plotData : [])
517
+ .join(
518
+ enter => lineFor(
519
+ enter.append<SVGLineElement>('line').attr('class', BAR_CHART_CLASS_IDS.meanValue),
520
+ {
521
+ x1: () => lower(x), y1: () => meanLineY,
522
+ x2: () => upper(x), y2: () => meanLineY
523
+ },
524
+ meanValueLineStyle.regular
525
+ ),
526
+ update => lineFor(
527
+ update,
528
+ {
529
+ x1: () => lower(x), y1: () => meanLineY,
530
+ x2: () => upper(x), y2: () => meanLineY
531
+ },
532
+ meanValueLineStyle.regular
533
+ ),
534
+ exit => exit.remove()
535
+ )
536
+ .on(
537
+ "mouseover",
538
+ (event,) =>
539
+ handleMouseOverBar(
540
+ container,
541
+ yAxis,
542
+ series,
543
+ statsRef.current,
544
+ event,
545
+ margin,
546
+ seriesStyles,
547
+ barSeriesStyle,
548
+ allowTooltipRef.current,
549
+ mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.meanValue),
550
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.meanValue
551
+ )
552
+ )
553
+ .on(
554
+ "mouseleave",
555
+ event => handleMouseLeaveSeries(
556
+ series.name,
557
+ event.currentTarget,
558
+ seriesStyles,
559
+ barSeriesStyle,
560
+ mouseLeaveHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.meanValue),
561
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.meanValue
562
+ )
563
+ )
564
+
565
+ const windowedMeanLineY = yAxis.scale(isNaN(seriesWindowedStats.mean) ? 0 : seriesWindowedStats.mean)
566
+ svg
567
+ .select<SVGGElement>(`#${series.name}-${chartId}-bar`)
568
+ .selectAll<SVGLineElement, PlotData>(classIdFor(BAR_CHART_CLASS_IDS.windowedMeanValue))
569
+ .data(showWindowedMeanValueLines ? plotData : [])
570
+ .join(
571
+ enter => lineFor(
572
+ enter.append<SVGLineElement>('line').attr('class', BAR_CHART_CLASS_IDS.windowedMeanValue),
573
+ {
574
+ x1: () => lower(x), y1: () => windowedMeanLineY,
575
+ x2: () => upper(x), y2: () => windowedMeanLineY
576
+ },
577
+ windowedMeanLineStyle.regular
578
+ ),
579
+ update => lineFor(
580
+ update,
581
+ {
582
+ x1: () => lower(x), y1: () => windowedMeanLineY,
583
+ x2: () => upper(x), y2: () => windowedMeanLineY
584
+ },
585
+ windowedMeanLineStyle.regular
586
+ ),
587
+ exit => exit.remove()
588
+ )
589
+ .on(
590
+ "mouseover",
591
+ (event,) =>
592
+ handleMouseOverBar(
593
+ container,
594
+ yAxis,
595
+ series,
596
+ statsRef.current,
597
+ event,
598
+ margin,
599
+ seriesStyles,
600
+ barSeriesStyle,
601
+ allowTooltipRef.current,
602
+ mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMeanValue),
603
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMeanValue
604
+ )
605
+ )
606
+ .on(
607
+ "mouseleave",
608
+ event => handleMouseLeaveSeries(
609
+ series.name,
610
+ event.currentTarget,
611
+ seriesStyles,
612
+ barSeriesStyle,
613
+ mouseLeaveHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMeanValue),
614
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMeanValue
615
+ )
616
+ )
617
+ }
618
+
619
+ //
620
+ // value lines
621
+ svg
622
+ .select<SVGGElement>(`#${series.name}-${chartId}-bar`)
623
+ .selectAll<SVGLineElement, PlotData>(classIdFor(BAR_CHART_CLASS_IDS.currentValue))
624
+ .data(showValueLines ? plotData : [])
625
+ .join(
626
+ enter => lineFor(
627
+ enter.append<SVGLineElement>('line').attr('class', BAR_CHART_CLASS_IDS.currentValue),
628
+ {
629
+ x1: () => lower(x), y1: datum => yAxis.scale(datum.value),
630
+ x2: () => upper(x), y2: datum => yAxis.scale(datum.value)
631
+ },
632
+ valueLineStyle.regular
633
+ ),
634
+ update => lineFor(
635
+ update,
636
+ {
637
+ x1: () => lower(x), y1: datum => yAxis.scale(datum.value),
638
+ x2: () => upper(x), y2: datum => yAxis.scale(datum.value)
639
+ },
640
+ valueLineStyle.regular
641
+ ),
642
+ exit => exit.remove()
643
+ )
644
+ .on(
645
+ "mouseover",
646
+ (event,) =>
647
+ handleMouseOverBar(
648
+ container,
649
+ yAxis,
650
+ series,
651
+ statsRef.current,
652
+ event,
653
+ margin,
654
+ seriesStyles,
655
+ barSeriesStyle,
656
+ allowTooltipRef.current,
657
+ mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.currentValue),
658
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.currentValue
659
+ )
660
+ )
661
+ .on(
662
+ "mouseleave",
663
+ event => handleMouseLeaveSeries(
664
+ series.name,
665
+ event.currentTarget,
666
+ seriesStyles,
667
+ barSeriesStyle,
668
+ mouseLeaveHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.currentValue),
669
+ BAR_CHART_TOOLTIP_PROVIDER_IDS.currentValue
670
+ )
671
+ )
672
+ })
673
+ }
674
+ },
675
+ [
676
+ container,
677
+ panEnabled, zoomEnabled,
678
+ onPan, onZoom,
679
+ plotDimensions, margin, zoomKeyModifiersRequired,
680
+ axisAssignments, xAxesState, yAxesState,
681
+ barMargin, seriesStyles, barSeriesStyle,
682
+ seriesFilter,
683
+ chartId,
684
+ showValueLines,
685
+ showMinMaxBars,
686
+ mouseOverHandlerFor, mouseLeaveHandlerFor,
687
+ showMeanValueLines,
688
+ showWindowedMeanValueLines,
689
+ showWindowedMinMaxBars
690
+ ]
691
+ )
692
+
693
+ // need to keep the function references for use by the subscription, which forms a closure
694
+ // on them. without the references, the closures become stale, and resizing during streaming
695
+ // doesn't work properly
696
+ const updatePlotRef = useRef<(ordinalRange: Map<string, OrdinalAxisRange>, g: GSelection) => void>(noop)
697
+ useEffect(
698
+ () => {
699
+ if (mainG != null && container != null) {
700
+ // when the update plot function doesn't yet exist, then create the container holding the plot
701
+ const clipPathId = setClipPathG(chartId, mainG, plotDimensions)
702
+ if (updatePlotRef.current === noop) {
703
+ mainG
704
+ .selectAll<SVGGElement, BaseSeries<OrdinalDatum>>('g')
705
+ .attr("clip-path", `url(#${clipPathId})`)
706
+ .data<BaseSeries<OrdinalDatum>>(dataRef.current)
707
+ .enter()
708
+ .append('g')
709
+ .attr('class', 'spikes-series')
710
+ .attr('id', series => `${series.name}-${chartId}-bar`)
711
+ .attr('transform', `translate(${margin.left}, ${margin.top})`)
712
+
713
+ } else {
714
+ mainG
715
+ .selectAll<SVGGElement, TimeSeries>('g')
716
+ .attr("clip-path", `url(#${clipPathId})`)
717
+ }
718
+ updatePlotRef.current = updatePlot
719
+ }
720
+ },
721
+ [chartId, container, mainG, margin, plotDimensions, updatePlot]
722
+ )
723
+
724
+ // memoized function for subscribing to the chart-data observable
725
+ const subscribe = useCallback(
726
+ () => {
727
+ if (seriesObservable === undefined || mainG === null) return undefined
728
+ return subscriptionOrdinalXFor(
729
+ seriesObservable,
730
+ onSubscribe,
731
+ windowingTime,
732
+ axisAssignments,
733
+ yAxesState,
734
+ onUpdateData,
735
+ dropDataAfter,
736
+ updateTimingAndPlot,
737
+ // as new data flows into the subscription, the subscription
738
+ // updates this map directly (for performance)
739
+ seriesRef.current,
740
+ statsRef,
741
+ (currentTime: number) => currentTimeRef.current = currentTime,
742
+ AxisInterval.from(0, plotDimensions.width)
743
+ )
744
+ },
745
+ [
746
+ axisAssignments, dropDataAfter, mainG,
747
+ onSubscribe, onUpdateData,
748
+ seriesObservable, updateTimingAndPlot, windowingTime, yAxesState,
749
+ plotDimensions.width
750
+ ]
751
+ )
752
+
753
+ useEffect(
754
+ () => {
755
+ if (container && mainG) {
756
+ const ordinalAxesRanges = axesRanges()
757
+ // so this gets a bit complicated. the ordinal-ranges need to be updated whenever the ordinal-ranges
758
+ // change. for example, when the window is resized, and then we need to update the
759
+ // ordinal-range. however, we want to keep the ordinal-ranges to reflect their original scale so that
760
+ // we can zoom properly (so the updates can't fuck with the scale).
761
+ if (ordinalAxesRanges.size === 0) {
762
+ // when no time-ranges have yet been created, then create them and hold on to a mutable
763
+ // reference to them
764
+ updatePlot(ordinalAxisRanges(xAxesState.axes, AxisInterval.from(0, plotDimensions.width)), mainG)
765
+ } else {
766
+ // when the ordinal-ranges already exist, then we want to update the ordinal-ranges for each
767
+ // existing ordinal-range in a way that maintains the original scale.
768
+ const intervals = ordinalAxisIntervals(xAxesState.axes)
769
+ // todo instead of updating the underlying map, this should use setter methods to make the updates
770
+ ordinalAxesRanges
771
+ .forEach((range: OrdinalAxisRange, id: string, rangesMap: Map<string, OrdinalAxisRange>) => {
772
+ Optional
773
+ .ofNullable(intervals.get(id))
774
+ .map(intervalInfo => intervalInfo.interval.asTuple())
775
+ .ifPresent(([start, end]) => {
776
+ // update the reference map with the new (start, end) portion of the range,
777
+ // while keeping the original scale intact
778
+ rangesMap.set(id, range.update(start, end) as OrdinalAxisRange)
779
+ })
780
+ })
781
+ updatePlot(ordinalAxesRanges, mainG)
782
+ }
783
+ }
784
+ },
785
+ [axesRanges, container, mainG, plotDimensions.width, updatePlot, xAxesState.axes]
786
+ )
787
+
788
+ // subscribe/unsubscribe to the observable chart data. when the `shouldSubscribe`
789
+ // is changed to `true` and we haven't subscribed yet, then subscribe. when the
790
+ // `shouldSubscribe` is `false` and we had subscribed, then unsubscribe. otherwise,
791
+ // do nothing.
792
+ useEffect(
793
+ () => {
794
+ if (shouldSubscribe && subscriptionRef.current === undefined) {
795
+ subscriptionRef.current = subscribe()
796
+ allowTooltipRef.current = false
797
+ } else if (!shouldSubscribe && subscriptionRef.current !== undefined) {
798
+ subscriptionRef.current?.unsubscribe()
799
+ subscriptionRef.current = undefined
800
+ allowTooltipRef.current = true
801
+ }
802
+ },
803
+ [shouldSubscribe, subscribe]
804
+ )
805
+
806
+ return null
807
+ }
808
+
809
+ /*
810
+ Helper functions and types
811
+ */
812
+
813
+ type PlotData = {
814
+ data: OrdinalDatum,
815
+ stats: OrdinalStats,
816
+ }
817
+
818
+ type BarDimensions = {
819
+ upperX: number
820
+ upperY: number
821
+ width: number
822
+ height: number
823
+ }
824
+
825
+ function barStyleFor(isVisible: boolean, style: BarStyle): BarStyle {
826
+ return {
827
+ ...style,
828
+ fill: updateOpacityFor<SvgFillStyle>(isVisible, style.fill),
829
+ stroke: updateOpacityFor<SvgStrokeStyle>(isVisible, style.stroke)
830
+ }
831
+ }
832
+
833
+ function updateOpacityFor<S extends SvgFillStyle | SvgStrokeStyle>(isVisible: boolean, style: S): S {
834
+ return {
835
+ ...style,
836
+ opacity: isVisible ? style.opacity : 0
837
+ }
838
+ }
839
+
840
+ /**
841
+ * Sets the attributes for the bar based on the d3 selection
842
+ * @param selection The d3 selection (rect SVG element)
843
+ * @param dimensions The bar dimensions
844
+ * @param style The bar style holding the fill and stroke styles
845
+ * @return The updated SVG selection (rect SVG element)
846
+ */
847
+ function barFor(
848
+ selection: d3.Selection<SVGRectElement, OrdinalDatum, SVGGElement, any>,
849
+ dimensions: BarDimensions,
850
+ style: BarStyle
851
+ ): d3.Selection<SVGRectElement, OrdinalDatum, SVGGElement, any> {
852
+ selection
853
+ .attr('x', () => dimensions.upperX)
854
+ .attr('y', () => dimensions.upperY)
855
+ .attr('width', () => dimensions.width)
856
+ .attr('height', () => dimensions.height)
857
+
858
+ return applyStrokeStylesTo(applyFillStylesTo(selection, style.fill), style.stroke)
859
+ }
860
+
861
+ type PathSegment = {
862
+ x1: (datum: OrdinalDatum) => number,
863
+ y1: (datum: OrdinalDatum) => number,
864
+ x2: (datum: OrdinalDatum) => number,
865
+ y2: (datum: OrdinalDatum) => number,
866
+ }
867
+
868
+ /**
869
+ * Creates the line segment for values and means
870
+ * @param selection The d3 selection (rect SVG element)
871
+ * @param pathSegment The object containing functions for extracting the (x1, y1) and (x2, y2)
872
+ * points for the path segment
873
+ * @param strokeStyle The stroke style
874
+ * @return The updated SVG selection (rect SVG element)
875
+ */
876
+ function lineFor(
877
+ selection: d3.Selection<SVGLineElement, OrdinalDatum, SVGGElement, any>,
878
+ pathSegment: PathSegment,
879
+ strokeStyle: Partial<SvgStrokeStyle>
880
+ ) {
881
+ selection
882
+ .attr('x1', datum => pathSegment.x1(datum))
883
+ .attr('y1', datum => pathSegment.y1(datum))
884
+ .attr('x2', datum => pathSegment.x2(datum))
885
+ .attr('y2', datum => pathSegment.y2(datum))
886
+
887
+ return applyStrokeStylesTo(selection, strokeStyle)
888
+ }
889
+
890
+ /**
891
+ * Calculates the upper (x, y) coordinates of the bar, and the width and height of the bar
892
+ * @param widthFraction The fraction of the category width that the bar should take
893
+ * @param lowerX (Scaled to the axis) The lower bounds of the bar on the x-axis
894
+ * @param upperX (Scaled to the axis) The upper bounds of the bar on the x-axis
895
+ * @param min The minimum value for the category (NOT scaled to the axis)
896
+ * @param max The maximum value for the category (NOT scaled to the axis)
897
+ * @param axis The axis (needed for scaling)
898
+ * @return The bar's upper (x, y) coordinates, the width, and height
899
+ */
900
+ function barDimensions(widthFraction: number, lowerX: number, upperX: number, min: number, max: number, axis: ContinuousNumericAxis): BarDimensions {
901
+ const x = lowerX + Math.max(0, 0.5 - widthFraction / 2) * (upperX - lowerX)
902
+
903
+ const maxValue = (isNaN(max) || max === -Infinity) ? 0 : max
904
+ const y = axis.scale(maxValue)
905
+
906
+ const width = Math.max(0, widthFraction * (upperX - lowerX))
907
+
908
+ const minValue = (isNaN(min) || min === -Infinity) ? 0 : min
909
+ const height = Math.max(0, axis.scale(minValue) - axis.scale(maxValue))
910
+ return {
911
+ upperX: x,
912
+ upperY: y,
913
+ width,
914
+ height,
915
+ }
916
+ }
917
+
918
+ /**
919
+ * Calculates the ordinal stats for each of the ordinal series (generally, initial data) and
920
+ * returns a {@link WindowedOrdinalStats} object
921
+ * @param series The array of ordinal series
922
+ * @return A {@link WindowedOrdinalStats} object with the stats for each of the series
923
+ */
924
+ function initialOrdinalStats(series: Array<OrdinalSeries>): WindowedOrdinalStats {
925
+ const ordinalStats = calculateOrdinalStats(series)
926
+ return {
927
+ ...ordinalStats,
928
+ windowedValueStatsForSeries: copyValueStatsForSeries(ordinalStats.valueStatsForSeries)
929
+ }
930
+ }
931
+
932
+ /**
933
+ * Functions that return the bounds of the category. The {@link lower} function
934
+ * returns the lower bound of the category within which the value falls. The
935
+ * {@link upper} function returns the upper bound of the category within which the
936
+ * value falls
937
+ */
938
+ type CategoryBounds = {
939
+ lower: (value: number) => number
940
+ upper: (value: number) => number,
941
+ }
942
+
943
+ /**
944
+ * Attempts to locate the x- and y-axes for the specified series. If no axis is found for the
945
+ * series name, then uses the default returned by the useChart() hook.
946
+ * @param seriesName Name of the series for which to retrieve the axis
947
+ * @param axisAssignments A map holding the series name and the associated x- and y-axes assigned
948
+ * to that series. Note that the series in the axis-assignment map is merely a subset of the set
949
+ * of series names.
950
+ * @param xAxisFor The function that accepts an axis ID and returns the corresponding x-axis
951
+ * @param yAxisFor The function that accepts an axis ID and returns the corresponding y-axis
952
+ * @return A 2d tuple holding the linear x-axis as its first element and the category y-axis as
953
+ * the second element.
954
+ */
955
+ function axesFor(
956
+ seriesName: string,
957
+ axisAssignments: Map<string, AxesAssignment>,
958
+ xAxisFor: (id: string) => BaseAxis | undefined,
959
+ yAxisFor: (id: string) => BaseAxis | undefined,
960
+ ): [xAxis: OrdinalStringAxis, yAxis: ContinuousNumericAxis] {
961
+ const axes = axisAssignments.get(seriesName)
962
+ const xAxis = xAxisFor(axes?.xAxis || "")
963
+ const xAxisCategory = xAxis as OrdinalStringAxis
964
+ if (xAxis && !xAxisCategory) {
965
+ throw Error("Bar plot requires that x-axis be of type CategoryAxis")
966
+ }
967
+ const yAxis = yAxisFor(axes?.yAxis || "")
968
+ const yAxisContinuous = yAxis as ContinuousNumericAxis
969
+ if (yAxis && !yAxisContinuous) {
970
+ throw Error("Bar plot requires that y-axis be of type ContinuousNumericAxis")
971
+ }
972
+ return [xAxisCategory, yAxisContinuous]
973
+ }
974
+
975
+ /**
976
+ * Calculates the upper and lower coordinate for the category
977
+ * @param categorySize The size of the category (i.e. plot_height / num_series)
978
+ * @param lineWidth The width of the series line
979
+ * @param margin The margin applied to the top and bottom of the spike line (vertical spacing)
980
+ * @return An object with two functions, that when handed a y-coordinate, return the location
981
+ * for the start (yUpper) or end (yLower) of the spikes line.
982
+ */
983
+ function xAxisCategoryBoundsFn(categorySize: number, lineWidth: number, margin: number): CategoryBounds {
984
+ if (categorySize <= margin) return {
985
+ upper: value => value + lineWidth,
986
+ lower: value => value
987
+ }
988
+ return {
989
+ upper: value => value + categorySize - margin,
990
+ lower: value => value + margin
991
+ }
992
+ }
993
+
994
+ /**
995
+ * Renders a tooltip showing for the bar in the bar chart (see {@link BarPlotTooltipContent})
996
+ * @param container The chart container
997
+ * @param yAxis The y-axis
998
+ * @param selectedSeries The selected series
999
+ * @param seriesStats The statistics about the current series
1000
+ * @param event The mouse-over series event holding the line element
1001
+ * @param margin The plot margin
1002
+ * @param barStyles The series style information (needed for (un)highlighting)
1003
+ * @param defaultBarSeriesStyle The default bar series style that is used if no style is found for the series
1004
+ * @param allowTooltip When set to `false` won't show tooltip, even if it is visible (used by pan)
1005
+ * @param mouseOverHandlerFor The handler for the mouse over (registered by the <Tooltip/>)
1006
+ * @param tooltipProvider The ID of the tooltip provider
1007
+ */
1008
+ function handleMouseOverBar(
1009
+ container: SVGSVGElement,
1010
+ yAxis: ContinuousNumericAxis,
1011
+ selectedSeries: BaseSeries<OrdinalDatum>,
1012
+ seriesStats: WindowedOrdinalStats,
1013
+ event: React.MouseEvent<SVGLineElement>,
1014
+ margin: Margin,
1015
+ barStyles: Map<string, BarSeriesStyle>,
1016
+ defaultBarSeriesStyle: BarSeriesStyle,
1017
+ allowTooltip: boolean,
1018
+ mouseOverHandlerFor: ((
1019
+ seriesName: string,
1020
+ value: number,
1021
+ tooltipData: TooltipData<OrdinalDatum, WindowedOrdinalStats>,
1022
+ mouseCoords: [x: number, y: number]
1023
+ ) => void) | undefined,
1024
+ tooltipProvider?: string
1025
+ ): void {
1026
+ // grab the time needed for the tooltip ID
1027
+ const [x, y] = d3.pointer(event, container)
1028
+ const value = yAxis.scale.invert(y - margin.top)
1029
+
1030
+ const {name: categoryName, data: selectedData} = selectedSeries
1031
+
1032
+ const barSeriesStyle = barStyles.get(categoryName) || defaultBarSeriesStyle
1033
+ const lineStyle = lineStyleFor(tooltipProvider, barSeriesStyle)
1034
+ if (lineStyle !== undefined) {
1035
+ d3.select<SVGPathElement, Datum>(event.currentTarget)
1036
+ .style(STROKE_COLOR, lineStyle.highlight.color)
1037
+ .style(STROKE_WIDTH, lineStyle.highlight.width)
1038
+ .style(STROKE_OPACITY, lineStyle.highlight.opacity)
1039
+ }
1040
+
1041
+ if (mouseOverHandlerFor && allowTooltip) {
1042
+ // the contract for the mouse over handler is for a series
1043
+ mouseOverHandlerFor(categoryName, value, {series: selectedData, metadata: seriesStats}, [x, y])
1044
+ }
1045
+ }
1046
+
1047
+ /**
1048
+ * Unselects the time series and calls the mouse-leave-series handler registered for this series.
1049
+ * @param seriesName The name of the series (i.e. the neuron ID)
1050
+ * @param segment The SVG line element representing the spike, over which the mouse is hovering.
1051
+ * @param seriesStyles The styles for the series (for (un)highlighting)
1052
+ * @param defaultBarSeriesStyle The default bar series style that is used if no style is found for the series
1053
+ * @param mouseLeaverHandlerFor Registered handler for the series when the mouse leaves
1054
+ * @param tooltipProvider The tooltip provider ID for the mouse-over event
1055
+ */
1056
+ function handleMouseLeaveSeries(
1057
+ seriesName: string,
1058
+ segment: SVGLineElement,
1059
+ seriesStyles: Map<string, BarSeriesStyle>,
1060
+ defaultBarSeriesStyle: BarSeriesStyle,
1061
+ mouseLeaverHandlerFor: ((seriesName: string) => void) | undefined,
1062
+ tooltipProvider?: string
1063
+ ): void {
1064
+ const barSeriesStyle = seriesStyles.get(seriesName) || defaultBarSeriesStyle
1065
+ const lineStyle = lineStyleFor(tooltipProvider, barSeriesStyle)
1066
+ if (lineStyle !== undefined) {
1067
+ d3.select<SVGPathElement, Datum>(segment)
1068
+ .style(STROKE_COLOR, lineStyle.regular.color)
1069
+ .style(STROKE_WIDTH, lineStyle.regular.width)
1070
+ .style(STROKE_OPACITY, lineStyle.regular.opacity)
1071
+ }
1072
+
1073
+ if (mouseLeaverHandlerFor) {
1074
+ mouseLeaverHandlerFor(seriesName)
1075
+ }
1076
+ }
1077
+
1078
+ function lineStyleFor(tooltipProvider: string | undefined, barSeriesStyle: BarSeriesStyle): LineStyle | undefined {
1079
+ const {valueLine, windowedMeanValueLine} = barSeriesStyle
1080
+ switch (tooltipProvider) {
1081
+ case BAR_CHART_TOOLTIP_PROVIDER_IDS.currentValue:
1082
+ return valueLine
1083
+ case BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMeanValue:
1084
+ return windowedMeanValueLine
1085
+ default:
1086
+ return undefined
1087
+ }
1088
+ }