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.
- package/README.md +758 -345
- package/changes.md +10 -0
- package/dist/src/app/charts/Chart.d.ts +116 -87
- package/dist/src/app/charts/Chart.js +96 -101
- package/dist/src/app/charts/Chart.js.map +1 -1
- package/dist/src/app/charts/axes/AxesState.d.ts +64 -0
- package/dist/src/app/charts/axes/AxesState.js +97 -0
- package/dist/src/app/charts/axes/AxesState.js.map +1 -0
- package/dist/src/app/charts/axes/AxesState.test.js +30 -0
- package/dist/src/app/charts/axes/AxesState.test.js.map +1 -0
- package/dist/src/app/charts/axes/AxisInterval.d.ts +92 -0
- package/dist/src/app/charts/axes/AxisInterval.js +126 -0
- package/dist/src/app/charts/axes/AxisInterval.js.map +1 -0
- package/dist/src/app/charts/axes/AxisInterval.test.js +120 -0
- package/dist/src/app/charts/axes/AxisInterval.test.js.map +1 -0
- package/dist/src/app/charts/axes/BaseAxisRange.d.ts +83 -0
- package/dist/src/app/charts/axes/BaseAxisRange.js +58 -0
- package/dist/src/app/charts/axes/BaseAxisRange.js.map +1 -0
- package/dist/src/app/charts/axes/ContinuousAxis.d.ts +22 -0
- package/dist/src/app/charts/axes/ContinuousAxis.js +96 -0
- package/dist/src/app/charts/axes/ContinuousAxis.js.map +1 -0
- package/dist/src/app/charts/axes/ContinuousAxisRange.d.ts +59 -0
- package/dist/src/app/charts/axes/ContinuousAxisRange.js +84 -0
- package/dist/src/app/charts/axes/ContinuousAxisRange.js.map +1 -0
- package/dist/src/app/charts/axes/ContinuousAxisRange.test.js +47 -0
- package/dist/src/app/charts/axes/ContinuousAxisRange.test.js.map +1 -0
- package/dist/src/app/charts/axes/OrdinalAxis.d.ts +23 -0
- package/dist/src/app/charts/axes/OrdinalAxis.js +124 -0
- package/dist/src/app/charts/axes/OrdinalAxis.js.map +1 -0
- package/dist/src/app/charts/axes/OrdinalAxisRange.d.ts +69 -0
- package/dist/src/app/charts/axes/OrdinalAxisRange.js +100 -0
- package/dist/src/app/charts/axes/OrdinalAxisRange.js.map +1 -0
- package/dist/src/app/charts/axes/OrdinalAxisRange.test.js +48 -0
- package/dist/src/app/charts/axes/OrdinalAxisRange.test.js.map +1 -0
- package/dist/src/app/charts/axes/axes.d.ts +391 -0
- package/dist/src/app/charts/axes/axes.js +1046 -0
- package/dist/src/app/charts/axes/axes.js.map +1 -0
- package/dist/src/app/charts/d3types.d.ts +10 -10
- package/dist/src/app/charts/filters/regexFilter.d.ts +9 -0
- package/dist/src/app/charts/filters/regexFilter.js +18 -0
- package/dist/src/app/charts/filters/regexFilter.js.map +1 -0
- package/dist/src/app/charts/filters/regexFilter.test.d.ts +1 -0
- package/dist/src/app/charts/filters/regexFilter.test.js +11 -0
- package/dist/src/app/charts/filters/regexFilter.test.js.map +1 -0
- package/dist/src/app/charts/hooks/useAxes.d.ts +144 -0
- package/dist/src/app/charts/hooks/useAxes.js +187 -0
- package/dist/src/app/charts/hooks/useAxes.js.map +1 -0
- package/dist/src/app/charts/hooks/useChart.d.ts +30 -201
- package/dist/src/app/charts/hooks/useChart.js +33 -134
- package/dist/src/app/charts/hooks/useChart.js.map +1 -1
- package/dist/src/app/charts/hooks/useDataObservable.d.ts +74 -0
- package/dist/src/app/charts/hooks/useDataObservable.js +76 -0
- package/dist/src/app/charts/hooks/useDataObservable.js.map +1 -0
- package/dist/src/app/charts/hooks/useInitialData.d.ts +38 -0
- package/dist/src/app/charts/hooks/useInitialData.js +29 -0
- package/dist/src/app/charts/hooks/useInitialData.js.map +1 -0
- package/dist/src/app/charts/hooks/useMouse.d.ts +53 -0
- package/dist/src/app/charts/hooks/useMouse.js +65 -0
- package/dist/src/app/charts/hooks/useMouse.js.map +1 -0
- package/dist/src/app/charts/hooks/usePlotDimensions.d.ts +34 -0
- package/dist/src/app/charts/hooks/usePlotDimensions.js +73 -0
- package/dist/src/app/charts/hooks/usePlotDimensions.js.map +1 -0
- package/dist/src/app/charts/hooks/useTooltip.d.ts +64 -0
- package/dist/src/app/charts/hooks/useTooltip.js +52 -0
- package/dist/src/app/charts/hooks/useTooltip.js.map +1 -0
- package/dist/src/app/charts/observables/ChartData.d.ts +23 -0
- package/dist/src/app/charts/observables/ChartData.js +20 -0
- package/dist/src/app/charts/observables/ChartData.js.map +1 -0
- package/dist/src/app/charts/observables/iterates.d.ts +52 -0
- package/dist/src/app/charts/observables/iterates.js +121 -0
- package/dist/src/app/charts/observables/iterates.js.map +1 -0
- package/dist/src/app/charts/observables/iterates.test.d.ts +1 -0
- package/dist/src/app/charts/observables/iterates.test.js +180 -0
- package/dist/src/app/charts/observables/iterates.test.js.map +1 -0
- package/dist/src/app/charts/observables/ordinals.d.ts +75 -0
- package/dist/src/app/charts/observables/ordinals.js +141 -0
- package/dist/src/app/charts/observables/ordinals.js.map +1 -0
- package/dist/src/app/charts/observables/ordinals.test.d.ts +1 -0
- package/dist/src/app/charts/observables/ordinals.test.js +102 -0
- package/dist/src/app/charts/observables/ordinals.test.js.map +1 -0
- package/dist/src/app/charts/plots/BarPlot.d.ts +81 -0
- package/dist/src/app/charts/plots/BarPlot.js +557 -0
- package/dist/src/app/charts/plots/BarPlot.js.map +1 -0
- package/dist/src/app/charts/plots/PoincarePlot.d.ts +78 -0
- package/dist/src/app/charts/plots/PoincarePlot.js +504 -0
- package/dist/src/app/charts/plots/PoincarePlot.js.map +1 -0
- package/dist/src/app/charts/{RasterPlot.d.ts → plots/RasterPlot.d.ts} +1 -1
- package/dist/src/app/charts/{RasterPlot.js → plots/RasterPlot.js} +152 -133
- package/dist/src/app/charts/plots/RasterPlot.js.map +1 -0
- package/dist/src/app/charts/{ScatterPlot.d.ts → plots/ScatterPlot.d.ts} +10 -8
- package/dist/src/app/charts/plots/ScatterPlot.js +379 -0
- package/dist/src/app/charts/plots/ScatterPlot.js.map +1 -0
- package/dist/src/app/charts/{plot.d.ts → plots/plot.d.ts} +12 -14
- package/dist/src/app/charts/{plot.js → plots/plot.js} +19 -17
- package/dist/src/app/charts/plots/plot.js.map +1 -0
- package/dist/src/app/charts/series/baseSeries.d.ts +38 -0
- package/dist/src/app/charts/series/baseSeries.js +36 -0
- package/dist/src/app/charts/series/baseSeries.js.map +1 -0
- package/dist/src/app/charts/series/datumSeries.test.d.ts +1 -0
- package/dist/src/app/charts/series/datumSeries.test.js +50 -0
- package/dist/src/app/charts/series/datumSeries.test.js.map +1 -0
- package/dist/src/app/charts/series/iterateSeries.d.ts +42 -0
- package/dist/src/app/charts/series/iterateSeries.js +34 -0
- package/dist/src/app/charts/series/iterateSeries.js.map +1 -0
- package/dist/src/app/charts/series/ordinalSeries.d.ts +54 -0
- package/dist/src/app/charts/series/ordinalSeries.js +98 -0
- package/dist/src/app/charts/series/ordinalSeries.js.map +1 -0
- package/dist/src/app/charts/series/timeSeries.d.ts +30 -0
- package/dist/src/app/charts/series/timeSeries.js +19 -0
- package/dist/src/app/charts/series/timeSeries.js.map +1 -0
- package/dist/src/app/charts/{chartData.d.ts → series/timeSeriesChartData.d.ts} +11 -6
- package/dist/src/app/charts/series/timeSeriesChartData.js +32 -0
- package/dist/src/app/charts/series/timeSeriesChartData.js.map +1 -0
- package/dist/src/app/charts/styling/barPlotStyle.d.ts +41 -0
- package/dist/src/app/charts/styling/barPlotStyle.js +84 -0
- package/dist/src/app/charts/styling/barPlotStyle.js.map +1 -0
- package/dist/src/app/charts/{margins.d.ts → styling/margins.d.ts} +7 -5
- package/dist/src/app/charts/{margins.js → styling/margins.js} +19 -9
- package/dist/src/app/charts/styling/margins.js.map +1 -0
- package/dist/src/app/charts/styling/margins.test.d.ts +1 -0
- package/dist/src/app/charts/styling/margins.test.js +19 -0
- package/dist/src/app/charts/styling/margins.test.js.map +1 -0
- package/dist/src/app/charts/styling/svgStyle.d.ts +76 -0
- package/dist/src/app/charts/styling/svgStyle.js +84 -0
- package/dist/src/app/charts/styling/svgStyle.js.map +1 -0
- package/dist/src/app/charts/subscriptions/subscription.test.d.ts +1 -0
- package/dist/src/app/charts/subscriptions/subscription.test.js +395 -0
- package/dist/src/app/charts/subscriptions/subscription.test.js.map +1 -0
- package/dist/src/app/charts/subscriptions/subscriptions.d.ts +100 -0
- package/dist/src/app/charts/subscriptions/subscriptions.js +449 -0
- package/dist/src/app/charts/subscriptions/subscriptions.js.map +1 -0
- package/dist/src/app/charts/tooltips/BarPlotTooltipContent.d.ts +90 -0
- package/dist/src/app/charts/tooltips/BarPlotTooltipContent.js +302 -0
- package/dist/src/app/charts/tooltips/BarPlotTooltipContent.js.map +1 -0
- package/dist/src/app/charts/tooltips/PoincarePlotTooltipContent.d.ts +100 -0
- package/dist/src/app/charts/tooltips/PoincarePlotTooltipContent.js +190 -0
- package/dist/src/app/charts/tooltips/PoincarePlotTooltipContent.js.map +1 -0
- package/dist/src/app/charts/{RasterPlotTooltipContent.js → tooltips/RasterPlotTooltipContent.js} +40 -44
- package/dist/src/app/charts/tooltips/RasterPlotTooltipContent.js.map +1 -0
- package/dist/src/app/charts/tooltips/ScatterPlotTooltipContent.js +154 -0
- package/dist/src/app/charts/tooltips/ScatterPlotTooltipContent.js.map +1 -0
- package/dist/src/app/charts/{Tooltip.d.ts → tooltips/Tooltip.d.ts} +4 -2
- package/dist/src/app/charts/{Tooltip.js → tooltips/Tooltip.js} +23 -28
- package/dist/src/app/charts/tooltips/Tooltip.js.map +1 -0
- package/dist/src/app/charts/{tooltipUtils.d.ts → tooltips/tooltipUtils.d.ts} +17 -11
- package/dist/src/app/charts/{tooltipUtils.js → tooltips/tooltipUtils.js} +52 -25
- package/dist/src/app/charts/tooltips/tooltipUtils.js.map +1 -0
- package/dist/src/app/charts/{Tracker.d.ts → trackers/Tracker.d.ts} +6 -5
- package/dist/src/app/charts/trackers/Tracker.js +60 -0
- package/dist/src/app/charts/trackers/Tracker.js.map +1 -0
- package/dist/src/app/charts/{trackerUtils.d.ts → trackers/trackerUtils.d.ts} +13 -8
- package/dist/src/app/charts/trackers/trackerUtils.js +359 -0
- package/dist/src/app/charts/trackers/trackerUtils.js.map +1 -0
- package/dist/src/app/charts/utils.d.ts +22 -32
- package/dist/src/app/charts/utils.js +36 -37
- package/dist/src/app/charts/utils.js.map +1 -1
- package/dist/src/index.d.ts +48 -15
- package/dist/src/index.js +71 -15
- package/dist/src/index.js.map +1 -1
- package/package.json +30 -20
- package/src/app/charts/Chart.tsx +334 -0
- package/src/app/charts/axes/AxesState.test.ts +34 -0
- package/src/app/charts/axes/AxesState.ts +112 -0
- package/src/app/charts/axes/AxisInterval.test.ts +143 -0
- package/src/app/charts/axes/AxisInterval.ts +139 -0
- package/src/app/charts/axes/BaseAxisRange.ts +113 -0
- package/src/app/charts/axes/ContinuousAxis.tsx +192 -0
- package/src/app/charts/axes/ContinuousAxisRange.test.ts +55 -0
- package/src/app/charts/axes/ContinuousAxisRange.ts +96 -0
- package/src/app/charts/axes/OrdinalAxis.tsx +223 -0
- package/src/app/charts/axes/OrdinalAxisRange.test.ts +55 -0
- package/src/app/charts/axes/OrdinalAxisRange.ts +112 -0
- package/src/app/charts/axes/axes.ts +1569 -0
- package/src/app/charts/d3types.ts +16 -0
- package/src/app/charts/filters/regexFilter.test.ts +13 -0
- package/src/app/charts/filters/regexFilter.ts +17 -0
- package/src/app/charts/hooks/useAxes.tsx +341 -0
- package/src/app/charts/hooks/useChart.tsx +159 -0
- package/src/app/charts/hooks/useDataObservable.tsx +175 -0
- package/src/app/charts/hooks/useInitialData.tsx +65 -0
- package/src/app/charts/hooks/useMouse.tsx +125 -0
- package/src/app/charts/hooks/usePlotDimensions.tsx +120 -0
- package/src/app/charts/hooks/useTooltip.tsx +112 -0
- package/src/app/charts/observables/ChartData.ts +31 -0
- package/src/app/charts/observables/iterates.test.ts +201 -0
- package/src/app/charts/observables/iterates.ts +203 -0
- package/src/app/charts/observables/ordinals.test.ts +135 -0
- package/src/app/charts/observables/ordinals.ts +248 -0
- package/src/app/charts/plots/BarPlot.tsx +1088 -0
- package/src/app/charts/plots/PoincarePlot.tsx +832 -0
- package/src/app/charts/plots/RasterPlot.tsx +676 -0
- package/src/app/charts/plots/ScatterPlot.tsx +653 -0
- package/src/app/charts/plots/plot.ts +75 -0
- package/src/app/charts/series/baseSeries.ts +52 -0
- package/src/app/charts/series/datumSeries.test.ts +60 -0
- package/src/app/charts/series/iterateSeries.ts +56 -0
- package/src/app/charts/series/ordinalSeries.ts +164 -0
- package/src/app/charts/series/timeSeries.ts +40 -0
- package/src/app/charts/series/timeSeriesChartData.ts +68 -0
- package/src/app/charts/styling/barPlotStyle.ts +126 -0
- package/src/app/charts/styling/margins.test.ts +22 -0
- package/src/app/charts/styling/margins.ts +58 -0
- package/src/app/charts/styling/svgStyle.ts +118 -0
- package/src/app/charts/subscriptions/subscription.test.ts +499 -0
- package/src/app/charts/subscriptions/subscriptions.ts +635 -0
- package/src/app/charts/tooltips/BarPlotTooltipContent.tsx +440 -0
- package/src/app/charts/tooltips/PoincarePlotTooltipContent.tsx +352 -0
- package/src/app/charts/tooltips/RasterPlotTooltipContent.tsx +274 -0
- package/src/app/charts/tooltips/ScatterPlotTooltipContent.tsx +319 -0
- package/src/app/charts/tooltips/Tooltip.tsx +148 -0
- package/src/app/charts/tooltips/tooltipUtils.ts +216 -0
- package/src/app/charts/trackers/Tracker.tsx +129 -0
- package/src/app/charts/trackers/trackerUtils.ts +494 -0
- package/src/app/charts/utils.ts +109 -0
- package/dist/src/app/charts/CategoryAxis.d.ts +0 -20
- package/dist/src/app/charts/CategoryAxis.js +0 -112
- package/dist/src/app/charts/CategoryAxis.js.map +0 -1
- package/dist/src/app/charts/ContinuousAxis.d.ts +0 -16
- package/dist/src/app/charts/ContinuousAxis.js +0 -186
- package/dist/src/app/charts/ContinuousAxis.js.map +0 -1
- package/dist/src/app/charts/RasterPlot.js.map +0 -1
- package/dist/src/app/charts/RasterPlotTooltipContent.js.map +0 -1
- package/dist/src/app/charts/ScatterPlot.js +0 -351
- package/dist/src/app/charts/ScatterPlot.js.map +0 -1
- package/dist/src/app/charts/ScatterPlotTooltipContent.js +0 -163
- package/dist/src/app/charts/ScatterPlotTooltipContent.js.map +0 -1
- package/dist/src/app/charts/Tooltip.js.map +0 -1
- package/dist/src/app/charts/Tracker.js +0 -65
- package/dist/src/app/charts/Tracker.js.map +0 -1
- package/dist/src/app/charts/axes.d.ts +0 -132
- package/dist/src/app/charts/axes.js +0 -265
- package/dist/src/app/charts/axes.js.map +0 -1
- package/dist/src/app/charts/chartData.js +0 -32
- package/dist/src/app/charts/chartData.js.map +0 -1
- package/dist/src/app/charts/continuousAxisRangeFor.d.ts +0 -20
- package/dist/src/app/charts/continuousAxisRangeFor.js +0 -68
- package/dist/src/app/charts/continuousAxisRangeFor.js.map +0 -1
- package/dist/src/app/charts/datumSeries.d.ts +0 -59
- package/dist/src/app/charts/datumSeries.js +0 -55
- package/dist/src/app/charts/datumSeries.js.map +0 -1
- package/dist/src/app/charts/datumSeries.test.js +0 -50
- package/dist/src/app/charts/datumSeries.test.js.map +0 -1
- package/dist/src/app/charts/hooks/AxesState.d.ts +0 -37
- package/dist/src/app/charts/hooks/AxesState.js +0 -44
- package/dist/src/app/charts/hooks/AxesState.js.map +0 -1
- package/dist/src/app/charts/margins.js.map +0 -1
- package/dist/src/app/charts/margins.test.js +0 -14
- package/dist/src/app/charts/margins.test.js.map +0 -1
- package/dist/src/app/charts/plot.js.map +0 -1
- package/dist/src/app/charts/regexFilter.d.ts +0 -9
- package/dist/src/app/charts/regexFilter.js +0 -17
- package/dist/src/app/charts/regexFilter.js.map +0 -1
- package/dist/src/app/charts/regexFilter.test.js +0 -11
- package/dist/src/app/charts/regexFilter.test.js.map +0 -1
- package/dist/src/app/charts/subscriptions.d.ts +0 -43
- package/dist/src/app/charts/subscriptions.js +0 -213
- package/dist/src/app/charts/subscriptions.js.map +0 -1
- package/dist/src/app/charts/svgStyle.d.ts +0 -27
- package/dist/src/app/charts/svgStyle.js +0 -32
- package/dist/src/app/charts/svgStyle.js.map +0 -1
- package/dist/src/app/charts/timeRange.test.js +0 -54
- package/dist/src/app/charts/timeRange.test.js.map +0 -1
- package/dist/src/app/charts/tooltipUtils.js.map +0 -1
- package/dist/src/app/charts/trackerUtils.js +0 -130
- package/dist/src/app/charts/trackerUtils.js.map +0 -1
- /package/dist/src/app/charts/{datumSeries.test.d.ts → axes/AxesState.test.d.ts} +0 -0
- /package/dist/src/app/charts/{margins.test.d.ts → axes/AxisInterval.test.d.ts} +0 -0
- /package/dist/src/app/charts/{regexFilter.test.d.ts → axes/ContinuousAxisRange.test.d.ts} +0 -0
- /package/dist/src/app/charts/{timeRange.test.d.ts → axes/OrdinalAxisRange.test.d.ts} +0 -0
- /package/dist/src/app/charts/{RasterPlotTooltipContent.d.ts → tooltips/RasterPlotTooltipContent.d.ts} +0 -0
- /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
|
+
}
|