@sybilion/uilib 1.3.53 → 1.3.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/ui/Card/Card.js +6 -6
- package/dist/esm/components/ui/Dialog/Dialog.js +2 -2
- package/dist/esm/components/widgets/DriversComparisonChart/DriversComparisonChart.js +27 -16
- package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js +91 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/types/src/components/ui/Card/Card.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Card/Card.types.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Dialog/Dialog.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Dialog/Dialog.types.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriversComparisonChart/DriversComparisonChart.d.ts +5 -1
- package/dist/esm/types/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.d.ts +10 -0
- package/dist/esm/types/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriversComparisonChart/index.d.ts +2 -2
- package/package.json +1 -1
- package/src/components/ui/Card/Card.tsx +13 -4
- package/src/components/ui/Card/Card.types.ts +1 -0
- package/src/components/ui/Dialog/Dialog.tsx +2 -0
- package/src/components/ui/Dialog/Dialog.types.ts +1 -0
- package/src/components/widgets/DriversComparisonChart/AGENT.md +6 -1
- package/src/components/widgets/DriversComparisonChart/DriversComparisonChart.tsx +83 -13
- package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.ts +135 -0
- package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.ts +108 -0
- package/src/components/widgets/DriversComparisonChart/index.ts +7 -0
- package/src/docs/pages/DriversComparisonChartPage.tsx +33 -3
|
@@ -24,12 +24,12 @@ function CardDescription({ className, ...props }) {
|
|
|
24
24
|
function CardAction({ className, ...props }) {
|
|
25
25
|
return (jsx("div", { "data-slot": "card-action", className: cn(S.action, className), ...props }));
|
|
26
26
|
}
|
|
27
|
-
function CardContent({ className, centered, fullHeight, noScroll, children, autoScrollBottom, ...props }) {
|
|
28
|
-
const
|
|
27
|
+
function CardContent({ className, centered, fullHeight, noScroll, children, autoScrollBottom, yScrollbarClassName, ...props }) {
|
|
28
|
+
const scrollInnerRef = useRef(null);
|
|
29
29
|
const contentDiv = (jsx("div", { "data-slot": "card-content", className: cn(className, centered && S.centered, fullHeight && S.fullHeight), ...props, children: children }));
|
|
30
30
|
useEffect(() => {
|
|
31
31
|
if (!noScroll && autoScrollBottom) {
|
|
32
|
-
const inner =
|
|
32
|
+
const inner = scrollInnerRef.current;
|
|
33
33
|
if (!inner)
|
|
34
34
|
return;
|
|
35
35
|
if (inner.scrollHeight > SCROLL_OFFSET) {
|
|
@@ -46,9 +46,9 @@ function CardContent({ className, centered, fullHeight, noScroll, children, auto
|
|
|
46
46
|
}, [noScroll, autoScrollBottom]);
|
|
47
47
|
if (noScroll)
|
|
48
48
|
return contentDiv;
|
|
49
|
-
return (
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
return (jsx(Scroll, { y: true, autoHide: true, fadeSize: "m", className: S.scroll, yScrollbarClassName: yScrollbarClassName, onInnerRef: el => {
|
|
50
|
+
scrollInnerRef.current = el;
|
|
51
|
+
}, children: contentDiv }));
|
|
52
52
|
}
|
|
53
53
|
function CardFooter({ className, ...props }) {
|
|
54
54
|
return (jsx("div", { "data-slot": "card-footer", className: cn(S.footer, className), ...props }));
|
|
@@ -7,7 +7,7 @@ import { APP_MODAL_ID } from '../../../constants/appMount.js';
|
|
|
7
7
|
import { XIcon } from '@phosphor-icons/react';
|
|
8
8
|
import S from './Dialog.styl.js';
|
|
9
9
|
|
|
10
|
-
function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disableCloseButton, icon, content, contentClassName, footer, footerClassName, footerAlignment = 'center', size = 'default', className, noScroll, maxHeight, autoScrollBottom, width, }) {
|
|
10
|
+
function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disableCloseButton, icon, content, contentClassName, contentScrollbarClassName, footer, footerClassName, footerAlignment = 'center', size = 'default', className, noScroll, maxHeight, autoScrollBottom, width, }) {
|
|
11
11
|
const [isMounted, setIsMounted] = useState(false);
|
|
12
12
|
const [modalContainer, setModalContainer] = useState(null);
|
|
13
13
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
@@ -123,7 +123,7 @@ function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disabl
|
|
|
123
123
|
maxHeight: maxHeight
|
|
124
124
|
? `min(${maxHeight}, calc(100% - var(--p-4)))`
|
|
125
125
|
: null,
|
|
126
|
-
}, children: [(title || subtitle || icon) && (jsx(CardHeader, { icon: icon, title: title, description: subtitle })), !disableCloseButton && (jsx("button", { className: S.dialogClose, onClick: handleCloseClick, "aria-label": "Close dialog", type: "button", children: jsx(XIcon, { size: 16 }) })), content && (jsx(CardContent, { className: contentClassName, noScroll: noScroll, autoScrollBottom: autoScrollBottom, children: content })), footer && (jsx(CardFooter, { className: cn(S.footer, S[`align-${footerAlignment}`], footerClassName), children: footer }))] })] }));
|
|
126
|
+
}, children: [(title || subtitle || icon) && (jsx(CardHeader, { icon: icon, title: title, description: subtitle })), !disableCloseButton && (jsx("button", { className: S.dialogClose, onClick: handleCloseClick, "aria-label": "Close dialog", type: "button", children: jsx(XIcon, { size: 16 }) })), content && (jsx(CardContent, { className: contentClassName, yScrollbarClassName: contentScrollbarClassName, noScroll: noScroll, autoScrollBottom: autoScrollBottom, children: content })), footer && (jsx(CardFooter, { className: cn(S.footer, S[`align-${footerAlignment}`], footerClassName), children: footer }))] })] }));
|
|
127
127
|
const onTriggerClick = !disabled ? () => onOpenChange(true) : undefined;
|
|
128
128
|
return (jsxs(Fragment, { children: [trigger && jsx("div", { onClick: onTriggerClick, children: trigger }), open &&
|
|
129
129
|
!disabled &&
|
|
@@ -7,10 +7,10 @@ import { getForecastColor } from '../../ui/ChartAreaInteractive/ChartLines.js';
|
|
|
7
7
|
import '../../ui/Page/AppShell/AppShell.js';
|
|
8
8
|
import 'react-router-dom';
|
|
9
9
|
import '../../ui/Sidebar/Sidebar.js';
|
|
10
|
-
import 'lucide-react';
|
|
10
|
+
import { InfoIcon } from 'lucide-react';
|
|
11
11
|
import '../../ui/Page/Breadcrumbs/Breadcrumbs.styl.js';
|
|
12
12
|
import '../../ui/Page/pageContext.js';
|
|
13
|
-
import '../../ui/Tooltip/Tooltip.js';
|
|
13
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from '../../ui/Tooltip/Tooltip.js';
|
|
14
14
|
import '../../ui/Page/PageHeader/PageHeader.styl.js';
|
|
15
15
|
import '../../ui/Page/PageEmptyCanvas/PageEmptyCanvas.styl.js';
|
|
16
16
|
import '../../ui/Page/PageContent/PageContent.styl.js';
|
|
@@ -24,20 +24,22 @@ import '../../ui/Tabs/Tabs.styl.js';
|
|
|
24
24
|
import '../../ui/Page/PageTabs/PageTabs.styl.js';
|
|
25
25
|
import '../../ui/Page/PageColumns/PageColumns.styl.js';
|
|
26
26
|
import '../../ui/Page/SectionHeader/SectionHeader.styl.js';
|
|
27
|
-
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../../ui/Table/Table.js';
|
|
27
|
+
import { Table, TableHeader, TableRow, TableHead, TableCellValue, TableBody, TableCell } from '../../ui/Table/Table.js';
|
|
28
28
|
import { TextShimmer } from '../../ui/TextShimmer/TextShimmer.js';
|
|
29
29
|
import { TIME_RANGES } from '../../ui/TimeRangeControls/TimeRangeControls.constants.js';
|
|
30
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';
|
|
31
|
+
import { applyDriversComparisonViewToPayload, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, DRIVER_FORECAST_ID_BASE, getLagDisplayForView, resolveDriverLagLabel, INITIAL_VISIBLE_SERIES_COUNT, buildDriversComparisonChartData, formatSeriesImportance } from './driversComparisonChart.helpers.js';
|
|
32
32
|
|
|
33
33
|
const ALL_TIME_RANGE = TIME_RANGES[TIME_RANGES.length - 1];
|
|
34
34
|
function toForecastDataKey(id) {
|
|
35
35
|
const s = String(id);
|
|
36
36
|
return s.startsWith('forecast_') ? s : `forecast_${id}`;
|
|
37
37
|
}
|
|
38
|
-
function DriversComparisonChart({ payload, datasetHistorical = [], loading = false, chartLoading = false, statusHint = null, statusTone = 'muted', runAnalysisHint = false, timeRange: timeRangeProp, onTimeRangeChange, isDarkTheme = false, className, seriesInitKey, }) {
|
|
38
|
+
function DriversComparisonChart({ payload, datasetHistorical = [], loading = false, chartLoading = false, statusHint = null, statusTone = 'muted', runAnalysisHint = false, timeRange: timeRangeProp, onTimeRangeChange, isDarkTheme = false, className, seriesInitKey, viewTab: viewTabProp, onViewTabChange, }) {
|
|
39
39
|
const [internalTimeRange, setInternalTimeRange] = useState(ALL_TIME_RANGE);
|
|
40
|
+
const [internalViewTab, setInternalViewTab] = useState('overlapped');
|
|
40
41
|
const [hiddenSeries, setHiddenSeries] = useState(new Set());
|
|
42
|
+
const viewTab = viewTabProp ?? internalViewTab;
|
|
41
43
|
const timeRange = timeRangeProp ?? internalTimeRange;
|
|
42
44
|
const handleTimeRangeChange = useCallback((val) => {
|
|
43
45
|
if (!val)
|
|
@@ -68,42 +70,51 @@ function DriversComparisonChart({ payload, datasetHistorical = [], loading = fal
|
|
|
68
70
|
return next;
|
|
69
71
|
});
|
|
70
72
|
}, []);
|
|
73
|
+
const payloadForView = useMemo(() => applyDriversComparisonViewToPayload(payload, viewTab), [payload, viewTab]);
|
|
71
74
|
const sortedDriversWithData = useMemo(() => {
|
|
72
|
-
const driversList =
|
|
75
|
+
const driversList = payloadForView?.drivers ?? [];
|
|
73
76
|
if (!driversList.length)
|
|
74
77
|
return [];
|
|
75
78
|
return [...driversList]
|
|
76
79
|
.filter(d => d.normalized_series &&
|
|
77
80
|
Object.keys(d.normalized_series).some(key => d.normalized_series[key] != null))
|
|
78
81
|
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
79
|
-
}, [
|
|
80
|
-
const mergedChartData = useMemo(() => mergeBacktestsChartData(
|
|
82
|
+
}, [payloadForView?.drivers]);
|
|
83
|
+
const mergedChartData = useMemo(() => mergeBacktestsChartData(payloadForView), [payloadForView]);
|
|
81
84
|
const mergedWithHistorical = useMemo(() => mergeDatasetHistoricalWithBacktestsChartData(datasetHistorical, mergedChartData), [datasetHistorical, mergedChartData]);
|
|
82
85
|
const chartForecastData = useMemo(() => {
|
|
83
|
-
if (!
|
|
86
|
+
if (!payloadForView?.target?.normalized_series)
|
|
84
87
|
return [];
|
|
85
88
|
return sortedDriversWithData.map((driver, idx) => ({
|
|
86
89
|
id: DRIVER_FORECAST_ID_BASE + idx,
|
|
87
90
|
name: driver.name || String(driver.id),
|
|
88
91
|
color: getForecastColor(idx + 1),
|
|
89
92
|
}));
|
|
90
|
-
}, [
|
|
93
|
+
}, [payloadForView?.target?.normalized_series, sortedDriversWithData]);
|
|
94
|
+
const originalDriversById = useMemo(() => {
|
|
95
|
+
const map = new Map();
|
|
96
|
+
for (const driver of payload?.drivers ?? []) {
|
|
97
|
+
map.set(String(driver.id), driver);
|
|
98
|
+
}
|
|
99
|
+
return map;
|
|
100
|
+
}, [payload?.drivers]);
|
|
91
101
|
const tableSeriesRows = useMemo(() => {
|
|
92
102
|
if (sortedDriversWithData.length === 0)
|
|
93
103
|
return [];
|
|
94
104
|
return sortedDriversWithData.map((driver, idx) => {
|
|
95
105
|
const raw = driver.importance;
|
|
96
106
|
const importance = typeof raw === 'number' && Number.isFinite(raw) ? raw : null;
|
|
107
|
+
const lagSource = originalDriversById.get(String(driver.id)) ?? driver;
|
|
97
108
|
return {
|
|
98
109
|
id: DRIVER_FORECAST_ID_BASE + idx,
|
|
99
110
|
name: driver.name || String(driver.id),
|
|
100
111
|
color: getForecastColor(idx + 1),
|
|
101
112
|
importance,
|
|
102
|
-
lag:
|
|
113
|
+
lag: getLagDisplayForView(resolveDriverLagLabel(lagSource), viewTab),
|
|
103
114
|
};
|
|
104
115
|
});
|
|
105
|
-
}, [sortedDriversWithData]);
|
|
106
|
-
const seriesInitKeyResolved = seriesInitKey ?? `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`;
|
|
116
|
+
}, [originalDriversById, sortedDriversWithData, viewTab]);
|
|
117
|
+
const seriesInitKeyResolved = `${seriesInitKey ?? `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`}:${viewTab}`;
|
|
107
118
|
const backtestsSeriesInitKeyRef = useRef('');
|
|
108
119
|
useEffect(() => {
|
|
109
120
|
if (tableSeriesRows.length === 0)
|
|
@@ -125,14 +136,14 @@ function DriversComparisonChart({ payload, datasetHistorical = [], loading = fal
|
|
|
125
136
|
}, [seriesInitKeyResolved, tableSeriesRows]);
|
|
126
137
|
const driversComparisonChartData = useMemo(() => buildDriversComparisonChartData(mergedWithHistorical, datasetHistorical, chartForecastData.map(f => f.id)), [chartForecastData, datasetHistorical, mergedWithHistorical]);
|
|
127
138
|
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
|
|
139
|
+
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, { chartRenderId: `drivers-comparison-${viewTab}`, 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 }, viewTab) }), 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
140
|
? '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 => {
|
|
141
|
+
: 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: jsxs(TableCellValue, { children: ["Importance", jsxs(Tooltip, { children: [jsx(TooltipTrigger, { asChild: true, children: jsx("span", { children: jsx(InfoIcon, { size: 16, style: { cursor: 'help' } }) }) }), jsx(TooltipContent, { side: "top", maxWidth: 300, children: "How much this driver contributes to price movements in the forecast model. Higher = stronger influence. Reflects relative weight of each driver, scored from 0% to 100%." })] })] }) }), jsx(TableHead, { children: jsxs(TableCellValue, { children: ["Lag", jsxs(Tooltip, { children: [jsx(TooltipTrigger, { asChild: true, children: jsx("span", { children: jsx(InfoIcon, { size: 16, style: { cursor: 'help' } }) }) }), jsx(TooltipContent, { side: "top", maxWidth: 300, children: "Lag shows how far ahead this driver predicts price movement. A range (e.g. 9\u201312 months) means the predictive signal is strongest somewhere in that window \u2014 not at a single fixed point." })] })] }) })] }) }), jsx(TableBody, { children: tableSeriesRows.map(row => {
|
|
131
142
|
const dataKey = toForecastDataKey(row.id);
|
|
132
143
|
const hidden = hiddenSeries.has(dataKey);
|
|
133
144
|
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
145
|
backgroundColor: row.color,
|
|
135
|
-
} }), row.name ?? String(row.id)] }) }), jsx(TableCell, { children: formatSeriesImportance(row.importance) }), jsx(TableCell, { children: row.lag
|
|
146
|
+
} }), row.name ?? String(row.id)] }) }), jsx(TableCell, { children: formatSeriesImportance(row.importance) }), jsx(TableCell, { children: row.lag })] }, row.id));
|
|
136
147
|
}) })] }) }) })) })] }));
|
|
137
148
|
}
|
|
138
149
|
|
package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeToMonthStart,
|
|
1
|
+
import { normalizeToMonthStart, getNextMonth, getPreviousMonth } from '../../../utils/chartConnectionPoint.js';
|
|
2
2
|
|
|
3
3
|
const DRIVER_FORECAST_ID_BASE = 8_000_000;
|
|
4
4
|
const INITIAL_VISIBLE_SERIES_COUNT = 3;
|
|
@@ -11,6 +11,95 @@ function formatSeriesImportance(value) {
|
|
|
11
11
|
return '—';
|
|
12
12
|
return `${value.toFixed(1)}%`;
|
|
13
13
|
}
|
|
14
|
+
function resolveDriverLagLabel(driver) {
|
|
15
|
+
if (typeof driver.lag === 'string' && driver.lag.trim()) {
|
|
16
|
+
return driver.lag;
|
|
17
|
+
}
|
|
18
|
+
const raw = driver;
|
|
19
|
+
const overallLag = raw.overall_lag ?? raw.overallLag;
|
|
20
|
+
if (typeof overallLag === 'string' && overallLag.trim()) {
|
|
21
|
+
return overallLag;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/** Parse lag label to month count; ranges use the upper bound. */
|
|
26
|
+
function parseLagMonthsFromLabel(lag) {
|
|
27
|
+
if (lag == null)
|
|
28
|
+
return null;
|
|
29
|
+
const trimmed = lag.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
return null;
|
|
32
|
+
const lower = trimmed.toLowerCase().replace(/\u2013|\u2014/g, '-');
|
|
33
|
+
if (lower === 'unknown' || lower === 'no lag' || lower.includes('no lag')) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const rangeMatch = lower.match(/(\d+)\s*(?:to|-)\s*(\d+)\s*months?/);
|
|
37
|
+
if (rangeMatch) {
|
|
38
|
+
const a = parseInt(rangeMatch[1], 10);
|
|
39
|
+
const b = parseInt(rangeMatch[2], 10);
|
|
40
|
+
if (Number.isFinite(a) && Number.isFinite(b))
|
|
41
|
+
return Math.max(a, b);
|
|
42
|
+
}
|
|
43
|
+
const quarterMatch = lower.match(/~?\s*(\d+)\s*quarter/);
|
|
44
|
+
if (quarterMatch) {
|
|
45
|
+
const q = parseInt(quarterMatch[1], 10);
|
|
46
|
+
if (Number.isFinite(q) && q > 0)
|
|
47
|
+
return q * 3;
|
|
48
|
+
}
|
|
49
|
+
const monthMatch = lower.match(/~?\s*(\d+)\s*months?/);
|
|
50
|
+
if (monthMatch) {
|
|
51
|
+
const m = parseInt(monthMatch[1], 10);
|
|
52
|
+
if (Number.isFinite(m) && m > 0)
|
|
53
|
+
return m;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function formatLagMonthsLabel(months) {
|
|
58
|
+
return `${months} month(s)`;
|
|
59
|
+
}
|
|
60
|
+
function getLagDisplayForView(lag, tab) {
|
|
61
|
+
if (tab === 'overlapped') {
|
|
62
|
+
return lag?.trim() ? lag : '—';
|
|
63
|
+
}
|
|
64
|
+
const months = parseLagMonthsFromLabel(lag);
|
|
65
|
+
return months != null ? formatLagMonthsLabel(months) : '—';
|
|
66
|
+
}
|
|
67
|
+
function shiftNormalizedSeriesForward(series, lagMonths) {
|
|
68
|
+
if (lagMonths <= 0)
|
|
69
|
+
return { ...series };
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const [dateStr, val] of Object.entries(series)) {
|
|
72
|
+
if (val === null || val === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
let shifted = normalizeToMonthStart(dateStr);
|
|
75
|
+
for (let i = 0; i < lagMonths; i++) {
|
|
76
|
+
shifted = getNextMonth(shifted);
|
|
77
|
+
}
|
|
78
|
+
out[shifted] = val;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function applyDriversComparisonViewToPayload(payload, tab) {
|
|
83
|
+
if (!payload || tab === 'overlapped')
|
|
84
|
+
return payload;
|
|
85
|
+
return {
|
|
86
|
+
target: {
|
|
87
|
+
...payload.target,
|
|
88
|
+
normalized_series: { ...payload.target.normalized_series },
|
|
89
|
+
},
|
|
90
|
+
drivers: payload.drivers.map(driver => {
|
|
91
|
+
const lagMonths = parseLagMonthsFromLabel(resolveDriverLagLabel(driver));
|
|
92
|
+
const series = driver.normalized_series ?? {};
|
|
93
|
+
const normalized_series = lagMonths != null && lagMonths > 0
|
|
94
|
+
? shiftNormalizedSeriesForward(series, lagMonths)
|
|
95
|
+
: { ...series };
|
|
96
|
+
return {
|
|
97
|
+
...driver,
|
|
98
|
+
normalized_series,
|
|
99
|
+
};
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
14
103
|
function mergeBacktestsChartData(payload) {
|
|
15
104
|
if (!payload?.target?.normalized_series)
|
|
16
105
|
return [];
|
|
@@ -156,4 +245,4 @@ function buildDriversComparisonChartData(mergedWithHistorical, datasetHistorical
|
|
|
156
245
|
return withLead.filter(p => normalizeToMonthStart(p.date) >= xMin);
|
|
157
246
|
}
|
|
158
247
|
|
|
159
|
-
export { DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, INITIAL_VISIBLE_SERIES_COUNT, buildDriversComparisonChartData, formatSeriesImportance, getZoomAnchorMonthFromDriverMaterialStarts, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, prependHistoricalLeadFromDataset, subtractMonthsFromMonthStart };
|
|
248
|
+
export { DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, INITIAL_VISIBLE_SERIES_COUNT, applyDriversComparisonViewToPayload, buildDriversComparisonChartData, formatLagMonthsLabel, formatSeriesImportance, getLagDisplayForView, getZoomAnchorMonthFromDriverMaterialStarts, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, parseLagMonthsFromLabel, prependHistoricalLeadFromDataset, resolveDriverLagLabel, shiftNormalizedSeriesForward, subtractMonthsFromMonthStart };
|
package/dist/esm/index.js
CHANGED
|
@@ -108,7 +108,7 @@ export { getCategoryIcon } from './components/widgets/DriverMap/driverCategoryIc
|
|
|
108
108
|
export { getDriverImportance, getHighestImportanceDriver } from './components/widgets/DriverMap/driverMapSelection.js';
|
|
109
109
|
export { geographicCoordinates, geographicToSVG, getContinentFromRegion, getPreciseCoordinates, getResponsiveCoordinates, svgToPercentage } from './components/widgets/DriverMap/driverMapGeography.js';
|
|
110
110
|
export { DriversComparisonChart } from './components/widgets/DriversComparisonChart/DriversComparisonChart.js';
|
|
111
|
-
export { DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, INITIAL_VISIBLE_SERIES_COUNT, buildDriversComparisonChartData, formatSeriesImportance, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData } from './components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js';
|
|
111
|
+
export { DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, INITIAL_VISIBLE_SERIES_COUNT, applyDriversComparisonViewToPayload, buildDriversComparisonChartData, formatLagMonthsLabel, formatSeriesImportance, getLagDisplayForView, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, parseLagMonthsFromLabel, resolveDriverLagLabel, shiftNormalizedSeriesForward } from './components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js';
|
|
112
112
|
export { PerformanceChart } from './components/widgets/PerformanceChart/PerformanceChart.js';
|
|
113
113
|
export { PerformanceTable } from './components/widgets/PerformanceChart/PerformanceTable.js';
|
|
114
114
|
export { SPAGHETTI_DRIFT_PER_HORIZON_ID_BASE, SPAGHETTI_MODEL_PER_HORIZON_ID_BASE, averageForecastErrorsVsHistoricalForMatrixColumn, buildDriftSpaghettiMatrixForCustomDialog, buildPerHorizonSpaghettiEntries, buildSpaghettiMergedChartData, calculateYRangeFromChartData, getForecastModelDisplayName, spaghettiGridFromHistoricalPreviousMonth } from './components/widgets/PerformanceChart/performanceChart.helpers.js';
|
|
@@ -6,6 +6,6 @@ declare function CardHeader({ className, icon, iconWithBackground, textsClassNam
|
|
|
6
6
|
declare function CardTitle({ className, ...props }: CardTitleProps): import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
declare function CardDescription({ className, ...props }: CardDescriptionProps): import("react/jsx-runtime").JSX.Element;
|
|
8
8
|
declare function CardAction({ className, ...props }: CardActionProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
-
declare function CardContent({ className, centered, fullHeight, noScroll, children, autoScrollBottom, ...props }: CardContentProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
declare function CardContent({ className, centered, fullHeight, noScroll, children, autoScrollBottom, yScrollbarClassName, ...props }: CardContentProps): import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
declare function CardFooter({ className, ...props }: CardFooterProps): import("react/jsx-runtime").JSX.Element;
|
|
11
11
|
export { Card, LinkCard, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
|
|
@@ -24,6 +24,7 @@ export interface CardContentProps extends React.ComponentProps<'div'> {
|
|
|
24
24
|
fullHeight?: boolean;
|
|
25
25
|
noScroll?: boolean;
|
|
26
26
|
autoScrollBottom?: boolean;
|
|
27
|
+
yScrollbarClassName?: string;
|
|
27
28
|
}
|
|
28
29
|
export interface CardFooterProps extends React.ComponentProps<'div'> {
|
|
29
30
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { DialogProps } from './Dialog.types';
|
|
2
|
-
export declare function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disableCloseButton, icon, content, contentClassName, footer, footerClassName, footerAlignment, size, className, noScroll, maxHeight, autoScrollBottom, width, }: DialogProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disableCloseButton, icon, content, contentClassName, contentScrollbarClassName, footer, footerClassName, footerAlignment, size, className, noScroll, maxHeight, autoScrollBottom, width, }: DialogProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
export { default as DialogStyles } from './Dialog.styl';
|
package/dist/esm/types/src/components/widgets/DriversComparisonChart/DriversComparisonChart.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
2
2
|
import type { BacktestsComponentPayload } from '@sybilion/platform-sdk';
|
|
3
|
+
import { type DriversComparisonViewTab } from './driversComparisonChart.helpers';
|
|
3
4
|
export type DriversComparisonChartProps = {
|
|
4
5
|
payload: BacktestsComponentPayload | null;
|
|
5
6
|
datasetHistorical?: ChartDataPoint[];
|
|
@@ -14,5 +15,8 @@ export type DriversComparisonChartProps = {
|
|
|
14
15
|
className?: string;
|
|
15
16
|
/** Resets visible series when this key changes (e.g. selected analysis id). */
|
|
16
17
|
seriesInitKey?: string;
|
|
18
|
+
viewTab?: DriversComparisonViewTab;
|
|
19
|
+
onViewTabChange?: (tab: DriversComparisonViewTab) => void;
|
|
17
20
|
};
|
|
18
|
-
export
|
|
21
|
+
export type { DriversComparisonViewTab } from './driversComparisonChart.helpers';
|
|
22
|
+
export declare function DriversComparisonChart({ payload, datasetHistorical, loading, chartLoading, statusHint, statusTone, runAnalysisHint, timeRange: timeRangeProp, onTimeRangeChange, isDarkTheme, className, seriesInitKey, viewTab: viewTabProp, onViewTabChange, }: DriversComparisonChartProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,7 +4,17 @@ export declare const DRIVER_FORECAST_ID_BASE = 8000000;
|
|
|
4
4
|
export declare const INITIAL_VISIBLE_SERIES_COUNT = 3;
|
|
5
5
|
/** Months of historical context before the earliest forecast month (inclusive). */
|
|
6
6
|
export declare const DRIVER_COMPARISON_CHART_LEAD_MONTHS = 6;
|
|
7
|
+
export type DriversComparisonViewTab = 'overlapped' | 'lagged';
|
|
7
8
|
export declare function formatSeriesImportance(value: number | null): string;
|
|
9
|
+
export declare function resolveDriverLagLabel(driver: {
|
|
10
|
+
lag?: string | null;
|
|
11
|
+
}): string | null;
|
|
12
|
+
/** Parse lag label to month count; ranges use the upper bound. */
|
|
13
|
+
export declare function parseLagMonthsFromLabel(lag: string | null | undefined): number | null;
|
|
14
|
+
export declare function formatLagMonthsLabel(months: number): string;
|
|
15
|
+
export declare function getLagDisplayForView(lag: string | null | undefined, tab: DriversComparisonViewTab): string;
|
|
16
|
+
export declare function shiftNormalizedSeriesForward(series: Record<string, number | null>, lagMonths: number): Record<string, number | null>;
|
|
17
|
+
export declare function applyDriversComparisonViewToPayload(payload: BacktestsComponentPayload | null, tab: DriversComparisonViewTab): BacktestsComponentPayload | null;
|
|
8
18
|
export declare function mergeBacktestsChartData(payload: BacktestsComponentPayload | null): ChartDataPoint[];
|
|
9
19
|
/** While drivers comparison is not loaded: dataset historical only. After load: normalized historical from target.normalized_series (see mergeBacktestsChartData) replaces it. */
|
|
10
20
|
export declare function mergeDatasetHistoricalWithBacktestsChartData(datasetHistorical: ChartDataPoint[], backtestsMerged: ChartDataPoint[]): ChartDataPoint[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { DriversComparisonChart, type DriversComparisonChartProps, } from './DriversComparisonChart';
|
|
2
|
-
export { buildDriversComparisonChartData, DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, formatSeriesImportance, INITIAL_VISIBLE_SERIES_COUNT, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, } from './driversComparisonChart.helpers';
|
|
1
|
+
export { DriversComparisonChart, type DriversComparisonChartProps, type DriversComparisonViewTab, } from './DriversComparisonChart';
|
|
2
|
+
export { applyDriversComparisonViewToPayload, buildDriversComparisonChartData, DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, formatLagMonthsLabel, formatSeriesImportance, getLagDisplayForView, resolveDriverLagLabel, INITIAL_VISIBLE_SERIES_COUNT, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, parseLagMonthsFromLabel, shiftNormalizedSeriesForward, } from './driversComparisonChart.helpers';
|
package/package.json
CHANGED
|
@@ -129,9 +129,10 @@ function CardContent({
|
|
|
129
129
|
noScroll,
|
|
130
130
|
children,
|
|
131
131
|
autoScrollBottom,
|
|
132
|
+
yScrollbarClassName,
|
|
132
133
|
...props
|
|
133
134
|
}: CardContentProps) {
|
|
134
|
-
const
|
|
135
|
+
const scrollInnerRef = useRef<HTMLDivElement | null>(null);
|
|
135
136
|
|
|
136
137
|
const contentDiv = (
|
|
137
138
|
<div
|
|
@@ -149,7 +150,7 @@ function CardContent({
|
|
|
149
150
|
|
|
150
151
|
useEffect(() => {
|
|
151
152
|
if (!noScroll && autoScrollBottom) {
|
|
152
|
-
const inner =
|
|
153
|
+
const inner = scrollInnerRef.current;
|
|
153
154
|
|
|
154
155
|
if (!inner) return;
|
|
155
156
|
|
|
@@ -170,8 +171,16 @@ function CardContent({
|
|
|
170
171
|
if (noScroll) return contentDiv;
|
|
171
172
|
|
|
172
173
|
return (
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
<Scroll
|
|
175
|
+
y
|
|
176
|
+
autoHide
|
|
177
|
+
fadeSize="m"
|
|
178
|
+
className={S.scroll}
|
|
179
|
+
yScrollbarClassName={yScrollbarClassName}
|
|
180
|
+
onInnerRef={el => {
|
|
181
|
+
scrollInnerRef.current = el;
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
175
184
|
{contentDiv}
|
|
176
185
|
</Scroll>
|
|
177
186
|
);
|
|
@@ -30,6 +30,7 @@ export interface CardContentProps extends React.ComponentProps<'div'> {
|
|
|
30
30
|
fullHeight?: boolean;
|
|
31
31
|
noScroll?: boolean;
|
|
32
32
|
autoScrollBottom?: boolean;
|
|
33
|
+
yScrollbarClassName?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export interface CardFooterProps extends React.ComponentProps<'div'> {}
|
|
@@ -31,6 +31,7 @@ export function Dialog({
|
|
|
31
31
|
icon,
|
|
32
32
|
content,
|
|
33
33
|
contentClassName,
|
|
34
|
+
contentScrollbarClassName,
|
|
34
35
|
footer,
|
|
35
36
|
footerClassName,
|
|
36
37
|
footerAlignment = 'center',
|
|
@@ -215,6 +216,7 @@ export function Dialog({
|
|
|
215
216
|
{content && (
|
|
216
217
|
<CardContent
|
|
217
218
|
className={contentClassName}
|
|
219
|
+
yScrollbarClassName={contentScrollbarClassName}
|
|
218
220
|
noScroll={noScroll}
|
|
219
221
|
autoScrollBottom={autoScrollBottom}
|
|
220
222
|
>
|
|
@@ -10,9 +10,14 @@ Host provides:
|
|
|
10
10
|
- `payload`: BacktestsComponentPayload from platform SDK (host fetch per analysis)
|
|
11
11
|
- Optional `datasetHistorical` overlay
|
|
12
12
|
- `seriesInitKey` when selected analysis changes
|
|
13
|
+
- `viewTab` / `onViewTabChange`: `overlapped` (calendar-aligned) or `lagged` (driver series shifted forward by parsed lag months)
|
|
14
|
+
|
|
15
|
+
View tabs: host should render uilib `Tabs variant="button"` with **Lagged** / **Overlapped** in the toolbar (analysis selector left, tabs right). Chart applies `applyDriversComparisonViewToPayload` internally.
|
|
13
16
|
|
|
14
17
|
Report tile: `drivers_comparison_chart` — host loads normalized backtests payload + dataset historical; built-in analysis selector.
|
|
15
18
|
|
|
16
|
-
Requires: `payload` — target + driver normalized_series; `loading` / `chartLoading` — spinners; `seriesInitKey` — reset visible series on analysis change; `runAnalysisHint` / `statusHint` — empty/error text.
|
|
19
|
+
Requires: `payload` — target + driver normalized_series; `loading` / `chartLoading` — spinners; `seriesInitKey` — reset visible series on analysis or view tab change; `runAnalysisHint` / `statusHint` — empty/error text.
|
|
20
|
+
|
|
21
|
+
Lag column: **Overlapped** shows API `lag` string (may be a range). **Lagged** shows single `N month(s)` from `parseLagMonthsFromLabel` (range uses max month).
|
|
17
22
|
|
|
18
23
|
Empty/loading: loading props shimmer chart; null `payload` with `runAnalysisHint` prompts to run analysis.
|
|
@@ -13,22 +13,33 @@ import {
|
|
|
13
13
|
Table,
|
|
14
14
|
TableBody,
|
|
15
15
|
TableCell,
|
|
16
|
+
TableCellValue,
|
|
16
17
|
TableHead,
|
|
17
18
|
TableHeader,
|
|
18
19
|
TableRow,
|
|
19
20
|
} from '#uilib/components/ui/Table';
|
|
20
21
|
import { TextShimmer } from '#uilib/components/ui/TextShimmer/TextShimmer';
|
|
21
22
|
import { TIME_RANGES } from '#uilib/components/ui/TimeRangeControls/TimeRangeControls.constants';
|
|
23
|
+
import {
|
|
24
|
+
Tooltip,
|
|
25
|
+
TooltipContent,
|
|
26
|
+
TooltipTrigger,
|
|
27
|
+
} from '#uilib/components/ui/Tooltip';
|
|
22
28
|
import type { BacktestsComponentPayload } from '@sybilion/platform-sdk';
|
|
29
|
+
import { InfoIcon } from 'lucide-react';
|
|
23
30
|
|
|
24
31
|
import S from './DriversComparisonChart.styl';
|
|
25
32
|
import {
|
|
26
33
|
DRIVER_FORECAST_ID_BASE,
|
|
34
|
+
type DriversComparisonViewTab,
|
|
27
35
|
INITIAL_VISIBLE_SERIES_COUNT,
|
|
36
|
+
applyDriversComparisonViewToPayload,
|
|
28
37
|
buildDriversComparisonChartData,
|
|
29
38
|
formatSeriesImportance,
|
|
39
|
+
getLagDisplayForView,
|
|
30
40
|
mergeBacktestsChartData,
|
|
31
41
|
mergeDatasetHistoricalWithBacktestsChartData,
|
|
42
|
+
resolveDriverLagLabel,
|
|
32
43
|
} from './driversComparisonChart.helpers';
|
|
33
44
|
|
|
34
45
|
const ALL_TIME_RANGE = TIME_RANGES[TIME_RANGES.length - 1];
|
|
@@ -52,8 +63,12 @@ export type DriversComparisonChartProps = {
|
|
|
52
63
|
className?: string;
|
|
53
64
|
/** Resets visible series when this key changes (e.g. selected analysis id). */
|
|
54
65
|
seriesInitKey?: string;
|
|
66
|
+
viewTab?: DriversComparisonViewTab;
|
|
67
|
+
onViewTabChange?: (tab: DriversComparisonViewTab) => void;
|
|
55
68
|
};
|
|
56
69
|
|
|
70
|
+
export type { DriversComparisonViewTab } from './driversComparisonChart.helpers';
|
|
71
|
+
|
|
57
72
|
export function DriversComparisonChart({
|
|
58
73
|
payload,
|
|
59
74
|
datasetHistorical = [],
|
|
@@ -67,11 +82,17 @@ export function DriversComparisonChart({
|
|
|
67
82
|
isDarkTheme = false,
|
|
68
83
|
className,
|
|
69
84
|
seriesInitKey,
|
|
85
|
+
viewTab: viewTabProp,
|
|
86
|
+
onViewTabChange,
|
|
70
87
|
}: DriversComparisonChartProps) {
|
|
71
88
|
const [internalTimeRange, setInternalTimeRange] =
|
|
72
89
|
useState<string>(ALL_TIME_RANGE);
|
|
90
|
+
const [internalViewTab, setInternalViewTab] =
|
|
91
|
+
useState<DriversComparisonViewTab>('overlapped');
|
|
73
92
|
const [hiddenSeries, setHiddenSeries] = useState<Set<string>>(new Set());
|
|
74
93
|
|
|
94
|
+
const viewTab = viewTabProp ?? internalViewTab;
|
|
95
|
+
|
|
75
96
|
const timeRange = timeRangeProp ?? internalTimeRange;
|
|
76
97
|
const handleTimeRangeChange = useCallback(
|
|
77
98
|
(val: string) => {
|
|
@@ -104,8 +125,13 @@ export function DriversComparisonChart({
|
|
|
104
125
|
});
|
|
105
126
|
}, []);
|
|
106
127
|
|
|
128
|
+
const payloadForView = useMemo(
|
|
129
|
+
() => applyDriversComparisonViewToPayload(payload, viewTab),
|
|
130
|
+
[payload, viewTab],
|
|
131
|
+
);
|
|
132
|
+
|
|
107
133
|
const sortedDriversWithData = useMemo(() => {
|
|
108
|
-
const driversList =
|
|
134
|
+
const driversList = payloadForView?.drivers ?? [];
|
|
109
135
|
if (!driversList.length) return [];
|
|
110
136
|
return [...driversList]
|
|
111
137
|
.filter(
|
|
@@ -116,11 +142,11 @@ export function DriversComparisonChart({
|
|
|
116
142
|
),
|
|
117
143
|
)
|
|
118
144
|
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
119
|
-
}, [
|
|
145
|
+
}, [payloadForView?.drivers]);
|
|
120
146
|
|
|
121
147
|
const mergedChartData = useMemo(
|
|
122
|
-
() => mergeBacktestsChartData(
|
|
123
|
-
[
|
|
148
|
+
() => mergeBacktestsChartData(payloadForView),
|
|
149
|
+
[payloadForView],
|
|
124
150
|
);
|
|
125
151
|
|
|
126
152
|
const mergedWithHistorical = useMemo(
|
|
@@ -133,13 +159,21 @@ export function DriversComparisonChart({
|
|
|
133
159
|
);
|
|
134
160
|
|
|
135
161
|
const chartForecastData = useMemo((): ForecastItemData[] => {
|
|
136
|
-
if (!
|
|
162
|
+
if (!payloadForView?.target?.normalized_series) return [];
|
|
137
163
|
return sortedDriversWithData.map((driver, idx) => ({
|
|
138
164
|
id: DRIVER_FORECAST_ID_BASE + idx,
|
|
139
165
|
name: driver.name || String(driver.id),
|
|
140
166
|
color: getForecastColor(idx + 1),
|
|
141
167
|
}));
|
|
142
|
-
}, [
|
|
168
|
+
}, [payloadForView?.target?.normalized_series, sortedDriversWithData]);
|
|
169
|
+
|
|
170
|
+
const originalDriversById = useMemo(() => {
|
|
171
|
+
const map = new Map<string, (typeof sortedDriversWithData)[number]>();
|
|
172
|
+
for (const driver of payload?.drivers ?? []) {
|
|
173
|
+
map.set(String(driver.id), driver);
|
|
174
|
+
}
|
|
175
|
+
return map;
|
|
176
|
+
}, [payload?.drivers]);
|
|
143
177
|
|
|
144
178
|
const tableSeriesRows = useMemo(() => {
|
|
145
179
|
if (sortedDriversWithData.length === 0) return [];
|
|
@@ -147,18 +181,18 @@ export function DriversComparisonChart({
|
|
|
147
181
|
const raw = driver.importance;
|
|
148
182
|
const importance =
|
|
149
183
|
typeof raw === 'number' && Number.isFinite(raw) ? raw : null;
|
|
184
|
+
const lagSource = originalDriversById.get(String(driver.id)) ?? driver;
|
|
150
185
|
return {
|
|
151
186
|
id: DRIVER_FORECAST_ID_BASE + idx,
|
|
152
187
|
name: driver.name || String(driver.id),
|
|
153
188
|
color: getForecastColor(idx + 1),
|
|
154
189
|
importance,
|
|
155
|
-
lag:
|
|
190
|
+
lag: getLagDisplayForView(resolveDriverLagLabel(lagSource), viewTab),
|
|
156
191
|
};
|
|
157
192
|
});
|
|
158
|
-
}, [sortedDriversWithData]);
|
|
193
|
+
}, [originalDriversById, sortedDriversWithData, viewTab]);
|
|
159
194
|
|
|
160
|
-
const seriesInitKeyResolved =
|
|
161
|
-
seriesInitKey ?? `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`;
|
|
195
|
+
const seriesInitKeyResolved = `${seriesInitKey ?? `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`}:${viewTab}`;
|
|
162
196
|
|
|
163
197
|
const backtestsSeriesInitKeyRef = useRef<string>('');
|
|
164
198
|
|
|
@@ -204,6 +238,8 @@ export function DriversComparisonChart({
|
|
|
204
238
|
)}
|
|
205
239
|
>
|
|
206
240
|
<ChartAreaInteractive
|
|
241
|
+
key={viewTab}
|
|
242
|
+
chartRenderId={`drivers-comparison-${viewTab}`}
|
|
207
243
|
disableHistoricalAnimation
|
|
208
244
|
disableTimeRangeSelector
|
|
209
245
|
enableTimeRangeBrush
|
|
@@ -282,8 +318,42 @@ export function DriversComparisonChart({
|
|
|
282
318
|
<TableHead className={S.seriesColSeries}>
|
|
283
319
|
Driver name
|
|
284
320
|
</TableHead>
|
|
285
|
-
<TableHead>
|
|
286
|
-
|
|
321
|
+
<TableHead>
|
|
322
|
+
<TableCellValue>
|
|
323
|
+
Importance
|
|
324
|
+
<Tooltip>
|
|
325
|
+
<TooltipTrigger asChild>
|
|
326
|
+
<span>
|
|
327
|
+
<InfoIcon size={16} style={{ cursor: 'help' }} />
|
|
328
|
+
</span>
|
|
329
|
+
</TooltipTrigger>
|
|
330
|
+
<TooltipContent side="top" maxWidth={300}>
|
|
331
|
+
How much this driver contributes to price movements
|
|
332
|
+
in the forecast model. Higher = stronger influence.
|
|
333
|
+
Reflects relative weight of each driver, scored from
|
|
334
|
+
0% to 100%.
|
|
335
|
+
</TooltipContent>
|
|
336
|
+
</Tooltip>
|
|
337
|
+
</TableCellValue>
|
|
338
|
+
</TableHead>
|
|
339
|
+
<TableHead>
|
|
340
|
+
<TableCellValue>
|
|
341
|
+
Lag
|
|
342
|
+
<Tooltip>
|
|
343
|
+
<TooltipTrigger asChild>
|
|
344
|
+
<span>
|
|
345
|
+
<InfoIcon size={16} style={{ cursor: 'help' }} />
|
|
346
|
+
</span>
|
|
347
|
+
</TooltipTrigger>
|
|
348
|
+
<TooltipContent side="top" maxWidth={300}>
|
|
349
|
+
Lag shows how far ahead this driver predicts price
|
|
350
|
+
movement. A range (e.g. 9–12 months) means the
|
|
351
|
+
predictive signal is strongest somewhere in that
|
|
352
|
+
window — not at a single fixed point.
|
|
353
|
+
</TooltipContent>
|
|
354
|
+
</Tooltip>
|
|
355
|
+
</TableCellValue>
|
|
356
|
+
</TableHead>
|
|
287
357
|
</TableRow>
|
|
288
358
|
</TableHeader>
|
|
289
359
|
<TableBody>
|
|
@@ -310,7 +380,7 @@ export function DriversComparisonChart({
|
|
|
310
380
|
<TableCell>
|
|
311
381
|
{formatSeriesImportance(row.importance)}
|
|
312
382
|
</TableCell>
|
|
313
|
-
<TableCell>{row.lag
|
|
383
|
+
<TableCell>{row.lag}</TableCell>
|
|
314
384
|
</TableRow>
|
|
315
385
|
);
|
|
316
386
|
})}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyDriversComparisonViewToPayload,
|
|
3
|
+
formatLagMonthsLabel,
|
|
4
|
+
getLagDisplayForView,
|
|
5
|
+
parseLagMonthsFromLabel,
|
|
6
|
+
resolveDriverLagLabel,
|
|
7
|
+
shiftNormalizedSeriesForward,
|
|
8
|
+
} from './driversComparisonChart.helpers';
|
|
9
|
+
|
|
10
|
+
describe('parseLagMonthsFromLabel', () => {
|
|
11
|
+
it('uses max month for ranges', () => {
|
|
12
|
+
expect(parseLagMonthsFromLabel('9 to 12 month(s)')).toBe(12);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('parses single month labels', () => {
|
|
16
|
+
expect(parseLagMonthsFromLabel('2 month(s)')).toBe(2);
|
|
17
|
+
expect(parseLagMonthsFromLabel('~1 month(s)')).toBe(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('converts quarters to months', () => {
|
|
21
|
+
expect(parseLagMonthsFromLabel('1 quarter(s)')).toBe(3);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns null for unknown or empty labels', () => {
|
|
25
|
+
expect(parseLagMonthsFromLabel('Unknown')).toBeNull();
|
|
26
|
+
expect(parseLagMonthsFromLabel('no lag')).toBeNull();
|
|
27
|
+
expect(parseLagMonthsFromLabel('')).toBeNull();
|
|
28
|
+
expect(parseLagMonthsFromLabel(null)).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('parses en-dash month ranges', () => {
|
|
32
|
+
expect(parseLagMonthsFromLabel('9–12 month(s)')).toBe(12);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('resolveDriverLagLabel', () => {
|
|
37
|
+
it('reads lag or overall_lag', () => {
|
|
38
|
+
expect(resolveDriverLagLabel({ lag: '2 month(s)' })).toBe('2 month(s)');
|
|
39
|
+
expect(resolveDriverLagLabel({ overall_lag: '3 month(s)' })).toBe(
|
|
40
|
+
'3 month(s)',
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('formatLagMonthsLabel', () => {
|
|
46
|
+
it('formats month count', () => {
|
|
47
|
+
expect(formatLagMonthsLabel(12)).toBe('12 month(s)');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('getLagDisplayForView', () => {
|
|
52
|
+
it('shows API lag on overlapped tab', () => {
|
|
53
|
+
expect(getLagDisplayForView('9 to 12 month(s)', 'overlapped')).toBe(
|
|
54
|
+
'9 to 12 month(s)',
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('shows parsed month on lagged tab', () => {
|
|
59
|
+
expect(getLagDisplayForView('9 to 12 month(s)', 'lagged')).toBe(
|
|
60
|
+
'12 month(s)',
|
|
61
|
+
);
|
|
62
|
+
expect(getLagDisplayForView('Unknown', 'lagged')).toBe('—');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('shiftNormalizedSeriesForward', () => {
|
|
67
|
+
it('moves points forward by lag months', () => {
|
|
68
|
+
const shifted = shiftNormalizedSeriesForward(
|
|
69
|
+
{ '2015-01-01': 1.2, '2015-02-01': 1.4 },
|
|
70
|
+
2,
|
|
71
|
+
);
|
|
72
|
+
expect(shifted['2015-03-01']).toBe(1.2);
|
|
73
|
+
expect(shifted['2015-04-01']).toBe(1.4);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns a shallow copy when lag is zero', () => {
|
|
77
|
+
const series = { '2015-01-01': 1 };
|
|
78
|
+
const shifted = shiftNormalizedSeriesForward(series, 0);
|
|
79
|
+
expect(shifted).toEqual(series);
|
|
80
|
+
expect(shifted).not.toBe(series);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('applyDriversComparisonViewToPayload', () => {
|
|
85
|
+
const payload = {
|
|
86
|
+
target: {
|
|
87
|
+
id: 'target',
|
|
88
|
+
name: 'Target',
|
|
89
|
+
normalized_series: { '2015-01-01': 1 },
|
|
90
|
+
},
|
|
91
|
+
drivers: [
|
|
92
|
+
{
|
|
93
|
+
id: 'd1',
|
|
94
|
+
name: 'Driver',
|
|
95
|
+
lag: '2 month(s)',
|
|
96
|
+
normalized_series: { '2015-01-01': 0.5 },
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
it('returns original payload for overlapped tab', () => {
|
|
102
|
+
expect(applyDriversComparisonViewToPayload(payload, 'overlapped')).toBe(
|
|
103
|
+
payload,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('shifts driver series for lagged tab without mutating source', () => {
|
|
108
|
+
const lagged = applyDriversComparisonViewToPayload(payload, 'lagged');
|
|
109
|
+
expect(lagged).not.toBe(payload);
|
|
110
|
+
expect(payload.drivers[0].normalized_series['2015-01-01']).toBe(0.5);
|
|
111
|
+
expect(lagged?.drivers[0].normalized_series['2015-03-01']).toBe(0.5);
|
|
112
|
+
expect(lagged?.target.normalized_series).toEqual(
|
|
113
|
+
payload.target.normalized_series,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('shifts using overall_lag when lag is absent', () => {
|
|
118
|
+
const withOverallLag = {
|
|
119
|
+
...payload,
|
|
120
|
+
drivers: [
|
|
121
|
+
{
|
|
122
|
+
id: 'd2',
|
|
123
|
+
name: 'Driver 2',
|
|
124
|
+
overall_lag: '1 month(s)',
|
|
125
|
+
normalized_series: { '2015-06-01': 0.8 },
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
const lagged = applyDriversComparisonViewToPayload(
|
|
130
|
+
withOverallLag,
|
|
131
|
+
'lagged',
|
|
132
|
+
);
|
|
133
|
+
expect(lagged?.drivers[0].normalized_series['2015-07-01']).toBe(0.8);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -11,6 +11,8 @@ export const INITIAL_VISIBLE_SERIES_COUNT = 3;
|
|
|
11
11
|
/** Months of historical context before the earliest forecast month (inclusive). */
|
|
12
12
|
export const DRIVER_COMPARISON_CHART_LEAD_MONTHS = 6;
|
|
13
13
|
|
|
14
|
+
export type DriversComparisonViewTab = 'overlapped' | 'lagged';
|
|
15
|
+
|
|
14
16
|
/** Ignore leading months where drivers are numerically zero (API often fills 0; lines look empty until real signal). */
|
|
15
17
|
const DRIVER_FORECAST_NONZERO_EPS = 1e-9;
|
|
16
18
|
|
|
@@ -19,6 +21,112 @@ export function formatSeriesImportance(value: number | null): string {
|
|
|
19
21
|
return `${value.toFixed(1)}%`;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
export function resolveDriverLagLabel(driver: {
|
|
25
|
+
lag?: string | null;
|
|
26
|
+
}): string | null {
|
|
27
|
+
if (typeof driver.lag === 'string' && driver.lag.trim()) {
|
|
28
|
+
return driver.lag;
|
|
29
|
+
}
|
|
30
|
+
const raw = driver as Record<string, unknown>;
|
|
31
|
+
const overallLag = raw.overall_lag ?? raw.overallLag;
|
|
32
|
+
if (typeof overallLag === 'string' && overallLag.trim()) {
|
|
33
|
+
return overallLag;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Parse lag label to month count; ranges use the upper bound. */
|
|
39
|
+
export function parseLagMonthsFromLabel(
|
|
40
|
+
lag: string | null | undefined,
|
|
41
|
+
): number | null {
|
|
42
|
+
if (lag == null) return null;
|
|
43
|
+
const trimmed = lag.trim();
|
|
44
|
+
if (!trimmed) return null;
|
|
45
|
+
const lower = trimmed.toLowerCase().replace(/\u2013|\u2014/g, '-');
|
|
46
|
+
if (lower === 'unknown' || lower === 'no lag' || lower.includes('no lag')) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rangeMatch = lower.match(/(\d+)\s*(?:to|-)\s*(\d+)\s*months?/);
|
|
51
|
+
if (rangeMatch) {
|
|
52
|
+
const a = parseInt(rangeMatch[1], 10);
|
|
53
|
+
const b = parseInt(rangeMatch[2], 10);
|
|
54
|
+
if (Number.isFinite(a) && Number.isFinite(b)) return Math.max(a, b);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const quarterMatch = lower.match(/~?\s*(\d+)\s*quarter/);
|
|
58
|
+
if (quarterMatch) {
|
|
59
|
+
const q = parseInt(quarterMatch[1], 10);
|
|
60
|
+
if (Number.isFinite(q) && q > 0) return q * 3;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const monthMatch = lower.match(/~?\s*(\d+)\s*months?/);
|
|
64
|
+
if (monthMatch) {
|
|
65
|
+
const m = parseInt(monthMatch[1], 10);
|
|
66
|
+
if (Number.isFinite(m) && m > 0) return m;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatLagMonthsLabel(months: number): string {
|
|
73
|
+
return `${months} month(s)`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getLagDisplayForView(
|
|
77
|
+
lag: string | null | undefined,
|
|
78
|
+
tab: DriversComparisonViewTab,
|
|
79
|
+
): string {
|
|
80
|
+
if (tab === 'overlapped') {
|
|
81
|
+
return lag?.trim() ? lag : '—';
|
|
82
|
+
}
|
|
83
|
+
const months = parseLagMonthsFromLabel(lag);
|
|
84
|
+
return months != null ? formatLagMonthsLabel(months) : '—';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function shiftNormalizedSeriesForward(
|
|
88
|
+
series: Record<string, number | null>,
|
|
89
|
+
lagMonths: number,
|
|
90
|
+
): Record<string, number | null> {
|
|
91
|
+
if (lagMonths <= 0) return { ...series };
|
|
92
|
+
const out: Record<string, number | null> = {};
|
|
93
|
+
for (const [dateStr, val] of Object.entries(series)) {
|
|
94
|
+
if (val === null || val === undefined) continue;
|
|
95
|
+
let shifted = normalizeToMonthStart(dateStr);
|
|
96
|
+
for (let i = 0; i < lagMonths; i++) {
|
|
97
|
+
shifted = getNextMonth(shifted);
|
|
98
|
+
}
|
|
99
|
+
out[shifted] = val;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function applyDriversComparisonViewToPayload(
|
|
105
|
+
payload: BacktestsComponentPayload | null,
|
|
106
|
+
tab: DriversComparisonViewTab,
|
|
107
|
+
): BacktestsComponentPayload | null {
|
|
108
|
+
if (!payload || tab === 'overlapped') return payload;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
target: {
|
|
112
|
+
...payload.target,
|
|
113
|
+
normalized_series: { ...payload.target.normalized_series },
|
|
114
|
+
},
|
|
115
|
+
drivers: payload.drivers.map(driver => {
|
|
116
|
+
const lagMonths = parseLagMonthsFromLabel(resolveDriverLagLabel(driver));
|
|
117
|
+
const series = driver.normalized_series ?? {};
|
|
118
|
+
const normalized_series =
|
|
119
|
+
lagMonths != null && lagMonths > 0
|
|
120
|
+
? shiftNormalizedSeriesForward(series, lagMonths)
|
|
121
|
+
: { ...series };
|
|
122
|
+
return {
|
|
123
|
+
...driver,
|
|
124
|
+
normalized_series,
|
|
125
|
+
};
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
22
130
|
export function mergeBacktestsChartData(
|
|
23
131
|
payload: BacktestsComponentPayload | null,
|
|
24
132
|
): ChartDataPoint[] {
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
export {
|
|
2
2
|
DriversComparisonChart,
|
|
3
3
|
type DriversComparisonChartProps,
|
|
4
|
+
type DriversComparisonViewTab,
|
|
4
5
|
} from './DriversComparisonChart';
|
|
5
6
|
export {
|
|
7
|
+
applyDriversComparisonViewToPayload,
|
|
6
8
|
buildDriversComparisonChartData,
|
|
7
9
|
DRIVER_COMPARISON_CHART_LEAD_MONTHS,
|
|
8
10
|
DRIVER_FORECAST_ID_BASE,
|
|
11
|
+
formatLagMonthsLabel,
|
|
9
12
|
formatSeriesImportance,
|
|
13
|
+
getLagDisplayForView,
|
|
14
|
+
resolveDriverLagLabel,
|
|
10
15
|
INITIAL_VISIBLE_SERIES_COUNT,
|
|
11
16
|
mergeBacktestsChartData,
|
|
12
17
|
mergeDatasetHistoricalWithBacktestsChartData,
|
|
18
|
+
parseLagMonthsFromLabel,
|
|
19
|
+
shiftNormalizedSeriesForward,
|
|
13
20
|
} from './driversComparisonChart.helpers';
|
|
@@ -3,8 +3,12 @@ import { useMemo, useState } from 'react';
|
|
|
3
3
|
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
4
4
|
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
5
5
|
import { Switch } from '#uilib/components/ui/Switch';
|
|
6
|
+
import { Tabs, TabsList, TabsTrigger } from '#uilib/components/ui/Tabs';
|
|
6
7
|
import { TIME_RANGES } from '#uilib/components/ui/TimeRangeControls/TimeRangeControls.constants';
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
DriversComparisonChart,
|
|
10
|
+
type DriversComparisonViewTab,
|
|
11
|
+
} from '#uilib/components/widgets/DriversComparisonChart';
|
|
8
12
|
import { useTheme } from '#uilib/contexts/theme-context';
|
|
9
13
|
import type { BacktestsComponentPayload } from '@sybilion/platform-sdk';
|
|
10
14
|
|
|
@@ -82,7 +86,7 @@ const MOCK_PAYLOAD: BacktestsComponentPayload = {
|
|
|
82
86
|
id: 'driver-china-credit',
|
|
83
87
|
name: 'China aggregate financing impulse',
|
|
84
88
|
importance: 58.3,
|
|
85
|
-
lag: '
|
|
89
|
+
lag: '9 to 12 month(s)',
|
|
86
90
|
normalized_series: buildMonthlySeries(2020, 1, 36, 0.29, 0.018, 4.2),
|
|
87
91
|
},
|
|
88
92
|
],
|
|
@@ -102,6 +106,8 @@ export default function DriversComparisonChartPage() {
|
|
|
102
106
|
const [runAnalysisHint, setRunAnalysisHint] = useState(false);
|
|
103
107
|
const [emptyPayload, setEmptyPayload] = useState(false);
|
|
104
108
|
const [timeRange, setTimeRange] = useState<string>(ALL_TIME_RANGE);
|
|
109
|
+
const [viewTab, setViewTab] =
|
|
110
|
+
useState<DriversComparisonViewTab>('overlapped');
|
|
105
111
|
|
|
106
112
|
const payload = useMemo(
|
|
107
113
|
() => (emptyPayload ? null : MOCK_PAYLOAD),
|
|
@@ -114,7 +120,7 @@ export default function DriversComparisonChartPage() {
|
|
|
114
120
|
<AppPageHeader
|
|
115
121
|
breadcrumbs={[{ label: 'Drivers comparison chart' }]}
|
|
116
122
|
title="DriversComparisonChart"
|
|
117
|
-
subheader="Normalized target line with driver series; table rows toggle chart visibility."
|
|
123
|
+
subheader="Normalized target line with driver series; Lagged/Overlapped tabs shift driver alignment; table rows toggle chart visibility."
|
|
118
124
|
actions={<DocsHeaderActions />}
|
|
119
125
|
/>
|
|
120
126
|
<PageContentSection>
|
|
@@ -152,6 +158,28 @@ export default function DriversComparisonChartPage() {
|
|
|
152
158
|
<label htmlFor="empty-payload">Empty payload</label>
|
|
153
159
|
</div>
|
|
154
160
|
</div>
|
|
161
|
+
<div
|
|
162
|
+
style={{
|
|
163
|
+
display: 'flex',
|
|
164
|
+
justifyContent: 'flex-end',
|
|
165
|
+
marginBottom: 16,
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<Tabs
|
|
169
|
+
variant="button"
|
|
170
|
+
value={viewTab}
|
|
171
|
+
onValueChange={v => {
|
|
172
|
+
if (v === 'lagged' || v === 'overlapped') {
|
|
173
|
+
setViewTab(v);
|
|
174
|
+
}
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
<TabsList>
|
|
178
|
+
<TabsTrigger value="lagged">Lagged</TabsTrigger>
|
|
179
|
+
<TabsTrigger value="overlapped">Overlapped</TabsTrigger>
|
|
180
|
+
</TabsList>
|
|
181
|
+
</Tabs>
|
|
182
|
+
</div>
|
|
155
183
|
<DriversComparisonChart
|
|
156
184
|
payload={payload}
|
|
157
185
|
datasetHistorical={datasetHistorical}
|
|
@@ -167,6 +195,8 @@ export default function DriversComparisonChartPage() {
|
|
|
167
195
|
onTimeRangeChange={setTimeRange}
|
|
168
196
|
isDarkTheme={isDarkMode}
|
|
169
197
|
seriesInitKey={emptyPayload ? 'empty' : 'mock'}
|
|
198
|
+
viewTab={viewTab}
|
|
199
|
+
onViewTabChange={setViewTab}
|
|
170
200
|
/>
|
|
171
201
|
</PageContentSection>
|
|
172
202
|
</>
|