@sybilion/uilib 1.3.0 → 1.3.1

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
@@ -16,6 +16,10 @@ import '../TextShimmer/TextShimmer.js';
16
16
  import '@phosphor-icons/react';
17
17
  import '../AnalysesSelector/AnalysesSelector.styl.js';
18
18
  import './components/CustomChartLegend/CustomChartLegend.styl.js';
19
+ import '../ChartAreaInteractive/ChartLines.js';
20
+ import '../Skeleton/Skeleton.styl.js';
21
+ import 'lightweight-charts';
22
+ import './lightweight/LightweightForecastChart.styl.js';
19
23
  import './components/ChartEmptyState/ChartEmptyState.styl.js';
20
24
 
21
25
  const ChartTooltip = RechartsPrimitive.Tooltip;
@@ -0,0 +1,460 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import cn from 'classnames';
3
+ import { useId, useRef, useState, useMemo, useEffect, useCallback } from 'react';
4
+ import { ChartContext } from '../Chart.context.js';
5
+ import { ChartStyle } from '../components/ChartContainer.js';
6
+ import { ChartTooltipContent } from '../components/ChartTooltipContent.js';
7
+ import { CustomChartLegend } from '../components/CustomChartLegend/CustomChartLegend.js';
8
+ import { formatDate } from '../tools/formatters.js';
9
+ import { getForecastColor, getForecastQuantileBandColor } from '../../ChartAreaInteractive/ChartLines.js';
10
+ import { Skeleton } from '../../Skeleton/Skeleton.js';
11
+ import { ensureChartForecastBridge } from '../../../../utils/chartConnectionPoint.js';
12
+ import { createChart, LineSeries, LineType, LineStyle } from 'lightweight-charts';
13
+ import S from './LightweightForecastChart.styl.js';
14
+ import { buildLightweightChartOptions, buildHistoricalLineData, buildForecastLineData, buildQuantileBandCustomData, findNearestChartRow } from './lightweightForecastChart.helpers.js';
15
+ import { QuantileBandPaneView } from './quantileBandCustomSeries.js';
16
+
17
+ function clampTooltipTranslate(args) {
18
+ const { coordinate, viewW, viewH, tooltipWidth: tw, tooltipHeight: th, offset, edgeMargin, } = args;
19
+ const minX = edgeMargin;
20
+ const maxX = Math.max(edgeMargin, viewW - tw - edgeMargin);
21
+ const minY = edgeMargin;
22
+ const maxY = Math.max(edgeMargin, viewH - th - edgeMargin);
23
+ const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
24
+ let tx = coordinate.x + offset;
25
+ if (tx + tw > viewW - edgeMargin) {
26
+ tx = coordinate.x - tw - offset;
27
+ }
28
+ tx = clamp(tx, minX, maxX);
29
+ let ty = coordinate.y + offset;
30
+ if (ty + th > viewH - edgeMargin) {
31
+ ty = coordinate.y - th - offset;
32
+ }
33
+ ty = clamp(ty, minY, maxY);
34
+ return { x: tx, y: ty };
35
+ }
36
+ function scheduleFitTimeScale(chart) {
37
+ requestAnimationFrame(() => {
38
+ chart.timeScale().fitContent();
39
+ });
40
+ }
41
+ function LightweightForecastChart(props) {
42
+ const { chartData, forecastData = [], quantileBands, chartConfig: chartConfigProp = {}, historicalLineColor: historicalLineColorProp, isDarkTheme, height, className, hiddenSeries: hiddenSeriesProp, onLegendClick, disableForecastHistoricalBridge = false, forecastLineStyle = 'dashed', formatDate: formatDateFn = formatDate, formatNumber, loading = false, error = null, noDataMessage = 'No data available', showLegend = true, showTooltip = true, } = props;
43
+ const chartId = useId().replace(/:/g, '');
44
+ const shellRef = useRef(null);
45
+ const hostRef = useRef(null);
46
+ const modelRef = useRef(null);
47
+ const bridgedRef = useRef([]);
48
+ const tooltipRef = useRef(null);
49
+ const tooltipSizeRef = useRef({ width: 0, height: 0 });
50
+ const [localHidden, setLocalHidden] = useState(() => new Set());
51
+ const hiddenSeries = hiddenSeriesProp ?? localHidden;
52
+ const hiddenControlled = hiddenSeriesProp !== undefined;
53
+ const pixelHeight = height ?? 280;
54
+ const historicalLineColor = historicalLineColorProp ?? (isDarkTheme ? '#ffffff' : '#000000');
55
+ const bridgedChartData = useMemo(() => {
56
+ if (disableForecastHistoricalBridge) {
57
+ return chartData;
58
+ }
59
+ return ensureChartForecastBridge(chartData, {
60
+ forecastSeriesIds: forecastData?.map(f => f.id),
61
+ });
62
+ }, [chartData, disableForecastHistoricalBridge, forecastData]);
63
+ bridgedRef.current = bridgedChartData;
64
+ const hiddenRef = useRef(hiddenSeries);
65
+ const forecastRef = useRef(forecastData);
66
+ const quantileBandsRef = useRef(quantileBands);
67
+ const formatDateRef = useRef(formatDateFn);
68
+ const histColorRef = useRef(historicalLineColor);
69
+ useEffect(() => {
70
+ hiddenRef.current = hiddenSeries;
71
+ }, [hiddenSeries]);
72
+ useEffect(() => {
73
+ forecastRef.current = forecastData;
74
+ }, [forecastData]);
75
+ useEffect(() => {
76
+ quantileBandsRef.current = quantileBands;
77
+ }, [quantileBands]);
78
+ useEffect(() => {
79
+ formatDateRef.current = formatDateFn;
80
+ }, [formatDateFn]);
81
+ useEffect(() => {
82
+ histColorRef.current = historicalLineColor;
83
+ }, [historicalLineColor]);
84
+ const mergedChartConfig = useMemo(() => {
85
+ const base = {
86
+ historical: { label: 'Historical Data', color: historicalLineColor },
87
+ ...chartConfigProp,
88
+ };
89
+ forecastData.forEach((f, index) => {
90
+ const key = `forecast_${f.id}`;
91
+ base[key] = {
92
+ label: f.name?.toString() ?? key,
93
+ color: f.color?.toString() ?? getForecastColor(index),
94
+ };
95
+ });
96
+ quantileBands?.forEach((band, index) => {
97
+ base[band.key] = {
98
+ label: band.name,
99
+ color: band.color ?? getForecastQuantileBandColor(index),
100
+ };
101
+ });
102
+ return base;
103
+ }, [chartConfigProp, forecastData, historicalLineColor, quantileBands]);
104
+ const legendPayload = useMemo(() => {
105
+ if (!showLegend)
106
+ return [];
107
+ return forecastData.map((item, index) => ({
108
+ value: item.name?.toString() || item.id?.toString() || '',
109
+ color: item.color?.toString() || getForecastColor(index),
110
+ dataKey: `forecast_${item.id}`,
111
+ icon: item.icon,
112
+ status: item.status,
113
+ dimmed: item.dimmed,
114
+ updated_at: item.updated_at,
115
+ }));
116
+ }, [forecastData, showLegend]);
117
+ const structureKey = useMemo(() => [
118
+ quantileBands?.map(b => b.key).join(',') ?? '',
119
+ forecastData.map(f => f.id).join(','),
120
+ ].join('|'), [forecastData, quantileBands]);
121
+ const [tooltipState, setTooltipState] = useState({
122
+ active: false,
123
+ x: 0,
124
+ y: 0,
125
+ label: '',
126
+ payload: [],
127
+ });
128
+ const applyTooltipPosition = useCallback(() => {
129
+ const shell = shellRef.current;
130
+ const wrapper = tooltipRef.current;
131
+ if (!shell || !wrapper)
132
+ return;
133
+ const tw = tooltipSizeRef.current.width || wrapper.offsetWidth;
134
+ const th = tooltipSizeRef.current.height || wrapper.offsetHeight;
135
+ if (tw <= 0 || th <= 0)
136
+ return;
137
+ const { width: viewW, height: viewH } = shell.getBoundingClientRect();
138
+ const next = clampTooltipTranslate({
139
+ coordinate: { x: tooltipState.x, y: tooltipState.y },
140
+ viewW,
141
+ viewH,
142
+ tooltipWidth: tw,
143
+ tooltipHeight: th,
144
+ offset: 10,
145
+ edgeMargin: 8,
146
+ });
147
+ wrapper.style.transform = `translate(${next.x}px, ${next.y}px)`;
148
+ }, [tooltipState.x, tooltipState.y]);
149
+ useEffect(() => {
150
+ if (!tooltipState.active)
151
+ return;
152
+ applyTooltipPosition();
153
+ }, [
154
+ applyTooltipPosition,
155
+ tooltipState.active,
156
+ tooltipState.payload,
157
+ tooltipState.label,
158
+ tooltipState.x,
159
+ tooltipState.y,
160
+ ]);
161
+ useEffect(() => {
162
+ const el = tooltipRef.current;
163
+ if (!el || typeof ResizeObserver === 'undefined')
164
+ return;
165
+ const ro = new ResizeObserver(entries => {
166
+ const entry = entries[0];
167
+ if (!entry)
168
+ return;
169
+ const { width, height: h } = entry.contentRect;
170
+ tooltipSizeRef.current = { width, height: h };
171
+ requestAnimationFrame(() => applyTooltipPosition());
172
+ });
173
+ ro.observe(el);
174
+ return () => ro.disconnect();
175
+ }, [applyTooltipPosition]);
176
+ useEffect(() => {
177
+ const onWin = () => requestAnimationFrame(() => {
178
+ applyTooltipPosition();
179
+ });
180
+ window.addEventListener('resize', onWin);
181
+ return () => window.removeEventListener('resize', onWin);
182
+ }, [applyTooltipPosition]);
183
+ // Structural chart lifecycle
184
+ useEffect(() => {
185
+ const host = hostRef.current;
186
+ if (!host)
187
+ return;
188
+ if (!bridgedChartData.length) {
189
+ modelRef.current?.chart.remove();
190
+ modelRef.current = null;
191
+ return;
192
+ }
193
+ const iw = Math.max(1, Math.floor(host.clientWidth || host.offsetWidth || 640));
194
+ const ih = Math.max(1, Math.floor(pixelHeight));
195
+ const chart = createChart(host, {
196
+ ...buildLightweightChartOptions({
197
+ isDarkTheme,
198
+ width: iw,
199
+ height: ih,
200
+ }),
201
+ });
202
+ const bands = new Map();
203
+ quantileBands?.forEach((band, index) => {
204
+ const fill = band.color ?? getForecastQuantileBandColor(index);
205
+ const view = new QuantileBandPaneView({
206
+ fill,
207
+ stroke: band.strokeWidth ? fill : undefined,
208
+ strokeWidth: band.strokeWidth ?? 0,
209
+ strokeDasharray: band.strokeDasharray,
210
+ strokeOpacity: band.strokeOpacity,
211
+ });
212
+ const api = chart.addCustomSeries(view, {
213
+ color: fill,
214
+ lastValueVisible: false,
215
+ priceLineVisible: false,
216
+ visible: !hiddenRef.current.has(band.key),
217
+ });
218
+ bands.set(band.key, { api, view });
219
+ });
220
+ const forecasts = new Map();
221
+ forecastData.forEach((f, index) => {
222
+ const key = `forecast_${f.id}`;
223
+ const color = f.color?.toString() ?? getForecastColor(index);
224
+ const api = chart.addSeries(LineSeries, {
225
+ color,
226
+ lineWidth: 1,
227
+ lineType: LineType.Curved,
228
+ lineStyle: forecastLineStyle === 'dashed' ? LineStyle.Dashed : LineStyle.Solid,
229
+ lastValueVisible: false,
230
+ priceLineVisible: false,
231
+ visible: !hiddenRef.current.has(key),
232
+ });
233
+ forecasts.set(key, api);
234
+ });
235
+ const historical = chart.addSeries(LineSeries, {
236
+ color: historicalLineColor,
237
+ lineWidth: 1,
238
+ lineType: LineType.Curved,
239
+ lineStyle: LineStyle.Solid,
240
+ lastValueVisible: false,
241
+ priceLineVisible: false,
242
+ visible: !hiddenRef.current.has('historical'),
243
+ });
244
+ modelRef.current = {
245
+ chart,
246
+ historical,
247
+ forecasts,
248
+ bands,
249
+ };
250
+ const onMove = (param) => {
251
+ if (!showTooltip) {
252
+ setTooltipState(s => ({ ...s, active: false, payload: [] }));
253
+ return;
254
+ }
255
+ const rows = bridgedRef.current;
256
+ if (!param.point || param.time === undefined || rows.length === 0) {
257
+ setTooltipState(s => ({ ...s, active: false, payload: [] }));
258
+ return;
259
+ }
260
+ const time = param.time;
261
+ const row = findNearestChartRow(rows, time);
262
+ if (!row) {
263
+ setTooltipState(s => ({ ...s, active: false, payload: [] }));
264
+ return;
265
+ }
266
+ const forecastsList = forecastRef.current;
267
+ const bandsCfg = quantileBandsRef.current;
268
+ const hid = hiddenRef.current;
269
+ const fmt = formatDateRef.current;
270
+ const histColor = histColorRef.current;
271
+ const payload = [];
272
+ const hVal = row.historical;
273
+ if (typeof hVal === 'number' && Number.isFinite(hVal)) {
274
+ payload.push({
275
+ type: 'line',
276
+ name: 'Historical Data',
277
+ value: hVal,
278
+ color: histColor,
279
+ dataKey: 'historical',
280
+ payload: row,
281
+ });
282
+ }
283
+ forecastsList.forEach((f, index) => {
284
+ const forecastKey = `forecast_${f.id}`;
285
+ if (hid.has(forecastKey))
286
+ return;
287
+ const value = row[forecastKey];
288
+ if (typeof value !== 'number' || !Number.isFinite(value))
289
+ return;
290
+ const color = f.color?.toString() ?? getForecastColor(index);
291
+ payload.push({
292
+ type: 'line',
293
+ name: f.name?.toString() ?? forecastKey,
294
+ value,
295
+ color,
296
+ dataKey: forecastKey,
297
+ payload: row,
298
+ });
299
+ });
300
+ bandsCfg?.forEach((band, index) => {
301
+ if (hid.has(band.key))
302
+ return;
303
+ const tuple = row[band.key];
304
+ if (Array.isArray(tuple) &&
305
+ tuple.length === 2 &&
306
+ typeof tuple[0] === 'number' &&
307
+ typeof tuple[1] === 'number') {
308
+ const color = band.color ?? getForecastQuantileBandColor(index);
309
+ payload.push({
310
+ type: 'line',
311
+ name: band.name,
312
+ value: [tuple[0], tuple[1]],
313
+ color,
314
+ dataKey: band.key,
315
+ payload: row,
316
+ });
317
+ }
318
+ });
319
+ if (!payload.length) {
320
+ setTooltipState(s => ({ ...s, active: false, payload: [] }));
321
+ return;
322
+ }
323
+ const label = fmt(row.date, true);
324
+ setTooltipState({
325
+ active: true,
326
+ x: param.point.x,
327
+ y: param.point.y,
328
+ label,
329
+ payload,
330
+ });
331
+ };
332
+ chart.subscribeCrosshairMove(onMove);
333
+ const resizeToHost = () => {
334
+ const ww = Math.max(1, Math.floor(host.clientWidth));
335
+ const hh = Math.max(1, Math.floor(host.clientHeight));
336
+ chart.resize(ww, hh);
337
+ scheduleFitTimeScale(chart);
338
+ };
339
+ let resizeObserver;
340
+ if (typeof ResizeObserver !== 'undefined') {
341
+ resizeObserver = new ResizeObserver(() => {
342
+ requestAnimationFrame(resizeToHost);
343
+ });
344
+ resizeObserver.observe(host);
345
+ }
346
+ requestAnimationFrame(resizeToHost);
347
+ return () => {
348
+ resizeObserver?.disconnect();
349
+ chart.unsubscribeCrosshairMove(onMove);
350
+ chart.remove();
351
+ modelRef.current = null;
352
+ };
353
+ }, [
354
+ structureKey,
355
+ isDarkTheme,
356
+ pixelHeight,
357
+ showTooltip,
358
+ bridgedChartData.length,
359
+ ]);
360
+ // Push data / band styles
361
+ useEffect(() => {
362
+ const model = modelRef.current;
363
+ if (!model)
364
+ return;
365
+ model.historical?.setData(buildHistoricalLineData(bridgedChartData));
366
+ for (const [key, api] of model.forecasts.entries()) {
367
+ api.setData(buildForecastLineData(bridgedChartData, key));
368
+ }
369
+ quantileBands?.forEach((band, index) => {
370
+ const entry = model.bands.get(band.key);
371
+ if (!entry)
372
+ return;
373
+ const fill = band.color ?? getForecastQuantileBandColor(index);
374
+ entry.view.updateStyle({
375
+ fill,
376
+ stroke: band.strokeWidth ? fill : undefined,
377
+ strokeWidth: band.strokeWidth ?? 0,
378
+ strokeDasharray: band.strokeDasharray,
379
+ strokeOpacity: band.strokeOpacity,
380
+ });
381
+ entry.api.applyOptions({ color: fill });
382
+ entry.api.setData(buildQuantileBandCustomData(bridgedChartData, band.key));
383
+ });
384
+ scheduleFitTimeScale(model.chart);
385
+ }, [bridgedChartData, quantileBands]);
386
+ // Visibility toggles
387
+ useEffect(() => {
388
+ const model = modelRef.current;
389
+ if (!model)
390
+ return;
391
+ model.historical?.applyOptions({
392
+ visible: !hiddenSeries.has('historical'),
393
+ });
394
+ for (const [key, api] of model.forecasts.entries()) {
395
+ api.applyOptions({ visible: !hiddenSeries.has(key) });
396
+ }
397
+ for (const [key, { api }] of model.bands.entries()) {
398
+ api.applyOptions({ visible: !hiddenSeries.has(key) });
399
+ }
400
+ scheduleFitTimeScale(model.chart);
401
+ }, [hiddenSeries]);
402
+ // Line styling updates without structural rebuild
403
+ useEffect(() => {
404
+ const model = modelRef.current;
405
+ if (!model?.historical)
406
+ return;
407
+ model.historical.applyOptions({ color: historicalLineColor });
408
+ }, [historicalLineColor]);
409
+ useEffect(() => {
410
+ const model = modelRef.current;
411
+ if (!model)
412
+ return;
413
+ const style = forecastLineStyle === 'dashed' ? LineStyle.Dashed : LineStyle.Solid;
414
+ for (const api of model.forecasts.values()) {
415
+ api.applyOptions({ lineStyle: style });
416
+ }
417
+ }, [forecastLineStyle]);
418
+ const handleLegendClick = useCallback((data, index, event) => {
419
+ const payloadItem = data;
420
+ const key = payloadItem.dataKey;
421
+ if (key) {
422
+ if (!hiddenControlled) {
423
+ setLocalHidden(prev => {
424
+ const next = new Set(prev);
425
+ if (next.has(key))
426
+ next.delete(key);
427
+ else
428
+ next.add(key);
429
+ return next;
430
+ });
431
+ }
432
+ }
433
+ onLegendClick?.(data, index, event);
434
+ }, [hiddenControlled, onLegendClick]);
435
+ if (error) {
436
+ return (jsx("div", { className: cn(S.root, className), children: jsxs("div", { style: { color: 'var(--destructive, #f43f5e)' }, children: ["Error: ", error] }) }));
437
+ }
438
+ if (loading) {
439
+ return (jsx("div", { className: cn(S.root, className), children: jsx("div", { style: { height: pixelHeight }, children: jsx(Skeleton, { style: { width: '100%', height: '100%' } }) }) }));
440
+ }
441
+ if (!bridgedChartData.length) {
442
+ return (jsx("div", { className: cn(S.root, className), children: jsx("div", { style: { height: pixelHeight }, children: noDataMessage }) }));
443
+ }
444
+ return (jsx(ChartContext.Provider, { value: { config: mergedChartConfig }, children: jsxs("div", { className: cn(S.root, className), children: [jsxs("div", { "data-slot": "chart", "data-chart": `chart-${chartId}`, className: S.shell, ref: shellRef, style: { position: 'relative', width: '100%' }, children: [jsx(ChartStyle, { id: `chart-${chartId}`, config: mergedChartConfig }), jsx("div", { ref: hostRef, className: S.host, style: { width: '100%', height: pixelHeight } }), showTooltip ? (jsx("div", { ref: tooltipRef, className: S.tooltipMove, style: {
445
+ opacity: tooltipState.active && tooltipState.payload.length ? 1 : 0,
446
+ }, children: jsx(ChartTooltipContent, { active: tooltipState.active && tooltipState.payload.length > 0, label: tooltipState.label, payload: tooltipState.active && tooltipState.payload.length
447
+ ? tooltipState.payload
448
+ : [], labelFormatter: lbl => formatDateFn(typeof lbl === 'string' ? lbl : String(lbl), true), formatter: formatNumber
449
+ ? (value, name) => {
450
+ const v = typeof value === 'number'
451
+ ? formatNumber(value)
452
+ : Array.isArray(value)
453
+ ? `${formatNumber(value[0])} – ${formatNumber(value[1])}`
454
+ : String(value ?? '');
455
+ return (jsxs(Fragment, { children: [jsxs("span", { children: [name, ": "] }), jsx("span", { children: v })] }));
456
+ }
457
+ : undefined }) })) : null] }), showLegend ? (jsx("div", { className: S.footer, children: jsx(CustomChartLegend, { payload: legendPayload, hiddenSeries: hiddenSeries, onClick: handleLegendClick }) })) : null] }) }));
458
+ }
459
+
460
+ export { LightweightForecastChart };
@@ -0,0 +1,7 @@
1
+ import styleInject from 'style-inject';
2
+
3
+ var css_248z = ".LightweightForecastChart_root__QK05J{display:flex;flex-direction:column;gap:12px;width:100%}.LightweightForecastChart_shell__fRJV5{position:relative;width:100%}.LightweightForecastChart_host__lxD2b{box-sizing:border-box;display:block;width:100%}.LightweightForecastChart_host__lxD2b #tv-attr-logo{display:none}.LightweightForecastChart_tooltipMove__BIKnO{pointer-events:none;position:absolute;z-index:5}.LightweightForecastChart_footer__t5N7A{width:100%}";
4
+ var S = {"root":"LightweightForecastChart_root__QK05J","shell":"LightweightForecastChart_shell__fRJV5","host":"LightweightForecastChart_host__lxD2b","tooltipMove":"LightweightForecastChart_tooltipMove__BIKnO","footer":"LightweightForecastChart_footer__t5N7A"};
5
+ styleInject(css_248z);
6
+
7
+ export { S as default };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Parse `YYYY-MM-DD` dates as UTC midnight → Lightweight Charts unix seconds.
3
+ */
4
+ function chartDateToUtcTimestamp(dateStr) {
5
+ const trimmed = dateStr.slice(0, 10);
6
+ const [y, m, d] = trimmed.split('-').map(Number);
7
+ if (!Number.isFinite(y) ||
8
+ !Number.isFinite(m) ||
9
+ !Number.isFinite(d) ||
10
+ trimmed.length !== 10) {
11
+ return Math.floor(new Date(dateStr).getTime() / 1000);
12
+ }
13
+ return Math.floor(Date.UTC(y, m - 1, d, 0, 0, 0, 0) / 1000);
14
+ }
15
+
16
+ export { chartDateToUtcTimestamp };
@@ -0,0 +1,114 @@
1
+ import { ColorType } from 'lightweight-charts';
2
+ import { applyQuantileBandConeToChartData } from '../quantileBandConeChartData.js';
3
+ import { chartDateToUtcTimestamp } from './chartTime.js';
4
+
5
+ function buildLightweightChartOptions(args) {
6
+ const { isDarkTheme, width, height, autoSize = false } = args;
7
+ return {
8
+ autoSize,
9
+ width: Math.max(1, Math.floor(width)),
10
+ height: Math.max(1, Math.floor(height)),
11
+ layout: {
12
+ background: {
13
+ type: ColorType.Solid,
14
+ color: 'transparent',
15
+ },
16
+ textColor: isDarkTheme ? '#d8dee9' : '#191919',
17
+ },
18
+ grid: {
19
+ vertLines: {
20
+ color: isDarkTheme ? '#2f3440' : '#e6edf3',
21
+ },
22
+ horzLines: {
23
+ color: isDarkTheme ? '#2f3440' : '#e6edf3',
24
+ },
25
+ },
26
+ rightPriceScale: {
27
+ borderVisible: false,
28
+ scaleMargins: {
29
+ top: 0.08,
30
+ bottom: 0.08,
31
+ },
32
+ },
33
+ timeScale: {
34
+ borderVisible: false,
35
+ timeVisible: true,
36
+ secondsVisible: false,
37
+ fixLeftEdge: true,
38
+ fixRightEdge: true,
39
+ },
40
+ localization: {
41
+ locale: typeof navigator !== 'undefined' && navigator.language
42
+ ? navigator.language
43
+ : 'en-US',
44
+ },
45
+ };
46
+ }
47
+ function buildHistoricalLineData(rows) {
48
+ const out = [];
49
+ for (const row of rows) {
50
+ const v = row.historical;
51
+ if (typeof v !== 'number' || !Number.isFinite(v))
52
+ continue;
53
+ out.push({
54
+ time: chartDateToUtcTimestamp(row.date),
55
+ value: v,
56
+ });
57
+ }
58
+ out.sort((a, b) => Number(a.time) - Number(b.time));
59
+ return out;
60
+ }
61
+ function buildForecastLineData(rows, forecastKey) {
62
+ const out = [];
63
+ for (const row of rows) {
64
+ const v = row[forecastKey];
65
+ if (typeof v !== 'number' || !Number.isFinite(v))
66
+ continue;
67
+ out.push({
68
+ time: chartDateToUtcTimestamp(row.date),
69
+ value: v,
70
+ });
71
+ }
72
+ out.sort((a, b) => Number(a.time) - Number(b.time));
73
+ return out;
74
+ }
75
+ function buildQuantileBandCustomData(rows, bandKey) {
76
+ const coned = applyQuantileBandConeToChartData(rows, bandKey);
77
+ const out = [];
78
+ for (const row of coned) {
79
+ const v = row[bandKey];
80
+ if (!Array.isArray(v) ||
81
+ v.length !== 2 ||
82
+ typeof v[0] !== 'number' ||
83
+ typeof v[1] !== 'number' ||
84
+ !Number.isFinite(v[0]) ||
85
+ !Number.isFinite(v[1])) {
86
+ continue;
87
+ }
88
+ out.push({
89
+ time: chartDateToUtcTimestamp(row.date),
90
+ lower: v[0],
91
+ upper: v[1],
92
+ });
93
+ }
94
+ out.sort((a, b) => Number(a.time) - Number(b.time));
95
+ return out;
96
+ }
97
+ function findNearestChartRow(rows, time) {
98
+ if (!rows.length)
99
+ return null;
100
+ const target = Number(time) * 1000;
101
+ let best = null;
102
+ let bestDist = Infinity;
103
+ for (const row of rows) {
104
+ const t = Number(chartDateToUtcTimestamp(row.date)) * 1000;
105
+ const dist = Math.abs(t - target);
106
+ if (dist < bestDist) {
107
+ bestDist = dist;
108
+ best = row;
109
+ }
110
+ }
111
+ return best;
112
+ }
113
+
114
+ export { buildForecastLineData, buildHistoricalLineData, buildLightweightChartOptions, buildQuantileBandCustomData, findNearestChartRow };