@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,18 @@
|
|
|
1
|
+
import type { UTCTimestamp } from 'lightweight-charts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse `YYYY-MM-DD` dates as UTC midnight → Lightweight Charts unix seconds.
|
|
5
|
+
*/
|
|
6
|
+
export function chartDateToUtcTimestamp(dateStr: string): UTCTimestamp {
|
|
7
|
+
const trimmed = dateStr.slice(0, 10);
|
|
8
|
+
const [y, m, d] = trimmed.split('-').map(Number);
|
|
9
|
+
if (
|
|
10
|
+
!Number.isFinite(y) ||
|
|
11
|
+
!Number.isFinite(m) ||
|
|
12
|
+
!Number.isFinite(d) ||
|
|
13
|
+
trimmed.length !== 10
|
|
14
|
+
) {
|
|
15
|
+
return Math.floor(new Date(dateStr).getTime() / 1000) as UTCTimestamp;
|
|
16
|
+
}
|
|
17
|
+
return Math.floor(Date.UTC(y, m - 1, d, 0, 0, 0, 0) / 1000) as UTCTimestamp;
|
|
18
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
2
|
+
import type {
|
|
3
|
+
ChartOptions,
|
|
4
|
+
DeepPartial,
|
|
5
|
+
LineData,
|
|
6
|
+
UTCTimestamp,
|
|
7
|
+
} from 'lightweight-charts';
|
|
8
|
+
import { ColorType } from 'lightweight-charts';
|
|
9
|
+
|
|
10
|
+
import { applyQuantileBandConeToChartData } from '../quantileBandConeChartData';
|
|
11
|
+
import { chartDateToUtcTimestamp } from './chartTime';
|
|
12
|
+
import type { QuantileBandCustomData } from './quantileBandCustomSeries';
|
|
13
|
+
|
|
14
|
+
export function buildLightweightChartOptions(args: {
|
|
15
|
+
isDarkTheme: boolean;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
autoSize?: boolean;
|
|
19
|
+
}): DeepPartial<ChartOptions> {
|
|
20
|
+
const { isDarkTheme, width, height, autoSize = false } = args;
|
|
21
|
+
return {
|
|
22
|
+
autoSize,
|
|
23
|
+
width: Math.max(1, Math.floor(width)),
|
|
24
|
+
height: Math.max(1, Math.floor(height)),
|
|
25
|
+
layout: {
|
|
26
|
+
background: {
|
|
27
|
+
type: ColorType.Solid,
|
|
28
|
+
color: 'transparent',
|
|
29
|
+
},
|
|
30
|
+
textColor: isDarkTheme ? '#d8dee9' : '#191919',
|
|
31
|
+
},
|
|
32
|
+
grid: {
|
|
33
|
+
vertLines: {
|
|
34
|
+
color: isDarkTheme ? '#2f3440' : '#e6edf3',
|
|
35
|
+
},
|
|
36
|
+
horzLines: {
|
|
37
|
+
color: isDarkTheme ? '#2f3440' : '#e6edf3',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
rightPriceScale: {
|
|
41
|
+
borderVisible: false,
|
|
42
|
+
scaleMargins: {
|
|
43
|
+
top: 0.08,
|
|
44
|
+
bottom: 0.08,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
timeScale: {
|
|
48
|
+
borderVisible: false,
|
|
49
|
+
timeVisible: true,
|
|
50
|
+
secondsVisible: false,
|
|
51
|
+
fixLeftEdge: true,
|
|
52
|
+
fixRightEdge: true,
|
|
53
|
+
},
|
|
54
|
+
localization: {
|
|
55
|
+
locale:
|
|
56
|
+
typeof navigator !== 'undefined' && navigator.language
|
|
57
|
+
? navigator.language
|
|
58
|
+
: 'en-US',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildHistoricalLineData(
|
|
64
|
+
rows: ChartDataPoint[],
|
|
65
|
+
): LineData<UTCTimestamp>[] {
|
|
66
|
+
const out: LineData<UTCTimestamp>[] = [];
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
const v = row.historical;
|
|
69
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) continue;
|
|
70
|
+
out.push({
|
|
71
|
+
time: chartDateToUtcTimestamp(row.date),
|
|
72
|
+
value: v,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
out.sort((a, b) => Number(a.time) - Number(b.time));
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildForecastLineData(
|
|
80
|
+
rows: ChartDataPoint[],
|
|
81
|
+
forecastKey: string,
|
|
82
|
+
): LineData<UTCTimestamp>[] {
|
|
83
|
+
const out: LineData<UTCTimestamp>[] = [];
|
|
84
|
+
for (const row of rows) {
|
|
85
|
+
const v = row[forecastKey];
|
|
86
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) continue;
|
|
87
|
+
out.push({
|
|
88
|
+
time: chartDateToUtcTimestamp(row.date),
|
|
89
|
+
value: v,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
out.sort((a, b) => Number(a.time) - Number(b.time));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildQuantileBandCustomData(
|
|
97
|
+
rows: ChartDataPoint[],
|
|
98
|
+
bandKey: string,
|
|
99
|
+
): QuantileBandCustomData[] {
|
|
100
|
+
const coned = applyQuantileBandConeToChartData(rows, bandKey);
|
|
101
|
+
const out: QuantileBandCustomData[] = [];
|
|
102
|
+
for (const row of coned) {
|
|
103
|
+
const v = row[bandKey];
|
|
104
|
+
if (
|
|
105
|
+
!Array.isArray(v) ||
|
|
106
|
+
v.length !== 2 ||
|
|
107
|
+
typeof v[0] !== 'number' ||
|
|
108
|
+
typeof v[1] !== 'number' ||
|
|
109
|
+
!Number.isFinite(v[0]) ||
|
|
110
|
+
!Number.isFinite(v[1])
|
|
111
|
+
) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
out.push({
|
|
115
|
+
time: chartDateToUtcTimestamp(row.date),
|
|
116
|
+
lower: v[0],
|
|
117
|
+
upper: v[1],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
out.sort((a, b) => Number(a.time) - Number(b.time));
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function findNearestChartRow(
|
|
125
|
+
rows: ChartDataPoint[],
|
|
126
|
+
time: UTCTimestamp,
|
|
127
|
+
): ChartDataPoint | null {
|
|
128
|
+
if (!rows.length) return null;
|
|
129
|
+
const target = Number(time) * 1000;
|
|
130
|
+
let best: ChartDataPoint | null = null;
|
|
131
|
+
let bestDist = Infinity;
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
const t = Number(chartDateToUtcTimestamp(row.date)) * 1000;
|
|
134
|
+
const dist = Math.abs(t - target);
|
|
135
|
+
if (dist < bestDist) {
|
|
136
|
+
bestDist = dist;
|
|
137
|
+
best = row;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return best;
|
|
141
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { CanvasRenderingTarget2D } from 'fancy-canvas';
|
|
2
|
+
import type {
|
|
3
|
+
CustomData,
|
|
4
|
+
CustomSeriesOptions,
|
|
5
|
+
CustomSeriesWhitespaceData,
|
|
6
|
+
ICustomSeriesPaneRenderer,
|
|
7
|
+
ICustomSeriesPaneView,
|
|
8
|
+
PaneRendererCustomData,
|
|
9
|
+
PriceToCoordinateConverter,
|
|
10
|
+
UTCTimestamp,
|
|
11
|
+
} from 'lightweight-charts';
|
|
12
|
+
import { customSeriesDefaultOptions } from 'lightweight-charts';
|
|
13
|
+
|
|
14
|
+
export interface QuantileBandCustomData extends CustomData<UTCTimestamp> {
|
|
15
|
+
lower: number;
|
|
16
|
+
upper: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type QuantileBandStyle = {
|
|
20
|
+
fill: string;
|
|
21
|
+
stroke?: string;
|
|
22
|
+
strokeWidth: number;
|
|
23
|
+
strokeDasharray?: string;
|
|
24
|
+
strokeOpacity?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type MediaXY = { x: number; y: number };
|
|
28
|
+
|
|
29
|
+
/** Catmull–Rom segment chain → cubic Béziers (matches curved line feel). */
|
|
30
|
+
function appendCatmullRomBezierChain(
|
|
31
|
+
ctx: CanvasRenderingContext2D,
|
|
32
|
+
pts: MediaXY[],
|
|
33
|
+
startWithMoveTo: boolean,
|
|
34
|
+
): void {
|
|
35
|
+
if (pts.length < 2) return;
|
|
36
|
+
|
|
37
|
+
if (pts.length === 2) {
|
|
38
|
+
if (startWithMoveTo) ctx.moveTo(pts[0].x, pts[0].y);
|
|
39
|
+
ctx.lineTo(pts[1].x, pts[1].y);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (startWithMoveTo) {
|
|
44
|
+
ctx.moveTo(pts[0].x, pts[0].y);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < pts.length - 1; i += 1) {
|
|
48
|
+
const p0 = pts[Math.max(0, i - 1)];
|
|
49
|
+
const p1 = pts[i];
|
|
50
|
+
const p2 = pts[i + 1];
|
|
51
|
+
const p3 = pts[Math.min(pts.length - 1, i + 2)];
|
|
52
|
+
const cp1x = p1.x + (p2.x - p0.x) / 6;
|
|
53
|
+
const cp1y = p1.y + (p2.y - p0.y) / 6;
|
|
54
|
+
const cp2x = p2.x - (p3.x - p1.x) / 6;
|
|
55
|
+
const cp2y = p2.y - (p3.y - p1.y) / 6;
|
|
56
|
+
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class QuantileBandPaneRenderer implements ICustomSeriesPaneRenderer {
|
|
61
|
+
public constructor(
|
|
62
|
+
private readonly getData: () =>
|
|
63
|
+
| PaneRendererCustomData<UTCTimestamp, QuantileBandCustomData>
|
|
64
|
+
| undefined,
|
|
65
|
+
private readonly getStyle: () => QuantileBandStyle,
|
|
66
|
+
private readonly isWhitespaceFn: (
|
|
67
|
+
data: QuantileBandCustomData | CustomSeriesWhitespaceData<UTCTimestamp>,
|
|
68
|
+
) => data is CustomSeriesWhitespaceData<UTCTimestamp>,
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
public draw(
|
|
72
|
+
target: CanvasRenderingTarget2D,
|
|
73
|
+
priceConverter: PriceToCoordinateConverter,
|
|
74
|
+
): void {
|
|
75
|
+
const pane = this.getData() as
|
|
76
|
+
| PaneRendererCustomData<UTCTimestamp, QuantileBandCustomData>
|
|
77
|
+
| undefined;
|
|
78
|
+
if (!pane?.bars?.length) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const style = this.getStyle();
|
|
83
|
+
const segments: Array<{ x: number; yU: number; yL: number }> = [];
|
|
84
|
+
|
|
85
|
+
for (const bar of pane.bars) {
|
|
86
|
+
const d = bar.originalData;
|
|
87
|
+
if (this.isWhitespaceFn(d)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const p = d as QuantileBandCustomData;
|
|
91
|
+
const yU = priceConverter(p.upper);
|
|
92
|
+
const yL = priceConverter(p.lower);
|
|
93
|
+
if (yU === null || yL === null) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
segments.push({ x: bar.x, yU, yL });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (segments.length < 2) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
target.useMediaCoordinateSpace(({ context }) => {
|
|
104
|
+
const opacity =
|
|
105
|
+
typeof style.strokeOpacity === 'number'
|
|
106
|
+
? style.strokeOpacity
|
|
107
|
+
: undefined;
|
|
108
|
+
const prevAlpha = context.globalAlpha;
|
|
109
|
+
const upper: MediaXY[] = segments.map(s => ({ x: s.x, y: s.yU }));
|
|
110
|
+
const lowerRev: MediaXY[] = segments
|
|
111
|
+
.map(s => ({ x: s.x, y: s.yL }))
|
|
112
|
+
.reverse();
|
|
113
|
+
|
|
114
|
+
context.beginPath();
|
|
115
|
+
appendCatmullRomBezierChain(context, upper, true);
|
|
116
|
+
const last = segments[segments.length - 1]!;
|
|
117
|
+
context.lineTo(last.x, last.yL);
|
|
118
|
+
appendCatmullRomBezierChain(context, lowerRev, false);
|
|
119
|
+
context.closePath();
|
|
120
|
+
context.fillStyle = style.fill;
|
|
121
|
+
context.fill();
|
|
122
|
+
|
|
123
|
+
const sw = style.strokeWidth;
|
|
124
|
+
if (sw > 0) {
|
|
125
|
+
if (opacity !== undefined) context.globalAlpha = opacity;
|
|
126
|
+
context.lineWidth = sw;
|
|
127
|
+
context.strokeStyle = style.stroke ?? style.fill;
|
|
128
|
+
if (style.strokeDasharray) {
|
|
129
|
+
const parts = style.strokeDasharray
|
|
130
|
+
.split(/[\s,]+/)
|
|
131
|
+
.map(Number)
|
|
132
|
+
.filter(n => Number.isFinite(n));
|
|
133
|
+
if (parts.length) context.setLineDash(parts);
|
|
134
|
+
else context.setLineDash([]);
|
|
135
|
+
} else {
|
|
136
|
+
context.setLineDash([]);
|
|
137
|
+
}
|
|
138
|
+
context.stroke();
|
|
139
|
+
context.setLineDash([]);
|
|
140
|
+
}
|
|
141
|
+
context.globalAlpha = prevAlpha;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class QuantileBandPaneView implements ICustomSeriesPaneView<
|
|
147
|
+
UTCTimestamp,
|
|
148
|
+
QuantileBandCustomData,
|
|
149
|
+
CustomSeriesOptions
|
|
150
|
+
> {
|
|
151
|
+
private _paneData:
|
|
152
|
+
| PaneRendererCustomData<UTCTimestamp, QuantileBandCustomData>
|
|
153
|
+
| undefined;
|
|
154
|
+
|
|
155
|
+
private _style!: QuantileBandStyle;
|
|
156
|
+
|
|
157
|
+
public constructor(initialStyle: QuantileBandStyle) {
|
|
158
|
+
this._style = initialStyle;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public updateStyle(style: QuantileBandStyle): void {
|
|
162
|
+
this._style = style;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public renderer(): ICustomSeriesPaneRenderer {
|
|
166
|
+
return new QuantileBandPaneRenderer(
|
|
167
|
+
() => this._paneData,
|
|
168
|
+
() => this._style,
|
|
169
|
+
(d): d is CustomSeriesWhitespaceData<UTCTimestamp> =>
|
|
170
|
+
this.isWhitespace(d),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public update(
|
|
175
|
+
paneData: PaneRendererCustomData<UTCTimestamp, QuantileBandCustomData>,
|
|
176
|
+
seriesOptions: CustomSeriesOptions,
|
|
177
|
+
): void {
|
|
178
|
+
this._paneData = paneData;
|
|
179
|
+
const c = seriesOptions.color;
|
|
180
|
+
if (typeof c === 'string') {
|
|
181
|
+
this._style = { ...this._style, fill: c };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public priceValueBuilder(plotRow: QuantileBandCustomData): number[] {
|
|
186
|
+
if (this.isWhitespace(plotRow)) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const p = plotRow as QuantileBandCustomData;
|
|
190
|
+
return [p.lower, p.upper, (p.lower + p.upper) / 2];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public isWhitespace(
|
|
194
|
+
data: QuantileBandCustomData | CustomSeriesWhitespaceData<UTCTimestamp>,
|
|
195
|
+
): data is CustomSeriesWhitespaceData<UTCTimestamp> {
|
|
196
|
+
const row = data as QuantileBandCustomData;
|
|
197
|
+
if (
|
|
198
|
+
typeof row.lower !== 'number' ||
|
|
199
|
+
typeof row.upper !== 'number' ||
|
|
200
|
+
!Number.isFinite(row.lower) ||
|
|
201
|
+
!Number.isFinite(row.upper)
|
|
202
|
+
) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public defaultOptions(): CustomSeriesOptions {
|
|
209
|
+
return customSeriesDefaultOptions;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public destroy(): void {
|
|
213
|
+
this._paneData = undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
2
|
+
|
|
3
|
+
function isFiniteBandTuple(v: unknown): v is [number, number] {
|
|
4
|
+
return (
|
|
5
|
+
Array.isArray(v) &&
|
|
6
|
+
v.length === 2 &&
|
|
7
|
+
typeof v[0] === 'number' &&
|
|
8
|
+
typeof v[1] === 'number' &&
|
|
9
|
+
Number.isFinite(v[0]) &&
|
|
10
|
+
Number.isFinite(v[1])
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Matches ChartAreaInteractive `mode="intervals"`: cone / fan chart hand-off — collapse the
|
|
16
|
+
* interval to a point at the historical→forecast boundary, then expand along forecast anchors.
|
|
17
|
+
* (Same rules as `useQuantileBands`, without requiring `ForecastData` — reads tuples from each row.)
|
|
18
|
+
*/
|
|
19
|
+
export function applyQuantileBandConeToChartData(
|
|
20
|
+
chartData: ChartDataPoint[],
|
|
21
|
+
bandKey: string,
|
|
22
|
+
): ChartDataPoint[] {
|
|
23
|
+
if (!chartData.length) return chartData;
|
|
24
|
+
|
|
25
|
+
const originalBandByDate = new Map<string, [number, number]>();
|
|
26
|
+
for (const p of chartData) {
|
|
27
|
+
const t = p[bandKey];
|
|
28
|
+
if (isFiniteBandTuple(t)) originalBandByDate.set(p.date, [t[0], t[1]]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const forecastDateList = [...originalBandByDate.keys()].sort(
|
|
32
|
+
(a, b) => new Date(a).getTime() - new Date(b).getTime(),
|
|
33
|
+
);
|
|
34
|
+
if (forecastDateList.length === 0) return chartData;
|
|
35
|
+
|
|
36
|
+
const forecastDatesSet = new Set(forecastDateList);
|
|
37
|
+
const dateToQuantileIndex = new Map<string, number>();
|
|
38
|
+
forecastDateList.forEach((d, i) => dateToQuantileIndex.set(d, i));
|
|
39
|
+
|
|
40
|
+
const clonedData = [...chartData];
|
|
41
|
+
|
|
42
|
+
const historicalPoints = clonedData.filter(
|
|
43
|
+
p =>
|
|
44
|
+
p.historical !== undefined &&
|
|
45
|
+
p.historical !== null &&
|
|
46
|
+
typeof p.historical === 'number' &&
|
|
47
|
+
Number.isFinite(p.historical),
|
|
48
|
+
);
|
|
49
|
+
const lastHistoricalPoint =
|
|
50
|
+
historicalPoints.length > 0
|
|
51
|
+
? historicalPoints[historicalPoints.length - 1]
|
|
52
|
+
: null;
|
|
53
|
+
const lastHistoricalDate = lastHistoricalPoint?.date;
|
|
54
|
+
const lastHistoricalValue = lastHistoricalPoint?.historical as
|
|
55
|
+
| number
|
|
56
|
+
| undefined;
|
|
57
|
+
|
|
58
|
+
const firstForecastDate = forecastDateList[0];
|
|
59
|
+
const firstForecastDateObj = firstForecastDate
|
|
60
|
+
? new Date(firstForecastDate)
|
|
61
|
+
: null;
|
|
62
|
+
const lastHistoricalDateObj = lastHistoricalDate
|
|
63
|
+
? new Date(lastHistoricalDate)
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
const hasGap =
|
|
67
|
+
!!lastHistoricalDate &&
|
|
68
|
+
!!firstForecastDate &&
|
|
69
|
+
lastHistoricalValue !== undefined &&
|
|
70
|
+
firstForecastDateObj &&
|
|
71
|
+
lastHistoricalDateObj &&
|
|
72
|
+
firstForecastDateObj.getTime() > lastHistoricalDateObj.getTime();
|
|
73
|
+
|
|
74
|
+
const needsBridgePoint =
|
|
75
|
+
!!lastHistoricalDate &&
|
|
76
|
+
!!firstForecastDate &&
|
|
77
|
+
lastHistoricalValue !== undefined &&
|
|
78
|
+
firstForecastDateObj &&
|
|
79
|
+
lastHistoricalDateObj &&
|
|
80
|
+
firstForecastDateObj.getTime() <= lastHistoricalDateObj.getTime();
|
|
81
|
+
|
|
82
|
+
let bridgePoint: (typeof historicalPoints)[number] | null = null;
|
|
83
|
+
let pointBeforeForecast: (typeof historicalPoints)[number] | null = null;
|
|
84
|
+
if (needsBridgePoint && historicalPoints.length > 0) {
|
|
85
|
+
bridgePoint =
|
|
86
|
+
firstForecastDateObj &&
|
|
87
|
+
lastHistoricalDateObj &&
|
|
88
|
+
firstForecastDateObj.getTime() === lastHistoricalDateObj.getTime()
|
|
89
|
+
? lastHistoricalPoint
|
|
90
|
+
: [...historicalPoints]
|
|
91
|
+
.reverse()
|
|
92
|
+
.find(
|
|
93
|
+
p =>
|
|
94
|
+
firstForecastDateObj &&
|
|
95
|
+
new Date(p.date).getTime() < firstForecastDateObj.getTime(),
|
|
96
|
+
) || lastHistoricalPoint;
|
|
97
|
+
|
|
98
|
+
if (firstForecastDateObj) {
|
|
99
|
+
pointBeforeForecast =
|
|
100
|
+
historicalPoints.findLast(
|
|
101
|
+
p => new Date(p.date).getTime() < firstForecastDateObj.getTime(),
|
|
102
|
+
) ?? null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return clonedData.map(point => {
|
|
107
|
+
const newPoint: ChartDataPoint = { ...point };
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
hasGap &&
|
|
111
|
+
point.date === lastHistoricalDate &&
|
|
112
|
+
lastHistoricalValue !== undefined
|
|
113
|
+
) {
|
|
114
|
+
newPoint[bandKey] = [lastHistoricalValue, lastHistoricalValue] as [
|
|
115
|
+
number,
|
|
116
|
+
number,
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isBridgePointDate =
|
|
121
|
+
needsBridgePoint &&
|
|
122
|
+
bridgePoint &&
|
|
123
|
+
bridgePoint.historical !== undefined &&
|
|
124
|
+
point.date === bridgePoint.date;
|
|
125
|
+
const isPointBeforeForecast =
|
|
126
|
+
needsBridgePoint &&
|
|
127
|
+
pointBeforeForecast &&
|
|
128
|
+
pointBeforeForecast.historical !== undefined &&
|
|
129
|
+
point.date === pointBeforeForecast.date;
|
|
130
|
+
const isAlsoForecastDate = forecastDatesSet.has(point.date);
|
|
131
|
+
|
|
132
|
+
if (isPointBeforeForecast && !isAlsoForecastDate) {
|
|
133
|
+
newPoint[bandKey] = [
|
|
134
|
+
pointBeforeForecast.historical as number,
|
|
135
|
+
pointBeforeForecast.historical as number,
|
|
136
|
+
];
|
|
137
|
+
} else if (isBridgePointDate && !isAlsoForecastDate) {
|
|
138
|
+
newPoint[bandKey] = [
|
|
139
|
+
bridgePoint.historical as number,
|
|
140
|
+
bridgePoint.historical as number,
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (forecastDatesSet.has(point.date)) {
|
|
145
|
+
const quantileIndex = dateToQuantileIndex.get(point.date);
|
|
146
|
+
if (quantileIndex !== undefined) {
|
|
147
|
+
const bandValues = originalBandByDate.get(point.date);
|
|
148
|
+
if (bandValues) {
|
|
149
|
+
const isBridgePointDateForecast =
|
|
150
|
+
needsBridgePoint &&
|
|
151
|
+
bridgePoint &&
|
|
152
|
+
point.date === bridgePoint.date &&
|
|
153
|
+
bridgePoint.historical !== undefined;
|
|
154
|
+
|
|
155
|
+
if (isBridgePointDateForecast && quantileIndex === 0) {
|
|
156
|
+
newPoint[bandKey] = [
|
|
157
|
+
bridgePoint.historical as number,
|
|
158
|
+
bandValues[1],
|
|
159
|
+
];
|
|
160
|
+
} else {
|
|
161
|
+
newPoint[bandKey] = bandValues;
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
delete newPoint[bandKey];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return newPoint;
|
|
170
|
+
});
|
|
171
|
+
}
|