@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.
- package/dist/esm/components/ui/Chart/Chart.js +4 -0
- package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.js +460 -0
- package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.styl.js +7 -0
- package/dist/esm/components/ui/Chart/lightweight/chartTime.js +16 -0
- package/dist/esm/components/ui/Chart/lightweight/lightweightForecastChart.helpers.js +114 -0
- package/dist/esm/components/ui/Chart/lightweight/quantileBandCustomSeries.js +147 -0
- package/dist/esm/components/ui/Chart/quantileBandConeChartData.js +131 -0
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/useQuantileBands.js +4 -102
- package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.js +4 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/types/src/components/ui/Chart/Chart.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Chart/lightweight/LightweightForecastChart.d.ts +26 -0
- package/dist/esm/types/src/components/ui/Chart/lightweight/chartTime.d.ts +5 -0
- package/dist/esm/types/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.d.ts +13 -0
- package/dist/esm/types/src/components/ui/Chart/lightweight/quantileBandCustomSeries.d.ts +24 -0
- package/dist/esm/types/src/components/ui/Chart/quantileBandConeChartData.d.ts +7 -0
- package/dist/esm/types/src/docs/pages/LightweightChartPage.d.ts +1 -0
- package/package.json +3 -2
- package/src/components/ui/Chart/Chart.tsx +4 -0
- package/src/components/ui/Chart/lightweight/LightweightForecastChart.styl +25 -0
- package/src/components/ui/Chart/lightweight/LightweightForecastChart.styl.d.ts +11 -0
- package/src/components/ui/Chart/lightweight/LightweightForecastChart.tsx +721 -0
- package/src/components/ui/Chart/lightweight/chartTime.ts +18 -0
- package/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.ts +141 -0
- package/src/components/ui/Chart/lightweight/quantileBandCustomSeries.ts +215 -0
- package/src/components/ui/Chart/quantileBandConeChartData.ts +171 -0
- package/src/components/ui/ChartAreaInteractive/overlays/useQuantileBands.ts +5 -131
- package/src/declarations.d.ts +2 -0
- package/src/docs/config/webpack.config.js +25 -2
- package/src/docs/index.tsx +1 -1
- package/src/docs/pages/LightweightChartPage.styl +18 -0
- package/src/docs/pages/LightweightChartPage.styl.d.ts +10 -0
- package/src/docs/pages/LightweightChartPage.tsx +195 -0
- package/src/docs/registry.ts +6 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { customSeriesDefaultOptions } from 'lightweight-charts';
|
|
2
|
+
|
|
3
|
+
/** Catmull–Rom segment chain → cubic Béziers (matches curved line feel). */
|
|
4
|
+
function appendCatmullRomBezierChain(ctx, pts, startWithMoveTo) {
|
|
5
|
+
if (pts.length < 2)
|
|
6
|
+
return;
|
|
7
|
+
if (pts.length === 2) {
|
|
8
|
+
if (startWithMoveTo)
|
|
9
|
+
ctx.moveTo(pts[0].x, pts[0].y);
|
|
10
|
+
ctx.lineTo(pts[1].x, pts[1].y);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (startWithMoveTo) {
|
|
14
|
+
ctx.moveTo(pts[0].x, pts[0].y);
|
|
15
|
+
}
|
|
16
|
+
for (let i = 0; i < pts.length - 1; i += 1) {
|
|
17
|
+
const p0 = pts[Math.max(0, i - 1)];
|
|
18
|
+
const p1 = pts[i];
|
|
19
|
+
const p2 = pts[i + 1];
|
|
20
|
+
const p3 = pts[Math.min(pts.length - 1, i + 2)];
|
|
21
|
+
const cp1x = p1.x + (p2.x - p0.x) / 6;
|
|
22
|
+
const cp1y = p1.y + (p2.y - p0.y) / 6;
|
|
23
|
+
const cp2x = p2.x - (p3.x - p1.x) / 6;
|
|
24
|
+
const cp2y = p2.y - (p3.y - p1.y) / 6;
|
|
25
|
+
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
class QuantileBandPaneRenderer {
|
|
29
|
+
getData;
|
|
30
|
+
getStyle;
|
|
31
|
+
isWhitespaceFn;
|
|
32
|
+
constructor(getData, getStyle, isWhitespaceFn) {
|
|
33
|
+
this.getData = getData;
|
|
34
|
+
this.getStyle = getStyle;
|
|
35
|
+
this.isWhitespaceFn = isWhitespaceFn;
|
|
36
|
+
}
|
|
37
|
+
draw(target, priceConverter) {
|
|
38
|
+
const pane = this.getData();
|
|
39
|
+
if (!pane?.bars?.length) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const style = this.getStyle();
|
|
43
|
+
const segments = [];
|
|
44
|
+
for (const bar of pane.bars) {
|
|
45
|
+
const d = bar.originalData;
|
|
46
|
+
if (this.isWhitespaceFn(d)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const p = d;
|
|
50
|
+
const yU = priceConverter(p.upper);
|
|
51
|
+
const yL = priceConverter(p.lower);
|
|
52
|
+
if (yU === null || yL === null) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
segments.push({ x: bar.x, yU, yL });
|
|
56
|
+
}
|
|
57
|
+
if (segments.length < 2) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
target.useMediaCoordinateSpace(({ context }) => {
|
|
61
|
+
const opacity = typeof style.strokeOpacity === 'number'
|
|
62
|
+
? style.strokeOpacity
|
|
63
|
+
: undefined;
|
|
64
|
+
const prevAlpha = context.globalAlpha;
|
|
65
|
+
const upper = segments.map(s => ({ x: s.x, y: s.yU }));
|
|
66
|
+
const lowerRev = segments
|
|
67
|
+
.map(s => ({ x: s.x, y: s.yL }))
|
|
68
|
+
.reverse();
|
|
69
|
+
context.beginPath();
|
|
70
|
+
appendCatmullRomBezierChain(context, upper, true);
|
|
71
|
+
const last = segments[segments.length - 1];
|
|
72
|
+
context.lineTo(last.x, last.yL);
|
|
73
|
+
appendCatmullRomBezierChain(context, lowerRev, false);
|
|
74
|
+
context.closePath();
|
|
75
|
+
context.fillStyle = style.fill;
|
|
76
|
+
context.fill();
|
|
77
|
+
const sw = style.strokeWidth;
|
|
78
|
+
if (sw > 0) {
|
|
79
|
+
if (opacity !== undefined)
|
|
80
|
+
context.globalAlpha = opacity;
|
|
81
|
+
context.lineWidth = sw;
|
|
82
|
+
context.strokeStyle = style.stroke ?? style.fill;
|
|
83
|
+
if (style.strokeDasharray) {
|
|
84
|
+
const parts = style.strokeDasharray
|
|
85
|
+
.split(/[\s,]+/)
|
|
86
|
+
.map(Number)
|
|
87
|
+
.filter(n => Number.isFinite(n));
|
|
88
|
+
if (parts.length)
|
|
89
|
+
context.setLineDash(parts);
|
|
90
|
+
else
|
|
91
|
+
context.setLineDash([]);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
context.setLineDash([]);
|
|
95
|
+
}
|
|
96
|
+
context.stroke();
|
|
97
|
+
context.setLineDash([]);
|
|
98
|
+
}
|
|
99
|
+
context.globalAlpha = prevAlpha;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
class QuantileBandPaneView {
|
|
104
|
+
_paneData;
|
|
105
|
+
_style;
|
|
106
|
+
constructor(initialStyle) {
|
|
107
|
+
this._style = initialStyle;
|
|
108
|
+
}
|
|
109
|
+
updateStyle(style) {
|
|
110
|
+
this._style = style;
|
|
111
|
+
}
|
|
112
|
+
renderer() {
|
|
113
|
+
return new QuantileBandPaneRenderer(() => this._paneData, () => this._style, (d) => this.isWhitespace(d));
|
|
114
|
+
}
|
|
115
|
+
update(paneData, seriesOptions) {
|
|
116
|
+
this._paneData = paneData;
|
|
117
|
+
const c = seriesOptions.color;
|
|
118
|
+
if (typeof c === 'string') {
|
|
119
|
+
this._style = { ...this._style, fill: c };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
priceValueBuilder(plotRow) {
|
|
123
|
+
if (this.isWhitespace(plotRow)) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const p = plotRow;
|
|
127
|
+
return [p.lower, p.upper, (p.lower + p.upper) / 2];
|
|
128
|
+
}
|
|
129
|
+
isWhitespace(data) {
|
|
130
|
+
const row = data;
|
|
131
|
+
if (typeof row.lower !== 'number' ||
|
|
132
|
+
typeof row.upper !== 'number' ||
|
|
133
|
+
!Number.isFinite(row.lower) ||
|
|
134
|
+
!Number.isFinite(row.upper)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
defaultOptions() {
|
|
140
|
+
return customSeriesDefaultOptions;
|
|
141
|
+
}
|
|
142
|
+
destroy() {
|
|
143
|
+
this._paneData = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { QuantileBandPaneView };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
function isFiniteBandTuple(v) {
|
|
2
|
+
return (Array.isArray(v) &&
|
|
3
|
+
v.length === 2 &&
|
|
4
|
+
typeof v[0] === 'number' &&
|
|
5
|
+
typeof v[1] === 'number' &&
|
|
6
|
+
Number.isFinite(v[0]) &&
|
|
7
|
+
Number.isFinite(v[1]));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Matches ChartAreaInteractive `mode="intervals"`: cone / fan chart hand-off — collapse the
|
|
11
|
+
* interval to a point at the historical→forecast boundary, then expand along forecast anchors.
|
|
12
|
+
* (Same rules as `useQuantileBands`, without requiring `ForecastData` — reads tuples from each row.)
|
|
13
|
+
*/
|
|
14
|
+
function applyQuantileBandConeToChartData(chartData, bandKey) {
|
|
15
|
+
if (!chartData.length)
|
|
16
|
+
return chartData;
|
|
17
|
+
const originalBandByDate = new Map();
|
|
18
|
+
for (const p of chartData) {
|
|
19
|
+
const t = p[bandKey];
|
|
20
|
+
if (isFiniteBandTuple(t))
|
|
21
|
+
originalBandByDate.set(p.date, [t[0], t[1]]);
|
|
22
|
+
}
|
|
23
|
+
const forecastDateList = [...originalBandByDate.keys()].sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
|
|
24
|
+
if (forecastDateList.length === 0)
|
|
25
|
+
return chartData;
|
|
26
|
+
const forecastDatesSet = new Set(forecastDateList);
|
|
27
|
+
const dateToQuantileIndex = new Map();
|
|
28
|
+
forecastDateList.forEach((d, i) => dateToQuantileIndex.set(d, i));
|
|
29
|
+
const clonedData = [...chartData];
|
|
30
|
+
const historicalPoints = clonedData.filter(p => p.historical !== undefined &&
|
|
31
|
+
p.historical !== null &&
|
|
32
|
+
typeof p.historical === 'number' &&
|
|
33
|
+
Number.isFinite(p.historical));
|
|
34
|
+
const lastHistoricalPoint = historicalPoints.length > 0
|
|
35
|
+
? historicalPoints[historicalPoints.length - 1]
|
|
36
|
+
: null;
|
|
37
|
+
const lastHistoricalDate = lastHistoricalPoint?.date;
|
|
38
|
+
const lastHistoricalValue = lastHistoricalPoint?.historical;
|
|
39
|
+
const firstForecastDate = forecastDateList[0];
|
|
40
|
+
const firstForecastDateObj = firstForecastDate
|
|
41
|
+
? new Date(firstForecastDate)
|
|
42
|
+
: null;
|
|
43
|
+
const lastHistoricalDateObj = lastHistoricalDate
|
|
44
|
+
? new Date(lastHistoricalDate)
|
|
45
|
+
: null;
|
|
46
|
+
const hasGap = !!lastHistoricalDate &&
|
|
47
|
+
!!firstForecastDate &&
|
|
48
|
+
lastHistoricalValue !== undefined &&
|
|
49
|
+
firstForecastDateObj &&
|
|
50
|
+
lastHistoricalDateObj &&
|
|
51
|
+
firstForecastDateObj.getTime() > lastHistoricalDateObj.getTime();
|
|
52
|
+
const needsBridgePoint = !!lastHistoricalDate &&
|
|
53
|
+
!!firstForecastDate &&
|
|
54
|
+
lastHistoricalValue !== undefined &&
|
|
55
|
+
firstForecastDateObj &&
|
|
56
|
+
lastHistoricalDateObj &&
|
|
57
|
+
firstForecastDateObj.getTime() <= lastHistoricalDateObj.getTime();
|
|
58
|
+
let bridgePoint = null;
|
|
59
|
+
let pointBeforeForecast = null;
|
|
60
|
+
if (needsBridgePoint && historicalPoints.length > 0) {
|
|
61
|
+
bridgePoint =
|
|
62
|
+
firstForecastDateObj &&
|
|
63
|
+
lastHistoricalDateObj &&
|
|
64
|
+
firstForecastDateObj.getTime() === lastHistoricalDateObj.getTime()
|
|
65
|
+
? lastHistoricalPoint
|
|
66
|
+
: [...historicalPoints]
|
|
67
|
+
.reverse()
|
|
68
|
+
.find(p => firstForecastDateObj &&
|
|
69
|
+
new Date(p.date).getTime() < firstForecastDateObj.getTime()) || lastHistoricalPoint;
|
|
70
|
+
if (firstForecastDateObj) {
|
|
71
|
+
pointBeforeForecast =
|
|
72
|
+
historicalPoints.findLast(p => new Date(p.date).getTime() < firstForecastDateObj.getTime()) ?? null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return clonedData.map(point => {
|
|
76
|
+
const newPoint = { ...point };
|
|
77
|
+
if (hasGap &&
|
|
78
|
+
point.date === lastHistoricalDate &&
|
|
79
|
+
lastHistoricalValue !== undefined) {
|
|
80
|
+
newPoint[bandKey] = [lastHistoricalValue, lastHistoricalValue];
|
|
81
|
+
}
|
|
82
|
+
const isBridgePointDate = needsBridgePoint &&
|
|
83
|
+
bridgePoint &&
|
|
84
|
+
bridgePoint.historical !== undefined &&
|
|
85
|
+
point.date === bridgePoint.date;
|
|
86
|
+
const isPointBeforeForecast = needsBridgePoint &&
|
|
87
|
+
pointBeforeForecast &&
|
|
88
|
+
pointBeforeForecast.historical !== undefined &&
|
|
89
|
+
point.date === pointBeforeForecast.date;
|
|
90
|
+
const isAlsoForecastDate = forecastDatesSet.has(point.date);
|
|
91
|
+
if (isPointBeforeForecast && !isAlsoForecastDate) {
|
|
92
|
+
newPoint[bandKey] = [
|
|
93
|
+
pointBeforeForecast.historical,
|
|
94
|
+
pointBeforeForecast.historical,
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
else if (isBridgePointDate && !isAlsoForecastDate) {
|
|
98
|
+
newPoint[bandKey] = [
|
|
99
|
+
bridgePoint.historical,
|
|
100
|
+
bridgePoint.historical,
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
if (forecastDatesSet.has(point.date)) {
|
|
104
|
+
const quantileIndex = dateToQuantileIndex.get(point.date);
|
|
105
|
+
if (quantileIndex !== undefined) {
|
|
106
|
+
const bandValues = originalBandByDate.get(point.date);
|
|
107
|
+
if (bandValues) {
|
|
108
|
+
const isBridgePointDateForecast = needsBridgePoint &&
|
|
109
|
+
bridgePoint &&
|
|
110
|
+
point.date === bridgePoint.date &&
|
|
111
|
+
bridgePoint.historical !== undefined;
|
|
112
|
+
if (isBridgePointDateForecast && quantileIndex === 0) {
|
|
113
|
+
newPoint[bandKey] = [
|
|
114
|
+
bridgePoint.historical,
|
|
115
|
+
bandValues[1],
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
newPoint[bandKey] = bandValues;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
delete newPoint[bandKey];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return newPoint;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { applyQuantileBandConeToChartData };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
import { applyQuantileBandConeToChartData } from '../../Chart/quantileBandConeChartData.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Hook to transform chart data and create quantile band configuration
|
|
@@ -12,128 +13,29 @@ function useQuantileBands({ color, chartData, selectedForecastId, forecastData,
|
|
|
12
13
|
const forecastDataForSelected = forecastData[selectedForecastId];
|
|
13
14
|
const allQuantilesData = forecastDataForSelected.allQuantiles;
|
|
14
15
|
const clonedData = [...chartData];
|
|
15
|
-
// Get forecast dates to map quantile array indices correctly
|
|
16
16
|
const forecastDates = forecastDataForSelected.dates || [];
|
|
17
17
|
const forecastDatesSet = new Set(forecastDates);
|
|
18
|
-
// Find the last historical point for connection
|
|
19
|
-
const historicalPoints = clonedData.filter(point => point.historical !== undefined);
|
|
20
|
-
const lastHistoricalPoint = historicalPoints.length > 0
|
|
21
|
-
? historicalPoints[historicalPoints.length - 1]
|
|
22
|
-
: null;
|
|
23
|
-
const lastHistoricalDate = lastHistoricalPoint?.date;
|
|
24
|
-
const lastHistoricalValue = lastHistoricalPoint?.historical;
|
|
25
|
-
// Get first forecast date
|
|
26
|
-
const firstForecastDate = forecastDates[0];
|
|
27
|
-
const firstForecastDateObj = firstForecastDate
|
|
28
|
-
? new Date(firstForecastDate)
|
|
29
|
-
: null;
|
|
30
|
-
const lastHistoricalDateObj = lastHistoricalDate
|
|
31
|
-
? new Date(lastHistoricalDate)
|
|
32
|
-
: null;
|
|
33
|
-
// Check if there's a gap between historical and forecast data (forecast starts after historical)
|
|
34
|
-
const hasGap = lastHistoricalDate &&
|
|
35
|
-
firstForecastDate &&
|
|
36
|
-
lastHistoricalValue !== undefined &&
|
|
37
|
-
firstForecastDateObj &&
|
|
38
|
-
lastHistoricalDateObj &&
|
|
39
|
-
firstForecastDateObj.getTime() > lastHistoricalDateObj.getTime();
|
|
40
|
-
// Check if forecast starts before or at last historical point (need bridge point)
|
|
41
|
-
const needsBridgePoint = lastHistoricalDate &&
|
|
42
|
-
firstForecastDate &&
|
|
43
|
-
lastHistoricalValue !== undefined &&
|
|
44
|
-
firstForecastDateObj &&
|
|
45
|
-
lastHistoricalDateObj &&
|
|
46
|
-
firstForecastDateObj.getTime() <= lastHistoricalDateObj.getTime();
|
|
47
|
-
// Find bridge point when forecast starts before or at last historical point
|
|
48
|
-
let bridgePoint = null;
|
|
49
|
-
let pointBeforeForecast = null;
|
|
50
|
-
if (needsBridgePoint && historicalPoints.length > 0) {
|
|
51
|
-
// Find the last historical point before or at the first forecast date
|
|
52
|
-
// If dates are equal, use lastHistoricalPoint; otherwise find the point before
|
|
53
|
-
bridgePoint =
|
|
54
|
-
firstForecastDateObj &&
|
|
55
|
-
lastHistoricalDateObj &&
|
|
56
|
-
firstForecastDateObj.getTime() === lastHistoricalDateObj.getTime()
|
|
57
|
-
? lastHistoricalPoint
|
|
58
|
-
: [...historicalPoints]
|
|
59
|
-
.reverse()
|
|
60
|
-
.find(p => firstForecastDateObj &&
|
|
61
|
-
new Date(p.date).getTime() < firstForecastDateObj.getTime()) || lastHistoricalPoint;
|
|
62
|
-
// Find the actual point BEFORE the first forecast date for connection
|
|
63
|
-
if (firstForecastDateObj) {
|
|
64
|
-
pointBeforeForecast = [...historicalPoints].findLast(p => new Date(p.date).getTime() < firstForecastDateObj.getTime());
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// Create a map from date to quantile array index
|
|
68
18
|
const dateToQuantileIndex = new Map();
|
|
69
19
|
forecastDates.forEach((date, index) => {
|
|
70
20
|
dateToQuantileIndex.set(date, index);
|
|
71
21
|
});
|
|
72
|
-
const
|
|
22
|
+
const withRawBands = clonedData.map(point => {
|
|
73
23
|
const newPoint = { ...point };
|
|
74
|
-
// If there's a gap and this is the last historical point, add band data for connection
|
|
75
|
-
if (hasGap &&
|
|
76
|
-
point.date === lastHistoricalDate &&
|
|
77
|
-
lastHistoricalValue !== undefined) {
|
|
78
|
-
// Set zero-width band at the last historical value for visual connection
|
|
79
|
-
newPoint[bandKey] = [lastHistoricalValue, lastHistoricalValue];
|
|
80
|
-
}
|
|
81
|
-
// If forecast starts before or at last historical point, add bridge point connection
|
|
82
|
-
// Set connection band at the point BEFORE the first forecast date (if exists)
|
|
83
|
-
const isBridgePointDate = needsBridgePoint &&
|
|
84
|
-
bridgePoint &&
|
|
85
|
-
bridgePoint.historical !== undefined &&
|
|
86
|
-
point.date === bridgePoint.date;
|
|
87
|
-
const isPointBeforeForecast = needsBridgePoint &&
|
|
88
|
-
pointBeforeForecast &&
|
|
89
|
-
pointBeforeForecast.historical !== undefined &&
|
|
90
|
-
point.date === pointBeforeForecast.date;
|
|
91
|
-
const isAlsoForecastDate = forecastDatesSet.has(point.date);
|
|
92
|
-
// Set zero-width connection band at the point BEFORE forecast starts
|
|
93
|
-
if (isPointBeforeForecast && !isAlsoForecastDate) {
|
|
94
|
-
newPoint[bandKey] = [
|
|
95
|
-
pointBeforeForecast.historical,
|
|
96
|
-
pointBeforeForecast.historical,
|
|
97
|
-
];
|
|
98
|
-
}
|
|
99
|
-
else if (isBridgePointDate && !isAlsoForecastDate) {
|
|
100
|
-
// Fallback: if no point before forecast, use bridge point
|
|
101
|
-
newPoint[bandKey] = [
|
|
102
|
-
bridgePoint.historical,
|
|
103
|
-
bridgePoint.historical,
|
|
104
|
-
];
|
|
105
|
-
}
|
|
106
|
-
// Only update band data for forecast dates
|
|
107
24
|
if (forecastDatesSet.has(point.date)) {
|
|
108
25
|
const quantileIndex = dateToQuantileIndex.get(point.date);
|
|
109
26
|
if (quantileIndex !== undefined) {
|
|
110
27
|
const bandValues = getBandValues(point.date, quantileIndex, allQuantilesData);
|
|
111
28
|
if (bandValues) {
|
|
112
|
-
|
|
113
|
-
// start the band from the historical value for smooth connection
|
|
114
|
-
const isBridgePointDate = needsBridgePoint &&
|
|
115
|
-
bridgePoint &&
|
|
116
|
-
point.date === bridgePoint.date &&
|
|
117
|
-
bridgePoint.historical !== undefined;
|
|
118
|
-
if (isBridgePointDate && quantileIndex === 0) {
|
|
119
|
-
// Start from historical value, expand to forecast upper bound
|
|
120
|
-
newPoint[bandKey] = [bridgePoint.historical, bandValues[1]];
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
newPoint[bandKey] = bandValues;
|
|
124
|
-
}
|
|
29
|
+
newPoint[bandKey] = bandValues;
|
|
125
30
|
}
|
|
126
31
|
else {
|
|
127
|
-
// Remove band data if values don't exist
|
|
128
32
|
delete newPoint[bandKey];
|
|
129
33
|
}
|
|
130
34
|
}
|
|
131
35
|
}
|
|
132
|
-
// For non-forecast dates, preserve existing band data if it exists
|
|
133
|
-
// This ensures continuity of the band visualization
|
|
134
36
|
return newPoint;
|
|
135
37
|
});
|
|
136
|
-
return
|
|
38
|
+
return applyQuantileBandConeToChartData(withRawBands, bandKey);
|
|
137
39
|
}, [chartData, selectedForecastId, forecastData, bandKey, getBandValues]);
|
|
138
40
|
const quantileBands = useMemo(() => {
|
|
139
41
|
if (!selectedForecastId || !forecastData[selectedForecastId]) {
|
|
@@ -17,6 +17,10 @@ import '../../ui/TextShimmer/TextShimmer.js';
|
|
|
17
17
|
import '@phosphor-icons/react';
|
|
18
18
|
import '../../ui/AnalysesSelector/AnalysesSelector.styl.js';
|
|
19
19
|
import '../../ui/Chart/components/CustomChartLegend/CustomChartLegend.styl.js';
|
|
20
|
+
import '../../ui/ChartAreaInteractive/ChartLines.js';
|
|
21
|
+
import '../../ui/Skeleton/Skeleton.styl.js';
|
|
22
|
+
import 'lightweight-charts';
|
|
23
|
+
import '../../ui/Chart/lightweight/LightweightForecastChart.styl.js';
|
|
20
24
|
import { ChartEmptyState } from '../../ui/Chart/components/ChartEmptyState/ChartEmptyState.js';
|
|
21
25
|
import S from './DriverPerformanceChart.styl.js';
|
|
22
26
|
import { generateDriverChartData } from './driverPerformanceChartData.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -103,6 +103,7 @@ export { SignInPage } from './components/widgets/SignInPage/SignInPage.js';
|
|
|
103
103
|
export { ChartTooltipItem } from './components/ui/Chart/components/ChartTooltipItem.js';
|
|
104
104
|
export { ChartLegendItem } from './components/ui/Chart/components/ChartLegendItem.js';
|
|
105
105
|
export { CustomChartLegend } from './components/ui/Chart/components/CustomChartLegend/CustomChartLegend.js';
|
|
106
|
+
export { LightweightForecastChart } from './components/ui/Chart/lightweight/LightweightForecastChart.js';
|
|
106
107
|
export { ChartEmptyState } from './components/ui/Chart/components/ChartEmptyState/ChartEmptyState.js';
|
|
107
108
|
export { ChartContainer, ChartStyle } from './components/ui/Chart/components/ChartContainer.js';
|
|
108
109
|
export { ChartTooltipContent } from './components/ui/Chart/components/ChartTooltipContent.js';
|
|
@@ -10,4 +10,5 @@ export { ChartTooltipItem } from './components/ChartTooltipItem';
|
|
|
10
10
|
export { ChartLegendItem } from './components/ChartLegendItem';
|
|
11
11
|
export { CustomChartLegend } from './components/CustomChartLegend/CustomChartLegend';
|
|
12
12
|
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, BaseChartWrapper, };
|
|
13
|
+
export { LightweightForecastChart, type LightweightForecastChartProps, } from './lightweight/LightweightForecastChart';
|
|
13
14
|
export { ChartEmptyState, type ChartEmptyStateProps, type ChartEmptyStatusTone, } from './components/ChartEmptyState/ChartEmptyState';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ChartConfig } from '#uilib/components/ui/Chart/Chart.types';
|
|
2
|
+
import type { QuantileBandConfig } from '#uilib/components/ui/Chart/chartForecastVisualization.types';
|
|
3
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
4
|
+
import { ForecastItemData } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
|
|
5
|
+
export interface LightweightForecastChartProps {
|
|
6
|
+
chartData: ChartDataPoint[];
|
|
7
|
+
forecastData?: ForecastItemData[];
|
|
8
|
+
quantileBands?: QuantileBandConfig[];
|
|
9
|
+
chartConfig?: ChartConfig;
|
|
10
|
+
historicalLineColor?: string;
|
|
11
|
+
isDarkTheme: boolean;
|
|
12
|
+
height?: number;
|
|
13
|
+
className?: string;
|
|
14
|
+
hiddenSeries?: Set<string>;
|
|
15
|
+
onLegendClick?: (data: unknown, index: number, event: unknown) => void;
|
|
16
|
+
disableForecastHistoricalBridge?: boolean;
|
|
17
|
+
forecastLineStyle?: 'dashed' | 'solid';
|
|
18
|
+
formatDate?: (value: string, detailed?: boolean) => string;
|
|
19
|
+
formatNumber?: (value: number) => string;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
error?: string | null;
|
|
22
|
+
noDataMessage?: string;
|
|
23
|
+
showLegend?: boolean;
|
|
24
|
+
showTooltip?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function LightweightForecastChart(props: LightweightForecastChartProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/esm/types/src/components/ui/Chart/lightweight/lightweightForecastChart.helpers.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
2
|
+
import type { ChartOptions, DeepPartial, LineData, UTCTimestamp } from 'lightweight-charts';
|
|
3
|
+
import type { QuantileBandCustomData } from './quantileBandCustomSeries';
|
|
4
|
+
export declare function buildLightweightChartOptions(args: {
|
|
5
|
+
isDarkTheme: boolean;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
autoSize?: boolean;
|
|
9
|
+
}): DeepPartial<ChartOptions>;
|
|
10
|
+
export declare function buildHistoricalLineData(rows: ChartDataPoint[]): LineData<UTCTimestamp>[];
|
|
11
|
+
export declare function buildForecastLineData(rows: ChartDataPoint[], forecastKey: string): LineData<UTCTimestamp>[];
|
|
12
|
+
export declare function buildQuantileBandCustomData(rows: ChartDataPoint[], bandKey: string): QuantileBandCustomData[];
|
|
13
|
+
export declare function findNearestChartRow(rows: ChartDataPoint[], time: UTCTimestamp): ChartDataPoint | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CustomData, CustomSeriesOptions, CustomSeriesWhitespaceData, ICustomSeriesPaneRenderer, ICustomSeriesPaneView, PaneRendererCustomData, UTCTimestamp } from 'lightweight-charts';
|
|
2
|
+
export interface QuantileBandCustomData extends CustomData<UTCTimestamp> {
|
|
3
|
+
lower: number;
|
|
4
|
+
upper: number;
|
|
5
|
+
}
|
|
6
|
+
export type QuantileBandStyle = {
|
|
7
|
+
fill: string;
|
|
8
|
+
stroke?: string;
|
|
9
|
+
strokeWidth: number;
|
|
10
|
+
strokeDasharray?: string;
|
|
11
|
+
strokeOpacity?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare class QuantileBandPaneView implements ICustomSeriesPaneView<UTCTimestamp, QuantileBandCustomData, CustomSeriesOptions> {
|
|
14
|
+
private _paneData;
|
|
15
|
+
private _style;
|
|
16
|
+
constructor(initialStyle: QuantileBandStyle);
|
|
17
|
+
updateStyle(style: QuantileBandStyle): void;
|
|
18
|
+
renderer(): ICustomSeriesPaneRenderer;
|
|
19
|
+
update(paneData: PaneRendererCustomData<UTCTimestamp, QuantileBandCustomData>, seriesOptions: CustomSeriesOptions): void;
|
|
20
|
+
priceValueBuilder(plotRow: QuantileBandCustomData): number[];
|
|
21
|
+
isWhitespace(data: QuantileBandCustomData | CustomSeriesWhitespaceData<UTCTimestamp>): data is CustomSeriesWhitespaceData<UTCTimestamp>;
|
|
22
|
+
defaultOptions(): CustomSeriesOptions;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
2
|
+
/**
|
|
3
|
+
* Matches ChartAreaInteractive `mode="intervals"`: cone / fan chart hand-off — collapse the
|
|
4
|
+
* interval to a point at the historical→forecast boundary, then expand along forecast anchors.
|
|
5
|
+
* (Same rules as `useQuantileBands`, without requiring `ForecastData` — reads tuples from each row.)
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyQuantileBandConeToChartData(chartData: ChartDataPoint[], bandKey: string): ChartDataPoint[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function LightweightChartPage(): 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.1",
|
|
4
4
|
"description": "Sybilion Design System — React UI components (Webpack + Stylus)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
101
101
|
"@vimeo/player": "^2.29.3",
|
|
102
102
|
"classnames": "^2.3.2",
|
|
103
|
+
"lightweight-charts": "^5.0.9",
|
|
103
104
|
"lucide-react": "^0.546.0",
|
|
104
105
|
"motion": "^12.23.12",
|
|
105
106
|
"recharts": "^3.2.1",
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
},
|
|
131
132
|
"devDependencies": {
|
|
132
133
|
"@auth0/auth0-react": "^2.3.1",
|
|
133
|
-
"@sybilion/platform-sdk": "file:../sdk",
|
|
134
|
+
"@sybilion/platform-sdk": "file:../platform-sdk",
|
|
134
135
|
"@babel/core": "^7.20.12",
|
|
135
136
|
"@babel/preset-typescript": "^7.21.0",
|
|
136
137
|
"@homecode/ui": "^4.30.6",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.root
|
|
2
|
+
display flex
|
|
3
|
+
flex-direction column
|
|
4
|
+
gap 12px
|
|
5
|
+
width 100%
|
|
6
|
+
|
|
7
|
+
.shell
|
|
8
|
+
position relative
|
|
9
|
+
width 100%
|
|
10
|
+
|
|
11
|
+
.host
|
|
12
|
+
display block
|
|
13
|
+
width 100%
|
|
14
|
+
box-sizing border-box
|
|
15
|
+
|
|
16
|
+
:global(#tv-attr-logo)
|
|
17
|
+
display none
|
|
18
|
+
|
|
19
|
+
.tooltipMove
|
|
20
|
+
position absolute
|
|
21
|
+
z-index 5
|
|
22
|
+
pointer-events none
|
|
23
|
+
|
|
24
|
+
.footer
|
|
25
|
+
width 100%
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is automatically generated.
|
|
2
|
+
// Please do not change this file!
|
|
3
|
+
interface CssExports {
|
|
4
|
+
'footer': string;
|
|
5
|
+
'host': string;
|
|
6
|
+
'root': string;
|
|
7
|
+
'shell': string;
|
|
8
|
+
'tooltipMove': string;
|
|
9
|
+
}
|
|
10
|
+
export const cssExports: CssExports;
|
|
11
|
+
export default cssExports;
|