@sybilion/uilib 1.3.63 → 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/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/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/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 };
|
|
@@ -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 {};
|
|
@@ -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
|
|
@@ -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',
|