@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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { useRef, useState, useLayoutEffect } from 'react';
|
|
3
|
+
import debounce from '../../../tools/debounce.js';
|
|
4
|
+
import { CHART_MARGINS } from './ChartAreaInteractive.constants.js';
|
|
5
|
+
import { encodeDragTimeRange } from './ChartAreaInteractive.helpers.js';
|
|
6
|
+
import S from './TimeRangeBrushLayer.styl.js';
|
|
7
|
+
import { BRUSH_LAYOUT_RESIZE_DEBOUNCE_MS, BRUSH_MIN_DRAG_PX, brushClientXToDate, BRUSH_DOUBLE_TAP_MS, BRUSH_DOUBLE_TAP_MAX_DIST_PX, measureBrushPlotLayout, brushPlotLayoutsEqual } from './TimeRangeBrushLayout.helpers.js';
|
|
8
|
+
import { resolveChartMargin } from '../Chart/tools/chartPlotGeometry.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wraps chart; pointerdown on SVG starts horizontal brush. Plot box tracks the
|
|
12
|
+
* painted grid / Recharts plot (see TimeRangeBrushLayout.helpers).
|
|
13
|
+
*/
|
|
14
|
+
function TimeRangeBrushHost({ chartData, onTimeRangeChange, enabled, layoutKey, children, }) {
|
|
15
|
+
const hostRef = useRef(null);
|
|
16
|
+
const plotRef = useRef(null);
|
|
17
|
+
const lastUpRef = useRef(null);
|
|
18
|
+
const dragPointerIdRef = useRef(null);
|
|
19
|
+
const startClientXRef = useRef(0);
|
|
20
|
+
const chartDataRef = useRef(chartData);
|
|
21
|
+
const onTimeRangeChangeRef = useRef(onTimeRangeChange);
|
|
22
|
+
const lastLayoutRef = useRef(null);
|
|
23
|
+
chartDataRef.current = chartData;
|
|
24
|
+
onTimeRangeChangeRef.current = onTimeRangeChange;
|
|
25
|
+
const moveRef = useRef(() => { });
|
|
26
|
+
const upRef = useRef(() => { });
|
|
27
|
+
const onMoveStableRef = useRef((e) => moveRef.current(e));
|
|
28
|
+
const onUpStableRef = useRef((e) => upRef.current(e));
|
|
29
|
+
const [band, setBand] = useState(null);
|
|
30
|
+
const [plotLayout, setPlotLayout] = useState(null);
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
if (!enabled) {
|
|
33
|
+
lastLayoutRef.current = null;
|
|
34
|
+
setPlotLayout(null);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const host = hostRef.current;
|
|
38
|
+
if (!host)
|
|
39
|
+
return;
|
|
40
|
+
const margin = resolveChartMargin(CHART_MARGINS);
|
|
41
|
+
let raf = 0;
|
|
42
|
+
let wrapperRo = null;
|
|
43
|
+
let observedWrapper = null;
|
|
44
|
+
const commitLayout = (next) => {
|
|
45
|
+
if (brushPlotLayoutsEqual(next, lastLayoutRef.current))
|
|
46
|
+
return;
|
|
47
|
+
lastLayoutRef.current = next;
|
|
48
|
+
setPlotLayout(next);
|
|
49
|
+
};
|
|
50
|
+
const runSync = () => {
|
|
51
|
+
cancelAnimationFrame(raf);
|
|
52
|
+
raf = requestAnimationFrame(() => {
|
|
53
|
+
const h = hostRef.current;
|
|
54
|
+
if (!h) {
|
|
55
|
+
commitLayout(null);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const wrappers = h.querySelectorAll('.recharts-wrapper');
|
|
59
|
+
const wrapper = wrappers[wrappers.length - 1];
|
|
60
|
+
if (!(wrapper instanceof HTMLElement)) {
|
|
61
|
+
if (wrapperRo) {
|
|
62
|
+
wrapperRo.disconnect();
|
|
63
|
+
wrapperRo = null;
|
|
64
|
+
}
|
|
65
|
+
observedWrapper = null;
|
|
66
|
+
commitLayout(null);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (wrapper !== observedWrapper) {
|
|
70
|
+
if (wrapperRo) {
|
|
71
|
+
wrapperRo.disconnect();
|
|
72
|
+
wrapperRo = null;
|
|
73
|
+
}
|
|
74
|
+
observedWrapper = wrapper;
|
|
75
|
+
wrapperRo = new ResizeObserver(() => runSync());
|
|
76
|
+
wrapperRo.observe(wrapper);
|
|
77
|
+
}
|
|
78
|
+
let next = measureBrushPlotLayout(h, margin);
|
|
79
|
+
if (!next) {
|
|
80
|
+
requestAnimationFrame(() => {
|
|
81
|
+
const h2 = hostRef.current;
|
|
82
|
+
if (!h2) {
|
|
83
|
+
commitLayout(null);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
commitLayout(measureBrushPlotLayout(h2, margin));
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
commitLayout(next);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const debouncedWindowResize = debounce(runSync, BRUSH_LAYOUT_RESIZE_DEBOUNCE_MS, { leading: false });
|
|
94
|
+
runSync();
|
|
95
|
+
const roHost = new ResizeObserver(() => runSync());
|
|
96
|
+
roHost.observe(host);
|
|
97
|
+
const mo = new MutationObserver(() => runSync());
|
|
98
|
+
mo.observe(host, { childList: true, subtree: true });
|
|
99
|
+
window.addEventListener('resize', debouncedWindowResize);
|
|
100
|
+
return () => {
|
|
101
|
+
cancelAnimationFrame(raf);
|
|
102
|
+
debouncedWindowResize.cancel();
|
|
103
|
+
roHost.disconnect();
|
|
104
|
+
mo.disconnect();
|
|
105
|
+
wrapperRo?.disconnect();
|
|
106
|
+
window.removeEventListener('resize', debouncedWindowResize);
|
|
107
|
+
};
|
|
108
|
+
}, [enabled, chartData.length, layoutKey]);
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
moveRef.current = (e) => {
|
|
111
|
+
if (e.pointerId !== dragPointerIdRef.current)
|
|
112
|
+
return;
|
|
113
|
+
const plot = plotRef.current?.getBoundingClientRect();
|
|
114
|
+
if (!plot)
|
|
115
|
+
return;
|
|
116
|
+
const x0 = startClientXRef.current;
|
|
117
|
+
const x1 = e.clientX;
|
|
118
|
+
const left = Math.min(x0, x1);
|
|
119
|
+
const right = Math.max(x0, x1);
|
|
120
|
+
const leftClamped = Math.max(plot.left, Math.min(plot.right, left));
|
|
121
|
+
const rightClamped = Math.max(plot.left, Math.min(plot.right, right));
|
|
122
|
+
setBand({
|
|
123
|
+
left: leftClamped - plot.left,
|
|
124
|
+
width: Math.max(0, rightClamped - leftClamped),
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
upRef.current = (e) => {
|
|
128
|
+
if (e.pointerId !== dragPointerIdRef.current)
|
|
129
|
+
return;
|
|
130
|
+
const mv = onMoveStableRef.current;
|
|
131
|
+
const up = onUpStableRef.current;
|
|
132
|
+
if (mv)
|
|
133
|
+
window.removeEventListener('pointermove', mv);
|
|
134
|
+
if (up) {
|
|
135
|
+
window.removeEventListener('pointerup', up);
|
|
136
|
+
window.removeEventListener('pointercancel', up);
|
|
137
|
+
}
|
|
138
|
+
dragPointerIdRef.current = null;
|
|
139
|
+
const data = chartDataRef.current;
|
|
140
|
+
const plot = plotRef.current?.getBoundingClientRect();
|
|
141
|
+
const span = Math.abs(e.clientX - startClientXRef.current);
|
|
142
|
+
let committed = false;
|
|
143
|
+
if (plot && data.length > 1 && span >= BRUSH_MIN_DRAG_PX) {
|
|
144
|
+
const d0 = brushClientXToDate(startClientXRef.current, plot, data);
|
|
145
|
+
const d1 = brushClientXToDate(e.clientX, plot, data);
|
|
146
|
+
if (d0 && d1) {
|
|
147
|
+
onTimeRangeChangeRef.current(encodeDragTimeRange(d0, d1));
|
|
148
|
+
committed = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
setBand(null);
|
|
152
|
+
if (committed) {
|
|
153
|
+
lastUpRef.current = null;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const now = performance.now();
|
|
157
|
+
const prev = lastUpRef.current;
|
|
158
|
+
if (prev &&
|
|
159
|
+
now - prev.t <= BRUSH_DOUBLE_TAP_MS &&
|
|
160
|
+
Math.hypot(e.clientX - prev.x, e.clientY - prev.y) <=
|
|
161
|
+
BRUSH_DOUBLE_TAP_MAX_DIST_PX) {
|
|
162
|
+
onTimeRangeChangeRef.current('All');
|
|
163
|
+
lastUpRef.current = null;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
lastUpRef.current = { t: now, x: e.clientX, y: e.clientY };
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}, []);
|
|
170
|
+
const onPointerDown = (e) => {
|
|
171
|
+
if (!enabled || e.button !== 0)
|
|
172
|
+
return;
|
|
173
|
+
if (e.target.tagName !== 'svg')
|
|
174
|
+
return;
|
|
175
|
+
if (chartData.length < 2)
|
|
176
|
+
return;
|
|
177
|
+
dragPointerIdRef.current = e.pointerId;
|
|
178
|
+
startClientXRef.current = e.clientX;
|
|
179
|
+
const plot = plotRef.current?.getBoundingClientRect();
|
|
180
|
+
if (plot) {
|
|
181
|
+
const x = Math.max(plot.left, Math.min(plot.right, e.clientX));
|
|
182
|
+
setBand({ left: x - plot.left, width: 0 });
|
|
183
|
+
}
|
|
184
|
+
window.addEventListener('pointermove', onMoveStableRef.current);
|
|
185
|
+
window.addEventListener('pointerup', onUpStableRef.current);
|
|
186
|
+
window.addEventListener('pointercancel', onUpStableRef.current);
|
|
187
|
+
};
|
|
188
|
+
if (!enabled) {
|
|
189
|
+
return jsx(Fragment, { children: children });
|
|
190
|
+
}
|
|
191
|
+
const plotBoxStyle = plotLayout && plotLayout.width > 0 && plotLayout.height > 0
|
|
192
|
+
? {
|
|
193
|
+
position: 'absolute',
|
|
194
|
+
zIndex: 8,
|
|
195
|
+
pointerEvents: 'none',
|
|
196
|
+
left: plotLayout.left,
|
|
197
|
+
top: plotLayout.top,
|
|
198
|
+
width: plotLayout.width,
|
|
199
|
+
height: plotLayout.height,
|
|
200
|
+
}
|
|
201
|
+
: { display: 'none' };
|
|
202
|
+
return (jsxs("div", { ref: hostRef, className: S.host, onPointerDown: onPointerDown, children: [children, jsx("div", { ref: plotRef, className: S.plotBox, style: plotBoxStyle, "aria-hidden": true, children: band != null && band.width > 0 && (jsx("div", { className: S.selection, style: { left: band.left, width: band.width } })) })] }));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export { TimeRangeBrushHost };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import styleInject from 'style-inject';
|
|
2
|
+
|
|
3
|
+
var css_248z = ".TimeRangeBrushLayer_selection__X7h7U{background-color:var(--brand-color-500);border:1px solid var(--ring);bottom:0;box-sizing:border-box;opacity:.1;pointer-events:none;position:absolute;top:0}.TimeRangeBrushLayer_host__aAQTn{overflow:visible;position:relative;touch-action:pan-y;width:100%}.TimeRangeBrushLayer_plotBox__YRfgK{box-sizing:border-box;pointer-events:none;position:absolute;z-index:8}";
|
|
4
|
+
var S = {"selection":"TimeRangeBrushLayer_selection__X7h7U","host":"TimeRangeBrushLayer_host__aAQTn","plotBox":"TimeRangeBrushLayer_plotBox__YRfgK"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { S as default };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { measureHostRelativePlotRect } from '../Chart/tools/chartPlotGeometry.js';
|
|
2
|
+
export { resolveChartMargin } from '../Chart/tools/chartPlotGeometry.js';
|
|
3
|
+
|
|
4
|
+
/** Debounce for `window` resize only (RO uses rAF-coalesced sync). */
|
|
5
|
+
const BRUSH_LAYOUT_RESIZE_DEBOUNCE_MS = 500;
|
|
6
|
+
const BRUSH_MIN_DRAG_PX = 8;
|
|
7
|
+
const BRUSH_DOUBLE_TAP_MS = 300;
|
|
8
|
+
const BRUSH_DOUBLE_TAP_MAX_DIST_PX = 24;
|
|
9
|
+
/** Host-relative plot rect (grid-first, then wrapper + margins). */
|
|
10
|
+
const measureBrushPlotLayout = measureHostRelativePlotRect;
|
|
11
|
+
function brushPlotLayoutsEqual(a, b) {
|
|
12
|
+
if (a === b)
|
|
13
|
+
return true;
|
|
14
|
+
if (!a || !b)
|
|
15
|
+
return false;
|
|
16
|
+
return (a.left === b.left &&
|
|
17
|
+
a.top === b.top &&
|
|
18
|
+
a.width === b.width &&
|
|
19
|
+
a.height === b.height);
|
|
20
|
+
}
|
|
21
|
+
function brushClientXToDate(clientX, plotRect, chartData) {
|
|
22
|
+
if (!chartData.length || plotRect.width <= 0)
|
|
23
|
+
return null;
|
|
24
|
+
const rel = clientX - plotRect.left;
|
|
25
|
+
const pct = Math.max(0, Math.min(100, (rel / plotRect.width) * 100));
|
|
26
|
+
const n = chartData.length;
|
|
27
|
+
if (n === 1) {
|
|
28
|
+
const d = chartData[0]?.date;
|
|
29
|
+
return d ? new Date(d) : null;
|
|
30
|
+
}
|
|
31
|
+
const idx = Math.round((pct / 100) * (n - 1));
|
|
32
|
+
const clamped = Math.max(0, Math.min(n - 1, idx));
|
|
33
|
+
const raw = chartData[clamped]?.date;
|
|
34
|
+
return raw ? new Date(raw) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { BRUSH_DOUBLE_TAP_MAX_DIST_PX, BRUSH_DOUBLE_TAP_MS, BRUSH_LAYOUT_RESIZE_DEBOUNCE_MS, BRUSH_MIN_DRAG_PX, brushClientXToDate, brushPlotLayoutsEqual, measureBrushPlotLayout };
|
|
@@ -76,6 +76,7 @@ function useQuantileButton({ buttonRef, overlayContainerRef, quantiles, selected
|
|
|
76
76
|
onQuantileChange,
|
|
77
77
|
]);
|
|
78
78
|
const handleDragStart = useCallback((e) => {
|
|
79
|
+
e.stopPropagation();
|
|
79
80
|
setIsDragging(true);
|
|
80
81
|
if (buttonRef.current) {
|
|
81
82
|
buttonRef.current.style.transition = 'none';
|
|
@@ -6,23 +6,19 @@ import 'recharts';
|
|
|
6
6
|
import '../../../Chart/Chart.context.js';
|
|
7
7
|
import '../../../Chart/Chart.styl.js';
|
|
8
8
|
import { useDebounceCallback } from '../../../../../hooks/useDebounceCallback.js';
|
|
9
|
-
import useElemDrag from '../../../../../hooks/useDragElem.js';
|
|
10
9
|
import { ChevronsLeftRight } from 'lucide-react';
|
|
11
10
|
import S from '../../ChartAreaInteractive.styl.js';
|
|
12
11
|
import { useChartYRange } from '../useChartYRange.js';
|
|
13
12
|
import S$1 from './PinOverlay.styl.js';
|
|
14
13
|
|
|
15
|
-
function PinOverlay({ baseChartProps, pinMonth, onPinMonthChange, onPreviewMonthChange, className, }) {
|
|
14
|
+
function PinOverlay({ baseChartProps, pinMonth, onPinMonthChange, onPreviewMonthChange: _onPreviewMonthChange, className, }) {
|
|
16
15
|
const { chartData } = baseChartProps;
|
|
17
16
|
const chartRef = useRef(null);
|
|
18
17
|
const pinRef = useRef(null);
|
|
19
18
|
const pinPlaceholderRef = useRef(null);
|
|
20
19
|
const pinContainerRef = useRef(null);
|
|
21
|
-
const [isDraggingPin, setIsDraggingPin] = useState(false);
|
|
22
|
-
const [isPinAnimating, setIsPinAnimating] = useState(true);
|
|
23
20
|
const [isPinHovered, setIsPinHovered] = useState(false);
|
|
24
21
|
const pinPosRef = useRef(0);
|
|
25
|
-
const containerRectRef = useRef(null);
|
|
26
22
|
const currPinMonthRef = useRef(pinMonth);
|
|
27
23
|
const debouncedOnPinMonthChange = useDebounceCallback((...args) => onPinMonthChange?.(args[0]), 500, [onPinMonthChange]);
|
|
28
24
|
const { yMin, yMax } = useChartYRange({
|
|
@@ -38,7 +34,6 @@ function PinOverlay({ baseChartProps, pinMonth, onPinMonthChange, onPreviewMonth
|
|
|
38
34
|
pinPlaceholderRef.current.style.left = `${position}%`;
|
|
39
35
|
}
|
|
40
36
|
};
|
|
41
|
-
// Get full month and year for pin position to send to parent component
|
|
42
37
|
const getPinMonthAndYear = (position) => {
|
|
43
38
|
if (!chartData.length)
|
|
44
39
|
return null;
|
|
@@ -54,18 +49,15 @@ function PinOverlay({ baseChartProps, pinMonth, onPinMonthChange, onPreviewMonth
|
|
|
54
49
|
return `${month} ${year}`;
|
|
55
50
|
}
|
|
56
51
|
};
|
|
57
|
-
// Handle chart click to move pin to specific position
|
|
58
52
|
const snapPinToPosition = (eventX, needMonthUpdate = true, immediateMonthUpdate = false) => {
|
|
59
53
|
if (!pinContainerRef.current || !chartData.length)
|
|
60
54
|
return;
|
|
61
55
|
const pinContainerRect = pinContainerRef.current.getBoundingClientRect();
|
|
62
56
|
const effectiveLeft = pinContainerRect.left;
|
|
63
57
|
const effectiveWidth = pinContainerRect.width;
|
|
64
|
-
// Calculate relative position within plotted area
|
|
65
58
|
const relativeX = eventX - effectiveLeft;
|
|
66
59
|
const rawPercentage = effectiveWidth > 0 ? (relativeX / effectiveWidth) * 100 : 0;
|
|
67
60
|
const percentage = Math.max(0, Math.min(100, isNaN(rawPercentage) ? 0 : rawPercentage));
|
|
68
|
-
// Snap to nearest data point
|
|
69
61
|
const totalPoints = chartData.length;
|
|
70
62
|
if (totalPoints > 1) {
|
|
71
63
|
const nearestIndex = Math.round((percentage / 100) * (totalPoints - 1));
|
|
@@ -73,13 +65,11 @@ function PinOverlay({ baseChartProps, pinMonth, onPinMonthChange, onPreviewMonth
|
|
|
73
65
|
const snappedPercentage = (clampedIndex / (totalPoints - 1)) * 100;
|
|
74
66
|
setPinPosition(isNaN(snappedPercentage) ? 0 : snappedPercentage);
|
|
75
67
|
if (needMonthUpdate) {
|
|
76
|
-
// Update news for all months (historical and forecast)
|
|
77
68
|
const monthAndYear = getPinMonthAndYear();
|
|
78
69
|
if (monthAndYear) {
|
|
79
70
|
const isNewMonth = monthAndYear !== currPinMonthRef.current;
|
|
80
71
|
if (isNewMonth)
|
|
81
72
|
currPinMonthRef.current = monthAndYear;
|
|
82
|
-
// When immediate (e.g. dragEnd), always notify parent so showFutureOutlook is correct
|
|
83
73
|
if (immediateMonthUpdate) {
|
|
84
74
|
onPinMonthChange?.(monthAndYear);
|
|
85
75
|
}
|
|
@@ -90,11 +80,9 @@ function PinOverlay({ baseChartProps, pinMonth, onPinMonthChange, onPreviewMonth
|
|
|
90
80
|
}
|
|
91
81
|
}
|
|
92
82
|
};
|
|
93
|
-
// Update pin position when pinMonth prop changes
|
|
94
83
|
useEffect(() => {
|
|
95
84
|
if (!pinMonth || !chartData.length)
|
|
96
85
|
return;
|
|
97
|
-
// Find the data point index for the given month
|
|
98
86
|
const dataPointIndex = chartData.findIndex(point => {
|
|
99
87
|
const date = new Date(point.date);
|
|
100
88
|
const month = date.toLocaleDateString('en-US', { month: 'short' });
|
|
@@ -109,57 +97,16 @@ function PinOverlay({ baseChartProps, pinMonth, onPinMonthChange, onPreviewMonth
|
|
|
109
97
|
currPinMonthRef.current = pinMonth;
|
|
110
98
|
}
|
|
111
99
|
}, [pinMonth, chartData]);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
pinContainerRef.current?.getBoundingClientRect() || null;
|
|
118
|
-
},
|
|
119
|
-
onDrag: (delta) => {
|
|
120
|
-
if (pinRef.current) {
|
|
121
|
-
const containerRect = containerRectRef.current;
|
|
122
|
-
if (!containerRect)
|
|
123
|
-
return;
|
|
124
|
-
const pinCurrentLeft = (pinPosRef.current / 100) * containerRect.width;
|
|
125
|
-
const leftLimit = -pinCurrentLeft;
|
|
126
|
-
const rightLimit = containerRect.width - pinCurrentLeft;
|
|
127
|
-
const clampedDeltaX = Math.max(leftLimit, Math.min(rightLimit, delta.x));
|
|
128
|
-
setIsPinAnimating(false);
|
|
129
|
-
pinRef.current.style.transform = `translateX(${clampedDeltaX}px)`;
|
|
130
|
-
// Calculate preview month based on current drag position
|
|
131
|
-
if (onPreviewMonthChange && chartData.length > 0) {
|
|
132
|
-
const newPositionPixels = pinCurrentLeft + clampedDeltaX;
|
|
133
|
-
const newPercentage = (newPositionPixels / containerRect.width) * 100;
|
|
134
|
-
const clampedPercentage = Math.max(0, Math.min(100, newPercentage));
|
|
135
|
-
const previewMonth = getPinMonthAndYear(clampedPercentage);
|
|
136
|
-
if (previewMonth) {
|
|
137
|
-
onPreviewMonthChange(previewMonth);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
onDragEnd: (e) => {
|
|
143
|
-
setIsDraggingPin(false);
|
|
144
|
-
setTimeout(() => setIsPinAnimating(true), 200);
|
|
145
|
-
if (pinRef.current)
|
|
146
|
-
pinRef.current.style.transform = '';
|
|
147
|
-
// Snap first so onPinMonthChange runs before we clear preview (avoids showFutureOutlook fallback)
|
|
148
|
-
snapPinToPosition(e.clientX, true, true);
|
|
149
|
-
if (onPreviewMonthChange) {
|
|
150
|
-
onPreviewMonthChange(undefined);
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
const onPointerDown = (e) => {
|
|
155
|
-
if (e.target.tagName !== 'svg')
|
|
100
|
+
const onChartClick = (e) => {
|
|
101
|
+
if (e.button !== 0)
|
|
102
|
+
return;
|
|
103
|
+
const target = e.target;
|
|
104
|
+
if (!target.closest?.('svg'))
|
|
156
105
|
return;
|
|
157
106
|
snapPinToPosition(e.clientX, true, false);
|
|
158
107
|
};
|
|
159
108
|
const overlay = (jsxs(Fragment, { children: [jsx("div", { className: cn(S.overlay, S$1.pinContainer), ref: pinContainerRef, children: jsx("div", { className: S$1.pinLineBase, style: { left: `${pinPosRef.current}%` }, ref: pinRef, children: jsx("div", { className: S$1.pinButton, "aria-label": "News pin", title: `Current month: ${getPinMonthAndYear() || 'Loading...'}`, children: jsx(ChevronsLeftRight, { className: S$1.pinIcon, "aria-hidden": "true" }) }) }) }), jsx("div", { className: S$1.pinPlaceholder, style: { left: `${pinPosRef.current}%` }, ref: pinPlaceholderRef, onPointerEnter: () => setIsPinHovered(true), onPointerLeave: () => setIsPinHovered(false) })] }));
|
|
160
|
-
return (jsx("div", { className: cn(className, S$1.root,
|
|
161
|
-
// onClick={e => snapPinToPosition(e.clientX)}
|
|
162
|
-
ref: chartRef, children: jsx(BaseChartWrapper, { ...baseChartProps, yMin: yMin, yMax: yMax, autoScaleYAxis: false, overlayElements: overlay }) }));
|
|
109
|
+
return (jsx("div", { className: cn(className, S$1.root, S$1.pinAnimating, isPinHovered && S$1.pinHovered), onClick: onChartClick, ref: chartRef, children: jsx(BaseChartWrapper, { ...baseChartProps, yMin: yMin, yMax: yMax, autoScaleYAxis: false, overlayElements: overlay }) }));
|
|
163
110
|
}
|
|
164
111
|
|
|
165
112
|
export { PinOverlay };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = ".PinOverlay_root__RUZxO{touch-action:pan-y}.PinOverlay_pinContainer__6sc1J{height:calc(var(--chart-height) - 70px);inset:0;margin:var(--p-5) var(--p-8) var(--p-24) var(--p-12);pointer-events:none;position:absolute;touch-action:pan-y;transition:transform .2s ease-out,z-index 0ms;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:2}.PinOverlay_pinHovered__cGPjN .PinOverlay_pinContainer__6sc1J{z-index:10}.PinOverlay_pinLineBase__0BWsD{height:100%;pointer-events:none;position:absolute;width:3px}.PinOverlay_pinLineBase__0BWsD:before{border-radius:50%;box-shadow:0 4px 6px -1px var(--shadow-color);content:\"\";height:36px;left:-16px;position:absolute;top:-8px;width:36px;z-index:-1}.PinOverlay_pinLineBase__0BWsD:after{background-color:var(--background);border-radius:12px;bottom:0;content:\"\";left:0;position:absolute;top:0;transform:translate3d(.5px,0,0);transition:background-color .3s ease-out;width:100%;z-index:1}.
|
|
4
|
-
var S = {"root":"PinOverlay_root__RUZxO","pinContainer":"PinOverlay_pinContainer__6sc1J","pinHovered":"PinOverlay_pinHovered__cGPjN","pinLineBase":"PinOverlay_pinLineBase__0BWsD","
|
|
3
|
+
var css_248z = ".PinOverlay_root__RUZxO{touch-action:pan-y}.PinOverlay_pinContainer__6sc1J{height:calc(var(--chart-height) - 70px);inset:0;margin:var(--p-5) var(--p-8) var(--p-24) var(--p-12);pointer-events:none;position:absolute;touch-action:pan-y;transition:transform .2s ease-out,z-index 0ms;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:2}.PinOverlay_pinHovered__cGPjN .PinOverlay_pinContainer__6sc1J{z-index:10}.PinOverlay_pinLineBase__0BWsD{height:100%;pointer-events:none;position:absolute;width:3px}.PinOverlay_pinLineBase__0BWsD:before{border-radius:50%;box-shadow:0 4px 6px -1px var(--shadow-color);content:\"\";height:36px;left:-16px;position:absolute;top:-8px;width:36px;z-index:-1}.PinOverlay_pinLineBase__0BWsD:after{background-color:var(--background);border-radius:12px;bottom:0;content:\"\";left:0;position:absolute;top:0;transform:translate3d(.5px,0,0);transition:background-color .3s ease-out;width:100%;z-index:1}.PinOverlay_pinAnimating__5XMJG .PinOverlay_pinLineBase__0BWsD{transition:left .3s ease-out}.PinOverlay_pinButton__cnV1K{position:absolute;--offset:-8px;align-items:center;background-color:var(--background);border-radius:9999px;cursor:pointer;display:flex;justify-content:center;left:-8px;left:var(--offset);margin-left:-8px;margin-left:var(--offset);padding:10px;pointer-events:auto;top:-18px;touch-action:none;transition:transform .2s ease-out;-webkit-user-select:none;-moz-user-select:none;user-select:none}.PinOverlay_pinHovered__cGPjN .PinOverlay_pinButton__cnV1K{transform:scale(1.1)}.PinOverlay_pinButton__cnV1K:active{cursor:grabbing}.PinOverlay_pinIcon__s7Ze0{color:var(--foreground);height:16px;width:16px}.PinOverlay_pinPlaceholder__JhKcQ{cursor:pointer;height:72px;margin-left:-40px;margin-top:-40px;pointer-events:auto;position:absolute;top:var(--p-5);touch-action:none;width:72px;z-index:20}.PinOverlay_pinAnimating__5XMJG .PinOverlay_pinPlaceholder__JhKcQ{transition:left .3s ease-out}.PinOverlay_pinPlaceholder__JhKcQ:active{cursor:grabbing}";
|
|
4
|
+
var S = {"root":"PinOverlay_root__RUZxO","pinContainer":"PinOverlay_pinContainer__6sc1J","pinHovered":"PinOverlay_pinHovered__cGPjN","pinLineBase":"PinOverlay_pinLineBase__0BWsD","pinAnimating":"PinOverlay_pinAnimating__5XMJG","pinButton":"PinOverlay_pinButton__cnV1K","pinIcon":"PinOverlay_pinIcon__s7Ze0","pinPlaceholder":"PinOverlay_pinPlaceholder__JhKcQ"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
7
|
export { S as default };
|
|
@@ -70,6 +70,7 @@ function useThresholdButton({ lineRef, overlayContainerRef, initialValue, minVal
|
|
|
70
70
|
onValueChangeThrottled(clampedValue);
|
|
71
71
|
}, [clientYToValue, valueToPercent, minValue, maxValue]);
|
|
72
72
|
const handleDragStart = useCallback((e) => {
|
|
73
|
+
e.stopPropagation();
|
|
73
74
|
initialValueRef.current = initialValue;
|
|
74
75
|
startYRef.current = e.clientY;
|
|
75
76
|
setIsDragging(true);
|
|
@@ -15,11 +15,9 @@ function useChartYRange({ baseChartProps, chartData, disableRescaleWhenQuantileC
|
|
|
15
15
|
Object.entries(point).forEach(([key, value]) => {
|
|
16
16
|
if (key === 'date')
|
|
17
17
|
return;
|
|
18
|
-
// When selectedForecastId is provided, scale
|
|
19
|
-
// (exclude
|
|
18
|
+
// When selectedForecastId is provided, scale from historical + selected forecast + its
|
|
19
|
+
// quantile band (exclude other forecasts and their quantile values only).
|
|
20
20
|
if (forecastId !== undefined) {
|
|
21
|
-
if (key === 'historical')
|
|
22
|
-
return;
|
|
23
21
|
if (key.startsWith('forecast_') && key !== `forecast_${forecastId}`)
|
|
24
22
|
return;
|
|
25
23
|
// Exclude q{quantile}_{otherAnalysisId} - only include selected forecast's quantiles
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
import { applyQuantileBandConeToChartData } from '../../Chart/quantileBandConeChartData.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Hook to transform chart data and create quantile band configuration
|
|
@@ -12,128 +13,29 @@ function useQuantileBands({ color, chartData, selectedForecastId, forecastData,
|
|
|
12
13
|
const forecastDataForSelected = forecastData[selectedForecastId];
|
|
13
14
|
const allQuantilesData = forecastDataForSelected.allQuantiles;
|
|
14
15
|
const clonedData = [...chartData];
|
|
15
|
-
// Get forecast dates to map quantile array indices correctly
|
|
16
16
|
const forecastDates = forecastDataForSelected.dates || [];
|
|
17
17
|
const forecastDatesSet = new Set(forecastDates);
|
|
18
|
-
// Find the last historical point for connection
|
|
19
|
-
const historicalPoints = clonedData.filter(point => point.historical !== undefined);
|
|
20
|
-
const lastHistoricalPoint = historicalPoints.length > 0
|
|
21
|
-
? historicalPoints[historicalPoints.length - 1]
|
|
22
|
-
: null;
|
|
23
|
-
const lastHistoricalDate = lastHistoricalPoint?.date;
|
|
24
|
-
const lastHistoricalValue = lastHistoricalPoint?.historical;
|
|
25
|
-
// Get first forecast date
|
|
26
|
-
const firstForecastDate = forecastDates[0];
|
|
27
|
-
const firstForecastDateObj = firstForecastDate
|
|
28
|
-
? new Date(firstForecastDate)
|
|
29
|
-
: null;
|
|
30
|
-
const lastHistoricalDateObj = lastHistoricalDate
|
|
31
|
-
? new Date(lastHistoricalDate)
|
|
32
|
-
: null;
|
|
33
|
-
// Check if there's a gap between historical and forecast data (forecast starts after historical)
|
|
34
|
-
const hasGap = lastHistoricalDate &&
|
|
35
|
-
firstForecastDate &&
|
|
36
|
-
lastHistoricalValue !== undefined &&
|
|
37
|
-
firstForecastDateObj &&
|
|
38
|
-
lastHistoricalDateObj &&
|
|
39
|
-
firstForecastDateObj.getTime() > lastHistoricalDateObj.getTime();
|
|
40
|
-
// Check if forecast starts before or at last historical point (need bridge point)
|
|
41
|
-
const needsBridgePoint = lastHistoricalDate &&
|
|
42
|
-
firstForecastDate &&
|
|
43
|
-
lastHistoricalValue !== undefined &&
|
|
44
|
-
firstForecastDateObj &&
|
|
45
|
-
lastHistoricalDateObj &&
|
|
46
|
-
firstForecastDateObj.getTime() <= lastHistoricalDateObj.getTime();
|
|
47
|
-
// Find bridge point when forecast starts before or at last historical point
|
|
48
|
-
let bridgePoint = null;
|
|
49
|
-
let pointBeforeForecast = null;
|
|
50
|
-
if (needsBridgePoint && historicalPoints.length > 0) {
|
|
51
|
-
// Find the last historical point before or at the first forecast date
|
|
52
|
-
// If dates are equal, use lastHistoricalPoint; otherwise find the point before
|
|
53
|
-
bridgePoint =
|
|
54
|
-
firstForecastDateObj &&
|
|
55
|
-
lastHistoricalDateObj &&
|
|
56
|
-
firstForecastDateObj.getTime() === lastHistoricalDateObj.getTime()
|
|
57
|
-
? lastHistoricalPoint
|
|
58
|
-
: [...historicalPoints]
|
|
59
|
-
.reverse()
|
|
60
|
-
.find(p => firstForecastDateObj &&
|
|
61
|
-
new Date(p.date).getTime() < firstForecastDateObj.getTime()) || lastHistoricalPoint;
|
|
62
|
-
// Find the actual point BEFORE the first forecast date for connection
|
|
63
|
-
if (firstForecastDateObj) {
|
|
64
|
-
pointBeforeForecast = [...historicalPoints].findLast(p => new Date(p.date).getTime() < firstForecastDateObj.getTime());
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// Create a map from date to quantile array index
|
|
68
18
|
const dateToQuantileIndex = new Map();
|
|
69
19
|
forecastDates.forEach((date, index) => {
|
|
70
20
|
dateToQuantileIndex.set(date, index);
|
|
71
21
|
});
|
|
72
|
-
const
|
|
22
|
+
const withRawBands = clonedData.map(point => {
|
|
73
23
|
const newPoint = { ...point };
|
|
74
|
-
// If there's a gap and this is the last historical point, add band data for connection
|
|
75
|
-
if (hasGap &&
|
|
76
|
-
point.date === lastHistoricalDate &&
|
|
77
|
-
lastHistoricalValue !== undefined) {
|
|
78
|
-
// Set zero-width band at the last historical value for visual connection
|
|
79
|
-
newPoint[bandKey] = [lastHistoricalValue, lastHistoricalValue];
|
|
80
|
-
}
|
|
81
|
-
// If forecast starts before or at last historical point, add bridge point connection
|
|
82
|
-
// Set connection band at the point BEFORE the first forecast date (if exists)
|
|
83
|
-
const isBridgePointDate = needsBridgePoint &&
|
|
84
|
-
bridgePoint &&
|
|
85
|
-
bridgePoint.historical !== undefined &&
|
|
86
|
-
point.date === bridgePoint.date;
|
|
87
|
-
const isPointBeforeForecast = needsBridgePoint &&
|
|
88
|
-
pointBeforeForecast &&
|
|
89
|
-
pointBeforeForecast.historical !== undefined &&
|
|
90
|
-
point.date === pointBeforeForecast.date;
|
|
91
|
-
const isAlsoForecastDate = forecastDatesSet.has(point.date);
|
|
92
|
-
// Set zero-width connection band at the point BEFORE forecast starts
|
|
93
|
-
if (isPointBeforeForecast && !isAlsoForecastDate) {
|
|
94
|
-
newPoint[bandKey] = [
|
|
95
|
-
pointBeforeForecast.historical,
|
|
96
|
-
pointBeforeForecast.historical,
|
|
97
|
-
];
|
|
98
|
-
}
|
|
99
|
-
else if (isBridgePointDate && !isAlsoForecastDate) {
|
|
100
|
-
// Fallback: if no point before forecast, use bridge point
|
|
101
|
-
newPoint[bandKey] = [
|
|
102
|
-
bridgePoint.historical,
|
|
103
|
-
bridgePoint.historical,
|
|
104
|
-
];
|
|
105
|
-
}
|
|
106
|
-
// Only update band data for forecast dates
|
|
107
24
|
if (forecastDatesSet.has(point.date)) {
|
|
108
25
|
const quantileIndex = dateToQuantileIndex.get(point.date);
|
|
109
26
|
if (quantileIndex !== undefined) {
|
|
110
27
|
const bandValues = getBandValues(point.date, quantileIndex, allQuantilesData);
|
|
111
28
|
if (bandValues) {
|
|
112
|
-
|
|
113
|
-
// start the band from the historical value for smooth connection
|
|
114
|
-
const isBridgePointDate = needsBridgePoint &&
|
|
115
|
-
bridgePoint &&
|
|
116
|
-
point.date === bridgePoint.date &&
|
|
117
|
-
bridgePoint.historical !== undefined;
|
|
118
|
-
if (isBridgePointDate && quantileIndex === 0) {
|
|
119
|
-
// Start from historical value, expand to forecast upper bound
|
|
120
|
-
newPoint[bandKey] = [bridgePoint.historical, bandValues[1]];
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
newPoint[bandKey] = bandValues;
|
|
124
|
-
}
|
|
29
|
+
newPoint[bandKey] = bandValues;
|
|
125
30
|
}
|
|
126
31
|
else {
|
|
127
|
-
// Remove band data if values don't exist
|
|
128
32
|
delete newPoint[bandKey];
|
|
129
33
|
}
|
|
130
34
|
}
|
|
131
35
|
}
|
|
132
|
-
// For non-forecast dates, preserve existing band data if it exists
|
|
133
|
-
// This ensures continuity of the band visualization
|
|
134
36
|
return newPoint;
|
|
135
37
|
});
|
|
136
|
-
return
|
|
38
|
+
return applyQuantileBandConeToChartData(withRawBands, bandKey);
|
|
137
39
|
}, [chartData, selectedForecastId, forecastData, bandKey, getBandValues]);
|
|
138
40
|
const quantileBands = useMemo(() => {
|
|
139
41
|
if (!selectedForecastId || !forecastData[selectedForecastId]) {
|
|
@@ -5,9 +5,14 @@ import { ToggleGroup, ToggleGroupItem } from '../ToggleGroup/ToggleGroup.js';
|
|
|
5
5
|
import { TIME_RANGES } from './TimeRangeControls.constants.js';
|
|
6
6
|
import S from './TimeRangeControls.styl.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function toggleValueForTimeRange(timeRange) {
|
|
9
|
+
return TIME_RANGES.includes(timeRange)
|
|
10
|
+
? timeRange
|
|
11
|
+
: '';
|
|
12
|
+
}
|
|
13
|
+
const TimeRangeControls = memo(({ timeRange, onTimeRangeChange, loading }) => (jsx("div", { className: S.timeRangeContainer, children: jsx(ToggleGroup, { type: "single", value: toggleValueForTimeRange(timeRange), onValueChange: onTimeRangeChange, variant: "outline", disabled: loading, className: S.timeRangeToggleGroup, children: TIME_RANGES.map(range => (jsx(ToggleGroupItem, { value: range, className: S.timeRangeToggleItem, children: range }, range))) }) })));
|
|
9
14
|
TimeRangeControls.displayName = 'TimeRangeControls';
|
|
10
|
-
const TimeRangeSelect = memo(({ timeRange, onTimeRangeChange, loading, }) => (jsxs(Select, { value: timeRange, onValueChange: onTimeRangeChange, disabled: loading, children: [jsx(SelectTrigger, { className: S.timeRangeSelectTrigger, size: "sm", "aria-label": "Select a value", children: jsx(SelectValue, { placeholder: "1 year" }) }), jsx(SelectContent, { className: S.timeRangeSelectContent, children: TIME_RANGES.map(range => (jsx(SelectItem, { value: range, className: S.timeRangeSelectItem, children: range }, range))) })] })));
|
|
15
|
+
const TimeRangeSelect = memo(({ timeRange, onTimeRangeChange, loading, }) => (jsxs(Select, { value: toggleValueForTimeRange(timeRange) || undefined, onValueChange: onTimeRangeChange, disabled: loading, children: [jsx(SelectTrigger, { className: S.timeRangeSelectTrigger, size: "sm", "aria-label": "Select a value", children: jsx(SelectValue, { placeholder: "1 year" }) }), jsx(SelectContent, { className: S.timeRangeSelectContent, children: TIME_RANGES.map(range => (jsx(SelectItem, { value: range, className: S.timeRangeSelectItem, children: range }, range))) })] })));
|
|
11
16
|
TimeRangeSelect.displayName = 'TimeRangeSelect';
|
|
12
17
|
|
|
13
18
|
export { TimeRangeControls, TimeRangeSelect };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import S from './WorldMap.styl.js';
|
|
4
|
+
import mapBgUrl from './map.svg.js';
|
|
5
|
+
|
|
6
|
+
function WorldMap({ className }) {
|
|
7
|
+
const src = mapBgUrl;
|
|
8
|
+
return (jsx("img", { alt: "", className: cn(S.worldMap, className), decoding: "async", draggable: false, src: src }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { WorldMap };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import styleInject from 'style-inject';
|
|
2
|
+
|
|
3
|
+
var css_248z = ".WorldMap_worldMap__XTiex{aspect-ratio:623.2/341.276;display:block;height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain;-o-object-position:center;object-position:center;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}";
|
|
4
|
+
var S = {"worldMap":"WorldMap_worldMap__XTiex"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { S as default };
|