@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.
- package/dist/esm/components/widgets/DriversComparisonChart/DriversComparisonChart.js +139 -0
- package/dist/esm/components/widgets/DriversComparisonChart/DriversComparisonChart.styl.js +7 -0
- package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js +159 -0
- package/dist/esm/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.js +34 -0
- package/dist/esm/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.styl.js +7 -0
- package/dist/esm/components/widgets/PerformanceChart/PerformanceChart.constants.js +17 -0
- package/dist/esm/components/widgets/PerformanceChart/PerformanceChart.js +807 -0
- package/dist/esm/components/widgets/PerformanceChart/PerformanceChart.styl.js +7 -0
- package/dist/esm/components/widgets/PerformanceChart/PerformanceTable.js +130 -0
- package/dist/esm/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.js +20 -0
- package/dist/esm/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.styl.js +7 -0
- package/dist/esm/components/widgets/PerformanceChart/performanceChart.helpers.js +591 -0
- package/dist/esm/components/widgets/PerformanceChart/performanceChartUserSeries.js +109 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/types/src/components/widgets/DriversComparisonChart/DriversComparisonChart.d.ts +18 -0
- package/dist/esm/types/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.d.ts +26 -0
- package/dist/esm/types/src/components/widgets/DriversComparisonChart/index.d.ts +2 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.d.ts +7 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/PerformanceChart.constants.d.ts +3 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/PerformanceChart.d.ts +54 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/PerformanceTable.d.ts +31 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.d.ts +20 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/index.d.ts +4 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/performanceChart.helpers.d.ts +212 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/performanceChartUserSeries.d.ts +20 -0
- package/dist/esm/types/src/docs/pages/DriversComparisonChartPage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/PerformanceChartPage.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +2 -0
- package/dist/esm/utils/chartConnectionPoint.js +9 -1
- package/package.json +1 -1
- package/src/components/widgets/DriversComparisonChart/DriversComparisonChart.styl +145 -0
- package/src/components/widgets/DriversComparisonChart/DriversComparisonChart.styl.d.ts +29 -0
- package/src/components/widgets/DriversComparisonChart/DriversComparisonChart.tsx +325 -0
- package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.ts +206 -0
- package/src/components/widgets/DriversComparisonChart/index.ts +13 -0
- package/src/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.styl +25 -0
- package/src/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.styl.d.ts +11 -0
- package/src/components/widgets/PerformanceChart/HorizonsSelector/HorizonsSelector.tsx +67 -0
- package/src/components/widgets/PerformanceChart/PerformanceChart.constants.ts +17 -0
- package/src/components/widgets/PerformanceChart/PerformanceChart.styl +194 -0
- package/src/components/widgets/PerformanceChart/PerformanceChart.styl.d.ts +30 -0
- package/src/components/widgets/PerformanceChart/PerformanceChart.tsx +1251 -0
- package/src/components/widgets/PerformanceChart/PerformanceTable.tsx +381 -0
- package/src/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.styl +49 -0
- package/src/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.styl.d.ts +12 -0
- package/src/components/widgets/PerformanceChart/PerformanceUnderChartLegend/PerformanceUnderChartLegend.tsx +83 -0
- package/src/components/widgets/PerformanceChart/index.ts +28 -0
- package/src/components/widgets/PerformanceChart/performanceChart.helpers.ts +790 -0
- package/src/components/widgets/PerformanceChart/performanceChartUserSeries.ts +149 -0
- package/src/docs/pages/DriversComparisonChartPage.tsx +174 -0
- package/src/docs/pages/PerformanceChartPage.tsx +211 -0
- package/src/docs/registry.ts +12 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import {
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
import { ChartEmptyState } from '#uilib/components/ui/Chart/components/ChartEmptyState/ChartEmptyState';
|
|
11
|
+
import { ChartAreaInteractive } from '#uilib/components/ui/ChartAreaInteractive';
|
|
12
|
+
import { filterDataForTimeRange } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers';
|
|
13
|
+
import type { OverlayMode } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
14
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
15
|
+
import {
|
|
16
|
+
FORECAST_LINE_COLORS,
|
|
17
|
+
type ForecastItemData,
|
|
18
|
+
} from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
|
|
19
|
+
import { Tabs, TabsList, TabsTrigger } from '#uilib/components/ui/Tabs';
|
|
20
|
+
import { TextShimmer } from '#uilib/components/ui/TextShimmer/TextShimmer';
|
|
21
|
+
import {
|
|
22
|
+
TIME_RANGES,
|
|
23
|
+
type TimeRange,
|
|
24
|
+
} from '#uilib/components/ui/TimeRangeControls';
|
|
25
|
+
import type { ForecastData } from '#uilib/types/forecast-data';
|
|
26
|
+
import { normalizeToMonthStart } from '#uilib/utils/chartConnectionPoint';
|
|
27
|
+
|
|
28
|
+
import { HorizonsSelector } from './HorizonsSelector/HorizonsSelector';
|
|
29
|
+
import { MONTHS_24, MONTH_NAMES } from './PerformanceChart.constants';
|
|
30
|
+
import S from './PerformanceChart.styl';
|
|
31
|
+
import { PerformanceTable } from './PerformanceTable';
|
|
32
|
+
import type { AdjustParameters } from './PerformanceTable';
|
|
33
|
+
import { PerformanceUnderChartLegendFromItems } from './PerformanceUnderChartLegend/PerformanceUnderChartLegend';
|
|
34
|
+
import {
|
|
35
|
+
SPAGHETTI_DRIFT_PER_HORIZON_ID_BASE,
|
|
36
|
+
SPAGHETTI_MODEL_PER_HORIZON_ID_BASE,
|
|
37
|
+
averageForecastErrorsVsHistoricalForMatrixColumn,
|
|
38
|
+
buildPerHorizonSpaghettiEntries,
|
|
39
|
+
buildSpaghettiMergedChartData,
|
|
40
|
+
calculateYRangeFromChartData,
|
|
41
|
+
filterSpaghettiDataFromEarliestForecastStart,
|
|
42
|
+
getForecastModelDisplayName,
|
|
43
|
+
isSpaghettiDriftPerHorizonLineId,
|
|
44
|
+
isSpaghettiModelPerHorizonLineId,
|
|
45
|
+
mergeHistoricalIntoSpaghettiChartData,
|
|
46
|
+
mergeSpaghettiMergedBases,
|
|
47
|
+
mergeSpaghettiUserSeriesFromForecastData,
|
|
48
|
+
} from './performanceChart.helpers';
|
|
49
|
+
import {
|
|
50
|
+
SPAGHETTI_LOCAL_LS_USER_SERIES_ROW_ID,
|
|
51
|
+
type SpaghettiPerformanceMatrixPayload,
|
|
52
|
+
getCustomMatrixSeriesForHorizonTab,
|
|
53
|
+
isSpaghettiMatrixSyntheticLineId,
|
|
54
|
+
} from './performanceChartUserSeries';
|
|
55
|
+
|
|
56
|
+
type PerformanceUnderChartLegendItemConfig = Parameters<
|
|
57
|
+
typeof PerformanceUnderChartLegendFromItems
|
|
58
|
+
>[0]['items'][number];
|
|
59
|
+
|
|
60
|
+
export type PerformanceViewTab = 'perHorizon' | 'spaghetti';
|
|
61
|
+
|
|
62
|
+
export type PerformanceChartPayload = Record<
|
|
63
|
+
string,
|
|
64
|
+
{
|
|
65
|
+
forecasts?: Record<string, Record<string, number>>;
|
|
66
|
+
metrics_history?: Record<
|
|
67
|
+
string,
|
|
68
|
+
{ mae?: Record<string, number>; mape?: Record<string, number> }
|
|
69
|
+
>;
|
|
70
|
+
}
|
|
71
|
+
>;
|
|
72
|
+
|
|
73
|
+
const SPAGHETTI_DRIFT_LINE_COLOR = FORECAST_LINE_COLORS[0];
|
|
74
|
+
const SPAGHETTI_MODEL_LINE_COLOR = 'var(--sb-cyan-400)';
|
|
75
|
+
const SPAGHETTI_CUSTOM_PERFORMANCE_LINE_COLOR = 'var(--sb-purple-400)';
|
|
76
|
+
|
|
77
|
+
function reorderSpaghettiMatrixToHorizons(
|
|
78
|
+
m: SpaghettiPerformanceMatrixPayload,
|
|
79
|
+
targetKeys: string[],
|
|
80
|
+
): SpaghettiPerformanceMatrixPayload {
|
|
81
|
+
if (m.horizonKeys.length !== targetKeys.length) return m;
|
|
82
|
+
const idxMap = targetKeys.map(tk => m.horizonKeys.indexOf(tk));
|
|
83
|
+
if (idxMap.some(j => j < 0)) return m;
|
|
84
|
+
const perHorizonDates =
|
|
85
|
+
m.perHorizonDates &&
|
|
86
|
+
m.perHorizonDates.length === m.grid.length &&
|
|
87
|
+
m.perHorizonDates.every(
|
|
88
|
+
(row, r) =>
|
|
89
|
+
row.length === m.horizonKeys.length && row.length === m.grid[r].length,
|
|
90
|
+
)
|
|
91
|
+
? m.perHorizonDates.map(row => idxMap.map(j => row[j]))
|
|
92
|
+
: m.perHorizonDates;
|
|
93
|
+
return {
|
|
94
|
+
...m,
|
|
95
|
+
horizonKeys: [...targetKeys],
|
|
96
|
+
grid: m.grid.map(row => idxMap.map(j => row[j])),
|
|
97
|
+
perHorizonDates,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function spaghettiForecastLineColor(id: number, index: number): string {
|
|
102
|
+
if (isSpaghettiDriftPerHorizonLineId(id)) return SPAGHETTI_DRIFT_LINE_COLOR;
|
|
103
|
+
if (isSpaghettiModelPerHorizonLineId(id)) return SPAGHETTI_MODEL_LINE_COLOR;
|
|
104
|
+
if (isSpaghettiMatrixSyntheticLineId(id))
|
|
105
|
+
return SPAGHETTI_CUSTOM_PERFORMANCE_LINE_COLOR;
|
|
106
|
+
return FORECAST_LINE_COLORS[index % FORECAST_LINE_COLORS.length];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type PerformanceChartProps = {
|
|
110
|
+
performanceData: PerformanceChartPayload | null;
|
|
111
|
+
historicalData?: ChartDataPoint[];
|
|
112
|
+
combinedData?: ChartDataPoint[];
|
|
113
|
+
forecastData?: Record<string, ForecastData>;
|
|
114
|
+
userSeries?: { id: number; name?: string }[];
|
|
115
|
+
spaghettiUserSeries?: { id: number; name?: string }[];
|
|
116
|
+
customPerformanceMatrix?: SpaghettiPerformanceMatrixPayload | null;
|
|
117
|
+
customPerformanceLabel?: string | null;
|
|
118
|
+
|
|
119
|
+
loading?: boolean;
|
|
120
|
+
chartLoading?: boolean;
|
|
121
|
+
performanceSectionPending?: boolean;
|
|
122
|
+
isEmpty?: boolean;
|
|
123
|
+
perfFetchSettled?: boolean;
|
|
124
|
+
performanceDataLoading?: boolean;
|
|
125
|
+
performanceAnalysisId?: number | null;
|
|
126
|
+
|
|
127
|
+
statusHint?: string | null;
|
|
128
|
+
statusTone?: 'destructive' | 'muted';
|
|
129
|
+
runAnalysisHint?: boolean;
|
|
130
|
+
|
|
131
|
+
timeRange?: string;
|
|
132
|
+
onTimeRangeChange?: (
|
|
133
|
+
range: string,
|
|
134
|
+
meta?: { viewTab: PerformanceViewTab },
|
|
135
|
+
) => void;
|
|
136
|
+
|
|
137
|
+
isDarkTheme?: boolean;
|
|
138
|
+
className?: string;
|
|
139
|
+
seriesInitKey?: string;
|
|
140
|
+
|
|
141
|
+
toolbarStart?: ReactNode;
|
|
142
|
+
|
|
143
|
+
onEditCustomPerformance?: () => void;
|
|
144
|
+
showAddEditCustomDataButton?: boolean;
|
|
145
|
+
addEditCustomDataDisabled?: boolean;
|
|
146
|
+
|
|
147
|
+
hiddenSeries?: Set<string>;
|
|
148
|
+
onToggleLegendSeries?: (key: string) => void;
|
|
149
|
+
onEnsureSeriesVisible?: (key: string) => void;
|
|
150
|
+
onHiddenSeriesChange?: (
|
|
151
|
+
update: Set<string> | ((prev: Set<string>) => Set<string>),
|
|
152
|
+
) => void;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export function PerformanceChart({
|
|
156
|
+
performanceData,
|
|
157
|
+
historicalData = [],
|
|
158
|
+
combinedData = [],
|
|
159
|
+
forecastData = {},
|
|
160
|
+
userSeries = [],
|
|
161
|
+
spaghettiUserSeries = [],
|
|
162
|
+
customPerformanceMatrix = null,
|
|
163
|
+
customPerformanceLabel = null,
|
|
164
|
+
|
|
165
|
+
loading = false,
|
|
166
|
+
chartLoading = false,
|
|
167
|
+
performanceSectionPending = false,
|
|
168
|
+
isEmpty = false,
|
|
169
|
+
perfFetchSettled = true,
|
|
170
|
+
performanceDataLoading = false,
|
|
171
|
+
performanceAnalysisId = null,
|
|
172
|
+
|
|
173
|
+
statusHint = null,
|
|
174
|
+
statusTone = 'muted',
|
|
175
|
+
runAnalysisHint = false,
|
|
176
|
+
|
|
177
|
+
timeRange: timeRangeProp,
|
|
178
|
+
onTimeRangeChange,
|
|
179
|
+
|
|
180
|
+
isDarkTheme = false,
|
|
181
|
+
className,
|
|
182
|
+
seriesInitKey,
|
|
183
|
+
|
|
184
|
+
toolbarStart,
|
|
185
|
+
|
|
186
|
+
onEditCustomPerformance,
|
|
187
|
+
showAddEditCustomDataButton = false,
|
|
188
|
+
addEditCustomDataDisabled = false,
|
|
189
|
+
|
|
190
|
+
hiddenSeries: hiddenSeriesProp,
|
|
191
|
+
onToggleLegendSeries,
|
|
192
|
+
onEnsureSeriesVisible,
|
|
193
|
+
onHiddenSeriesChange,
|
|
194
|
+
}: PerformanceChartProps) {
|
|
195
|
+
const [viewTab, setViewTab] = useState<PerformanceViewTab>('perHorizon');
|
|
196
|
+
const [pinMonth, setPinMonth] = useState<string | undefined>(undefined);
|
|
197
|
+
const [spaghettiTimeRange, setSpaghettiTimeRange] = useState<string>('All');
|
|
198
|
+
const [perHorizonChartTimeRange, setPerHorizonChartTimeRange] =
|
|
199
|
+
useState<string>('All');
|
|
200
|
+
const [internalHiddenSeries, setInternalHiddenSeries] = useState<Set<string>>(
|
|
201
|
+
new Set(),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const [adjustParameters, setAdjustParameters] = useState<AdjustParameters>({
|
|
205
|
+
procurementVolume: 100000000,
|
|
206
|
+
variableRawMaterialCostShare: 50,
|
|
207
|
+
controllableCosts: 60,
|
|
208
|
+
currentForecastAccuracy: 70,
|
|
209
|
+
expectedImprovement: 10,
|
|
210
|
+
analystHourlyRate: 80,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const hiddenSeriesControlled = hiddenSeriesProp !== undefined;
|
|
214
|
+
const hiddenSeries = hiddenSeriesProp ?? internalHiddenSeries;
|
|
215
|
+
|
|
216
|
+
const setHiddenSeries = useCallback(
|
|
217
|
+
(update: Set<string> | ((prev: Set<string>) => Set<string>)) => {
|
|
218
|
+
if (onHiddenSeriesChange) {
|
|
219
|
+
onHiddenSeriesChange(update);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
setInternalHiddenSeries(update);
|
|
223
|
+
},
|
|
224
|
+
[onHiddenSeriesChange],
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const toggleLegendSeriesKey = useCallback(
|
|
228
|
+
(key: string) => {
|
|
229
|
+
if (onToggleLegendSeries) {
|
|
230
|
+
onToggleLegendSeries(key);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
setHiddenSeries(prev => {
|
|
234
|
+
const next = new Set(prev);
|
|
235
|
+
if (next.has(key)) next.delete(key);
|
|
236
|
+
else next.add(key);
|
|
237
|
+
return next;
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
[onToggleLegendSeries, setHiddenSeries],
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const ensureSeriesVisible = useCallback(
|
|
244
|
+
(key: string) => {
|
|
245
|
+
if (onEnsureSeriesVisible) {
|
|
246
|
+
onEnsureSeriesVisible(key);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
setHiddenSeries(prev => {
|
|
250
|
+
if (!prev.has(key)) return prev;
|
|
251
|
+
const next = new Set(prev);
|
|
252
|
+
next.delete(key);
|
|
253
|
+
return next;
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
[onEnsureSeriesVisible, setHiddenSeries],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const userSeriesForSpaghetti = useMemo(
|
|
260
|
+
() => [...userSeries, ...spaghettiUserSeries],
|
|
261
|
+
[userSeries, spaghettiUserSeries],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const availableHorizons = useMemo(() => {
|
|
265
|
+
if (!performanceData) return [];
|
|
266
|
+
const driftModelData = performanceData['drift'];
|
|
267
|
+
if (!driftModelData) return [];
|
|
268
|
+
const forecasts = driftModelData?.forecasts;
|
|
269
|
+
if (forecasts && typeof forecasts === 'object') {
|
|
270
|
+
return Object.keys(forecasts).filter(key => key.startsWith('horizon_'));
|
|
271
|
+
}
|
|
272
|
+
return [];
|
|
273
|
+
}, [performanceData]);
|
|
274
|
+
|
|
275
|
+
const sortedHorizonKeysForCustom = useMemo(
|
|
276
|
+
() =>
|
|
277
|
+
[...availableHorizons].sort((a, b) => {
|
|
278
|
+
const na = parseInt(a.replace(/\D/g, ''), 10) || 0;
|
|
279
|
+
const nb = parseInt(b.replace(/\D/g, ''), 10) || 0;
|
|
280
|
+
return na - nb;
|
|
281
|
+
}),
|
|
282
|
+
[availableHorizons],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const parsedCustomMatrixForCharts = useMemo(() => {
|
|
286
|
+
if (!customPerformanceMatrix) return null;
|
|
287
|
+
return {
|
|
288
|
+
matrix: reorderSpaghettiMatrixToHorizons(
|
|
289
|
+
customPerformanceMatrix,
|
|
290
|
+
sortedHorizonKeysForCustom,
|
|
291
|
+
),
|
|
292
|
+
displayLabel: customPerformanceLabel?.trim() || 'Custom performance',
|
|
293
|
+
};
|
|
294
|
+
}, [
|
|
295
|
+
customPerformanceMatrix,
|
|
296
|
+
customPerformanceLabel,
|
|
297
|
+
sortedHorizonKeysForCustom,
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
const [selectedHorizon, setSelectedHorizon] = useState<string>(
|
|
301
|
+
availableHorizons[0] || 'horizon_1',
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (
|
|
306
|
+
availableHorizons.length > 0 &&
|
|
307
|
+
!availableHorizons.includes(selectedHorizon)
|
|
308
|
+
) {
|
|
309
|
+
setSelectedHorizon(availableHorizons[0]);
|
|
310
|
+
}
|
|
311
|
+
}, [availableHorizons, selectedHorizon]);
|
|
312
|
+
|
|
313
|
+
const getTimeRangeForForecastLength = useCallback(
|
|
314
|
+
(mode: OverlayMode): TimeRange => {
|
|
315
|
+
if (mode === 'pin') return '5y';
|
|
316
|
+
if (
|
|
317
|
+
performanceAnalysisId == null ||
|
|
318
|
+
!forecastData[String(performanceAnalysisId)]
|
|
319
|
+
) {
|
|
320
|
+
return TIME_RANGES[0];
|
|
321
|
+
}
|
|
322
|
+
const forecastDataForSelected =
|
|
323
|
+
forecastData[String(performanceAnalysisId)];
|
|
324
|
+
const dates = forecastDataForSelected.dates;
|
|
325
|
+
if (!dates || dates.length === 0) return TIME_RANGES[0];
|
|
326
|
+
const firstDate = new Date(dates[0]);
|
|
327
|
+
const lastDate = new Date(dates[dates.length - 1]);
|
|
328
|
+
const monthsDiff =
|
|
329
|
+
(lastDate.getFullYear() - firstDate.getFullYear()) * 12 +
|
|
330
|
+
(lastDate.getMonth() - firstDate.getMonth()) +
|
|
331
|
+
1;
|
|
332
|
+
if (monthsDiff < 6) return '6m';
|
|
333
|
+
if (monthsDiff < 12) return '1y';
|
|
334
|
+
if (monthsDiff < 36) return '3y';
|
|
335
|
+
if (monthsDiff < 60) return '5y';
|
|
336
|
+
return 'All';
|
|
337
|
+
},
|
|
338
|
+
[performanceAnalysisId, forecastData],
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const forecastModelsData = useMemo(() => {
|
|
342
|
+
if (!performanceData || !selectedHorizon) return [];
|
|
343
|
+
const models: { key: string; mae: number; mape: number }[] = [];
|
|
344
|
+
['model', 'drift'].forEach(key => {
|
|
345
|
+
const modelData = performanceData[key];
|
|
346
|
+
const horizonMetrics = modelData?.metrics_history?.[selectedHorizon];
|
|
347
|
+
if (horizonMetrics?.mae && horizonMetrics?.mape) {
|
|
348
|
+
const maeValues = Object.values(horizonMetrics.mae) as number[];
|
|
349
|
+
const mapeValues = Object.values(horizonMetrics.mape) as number[];
|
|
350
|
+
models.push({
|
|
351
|
+
key,
|
|
352
|
+
mae: maeValues.reduce((sum, val) => sum + val, 0) / maeValues.length,
|
|
353
|
+
mape:
|
|
354
|
+
mapeValues.reduce((sum, val) => sum + val, 0) / mapeValues.length,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
return models;
|
|
359
|
+
}, [performanceData, selectedHorizon]);
|
|
360
|
+
|
|
361
|
+
const historicalChartData = useMemo(
|
|
362
|
+
() => historicalData.map(point => ({ ...point })),
|
|
363
|
+
[historicalData],
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const chartForecastData = useMemo((): ForecastItemData[] => {
|
|
367
|
+
const forecastItems: ForecastItemData[] = [];
|
|
368
|
+
if (performanceData) {
|
|
369
|
+
['model', 'drift'].forEach((key, index) => {
|
|
370
|
+
const modelData = performanceData[key];
|
|
371
|
+
const horizonData = modelData?.forecasts?.[selectedHorizon];
|
|
372
|
+
if (horizonData) {
|
|
373
|
+
forecastItems.push({
|
|
374
|
+
id: index + 1000,
|
|
375
|
+
name: getForecastModelDisplayName(key),
|
|
376
|
+
color:
|
|
377
|
+
key === 'model' ? 'var(--sb-cyan-400)' : FORECAST_LINE_COLORS[0],
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (parsedCustomMatrixForCharts) {
|
|
383
|
+
const slice = getCustomMatrixSeriesForHorizonTab(
|
|
384
|
+
parsedCustomMatrixForCharts.matrix,
|
|
385
|
+
selectedHorizon,
|
|
386
|
+
SPAGHETTI_LOCAL_LS_USER_SERIES_ROW_ID,
|
|
387
|
+
forecastData,
|
|
388
|
+
);
|
|
389
|
+
if (slice) {
|
|
390
|
+
forecastItems.push({
|
|
391
|
+
id: slice.lineId,
|
|
392
|
+
name: parsedCustomMatrixForCharts.displayLabel,
|
|
393
|
+
color: SPAGHETTI_CUSTOM_PERFORMANCE_LINE_COLOR,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return forecastItems;
|
|
398
|
+
}, [
|
|
399
|
+
performanceData,
|
|
400
|
+
selectedHorizon,
|
|
401
|
+
parsedCustomMatrixForCharts,
|
|
402
|
+
forecastData,
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
const forecastChartData = useMemo(() => {
|
|
406
|
+
const chartDataMap = new Map<string, ChartDataPoint>();
|
|
407
|
+
const modelKeys = ['model', 'drift'];
|
|
408
|
+
const normalizeDate = (dateStr: string): string => dateStr.split(' ')[0];
|
|
409
|
+
const latestHistoricalDate =
|
|
410
|
+
historicalChartData.length > 0
|
|
411
|
+
? historicalChartData[historicalChartData.length - 1].date
|
|
412
|
+
: null;
|
|
413
|
+
|
|
414
|
+
historicalChartData.forEach(point => {
|
|
415
|
+
chartDataMap.set(point.date, { ...point });
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (performanceData) {
|
|
419
|
+
modelKeys.forEach(key => {
|
|
420
|
+
const index = modelKeys.indexOf(key);
|
|
421
|
+
const modelData = performanceData[key];
|
|
422
|
+
const horizonData = modelData?.forecasts?.[selectedHorizon];
|
|
423
|
+
const dataKey = `forecast_${index + 1000}`;
|
|
424
|
+
if (horizonData) {
|
|
425
|
+
Object.entries(horizonData).forEach(([date, value]) => {
|
|
426
|
+
if (value !== null && value !== undefined) {
|
|
427
|
+
const normalizedDate = normalizeDate(String(date));
|
|
428
|
+
if (
|
|
429
|
+
latestHistoricalDate &&
|
|
430
|
+
normalizedDate > latestHistoricalDate
|
|
431
|
+
) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const existingPoint = chartDataMap.get(normalizedDate);
|
|
435
|
+
if (existingPoint) {
|
|
436
|
+
chartDataMap.set(normalizedDate, {
|
|
437
|
+
...existingPoint,
|
|
438
|
+
[dataKey]: value as number,
|
|
439
|
+
});
|
|
440
|
+
} else {
|
|
441
|
+
chartDataMap.set(normalizedDate, {
|
|
442
|
+
date: normalizedDate,
|
|
443
|
+
[dataKey]: value as number,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const customSlice = parsedCustomMatrixForCharts
|
|
453
|
+
? getCustomMatrixSeriesForHorizonTab(
|
|
454
|
+
parsedCustomMatrixForCharts.matrix,
|
|
455
|
+
selectedHorizon,
|
|
456
|
+
SPAGHETTI_LOCAL_LS_USER_SERIES_ROW_ID,
|
|
457
|
+
forecastData,
|
|
458
|
+
)
|
|
459
|
+
: null;
|
|
460
|
+
if (customSlice) {
|
|
461
|
+
const dataKey = `forecast_${customSlice.lineId}`;
|
|
462
|
+
const n = Math.min(
|
|
463
|
+
customSlice.dates.length,
|
|
464
|
+
customSlice.forecastValues.length,
|
|
465
|
+
);
|
|
466
|
+
for (let i = 0; i < n; i++) {
|
|
467
|
+
const value = customSlice.forecastValues[i];
|
|
468
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) continue;
|
|
469
|
+
const normalizedDate = normalizeDate(String(customSlice.dates[i]));
|
|
470
|
+
if (latestHistoricalDate && normalizedDate > latestHistoricalDate) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const existingPoint = chartDataMap.get(normalizedDate);
|
|
474
|
+
if (existingPoint) {
|
|
475
|
+
chartDataMap.set(normalizedDate, {
|
|
476
|
+
...existingPoint,
|
|
477
|
+
[dataKey]: value,
|
|
478
|
+
});
|
|
479
|
+
} else {
|
|
480
|
+
chartDataMap.set(normalizedDate, {
|
|
481
|
+
date: normalizedDate,
|
|
482
|
+
[dataKey]: value,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const sortedData = Array.from(chartDataMap.values()).sort((a, b) =>
|
|
489
|
+
a.date.localeCompare(b.date),
|
|
490
|
+
);
|
|
491
|
+
if (latestHistoricalDate) {
|
|
492
|
+
return sortedData.filter(point => {
|
|
493
|
+
if (historicalChartData.some(h => h.date === point.date)) return true;
|
|
494
|
+
return point.date <= latestHistoricalDate;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return sortedData;
|
|
498
|
+
}, [
|
|
499
|
+
performanceData,
|
|
500
|
+
selectedHorizon,
|
|
501
|
+
historicalChartData,
|
|
502
|
+
parsedCustomMatrixForCharts,
|
|
503
|
+
forecastData,
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
const maxYScale = useMemo(() => {
|
|
507
|
+
if (!performanceData || availableHorizons.length === 0) return undefined;
|
|
508
|
+
const modelKeys = ['model', 'drift'];
|
|
509
|
+
const normalizeDate = (dateStr: string): string => dateStr.split(' ')[0];
|
|
510
|
+
const latestHistoricalDate =
|
|
511
|
+
historicalChartData.length > 0
|
|
512
|
+
? historicalChartData[historicalChartData.length - 1].date
|
|
513
|
+
: null;
|
|
514
|
+
const allValues: number[] = [];
|
|
515
|
+
|
|
516
|
+
availableHorizons.forEach(horizon => {
|
|
517
|
+
const chartDataMap = new Map<string, ChartDataPoint>();
|
|
518
|
+
historicalChartData.forEach(point => {
|
|
519
|
+
chartDataMap.set(point.date, { ...point });
|
|
520
|
+
});
|
|
521
|
+
modelKeys.forEach(key => {
|
|
522
|
+
const index = modelKeys.indexOf(key);
|
|
523
|
+
const modelData = performanceData[key];
|
|
524
|
+
const horizonData = modelData?.forecasts?.[horizon];
|
|
525
|
+
const dataKey = `forecast_${index + 1000}`;
|
|
526
|
+
if (horizonData) {
|
|
527
|
+
Object.entries(horizonData).forEach(([date, value]) => {
|
|
528
|
+
if (value !== null && value !== undefined) {
|
|
529
|
+
const normalizedDate = normalizeDate(String(date));
|
|
530
|
+
if (
|
|
531
|
+
latestHistoricalDate &&
|
|
532
|
+
normalizedDate > latestHistoricalDate
|
|
533
|
+
) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const existingPoint = chartDataMap.get(normalizedDate);
|
|
537
|
+
if (existingPoint) {
|
|
538
|
+
chartDataMap.set(normalizedDate, {
|
|
539
|
+
...existingPoint,
|
|
540
|
+
[dataKey]: value as number,
|
|
541
|
+
});
|
|
542
|
+
} else {
|
|
543
|
+
chartDataMap.set(normalizedDate, {
|
|
544
|
+
date: normalizedDate,
|
|
545
|
+
[dataKey]: value as number,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
if (parsedCustomMatrixForCharts) {
|
|
553
|
+
const customSlice = getCustomMatrixSeriesForHorizonTab(
|
|
554
|
+
parsedCustomMatrixForCharts.matrix,
|
|
555
|
+
horizon,
|
|
556
|
+
SPAGHETTI_LOCAL_LS_USER_SERIES_ROW_ID,
|
|
557
|
+
forecastData,
|
|
558
|
+
);
|
|
559
|
+
if (customSlice) {
|
|
560
|
+
const dataKey = `forecast_${customSlice.lineId}`;
|
|
561
|
+
const n = Math.min(
|
|
562
|
+
customSlice.dates.length,
|
|
563
|
+
customSlice.forecastValues.length,
|
|
564
|
+
);
|
|
565
|
+
for (let i = 0; i < n; i++) {
|
|
566
|
+
const value = customSlice.forecastValues[i];
|
|
567
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) continue;
|
|
568
|
+
const normalizedDate = normalizeDate(String(customSlice.dates[i]));
|
|
569
|
+
if (latestHistoricalDate && normalizedDate > latestHistoricalDate) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const existingPoint = chartDataMap.get(normalizedDate);
|
|
573
|
+
if (existingPoint) {
|
|
574
|
+
chartDataMap.set(normalizedDate, {
|
|
575
|
+
...existingPoint,
|
|
576
|
+
[dataKey]: value,
|
|
577
|
+
});
|
|
578
|
+
} else {
|
|
579
|
+
chartDataMap.set(normalizedDate, {
|
|
580
|
+
date: normalizedDate,
|
|
581
|
+
[dataKey]: value,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const sortedData = Array.from(chartDataMap.values()).sort((a, b) =>
|
|
588
|
+
a.date.localeCompare(b.date),
|
|
589
|
+
);
|
|
590
|
+
const filteredData = latestHistoricalDate
|
|
591
|
+
? sortedData.filter(point => {
|
|
592
|
+
if (historicalChartData.some(h => h.date === point.date)) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
return point.date <= latestHistoricalDate;
|
|
596
|
+
})
|
|
597
|
+
: sortedData;
|
|
598
|
+
if (filteredData.length > 0) {
|
|
599
|
+
const cutoffDate = new Date(filteredData[filteredData.length - 1].date);
|
|
600
|
+
cutoffDate.setMonth(cutoffDate.getMonth() - MONTHS_24);
|
|
601
|
+
filteredData
|
|
602
|
+
.filter(point => new Date(point.date) >= cutoffDate)
|
|
603
|
+
.forEach(point => {
|
|
604
|
+
Object.entries(point).forEach(([key, value]) => {
|
|
605
|
+
if (key === 'date') return;
|
|
606
|
+
if (typeof value === 'number') allValues.push(value);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
if (allValues.length === 0) return undefined;
|
|
613
|
+
const min = Math.min(...allValues);
|
|
614
|
+
const max = Math.max(...allValues);
|
|
615
|
+
const diff = max - min;
|
|
616
|
+
return { yMin: min - diff * 0.1, yMax: max + diff * 0.1 };
|
|
617
|
+
}, [
|
|
618
|
+
performanceData,
|
|
619
|
+
availableHorizons,
|
|
620
|
+
historicalChartData,
|
|
621
|
+
parsedCustomMatrixForCharts,
|
|
622
|
+
forecastData,
|
|
623
|
+
]);
|
|
624
|
+
|
|
625
|
+
const filtered24mData = useMemo(() => {
|
|
626
|
+
const dataToUse =
|
|
627
|
+
forecastChartData.length > 0 ? forecastChartData : combinedData;
|
|
628
|
+
if (dataToUse.length === 0) return [];
|
|
629
|
+
const sorted = [...dataToUse].sort((a, b) => a.date.localeCompare(b.date));
|
|
630
|
+
const cutoffDate = new Date(sorted[sorted.length - 1].date);
|
|
631
|
+
cutoffDate.setMonth(cutoffDate.getMonth() - MONTHS_24);
|
|
632
|
+
return sorted.filter(point => new Date(point.date) >= cutoffDate);
|
|
633
|
+
}, [forecastChartData, combinedData]);
|
|
634
|
+
|
|
635
|
+
const historicalByDateForCustomMetrics = useMemo(() => {
|
|
636
|
+
const norm = (d: string) =>
|
|
637
|
+
normalizeToMonthStart(String(d).split(' ')[0] ?? d);
|
|
638
|
+
const m = new Map<string, number>();
|
|
639
|
+
historicalChartData.forEach(p => {
|
|
640
|
+
const v = p.historical;
|
|
641
|
+
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
642
|
+
m.set(norm(p.date), v);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
return m;
|
|
646
|
+
}, [historicalChartData]);
|
|
647
|
+
|
|
648
|
+
const customPerformanceTableRow = useMemo(() => {
|
|
649
|
+
if (!parsedCustomMatrixForCharts) return null;
|
|
650
|
+
const colIdx = sortedHorizonKeysForCustom.indexOf(selectedHorizon);
|
|
651
|
+
if (colIdx < 0) return null;
|
|
652
|
+
const errs = averageForecastErrorsVsHistoricalForMatrixColumn(
|
|
653
|
+
parsedCustomMatrixForCharts.matrix,
|
|
654
|
+
colIdx,
|
|
655
|
+
historicalByDateForCustomMetrics,
|
|
656
|
+
);
|
|
657
|
+
if (!errs) return null;
|
|
658
|
+
return {
|
|
659
|
+
label: parsedCustomMatrixForCharts.displayLabel,
|
|
660
|
+
mae: errs.mae,
|
|
661
|
+
mape: errs.mape,
|
|
662
|
+
};
|
|
663
|
+
}, [
|
|
664
|
+
parsedCustomMatrixForCharts,
|
|
665
|
+
sortedHorizonKeysForCustom,
|
|
666
|
+
selectedHorizon,
|
|
667
|
+
historicalByDateForCustomMetrics,
|
|
668
|
+
]);
|
|
669
|
+
|
|
670
|
+
const perHorizonWindowStart = useMemo(() => {
|
|
671
|
+
if (filtered24mData.length === 0) return null;
|
|
672
|
+
const sorted = [...filtered24mData].sort((a, b) =>
|
|
673
|
+
a.date.localeCompare(b.date),
|
|
674
|
+
);
|
|
675
|
+
return sorted[0]?.date ?? null;
|
|
676
|
+
}, [filtered24mData]);
|
|
677
|
+
|
|
678
|
+
const perHorizonWindowEnd = useMemo(() => {
|
|
679
|
+
if (filtered24mData.length === 0) return null;
|
|
680
|
+
const sorted = [...filtered24mData].sort((a, b) =>
|
|
681
|
+
a.date.localeCompare(b.date),
|
|
682
|
+
);
|
|
683
|
+
return sorted[sorted.length - 1]?.date ?? null;
|
|
684
|
+
}, [filtered24mData]);
|
|
685
|
+
|
|
686
|
+
const modelPerHorizonEntries = useMemo(
|
|
687
|
+
() =>
|
|
688
|
+
buildPerHorizonSpaghettiEntries(
|
|
689
|
+
performanceData?.model,
|
|
690
|
+
availableHorizons,
|
|
691
|
+
),
|
|
692
|
+
[performanceData, availableHorizons],
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
const driftPerHorizonEntries = useMemo(
|
|
696
|
+
() =>
|
|
697
|
+
buildPerHorizonSpaghettiEntries(
|
|
698
|
+
performanceData?.drift,
|
|
699
|
+
availableHorizons,
|
|
700
|
+
),
|
|
701
|
+
[performanceData, availableHorizons],
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const spaghettiPipeline = useMemo(() => {
|
|
705
|
+
if (viewTab !== 'spaghetti') {
|
|
706
|
+
return {
|
|
707
|
+
merged: {
|
|
708
|
+
mergedData: [] as ChartDataPoint[],
|
|
709
|
+
seriesMeta: [] as { id: number; label: string }[],
|
|
710
|
+
},
|
|
711
|
+
driftSeriesMeta: [] as { id: number; label: string }[],
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const driftMerged =
|
|
715
|
+
driftPerHorizonEntries.length > 0
|
|
716
|
+
? buildSpaghettiMergedChartData(
|
|
717
|
+
driftPerHorizonEntries,
|
|
718
|
+
historicalChartData,
|
|
719
|
+
SPAGHETTI_DRIFT_PER_HORIZON_ID_BASE,
|
|
720
|
+
)
|
|
721
|
+
: { mergedData: [] as ChartDataPoint[], seriesMeta: [] };
|
|
722
|
+
const modelMerged =
|
|
723
|
+
modelPerHorizonEntries.length > 0
|
|
724
|
+
? buildSpaghettiMergedChartData(
|
|
725
|
+
modelPerHorizonEntries,
|
|
726
|
+
historicalChartData,
|
|
727
|
+
SPAGHETTI_MODEL_PER_HORIZON_ID_BASE,
|
|
728
|
+
)
|
|
729
|
+
: { mergedData: [] as ChartDataPoint[], seriesMeta: [] };
|
|
730
|
+
|
|
731
|
+
let merged: {
|
|
732
|
+
mergedData: ChartDataPoint[];
|
|
733
|
+
seriesMeta: { id: number; label: string }[];
|
|
734
|
+
};
|
|
735
|
+
if (
|
|
736
|
+
driftMerged.seriesMeta.length === 0 &&
|
|
737
|
+
modelMerged.seriesMeta.length === 0
|
|
738
|
+
) {
|
|
739
|
+
const historicalOnly =
|
|
740
|
+
historicalChartData.length > 0
|
|
741
|
+
? [...historicalChartData]
|
|
742
|
+
.map(p => ({
|
|
743
|
+
...p,
|
|
744
|
+
date: normalizeToMonthStart(
|
|
745
|
+
String(p.date).split(' ')[0] ?? p.date,
|
|
746
|
+
),
|
|
747
|
+
}))
|
|
748
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
749
|
+
: [];
|
|
750
|
+
merged = { mergedData: historicalOnly, seriesMeta: [] };
|
|
751
|
+
} else if (driftMerged.seriesMeta.length === 0) {
|
|
752
|
+
merged = modelMerged;
|
|
753
|
+
} else if (modelMerged.seriesMeta.length === 0) {
|
|
754
|
+
merged = driftMerged;
|
|
755
|
+
} else {
|
|
756
|
+
merged = mergeSpaghettiMergedBases(driftMerged, modelMerged);
|
|
757
|
+
}
|
|
758
|
+
return { merged, driftSeriesMeta: driftMerged.seriesMeta };
|
|
759
|
+
}, [
|
|
760
|
+
viewTab,
|
|
761
|
+
driftPerHorizonEntries,
|
|
762
|
+
historicalChartData,
|
|
763
|
+
modelPerHorizonEntries,
|
|
764
|
+
]);
|
|
765
|
+
|
|
766
|
+
const spaghettiWithUserSeries = useMemo(() => {
|
|
767
|
+
if (viewTab !== 'spaghetti') {
|
|
768
|
+
return { mergedData: [] as ChartDataPoint[], seriesMeta: [] };
|
|
769
|
+
}
|
|
770
|
+
return mergeSpaghettiUserSeriesFromForecastData(
|
|
771
|
+
spaghettiPipeline.merged,
|
|
772
|
+
userSeriesForSpaghetti,
|
|
773
|
+
forecastData,
|
|
774
|
+
);
|
|
775
|
+
}, [viewTab, spaghettiPipeline.merged, userSeriesForSpaghetti, forecastData]);
|
|
776
|
+
|
|
777
|
+
const spaghettiFilteredChartData = useMemo(() => {
|
|
778
|
+
if (viewTab !== 'spaghetti') return [];
|
|
779
|
+
const filtered = filterSpaghettiDataFromEarliestForecastStart(
|
|
780
|
+
spaghettiWithUserSeries.mergedData,
|
|
781
|
+
[],
|
|
782
|
+
userSeriesForSpaghetti,
|
|
783
|
+
forecastData,
|
|
784
|
+
spaghettiPipeline.driftSeriesMeta,
|
|
785
|
+
MONTHS_24,
|
|
786
|
+
[...modelPerHorizonEntries, ...driftPerHorizonEntries],
|
|
787
|
+
perHorizonWindowStart,
|
|
788
|
+
perHorizonWindowEnd,
|
|
789
|
+
);
|
|
790
|
+
return mergeHistoricalIntoSpaghettiChartData(filtered, historicalChartData);
|
|
791
|
+
}, [
|
|
792
|
+
viewTab,
|
|
793
|
+
spaghettiWithUserSeries.mergedData,
|
|
794
|
+
userSeriesForSpaghetti,
|
|
795
|
+
forecastData,
|
|
796
|
+
spaghettiPipeline.driftSeriesMeta,
|
|
797
|
+
modelPerHorizonEntries,
|
|
798
|
+
driftPerHorizonEntries,
|
|
799
|
+
perHorizonWindowStart,
|
|
800
|
+
perHorizonWindowEnd,
|
|
801
|
+
historicalChartData,
|
|
802
|
+
]);
|
|
803
|
+
|
|
804
|
+
const spaghettiDisplayedForScale = useMemo(
|
|
805
|
+
() =>
|
|
806
|
+
filterDataForTimeRange(
|
|
807
|
+
spaghettiFilteredChartData,
|
|
808
|
+
spaghettiTimeRange,
|
|
809
|
+
undefined,
|
|
810
|
+
),
|
|
811
|
+
[spaghettiFilteredChartData, spaghettiTimeRange],
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
const spaghettiYScale = useMemo(() => {
|
|
815
|
+
if (spaghettiDisplayedForScale.length === 0) return undefined;
|
|
816
|
+
return calculateYRangeFromChartData(spaghettiDisplayedForScale);
|
|
817
|
+
}, [spaghettiDisplayedForScale]);
|
|
818
|
+
|
|
819
|
+
const spaghettiForecastItems = useMemo((): ForecastItemData[] => {
|
|
820
|
+
return spaghettiWithUserSeries.seriesMeta.map((meta, i) => ({
|
|
821
|
+
id: meta.id,
|
|
822
|
+
name: meta.label,
|
|
823
|
+
color: spaghettiForecastLineColor(meta.id, i),
|
|
824
|
+
}));
|
|
825
|
+
}, [spaghettiWithUserSeries.seriesMeta]);
|
|
826
|
+
|
|
827
|
+
useEffect(() => {
|
|
828
|
+
if (
|
|
829
|
+
performanceAnalysisId != null &&
|
|
830
|
+
forecastData[String(performanceAnalysisId)] &&
|
|
831
|
+
performanceData
|
|
832
|
+
) {
|
|
833
|
+
setSpaghettiTimeRange(getTimeRangeForForecastLength('pin'));
|
|
834
|
+
}
|
|
835
|
+
}, [
|
|
836
|
+
performanceAnalysisId,
|
|
837
|
+
forecastData,
|
|
838
|
+
performanceData,
|
|
839
|
+
getTimeRangeForForecastLength,
|
|
840
|
+
]);
|
|
841
|
+
|
|
842
|
+
useEffect(() => {
|
|
843
|
+
if (isEmpty && viewTab === 'spaghetti') {
|
|
844
|
+
setSpaghettiTimeRange('All');
|
|
845
|
+
}
|
|
846
|
+
}, [isEmpty, viewTab]);
|
|
847
|
+
|
|
848
|
+
const updatePinMonth = useCallback((month: string | undefined) => {
|
|
849
|
+
if (month) setPinMonth(month);
|
|
850
|
+
}, []);
|
|
851
|
+
|
|
852
|
+
const handlePerformanceChartTimeRangeChange = useCallback(
|
|
853
|
+
(val: string) => {
|
|
854
|
+
if (!val) return;
|
|
855
|
+
onTimeRangeChange?.(val, { viewTab });
|
|
856
|
+
if (timeRangeProp === undefined) {
|
|
857
|
+
if (viewTab === 'spaghetti') setSpaghettiTimeRange(val);
|
|
858
|
+
else setPerHorizonChartTimeRange(val);
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
[onTimeRangeChange, timeRangeProp, viewTab],
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
useEffect(() => {
|
|
865
|
+
if (filtered24mData.length > 0 && historicalData.length > 0) {
|
|
866
|
+
const lastHistoricalPoint = historicalData[historicalData.length - 1];
|
|
867
|
+
const date = new Date(lastHistoricalPoint.date);
|
|
868
|
+
updatePinMonth(`${MONTH_NAMES[date.getMonth()]} ${date.getFullYear()}`);
|
|
869
|
+
}
|
|
870
|
+
}, [filtered24mData, historicalData, updatePinMonth]);
|
|
871
|
+
|
|
872
|
+
useEffect(() => {
|
|
873
|
+
if (hiddenSeriesControlled) return;
|
|
874
|
+
if (viewTab === 'spaghetti') {
|
|
875
|
+
setHiddenSeries(prev => {
|
|
876
|
+
const next = new Set(prev);
|
|
877
|
+
next.delete('forecast_1000');
|
|
878
|
+
next.delete('forecast_1001');
|
|
879
|
+
return next;
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}, [viewTab, hiddenSeriesControlled, setHiddenSeries]);
|
|
883
|
+
|
|
884
|
+
useEffect(() => {
|
|
885
|
+
if (hiddenSeriesControlled) return;
|
|
886
|
+
if (viewTab === 'perHorizon') {
|
|
887
|
+
setHiddenSeries(prev => {
|
|
888
|
+
const next = new Set(prev);
|
|
889
|
+
let changed = false;
|
|
890
|
+
prev.forEach(k => {
|
|
891
|
+
const m = /^forecast_(\d+)$/.exec(k);
|
|
892
|
+
if (!m) return;
|
|
893
|
+
const id = parseInt(m[1], 10);
|
|
894
|
+
if (id >= 6000 && id < 9000) {
|
|
895
|
+
next.delete(k);
|
|
896
|
+
changed = true;
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
return changed ? next : prev;
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}, [viewTab, hiddenSeriesControlled, setHiddenSeries]);
|
|
903
|
+
|
|
904
|
+
const performanceLineIdsForLegend = useMemo(
|
|
905
|
+
() =>
|
|
906
|
+
spaghettiWithUserSeries.seriesMeta
|
|
907
|
+
.filter(m => isSpaghettiModelPerHorizonLineId(m.id))
|
|
908
|
+
.map(m => m.id),
|
|
909
|
+
[spaghettiWithUserSeries.seriesMeta],
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
const driftLineIdsForLegend = useMemo(
|
|
913
|
+
() =>
|
|
914
|
+
spaghettiWithUserSeries.seriesMeta
|
|
915
|
+
.filter(m => isSpaghettiDriftPerHorizonLineId(m.id))
|
|
916
|
+
.map(m => m.id),
|
|
917
|
+
[spaghettiWithUserSeries.seriesMeta],
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
const customMatrixLineIdsForLegend = useMemo(
|
|
921
|
+
() =>
|
|
922
|
+
spaghettiWithUserSeries.seriesMeta
|
|
923
|
+
.filter(m => isSpaghettiMatrixSyntheticLineId(m.id))
|
|
924
|
+
.map(m => m.id),
|
|
925
|
+
[spaghettiWithUserSeries.seriesMeta],
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
const customMatrixLegendGroupLabel = useMemo(() => {
|
|
929
|
+
const first = spaghettiWithUserSeries.seriesMeta.find(m =>
|
|
930
|
+
isSpaghettiMatrixSyntheticLineId(m.id),
|
|
931
|
+
);
|
|
932
|
+
if (!first?.label) return 'Custom performance';
|
|
933
|
+
const head = first.label.split(' · ')[0]?.trim();
|
|
934
|
+
return head || 'Custom performance';
|
|
935
|
+
}, [spaghettiWithUserSeries.seriesMeta]);
|
|
936
|
+
|
|
937
|
+
const performanceGroupHidden = useMemo(() => {
|
|
938
|
+
if (performanceLineIdsForLegend.length === 0) return false;
|
|
939
|
+
return performanceLineIdsForLegend.every(id =>
|
|
940
|
+
hiddenSeries.has(`forecast_${id}`),
|
|
941
|
+
);
|
|
942
|
+
}, [performanceLineIdsForLegend, hiddenSeries]);
|
|
943
|
+
|
|
944
|
+
const driftGroupHidden = useMemo(() => {
|
|
945
|
+
if (driftLineIdsForLegend.length === 0) return false;
|
|
946
|
+
return driftLineIdsForLegend.every(id =>
|
|
947
|
+
hiddenSeries.has(`forecast_${id}`),
|
|
948
|
+
);
|
|
949
|
+
}, [driftLineIdsForLegend, hiddenSeries]);
|
|
950
|
+
|
|
951
|
+
const customMatrixGroupHidden = useMemo(() => {
|
|
952
|
+
if (customMatrixLineIdsForLegend.length === 0) return false;
|
|
953
|
+
return customMatrixLineIdsForLegend.every(id =>
|
|
954
|
+
hiddenSeries.has(`forecast_${id}`),
|
|
955
|
+
);
|
|
956
|
+
}, [customMatrixLineIdsForLegend, hiddenSeries]);
|
|
957
|
+
|
|
958
|
+
const toggleSpaghettiGroup = useCallback(
|
|
959
|
+
(group: 'performance' | 'drift' | 'custom') => {
|
|
960
|
+
const ids =
|
|
961
|
+
group === 'performance'
|
|
962
|
+
? performanceLineIdsForLegend
|
|
963
|
+
: group === 'drift'
|
|
964
|
+
? driftLineIdsForLegend
|
|
965
|
+
: customMatrixLineIdsForLegend;
|
|
966
|
+
if (ids.length === 0) return;
|
|
967
|
+
setHiddenSeries(prev => {
|
|
968
|
+
const next = new Set(prev);
|
|
969
|
+
const allHidden = ids.every(id => next.has(`forecast_${id}`));
|
|
970
|
+
if (allHidden) ids.forEach(id => next.delete(`forecast_${id}`));
|
|
971
|
+
else ids.forEach(id => next.add(`forecast_${id}`));
|
|
972
|
+
return next;
|
|
973
|
+
});
|
|
974
|
+
},
|
|
975
|
+
[
|
|
976
|
+
performanceLineIdsForLegend,
|
|
977
|
+
driftLineIdsForLegend,
|
|
978
|
+
customMatrixLineIdsForLegend,
|
|
979
|
+
setHiddenSeries,
|
|
980
|
+
],
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
const performanceHistoricalLegendLineColor = isDarkTheme
|
|
984
|
+
? '#ffffff'
|
|
985
|
+
: '#000000';
|
|
986
|
+
|
|
987
|
+
const performanceUnderChartLegend = useMemo(() => {
|
|
988
|
+
if (isEmpty) return null;
|
|
989
|
+
const items: PerformanceUnderChartLegendItemConfig[] = [];
|
|
990
|
+
if (historicalChartData.length > 0) {
|
|
991
|
+
items.push({
|
|
992
|
+
id: 'historical',
|
|
993
|
+
label: 'Historical',
|
|
994
|
+
lineColor: performanceHistoricalLegendLineColor,
|
|
995
|
+
lineStyle: 'solid',
|
|
996
|
+
hidden: hiddenSeries.has('historical'),
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
if (viewTab === 'perHorizon') {
|
|
1000
|
+
chartForecastData.forEach(item => {
|
|
1001
|
+
items.push({
|
|
1002
|
+
id: `forecast-${item.id}`,
|
|
1003
|
+
label: item.name ?? `Series ${item.id}`,
|
|
1004
|
+
lineColor: String(item.color ?? 'var(--foreground)'),
|
|
1005
|
+
lineStyle: 'dashed',
|
|
1006
|
+
hidden: hiddenSeries.has(`forecast_${item.id}`),
|
|
1007
|
+
onToggle: () => toggleLegendSeriesKey(`forecast_${item.id}`),
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
if (viewTab === 'spaghetti') {
|
|
1012
|
+
if (performanceLineIdsForLegend.length > 0) {
|
|
1013
|
+
items.push({
|
|
1014
|
+
id: 'spaghetti-performance',
|
|
1015
|
+
label: 'Sybilion AI',
|
|
1016
|
+
lineColor: SPAGHETTI_MODEL_LINE_COLOR,
|
|
1017
|
+
lineStyle: 'dashed',
|
|
1018
|
+
hidden: performanceGroupHidden,
|
|
1019
|
+
onToggle: () => toggleSpaghettiGroup('performance'),
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (driftLineIdsForLegend.length > 0) {
|
|
1023
|
+
items.push({
|
|
1024
|
+
id: 'spaghetti-drift',
|
|
1025
|
+
label: getForecastModelDisplayName('drift'),
|
|
1026
|
+
lineColor: SPAGHETTI_DRIFT_LINE_COLOR,
|
|
1027
|
+
lineStyle: 'dashed',
|
|
1028
|
+
hidden: driftGroupHidden,
|
|
1029
|
+
onToggle: () => toggleSpaghettiGroup('drift'),
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (customMatrixLineIdsForLegend.length > 0) {
|
|
1033
|
+
items.push({
|
|
1034
|
+
id: 'spaghetti-custom-matrix',
|
|
1035
|
+
label: customMatrixLegendGroupLabel,
|
|
1036
|
+
lineColor: SPAGHETTI_CUSTOM_PERFORMANCE_LINE_COLOR,
|
|
1037
|
+
lineStyle: 'dashed',
|
|
1038
|
+
hidden: customMatrixGroupHidden,
|
|
1039
|
+
onToggle: () => toggleSpaghettiGroup('custom'),
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return <PerformanceUnderChartLegendFromItems items={items} />;
|
|
1044
|
+
}, [
|
|
1045
|
+
isEmpty,
|
|
1046
|
+
viewTab,
|
|
1047
|
+
historicalChartData.length,
|
|
1048
|
+
chartForecastData,
|
|
1049
|
+
performanceHistoricalLegendLineColor,
|
|
1050
|
+
hiddenSeries,
|
|
1051
|
+
toggleLegendSeriesKey,
|
|
1052
|
+
performanceLineIdsForLegend.length,
|
|
1053
|
+
driftLineIdsForLegend.length,
|
|
1054
|
+
performanceGroupHidden,
|
|
1055
|
+
driftGroupHidden,
|
|
1056
|
+
toggleSpaghettiGroup,
|
|
1057
|
+
customMatrixLineIdsForLegend.length,
|
|
1058
|
+
customMatrixLegendGroupLabel,
|
|
1059
|
+
customMatrixGroupHidden,
|
|
1060
|
+
]);
|
|
1061
|
+
|
|
1062
|
+
const isSpaghettiView = viewTab === 'spaghetti';
|
|
1063
|
+
const hasSpaghettiPoints = spaghettiFilteredChartData.length > 0;
|
|
1064
|
+
const spaghettiChartLoading = isSpaghettiView && performanceSectionPending;
|
|
1065
|
+
const spaghettiTabShowsChartData = isSpaghettiView && hasSpaghettiPoints;
|
|
1066
|
+
const spaghettiOverridesPerformanceEmpty =
|
|
1067
|
+
isSpaghettiView &&
|
|
1068
|
+
(hasSpaghettiPoints || performanceDataLoading || spaghettiChartLoading);
|
|
1069
|
+
|
|
1070
|
+
const isLoaded = useMemo(
|
|
1071
|
+
() =>
|
|
1072
|
+
!loading &&
|
|
1073
|
+
!performanceDataLoading &&
|
|
1074
|
+
perfFetchSettled &&
|
|
1075
|
+
(!isEmpty || spaghettiOverridesPerformanceEmpty),
|
|
1076
|
+
[
|
|
1077
|
+
loading,
|
|
1078
|
+
performanceDataLoading,
|
|
1079
|
+
perfFetchSettled,
|
|
1080
|
+
isEmpty,
|
|
1081
|
+
spaghettiOverridesPerformanceEmpty,
|
|
1082
|
+
],
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
const spaghettiNoDataMessage =
|
|
1086
|
+
isSpaghettiView &&
|
|
1087
|
+
perfFetchSettled &&
|
|
1088
|
+
!performanceDataLoading &&
|
|
1089
|
+
spaghettiFilteredChartData.length === 0
|
|
1090
|
+
? 'No spaghetti plot data for this dataset.'
|
|
1091
|
+
: undefined;
|
|
1092
|
+
|
|
1093
|
+
const activeTimeRange =
|
|
1094
|
+
timeRangeProp ??
|
|
1095
|
+
(isSpaghettiView ? spaghettiTimeRange : perHorizonChartTimeRange);
|
|
1096
|
+
|
|
1097
|
+
const perHorizonChartProps =
|
|
1098
|
+
viewTab === 'perHorizon'
|
|
1099
|
+
? {
|
|
1100
|
+
chartData: filtered24mData,
|
|
1101
|
+
forecastData: chartForecastData,
|
|
1102
|
+
yMin: maxYScale?.yMin,
|
|
1103
|
+
yMax: maxYScale?.yMax,
|
|
1104
|
+
loading: performanceSectionPending,
|
|
1105
|
+
noDataMessage: undefined as string | undefined,
|
|
1106
|
+
noDataStatusTone: undefined as 'muted' | 'destructive' | undefined,
|
|
1107
|
+
hasCombinedData: undefined as boolean | undefined,
|
|
1108
|
+
}
|
|
1109
|
+
: {
|
|
1110
|
+
chartData: spaghettiFilteredChartData,
|
|
1111
|
+
forecastData: spaghettiForecastItems,
|
|
1112
|
+
yMin: spaghettiYScale?.yMin ?? maxYScale?.yMin,
|
|
1113
|
+
yMax: spaghettiYScale?.yMax ?? maxYScale?.yMax,
|
|
1114
|
+
loading: spaghettiChartLoading,
|
|
1115
|
+
noDataMessage: spaghettiNoDataMessage,
|
|
1116
|
+
noDataStatusTone: spaghettiNoDataMessage
|
|
1117
|
+
? ('destructive' as const)
|
|
1118
|
+
: undefined,
|
|
1119
|
+
hasCombinedData: spaghettiFilteredChartData.length > 0,
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
const showEmptyOverlay =
|
|
1123
|
+
(runAnalysisHint || Boolean(statusHint) || isEmpty) && !loading;
|
|
1124
|
+
|
|
1125
|
+
return (
|
|
1126
|
+
<div
|
|
1127
|
+
className={cn(
|
|
1128
|
+
S.root,
|
|
1129
|
+
performanceSectionPending && !spaghettiTabShowsChartData && S.loading,
|
|
1130
|
+
isLoaded && S.loaded,
|
|
1131
|
+
isEmpty && S.isEmpty,
|
|
1132
|
+
className,
|
|
1133
|
+
)}
|
|
1134
|
+
>
|
|
1135
|
+
<Tabs
|
|
1136
|
+
variant="button"
|
|
1137
|
+
value={viewTab}
|
|
1138
|
+
onValueChange={v => {
|
|
1139
|
+
if (v === 'perHorizon' || v === 'spaghetti') {
|
|
1140
|
+
setViewTab(v);
|
|
1141
|
+
}
|
|
1142
|
+
}}
|
|
1143
|
+
className={S.viewTabs}
|
|
1144
|
+
>
|
|
1145
|
+
<div className={S.toolbarRow}>
|
|
1146
|
+
{toolbarStart}
|
|
1147
|
+
<TabsList className={S.viewTabsList}>
|
|
1148
|
+
<TabsTrigger value="perHorizon">Per horizon plot</TabsTrigger>
|
|
1149
|
+
<TabsTrigger value="spaghetti">Spaghetti plots</TabsTrigger>
|
|
1150
|
+
</TabsList>
|
|
1151
|
+
<div className={S.toolBarRight}>
|
|
1152
|
+
{viewTab === 'perHorizon' && performanceData && (
|
|
1153
|
+
<HorizonsSelector
|
|
1154
|
+
selectedHorizon={selectedHorizon}
|
|
1155
|
+
onHorizonChange={setSelectedHorizon}
|
|
1156
|
+
availableHorizons={availableHorizons}
|
|
1157
|
+
/>
|
|
1158
|
+
)}
|
|
1159
|
+
</div>
|
|
1160
|
+
</div>
|
|
1161
|
+
|
|
1162
|
+
<div
|
|
1163
|
+
className={cn(
|
|
1164
|
+
S.perHorizonChartWrap,
|
|
1165
|
+
performanceSectionPending && S.perHorizonChartWrapLoading,
|
|
1166
|
+
)}
|
|
1167
|
+
>
|
|
1168
|
+
<ChartAreaInteractive
|
|
1169
|
+
disableHistoricalAnimation
|
|
1170
|
+
disableTimeRangeSelector
|
|
1171
|
+
enableTimeRangeBrush
|
|
1172
|
+
disableAnimation={isSpaghettiView}
|
|
1173
|
+
chartContainerClassName={S.chartContainer}
|
|
1174
|
+
chartData={perHorizonChartProps.chartData}
|
|
1175
|
+
forecastData={perHorizonChartProps.forecastData}
|
|
1176
|
+
timeRange={activeTimeRange}
|
|
1177
|
+
onTimeRangeChange={handlePerformanceChartTimeRangeChange}
|
|
1178
|
+
pinMonth={pinMonth}
|
|
1179
|
+
onPinMonthChange={updatePinMonth}
|
|
1180
|
+
isDarkTheme={isDarkTheme}
|
|
1181
|
+
loading={perHorizonChartProps.loading || chartLoading}
|
|
1182
|
+
yMin={perHorizonChartProps.yMin}
|
|
1183
|
+
yMax={perHorizonChartProps.yMax}
|
|
1184
|
+
autoScaleYAxis={false}
|
|
1185
|
+
maxVisibleItems={2}
|
|
1186
|
+
preventDeselection
|
|
1187
|
+
showLegend={false}
|
|
1188
|
+
footerActions={performanceUnderChartLegend ?? undefined}
|
|
1189
|
+
hasCombinedData={perHorizonChartProps.hasCombinedData}
|
|
1190
|
+
hiddenSeries={hiddenSeries}
|
|
1191
|
+
toggleLegendSeries={toggleLegendSeriesKey}
|
|
1192
|
+
ensureAnalysisSeriesVisible={ensureSeriesVisible}
|
|
1193
|
+
/>
|
|
1194
|
+
|
|
1195
|
+
{showEmptyOverlay && (
|
|
1196
|
+
<div
|
|
1197
|
+
className={S.chartEmptyOverlay}
|
|
1198
|
+
role="status"
|
|
1199
|
+
aria-live="polite"
|
|
1200
|
+
>
|
|
1201
|
+
<ChartEmptyState
|
|
1202
|
+
variant="inline"
|
|
1203
|
+
hint={
|
|
1204
|
+
runAnalysisHint ? (
|
|
1205
|
+
'Run a completed analysis to load performance data for an analysis.'
|
|
1206
|
+
) : isEmpty ? (
|
|
1207
|
+
<>
|
|
1208
|
+
No performance data for this dataset.
|
|
1209
|
+
<br />
|
|
1210
|
+
Please contact{' '}
|
|
1211
|
+
<a href="mailto:support@sybilion.com">support team</a> to
|
|
1212
|
+
gain access to the latest performance data.
|
|
1213
|
+
</>
|
|
1214
|
+
) : undefined
|
|
1215
|
+
}
|
|
1216
|
+
status={statusHint ?? undefined}
|
|
1217
|
+
statusTone={statusTone}
|
|
1218
|
+
/>
|
|
1219
|
+
</div>
|
|
1220
|
+
)}
|
|
1221
|
+
|
|
1222
|
+
{performanceSectionPending && (
|
|
1223
|
+
<div
|
|
1224
|
+
className={S.performanceLoadingLayer}
|
|
1225
|
+
aria-busy="true"
|
|
1226
|
+
aria-live="polite"
|
|
1227
|
+
>
|
|
1228
|
+
<div className={S.emptyState}>
|
|
1229
|
+
<TextShimmer as="span" className={S.performanceLoadingText}>
|
|
1230
|
+
Loading performance data…
|
|
1231
|
+
</TextShimmer>
|
|
1232
|
+
</div>
|
|
1233
|
+
</div>
|
|
1234
|
+
)}
|
|
1235
|
+
</div>
|
|
1236
|
+
|
|
1237
|
+
{!performanceDataLoading && performanceData && (
|
|
1238
|
+
<PerformanceTable
|
|
1239
|
+
forecastModels={forecastModelsData}
|
|
1240
|
+
adjustParameters={adjustParameters}
|
|
1241
|
+
onAdjustParametersChange={setAdjustParameters}
|
|
1242
|
+
customPerformance={customPerformanceTableRow}
|
|
1243
|
+
onEditCustomPerformance={onEditCustomPerformance}
|
|
1244
|
+
showAddEditCustomDataButton={showAddEditCustomDataButton}
|
|
1245
|
+
addEditCustomDataDisabled={addEditCustomDataDisabled}
|
|
1246
|
+
/>
|
|
1247
|
+
)}
|
|
1248
|
+
</Tabs>
|
|
1249
|
+
</div>
|
|
1250
|
+
);
|
|
1251
|
+
}
|