@sybilion/uilib 1.2.26 → 1.3.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/dist/esm/components/ui/Chart/Chart.js +5 -0
- package/dist/esm/components/ui/Chart/components/BaseChartWrapper.js +7 -32
- package/dist/esm/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.js +21 -0
- package/dist/esm/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.js +7 -0
- package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.js +460 -0
- package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.styl.js +7 -0
- package/dist/esm/components/ui/Chart/lightweight/chartTime.js +16 -0
- package/dist/esm/components/ui/Chart/lightweight/lightweightForecastChart.helpers.js +114 -0
- package/dist/esm/components/ui/Chart/lightweight/quantileBandCustomSeries.js +147 -0
- package/dist/esm/components/ui/Chart/quantileBandConeChartData.js +131 -0
- package/dist/esm/components/ui/Chart/tools/chartPlotGeometry.js +65 -0
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +37 -1
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +5 -2
- package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.js +205 -0
- package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.js +7 -0
- package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.js +37 -0
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.js +1 -0
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.js +7 -60
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.js +2 -2
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.js +1 -0
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/useChartYRange.js +2 -4
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/useQuantileBands.js +4 -102
- package/dist/esm/components/ui/TimeRangeControls/TimeRangeControls.js +7 -2
- package/dist/esm/components/ui/WorldMap/WorldMap.js +11 -0
- package/dist/esm/components/ui/WorldMap/WorldMap.styl.js +7 -0
- package/dist/esm/components/widgets/DriverCard/DriverCard.js +89 -0
- package/dist/esm/components/widgets/DriverCard/DriverCard.styl.js +7 -0
- package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.js +83 -0
- package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.styl.js +7 -0
- package/dist/esm/components/widgets/DriverCard/driverPerformanceChartData.js +50 -0
- package/dist/esm/components/widgets/DriverMap/DriverMap.js +2 -2
- package/dist/esm/components/widgets/DriverMap/DriverMap.styl.js +2 -2
- package/dist/esm/index.js +4 -2
- package/dist/esm/types/src/components/ui/Chart/Chart.d.ts +2 -0
- package/dist/esm/types/src/components/ui/Chart/components/BaseChartWrapper.d.ts +2 -1
- package/dist/esm/types/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.d.ts +14 -0
- package/dist/esm/types/src/components/ui/Chart/lightweight/LightweightForecastChart.d.ts +26 -0
- package/dist/esm/types/src/components/ui/Chart/lightweight/chartTime.d.ts +5 -0
- package/dist/esm/types/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.d.ts +13 -0
- package/dist/esm/types/src/components/ui/Chart/lightweight/quantileBandCustomSeries.d.ts +24 -0
- package/dist/esm/types/src/components/ui/Chart/quantileBandConeChartData.d.ts +7 -0
- package/dist/esm/types/src/components/ui/Chart/tools/chartPlotGeometry.d.ts +30 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.d.ts +1 -1
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +11 -2
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.d.ts +2 -2
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.d.ts +15 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.d.ts +14 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Page/PageColumns/PageColumns.d.ts +1 -1
- package/dist/esm/types/src/components/ui/TimeRangeControls/TimeRangeControls.d.ts +5 -7
- package/dist/esm/types/src/components/ui/WorldMap/WorldMap.d.ts +4 -0
- package/dist/esm/types/src/components/ui/WorldMap/index.d.ts +2 -0
- package/dist/esm/types/src/components/widgets/DriverCard/DriverCard.d.ts +9 -0
- package/dist/esm/types/src/components/widgets/DriverCard/DriverPerformanceChart.d.ts +5 -0
- package/dist/esm/types/src/components/widgets/DriverCard/driverPerformanceChartData.d.ts +7 -0
- package/dist/esm/types/src/components/widgets/DriverCard/index.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriverMap/index.d.ts +0 -2
- package/dist/esm/types/src/docs/pages/LightweightChartPage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/PageColumnsPage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/WorldMapPage.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +2 -0
- package/package.json +3 -2
- package/src/components/ui/Chart/Chart.tsx +9 -0
- package/src/components/ui/Chart/components/BaseChartWrapper.tsx +8 -41
- package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl +60 -0
- package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.d.ts +15 -0
- package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.tsx +66 -0
- package/src/components/ui/Chart/lightweight/LightweightForecastChart.styl +25 -0
- package/src/components/ui/Chart/lightweight/LightweightForecastChart.styl.d.ts +11 -0
- package/src/components/ui/Chart/lightweight/LightweightForecastChart.tsx +721 -0
- package/src/components/ui/Chart/lightweight/chartTime.ts +18 -0
- package/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.ts +141 -0
- package/src/components/ui/Chart/lightweight/quantileBandCustomSeries.ts +215 -0
- package/src/components/ui/Chart/quantileBandConeChartData.ts +171 -0
- package/src/components/ui/Chart/tools/chartPlotGeometry.ts +89 -0
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +44 -2
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +14 -1
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.ts +2 -3
- package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl +21 -0
- package/src/components/{widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl.d.ts → ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.d.ts} +3 -3
- package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.tsx +285 -0
- package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.ts +55 -0
- package/src/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.ts +1 -0
- package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl +2 -7
- package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.d.ts +0 -1
- package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.tsx +7 -71
- package/src/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.ts +1 -0
- package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.ts +2 -3
- package/src/components/ui/ChartAreaInteractive/overlays/useQuantileBands.ts +5 -131
- package/src/components/ui/Page/PageColumns/PageColumns.tsx +1 -1
- package/src/components/ui/TimeRangeControls/TimeRangeControls.tsx +16 -17
- package/src/components/{widgets/DriverMap/MapBackground/MapBackground.styl → ui/WorldMap/WorldMap.styl} +1 -3
- package/src/components/{widgets/DriverMap/MapBackground/MapBackground.styl.d.ts → ui/WorldMap/WorldMap.styl.d.ts} +1 -1
- package/src/components/ui/WorldMap/WorldMap.tsx +22 -0
- package/src/components/ui/WorldMap/index.ts +2 -0
- package/src/components/widgets/DriverCard/DriverCard.styl +169 -0
- package/src/components/widgets/DriverCard/DriverCard.styl.d.ts +40 -0
- package/src/components/widgets/DriverCard/DriverCard.tsx +219 -0
- package/src/components/widgets/DriverCard/DriverPerformanceChart.styl +43 -0
- package/src/components/widgets/DriverCard/DriverPerformanceChart.styl.d.ts +13 -0
- package/src/components/widgets/DriverCard/DriverPerformanceChart.tsx +150 -0
- package/src/components/widgets/DriverCard/driverPerformanceChartData.ts +64 -0
- package/src/components/widgets/DriverCard/index.ts +1 -0
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.tsx +0 -2
- package/src/components/widgets/DriverMap/DriverMap.styl +6 -1
- package/src/components/widgets/DriverMap/DriverMap.styl.d.ts +1 -0
- package/src/components/widgets/DriverMap/DriverMap.tsx +2 -4
- package/src/components/widgets/DriverMap/driverCategoryIcon.tsx +0 -2
- package/src/components/widgets/DriverMap/index.ts +0 -2
- package/src/declarations.d.ts +2 -0
- package/src/docs/config/webpack.config.js +26 -3
- package/src/docs/index.tsx +1 -1
- package/src/docs/pages/ChartAreaInteractivePage.tsx +2 -3
- package/src/docs/pages/DriverMapPage.tsx +214 -60
- package/src/docs/pages/LightweightChartPage.styl +18 -0
- package/src/docs/pages/LightweightChartPage.styl.d.ts +10 -0
- package/src/docs/pages/LightweightChartPage.tsx +195 -0
- package/src/docs/pages/PageColumnsPage.tsx +92 -0
- package/src/docs/pages/TimeRangeControlsPage.tsx +2 -3
- package/src/docs/pages/WorldMapPage.styl +14 -0
- package/src/docs/pages/WorldMapPage.styl.d.ts +8 -0
- package/src/docs/pages/WorldMapPage.tsx +26 -0
- package/src/docs/registry.ts +19 -1
- package/src/index.ts +2 -0
- package/dist/esm/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.js +0 -8
- package/dist/esm/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl.js +0 -7
- package/dist/esm/components/widgets/DriverMap/MapBackground/MapBackground.js +0 -10
- package/dist/esm/components/widgets/DriverMap/MapBackground/MapBackground.styl.js +0 -7
- package/dist/esm/types/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.d.ts +0 -1
- package/dist/esm/types/src/components/widgets/DriverMap/MapBackground/MapBackground.d.ts +0 -1
- package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl +0 -24
- package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.tsx +0 -11
- package/src/components/widgets/DriverMap/MapBackground/MapBackground.tsx +0 -18
- /package/dist/esm/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/map.svg.js +0 -0
- /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/map.svg +0 -0
- /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/mapAspect.mixin.styl +0 -0
- /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/mapAspect.mixin.styl.d.ts +0 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './ChartAreaInteractive.helpers';
|
|
19
19
|
import S from './ChartAreaInteractive.styl';
|
|
20
20
|
import { ChartAreaInteractiveProps } from './ChartAreaInteractive.types';
|
|
21
|
+
import { TimeRangeBrushHost } from './TimeRangeBrushLayer';
|
|
21
22
|
import { IntervalsOverlay, PinOverlay, ThresholdsOverlay } from './overlays';
|
|
22
23
|
|
|
23
24
|
export const chartConfig = {
|
|
@@ -68,6 +69,7 @@ export function ChartAreaInteractive({
|
|
|
68
69
|
overlayForecastData = {},
|
|
69
70
|
hiddenSeries,
|
|
70
71
|
disableForecastHistoricalBridge,
|
|
72
|
+
overlayElements: overlayElementsProp,
|
|
71
73
|
...restProps
|
|
72
74
|
}: ChartAreaInteractiveProps) {
|
|
73
75
|
const seriesHidden = hiddenSeries ?? new Set<string>();
|
|
@@ -161,10 +163,14 @@ export function ChartAreaInteractive({
|
|
|
161
163
|
loadingMessage,
|
|
162
164
|
excludeLegendIds,
|
|
163
165
|
forecastLineStyle,
|
|
166
|
+
overlayElements: overlayElementsProp,
|
|
164
167
|
// loadingAnalyses,
|
|
165
168
|
...restProps,
|
|
166
169
|
};
|
|
167
170
|
|
|
171
|
+
const brushEnabled =
|
|
172
|
+
!disableTimeRangeSelector && !loading && bridgedChartData.length > 1;
|
|
173
|
+
|
|
168
174
|
const renderChart = () => {
|
|
169
175
|
const overlayClassName = cn(chartContainerClassName);
|
|
170
176
|
|
|
@@ -237,7 +243,14 @@ export function ChartAreaInteractive({
|
|
|
237
243
|
</div>
|
|
238
244
|
)}
|
|
239
245
|
|
|
240
|
-
|
|
246
|
+
<TimeRangeBrushHost
|
|
247
|
+
chartData={bridgedChartData}
|
|
248
|
+
onTimeRangeChange={onTimeRangeChange}
|
|
249
|
+
enabled={brushEnabled}
|
|
250
|
+
layoutKey={chartRenderId ?? null}
|
|
251
|
+
>
|
|
252
|
+
{renderChart()}
|
|
253
|
+
</TimeRangeBrushHost>
|
|
241
254
|
</InteractionOverlay>
|
|
242
255
|
);
|
|
243
256
|
}
|
|
@@ -2,8 +2,6 @@ import { BaseChartWrapperProps } from '#uilib/components/ui/Chart/components/Bas
|
|
|
2
2
|
import type { ForecastData } from '#uilib/types/forecast-data';
|
|
3
3
|
import { LegendPayload } from 'recharts';
|
|
4
4
|
|
|
5
|
-
import { TimeRange } from './ChartAreaInteractive.helpers';
|
|
6
|
-
|
|
7
5
|
export type OverlayMode = 'pin' | 'intervals' | 'thresholds';
|
|
8
6
|
|
|
9
7
|
export interface ChartDataPoint {
|
|
@@ -28,7 +26,8 @@ export interface Analysis {
|
|
|
28
26
|
|
|
29
27
|
export interface ChartAreaInteractiveProps extends BaseChartWrapperProps {
|
|
30
28
|
chartContainerClassName?: string;
|
|
31
|
-
|
|
29
|
+
/** Preset (`6m`, `1y`, …, `All`) or `__drag:startMs,endMs` from chart brush. */
|
|
30
|
+
timeRange: string;
|
|
32
31
|
onTimeRangeChange: (range: string) => void;
|
|
33
32
|
pinMonth: string | undefined;
|
|
34
33
|
onPinMonthChange: (month: string | undefined) => void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.selection
|
|
2
|
+
position absolute
|
|
3
|
+
top 0
|
|
4
|
+
bottom 0
|
|
5
|
+
background-color var(--brand-color-500)
|
|
6
|
+
opacity 0.1
|
|
7
|
+
border 1px solid var(--ring)
|
|
8
|
+
pointer-events none
|
|
9
|
+
box-sizing border-box
|
|
10
|
+
|
|
11
|
+
.host
|
|
12
|
+
position relative
|
|
13
|
+
width 100%
|
|
14
|
+
overflow visible
|
|
15
|
+
touch-action pan-y
|
|
16
|
+
|
|
17
|
+
.plotBox
|
|
18
|
+
position absolute
|
|
19
|
+
z-index 8
|
|
20
|
+
pointer-events none
|
|
21
|
+
box-sizing border-box
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated.
|
|
2
2
|
// Please do not change this file!
|
|
3
3
|
interface CssExports {
|
|
4
|
-
'
|
|
5
|
-
'
|
|
6
|
-
'
|
|
4
|
+
'host': string;
|
|
5
|
+
'plotBox': string;
|
|
6
|
+
'selection': string;
|
|
7
7
|
}
|
|
8
8
|
export const cssExports: CssExports;
|
|
9
9
|
export default cssExports;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CSSProperties,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
10
|
+
import debounce from '#uilib/tools/debounce';
|
|
11
|
+
|
|
12
|
+
import { CHART_MARGINS } from './ChartAreaInteractive.constants';
|
|
13
|
+
import { encodeDragTimeRange } from './ChartAreaInteractive.helpers';
|
|
14
|
+
import S from './TimeRangeBrushLayer.styl';
|
|
15
|
+
import {
|
|
16
|
+
BRUSH_DOUBLE_TAP_MAX_DIST_PX,
|
|
17
|
+
BRUSH_DOUBLE_TAP_MS,
|
|
18
|
+
BRUSH_LAYOUT_RESIZE_DEBOUNCE_MS,
|
|
19
|
+
BRUSH_MIN_DRAG_PX,
|
|
20
|
+
type BrushPlotLayout,
|
|
21
|
+
brushClientXToDate,
|
|
22
|
+
brushPlotLayoutsEqual,
|
|
23
|
+
measureBrushPlotLayout,
|
|
24
|
+
resolveChartMargin,
|
|
25
|
+
} from './TimeRangeBrushLayout.helpers';
|
|
26
|
+
|
|
27
|
+
type LastUp = { t: number; x: number; y: number };
|
|
28
|
+
|
|
29
|
+
export interface TimeRangeBrushHostProps {
|
|
30
|
+
chartData: ChartDataPoint[];
|
|
31
|
+
onTimeRangeChange: (range: string) => void;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
/** Bumps layout sync when chart remounts (e.g. dataset / render id). */
|
|
34
|
+
layoutKey?: string | number | null;
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Wraps chart; pointerdown on SVG starts horizontal brush. Plot box tracks the
|
|
40
|
+
* painted grid / Recharts plot (see TimeRangeBrushLayout.helpers).
|
|
41
|
+
*/
|
|
42
|
+
export function TimeRangeBrushHost({
|
|
43
|
+
chartData,
|
|
44
|
+
onTimeRangeChange,
|
|
45
|
+
enabled,
|
|
46
|
+
layoutKey,
|
|
47
|
+
children,
|
|
48
|
+
}: TimeRangeBrushHostProps) {
|
|
49
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
const plotRef = useRef<HTMLDivElement>(null);
|
|
51
|
+
const lastUpRef = useRef<LastUp | null>(null);
|
|
52
|
+
const dragPointerIdRef = useRef<number | null>(null);
|
|
53
|
+
const startClientXRef = useRef(0);
|
|
54
|
+
const chartDataRef = useRef(chartData);
|
|
55
|
+
const onTimeRangeChangeRef = useRef(onTimeRangeChange);
|
|
56
|
+
const lastLayoutRef = useRef<BrushPlotLayout | null>(null);
|
|
57
|
+
|
|
58
|
+
chartDataRef.current = chartData;
|
|
59
|
+
onTimeRangeChangeRef.current = onTimeRangeChange;
|
|
60
|
+
|
|
61
|
+
const moveRef = useRef<(e: PointerEvent) => void>(() => {});
|
|
62
|
+
const upRef = useRef<(e: PointerEvent) => void>(() => {});
|
|
63
|
+
|
|
64
|
+
const onMoveStableRef = useRef<(e: PointerEvent) => void>((e: PointerEvent) =>
|
|
65
|
+
moveRef.current(e),
|
|
66
|
+
);
|
|
67
|
+
const onUpStableRef = useRef<(e: PointerEvent) => void>((e: PointerEvent) =>
|
|
68
|
+
upRef.current(e),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const [band, setBand] = useState<{ left: number; width: number } | null>(
|
|
72
|
+
null,
|
|
73
|
+
);
|
|
74
|
+
const [plotLayout, setPlotLayout] = useState<BrushPlotLayout | null>(null);
|
|
75
|
+
|
|
76
|
+
useLayoutEffect(() => {
|
|
77
|
+
if (!enabled) {
|
|
78
|
+
lastLayoutRef.current = null;
|
|
79
|
+
setPlotLayout(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const host = hostRef.current;
|
|
84
|
+
if (!host) return;
|
|
85
|
+
|
|
86
|
+
const margin = resolveChartMargin(CHART_MARGINS);
|
|
87
|
+
|
|
88
|
+
let raf = 0;
|
|
89
|
+
let wrapperRo: ResizeObserver | null = null;
|
|
90
|
+
let observedWrapper: Element | null = null;
|
|
91
|
+
|
|
92
|
+
const commitLayout = (next: BrushPlotLayout | null) => {
|
|
93
|
+
if (brushPlotLayoutsEqual(next, lastLayoutRef.current)) return;
|
|
94
|
+
lastLayoutRef.current = next;
|
|
95
|
+
setPlotLayout(next);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const runSync = () => {
|
|
99
|
+
cancelAnimationFrame(raf);
|
|
100
|
+
raf = requestAnimationFrame(() => {
|
|
101
|
+
const h = hostRef.current;
|
|
102
|
+
if (!h) {
|
|
103
|
+
commitLayout(null);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const wrappers = h.querySelectorAll('.recharts-wrapper');
|
|
108
|
+
const wrapper = wrappers[wrappers.length - 1];
|
|
109
|
+
if (!(wrapper instanceof HTMLElement)) {
|
|
110
|
+
if (wrapperRo) {
|
|
111
|
+
wrapperRo.disconnect();
|
|
112
|
+
wrapperRo = null;
|
|
113
|
+
}
|
|
114
|
+
observedWrapper = null;
|
|
115
|
+
commitLayout(null);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (wrapper !== observedWrapper) {
|
|
120
|
+
if (wrapperRo) {
|
|
121
|
+
wrapperRo.disconnect();
|
|
122
|
+
wrapperRo = null;
|
|
123
|
+
}
|
|
124
|
+
observedWrapper = wrapper;
|
|
125
|
+
wrapperRo = new ResizeObserver(() => runSync());
|
|
126
|
+
wrapperRo.observe(wrapper);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let next = measureBrushPlotLayout(h, margin);
|
|
130
|
+
if (!next) {
|
|
131
|
+
requestAnimationFrame(() => {
|
|
132
|
+
const h2 = hostRef.current;
|
|
133
|
+
if (!h2) {
|
|
134
|
+
commitLayout(null);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
commitLayout(measureBrushPlotLayout(h2, margin));
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
commitLayout(next);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const debouncedWindowResize = debounce(
|
|
146
|
+
runSync,
|
|
147
|
+
BRUSH_LAYOUT_RESIZE_DEBOUNCE_MS,
|
|
148
|
+
{ leading: false },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
runSync();
|
|
152
|
+
|
|
153
|
+
const roHost = new ResizeObserver(() => runSync());
|
|
154
|
+
roHost.observe(host);
|
|
155
|
+
|
|
156
|
+
const mo = new MutationObserver(() => runSync());
|
|
157
|
+
mo.observe(host, { childList: true, subtree: true });
|
|
158
|
+
|
|
159
|
+
window.addEventListener('resize', debouncedWindowResize);
|
|
160
|
+
|
|
161
|
+
return () => {
|
|
162
|
+
cancelAnimationFrame(raf);
|
|
163
|
+
debouncedWindowResize.cancel();
|
|
164
|
+
roHost.disconnect();
|
|
165
|
+
mo.disconnect();
|
|
166
|
+
wrapperRo?.disconnect();
|
|
167
|
+
window.removeEventListener('resize', debouncedWindowResize);
|
|
168
|
+
};
|
|
169
|
+
}, [enabled, chartData.length, layoutKey]);
|
|
170
|
+
|
|
171
|
+
useLayoutEffect(() => {
|
|
172
|
+
moveRef.current = (e: PointerEvent) => {
|
|
173
|
+
if (e.pointerId !== dragPointerIdRef.current) return;
|
|
174
|
+
const plot = plotRef.current?.getBoundingClientRect();
|
|
175
|
+
if (!plot) return;
|
|
176
|
+
const x0 = startClientXRef.current;
|
|
177
|
+
const x1 = e.clientX;
|
|
178
|
+
const left = Math.min(x0, x1);
|
|
179
|
+
const right = Math.max(x0, x1);
|
|
180
|
+
const leftClamped = Math.max(plot.left, Math.min(plot.right, left));
|
|
181
|
+
const rightClamped = Math.max(plot.left, Math.min(plot.right, right));
|
|
182
|
+
setBand({
|
|
183
|
+
left: leftClamped - plot.left,
|
|
184
|
+
width: Math.max(0, rightClamped - leftClamped),
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
upRef.current = (e: PointerEvent) => {
|
|
189
|
+
if (e.pointerId !== dragPointerIdRef.current) return;
|
|
190
|
+
const mv = onMoveStableRef.current;
|
|
191
|
+
const up = onUpStableRef.current;
|
|
192
|
+
if (mv) window.removeEventListener('pointermove', mv);
|
|
193
|
+
if (up) {
|
|
194
|
+
window.removeEventListener('pointerup', up);
|
|
195
|
+
window.removeEventListener('pointercancel', up);
|
|
196
|
+
}
|
|
197
|
+
dragPointerIdRef.current = null;
|
|
198
|
+
|
|
199
|
+
const data = chartDataRef.current;
|
|
200
|
+
const plot = plotRef.current?.getBoundingClientRect();
|
|
201
|
+
const span = Math.abs(e.clientX - startClientXRef.current);
|
|
202
|
+
let committed = false;
|
|
203
|
+
|
|
204
|
+
if (plot && data.length > 1 && span >= BRUSH_MIN_DRAG_PX) {
|
|
205
|
+
const d0 = brushClientXToDate(startClientXRef.current, plot, data);
|
|
206
|
+
const d1 = brushClientXToDate(e.clientX, plot, data);
|
|
207
|
+
if (d0 && d1) {
|
|
208
|
+
onTimeRangeChangeRef.current(encodeDragTimeRange(d0, d1));
|
|
209
|
+
committed = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
setBand(null);
|
|
214
|
+
|
|
215
|
+
if (committed) {
|
|
216
|
+
lastUpRef.current = null;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const now = performance.now();
|
|
221
|
+
const prev = lastUpRef.current;
|
|
222
|
+
if (
|
|
223
|
+
prev &&
|
|
224
|
+
now - prev.t <= BRUSH_DOUBLE_TAP_MS &&
|
|
225
|
+
Math.hypot(e.clientX - prev.x, e.clientY - prev.y) <=
|
|
226
|
+
BRUSH_DOUBLE_TAP_MAX_DIST_PX
|
|
227
|
+
) {
|
|
228
|
+
onTimeRangeChangeRef.current('All');
|
|
229
|
+
lastUpRef.current = null;
|
|
230
|
+
} else {
|
|
231
|
+
lastUpRef.current = { t: now, x: e.clientX, y: e.clientY };
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}, []);
|
|
235
|
+
|
|
236
|
+
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
237
|
+
if (!enabled || e.button !== 0) return;
|
|
238
|
+
if ((e.target as HTMLElement).tagName !== 'svg') return;
|
|
239
|
+
if (chartData.length < 2) return;
|
|
240
|
+
|
|
241
|
+
dragPointerIdRef.current = e.pointerId;
|
|
242
|
+
startClientXRef.current = e.clientX;
|
|
243
|
+
|
|
244
|
+
const plot = plotRef.current?.getBoundingClientRect();
|
|
245
|
+
if (plot) {
|
|
246
|
+
const x = Math.max(plot.left, Math.min(plot.right, e.clientX));
|
|
247
|
+
setBand({ left: x - plot.left, width: 0 });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
window.addEventListener('pointermove', onMoveStableRef.current);
|
|
251
|
+
window.addEventListener('pointerup', onUpStableRef.current);
|
|
252
|
+
window.addEventListener('pointercancel', onUpStableRef.current);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (!enabled) {
|
|
256
|
+
return <>{children}</>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const plotBoxStyle: CSSProperties | undefined =
|
|
260
|
+
plotLayout && plotLayout.width > 0 && plotLayout.height > 0
|
|
261
|
+
? {
|
|
262
|
+
position: 'absolute',
|
|
263
|
+
zIndex: 8,
|
|
264
|
+
pointerEvents: 'none',
|
|
265
|
+
left: plotLayout.left,
|
|
266
|
+
top: plotLayout.top,
|
|
267
|
+
width: plotLayout.width,
|
|
268
|
+
height: plotLayout.height,
|
|
269
|
+
}
|
|
270
|
+
: { display: 'none' };
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div ref={hostRef} className={S.host} onPointerDown={onPointerDown}>
|
|
274
|
+
{children}
|
|
275
|
+
<div ref={plotRef} className={S.plotBox} style={plotBoxStyle} aria-hidden>
|
|
276
|
+
{band != null && band.width > 0 && (
|
|
277
|
+
<div
|
|
278
|
+
className={S.selection}
|
|
279
|
+
style={{ left: band.left, width: band.width }}
|
|
280
|
+
/>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChartMargin,
|
|
3
|
+
type PlotRect,
|
|
4
|
+
measureHostRelativePlotRect,
|
|
5
|
+
resolveChartMargin,
|
|
6
|
+
} from '#uilib/components/ui/Chart/tools/chartPlotGeometry';
|
|
7
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
8
|
+
|
|
9
|
+
/** Debounce for `window` resize only (RO uses rAF-coalesced sync). */
|
|
10
|
+
export const BRUSH_LAYOUT_RESIZE_DEBOUNCE_MS = 500;
|
|
11
|
+
|
|
12
|
+
export const BRUSH_MIN_DRAG_PX = 8;
|
|
13
|
+
export const BRUSH_DOUBLE_TAP_MS = 300;
|
|
14
|
+
export const BRUSH_DOUBLE_TAP_MAX_DIST_PX = 24;
|
|
15
|
+
|
|
16
|
+
export type { ChartMargin };
|
|
17
|
+
export { resolveChartMargin };
|
|
18
|
+
|
|
19
|
+
export type BrushPlotLayout = PlotRect;
|
|
20
|
+
|
|
21
|
+
/** Host-relative plot rect (grid-first, then wrapper + margins). */
|
|
22
|
+
export const measureBrushPlotLayout = measureHostRelativePlotRect;
|
|
23
|
+
|
|
24
|
+
export function brushPlotLayoutsEqual(
|
|
25
|
+
a: BrushPlotLayout | null,
|
|
26
|
+
b: BrushPlotLayout | null,
|
|
27
|
+
): boolean {
|
|
28
|
+
if (a === b) return true;
|
|
29
|
+
if (!a || !b) return false;
|
|
30
|
+
return (
|
|
31
|
+
a.left === b.left &&
|
|
32
|
+
a.top === b.top &&
|
|
33
|
+
a.width === b.width &&
|
|
34
|
+
a.height === b.height
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function brushClientXToDate(
|
|
39
|
+
clientX: number,
|
|
40
|
+
plotRect: DOMRect,
|
|
41
|
+
chartData: ChartDataPoint[],
|
|
42
|
+
): Date | null {
|
|
43
|
+
if (!chartData.length || plotRect.width <= 0) return null;
|
|
44
|
+
const rel = clientX - plotRect.left;
|
|
45
|
+
const pct = Math.max(0, Math.min(100, (rel / plotRect.width) * 100));
|
|
46
|
+
const n = chartData.length;
|
|
47
|
+
if (n === 1) {
|
|
48
|
+
const d = chartData[0]?.date;
|
|
49
|
+
return d ? new Date(d) : null;
|
|
50
|
+
}
|
|
51
|
+
const idx = Math.round((pct / 100) * (n - 1));
|
|
52
|
+
const clamped = Math.max(0, Math.min(n - 1, idx));
|
|
53
|
+
const raw = chartData[clamped]?.date;
|
|
54
|
+
return raw ? new Date(raw) : null;
|
|
55
|
+
}
|
|
@@ -46,8 +46,6 @@
|
|
|
46
46
|
transition background-color 300ms ease-out
|
|
47
47
|
border-radius 12px
|
|
48
48
|
|
|
49
|
-
.pinDragging &::after
|
|
50
|
-
background-color var(--background-alpha-700)
|
|
51
49
|
.pinAnimating &
|
|
52
50
|
transition left 300ms ease-out
|
|
53
51
|
|
|
@@ -64,14 +62,11 @@
|
|
|
64
62
|
border-radius 9999px
|
|
65
63
|
transition transform 200ms ease-out
|
|
66
64
|
background-color var(--background)
|
|
67
|
-
cursor
|
|
65
|
+
cursor pointer
|
|
68
66
|
user-select none
|
|
69
67
|
pointer-events auto
|
|
70
68
|
touch-action none
|
|
71
69
|
|
|
72
|
-
.pinDragging &
|
|
73
|
-
transition none
|
|
74
|
-
|
|
75
70
|
.pinHovered &
|
|
76
71
|
transform scale(1.1)
|
|
77
72
|
|
|
@@ -94,7 +89,7 @@
|
|
|
94
89
|
margin-top -40px
|
|
95
90
|
z-index 20
|
|
96
91
|
pointer-events auto
|
|
97
|
-
cursor
|
|
92
|
+
cursor pointer
|
|
98
93
|
touch-action none
|
|
99
94
|
// Debug: uncomment to visualize
|
|
100
95
|
// background-color rgba(255, 0, 0, 0.1)
|
|
@@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { BaseChartWrapper } from '#uilib/components/ui/Chart/components';
|
|
5
5
|
import { BaseChartWrapperProps } from '#uilib/components/ui/Chart/components/BaseChartWrapper';
|
|
6
6
|
import { useDebounceCallback } from '#uilib/hooks/useDebounceCallback';
|
|
7
|
-
import useDragElem, { Delta } from '#uilib/hooks/useDragElem';
|
|
8
7
|
import { ChevronsLeftRight } from 'lucide-react';
|
|
9
8
|
|
|
10
9
|
import CAS from '../../ChartAreaInteractive.styl';
|
|
@@ -23,7 +22,7 @@ export function PinOverlay({
|
|
|
23
22
|
baseChartProps,
|
|
24
23
|
pinMonth,
|
|
25
24
|
onPinMonthChange,
|
|
26
|
-
onPreviewMonthChange,
|
|
25
|
+
onPreviewMonthChange: _onPreviewMonthChange,
|
|
27
26
|
className,
|
|
28
27
|
}: PinOverlayProps) {
|
|
29
28
|
const { chartData } = baseChartProps;
|
|
@@ -32,12 +31,9 @@ export function PinOverlay({
|
|
|
32
31
|
const pinPlaceholderRef = useRef<HTMLDivElement>(null);
|
|
33
32
|
const pinContainerRef = useRef<HTMLDivElement>(null);
|
|
34
33
|
|
|
35
|
-
const [isDraggingPin, setIsDraggingPin] = useState(false);
|
|
36
|
-
const [isPinAnimating, setIsPinAnimating] = useState(true);
|
|
37
34
|
const [isPinHovered, setIsPinHovered] = useState(false);
|
|
38
35
|
|
|
39
36
|
const pinPosRef = useRef<number>(0);
|
|
40
|
-
const containerRectRef = useRef<DOMRect | null>(null);
|
|
41
37
|
const currPinMonthRef = useRef<string | undefined>(pinMonth);
|
|
42
38
|
|
|
43
39
|
const debouncedOnPinMonthChange = useDebounceCallback(
|
|
@@ -61,7 +57,6 @@ export function PinOverlay({
|
|
|
61
57
|
}
|
|
62
58
|
};
|
|
63
59
|
|
|
64
|
-
// Get full month and year for pin position to send to parent component
|
|
65
60
|
const getPinMonthAndYear = (position?: number) => {
|
|
66
61
|
if (!chartData.length) return null;
|
|
67
62
|
|
|
@@ -79,7 +74,6 @@ export function PinOverlay({
|
|
|
79
74
|
}
|
|
80
75
|
};
|
|
81
76
|
|
|
82
|
-
// Handle chart click to move pin to specific position
|
|
83
77
|
const snapPinToPosition = (
|
|
84
78
|
eventX: number,
|
|
85
79
|
needMonthUpdate: boolean = true,
|
|
@@ -91,7 +85,6 @@ export function PinOverlay({
|
|
|
91
85
|
const effectiveLeft = pinContainerRect.left;
|
|
92
86
|
const effectiveWidth = pinContainerRect.width;
|
|
93
87
|
|
|
94
|
-
// Calculate relative position within plotted area
|
|
95
88
|
const relativeX = eventX - effectiveLeft;
|
|
96
89
|
const rawPercentage =
|
|
97
90
|
effectiveWidth > 0 ? (relativeX / effectiveWidth) * 100 : 0;
|
|
@@ -100,7 +93,6 @@ export function PinOverlay({
|
|
|
100
93
|
Math.min(100, isNaN(rawPercentage) ? 0 : rawPercentage),
|
|
101
94
|
);
|
|
102
95
|
|
|
103
|
-
// Snap to nearest data point
|
|
104
96
|
const totalPoints = chartData.length;
|
|
105
97
|
if (totalPoints > 1) {
|
|
106
98
|
const nearestIndex = Math.round((percentage / 100) * (totalPoints - 1));
|
|
@@ -110,12 +102,10 @@ export function PinOverlay({
|
|
|
110
102
|
setPinPosition(isNaN(snappedPercentage) ? 0 : snappedPercentage);
|
|
111
103
|
|
|
112
104
|
if (needMonthUpdate) {
|
|
113
|
-
// Update news for all months (historical and forecast)
|
|
114
105
|
const monthAndYear = getPinMonthAndYear();
|
|
115
106
|
if (monthAndYear) {
|
|
116
107
|
const isNewMonth = monthAndYear !== currPinMonthRef.current;
|
|
117
108
|
if (isNewMonth) currPinMonthRef.current = monthAndYear;
|
|
118
|
-
// When immediate (e.g. dragEnd), always notify parent so showFutureOutlook is correct
|
|
119
109
|
if (immediateMonthUpdate) {
|
|
120
110
|
onPinMonthChange?.(monthAndYear);
|
|
121
111
|
} else if (isNewMonth) {
|
|
@@ -126,11 +116,9 @@ export function PinOverlay({
|
|
|
126
116
|
}
|
|
127
117
|
};
|
|
128
118
|
|
|
129
|
-
// Update pin position when pinMonth prop changes
|
|
130
119
|
useEffect(() => {
|
|
131
120
|
if (!pinMonth || !chartData.length) return;
|
|
132
121
|
|
|
133
|
-
// Find the data point index for the given month
|
|
134
122
|
const dataPointIndex = chartData.findIndex(point => {
|
|
135
123
|
const date = new Date(point.date);
|
|
136
124
|
const month = date.toLocaleDateString('en-US', { month: 'short' });
|
|
@@ -147,59 +135,10 @@ export function PinOverlay({
|
|
|
147
135
|
}
|
|
148
136
|
}, [pinMonth, chartData]);
|
|
149
137
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
containerRectRef.current =
|
|
156
|
-
pinContainerRef.current?.getBoundingClientRect() || null;
|
|
157
|
-
},
|
|
158
|
-
onDrag: (delta: Delta) => {
|
|
159
|
-
if (pinRef.current) {
|
|
160
|
-
const containerRect = containerRectRef.current;
|
|
161
|
-
if (!containerRect) return;
|
|
162
|
-
|
|
163
|
-
const pinCurrentLeft = (pinPosRef.current / 100) * containerRect.width;
|
|
164
|
-
|
|
165
|
-
const leftLimit = -pinCurrentLeft;
|
|
166
|
-
const rightLimit = containerRect.width - pinCurrentLeft;
|
|
167
|
-
|
|
168
|
-
const clampedDeltaX = Math.max(
|
|
169
|
-
leftLimit,
|
|
170
|
-
Math.min(rightLimit, delta.x),
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
setIsPinAnimating(false);
|
|
174
|
-
pinRef.current.style.transform = `translateX(${clampedDeltaX}px)`;
|
|
175
|
-
|
|
176
|
-
// Calculate preview month based on current drag position
|
|
177
|
-
if (onPreviewMonthChange && chartData.length > 0) {
|
|
178
|
-
const newPositionPixels = pinCurrentLeft + clampedDeltaX;
|
|
179
|
-
const newPercentage = (newPositionPixels / containerRect.width) * 100;
|
|
180
|
-
const clampedPercentage = Math.max(0, Math.min(100, newPercentage));
|
|
181
|
-
const previewMonth = getPinMonthAndYear(clampedPercentage);
|
|
182
|
-
|
|
183
|
-
if (previewMonth) {
|
|
184
|
-
onPreviewMonthChange(previewMonth);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
onDragEnd: (e: PointerEvent) => {
|
|
190
|
-
setIsDraggingPin(false);
|
|
191
|
-
setTimeout(() => setIsPinAnimating(true), 200);
|
|
192
|
-
if (pinRef.current) pinRef.current.style.transform = '';
|
|
193
|
-
// Snap first so onPinMonthChange runs before we clear preview (avoids showFutureOutlook fallback)
|
|
194
|
-
snapPinToPosition(e.clientX, true, true);
|
|
195
|
-
if (onPreviewMonthChange) {
|
|
196
|
-
onPreviewMonthChange(undefined);
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
202
|
-
if ((e.target as HTMLElement).tagName !== 'svg') return;
|
|
138
|
+
const onChartClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
139
|
+
if (e.button !== 0) return;
|
|
140
|
+
const target = e.target as HTMLElement;
|
|
141
|
+
if (!target.closest?.('svg')) return;
|
|
203
142
|
snapPinToPosition(e.clientX, true, false);
|
|
204
143
|
};
|
|
205
144
|
|
|
@@ -220,7 +159,6 @@ export function PinOverlay({
|
|
|
220
159
|
</div>
|
|
221
160
|
</div>
|
|
222
161
|
</div>
|
|
223
|
-
{/* Hover placeholder for easier pin access - on top of everything */}
|
|
224
162
|
<div
|
|
225
163
|
className={S.pinPlaceholder}
|
|
226
164
|
style={{ left: `${pinPosRef.current}%` }}
|
|
@@ -236,12 +174,10 @@ export function PinOverlay({
|
|
|
236
174
|
className={cn(
|
|
237
175
|
className,
|
|
238
176
|
S.root,
|
|
239
|
-
|
|
240
|
-
isPinAnimating && S.pinAnimating,
|
|
177
|
+
S.pinAnimating,
|
|
241
178
|
isPinHovered && S.pinHovered,
|
|
242
179
|
)}
|
|
243
|
-
|
|
244
|
-
// onClick={e => snapPinToPosition(e.clientX)}
|
|
180
|
+
onClick={onChartClick}
|
|
245
181
|
ref={chartRef}
|
|
246
182
|
>
|
|
247
183
|
<BaseChartWrapper
|
|
@@ -42,10 +42,9 @@ export function useChartYRange({
|
|
|
42
42
|
Object.entries(point).forEach(([key, value]) => {
|
|
43
43
|
if (key === 'date') return;
|
|
44
44
|
|
|
45
|
-
// When selectedForecastId is provided, scale
|
|
46
|
-
// (exclude
|
|
45
|
+
// When selectedForecastId is provided, scale from historical + selected forecast + its
|
|
46
|
+
// quantile band (exclude other forecasts and their quantile values only).
|
|
47
47
|
if (forecastId !== undefined) {
|
|
48
|
-
if (key === 'historical') return;
|
|
49
48
|
if (key.startsWith('forecast_') && key !== `forecast_${forecastId}`)
|
|
50
49
|
return;
|
|
51
50
|
// Exclude q{quantile}_{otherAnalysisId} - only include selected forecast's quantiles
|