stream-charts 1.1.2 → 1.2.1
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 +70 -4
- package/changes.md +8 -0
- package/dist/{src/app → app}/charts/Chart.d.ts +27 -6
- package/dist/{src/app → app}/charts/Chart.js +2 -2
- package/dist/app/charts/Chart.js.map +1 -0
- package/dist/{src/app → app}/charts/axes/AxesState.d.ts +4 -4
- package/dist/{src/app → app}/charts/axes/AxesState.js +4 -4
- package/dist/app/charts/axes/AxesState.js.map +1 -0
- package/dist/app/charts/axes/AxesState.test.js.map +1 -0
- package/dist/app/charts/axes/AxisInterval.js.map +1 -0
- package/dist/app/charts/axes/AxisInterval.test.js.map +1 -0
- package/dist/app/charts/axes/BaseAxisRange.js.map +1 -0
- package/dist/{src/app → app}/charts/axes/ContinuousAxis.d.ts +1 -3
- package/dist/{src/app → app}/charts/axes/ContinuousAxis.js +2 -3
- package/dist/app/charts/axes/ContinuousAxis.js.map +1 -0
- package/dist/app/charts/axes/ContinuousAxisRange.js.map +1 -0
- package/dist/app/charts/axes/ContinuousAxisRange.test.js.map +1 -0
- package/dist/app/charts/axes/EmptyAxis.d.ts +12 -0
- package/dist/app/charts/axes/EmptyAxis.js +70 -0
- package/dist/app/charts/axes/EmptyAxis.js.map +1 -0
- package/dist/{src/app → app}/charts/axes/OrdinalAxis.d.ts +1 -3
- package/dist/{src/app → app}/charts/axes/OrdinalAxis.js +10 -4
- package/dist/app/charts/axes/OrdinalAxis.js.map +1 -0
- package/dist/app/charts/axes/OrdinalAxisRange.js.map +1 -0
- package/dist/app/charts/axes/OrdinalAxisRange.test.js.map +1 -0
- package/dist/{src/app → app}/charts/axes/axes.d.ts +28 -0
- package/dist/{src/app → app}/charts/axes/axes.js +86 -6
- package/dist/app/charts/axes/axes.js.map +1 -0
- package/dist/{src/app → app}/charts/d3types.js.map +1 -1
- package/dist/app/charts/filters/regexFilter.js.map +1 -0
- package/dist/app/charts/filters/regexFilter.test.js.map +1 -0
- package/dist/{src/app → app}/charts/hooks/useAxes.d.ts +3 -5
- package/dist/{src/app → app}/charts/hooks/useAxes.js +0 -1
- package/dist/app/charts/hooks/useAxes.js.map +1 -0
- package/dist/{src/app → app}/charts/hooks/useChart.d.ts +9 -5
- package/dist/{src/app → app}/charts/hooks/useChart.js +4 -3
- package/dist/app/charts/hooks/useChart.js.map +1 -0
- package/dist/{src/app → app}/charts/hooks/useDataObservable.d.ts +2 -4
- package/dist/{src/app → app}/charts/hooks/useDataObservable.js +0 -2
- package/dist/app/charts/hooks/useDataObservable.js.map +1 -0
- package/dist/{src/app → app}/charts/hooks/useInitialData.d.ts +2 -4
- package/dist/{src/app → app}/charts/hooks/useInitialData.js +0 -1
- package/dist/app/charts/hooks/useInitialData.js.map +1 -0
- package/dist/{src/app → app}/charts/hooks/useMouse.d.ts +6 -2
- package/dist/{src/app → app}/charts/hooks/useMouse.js +14 -14
- package/dist/app/charts/hooks/useMouse.js.map +1 -0
- package/dist/{src/app → app}/charts/hooks/usePlotDimensions.d.ts +1 -2
- package/dist/{src/app → app}/charts/hooks/usePlotDimensions.js +0 -1
- package/dist/app/charts/hooks/usePlotDimensions.js.map +1 -0
- package/dist/{src/app → app}/charts/hooks/useTooltip.d.ts +4 -4
- package/dist/{src/app → app}/charts/hooks/useTooltip.js +0 -1
- package/dist/app/charts/hooks/useTooltip.js.map +1 -0
- package/dist/app/charts/legends/Legend.d.ts +88 -0
- package/dist/app/charts/legends/Legend.js +425 -0
- package/dist/app/charts/legends/Legend.js.map +1 -0
- package/dist/app/charts/observables/ChartData.js.map +1 -0
- package/dist/app/charts/observables/iterates.js.map +1 -0
- package/dist/app/charts/observables/iterates.test.js.map +1 -0
- package/dist/app/charts/observables/ordinals.js.map +1 -0
- package/dist/app/charts/observables/ordinals.test.js.map +1 -0
- package/dist/{src/app → app}/charts/plots/BarPlot.d.ts +10 -12
- package/dist/{src/app → app}/charts/plots/BarPlot.js +14 -15
- package/dist/app/charts/plots/BarPlot.js.map +1 -0
- package/dist/{src/app → app}/charts/plots/PoincarePlot.d.ts +1 -3
- package/dist/{src/app → app}/charts/plots/PoincarePlot.js +0 -1
- package/dist/app/charts/plots/PoincarePlot.js.map +1 -0
- package/dist/{src/app → app}/charts/plots/RasterPlot.d.ts +1 -3
- package/dist/{src/app → app}/charts/plots/RasterPlot.js +12 -7
- package/dist/app/charts/plots/RasterPlot.js.map +1 -0
- package/dist/{src/app → app}/charts/plots/ScatterPlot.d.ts +1 -3
- package/dist/{src/app → app}/charts/plots/ScatterPlot.js +7 -6
- package/dist/app/charts/plots/ScatterPlot.js.map +1 -0
- package/dist/app/charts/plots/plot.js.map +1 -0
- package/dist/app/charts/series/baseSeries.js.map +1 -0
- package/dist/app/charts/series/datumSeries.test.js.map +1 -0
- package/dist/app/charts/series/iterateSeries.js.map +1 -0
- package/dist/app/charts/series/ordinalSeries.js.map +1 -0
- package/dist/app/charts/series/timeSeries.js.map +1 -0
- package/dist/app/charts/series/timeSeriesChartData.js.map +1 -0
- package/dist/app/charts/styling/barPlotStyle.js.map +1 -0
- package/dist/app/charts/styling/margins.js.map +1 -0
- package/dist/app/charts/styling/margins.test.js.map +1 -0
- package/dist/app/charts/styling/svgStyle.js.map +1 -0
- package/dist/app/charts/subscriptions/subscription.test.js.map +1 -0
- package/dist/app/charts/subscriptions/subscriptions.js.map +1 -0
- package/dist/{src/app → app}/charts/tooltips/BarPlotTooltipContent.d.ts +1 -3
- package/dist/{src/app → app}/charts/tooltips/BarPlotTooltipContent.js +0 -1
- package/dist/app/charts/tooltips/BarPlotTooltipContent.js.map +1 -0
- package/dist/{src/app → app}/charts/tooltips/PoincarePlotTooltipContent.d.ts +1 -3
- package/dist/{src/app → app}/charts/tooltips/PoincarePlotTooltipContent.js +0 -1
- package/dist/app/charts/tooltips/PoincarePlotTooltipContent.js.map +1 -0
- package/dist/{src/app → app}/charts/tooltips/RasterPlotTooltipContent.d.ts +1 -3
- package/dist/{src/app → app}/charts/tooltips/RasterPlotTooltipContent.js +0 -1
- package/dist/app/charts/tooltips/RasterPlotTooltipContent.js.map +1 -0
- package/dist/{src/app → app}/charts/tooltips/ScatterPlotTooltipContent.d.ts +1 -3
- package/dist/{src/app → app}/charts/tooltips/ScatterPlotTooltipContent.js +3 -2
- package/dist/app/charts/tooltips/ScatterPlotTooltipContent.js.map +1 -0
- package/dist/{src/app → app}/charts/tooltips/Tooltip.d.ts +10 -11
- package/dist/{src/app → app}/charts/tooltips/Tooltip.js +12 -12
- package/dist/app/charts/tooltips/Tooltip.js.map +1 -0
- package/dist/app/charts/tooltips/tooltipUtils.js.map +1 -0
- package/dist/{src/app → app}/charts/trackers/Tracker.d.ts +1 -3
- package/dist/{src/app → app}/charts/trackers/Tracker.js +0 -1
- package/dist/app/charts/trackers/Tracker.js.map +1 -0
- package/dist/app/charts/trackers/trackerUtils.js.map +1 -0
- package/dist/app/charts/utils.js.map +1 -0
- package/dist/{src/index.d.ts → index.d.ts} +20 -24
- package/dist/{src/index.js → index.js} +19 -19
- package/dist/index.js.map +1 -0
- package/package.json +20 -8
- package/src/app/charts/Chart.tsx +29 -5
- package/src/app/charts/axes/AxesState.ts +4 -4
- package/src/app/charts/axes/ContinuousAxis.tsx +3 -4
- package/src/app/charts/axes/EmptyAxis.tsx +119 -0
- package/src/app/charts/axes/OrdinalAxis.tsx +15 -4
- package/src/app/charts/axes/axes.ts +116 -0
- package/src/app/charts/hooks/useAxes.tsx +3 -4
- package/src/app/charts/hooks/useChart.tsx +13 -5
- package/src/app/charts/hooks/useDataObservable.tsx +2 -7
- package/src/app/charts/hooks/useInitialData.tsx +2 -3
- package/src/app/charts/hooks/useMouse.tsx +16 -13
- package/src/app/charts/hooks/usePlotDimensions.tsx +1 -2
- package/src/app/charts/hooks/useTooltip.tsx +4 -5
- package/src/app/charts/legends/Legend.tsx +600 -0
- package/src/app/charts/plots/BarPlot.tsx +15 -16
- package/src/app/charts/plots/PoincarePlot.tsx +1 -2
- package/src/app/charts/plots/RasterPlot.tsx +21 -8
- package/src/app/charts/plots/ScatterPlot.tsx +9 -11
- package/src/app/charts/tooltips/BarPlotTooltipContent.tsx +1 -2
- package/src/app/charts/tooltips/PoincarePlotTooltipContent.tsx +1 -2
- package/src/app/charts/tooltips/RasterPlotTooltipContent.tsx +1 -2
- package/src/app/charts/tooltips/ScatterPlotTooltipContent.tsx +3 -2
- package/src/app/charts/tooltips/Tooltip.tsx +11 -11
- package/src/app/charts/trackers/Tracker.tsx +1 -2
- package/src/index.ts +322 -0
- package/dist/src/app/charts/Chart.js.map +0 -1
- package/dist/src/app/charts/axes/AxesState.js.map +0 -1
- package/dist/src/app/charts/axes/AxesState.test.js.map +0 -1
- package/dist/src/app/charts/axes/AxisInterval.js.map +0 -1
- package/dist/src/app/charts/axes/AxisInterval.test.js.map +0 -1
- package/dist/src/app/charts/axes/BaseAxisRange.js.map +0 -1
- package/dist/src/app/charts/axes/ContinuousAxis.js.map +0 -1
- package/dist/src/app/charts/axes/ContinuousAxisRange.js.map +0 -1
- package/dist/src/app/charts/axes/ContinuousAxisRange.test.js.map +0 -1
- package/dist/src/app/charts/axes/OrdinalAxis.js.map +0 -1
- package/dist/src/app/charts/axes/OrdinalAxisRange.js.map +0 -1
- package/dist/src/app/charts/axes/OrdinalAxisRange.test.js.map +0 -1
- package/dist/src/app/charts/axes/axes.js.map +0 -1
- package/dist/src/app/charts/filters/regexFilter.js.map +0 -1
- package/dist/src/app/charts/filters/regexFilter.test.js.map +0 -1
- package/dist/src/app/charts/hooks/useAxes.js.map +0 -1
- package/dist/src/app/charts/hooks/useChart.js.map +0 -1
- package/dist/src/app/charts/hooks/useDataObservable.js.map +0 -1
- package/dist/src/app/charts/hooks/useInitialData.js.map +0 -1
- package/dist/src/app/charts/hooks/useMouse.js.map +0 -1
- package/dist/src/app/charts/hooks/usePlotDimensions.js.map +0 -1
- package/dist/src/app/charts/hooks/useTooltip.js.map +0 -1
- package/dist/src/app/charts/observables/ChartData.js.map +0 -1
- package/dist/src/app/charts/observables/iterates.js.map +0 -1
- package/dist/src/app/charts/observables/iterates.test.js.map +0 -1
- package/dist/src/app/charts/observables/ordinals.js.map +0 -1
- package/dist/src/app/charts/observables/ordinals.test.js.map +0 -1
- package/dist/src/app/charts/plots/BarPlot.js.map +0 -1
- package/dist/src/app/charts/plots/PoincarePlot.js.map +0 -1
- package/dist/src/app/charts/plots/RasterPlot.js.map +0 -1
- package/dist/src/app/charts/plots/ScatterPlot.js.map +0 -1
- package/dist/src/app/charts/plots/plot.js.map +0 -1
- package/dist/src/app/charts/series/baseSeries.js.map +0 -1
- package/dist/src/app/charts/series/datumSeries.test.js.map +0 -1
- package/dist/src/app/charts/series/iterateSeries.js.map +0 -1
- package/dist/src/app/charts/series/ordinalSeries.js.map +0 -1
- package/dist/src/app/charts/series/timeSeries.js.map +0 -1
- package/dist/src/app/charts/series/timeSeriesChartData.js.map +0 -1
- package/dist/src/app/charts/styling/barPlotStyle.js.map +0 -1
- package/dist/src/app/charts/styling/margins.js.map +0 -1
- package/dist/src/app/charts/styling/margins.test.js.map +0 -1
- package/dist/src/app/charts/styling/svgStyle.js.map +0 -1
- package/dist/src/app/charts/subscriptions/subscription.test.js.map +0 -1
- package/dist/src/app/charts/subscriptions/subscriptions.js.map +0 -1
- package/dist/src/app/charts/tooltips/BarPlotTooltipContent.js.map +0 -1
- package/dist/src/app/charts/tooltips/PoincarePlotTooltipContent.js.map +0 -1
- package/dist/src/app/charts/tooltips/RasterPlotTooltipContent.js.map +0 -1
- package/dist/src/app/charts/tooltips/ScatterPlotTooltipContent.js.map +0 -1
- package/dist/src/app/charts/tooltips/Tooltip.js.map +0 -1
- package/dist/src/app/charts/tooltips/tooltipUtils.js.map +0 -1
- package/dist/src/app/charts/trackers/Tracker.js.map +0 -1
- package/dist/src/app/charts/trackers/trackerUtils.js.map +0 -1
- package/dist/src/app/charts/utils.js.map +0 -1
- package/dist/src/index.js.map +0 -1
- /package/dist/{src/app → app}/charts/axes/AxesState.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/axes/AxesState.test.js +0 -0
- /package/dist/{src/app → app}/charts/axes/AxisInterval.d.ts +0 -0
- /package/dist/{src/app → app}/charts/axes/AxisInterval.js +0 -0
- /package/dist/{src/app → app}/charts/axes/AxisInterval.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/axes/AxisInterval.test.js +0 -0
- /package/dist/{src/app → app}/charts/axes/BaseAxisRange.d.ts +0 -0
- /package/dist/{src/app → app}/charts/axes/BaseAxisRange.js +0 -0
- /package/dist/{src/app → app}/charts/axes/ContinuousAxisRange.d.ts +0 -0
- /package/dist/{src/app → app}/charts/axes/ContinuousAxisRange.js +0 -0
- /package/dist/{src/app → app}/charts/axes/ContinuousAxisRange.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/axes/ContinuousAxisRange.test.js +0 -0
- /package/dist/{src/app → app}/charts/axes/OrdinalAxisRange.d.ts +0 -0
- /package/dist/{src/app → app}/charts/axes/OrdinalAxisRange.js +0 -0
- /package/dist/{src/app → app}/charts/axes/OrdinalAxisRange.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/axes/OrdinalAxisRange.test.js +0 -0
- /package/dist/{src/app → app}/charts/d3types.d.ts +0 -0
- /package/dist/{src/app → app}/charts/d3types.js +0 -0
- /package/dist/{src/app → app}/charts/filters/regexFilter.d.ts +0 -0
- /package/dist/{src/app → app}/charts/filters/regexFilter.js +0 -0
- /package/dist/{src/app → app}/charts/filters/regexFilter.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/filters/regexFilter.test.js +0 -0
- /package/dist/{src/app → app}/charts/observables/ChartData.d.ts +0 -0
- /package/dist/{src/app → app}/charts/observables/ChartData.js +0 -0
- /package/dist/{src/app → app}/charts/observables/iterates.d.ts +0 -0
- /package/dist/{src/app → app}/charts/observables/iterates.js +0 -0
- /package/dist/{src/app → app}/charts/observables/iterates.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/observables/iterates.test.js +0 -0
- /package/dist/{src/app → app}/charts/observables/ordinals.d.ts +0 -0
- /package/dist/{src/app → app}/charts/observables/ordinals.js +0 -0
- /package/dist/{src/app → app}/charts/observables/ordinals.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/observables/ordinals.test.js +0 -0
- /package/dist/{src/app → app}/charts/plots/plot.d.ts +0 -0
- /package/dist/{src/app → app}/charts/plots/plot.js +0 -0
- /package/dist/{src/app → app}/charts/series/baseSeries.d.ts +0 -0
- /package/dist/{src/app → app}/charts/series/baseSeries.js +0 -0
- /package/dist/{src/app → app}/charts/series/datumSeries.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/series/datumSeries.test.js +0 -0
- /package/dist/{src/app → app}/charts/series/iterateSeries.d.ts +0 -0
- /package/dist/{src/app → app}/charts/series/iterateSeries.js +0 -0
- /package/dist/{src/app → app}/charts/series/ordinalSeries.d.ts +0 -0
- /package/dist/{src/app → app}/charts/series/ordinalSeries.js +0 -0
- /package/dist/{src/app → app}/charts/series/timeSeries.d.ts +0 -0
- /package/dist/{src/app → app}/charts/series/timeSeries.js +0 -0
- /package/dist/{src/app → app}/charts/series/timeSeriesChartData.d.ts +0 -0
- /package/dist/{src/app → app}/charts/series/timeSeriesChartData.js +0 -0
- /package/dist/{src/app → app}/charts/styling/barPlotStyle.d.ts +0 -0
- /package/dist/{src/app → app}/charts/styling/barPlotStyle.js +0 -0
- /package/dist/{src/app → app}/charts/styling/margins.d.ts +0 -0
- /package/dist/{src/app → app}/charts/styling/margins.js +0 -0
- /package/dist/{src/app → app}/charts/styling/margins.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/styling/margins.test.js +0 -0
- /package/dist/{src/app → app}/charts/styling/svgStyle.d.ts +0 -0
- /package/dist/{src/app → app}/charts/styling/svgStyle.js +0 -0
- /package/dist/{src/app → app}/charts/subscriptions/subscription.test.d.ts +0 -0
- /package/dist/{src/app → app}/charts/subscriptions/subscription.test.js +0 -0
- /package/dist/{src/app → app}/charts/subscriptions/subscriptions.d.ts +0 -0
- /package/dist/{src/app → app}/charts/subscriptions/subscriptions.js +0 -0
- /package/dist/{src/app → app}/charts/tooltips/tooltipUtils.d.ts +0 -0
- /package/dist/{src/app → app}/charts/tooltips/tooltipUtils.js +0 -0
- /package/dist/{src/app → app}/charts/trackers/trackerUtils.d.ts +0 -0
- /package/dist/{src/app → app}/charts/trackers/trackerUtils.js +0 -0
- /package/dist/{src/app → app}/charts/utils.d.ts +0 -0
- /package/dist/{src/app → app}/charts/utils.js +0 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import {BaseAxis, defaultLineStyle, SeriesLineStyle, SeriesStyle} from "../axes/axes";
|
|
2
|
+
import {BaseAxisRange} from "../axes/BaseAxisRange";
|
|
3
|
+
import {usePlotDimensions} from "../hooks/usePlotDimensions";
|
|
4
|
+
import {useChart} from "../hooks/useChart";
|
|
5
|
+
import {useInitialData} from "../hooks/useInitialData";
|
|
6
|
+
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
|
7
|
+
import {createPortal} from "react-dom";
|
|
8
|
+
import * as d3 from "d3";
|
|
9
|
+
|
|
10
|
+
export enum LegendLocation {
|
|
11
|
+
// noinspection JSUnusedGlobalSymbols
|
|
12
|
+
TOP_LEFT = "top-left",
|
|
13
|
+
TOP_RIGHT = "top-right",
|
|
14
|
+
BOTTOM_LEFT = "bottom-left",
|
|
15
|
+
BOTTOM_RIGHT = "bottom-right",
|
|
16
|
+
EXTERNAL_CONTAINER = "external-container"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LegendStyle {
|
|
20
|
+
/** Font size for the legend labels */
|
|
21
|
+
fontSize: number
|
|
22
|
+
/** Font family for the legend labels */
|
|
23
|
+
fontFamily: string
|
|
24
|
+
/** Font color for the legend labels */
|
|
25
|
+
fontColor: string
|
|
26
|
+
/** Background fill color for the legend box */
|
|
27
|
+
backgroundColor: string
|
|
28
|
+
/** Background opacity for the legend box */
|
|
29
|
+
backgroundOpacity: number
|
|
30
|
+
/** Border/stroke color for the legend box */
|
|
31
|
+
borderColor: string
|
|
32
|
+
/** Border width for the legend box */
|
|
33
|
+
borderWidth: number
|
|
34
|
+
/** Border opacity for the legend box */
|
|
35
|
+
borderOpacity: number
|
|
36
|
+
/** Corner radius of the legend box */
|
|
37
|
+
borderRadius: number
|
|
38
|
+
/** Padding inside the legend box (in pixels) */
|
|
39
|
+
padding: number
|
|
40
|
+
/** Vertical space between legend entries */
|
|
41
|
+
rowGap: number
|
|
42
|
+
/** Width of the color swatch next to each label */
|
|
43
|
+
swatchWidth: number
|
|
44
|
+
/** Height of the color swatch next to each label */
|
|
45
|
+
swatchHeight: number
|
|
46
|
+
/** Gap between the swatch and the label text */
|
|
47
|
+
swatchLabelGap: number
|
|
48
|
+
/** Maximum height of the legend before it starts scrolling (in pixels). If not provided, defaults to the plot height. */
|
|
49
|
+
maxHeight?: number
|
|
50
|
+
/** Duration of the visibility transition in milliseconds */
|
|
51
|
+
transitionDuration: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const defaultLegendStyle: LegendStyle = {
|
|
55
|
+
fontSize: 12,
|
|
56
|
+
fontFamily: "sans-serif",
|
|
57
|
+
fontColor: "#d2933f",
|
|
58
|
+
backgroundColor: "#202020",
|
|
59
|
+
backgroundOpacity: 0.85,
|
|
60
|
+
borderColor: "#d2933f",
|
|
61
|
+
borderWidth: 1,
|
|
62
|
+
borderOpacity: 0.7,
|
|
63
|
+
borderRadius: 4,
|
|
64
|
+
padding: 8,
|
|
65
|
+
rowGap: 6,
|
|
66
|
+
swatchWidth: 16,
|
|
67
|
+
swatchHeight: 3,
|
|
68
|
+
swatchLabelGap: 6,
|
|
69
|
+
transitionDuration: 350,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Props {
|
|
73
|
+
/** Whether the legend is visible */
|
|
74
|
+
visible: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Where to anchor the legend within the plot area.
|
|
77
|
+
* Ignored when `container` is provided.
|
|
78
|
+
* @default LegendLocation.TOP_RIGHT
|
|
79
|
+
*/
|
|
80
|
+
location?: LegendLocation
|
|
81
|
+
/** Style overrides for the legend */
|
|
82
|
+
style?: Partial<LegendStyle>
|
|
83
|
+
/**
|
|
84
|
+
* Optional offset in pixels from the chosen corner, applied after the margin.
|
|
85
|
+
* Ignored when `container` is provided.
|
|
86
|
+
* @default { x: 10, y: 10 }
|
|
87
|
+
*/
|
|
88
|
+
offset?: { x: number; y: number }
|
|
89
|
+
/**
|
|
90
|
+
* When provided, the legend renders as an HTML element portal into this
|
|
91
|
+
* external container instead of inside the chart SVG. Position the container
|
|
92
|
+
* however you like — the legend fills it.
|
|
93
|
+
*/
|
|
94
|
+
container?: React.RefObject<HTMLElement | null>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const LEGEND_CONTAINER_ID_PREFIX = "stream-charts-legend"
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A legend component that can be placed inside any `<Chart>` alongside any Plot.
|
|
101
|
+
* It automatically reads the series names from the initial data and their colors
|
|
102
|
+
* from the `seriesStyles` map (falling back to the chart's base `color`).
|
|
103
|
+
* The legend respects the active `seriesFilter`, showing only the matching series.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* <Chart ...>
|
|
108
|
+
* <ContinuousAxis ... />
|
|
109
|
+
* <ScatterPlot ... />
|
|
110
|
+
* <Legend visible={true} location={LegendLocation.TOP_RIGHT} />
|
|
111
|
+
* </Chart>
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
// noinspection JSUnusedGlobalSymbols
|
|
115
|
+
export function Legend<D, S extends SeriesStyle, TM, AR extends BaseAxisRange, A extends BaseAxis>(
|
|
116
|
+
props: Props
|
|
117
|
+
): React.ReactElement | null {
|
|
118
|
+
const {
|
|
119
|
+
visible,
|
|
120
|
+
location = LegendLocation.TOP_RIGHT,
|
|
121
|
+
offset = {x: 10, y: 10},
|
|
122
|
+
style,
|
|
123
|
+
container: externalContainer
|
|
124
|
+
} = props
|
|
125
|
+
|
|
126
|
+
const {chartId, container, color, seriesStyles, seriesFilter, mouse, hoveredSeriesRef} = useChart<D, S, TM, AR, A>()
|
|
127
|
+
const {margin, plotDimensions} = usePlotDimensions()
|
|
128
|
+
const {initialData} = useInitialData<any, D>()
|
|
129
|
+
|
|
130
|
+
// the mouse over series name interferes with scrolling, so we keep track of the
|
|
131
|
+
// scrolling state so the series-name mouse-over can be disabled during scrolling
|
|
132
|
+
const wheelTimeoutRef = useRef<NodeJS.Timeout>(undefined)
|
|
133
|
+
const isWheelingRef = useRef(false)
|
|
134
|
+
|
|
135
|
+
const scrollYRef = useRef<number>(0)
|
|
136
|
+
|
|
137
|
+
// keep track of whether the mouse is in the legend so that we can restore the legend
|
|
138
|
+
// opacity when the mouse leaves and is not hovering over a series
|
|
139
|
+
const [mouseInLegend, setMouseInLegend] = useState<boolean>(false)
|
|
140
|
+
|
|
141
|
+
const legendStyle = useMemo<LegendStyle>(
|
|
142
|
+
() => ({
|
|
143
|
+
...defaultLegendStyle,
|
|
144
|
+
maxHeight: style?.maxHeight ?? plotDimensions.height - 4 * Math.max(style?.swatchHeight ?? defaultLegendStyle.swatchHeight, style?.fontSize ?? defaultLegendStyle.swatchHeight),
|
|
145
|
+
...style
|
|
146
|
+
}),
|
|
147
|
+
[style, plotDimensions.height]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
// Refs don't trigger re-renders when populated, so track readiness in state
|
|
151
|
+
const [externalContainerReady, setExternalContainerReady] = useState(false)
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
setExternalContainerReady(!!externalContainer?.current)
|
|
154
|
+
}, [externalContainer])
|
|
155
|
+
|
|
156
|
+
// Track the currently hovered series name so legend entries can be highlighted
|
|
157
|
+
const [hoveredSeriesName, setHoveredSeriesName] = useState<string | null>(null)
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const handlerId = `legend-${chartId}`
|
|
160
|
+
mouse.registerMouseOverHandler(handlerId, seriesName => setHoveredSeriesName(seriesName))
|
|
161
|
+
mouse.registerMouseLeaveHandler(handlerId, () => setHoveredSeriesName(null))
|
|
162
|
+
return () => {
|
|
163
|
+
mouse.unregisterMouseOverHandler(handlerId)
|
|
164
|
+
mouse.unregisterMouseLeaveHandler(handlerId)
|
|
165
|
+
}
|
|
166
|
+
}, [chartId, mouse])
|
|
167
|
+
|
|
168
|
+
// Keep a ref so D3 closures in the SVG legend always read current styles
|
|
169
|
+
const seriesStylesRef = useRef<Map<string, S>>(seriesStyles)
|
|
170
|
+
seriesStylesRef.current = seriesStyles
|
|
171
|
+
|
|
172
|
+
const highlightSeriesInPlot = useCallback<(name: string) => void>(name => {
|
|
173
|
+
hoveredSeriesRef.current = name
|
|
174
|
+
if (!container) return
|
|
175
|
+
const {
|
|
176
|
+
highlightColor,
|
|
177
|
+
highlightWidth
|
|
178
|
+
} = (seriesStylesRef.current.get(name) as SeriesLineStyle | undefined) || defaultLineStyle()
|
|
179
|
+
d3.select(container)
|
|
180
|
+
.selectAll<SVGPathElement, unknown>(`path[data-series-name="${name}"], line[data-series-name="${name}"]`)
|
|
181
|
+
.attr('stroke', highlightColor)
|
|
182
|
+
.attr('stroke-width', highlightWidth)
|
|
183
|
+
}, [container, hoveredSeriesRef])
|
|
184
|
+
|
|
185
|
+
const restoreSeriesInPlot = useCallback<(name: string) => void>(name => {
|
|
186
|
+
hoveredSeriesRef.current = null
|
|
187
|
+
if (!container) return
|
|
188
|
+
const {
|
|
189
|
+
color,
|
|
190
|
+
lineWidth
|
|
191
|
+
} = (seriesStylesRef.current.get(name) as SeriesLineStyle | undefined) || defaultLineStyle()
|
|
192
|
+
d3.select(container)
|
|
193
|
+
.selectAll<SVGPathElement, unknown>(`path[data-series-name="${name}"], line[data-series-name="${name}"]`)
|
|
194
|
+
.attr('stroke', color)
|
|
195
|
+
.attr('stroke-width', lineWidth)
|
|
196
|
+
}, [container, hoveredSeriesRef])
|
|
197
|
+
|
|
198
|
+
// Derive the filtered list of series names
|
|
199
|
+
const visibleSeriesNames = useMemo<Array<string>>(
|
|
200
|
+
() => initialData.map(s => s.name).filter(name => seriesFilter.test(name)),
|
|
201
|
+
[initialData, seriesFilter]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
useEffect(
|
|
205
|
+
() => {
|
|
206
|
+
if (!container) return
|
|
207
|
+
|
|
208
|
+
const legendId = `${LEGEND_CONTAINER_ID_PREFIX}-${chartId}`
|
|
209
|
+
const svg = d3.select<SVGSVGElement, unknown>(container)
|
|
210
|
+
const {transitionDuration} = legendStyle
|
|
211
|
+
|
|
212
|
+
if (externalContainer) {
|
|
213
|
+
svg.select(`#${legendId}`).remove()
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!visible || visibleSeriesNames.length === 0) {
|
|
218
|
+
svg.select(`#${legendId}`).remove()
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Remove any existing legend (before we potentially redraw) or just reuse?
|
|
223
|
+
// Usually, redrawing is safer if we want to ensure everything is in the right place.
|
|
224
|
+
// But if we're transitioning from visible: false to true, the old one might be gone.
|
|
225
|
+
// Let's just remove without transition for redrawing, except when explicitly making it invisible.
|
|
226
|
+
svg.select(`#${legendId}`).remove()
|
|
227
|
+
|
|
228
|
+
const {
|
|
229
|
+
fontSize,
|
|
230
|
+
fontFamily,
|
|
231
|
+
fontColor,
|
|
232
|
+
backgroundColor,
|
|
233
|
+
backgroundOpacity,
|
|
234
|
+
borderColor,
|
|
235
|
+
borderWidth,
|
|
236
|
+
borderOpacity,
|
|
237
|
+
borderRadius,
|
|
238
|
+
padding,
|
|
239
|
+
rowGap,
|
|
240
|
+
swatchWidth,
|
|
241
|
+
swatchHeight,
|
|
242
|
+
swatchLabelGap,
|
|
243
|
+
maxHeight,
|
|
244
|
+
} = legendStyle
|
|
245
|
+
|
|
246
|
+
const rowHeight = Math.max(swatchHeight, fontSize)
|
|
247
|
+
const totalRows = visibleSeriesNames.length
|
|
248
|
+
const contentHeight = totalRows * rowHeight + (totalRows - 1) * rowGap
|
|
249
|
+
const totalContentHeight = contentHeight + 2 * padding
|
|
250
|
+
const boxHeight = maxHeight !== undefined ? Math.min(totalContentHeight, maxHeight) : totalContentHeight
|
|
251
|
+
|
|
252
|
+
// Use a temporary hidden SVG text element to measure max label width
|
|
253
|
+
const tempText = svg
|
|
254
|
+
.append<SVGTextElement>("text")
|
|
255
|
+
.style("font-size", `${fontSize}px`)
|
|
256
|
+
.style("font-family", fontFamily)
|
|
257
|
+
.style("visibility", "hidden")
|
|
258
|
+
|
|
259
|
+
let maxLabelWidth = 0
|
|
260
|
+
visibleSeriesNames.forEach(name => {
|
|
261
|
+
tempText.text(name)
|
|
262
|
+
const w = tempText.node()?.getBBox().width ?? name.length * (fontSize * 0.6)
|
|
263
|
+
if (w > maxLabelWidth) maxLabelWidth = w
|
|
264
|
+
})
|
|
265
|
+
tempText.remove()
|
|
266
|
+
|
|
267
|
+
const boxWidth = padding + swatchWidth + swatchLabelGap + maxLabelWidth + padding
|
|
268
|
+
|
|
269
|
+
// Determine the (x, y) position of the legend box within the SVG (plot area coordinates)
|
|
270
|
+
const plotLeft = margin.left
|
|
271
|
+
const plotTop = margin.top
|
|
272
|
+
const plotRight = margin.left + plotDimensions.width
|
|
273
|
+
const plotBottom = margin.top + plotDimensions.height
|
|
274
|
+
|
|
275
|
+
let boxX: number
|
|
276
|
+
let boxY: number
|
|
277
|
+
|
|
278
|
+
switch (location) {
|
|
279
|
+
case LegendLocation.TOP_LEFT:
|
|
280
|
+
boxX = plotLeft + offset.x
|
|
281
|
+
boxY = plotTop + offset.y
|
|
282
|
+
break
|
|
283
|
+
case LegendLocation.TOP_RIGHT:
|
|
284
|
+
boxX = plotRight - boxWidth - offset.x
|
|
285
|
+
boxY = plotTop + offset.y
|
|
286
|
+
break
|
|
287
|
+
case LegendLocation.BOTTOM_LEFT:
|
|
288
|
+
boxX = plotLeft + offset.x
|
|
289
|
+
boxY = plotBottom - boxHeight - offset.y
|
|
290
|
+
break
|
|
291
|
+
case LegendLocation.BOTTOM_RIGHT:
|
|
292
|
+
default:
|
|
293
|
+
boxX = plotRight - boxWidth - offset.x
|
|
294
|
+
boxY = plotBottom - boxHeight - offset.y
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create the legend container group
|
|
299
|
+
const legendG = svg
|
|
300
|
+
.append<SVGGElement>("g")
|
|
301
|
+
.attr("id", legendId)
|
|
302
|
+
.attr("transform", `translate(${boxX}, ${boxY})`)
|
|
303
|
+
.style("opacity", 0)
|
|
304
|
+
.on("mouseover", () => {
|
|
305
|
+
setMouseInLegend(true)
|
|
306
|
+
})
|
|
307
|
+
.on("mouseleave", () => {
|
|
308
|
+
setMouseInLegend(false)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// Apply transition for shimmering effect
|
|
312
|
+
legendG
|
|
313
|
+
.style("transition", `opacity ${transitionDuration}ms ease-in-out`)
|
|
314
|
+
.style("opacity", 1)
|
|
315
|
+
|
|
316
|
+
// Background box
|
|
317
|
+
legendG
|
|
318
|
+
.append("rect")
|
|
319
|
+
.attr("x", 0)
|
|
320
|
+
.attr("y", 0)
|
|
321
|
+
.attr("width", boxWidth)
|
|
322
|
+
.attr("height", boxHeight)
|
|
323
|
+
.attr("rx", borderRadius)
|
|
324
|
+
.attr("fill", backgroundColor)
|
|
325
|
+
.attr("fill-opacity", backgroundOpacity)
|
|
326
|
+
.attr("stroke", borderColor)
|
|
327
|
+
.attr("stroke-width", borderWidth)
|
|
328
|
+
.attr("stroke-opacity", borderOpacity)
|
|
329
|
+
|
|
330
|
+
const innerG = legendG
|
|
331
|
+
.append("g")
|
|
332
|
+
.attr("class", "legend-content")
|
|
333
|
+
.attr("transform", `translate(0, ${-scrollYRef.current})`)
|
|
334
|
+
|
|
335
|
+
// If we have a max height and the content is taller, we'd need a scrollbar.
|
|
336
|
+
// In SVG, we can use a clipPath and handle scroll events manually, or use foreignObject.
|
|
337
|
+
// foreignObject is generally better for this if we want real scrollbars.
|
|
338
|
+
const isScrolling = maxHeight !== undefined && totalContentHeight > maxHeight
|
|
339
|
+
|
|
340
|
+
if (isScrolling) {
|
|
341
|
+
const clipId = `legend-clip-${chartId}`
|
|
342
|
+
// Remove existing clipPath for this legend to avoid duplicates
|
|
343
|
+
svg.select(`#${clipId}`).remove()
|
|
344
|
+
//
|
|
345
|
+
svg.append("defs")
|
|
346
|
+
.append("clipPath")
|
|
347
|
+
.attr("id", clipId)
|
|
348
|
+
.attr("clipPathUnits", "userSpaceOnUse")
|
|
349
|
+
.append("rect")
|
|
350
|
+
.attr("x", 0)
|
|
351
|
+
.attr("y", 0)
|
|
352
|
+
.attr("width", boxWidth)
|
|
353
|
+
.attr("height", boxHeight)
|
|
354
|
+
|
|
355
|
+
legendG.attr("clip-path", `url(#${clipId})`)
|
|
356
|
+
|
|
357
|
+
// Simple scroll handling via mouse wheel
|
|
358
|
+
legendG.on("wheel", (event: WheelEvent) => {
|
|
359
|
+
event.preventDefault()
|
|
360
|
+
// scrollBarVisibleRef.current = true
|
|
361
|
+
isWheelingRef.current = true
|
|
362
|
+
const maxScroll = totalContentHeight - boxHeight
|
|
363
|
+
scrollYRef.current = Math.max(0, Math.min(maxScroll, scrollYRef.current + event.deltaY))
|
|
364
|
+
innerG.attr("transform", `translate(0, ${-scrollYRef.current})`)
|
|
365
|
+
|
|
366
|
+
// Clear the existing timer if the user is still wheeling
|
|
367
|
+
clearTimeout(wheelTimeoutRef.current);
|
|
368
|
+
|
|
369
|
+
// Set a new timer to fire after 100-200ms of inactivity
|
|
370
|
+
wheelTimeoutRef.current = setTimeout(() => {
|
|
371
|
+
isWheelingRef.current = false
|
|
372
|
+
// scrollBarVisibleRef.current = false
|
|
373
|
+
console.log(`Wheel movement has ended. ${isWheelingRef.current ? "Still wheeling." : "No longer wheeling."}`);
|
|
374
|
+
}, 150);
|
|
375
|
+
}, {passive: false})
|
|
376
|
+
|
|
377
|
+
// Visual scrollbar (optional but good for visibility)
|
|
378
|
+
const scrollbarWidth = 4
|
|
379
|
+
const scrollbarHeight = (boxHeight / totalContentHeight) * boxHeight - padding
|
|
380
|
+
const scrollbar = legendG.append("rect")
|
|
381
|
+
.attr("class", "legend-scrollbar")
|
|
382
|
+
.attr("x", boxWidth - scrollbarWidth - 2)
|
|
383
|
+
.attr("y", calculateScrollbarY(totalContentHeight, boxHeight, scrollYRef.current, scrollbarHeight, padding))
|
|
384
|
+
.attr("width", scrollbarWidth)
|
|
385
|
+
.attr("height", scrollbarHeight)
|
|
386
|
+
.attr("rx", scrollbarWidth / 2)
|
|
387
|
+
.style("fill-opacity", mouseInLegend ? 0.25 : 0)
|
|
388
|
+
.attr("fill", fontColor)
|
|
389
|
+
|
|
390
|
+
legendG.on("wheel.scrollbar", (_: WheelEvent) => {
|
|
391
|
+
scrollbar.attr("y", calculateScrollbarY(totalContentHeight, boxHeight, scrollYRef.current, scrollbarHeight, padding))
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Legend rows
|
|
396
|
+
visibleSeriesNames.forEach((name, i) => {
|
|
397
|
+
const seriesColor = seriesStyles.get(name)?.color ?? color
|
|
398
|
+
const rowY = padding + i * (rowHeight + rowGap)
|
|
399
|
+
const swatchMidY = rowY + rowHeight / 2
|
|
400
|
+
|
|
401
|
+
const rowG = innerG
|
|
402
|
+
.append("g")
|
|
403
|
+
.attr("class", "legend-row")
|
|
404
|
+
.attr("data-series-name", name)
|
|
405
|
+
.style("cursor", "default")
|
|
406
|
+
|
|
407
|
+
// Color swatch — a short horizontal line to mimic series appearance
|
|
408
|
+
rowG
|
|
409
|
+
.append("line")
|
|
410
|
+
.attr("x1", padding)
|
|
411
|
+
.attr("y1", swatchMidY)
|
|
412
|
+
.attr("x2", padding + swatchWidth)
|
|
413
|
+
.attr("y2", swatchMidY)
|
|
414
|
+
.attr("stroke", seriesColor)
|
|
415
|
+
.attr("stroke-width", swatchHeight)
|
|
416
|
+
.attr("stroke-linecap", "round")
|
|
417
|
+
|
|
418
|
+
// Series name label
|
|
419
|
+
rowG
|
|
420
|
+
.append("text")
|
|
421
|
+
.attr("x", padding + swatchWidth + swatchLabelGap)
|
|
422
|
+
.attr("y", swatchMidY)
|
|
423
|
+
.attr("dominant-baseline", "middle")
|
|
424
|
+
.attr("data-series-name", name)
|
|
425
|
+
.style("font-size", `${fontSize}px`)
|
|
426
|
+
.style("font-family", fontFamily)
|
|
427
|
+
.style("fill", fontColor)
|
|
428
|
+
.text(name)
|
|
429
|
+
|
|
430
|
+
// keep this at the end
|
|
431
|
+
rowG
|
|
432
|
+
.append("rect")
|
|
433
|
+
.attr("x", 0)
|
|
434
|
+
.attr("width", boxWidth)
|
|
435
|
+
.attr("y", padding + i * (rowHeight + rowGap) - rowGap)
|
|
436
|
+
.attr("height", rowHeight + rowGap)
|
|
437
|
+
.style("fill", backgroundColor)
|
|
438
|
+
.style("fill-opacity", 0)
|
|
439
|
+
.on("mouseover", () => {
|
|
440
|
+
if (isWheelingRef.current) return
|
|
441
|
+
setHoveredSeriesName(prevName => {
|
|
442
|
+
// restore any previous names in case the events
|
|
443
|
+
// get out of order, this prevents multiple series
|
|
444
|
+
// being highlighted when the mouse moves quickly
|
|
445
|
+
if (prevName && prevName !== name) {
|
|
446
|
+
restoreSeriesInPlot(prevName)
|
|
447
|
+
}
|
|
448
|
+
return name
|
|
449
|
+
})
|
|
450
|
+
highlightSeriesInPlot(name)
|
|
451
|
+
})
|
|
452
|
+
.on("mouseleave", () => {
|
|
453
|
+
setHoveredSeriesName(null)
|
|
454
|
+
restoreSeriesInPlot(name)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
})
|
|
458
|
+
},
|
|
459
|
+
[
|
|
460
|
+
visible, container, externalContainer, chartId, visibleSeriesNames,
|
|
461
|
+
legendStyle, location, offset, margin, plotDimensions, color,
|
|
462
|
+
seriesStyles, highlightSeriesInPlot, restoreSeriesInPlot, mouseInLegend
|
|
463
|
+
]
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
// Update SVG row opacity when the hovered series changes
|
|
467
|
+
useEffect(
|
|
468
|
+
() => {
|
|
469
|
+
if (!container || externalContainer) return
|
|
470
|
+
const legendG = d3.select(container).select(`#${LEGEND_CONTAINER_ID_PREFIX}-${chartId}`)
|
|
471
|
+
if (!legendG.empty()) {
|
|
472
|
+
const FADE_BACK_OPACITY = 0.35
|
|
473
|
+
legendG.selectAll<SVGGElement, unknown>("g.legend-row")
|
|
474
|
+
.attr("transition", `opacity ${350}ms ease-in-out`)
|
|
475
|
+
.style("opacity", function (): number {
|
|
476
|
+
const seriesName = d3.select(this).attr("data-series-name")
|
|
477
|
+
if (mouseInLegend) {
|
|
478
|
+
return hoveredSeriesName !== null && seriesName === hoveredSeriesName ? 1 : FADE_BACK_OPACITY
|
|
479
|
+
} else if (hoveredSeriesName !== null && seriesName !== hoveredSeriesName) {
|
|
480
|
+
return FADE_BACK_OPACITY
|
|
481
|
+
} else {
|
|
482
|
+
return 1
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
legendG.selectAll<SVGTextElement, unknown>("text[data-series-name]")
|
|
486
|
+
.style("font-weight", function (): string {
|
|
487
|
+
const seriesName: string = d3.select(this).attr("data-series-name")
|
|
488
|
+
return hoveredSeriesName !== null && seriesName === hoveredSeriesName ? "bold" : "normal"
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
[hoveredSeriesName, container, externalContainer, chartId, mouseInLegend]
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
// HTML portal legend — rendered outside the SVG into an external container
|
|
496
|
+
if (externalContainerReady && externalContainer?.current && visibleSeriesNames.length > 0) {
|
|
497
|
+
const {
|
|
498
|
+
fontSize,
|
|
499
|
+
fontFamily,
|
|
500
|
+
fontColor,
|
|
501
|
+
backgroundColor,
|
|
502
|
+
backgroundOpacity,
|
|
503
|
+
borderColor,
|
|
504
|
+
borderWidth,
|
|
505
|
+
borderOpacity,
|
|
506
|
+
borderRadius,
|
|
507
|
+
padding,
|
|
508
|
+
rowGap,
|
|
509
|
+
swatchWidth,
|
|
510
|
+
swatchHeight,
|
|
511
|
+
swatchLabelGap,
|
|
512
|
+
maxHeight,
|
|
513
|
+
transitionDuration,
|
|
514
|
+
} = legendStyle
|
|
515
|
+
|
|
516
|
+
const bg = d3.color(backgroundColor) as d3.RGBColor | undefined
|
|
517
|
+
const bgWithOpacity = bg
|
|
518
|
+
? `rgba(${bg.r},${bg.g},${bg.b},${backgroundOpacity})`
|
|
519
|
+
: backgroundColor
|
|
520
|
+
const bd = d3.color(borderColor) as d3.RGBColor | undefined
|
|
521
|
+
const bdWithOpacity = bd
|
|
522
|
+
? `rgba(${bd.r},${bd.g},${bd.b},${borderOpacity})`
|
|
523
|
+
: borderColor
|
|
524
|
+
|
|
525
|
+
const boxStyle: React.CSSProperties = {
|
|
526
|
+
display: "inline-flex",
|
|
527
|
+
flexDirection: "column",
|
|
528
|
+
backgroundColor: bgWithOpacity,
|
|
529
|
+
border: `${borderWidth}px solid ${bdWithOpacity}`,
|
|
530
|
+
borderRadius,
|
|
531
|
+
padding,
|
|
532
|
+
maxHeight,
|
|
533
|
+
overflowY: "auto",
|
|
534
|
+
fontFamily,
|
|
535
|
+
fontSize,
|
|
536
|
+
color: fontColor,
|
|
537
|
+
boxSizing: "border-box",
|
|
538
|
+
opacity: visible ? 1 : 0,
|
|
539
|
+
transition: visible ? `opacity ${transitionDuration}ms ease-in-out` : "none",
|
|
540
|
+
pointerEvents: visible ? "auto" : "none",
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const anyHovered = hoveredSeriesName !== null
|
|
544
|
+
return createPortal(
|
|
545
|
+
<div style={boxStyle}>
|
|
546
|
+
{visibleSeriesNames.map(name => {
|
|
547
|
+
const seriesColor = seriesStyles.get(name)?.color ?? color
|
|
548
|
+
const isHovered = name === hoveredSeriesName
|
|
549
|
+
const rowStyle: React.CSSProperties = {
|
|
550
|
+
display: "flex",
|
|
551
|
+
alignItems: "center",
|
|
552
|
+
gap: swatchLabelGap,
|
|
553
|
+
opacity: anyHovered && !isHovered ? 0.35 : 1,
|
|
554
|
+
fontWeight: isHovered ? "bold" : "normal",
|
|
555
|
+
transition: "opacity 0.15s, font-weight 0s",
|
|
556
|
+
height: rowGap + fontSize,
|
|
557
|
+
}
|
|
558
|
+
return (
|
|
559
|
+
<div
|
|
560
|
+
key={name}
|
|
561
|
+
style={{...rowStyle, cursor: "default"}}
|
|
562
|
+
onMouseEnter={() => {
|
|
563
|
+
setHoveredSeriesName(name)
|
|
564
|
+
highlightSeriesInPlot(name)
|
|
565
|
+
}}
|
|
566
|
+
onMouseLeave={() => {
|
|
567
|
+
setHoveredSeriesName(null)
|
|
568
|
+
restoreSeriesInPlot(name)
|
|
569
|
+
}}
|
|
570
|
+
>
|
|
571
|
+
<span style={{
|
|
572
|
+
display: "inline-block",
|
|
573
|
+
width: swatchWidth,
|
|
574
|
+
height: swatchHeight,
|
|
575
|
+
backgroundColor: seriesColor,
|
|
576
|
+
borderRadius: swatchHeight / 2,
|
|
577
|
+
flexShrink: 0,
|
|
578
|
+
}}/>
|
|
579
|
+
<span style={{
|
|
580
|
+
height: rowGap + fontSize,
|
|
581
|
+
alignItems: "center",
|
|
582
|
+
display: "inline-flex"
|
|
583
|
+
}}>{name}</span>
|
|
584
|
+
</div>
|
|
585
|
+
)
|
|
586
|
+
})}
|
|
587
|
+
</div>,
|
|
588
|
+
externalContainer.current
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return null
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function calculateScrollbarY(totalContentHeight: number, boxHeight: number, scrollY: number, scrollbarHeight: number, padding: number): number {
|
|
596
|
+
const maxScroll = totalContentHeight - boxHeight
|
|
597
|
+
const scrollPercent = scrollY / maxScroll
|
|
598
|
+
const scrollbarMaxY = boxHeight - scrollbarHeight - 4 - padding
|
|
599
|
+
return Math.max(padding, 2 + scrollPercent * scrollbarMaxY)
|
|
600
|
+
}
|
|
@@ -73,7 +73,7 @@ export const BAR_CHART_TOOLTIP_PROVIDER_IDS: BarChartElementId = {
|
|
|
73
73
|
windowedMinMax: TOOLTIP_PROVIDER_ID + '-windowed-mean'
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
interface Props {
|
|
76
|
+
export interface Props {
|
|
77
77
|
/**
|
|
78
78
|
* Holds the mapping between a series and the axis it uses (is assigned). The
|
|
79
79
|
* map's key holds the series name, and the value is an {@link AxesAssignment}
|
|
@@ -124,22 +124,21 @@ interface Props {
|
|
|
124
124
|
* <a href="https://github.com/robphilipp/stream-charts-examples">`StreamingBarChart` example</a>
|
|
125
125
|
*
|
|
126
126
|
* @param props The properties associated with the bar plot
|
|
127
|
-
* @constructor
|
|
128
127
|
* @example
|
|
129
128
|
* ```typescript
|
|
130
129
|
* <BarPlot
|
|
131
130
|
* barMargin={1}
|
|
132
131
|
* dropDataAfter={5000}
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
132
|
+
* panEnabled={true}
|
|
133
|
+
* zoomEnabled={true}
|
|
134
|
+
* zoomKeyModifiersRequired={true}
|
|
135
|
+
* withCadenceOf={50}
|
|
137
136
|
*
|
|
138
|
-
* showMinMaxBars={
|
|
139
|
-
* showValueLines={
|
|
140
|
-
* showMeanValueLines={
|
|
141
|
-
* showWindowedMinMaxBars={
|
|
142
|
-
* showWindowedMeanValueLines={
|
|
137
|
+
* showMinMaxBars={true}
|
|
138
|
+
* showValueLines={true}
|
|
139
|
+
* showMeanValueLines={true}
|
|
140
|
+
* showWindowedMinMaxBars={true}
|
|
141
|
+
* showWindowedMeanValueLines={true}
|
|
143
142
|
* />
|
|
144
143
|
* ```
|
|
145
144
|
*/
|
|
@@ -434,7 +433,7 @@ export function BarPlot(props: Props): null {
|
|
|
434
433
|
margin,
|
|
435
434
|
seriesStyles,
|
|
436
435
|
barSeriesStyle,
|
|
437
|
-
allowTooltipRef.current,
|
|
436
|
+
allowTooltipRef.current && showMinMaxBars,
|
|
438
437
|
mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.minMax),
|
|
439
438
|
BAR_CHART_TOOLTIP_PROVIDER_IDS.minMax
|
|
440
439
|
)
|
|
@@ -490,7 +489,7 @@ export function BarPlot(props: Props): null {
|
|
|
490
489
|
margin,
|
|
491
490
|
seriesStyles,
|
|
492
491
|
barSeriesStyle,
|
|
493
|
-
allowTooltipRef.current,
|
|
492
|
+
allowTooltipRef.current && showWindowedMinMaxBars,
|
|
494
493
|
mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMinMax),
|
|
495
494
|
BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMinMax
|
|
496
495
|
)
|
|
@@ -545,7 +544,7 @@ export function BarPlot(props: Props): null {
|
|
|
545
544
|
margin,
|
|
546
545
|
seriesStyles,
|
|
547
546
|
barSeriesStyle,
|
|
548
|
-
allowTooltipRef.current,
|
|
547
|
+
allowTooltipRef.current && showMeanValueLines,
|
|
549
548
|
mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.meanValue),
|
|
550
549
|
BAR_CHART_TOOLTIP_PROVIDER_IDS.meanValue
|
|
551
550
|
)
|
|
@@ -598,7 +597,7 @@ export function BarPlot(props: Props): null {
|
|
|
598
597
|
margin,
|
|
599
598
|
seriesStyles,
|
|
600
599
|
barSeriesStyle,
|
|
601
|
-
allowTooltipRef.current,
|
|
600
|
+
allowTooltipRef.current && showWindowedMeanValueLines,
|
|
602
601
|
mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMeanValue),
|
|
603
602
|
BAR_CHART_TOOLTIP_PROVIDER_IDS.windowedMeanValue
|
|
604
603
|
)
|
|
@@ -653,7 +652,7 @@ export function BarPlot(props: Props): null {
|
|
|
653
652
|
margin,
|
|
654
653
|
seriesStyles,
|
|
655
654
|
barSeriesStyle,
|
|
656
|
-
allowTooltipRef.current,
|
|
655
|
+
allowTooltipRef.current && showValueLines,
|
|
657
656
|
mouseOverHandlerFor(`tooltip-${chartId}`, BAR_CHART_TOOLTIP_PROVIDER_IDS.currentValue),
|
|
658
657
|
BAR_CHART_TOOLTIP_PROVIDER_IDS.currentValue
|
|
659
658
|
)
|