@sybilion/uilib 1.2.26 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/esm/components/ui/Chart/Chart.js +1 -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/tools/chartPlotGeometry.js +65 -0
  6. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +37 -1
  7. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +5 -2
  8. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.js +205 -0
  9. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.js +7 -0
  10. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.js +37 -0
  11. package/dist/esm/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.js +1 -0
  12. package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.js +7 -60
  13. package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.js +2 -2
  14. package/dist/esm/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.js +1 -0
  15. package/dist/esm/components/ui/ChartAreaInteractive/overlays/useChartYRange.js +2 -4
  16. package/dist/esm/components/ui/TimeRangeControls/TimeRangeControls.js +7 -2
  17. package/dist/esm/components/ui/WorldMap/WorldMap.js +11 -0
  18. package/dist/esm/components/ui/WorldMap/WorldMap.styl.js +7 -0
  19. package/dist/esm/components/widgets/DriverCard/DriverCard.js +89 -0
  20. package/dist/esm/components/widgets/DriverCard/DriverCard.styl.js +7 -0
  21. package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.js +79 -0
  22. package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.styl.js +7 -0
  23. package/dist/esm/components/widgets/DriverCard/driverPerformanceChartData.js +50 -0
  24. package/dist/esm/components/widgets/DriverMap/DriverMap.js +2 -2
  25. package/dist/esm/components/widgets/DriverMap/DriverMap.styl.js +2 -2
  26. package/dist/esm/index.js +3 -2
  27. package/dist/esm/types/src/components/ui/Chart/Chart.d.ts +1 -0
  28. package/dist/esm/types/src/components/ui/Chart/components/BaseChartWrapper.d.ts +2 -1
  29. package/dist/esm/types/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.d.ts +14 -0
  30. package/dist/esm/types/src/components/ui/Chart/tools/chartPlotGeometry.d.ts +30 -0
  31. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.d.ts +1 -1
  32. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +11 -2
  33. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.d.ts +2 -2
  34. package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.d.ts +15 -0
  35. package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.d.ts +14 -0
  36. package/dist/esm/types/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.d.ts +1 -1
  37. package/dist/esm/types/src/components/ui/Page/PageColumns/PageColumns.d.ts +1 -1
  38. package/dist/esm/types/src/components/ui/TimeRangeControls/TimeRangeControls.d.ts +5 -7
  39. package/dist/esm/types/src/components/ui/WorldMap/WorldMap.d.ts +4 -0
  40. package/dist/esm/types/src/components/ui/WorldMap/index.d.ts +2 -0
  41. package/dist/esm/types/src/components/widgets/DriverCard/DriverCard.d.ts +9 -0
  42. package/dist/esm/types/src/components/widgets/DriverCard/DriverPerformanceChart.d.ts +5 -0
  43. package/dist/esm/types/src/components/widgets/DriverCard/driverPerformanceChartData.d.ts +7 -0
  44. package/dist/esm/types/src/components/widgets/DriverCard/index.d.ts +1 -0
  45. package/dist/esm/types/src/components/widgets/DriverMap/index.d.ts +0 -2
  46. package/dist/esm/types/src/docs/pages/PageColumnsPage.d.ts +1 -0
  47. package/dist/esm/types/src/docs/pages/WorldMapPage.d.ts +1 -0
  48. package/dist/esm/types/src/index.d.ts +2 -0
  49. package/package.json +1 -1
  50. package/src/components/ui/Chart/Chart.tsx +5 -0
  51. package/src/components/ui/Chart/components/BaseChartWrapper.tsx +8 -41
  52. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl +60 -0
  53. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.d.ts +15 -0
  54. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.tsx +66 -0
  55. package/src/components/ui/Chart/tools/chartPlotGeometry.ts +89 -0
  56. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +44 -2
  57. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +14 -1
  58. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.ts +2 -3
  59. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl +21 -0
  60. package/src/components/{widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl.d.ts → ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.d.ts} +3 -3
  61. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.tsx +285 -0
  62. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.ts +55 -0
  63. package/src/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.ts +1 -0
  64. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl +2 -7
  65. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.d.ts +0 -1
  66. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.tsx +7 -71
  67. package/src/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.ts +1 -0
  68. package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.ts +2 -3
  69. package/src/components/ui/Page/PageColumns/PageColumns.tsx +1 -1
  70. package/src/components/ui/TimeRangeControls/TimeRangeControls.tsx +16 -17
  71. package/src/components/{widgets/DriverMap/MapBackground/MapBackground.styl → ui/WorldMap/WorldMap.styl} +1 -3
  72. package/src/components/{widgets/DriverMap/MapBackground/MapBackground.styl.d.ts → ui/WorldMap/WorldMap.styl.d.ts} +1 -1
  73. package/src/components/ui/WorldMap/WorldMap.tsx +22 -0
  74. package/src/components/ui/WorldMap/index.ts +2 -0
  75. package/src/components/widgets/DriverCard/DriverCard.styl +169 -0
  76. package/src/components/widgets/DriverCard/DriverCard.styl.d.ts +40 -0
  77. package/src/components/widgets/DriverCard/DriverCard.tsx +219 -0
  78. package/src/components/widgets/DriverCard/DriverPerformanceChart.styl +43 -0
  79. package/src/components/widgets/DriverCard/DriverPerformanceChart.styl.d.ts +13 -0
  80. package/src/components/widgets/DriverCard/DriverPerformanceChart.tsx +150 -0
  81. package/src/components/widgets/DriverCard/driverPerformanceChartData.ts +64 -0
  82. package/src/components/widgets/DriverCard/index.ts +1 -0
  83. package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.tsx +0 -2
  84. package/src/components/widgets/DriverMap/DriverMap.styl +6 -1
  85. package/src/components/widgets/DriverMap/DriverMap.styl.d.ts +1 -0
  86. package/src/components/widgets/DriverMap/DriverMap.tsx +2 -4
  87. package/src/components/widgets/DriverMap/driverCategoryIcon.tsx +0 -2
  88. package/src/components/widgets/DriverMap/index.ts +0 -2
  89. package/src/docs/config/webpack.config.js +1 -1
  90. package/src/docs/pages/ChartAreaInteractivePage.tsx +2 -3
  91. package/src/docs/pages/DriverMapPage.tsx +214 -60
  92. package/src/docs/pages/PageColumnsPage.tsx +92 -0
  93. package/src/docs/pages/TimeRangeControlsPage.tsx +2 -3
  94. package/src/docs/pages/WorldMapPage.styl +14 -0
  95. package/src/docs/pages/WorldMapPage.styl.d.ts +8 -0
  96. package/src/docs/pages/WorldMapPage.tsx +26 -0
  97. package/src/docs/registry.ts +13 -1
  98. package/src/index.ts +2 -0
  99. package/dist/esm/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.js +0 -8
  100. package/dist/esm/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl.js +0 -7
  101. package/dist/esm/components/widgets/DriverMap/MapBackground/MapBackground.js +0 -10
  102. package/dist/esm/components/widgets/DriverMap/MapBackground/MapBackground.styl.js +0 -7
  103. package/dist/esm/types/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.d.ts +0 -1
  104. package/dist/esm/types/src/components/widgets/DriverMap/MapBackground/MapBackground.d.ts +0 -1
  105. package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl +0 -24
  106. package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.tsx +0 -11
  107. package/src/components/widgets/DriverMap/MapBackground/MapBackground.tsx +0 -18
  108. /package/dist/esm/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/map.svg.js +0 -0
  109. /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/map.svg +0 -0
  110. /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/mapAspect.mixin.styl +0 -0
  111. /package/src/components/{widgets/DriverMap/MapBackground → ui/WorldMap}/mapAspect.mixin.styl.d.ts +0 -0
@@ -16,6 +16,7 @@ import '../TextShimmer/TextShimmer.js';
16
16
  import '@phosphor-icons/react';
17
17
  import '../AnalysesSelector/AnalysesSelector.styl.js';
18
18
  import './components/CustomChartLegend/CustomChartLegend.styl.js';
19
+ import './components/ChartEmptyState/ChartEmptyState.styl.js';
19
20
 
20
21
  const ChartTooltip = RechartsPrimitive.Tooltip;
21
22
  const ChartLegend = RechartsPrimitive.Legend;
@@ -1,46 +1,21 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import cn from 'classnames';
3
3
  import { forwardRef, useState, useRef, useMemo, useEffect } from 'react';
4
- import { ChartTooltip } from '../Chart.js';
5
- import { QuantileBands } from './QuantileBands.js';
6
4
  import { getForecastColor, ChartLines } from '../../ChartAreaInteractive/ChartLines.js';
7
5
  import { Skeleton } from '../../Skeleton/Skeleton.js';
8
6
  import { chartRenderQueue } from '../../../../utils/chartRenderQueue.js';
9
- import { LineChart, ComposedChart } from 'recharts';
7
+ import { Tooltip, LineChart, ComposedChart } from 'recharts';
8
+ import { ChartContainer } from './ChartContainer.js';
9
+ import { ChartTooltipContent } from './ChartTooltipContent.js';
10
+ import { CustomChartLegend } from './CustomChartLegend/CustomChartLegend.js';
11
+ import { QuantileBands } from './QuantileBands.js';
12
+ import { resolveChartMargin, getPlotViewBox } from '../tools/chartPlotGeometry.js';
10
13
  import { formatDate } from '../tools/formatters.js';
11
14
  import S from './BaseChartWrapper.styl.js';
12
15
  import { ChartAxes } from './ChartAxes.js';
13
16
  import { ChartGrid } from './ChartGrid.js';
14
17
  import { LegendSvg } from './LegendSvg/LegendSvg.js';
15
- import { ChartContainer } from './ChartContainer.js';
16
- import { CustomChartLegend } from './CustomChartLegend/CustomChartLegend.js';
17
- import { ChartTooltipContent } from './ChartTooltipContent.js';
18
18
 
19
- const DEFAULT_CHART_MARGIN = {
20
- top: 5,
21
- right: 5,
22
- bottom: 5,
23
- left: 5,
24
- };
25
- function resolveChartMargin(margin) {
26
- return {
27
- top: margin?.top ?? DEFAULT_CHART_MARGIN.top,
28
- right: margin?.right ?? DEFAULT_CHART_MARGIN.right,
29
- bottom: margin?.bottom ?? DEFAULT_CHART_MARGIN.bottom,
30
- left: margin?.left ?? DEFAULT_CHART_MARGIN.left,
31
- };
32
- }
33
- /** Plot box inside `.recharts-wrapper`, same convention as Recharts cartesian viewBox. */
34
- function getPlotViewBox(wrapper, m) {
35
- const w = wrapper.clientWidth;
36
- const h = wrapper.clientHeight;
37
- return {
38
- x: m.left,
39
- y: m.top,
40
- width: Math.max(0, w - m.left - m.right),
41
- height: Math.max(0, h - m.top - m.bottom),
42
- };
43
- }
44
19
  function clampTooltipTranslate(args) {
45
20
  const { coordinate, viewBox, tooltipWidth: tw, tooltipHeight: th, offset, edgeMargin, } = args;
46
21
  const minX = viewBox.x + edgeMargin;
@@ -332,7 +307,7 @@ const BaseChartWrapperContent = forwardRef((props, ref) => {
332
307
  }
333
308
  const ChartComponent = chartType === 'line' ? LineChart : ComposedChart;
334
309
  const defaultLabelFormatter = (v) => formatDateFn(v, true);
335
- return (jsxs("div", { className: cn(S.root, !showLegend && S.noLegend, !showChartAxesLegend && S.hideChartAxesLegend, isLoaded && S.loaded, className), ref: setRefs, children: [loading && (jsx("div", { className: S.loadingOverlay, children: jsx(Skeleton, {}) })), showGrid && (jsx(ChartContainer, { config: chartConfig, className: cn(S.gridLayer, chartClassName), style: height ? { height: `${height}px` } : undefined, children: jsxs(ChartComponent, { data: chartData, margin: margin, children: [jsx(ChartGrid, {}), showAxes && (jsx(ChartAxes, { formatDate: formatDateFn, formatNumber: formatNumber, xAxisClassName: xAxisClassName, yAxisClassName: yAxisClassName, xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax, autoScaleYAxis: effectiveAutoScale }))] }) })), jsx(ChartContainer, { config: chartConfig, className: cn(S.chartLayer, chartClassName), style: height ? { height: `${height}px` } : undefined, ...containerProps, children: jsxs(ChartComponent, { data: chartData, margin: margin, children: [showAxes && (jsx(ChartAxes, { formatDate: formatDateFn, formatNumber: formatNumber, xAxisClassName: cn(xAxisClassName), yAxisClassName: cn(yAxisClassName), xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax, autoScaleYAxis: effectiveAutoScale })), quantileBands?.[0] && (jsx(QuantileBands, { hiddenBands: hiddenSeries, quantileBandKey: quantileBandKey, animate: true, animationDuration: 150, animationBegin: 0, customBands: quantileBands, showLegend: showLegend })), jsx(ChartLines, { historicalLineColor: historicalLineColor, chartData: chartData, forecastData: forecastData, hiddenSeries: hiddenSeries, isDarkTheme: isDarkTheme, shouldAnimate: shouldAnimate, showLegend: showLegend, forecastLineStyle: forecastLineStyle }), showTooltip && (jsx("div", { children: jsx(ChartTooltip, { cursor: false, offset: TOOLTIP_OFFSET, allowEscapeViewBox: { x: false, y: false }, content: renderTooltipContent }) }))] }) }), overlayElements, jsxs("div", { className: cn(S.footer, footerClassName), children: [showLegend &&
310
+ return (jsxs("div", { className: cn(S.root, !showLegend && S.noLegend, !showChartAxesLegend && S.hideChartAxesLegend, isLoaded && S.loaded, className), ref: setRefs, children: [loading && (jsx("div", { className: S.loadingOverlay, children: jsx(Skeleton, {}) })), showGrid && (jsx(ChartContainer, { config: chartConfig, className: cn(S.gridLayer, chartClassName), style: height ? { height: `${height}px` } : undefined, children: jsxs(ChartComponent, { data: chartData, margin: margin, children: [jsx(ChartGrid, {}), showAxes && (jsx(ChartAxes, { formatDate: formatDateFn, formatNumber: formatNumber, xAxisClassName: xAxisClassName, yAxisClassName: yAxisClassName, xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax, autoScaleYAxis: effectiveAutoScale }))] }) })), jsx(ChartContainer, { config: chartConfig, className: cn(S.chartLayer, chartClassName), style: height ? { height: `${height}px` } : undefined, ...containerProps, children: jsxs(ChartComponent, { data: chartData, margin: margin, children: [showAxes && (jsx(ChartAxes, { formatDate: formatDateFn, formatNumber: formatNumber, xAxisClassName: cn(xAxisClassName), yAxisClassName: cn(yAxisClassName), xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax, autoScaleYAxis: effectiveAutoScale })), quantileBands?.[0] && (jsx(QuantileBands, { hiddenBands: hiddenSeries, quantileBandKey: quantileBandKey, animate: true, animationDuration: 150, animationBegin: 0, customBands: quantileBands, showLegend: showLegend })), jsx(ChartLines, { historicalLineColor: historicalLineColor, chartData: chartData, forecastData: forecastData, hiddenSeries: hiddenSeries, isDarkTheme: isDarkTheme, shouldAnimate: shouldAnimate, showLegend: showLegend, forecastLineStyle: forecastLineStyle }), showTooltip && (jsx("div", { children: jsx(Tooltip, { cursor: false, offset: TOOLTIP_OFFSET, allowEscapeViewBox: { x: false, y: false }, content: renderTooltipContent }) }))] }) }), overlayElements, jsxs("div", { className: cn(S.footer, footerClassName), children: [showLegend &&
336
311
  (legendVariant === 'svg' ? (jsx(LegendSvg, { payload: legendPayload.map(p => ({
337
312
  value: p.value,
338
313
  color: p.color,
@@ -0,0 +1,21 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import cn from 'classnames';
3
+ import S from './ChartEmptyState.styl.js';
4
+
5
+ function isNonEmpty(node) {
6
+ if (node == null || node === false)
7
+ return false;
8
+ if (typeof node === 'string')
9
+ return node.trim().length > 0;
10
+ return true;
11
+ }
12
+ function ChartEmptyState({ className, hint, status, statusTone = 'muted', variant = 'panel', align = 'center', }) {
13
+ const hasHint = isNonEmpty(hint);
14
+ const hasStatus = isNonEmpty(status);
15
+ if (!hasHint && !hasStatus) {
16
+ return null;
17
+ }
18
+ return (jsxs("div", { className: cn(S.root, variant === 'panel' && S.rootPanel, variant === 'inline' && S.rootInline, align === 'start' && S.rootAlignStart, align === 'center' && S.rootAlignCenter, className), children: [hasHint && jsx("div", { className: S.hint, children: hint }), hasStatus && (jsx("div", { className: cn(S.status, statusTone === 'destructive' && S.statusDestructive, statusTone === 'muted' && S.statusMuted), children: status }))] }));
19
+ }
20
+
21
+ export { ChartEmptyState };
@@ -0,0 +1,7 @@
1
+ import styleInject from 'style-inject';
2
+
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.ChartEmptyState_root__-Dzme{backdrop-filter:blur(4px);border-radius:var(--p-4);box-shadow:0 0 0 2px var(--page-color);display:flex;flex-direction:column;gap:var(--p-1);max-width:42rem;padding:0 var(--p-3);pointer-events:auto}.ChartEmptyState_rootPanel__hiHyM{background:var(--muted)/.35;border-radius:var(--radius-md);justify-content:center;min-height:var(--chart-height);padding:var(--p-6);width:100%}.ChartEmptyState_rootInline__NV7yu{max-width:100%;width:-moz-fit-content;width:fit-content}.ChartEmptyState_rootAlignCenter__9LySU{align-items:center;text-align:center}.ChartEmptyState_rootAlignStart__fA7lc{align-items:flex-start;text-align:left}.ChartEmptyState_hint__9PSr1{color:var(--muted-foreground);font-size:14px;font-weight:400;line-height:1.5;margin:0}.ChartEmptyState_hint__9PSr1 a{color:var(--sb-cyan-400);font-weight:500;text-decoration:underline}.ChartEmptyState_hint__9PSr1 a:hover{opacity:.9}.ChartEmptyState_status__sR1qq{font-size:14px;font-weight:400;line-height:1.5;margin:0}.ChartEmptyState_statusMuted__h0nek{color:var(--muted-foreground)}.ChartEmptyState_statusDestructive__cMCuF{color:var(--destructive)}";
4
+ var S = {"root":"ChartEmptyState_root__-Dzme","rootPanel":"ChartEmptyState_rootPanel__hiHyM","rootInline":"ChartEmptyState_rootInline__NV7yu","rootAlignCenter":"ChartEmptyState_rootAlignCenter__9LySU","rootAlignStart":"ChartEmptyState_rootAlignStart__fA7lc","hint":"ChartEmptyState_hint__9PSr1","status":"ChartEmptyState_status__sR1qq","statusMuted":"ChartEmptyState_statusMuted__h0nek","statusDestructive":"ChartEmptyState_statusDestructive__cMCuF"};
5
+ styleInject(css_248z);
6
+
7
+ export { S as default };
@@ -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 };
@@ -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';