@sybilion/uilib 1.3.0 → 1.3.2

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.
Files changed (34) hide show
  1. package/dist/esm/components/ui/Chart/Chart.js +4 -0
  2. package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.js +460 -0
  3. package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.styl.js +7 -0
  4. package/dist/esm/components/ui/Chart/lightweight/chartTime.js +16 -0
  5. package/dist/esm/components/ui/Chart/lightweight/lightweightForecastChart.helpers.js +114 -0
  6. package/dist/esm/components/ui/Chart/lightweight/quantileBandCustomSeries.js +147 -0
  7. package/dist/esm/components/ui/Chart/quantileBandConeChartData.js +131 -0
  8. package/dist/esm/components/ui/ChartAreaInteractive/overlays/useQuantileBands.js +4 -102
  9. package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.js +4 -0
  10. package/dist/esm/index.js +1 -0
  11. package/dist/esm/types/src/components/ui/Chart/Chart.d.ts +1 -0
  12. package/dist/esm/types/src/components/ui/Chart/lightweight/LightweightForecastChart.d.ts +26 -0
  13. package/dist/esm/types/src/components/ui/Chart/lightweight/chartTime.d.ts +5 -0
  14. package/dist/esm/types/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.d.ts +13 -0
  15. package/dist/esm/types/src/components/ui/Chart/lightweight/quantileBandCustomSeries.d.ts +24 -0
  16. package/dist/esm/types/src/components/ui/Chart/quantileBandConeChartData.d.ts +7 -0
  17. package/dist/esm/types/src/docs/pages/LightweightChartPage.d.ts +1 -0
  18. package/package.json +3 -2
  19. package/src/components/ui/Chart/Chart.tsx +4 -0
  20. package/src/components/ui/Chart/lightweight/LightweightForecastChart.styl +25 -0
  21. package/src/components/ui/Chart/lightweight/LightweightForecastChart.styl.d.ts +11 -0
  22. package/src/components/ui/Chart/lightweight/LightweightForecastChart.tsx +721 -0
  23. package/src/components/ui/Chart/lightweight/chartTime.ts +18 -0
  24. package/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.ts +141 -0
  25. package/src/components/ui/Chart/lightweight/quantileBandCustomSeries.ts +215 -0
  26. package/src/components/ui/Chart/quantileBandConeChartData.ts +171 -0
  27. package/src/components/ui/ChartAreaInteractive/overlays/useQuantileBands.ts +5 -131
  28. package/src/declarations.d.ts +2 -0
  29. package/src/docs/config/webpack.config.js +25 -2
  30. package/src/docs/index.tsx +1 -1
  31. package/src/docs/pages/LightweightChartPage.styl +18 -0
  32. package/src/docs/pages/LightweightChartPage.styl.d.ts +10 -0
  33. package/src/docs/pages/LightweightChartPage.tsx +195 -0
  34. package/src/docs/registry.ts +6 -0
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from 'react';
2
2
 
3
3
  import type { QuantileBandConfig } from '#uilib/components/ui/Chart/chartForecastVisualization.types';
4
+ import { applyQuantileBandConeToChartData } from '#uilib/components/ui/Chart/quantileBandConeChartData';
4
5
  import { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
5
6
  import type { ForecastData } from '#uilib/types/forecast-data';
6
7
 
@@ -45,126 +46,17 @@ export function useQuantileBands({
45
46
  const allQuantilesData = forecastDataForSelected.allQuantiles;
46
47
  const clonedData = [...chartData];
47
48
 
48
- // Get forecast dates to map quantile array indices correctly
49
49
  const forecastDates = forecastDataForSelected.dates || [];
50
50
  const forecastDatesSet = new Set(forecastDates);
51
51
 
52
- // Find the last historical point for connection
53
- const historicalPoints = clonedData.filter(
54
- point => point.historical !== undefined,
55
- );
56
- const lastHistoricalPoint =
57
- historicalPoints.length > 0
58
- ? historicalPoints[historicalPoints.length - 1]
59
- : null;
60
- const lastHistoricalDate = lastHistoricalPoint?.date;
61
- const lastHistoricalValue = lastHistoricalPoint?.historical;
62
-
63
- // Get first forecast date
64
- const firstForecastDate = forecastDates[0];
65
- const firstForecastDateObj = firstForecastDate
66
- ? new Date(firstForecastDate)
67
- : null;
68
- const lastHistoricalDateObj = lastHistoricalDate
69
- ? new Date(lastHistoricalDate)
70
- : null;
71
-
72
- // Check if there's a gap between historical and forecast data (forecast starts after historical)
73
- const hasGap =
74
- lastHistoricalDate &&
75
- firstForecastDate &&
76
- lastHistoricalValue !== undefined &&
77
- firstForecastDateObj &&
78
- lastHistoricalDateObj &&
79
- firstForecastDateObj.getTime() > lastHistoricalDateObj.getTime();
80
-
81
- // Check if forecast starts before or at last historical point (need bridge point)
82
- const needsBridgePoint =
83
- lastHistoricalDate &&
84
- firstForecastDate &&
85
- lastHistoricalValue !== undefined &&
86
- firstForecastDateObj &&
87
- lastHistoricalDateObj &&
88
- firstForecastDateObj.getTime() <= lastHistoricalDateObj.getTime();
89
-
90
- // Find bridge point when forecast starts before or at last historical point
91
- let bridgePoint = null;
92
- let pointBeforeForecast = null;
93
- if (needsBridgePoint && historicalPoints.length > 0) {
94
- // Find the last historical point before or at the first forecast date
95
- // If dates are equal, use lastHistoricalPoint; otherwise find the point before
96
- bridgePoint =
97
- firstForecastDateObj &&
98
- lastHistoricalDateObj &&
99
- firstForecastDateObj.getTime() === lastHistoricalDateObj.getTime()
100
- ? lastHistoricalPoint
101
- : [...historicalPoints]
102
- .reverse()
103
- .find(
104
- p =>
105
- firstForecastDateObj &&
106
- new Date(p.date).getTime() < firstForecastDateObj.getTime(),
107
- ) || lastHistoricalPoint;
108
-
109
- // Find the actual point BEFORE the first forecast date for connection
110
- if (firstForecastDateObj) {
111
- pointBeforeForecast = [...historicalPoints].findLast(
112
- p => new Date(p.date).getTime() < firstForecastDateObj.getTime(),
113
- );
114
- }
115
- }
116
-
117
- // Create a map from date to quantile array index
118
52
  const dateToQuantileIndex = new Map<string, number>();
119
53
  forecastDates.forEach((date, index) => {
120
54
  dateToQuantileIndex.set(date, index);
121
55
  });
122
56
 
123
- const result = clonedData.map(point => {
57
+ const withRawBands = clonedData.map(point => {
124
58
  const newPoint = { ...point };
125
59
 
126
- // If there's a gap and this is the last historical point, add band data for connection
127
- if (
128
- hasGap &&
129
- point.date === lastHistoricalDate &&
130
- lastHistoricalValue !== undefined
131
- ) {
132
- // Set zero-width band at the last historical value for visual connection
133
- newPoint[bandKey] = [lastHistoricalValue, lastHistoricalValue] as [
134
- number,
135
- number,
136
- ];
137
- }
138
-
139
- // If forecast starts before or at last historical point, add bridge point connection
140
- // Set connection band at the point BEFORE the first forecast date (if exists)
141
- const isBridgePointDate =
142
- needsBridgePoint &&
143
- bridgePoint &&
144
- bridgePoint.historical !== undefined &&
145
- point.date === bridgePoint.date;
146
- const isPointBeforeForecast =
147
- needsBridgePoint &&
148
- pointBeforeForecast &&
149
- pointBeforeForecast.historical !== undefined &&
150
- point.date === pointBeforeForecast.date;
151
- const isAlsoForecastDate = forecastDatesSet.has(point.date);
152
-
153
- // Set zero-width connection band at the point BEFORE forecast starts
154
- if (isPointBeforeForecast && !isAlsoForecastDate) {
155
- newPoint[bandKey] = [
156
- pointBeforeForecast.historical,
157
- pointBeforeForecast.historical,
158
- ] as [number, number];
159
- } else if (isBridgePointDate && !isAlsoForecastDate) {
160
- // Fallback: if no point before forecast, use bridge point
161
- newPoint[bandKey] = [
162
- bridgePoint.historical,
163
- bridgePoint.historical,
164
- ] as [number, number];
165
- }
166
-
167
- // Only update band data for forecast dates
168
60
  if (forecastDatesSet.has(point.date)) {
169
61
  const quantileIndex = dateToQuantileIndex.get(point.date);
170
62
 
@@ -176,35 +68,17 @@ export function useQuantileBands({
176
68
  );
177
69
 
178
70
  if (bandValues) {
179
- // If this is also the bridge point (forecast starts at same date as last historical),
180
- // start the band from the historical value for smooth connection
181
- const isBridgePointDate =
182
- needsBridgePoint &&
183
- bridgePoint &&
184
- point.date === bridgePoint.date &&
185
- bridgePoint.historical !== undefined;
186
-
187
- if (isBridgePointDate && quantileIndex === 0) {
188
- // Start from historical value, expand to forecast upper bound
189
- newPoint[bandKey] = [bridgePoint.historical, bandValues[1]] as [
190
- number,
191
- number,
192
- ];
193
- } else {
194
- newPoint[bandKey] = bandValues;
195
- }
71
+ newPoint[bandKey] = bandValues;
196
72
  } else {
197
- // Remove band data if values don't exist
198
73
  delete newPoint[bandKey];
199
74
  }
200
75
  }
201
76
  }
202
- // For non-forecast dates, preserve existing band data if it exists
203
- // This ensures continuity of the band visualization
204
77
 
205
78
  return newPoint;
206
79
  });
207
- return result;
80
+
81
+ return applyQuantileBandConeToChartData(withRawBands, bandKey);
208
82
  }, [chartData, selectedForecastId, forecastData, bandKey, getBandValues]);
209
83
 
210
84
  const quantileBands: QuantileBandConfig[] = useMemo(() => {
@@ -1,4 +1,6 @@
1
1
  declare const VERSION: string;
2
+ /** Base URL path for docs Router (empty at site root; e.g. `/uilib` on GitHub Pages project site). */
3
+ declare const DOCS_ROUTER_BASENAME: string;
2
4
 
3
5
  declare module '*.png';
4
6
  declare module '*.json';
@@ -22,9 +22,27 @@ const pkg = require('../../../package.json');
22
22
  const themeStyl = pathResolve(paths.src, 'theme.styl');
23
23
  const logoSvgPath = pathResolve(paths.assets, 'logo.svg');
24
24
 
25
+ /** GitHub Pages project sites live at /<repo>/; set PUBLIC_PATH=/repo-name/ for production deploy. */
26
+ function normalizePublicPath(raw) {
27
+ const v = (raw && String(raw).trim()) || '/';
28
+ if (v === '/') {
29
+ return '/';
30
+ }
31
+ const withLeading = v.startsWith('/') ? v : `/${v}`;
32
+ return withLeading.endsWith('/') ? withLeading : `${withLeading}/`;
33
+ }
34
+
35
+ function routerBasenameFromPublicPath(publicPath) {
36
+ if (publicPath === '/') {
37
+ return '';
38
+ }
39
+ return publicPath.replace(/\/$/, '');
40
+ }
41
+
25
42
  export default (env, argv) => {
26
43
  const isDev = argv.mode === 'development';
27
- const analyticsPath = `${paths.assets}/analytics.html`;
44
+ const publicPath = normalizePublicPath(process.env.PUBLIC_PATH);
45
+ const docsRouterBasename = routerBasenameFromPublicPath(publicPath);
28
46
  const alias = {
29
47
  '#uilib': paths.src,
30
48
  uilib: paths.src,
@@ -38,6 +56,7 @@ export default (env, argv) => {
38
56
  entry: [paths.docs],
39
57
  output: {
40
58
  path: paths.build,
59
+ publicPath,
41
60
  },
42
61
 
43
62
  resolve: {
@@ -152,6 +171,7 @@ export default (env, argv) => {
152
171
  new webpack.DefinePlugin({
153
172
  isDEV: JSON.stringify(isDev),
154
173
  VERSION: JSON.stringify(pkg.version),
174
+ DOCS_ROUTER_BASENAME: JSON.stringify(docsRouterBasename),
155
175
  }),
156
176
  new CopyWebpackPlugin({
157
177
  patterns: [
@@ -174,7 +194,7 @@ export default (env, argv) => {
174
194
 
175
195
  new HtmlWebpackPlugin({
176
196
  lang: 'en',
177
- baseUrl: '/',
197
+ baseUrl: publicPath,
178
198
  filename: 'index.html',
179
199
  template: `${paths.assets}/index.html`,
180
200
  minify: isDev
@@ -221,6 +241,9 @@ export default (env, argv) => {
221
241
  hot: true,
222
242
  port: process.env.PORT || 8181,
223
243
  historyApiFallback: true,
244
+ ...(publicPath !== '/' && {
245
+ devMiddleware: { publicPath },
246
+ }),
224
247
  },
225
248
  });
226
249
  }
@@ -10,7 +10,7 @@ const elem = document.getElementById('app-root') as HTMLElement;
10
10
  const root = createRoot(elem);
11
11
 
12
12
  root.render(
13
- <BrowserRouter>
13
+ <BrowserRouter basename={DOCS_ROUTER_BASENAME || undefined}>
14
14
  <ThemeProvider activeColor={DEFAULT_THEME_ACTIVE_COLOR}>
15
15
  <App />
16
16
  </ThemeProvider>
@@ -0,0 +1,18 @@
1
+ .sectionTitle
2
+ font-size var(--font-size-heading-5, 14px)
3
+ font-weight 600
4
+ margin 0 0 8px
5
+
6
+ .caption
7
+ margin 0 0 10px
8
+ color var(--muted-foreground, #71717a)
9
+ font-size var(--font-size-body-5, 12px)
10
+
11
+ .demo
12
+ width 100%
13
+ max-width 56rem
14
+
15
+ .stack
16
+ display flex
17
+ flex-direction column
18
+ gap 28px
@@ -0,0 +1,10 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'caption': string;
5
+ 'demo': string;
6
+ 'sectionTitle': string;
7
+ 'stack': string;
8
+ }
9
+ export const cssExports: CssExports;
10
+ export default cssExports;
@@ -0,0 +1,195 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { LightweightForecastChart } from '#uilib/components/ui/Chart';
4
+ import type { QuantileBandConfig } from '#uilib/components/ui/Chart/chartForecastVisualization.types';
5
+ import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
6
+ import type { ForecastItemData } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
7
+ import { PageContentSection } from '#uilib/components/ui/Page';
8
+ import { useTheme } from '#uilib/contexts/theme-context';
9
+
10
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
11
+ import { DocsHeaderActions } from '../docsHeaderActions';
12
+ import S from './LightweightChartPage.styl';
13
+
14
+ const DEMO_FORECAST_ID = 1;
15
+
16
+ const DEMO_FORECAST_DATES = ['2023-06-01', '2023-07-01', '2023-08-01'] as const;
17
+
18
+ const DEMO_OVERLAY_QUANTILES = {
19
+ '0.1': [12.5, 13.2, 14],
20
+ '0.25': [13, 13.6, 14.5],
21
+ '0.5': [13.5, 14, 15],
22
+ '0.75': [14, 14.5, 15.5],
23
+ '0.9': [14.5, 15, 16.2],
24
+ } as const;
25
+
26
+ const DEMO_BASE: ChartDataPoint[] = [
27
+ { date: '2021-01-01', historical: 6 },
28
+ { date: '2021-02-01', historical: 8 },
29
+ { date: '2021-03-01', historical: 7 },
30
+ { date: '2021-04-01', historical: 10 },
31
+ { date: '2021-05-01', historical: 9 },
32
+ { date: '2021-06-01', historical: 10 },
33
+ { date: '2021-07-01', historical: 11 },
34
+ { date: '2021-08-01', historical: 10 },
35
+ { date: '2021-09-01', historical: 12 },
36
+ { date: '2021-10-01', historical: 11 },
37
+ { date: '2021-11-01', historical: 13 },
38
+ { date: '2021-12-01', historical: 12 },
39
+ { date: '2022-01-01', historical: 8 },
40
+ { date: '2022-02-01', historical: 10 },
41
+ { date: '2022-03-01', historical: 9 },
42
+ { date: '2022-04-01', historical: 12 },
43
+ { date: '2022-05-01', historical: 11 },
44
+ { date: '2022-06-01', historical: 12 },
45
+ { date: '2022-07-01', historical: 13 },
46
+ { date: '2022-08-01', historical: 12 },
47
+ { date: '2022-09-01', historical: 14 },
48
+ { date: '2022-10-01', historical: 13 },
49
+ { date: '2022-11-01', historical: 15 },
50
+ { date: '2022-12-01', historical: 14 },
51
+ { date: '2023-01-01', historical: 10 },
52
+ { date: '2023-02-01', historical: 12 },
53
+ { date: '2023-03-01', historical: 11 },
54
+ { date: '2023-04-01', historical: 14 },
55
+ { date: '2023-05-01', historical: 13 },
56
+ { date: '2023-06-01', [`forecast_${DEMO_FORECAST_ID}`]: 13.5 },
57
+ { date: '2023-07-01', [`forecast_${DEMO_FORECAST_ID}`]: 14 },
58
+ { date: '2023-08-01', [`forecast_${DEMO_FORECAST_ID}`]: 15 },
59
+ ];
60
+
61
+ const DEMO_FORECAST_ITEMS: ForecastItemData[] = [
62
+ { id: DEMO_FORECAST_ID, name: 'My custom forecast' },
63
+ ];
64
+
65
+ function withDemoBands(data: ChartDataPoint[]): ChartDataPoint[] {
66
+ return data.map(row => {
67
+ const idx = (DEMO_FORECAST_DATES as readonly string[]).indexOf(row.date);
68
+ if (idx === -1) return row;
69
+ const lowerWide = DEMO_OVERLAY_QUANTILES['0.1'][idx];
70
+ const upperWide = DEMO_OVERLAY_QUANTILES['0.9'][idx];
71
+ const lowerMid = DEMO_OVERLAY_QUANTILES['0.25'][idx];
72
+ const upperMid = DEMO_OVERLAY_QUANTILES['0.75'][idx];
73
+ return {
74
+ ...row,
75
+ demo_band_wide: [lowerWide, upperWide],
76
+ demo_band_mid: [lowerMid, upperMid],
77
+ };
78
+ });
79
+ }
80
+
81
+ const DEMO_QUANTILE_BANDS: QuantileBandConfig[] = [
82
+ {
83
+ key: 'demo_band_wide',
84
+ quantiles: ['0.1', '0.9'],
85
+ opacity: 0.35,
86
+ name: 'P10–P90',
87
+ type: 'monotone',
88
+ color: 'rgba(100,190,220,0.35)',
89
+ strokeWidth: 0,
90
+ },
91
+ {
92
+ key: 'demo_band_mid',
93
+ quantiles: ['0.25', '0.75'],
94
+ opacity: 0.45,
95
+ name: 'P25–P75',
96
+ type: 'monotone',
97
+ color: 'rgba(65,165,238,0.45)',
98
+ strokeWidth: 0,
99
+ },
100
+ ];
101
+
102
+ export default function LightweightChartPage() {
103
+ const { isDarkMode } = useTheme();
104
+ const historicalOnlyData = useMemo(
105
+ () => DEMO_BASE.filter(r => r.date <= '2023-05-01'),
106
+ [],
107
+ );
108
+ const bandsData = useMemo(() => withDemoBands(DEMO_BASE), []);
109
+
110
+ return (
111
+ <>
112
+ <AppPageHeader
113
+ breadcrumbs={[{ label: 'LightweightForecastChart' }]}
114
+ title="LightweightForecastChart"
115
+ subheader={
116
+ <>
117
+ TradingView{' '}
118
+ <a href="https://www.tradingview.com/lightweight-charts/">
119
+ Lightweight Charts
120
+ </a>{' '}
121
+ with historical + forecast lines, quantile fills, tooltip, legend,
122
+ and shared <code>ensureChartForecastBridge</code> preprocessing.
123
+ </>
124
+ }
125
+ actions={<DocsHeaderActions />}
126
+ />
127
+ <PageContentSection>
128
+ <div className={S.stack}>
129
+ <div>
130
+ <h3 className={S.sectionTitle}>Historical line</h3>
131
+ <p className={S.caption}>Subset of demo months through May 2023.</p>
132
+ <div className={S.demo}>
133
+ <LightweightForecastChart
134
+ chartData={historicalOnlyData}
135
+ forecastData={[]}
136
+ isDarkTheme={isDarkMode}
137
+ height={320}
138
+ showLegend={false}
139
+ formatNumber={v =>
140
+ Number.isFinite(v) ? v.toFixed(2) : String(v)
141
+ }
142
+ />
143
+ </div>
144
+ </div>
145
+
146
+ <div>
147
+ <h3 className={S.sectionTitle}>
148
+ Historical + dashed forecast + bridge helper
149
+ </h3>
150
+ <p className={S.caption}>
151
+ Rows use raw forecast anchors; the chart applies{' '}
152
+ <code>ensureChartForecastBridge</code> internally (same defaults
153
+ as <code>ChartAreaInteractive</code>).
154
+ </p>
155
+ <div className={S.demo}>
156
+ <LightweightForecastChart
157
+ chartData={DEMO_BASE}
158
+ forecastData={DEMO_FORECAST_ITEMS}
159
+ isDarkTheme={isDarkMode}
160
+ height={320}
161
+ forecastLineStyle="dashed"
162
+ formatNumber={v =>
163
+ Number.isFinite(v) ? v.toFixed(2) : String(v)
164
+ }
165
+ />
166
+ </div>
167
+ </div>
168
+
169
+ <div>
170
+ <h3 className={S.sectionTitle}>
171
+ Forecast lines + stacked quantile bands
172
+ </h3>
173
+ <p className={S.caption}>
174
+ Each band key maps to tuples on forecast months; overlays from{' '}
175
+ <code>ChartAreaInteractive</code> are intentionally omitted here.
176
+ </p>
177
+ <div className={S.demo}>
178
+ <LightweightForecastChart
179
+ chartData={bandsData}
180
+ forecastData={DEMO_FORECAST_ITEMS}
181
+ quantileBands={DEMO_QUANTILE_BANDS}
182
+ isDarkTheme={isDarkMode}
183
+ height={360}
184
+ forecastLineStyle="dashed"
185
+ formatNumber={v =>
186
+ Number.isFinite(v) ? v.toFixed(2) : String(v)
187
+ }
188
+ />
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </PageContentSection>
193
+ </>
194
+ );
195
+ }
@@ -91,6 +91,12 @@ export const DOC_REGISTRY: DocEntry[] = [
91
91
  section: 'Charts',
92
92
  load: () => import('./pages/ChartAreaInteractivePage'),
93
93
  },
94
+ {
95
+ slug: 'lightweight-forecast-chart',
96
+ title: 'LightweightForecastChart',
97
+ section: 'Charts',
98
+ load: () => import('./pages/LightweightChartPage'),
99
+ },
94
100
  {
95
101
  slug: 'chat',
96
102
  title: 'Chat',