@sybilion/uilib 1.3.62 → 1.3.64
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/Chart/components/BaseChartWrapper.js +10 -5
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/useChartYRange.js +111 -61
- package/dist/esm/components/widgets/DriversComparisonChart/DriversComparisonChart.js +2 -2
- package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js +21 -5
- package/dist/esm/types/src/components/ui/Chart/components/BaseChartWrapper.d.ts +2 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.d.ts +15 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.test.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.d.ts +3 -1
- package/dist/esm/types/src/docs/pages/IncludeHiddenInYScalePage.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/ui/Chart/components/BaseChartWrapper.tsx +14 -4
- package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.test.ts +87 -0
- package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.ts +152 -73
- package/src/components/widgets/DriversComparisonChart/AGENT.md +3 -3
- package/src/components/widgets/DriversComparisonChart/DriversComparisonChart.tsx +2 -2
- package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.ts +65 -45
- package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.ts +22 -4
- package/src/docs/pages/IncludeHiddenInYScalePage.tsx +152 -0
- package/src/docs/registry.ts +6 -0
|
@@ -48,7 +48,7 @@ BaseChartWrapperLoading.displayName = 'BaseChartWrapperLoading';
|
|
|
48
48
|
* Separated to maintain hook order consistency
|
|
49
49
|
*/
|
|
50
50
|
const BaseChartWrapperContent = forwardRef((props, ref) => {
|
|
51
|
-
const { chartConfig = {}, chartData, historicalLineColor, forecastData = [], loading, hasCombinedData, renderId, isDarkTheme, height, className, loadingComponentClassName, footerClassName, chartClassName, xAxisClassName, yAxisClassName, legendClassName, footerActions, quantileBands, quantileBandKey, xMin, xMax, yMin, yMax, autoScaleYAxis = true, formatNumber, formatDate: formatDateFn = formatDate, labelFormatter, onLegendClick, margin, chartType = 'composed', disableAnimation = false, disableLineDrawAnimation = false, showGrid = true, showAxes = true, showTooltip = true, showLegend = true, showChartAxesLegend = true, xAxisLabel, yAxisLabel, showActiveDots = true, overlayElements, hiddenSeries, excludeLegendIds, onAnalysisSelect, onFailedAnalysisClick, containerProps, error, loadingMessage, noDataMessage = 'No data available', onGridHeightChange, forecastLineStyle = 'dashed', disableHistoricalAnimation = false, onShowAll: _onShowAll, onShowOnly: _onShowOnly, maxVisibleItems, preventDeselection, legendVariant = 'default', legendWidth = 1000, legendMarginLeft = 0, } = props;
|
|
51
|
+
const { chartConfig = {}, chartData, historicalLineColor, forecastData = [], loading, hasCombinedData, renderId, isDarkTheme, height, className, loadingComponentClassName, footerClassName, chartClassName, xAxisClassName, yAxisClassName, legendClassName, footerActions, quantileBands, quantileBandKey, xMin, xMax, yMin, yMax, autoScaleYAxis = true, formatNumber, formatDate: formatDateFn = formatDate, labelFormatter, onLegendClick, margin, chartType = 'composed', disableAnimation = false, disableLineDrawAnimation = false, showGrid = true, showAxes = true, showTooltip = true, showLegend = true, showChartAxesLegend = true, xAxisLabel, yAxisLabel, showActiveDots = true, overlayElements, hiddenSeries, includeHiddenInYScale = false, excludeLegendIds, onAnalysisSelect, onFailedAnalysisClick, containerProps, error, loadingMessage, noDataMessage = 'No data available', onGridHeightChange, forecastLineStyle = 'dashed', disableHistoricalAnimation = false, onShowAll: _onShowAll, onShowOnly: _onShowOnly, maxVisibleItems, preventDeselection, legendVariant = 'default', legendWidth = 1000, legendMarginLeft = 0, } = props;
|
|
52
52
|
const [shouldAnimate, setShouldAnimate] = useState(false);
|
|
53
53
|
const lineDataInitializedRef = useRef(false);
|
|
54
54
|
const rootRef = useRef(null);
|
|
@@ -291,12 +291,17 @@ const BaseChartWrapperContent = forwardRef((props, ref) => {
|
|
|
291
291
|
let effectiveAutoScale = autoScaleYAxis;
|
|
292
292
|
if (autoScaleYAxis !== false && yMin !== undefined && yMax !== undefined) {
|
|
293
293
|
const dataKeys = chartData.length > 0 ? Object.keys(chartData[0]) : [];
|
|
294
|
-
const historicalValues =
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
const historicalValues = !includeHiddenInYScale && hiddenSeries?.has('historical')
|
|
295
|
+
? []
|
|
296
|
+
: chartData
|
|
297
|
+
.map(p => p.historical)
|
|
298
|
+
.filter(v => v !== null && v !== undefined);
|
|
297
299
|
const forecastKeys = dataKeys.filter(k => k.startsWith('forecast_'));
|
|
300
|
+
const visibleForecastKeys = !includeHiddenInYScale && hiddenSeries
|
|
301
|
+
? forecastKeys.filter(k => !hiddenSeries.has(k))
|
|
302
|
+
: forecastKeys;
|
|
298
303
|
const forecastValues = chartData
|
|
299
|
-
.flatMap(p =>
|
|
304
|
+
.flatMap(p => visibleForecastKeys.map(k => p[k]))
|
|
300
305
|
.filter(v => v !== null && v !== undefined);
|
|
301
306
|
const allLineValues = [...historicalValues, ...forecastValues];
|
|
302
307
|
if (allLineValues.length > 0) {
|
|
@@ -1,75 +1,116 @@
|
|
|
1
1
|
import { useRef, useMemo } from 'react';
|
|
2
2
|
|
|
3
|
+
/** Serialize hidden series keys for stable cache comparison. */
|
|
4
|
+
function serializeHiddenSeries(hiddenSeries) {
|
|
5
|
+
if (!hiddenSeries || hiddenSeries.size === 0)
|
|
6
|
+
return '';
|
|
7
|
+
return [...hiddenSeries].sort().join(',');
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Optionally includes quantile values from forecast data
|
|
10
|
+
* Returns true when a chart data key should be skipped for Y-scale (series hidden).
|
|
6
11
|
*/
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
function isKeyExcludedFromYScale(key, hiddenSeries) {
|
|
13
|
+
if (!hiddenSeries || hiddenSeries.size === 0)
|
|
14
|
+
return false;
|
|
15
|
+
if (key === 'historical') {
|
|
16
|
+
return hiddenSeries.has('historical');
|
|
17
|
+
}
|
|
18
|
+
if (key.startsWith('forecast_')) {
|
|
19
|
+
return hiddenSeries.has(key);
|
|
20
|
+
}
|
|
21
|
+
if (key.startsWith('q') && key.includes('_')) {
|
|
22
|
+
const forecastId = key.slice(key.lastIndexOf('_') + 1);
|
|
23
|
+
return hiddenSeries.has(`forecast_${forecastId}`);
|
|
24
|
+
}
|
|
25
|
+
if (key.startsWith('band_')) {
|
|
26
|
+
const forecastId = key.slice(key.lastIndexOf('_') + 1);
|
|
27
|
+
return hiddenSeries.has(`forecast_${forecastId}`);
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/** Pure Y-range calculation from chart data points. */
|
|
32
|
+
function calculateChartYRange(dataToUse, { excludeQuantileBands, forecastId, hiddenSeries, includeHiddenInYScale = false, }) {
|
|
33
|
+
const allValues = [];
|
|
34
|
+
dataToUse.forEach(point => {
|
|
35
|
+
Object.entries(point).forEach(([key, value]) => {
|
|
36
|
+
if (key === 'date')
|
|
37
|
+
return;
|
|
38
|
+
if (!includeHiddenInYScale &&
|
|
39
|
+
isKeyExcludedFromYScale(key, hiddenSeries)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// When selectedForecastId is provided, scale from historical + selected forecast + its
|
|
43
|
+
// quantile band (exclude other forecasts and their quantile values only).
|
|
44
|
+
if (forecastId !== undefined) {
|
|
45
|
+
if (key.startsWith('forecast_') && key !== `forecast_${forecastId}`)
|
|
46
|
+
return;
|
|
47
|
+
// Exclude q{quantile}_{otherAnalysisId} - only include selected forecast's quantiles
|
|
48
|
+
if (key.startsWith('q') && !key.endsWith(`_${forecastId}`))
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// When excludeQuantileBands is true, exclude:
|
|
52
|
+
// 1. Quantile band arrays ([number, number])
|
|
53
|
+
// 2. Individual quantile values (q{quantile}_{analysisId} properties)
|
|
54
|
+
// Only include historical and forecast line values
|
|
55
|
+
if (excludeQuantileBands) {
|
|
56
|
+
// Skip quantile band arrays
|
|
57
|
+
if (Array.isArray(value) &&
|
|
58
|
+
value.length === 2 &&
|
|
59
|
+
typeof value[0] === 'number' &&
|
|
60
|
+
typeof value[1] === 'number') {
|
|
17
61
|
return;
|
|
18
|
-
// When selectedForecastId is provided, scale from historical + selected forecast + its
|
|
19
|
-
// quantile band (exclude other forecasts and their quantile values only).
|
|
20
|
-
if (forecastId !== undefined) {
|
|
21
|
-
if (key.startsWith('forecast_') && key !== `forecast_${forecastId}`)
|
|
22
|
-
return;
|
|
23
|
-
// Exclude q{quantile}_{otherAnalysisId} - only include selected forecast's quantiles
|
|
24
|
-
if (key.startsWith('q') && !key.endsWith(`_${forecastId}`))
|
|
25
|
-
return;
|
|
26
62
|
}
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
// Only include historical and forecast line values
|
|
31
|
-
if (excludeQuantileBands) {
|
|
32
|
-
// Skip quantile band arrays
|
|
33
|
-
if (Array.isArray(value) &&
|
|
34
|
-
value.length === 2 &&
|
|
35
|
-
typeof value[0] === 'number' &&
|
|
36
|
-
typeof value[1] === 'number') {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
// Skip individual quantile values (properties starting with 'q' followed by number/underscore)
|
|
40
|
-
// Format: q{quantile}_{analysisId} (e.g., q0.05_123, q0.95_123)
|
|
41
|
-
if (key.startsWith('q') && typeof value === 'number') {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
// Include only historical and forecast line values
|
|
45
|
-
if (typeof value === 'number') {
|
|
46
|
-
allValues.push(value);
|
|
47
|
-
}
|
|
63
|
+
// Skip individual quantile values (properties starting with 'q' followed by number/underscore)
|
|
64
|
+
// Format: q{quantile}_{analysisId} (e.g., q0.05_123, q0.95_123)
|
|
65
|
+
if (key.startsWith('q') && typeof value === 'number') {
|
|
48
66
|
return;
|
|
49
67
|
}
|
|
50
|
-
//
|
|
68
|
+
// Include only historical and forecast line values
|
|
51
69
|
if (typeof value === 'number') {
|
|
52
70
|
allValues.push(value);
|
|
53
71
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// When not excluding, include both numbers and quantile band arrays
|
|
75
|
+
if (typeof value === 'number') {
|
|
76
|
+
allValues.push(value);
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(value) &&
|
|
79
|
+
value.length === 2 &&
|
|
80
|
+
typeof value[0] === 'number' &&
|
|
81
|
+
typeof value[1] === 'number') {
|
|
82
|
+
allValues.push(value[0], value[1]);
|
|
83
|
+
}
|
|
61
84
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
});
|
|
86
|
+
if (allValues.length === 0) {
|
|
87
|
+
return { yMin: 0, yMax: 100 };
|
|
88
|
+
}
|
|
89
|
+
const min = Math.min(...allValues);
|
|
90
|
+
const max = Math.max(...allValues);
|
|
91
|
+
const diff = max - min;
|
|
92
|
+
return {
|
|
93
|
+
yMin: min - diff * 0.1,
|
|
94
|
+
yMax: max + diff * 0.1,
|
|
72
95
|
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Hook to calculate yMin and yMax from chart data
|
|
99
|
+
* Optionally includes quantile values from forecast data
|
|
100
|
+
*/
|
|
101
|
+
function useChartYRange({ baseChartProps, chartData, disableRescaleWhenQuantileChanges, excludeQuantileBands = false, selectedForecastId, }) {
|
|
102
|
+
const includeHiddenInYScale = baseChartProps.includeHiddenInYScale ?? false;
|
|
103
|
+
const hiddenSeries = baseChartProps.hiddenSeries;
|
|
104
|
+
// Store initial Y-range when disableRescaleWhenQuantileChanges is true
|
|
105
|
+
const stableYRangeRef = useRef(null);
|
|
106
|
+
const prevBaseChartDataRef = useRef(baseChartProps.chartData);
|
|
107
|
+
const prevHiddenSeriesKeyRef = useRef(serializeHiddenSeries(hiddenSeries));
|
|
108
|
+
const calculateYRange = (dataToUse, excludeBands, forecastId) => calculateChartYRange(dataToUse, {
|
|
109
|
+
excludeQuantileBands: excludeBands,
|
|
110
|
+
forecastId,
|
|
111
|
+
hiddenSeries,
|
|
112
|
+
includeHiddenInYScale,
|
|
113
|
+
});
|
|
73
114
|
// When disableRescaleWhenQuantileChanges is true, calculate initial stable range
|
|
74
115
|
// If chartData (transformedChartData) is provided, use it WITH quantile bands for initial calculation
|
|
75
116
|
// Then keep it stable when dragging quantiles
|
|
@@ -84,7 +125,7 @@ function useChartYRange({ baseChartProps, chartData, disableRescaleWhenQuantileC
|
|
|
84
125
|
if (!disableRescaleWhenQuantileChanges) {
|
|
85
126
|
return null;
|
|
86
127
|
}
|
|
87
|
-
// Reset stable range when dataset
|
|
128
|
+
// Reset stable range when dataset, selected forecast, or hidden series changes
|
|
88
129
|
if (prevBaseChartDataRef.current !== baseChartProps.chartData) {
|
|
89
130
|
prevBaseChartDataRef.current = baseChartProps.chartData;
|
|
90
131
|
stableYRangeRef.current = null;
|
|
@@ -92,6 +133,11 @@ function useChartYRange({ baseChartProps, chartData, disableRescaleWhenQuantileC
|
|
|
92
133
|
if (stableYRangeRef.current?.forecastId !== selectedForecastId) {
|
|
93
134
|
stableYRangeRef.current = null;
|
|
94
135
|
}
|
|
136
|
+
const hiddenSeriesKey = serializeHiddenSeries(hiddenSeries);
|
|
137
|
+
if (prevHiddenSeriesKeyRef.current !== hiddenSeriesKey) {
|
|
138
|
+
prevHiddenSeriesKeyRef.current = hiddenSeriesKey;
|
|
139
|
+
stableYRangeRef.current = null;
|
|
140
|
+
}
|
|
95
141
|
// If chartData (transformedChartData) is provided, use it WITH quantile bands for initial calculation
|
|
96
142
|
// This ensures IntervalsOverlay includes quantile bands in Y-scale
|
|
97
143
|
const dataToUse = chartData ?? baseChartProps.chartData;
|
|
@@ -115,6 +161,8 @@ function useChartYRange({ baseChartProps, chartData, disableRescaleWhenQuantileC
|
|
|
115
161
|
disableRescaleWhenQuantileChanges,
|
|
116
162
|
excludeQuantileBands,
|
|
117
163
|
selectedForecastId,
|
|
164
|
+
hiddenSeries,
|
|
165
|
+
includeHiddenInYScale,
|
|
118
166
|
]);
|
|
119
167
|
// When disableRescaleWhenQuantileChanges is false, calculate from transformed data
|
|
120
168
|
const dynamicRange = useMemo(() => {
|
|
@@ -138,6 +186,8 @@ function useChartYRange({ baseChartProps, chartData, disableRescaleWhenQuantileC
|
|
|
138
186
|
disableRescaleWhenQuantileChanges,
|
|
139
187
|
excludeQuantileBands,
|
|
140
188
|
selectedForecastId,
|
|
189
|
+
hiddenSeries,
|
|
190
|
+
includeHiddenInYScale,
|
|
141
191
|
]);
|
|
142
192
|
// Return appropriate range
|
|
143
193
|
if (disableRescaleWhenQuantileChanges) {
|
|
@@ -150,4 +200,4 @@ function useChartYRange({ baseChartProps, chartData, disableRescaleWhenQuantileC
|
|
|
150
200
|
return dynamicRange;
|
|
151
201
|
}
|
|
152
202
|
|
|
153
|
-
export { useChartYRange };
|
|
203
|
+
export { calculateChartYRange, isKeyExcludedFromYScale, useChartYRange };
|
|
@@ -83,8 +83,8 @@ function DriversComparisonChart({ payload, datasetHistorical = [], loading = fal
|
|
|
83
83
|
const mergedChartData = useMemo(() => mergeBacktestsChartData(payloadForView), [payloadForView]);
|
|
84
84
|
const mergedWithHistorical = useMemo(() => mergeDatasetHistoricalWithBacktestsChartData(datasetHistorical, mergedChartData), [datasetHistorical, mergedChartData]);
|
|
85
85
|
const historicalWindowFloor = useMemo(() => {
|
|
86
|
-
const
|
|
87
|
-
const merged = mergeDatasetHistoricalWithBacktestsChartData(datasetHistorical,
|
|
86
|
+
const laggedMerged = mergeBacktestsChartData(payload);
|
|
87
|
+
const merged = mergeDatasetHistoricalWithBacktestsChartData(datasetHistorical, laggedMerged);
|
|
88
88
|
const sortedDrivers = [...(payload?.drivers ?? [])]
|
|
89
89
|
.filter(d => d.normalized_series &&
|
|
90
90
|
Object.keys(d.normalized_series).some(key => d.normalized_series[key] != null))
|
package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js
CHANGED
|
@@ -57,7 +57,7 @@ function formatLagMonthsLabel(months) {
|
|
|
57
57
|
return `${months} month(s)`;
|
|
58
58
|
}
|
|
59
59
|
function getLagDisplayForView(lag, tab) {
|
|
60
|
-
if (tab === '
|
|
60
|
+
if (tab === 'lagged') {
|
|
61
61
|
return lag?.trim() ? lag : '—';
|
|
62
62
|
}
|
|
63
63
|
const months = parseLagMonthsFromLabel(lag);
|
|
@@ -78,8 +78,24 @@ function shiftNormalizedSeriesForward(series, lagMonths) {
|
|
|
78
78
|
}
|
|
79
79
|
return out;
|
|
80
80
|
}
|
|
81
|
+
/** Shift driver points left (earlier months) by lag — overlapped alignment vs target. */
|
|
82
|
+
function shiftNormalizedSeriesBackward(series, lagMonths) {
|
|
83
|
+
if (lagMonths <= 0)
|
|
84
|
+
return { ...series };
|
|
85
|
+
const out = {};
|
|
86
|
+
for (const [dateStr, val] of Object.entries(series)) {
|
|
87
|
+
if (val === null || val === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
let shifted = normalizeToMonthStart(dateStr);
|
|
90
|
+
for (let i = 0; i < lagMonths; i++) {
|
|
91
|
+
shifted = getPreviousMonth(shifted);
|
|
92
|
+
}
|
|
93
|
+
out[shifted] = val;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
81
97
|
function applyDriversComparisonViewToPayload(payload, tab) {
|
|
82
|
-
if (!payload || tab === '
|
|
98
|
+
if (!payload || tab === 'lagged')
|
|
83
99
|
return payload;
|
|
84
100
|
return {
|
|
85
101
|
target: {
|
|
@@ -90,7 +106,7 @@ function applyDriversComparisonViewToPayload(payload, tab) {
|
|
|
90
106
|
const lagMonths = parseLagMonthsFromLabel(resolveDriverLagLabel(driver));
|
|
91
107
|
const series = driver.normalized_series ?? {};
|
|
92
108
|
const normalized_series = lagMonths != null && lagMonths > 0
|
|
93
|
-
?
|
|
109
|
+
? shiftNormalizedSeriesBackward(series, lagMonths)
|
|
94
110
|
: { ...series };
|
|
95
111
|
return {
|
|
96
112
|
...driver,
|
|
@@ -231,7 +247,7 @@ function prependHistoricalLeadFromDataset(points, datasetHistorical, xMin) {
|
|
|
231
247
|
}
|
|
232
248
|
return [...lead, ...points];
|
|
233
249
|
}
|
|
234
|
-
/**
|
|
250
|
+
/** Lagged (unshifted) anchor minus lead months — stable xMin across lagged/overlapped tabs. */
|
|
235
251
|
function computeDriversComparisonHistoricalWindowFloor(mergedWithHistorical, forecastIds) {
|
|
236
252
|
if (mergedWithHistorical.length === 0 || forecastIds.length === 0)
|
|
237
253
|
return null;
|
|
@@ -253,4 +269,4 @@ function buildDriversComparisonChartData(mergedWithHistorical, datasetHistorical
|
|
|
253
269
|
return withLead.filter(p => normalizeToMonthStart(p.date) >= xMin);
|
|
254
270
|
}
|
|
255
271
|
|
|
256
|
-
export { DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, INITIAL_VISIBLE_SERIES_COUNT, applyDriversComparisonViewToPayload, buildDriversComparisonChartData, computeDriversComparisonHistoricalWindowFloor, formatLagMonthsLabel, formatSeriesImportance, getLagDisplayForView, getZoomAnchorMonthFromDriverMaterialStarts, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, parseLagMonthsFromLabel, prependHistoricalLeadFromDataset, resolveDriverLagLabel, shiftNormalizedSeriesForward, subtractMonthsFromMonthStart };
|
|
272
|
+
export { DRIVER_COMPARISON_CHART_LEAD_MONTHS, DRIVER_FORECAST_ID_BASE, INITIAL_VISIBLE_SERIES_COUNT, applyDriversComparisonViewToPayload, buildDriversComparisonChartData, computeDriversComparisonHistoricalWindowFloor, formatLagMonthsLabel, formatSeriesImportance, getLagDisplayForView, getZoomAnchorMonthFromDriverMaterialStarts, mergeBacktestsChartData, mergeDatasetHistoricalWithBacktestsChartData, parseLagMonthsFromLabel, prependHistoricalLeadFromDataset, resolveDriverLagLabel, shiftNormalizedSeriesBackward, shiftNormalizedSeriesForward, subtractMonthsFromMonthStart };
|
|
@@ -58,6 +58,8 @@ export interface BaseChartWrapperProps {
|
|
|
58
58
|
showActiveDots?: boolean;
|
|
59
59
|
overlayElements?: ReactNode;
|
|
60
60
|
hiddenSeries?: Set<string>;
|
|
61
|
+
/** When false (default), hidden series values are excluded from Y-axis domain calculation. */
|
|
62
|
+
includeHiddenInYScale?: boolean;
|
|
61
63
|
excludeLegendIds?: number[];
|
|
62
64
|
onAnalysisSelect?: (analysisId: number | string) => void;
|
|
63
65
|
onFailedAnalysisClick?: (analysisId?: number | string) => void;
|
|
@@ -7,6 +7,21 @@ interface UseChartYRangeOptions {
|
|
|
7
7
|
excludeQuantileBands?: boolean;
|
|
8
8
|
selectedForecastId?: number;
|
|
9
9
|
}
|
|
10
|
+
export interface CalculateChartYRangeOptions {
|
|
11
|
+
excludeQuantileBands: boolean;
|
|
12
|
+
forecastId?: number;
|
|
13
|
+
hiddenSeries?: Set<string>;
|
|
14
|
+
includeHiddenInYScale?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns true when a chart data key should be skipped for Y-scale (series hidden).
|
|
18
|
+
*/
|
|
19
|
+
export declare function isKeyExcludedFromYScale(key: string, hiddenSeries?: Set<string>): boolean;
|
|
20
|
+
/** Pure Y-range calculation from chart data points. */
|
|
21
|
+
export declare function calculateChartYRange(dataToUse: ChartDataPoint[], { excludeQuantileBands, forecastId, hiddenSeries, includeHiddenInYScale, }: CalculateChartYRangeOptions): {
|
|
22
|
+
yMin: number;
|
|
23
|
+
yMax: number;
|
|
24
|
+
};
|
|
10
25
|
/**
|
|
11
26
|
* Hook to calculate yMin and yMax from chart data
|
|
12
27
|
* Optionally includes quantile values from forecast data
|
package/dist/esm/types/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -16,6 +16,8 @@ export declare function parseLagMonthsFromLabel(lag: string | null | undefined):
|
|
|
16
16
|
export declare function formatLagMonthsLabel(months: number): string;
|
|
17
17
|
export declare function getLagDisplayForView(lag: string | null | undefined, tab: DriversComparisonViewTab): string;
|
|
18
18
|
export declare function shiftNormalizedSeriesForward(series: Record<string, number | null>, lagMonths: number): Record<string, number | null>;
|
|
19
|
+
/** Shift driver points left (earlier months) by lag — overlapped alignment vs target. */
|
|
20
|
+
export declare function shiftNormalizedSeriesBackward(series: Record<string, number | null>, lagMonths: number): Record<string, number | null>;
|
|
19
21
|
export declare function applyDriversComparisonViewToPayload(payload: BacktestsComponentPayload | null, tab: DriversComparisonViewTab): BacktestsComponentPayload | null;
|
|
20
22
|
export declare function mergeBacktestsChartData(payload: BacktestsComponentPayload | null): ChartDataPoint[];
|
|
21
23
|
/** While drivers comparison is not loaded: dataset historical only. After load: normalized historical from target.normalized_series (see mergeBacktestsChartData) replaces it. */
|
|
@@ -35,6 +37,6 @@ export declare function subtractMonthsFromMonthStart(date: string, count: number
|
|
|
35
37
|
* (scaled to the first normalized point) so the lead-in shows the target line alone.
|
|
36
38
|
*/
|
|
37
39
|
export declare function prependHistoricalLeadFromDataset(points: ChartDataPoint[], datasetHistorical: ChartDataPoint[], xMin: string): ChartDataPoint[];
|
|
38
|
-
/**
|
|
40
|
+
/** Lagged (unshifted) anchor minus lead months — stable xMin across lagged/overlapped tabs. */
|
|
39
41
|
export declare function computeDriversComparisonHistoricalWindowFloor(mergedWithHistorical: ChartDataPoint[], forecastIds: number[]): string | null;
|
|
40
42
|
export declare function buildDriversComparisonChartData(mergedWithHistorical: ChartDataPoint[], datasetHistorical: ChartDataPoint[], forecastIds: number[], historicalWindowFloor?: string | null): ChartDataPoint[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function IncludeHiddenInYScalePage(): import("react/jsx-runtime").JSX.Element;
|
package/package.json
CHANGED
|
@@ -121,6 +121,8 @@ export interface BaseChartWrapperProps {
|
|
|
121
121
|
showActiveDots?: boolean;
|
|
122
122
|
overlayElements?: ReactNode;
|
|
123
123
|
hiddenSeries?: Set<string>;
|
|
124
|
+
/** When false (default), hidden series values are excluded from Y-axis domain calculation. */
|
|
125
|
+
includeHiddenInYScale?: boolean;
|
|
124
126
|
excludeLegendIds?: number[];
|
|
125
127
|
onAnalysisSelect?: (analysisId: number | string) => void;
|
|
126
128
|
onFailedAnalysisClick?: (analysisId?: number | string) => void;
|
|
@@ -225,6 +227,7 @@ const BaseChartWrapperContent = forwardRef<
|
|
|
225
227
|
showActiveDots = true,
|
|
226
228
|
overlayElements,
|
|
227
229
|
hiddenSeries,
|
|
230
|
+
includeHiddenInYScale = false,
|
|
228
231
|
excludeLegendIds,
|
|
229
232
|
onAnalysisSelect,
|
|
230
233
|
onFailedAnalysisClick,
|
|
@@ -567,12 +570,19 @@ const BaseChartWrapperContent = forwardRef<
|
|
|
567
570
|
let effectiveAutoScale = autoScaleYAxis;
|
|
568
571
|
if (autoScaleYAxis !== false && yMin !== undefined && yMax !== undefined) {
|
|
569
572
|
const dataKeys = chartData.length > 0 ? Object.keys(chartData[0]) : [];
|
|
570
|
-
const historicalValues =
|
|
571
|
-
|
|
572
|
-
|
|
573
|
+
const historicalValues =
|
|
574
|
+
!includeHiddenInYScale && hiddenSeries?.has('historical')
|
|
575
|
+
? []
|
|
576
|
+
: chartData
|
|
577
|
+
.map(p => p.historical)
|
|
578
|
+
.filter(v => v !== null && v !== undefined);
|
|
573
579
|
const forecastKeys = dataKeys.filter(k => k.startsWith('forecast_'));
|
|
580
|
+
const visibleForecastKeys =
|
|
581
|
+
!includeHiddenInYScale && hiddenSeries
|
|
582
|
+
? forecastKeys.filter(k => !hiddenSeries.has(k))
|
|
583
|
+
: forecastKeys;
|
|
574
584
|
const forecastValues = chartData
|
|
575
|
-
.flatMap(p =>
|
|
585
|
+
.flatMap(p => visibleForecastKeys.map(k => p[k]))
|
|
576
586
|
.filter(v => v !== null && v !== undefined);
|
|
577
587
|
const allLineValues = [...historicalValues, ...forecastValues];
|
|
578
588
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
calculateChartYRange,
|
|
5
|
+
isKeyExcludedFromYScale,
|
|
6
|
+
} from './useChartYRange';
|
|
7
|
+
|
|
8
|
+
const makePoint = (values: Record<string, number>): ChartDataPoint => ({
|
|
9
|
+
date: '2024-01-01',
|
|
10
|
+
...values,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('isKeyExcludedFromYScale', () => {
|
|
14
|
+
it('excludes historical when hidden', () => {
|
|
15
|
+
expect(isKeyExcludedFromYScale('historical', new Set(['historical']))).toBe(
|
|
16
|
+
true,
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('excludes quantile keys when parent forecast is hidden', () => {
|
|
21
|
+
expect(
|
|
22
|
+
isKeyExcludedFromYScale('q0.05_123', new Set(['forecast_123'])),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('excludes band keys when parent forecast is hidden', () => {
|
|
27
|
+
expect(
|
|
28
|
+
isKeyExcludedFromYScale('band_10_90_123', new Set(['forecast_123'])),
|
|
29
|
+
).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('calculateChartYRange hidden series exclusion', () => {
|
|
34
|
+
const chartData: ChartDataPoint[] = [
|
|
35
|
+
makePoint({ historical: 10, forecast_1: 100, forecast_2: 50 }),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
it('pin mode: two forecasts, one hidden - Y range from visible only', () => {
|
|
39
|
+
const hiddenSeries = new Set(['forecast_1']);
|
|
40
|
+
const range = calculateChartYRange(chartData, {
|
|
41
|
+
excludeQuantileBands: true,
|
|
42
|
+
hiddenSeries,
|
|
43
|
+
includeHiddenInYScale: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(range).toEqual({ yMin: 6, yMax: 54 });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('hidden historical excluded from Y range', () => {
|
|
50
|
+
const dataWithOutlierHistorical: ChartDataPoint[] = [
|
|
51
|
+
makePoint({ historical: 1000, forecast_1: 50, forecast_2: 60 }),
|
|
52
|
+
];
|
|
53
|
+
const hiddenSeries = new Set(['historical']);
|
|
54
|
+
const range = calculateChartYRange(dataWithOutlierHistorical, {
|
|
55
|
+
excludeQuantileBands: true,
|
|
56
|
+
hiddenSeries,
|
|
57
|
+
includeHiddenInYScale: false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(range).toEqual({ yMin: 49, yMax: 61 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('includeHiddenInYScale=true preserves old behavior', () => {
|
|
64
|
+
const hiddenSeries = new Set(['forecast_1']);
|
|
65
|
+
const range = calculateChartYRange(chartData, {
|
|
66
|
+
excludeQuantileBands: true,
|
|
67
|
+
hiddenSeries,
|
|
68
|
+
includeHiddenInYScale: true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(range).toEqual({ yMin: 1, yMax: 109 });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('quantile keys excluded when forecast hidden', () => {
|
|
75
|
+
const dataWithQuantile: ChartDataPoint[] = [
|
|
76
|
+
makePoint({ forecast_456: 50, forecast_789: 70, 'q0.05_123': 500 }),
|
|
77
|
+
];
|
|
78
|
+
const hiddenSeries = new Set(['forecast_123']);
|
|
79
|
+
const range = calculateChartYRange(dataWithQuantile, {
|
|
80
|
+
excludeQuantileBands: false,
|
|
81
|
+
hiddenSeries,
|
|
82
|
+
includeHiddenInYScale: false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(range).toEqual({ yMin: 48, yMax: 72 });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -11,6 +11,137 @@ interface UseChartYRangeOptions {
|
|
|
11
11
|
selectedForecastId?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface CalculateChartYRangeOptions {
|
|
15
|
+
excludeQuantileBands: boolean;
|
|
16
|
+
forecastId?: number;
|
|
17
|
+
hiddenSeries?: Set<string>;
|
|
18
|
+
includeHiddenInYScale?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Serialize hidden series keys for stable cache comparison. */
|
|
22
|
+
function serializeHiddenSeries(hiddenSeries?: Set<string>): string {
|
|
23
|
+
if (!hiddenSeries || hiddenSeries.size === 0) return '';
|
|
24
|
+
return [...hiddenSeries].sort().join(',');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when a chart data key should be skipped for Y-scale (series hidden).
|
|
29
|
+
*/
|
|
30
|
+
export function isKeyExcludedFromYScale(
|
|
31
|
+
key: string,
|
|
32
|
+
hiddenSeries?: Set<string>,
|
|
33
|
+
): boolean {
|
|
34
|
+
if (!hiddenSeries || hiddenSeries.size === 0) return false;
|
|
35
|
+
|
|
36
|
+
if (key === 'historical') {
|
|
37
|
+
return hiddenSeries.has('historical');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (key.startsWith('forecast_')) {
|
|
41
|
+
return hiddenSeries.has(key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (key.startsWith('q') && key.includes('_')) {
|
|
45
|
+
const forecastId = key.slice(key.lastIndexOf('_') + 1);
|
|
46
|
+
return hiddenSeries.has(`forecast_${forecastId}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (key.startsWith('band_')) {
|
|
50
|
+
const forecastId = key.slice(key.lastIndexOf('_') + 1);
|
|
51
|
+
return hiddenSeries.has(`forecast_${forecastId}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Pure Y-range calculation from chart data points. */
|
|
58
|
+
export function calculateChartYRange(
|
|
59
|
+
dataToUse: ChartDataPoint[],
|
|
60
|
+
{
|
|
61
|
+
excludeQuantileBands,
|
|
62
|
+
forecastId,
|
|
63
|
+
hiddenSeries,
|
|
64
|
+
includeHiddenInYScale = false,
|
|
65
|
+
}: CalculateChartYRangeOptions,
|
|
66
|
+
) {
|
|
67
|
+
const allValues: number[] = [];
|
|
68
|
+
|
|
69
|
+
dataToUse.forEach(point => {
|
|
70
|
+
Object.entries(point).forEach(([key, value]) => {
|
|
71
|
+
if (key === 'date') return;
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
!includeHiddenInYScale &&
|
|
75
|
+
isKeyExcludedFromYScale(key, hiddenSeries)
|
|
76
|
+
) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// When selectedForecastId is provided, scale from historical + selected forecast + its
|
|
81
|
+
// quantile band (exclude other forecasts and their quantile values only).
|
|
82
|
+
if (forecastId !== undefined) {
|
|
83
|
+
if (key.startsWith('forecast_') && key !== `forecast_${forecastId}`)
|
|
84
|
+
return;
|
|
85
|
+
// Exclude q{quantile}_{otherAnalysisId} - only include selected forecast's quantiles
|
|
86
|
+
if (key.startsWith('q') && !key.endsWith(`_${forecastId}`)) return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// When excludeQuantileBands is true, exclude:
|
|
90
|
+
// 1. Quantile band arrays ([number, number])
|
|
91
|
+
// 2. Individual quantile values (q{quantile}_{analysisId} properties)
|
|
92
|
+
// Only include historical and forecast line values
|
|
93
|
+
if (excludeQuantileBands) {
|
|
94
|
+
// Skip quantile band arrays
|
|
95
|
+
if (
|
|
96
|
+
Array.isArray(value) &&
|
|
97
|
+
value.length === 2 &&
|
|
98
|
+
typeof value[0] === 'number' &&
|
|
99
|
+
typeof value[1] === 'number'
|
|
100
|
+
) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Skip individual quantile values (properties starting with 'q' followed by number/underscore)
|
|
105
|
+
// Format: q{quantile}_{analysisId} (e.g., q0.05_123, q0.95_123)
|
|
106
|
+
if (key.startsWith('q') && typeof value === 'number') {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Include only historical and forecast line values
|
|
111
|
+
if (typeof value === 'number') {
|
|
112
|
+
allValues.push(value);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// When not excluding, include both numbers and quantile band arrays
|
|
118
|
+
if (typeof value === 'number') {
|
|
119
|
+
allValues.push(value);
|
|
120
|
+
} else if (
|
|
121
|
+
Array.isArray(value) &&
|
|
122
|
+
value.length === 2 &&
|
|
123
|
+
typeof value[0] === 'number' &&
|
|
124
|
+
typeof value[1] === 'number'
|
|
125
|
+
) {
|
|
126
|
+
allValues.push(value[0], value[1]);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (allValues.length === 0) {
|
|
132
|
+
return { yMin: 0, yMax: 100 };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const min = Math.min(...allValues);
|
|
136
|
+
const max = Math.max(...allValues);
|
|
137
|
+
const diff = max - min;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
yMin: min - diff * 0.1,
|
|
141
|
+
yMax: max + diff * 0.1,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
14
145
|
/**
|
|
15
146
|
* Hook to calculate yMin and yMax from chart data
|
|
16
147
|
* Optionally includes quantile values from forecast data
|
|
@@ -22,6 +153,9 @@ export function useChartYRange({
|
|
|
22
153
|
excludeQuantileBands = false,
|
|
23
154
|
selectedForecastId,
|
|
24
155
|
}: UseChartYRangeOptions) {
|
|
156
|
+
const includeHiddenInYScale = baseChartProps.includeHiddenInYScale ?? false;
|
|
157
|
+
const hiddenSeries = baseChartProps.hiddenSeries;
|
|
158
|
+
|
|
25
159
|
// Store initial Y-range when disableRescaleWhenQuantileChanges is true
|
|
26
160
|
const stableYRangeRef = useRef<{
|
|
27
161
|
yMin: number;
|
|
@@ -29,84 +163,20 @@ export function useChartYRange({
|
|
|
29
163
|
forecastId?: number;
|
|
30
164
|
} | null>(null);
|
|
31
165
|
const prevBaseChartDataRef = useRef(baseChartProps.chartData);
|
|
166
|
+
const prevHiddenSeriesKeyRef = useRef(serializeHiddenSeries(hiddenSeries));
|
|
32
167
|
|
|
33
|
-
// Helper function to calculate Y-range from data
|
|
34
168
|
const calculateYRange = (
|
|
35
169
|
dataToUse: ChartDataPoint[],
|
|
36
|
-
|
|
170
|
+
excludeBands: boolean,
|
|
37
171
|
forecastId?: number,
|
|
38
|
-
) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// When selectedForecastId is provided, scale from historical + selected forecast + its
|
|
46
|
-
// quantile band (exclude other forecasts and their quantile values only).
|
|
47
|
-
if (forecastId !== undefined) {
|
|
48
|
-
if (key.startsWith('forecast_') && key !== `forecast_${forecastId}`)
|
|
49
|
-
return;
|
|
50
|
-
// Exclude q{quantile}_{otherAnalysisId} - only include selected forecast's quantiles
|
|
51
|
-
if (key.startsWith('q') && !key.endsWith(`_${forecastId}`)) return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// When excludeQuantileBands is true, exclude:
|
|
55
|
-
// 1. Quantile band arrays ([number, number])
|
|
56
|
-
// 2. Individual quantile values (q{quantile}_{analysisId} properties)
|
|
57
|
-
// Only include historical and forecast line values
|
|
58
|
-
if (excludeQuantileBands) {
|
|
59
|
-
// Skip quantile band arrays
|
|
60
|
-
if (
|
|
61
|
-
Array.isArray(value) &&
|
|
62
|
-
value.length === 2 &&
|
|
63
|
-
typeof value[0] === 'number' &&
|
|
64
|
-
typeof value[1] === 'number'
|
|
65
|
-
) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Skip individual quantile values (properties starting with 'q' followed by number/underscore)
|
|
70
|
-
// Format: q{quantile}_{analysisId} (e.g., q0.05_123, q0.95_123)
|
|
71
|
-
if (key.startsWith('q') && typeof value === 'number') {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Include only historical and forecast line values
|
|
76
|
-
if (typeof value === 'number') {
|
|
77
|
-
allValues.push(value);
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// When not excluding, include both numbers and quantile band arrays
|
|
83
|
-
if (typeof value === 'number') {
|
|
84
|
-
allValues.push(value);
|
|
85
|
-
} else if (
|
|
86
|
-
Array.isArray(value) &&
|
|
87
|
-
value.length === 2 &&
|
|
88
|
-
typeof value[0] === 'number' &&
|
|
89
|
-
typeof value[1] === 'number'
|
|
90
|
-
) {
|
|
91
|
-
allValues.push(value[0], value[1]);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
172
|
+
) =>
|
|
173
|
+
calculateChartYRange(dataToUse, {
|
|
174
|
+
excludeQuantileBands: excludeBands,
|
|
175
|
+
forecastId,
|
|
176
|
+
hiddenSeries,
|
|
177
|
+
includeHiddenInYScale,
|
|
94
178
|
});
|
|
95
179
|
|
|
96
|
-
if (allValues.length === 0) {
|
|
97
|
-
return { yMin: 0, yMax: 100 };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const min = Math.min(...allValues);
|
|
101
|
-
const max = Math.max(...allValues);
|
|
102
|
-
const diff = max - min;
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
yMin: min - diff * 0.1,
|
|
106
|
-
yMax: max + diff * 0.1,
|
|
107
|
-
};
|
|
108
|
-
};
|
|
109
|
-
|
|
110
180
|
// When disableRescaleWhenQuantileChanges is true, calculate initial stable range
|
|
111
181
|
// If chartData (transformedChartData) is provided, use it WITH quantile bands for initial calculation
|
|
112
182
|
// Then keep it stable when dragging quantiles
|
|
@@ -125,7 +195,7 @@ export function useChartYRange({
|
|
|
125
195
|
return null;
|
|
126
196
|
}
|
|
127
197
|
|
|
128
|
-
// Reset stable range when dataset
|
|
198
|
+
// Reset stable range when dataset, selected forecast, or hidden series changes
|
|
129
199
|
if (prevBaseChartDataRef.current !== baseChartProps.chartData) {
|
|
130
200
|
prevBaseChartDataRef.current = baseChartProps.chartData;
|
|
131
201
|
stableYRangeRef.current = null;
|
|
@@ -133,6 +203,11 @@ export function useChartYRange({
|
|
|
133
203
|
if (stableYRangeRef.current?.forecastId !== selectedForecastId) {
|
|
134
204
|
stableYRangeRef.current = null;
|
|
135
205
|
}
|
|
206
|
+
const hiddenSeriesKey = serializeHiddenSeries(hiddenSeries);
|
|
207
|
+
if (prevHiddenSeriesKeyRef.current !== hiddenSeriesKey) {
|
|
208
|
+
prevHiddenSeriesKeyRef.current = hiddenSeriesKey;
|
|
209
|
+
stableYRangeRef.current = null;
|
|
210
|
+
}
|
|
136
211
|
|
|
137
212
|
// If chartData (transformedChartData) is provided, use it WITH quantile bands for initial calculation
|
|
138
213
|
// This ensures IntervalsOverlay includes quantile bands in Y-scale
|
|
@@ -164,6 +239,8 @@ export function useChartYRange({
|
|
|
164
239
|
disableRescaleWhenQuantileChanges,
|
|
165
240
|
excludeQuantileBands,
|
|
166
241
|
selectedForecastId,
|
|
242
|
+
hiddenSeries,
|
|
243
|
+
includeHiddenInYScale,
|
|
167
244
|
]);
|
|
168
245
|
|
|
169
246
|
// When disableRescaleWhenQuantileChanges is false, calculate from transformed data
|
|
@@ -192,6 +269,8 @@ export function useChartYRange({
|
|
|
192
269
|
disableRescaleWhenQuantileChanges,
|
|
193
270
|
excludeQuantileBands,
|
|
194
271
|
selectedForecastId,
|
|
272
|
+
hiddenSeries,
|
|
273
|
+
includeHiddenInYScale,
|
|
195
274
|
]);
|
|
196
275
|
|
|
197
276
|
// Return appropriate range
|
|
@@ -10,7 +10,7 @@ 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`: `
|
|
13
|
+
- `viewTab` / `onViewTabChange`: `lagged` (calendar-aligned, raw dates) or `overlapped` (driver series shifted backward by parsed lag months)
|
|
14
14
|
|
|
15
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.
|
|
16
16
|
|
|
@@ -18,8 +18,8 @@ Report tile: `drivers_comparison_chart` — host loads normalized backtests payl
|
|
|
18
18
|
|
|
19
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
20
|
|
|
21
|
-
Lag column: **
|
|
21
|
+
Lag column: **Lagged** shows API `lag` string (may be a range). **Overlapped** shows single `N month(s)` from `parseLagMonthsFromLabel` (range uses max month).
|
|
22
22
|
|
|
23
|
-
Historical window: lead-in is anchored to the **
|
|
23
|
+
Historical window: lead-in is anchored to the **lagged** (unshifted) view (`computeDriversComparisonHistoricalWindowFloor`); overlapped tab shifts driver lines backward only — switching tabs does not change historical extent when floor is pinned.
|
|
24
24
|
|
|
25
25
|
Empty/loading: loading props shimmer chart; null `payload` with `runAnalysisHint` prompts to run analysis.
|
|
@@ -160,10 +160,10 @@ export function DriversComparisonChart({
|
|
|
160
160
|
);
|
|
161
161
|
|
|
162
162
|
const historicalWindowFloor = useMemo(() => {
|
|
163
|
-
const
|
|
163
|
+
const laggedMerged = mergeBacktestsChartData(payload);
|
|
164
164
|
const merged = mergeDatasetHistoricalWithBacktestsChartData(
|
|
165
165
|
datasetHistorical,
|
|
166
|
-
|
|
166
|
+
laggedMerged,
|
|
167
167
|
);
|
|
168
168
|
const sortedDrivers = [...(payload?.drivers ?? [])]
|
|
169
169
|
.filter(
|
package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
mergeDatasetHistoricalWithBacktestsChartData,
|
|
13
13
|
parseLagMonthsFromLabel,
|
|
14
14
|
resolveDriverLagLabel,
|
|
15
|
+
shiftNormalizedSeriesBackward,
|
|
15
16
|
shiftNormalizedSeriesForward,
|
|
16
17
|
subtractMonthsFromMonthStart,
|
|
17
18
|
} from './driversComparisonChart.helpers';
|
|
@@ -58,17 +59,17 @@ describe('formatLagMonthsLabel', () => {
|
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
describe('getLagDisplayForView', () => {
|
|
61
|
-
it('shows API lag on
|
|
62
|
-
expect(getLagDisplayForView('9 to 12 month(s)', '
|
|
62
|
+
it('shows API lag on lagged tab', () => {
|
|
63
|
+
expect(getLagDisplayForView('9 to 12 month(s)', 'lagged')).toBe(
|
|
63
64
|
'9 to 12 month(s)',
|
|
64
65
|
);
|
|
65
66
|
});
|
|
66
67
|
|
|
67
|
-
it('shows parsed month on
|
|
68
|
-
expect(getLagDisplayForView('9 to 12 month(s)', '
|
|
68
|
+
it('shows parsed month on overlapped tab', () => {
|
|
69
|
+
expect(getLagDisplayForView('9 to 12 month(s)', 'overlapped')).toBe(
|
|
69
70
|
'12 month(s)',
|
|
70
71
|
);
|
|
71
|
-
expect(getLagDisplayForView('Unknown', '
|
|
72
|
+
expect(getLagDisplayForView('Unknown', 'overlapped')).toBe('—');
|
|
72
73
|
});
|
|
73
74
|
});
|
|
74
75
|
|
|
@@ -90,6 +91,24 @@ describe('shiftNormalizedSeriesForward', () => {
|
|
|
90
91
|
});
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
describe('shiftNormalizedSeriesBackward', () => {
|
|
95
|
+
it('moves points backward by lag months', () => {
|
|
96
|
+
const shifted = shiftNormalizedSeriesBackward(
|
|
97
|
+
{ '2015-03-01': 1.2, '2015-04-01': 1.4 },
|
|
98
|
+
2,
|
|
99
|
+
);
|
|
100
|
+
expect(shifted['2015-01-01']).toBe(1.2);
|
|
101
|
+
expect(shifted['2015-02-01']).toBe(1.4);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns a shallow copy when lag is zero', () => {
|
|
105
|
+
const series = { '2015-01-01': 1 };
|
|
106
|
+
const shifted = shiftNormalizedSeriesBackward(series, 0);
|
|
107
|
+
expect(shifted).toEqual(series);
|
|
108
|
+
expect(shifted).not.toBe(series);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
93
112
|
describe('applyDriversComparisonViewToPayload', () => {
|
|
94
113
|
const payload = {
|
|
95
114
|
target: {
|
|
@@ -107,18 +126,19 @@ describe('applyDriversComparisonViewToPayload', () => {
|
|
|
107
126
|
],
|
|
108
127
|
};
|
|
109
128
|
|
|
110
|
-
it('returns original payload for
|
|
111
|
-
expect(applyDriversComparisonViewToPayload(payload, '
|
|
112
|
-
payload,
|
|
113
|
-
);
|
|
129
|
+
it('returns original payload for lagged tab', () => {
|
|
130
|
+
expect(applyDriversComparisonViewToPayload(payload, 'lagged')).toBe(payload);
|
|
114
131
|
});
|
|
115
132
|
|
|
116
|
-
it('shifts driver series for
|
|
117
|
-
const
|
|
118
|
-
|
|
133
|
+
it('shifts driver series backward for overlapped tab without mutating source', () => {
|
|
134
|
+
const overlapped = applyDriversComparisonViewToPayload(
|
|
135
|
+
payload,
|
|
136
|
+
'overlapped',
|
|
137
|
+
);
|
|
138
|
+
expect(overlapped).not.toBe(payload);
|
|
119
139
|
expect(payload.drivers[0].normalized_series['2015-01-01']).toBe(0.5);
|
|
120
|
-
expect(
|
|
121
|
-
expect(
|
|
140
|
+
expect(overlapped?.drivers[0].normalized_series['2014-11-01']).toBe(0.5);
|
|
141
|
+
expect(overlapped?.target.normalized_series).toEqual(
|
|
122
142
|
payload.target.normalized_series,
|
|
123
143
|
);
|
|
124
144
|
});
|
|
@@ -135,11 +155,11 @@ describe('applyDriversComparisonViewToPayload', () => {
|
|
|
135
155
|
},
|
|
136
156
|
],
|
|
137
157
|
};
|
|
138
|
-
const
|
|
158
|
+
const overlapped = applyDriversComparisonViewToPayload(
|
|
139
159
|
withOverallLag,
|
|
140
|
-
'
|
|
160
|
+
'overlapped',
|
|
141
161
|
);
|
|
142
|
-
expect(
|
|
162
|
+
expect(overlapped?.drivers[0].normalized_series['2015-05-01']).toBe(0.8);
|
|
143
163
|
});
|
|
144
164
|
});
|
|
145
165
|
|
|
@@ -184,7 +204,7 @@ describe('computeDriversComparisonHistoricalWindowFloor', () => {
|
|
|
184
204
|
|
|
185
205
|
const forecastIds = [DRIVER_FORECAST_ID_BASE];
|
|
186
206
|
|
|
187
|
-
it('returns anchor minus lead months from
|
|
207
|
+
it('returns anchor minus lead months from lagged (unshifted) merged data', () => {
|
|
188
208
|
const merged = mergeDatasetHistoricalWithBacktestsChartData(
|
|
189
209
|
datasetHistorical,
|
|
190
210
|
mergeBacktestsChartData(laggedDriverPayload),
|
|
@@ -245,70 +265,70 @@ describe('buildDriversComparisonChartData historical window floor', () => {
|
|
|
245
265
|
const forecastIds = [DRIVER_FORECAST_ID_BASE];
|
|
246
266
|
|
|
247
267
|
it('keeps same xMin for lagged and overlapped when floor is pinned', () => {
|
|
248
|
-
const
|
|
268
|
+
const laggedMerged = mergeDatasetHistoricalWithBacktestsChartData(
|
|
249
269
|
datasetHistorical,
|
|
250
270
|
mergeBacktestsChartData(laggedDriverPayload),
|
|
251
271
|
);
|
|
252
|
-
const
|
|
272
|
+
const overlappedPayload = applyDriversComparisonViewToPayload(
|
|
253
273
|
laggedDriverPayload,
|
|
254
|
-
'
|
|
274
|
+
'overlapped',
|
|
255
275
|
);
|
|
256
|
-
const
|
|
276
|
+
const overlappedMerged = mergeDatasetHistoricalWithBacktestsChartData(
|
|
257
277
|
datasetHistorical,
|
|
258
|
-
mergeBacktestsChartData(
|
|
278
|
+
mergeBacktestsChartData(overlappedPayload),
|
|
259
279
|
);
|
|
260
280
|
const floor = computeDriversComparisonHistoricalWindowFloor(
|
|
261
|
-
|
|
281
|
+
laggedMerged,
|
|
262
282
|
forecastIds,
|
|
263
283
|
)!;
|
|
264
284
|
|
|
265
|
-
const
|
|
266
|
-
|
|
285
|
+
const laggedChart = buildDriversComparisonChartData(
|
|
286
|
+
laggedMerged,
|
|
267
287
|
datasetHistorical,
|
|
268
288
|
forecastIds,
|
|
269
289
|
floor,
|
|
270
290
|
);
|
|
271
|
-
const
|
|
272
|
-
|
|
291
|
+
const overlappedChart = buildDriversComparisonChartData(
|
|
292
|
+
overlappedMerged,
|
|
273
293
|
datasetHistorical,
|
|
274
294
|
forecastIds,
|
|
275
295
|
floor,
|
|
276
296
|
);
|
|
277
297
|
|
|
278
|
-
expect(overlappedChart[0]?.date).toBe('2014-10-01');
|
|
279
298
|
expect(laggedChart[0]?.date).toBe('2014-10-01');
|
|
280
|
-
expect(
|
|
299
|
+
expect(overlappedChart[0]?.date).toBe('2014-10-01');
|
|
300
|
+
expect(overlappedChart[0]?.date).toBe(laggedChart[0]?.date);
|
|
281
301
|
});
|
|
282
302
|
|
|
283
|
-
it('without floor override
|
|
284
|
-
const
|
|
303
|
+
it('without floor override overlapped chart starts earlier than lagged', () => {
|
|
304
|
+
const laggedMerged = mergeDatasetHistoricalWithBacktestsChartData(
|
|
285
305
|
datasetHistorical,
|
|
286
306
|
mergeBacktestsChartData(laggedDriverPayload),
|
|
287
307
|
);
|
|
288
|
-
const
|
|
308
|
+
const overlappedPayload = applyDriversComparisonViewToPayload(
|
|
289
309
|
laggedDriverPayload,
|
|
290
|
-
'
|
|
310
|
+
'overlapped',
|
|
291
311
|
);
|
|
292
|
-
const
|
|
312
|
+
const overlappedMerged = mergeDatasetHistoricalWithBacktestsChartData(
|
|
293
313
|
datasetHistorical,
|
|
294
|
-
mergeBacktestsChartData(
|
|
314
|
+
mergeBacktestsChartData(overlappedPayload),
|
|
295
315
|
);
|
|
296
316
|
|
|
297
|
-
const
|
|
298
|
-
|
|
317
|
+
const laggedChart = buildDriversComparisonChartData(
|
|
318
|
+
laggedMerged,
|
|
299
319
|
datasetHistorical,
|
|
300
320
|
forecastIds,
|
|
301
321
|
);
|
|
302
|
-
const
|
|
303
|
-
|
|
322
|
+
const overlappedChart = buildDriversComparisonChartData(
|
|
323
|
+
overlappedMerged,
|
|
304
324
|
datasetHistorical,
|
|
305
325
|
forecastIds,
|
|
306
326
|
);
|
|
307
327
|
|
|
308
|
-
expect(
|
|
309
|
-
expect(
|
|
310
|
-
expect(
|
|
311
|
-
|
|
312
|
-
);
|
|
328
|
+
expect(laggedChart[0]?.date).toBe('2014-10-01');
|
|
329
|
+
expect(overlappedChart[0]?.date).toBe('2014-07-01');
|
|
330
|
+
expect(
|
|
331
|
+
overlappedChart[0]?.date!.localeCompare(laggedChart[0]?.date!),
|
|
332
|
+
).toBe(-1);
|
|
313
333
|
});
|
|
314
334
|
});
|
|
@@ -78,7 +78,7 @@ export function getLagDisplayForView(
|
|
|
78
78
|
lag: string | null | undefined,
|
|
79
79
|
tab: DriversComparisonViewTab,
|
|
80
80
|
): string {
|
|
81
|
-
if (tab === '
|
|
81
|
+
if (tab === 'lagged') {
|
|
82
82
|
return lag?.trim() ? lag : '—';
|
|
83
83
|
}
|
|
84
84
|
const months = parseLagMonthsFromLabel(lag);
|
|
@@ -102,11 +102,29 @@ export function shiftNormalizedSeriesForward(
|
|
|
102
102
|
return out;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** Shift driver points left (earlier months) by lag — overlapped alignment vs target. */
|
|
106
|
+
export function shiftNormalizedSeriesBackward(
|
|
107
|
+
series: Record<string, number | null>,
|
|
108
|
+
lagMonths: number,
|
|
109
|
+
): Record<string, number | null> {
|
|
110
|
+
if (lagMonths <= 0) return { ...series };
|
|
111
|
+
const out: Record<string, number | null> = {};
|
|
112
|
+
for (const [dateStr, val] of Object.entries(series)) {
|
|
113
|
+
if (val === null || val === undefined) continue;
|
|
114
|
+
let shifted = normalizeToMonthStart(dateStr);
|
|
115
|
+
for (let i = 0; i < lagMonths; i++) {
|
|
116
|
+
shifted = getPreviousMonth(shifted);
|
|
117
|
+
}
|
|
118
|
+
out[shifted] = val;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
105
123
|
export function applyDriversComparisonViewToPayload(
|
|
106
124
|
payload: BacktestsComponentPayload | null,
|
|
107
125
|
tab: DriversComparisonViewTab,
|
|
108
126
|
): BacktestsComponentPayload | null {
|
|
109
|
-
if (!payload || tab === '
|
|
127
|
+
if (!payload || tab === 'lagged') return payload;
|
|
110
128
|
|
|
111
129
|
return {
|
|
112
130
|
target: {
|
|
@@ -118,7 +136,7 @@ export function applyDriversComparisonViewToPayload(
|
|
|
118
136
|
const series = driver.normalized_series ?? {};
|
|
119
137
|
const normalized_series =
|
|
120
138
|
lagMonths != null && lagMonths > 0
|
|
121
|
-
?
|
|
139
|
+
? shiftNormalizedSeriesBackward(series, lagMonths)
|
|
122
140
|
: { ...series };
|
|
123
141
|
return {
|
|
124
142
|
...driver,
|
|
@@ -290,7 +308,7 @@ export function prependHistoricalLeadFromDataset(
|
|
|
290
308
|
return [...lead, ...points];
|
|
291
309
|
}
|
|
292
310
|
|
|
293
|
-
/**
|
|
311
|
+
/** Lagged (unshifted) anchor minus lead months — stable xMin across lagged/overlapped tabs. */
|
|
294
312
|
export function computeDriversComparisonHistoricalWindowFloor(
|
|
295
313
|
mergedWithHistorical: ChartDataPoint[],
|
|
296
314
|
forecastIds: number[],
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ChartAreaInteractive } from '#uilib/components/ui/ChartAreaInteractive';
|
|
4
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
5
|
+
import type { ForecastItemData } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
|
|
6
|
+
import { calculateChartYRange } from '#uilib/components/ui/ChartAreaInteractive/overlays/useChartYRange';
|
|
7
|
+
import { Label } from '#uilib/components/ui/Label';
|
|
8
|
+
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
9
|
+
import { Switch } from '#uilib/components/ui/Switch';
|
|
10
|
+
import { useTheme } from '#uilib/contexts/theme-context';
|
|
11
|
+
|
|
12
|
+
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
13
|
+
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
14
|
+
|
|
15
|
+
const FORECAST_BASELINE_ID = 1;
|
|
16
|
+
const FORECAST_HIGH_ID = 2;
|
|
17
|
+
|
|
18
|
+
const DEMO_FORECAST_ITEMS: ForecastItemData[] = [
|
|
19
|
+
{ id: FORECAST_BASELINE_ID, name: 'Baseline forecast' },
|
|
20
|
+
{ id: FORECAST_HIGH_ID, name: 'High scenario (hidden)' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const INITIAL_CHART: ChartDataPoint[] = [
|
|
24
|
+
{ date: '2022-01-01', historical: 8 },
|
|
25
|
+
{ date: '2022-02-01', historical: 10 },
|
|
26
|
+
{ date: '2022-03-01', historical: 9 },
|
|
27
|
+
{ date: '2022-04-01', historical: 12 },
|
|
28
|
+
{ date: '2022-05-01', historical: 11 },
|
|
29
|
+
{ date: '2022-06-01', historical: 12 },
|
|
30
|
+
{ date: '2022-07-01', historical: 13 },
|
|
31
|
+
{ date: '2022-08-01', historical: 12 },
|
|
32
|
+
{ date: '2022-09-01', historical: 14 },
|
|
33
|
+
{ date: '2022-10-01', historical: 13 },
|
|
34
|
+
{ date: '2022-11-01', historical: 15 },
|
|
35
|
+
{ date: '2022-12-01', historical: 14 },
|
|
36
|
+
{ date: '2023-01-01', historical: 10 },
|
|
37
|
+
{ date: '2023-02-01', historical: 12 },
|
|
38
|
+
{ date: '2023-03-01', historical: 11 },
|
|
39
|
+
{ date: '2023-04-01', historical: 14 },
|
|
40
|
+
{ date: '2023-05-01', historical: 13 },
|
|
41
|
+
{
|
|
42
|
+
date: '2023-06-01',
|
|
43
|
+
[`forecast_${FORECAST_BASELINE_ID}`]: 13.5,
|
|
44
|
+
[`forecast_${FORECAST_HIGH_ID}`]: 88,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
date: '2023-07-01',
|
|
48
|
+
[`forecast_${FORECAST_BASELINE_ID}`]: 14,
|
|
49
|
+
[`forecast_${FORECAST_HIGH_ID}`]: 92,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
date: '2023-08-01',
|
|
53
|
+
[`forecast_${FORECAST_BASELINE_ID}`]: 15,
|
|
54
|
+
[`forecast_${FORECAST_HIGH_ID}`]: 95,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export default function IncludeHiddenInYScalePage() {
|
|
59
|
+
const { isDarkMode } = useTheme();
|
|
60
|
+
const [timeRange, setTimeRange] = useState<string>('All');
|
|
61
|
+
const [chartData] = useState<ChartDataPoint[]>(INITIAL_CHART);
|
|
62
|
+
const [hidden, setHidden] = useState<Set<string>>(
|
|
63
|
+
() => new Set([`forecast_${FORECAST_HIGH_ID}`]),
|
|
64
|
+
);
|
|
65
|
+
const [includeHiddenInYScale, setIncludeHiddenInYScale] = useState(false);
|
|
66
|
+
|
|
67
|
+
const toggleLegendSeries = useCallback((key: string) => {
|
|
68
|
+
setHidden(prev => {
|
|
69
|
+
const next = new Set(prev);
|
|
70
|
+
if (next.has(key)) next.delete(key);
|
|
71
|
+
else next.add(key);
|
|
72
|
+
return next;
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const ensureAnalysisSeriesVisible = useCallback(() => {}, []);
|
|
77
|
+
|
|
78
|
+
const yRange = useMemo(
|
|
79
|
+
() =>
|
|
80
|
+
calculateChartYRange(chartData, {
|
|
81
|
+
excludeQuantileBands: true,
|
|
82
|
+
hiddenSeries: hidden,
|
|
83
|
+
includeHiddenInYScale,
|
|
84
|
+
}),
|
|
85
|
+
[chartData, hidden, includeHiddenInYScale],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<>
|
|
90
|
+
<AppPageHeader
|
|
91
|
+
breadcrumbs={[{ label: 'includeHiddenInYScale' }]}
|
|
92
|
+
title="includeHiddenInYScale"
|
|
93
|
+
subheader={
|
|
94
|
+
<>
|
|
95
|
+
<code>BaseChartWrapper</code> / <code>ChartAreaInteractive</code>{' '}
|
|
96
|
+
prop. When <code>false</code> (default), hidden legend series are
|
|
97
|
+
excluded from Y-axis domain; when <code>true</code>, hidden values
|
|
98
|
+
still expand the scale (legacy behavior). Toggle the high scenario
|
|
99
|
+
in the legend, then flip the switch to compare.
|
|
100
|
+
</>
|
|
101
|
+
}
|
|
102
|
+
actions={<DocsHeaderActions />}
|
|
103
|
+
/>
|
|
104
|
+
<PageContentSection>
|
|
105
|
+
<div
|
|
106
|
+
style={{
|
|
107
|
+
display: 'flex',
|
|
108
|
+
flexWrap: 'wrap',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
gap: 16,
|
|
111
|
+
marginBottom: 16,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
115
|
+
<Switch
|
|
116
|
+
id="include-hidden-y-scale"
|
|
117
|
+
checked={includeHiddenInYScale}
|
|
118
|
+
onCheckedChange={setIncludeHiddenInYScale}
|
|
119
|
+
/>
|
|
120
|
+
<Label htmlFor="include-hidden-y-scale">
|
|
121
|
+
includeHiddenInYScale
|
|
122
|
+
</Label>
|
|
123
|
+
</div>
|
|
124
|
+
<span style={{ color: 'var(--muted-foreground)', fontSize: 13 }}>
|
|
125
|
+
Y domain: {yRange.yMin} – {yRange.yMax}
|
|
126
|
+
{hidden.has(`forecast_${FORECAST_HIGH_ID}`)
|
|
127
|
+
? ' (high scenario hidden)'
|
|
128
|
+
: ' (all series visible)'}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<ChartAreaInteractive
|
|
132
|
+
timeRange={timeRange}
|
|
133
|
+
onTimeRangeChange={setTimeRange}
|
|
134
|
+
pinMonth={undefined}
|
|
135
|
+
onPinMonthChange={() => {}}
|
|
136
|
+
chartData={chartData}
|
|
137
|
+
forecastData={DEMO_FORECAST_ITEMS}
|
|
138
|
+
loading={false}
|
|
139
|
+
isDarkTheme={isDarkMode}
|
|
140
|
+
toggleLegendSeries={toggleLegendSeries}
|
|
141
|
+
ensureAnalysisSeriesVisible={ensureAnalysisSeriesVisible}
|
|
142
|
+
hiddenSeries={hidden}
|
|
143
|
+
includeHiddenInYScale={includeHiddenInYScale}
|
|
144
|
+
yMin={yRange.yMin}
|
|
145
|
+
yMax={yRange.yMax}
|
|
146
|
+
autoScaleYAxis={false}
|
|
147
|
+
disableTimeRangeSelector
|
|
148
|
+
/>
|
|
149
|
+
</PageContentSection>
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
}
|
package/src/docs/registry.ts
CHANGED
|
@@ -97,6 +97,12 @@ export const DOC_REGISTRY: DocEntry[] = [
|
|
|
97
97
|
section: 'Charts',
|
|
98
98
|
load: () => import('./pages/ChartAreaInteractivePage'),
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
slug: 'include-hidden-in-y-scale',
|
|
102
|
+
title: 'includeHiddenInYScale',
|
|
103
|
+
section: 'Charts',
|
|
104
|
+
load: () => import('./pages/IncludeHiddenInYScalePage'),
|
|
105
|
+
},
|
|
100
106
|
{
|
|
101
107
|
slug: 'lightweight-forecast-chart',
|
|
102
108
|
title: 'LightweightForecastChart',
|