@sybilion/uilib 1.3.63 → 1.3.65
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/Chart/components/QuantileBands.js +1 -1
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +60 -1
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/useChartYRange.js +111 -61
- package/dist/esm/components/widgets/PerformanceChart/performanceChart.helpers.js +132 -33
- package/dist/esm/index.js +3 -1
- package/dist/esm/types/src/components/ui/Chart/components/BaseChartWrapper.d.ts +2 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +3 -2
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/index.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/PerformanceChart/index.d.ts +1 -1
- package/dist/esm/types/src/components/widgets/PerformanceChart/performanceChart.helpers.d.ts +22 -2
- package/dist/esm/types/src/docs/pages/IncludeHiddenInYScalePage.d.ts +1 -0
- package/package.json +4 -2
- package/src/components/ui/Chart/components/BaseChartWrapper.tsx +14 -4
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +4 -3
- package/src/components/ui/ChartAreaInteractive/index.ts +2 -0
- package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.test.ts +87 -0
- package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.ts +152 -73
- package/src/components/ui/Page/AGENT.md +165 -0
- package/src/components/widgets/AGENT.md +1 -1
- package/src/components/widgets/PerformanceChart/index.ts +3 -0
- package/src/components/widgets/PerformanceChart/performanceChart.helpers.ts +197 -41
- package/src/docs/pages/IncludeHiddenInYScalePage.tsx +152 -0
- package/src/docs/registry.ts +6 -0
package/dist/esm/types/src/components/widgets/PerformanceChart/performanceChart.helpers.d.ts
CHANGED
|
@@ -21,14 +21,14 @@ export declare function isSpaghettiDriftPerHorizonLineId(id: number): boolean;
|
|
|
21
21
|
/**
|
|
22
22
|
* Converts `performance.model` / `performance.drift` per-horizon forecasts into synthetic
|
|
23
23
|
* backtest-shaped entries: each line is [horizon_1[i], …, horizon_n[i]] as date→value points
|
|
24
|
-
* (aligned
|
|
24
|
+
* (aligned on the latest shared months per horizon).
|
|
25
25
|
*/
|
|
26
26
|
export declare function buildPerHorizonSpaghettiEntries(forecastRoot: {
|
|
27
27
|
forecasts?: Record<string, Record<string, number>>;
|
|
28
28
|
} | null | undefined, horizonKeys: string[]): RealBacktestsEntry[];
|
|
29
29
|
/**
|
|
30
30
|
* Same row alignment as {@link buildPerHorizonSpaghettiEntries}: row `i` uses each horizon's
|
|
31
|
-
*
|
|
31
|
+
* latest-aligned forecast month; `dates[i]` is horizon_1's month at that row (Date column).
|
|
32
32
|
* Use this for custom dialog seed + “copy statistical baseline (drift)” prefill.
|
|
33
33
|
*/
|
|
34
34
|
export declare function buildDriftSpaghettiMatrixForCustomDialog(driftRoot: {
|
|
@@ -38,6 +38,26 @@ export declare function buildDriftSpaghettiMatrixForCustomDialog(driftRoot: {
|
|
|
38
38
|
grid: number[][];
|
|
39
39
|
perHorizonDates: string[][];
|
|
40
40
|
} | null;
|
|
41
|
+
export declare function latestHistoricalMonthKey(historicalByDate: Map<string, number>): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Append monthly spaghetti rows after drift backtests so the dialog (and chart) reach the latest
|
|
44
|
+
* historical month (e.g. Apr 2026), not only the last drift origin (e.g. Nov 2025).
|
|
45
|
+
*/
|
|
46
|
+
export declare function extendCustomPerformanceDriftSeedToHistoricalEnd(seed: {
|
|
47
|
+
dates: string[];
|
|
48
|
+
grid: number[][];
|
|
49
|
+
perHorizonDates: string[][];
|
|
50
|
+
}, historicalByDate: Map<string, number>): {
|
|
51
|
+
dates: string[];
|
|
52
|
+
grid: number[][];
|
|
53
|
+
perHorizonDates: string[][];
|
|
54
|
+
};
|
|
55
|
+
/** Pad a saved custom matrix with drift/baseline rows so edit + chart cover latest backtest months. */
|
|
56
|
+
export declare function extendCustomPerformanceMatrixWithDriftSeed(saved: SpaghettiPerformanceMatrixPayload, driftSeed: {
|
|
57
|
+
dates: string[];
|
|
58
|
+
grid: number[][];
|
|
59
|
+
perHorizonDates: string[][];
|
|
60
|
+
}, baselineGrid?: number[][]): SpaghettiPerformanceMatrixPayload;
|
|
41
61
|
/**
|
|
42
62
|
* Prefill for custom performance when copying drift layout: each spaghetti row is flat at the
|
|
43
63
|
* historical value for the month before the earliest forecast month in that row (same anchor as
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function IncludeHiddenInYScalePage(): import("react/jsx-runtime").JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sybilion/uilib",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.65",
|
|
4
4
|
"description": "Sybilion Design System — React UI components (Webpack + Stylus)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"docs",
|
|
43
43
|
"src",
|
|
44
44
|
"dist",
|
|
45
|
+
"dist/agent-glossary",
|
|
45
46
|
"package.json"
|
|
46
47
|
],
|
|
47
48
|
"directories": {
|
|
@@ -53,7 +54,8 @@
|
|
|
53
54
|
"ts": "tsc --noEmit",
|
|
54
55
|
"dev": "NODE_OPTIONS='--loader ts-node/esm' NODE_ENV=development webpack-dev-server --mode=development --config ./src/docs/config/webpack.config.js --progress",
|
|
55
56
|
"prebuild": "cp src/components/ui/Logo/logo.svg assets/logo.svg",
|
|
56
|
-
"build": "
|
|
57
|
+
"build:agent-glossary": "node scripts/build-agent-glossary.mjs --all",
|
|
58
|
+
"build": "yarn build:agent-glossary && rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript",
|
|
57
59
|
"build:watch": "yarn build --watch",
|
|
58
60
|
"build:compress": "COMPRESS=true yarn build",
|
|
59
61
|
"publish:npm": "yarn build && npm publish --access public",
|
|
@@ -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
|
|
|
@@ -67,9 +67,6 @@ const timeRangeToMonths = {
|
|
|
67
67
|
|
|
68
68
|
export type TimeRangePreset = keyof typeof timeRangeToMonths;
|
|
69
69
|
|
|
70
|
-
/** @deprecated Use `TimeRangePreset` or `string` for brush-encoded ranges. */
|
|
71
|
-
export type TimeRange = TimeRangePreset;
|
|
72
|
-
|
|
73
70
|
export const DRAG_TIME_RANGE_PREFIX = '__drag:' as const;
|
|
74
71
|
|
|
75
72
|
export function encodeDragTimeRange(a: Date, b: Date): string {
|
|
@@ -258,3 +255,7 @@ export const isAnalysisFailed = (
|
|
|
258
255
|
export const isAnalysisNotFailed = (
|
|
259
256
|
analysis: Analysis | { status?: string },
|
|
260
257
|
): boolean => !isAnalysisFailed(analysis);
|
|
258
|
+
|
|
259
|
+
export const isAnalysisDone = (
|
|
260
|
+
analysis: Analysis | { status?: string },
|
|
261
|
+
): boolean => analysis.status?.toLowerCase() === 'done';
|
|
@@ -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,165 @@
|
|
|
1
|
+
# Page layout system
|
|
2
|
+
|
|
3
|
+
Sybilion workspace and report surfaces use a small set of **Page** primitives from `@sybilion/uilib`. Agents must compose these instead of inventing layout with inline styles, Tailwind structure classes, or ad-hoc wrappers.
|
|
4
|
+
|
|
5
|
+
## Two modes
|
|
6
|
+
|
|
7
|
+
| Mode | When | Shell |
|
|
8
|
+
| ------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
9
|
+
| **workspace-page** | Route inside `AppShell` / scrolled main column | `PageHeader` → `PageContent` → `PageContentSection`(s). Optional `PageTabs`. |
|
|
10
|
+
| **content** | Report tiles, json-dashboard, embedded body only | No `PageHeader` / `AppShell` chrome. Map React blocks to dashboard tiles (see below). |
|
|
11
|
+
|
|
12
|
+
Do not nest workspace shell components inside **content** mode output.
|
|
13
|
+
|
|
14
|
+
## Mandatory workspace-page skeleton
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
<PageHeader title="…" subheader="…" breadcrumbs={[…]} actions={…} />
|
|
18
|
+
<PageContent>
|
|
19
|
+
<PageContentSection>{/* section A */}</PageContentSection>
|
|
20
|
+
<PageContentSection grow={false}>{/* section B */}</PageContentSection>
|
|
21
|
+
</PageContent>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **PageHeader**: one per route view in the main column; title, optional subheader, breadcrumbs, actions.
|
|
25
|
+
- **PageContent**: vertical stack for the body below the header; `variant="clean"` when you need a flatter body (no extra chrome).
|
|
26
|
+
- **PageContentSection**: primary vertical band; applies horizontal **page-x-padding** via Stylus (`pageXPadding()`). Use multiple sections to separate logical blocks.
|
|
27
|
+
|
|
28
|
+
Global tokens (host app `:root`, e.g. `standalone-global.css`): `--page-width`, `--page-x-padding`, `--page-y-padding`. `PageContent` caps width at `--page-width`.
|
|
29
|
+
|
|
30
|
+
## PageTabs — rules and patterns
|
|
31
|
+
|
|
32
|
+
**NEVER** place `PageTabs` inside `PageContentSection`. Tabs manage their own horizontal inset via `PageXScroll` / `tabsListProps.withPaddings`.
|
|
33
|
+
|
|
34
|
+
### Unified (tabs + panels inside PageContent)
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<PageHeader … />
|
|
38
|
+
<PageContent>
|
|
39
|
+
<PageTabs
|
|
40
|
+
items={[
|
|
41
|
+
{ value: 'a', label: 'Tab A', content: <PageContentSection>…</PageContentSection> },
|
|
42
|
+
{ value: 'b', label: 'Tab B', content: <PageContentSection>…</PageContentSection> },
|
|
43
|
+
]}
|
|
44
|
+
/>
|
|
45
|
+
</PageContent>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Tab **panels** use `PageContentSection` so section padding stays correct.
|
|
49
|
+
|
|
50
|
+
### Split (bar-only sibling)
|
|
51
|
+
|
|
52
|
+
Use when the tab bar must sit between header and a custom scroll region:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<PageHeader … />
|
|
56
|
+
<PageTabs
|
|
57
|
+
items={[
|
|
58
|
+
{ value: 'a', label: 'Tab A', content: null },
|
|
59
|
+
{ value: 'b', label: 'Tab B', content: null },
|
|
60
|
+
]}
|
|
61
|
+
/>
|
|
62
|
+
<PageContent>…</PageContent>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Set `content: null` (or omit) for bar-only items; render panel bodies in `PageContent` yourself.
|
|
66
|
+
|
|
67
|
+
## Spacing ownership
|
|
68
|
+
|
|
69
|
+
| Component | Owns horizontal page padding? |
|
|
70
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
71
|
+
| **PageContentSection** | Yes (`pageXPadding`) |
|
|
72
|
+
| **PageTabs** | Via `tabsListProps.withPaddings` + internal `PageXScroll` |
|
|
73
|
+
| **PageXScroll** | Reads `--page-x-padding` for inset when used standalone |
|
|
74
|
+
| **PageHeader** | Own header inner layout (not `PageContentSection`) |
|
|
75
|
+
|
|
76
|
+
Do not duplicate `padding-left/right` on children that already sit in `PageContentSection`.
|
|
77
|
+
|
|
78
|
+
## Styling rules
|
|
79
|
+
|
|
80
|
+
- **No** `style={{…}}` for layout or spacing (margins, padding, flex, width, gap).
|
|
81
|
+
- **No** Tailwind (or other) utility classes for **structural** layout on page primitives.
|
|
82
|
+
- Custom layout CSS only in co-located `ComponentName.styl`, using design tokens (`var(--p-*)`, `var(--page-x-padding)`, `pageXPadding()`, etc.).
|
|
83
|
+
- `className` on Page components is for Stylus module classes or state hooks, not one-off pixel tweaks.
|
|
84
|
+
|
|
85
|
+
## Component reference
|
|
86
|
+
|
|
87
|
+
### PageHeader
|
|
88
|
+
|
|
89
|
+
Sticky-style page title row: breadcrumbs (optional sidebar trigger in full app), `title`, `subheader`, `actions`. Lives **above** `PageContent`, never inside a section.
|
|
90
|
+
|
|
91
|
+
### PageContent / PageContentSection
|
|
92
|
+
|
|
93
|
+
`PageContent`: column flex child, `max-width: var(--page-width)`.
|
|
94
|
+
`PageContentSection`: `grow` (default `true`) lets the section absorb remaining height in flex layouts.
|
|
95
|
+
|
|
96
|
+
### PageColumns
|
|
97
|
+
|
|
98
|
+
Side-by-side columns with gap. **`fill`** controls which column(s) grow:
|
|
99
|
+
|
|
100
|
+
| `fill` | Behavior |
|
|
101
|
+
| --------------- | ----------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `all` (default) | Every column `flex-grow: 1`, `max-width` unset — use for equal split or fluid grids of columns. |
|
|
103
|
+
| `left` | First column grows; others stay at fixed min/max column width. |
|
|
104
|
+
| `right` | Last column grows. |
|
|
105
|
+
|
|
106
|
+
Pass `columns={[node1, node2, …]}`. On mobile, stacks vertically. Typical pattern: main + aside inside one `PageContentSection`.
|
|
107
|
+
|
|
108
|
+
### PageXScroll
|
|
109
|
+
|
|
110
|
+
Horizontal scroll strip; used inside `PageTabs` and anywhere a overflowing row needs page-aligned padding. Props: `size`, `fullWidth`, `innerClassName`, `scrollbarClassName`.
|
|
111
|
+
|
|
112
|
+
### SectionHeader
|
|
113
|
+
|
|
114
|
+
In-section title block (`title`, `description`, `actions`). Use **inside** `PageContentSection`, not as a replacement for `PageHeader`.
|
|
115
|
+
|
|
116
|
+
### PageEmptyCanvas
|
|
117
|
+
|
|
118
|
+
Centered empty state: `title`, `hint`, optional `children` (e.g. CTA button). Use when a route has no data yet.
|
|
119
|
+
|
|
120
|
+
### GridLayout
|
|
121
|
+
|
|
122
|
+
Responsive CSS grid (`repeat(auto-fit, minmax(colWidth, 1fr))`). Use for card/tile dashboards inside a section. Report mapping: dashboard **grid** rows / multi-column tile groups.
|
|
123
|
+
|
|
124
|
+
### Card / Foldable
|
|
125
|
+
|
|
126
|
+
Use inside sections for grouped content; see package `AGENT.md` on those components when present. Do not wrap the whole page in Card.
|
|
127
|
+
|
|
128
|
+
## Content mode — React → json-dashboard tiles
|
|
129
|
+
|
|
130
|
+
When generating **report** / dashboard specs (not workspace routes), map UI blocks to tiles:
|
|
131
|
+
|
|
132
|
+
| React / intent | Dashboard tile / markdown |
|
|
133
|
+
| ------------------------------- | ----------------------------------------------------------------- |
|
|
134
|
+
| `PageHeader` title text | First `markdown` tile `# Title` or report title field |
|
|
135
|
+
| `SectionHeader` `title` | `markdown` with `## …` |
|
|
136
|
+
| `SectionHeader` `description` | markdown paragraph under that heading |
|
|
137
|
+
| `PageContentSection` block | group of tiles or one markdown section |
|
|
138
|
+
| `GridLayout` children | grid row of widget tiles |
|
|
139
|
+
| `ChartAreaInteractive` / charts | `dataset_card`, `performance_chart`, etc. (see widget `AGENT.md`) |
|
|
140
|
+
| `PageEmptyCanvas` copy | `markdown` explanatory tile |
|
|
141
|
+
|
|
142
|
+
See `AGENT.report-only-tiles.md` in the app repo for tiles without a uilib widget.
|
|
143
|
+
|
|
144
|
+
## Shell components (workspace only)
|
|
145
|
+
|
|
146
|
+
Not part of the per-route skeleton but part of **workspace-page** host wiring:
|
|
147
|
+
|
|
148
|
+
- **AppShell** / **AppShellMainContent** — sidebar + main column + header slot + footer.
|
|
149
|
+
- **PageScroll** — scroll root for the main column.
|
|
150
|
+
- **SybilionAppHeader** — workspace app switcher + user menu (lives in header slot, not inside `PageContent`).
|
|
151
|
+
|
|
152
|
+
## Anti-patterns
|
|
153
|
+
|
|
154
|
+
- `PageTabs` inside `PageContentSection`.
|
|
155
|
+
- Inline styles or Tailwind for page structure.
|
|
156
|
+
- Second `PageHeader` nested in content.
|
|
157
|
+
- `PageContent` without sections for multi-block pages (prefer multiple `PageContentSection`).
|
|
158
|
+
- Wrapping data widgets in extra layout divs with hard-coded padding.
|
|
159
|
+
|
|
160
|
+
## Future candidates (not implemented)
|
|
161
|
+
|
|
162
|
+
- **PageToolbar** — dedicated action row between header and tabs.
|
|
163
|
+
- **PageFullBleed** — edge-to-edge section escape hatch.
|
|
164
|
+
|
|
165
|
+
Do not add these locally; extend uilib when product needs them.
|
|
@@ -6,7 +6,7 @@ Each component file ≤18 lines. Signal for LLM prompts — no noise.
|
|
|
6
6
|
|
|
7
7
|
**Include:** Renders (1 sentence); Use when / Not when; Host provides (3–5 bullets); Report tile (one line or "Not used"); Requires (prop names + role); Empty/loading (one line).
|
|
8
8
|
|
|
9
|
-
**Do not include:** import examples; type/doc/demo/glossary links; page-shell boilerplate; related-components lists unless choosing between exports; implementation/styling/keyboard notes unless binding-relevant; secrets, env vars, API URLs.
|
|
9
|
+
**Do not include:** import examples; type/doc/demo/glossary links; page-shell boilerplate; related-components lists unless choosing between exports; implementation/styling/keyboard notes unless binding-relevant; secrets, env vars, API URLs; inline style examples (`style={{…}}`).
|
|
10
10
|
|
|
11
11
|
```markdown
|
|
12
12
|
# Name
|