@sybilion/uilib 1.3.22 → 1.3.25

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 (53) hide show
  1. package/dist/esm/components/widgets/DriversComparisonChart/DriversComparisonChart.js +139 -0
  2. package/dist/esm/components/widgets/DriversComparisonChart/DriversComparisonChart.styl.js +7 -0
  3. package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js +159 -0
  4. package/dist/esm/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.js +34 -0
  5. package/dist/esm/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.styl.js +7 -0
  6. package/dist/esm/components/widgets/PerformanceChart/PerformanceChart.constants.js +17 -0
  7. package/dist/esm/components/widgets/PerformanceChart/PerformanceChart.js +807 -0
  8. package/dist/esm/components/widgets/PerformanceChart/PerformanceChart.styl.js +7 -0
  9. package/dist/esm/components/widgets/PerformanceChart/PerformanceTable.js +130 -0
  10. package/dist/esm/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.js +20 -0
  11. package/dist/esm/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.styl.js +7 -0
  12. package/dist/esm/components/widgets/PerformanceChart/performanceChart.helpers.js +591 -0
  13. package/dist/esm/components/widgets/PerformanceChart/performanceChartUserSeries.js +109 -0
  14. package/dist/esm/index.js +6 -0
  15. package/dist/esm/types/src/components/widgets/DriversComparisonChart/DriversComparisonChart.d.ts +18 -0
  16. package/dist/esm/types/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.d.ts +26 -0
  17. package/dist/esm/types/src/components/widgets/DriversComparisonChart/index.d.ts +2 -0
  18. package/dist/esm/types/src/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.d.ts +7 -0
  19. package/dist/esm/types/src/components/widgets/PerformanceChart/PerformanceChart.constants.d.ts +3 -0
  20. package/dist/esm/types/src/components/widgets/PerformanceChart/PerformanceChart.d.ts +54 -0
  21. package/dist/esm/types/src/components/widgets/PerformanceChart/PerformanceTable.d.ts +31 -0
  22. package/dist/esm/types/src/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.d.ts +20 -0
  23. package/dist/esm/types/src/components/widgets/PerformanceChart/index.d.ts +4 -0
  24. package/dist/esm/types/src/components/widgets/PerformanceChart/performanceChart.helpers.d.ts +212 -0
  25. package/dist/esm/types/src/components/widgets/PerformanceChart/performanceChartUserSeries.d.ts +20 -0
  26. package/dist/esm/types/src/docs/pages/DriversComparisonChartPage.d.ts +1 -0
  27. package/dist/esm/types/src/docs/pages/PerformanceChartPage.d.ts +1 -0
  28. package/dist/esm/types/src/index.d.ts +2 -0
  29. package/dist/esm/utils/chartConnectionPoint.js +9 -1
  30. package/package.json +1 -1
  31. package/src/components/widgets/DriversComparisonChart/DriversComparisonChart.styl +145 -0
  32. package/src/components/widgets/DriversComparisonChart/DriversComparisonChart.styl.d.ts +29 -0
  33. package/src/components/widgets/DriversComparisonChart/DriversComparisonChart.tsx +325 -0
  34. package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.ts +206 -0
  35. package/src/components/widgets/DriversComparisonChart/index.ts +13 -0
  36. package/src/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.styl +25 -0
  37. package/src/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.styl.d.ts +11 -0
  38. package/src/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.tsx +67 -0
  39. package/src/components/widgets/PerformanceChart/PerformanceChart.constants.ts +17 -0
  40. package/src/components/widgets/PerformanceChart/PerformanceChart.styl +194 -0
  41. package/src/components/widgets/PerformanceChart/PerformanceChart.styl.d.ts +30 -0
  42. package/src/components/widgets/PerformanceChart/PerformanceChart.tsx +1251 -0
  43. package/src/components/widgets/PerformanceChart/PerformanceTable.tsx +381 -0
  44. package/src/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.styl +49 -0
  45. package/src/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.styl.d.ts +12 -0
  46. package/src/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.tsx +83 -0
  47. package/src/components/widgets/PerformanceChart/index.ts +28 -0
  48. package/src/components/widgets/PerformanceChart/performanceChart.helpers.ts +790 -0
  49. package/src/components/widgets/PerformanceChart/performanceChartUserSeries.ts +149 -0
  50. package/src/docs/pages/DriversComparisonChartPage.tsx +174 -0
  51. package/src/docs/pages/PerformanceChartPage.tsx +211 -0
  52. package/src/docs/registry.ts +12 -0
  53. package/src/index.ts +2 -0
@@ -0,0 +1,139 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import cn from 'classnames';
3
+ import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
4
+ import { ChartEmptyState } from '../../ui/Chart/components/ChartEmptyState/ChartEmptyState.js';
5
+ import { ChartAreaInteractive } from '../../ui/ChartAreaInteractive/ChartAreaInteractive.js';
6
+ import { getForecastColor } from '../../ui/ChartAreaInteractive/ChartLines.js';
7
+ import '../../ui/Page/AppShell/AppShell.js';
8
+ import 'react-router-dom';
9
+ import '../../ui/Sidebar/Sidebar.js';
10
+ import 'lucide-react';
11
+ import '../../ui/Page/Breadcrumbs/Breadcrumbs.styl.js';
12
+ import '../../ui/Page/pageContext.js';
13
+ import '@radix-ui/react-tooltip';
14
+ import '../../ui/Tooltip/Tooltip.styl.js';
15
+ import '../../ui/Page/PageHeader/PageHeader.styl.js';
16
+ import '../../ui/Page/PageEmptyCanvas/PageEmptyCanvas.styl.js';
17
+ import '../../ui/Page/PageContent/PageContent.styl.js';
18
+ import '@homecode/ui';
19
+ import '../../ui/Page/PageScroll/PageScroll.styl.js';
20
+ import { PageXScroll } from '../../ui/Page/PageXScroll/PageXScroll.js';
21
+ import '../../ui/Page/PageFooter/PageFooter.styl.js';
22
+ import '@radix-ui/react-tabs';
23
+ import '../../ui/Tabs/Tabs.styl.js';
24
+ import '../../ui/Page/PageTabs/PageTabs.styl.js';
25
+ import '../../ui/Page/PageColumns/PageColumns.styl.js';
26
+ import '../../ui/Page/SectionHeader/SectionHeader.styl.js';
27
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../../ui/Table/Table.js';
28
+ import { TextShimmer } from '../../ui/TextShimmer/TextShimmer.js';
29
+ import { TIME_RANGES } from '../../ui/TimeRangeControls/TimeRangeControls.constants.js';
30
+ import S from './DriversComparisonChart.styl.js';
31
+ import { mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, DRIVER_FORECAST_ID_BASE, INITIAL_VISIBLE_SERIES_COUNT, buildDriversComparisonChartData, formatSeriesImportance } from './driversComparisonChart.helpers.js';
32
+
33
+ const ALL_TIME_RANGE = TIME_RANGES[TIME_RANGES.length - 1];
34
+ function toForecastDataKey(id) {
35
+ const s = String(id);
36
+ return s.startsWith('forecast_') ? s : `forecast_${id}`;
37
+ }
38
+ function DriversComparisonChart({ payload, datasetHistorical = [], loading = false, chartLoading = false, statusHint = null, statusTone = 'muted', runAnalysisHint = false, timeRange: timeRangeProp, onTimeRangeChange, isDarkTheme = false, className, seriesInitKey, }) {
39
+ const [internalTimeRange, setInternalTimeRange] = useState(ALL_TIME_RANGE);
40
+ const [hiddenSeries, setHiddenSeries] = useState(new Set());
41
+ const timeRange = timeRangeProp ?? internalTimeRange;
42
+ const handleTimeRangeChange = useCallback((val) => {
43
+ if (!val)
44
+ return;
45
+ onTimeRangeChange?.(val);
46
+ if (timeRangeProp === undefined) {
47
+ setInternalTimeRange(val);
48
+ }
49
+ }, [onTimeRangeChange, timeRangeProp]);
50
+ const toggleSeries = useCallback((id) => {
51
+ const key = toForecastDataKey(id);
52
+ setHiddenSeries(prev => {
53
+ const next = new Set(prev);
54
+ if (next.has(key))
55
+ next.delete(key);
56
+ else
57
+ next.add(key);
58
+ return next;
59
+ });
60
+ }, []);
61
+ const showSeries = useCallback((id) => {
62
+ const key = toForecastDataKey(id);
63
+ setHiddenSeries(prev => {
64
+ if (!prev.has(key))
65
+ return prev;
66
+ const next = new Set(prev);
67
+ next.delete(key);
68
+ return next;
69
+ });
70
+ }, []);
71
+ const sortedDriversWithData = useMemo(() => {
72
+ const driversList = payload?.drivers ?? [];
73
+ if (!driversList.length)
74
+ return [];
75
+ return [...driversList]
76
+ .filter(d => d.normalized_series &&
77
+ Object.keys(d.normalized_series).some(key => d.normalized_series[key] != null))
78
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)));
79
+ }, [payload?.drivers]);
80
+ const mergedChartData = useMemo(() => mergeBacktestsChartData(payload), [payload]);
81
+ const mergedWithHistorical = useMemo(() => mergeDatasetHistoricalWithBacktestsChartData(datasetHistorical, mergedChartData), [datasetHistorical, mergedChartData]);
82
+ const chartForecastData = useMemo(() => {
83
+ if (!payload?.target?.normalized_series)
84
+ return [];
85
+ return sortedDriversWithData.map((driver, idx) => ({
86
+ id: DRIVER_FORECAST_ID_BASE + idx,
87
+ name: driver.name || String(driver.id),
88
+ color: getForecastColor(idx + 1),
89
+ }));
90
+ }, [payload?.target?.normalized_series, sortedDriversWithData]);
91
+ const tableSeriesRows = useMemo(() => {
92
+ if (sortedDriversWithData.length === 0)
93
+ return [];
94
+ return sortedDriversWithData.map((driver, idx) => {
95
+ const raw = driver.importance;
96
+ const importance = typeof raw === 'number' && Number.isFinite(raw) ? raw : null;
97
+ return {
98
+ id: DRIVER_FORECAST_ID_BASE + idx,
99
+ name: driver.name || String(driver.id),
100
+ color: getForecastColor(idx + 1),
101
+ importance,
102
+ lag: driver.lag,
103
+ };
104
+ });
105
+ }, [sortedDriversWithData]);
106
+ const seriesInitKeyResolved = seriesInitKey ?? `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`;
107
+ const backtestsSeriesInitKeyRef = useRef('');
108
+ useEffect(() => {
109
+ if (tableSeriesRows.length === 0)
110
+ return;
111
+ const initKey = seriesInitKeyResolved;
112
+ if (backtestsSeriesInitKeyRef.current === initKey)
113
+ return;
114
+ backtestsSeriesInitKeyRef.current = initKey;
115
+ setHiddenSeries(() => {
116
+ const next = new Set();
117
+ tableSeriesRows.forEach((row, idx) => {
118
+ const key = toForecastDataKey(row.id);
119
+ if (idx >= INITIAL_VISIBLE_SERIES_COUNT) {
120
+ next.add(key);
121
+ }
122
+ });
123
+ return next;
124
+ });
125
+ }, [seriesInitKeyResolved, tableSeriesRows]);
126
+ const driversComparisonChartData = useMemo(() => buildDriversComparisonChartData(mergedWithHistorical, datasetHistorical, chartForecastData.map(f => f.id)), [chartForecastData, datasetHistorical, mergedWithHistorical]);
127
+ const showEmptyOverlay = (runAnalysisHint || Boolean(statusHint)) && !loading;
128
+ return (jsxs("div", { className: cn(S.root, className), children: [jsxs("div", { className: cn(S.chartShell, loading && S.chartShellLoading), children: [jsx("div", { className: S.chartSlot, children: jsxs("div", { className: S.chartWithOverlay, children: [jsx("div", { className: cn(S.chartInteractiveLayer, showEmptyOverlay && S.chartInteractiveDimmed), children: jsx(ChartAreaInteractive, { disableHistoricalAnimation: true, disableTimeRangeSelector: true, enableTimeRangeBrush: true, chartContainerClassName: S.chartContainer, chartData: driversComparisonChartData, forecastData: chartForecastData, timeRange: timeRange, onTimeRangeChange: handleTimeRangeChange, pinMonth: undefined, onPinMonthChange: () => { }, isDarkTheme: isDarkTheme, loading: chartLoading, hasCombinedData: mergedWithHistorical.length > 0, forecastLineStyle: "solid", showLegend: false, hiddenSeries: hiddenSeries, toggleLegendSeries: toggleSeries, ensureAnalysisSeriesVisible: showSeries }) }), showEmptyOverlay && (jsx("div", { className: S.chartEmptyOverlay, role: "status", "aria-live": "polite", children: jsx("div", { className: S.chartEmptyBlurb, children: jsx(ChartEmptyState, { variant: "inline", hint: runAnalysisHint
129
+ ? 'Run a completed analysis to load the drivers comparison chart for an analysis.'
130
+ : undefined, status: statusHint ?? undefined, statusTone: statusTone }) }) }))] }) }), loading && (jsx("div", { className: S.loadingLayer, "aria-busy": "true", "aria-live": "polite", children: jsx("div", { className: S.loadingMessage, children: jsx(TextShimmer, { as: "span", className: S.loadingText, children: "Loading drivers comparison\u2026" }) }) }))] }), jsx("div", { className: S.seriesSection, children: tableSeriesRows.length === 0 ? (jsx("div", { className: S.seriesEmptyWrap, children: jsx("div", { className: S.chartEmptyBlurb, children: jsx(ChartEmptyState, { variant: "inline", align: "center", status: "No series" }) }) })) : (jsx("div", { className: S.seriesTableWrapper, children: jsx(PageXScroll, { size: "md", fullWidth: true, innerClassName: S.seriesTableContainer, scrollbarClassName: S.seriesScrollbar, children: jsxs(Table, { withBackground: true, withPaddings: true, className: S.seriesTable, children: [jsx(TableHeader, { children: jsxs(TableRow, { children: [jsx(TableHead, { className: S.seriesColSeries, children: "Driver name" }), jsx(TableHead, { children: "Importance" }), jsx(TableHead, { children: "Lag" })] }) }), jsx(TableBody, { children: tableSeriesRows.map(row => {
131
+ const dataKey = toForecastDataKey(row.id);
132
+ const hidden = hiddenSeries.has(dataKey);
133
+ return (jsxs(TableRow, { className: cn(hidden && S.rowHidden), onClick: () => toggleSeries(row.id), children: [jsx(TableCell, { children: jsxs("span", { className: S.seriesLabel, children: [jsx("span", { className: S.colorSwatch, style: {
134
+ backgroundColor: row.color,
135
+ } }), row.name ?? String(row.id)] }) }), jsx(TableCell, { children: formatSeriesImportance(row.importance) }), jsx(TableCell, { children: row.lag ?? '—' })] }, row.id));
136
+ }) })] }) }) })) })] }));
137
+ }
138
+
139
+ export { DriversComparisonChart };
@@ -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)}}.DriversComparisonChart_root__GLnlF{display:flex;flex-direction:column;gap:var(--p-4);width:100%}.DriversComparisonChart_chartShell__0AzAz{position:relative;width:100%}.DriversComparisonChart_chartShellLoading__wTLod .DriversComparisonChart_chartSlot__WKe6N{opacity:.3;pointer-events:none}.DriversComparisonChart_chartShellLoading__wTLod .recharts-line-dots{display:none}.DriversComparisonChart_chartSlot__WKe6N{width:100%}.DriversComparisonChart_loadingLayer__HlEmA{inset:0;pointer-events:none;position:absolute;z-index:10}.DriversComparisonChart_loadingMessage__Li3S7{color:var(--foreground);display:block;left:0;position:absolute;text-align:center;top:calc(50% - 4em);width:100%;z-index:1}.DriversComparisonChart_loadingText__0y3Ms{backdrop-filter:blur(10px);border-radius:var(--p-4);box-shadow:0 0 0 2px var(--page-color);color:var(--foreground);font-size:16px;font-weight:500;padding:0 var(--p-3)}.DriversComparisonChart_chartWithOverlay__SMToK{position:relative;width:100%}.DriversComparisonChart_chartInteractiveLayer__0aquh{transition:opacity .2s ease-out;width:100%}.DriversComparisonChart_chartInteractiveDimmed__us1dv{opacity:.3;pointer-events:none}.DriversComparisonChart_chartInteractiveDimmed__us1dv .recharts-line-dots{display:none}.DriversComparisonChart_chartEmptyOverlay__eXqzk{align-items:center;display:flex;inset:0;justify-content:center;padding:var(--p-4);pointer-events:none;position:absolute;z-index:6}.DriversComparisonChart_chartEmptyBlurb__Tjy9E{max-width:42rem;padding:0 var(--p-3)}.DriversComparisonChart_seriesEmptyWrap__RyRs5{display:flex;justify-content:center;padding:var(--p-8) var(--p-4)}.DriversComparisonChart_seriesSection__yrf4I{width:100%}.DriversComparisonChart_seriesTableWrapper__oredC{margin:0 calc(var(--page-x-padding)*-1);position:relative}.DriversComparisonChart_seriesTableContainer__dhfXK{overflow-x:auto;position:relative;width:100%}.DriversComparisonChart_seriesScrollbar__2jgk6{bottom:calc(var(--p-7)*-1 + 2px)!important}.DriversComparisonChart_seriesTable__NybPR{max-width:100%;table-layout:auto;width:100%}.DriversComparisonChart_seriesTable__NybPR tr{cursor:pointer;position:relative}.DriversComparisonChart_seriesTable__NybPR td:not(:first-child),.DriversComparisonChart_seriesTable__NybPR th:not(:first-child){text-align:right}.DriversComparisonChart_seriesColSeries__--0mj{text-align:left;vertical-align:middle}.DriversComparisonChart_rowHidden__Fwhna{opacity:.4}.DriversComparisonChart_colorSwatch__o3ZVr{border-radius:2px;display:inline-flex;flex-shrink:0;height:10px;margin-right:var(--p-2);width:10px}.DriversComparisonChart_seriesLabel__Aidof{display:-webkit-box;-webkit-line-clamp:3;max-width:60vw;min-width:0;padding:var(--p-2) 0;-webkit-box-orient:vertical;white-space:break-spaces}.DriversComparisonChart_chartContainer__1eM-O{margin-left:calc(var(--page-x-padding)*-1 + 26px);max-width:calc(100% + 60px);transition:opacity .3s ease-out;width:calc(100% + 60px)}.DriversComparisonChart_chartContainer__1eM-O .recharts-yAxis-tick-labels{display:none}@media (max-width:768px){.DriversComparisonChart_chartContainer__1eM-O{margin-left:calc(var(--page-x-padding)*-1 + 10px);width:calc(100% + 30px)}}";
4
+ var S = {"root":"DriversComparisonChart_root__GLnlF","chartShell":"DriversComparisonChart_chartShell__0AzAz","chartShellLoading":"DriversComparisonChart_chartShellLoading__wTLod","chartSlot":"DriversComparisonChart_chartSlot__WKe6N","loadingLayer":"DriversComparisonChart_loadingLayer__HlEmA","loadingMessage":"DriversComparisonChart_loadingMessage__Li3S7","loadingText":"DriversComparisonChart_loadingText__0y3Ms","chartWithOverlay":"DriversComparisonChart_chartWithOverlay__SMToK","chartInteractiveLayer":"DriversComparisonChart_chartInteractiveLayer__0aquh","chartInteractiveDimmed":"DriversComparisonChart_chartInteractiveDimmed__us1dv","chartEmptyOverlay":"DriversComparisonChart_chartEmptyOverlay__eXqzk","chartEmptyBlurb":"DriversComparisonChart_chartEmptyBlurb__Tjy9E","seriesEmptyWrap":"DriversComparisonChart_seriesEmptyWrap__RyRs5","seriesSection":"DriversComparisonChart_seriesSection__yrf4I","seriesTableWrapper":"DriversComparisonChart_seriesTableWrapper__oredC","seriesTableContainer":"DriversComparisonChart_seriesTableContainer__dhfXK","seriesScrollbar":"DriversComparisonChart_seriesScrollbar__2jgk6","seriesTable":"DriversComparisonChart_seriesTable__NybPR","seriesColSeries":"DriversComparisonChart_seriesColSeries__--0mj","rowHidden":"DriversComparisonChart_rowHidden__Fwhna","colorSwatch":"DriversComparisonChart_colorSwatch__o3ZVr","seriesLabel":"DriversComparisonChart_seriesLabel__Aidof","chartContainer":"DriversComparisonChart_chartContainer__1eM-O"};
5
+ styleInject(css_248z);
6
+
7
+ export { S as default };
@@ -0,0 +1,159 @@
1
+ import { normalizeToMonthStart, getPreviousMonth, getNextMonth } from '../../../utils/chartConnectionPoint.js';
2
+
3
+ const DRIVER_FORECAST_ID_BASE = 8_000_000;
4
+ const INITIAL_VISIBLE_SERIES_COUNT = 3;
5
+ /** Months of historical context before the earliest forecast month (inclusive). */
6
+ const DRIVER_COMPARISON_CHART_LEAD_MONTHS = 6;
7
+ /** Ignore leading months where drivers are numerically zero (API often fills 0; lines look empty until real signal). */
8
+ const DRIVER_FORECAST_NONZERO_EPS = 1e-9;
9
+ function formatSeriesImportance(value) {
10
+ if (value === null)
11
+ return '—';
12
+ return `${value.toFixed(1)}%`;
13
+ }
14
+ function mergeBacktestsChartData(payload) {
15
+ if (!payload?.target?.normalized_series)
16
+ return [];
17
+ const norm = (d) => normalizeToMonthStart(d);
18
+ const map = new Map();
19
+ const targetSeries = payload.target.normalized_series;
20
+ Object.entries(targetSeries).forEach(([dateStr, val]) => {
21
+ if (val === null || val === undefined)
22
+ return;
23
+ const k = norm(dateStr);
24
+ const existing = map.get(k) ?? { date: k };
25
+ existing.historical = val;
26
+ map.set(k, existing);
27
+ });
28
+ if (payload.drivers?.length) {
29
+ const sorted = [...payload.drivers]
30
+ .filter(d => d.normalized_series &&
31
+ Object.keys(d.normalized_series).some(key => d.normalized_series[key] != null))
32
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)));
33
+ sorted.forEach((driver, idx) => {
34
+ const fid = DRIVER_FORECAST_ID_BASE + idx;
35
+ const series = driver.normalized_series;
36
+ Object.entries(series).forEach(([dateStr, val]) => {
37
+ if (val === null || val === undefined)
38
+ return;
39
+ const k = norm(dateStr);
40
+ const existing = map.get(k) ?? { date: k };
41
+ existing[`forecast_${fid}`] = val;
42
+ map.set(k, existing);
43
+ });
44
+ });
45
+ }
46
+ return Array.from(map.values()).sort((a, b) => a.date.localeCompare(b.date));
47
+ }
48
+ /** While drivers comparison is not loaded: dataset historical only. After load: normalized historical from target.normalized_series (see mergeBacktestsChartData) replaces it. */
49
+ function mergeDatasetHistoricalWithBacktestsChartData(datasetHistorical, backtestsMerged) {
50
+ if (backtestsMerged.length === 0) {
51
+ if (datasetHistorical.length === 0)
52
+ return [];
53
+ return datasetHistorical
54
+ .map(p => {
55
+ const k = normalizeToMonthStart(p.date);
56
+ const histVal = p.historical;
57
+ return {
58
+ date: k,
59
+ ...(typeof histVal === 'number' ? { historical: histVal } : {}),
60
+ };
61
+ })
62
+ .sort((a, b) => a.date.localeCompare(b.date));
63
+ }
64
+ return backtestsMerged;
65
+ }
66
+ /** Lower-median of ISO month strings (YYYY-MM-01 sorts lexicographically). */
67
+ function medianSortedIsoMonth(months) {
68
+ const s = [...months].sort((a, b) => a.localeCompare(b));
69
+ return s[Math.floor((s.length - 1) / 2)];
70
+ }
71
+ /**
72
+ * For each driver, first month with materially non-zero value; anchor = median of those months.
73
+ * (Min is too early with outliers; max matched logs where one driver started 2017-12 while most started 2015-01.)
74
+ */
75
+ function getZoomAnchorMonthFromDriverMaterialStarts(points, forecastIds) {
76
+ const perDriverFirstMaterial = [];
77
+ for (const id of forecastIds) {
78
+ let minForId = null;
79
+ for (const p of points) {
80
+ const v = p[`forecast_${id}`];
81
+ if (typeof v !== 'number' ||
82
+ !Number.isFinite(v) ||
83
+ Math.abs(v) <= DRIVER_FORECAST_NONZERO_EPS) {
84
+ continue;
85
+ }
86
+ const d = normalizeToMonthStart(p.date);
87
+ if (minForId === null || d.localeCompare(minForId) < 0)
88
+ minForId = d;
89
+ }
90
+ if (minForId === null) {
91
+ return { anchor: null, perDriverFirstMaterial };
92
+ }
93
+ perDriverFirstMaterial.push(minForId);
94
+ }
95
+ const anchor = medianSortedIsoMonth(perDriverFirstMaterial);
96
+ return { anchor, perDriverFirstMaterial };
97
+ }
98
+ function subtractMonthsFromMonthStart(date, count) {
99
+ let result = date;
100
+ for (let i = 0; i < count; i++) {
101
+ result = getPreviousMonth(result);
102
+ }
103
+ return result;
104
+ }
105
+ /**
106
+ * Backtests payload often starts at the same month for target + drivers, so xMin falls
107
+ * in a range with no rows. Prepend historical-only months from raw dataset series
108
+ * (scaled to the first normalized point) so the lead-in shows the target line alone.
109
+ */
110
+ function prependHistoricalLeadFromDataset(points, datasetHistorical, xMin) {
111
+ if (points.length === 0)
112
+ return points;
113
+ const firstMonth = normalizeToMonthStart(points[0].date);
114
+ if (xMin.localeCompare(firstMonth) >= 0)
115
+ return points;
116
+ const rawByMonth = new Map();
117
+ for (const p of datasetHistorical) {
118
+ const k = normalizeToMonthStart(p.date);
119
+ const v = p.historical;
120
+ if (typeof v === 'number' && Number.isFinite(v))
121
+ rawByMonth.set(k, v);
122
+ }
123
+ const histFirst = points[0].historical;
124
+ const rawFirst = rawByMonth.get(firstMonth);
125
+ if (typeof histFirst !== 'number' ||
126
+ !Number.isFinite(histFirst) ||
127
+ typeof rawFirst !== 'number' ||
128
+ !Number.isFinite(rawFirst) ||
129
+ Math.abs(rawFirst) <= 1e-15) {
130
+ return points;
131
+ }
132
+ const lead = [];
133
+ let m = xMin;
134
+ while (m.localeCompare(firstMonth) < 0) {
135
+ const rawM = rawByMonth.get(m);
136
+ if (typeof rawM === 'number' && Number.isFinite(rawM)) {
137
+ lead.push({
138
+ date: m,
139
+ historical: histFirst * (rawM / rawFirst),
140
+ });
141
+ }
142
+ m = getNextMonth(m);
143
+ }
144
+ return [...lead, ...points];
145
+ }
146
+ function buildDriversComparisonChartData(mergedWithHistorical, datasetHistorical, forecastIds) {
147
+ if (mergedWithHistorical.length === 0)
148
+ return mergedWithHistorical;
149
+ if (forecastIds.length === 0)
150
+ return mergedWithHistorical;
151
+ const { anchor: anchorMonth } = getZoomAnchorMonthFromDriverMaterialStarts(mergedWithHistorical, forecastIds);
152
+ if (anchorMonth === null)
153
+ return mergedWithHistorical;
154
+ const xMin = subtractMonthsFromMonthStart(anchorMonth, DRIVER_COMPARISON_CHART_LEAD_MONTHS);
155
+ const withLead = prependHistoricalLeadFromDataset(mergedWithHistorical, datasetHistorical, xMin);
156
+ return withLead.filter(p => normalizeToMonthStart(p.date) >= xMin);
157
+ }
158
+
159
+ export { DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, INITIAL_VISIBLE_SERIES_COUNT, buildDriversComparisonChartData, formatSeriesImportance, getZoomAnchorMonthFromDriverMaterialStarts, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, prependHistoricalLeadFromDataset, subtractMonthsFromMonthStart };
@@ -0,0 +1,34 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useMemo } from 'react';
3
+ import { Tabs, TabsList, TabsTrigger } from '../../../ui/Tabs/Tabs.js';
4
+ import { useThrottledCallback } from '../../../../hooks/useThrottledCallback.js';
5
+ import S from './HorizonsSelector.styl.js';
6
+
7
+ function HorizonsSelector({ selectedHorizon, onHorizonChange, availableHorizons, }) {
8
+ const horizons = useMemo(() => {
9
+ return availableHorizons
10
+ .sort((a, b) => {
11
+ // Sort by horizon number: horizon_1, horizon_2, etc.
12
+ const numA = parseInt(a.replace('horizon_', ''), 10);
13
+ const numB = parseInt(b.replace('horizon_', ''), 10);
14
+ return numA - numB;
15
+ })
16
+ .map(horizon => {
17
+ const num = horizon.replace('horizon_', '');
18
+ return {
19
+ value: horizon,
20
+ label: num,
21
+ };
22
+ });
23
+ }, [availableHorizons]);
24
+ const onChangeThrottled = useThrottledCallback(onHorizonChange, 300);
25
+ if (horizons.length === 0) {
26
+ return null;
27
+ }
28
+ return (jsxs("div", { className: S.root, children: [jsx("span", { className: S.horizonsLabel, children: "Planning horizon in months:" }), jsx(Tabs, { value: selectedHorizon, onValueChange: onHorizonChange, variant: "button", children: jsx(TabsList, { children: horizons.map(horizon => (jsx(TabsTrigger, { value: horizon.value, onPointerMove: () => {
29
+ if (selectedHorizon !== horizon.value)
30
+ onChangeThrottled(horizon.value);
31
+ }, children: horizon.label }, horizon.value))) }) })] }));
32
+ }
33
+
34
+ export { HorizonsSelector };
@@ -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)}}.HorizonsSelector_root__r5vAR{align-items:center;display:flex;flex-grow:1;gap:var(--p-4);justify-content:flex-end}.HorizonsSelector_horizonsLabel__-OzSL{color:var(--foreground);font-size:14px;font-weight:500}.HorizonsSelector_horizonsButtons__Z0aVg{display:flex;gap:var(--p-2)}.HorizonsSelector_horizonButton__YYpIJ{min-width:40px}.HorizonsSelector_infoIcon__7p03J{font-size:12px;margin-left:4px;opacity:.6}";
4
+ var S = {"root":"HorizonsSelector_root__r5vAR","horizonsLabel":"HorizonsSelector_horizonsLabel__-OzSL","horizonsButtons":"HorizonsSelector_horizonsButtons__Z0aVg","horizonButton":"HorizonsSelector_horizonButton__YYpIJ","infoIcon":"HorizonsSelector_infoIcon__7p03J"};
5
+ styleInject(css_248z);
6
+
7
+ export { S as default };
@@ -0,0 +1,17 @@
1
+ const MONTHS_24 = 24;
2
+ const MONTH_NAMES = [
3
+ 'Jan',
4
+ 'Feb',
5
+ 'Mar',
6
+ 'Apr',
7
+ 'May',
8
+ 'Jun',
9
+ 'Jul',
10
+ 'Aug',
11
+ 'Sep',
12
+ 'Oct',
13
+ 'Nov',
14
+ 'Dec',
15
+ ];
16
+
17
+ export { MONTHS_24, MONTH_NAMES };