aliencharts 0.3.0 → 0.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.
- package/README.md +67 -32
- package/dist/aliencharts.global.js +3327 -0
- package/dist/aliencharts.global.js.map +7 -0
- package/dist/aliencharts.global.min.js +159 -0
- package/dist/aliencharts.global.min.js.map +7 -0
- package/package.json +3 -1
|
@@ -0,0 +1,3327 @@
|
|
|
1
|
+
var AlienCharts = (() => {
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/vanilla/index.js
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BarSeries: () => BarSeries,
|
|
24
|
+
LineSeries: () => LineSeries,
|
|
25
|
+
createBarSeries: () => createBarSeries,
|
|
26
|
+
createChartGrid: () => createChartGrid,
|
|
27
|
+
createLineSeries: () => createLineSeries,
|
|
28
|
+
createMockCharts: () => createMockCharts,
|
|
29
|
+
createSeries: () => createSeries
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// src/icons.js
|
|
33
|
+
var icons = {
|
|
34
|
+
arrowLineRight: '<line x1="32" y1="128" x2="176" y2="128"/><polyline points="104 56 176 128 104 200"/><line x1="216" y1="40" x2="216" y2="216"/>',
|
|
35
|
+
pushPinSimple: '<path d="M224,176a8,8,0,0,1-8,8H136v56a8,8,0,0,1-16,0V184H40a8,8,0,0,1,0-16h9.29L70.46,48H64a8,8,0,0,1,0-16H192a8,8,0,0,1,0,16h-6.46l21.17,120H216A8,8,0,0,1,224,176Z"/>',
|
|
36
|
+
arrowCounterClockwise: '<polyline points="24 56 24 104 72 104"/><path d="M67.59,192A88,88,0,1,0,65.77,65.77L24,104"/>',
|
|
37
|
+
arrowsIn: '<polyline points="192 104 152 104 152 64"/><line x1="208" y1="48" x2="152" y2="104"/><polyline points="64 152 104 152 104 192"/><line x1="48" y1="208" x2="104" y2="152"/><polyline points="152 192 152 152 192 152"/><line x1="208" y1="208" x2="152" y2="152"/><polyline points="104 64 104 104 64 104"/><line x1="48" y1="48" x2="104" y2="104"/>',
|
|
38
|
+
arrowsOut: '<polyline points="160 48 208 48 208 96"/><line x1="152" y1="104" x2="208" y2="48"/><polyline points="96 208 48 208 48 160"/><line x1="104" y1="152" x2="48" y2="208"/><polyline points="208 160 208 208 160 208"/><line x1="152" y1="152" x2="208" y2="208"/><polyline points="48 96 48 48 96 48"/><line x1="104" y1="104" x2="48" y2="48"/>',
|
|
39
|
+
broom: '<path d="M112,224a95.2,95.2,0,0,1-29-48"/><path d="M192,152c0,31.67,13.31,59,40,72H61A103.65,103.65,0,0,1,32,152c0-28.21,11.23-50.89,29.47-69.64a8,8,0,0,1,8.67-1.81L95.52,90.83a16,16,0,0,0,20.82-9l21-53.11c4.15-10,15.47-15.32,25.63-11.53a20,20,0,0,1,11.51,26.4L153.13,96.69a16,16,0,0,0,8.93,20.76L187,127.29a8,8,0,0,1,5,7.43Z"/><line x1="40.54" y1="112.21" x2="194.26" y2="173.7"/>',
|
|
40
|
+
waveSine: '<path d="M24,128c104-221.7,104,221.7,208,0"/>',
|
|
41
|
+
magnifyingGlassPlus: '<line x1="80" y1="112" x2="144" y2="112"/><circle cx="112" cy="112" r="80"/><line x1="168.57" y1="168.57" x2="224" y2="224"/><line x1="112" y1="80" x2="112" y2="144"/>',
|
|
42
|
+
mapPin: '<circle cx="128" cy="104" r="32"/><path d="M208,104c0,72-80,128-80,128S48,176,48,104a80,80,0,0,1,160,0Z"/>',
|
|
43
|
+
minus: '<line x1="40" y1="128" x2="216" y2="128"/>',
|
|
44
|
+
eye: '<path d="M128,56C48,56,16,128,16,128s32,72,112,72,112-72,112-72S208,56,128,56Z"/><circle cx="128" cy="128" r="32"/>'
|
|
45
|
+
};
|
|
46
|
+
var filledIcons = /* @__PURE__ */ new Set(["pushPinSimple"]);
|
|
47
|
+
var iconSvg = (name, { size = 16, className = "" } = {}) => {
|
|
48
|
+
const body = icons[name];
|
|
49
|
+
if (!body) throw new Error(`Unknown AlienCharts icon: ${name}`);
|
|
50
|
+
const paint = filledIcons.has(name) ? 'fill="currentColor"' : 'fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"';
|
|
51
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="${size}" height="${size}" class="${className}" aria-hidden="true" ${paint}>${body}</svg>`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/core/drawingUtils.js
|
|
55
|
+
var DEFAULT_DRAWING_COLOR = "#60a5fa";
|
|
56
|
+
var DRAWING_HIT_DISTANCE = 8;
|
|
57
|
+
var DRAWING_HANDLE_HIT_DISTANCE = 10;
|
|
58
|
+
var PIN_HIT_DISTANCE = 14;
|
|
59
|
+
var DRAWING_TOOLS = /* @__PURE__ */ new Set(["trendline", "hline", "vline", "pin"]);
|
|
60
|
+
var getDrawingStyle = (drawing) => ({
|
|
61
|
+
color: drawing?.style?.color || DEFAULT_DRAWING_COLOR,
|
|
62
|
+
lineWidth: Number.isFinite(drawing?.style?.lineWidth) ? drawing.style.lineWidth : 2,
|
|
63
|
+
dashPattern: Array.isArray(drawing?.style?.dashPattern) ? drawing.style.dashPattern : [],
|
|
64
|
+
extendLeft: Boolean(drawing?.style?.extendLeft),
|
|
65
|
+
extendRight: drawing?.style?.extendRight !== false,
|
|
66
|
+
text: typeof drawing?.style?.text === "string" ? drawing.style.text : ""
|
|
67
|
+
});
|
|
68
|
+
var getDrawingsForChart = (drawings, chartId) => Array.isArray(drawings) ? drawings.filter((drawing) => drawing?.chartId === chartId) : [];
|
|
69
|
+
var getDefaultDrawingStyle = (type) => ({
|
|
70
|
+
color: DEFAULT_DRAWING_COLOR,
|
|
71
|
+
lineWidth: 2,
|
|
72
|
+
dashPattern: [],
|
|
73
|
+
extendLeft: false,
|
|
74
|
+
extendRight: type !== "vline",
|
|
75
|
+
text: ""
|
|
76
|
+
});
|
|
77
|
+
var createDrawing = ({
|
|
78
|
+
chartId,
|
|
79
|
+
type,
|
|
80
|
+
start,
|
|
81
|
+
end,
|
|
82
|
+
createDrawingId
|
|
83
|
+
}) => {
|
|
84
|
+
const id = createDrawingId?.({ chartId, type }) || `${chartId}-drawing-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
85
|
+
return {
|
|
86
|
+
id,
|
|
87
|
+
chartId,
|
|
88
|
+
type,
|
|
89
|
+
start,
|
|
90
|
+
end,
|
|
91
|
+
createdAt: Date.now(),
|
|
92
|
+
style: getDefaultDrawingStyle(type)
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
var createDraftDrawing = ({ chartId, type, start, end }) => ({
|
|
96
|
+
id: "__draft__",
|
|
97
|
+
chartId,
|
|
98
|
+
type,
|
|
99
|
+
start,
|
|
100
|
+
end,
|
|
101
|
+
createdAt: Date.now(),
|
|
102
|
+
style: getDefaultDrawingStyle(type)
|
|
103
|
+
});
|
|
104
|
+
var extendRayToPlotBoundary = (origin, through, plot) => {
|
|
105
|
+
const dx = through.x - origin.x;
|
|
106
|
+
const dy = through.y - origin.y;
|
|
107
|
+
if (Math.abs(dx) <= 1e-6 && Math.abs(dy) <= 1e-6) return through;
|
|
108
|
+
const candidates = [];
|
|
109
|
+
if (dx > 1e-6) candidates.push((plot.x + plot.width - origin.x) / dx);
|
|
110
|
+
else if (dx < -1e-6) candidates.push((plot.x - origin.x) / dx);
|
|
111
|
+
if (dy > 1e-6) candidates.push((plot.y + plot.height - origin.y) / dy);
|
|
112
|
+
else if (dy < -1e-6) candidates.push((plot.y - origin.y) / dy);
|
|
113
|
+
const boundaryScale = Math.min(
|
|
114
|
+
...candidates.filter((scale2) => Number.isFinite(scale2) && scale2 >= 0)
|
|
115
|
+
);
|
|
116
|
+
const scale = Number.isFinite(boundaryScale) ? Math.max(1, boundaryScale) : 1;
|
|
117
|
+
return {
|
|
118
|
+
x: origin.x + dx * scale,
|
|
119
|
+
y: origin.y + dy * scale
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
var getDrawingGeometry = ({ drawing, layout, projectPoint }) => {
|
|
123
|
+
if (!drawing?.start || !drawing?.end) return null;
|
|
124
|
+
const start = projectPoint(drawing.start, layout);
|
|
125
|
+
const end = projectPoint(drawing.end, layout);
|
|
126
|
+
const style = getDrawingStyle(drawing);
|
|
127
|
+
let lineStart = start;
|
|
128
|
+
let lineEnd = end;
|
|
129
|
+
if (drawing.type === "pin") {
|
|
130
|
+
return { start, end: start, lineStart: start, lineEnd: start, style };
|
|
131
|
+
}
|
|
132
|
+
if (drawing.type === "hline") {
|
|
133
|
+
const lineEndX = Math.abs(end.x - start.x) > 1e-6 ? end.x : layout.plot.x + layout.plot.width;
|
|
134
|
+
lineStart = {
|
|
135
|
+
x: style.extendRight ? layout.plot.x : start.x,
|
|
136
|
+
y: start.y
|
|
137
|
+
};
|
|
138
|
+
lineEnd = {
|
|
139
|
+
x: style.extendRight ? layout.plot.x + layout.plot.width : lineEndX,
|
|
140
|
+
y: start.y
|
|
141
|
+
};
|
|
142
|
+
} else if (drawing.type === "vline") {
|
|
143
|
+
lineStart = {
|
|
144
|
+
x: start.x,
|
|
145
|
+
y: layout.plot.y
|
|
146
|
+
};
|
|
147
|
+
lineEnd = {
|
|
148
|
+
x: start.x,
|
|
149
|
+
y: layout.plot.y + layout.plot.height
|
|
150
|
+
};
|
|
151
|
+
} else if (drawing.type === "trendline") {
|
|
152
|
+
if (style.extendLeft) lineStart = extendRayToPlotBoundary(end, start, layout.plot);
|
|
153
|
+
if (style.extendRight) lineEnd = extendRayToPlotBoundary(start, end, layout.plot);
|
|
154
|
+
}
|
|
155
|
+
return { start, end, lineStart, lineEnd, style };
|
|
156
|
+
};
|
|
157
|
+
var distanceToSegment = (point, start, end) => {
|
|
158
|
+
const dx = end.x - start.x;
|
|
159
|
+
const dy = end.y - start.y;
|
|
160
|
+
if (dx === 0 && dy === 0) {
|
|
161
|
+
return Math.hypot(point.x - start.x, point.y - start.y);
|
|
162
|
+
}
|
|
163
|
+
const t = Math.max(
|
|
164
|
+
0,
|
|
165
|
+
Math.min(
|
|
166
|
+
1,
|
|
167
|
+
((point.x - start.x) * dx + (point.y - start.y) * dy) / (dx * dx + dy * dy)
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy));
|
|
171
|
+
};
|
|
172
|
+
var hitTestDrawing = ({ point, drawing, layout, projectPoint }) => {
|
|
173
|
+
const geometry = getDrawingGeometry({
|
|
174
|
+
drawing,
|
|
175
|
+
layout,
|
|
176
|
+
projectPoint
|
|
177
|
+
});
|
|
178
|
+
if (!geometry) return null;
|
|
179
|
+
if (drawing.type !== "pin" && Math.hypot(point.x - geometry.start.x, point.y - geometry.start.y) <= DRAWING_HANDLE_HIT_DISTANCE) {
|
|
180
|
+
return { drawing, endpoint: "start" };
|
|
181
|
+
}
|
|
182
|
+
if (drawing.type === "pin" && Math.hypot(point.x - geometry.start.x, point.y - geometry.start.y) <= PIN_HIT_DISTANCE) {
|
|
183
|
+
return { drawing, endpoint: "move" };
|
|
184
|
+
}
|
|
185
|
+
if (drawing.type === "trendline" && Math.hypot(point.x - geometry.end.x, point.y - geometry.end.y) <= DRAWING_HANDLE_HIT_DISTANCE) {
|
|
186
|
+
return { drawing, endpoint: "end" };
|
|
187
|
+
}
|
|
188
|
+
const distance = distanceToSegment(point, geometry.lineStart, geometry.lineEnd);
|
|
189
|
+
return distance <= DRAWING_HIT_DISTANCE ? { drawing, endpoint: null } : null;
|
|
190
|
+
};
|
|
191
|
+
var updateDrawingById = (drawings, drawingId, updater) => (Array.isArray(drawings) ? drawings : []).map(
|
|
192
|
+
(drawing) => drawing?.id === drawingId ? updater(drawing) : drawing
|
|
193
|
+
);
|
|
194
|
+
var removeDrawingById = (drawings, drawingId) => (Array.isArray(drawings) ? drawings : []).filter(
|
|
195
|
+
(drawing) => drawing?.id !== drawingId
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// src/core/chartModel.js
|
|
199
|
+
var LINE_CAPABILITIES = Object.freeze({
|
|
200
|
+
appendAnimation: true,
|
|
201
|
+
drawings: true,
|
|
202
|
+
latestValue: true,
|
|
203
|
+
markers: true,
|
|
204
|
+
movingAverage: true
|
|
205
|
+
});
|
|
206
|
+
var BAR_CAPABILITIES = Object.freeze({
|
|
207
|
+
appendAnimation: false,
|
|
208
|
+
drawings: false,
|
|
209
|
+
latestValue: false,
|
|
210
|
+
markers: false,
|
|
211
|
+
movingAverage: false
|
|
212
|
+
});
|
|
213
|
+
var CHART_DEFINITIONS = Object.freeze({
|
|
214
|
+
line: Object.freeze({
|
|
215
|
+
capabilities: LINE_CAPABILITIES,
|
|
216
|
+
defaultOrientation: "vertical",
|
|
217
|
+
orientationFromSeries: false,
|
|
218
|
+
orientations: Object.freeze(["vertical"]),
|
|
219
|
+
rangeIncludesZero: false
|
|
220
|
+
}),
|
|
221
|
+
bar: Object.freeze({
|
|
222
|
+
capabilities: BAR_CAPABILITIES,
|
|
223
|
+
defaultOrientation: "vertical",
|
|
224
|
+
orientationFromSeries: true,
|
|
225
|
+
orientations: Object.freeze(["vertical", "horizontal"]),
|
|
226
|
+
rangeIncludesZero: true
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
var getChartCategories = (chart) => {
|
|
230
|
+
if (chart?.categories == null) return [];
|
|
231
|
+
if (!Array.isArray(chart.categories)) {
|
|
232
|
+
throw new TypeError(`Chart "${chart.id}" categories must be an array`);
|
|
233
|
+
}
|
|
234
|
+
const values = /* @__PURE__ */ new Set();
|
|
235
|
+
return chart.categories.map((category, index) => {
|
|
236
|
+
const normalized = typeof category === "string" ? { value: index, label: category } : { value: Number(category?.value), label: category?.label };
|
|
237
|
+
if (!Number.isFinite(normalized.value) || typeof normalized.label !== "string") {
|
|
238
|
+
throw new TypeError(
|
|
239
|
+
`Chart "${chart.id}" category ${index} must be a string or a { value, label } object`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (values.has(normalized.value)) {
|
|
243
|
+
throw new TypeError(
|
|
244
|
+
`Chart "${chart.id}" has duplicate category value ${normalized.value}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
values.add(normalized.value);
|
|
248
|
+
return normalized;
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
var getCategoryLabel = (chart, value) => getChartCategories(chart).find((category) => category.value === value)?.label;
|
|
252
|
+
var getSeriesType = (series) => {
|
|
253
|
+
if (series?.type == null) return "line";
|
|
254
|
+
if (Object.prototype.hasOwnProperty.call(CHART_DEFINITIONS, series.type)) {
|
|
255
|
+
return series.type;
|
|
256
|
+
}
|
|
257
|
+
throw new TypeError(`Unknown AlienCharts series type: "${series.type}"`);
|
|
258
|
+
};
|
|
259
|
+
var resolveChartDescriptor = (chart) => {
|
|
260
|
+
const series = Array.isArray(chart?.series) ? chart.series : [];
|
|
261
|
+
const categories = getChartCategories(chart);
|
|
262
|
+
const type = series.length ? getSeriesType(series[0]) : "line";
|
|
263
|
+
const definition = CHART_DEFINITIONS[type];
|
|
264
|
+
const orientation = definition.orientationFromSeries ? series[0]?.orientation || definition.defaultOrientation : definition.defaultOrientation;
|
|
265
|
+
series.forEach((item) => {
|
|
266
|
+
const itemType = getSeriesType(item);
|
|
267
|
+
if (itemType !== type) {
|
|
268
|
+
const mixedTypes = /* @__PURE__ */ new Set([type, itemType]);
|
|
269
|
+
const typeLabel = mixedTypes.has("line") && mixedTypes.has("bar") ? "line and bar" : `${type} and ${itemType}`;
|
|
270
|
+
throw new TypeError(
|
|
271
|
+
`Chart "${chart.id}" cannot mix ${typeLabel} series`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const itemOrientation = definition.orientationFromSeries ? item.orientation || definition.defaultOrientation : definition.defaultOrientation;
|
|
275
|
+
if (!definition.orientations.includes(itemOrientation)) {
|
|
276
|
+
throw new TypeError(
|
|
277
|
+
`Chart "${chart.id}" has an invalid ${itemType} orientation`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (itemOrientation !== orientation) {
|
|
281
|
+
throw new TypeError(
|
|
282
|
+
`Chart "${chart.id}" cannot mix ${itemType} orientations`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return Object.freeze({
|
|
287
|
+
capabilities: definition.capabilities,
|
|
288
|
+
categorical: categories.length > 0,
|
|
289
|
+
orientation,
|
|
290
|
+
rangeIncludesZero: definition.rangeIncludesZero,
|
|
291
|
+
rendererType: type,
|
|
292
|
+
type
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
var resolveChartDescriptors = (charts) => {
|
|
296
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
297
|
+
charts.forEach((chart) => {
|
|
298
|
+
descriptors.set(chart.id, resolveChartDescriptor(chart));
|
|
299
|
+
});
|
|
300
|
+
return descriptors;
|
|
301
|
+
};
|
|
302
|
+
var getChartDescriptor = (chart, descriptors) => descriptors?.get(chart?.id) || resolveChartDescriptor(chart);
|
|
303
|
+
var getOrderedSeries = (chart, seriesOrderByChart) => {
|
|
304
|
+
const series = Array.isArray(chart?.series) ? chart.series : [];
|
|
305
|
+
const order = seriesOrderByChart?.[chart?.id];
|
|
306
|
+
if (!Array.isArray(order) || order.length !== series.length) {
|
|
307
|
+
return series;
|
|
308
|
+
}
|
|
309
|
+
const used = /* @__PURE__ */ new Set();
|
|
310
|
+
const ordered = [];
|
|
311
|
+
order.forEach((seriesIndex) => {
|
|
312
|
+
if (!Number.isInteger(seriesIndex)) return;
|
|
313
|
+
if (seriesIndex < 0 || seriesIndex >= series.length) return;
|
|
314
|
+
if (used.has(seriesIndex)) return;
|
|
315
|
+
used.add(seriesIndex);
|
|
316
|
+
ordered.push(series[seriesIndex]);
|
|
317
|
+
});
|
|
318
|
+
return ordered.length === series.length ? ordered : series;
|
|
319
|
+
};
|
|
320
|
+
var CHART_TYPES = Object.freeze(Object.keys(CHART_DEFINITIONS));
|
|
321
|
+
|
|
322
|
+
// src/core/coordinateTransform.js
|
|
323
|
+
var safeSpan = (min, max) => Math.max(1e-12, max - min);
|
|
324
|
+
var createCoordinateTransform = ({
|
|
325
|
+
orientation = "vertical",
|
|
326
|
+
categoryRange,
|
|
327
|
+
valueRange,
|
|
328
|
+
plot
|
|
329
|
+
}) => {
|
|
330
|
+
const horizontal = orientation === "horizontal";
|
|
331
|
+
const categorySpan = safeSpan(categoryRange.min, categoryRange.max);
|
|
332
|
+
const valueSpan = safeSpan(valueRange.min, valueRange.max);
|
|
333
|
+
const categoryPixelLength = horizontal ? plot.height : plot.width;
|
|
334
|
+
const valuePixelLength = horizontal ? plot.width : plot.height;
|
|
335
|
+
const categoryRatio = (point) => horizontal ? (point.y - plot.y) / plot.height : (point.x - plot.x) / plot.width;
|
|
336
|
+
const valueRatio = (point) => horizontal ? (point.x - plot.x) / plot.width : 1 - (point.y - plot.y) / plot.height;
|
|
337
|
+
const dataToScreen = (point) => {
|
|
338
|
+
const category = point.category ?? point.x;
|
|
339
|
+
const value = point.value ?? point.y;
|
|
340
|
+
const categoryPosition = (category - categoryRange.min) / categorySpan;
|
|
341
|
+
const valuePosition = (value - valueRange.min) / valueSpan;
|
|
342
|
+
return horizontal ? {
|
|
343
|
+
x: plot.x + valuePosition * plot.width,
|
|
344
|
+
y: plot.y + categoryPosition * plot.height
|
|
345
|
+
} : {
|
|
346
|
+
x: plot.x + categoryPosition * plot.width,
|
|
347
|
+
y: plot.y + (1 - valuePosition) * plot.height
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
const screenToData = (point) => ({
|
|
351
|
+
x: categoryRange.min + categoryRatio(point) * categorySpan,
|
|
352
|
+
y: valueRange.min + valueRatio(point) * valueSpan
|
|
353
|
+
});
|
|
354
|
+
const categoryDragDelta = (start, current) => (horizontal ? current.y - start.y : current.x - start.x) / categoryPixelLength * categorySpan;
|
|
355
|
+
const valueOffsetDelta = (start, current) => horizontal ? -((current.x - start.x) / valuePixelLength) * valueSpan : (current.y - start.y) / valuePixelLength * valueSpan;
|
|
356
|
+
const categoryScaleDelta = (start, current) => horizontal ? current.y - start.y : current.x - start.x;
|
|
357
|
+
const valueScaleDelta = (start, current) => horizontal ? current.x - start.x : current.y - start.y;
|
|
358
|
+
const dataBoundsForRect = (rect) => {
|
|
359
|
+
const first = screenToData({ x: rect.left, y: rect.top });
|
|
360
|
+
const second = screenToData({
|
|
361
|
+
x: rect.left + rect.width,
|
|
362
|
+
y: rect.top + rect.height
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
categoryMin: Math.min(first.x, second.x),
|
|
366
|
+
categoryMax: Math.max(first.x, second.x),
|
|
367
|
+
valueMin: Math.min(first.y, second.y),
|
|
368
|
+
valueMax: Math.max(first.y, second.y)
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
return Object.freeze({
|
|
372
|
+
categoryDragDelta,
|
|
373
|
+
categoryPixelLength,
|
|
374
|
+
categoryRatio,
|
|
375
|
+
categoryScaleDelta,
|
|
376
|
+
dataBoundsForRect,
|
|
377
|
+
dataToScreen,
|
|
378
|
+
horizontal,
|
|
379
|
+
orientation,
|
|
380
|
+
screenToData,
|
|
381
|
+
valueOffsetDelta,
|
|
382
|
+
valuePixelLength,
|
|
383
|
+
valueRatio,
|
|
384
|
+
valueScaleDelta
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/core/renderers/webglUtils.js
|
|
389
|
+
var INITIAL_GPU_VERTEX_CAPACITY = 4096;
|
|
390
|
+
var createProgram = (gl, vertexSource, fragmentSource) => {
|
|
391
|
+
const compile = (type, source) => {
|
|
392
|
+
const shader = gl.createShader(type);
|
|
393
|
+
gl.shaderSource(shader, source);
|
|
394
|
+
gl.compileShader(shader);
|
|
395
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
396
|
+
throw new Error(gl.getShaderInfoLog(shader) || "Shader compile failed");
|
|
397
|
+
}
|
|
398
|
+
return shader;
|
|
399
|
+
};
|
|
400
|
+
const program = gl.createProgram();
|
|
401
|
+
const vertexShader = compile(gl.VERTEX_SHADER, vertexSource);
|
|
402
|
+
const fragmentShader = compile(gl.FRAGMENT_SHADER, fragmentSource);
|
|
403
|
+
gl.attachShader(program, vertexShader);
|
|
404
|
+
gl.attachShader(program, fragmentShader);
|
|
405
|
+
gl.linkProgram(program);
|
|
406
|
+
gl.deleteShader(vertexShader);
|
|
407
|
+
gl.deleteShader(fragmentShader);
|
|
408
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
409
|
+
throw new Error(gl.getProgramInfoLog(program) || "Program link failed");
|
|
410
|
+
}
|
|
411
|
+
return program;
|
|
412
|
+
};
|
|
413
|
+
var hexToRgb = (color) => {
|
|
414
|
+
const value = String(color || "#38bdf8").replace("#", "");
|
|
415
|
+
const normalized = value.length === 3 ? value.split("").map((char) => char + char).join("") : value.padEnd(6, "0").slice(0, 6);
|
|
416
|
+
const number = Number.parseInt(normalized, 16);
|
|
417
|
+
return [
|
|
418
|
+
(number >> 16 & 255) / 255,
|
|
419
|
+
(number >> 8 & 255) / 255,
|
|
420
|
+
(number & 255) / 255
|
|
421
|
+
];
|
|
422
|
+
};
|
|
423
|
+
var getNextVertexCapacity = (currentCapacity, requiredCapacity) => {
|
|
424
|
+
let nextCapacity = Math.max(
|
|
425
|
+
INITIAL_GPU_VERTEX_CAPACITY,
|
|
426
|
+
currentCapacity || 0
|
|
427
|
+
);
|
|
428
|
+
while (nextCapacity < requiredCapacity) nextCapacity *= 2;
|
|
429
|
+
return nextCapacity;
|
|
430
|
+
};
|
|
431
|
+
var getDynamicBuffer = (gl, cache, key, requiredFloats) => {
|
|
432
|
+
let entry = cache.get(key);
|
|
433
|
+
if (!entry) {
|
|
434
|
+
entry = {
|
|
435
|
+
buffer: gl.createBuffer(),
|
|
436
|
+
gpuCapacity: 0,
|
|
437
|
+
vertices: new Float32Array(
|
|
438
|
+
getNextVertexCapacity(0, requiredFloats)
|
|
439
|
+
)
|
|
440
|
+
};
|
|
441
|
+
cache.set(key, entry);
|
|
442
|
+
}
|
|
443
|
+
if (entry.vertices.length < requiredFloats) {
|
|
444
|
+
entry.vertices = new Float32Array(
|
|
445
|
+
getNextVertexCapacity(entry.vertices.length, requiredFloats)
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (entry.gpuCapacity < entry.vertices.length) {
|
|
449
|
+
entry.gpuCapacity = entry.vertices.length;
|
|
450
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
|
|
451
|
+
gl.bufferData(
|
|
452
|
+
gl.ARRAY_BUFFER,
|
|
453
|
+
entry.gpuCapacity * Float32Array.BYTES_PER_ELEMENT,
|
|
454
|
+
gl.DYNAMIC_DRAW
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
return entry;
|
|
458
|
+
};
|
|
459
|
+
var deleteBufferCache = (gl, cache) => {
|
|
460
|
+
cache.forEach((entry) => {
|
|
461
|
+
if (entry.buffer) gl.deleteBuffer(entry.buffer);
|
|
462
|
+
});
|
|
463
|
+
cache.clear();
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/core/renderers/barRenderer.js
|
|
467
|
+
var BAR_GROUP_WIDTH_RATIO = 0.8;
|
|
468
|
+
var BAR_SLICE_WIDTH_RATIO = 0.9;
|
|
469
|
+
var BAR_VERTEX_FLOAT_STRIDE = 3;
|
|
470
|
+
var VERTEX_SOURCE = `#version 300 es
|
|
471
|
+
in vec2 a_unit;
|
|
472
|
+
in vec3 a_bar;
|
|
473
|
+
uniform vec2 u_resolution;
|
|
474
|
+
uniform vec4 u_rect;
|
|
475
|
+
uniform vec2 u_categoryRange;
|
|
476
|
+
uniform vec2 u_valueRange;
|
|
477
|
+
uniform float u_baseline;
|
|
478
|
+
uniform float u_seriesIndex;
|
|
479
|
+
uniform float u_seriesCount;
|
|
480
|
+
uniform int u_orientation;
|
|
481
|
+
|
|
482
|
+
void main() {
|
|
483
|
+
float groupWidth = a_bar.z * ${BAR_GROUP_WIDTH_RATIO.toFixed(1)};
|
|
484
|
+
float sliceWidth = groupWidth / max(1.0, u_seriesCount);
|
|
485
|
+
float center = a_bar.x - groupWidth * 0.5
|
|
486
|
+
+ sliceWidth * (u_seriesIndex + 0.5);
|
|
487
|
+
float halfWidth = sliceWidth * ${(BAR_SLICE_WIDTH_RATIO / 2).toFixed(2)};
|
|
488
|
+
float category = mix(center - halfWidth, center + halfWidth, a_unit.x);
|
|
489
|
+
float value = mix(u_baseline, a_bar.y, a_unit.y);
|
|
490
|
+
float categoryDenom = max(
|
|
491
|
+
0.000000000001,
|
|
492
|
+
abs(u_categoryRange.y - u_categoryRange.x)
|
|
493
|
+
);
|
|
494
|
+
float valueDenom = max(
|
|
495
|
+
0.000000000001,
|
|
496
|
+
abs(u_valueRange.y - u_valueRange.x)
|
|
497
|
+
);
|
|
498
|
+
float categoryRatio = (category - u_categoryRange.x) / categoryDenom;
|
|
499
|
+
float valueRatio = (value - u_valueRange.x) / valueDenom;
|
|
500
|
+
vec2 px = u_orientation == 0
|
|
501
|
+
? vec2(
|
|
502
|
+
u_rect.x + categoryRatio * u_rect.z,
|
|
503
|
+
u_rect.y + (1.0 - valueRatio) * u_rect.w
|
|
504
|
+
)
|
|
505
|
+
: vec2(
|
|
506
|
+
u_rect.x + valueRatio * u_rect.z,
|
|
507
|
+
u_rect.y + categoryRatio * u_rect.w
|
|
508
|
+
);
|
|
509
|
+
vec2 clip = (px / u_resolution) * 2.0 - 1.0;
|
|
510
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
511
|
+
}
|
|
512
|
+
`;
|
|
513
|
+
var FRAGMENT_SOURCE = `#version 300 es
|
|
514
|
+
precision highp float;
|
|
515
|
+
uniform vec3 u_color;
|
|
516
|
+
out vec4 outColor;
|
|
517
|
+
|
|
518
|
+
void main() {
|
|
519
|
+
outColor = vec4(u_color, 1.0);
|
|
520
|
+
}
|
|
521
|
+
`;
|
|
522
|
+
var getBarCategoryInterval = ({
|
|
523
|
+
visiblePoints,
|
|
524
|
+
index,
|
|
525
|
+
categoryMin,
|
|
526
|
+
categoryMax
|
|
527
|
+
}) => {
|
|
528
|
+
const count = visiblePoints.pointCount;
|
|
529
|
+
const category = visiblePoints.x[index];
|
|
530
|
+
const fallbackInterval = (categoryMax - categoryMin) / Math.max(10, count || 1);
|
|
531
|
+
const previousGap = index > 0 ? category - visiblePoints.x[index - 1] : Infinity;
|
|
532
|
+
const nextGap = index + 1 < count ? visiblePoints.x[index + 1] - category : Infinity;
|
|
533
|
+
let interval = Math.min(
|
|
534
|
+
previousGap > 0 ? previousGap : Infinity,
|
|
535
|
+
nextGap > 0 ? nextGap : Infinity
|
|
536
|
+
);
|
|
537
|
+
if (!Number.isFinite(interval)) {
|
|
538
|
+
interval = Number.isFinite(previousGap) && previousGap > 0 ? previousGap : Number.isFinite(nextGap) && nextGap > 0 ? nextGap : fallbackInterval;
|
|
539
|
+
}
|
|
540
|
+
return Math.max(Number.EPSILON, interval || fallbackInterval || 1);
|
|
541
|
+
};
|
|
542
|
+
var getGroupedBarCategory = ({
|
|
543
|
+
visiblePoints,
|
|
544
|
+
pointIndex,
|
|
545
|
+
seriesIndex,
|
|
546
|
+
seriesCount,
|
|
547
|
+
categoryMin,
|
|
548
|
+
categoryMax
|
|
549
|
+
}) => {
|
|
550
|
+
const category = visiblePoints.x[pointIndex];
|
|
551
|
+
const interval = getBarCategoryInterval({
|
|
552
|
+
visiblePoints,
|
|
553
|
+
index: pointIndex,
|
|
554
|
+
categoryMin,
|
|
555
|
+
categoryMax
|
|
556
|
+
});
|
|
557
|
+
const groupWidth = interval * BAR_GROUP_WIDTH_RATIO;
|
|
558
|
+
const sliceWidth = groupWidth / Math.max(1, seriesCount);
|
|
559
|
+
return category - groupWidth / 2 + sliceWidth * (seriesIndex + 0.5);
|
|
560
|
+
};
|
|
561
|
+
var fillBarInstances = ({
|
|
562
|
+
vertices,
|
|
563
|
+
visiblePoints,
|
|
564
|
+
categoryMin,
|
|
565
|
+
categoryMax
|
|
566
|
+
}) => {
|
|
567
|
+
for (let index = 0; index < visiblePoints.pointCount; index += 1) {
|
|
568
|
+
const offset = index * BAR_VERTEX_FLOAT_STRIDE;
|
|
569
|
+
vertices[offset] = visiblePoints.x[index];
|
|
570
|
+
vertices[offset + 1] = visiblePoints.y[index];
|
|
571
|
+
vertices[offset + 2] = getBarCategoryInterval({
|
|
572
|
+
visiblePoints,
|
|
573
|
+
index,
|
|
574
|
+
categoryMin,
|
|
575
|
+
categoryMax
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return visiblePoints.pointCount;
|
|
579
|
+
};
|
|
580
|
+
var createBarRenderer = (gl) => {
|
|
581
|
+
const program = createProgram(gl, VERTEX_SOURCE, FRAGMENT_SOURCE);
|
|
582
|
+
const quadBuffer = gl.createBuffer();
|
|
583
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
584
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
585
|
+
gl.bufferData(
|
|
586
|
+
gl.ARRAY_BUFFER,
|
|
587
|
+
new Float32Array([
|
|
588
|
+
0,
|
|
589
|
+
0,
|
|
590
|
+
1,
|
|
591
|
+
0,
|
|
592
|
+
0,
|
|
593
|
+
1,
|
|
594
|
+
0,
|
|
595
|
+
1,
|
|
596
|
+
1,
|
|
597
|
+
0,
|
|
598
|
+
1,
|
|
599
|
+
1
|
|
600
|
+
]),
|
|
601
|
+
gl.STATIC_DRAW
|
|
602
|
+
);
|
|
603
|
+
const locations = Object.freeze({
|
|
604
|
+
aBar: gl.getAttribLocation(program, "a_bar"),
|
|
605
|
+
aUnit: gl.getAttribLocation(program, "a_unit"),
|
|
606
|
+
uBaseline: gl.getUniformLocation(program, "u_baseline"),
|
|
607
|
+
uCategoryRange: gl.getUniformLocation(program, "u_categoryRange"),
|
|
608
|
+
uColor: gl.getUniformLocation(program, "u_color"),
|
|
609
|
+
uOrientation: gl.getUniformLocation(program, "u_orientation"),
|
|
610
|
+
uRect: gl.getUniformLocation(program, "u_rect"),
|
|
611
|
+
uResolution: gl.getUniformLocation(program, "u_resolution"),
|
|
612
|
+
uSeriesCount: gl.getUniformLocation(program, "u_seriesCount"),
|
|
613
|
+
uSeriesIndex: gl.getUniformLocation(program, "u_seriesIndex"),
|
|
614
|
+
uValueRange: gl.getUniformLocation(program, "u_valueRange")
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
draw({
|
|
618
|
+
categoryPixelLength,
|
|
619
|
+
chart,
|
|
620
|
+
descriptor,
|
|
621
|
+
pixelHeight,
|
|
622
|
+
pixelWidth,
|
|
623
|
+
scaledPlot,
|
|
624
|
+
seriesOrderByChart,
|
|
625
|
+
state,
|
|
626
|
+
yRange
|
|
627
|
+
}) {
|
|
628
|
+
gl.disable(gl.BLEND);
|
|
629
|
+
gl.useProgram(program);
|
|
630
|
+
gl.enableVertexAttribArray(locations.aUnit);
|
|
631
|
+
gl.enableVertexAttribArray(locations.aBar);
|
|
632
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
633
|
+
gl.vertexAttribPointer(locations.aUnit, 2, gl.FLOAT, false, 0, 0);
|
|
634
|
+
gl.vertexAttribDivisor(locations.aUnit, 0);
|
|
635
|
+
gl.uniform2f(locations.uResolution, pixelWidth, pixelHeight);
|
|
636
|
+
gl.uniform4f(
|
|
637
|
+
locations.uRect,
|
|
638
|
+
scaledPlot.x,
|
|
639
|
+
scaledPlot.y,
|
|
640
|
+
scaledPlot.width,
|
|
641
|
+
scaledPlot.height
|
|
642
|
+
);
|
|
643
|
+
gl.uniform2f(
|
|
644
|
+
locations.uCategoryRange,
|
|
645
|
+
state.xMin,
|
|
646
|
+
state.xMax
|
|
647
|
+
);
|
|
648
|
+
gl.uniform2f(
|
|
649
|
+
locations.uValueRange,
|
|
650
|
+
yRange.minY,
|
|
651
|
+
yRange.maxY
|
|
652
|
+
);
|
|
653
|
+
gl.uniform1f(locations.uBaseline, 0);
|
|
654
|
+
gl.uniform1i(
|
|
655
|
+
locations.uOrientation,
|
|
656
|
+
descriptor.orientation === "horizontal" ? 1 : 0
|
|
657
|
+
);
|
|
658
|
+
const orderedSeries = getOrderedSeries(chart, seriesOrderByChart);
|
|
659
|
+
gl.uniform1f(locations.uSeriesCount, Math.max(1, orderedSeries.length));
|
|
660
|
+
orderedSeries.forEach((series, seriesIndex) => {
|
|
661
|
+
const visiblePoints = series.getVisiblePoints(
|
|
662
|
+
state.xMin,
|
|
663
|
+
state.xMax,
|
|
664
|
+
categoryPixelLength
|
|
665
|
+
);
|
|
666
|
+
if (!visiblePoints.pointCount) return;
|
|
667
|
+
const requiredFloats = visiblePoints.pointCount * BAR_VERTEX_FLOAT_STRIDE;
|
|
668
|
+
const bufferEntry = getDynamicBuffer(
|
|
669
|
+
gl,
|
|
670
|
+
buffers,
|
|
671
|
+
series.id,
|
|
672
|
+
requiredFloats
|
|
673
|
+
);
|
|
674
|
+
const instanceCount = fillBarInstances({
|
|
675
|
+
vertices: bufferEntry.vertices,
|
|
676
|
+
visiblePoints,
|
|
677
|
+
categoryMin: state.xMin,
|
|
678
|
+
categoryMax: state.xMax
|
|
679
|
+
});
|
|
680
|
+
const [r, g, b] = hexToRgb(series.color);
|
|
681
|
+
gl.uniform3f(locations.uColor, r, g, b);
|
|
682
|
+
gl.uniform1f(locations.uSeriesIndex, seriesIndex);
|
|
683
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, bufferEntry.buffer);
|
|
684
|
+
gl.vertexAttribPointer(
|
|
685
|
+
locations.aBar,
|
|
686
|
+
BAR_VERTEX_FLOAT_STRIDE,
|
|
687
|
+
gl.FLOAT,
|
|
688
|
+
false,
|
|
689
|
+
0,
|
|
690
|
+
0
|
|
691
|
+
);
|
|
692
|
+
gl.vertexAttribDivisor(locations.aBar, 1);
|
|
693
|
+
gl.bufferSubData(
|
|
694
|
+
gl.ARRAY_BUFFER,
|
|
695
|
+
0,
|
|
696
|
+
bufferEntry.vertices,
|
|
697
|
+
0,
|
|
698
|
+
instanceCount * BAR_VERTEX_FLOAT_STRIDE
|
|
699
|
+
);
|
|
700
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
|
|
701
|
+
});
|
|
702
|
+
gl.vertexAttribDivisor(locations.aBar, 0);
|
|
703
|
+
return { seriesEndpoints: /* @__PURE__ */ new Map() };
|
|
704
|
+
},
|
|
705
|
+
getTooltipCategory(payload) {
|
|
706
|
+
return getGroupedBarCategory(payload);
|
|
707
|
+
},
|
|
708
|
+
destroy() {
|
|
709
|
+
deleteBufferCache(gl, buffers);
|
|
710
|
+
gl.deleteBuffer(quadBuffer);
|
|
711
|
+
gl.deleteProgram(program);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/core/lodSeries.js
|
|
717
|
+
var DEFAULT_MAX_LEVELS = 18;
|
|
718
|
+
var toTypedArray = (value, Type) => {
|
|
719
|
+
if (value instanceof Type) return value;
|
|
720
|
+
if (ArrayBuffer.isView(value)) return new Type(value);
|
|
721
|
+
if (Array.isArray(value)) return new Type(value);
|
|
722
|
+
return new Type(0);
|
|
723
|
+
};
|
|
724
|
+
var lowerBound = (array, value) => {
|
|
725
|
+
let lo = 0;
|
|
726
|
+
let hi = array.length;
|
|
727
|
+
while (lo < hi) {
|
|
728
|
+
const mid = lo + hi >> 1;
|
|
729
|
+
if (array[mid] < value) lo = mid + 1;
|
|
730
|
+
else hi = mid;
|
|
731
|
+
}
|
|
732
|
+
return lo;
|
|
733
|
+
};
|
|
734
|
+
var upperBound = (array, value) => {
|
|
735
|
+
let lo = 0;
|
|
736
|
+
let hi = array.length;
|
|
737
|
+
while (lo < hi) {
|
|
738
|
+
const mid = lo + hi >> 1;
|
|
739
|
+
if (array[mid] <= value) lo = mid + 1;
|
|
740
|
+
else hi = mid;
|
|
741
|
+
}
|
|
742
|
+
return lo;
|
|
743
|
+
};
|
|
744
|
+
var pushUniqueSorted = (indices, index) => {
|
|
745
|
+
if (index < 0) return;
|
|
746
|
+
if (indices.includes(index)) return;
|
|
747
|
+
indices.push(index);
|
|
748
|
+
};
|
|
749
|
+
var buildLod = (rawX, rawY, bucketSize, startIndex = 0, endIndex = rawX.length) => {
|
|
750
|
+
if (bucketSize <= 1 || rawX.length <= bucketSize) {
|
|
751
|
+
return { x: rawX, y: rawY, bucketSize: 1 };
|
|
752
|
+
}
|
|
753
|
+
const alignedStart = Math.max(0, Math.floor(startIndex / bucketSize) * bucketSize);
|
|
754
|
+
const alignedEnd = Math.min(rawX.length, endIndex);
|
|
755
|
+
const estimated = Math.ceil((alignedEnd - alignedStart) / bucketSize) * 4;
|
|
756
|
+
const x = new Float64Array(estimated);
|
|
757
|
+
const y = new Float32Array(estimated);
|
|
758
|
+
let out = 0;
|
|
759
|
+
for (let start = alignedStart; start < alignedEnd; start += bucketSize) {
|
|
760
|
+
const end = Math.min(alignedEnd, start + bucketSize);
|
|
761
|
+
let minIndex = start;
|
|
762
|
+
let maxIndex = start;
|
|
763
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
764
|
+
if (rawY[i] < rawY[minIndex]) minIndex = i;
|
|
765
|
+
if (rawY[i] > rawY[maxIndex]) maxIndex = i;
|
|
766
|
+
}
|
|
767
|
+
const indices = [];
|
|
768
|
+
pushUniqueSorted(indices, start);
|
|
769
|
+
pushUniqueSorted(indices, minIndex);
|
|
770
|
+
pushUniqueSorted(indices, maxIndex);
|
|
771
|
+
pushUniqueSorted(indices, end - 1);
|
|
772
|
+
indices.sort((a, b) => a - b);
|
|
773
|
+
for (let i = 0; i < indices.length; i += 1) {
|
|
774
|
+
const sourceIndex = indices[i];
|
|
775
|
+
x[out] = rawX[sourceIndex];
|
|
776
|
+
y[out] = rawY[sourceIndex];
|
|
777
|
+
out += 1;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
x: x.subarray(0, out),
|
|
782
|
+
y: y.subarray(0, out),
|
|
783
|
+
bucketSize
|
|
784
|
+
};
|
|
785
|
+
};
|
|
786
|
+
var concatTyped = (Type, left, right) => {
|
|
787
|
+
if (!left?.length) return right;
|
|
788
|
+
if (!right?.length) return left;
|
|
789
|
+
const out = new Type(left.length + right.length);
|
|
790
|
+
out.set(left, 0);
|
|
791
|
+
out.set(right, left.length);
|
|
792
|
+
return out;
|
|
793
|
+
};
|
|
794
|
+
var LodSeries = class {
|
|
795
|
+
constructor({ id, name, color, x, y, maxLevels = DEFAULT_MAX_LEVELS }) {
|
|
796
|
+
this.id = id;
|
|
797
|
+
this.name = name || id;
|
|
798
|
+
this.color = color || "#38bdf8";
|
|
799
|
+
this.maxLevels = maxLevels;
|
|
800
|
+
const initialX = toTypedArray(x, Float64Array);
|
|
801
|
+
const initialY = toTypedArray(y, Float32Array);
|
|
802
|
+
this._length = Math.min(initialX.length, initialY.length);
|
|
803
|
+
const capacity = Math.max(1, this._length);
|
|
804
|
+
this.rawX = new Float64Array(capacity);
|
|
805
|
+
this.rawY = new Float32Array(capacity);
|
|
806
|
+
this.rawX.set(initialX.subarray(0, this._length));
|
|
807
|
+
this.rawY.set(initialY.subarray(0, this._length));
|
|
808
|
+
this.levels = [];
|
|
809
|
+
this.rebuildLevels();
|
|
810
|
+
}
|
|
811
|
+
get length() {
|
|
812
|
+
return this._length;
|
|
813
|
+
}
|
|
814
|
+
append(xValues, yValues) {
|
|
815
|
+
const nextX = toTypedArray(xValues, Float64Array);
|
|
816
|
+
const nextY = toTypedArray(yValues, Float32Array);
|
|
817
|
+
const appendLength = Math.min(nextX.length, nextY.length);
|
|
818
|
+
if (appendLength === 0) return;
|
|
819
|
+
const currentLength = this.length;
|
|
820
|
+
this.ensureRawCapacity(currentLength + appendLength);
|
|
821
|
+
this.rawX.set(nextX.subarray(0, appendLength), currentLength);
|
|
822
|
+
this.rawY.set(nextY.subarray(0, appendLength), currentLength);
|
|
823
|
+
this._length = currentLength + appendLength;
|
|
824
|
+
this.markLevelsDirty(currentLength);
|
|
825
|
+
}
|
|
826
|
+
rebuildLevels() {
|
|
827
|
+
const rawX = this.rawX.subarray(0, this.length);
|
|
828
|
+
const rawY = this.rawY.subarray(0, this.length);
|
|
829
|
+
this.levels = [{ x: rawX, y: rawY, bucketSize: 1 }];
|
|
830
|
+
let bucketSize = 4;
|
|
831
|
+
while (this.levels.length < this.maxLevels && bucketSize < Math.max(8, rawX.length)) {
|
|
832
|
+
this.levels.push(buildLod(rawX, rawY, bucketSize));
|
|
833
|
+
bucketSize *= 4;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
ensureRawCapacity(requiredLength) {
|
|
837
|
+
if (requiredLength <= this.rawX.length && requiredLength <= this.rawY.length) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const nextCapacity = Math.max(requiredLength, Math.ceil(this.rawX.length * 1.5));
|
|
841
|
+
const rawX = new Float64Array(nextCapacity);
|
|
842
|
+
const rawY = new Float32Array(nextCapacity);
|
|
843
|
+
rawX.set(this.rawX.subarray(0, this.length));
|
|
844
|
+
rawY.set(this.rawY.subarray(0, this.length));
|
|
845
|
+
this.rawX = rawX;
|
|
846
|
+
this.rawY = rawY;
|
|
847
|
+
}
|
|
848
|
+
markLevelsDirty(previousLength) {
|
|
849
|
+
const rawX = this.rawX.subarray(0, this.length);
|
|
850
|
+
const rawY = this.rawY.subarray(0, this.length);
|
|
851
|
+
this.levels[0] = { x: rawX, y: rawY, bucketSize: 1 };
|
|
852
|
+
let bucketSize = this.levels.length > 0 ? this.levels[this.levels.length - 1].bucketSize * 4 : 4;
|
|
853
|
+
while (this.levels.length < this.maxLevels && bucketSize < Math.max(8, rawX.length)) {
|
|
854
|
+
this.levels.push(buildLod(rawX, rawY, bucketSize));
|
|
855
|
+
bucketSize *= 4;
|
|
856
|
+
}
|
|
857
|
+
for (let i = 1; i < this.levels.length; i += 1) {
|
|
858
|
+
const level = this.levels[i];
|
|
859
|
+
const dirtyRawStart = Math.floor(previousLength / level.bucketSize) * level.bucketSize;
|
|
860
|
+
level.dirtyFrom = Math.min(level.dirtyFrom ?? dirtyRawStart, dirtyRawStart);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
ensureLevel(index) {
|
|
864
|
+
const level = this.levels[index];
|
|
865
|
+
if (!level || index === 0 || level.dirtyFrom == null) return level;
|
|
866
|
+
const rawX = this.rawX.subarray(0, this.length);
|
|
867
|
+
const rawY = this.rawY.subarray(0, this.length);
|
|
868
|
+
const dirtyRawStart = Math.floor(level.dirtyFrom / level.bucketSize) * level.bucketSize;
|
|
869
|
+
const cutoffX = rawX[dirtyRawStart] ?? rawX[0] ?? 0;
|
|
870
|
+
const keepCount = lowerBound(level.x, cutoffX);
|
|
871
|
+
const tail = buildLod(rawX, rawY, level.bucketSize, dirtyRawStart, rawX.length);
|
|
872
|
+
this.levels[index] = {
|
|
873
|
+
x: concatTyped(Float64Array, level.x.subarray(0, keepCount), tail.x),
|
|
874
|
+
y: concatTyped(Float32Array, level.y.subarray(0, keepCount), tail.y),
|
|
875
|
+
bucketSize: level.bucketSize
|
|
876
|
+
};
|
|
877
|
+
return this.levels[index];
|
|
878
|
+
}
|
|
879
|
+
selectLevel(xMin, xMax, pixelWidth, targetPointsPerPixel = 2.5) {
|
|
880
|
+
const rawX = this.levels[0].x;
|
|
881
|
+
if (rawX.length === 0) return this.levels[0];
|
|
882
|
+
const from = lowerBound(rawX, xMin);
|
|
883
|
+
const to = upperBound(rawX, xMax);
|
|
884
|
+
const visibleRawPoints = Math.max(0, to - from);
|
|
885
|
+
const targetPoints = Math.max(32, pixelWidth * targetPointsPerPixel);
|
|
886
|
+
const desiredBucket = Math.max(1, visibleRawPoints / targetPoints);
|
|
887
|
+
let selectedIndex = 0;
|
|
888
|
+
for (let i = 1; i < this.levels.length; i += 1) {
|
|
889
|
+
if (this.levels[i].bucketSize <= desiredBucket) {
|
|
890
|
+
selectedIndex = i;
|
|
891
|
+
} else {
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return this.ensureLevel(selectedIndex);
|
|
896
|
+
}
|
|
897
|
+
getVisiblePoints(xMin, xMax, pixelWidth) {
|
|
898
|
+
const level = this.selectLevel(xMin, xMax, pixelWidth);
|
|
899
|
+
const start = Math.max(0, lowerBound(level.x, xMin) - 1);
|
|
900
|
+
const end = Math.min(level.x.length, upperBound(level.x, xMax) + 1);
|
|
901
|
+
return {
|
|
902
|
+
x: level.x.subarray(start, end),
|
|
903
|
+
y: level.y.subarray(start, end),
|
|
904
|
+
bucketSize: level.bucketSize,
|
|
905
|
+
pointCount: Math.max(0, end - start)
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
var LineSeries = class extends LodSeries {
|
|
910
|
+
constructor(options) {
|
|
911
|
+
super(options);
|
|
912
|
+
this.type = "line";
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
var BarSeries = class extends LodSeries {
|
|
916
|
+
constructor({ orientation = "vertical", ...options }) {
|
|
917
|
+
super(options);
|
|
918
|
+
if (orientation !== "vertical" && orientation !== "horizontal") {
|
|
919
|
+
throw new TypeError(
|
|
920
|
+
'BarSeries orientation must be "vertical" or "horizontal"'
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
this.type = "bar";
|
|
924
|
+
this.orientation = orientation;
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
var createLineSeries = (options) => new LineSeries(options);
|
|
928
|
+
var createSeries = createLineSeries;
|
|
929
|
+
var createBarSeries = (options) => new BarSeries(options);
|
|
930
|
+
|
|
931
|
+
// src/core/renderers/lineRenderer.js
|
|
932
|
+
var AA_VERTEX_FLOAT_STRIDE = 6;
|
|
933
|
+
var AA_LINE_WIDTH_PX = 1.5;
|
|
934
|
+
var AA_EDGE_WIDTH_PX = 1;
|
|
935
|
+
var DASH_LENGTH_PX = 7;
|
|
936
|
+
var DASH_GAP_PX = 5;
|
|
937
|
+
var LINE_VERTEX_SOURCE = `#version 300 es
|
|
938
|
+
in vec2 a_xy;
|
|
939
|
+
uniform vec2 u_resolution;
|
|
940
|
+
uniform vec4 u_rect;
|
|
941
|
+
uniform vec2 u_xRange;
|
|
942
|
+
uniform vec2 u_yRange;
|
|
943
|
+
|
|
944
|
+
void main() {
|
|
945
|
+
float xDenom = max(0.000000000001, abs(u_xRange.y - u_xRange.x));
|
|
946
|
+
float yDenom = max(0.000000000001, abs(u_yRange.y - u_yRange.x));
|
|
947
|
+
float nx = (a_xy.x - u_xRange.x) / xDenom;
|
|
948
|
+
float ny = (a_xy.y - u_yRange.x) / yDenom;
|
|
949
|
+
float px = u_rect.x + nx * u_rect.z;
|
|
950
|
+
float py = u_rect.y + (1.0 - ny) * u_rect.w;
|
|
951
|
+
vec2 clip = (vec2(px, py) / u_resolution) * 2.0 - 1.0;
|
|
952
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
953
|
+
}
|
|
954
|
+
`;
|
|
955
|
+
var LINE_FRAGMENT_SOURCE = `#version 300 es
|
|
956
|
+
precision highp float;
|
|
957
|
+
uniform vec3 u_color;
|
|
958
|
+
out vec4 outColor;
|
|
959
|
+
|
|
960
|
+
void main() {
|
|
961
|
+
outColor = vec4(u_color, 1.0);
|
|
962
|
+
}
|
|
963
|
+
`;
|
|
964
|
+
var AA_VERTEX_SOURCE = `#version 300 es
|
|
965
|
+
in vec2 a_start;
|
|
966
|
+
in vec2 a_end;
|
|
967
|
+
in float a_side;
|
|
968
|
+
in float a_along;
|
|
969
|
+
uniform vec2 u_resolution;
|
|
970
|
+
uniform vec4 u_rect;
|
|
971
|
+
uniform vec2 u_xRange;
|
|
972
|
+
uniform vec2 u_yRange;
|
|
973
|
+
uniform float u_lineHalfWidth;
|
|
974
|
+
uniform float u_edgeWidth;
|
|
975
|
+
out float v_side;
|
|
976
|
+
|
|
977
|
+
vec2 toPixel(vec2 value) {
|
|
978
|
+
float xDenom = max(0.000000000001, abs(u_xRange.y - u_xRange.x));
|
|
979
|
+
float yDenom = max(0.000000000001, abs(u_yRange.y - u_yRange.x));
|
|
980
|
+
float nx = (value.x - u_xRange.x) / xDenom;
|
|
981
|
+
float ny = (value.y - u_yRange.x) / yDenom;
|
|
982
|
+
return vec2(
|
|
983
|
+
u_rect.x + nx * u_rect.z,
|
|
984
|
+
u_rect.y + (1.0 - ny) * u_rect.w
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
void main() {
|
|
989
|
+
vec2 startPx = toPixel(a_start);
|
|
990
|
+
vec2 endPx = toPixel(a_end);
|
|
991
|
+
vec2 delta = endPx - startPx;
|
|
992
|
+
float segmentLength = max(0.000001, length(delta));
|
|
993
|
+
vec2 direction = delta / segmentLength;
|
|
994
|
+
vec2 normal = vec2(-direction.y, direction.x);
|
|
995
|
+
float expand = u_lineHalfWidth + u_edgeWidth;
|
|
996
|
+
float cap = min(expand, segmentLength * 0.5);
|
|
997
|
+
vec2 px = mix(startPx, endPx, a_along)
|
|
998
|
+
+ direction * ((a_along * 2.0 - 1.0) * cap)
|
|
999
|
+
+ normal * a_side * expand;
|
|
1000
|
+
vec2 clip = (px / u_resolution) * 2.0 - 1.0;
|
|
1001
|
+
v_side = a_side;
|
|
1002
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
1003
|
+
}
|
|
1004
|
+
`;
|
|
1005
|
+
var AA_FRAGMENT_SOURCE = `#version 300 es
|
|
1006
|
+
precision highp float;
|
|
1007
|
+
uniform vec3 u_color;
|
|
1008
|
+
uniform float u_lineHalfWidth;
|
|
1009
|
+
uniform float u_edgeWidth;
|
|
1010
|
+
in float v_side;
|
|
1011
|
+
out vec4 outColor;
|
|
1012
|
+
|
|
1013
|
+
void main() {
|
|
1014
|
+
float expand = u_lineHalfWidth + u_edgeWidth;
|
|
1015
|
+
float distanceFromCenter = abs(v_side) * expand;
|
|
1016
|
+
float alpha = 1.0 - smoothstep(u_lineHalfWidth, expand, distanceFromCenter);
|
|
1017
|
+
outColor = vec4(u_color, alpha);
|
|
1018
|
+
}
|
|
1019
|
+
`;
|
|
1020
|
+
var fillSeriesPoints = ({
|
|
1021
|
+
vertices,
|
|
1022
|
+
visiblePoints,
|
|
1023
|
+
pointCount,
|
|
1024
|
+
animatedPoint
|
|
1025
|
+
}) => {
|
|
1026
|
+
for (let index = 0; index < pointCount; index += 1) {
|
|
1027
|
+
const source = animatedPoint && index === animatedPoint.index ? animatedPoint : { x: visiblePoints.x[index], y: visiblePoints.y[index] };
|
|
1028
|
+
vertices[index * 2] = source.x;
|
|
1029
|
+
vertices[index * 2 + 1] = source.y;
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
var getSinglePointSegment = ({ series, state, yRange, plot }) => {
|
|
1033
|
+
if (series.length !== 1) return null;
|
|
1034
|
+
const x = series.rawX[0];
|
|
1035
|
+
const y = series.rawY[0];
|
|
1036
|
+
if (x < state.xMin || x > state.xMax || y < yRange.minY || y > yRange.maxY) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
const halfWidth = (state.xMax - state.xMin) / Math.max(1, plot.width) * 3;
|
|
1040
|
+
return {
|
|
1041
|
+
x: new Float64Array([x - halfWidth, x + halfWidth]),
|
|
1042
|
+
y: new Float32Array([y, y]),
|
|
1043
|
+
bucketSize: 1,
|
|
1044
|
+
pointCount: 2,
|
|
1045
|
+
endpoint: { x, y }
|
|
1046
|
+
};
|
|
1047
|
+
};
|
|
1048
|
+
var writeAntialiasVertex = (vertices, offset, start, end, side, along) => {
|
|
1049
|
+
vertices[offset] = start.x;
|
|
1050
|
+
vertices[offset + 1] = start.y;
|
|
1051
|
+
vertices[offset + 2] = end.x;
|
|
1052
|
+
vertices[offset + 3] = end.y;
|
|
1053
|
+
vertices[offset + 4] = side;
|
|
1054
|
+
vertices[offset + 5] = along;
|
|
1055
|
+
};
|
|
1056
|
+
var getSeriesPoint = (visiblePoints, animatedPoint, index) => animatedPoint && index === animatedPoint.index ? { x: animatedPoint.x, y: animatedPoint.y } : { x: visiblePoints.x[index], y: visiblePoints.y[index] };
|
|
1057
|
+
var fillAntialiasSeriesSegments = ({
|
|
1058
|
+
vertices,
|
|
1059
|
+
visiblePoints,
|
|
1060
|
+
pointCount,
|
|
1061
|
+
animatedPoint
|
|
1062
|
+
}) => {
|
|
1063
|
+
let offset = 0;
|
|
1064
|
+
for (let index = 0; index < pointCount - 1; index += 1) {
|
|
1065
|
+
const start = getSeriesPoint(visiblePoints, animatedPoint, index);
|
|
1066
|
+
const end = getSeriesPoint(visiblePoints, animatedPoint, index + 1);
|
|
1067
|
+
[
|
|
1068
|
+
[-1, 0],
|
|
1069
|
+
[1, 0],
|
|
1070
|
+
[-1, 1],
|
|
1071
|
+
[-1, 1],
|
|
1072
|
+
[1, 0],
|
|
1073
|
+
[1, 1]
|
|
1074
|
+
].forEach(([side, along]) => {
|
|
1075
|
+
writeAntialiasVertex(vertices, offset, start, end, side, along);
|
|
1076
|
+
offset += AA_VERTEX_FLOAT_STRIDE;
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
return offset / AA_VERTEX_FLOAT_STRIDE;
|
|
1080
|
+
};
|
|
1081
|
+
var projectDataToPixel = ({ x, y, state, yRange, plot }) => ({
|
|
1082
|
+
x: plot.x + (x - state.xMin) / (state.xMax - state.xMin) * plot.width,
|
|
1083
|
+
y: plot.y + (yRange.maxY - y) / (yRange.maxY - yRange.minY) * plot.height
|
|
1084
|
+
});
|
|
1085
|
+
var fillDashedSeriesSegments = ({
|
|
1086
|
+
vertices,
|
|
1087
|
+
visiblePoints,
|
|
1088
|
+
pointCount,
|
|
1089
|
+
state,
|
|
1090
|
+
yRange,
|
|
1091
|
+
plot
|
|
1092
|
+
}) => {
|
|
1093
|
+
let offset = 0;
|
|
1094
|
+
let distance = 0;
|
|
1095
|
+
const dashTotal = DASH_LENGTH_PX + DASH_GAP_PX;
|
|
1096
|
+
for (let index = 0; index < pointCount - 1; index += 1) {
|
|
1097
|
+
const start = { x: visiblePoints.x[index], y: visiblePoints.y[index] };
|
|
1098
|
+
const end = {
|
|
1099
|
+
x: visiblePoints.x[index + 1],
|
|
1100
|
+
y: visiblePoints.y[index + 1]
|
|
1101
|
+
};
|
|
1102
|
+
const startPx = projectDataToPixel({ ...start, state, yRange, plot });
|
|
1103
|
+
const endPx = projectDataToPixel({ ...end, state, yRange, plot });
|
|
1104
|
+
const pixelLength = Math.hypot(endPx.x - startPx.x, endPx.y - startPx.y);
|
|
1105
|
+
if (!Number.isFinite(pixelLength) || pixelLength <= 0) continue;
|
|
1106
|
+
let consumed = 0;
|
|
1107
|
+
while (consumed < pixelLength) {
|
|
1108
|
+
const phase = distance % dashTotal;
|
|
1109
|
+
const boundary = phase < DASH_LENGTH_PX ? DASH_LENGTH_PX - phase : dashTotal - phase;
|
|
1110
|
+
const step = Math.min(boundary, pixelLength - consumed);
|
|
1111
|
+
if (phase < DASH_LENGTH_PX) {
|
|
1112
|
+
if (offset + 4 > vertices.length) return offset / 2;
|
|
1113
|
+
const t0 = consumed / pixelLength;
|
|
1114
|
+
const t1 = (consumed + step) / pixelLength;
|
|
1115
|
+
vertices[offset] = start.x + (end.x - start.x) * t0;
|
|
1116
|
+
vertices[offset + 1] = start.y + (end.y - start.y) * t0;
|
|
1117
|
+
vertices[offset + 2] = start.x + (end.x - start.x) * t1;
|
|
1118
|
+
vertices[offset + 3] = start.y + (end.y - start.y) * t1;
|
|
1119
|
+
offset += 4;
|
|
1120
|
+
}
|
|
1121
|
+
consumed += step;
|
|
1122
|
+
distance += step;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return offset / 2;
|
|
1126
|
+
};
|
|
1127
|
+
var normalizeMovingAverage = (movingAverage) => {
|
|
1128
|
+
if (!movingAverage?.enabled) return null;
|
|
1129
|
+
return {
|
|
1130
|
+
enabled: true,
|
|
1131
|
+
period: Number.isFinite(Number(movingAverage.period)) ? Math.max(1, Math.round(Number(movingAverage.period))) : 21,
|
|
1132
|
+
type: movingAverage.type === "sma" ? "sma" : "ema"
|
|
1133
|
+
};
|
|
1134
|
+
};
|
|
1135
|
+
var calculateMovingAverageChunk = ({
|
|
1136
|
+
sourceY,
|
|
1137
|
+
startIndex,
|
|
1138
|
+
period,
|
|
1139
|
+
type,
|
|
1140
|
+
previousAverage
|
|
1141
|
+
}) => {
|
|
1142
|
+
const out = new Float32Array(Math.max(0, sourceY.length - startIndex));
|
|
1143
|
+
if (!out.length) return out;
|
|
1144
|
+
if (type === "sma") {
|
|
1145
|
+
let sum = 0;
|
|
1146
|
+
const firstWindowStart = Math.max(0, startIndex - period + 1);
|
|
1147
|
+
for (let index = firstWindowStart; index < startIndex; index += 1) {
|
|
1148
|
+
sum += sourceY[index];
|
|
1149
|
+
}
|
|
1150
|
+
for (let index = startIndex; index < sourceY.length; index += 1) {
|
|
1151
|
+
sum += sourceY[index];
|
|
1152
|
+
const removeIndex = index - period;
|
|
1153
|
+
if (removeIndex >= firstWindowStart) sum -= sourceY[removeIndex];
|
|
1154
|
+
out[index - startIndex] = sum / Math.min(period, index + 1);
|
|
1155
|
+
}
|
|
1156
|
+
return out;
|
|
1157
|
+
}
|
|
1158
|
+
const multiplier = 2 / (period + 1);
|
|
1159
|
+
let previous = Number.isFinite(previousAverage) ? previousAverage : sourceY[startIndex];
|
|
1160
|
+
for (let index = startIndex; index < sourceY.length; index += 1) {
|
|
1161
|
+
previous = sourceY[index] * multiplier + previous * (1 - multiplier);
|
|
1162
|
+
out[index - startIndex] = previous;
|
|
1163
|
+
}
|
|
1164
|
+
return out;
|
|
1165
|
+
};
|
|
1166
|
+
var getMovingAverageSeriesForChart = ({
|
|
1167
|
+
chart,
|
|
1168
|
+
movingAverage,
|
|
1169
|
+
cache
|
|
1170
|
+
}) => {
|
|
1171
|
+
const normalized = normalizeMovingAverage(movingAverage);
|
|
1172
|
+
if (!normalized) return [];
|
|
1173
|
+
return chart.series.map((series) => {
|
|
1174
|
+
if (!series.length) return null;
|
|
1175
|
+
const key = `${series.id}::ma:${normalized.type}:${normalized.period}`;
|
|
1176
|
+
const cached = cache.get(key);
|
|
1177
|
+
if (cached && cached.sourceSeries === series && cached.sourceLength <= series.length) {
|
|
1178
|
+
const appendStart = cached.sourceLength;
|
|
1179
|
+
if (appendStart < series.length) {
|
|
1180
|
+
const appendedY = calculateMovingAverageChunk({
|
|
1181
|
+
sourceY: series.rawY.subarray(0, series.length),
|
|
1182
|
+
startIndex: appendStart,
|
|
1183
|
+
period: normalized.period,
|
|
1184
|
+
type: normalized.type,
|
|
1185
|
+
previousAverage: appendStart > 0 ? cached.series.rawY[appendStart - 1] : void 0
|
|
1186
|
+
});
|
|
1187
|
+
cached.series.append(
|
|
1188
|
+
series.rawX.subarray(appendStart, series.length),
|
|
1189
|
+
appendedY
|
|
1190
|
+
);
|
|
1191
|
+
cached.sourceLength = series.length;
|
|
1192
|
+
}
|
|
1193
|
+
cached.series.color = series.color;
|
|
1194
|
+
cached.series.name = `${series.name} ${normalized.type.toUpperCase()} ${normalized.period}`;
|
|
1195
|
+
return cached.series;
|
|
1196
|
+
}
|
|
1197
|
+
const movingY = calculateMovingAverageChunk({
|
|
1198
|
+
sourceY: series.rawY.subarray(0, series.length),
|
|
1199
|
+
startIndex: 0,
|
|
1200
|
+
period: normalized.period,
|
|
1201
|
+
type: normalized.type
|
|
1202
|
+
});
|
|
1203
|
+
const movingSeries = createLineSeries({
|
|
1204
|
+
id: key,
|
|
1205
|
+
name: `${series.name} ${normalized.type.toUpperCase()} ${normalized.period}`,
|
|
1206
|
+
color: series.color,
|
|
1207
|
+
x: series.rawX.subarray(0, series.length),
|
|
1208
|
+
y: movingY
|
|
1209
|
+
});
|
|
1210
|
+
cache.set(key, {
|
|
1211
|
+
sourceSeries: series,
|
|
1212
|
+
sourceLength: series.length,
|
|
1213
|
+
series: movingSeries
|
|
1214
|
+
});
|
|
1215
|
+
return movingSeries;
|
|
1216
|
+
}).filter(Boolean);
|
|
1217
|
+
};
|
|
1218
|
+
var getLineLocations = (gl, program) => Object.freeze({
|
|
1219
|
+
aXy: gl.getAttribLocation(program, "a_xy"),
|
|
1220
|
+
uColor: gl.getUniformLocation(program, "u_color"),
|
|
1221
|
+
uRect: gl.getUniformLocation(program, "u_rect"),
|
|
1222
|
+
uResolution: gl.getUniformLocation(program, "u_resolution"),
|
|
1223
|
+
uXRange: gl.getUniformLocation(program, "u_xRange"),
|
|
1224
|
+
uYRange: gl.getUniformLocation(program, "u_yRange")
|
|
1225
|
+
});
|
|
1226
|
+
var createLineRenderer = (gl) => {
|
|
1227
|
+
const program = createProgram(
|
|
1228
|
+
gl,
|
|
1229
|
+
LINE_VERTEX_SOURCE,
|
|
1230
|
+
LINE_FRAGMENT_SOURCE
|
|
1231
|
+
);
|
|
1232
|
+
const antialiasProgram = createProgram(
|
|
1233
|
+
gl,
|
|
1234
|
+
AA_VERTEX_SOURCE,
|
|
1235
|
+
AA_FRAGMENT_SOURCE
|
|
1236
|
+
);
|
|
1237
|
+
const locations = getLineLocations(gl, program);
|
|
1238
|
+
const aaLocations = Object.freeze({
|
|
1239
|
+
aAlong: gl.getAttribLocation(antialiasProgram, "a_along"),
|
|
1240
|
+
aEnd: gl.getAttribLocation(antialiasProgram, "a_end"),
|
|
1241
|
+
aSide: gl.getAttribLocation(antialiasProgram, "a_side"),
|
|
1242
|
+
aStart: gl.getAttribLocation(antialiasProgram, "a_start"),
|
|
1243
|
+
uColor: gl.getUniformLocation(antialiasProgram, "u_color"),
|
|
1244
|
+
uEdgeWidth: gl.getUniformLocation(antialiasProgram, "u_edgeWidth"),
|
|
1245
|
+
uLineHalfWidth: gl.getUniformLocation(antialiasProgram, "u_lineHalfWidth"),
|
|
1246
|
+
uRect: gl.getUniformLocation(antialiasProgram, "u_rect"),
|
|
1247
|
+
uResolution: gl.getUniformLocation(antialiasProgram, "u_resolution"),
|
|
1248
|
+
uXRange: gl.getUniformLocation(antialiasProgram, "u_xRange"),
|
|
1249
|
+
uYRange: gl.getUniformLocation(antialiasProgram, "u_yRange")
|
|
1250
|
+
});
|
|
1251
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
1252
|
+
const antialiasBuffers = /* @__PURE__ */ new Map();
|
|
1253
|
+
const movingAverageCache = /* @__PURE__ */ new Map();
|
|
1254
|
+
const setCommonUniforms = (activeLocations, pixelWidth, pixelHeight, scaledPlot, state, yRange) => {
|
|
1255
|
+
gl.uniform2f(activeLocations.uResolution, pixelWidth, pixelHeight);
|
|
1256
|
+
gl.uniform4f(
|
|
1257
|
+
activeLocations.uRect,
|
|
1258
|
+
scaledPlot.x,
|
|
1259
|
+
scaledPlot.y,
|
|
1260
|
+
scaledPlot.width,
|
|
1261
|
+
scaledPlot.height
|
|
1262
|
+
);
|
|
1263
|
+
gl.uniform2f(activeLocations.uXRange, state.xMin, state.xMax);
|
|
1264
|
+
gl.uniform2f(activeLocations.uYRange, yRange.minY, yRange.maxY);
|
|
1265
|
+
};
|
|
1266
|
+
return {
|
|
1267
|
+
draw({
|
|
1268
|
+
antialiasLines,
|
|
1269
|
+
chart,
|
|
1270
|
+
descriptor,
|
|
1271
|
+
dpr,
|
|
1272
|
+
getAppendAnimatedPoint,
|
|
1273
|
+
movingAverage,
|
|
1274
|
+
now,
|
|
1275
|
+
pixelHeight,
|
|
1276
|
+
pixelWidth,
|
|
1277
|
+
plot,
|
|
1278
|
+
scaledPlot,
|
|
1279
|
+
seriesOrderByChart,
|
|
1280
|
+
state,
|
|
1281
|
+
yRange
|
|
1282
|
+
}) {
|
|
1283
|
+
const useAntialias = Boolean(antialiasLines);
|
|
1284
|
+
const activeProgram = useAntialias ? antialiasProgram : program;
|
|
1285
|
+
const activeLocations = useAntialias ? aaLocations : locations;
|
|
1286
|
+
gl.useProgram(activeProgram);
|
|
1287
|
+
setCommonUniforms(
|
|
1288
|
+
activeLocations,
|
|
1289
|
+
pixelWidth,
|
|
1290
|
+
pixelHeight,
|
|
1291
|
+
scaledPlot,
|
|
1292
|
+
state,
|
|
1293
|
+
yRange
|
|
1294
|
+
);
|
|
1295
|
+
if (useAntialias) {
|
|
1296
|
+
gl.enable(gl.BLEND);
|
|
1297
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1298
|
+
gl.enableVertexAttribArray(aaLocations.aStart);
|
|
1299
|
+
gl.enableVertexAttribArray(aaLocations.aEnd);
|
|
1300
|
+
gl.enableVertexAttribArray(aaLocations.aSide);
|
|
1301
|
+
gl.enableVertexAttribArray(aaLocations.aAlong);
|
|
1302
|
+
gl.uniform1f(
|
|
1303
|
+
aaLocations.uLineHalfWidth,
|
|
1304
|
+
AA_LINE_WIDTH_PX * dpr / 2
|
|
1305
|
+
);
|
|
1306
|
+
gl.uniform1f(aaLocations.uEdgeWidth, AA_EDGE_WIDTH_PX * dpr);
|
|
1307
|
+
} else {
|
|
1308
|
+
gl.disable(gl.BLEND);
|
|
1309
|
+
gl.enableVertexAttribArray(locations.aXy);
|
|
1310
|
+
}
|
|
1311
|
+
const seriesEndpoints = /* @__PURE__ */ new Map();
|
|
1312
|
+
const hideBaseSeries = descriptor.capabilities.movingAverage && Boolean(movingAverage?.enabled) && Boolean(movingAverage?.hideBase);
|
|
1313
|
+
if (!hideBaseSeries) {
|
|
1314
|
+
getOrderedSeries(chart, seriesOrderByChart).forEach((series) => {
|
|
1315
|
+
let visiblePoints = series.getVisiblePoints(
|
|
1316
|
+
state.xMin,
|
|
1317
|
+
state.xMax,
|
|
1318
|
+
plot.width
|
|
1319
|
+
);
|
|
1320
|
+
let singlePointEndpoint = null;
|
|
1321
|
+
if (visiblePoints.pointCount < 2) {
|
|
1322
|
+
const singlePointSegment = getSinglePointSegment({
|
|
1323
|
+
series,
|
|
1324
|
+
state,
|
|
1325
|
+
yRange,
|
|
1326
|
+
plot
|
|
1327
|
+
});
|
|
1328
|
+
if (!singlePointSegment) return;
|
|
1329
|
+
visiblePoints = singlePointSegment;
|
|
1330
|
+
singlePointEndpoint = singlePointSegment.endpoint;
|
|
1331
|
+
}
|
|
1332
|
+
const animatedPoint = singlePointEndpoint ? null : getAppendAnimatedPoint({
|
|
1333
|
+
seriesId: series.id,
|
|
1334
|
+
visiblePoints,
|
|
1335
|
+
now
|
|
1336
|
+
});
|
|
1337
|
+
const pointCount = animatedPoint?.pointCount ?? visiblePoints.pointCount;
|
|
1338
|
+
if (animatedPoint) {
|
|
1339
|
+
seriesEndpoints.set(series.id, {
|
|
1340
|
+
x: animatedPoint.x,
|
|
1341
|
+
y: animatedPoint.y
|
|
1342
|
+
});
|
|
1343
|
+
} else if (singlePointEndpoint) {
|
|
1344
|
+
seriesEndpoints.set(series.id, singlePointEndpoint);
|
|
1345
|
+
}
|
|
1346
|
+
const [r, g, b] = hexToRgb(series.color);
|
|
1347
|
+
gl.uniform3f(activeLocations.uColor, r, g, b);
|
|
1348
|
+
if (useAntialias) {
|
|
1349
|
+
const requiredFloats = Math.max(0, pointCount - 1) * 6 * AA_VERTEX_FLOAT_STRIDE;
|
|
1350
|
+
const entry2 = getDynamicBuffer(
|
|
1351
|
+
gl,
|
|
1352
|
+
antialiasBuffers,
|
|
1353
|
+
series.id,
|
|
1354
|
+
requiredFloats
|
|
1355
|
+
);
|
|
1356
|
+
const drawVertexCount = fillAntialiasSeriesSegments({
|
|
1357
|
+
vertices: entry2.vertices,
|
|
1358
|
+
visiblePoints,
|
|
1359
|
+
pointCount,
|
|
1360
|
+
animatedPoint
|
|
1361
|
+
});
|
|
1362
|
+
const stride = AA_VERTEX_FLOAT_STRIDE * Float32Array.BYTES_PER_ELEMENT;
|
|
1363
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, entry2.buffer);
|
|
1364
|
+
gl.vertexAttribPointer(
|
|
1365
|
+
aaLocations.aStart,
|
|
1366
|
+
2,
|
|
1367
|
+
gl.FLOAT,
|
|
1368
|
+
false,
|
|
1369
|
+
stride,
|
|
1370
|
+
0
|
|
1371
|
+
);
|
|
1372
|
+
gl.vertexAttribPointer(
|
|
1373
|
+
aaLocations.aEnd,
|
|
1374
|
+
2,
|
|
1375
|
+
gl.FLOAT,
|
|
1376
|
+
false,
|
|
1377
|
+
stride,
|
|
1378
|
+
2 * Float32Array.BYTES_PER_ELEMENT
|
|
1379
|
+
);
|
|
1380
|
+
gl.vertexAttribPointer(
|
|
1381
|
+
aaLocations.aSide,
|
|
1382
|
+
1,
|
|
1383
|
+
gl.FLOAT,
|
|
1384
|
+
false,
|
|
1385
|
+
stride,
|
|
1386
|
+
4 * Float32Array.BYTES_PER_ELEMENT
|
|
1387
|
+
);
|
|
1388
|
+
gl.vertexAttribPointer(
|
|
1389
|
+
aaLocations.aAlong,
|
|
1390
|
+
1,
|
|
1391
|
+
gl.FLOAT,
|
|
1392
|
+
false,
|
|
1393
|
+
stride,
|
|
1394
|
+
5 * Float32Array.BYTES_PER_ELEMENT
|
|
1395
|
+
);
|
|
1396
|
+
gl.bufferSubData(
|
|
1397
|
+
gl.ARRAY_BUFFER,
|
|
1398
|
+
0,
|
|
1399
|
+
entry2.vertices,
|
|
1400
|
+
0,
|
|
1401
|
+
drawVertexCount * AA_VERTEX_FLOAT_STRIDE
|
|
1402
|
+
);
|
|
1403
|
+
gl.drawArrays(gl.TRIANGLES, 0, drawVertexCount);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const entry = getDynamicBuffer(
|
|
1407
|
+
gl,
|
|
1408
|
+
buffers,
|
|
1409
|
+
series.id,
|
|
1410
|
+
pointCount * 2
|
|
1411
|
+
);
|
|
1412
|
+
fillSeriesPoints({
|
|
1413
|
+
vertices: entry.vertices,
|
|
1414
|
+
visiblePoints,
|
|
1415
|
+
pointCount,
|
|
1416
|
+
animatedPoint
|
|
1417
|
+
});
|
|
1418
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
|
|
1419
|
+
gl.vertexAttribPointer(locations.aXy, 2, gl.FLOAT, false, 0, 0);
|
|
1420
|
+
gl.bufferSubData(
|
|
1421
|
+
gl.ARRAY_BUFFER,
|
|
1422
|
+
0,
|
|
1423
|
+
entry.vertices,
|
|
1424
|
+
0,
|
|
1425
|
+
pointCount * 2
|
|
1426
|
+
);
|
|
1427
|
+
gl.drawArrays(gl.LINE_STRIP, 0, pointCount);
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
const movingSeries = getMovingAverageSeriesForChart({
|
|
1431
|
+
chart,
|
|
1432
|
+
movingAverage: descriptor.capabilities.movingAverage ? movingAverage : null,
|
|
1433
|
+
cache: movingAverageCache
|
|
1434
|
+
});
|
|
1435
|
+
if (movingSeries.length) {
|
|
1436
|
+
gl.disable(gl.BLEND);
|
|
1437
|
+
gl.useProgram(program);
|
|
1438
|
+
gl.enableVertexAttribArray(locations.aXy);
|
|
1439
|
+
setCommonUniforms(
|
|
1440
|
+
locations,
|
|
1441
|
+
pixelWidth,
|
|
1442
|
+
pixelHeight,
|
|
1443
|
+
scaledPlot,
|
|
1444
|
+
state,
|
|
1445
|
+
yRange
|
|
1446
|
+
);
|
|
1447
|
+
getOrderedSeries(
|
|
1448
|
+
{ ...chart, series: movingSeries },
|
|
1449
|
+
seriesOrderByChart
|
|
1450
|
+
).forEach((series) => {
|
|
1451
|
+
const visiblePoints = series.getVisiblePoints(
|
|
1452
|
+
state.xMin,
|
|
1453
|
+
state.xMax,
|
|
1454
|
+
plot.width
|
|
1455
|
+
);
|
|
1456
|
+
if (visiblePoints.pointCount < 2) return;
|
|
1457
|
+
const entry = getDynamicBuffer(
|
|
1458
|
+
gl,
|
|
1459
|
+
buffers,
|
|
1460
|
+
series.id,
|
|
1461
|
+
visiblePoints.pointCount * 8 * 2
|
|
1462
|
+
);
|
|
1463
|
+
const drawVertexCount = fillDashedSeriesSegments({
|
|
1464
|
+
vertices: entry.vertices,
|
|
1465
|
+
visiblePoints,
|
|
1466
|
+
pointCount: visiblePoints.pointCount,
|
|
1467
|
+
state,
|
|
1468
|
+
yRange,
|
|
1469
|
+
plot
|
|
1470
|
+
});
|
|
1471
|
+
if (drawVertexCount < 2) return;
|
|
1472
|
+
const [r, g, b] = hexToRgb(series.color);
|
|
1473
|
+
gl.uniform3f(locations.uColor, r, g, b);
|
|
1474
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, entry.buffer);
|
|
1475
|
+
gl.vertexAttribPointer(locations.aXy, 2, gl.FLOAT, false, 0, 0);
|
|
1476
|
+
gl.bufferSubData(
|
|
1477
|
+
gl.ARRAY_BUFFER,
|
|
1478
|
+
0,
|
|
1479
|
+
entry.vertices,
|
|
1480
|
+
0,
|
|
1481
|
+
drawVertexCount * 2
|
|
1482
|
+
);
|
|
1483
|
+
gl.drawArrays(gl.LINES, 0, drawVertexCount);
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
gl.disable(gl.BLEND);
|
|
1487
|
+
return { seriesEndpoints };
|
|
1488
|
+
},
|
|
1489
|
+
destroy() {
|
|
1490
|
+
deleteBufferCache(gl, buffers);
|
|
1491
|
+
deleteBufferCache(gl, antialiasBuffers);
|
|
1492
|
+
movingAverageCache.clear();
|
|
1493
|
+
gl.deleteProgram(program);
|
|
1494
|
+
gl.deleteProgram(antialiasProgram);
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
// src/core/renderers/index.js
|
|
1500
|
+
var RENDERER_FACTORIES = Object.freeze({
|
|
1501
|
+
bar: createBarRenderer,
|
|
1502
|
+
line: createLineRenderer
|
|
1503
|
+
});
|
|
1504
|
+
var createRendererRegistry = (gl) => {
|
|
1505
|
+
const renderers = new Map(
|
|
1506
|
+
Object.entries(RENDERER_FACTORIES).map(([type, createRenderer]) => [
|
|
1507
|
+
type,
|
|
1508
|
+
createRenderer(gl)
|
|
1509
|
+
])
|
|
1510
|
+
);
|
|
1511
|
+
return {
|
|
1512
|
+
draw(type, payload) {
|
|
1513
|
+
const renderer = renderers.get(type);
|
|
1514
|
+
if (!renderer) {
|
|
1515
|
+
throw new TypeError(`No AlienCharts renderer registered for "${type}"`);
|
|
1516
|
+
}
|
|
1517
|
+
return renderer.draw(payload);
|
|
1518
|
+
},
|
|
1519
|
+
getTooltipCategory(type, payload) {
|
|
1520
|
+
const renderer = renderers.get(type);
|
|
1521
|
+
if (!renderer) {
|
|
1522
|
+
throw new TypeError(`No AlienCharts renderer registered for "${type}"`);
|
|
1523
|
+
}
|
|
1524
|
+
return renderer.getTooltipCategory?.(payload) ?? payload.visiblePoints.x[payload.pointIndex];
|
|
1525
|
+
},
|
|
1526
|
+
destroy() {
|
|
1527
|
+
renderers.forEach((renderer) => renderer.destroy());
|
|
1528
|
+
renderers.clear();
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
// src/core/chartRenderer.js
|
|
1534
|
+
var CHART_HEIGHT = 360;
|
|
1535
|
+
var RIGHT_AXIS_WIDTH = 58;
|
|
1536
|
+
var PLOT_PADDING = Object.freeze({
|
|
1537
|
+
left: 38,
|
|
1538
|
+
right: RIGHT_AXIS_WIDTH,
|
|
1539
|
+
top: 34,
|
|
1540
|
+
bottom: 28
|
|
1541
|
+
});
|
|
1542
|
+
var HORIZONTAL_PLOT_PADDING = Object.freeze({
|
|
1543
|
+
left: RIGHT_AXIS_WIDTH,
|
|
1544
|
+
right: 20,
|
|
1545
|
+
top: 34,
|
|
1546
|
+
bottom: 28
|
|
1547
|
+
});
|
|
1548
|
+
var CATEGORICAL_PLOT_PADDING = Object.freeze({
|
|
1549
|
+
...PLOT_PADDING,
|
|
1550
|
+
bottom: 42
|
|
1551
|
+
});
|
|
1552
|
+
var CATEGORICAL_HORIZONTAL_PLOT_PADDING = Object.freeze({
|
|
1553
|
+
...HORIZONTAL_PLOT_PADDING,
|
|
1554
|
+
left: 128
|
|
1555
|
+
});
|
|
1556
|
+
var Y_SCALE_MIN = 0.05;
|
|
1557
|
+
var Y_SCALE_MAX = 40;
|
|
1558
|
+
var Y_AXIS_TICK_COUNT = 5;
|
|
1559
|
+
var X_AXIS_TICK_COUNT = 5;
|
|
1560
|
+
var X_SCALE_MIN_SPAN = 10;
|
|
1561
|
+
var X_SCALE_MAX_SPAN = 1e9;
|
|
1562
|
+
var JUMP_LATEST_RIGHT_PADDING_RATIO = 0.1;
|
|
1563
|
+
var DEFAULT_CHART_BACKGROUND = "#f5f9ff";
|
|
1564
|
+
var getChartOrientation = (chart, descriptor) => (descriptor || resolveChartDescriptor(chart)).orientation;
|
|
1565
|
+
var getPlotPadding = (chart, descriptor) => {
|
|
1566
|
+
const categorical = getChartCategories(chart).length > 0;
|
|
1567
|
+
if (getChartOrientation(chart, descriptor) === "horizontal") {
|
|
1568
|
+
return categorical ? CATEGORICAL_HORIZONTAL_PLOT_PADDING : HORIZONTAL_PLOT_PADDING;
|
|
1569
|
+
}
|
|
1570
|
+
return categorical ? CATEGORICAL_PLOT_PADDING : PLOT_PADDING;
|
|
1571
|
+
};
|
|
1572
|
+
var getCategoryPixelLength = (chart, plot, descriptor) => getChartOrientation(chart, descriptor) === "horizontal" ? plot.height : plot.width;
|
|
1573
|
+
var getReadableTextColor = (backgroundColor) => {
|
|
1574
|
+
const [r, g, b] = hexToRgb(backgroundColor);
|
|
1575
|
+
const toLinear = (value) => value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
1576
|
+
const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
1577
|
+
return luminance > 0.45 ? "#111827" : "#ffffff";
|
|
1578
|
+
};
|
|
1579
|
+
var getSeriesLabelTextColor = (series) => {
|
|
1580
|
+
if (series.__labelTextColorFor !== series.color) {
|
|
1581
|
+
series.__labelTextColorFor = series.color;
|
|
1582
|
+
series.__labelTextColor = getReadableTextColor(series.color);
|
|
1583
|
+
}
|
|
1584
|
+
return series.__labelTextColor;
|
|
1585
|
+
};
|
|
1586
|
+
var lowerBound2 = (array, value) => {
|
|
1587
|
+
let low = 0;
|
|
1588
|
+
let high = array.length;
|
|
1589
|
+
while (low < high) {
|
|
1590
|
+
const middle = low + high >> 1;
|
|
1591
|
+
if (array[middle] < value) low = middle + 1;
|
|
1592
|
+
else high = middle;
|
|
1593
|
+
}
|
|
1594
|
+
return low;
|
|
1595
|
+
};
|
|
1596
|
+
var getNearestPointIndex = (xValues, xValue) => {
|
|
1597
|
+
if (!xValues.length) return -1;
|
|
1598
|
+
const nextIndex = lowerBound2(xValues, xValue);
|
|
1599
|
+
if (nextIndex <= 0) return 0;
|
|
1600
|
+
if (nextIndex >= xValues.length) return xValues.length - 1;
|
|
1601
|
+
const previousIndex = nextIndex - 1;
|
|
1602
|
+
return Math.abs(xValues[nextIndex] - xValue) < Math.abs(xValue - xValues[previousIndex]) ? nextIndex : previousIndex;
|
|
1603
|
+
};
|
|
1604
|
+
var getChartXBounds = (chart) => {
|
|
1605
|
+
let minX = Infinity;
|
|
1606
|
+
let maxX = -Infinity;
|
|
1607
|
+
chart.series.forEach((series) => {
|
|
1608
|
+
if (!series.length) return;
|
|
1609
|
+
minX = Math.min(minX, series.rawX[0]);
|
|
1610
|
+
maxX = Math.max(maxX, series.rawX[series.length - 1]);
|
|
1611
|
+
});
|
|
1612
|
+
return { minX, maxX };
|
|
1613
|
+
};
|
|
1614
|
+
var getMinimumCategorySpan = (chart) => {
|
|
1615
|
+
const categories = getChartCategories(chart).map((category) => category.value).sort((left, right) => left - right);
|
|
1616
|
+
if (!categories.length) return X_SCALE_MIN_SPAN;
|
|
1617
|
+
let minimumGap = Infinity;
|
|
1618
|
+
for (let index = 1; index < categories.length; index += 1) {
|
|
1619
|
+
minimumGap = Math.min(minimumGap, categories[index] - categories[index - 1]);
|
|
1620
|
+
}
|
|
1621
|
+
return Number.isFinite(minimumGap) ? minimumGap : 1;
|
|
1622
|
+
};
|
|
1623
|
+
var getInitialView = (chart, initialVisiblePoints = null) => {
|
|
1624
|
+
const categories = getChartCategories(chart).map((category) => category.value).sort((left, right) => left - right);
|
|
1625
|
+
if (categories.length) {
|
|
1626
|
+
const requestedCount = Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0 ? Math.max(1, Math.floor(initialVisiblePoints)) : categories.length;
|
|
1627
|
+
const visible = categories.slice(-requestedCount);
|
|
1628
|
+
const first = visible[0];
|
|
1629
|
+
const last = visible[visible.length - 1];
|
|
1630
|
+
const firstIndex = categories.indexOf(first);
|
|
1631
|
+
const lastIndex = categories.lastIndexOf(last);
|
|
1632
|
+
const leftGap = firstIndex > 0 ? first - categories[firstIndex - 1] : visible[1] - first || 1;
|
|
1633
|
+
const rightGap = lastIndex + 1 < categories.length ? categories[lastIndex + 1] - last : last - visible[visible.length - 2] || leftGap;
|
|
1634
|
+
return {
|
|
1635
|
+
xMin: first - leftGap / 2,
|
|
1636
|
+
xMax: last + rightGap / 2
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
const { minX, maxX } = getChartXBounds(chart);
|
|
1640
|
+
if (!Number.isFinite(minX) || !Number.isFinite(maxX)) {
|
|
1641
|
+
return { xMin: 0, xMax: 1 };
|
|
1642
|
+
}
|
|
1643
|
+
if (minX === maxX) {
|
|
1644
|
+
const span2 = Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0 ? Math.max(10, initialVisiblePoints) : 10;
|
|
1645
|
+
const nextMax = maxX + span2 * JUMP_LATEST_RIGHT_PADDING_RATIO;
|
|
1646
|
+
return { xMin: nextMax - span2, xMax: nextMax };
|
|
1647
|
+
}
|
|
1648
|
+
let span = maxX - minX;
|
|
1649
|
+
if (Number.isFinite(initialVisiblePoints) && initialVisiblePoints > 0) {
|
|
1650
|
+
span = Math.min(span, initialVisiblePoints);
|
|
1651
|
+
const nextMax = maxX + span * JUMP_LATEST_RIGHT_PADDING_RATIO;
|
|
1652
|
+
return { xMin: nextMax - span, xMax: nextMax };
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
xMin: minX,
|
|
1656
|
+
xMax: maxX + span * JUMP_LATEST_RIGHT_PADDING_RATIO
|
|
1657
|
+
};
|
|
1658
|
+
};
|
|
1659
|
+
var getYRange = (chart, xMin, xMax, width, suppliedDescriptor) => {
|
|
1660
|
+
const descriptor = suppliedDescriptor || resolveChartDescriptor(chart);
|
|
1661
|
+
let minY = Infinity;
|
|
1662
|
+
let maxY = -Infinity;
|
|
1663
|
+
let renderedPoints = 0;
|
|
1664
|
+
let bucketSize = 1;
|
|
1665
|
+
chart.series.forEach((series) => {
|
|
1666
|
+
const visible = series.getVisiblePoints(xMin, xMax, width);
|
|
1667
|
+
renderedPoints += visible.pointCount;
|
|
1668
|
+
bucketSize = Math.max(bucketSize, visible.bucketSize);
|
|
1669
|
+
for (let index = 0; index < visible.y.length; index += 1) {
|
|
1670
|
+
minY = Math.min(minY, visible.y[index]);
|
|
1671
|
+
maxY = Math.max(maxY, visible.y[index]);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
const fixedMinY = Number(chart.yRange?.min);
|
|
1675
|
+
const fixedMaxY = Number(chart.yRange?.max);
|
|
1676
|
+
if (Number.isFinite(fixedMinY) && Number.isFinite(fixedMaxY) && fixedMinY < fixedMaxY) {
|
|
1677
|
+
return {
|
|
1678
|
+
minY: fixedMinY,
|
|
1679
|
+
maxY: fixedMaxY,
|
|
1680
|
+
renderedPoints,
|
|
1681
|
+
bucketSize
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {
|
|
1685
|
+
return { minY: 0, maxY: 1, renderedPoints, bucketSize };
|
|
1686
|
+
}
|
|
1687
|
+
if (descriptor.rangeIncludesZero) {
|
|
1688
|
+
if (minY >= 0) {
|
|
1689
|
+
return {
|
|
1690
|
+
minY: 0,
|
|
1691
|
+
maxY: maxY === 0 ? 1 : maxY * 1.08,
|
|
1692
|
+
renderedPoints,
|
|
1693
|
+
bucketSize
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
if (maxY <= 0) {
|
|
1697
|
+
return {
|
|
1698
|
+
minY: minY === 0 ? -1 : minY * 1.08,
|
|
1699
|
+
maxY: 0,
|
|
1700
|
+
renderedPoints,
|
|
1701
|
+
bucketSize
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (minY === maxY) {
|
|
1706
|
+
minY -= 1;
|
|
1707
|
+
maxY += 1;
|
|
1708
|
+
}
|
|
1709
|
+
const padding = (maxY - minY) * 0.08;
|
|
1710
|
+
return {
|
|
1711
|
+
minY: minY - padding,
|
|
1712
|
+
maxY: maxY + padding,
|
|
1713
|
+
renderedPoints,
|
|
1714
|
+
bucketSize
|
|
1715
|
+
};
|
|
1716
|
+
};
|
|
1717
|
+
var applyYScale = (yRange, scale = 1, centerOffset = 0) => {
|
|
1718
|
+
const clampedScale = Math.min(Y_SCALE_MAX, Math.max(Y_SCALE_MIN, scale));
|
|
1719
|
+
const center = (yRange.minY + yRange.maxY) / 2 + centerOffset;
|
|
1720
|
+
const halfRange = (yRange.maxY - yRange.minY) * clampedScale / 2;
|
|
1721
|
+
return {
|
|
1722
|
+
...yRange,
|
|
1723
|
+
minY: center - halfRange,
|
|
1724
|
+
maxY: center + halfRange
|
|
1725
|
+
};
|
|
1726
|
+
};
|
|
1727
|
+
var trimFormattedNumber = (value) => value.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
|
|
1728
|
+
var formatNumber = (value) => {
|
|
1729
|
+
if (!Number.isFinite(value)) return "";
|
|
1730
|
+
const absolute = Math.abs(value);
|
|
1731
|
+
if (absolute !== 0 && absolute < 1e-4) return value.toExponential(2);
|
|
1732
|
+
if (absolute < 1) return trimFormattedNumber(value.toPrecision(5));
|
|
1733
|
+
if (absolute >= 1e5) return value.toFixed(0);
|
|
1734
|
+
if (absolute >= 1e3) return trimFormattedNumber(value.toFixed(1));
|
|
1735
|
+
if (absolute >= 10) return trimFormattedNumber(value.toFixed(2));
|
|
1736
|
+
return trimFormattedNumber(value.toFixed(4));
|
|
1737
|
+
};
|
|
1738
|
+
var formatCompactNumber = (value) => {
|
|
1739
|
+
if (!Number.isFinite(value)) return "";
|
|
1740
|
+
const absolute = Math.abs(value);
|
|
1741
|
+
if (absolute >= 1e9) {
|
|
1742
|
+
return `${(value / 1e9).toFixed(
|
|
1743
|
+
absolute >= 1e10 ? 0 : 1
|
|
1744
|
+
)}B`;
|
|
1745
|
+
}
|
|
1746
|
+
if (absolute >= 1e6) {
|
|
1747
|
+
return `${(value / 1e6).toFixed(
|
|
1748
|
+
absolute >= 1e7 ? 0 : 1
|
|
1749
|
+
)}M`;
|
|
1750
|
+
}
|
|
1751
|
+
if (absolute >= 1e3) {
|
|
1752
|
+
return `${(value / 1e3).toFixed(absolute >= 1e4 ? 0 : 1)}k`;
|
|
1753
|
+
}
|
|
1754
|
+
return formatNumber(value);
|
|
1755
|
+
};
|
|
1756
|
+
var normalizeRect = (start, end) => {
|
|
1757
|
+
if (!start || !end) return null;
|
|
1758
|
+
const left = Math.min(start.x, end.x);
|
|
1759
|
+
const top = Math.min(start.y, end.y);
|
|
1760
|
+
return {
|
|
1761
|
+
left,
|
|
1762
|
+
top,
|
|
1763
|
+
width: Math.abs(end.x - start.x),
|
|
1764
|
+
height: Math.abs(end.y - start.y)
|
|
1765
|
+
};
|
|
1766
|
+
};
|
|
1767
|
+
var clampPointToPlot = (point, plot) => ({
|
|
1768
|
+
x: Math.min(plot.x + plot.width, Math.max(plot.x, point.x)),
|
|
1769
|
+
y: Math.min(plot.y + plot.height, Math.max(plot.y, point.y))
|
|
1770
|
+
});
|
|
1771
|
+
var getScaledYRangeForLayout = ({
|
|
1772
|
+
chart,
|
|
1773
|
+
descriptor,
|
|
1774
|
+
state,
|
|
1775
|
+
plot,
|
|
1776
|
+
yScaleRef,
|
|
1777
|
+
yCenterOffsetRef
|
|
1778
|
+
}) => applyYScale(
|
|
1779
|
+
getYRange(
|
|
1780
|
+
chart,
|
|
1781
|
+
state.xMin,
|
|
1782
|
+
state.xMax,
|
|
1783
|
+
getCategoryPixelLength(chart, plot, descriptor),
|
|
1784
|
+
descriptor
|
|
1785
|
+
),
|
|
1786
|
+
yScaleRef.current.get(chart.id) ?? 1,
|
|
1787
|
+
yCenterOffsetRef.current.get(chart.id) ?? 0
|
|
1788
|
+
);
|
|
1789
|
+
var applyRectangleZoom = ({
|
|
1790
|
+
chart,
|
|
1791
|
+
descriptor: suppliedDescriptor,
|
|
1792
|
+
plot,
|
|
1793
|
+
start,
|
|
1794
|
+
end,
|
|
1795
|
+
initialVisiblePoints,
|
|
1796
|
+
viewStateRef,
|
|
1797
|
+
yScaleRef,
|
|
1798
|
+
yCenterOffsetRef,
|
|
1799
|
+
yManualScaleRef
|
|
1800
|
+
}) => {
|
|
1801
|
+
const rect = normalizeRect(
|
|
1802
|
+
clampPointToPlot(start, plot),
|
|
1803
|
+
clampPointToPlot(end, plot)
|
|
1804
|
+
);
|
|
1805
|
+
if (!rect || rect.width < 6 || rect.height < 6) return false;
|
|
1806
|
+
const descriptor = suppliedDescriptor || resolveChartDescriptor(chart);
|
|
1807
|
+
const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
|
|
1808
|
+
const currentYRange = getScaledYRangeForLayout({
|
|
1809
|
+
chart,
|
|
1810
|
+
descriptor,
|
|
1811
|
+
state,
|
|
1812
|
+
plot,
|
|
1813
|
+
yScaleRef,
|
|
1814
|
+
yCenterOffsetRef
|
|
1815
|
+
});
|
|
1816
|
+
const transform = createCoordinateTransform({
|
|
1817
|
+
orientation: descriptor.orientation,
|
|
1818
|
+
categoryRange: { min: state.xMin, max: state.xMax },
|
|
1819
|
+
valueRange: {
|
|
1820
|
+
min: currentYRange.minY,
|
|
1821
|
+
max: currentYRange.maxY
|
|
1822
|
+
},
|
|
1823
|
+
plot
|
|
1824
|
+
});
|
|
1825
|
+
const selected = transform.dataBoundsForRect(rect);
|
|
1826
|
+
if (selected.categoryMax - selected.categoryMin < getMinimumCategorySpan(chart)) {
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
const selectedSpan = selected.valueMax - selected.valueMin;
|
|
1830
|
+
if (!Number.isFinite(selectedSpan) || selectedSpan <= 0) return false;
|
|
1831
|
+
viewStateRef.current.set(chart.id, {
|
|
1832
|
+
xMin: selected.categoryMin,
|
|
1833
|
+
xMax: selected.categoryMax
|
|
1834
|
+
});
|
|
1835
|
+
const baseYRange = getYRange(
|
|
1836
|
+
chart,
|
|
1837
|
+
selected.categoryMin,
|
|
1838
|
+
selected.categoryMax,
|
|
1839
|
+
getCategoryPixelLength(chart, plot, descriptor),
|
|
1840
|
+
descriptor
|
|
1841
|
+
);
|
|
1842
|
+
const baseSpan = baseYRange.maxY - baseYRange.minY;
|
|
1843
|
+
if (Number.isFinite(baseSpan) && baseSpan > 0) {
|
|
1844
|
+
const selectedCenter = (selected.valueMin + selected.valueMax) / 2;
|
|
1845
|
+
const baseCenter = (baseYRange.minY + baseYRange.maxY) / 2;
|
|
1846
|
+
yScaleRef.current.set(
|
|
1847
|
+
chart.id,
|
|
1848
|
+
Math.min(
|
|
1849
|
+
Y_SCALE_MAX,
|
|
1850
|
+
Math.max(Y_SCALE_MIN, selectedSpan / baseSpan)
|
|
1851
|
+
)
|
|
1852
|
+
);
|
|
1853
|
+
yCenterOffsetRef.current.set(chart.id, selectedCenter - baseCenter);
|
|
1854
|
+
yManualScaleRef.current.add(chart.id);
|
|
1855
|
+
}
|
|
1856
|
+
return true;
|
|
1857
|
+
};
|
|
1858
|
+
var screenPointToDataPoint = ({
|
|
1859
|
+
point,
|
|
1860
|
+
chart,
|
|
1861
|
+
descriptor: suppliedDescriptor,
|
|
1862
|
+
plot,
|
|
1863
|
+
initialVisiblePoints,
|
|
1864
|
+
viewStateRef,
|
|
1865
|
+
yScaleRef,
|
|
1866
|
+
yCenterOffsetRef
|
|
1867
|
+
}) => {
|
|
1868
|
+
const descriptor = suppliedDescriptor || resolveChartDescriptor(chart);
|
|
1869
|
+
const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
|
|
1870
|
+
const yRange = getScaledYRangeForLayout({
|
|
1871
|
+
chart,
|
|
1872
|
+
descriptor,
|
|
1873
|
+
state,
|
|
1874
|
+
plot,
|
|
1875
|
+
yScaleRef,
|
|
1876
|
+
yCenterOffsetRef
|
|
1877
|
+
});
|
|
1878
|
+
return createCoordinateTransform({
|
|
1879
|
+
orientation: descriptor.orientation,
|
|
1880
|
+
categoryRange: { min: state.xMin, max: state.xMax },
|
|
1881
|
+
valueRange: { min: yRange.minY, max: yRange.maxY },
|
|
1882
|
+
plot
|
|
1883
|
+
}).screenToData(point);
|
|
1884
|
+
};
|
|
1885
|
+
var createAxisOverlay = ({
|
|
1886
|
+
chart,
|
|
1887
|
+
descriptor,
|
|
1888
|
+
plot,
|
|
1889
|
+
state,
|
|
1890
|
+
yRange,
|
|
1891
|
+
seriesEndpoints,
|
|
1892
|
+
seriesOrderByChart
|
|
1893
|
+
}) => {
|
|
1894
|
+
const horizontal = descriptor.orientation === "horizontal";
|
|
1895
|
+
const transform = createCoordinateTransform({
|
|
1896
|
+
orientation: descriptor.orientation,
|
|
1897
|
+
categoryRange: { min: state.xMin, max: state.xMax },
|
|
1898
|
+
valueRange: { min: yRange.minY, max: yRange.maxY },
|
|
1899
|
+
plot: { x: 0, y: 0, width: plot.width, height: plot.height }
|
|
1900
|
+
});
|
|
1901
|
+
const ticks = Array.from({ length: Y_AXIS_TICK_COUNT }, (_, index) => {
|
|
1902
|
+
const ratio = index / (Y_AXIS_TICK_COUNT - 1);
|
|
1903
|
+
const value = yRange.maxY - ratio * (yRange.maxY - yRange.minY);
|
|
1904
|
+
const screen = transform.dataToScreen({
|
|
1905
|
+
x: state.xMin,
|
|
1906
|
+
y: value
|
|
1907
|
+
});
|
|
1908
|
+
return {
|
|
1909
|
+
id: `${chart.id}-tick-${index}`,
|
|
1910
|
+
value,
|
|
1911
|
+
top: horizontal ? void 0 : screen.y,
|
|
1912
|
+
left: horizontal ? screen.x : void 0
|
|
1913
|
+
};
|
|
1914
|
+
});
|
|
1915
|
+
const categories = getChartCategories(chart);
|
|
1916
|
+
const visibleCategories = categories.filter(
|
|
1917
|
+
(category) => category.value >= state.xMin && category.value <= state.xMax
|
|
1918
|
+
).sort((left, right) => left.value - right.value);
|
|
1919
|
+
const categoryPixelLength = horizontal ? plot.height : plot.width;
|
|
1920
|
+
const maximumCategoryLabels = Math.max(
|
|
1921
|
+
1,
|
|
1922
|
+
Math.floor(categoryPixelLength / (horizontal ? 22 : 72))
|
|
1923
|
+
);
|
|
1924
|
+
const categoryLabelStride = Math.max(
|
|
1925
|
+
1,
|
|
1926
|
+
Math.ceil(visibleCategories.length / maximumCategoryLabels)
|
|
1927
|
+
);
|
|
1928
|
+
const displayedCategories = visibleCategories.filter(
|
|
1929
|
+
(_, index) => index % categoryLabelStride === 0 || index === visibleCategories.length - 1
|
|
1930
|
+
);
|
|
1931
|
+
const xTicks = categories.length ? displayedCategories.map((category, index) => {
|
|
1932
|
+
const screen = transform.dataToScreen({
|
|
1933
|
+
x: category.value,
|
|
1934
|
+
y: yRange.minY
|
|
1935
|
+
});
|
|
1936
|
+
return {
|
|
1937
|
+
id: `${chart.id}-category-${index}-${category.value}`,
|
|
1938
|
+
categorical: true,
|
|
1939
|
+
label: category.label,
|
|
1940
|
+
value: category.value,
|
|
1941
|
+
left: horizontal ? void 0 : screen.x,
|
|
1942
|
+
top: horizontal ? screen.y : void 0
|
|
1943
|
+
};
|
|
1944
|
+
}) : Array.from({ length: X_AXIS_TICK_COUNT }, (_, index) => {
|
|
1945
|
+
const ratio = index / (X_AXIS_TICK_COUNT - 1);
|
|
1946
|
+
return {
|
|
1947
|
+
id: `${chart.id}-x-tick-${index}`,
|
|
1948
|
+
value: state.xMin + ratio * (state.xMax - state.xMin),
|
|
1949
|
+
left: horizontal ? void 0 : 18 + ratio * Math.max(1, plot.width - 36),
|
|
1950
|
+
top: horizontal ? 10 + ratio * Math.max(1, plot.height - 20) : void 0
|
|
1951
|
+
};
|
|
1952
|
+
});
|
|
1953
|
+
const latestValues = descriptor.capabilities.latestValue ? getOrderedSeries(chart, seriesOrderByChart).map((series) => {
|
|
1954
|
+
if (!series.length) return null;
|
|
1955
|
+
const value = series.rawY[series.length - 1];
|
|
1956
|
+
const x = series.rawX[series.length - 1];
|
|
1957
|
+
const endpoint = seriesEndpoints.get(series.id) || { x, y: value };
|
|
1958
|
+
const screen = transform.dataToScreen(endpoint);
|
|
1959
|
+
return {
|
|
1960
|
+
id: series.id,
|
|
1961
|
+
color: series.color,
|
|
1962
|
+
textColor: getSeriesLabelTextColor(series),
|
|
1963
|
+
value: endpoint.y,
|
|
1964
|
+
rawValue: value,
|
|
1965
|
+
x,
|
|
1966
|
+
left: screen.x,
|
|
1967
|
+
top: screen.y
|
|
1968
|
+
};
|
|
1969
|
+
}).filter(
|
|
1970
|
+
(item) => item && Number.isFinite(item.top) && item.top >= -10 && item.top <= plot.height + 10
|
|
1971
|
+
) : [];
|
|
1972
|
+
const { maxX } = getChartXBounds(chart);
|
|
1973
|
+
return {
|
|
1974
|
+
orientation: descriptor.orientation,
|
|
1975
|
+
ticks,
|
|
1976
|
+
xTicks,
|
|
1977
|
+
latestValues,
|
|
1978
|
+
plotWidth: plot.width,
|
|
1979
|
+
showJumpLatest: Number.isFinite(maxX) && (maxX < state.xMin || maxX > state.xMax)
|
|
1980
|
+
};
|
|
1981
|
+
};
|
|
1982
|
+
var drawChartLayouts = ({
|
|
1983
|
+
canvas,
|
|
1984
|
+
width,
|
|
1985
|
+
height,
|
|
1986
|
+
gl,
|
|
1987
|
+
rendererRegistry,
|
|
1988
|
+
antialiasLines = false,
|
|
1989
|
+
layouts,
|
|
1990
|
+
viewStateRef,
|
|
1991
|
+
yScaleRef,
|
|
1992
|
+
yCenterOffsetRef,
|
|
1993
|
+
initialVisiblePoints,
|
|
1994
|
+
getAppendAnimatedPoint,
|
|
1995
|
+
movingAverageByChart,
|
|
1996
|
+
seriesOrderByChart
|
|
1997
|
+
}) => {
|
|
1998
|
+
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
1999
|
+
const pixelWidth = Math.max(1, Math.floor(width * dpr));
|
|
2000
|
+
const pixelHeight = Math.max(1, Math.floor(height * dpr));
|
|
2001
|
+
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
|
|
2002
|
+
canvas.width = pixelWidth;
|
|
2003
|
+
canvas.height = pixelHeight;
|
|
2004
|
+
canvas.style.width = `${width}px`;
|
|
2005
|
+
canvas.style.height = `${height}px`;
|
|
2006
|
+
}
|
|
2007
|
+
gl.viewport(0, 0, pixelWidth, pixelHeight);
|
|
2008
|
+
gl.clearColor(0, 0, 0, 0);
|
|
2009
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
2010
|
+
const now = performance.now();
|
|
2011
|
+
const nextAxisOverlays = {};
|
|
2012
|
+
layouts.forEach((layout) => {
|
|
2013
|
+
if (!layout.visible) return;
|
|
2014
|
+
const { chart, plot } = layout;
|
|
2015
|
+
const descriptor = layout.descriptor || getChartDescriptor(chart);
|
|
2016
|
+
const state = viewStateRef.current.get(chart.id) || getInitialView(chart, initialVisiblePoints);
|
|
2017
|
+
const scaledPlot = {
|
|
2018
|
+
x: plot.x * dpr,
|
|
2019
|
+
y: plot.y * dpr,
|
|
2020
|
+
width: plot.width * dpr,
|
|
2021
|
+
height: plot.height * dpr
|
|
2022
|
+
};
|
|
2023
|
+
const categoryPixelLength = getCategoryPixelLength(
|
|
2024
|
+
chart,
|
|
2025
|
+
plot,
|
|
2026
|
+
descriptor
|
|
2027
|
+
);
|
|
2028
|
+
const yRange = applyYScale(
|
|
2029
|
+
getYRange(
|
|
2030
|
+
chart,
|
|
2031
|
+
state.xMin,
|
|
2032
|
+
state.xMax,
|
|
2033
|
+
categoryPixelLength,
|
|
2034
|
+
descriptor
|
|
2035
|
+
),
|
|
2036
|
+
yScaleRef.current.get(chart.id) ?? 1,
|
|
2037
|
+
yCenterOffsetRef.current.get(chart.id) ?? 0
|
|
2038
|
+
);
|
|
2039
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
2040
|
+
gl.scissor(
|
|
2041
|
+
Math.floor(scaledPlot.x),
|
|
2042
|
+
Math.floor(pixelHeight - scaledPlot.y - scaledPlot.height),
|
|
2043
|
+
Math.ceil(scaledPlot.width),
|
|
2044
|
+
Math.ceil(scaledPlot.height)
|
|
2045
|
+
);
|
|
2046
|
+
const result = rendererRegistry.draw(descriptor.rendererType, {
|
|
2047
|
+
antialiasLines,
|
|
2048
|
+
categoryPixelLength,
|
|
2049
|
+
chart,
|
|
2050
|
+
descriptor,
|
|
2051
|
+
dpr,
|
|
2052
|
+
getAppendAnimatedPoint,
|
|
2053
|
+
movingAverage: movingAverageByChart?.[chart.id],
|
|
2054
|
+
now,
|
|
2055
|
+
pixelHeight,
|
|
2056
|
+
pixelWidth,
|
|
2057
|
+
plot,
|
|
2058
|
+
scaledPlot,
|
|
2059
|
+
seriesOrderByChart,
|
|
2060
|
+
state,
|
|
2061
|
+
yRange
|
|
2062
|
+
});
|
|
2063
|
+
nextAxisOverlays[chart.id] = createAxisOverlay({
|
|
2064
|
+
chart,
|
|
2065
|
+
descriptor,
|
|
2066
|
+
plot,
|
|
2067
|
+
state,
|
|
2068
|
+
yRange,
|
|
2069
|
+
seriesEndpoints: result?.seriesEndpoints || /* @__PURE__ */ new Map(),
|
|
2070
|
+
seriesOrderByChart
|
|
2071
|
+
});
|
|
2072
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
2073
|
+
});
|
|
2074
|
+
gl.disable(gl.BLEND);
|
|
2075
|
+
return nextAxisOverlays;
|
|
2076
|
+
};
|
|
2077
|
+
|
|
2078
|
+
// src/vanilla/createChartGrid.js
|
|
2079
|
+
var BASE_ROOT_CLASSES = ["aliencharts-root", "relative", "h-full", "overflow-y-auto"];
|
|
2080
|
+
var EMPTY_OBJECT = Object.freeze({});
|
|
2081
|
+
var controllerIdSequence = 0;
|
|
2082
|
+
var DEFAULT_OPTIONS = Object.freeze({
|
|
2083
|
+
charts: [],
|
|
2084
|
+
columns: 2,
|
|
2085
|
+
initialVisiblePoints: null,
|
|
2086
|
+
backgroundColor: DEFAULT_CHART_BACKGROUND,
|
|
2087
|
+
antialiasLines: false,
|
|
2088
|
+
gridLines: false,
|
|
2089
|
+
showToolbar: true,
|
|
2090
|
+
showLatestValueLine: true,
|
|
2091
|
+
showTooltips: true,
|
|
2092
|
+
followLatest: false,
|
|
2093
|
+
followVisibleLatest: true,
|
|
2094
|
+
xAxisLabel: "STEP",
|
|
2095
|
+
disableDrawings: false,
|
|
2096
|
+
seriesOrderByChart: EMPTY_OBJECT,
|
|
2097
|
+
topMarkers: [],
|
|
2098
|
+
formatXTick: formatCompactNumber,
|
|
2099
|
+
formatXValue: formatNumber,
|
|
2100
|
+
formatYValue: formatNumber
|
|
2101
|
+
});
|
|
2102
|
+
var APPEND_ANIMATION = Object.freeze({
|
|
2103
|
+
durationMs: 300,
|
|
2104
|
+
maxBucketSize: 64,
|
|
2105
|
+
maxRevealPoints: 100
|
|
2106
|
+
});
|
|
2107
|
+
var POINTER_SCALE_SENSITIVITY = 0.01;
|
|
2108
|
+
var WHEEL_SCALE_SENSITIVITY = 1e-3;
|
|
2109
|
+
var scaleValueRange = (scale, delta, sensitivity) => clamp(
|
|
2110
|
+
scale * Math.exp(delta * sensitivity),
|
|
2111
|
+
Y_SCALE_MIN,
|
|
2112
|
+
Y_SCALE_MAX
|
|
2113
|
+
);
|
|
2114
|
+
var escapeHtml = (value) => String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2115
|
+
var withAlpha = (color, alpha) => {
|
|
2116
|
+
const value = String(color || DEFAULT_CHART_BACKGROUND).trim();
|
|
2117
|
+
const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(value);
|
|
2118
|
+
if (!match) return value;
|
|
2119
|
+
return `rgba(${Number.parseInt(match[1], 16)}, ${Number.parseInt(match[2], 16)}, ${Number.parseInt(match[3], 16)}, ${alpha})`;
|
|
2120
|
+
};
|
|
2121
|
+
var getTextColor = (color) => {
|
|
2122
|
+
const value = String(color || "#38bdf8").replace("#", "");
|
|
2123
|
+
const normalized = value.length === 3 ? value.split("").map((part) => part + part).join("") : value.padEnd(6, "0").slice(0, 6);
|
|
2124
|
+
const number = Number.parseInt(normalized, 16);
|
|
2125
|
+
const rgb = [16, 8, 0].map((shift) => (number >> shift & 255) / 255);
|
|
2126
|
+
const linear = rgb.map((part) => part <= 0.03928 ? part / 12.92 : ((part + 0.055) / 1.055) ** 2.4);
|
|
2127
|
+
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] > 0.45 ? "#111827" : "#ffffff";
|
|
2128
|
+
};
|
|
2129
|
+
var hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
2130
|
+
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
2131
|
+
var toolbarButton = ({ action, title, icon, hotkey = "", active = false, className = "" }) => `
|
|
2132
|
+
<button type="button" data-action="${action}" title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}"
|
|
2133
|
+
class="relative inline-flex size-6 items-center justify-center rounded-sm text-foreground hover:bg-accent hover:text-accent-foreground ${active ? "bg-primary/15 text-primary ring-1 ring-primary/40" : ""} ${className}">
|
|
2134
|
+
${icon}
|
|
2135
|
+
${hotkey ? `<span class="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 text-[10px] font-semibold leading-none text-foreground/50">${hotkey}</span>` : ""}
|
|
2136
|
+
</button>`;
|
|
2137
|
+
var chartCellHtml = (chart, descriptor, index, backgroundColor) => {
|
|
2138
|
+
const padding = getPlotPadding(chart, descriptor);
|
|
2139
|
+
const horizontal = descriptor.orientation === "horizontal";
|
|
2140
|
+
const valueAxisStyle = horizontal ? `left:${padding.left}px;right:${padding.right}px;bottom:0;height:${padding.bottom}px;cursor:ew-resize` : `right:0;top:${padding.top}px;bottom:${padding.bottom}px;width:${padding.right}px;cursor:ns-resize`;
|
|
2141
|
+
const categoryAxisStyle = horizontal ? `left:0;top:${padding.top}px;bottom:${padding.bottom}px;width:${padding.left}px;cursor:ns-resize` : `left:${padding.left}px;right:${padding.right}px;bottom:0;height:${padding.bottom}px;cursor:ew-resize`;
|
|
2142
|
+
return `
|
|
2143
|
+
<div data-chart-index="${index}" class="relative select-none overflow-hidden rounded-sm" style="height:${CHART_HEIGHT}px;background-color:${escapeHtml(backgroundColor)}">
|
|
2144
|
+
<div class="pointer-events-none absolute inset-x-0 top-0 z-10 flex h-8 items-center px-2 backdrop-blur" data-header>
|
|
2145
|
+
<div class="flex min-w-0 items-center gap-1 rounded-sm px-1 text-sm font-semibold text-foreground" data-title-wrap>
|
|
2146
|
+
${chart.pinned ? iconSvg("pushPinSimple", { size: 14, className: "shrink-0 text-foreground/80" }) : ""}
|
|
2147
|
+
<span class="truncate">${escapeHtml(chart.title)}</span>
|
|
2148
|
+
</div>
|
|
2149
|
+
</div>
|
|
2150
|
+
<div data-grid-lines class="pointer-events-none absolute text-border/40"></div>
|
|
2151
|
+
<div data-latest-lines></div>
|
|
2152
|
+
<div data-y-axis class="absolute z-20" style="${valueAxisStyle}"></div>
|
|
2153
|
+
<div data-x-axis class="absolute z-20" style="${categoryAxisStyle}"></div>
|
|
2154
|
+
<div data-toolbar></div>
|
|
2155
|
+
</div>`;
|
|
2156
|
+
};
|
|
2157
|
+
var makeSurface = (canvas, gl) => ({
|
|
2158
|
+
canvas,
|
|
2159
|
+
gl,
|
|
2160
|
+
renderers: createRendererRegistry(gl)
|
|
2161
|
+
});
|
|
2162
|
+
var destroySurface = (surface) => {
|
|
2163
|
+
if (!surface) return;
|
|
2164
|
+
surface.renderers.destroy();
|
|
2165
|
+
surface.gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
2166
|
+
};
|
|
2167
|
+
var AppendAnimator = class {
|
|
2168
|
+
constructor(requestRender) {
|
|
2169
|
+
this.requestRender = requestRender;
|
|
2170
|
+
this.latest = /* @__PURE__ */ new Map();
|
|
2171
|
+
this.animations = /* @__PURE__ */ new Map();
|
|
2172
|
+
this.frame = null;
|
|
2173
|
+
}
|
|
2174
|
+
scan(charts, descriptors) {
|
|
2175
|
+
const now = performance.now();
|
|
2176
|
+
charts.forEach((chart) => {
|
|
2177
|
+
const descriptor = descriptors.get(chart.id);
|
|
2178
|
+
if (!descriptor?.capabilities.appendAnimation) return;
|
|
2179
|
+
chart.series.forEach((series) => {
|
|
2180
|
+
if (!series.length) return;
|
|
2181
|
+
const next = {
|
|
2182
|
+
x: series.rawX[series.length - 1],
|
|
2183
|
+
y: series.rawY[series.length - 1],
|
|
2184
|
+
length: series.length
|
|
2185
|
+
};
|
|
2186
|
+
const previous = this.latest.get(series.id);
|
|
2187
|
+
if (previous && next.x > previous.x) {
|
|
2188
|
+
this.animations.set(series.id, {
|
|
2189
|
+
fromX: previous.x,
|
|
2190
|
+
fromY: previous.y,
|
|
2191
|
+
toX: next.x,
|
|
2192
|
+
toY: next.y,
|
|
2193
|
+
appendedCount: next.length - previous.length,
|
|
2194
|
+
startedAt: now,
|
|
2195
|
+
duration: APPEND_ANIMATION.durationMs
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
this.latest.set(series.id, next);
|
|
2199
|
+
});
|
|
2200
|
+
});
|
|
2201
|
+
if (this.animations.size) this.start();
|
|
2202
|
+
}
|
|
2203
|
+
start() {
|
|
2204
|
+
if (this.frame != null) return;
|
|
2205
|
+
const tick = () => {
|
|
2206
|
+
const now = performance.now();
|
|
2207
|
+
this.animations.forEach((animation, id) => {
|
|
2208
|
+
if (now - animation.startedAt >= animation.duration) this.animations.delete(id);
|
|
2209
|
+
});
|
|
2210
|
+
this.requestRender();
|
|
2211
|
+
this.frame = this.animations.size ? requestAnimationFrame(tick) : null;
|
|
2212
|
+
};
|
|
2213
|
+
this.frame = requestAnimationFrame(tick);
|
|
2214
|
+
}
|
|
2215
|
+
getPoint({ seriesId, visiblePoints, now }) {
|
|
2216
|
+
if (visiblePoints.bucketSize > APPEND_ANIMATION.maxBucketSize) return null;
|
|
2217
|
+
const animation = this.animations.get(seriesId);
|
|
2218
|
+
if (!animation || visiblePoints.x[0] > animation.fromX || visiblePoints.x[visiblePoints.pointCount - 1] < animation.toX) return null;
|
|
2219
|
+
let stableCount = 0;
|
|
2220
|
+
while (stableCount < visiblePoints.pointCount && visiblePoints.x[stableCount] <= animation.fromX) stableCount += 1;
|
|
2221
|
+
if (!stableCount) return null;
|
|
2222
|
+
const rawProgress = clamp((now - animation.startedAt) / animation.duration, 0, 1);
|
|
2223
|
+
const progress = 1 - (1 - rawProgress) ** 2;
|
|
2224
|
+
if (animation.appendedCount > APPEND_ANIMATION.maxRevealPoints) {
|
|
2225
|
+
return {
|
|
2226
|
+
index: stableCount,
|
|
2227
|
+
pointCount: stableCount + 1,
|
|
2228
|
+
x: animation.fromX + (animation.toX - animation.fromX) * progress,
|
|
2229
|
+
y: animation.fromY + (animation.toY - animation.fromY) * progress
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
const appendedCount = visiblePoints.pointCount - stableCount;
|
|
2233
|
+
const position = progress * appendedCount;
|
|
2234
|
+
const revealed = Math.min(appendedCount, Math.floor(position));
|
|
2235
|
+
const nextIndex = Math.min(visiblePoints.pointCount - 1, stableCount + revealed);
|
|
2236
|
+
const previousIndex = Math.max(stableCount - 1, nextIndex - 1);
|
|
2237
|
+
const partial = position - revealed;
|
|
2238
|
+
const partialPoint = revealed < appendedCount;
|
|
2239
|
+
return {
|
|
2240
|
+
index: nextIndex,
|
|
2241
|
+
pointCount: Math.min(visiblePoints.pointCount, nextIndex + 1),
|
|
2242
|
+
x: partialPoint ? visiblePoints.x[previousIndex] + (visiblePoints.x[nextIndex] - visiblePoints.x[previousIndex]) * partial : visiblePoints.x[nextIndex],
|
|
2243
|
+
y: partialPoint ? visiblePoints.y[previousIndex] + (visiblePoints.y[nextIndex] - visiblePoints.y[previousIndex]) * partial : visiblePoints.y[nextIndex]
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
destroy() {
|
|
2247
|
+
if (this.frame != null) cancelAnimationFrame(this.frame);
|
|
2248
|
+
this.frame = null;
|
|
2249
|
+
this.animations.clear();
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
var ChartGridController = class {
|
|
2253
|
+
constructor(container, options) {
|
|
2254
|
+
if (!(container instanceof HTMLElement)) throw new TypeError("createChartGrid requires an HTMLElement target");
|
|
2255
|
+
this.container = container;
|
|
2256
|
+
this.controllerId = `aliencharts-${controllerIdSequence += 1}`;
|
|
2257
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
2258
|
+
if (!Array.isArray(this.options.charts)) throw new TypeError("charts must be an array");
|
|
2259
|
+
this.chartDescriptors = resolveChartDescriptors(this.options.charts);
|
|
2260
|
+
this.destroyed = false;
|
|
2261
|
+
this.frame = null;
|
|
2262
|
+
this.structureDirty = true;
|
|
2263
|
+
this.focusedChartId = null;
|
|
2264
|
+
this.fullscreenChartId = null;
|
|
2265
|
+
this.rectangleZoomChartId = null;
|
|
2266
|
+
this.rectangleZoomRect = null;
|
|
2267
|
+
this.crosshair = null;
|
|
2268
|
+
this.drag = null;
|
|
2269
|
+
this.draftDrawing = null;
|
|
2270
|
+
this.drawingSession = null;
|
|
2271
|
+
this.drawings = Array.isArray(options.drawings) ? [...options.drawings] : [];
|
|
2272
|
+
this.activeDrawingTool = options.activeDrawingTool ?? null;
|
|
2273
|
+
this.selectedDrawingId = options.selectedDrawingId ?? null;
|
|
2274
|
+
this.movingAverageByChart = { ...options.movingAverageByChart || {} };
|
|
2275
|
+
this.viewStates = /* @__PURE__ */ new Map();
|
|
2276
|
+
this.yScales = /* @__PURE__ */ new Map();
|
|
2277
|
+
this.yOffsets = /* @__PURE__ */ new Map();
|
|
2278
|
+
this.latestX = /* @__PURE__ */ new Map();
|
|
2279
|
+
this.axisOverlays = {};
|
|
2280
|
+
this.layouts = [];
|
|
2281
|
+
this.fullscreenLayouts = [];
|
|
2282
|
+
this.addedClasses = BASE_ROOT_CLASSES.filter((name) => !container.classList.contains(name));
|
|
2283
|
+
BASE_ROOT_CLASSES.forEach((name) => container.classList.add(name));
|
|
2284
|
+
this.canvas = document.createElement("canvas");
|
|
2285
|
+
this.canvas.className = "pointer-events-none absolute left-0 top-0 z-10 block";
|
|
2286
|
+
this.grid = document.createElement("div");
|
|
2287
|
+
this.grid.className = "relative z-0 grid gap-1 pt-1";
|
|
2288
|
+
this.overlays = document.createElement("div");
|
|
2289
|
+
this.overlays.className = "pointer-events-none absolute left-0 top-0 z-20";
|
|
2290
|
+
this.overlays.style.width = "100%";
|
|
2291
|
+
container.append(this.canvas, this.grid, this.overlays);
|
|
2292
|
+
const gl = this.canvas.getContext("webgl2", { antialias: true, alpha: true, depth: false, stencil: false });
|
|
2293
|
+
if (!gl) {
|
|
2294
|
+
this.cleanupDom();
|
|
2295
|
+
throw new Error("AlienCharts requires WebGL2 support");
|
|
2296
|
+
}
|
|
2297
|
+
this.surface = makeSurface(this.canvas, gl);
|
|
2298
|
+
this.fullscreenSurface = null;
|
|
2299
|
+
this.animator = new AppendAnimator(() => this.requestRender());
|
|
2300
|
+
this.animator.scan(this.options.charts, this.chartDescriptors);
|
|
2301
|
+
this.bound = {
|
|
2302
|
+
click: (event) => this.handleClick(event),
|
|
2303
|
+
input: (event) => this.handleInput(event),
|
|
2304
|
+
change: (event) => this.handleInput(event),
|
|
2305
|
+
pointerdown: (event) => this.handlePointerDown(event),
|
|
2306
|
+
pointermove: (event) => this.handlePointerMove(event),
|
|
2307
|
+
pointerup: (event) => this.handlePointerUp(event),
|
|
2308
|
+
pointerleave: () => this.clearCrosshair(),
|
|
2309
|
+
contextmenu: (event) => this.handleContextMenu(event),
|
|
2310
|
+
wheel: (event) => this.handleWheel(event),
|
|
2311
|
+
scroll: () => this.requestRender(),
|
|
2312
|
+
keydown: (event) => this.handleKeyDown(event)
|
|
2313
|
+
};
|
|
2314
|
+
Object.entries(this.bound).forEach(([name, handler]) => {
|
|
2315
|
+
const target = name === "keydown" ? window : container;
|
|
2316
|
+
target.addEventListener(name, handler, name === "wheel" ? { passive: false } : void 0);
|
|
2317
|
+
});
|
|
2318
|
+
this.resizeObserver = new ResizeObserver(() => this.requestRender());
|
|
2319
|
+
this.resizeObserver.observe(container);
|
|
2320
|
+
this.initializeChartState();
|
|
2321
|
+
this.requestRender();
|
|
2322
|
+
}
|
|
2323
|
+
assertAlive() {
|
|
2324
|
+
if (this.destroyed) throw new Error("AlienCharts controller has been destroyed");
|
|
2325
|
+
}
|
|
2326
|
+
initializeChartState() {
|
|
2327
|
+
const ids = new Set(this.options.charts.map((chart) => chart.id));
|
|
2328
|
+
[this.viewStates, this.yScales, this.yOffsets, this.latestX].forEach((map) => {
|
|
2329
|
+
[...map.keys()].forEach((id) => {
|
|
2330
|
+
if (!ids.has(id)) map.delete(id);
|
|
2331
|
+
});
|
|
2332
|
+
});
|
|
2333
|
+
this.options.charts.forEach((chart) => {
|
|
2334
|
+
if (!this.viewStates.has(chart.id)) this.viewStates.set(chart.id, getInitialView(chart, this.options.initialVisiblePoints));
|
|
2335
|
+
if (!this.yScales.has(chart.id)) this.yScales.set(chart.id, 1);
|
|
2336
|
+
if (!this.yOffsets.has(chart.id)) this.yOffsets.set(chart.id, 0);
|
|
2337
|
+
if (!this.latestX.has(chart.id)) this.latestX.set(chart.id, getChartXBounds(chart).maxX);
|
|
2338
|
+
});
|
|
2339
|
+
if (this.focusedChartId && !ids.has(this.focusedChartId)) this.focusedChartId = null;
|
|
2340
|
+
if (this.fullscreenChartId && !ids.has(this.fullscreenChartId)) this.closeFullscreen();
|
|
2341
|
+
}
|
|
2342
|
+
requestRender() {
|
|
2343
|
+
if (this.destroyed || this.frame != null) return;
|
|
2344
|
+
this.frame = requestAnimationFrame(() => {
|
|
2345
|
+
this.frame = null;
|
|
2346
|
+
this.render();
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
setOptions(next) {
|
|
2350
|
+
this.assertAlive();
|
|
2351
|
+
if (!next || typeof next !== "object") return;
|
|
2352
|
+
const previousCharts = this.options.charts;
|
|
2353
|
+
const previousActiveDrawingTool = this.activeDrawingTool;
|
|
2354
|
+
const nextOptions = { ...this.options, ...next };
|
|
2355
|
+
if (!Array.isArray(nextOptions.charts)) throw new TypeError("charts must be an array");
|
|
2356
|
+
const nextDescriptors = resolveChartDescriptors(nextOptions.charts);
|
|
2357
|
+
const descriptorLayoutChanged = nextOptions.charts.some((chart) => {
|
|
2358
|
+
const previous = this.chartDescriptors.get(chart.id);
|
|
2359
|
+
const current = nextDescriptors.get(chart.id);
|
|
2360
|
+
return previous?.rendererType !== current?.rendererType || previous?.orientation !== current?.orientation || previous?.categorical !== current?.categorical;
|
|
2361
|
+
});
|
|
2362
|
+
this.options = nextOptions;
|
|
2363
|
+
this.chartDescriptors = nextDescriptors;
|
|
2364
|
+
if (hasOwn(next, "drawings")) this.drawings = Array.isArray(next.drawings) ? [...next.drawings] : [];
|
|
2365
|
+
if (hasOwn(next, "activeDrawingTool")) {
|
|
2366
|
+
this.activeDrawingTool = next.activeDrawingTool ?? null;
|
|
2367
|
+
if (this.activeDrawingTool !== previousActiveDrawingTool) {
|
|
2368
|
+
this.draftDrawing = null;
|
|
2369
|
+
this.drawingSession = null;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
if (hasOwn(next, "selectedDrawingId")) this.selectedDrawingId = next.selectedDrawingId ?? null;
|
|
2373
|
+
if (hasOwn(next, "movingAverageByChart")) this.movingAverageByChart = { ...next.movingAverageByChart || {} };
|
|
2374
|
+
if (previousCharts !== this.options.charts || descriptorLayoutChanged || hasOwn(next, "columns") || hasOwn(next, "backgroundColor") || hasOwn(next, "gridLines")) this.structureDirty = true;
|
|
2375
|
+
this.initializeChartState();
|
|
2376
|
+
this.requestRender();
|
|
2377
|
+
}
|
|
2378
|
+
setCharts(charts) {
|
|
2379
|
+
this.setOptions({ charts });
|
|
2380
|
+
}
|
|
2381
|
+
invalidate() {
|
|
2382
|
+
this.assertAlive();
|
|
2383
|
+
this.followAppendedData();
|
|
2384
|
+
this.animator.scan(this.options.charts, this.chartDescriptors);
|
|
2385
|
+
this.requestRender();
|
|
2386
|
+
}
|
|
2387
|
+
followAppendedData() {
|
|
2388
|
+
this.options.charts.forEach((chart) => {
|
|
2389
|
+
const previous = this.latestX.get(chart.id);
|
|
2390
|
+
const next = getChartXBounds(chart).maxX;
|
|
2391
|
+
if (Number.isFinite(previous) && Number.isFinite(next) && next !== previous) {
|
|
2392
|
+
const state = this.viewStates.get(chart.id) || getInitialView(chart, this.options.initialVisiblePoints);
|
|
2393
|
+
const span = state.xMax - state.xMin;
|
|
2394
|
+
const nearEdge = previous >= state.xMax - span * 0.15 && previous >= state.xMin && previous <= state.xMax;
|
|
2395
|
+
if (this.options.followLatest || this.options.followVisibleLatest && nearEdge) {
|
|
2396
|
+
const delta = next - previous;
|
|
2397
|
+
this.viewStates.set(chart.id, { xMin: state.xMin + delta, xMax: state.xMax + delta });
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
this.latestX.set(chart.id, next);
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
jumpToLatest(chartId) {
|
|
2404
|
+
this.assertAlive();
|
|
2405
|
+
this.options.charts.forEach((chart) => {
|
|
2406
|
+
if (chartId != null && chart.id !== chartId) return;
|
|
2407
|
+
const maxX = getChartXBounds(chart).maxX;
|
|
2408
|
+
if (!Number.isFinite(maxX)) return;
|
|
2409
|
+
const state = this.viewStates.get(chart.id) || getInitialView(chart, this.options.initialVisiblePoints);
|
|
2410
|
+
const span = Math.max(
|
|
2411
|
+
getMinimumCategorySpan(chart),
|
|
2412
|
+
state.xMax - state.xMin
|
|
2413
|
+
);
|
|
2414
|
+
const nextMax = maxX + span * 0.1;
|
|
2415
|
+
this.viewStates.set(chart.id, { xMin: nextMax - span, xMax: nextMax });
|
|
2416
|
+
});
|
|
2417
|
+
this.requestRender();
|
|
2418
|
+
}
|
|
2419
|
+
scrollToTop(options = {}) {
|
|
2420
|
+
this.assertAlive();
|
|
2421
|
+
this.container.scrollTo({ ...options, top: 0 });
|
|
2422
|
+
}
|
|
2423
|
+
renderStructure() {
|
|
2424
|
+
this.grid.style.gridTemplateColumns = `repeat(${Math.max(1, Number(this.options.columns) || 1)}, minmax(18rem, 1fr))`;
|
|
2425
|
+
this.grid.innerHTML = this.options.charts.map((chart, index) => chartCellHtml(
|
|
2426
|
+
chart,
|
|
2427
|
+
this.chartDescriptors.get(chart.id),
|
|
2428
|
+
index,
|
|
2429
|
+
this.options.backgroundColor
|
|
2430
|
+
)).join("");
|
|
2431
|
+
this.chartNodes = [...this.grid.querySelectorAll("[data-chart-index]")];
|
|
2432
|
+
this.chartNodes.forEach((node) => {
|
|
2433
|
+
node.querySelector("[data-header]").style.backgroundColor = withAlpha(this.options.backgroundColor, 0.82);
|
|
2434
|
+
});
|
|
2435
|
+
this.structureDirty = false;
|
|
2436
|
+
}
|
|
2437
|
+
getLayouts(fullscreen = false) {
|
|
2438
|
+
const nodes = fullscreen ? this.fullscreenNode ? [this.fullscreenNode] : [] : this.chartNodes || [];
|
|
2439
|
+
const charts = fullscreen ? this.options.charts.filter((chart) => chart.id === this.fullscreenChartId) : this.options.charts;
|
|
2440
|
+
const host = fullscreen ? this.fullscreenOverlay : this.container;
|
|
2441
|
+
const hostRect = host.getBoundingClientRect();
|
|
2442
|
+
return nodes.map((node, index) => {
|
|
2443
|
+
const rect = node.getBoundingClientRect();
|
|
2444
|
+
const x = rect.left - hostRect.left + host.scrollLeft;
|
|
2445
|
+
const y = rect.top - hostRect.top + host.scrollTop;
|
|
2446
|
+
const chart = charts[index];
|
|
2447
|
+
const descriptor = this.chartDescriptors.get(chart.id);
|
|
2448
|
+
const padding = getPlotPadding(chart, descriptor);
|
|
2449
|
+
const layout = {
|
|
2450
|
+
chart,
|
|
2451
|
+
descriptor,
|
|
2452
|
+
rect: { x, y, width: rect.width, height: rect.height },
|
|
2453
|
+
plot: {
|
|
2454
|
+
x: x + padding.left,
|
|
2455
|
+
y: y + padding.top,
|
|
2456
|
+
width: Math.max(1, rect.width - padding.left - padding.right),
|
|
2457
|
+
height: Math.max(1, rect.height - padding.top - padding.bottom)
|
|
2458
|
+
},
|
|
2459
|
+
visible: fullscreen || rect.bottom >= hostRect.top && rect.top <= hostRect.bottom,
|
|
2460
|
+
node,
|
|
2461
|
+
fullscreen
|
|
2462
|
+
};
|
|
2463
|
+
return layout;
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
render() {
|
|
2467
|
+
const focusState = this.captureControlFocus();
|
|
2468
|
+
if (this.structureDirty) this.renderStructure();
|
|
2469
|
+
this.layouts = this.getLayouts(false);
|
|
2470
|
+
this.axisOverlays = this.drawSurface(this.surface, this.layouts, this.container.scrollWidth, this.container.scrollHeight);
|
|
2471
|
+
this.updateCells(this.layouts, this.axisOverlays, false);
|
|
2472
|
+
if (this.fullscreenChartId) {
|
|
2473
|
+
this.fullscreenLayouts = this.getLayouts(true);
|
|
2474
|
+
const axes = this.drawSurface(this.fullscreenSurface, this.fullscreenLayouts, this.fullscreenOverlay.clientWidth, this.fullscreenOverlay.clientHeight);
|
|
2475
|
+
this.updateCells(this.fullscreenLayouts, axes, true);
|
|
2476
|
+
}
|
|
2477
|
+
this.renderOverlays();
|
|
2478
|
+
this.restoreControlFocus(focusState);
|
|
2479
|
+
}
|
|
2480
|
+
captureControlFocus() {
|
|
2481
|
+
const active = document.activeElement;
|
|
2482
|
+
if (!(active instanceof HTMLInputElement)) return null;
|
|
2483
|
+
const selector = active.matches("[data-moving-period]") ? "[data-moving-period]" : active.matches("[data-pin-text]") ? "[data-pin-text]" : active.matches("[data-drawing-color]") ? "[data-drawing-color]" : null;
|
|
2484
|
+
if (!selector) return null;
|
|
2485
|
+
const fullscreen = Boolean(active.closest("[data-aliencharts-fullscreen]"));
|
|
2486
|
+
let selectionStart = null;
|
|
2487
|
+
let selectionEnd = null;
|
|
2488
|
+
if (active.type === "text") {
|
|
2489
|
+
selectionStart = active.selectionStart;
|
|
2490
|
+
selectionEnd = active.selectionEnd;
|
|
2491
|
+
}
|
|
2492
|
+
return { selector, fullscreen, selectionStart, selectionEnd };
|
|
2493
|
+
}
|
|
2494
|
+
restoreControlFocus(state) {
|
|
2495
|
+
if (!state) return;
|
|
2496
|
+
const host = state.fullscreen ? this.fullscreenOverlay : this.container;
|
|
2497
|
+
const input = host?.querySelector(state.selector);
|
|
2498
|
+
if (!(input instanceof HTMLInputElement)) return;
|
|
2499
|
+
input.focus({ preventScroll: true });
|
|
2500
|
+
if (input.type === "text" && state.selectionStart != null) {
|
|
2501
|
+
input.setSelectionRange(state.selectionStart, state.selectionEnd);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
drawSurface(surface, layouts, width, height) {
|
|
2505
|
+
if (!surface) return {};
|
|
2506
|
+
return drawChartLayouts({
|
|
2507
|
+
canvas: surface.canvas,
|
|
2508
|
+
width: Math.max(1, width),
|
|
2509
|
+
height: Math.max(1, height),
|
|
2510
|
+
gl: surface.gl,
|
|
2511
|
+
rendererRegistry: surface.renderers,
|
|
2512
|
+
antialiasLines: this.options.antialiasLines,
|
|
2513
|
+
layouts,
|
|
2514
|
+
viewStateRef: { current: this.viewStates },
|
|
2515
|
+
yScaleRef: { current: this.yScales },
|
|
2516
|
+
yCenterOffsetRef: { current: this.yOffsets },
|
|
2517
|
+
initialVisiblePoints: this.options.initialVisiblePoints,
|
|
2518
|
+
getAppendAnimatedPoint: (payload) => this.animator.getPoint(payload),
|
|
2519
|
+
movingAverageByChart: this.movingAverageByChart,
|
|
2520
|
+
seriesOrderByChart: this.options.seriesOrderByChart
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
updateCells(layouts, axes, fullscreen) {
|
|
2524
|
+
layouts.forEach(({ chart, descriptor, node }) => {
|
|
2525
|
+
const axis = axes[chart.id];
|
|
2526
|
+
const padding = getPlotPadding(chart, descriptor);
|
|
2527
|
+
const horizontal = axis?.orientation === "horizontal";
|
|
2528
|
+
node.querySelector("[data-title-wrap]").classList.toggle("ring-1", this.focusedChartId === chart.id);
|
|
2529
|
+
node.querySelector("[data-title-wrap]").classList.toggle("ring-primary/70", this.focusedChartId === chart.id);
|
|
2530
|
+
const grid = node.querySelector("[data-grid-lines]");
|
|
2531
|
+
const gridOptions = this.options.gridLines === true ? {} : this.options.gridLines || {};
|
|
2532
|
+
grid.style.display = this.options.gridLines ? "block" : "none";
|
|
2533
|
+
Object.assign(grid.style, {
|
|
2534
|
+
left: `${padding.left}px`,
|
|
2535
|
+
right: `${padding.right}px`,
|
|
2536
|
+
top: `${padding.top}px`,
|
|
2537
|
+
bottom: `${padding.bottom}px`,
|
|
2538
|
+
backgroundImage: "linear-gradient(to right, transparent calc(100% - 1px), currentColor calc(100% - 1px)), linear-gradient(to bottom, transparent calc(100% - 1px), currentColor calc(100% - 1px))",
|
|
2539
|
+
backgroundSize: `${Math.max(8, Number(gridOptions.xSpacing) || 80)}px 100%, 100% ${Math.max(8, Number(gridOptions.ySpacing) || 48)}px`
|
|
2540
|
+
});
|
|
2541
|
+
const yAxis = node.querySelector("[data-y-axis]");
|
|
2542
|
+
yAxis.style.backgroundColor = withAlpha(this.options.backgroundColor, 0.78);
|
|
2543
|
+
yAxis.innerHTML = (axis?.ticks || []).map((tick) => horizontal ? `<div class="pointer-events-none absolute top-1/2 -translate-x-1/2 -translate-y-1/2 text-center text-[11px] font-medium tabular-nums text-foreground/70" style="left:${tick.left}px">${escapeHtml(this.options.formatYValue(tick.value))}</div>` : `<div class="pointer-events-none absolute left-1/2 -translate-x-1/2 -translate-y-1/2 text-center text-[11px] font-medium tabular-nums text-foreground/70" style="top:${tick.top}px">${escapeHtml(this.options.formatYValue(tick.value))}</div>`).join("") + (axis?.latestValues || []).map((latest) => `<div class="pointer-events-none absolute left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-sm px-1.5 py-0.5 text-center text-[10px] font-bold tabular-nums shadow-sm" style="top:${latest.top}px;background:${latest.color};color:${getTextColor(latest.color)}">${escapeHtml(this.options.formatYValue(latest.value))}</div>`).join("");
|
|
2544
|
+
const xAxis = node.querySelector("[data-x-axis]");
|
|
2545
|
+
xAxis.style.backgroundColor = withAlpha(this.options.backgroundColor, 0.7);
|
|
2546
|
+
xAxis.innerHTML = (axis?.xTicks || []).map((tick) => {
|
|
2547
|
+
const label = tick.categorical ? tick.label : this.options.formatXTick(tick.value);
|
|
2548
|
+
if (horizontal && tick.categorical) {
|
|
2549
|
+
return `<div data-category-label title="${escapeHtml(label)}" class="pointer-events-none absolute inset-x-2 -translate-y-1/2 truncate text-right text-[11px] font-medium text-foreground/70" style="top:${tick.top}px">${escapeHtml(label)}</div>`;
|
|
2550
|
+
}
|
|
2551
|
+
if (tick.categorical) {
|
|
2552
|
+
return `<div data-category-label title="${escapeHtml(label)}" class="pointer-events-none absolute top-1/2 w-24 -translate-x-1/2 -translate-y-1/2 truncate text-center text-[11px] font-medium text-foreground/70" style="left:${tick.left}px">${escapeHtml(label)}</div>`;
|
|
2553
|
+
}
|
|
2554
|
+
return horizontal ? `<div class="pointer-events-none absolute left-1/2 -translate-x-1/2 -translate-y-1/2 text-center text-[11px] font-medium tabular-nums text-foreground/70" style="top:${tick.top}px">${escapeHtml(label)}</div>` : `<div class="pointer-events-none absolute top-1/2 -translate-x-1/2 -translate-y-1/2 text-center text-[11px] font-medium tabular-nums text-foreground/70" style="left:${tick.left}px">${escapeHtml(label)}</div>`;
|
|
2555
|
+
}).join("");
|
|
2556
|
+
const latestLayer = node.querySelector("[data-latest-lines]");
|
|
2557
|
+
latestLayer.innerHTML = this.options.showLatestValueLine ? (axis?.latestValues || []).filter((item) => item.left >= 0 && item.left <= axis.plotWidth).map((item) => `<div class="pointer-events-none absolute z-10 opacity-70" style="left:${padding.left + item.left}px;top:${padding.top + item.top}px;height:1px;width:${Math.max(0, axis.plotWidth - item.left)}px;background-image:repeating-linear-gradient(to right,${item.color} 0 4px,transparent 4px 8px)"></div>`).join("") : "";
|
|
2558
|
+
this.updateToolbar(node, chart, axis, fullscreen);
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
updateToolbar(node, chart, axis, fullscreen) {
|
|
2562
|
+
const target = node.querySelector("[data-toolbar]");
|
|
2563
|
+
const focused = fullscreen || this.focusedChartId === chart.id;
|
|
2564
|
+
const descriptor = this.chartDescriptors.get(chart.id);
|
|
2565
|
+
const drawingsAvailable = descriptor.capabilities.drawings;
|
|
2566
|
+
const movingAverageAvailable = descriptor.capabilities.movingAverage;
|
|
2567
|
+
const padding = getPlotPadding(chart, descriptor);
|
|
2568
|
+
let html = "";
|
|
2569
|
+
if (axis?.showJumpLatest) html += `<button type="button" data-action="jump-latest" aria-label="Jump to latest" class="absolute z-30 inline-flex size-7 items-center justify-center rounded-sm text-foreground hover:bg-accent" style="right:${padding.right + 6}px;bottom:${padding.bottom + 6}px;background:${withAlpha(this.options.backgroundColor, 0.7)}">${iconSvg("arrowLineRight", { className: axis.orientation === "horizontal" ? "rotate-90" : "" })}</button>`;
|
|
2570
|
+
if (focused && this.options.showToolbar) {
|
|
2571
|
+
const moving = movingAverageAvailable && Boolean(this.movingAverageByChart[chart.id]?.enabled);
|
|
2572
|
+
const hasDrawings = getDrawingsForChart(this.drawings, chart.id).length > 0;
|
|
2573
|
+
html += `<div class="absolute left-2 top-12 z-30 flex flex-col gap-0.5 rounded-sm bg-background/80 pr-3 text-foreground shadow-sm backdrop-blur">`;
|
|
2574
|
+
html += toolbarButton({ action: "fullscreen", title: fullscreen ? "Exit fullscreen" : "Maximize chart", hotkey: "F", icon: iconSvg(fullscreen ? "arrowsIn" : "arrowsOut") });
|
|
2575
|
+
html += toolbarButton({ action: "reset", title: "Reset chart", hotkey: "R", icon: iconSvg("arrowCounterClockwise") });
|
|
2576
|
+
html += toolbarButton({ action: "rectangle-zoom", title: "Rectangle zoom", hotkey: "Z", active: this.rectangleZoomChartId === chart.id, icon: iconSvg("magnifyingGlassPlus") });
|
|
2577
|
+
if (drawingsAvailable && !this.options.disableDrawings && hasDrawings) html += toolbarButton({ action: "clear-drawings", title: "Clear drawings", className: "text-orange-500", icon: iconSvg("broom") });
|
|
2578
|
+
if ((drawingsAvailable || movingAverageAvailable) && !this.options.disableDrawings) {
|
|
2579
|
+
html += '<div class="my-1 h-px w-full"></div>';
|
|
2580
|
+
if (drawingsAvailable) {
|
|
2581
|
+
html += toolbarButton({ action: "tool-trendline", title: "Draw trendline", hotkey: "T", active: this.activeDrawingTool === "trendline", icon: iconSvg("minus", { className: "-rotate-45" }) });
|
|
2582
|
+
html += toolbarButton({ action: "tool-hline", title: "Draw horizontal line", hotkey: "H", active: this.activeDrawingTool === "hline", icon: iconSvg("minus") });
|
|
2583
|
+
html += toolbarButton({ action: "tool-vline", title: "Draw vertical line", hotkey: "V", active: this.activeDrawingTool === "vline", icon: iconSvg("minus", { className: "rotate-90" }) });
|
|
2584
|
+
html += toolbarButton({ action: "tool-pin", title: "Place pin", hotkey: "P", active: this.activeDrawingTool === "pin", icon: iconSvg("mapPin") });
|
|
2585
|
+
}
|
|
2586
|
+
if (movingAverageAvailable) html += toolbarButton({ action: "moving-average", title: "Toggle moving average", hotkey: "M", active: moving, icon: iconSvg("waveSine") });
|
|
2587
|
+
}
|
|
2588
|
+
html += "</div>";
|
|
2589
|
+
if (movingAverageAvailable && !this.options.disableDrawings && moving) html += this.movingAverageOptionsHtml(chart);
|
|
2590
|
+
}
|
|
2591
|
+
target.innerHTML = html;
|
|
2592
|
+
}
|
|
2593
|
+
movingAverageOptionsHtml(chart) {
|
|
2594
|
+
const moving = this.movingAverageByChart[chart.id] || { enabled: true, period: 21, type: "ema", hideBase: false };
|
|
2595
|
+
return `<div data-moving-options class="absolute right-16 top-2 z-40 flex items-center gap-1 rounded-sm bg-background/85 p-1 text-xs text-foreground shadow-sm">
|
|
2596
|
+
<button type="button" data-action="toggle-base" title="${moving.hideBase ? "Show" : "Hide"} base series" aria-label="${moving.hideBase ? "Show" : "Hide"} base series" class="inline-flex size-6 items-center justify-center rounded-sm ${moving.hideBase ? "bg-primary/15 text-primary ring-1 ring-primary/40" : ""}">${iconSvg("eye", { size: 14 })}</button>
|
|
2597
|
+
<button type="button" data-action="moving-type" title="Toggle moving average type" class="h-6 rounded-sm px-2 text-[11px] font-semibold uppercase">${moving.type === "sma" ? "sma" : "ema"}</button>
|
|
2598
|
+
<input data-moving-period type="number" min="1" step="1" value="${Math.max(1, Number(moving.period) || 21)}" title="Moving average period" class="h-6 w-14 rounded-sm border border-border/50 px-1 text-xs" />
|
|
2599
|
+
</div>`;
|
|
2600
|
+
}
|
|
2601
|
+
renderOverlays() {
|
|
2602
|
+
const surface = this.fullscreenChartId ? this.fullscreenOverlay : this.container;
|
|
2603
|
+
const overlay = this.fullscreenChartId ? this.fullscreenOverlays : this.overlays;
|
|
2604
|
+
const layouts = this.fullscreenChartId ? this.fullscreenLayouts : this.layouts;
|
|
2605
|
+
if (!overlay) return;
|
|
2606
|
+
overlay.style.height = `${surface.scrollHeight}px`;
|
|
2607
|
+
let html = this.drawingOverlayHtml(layouts);
|
|
2608
|
+
html += this.topMarkersHtml(layouts);
|
|
2609
|
+
html += this.crosshairHtml();
|
|
2610
|
+
html += this.rectangleOverlayHtml();
|
|
2611
|
+
html += this.drawingOptionsHtml(layouts);
|
|
2612
|
+
overlay.innerHTML = html;
|
|
2613
|
+
}
|
|
2614
|
+
drawingOverlayHtml(layouts) {
|
|
2615
|
+
const drawings = this.draftDrawing ? [...this.drawings, this.draftDrawing] : this.drawings;
|
|
2616
|
+
if (!drawings.length) return "";
|
|
2617
|
+
const drawableLayouts = layouts.map((layout, index) => ({
|
|
2618
|
+
layout,
|
|
2619
|
+
index,
|
|
2620
|
+
drawings: layout.descriptor.capabilities.drawings ? getDrawingsForChart(drawings, layout.chart.id) : []
|
|
2621
|
+
})).filter((entry) => entry.drawings.length > 0);
|
|
2622
|
+
if (!drawableLayouts.length) return "";
|
|
2623
|
+
const definitions = drawableLayouts.map(({ layout, index }) => {
|
|
2624
|
+
const clipId = `${this.controllerId}-drawing-clip-${layout.fullscreen ? "fullscreen" : "grid"}-${index}`;
|
|
2625
|
+
return `<clipPath id="${clipId}"><rect data-drawing-clip="${escapeHtml(layout.chart.id)}" x="${layout.plot.x}" y="${layout.plot.y}" width="${layout.plot.width}" height="${layout.plot.height}"/></clipPath>`;
|
|
2626
|
+
}).join("");
|
|
2627
|
+
const body = drawableLayouts.map(({ layout, index, drawings: chartDrawings }) => {
|
|
2628
|
+
const clipId = `${this.controllerId}-drawing-clip-${layout.fullscreen ? "fullscreen" : "grid"}-${index}`;
|
|
2629
|
+
const chartBody = chartDrawings.map((drawing) => {
|
|
2630
|
+
const geometry = getDrawingGeometry({ drawing, layout, projectPoint: (point) => this.projectPoint(point, layout) });
|
|
2631
|
+
if (!geometry) return "";
|
|
2632
|
+
const selected = drawing.id === this.selectedDrawingId || drawing.id === "__draft__";
|
|
2633
|
+
if (drawing.type === "pin") {
|
|
2634
|
+
const text = escapeHtml(geometry.style.text?.trim() || "");
|
|
2635
|
+
return `<g opacity="${drawing.id === "__draft__" ? 0.72 : 1}"><g transform="translate(${geometry.start.x} ${geometry.start.y})"><path d="M0 -11 C-6 -11 -10 -6 -10 -1 C-10 6 -2 12 0 15 C2 12 10 6 10 -1 C10 -6 6 -11 0 -11 Z" fill="${geometry.style.color}" stroke="var(--background,#fff)" stroke-width="1.5"/><circle cy="-2" r="3" fill="var(--background,#fff)"/></g>${text ? `<text x="${geometry.start.x + 14}" y="${geometry.start.y}" fill="${geometry.style.color}" stroke="var(--background,#fff)" stroke-width="3" paint-order="stroke" font-size="12" font-weight="600">${text}</text>` : ""}${selected ? `<circle cx="${geometry.start.x}" cy="${geometry.start.y}" r="4" fill="${geometry.style.color}" stroke="var(--background,#fff)"/>` : ""}</g>`;
|
|
2636
|
+
}
|
|
2637
|
+
return `<g><line data-drawing-line x1="${geometry.lineStart.x}" y1="${geometry.lineStart.y}" x2="${geometry.lineEnd.x}" y2="${geometry.lineEnd.y}" stroke="${geometry.style.color}" stroke-width="${geometry.style.lineWidth}" stroke-dasharray="${geometry.style.dashPattern.join(" ")}" opacity="${drawing.id === "__draft__" ? 0.72 : 1}"/>${selected ? `<circle data-drawing-anchor="start" cx="${geometry.start.x}" cy="${geometry.start.y}" r="4" fill="${geometry.style.color}" stroke="var(--background,#fff)"/>${drawing.type === "trendline" ? `<circle data-drawing-anchor="end" cx="${geometry.end.x}" cy="${geometry.end.y}" r="4" fill="${geometry.style.color}" stroke="var(--background,#fff)"/>` : ""}` : ""}</g>`;
|
|
2638
|
+
}).join("");
|
|
2639
|
+
return `<g data-drawing-chart="${escapeHtml(layout.chart.id)}" clip-path="url(#${clipId})">${chartBody}</g>`;
|
|
2640
|
+
}).join("");
|
|
2641
|
+
return `<svg class="pointer-events-none absolute left-0 top-0 z-20 block overflow-visible" style="width:100%;height:100%"><defs>${definitions}</defs>${body}</svg>`;
|
|
2642
|
+
}
|
|
2643
|
+
topMarkersHtml(layouts) {
|
|
2644
|
+
if (!this.options.topMarkers?.length) return "";
|
|
2645
|
+
return layouts.flatMap((layout) => !layout.descriptor.capabilities.markers ? [] : this.options.topMarkers.map((marker, markerIndex) => {
|
|
2646
|
+
const value = Number(marker.x ?? marker.step);
|
|
2647
|
+
const state = this.viewStates.get(layout.chart.id);
|
|
2648
|
+
const ratio = (value - state.xMin) / (state.xMax - state.xMin);
|
|
2649
|
+
if (!Number.isFinite(value) || ratio < 0 || ratio > 1) return "";
|
|
2650
|
+
return `<button type="button" data-marker-index="${markerIndex}" class="pointer-events-auto absolute z-30 -translate-x-1/2 rounded-full" style="left:${layout.plot.x + ratio * layout.plot.width}px;top:${layout.plot.y + 4}px" title="${escapeHtml(marker.title || "")}" aria-label="${escapeHtml(marker.ariaLabel || marker.title || "Chart marker")}"><span class="block size-2.5 rounded-full border border-background shadow-sm" style="background:${marker.color || "#f97316"}"></span></button>`;
|
|
2651
|
+
})).join("");
|
|
2652
|
+
}
|
|
2653
|
+
crosshairHtml() {
|
|
2654
|
+
const crosshair = this.options.showTooltips ? this.crosshair : null;
|
|
2655
|
+
if (!crosshair) return "";
|
|
2656
|
+
const chart = this.options.charts.find(
|
|
2657
|
+
(item) => item.id === crosshair.chartId
|
|
2658
|
+
);
|
|
2659
|
+
const xAxisLabel = chart?.xAxisLabel ?? this.options.xAxisLabel;
|
|
2660
|
+
return `<div class="pointer-events-none absolute z-20 border-l border-foreground/40" style="left:${crosshair.x}px;top:0;height:100%"></div><div class="pointer-events-none absolute z-20 border-t border-foreground/40" style="top:${crosshair.y}px;left:0;width:100%"></div>
|
|
2661
|
+
${crosshair.points.map((point) => `<div class="pointer-events-none absolute z-30 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-background" style="left:${point.x}px;top:${point.y}px;background:${point.color}"></div>`).join("")}
|
|
2662
|
+
<div data-crosshair-tooltip class="pointer-events-none absolute z-30 w-[220px] rounded-sm border border-border/70 bg-popover/80 px-2 py-1.5 text-xs text-popover-foreground shadow-sm backdrop-blur-sm" style="left:${crosshair.tooltipX}px;top:${crosshair.tooltipY}px"><div class="mb-1 font-medium">${escapeHtml(xAxisLabel)}: ${escapeHtml(crosshair.categoryLabel ?? this.options.formatXValue(crosshair.xValue))}</div>${crosshair.points.map((point) => `<div class="grid grid-cols-[auto_1fr] gap-x-2"><span class="mt-1 size-2 rounded-full" style="background:${point.color}"></span><div class="min-w-0"><div class="truncate text-muted-foreground">${escapeHtml(point.name)}</div><div class="tabular-nums">${escapeHtml(this.options.formatYValue(point.yValue))}</div></div></div>`).join("")}</div>`;
|
|
2663
|
+
}
|
|
2664
|
+
rectangleOverlayHtml() {
|
|
2665
|
+
const rect = this.rectangleZoomRect && normalizeRect(this.rectangleZoomRect.start, this.rectangleZoomRect.end);
|
|
2666
|
+
return rect ? `<div class="pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/10" style="left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px"></div>` : "";
|
|
2667
|
+
}
|
|
2668
|
+
drawingOptionsHtml(layouts) {
|
|
2669
|
+
const drawing = this.drawings.find((item) => item.id === this.selectedDrawingId);
|
|
2670
|
+
if (!drawing) return "";
|
|
2671
|
+
const layout = layouts.find((item) => item.chart.id === drawing.chartId);
|
|
2672
|
+
if (!layout || !layout.descriptor.capabilities.drawings) return "";
|
|
2673
|
+
const style = drawing.style || {};
|
|
2674
|
+
const canExtend = drawing.type === "trendline" || drawing.type === "hline";
|
|
2675
|
+
const canText = drawing.type === "pin";
|
|
2676
|
+
const left = Math.max(
|
|
2677
|
+
layout.rect.x + 42,
|
|
2678
|
+
layout.rect.x + layout.rect.width - PLOT_PADDING.right - 8
|
|
2679
|
+
);
|
|
2680
|
+
return `<div data-drawing-options class="pointer-events-auto absolute z-40 flex items-center gap-1 rounded-sm bg-background/85 p-1 text-xs text-foreground shadow-sm backdrop-blur" style="left:${left}px;top:${layout.plot.y + 6}px;transform:translateX(-100%)">
|
|
2681
|
+
${canExtend ? `<button type="button" data-action="extend-drawing" class="h-6 rounded-sm px-2 text-[11px] font-semibold ${style.extendRight ? "bg-primary/15 text-primary ring-1 ring-primary/40" : ""}">Ext</button>` : ""}
|
|
2682
|
+
${canText ? `<input data-pin-text type="text" value="${escapeHtml(style.text || "")}" placeholder="Text" class="h-6 w-24 rounded-sm border border-border/50 px-1.5 text-xs"/>` : ""}
|
|
2683
|
+
<input data-drawing-color type="color" value="${/^#[0-9a-f]{6}$/i.test(style.color || "") ? style.color : "#60a5fa"}" title="Line color" class="size-6 rounded-sm border border-border/70"/>
|
|
2684
|
+
</div>`;
|
|
2685
|
+
}
|
|
2686
|
+
projectPoint(point, layout) {
|
|
2687
|
+
const state = this.viewStates.get(layout.chart.id);
|
|
2688
|
+
const range = this.getRange(layout, state);
|
|
2689
|
+
return this.getTransform(layout, state, range).dataToScreen(point);
|
|
2690
|
+
}
|
|
2691
|
+
getRange(layout, state = this.viewStates.get(layout.chart.id)) {
|
|
2692
|
+
return applyYScale(
|
|
2693
|
+
getYRange(
|
|
2694
|
+
layout.chart,
|
|
2695
|
+
state.xMin,
|
|
2696
|
+
state.xMax,
|
|
2697
|
+
getCategoryPixelLength(
|
|
2698
|
+
layout.chart,
|
|
2699
|
+
layout.plot,
|
|
2700
|
+
layout.descriptor
|
|
2701
|
+
),
|
|
2702
|
+
layout.descriptor
|
|
2703
|
+
),
|
|
2704
|
+
this.yScales.get(layout.chart.id),
|
|
2705
|
+
this.yOffsets.get(layout.chart.id)
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
getTransform(layout, state, range) {
|
|
2709
|
+
return createCoordinateTransform({
|
|
2710
|
+
orientation: layout.descriptor.orientation,
|
|
2711
|
+
categoryRange: { min: state.xMin, max: state.xMax },
|
|
2712
|
+
valueRange: { min: range.minY, max: range.maxY },
|
|
2713
|
+
plot: layout.plot
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
pointForEvent(event, host) {
|
|
2717
|
+
const rect = host.getBoundingClientRect();
|
|
2718
|
+
return { x: event.clientX - rect.left + host.scrollLeft, y: event.clientY - rect.top + host.scrollTop };
|
|
2719
|
+
}
|
|
2720
|
+
eventContext(event) {
|
|
2721
|
+
const cell = event.target.closest?.("[data-chart-index]");
|
|
2722
|
+
if (!cell) return null;
|
|
2723
|
+
const fullscreen = Boolean(cell.closest("[data-aliencharts-fullscreen]"));
|
|
2724
|
+
const layouts = fullscreen ? this.fullscreenLayouts : this.layouts;
|
|
2725
|
+
const index = Number(cell.dataset.chartIndex);
|
|
2726
|
+
const layout = fullscreen ? layouts[0] : layouts[index];
|
|
2727
|
+
const host = fullscreen ? this.fullscreenOverlay : this.container;
|
|
2728
|
+
return layout ? { layout, host, point: this.pointForEvent(event, host), fullscreen } : null;
|
|
2729
|
+
}
|
|
2730
|
+
handleClick(event) {
|
|
2731
|
+
const marker = event.target.closest?.("[data-marker-index]");
|
|
2732
|
+
if (marker) {
|
|
2733
|
+
event.stopPropagation();
|
|
2734
|
+
this.options.onTopMarkerClick?.(this.options.topMarkers[Number(marker.dataset.markerIndex)]);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const button = event.target.closest?.("[data-action]");
|
|
2738
|
+
if (!button) return;
|
|
2739
|
+
event.preventDefault();
|
|
2740
|
+
event.stopPropagation();
|
|
2741
|
+
const context = this.eventContext(event);
|
|
2742
|
+
const chartId = context?.layout.chart.id || this.focusedChartId || this.fullscreenChartId;
|
|
2743
|
+
switch (button.dataset.action) {
|
|
2744
|
+
case "fullscreen":
|
|
2745
|
+
this.fullscreenChartId ? this.closeFullscreen() : this.openFullscreen(chartId);
|
|
2746
|
+
break;
|
|
2747
|
+
case "reset":
|
|
2748
|
+
this.resetChart(chartId);
|
|
2749
|
+
break;
|
|
2750
|
+
case "jump-latest":
|
|
2751
|
+
this.jumpToLatest(chartId);
|
|
2752
|
+
break;
|
|
2753
|
+
case "rectangle-zoom":
|
|
2754
|
+
this.rectangleZoomChartId = this.rectangleZoomChartId === chartId ? null : chartId;
|
|
2755
|
+
this.activeDrawingTool = null;
|
|
2756
|
+
this.emit("onActiveDrawingToolChange", null);
|
|
2757
|
+
this.requestRender();
|
|
2758
|
+
break;
|
|
2759
|
+
case "clear-drawings":
|
|
2760
|
+
this.options.onClearDrawingsRequest ? this.emit("onClearDrawingsRequest", chartId) : this.setDrawings(this.drawings.filter((item) => item.chartId !== chartId));
|
|
2761
|
+
break;
|
|
2762
|
+
case "moving-average":
|
|
2763
|
+
this.toggleMovingAverage(chartId);
|
|
2764
|
+
break;
|
|
2765
|
+
case "tool-trendline":
|
|
2766
|
+
this.toggleDrawingTool("trendline");
|
|
2767
|
+
break;
|
|
2768
|
+
case "tool-hline":
|
|
2769
|
+
this.toggleDrawingTool("hline");
|
|
2770
|
+
break;
|
|
2771
|
+
case "tool-vline":
|
|
2772
|
+
this.toggleDrawingTool("vline");
|
|
2773
|
+
break;
|
|
2774
|
+
case "tool-pin":
|
|
2775
|
+
this.toggleDrawingTool("pin");
|
|
2776
|
+
break;
|
|
2777
|
+
case "toggle-base":
|
|
2778
|
+
this.updateMovingAverage(chartId, { hideBase: !this.movingAverageByChart[chartId]?.hideBase });
|
|
2779
|
+
break;
|
|
2780
|
+
case "moving-type":
|
|
2781
|
+
this.updateMovingAverage(chartId, { type: this.movingAverageByChart[chartId]?.type === "sma" ? "ema" : "sma" });
|
|
2782
|
+
break;
|
|
2783
|
+
case "extend-drawing":
|
|
2784
|
+
this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, extendRight: drawing.style?.extendRight === false } }));
|
|
2785
|
+
break;
|
|
2786
|
+
default:
|
|
2787
|
+
break;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
handleInput(event) {
|
|
2791
|
+
if (event.target.matches("[data-moving-period]")) {
|
|
2792
|
+
const context = this.eventContext(event);
|
|
2793
|
+
this.updateMovingAverage(context?.layout.chart.id, { period: Math.max(1, Math.round(Number(event.target.value) || 1)) });
|
|
2794
|
+
} else if (event.target.matches("[data-pin-text]")) {
|
|
2795
|
+
this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, text: event.target.value } }));
|
|
2796
|
+
} else if (event.target.matches("[data-drawing-color]")) {
|
|
2797
|
+
this.updateSelectedDrawing((drawing) => ({ ...drawing, style: { ...drawing.style, color: event.target.value } }));
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
handlePointerDown(event) {
|
|
2801
|
+
if (event.target.closest?.("button,input")) return;
|
|
2802
|
+
if (event.button !== 0) return;
|
|
2803
|
+
const context = this.eventContext(event);
|
|
2804
|
+
if (!context) return;
|
|
2805
|
+
const { layout, point } = context;
|
|
2806
|
+
this.activePointerContext = context;
|
|
2807
|
+
this.focusedChartId = layout.chart.id;
|
|
2808
|
+
const inPlot = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
|
|
2809
|
+
if (inPlot && this.activeDrawingTool && !this.options.disableDrawings && layout.descriptor.capabilities.drawings) {
|
|
2810
|
+
const dataPoint = screenPointToDataPoint({ point, chart: layout.chart, descriptor: layout.descriptor, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
|
|
2811
|
+
if (["hline", "vline", "pin"].includes(this.activeDrawingTool)) {
|
|
2812
|
+
this.commitDrawing(layout.chart.id, this.activeDrawingTool, dataPoint, dataPoint);
|
|
2813
|
+
} else if (this.drawingSession?.chartId === layout.chart.id) {
|
|
2814
|
+
this.commitDrawing(layout.chart.id, "trendline", this.drawingSession.start, dataPoint);
|
|
2815
|
+
} else {
|
|
2816
|
+
this.drawingSession = { chartId: layout.chart.id, type: "trendline", start: dataPoint };
|
|
2817
|
+
this.draftDrawing = createDraftDrawing({ chartId: layout.chart.id, type: "trendline", start: dataPoint, end: dataPoint });
|
|
2818
|
+
}
|
|
2819
|
+
} else if (inPlot && this.rectangleZoomChartId === layout.chart.id) {
|
|
2820
|
+
this.drag = { type: "rectangle", layout, start: point };
|
|
2821
|
+
this.rectangleZoomRect = { start: point, end: point };
|
|
2822
|
+
} else if (inPlot) {
|
|
2823
|
+
const hit = layout.descriptor.capabilities.drawings ? this.hitDrawing(point, layout) : null;
|
|
2824
|
+
if (hit) {
|
|
2825
|
+
this.selectedDrawingId = hit.drawing.id;
|
|
2826
|
+
this.emit("onSelectedDrawingIdChange", hit.drawing.id);
|
|
2827
|
+
if (hit.endpoint != null || ["hline", "vline", "pin"].includes(hit.drawing.type)) {
|
|
2828
|
+
this.drag = { type: "drawing-edit", layout, drawingId: hit.drawing.id, endpoint: hit.endpoint };
|
|
2829
|
+
}
|
|
2830
|
+
} else {
|
|
2831
|
+
this.selectedDrawingId = null;
|
|
2832
|
+
this.emit("onSelectedDrawingIdChange", null);
|
|
2833
|
+
this.drag = { type: "pan", layout, start: point, state: { ...this.viewStates.get(layout.chart.id) }, yOffset: this.yOffsets.get(layout.chart.id) || 0 };
|
|
2834
|
+
}
|
|
2835
|
+
} else if (layout.descriptor.orientation === "vertical" && point.x >= layout.plot.x + layout.plot.width) {
|
|
2836
|
+
this.drag = { type: "y-scale", layout, start: point, scale: this.yScales.get(layout.chart.id) || 1 };
|
|
2837
|
+
} else if (layout.descriptor.orientation === "horizontal" && point.y >= layout.plot.y + layout.plot.height) {
|
|
2838
|
+
this.drag = { type: "y-scale", layout, start: point, scale: this.yScales.get(layout.chart.id) || 1 };
|
|
2839
|
+
} else if (layout.descriptor.orientation === "vertical" && point.y >= layout.plot.y + layout.plot.height) {
|
|
2840
|
+
this.drag = { type: "x-scale", layout, start: point, state: { ...this.viewStates.get(layout.chart.id) } };
|
|
2841
|
+
} else if (layout.descriptor.orientation === "horizontal" && point.x <= layout.plot.x) {
|
|
2842
|
+
this.drag = { type: "x-scale", layout, start: point, state: { ...this.viewStates.get(layout.chart.id) } };
|
|
2843
|
+
}
|
|
2844
|
+
context.host.setPointerCapture?.(event.pointerId);
|
|
2845
|
+
this.pointerCaptureTarget = context.host;
|
|
2846
|
+
this.requestRender();
|
|
2847
|
+
}
|
|
2848
|
+
handlePointerMove(event) {
|
|
2849
|
+
if (event.target.closest?.("button,input") && !this.drag && !this.drawingSession) return;
|
|
2850
|
+
const context = this.eventContext(event) || (this.activePointerContext && (this.drag || this.drawingSession) ? { ...this.activePointerContext, point: this.pointForEvent(event, this.activePointerContext.host) } : null);
|
|
2851
|
+
if (!context) return this.clearCrosshair();
|
|
2852
|
+
const { layout, point } = context;
|
|
2853
|
+
if (this.drawingSession) {
|
|
2854
|
+
if (layout.chart.id !== this.drawingSession.chartId) return;
|
|
2855
|
+
const end = screenPointToDataPoint({ point, chart: layout.chart, descriptor: layout.descriptor, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
|
|
2856
|
+
this.draftDrawing = createDraftDrawing({ ...this.drawingSession, end });
|
|
2857
|
+
this.requestRender();
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
if (this.drag) {
|
|
2861
|
+
const { chart, descriptor, plot } = this.drag.layout;
|
|
2862
|
+
if (this.drag.type === "drawing-edit") {
|
|
2863
|
+
const dataPoint = screenPointToDataPoint({ point, chart, descriptor, plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } });
|
|
2864
|
+
const next = updateDrawingById(this.drawings, this.drag.drawingId, (drawing) => {
|
|
2865
|
+
if (drawing.type === "hline") {
|
|
2866
|
+
const deltaX = (drawing.end?.x ?? drawing.start.x) - drawing.start.x;
|
|
2867
|
+
return { ...drawing, start: { x: dataPoint.x, y: dataPoint.y }, end: { x: dataPoint.x + deltaX, y: dataPoint.y } };
|
|
2868
|
+
}
|
|
2869
|
+
if (drawing.type === "vline") return { ...drawing, start: { ...drawing.start, x: dataPoint.x }, end: { ...drawing.end, x: dataPoint.x } };
|
|
2870
|
+
if (drawing.type === "pin") return { ...drawing, start: dataPoint, end: dataPoint };
|
|
2871
|
+
if (this.drag.endpoint === "start" || this.drag.endpoint === "end") return { ...drawing, [this.drag.endpoint]: dataPoint };
|
|
2872
|
+
return drawing;
|
|
2873
|
+
});
|
|
2874
|
+
this.drawings = next;
|
|
2875
|
+
this.emit("onDrawingsChange", [...next]);
|
|
2876
|
+
this.requestRender();
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (this.drag.type === "rectangle") this.rectangleZoomRect = { start: this.drag.start, end: point };
|
|
2880
|
+
if (this.drag.type === "pan") {
|
|
2881
|
+
const span = this.drag.state.xMax - this.drag.state.xMin;
|
|
2882
|
+
const baseRange = getYRange(
|
|
2883
|
+
chart,
|
|
2884
|
+
this.drag.state.xMin,
|
|
2885
|
+
this.drag.state.xMax,
|
|
2886
|
+
getCategoryPixelLength(chart, plot, descriptor),
|
|
2887
|
+
descriptor
|
|
2888
|
+
);
|
|
2889
|
+
const rangeSpan = (baseRange.maxY - baseRange.minY) * (this.yScales.get(chart.id) || 1);
|
|
2890
|
+
const transform = createCoordinateTransform({
|
|
2891
|
+
orientation: descriptor.orientation,
|
|
2892
|
+
categoryRange: {
|
|
2893
|
+
min: this.drag.state.xMin,
|
|
2894
|
+
max: this.drag.state.xMax
|
|
2895
|
+
},
|
|
2896
|
+
valueRange: { min: 0, max: rangeSpan },
|
|
2897
|
+
plot
|
|
2898
|
+
});
|
|
2899
|
+
const categoryDelta = transform.categoryDragDelta(
|
|
2900
|
+
this.drag.start,
|
|
2901
|
+
point
|
|
2902
|
+
);
|
|
2903
|
+
const valueDelta = transform.valueOffsetDelta(
|
|
2904
|
+
this.drag.start,
|
|
2905
|
+
point
|
|
2906
|
+
);
|
|
2907
|
+
this.viewStates.set(chart.id, { xMin: this.drag.state.xMin - categoryDelta, xMax: this.drag.state.xMax - categoryDelta });
|
|
2908
|
+
this.yOffsets.set(chart.id, this.drag.yOffset + valueDelta);
|
|
2909
|
+
}
|
|
2910
|
+
if (this.drag.type === "y-scale") {
|
|
2911
|
+
const transform = createCoordinateTransform({
|
|
2912
|
+
orientation: descriptor.orientation,
|
|
2913
|
+
categoryRange: { min: 0, max: 1 },
|
|
2914
|
+
valueRange: { min: 0, max: 1 },
|
|
2915
|
+
plot
|
|
2916
|
+
});
|
|
2917
|
+
const delta = transform.valueScaleDelta(this.drag.start, point);
|
|
2918
|
+
this.yScales.set(
|
|
2919
|
+
chart.id,
|
|
2920
|
+
scaleValueRange(
|
|
2921
|
+
this.drag.scale,
|
|
2922
|
+
delta,
|
|
2923
|
+
POINTER_SCALE_SENSITIVITY
|
|
2924
|
+
)
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
if (this.drag.type === "x-scale") {
|
|
2928
|
+
const center = (this.drag.state.xMin + this.drag.state.xMax) / 2;
|
|
2929
|
+
const transform = createCoordinateTransform({
|
|
2930
|
+
orientation: descriptor.orientation,
|
|
2931
|
+
categoryRange: { min: 0, max: 1 },
|
|
2932
|
+
valueRange: { min: 0, max: 1 },
|
|
2933
|
+
plot
|
|
2934
|
+
});
|
|
2935
|
+
const delta = transform.categoryScaleDelta(this.drag.start, point);
|
|
2936
|
+
const span = clamp(
|
|
2937
|
+
(this.drag.state.xMax - this.drag.state.xMin) * Math.exp(delta * 0.01),
|
|
2938
|
+
getMinimumCategorySpan(chart),
|
|
2939
|
+
X_SCALE_MAX_SPAN
|
|
2940
|
+
);
|
|
2941
|
+
this.viewStates.set(chart.id, { xMin: center - span / 2, xMax: center + span / 2 });
|
|
2942
|
+
}
|
|
2943
|
+
if (this.drag.type === "pan") {
|
|
2944
|
+
this.updateCrosshair(point, layout);
|
|
2945
|
+
this.requestRender();
|
|
2946
|
+
} else {
|
|
2947
|
+
this.requestRender();
|
|
2948
|
+
}
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
this.updateCrosshair(point, layout, context.fullscreen);
|
|
2952
|
+
}
|
|
2953
|
+
handlePointerUp(event) {
|
|
2954
|
+
if (event.target.closest?.("button,input") && !this.drag && !this.drawingSession) return;
|
|
2955
|
+
const context = this.eventContext(event) || (this.activePointerContext ? { ...this.activePointerContext, point: this.pointForEvent(event, this.activePointerContext.host) } : null);
|
|
2956
|
+
if (this.drag?.type === "rectangle" && this.rectangleZoomRect) {
|
|
2957
|
+
const normalized = normalizeRect(this.rectangleZoomRect.start, this.rectangleZoomRect.end);
|
|
2958
|
+
if (normalized?.width >= 4 && normalized?.height >= 4) {
|
|
2959
|
+
const layout = this.drag.layout;
|
|
2960
|
+
applyRectangleZoom({
|
|
2961
|
+
chart: layout.chart,
|
|
2962
|
+
descriptor: layout.descriptor,
|
|
2963
|
+
plot: layout.plot,
|
|
2964
|
+
start: this.rectangleZoomRect.start,
|
|
2965
|
+
end: this.rectangleZoomRect.end,
|
|
2966
|
+
initialVisiblePoints: this.options.initialVisiblePoints,
|
|
2967
|
+
viewStateRef: { current: this.viewStates },
|
|
2968
|
+
yScaleRef: { current: this.yScales },
|
|
2969
|
+
yCenterOffsetRef: { current: this.yOffsets },
|
|
2970
|
+
yManualScaleRef: { current: /* @__PURE__ */ new Set() }
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
this.rectangleZoomChartId = null;
|
|
2974
|
+
this.rectangleZoomRect = null;
|
|
2975
|
+
}
|
|
2976
|
+
this.drag = null;
|
|
2977
|
+
this.pointerCaptureTarget?.releasePointerCapture?.(event.pointerId);
|
|
2978
|
+
this.pointerCaptureTarget = null;
|
|
2979
|
+
this.activePointerContext = null;
|
|
2980
|
+
this.requestRender();
|
|
2981
|
+
}
|
|
2982
|
+
handleWheel(event) {
|
|
2983
|
+
const context = this.eventContext(event);
|
|
2984
|
+
if (!context) return;
|
|
2985
|
+
const { layout, point } = context;
|
|
2986
|
+
const inPlot = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
|
|
2987
|
+
if (!inPlot) return;
|
|
2988
|
+
event.preventDefault();
|
|
2989
|
+
if (event.shiftKey) {
|
|
2990
|
+
const wheelDelta = event.deltaY || event.deltaX;
|
|
2991
|
+
const currentScale = this.yScales.get(layout.chart.id) || 1;
|
|
2992
|
+
this.yScales.set(
|
|
2993
|
+
layout.chart.id,
|
|
2994
|
+
scaleValueRange(
|
|
2995
|
+
currentScale,
|
|
2996
|
+
wheelDelta,
|
|
2997
|
+
WHEEL_SCALE_SENSITIVITY
|
|
2998
|
+
)
|
|
2999
|
+
);
|
|
3000
|
+
this.requestRender();
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
const state = this.viewStates.get(layout.chart.id);
|
|
3004
|
+
const range = this.getRange(layout, state);
|
|
3005
|
+
const ratio = clamp(
|
|
3006
|
+
this.getTransform(layout, state, range).categoryRatio(point),
|
|
3007
|
+
0,
|
|
3008
|
+
1
|
|
3009
|
+
);
|
|
3010
|
+
const anchor = state.xMin + ratio * (state.xMax - state.xMin);
|
|
3011
|
+
const zoom = Math.exp(event.deltaY * 1e-3);
|
|
3012
|
+
const span = clamp(
|
|
3013
|
+
(state.xMax - state.xMin) * zoom,
|
|
3014
|
+
getMinimumCategorySpan(layout.chart),
|
|
3015
|
+
X_SCALE_MAX_SPAN
|
|
3016
|
+
);
|
|
3017
|
+
this.viewStates.set(layout.chart.id, { xMin: anchor - ratio * span, xMax: anchor + (1 - ratio) * span });
|
|
3018
|
+
this.requestRender();
|
|
3019
|
+
}
|
|
3020
|
+
handleContextMenu(event) {
|
|
3021
|
+
const context = this.eventContext(event);
|
|
3022
|
+
if (!context || !this.options.onChartContextMenu) return;
|
|
3023
|
+
event.preventDefault();
|
|
3024
|
+
const { layout, point } = context;
|
|
3025
|
+
const inPlot = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
|
|
3026
|
+
const data = inPlot ? screenPointToDataPoint({ point, chart: layout.chart, descriptor: layout.descriptor, plot: layout.plot, initialVisiblePoints: this.options.initialVisiblePoints, viewStateRef: { current: this.viewStates }, yScaleRef: { current: this.yScales }, yCenterOffsetRef: { current: this.yOffsets } }) : null;
|
|
3027
|
+
this.options.onChartContextMenu({ chart: layout.chart, event, point: data });
|
|
3028
|
+
}
|
|
3029
|
+
updateCrosshair(point, layout) {
|
|
3030
|
+
const inPlot = point.x >= layout.plot.x && point.x <= layout.plot.x + layout.plot.width && point.y >= layout.plot.y && point.y <= layout.plot.y + layout.plot.height;
|
|
3031
|
+
if (!inPlot || !this.options.showTooltips) return this.clearCrosshair();
|
|
3032
|
+
const state = this.viewStates.get(layout.chart.id);
|
|
3033
|
+
const orientation = layout.descriptor.orientation;
|
|
3034
|
+
const range = this.getRange(layout, state);
|
|
3035
|
+
const transform = this.getTransform(layout, state, range);
|
|
3036
|
+
const xValue = transform.screenToData(point).x;
|
|
3037
|
+
const tooltipSeries = getOrderedSeries(
|
|
3038
|
+
layout.chart,
|
|
3039
|
+
this.options.seriesOrderByChart
|
|
3040
|
+
);
|
|
3041
|
+
const points = tooltipSeries.map((series, seriesIndex) => {
|
|
3042
|
+
const visible = series.getVisiblePoints(state.xMin, state.xMax, transform.categoryPixelLength);
|
|
3043
|
+
if (!visible.pointCount) return null;
|
|
3044
|
+
const index = getNearestPointIndex(visible.x, xValue);
|
|
3045
|
+
const category = this.surface.renderers.getTooltipCategory(
|
|
3046
|
+
layout.descriptor.rendererType,
|
|
3047
|
+
{
|
|
3048
|
+
visiblePoints: visible,
|
|
3049
|
+
pointIndex: index,
|
|
3050
|
+
seriesIndex,
|
|
3051
|
+
seriesCount: tooltipSeries.length,
|
|
3052
|
+
categoryMin: state.xMin,
|
|
3053
|
+
categoryMax: state.xMax
|
|
3054
|
+
}
|
|
3055
|
+
);
|
|
3056
|
+
const data = { x: category, y: visible.y[index] };
|
|
3057
|
+
const screen = transform.dataToScreen(data);
|
|
3058
|
+
return { id: series.id, name: series.name, color: series.color, x: screen.x, y: screen.y, xValue: visible.x[index], yValue: data.y };
|
|
3059
|
+
}).filter(Boolean);
|
|
3060
|
+
if (!points.length) return this.clearCrosshair();
|
|
3061
|
+
const nearest = points.reduce((best, item) => {
|
|
3062
|
+
const itemDistance = orientation === "horizontal" ? Math.abs(item.y - point.y) : Math.abs(item.x - point.x);
|
|
3063
|
+
const bestDistance = orientation === "horizontal" ? Math.abs(best.y - point.y) : Math.abs(best.x - point.x);
|
|
3064
|
+
return itemDistance < bestDistance ? item : best;
|
|
3065
|
+
});
|
|
3066
|
+
this.crosshair = {
|
|
3067
|
+
chartId: layout.chart.id,
|
|
3068
|
+
x: point.x,
|
|
3069
|
+
y: point.y,
|
|
3070
|
+
xValue: nearest.xValue,
|
|
3071
|
+
categoryLabel: getCategoryLabel(layout.chart, nearest.xValue),
|
|
3072
|
+
points,
|
|
3073
|
+
tooltipX: point.x + 232 > layout.rect.x + layout.rect.width ? point.x - 232 : point.x + 12,
|
|
3074
|
+
tooltipY: clamp(point.y + 12, layout.plot.y, layout.rect.y + layout.rect.height - 96)
|
|
3075
|
+
};
|
|
3076
|
+
this.requestRender();
|
|
3077
|
+
}
|
|
3078
|
+
clearCrosshair() {
|
|
3079
|
+
if (!this.crosshair) return;
|
|
3080
|
+
this.crosshair = null;
|
|
3081
|
+
this.requestRender();
|
|
3082
|
+
}
|
|
3083
|
+
hitDrawing(point, layout) {
|
|
3084
|
+
for (const drawing of getDrawingsForChart(this.drawings, layout.chart.id)) {
|
|
3085
|
+
const hit = hitTestDrawing({ point, drawing, layout, projectPoint: (data) => this.projectPoint(data, layout) });
|
|
3086
|
+
if (hit) return hit;
|
|
3087
|
+
}
|
|
3088
|
+
return null;
|
|
3089
|
+
}
|
|
3090
|
+
commitDrawing(chartId, type, start, end) {
|
|
3091
|
+
const drawing = createDrawing({ chartId, type, start, end, createDrawingId: this.options.createDrawingId });
|
|
3092
|
+
this.setDrawings([...this.drawings, drawing]);
|
|
3093
|
+
this.selectedDrawingId = drawing.id;
|
|
3094
|
+
this.activeDrawingTool = null;
|
|
3095
|
+
this.draftDrawing = null;
|
|
3096
|
+
this.drawingSession = null;
|
|
3097
|
+
this.emit("onSelectedDrawingIdChange", drawing.id);
|
|
3098
|
+
this.emit("onActiveDrawingToolChange", null);
|
|
3099
|
+
}
|
|
3100
|
+
setDrawings(drawings) {
|
|
3101
|
+
this.drawings = drawings;
|
|
3102
|
+
this.emit("onDrawingsChange", [...drawings]);
|
|
3103
|
+
this.requestRender();
|
|
3104
|
+
}
|
|
3105
|
+
updateSelectedDrawing(updater) {
|
|
3106
|
+
if (!this.selectedDrawingId) return;
|
|
3107
|
+
this.setDrawings(updateDrawingById(this.drawings, this.selectedDrawingId, updater));
|
|
3108
|
+
}
|
|
3109
|
+
toggleDrawingTool(tool) {
|
|
3110
|
+
if (!DRAWING_TOOLS.has(tool) || this.options.disableDrawings) return;
|
|
3111
|
+
this.activeDrawingTool = this.activeDrawingTool === tool ? null : tool;
|
|
3112
|
+
this.rectangleZoomChartId = null;
|
|
3113
|
+
this.draftDrawing = null;
|
|
3114
|
+
this.drawingSession = null;
|
|
3115
|
+
this.emit("onActiveDrawingToolChange", this.activeDrawingTool);
|
|
3116
|
+
this.requestRender();
|
|
3117
|
+
}
|
|
3118
|
+
toggleMovingAverage(chartId) {
|
|
3119
|
+
if (!chartId || this.options.disableDrawings) return;
|
|
3120
|
+
const chart = this.options.charts.find((item) => item.id === chartId);
|
|
3121
|
+
if (!chart || !this.chartDescriptors.get(chartId)?.capabilities.movingAverage) return;
|
|
3122
|
+
const current = this.movingAverageByChart[chartId] || { period: 21, type: "ema", hideBase: false };
|
|
3123
|
+
this.movingAverageByChart = { ...this.movingAverageByChart, [chartId]: { ...current, enabled: !current.enabled } };
|
|
3124
|
+
this.emit("onMovingAverageToggle", chartId);
|
|
3125
|
+
this.requestRender();
|
|
3126
|
+
}
|
|
3127
|
+
updateMovingAverage(chartId, changes) {
|
|
3128
|
+
if (!chartId) return;
|
|
3129
|
+
const next = { enabled: true, period: 21, type: "ema", hideBase: false, ...this.movingAverageByChart[chartId] || {}, ...changes };
|
|
3130
|
+
this.movingAverageByChart = { ...this.movingAverageByChart, [chartId]: next };
|
|
3131
|
+
this.emit("onMovingAverageChange", chartId, { ...next });
|
|
3132
|
+
this.requestRender();
|
|
3133
|
+
}
|
|
3134
|
+
resetChart(chartId) {
|
|
3135
|
+
const chart = this.options.charts.find((item) => item.id === chartId);
|
|
3136
|
+
if (!chart) return;
|
|
3137
|
+
this.viewStates.set(chartId, getInitialView(chart, this.options.initialVisiblePoints));
|
|
3138
|
+
this.yScales.set(chartId, 1);
|
|
3139
|
+
this.yOffsets.set(chartId, 0);
|
|
3140
|
+
this.requestRender();
|
|
3141
|
+
}
|
|
3142
|
+
openFullscreen(chartId) {
|
|
3143
|
+
const chart = this.options.charts.find((item) => item.id === chartId);
|
|
3144
|
+
if (!chart || this.fullscreenChartId) return;
|
|
3145
|
+
this.fullscreenChartId = chartId;
|
|
3146
|
+
const overlay = document.createElement("div");
|
|
3147
|
+
overlay.dataset.alienchartsFullscreen = "";
|
|
3148
|
+
overlay.className = "aliencharts-root fixed inset-0 z-[60] bg-background p-2";
|
|
3149
|
+
overlay.innerHTML = `<canvas class="pointer-events-none absolute left-0 top-0 z-10 block"></canvas><div data-fullscreen-cell>${chartCellHtml(chart, this.chartDescriptors.get(chart.id), 0, this.options.backgroundColor)}</div><div data-fullscreen-overlays class="pointer-events-none absolute left-0 top-0 z-20" style="width:100%;height:100%"></div>`;
|
|
3150
|
+
document.body.append(overlay);
|
|
3151
|
+
this.fullscreenOverlay = overlay;
|
|
3152
|
+
this.fullscreenNode = overlay.querySelector("[data-chart-index]");
|
|
3153
|
+
this.fullscreenNode.style.height = "calc(100vh - 16px)";
|
|
3154
|
+
this.fullscreenOverlays = overlay.querySelector("[data-fullscreen-overlays]");
|
|
3155
|
+
const canvas = overlay.querySelector("canvas");
|
|
3156
|
+
const gl = canvas.getContext("webgl2", { antialias: true, alpha: true, depth: false, stencil: false });
|
|
3157
|
+
if (!gl) {
|
|
3158
|
+
overlay.remove();
|
|
3159
|
+
this.fullscreenChartId = null;
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
this.fullscreenSurface = makeSurface(canvas, gl);
|
|
3163
|
+
this.fullscreenListeners = Object.entries(this.bound).filter(([name]) => name !== "keydown" && name !== "scroll");
|
|
3164
|
+
this.fullscreenListeners.forEach(([name, handler]) => overlay.addEventListener(name, handler, name === "wheel" ? { passive: false } : void 0));
|
|
3165
|
+
this.resizeObserver.observe(overlay);
|
|
3166
|
+
this.requestRender();
|
|
3167
|
+
}
|
|
3168
|
+
closeFullscreen() {
|
|
3169
|
+
if (!this.fullscreenChartId) return;
|
|
3170
|
+
if (this.fullscreenOverlay) this.resizeObserver?.unobserve(this.fullscreenOverlay);
|
|
3171
|
+
this.fullscreenListeners?.forEach(([name, handler]) => this.fullscreenOverlay?.removeEventListener(name, handler));
|
|
3172
|
+
this.fullscreenListeners = null;
|
|
3173
|
+
destroySurface(this.fullscreenSurface);
|
|
3174
|
+
this.fullscreenOverlay?.remove();
|
|
3175
|
+
this.fullscreenSurface = null;
|
|
3176
|
+
this.fullscreenOverlay = null;
|
|
3177
|
+
this.fullscreenNode = null;
|
|
3178
|
+
this.fullscreenOverlays = null;
|
|
3179
|
+
this.fullscreenLayouts = [];
|
|
3180
|
+
this.fullscreenChartId = null;
|
|
3181
|
+
this.requestRender();
|
|
3182
|
+
}
|
|
3183
|
+
handleKeyDown(event) {
|
|
3184
|
+
const editable = event.target instanceof HTMLElement && (event.target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName));
|
|
3185
|
+
if (editable) return;
|
|
3186
|
+
const chartId = this.fullscreenChartId || this.focusedChartId;
|
|
3187
|
+
const chart = this.options.charts.find((item) => item.id === chartId);
|
|
3188
|
+
const capabilities = chart && this.chartDescriptors.get(chartId)?.capabilities;
|
|
3189
|
+
const key = event.key.toLowerCase();
|
|
3190
|
+
if (["arrowleft", "arrowright", "arrowup", "arrowdown"].includes(key) && !this.fullscreenChartId) {
|
|
3191
|
+
this.moveFocus(key.replace("arrow", ""));
|
|
3192
|
+
event.preventDefault();
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
if (key === "r") {
|
|
3196
|
+
this.resetChart(chartId);
|
|
3197
|
+
event.preventDefault();
|
|
3198
|
+
} else if (key === "z") {
|
|
3199
|
+
this.rectangleZoomChartId = this.rectangleZoomChartId === chartId ? null : chartId;
|
|
3200
|
+
event.preventDefault();
|
|
3201
|
+
this.requestRender();
|
|
3202
|
+
} else if (key === "m" && capabilities?.movingAverage) {
|
|
3203
|
+
this.toggleMovingAverage(chartId);
|
|
3204
|
+
event.preventDefault();
|
|
3205
|
+
} else if (["t", "h", "v", "p"].includes(key) && capabilities?.drawings) {
|
|
3206
|
+
this.toggleDrawingTool({ t: "trendline", h: "hline", v: "vline", p: "pin" }[key]);
|
|
3207
|
+
event.preventDefault();
|
|
3208
|
+
} else if ((key === "delete" || key === "backspace") && this.selectedDrawingId) {
|
|
3209
|
+
this.setDrawings(removeDrawingById(this.drawings, this.selectedDrawingId));
|
|
3210
|
+
this.selectedDrawingId = null;
|
|
3211
|
+
this.emit("onSelectedDrawingIdChange", null);
|
|
3212
|
+
event.preventDefault();
|
|
3213
|
+
} else if (key === "escape" && (this.activeDrawingTool || this.draftDrawing || this.rectangleZoomChartId)) {
|
|
3214
|
+
this.activeDrawingTool = null;
|
|
3215
|
+
this.draftDrawing = null;
|
|
3216
|
+
this.drawingSession = null;
|
|
3217
|
+
this.rectangleZoomChartId = null;
|
|
3218
|
+
this.rectangleZoomRect = null;
|
|
3219
|
+
this.emit("onActiveDrawingToolChange", null);
|
|
3220
|
+
this.requestRender();
|
|
3221
|
+
event.preventDefault();
|
|
3222
|
+
} else if (key === "f" || key === "escape" && this.fullscreenChartId) {
|
|
3223
|
+
this.fullscreenChartId ? this.closeFullscreen() : this.openFullscreen(chartId);
|
|
3224
|
+
event.preventDefault();
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
moveFocus(direction) {
|
|
3228
|
+
const charts = this.options.charts;
|
|
3229
|
+
if (!charts.length) return;
|
|
3230
|
+
let index = Math.max(0, charts.findIndex((chart) => chart.id === this.focusedChartId));
|
|
3231
|
+
const columns = Math.max(1, Number(this.options.columns) || 1);
|
|
3232
|
+
if (direction === "left") index = Math.max(Math.floor(index / columns) * columns, index - 1);
|
|
3233
|
+
if (direction === "right") index = Math.min(charts.length - 1, index + 1);
|
|
3234
|
+
if (direction === "up") index = Math.max(0, index - columns);
|
|
3235
|
+
if (direction === "down") index = Math.min(charts.length - 1, index + columns);
|
|
3236
|
+
this.focusedChartId = charts[index].id;
|
|
3237
|
+
this.chartNodes[index]?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
3238
|
+
this.requestRender();
|
|
3239
|
+
}
|
|
3240
|
+
emit(name, ...args) {
|
|
3241
|
+
this.options[name]?.(...args);
|
|
3242
|
+
}
|
|
3243
|
+
cleanupDom() {
|
|
3244
|
+
this.canvas?.remove();
|
|
3245
|
+
this.grid?.remove();
|
|
3246
|
+
this.overlays?.remove();
|
|
3247
|
+
this.addedClasses?.forEach((name) => this.container.classList.remove(name));
|
|
3248
|
+
}
|
|
3249
|
+
destroy() {
|
|
3250
|
+
if (this.destroyed) return;
|
|
3251
|
+
this.closeFullscreen();
|
|
3252
|
+
this.destroyed = true;
|
|
3253
|
+
if (this.frame != null) cancelAnimationFrame(this.frame);
|
|
3254
|
+
this.animator.destroy();
|
|
3255
|
+
this.resizeObserver.disconnect();
|
|
3256
|
+
Object.entries(this.bound).forEach(([name, handler]) => {
|
|
3257
|
+
const target = name === "keydown" ? window : this.container;
|
|
3258
|
+
target.removeEventListener(name, handler);
|
|
3259
|
+
});
|
|
3260
|
+
destroySurface(this.surface);
|
|
3261
|
+
this.cleanupDom();
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
var createChartGrid = (container, options = {}) => new ChartGridController(container, options);
|
|
3265
|
+
|
|
3266
|
+
// src/core/mockData.js
|
|
3267
|
+
var palette = [
|
|
3268
|
+
"#38bdf8",
|
|
3269
|
+
"#22c55e",
|
|
3270
|
+
"#f59e0b",
|
|
3271
|
+
"#ef4444",
|
|
3272
|
+
"#a855f7",
|
|
3273
|
+
"#14b8a6"
|
|
3274
|
+
];
|
|
3275
|
+
var createRandom = (seed) => {
|
|
3276
|
+
let value = seed >>> 0;
|
|
3277
|
+
return () => {
|
|
3278
|
+
value = value * 1664525 + 1013904223 >>> 0;
|
|
3279
|
+
return value / 4294967296;
|
|
3280
|
+
};
|
|
3281
|
+
};
|
|
3282
|
+
var makeSeries = ({ chartIndex, seriesIndex, pointCount }) => {
|
|
3283
|
+
const x = new Float64Array(pointCount);
|
|
3284
|
+
const y = new Float32Array(pointCount);
|
|
3285
|
+
const random = createRandom((chartIndex + 1) * 1009 + seriesIndex * 9176);
|
|
3286
|
+
const baseline = 15 + random() * 45 + seriesIndex * 4;
|
|
3287
|
+
const waveA = 8e-4 + random() * 4e-3;
|
|
3288
|
+
const waveB = 6e-3 + random() * 0.03;
|
|
3289
|
+
const waveC = 5e-5 + random() * 6e-4;
|
|
3290
|
+
const amplitudeA = 0.015 + random() * 0.09;
|
|
3291
|
+
const amplitudeB = 5e-3 + random() * 0.04;
|
|
3292
|
+
const trend = (random() - 0.5) * 4e-3;
|
|
3293
|
+
const noise = 4e-3 + random() * 0.025;
|
|
3294
|
+
const spikeInterval = 180 + Math.floor(random() * 900);
|
|
3295
|
+
const spikeMagnitude = 0.12 + random() * 0.7;
|
|
3296
|
+
let value = baseline;
|
|
3297
|
+
for (let i = 0; i < pointCount; i += 1) {
|
|
3298
|
+
x[i] = i;
|
|
3299
|
+
const spike = (i + chartIndex * 13 + seriesIndex * 31) % spikeInterval === 0 ? spikeMagnitude * (random() > 0.45 ? 1 : -1) : 0;
|
|
3300
|
+
value += Math.sin(i * waveA + chartIndex) * amplitudeA + Math.cos(i * waveB + seriesIndex) * amplitudeB + Math.sin(i * waveC) * amplitudeA * 0.35 + trend + (random() - 0.5) * noise + spike;
|
|
3301
|
+
y[i] = value;
|
|
3302
|
+
}
|
|
3303
|
+
return createLineSeries({
|
|
3304
|
+
id: `chart-${chartIndex}-series-${seriesIndex}`,
|
|
3305
|
+
name: `Run ${seriesIndex + 1}`,
|
|
3306
|
+
color: palette[(chartIndex + seriesIndex) % palette.length],
|
|
3307
|
+
x,
|
|
3308
|
+
y
|
|
3309
|
+
});
|
|
3310
|
+
};
|
|
3311
|
+
var createMockCharts = ({
|
|
3312
|
+
chartCount = 50,
|
|
3313
|
+
seriesPerChart = 2,
|
|
3314
|
+
pointCount = 5e5
|
|
3315
|
+
} = {}) => {
|
|
3316
|
+
return Array.from({ length: chartCount }, (_, chartIndex) => ({
|
|
3317
|
+
id: `metric-${chartIndex + 1}`,
|
|
3318
|
+
title: `metric_${String(chartIndex + 1).padStart(2, "0")}`,
|
|
3319
|
+
series: Array.from(
|
|
3320
|
+
{ length: seriesPerChart },
|
|
3321
|
+
(_2, seriesIndex) => makeSeries({ chartIndex, seriesIndex, pointCount })
|
|
3322
|
+
)
|
|
3323
|
+
}));
|
|
3324
|
+
};
|
|
3325
|
+
return __toCommonJS(index_exports);
|
|
3326
|
+
})();
|
|
3327
|
+
//# sourceMappingURL=aliencharts.global.js.map
|