@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.
Files changed (137) hide show
  1. package/dist/esm/components/ui/Chart/Chart.js +5 -0
  2. package/dist/esm/components/ui/Chart/components/BaseChartWrapper.js +7 -32
  3. package/dist/esm/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.js +21 -0
  4. package/dist/esm/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.js +7 -0
  5. package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.js +460 -0
  6. package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.styl.js +7 -0
  7. package/dist/esm/components/ui/Chart/lightweight/chartTime.js +16 -0
  8. package/dist/esm/components/ui/Chart/lightweight/lightweightForecastChart.helpers.js +114 -0
  9. package/dist/esm/components/ui/Chart/lightweight/quantileBandCustomSeries.js +147 -0
  10. package/dist/esm/components/ui/Chart/quantileBandConeChartData.js +131 -0
  11. package/dist/esm/components/ui/Chart/tools/chartPlotGeometry.js +65 -0
  12. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +37 -1
  13. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +5 -2
  14. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.js +205 -0
  15. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.js +7 -0
  16. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.js +37 -0
  17. package/dist/esm/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.js +1 -0
  18. package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.js +7 -60
  19. package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.js +2 -2
  20. package/dist/esm/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.js +1 -0
  21. package/dist/esm/components/ui/ChartAreaInteractive/overlays/useChartYRange.js +2 -4
  22. package/dist/esm/components/ui/ChartAreaInteractive/overlays/useQuantileBands.js +4 -102
  23. package/dist/esm/components/ui/TimeRangeControls/TimeRangeControls.js +7 -2
  24. package/dist/esm/components/ui/WorldMap/WorldMap.js +11 -0
  25. package/dist/esm/components/ui/WorldMap/WorldMap.styl.js +7 -0
  26. package/dist/esm/components/widgets/DriverCard/DriverCard.js +89 -0
  27. package/dist/esm/components/widgets/DriverCard/DriverCard.styl.js +7 -0
  28. package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.js +83 -0
  29. package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.styl.js +7 -0
  30. package/dist/esm/components/widgets/DriverCard/driverPerformanceChartData.js +50 -0
  31. package/dist/esm/components/widgets/DriverMap/DriverMap.js +2 -2
  32. package/dist/esm/components/widgets/DriverMap/DriverMap.styl.js +2 -2
  33. package/dist/esm/index.js +4 -2
  34. package/dist/esm/types/src/components/ui/Chart/Chart.d.ts +2 -0
  35. package/dist/esm/types/src/components/ui/Chart/components/BaseChartWrapper.d.ts +2 -1
  36. package/dist/esm/types/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.d.ts +14 -0
  37. package/dist/esm/types/src/components/ui/Chart/lightweight/LightweightForecastChart.d.ts +26 -0
  38. package/dist/esm/types/src/components/ui/Chart/lightweight/chartTime.d.ts +5 -0
  39. package/dist/esm/types/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.d.ts +13 -0
  40. package/dist/esm/types/src/components/ui/Chart/lightweight/quantileBandCustomSeries.d.ts +24 -0
  41. package/dist/esm/types/src/components/ui/Chart/quantileBandConeChartData.d.ts +7 -0
  42. package/dist/esm/types/src/components/ui/Chart/tools/chartPlotGeometry.d.ts +30 -0
  43. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.d.ts +1 -1
  44. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +11 -2
  45. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.d.ts +2 -2
  46. package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.d.ts +15 -0
  47. package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.d.ts +14 -0
  48. package/dist/esm/types/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.d.ts +1 -1
  49. package/dist/esm/types/src/components/ui/Page/PageColumns/PageColumns.d.ts +1 -1
  50. package/dist/esm/types/src/components/ui/TimeRangeControls/TimeRangeControls.d.ts +5 -7
  51. package/dist/esm/types/src/components/ui/WorldMap/WorldMap.d.ts +4 -0
  52. package/dist/esm/types/src/components/ui/WorldMap/index.d.ts +2 -0
  53. package/dist/esm/types/src/components/widgets/DriverCard/DriverCard.d.ts +9 -0
  54. package/dist/esm/types/src/components/widgets/DriverCard/DriverPerformanceChart.d.ts +5 -0
  55. package/dist/esm/types/src/components/widgets/DriverCard/driverPerformanceChartData.d.ts +7 -0
  56. package/dist/esm/types/src/components/widgets/DriverCard/index.d.ts +1 -0
  57. package/dist/esm/types/src/components/widgets/DriverMap/index.d.ts +0 -2
  58. package/dist/esm/types/src/docs/pages/LightweightChartPage.d.ts +1 -0
  59. package/dist/esm/types/src/docs/pages/PageColumnsPage.d.ts +1 -0
  60. package/dist/esm/types/src/docs/pages/WorldMapPage.d.ts +1 -0
  61. package/dist/esm/types/src/index.d.ts +2 -0
  62. package/package.json +3 -2
  63. package/src/components/ui/Chart/Chart.tsx +9 -0
  64. package/src/components/ui/Chart/components/BaseChartWrapper.tsx +8 -41
  65. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl +60 -0
  66. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.d.ts +15 -0
  67. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.tsx +66 -0
  68. package/src/components/ui/Chart/lightweight/LightweightForecastChart.styl +25 -0
  69. package/src/components/ui/Chart/lightweight/LightweightForecastChart.styl.d.ts +11 -0
  70. package/src/components/ui/Chart/lightweight/LightweightForecastChart.tsx +721 -0
  71. package/src/components/ui/Chart/lightweight/chartTime.ts +18 -0
  72. package/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.ts +141 -0
  73. package/src/components/ui/Chart/lightweight/quantileBandCustomSeries.ts +215 -0
  74. package/src/components/ui/Chart/quantileBandConeChartData.ts +171 -0
  75. package/src/components/ui/Chart/tools/chartPlotGeometry.ts +89 -0
  76. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +44 -2
  77. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +14 -1
  78. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.ts +2 -3
  79. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl +21 -0
  80. package/src/components/{widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl.d.ts → ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.d.ts} +3 -3
  81. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.tsx +285 -0
  82. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.ts +55 -0
  83. package/src/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.ts +1 -0
  84. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl +2 -7
  85. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.d.ts +0 -1
  86. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.tsx +7 -71
  87. package/src/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.ts +1 -0
  88. package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.ts +2 -3
  89. package/src/components/ui/ChartAreaInteractive/overlays/useQuantileBands.ts +5 -131
  90. package/src/components/ui/Page/PageColumns/PageColumns.tsx +1 -1
  91. package/src/components/ui/TimeRangeControls/TimeRangeControls.tsx +16 -17
  92. package/src/components/{widgets/DriverMap/MapBackground/MapBackground.styl → ui/WorldMap/WorldMap.styl} +1 -3
  93. package/src/components/{widgets/DriverMap/MapBackground/MapBackground.styl.d.ts → ui/WorldMap/WorldMap.styl.d.ts} +1 -1
  94. package/src/components/ui/WorldMap/WorldMap.tsx +22 -0
  95. package/src/components/ui/WorldMap/index.ts +2 -0
  96. package/src/components/widgets/DriverCard/DriverCard.styl +169 -0
  97. package/src/components/widgets/DriverCard/DriverCard.styl.d.ts +40 -0
  98. package/src/components/widgets/DriverCard/DriverCard.tsx +219 -0
  99. package/src/components/widgets/DriverCard/DriverPerformanceChart.styl +43 -0
  100. package/src/components/widgets/DriverCard/DriverPerformanceChart.styl.d.ts +13 -0
  101. package/src/components/widgets/DriverCard/DriverPerformanceChart.tsx +150 -0
  102. package/src/components/widgets/DriverCard/driverPerformanceChartData.ts +64 -0
  103. package/src/components/widgets/DriverCard/index.ts +1 -0
  104. package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.tsx +0 -2
  105. package/src/components/widgets/DriverMap/DriverMap.styl +6 -1
  106. package/src/components/widgets/DriverMap/DriverMap.styl.d.ts +1 -0
  107. package/src/components/widgets/DriverMap/DriverMap.tsx +2 -4
  108. package/src/components/widgets/DriverMap/driverCategoryIcon.tsx +0 -2
  109. package/src/components/widgets/DriverMap/index.ts +0 -2
  110. package/src/declarations.d.ts +2 -0
  111. package/src/docs/config/webpack.config.js +26 -3
  112. package/src/docs/index.tsx +1 -1
  113. package/src/docs/pages/ChartAreaInteractivePage.tsx +2 -3
  114. package/src/docs/pages/DriverMapPage.tsx +214 -60
  115. package/src/docs/pages/LightweightChartPage.styl +18 -0
  116. package/src/docs/pages/LightweightChartPage.styl.d.ts +10 -0
  117. package/src/docs/pages/LightweightChartPage.tsx +195 -0
  118. package/src/docs/pages/PageColumnsPage.tsx +92 -0
  119. package/src/docs/pages/TimeRangeControlsPage.tsx +2 -3
  120. package/src/docs/pages/WorldMapPage.styl +14 -0
  121. package/src/docs/pages/WorldMapPage.styl.d.ts +8 -0
  122. package/src/docs/pages/WorldMapPage.tsx +26 -0
  123. package/src/docs/registry.ts +19 -1
  124. package/src/index.ts +2 -0
  125. package/dist/esm/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.js +0 -8
  126. package/dist/esm/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl.js +0 -7
  127. package/dist/esm/components/widgets/DriverMap/MapBackground/MapBackground.js +0 -10
  128. package/dist/esm/components/widgets/DriverMap/MapBackground/MapBackground.styl.js +0 -7
  129. package/dist/esm/types/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.d.ts +0 -1
  130. package/dist/esm/types/src/components/widgets/DriverMap/MapBackground/MapBackground.d.ts +0 -1
  131. package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl +0 -24
  132. package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.tsx +0 -11
  133. package/src/components/widgets/DriverMap/MapBackground/MapBackground.tsx +0 -18
  134. /package/dist/esm/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/map.svg.js +0 -0
  135. /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/map.svg +0 -0
  136. /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/mapAspect.mixin.styl +0 -0
  137. /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/mapAspect.mixin.styl.d.ts +0 -0
@@ -0,0 +1,114 @@
1
+ import { ColorType } from 'lightweight-charts';
2
+ import { applyQuantileBandConeToChartData } from '../quantileBandConeChartData.js';
3
+ import { chartDateToUtcTimestamp } from './chartTime.js';
4
+
5
+ function buildLightweightChartOptions(args) {
6
+ const { isDarkTheme, width, height, autoSize = false } = args;
7
+ return {
8
+ autoSize,
9
+ width: Math.max(1, Math.floor(width)),
10
+ height: Math.max(1, Math.floor(height)),
11
+ layout: {
12
+ background: {
13
+ type: ColorType.Solid,
14
+ color: 'transparent',
15
+ },
16
+ textColor: isDarkTheme ? '#d8dee9' : '#191919',
17
+ },
18
+ grid: {
19
+ vertLines: {
20
+ color: isDarkTheme ? '#2f3440' : '#e6edf3',
21
+ },
22
+ horzLines: {
23
+ color: isDarkTheme ? '#2f3440' : '#e6edf3',
24
+ },
25
+ },
26
+ rightPriceScale: {
27
+ borderVisible: false,
28
+ scaleMargins: {
29
+ top: 0.08,
30
+ bottom: 0.08,
31
+ },
32
+ },
33
+ timeScale: {
34
+ borderVisible: false,
35
+ timeVisible: true,
36
+ secondsVisible: false,
37
+ fixLeftEdge: true,
38
+ fixRightEdge: true,
39
+ },
40
+ localization: {
41
+ locale: typeof navigator !== 'undefined' && navigator.language
42
+ ? navigator.language
43
+ : 'en-US',
44
+ },
45
+ };
46
+ }
47
+ function buildHistoricalLineData(rows) {
48
+ const out = [];
49
+ for (const row of rows) {
50
+ const v = row.historical;
51
+ if (typeof v !== 'number' || !Number.isFinite(v))
52
+ continue;
53
+ out.push({
54
+ time: chartDateToUtcTimestamp(row.date),
55
+ value: v,
56
+ });
57
+ }
58
+ out.sort((a, b) => Number(a.time) - Number(b.time));
59
+ return out;
60
+ }
61
+ function buildForecastLineData(rows, forecastKey) {
62
+ const out = [];
63
+ for (const row of rows) {
64
+ const v = row[forecastKey];
65
+ if (typeof v !== 'number' || !Number.isFinite(v))
66
+ continue;
67
+ out.push({
68
+ time: chartDateToUtcTimestamp(row.date),
69
+ value: v,
70
+ });
71
+ }
72
+ out.sort((a, b) => Number(a.time) - Number(b.time));
73
+ return out;
74
+ }
75
+ function buildQuantileBandCustomData(rows, bandKey) {
76
+ const coned = applyQuantileBandConeToChartData(rows, bandKey);
77
+ const out = [];
78
+ for (const row of coned) {
79
+ const v = row[bandKey];
80
+ if (!Array.isArray(v) ||
81
+ v.length !== 2 ||
82
+ typeof v[0] !== 'number' ||
83
+ typeof v[1] !== 'number' ||
84
+ !Number.isFinite(v[0]) ||
85
+ !Number.isFinite(v[1])) {
86
+ continue;
87
+ }
88
+ out.push({
89
+ time: chartDateToUtcTimestamp(row.date),
90
+ lower: v[0],
91
+ upper: v[1],
92
+ });
93
+ }
94
+ out.sort((a, b) => Number(a.time) - Number(b.time));
95
+ return out;
96
+ }
97
+ function findNearestChartRow(rows, time) {
98
+ if (!rows.length)
99
+ return null;
100
+ const target = Number(time) * 1000;
101
+ let best = null;
102
+ let bestDist = Infinity;
103
+ for (const row of rows) {
104
+ const t = Number(chartDateToUtcTimestamp(row.date)) * 1000;
105
+ const dist = Math.abs(t - target);
106
+ if (dist < bestDist) {
107
+ bestDist = dist;
108
+ best = row;
109
+ }
110
+ }
111
+ return best;
112
+ }
113
+
114
+ export { buildForecastLineData, buildHistoricalLineData, buildLightweightChartOptions, buildQuantileBandCustomData, findNearestChartRow };
@@ -0,0 +1,147 @@
1
+ import { customSeriesDefaultOptions } from 'lightweight-charts';
2
+
3
+ /** Catmull–Rom segment chain → cubic Béziers (matches curved line feel). */
4
+ function appendCatmullRomBezierChain(ctx, pts, startWithMoveTo) {
5
+ if (pts.length < 2)
6
+ return;
7
+ if (pts.length === 2) {
8
+ if (startWithMoveTo)
9
+ ctx.moveTo(pts[0].x, pts[0].y);
10
+ ctx.lineTo(pts[1].x, pts[1].y);
11
+ return;
12
+ }
13
+ if (startWithMoveTo) {
14
+ ctx.moveTo(pts[0].x, pts[0].y);
15
+ }
16
+ for (let i = 0; i < pts.length - 1; i += 1) {
17
+ const p0 = pts[Math.max(0, i - 1)];
18
+ const p1 = pts[i];
19
+ const p2 = pts[i + 1];
20
+ const p3 = pts[Math.min(pts.length - 1, i + 2)];
21
+ const cp1x = p1.x + (p2.x - p0.x) / 6;
22
+ const cp1y = p1.y + (p2.y - p0.y) / 6;
23
+ const cp2x = p2.x - (p3.x - p1.x) / 6;
24
+ const cp2y = p2.y - (p3.y - p1.y) / 6;
25
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
26
+ }
27
+ }
28
+ class QuantileBandPaneRenderer {
29
+ getData;
30
+ getStyle;
31
+ isWhitespaceFn;
32
+ constructor(getData, getStyle, isWhitespaceFn) {
33
+ this.getData = getData;
34
+ this.getStyle = getStyle;
35
+ this.isWhitespaceFn = isWhitespaceFn;
36
+ }
37
+ draw(target, priceConverter) {
38
+ const pane = this.getData();
39
+ if (!pane?.bars?.length) {
40
+ return;
41
+ }
42
+ const style = this.getStyle();
43
+ const segments = [];
44
+ for (const bar of pane.bars) {
45
+ const d = bar.originalData;
46
+ if (this.isWhitespaceFn(d)) {
47
+ continue;
48
+ }
49
+ const p = d;
50
+ const yU = priceConverter(p.upper);
51
+ const yL = priceConverter(p.lower);
52
+ if (yU === null || yL === null) {
53
+ continue;
54
+ }
55
+ segments.push({ x: bar.x, yU, yL });
56
+ }
57
+ if (segments.length < 2) {
58
+ return;
59
+ }
60
+ target.useMediaCoordinateSpace(({ context }) => {
61
+ const opacity = typeof style.strokeOpacity === 'number'
62
+ ? style.strokeOpacity
63
+ : undefined;
64
+ const prevAlpha = context.globalAlpha;
65
+ const upper = segments.map(s => ({ x: s.x, y: s.yU }));
66
+ const lowerRev = segments
67
+ .map(s => ({ x: s.x, y: s.yL }))
68
+ .reverse();
69
+ context.beginPath();
70
+ appendCatmullRomBezierChain(context, upper, true);
71
+ const last = segments[segments.length - 1];
72
+ context.lineTo(last.x, last.yL);
73
+ appendCatmullRomBezierChain(context, lowerRev, false);
74
+ context.closePath();
75
+ context.fillStyle = style.fill;
76
+ context.fill();
77
+ const sw = style.strokeWidth;
78
+ if (sw > 0) {
79
+ if (opacity !== undefined)
80
+ context.globalAlpha = opacity;
81
+ context.lineWidth = sw;
82
+ context.strokeStyle = style.stroke ?? style.fill;
83
+ if (style.strokeDasharray) {
84
+ const parts = style.strokeDasharray
85
+ .split(/[\s,]+/)
86
+ .map(Number)
87
+ .filter(n => Number.isFinite(n));
88
+ if (parts.length)
89
+ context.setLineDash(parts);
90
+ else
91
+ context.setLineDash([]);
92
+ }
93
+ else {
94
+ context.setLineDash([]);
95
+ }
96
+ context.stroke();
97
+ context.setLineDash([]);
98
+ }
99
+ context.globalAlpha = prevAlpha;
100
+ });
101
+ }
102
+ }
103
+ class QuantileBandPaneView {
104
+ _paneData;
105
+ _style;
106
+ constructor(initialStyle) {
107
+ this._style = initialStyle;
108
+ }
109
+ updateStyle(style) {
110
+ this._style = style;
111
+ }
112
+ renderer() {
113
+ return new QuantileBandPaneRenderer(() => this._paneData, () => this._style, (d) => this.isWhitespace(d));
114
+ }
115
+ update(paneData, seriesOptions) {
116
+ this._paneData = paneData;
117
+ const c = seriesOptions.color;
118
+ if (typeof c === 'string') {
119
+ this._style = { ...this._style, fill: c };
120
+ }
121
+ }
122
+ priceValueBuilder(plotRow) {
123
+ if (this.isWhitespace(plotRow)) {
124
+ return [];
125
+ }
126
+ const p = plotRow;
127
+ return [p.lower, p.upper, (p.lower + p.upper) / 2];
128
+ }
129
+ isWhitespace(data) {
130
+ const row = data;
131
+ if (typeof row.lower !== 'number' ||
132
+ typeof row.upper !== 'number' ||
133
+ !Number.isFinite(row.lower) ||
134
+ !Number.isFinite(row.upper)) {
135
+ return true;
136
+ }
137
+ return false;
138
+ }
139
+ defaultOptions() {
140
+ return customSeriesDefaultOptions;
141
+ }
142
+ destroy() {
143
+ this._paneData = undefined;
144
+ }
145
+ }
146
+
147
+ export { QuantileBandPaneView };
@@ -0,0 +1,131 @@
1
+ function isFiniteBandTuple(v) {
2
+ return (Array.isArray(v) &&
3
+ v.length === 2 &&
4
+ typeof v[0] === 'number' &&
5
+ typeof v[1] === 'number' &&
6
+ Number.isFinite(v[0]) &&
7
+ Number.isFinite(v[1]));
8
+ }
9
+ /**
10
+ * Matches ChartAreaInteractive `mode="intervals"`: cone / fan chart hand-off — collapse the
11
+ * interval to a point at the historical→forecast boundary, then expand along forecast anchors.
12
+ * (Same rules as `useQuantileBands`, without requiring `ForecastData` — reads tuples from each row.)
13
+ */
14
+ function applyQuantileBandConeToChartData(chartData, bandKey) {
15
+ if (!chartData.length)
16
+ return chartData;
17
+ const originalBandByDate = new Map();
18
+ for (const p of chartData) {
19
+ const t = p[bandKey];
20
+ if (isFiniteBandTuple(t))
21
+ originalBandByDate.set(p.date, [t[0], t[1]]);
22
+ }
23
+ const forecastDateList = [...originalBandByDate.keys()].sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
24
+ if (forecastDateList.length === 0)
25
+ return chartData;
26
+ const forecastDatesSet = new Set(forecastDateList);
27
+ const dateToQuantileIndex = new Map();
28
+ forecastDateList.forEach((d, i) => dateToQuantileIndex.set(d, i));
29
+ const clonedData = [...chartData];
30
+ const historicalPoints = clonedData.filter(p => p.historical !== undefined &&
31
+ p.historical !== null &&
32
+ typeof p.historical === 'number' &&
33
+ Number.isFinite(p.historical));
34
+ const lastHistoricalPoint = historicalPoints.length > 0
35
+ ? historicalPoints[historicalPoints.length - 1]
36
+ : null;
37
+ const lastHistoricalDate = lastHistoricalPoint?.date;
38
+ const lastHistoricalValue = lastHistoricalPoint?.historical;
39
+ const firstForecastDate = forecastDateList[0];
40
+ const firstForecastDateObj = firstForecastDate
41
+ ? new Date(firstForecastDate)
42
+ : null;
43
+ const lastHistoricalDateObj = lastHistoricalDate
44
+ ? new Date(lastHistoricalDate)
45
+ : null;
46
+ const hasGap = !!lastHistoricalDate &&
47
+ !!firstForecastDate &&
48
+ lastHistoricalValue !== undefined &&
49
+ firstForecastDateObj &&
50
+ lastHistoricalDateObj &&
51
+ firstForecastDateObj.getTime() > lastHistoricalDateObj.getTime();
52
+ const needsBridgePoint = !!lastHistoricalDate &&
53
+ !!firstForecastDate &&
54
+ lastHistoricalValue !== undefined &&
55
+ firstForecastDateObj &&
56
+ lastHistoricalDateObj &&
57
+ firstForecastDateObj.getTime() <= lastHistoricalDateObj.getTime();
58
+ let bridgePoint = null;
59
+ let pointBeforeForecast = null;
60
+ if (needsBridgePoint && historicalPoints.length > 0) {
61
+ bridgePoint =
62
+ firstForecastDateObj &&
63
+ lastHistoricalDateObj &&
64
+ firstForecastDateObj.getTime() === lastHistoricalDateObj.getTime()
65
+ ? lastHistoricalPoint
66
+ : [...historicalPoints]
67
+ .reverse()
68
+ .find(p => firstForecastDateObj &&
69
+ new Date(p.date).getTime() < firstForecastDateObj.getTime()) || lastHistoricalPoint;
70
+ if (firstForecastDateObj) {
71
+ pointBeforeForecast =
72
+ historicalPoints.findLast(p => new Date(p.date).getTime() < firstForecastDateObj.getTime()) ?? null;
73
+ }
74
+ }
75
+ return clonedData.map(point => {
76
+ const newPoint = { ...point };
77
+ if (hasGap &&
78
+ point.date === lastHistoricalDate &&
79
+ lastHistoricalValue !== undefined) {
80
+ newPoint[bandKey] = [lastHistoricalValue, lastHistoricalValue];
81
+ }
82
+ const isBridgePointDate = needsBridgePoint &&
83
+ bridgePoint &&
84
+ bridgePoint.historical !== undefined &&
85
+ point.date === bridgePoint.date;
86
+ const isPointBeforeForecast = needsBridgePoint &&
87
+ pointBeforeForecast &&
88
+ pointBeforeForecast.historical !== undefined &&
89
+ point.date === pointBeforeForecast.date;
90
+ const isAlsoForecastDate = forecastDatesSet.has(point.date);
91
+ if (isPointBeforeForecast && !isAlsoForecastDate) {
92
+ newPoint[bandKey] = [
93
+ pointBeforeForecast.historical,
94
+ pointBeforeForecast.historical,
95
+ ];
96
+ }
97
+ else if (isBridgePointDate && !isAlsoForecastDate) {
98
+ newPoint[bandKey] = [
99
+ bridgePoint.historical,
100
+ bridgePoint.historical,
101
+ ];
102
+ }
103
+ if (forecastDatesSet.has(point.date)) {
104
+ const quantileIndex = dateToQuantileIndex.get(point.date);
105
+ if (quantileIndex !== undefined) {
106
+ const bandValues = originalBandByDate.get(point.date);
107
+ if (bandValues) {
108
+ const isBridgePointDateForecast = needsBridgePoint &&
109
+ bridgePoint &&
110
+ point.date === bridgePoint.date &&
111
+ bridgePoint.historical !== undefined;
112
+ if (isBridgePointDateForecast && quantileIndex === 0) {
113
+ newPoint[bandKey] = [
114
+ bridgePoint.historical,
115
+ bandValues[1],
116
+ ];
117
+ }
118
+ else {
119
+ newPoint[bandKey] = bandValues;
120
+ }
121
+ }
122
+ else {
123
+ delete newPoint[bandKey];
124
+ }
125
+ }
126
+ }
127
+ return newPoint;
128
+ });
129
+ }
130
+
131
+ export { applyQuantileBandConeToChartData };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Shared Recharts plot / margin math (DOM-agnostic except measurement entry points).
3
+ * Keeps BaseChartWrapper tooltip clamp and ChartAreaInteractive brush in sync.
4
+ */
5
+ const DEFAULT_CHART_MARGIN = {
6
+ top: 5,
7
+ right: 5,
8
+ bottom: 5,
9
+ left: 5,
10
+ };
11
+ function resolveChartMargin(margin) {
12
+ return {
13
+ top: margin?.top ?? DEFAULT_CHART_MARGIN.top,
14
+ right: margin?.right ?? DEFAULT_CHART_MARGIN.right,
15
+ bottom: margin?.bottom ?? DEFAULT_CHART_MARGIN.bottom,
16
+ left: margin?.left ?? DEFAULT_CHART_MARGIN.left,
17
+ };
18
+ }
19
+ /** Plot box inside `.recharts-wrapper` (Recharts cartesian convention). */
20
+ function getPlotViewBox(wrapper, m) {
21
+ const w = wrapper.clientWidth;
22
+ const h = wrapper.clientHeight;
23
+ return {
24
+ x: m.left,
25
+ y: m.top,
26
+ width: Math.max(0, w - m.left - m.right),
27
+ height: Math.max(0, h - m.top - m.bottom),
28
+ };
29
+ }
30
+ const GRID_BOUNDS_MIN_PX = 2;
31
+ /**
32
+ * Plot area in `host` local px: prefer painted `.recharts-cartesian-grid`, else
33
+ * last `.recharts-wrapper` + margins. One `hostRect` read; grid/wrapper rects as needed.
34
+ */
35
+ function measureHostRelativePlotRect(host, margin) {
36
+ const hostRect = host.getBoundingClientRect();
37
+ const grid = host.querySelector('.recharts-cartesian-grid');
38
+ if (grid) {
39
+ const gr = grid.getBoundingClientRect();
40
+ if (gr.width > GRID_BOUNDS_MIN_PX && gr.height > GRID_BOUNDS_MIN_PX) {
41
+ return {
42
+ left: gr.left - hostRect.left,
43
+ top: gr.top - hostRect.top,
44
+ width: gr.width,
45
+ height: gr.height,
46
+ };
47
+ }
48
+ }
49
+ const wrappers = host.querySelectorAll('.recharts-wrapper');
50
+ const wrapper = wrappers[wrappers.length - 1];
51
+ if (!(wrapper instanceof HTMLElement))
52
+ return null;
53
+ const vb = getPlotViewBox(wrapper, margin);
54
+ if (vb.width <= 0 || vb.height <= 0)
55
+ return null;
56
+ const wrapRect = wrapper.getBoundingClientRect();
57
+ return {
58
+ left: wrapRect.left - hostRect.left + vb.x,
59
+ top: wrapRect.top - hostRect.top + vb.y,
60
+ width: vb.width,
61
+ height: vb.height,
62
+ };
63
+ }
64
+
65
+ export { DEFAULT_CHART_MARGIN, getPlotViewBox, measureHostRelativePlotRect, resolveChartMargin };
@@ -6,6 +6,30 @@ const timeRangeToMonths = {
6
6
  '5y': 60,
7
7
  All: 12,
8
8
  };
9
+ const DRAG_TIME_RANGE_PREFIX = '__drag:';
10
+ function encodeDragTimeRange(a, b) {
11
+ const t0 = Math.min(a.getTime(), b.getTime());
12
+ const t1 = Math.max(a.getTime(), b.getTime());
13
+ return `${DRAG_TIME_RANGE_PREFIX}${t0},${t1}`;
14
+ }
15
+ function parseDragTimeRange(s) {
16
+ if (!s.startsWith(DRAG_TIME_RANGE_PREFIX))
17
+ return null;
18
+ const body = s.slice(DRAG_TIME_RANGE_PREFIX.length);
19
+ const comma = body.indexOf(',');
20
+ if (comma === -1)
21
+ return null;
22
+ const a = Number(body.slice(0, comma));
23
+ const b = Number(body.slice(comma + 1));
24
+ if (!Number.isFinite(a) || !Number.isFinite(b))
25
+ return null;
26
+ const t0 = Math.min(a, b);
27
+ const t1 = Math.max(a, b);
28
+ return { start: new Date(t0), end: new Date(t1) };
29
+ }
30
+ function isTimeRangePreset(s) {
31
+ return Object.prototype.hasOwnProperty.call(timeRangeToMonths, s);
32
+ }
9
33
  function isPlottableNumber(value) {
10
34
  return typeof value === 'number' && Number.isFinite(value);
11
35
  }
@@ -76,8 +100,20 @@ function computeLatestPlottableDate(data, options) {
76
100
  return latest;
77
101
  }
78
102
  const filterDataForTimeRange = (data, currentTimeRange, options) => {
103
+ const dragRange = parseDragTimeRange(currentTimeRange);
104
+ if (dragRange) {
105
+ const { start, end } = dragRange;
106
+ return data.filter(item => {
107
+ if (!item.date)
108
+ return false;
109
+ const d = new Date(item.date);
110
+ return d >= start && d <= end;
111
+ });
112
+ }
79
113
  if (currentTimeRange === 'All')
80
114
  return data;
115
+ if (!isTimeRangePreset(currentTimeRange))
116
+ return data;
81
117
  const latestDate = computeLatestPlottableDate(data, options);
82
118
  // Pre-compute start date based on latest date in data
83
119
  let startDate = null;
@@ -112,4 +148,4 @@ const longDateFormatter = (value) => {
112
148
  });
113
149
  };
114
150
 
115
- export { filterDataForTimeRange, longDateFormatter, shortDateFormatter };
151
+ export { DRAG_TIME_RANGE_PREFIX, encodeDragTimeRange, filterDataForTimeRange, isTimeRangePreset, longDateFormatter, parseDragTimeRange, shortDateFormatter };
@@ -8,6 +8,7 @@ import { ensureChartForecastBridge } from '../../../utils/chartConnectionPoint.j
8
8
  import { CHART_MARGINS } from './ChartAreaInteractive.constants.js';
9
9
  import { filterDataForTimeRange, shortDateFormatter, longDateFormatter } from './ChartAreaInteractive.helpers.js';
10
10
  import S from './ChartAreaInteractive.styl.js';
11
+ import { TimeRangeBrushHost } from './TimeRangeBrushLayer.js';
11
12
  import { PinOverlay } from './overlays/PinOverlay/PinOverlay.js';
12
13
  import { IntervalsOverlay } from './overlays/IntervalsOverlay/IntervalsOverlay.js';
13
14
  import { ThresholdsOverlay } from './overlays/ThresholdsOverlay/ThresholdsOverlay.js';
@@ -15,7 +16,7 @@ import { ThresholdsOverlay } from './overlays/ThresholdsOverlay/ThresholdsOverla
15
16
  const chartConfig = {
16
17
  // Chart now supports light/dark themes with dynamic colors
17
18
  };
18
- function ChartAreaInteractive({ className, chartContainerClassName, legendClassName, xAxisClassName, yAxisClassName, timeRange, onTimeRangeChange, pinMonth, onPinMonthChange, onPreviewMonthChange, chartData, forecastData, error, loading = false, isDarkTheme = false, footerActions, mode, selectedForecast, upperQuantiles, lowerQuantiles, selectedLowerQuantile, selectedUpperQuantile, onLowerQuantileChange, onUpperQuantileChange, lowerThreshold, upperThreshold, onLowerThresholdChange, onUpperThresholdChange, discreteThresholdValues, headerActions, excludeLegendIds, loadingAnalyses, onLegendClick, onAnalysisSelect, disableTimeRangeSelector, forecastLineStyle = 'dashed', selectedAnalysisId, chartRenderId, toggleLegendSeries, ensureAnalysisSeriesVisible, overlayForecastData = {}, hiddenSeries, disableForecastHistoricalBridge, ...restProps }) {
19
+ function ChartAreaInteractive({ className, chartContainerClassName, legendClassName, xAxisClassName, yAxisClassName, timeRange, onTimeRangeChange, pinMonth, onPinMonthChange, onPreviewMonthChange, chartData, forecastData, error, loading = false, isDarkTheme = false, footerActions, mode, selectedForecast, upperQuantiles, lowerQuantiles, selectedLowerQuantile, selectedUpperQuantile, onLowerQuantileChange, onUpperQuantileChange, lowerThreshold, upperThreshold, onLowerThresholdChange, onUpperThresholdChange, discreteThresholdValues, headerActions, excludeLegendIds, loadingAnalyses, onLegendClick, onAnalysisSelect, disableTimeRangeSelector, forecastLineStyle = 'dashed', selectedAnalysisId, chartRenderId, toggleLegendSeries, ensureAnalysisSeriesVisible, overlayForecastData = {}, hiddenSeries, disableForecastHistoricalBridge, overlayElements: overlayElementsProp, ...restProps }) {
19
20
  const seriesHidden = hiddenSeries ?? new Set();
20
21
  const hiddenSeriesRef = useRef(seriesHidden);
21
22
  const prevSelectedAnalysisIdRef = useRef(selectedAnalysisId);
@@ -90,9 +91,11 @@ function ChartAreaInteractive({ className, chartContainerClassName, legendClassN
90
91
  loadingMessage,
91
92
  excludeLegendIds,
92
93
  forecastLineStyle,
94
+ overlayElements: overlayElementsProp,
93
95
  // loadingAnalyses,
94
96
  ...restProps,
95
97
  };
98
+ const brushEnabled = !disableTimeRangeSelector && !loading && bridgedChartData.length > 1;
96
99
  const renderChart = () => {
97
100
  const overlayClassName = cn(chartContainerClassName);
98
101
  switch (mode) {
@@ -106,7 +109,7 @@ function ChartAreaInteractive({ className, chartContainerClassName, legendClassN
106
109
  return (jsx(BaseChartWrapper, { ...baseChartProps, chartClassName: cn(S.chartContainer, chartContainerClassName) }));
107
110
  }
108
111
  };
109
- return (jsxs(InteractionOverlay, { className: cn(className, loading && S.loading), children: [(!disableTimeRangeSelector || headerActions) && (jsxs("div", { className: S.chartHeaderContainer, children: [!disableTimeRangeSelector && (jsx(TimeRangeControls, { timeRange: timeRange, onTimeRangeChange: onTimeRangeChange, loading: loading })), headerActions] })), renderChart()] }));
112
+ return (jsxs(InteractionOverlay, { className: cn(className, loading && S.loading), children: [(!disableTimeRangeSelector || headerActions) && (jsxs("div", { className: S.chartHeaderContainer, children: [!disableTimeRangeSelector && (jsx(TimeRangeControls, { timeRange: timeRange, onTimeRangeChange: onTimeRangeChange, loading: loading })), headerActions] })), jsx(TimeRangeBrushHost, { chartData: bridgedChartData, onTimeRangeChange: onTimeRangeChange, enabled: brushEnabled, layoutKey: chartRenderId ?? null, children: renderChart() })] }));
110
113
  }
111
114
 
112
115
  export { ChartAreaInteractive, chartConfig };