@baron1996/klinecharts-adapter 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +27 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +638 -26
- package/dist/conversion/chart-options.d.ts.map +1 -1
- package/dist/conversion/chart-options.js +6 -0
- package/dist/conversion/overlays.d.ts +2 -1
- package/dist/conversion/overlays.d.ts.map +1 -1
- package/dist/conversion/overlays.js +1 -1
- package/dist/conversion/price.d.ts.map +1 -1
- package/dist/conversion/price.js +10 -39
- package/dist/drawing/engine-port.d.ts +71 -0
- package/dist/drawing/engine-port.d.ts.map +1 -0
- package/dist/drawing/engine-port.js +1 -0
- package/dist/drawing/interaction-normalization.d.ts +18 -0
- package/dist/drawing/interaction-normalization.d.ts.map +1 -0
- package/dist/drawing/interaction-normalization.js +61 -0
- package/dist/drawing/kline-projection-policy.d.ts +8 -0
- package/dist/drawing/kline-projection-policy.d.ts.map +1 -0
- package/dist/drawing/kline-projection-policy.js +69 -0
- package/dist/drawing/overlay-conversion.d.ts +9 -0
- package/dist/drawing/overlay-conversion.d.ts.map +1 -0
- package/dist/drawing/overlay-conversion.js +245 -0
- package/dist/drawing/projection-policy.d.ts +34 -0
- package/dist/drawing/projection-policy.d.ts.map +1 -0
- package/dist/drawing/projection-policy.js +10 -0
- package/dist/drawing/time-series-projection-policy.d.ts +6 -0
- package/dist/drawing/time-series-projection-policy.d.ts.map +1 -0
- package/dist/drawing/time-series-projection-policy.js +28 -0
- package/dist/extensions/price-measurement.d.ts +1 -1
- package/dist/extensions/price-measurement.d.ts.map +1 -1
- package/dist/extensions/price-measurement.js +8 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/main-series-presentation.d.ts +35 -0
- package/dist/main-series-presentation.d.ts.map +1 -0
- package/dist/main-series-presentation.js +52 -0
- package/dist/time-series/adapter.d.ts +25 -2
- package/dist/time-series/adapter.d.ts.map +1 -1
- package/dist/time-series/adapter.js +889 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { parseTimeSeriesScene, TimeSeriesSceneError, } from '@baron1996/kline-scene-schema';
|
|
1
|
+
import { parseDrawableWorkspaceDocument, parseTimeSeriesScene, TimeSeriesSceneError, SceneError, } from '@baron1996/kline-scene-schema';
|
|
2
2
|
import { toKLineChartsTimeSeriesOptions } from '../conversion/chart-options.js';
|
|
3
|
+
import { registerProjectOverlays } from '../extensions/register.js';
|
|
3
4
|
import { applyViewport } from '../conversion/viewport.js';
|
|
5
|
+
import { fromEngineOverlay, toEngineOverlay, toEngineOverlayDrawing, toOverlayStyles, } from '../conversion/overlays.js';
|
|
6
|
+
import { createDragCandidate } from '../interaction/dragging.js';
|
|
7
|
+
import { hitTestOverlayGeometries, } from '../interaction/hit-testing.js';
|
|
8
|
+
import { shouldIgnoreStaleOverlayDeselection } from '../interaction/selection-arbitration.js';
|
|
9
|
+
import { normalizePriceValue } from '../conversion/price.js';
|
|
10
|
+
import { drawingToSceneOverlay, sceneOverlayToDrawing, } from '../drawing/overlay-conversion.js';
|
|
4
11
|
import { TIME_SERIES_INDICATOR_NAME, TIME_SERIES_PANE_ID, TIME_SERIES_Y_AXIS_ID, timeSeriesIndicatorTemplate, toTimeSeriesIndicatorCreate, } from './indicator.js';
|
|
5
12
|
function carrierData(data) {
|
|
6
13
|
return data.map(({ timestamp, values }) => ({
|
|
@@ -49,6 +56,27 @@ function dataError(error) {
|
|
|
49
56
|
}));
|
|
50
57
|
return new TimeSeriesSceneError('TIME_SERIES_DATA_INVALID', '/data', 'Time Series replacement data is invalid.', issues);
|
|
51
58
|
}
|
|
59
|
+
function createTimeSeriesOverlayIdMap() {
|
|
60
|
+
const paneToEngine = new Map([
|
|
61
|
+
[TIME_SERIES_PANE_ID, TIME_SERIES_PANE_ID],
|
|
62
|
+
]);
|
|
63
|
+
const yAxisToEngine = new Map([
|
|
64
|
+
[TIME_SERIES_Y_AXIS_ID, TIME_SERIES_Y_AXIS_ID],
|
|
65
|
+
]);
|
|
66
|
+
return {
|
|
67
|
+
paneToEngine,
|
|
68
|
+
paneFromEngine: new Map(Array.from(paneToEngine, ([sceneId, engineId]) => [engineId, sceneId])),
|
|
69
|
+
yAxisToEngine,
|
|
70
|
+
yAxisFromEngine: new Map(Array.from(yAxisToEngine, ([sceneId, engineId]) => [engineId, sceneId])),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function requireMappedId(map, sceneId, path) {
|
|
74
|
+
const mapped = map.get(sceneId);
|
|
75
|
+
if (mapped === undefined) {
|
|
76
|
+
throw adapterError(`${path} ${sceneId} is not mapped.`);
|
|
77
|
+
}
|
|
78
|
+
return mapped;
|
|
79
|
+
}
|
|
52
80
|
/** TimeSeriesScene 与 KLineCharts 之间的唯一受控边界。 */
|
|
53
81
|
export class TimeSeriesChartsAdapter {
|
|
54
82
|
/** KLineCharts 实例,只能在 Adapter 内使用。 */
|
|
@@ -57,6 +85,16 @@ export class TimeSeriesChartsAdapter {
|
|
|
57
85
|
#engine;
|
|
58
86
|
/** 当前可导出的规范化 Scene。 */
|
|
59
87
|
#scene;
|
|
88
|
+
/** TimeSeries 场景上图表层绘制的 overlay id 映射。 */
|
|
89
|
+
#overlayIdMap;
|
|
90
|
+
/** 运行时可见的 overlay 草稿状态,受该 Adapter 控制。 */
|
|
91
|
+
#drawings;
|
|
92
|
+
/** 当前运行时选中的 overlay id。 */
|
|
93
|
+
#selectedOverlayId = null;
|
|
94
|
+
/** 当前交互中的 priceMeasurement overlay 来源。 */
|
|
95
|
+
#interactiveDrawing = null;
|
|
96
|
+
/** 当前场景主价精度,用于 overlay 坐标规范化。 */
|
|
97
|
+
#pricePrecision;
|
|
60
98
|
/** 按时间戳索引的原始点,保证十字线查询为 O(1)。 */
|
|
61
99
|
#pointByTimestamp;
|
|
62
100
|
/** Adapter 独占的图表容器。 */
|
|
@@ -67,17 +105,38 @@ export class TimeSeriesChartsAdapter {
|
|
|
67
105
|
#originalBackground;
|
|
68
106
|
/** 十字线纯数据监听器。 */
|
|
69
107
|
#crosshairListeners = new Set();
|
|
108
|
+
/** 当前受控拖拽会话。 */
|
|
109
|
+
#pointerInteraction;
|
|
110
|
+
/** 内部确定性交互序号。 */
|
|
111
|
+
#interactionSequence = 0;
|
|
70
112
|
/** 防止重复销毁底层引擎。 */
|
|
71
113
|
#disposed = false;
|
|
114
|
+
/** 显式 Workspace 模式。 */
|
|
115
|
+
#workspaceMode = false;
|
|
116
|
+
/** Workspace 模式权威业务 Drawing。 */
|
|
117
|
+
#workspaceSources = new Map();
|
|
118
|
+
/** 公共 Drawing 端口监听器。 */
|
|
119
|
+
#portListeners = new Set();
|
|
120
|
+
/** 交互启用开关。 */
|
|
121
|
+
#mutationsEnabled = true;
|
|
122
|
+
/** 当前拖动会话编辑维度。 */
|
|
123
|
+
#interactionDimensions = {
|
|
124
|
+
horizontal: false,
|
|
125
|
+
vertical: false,
|
|
126
|
+
};
|
|
72
127
|
constructor(container, scene, chart, engine, originalBackground, interactiveRoot) {
|
|
73
128
|
this.#container = container;
|
|
74
129
|
this.#scene = scene;
|
|
130
|
+
this.#overlayIdMap = createTimeSeriesOverlayIdMap();
|
|
131
|
+
this.#drawings = [];
|
|
132
|
+
this.#pricePrecision = scene.series[0]?.precision ?? 0;
|
|
75
133
|
this.#pointByTimestamp = new Map(scene.data.map((point) => [point.timestamp, point]));
|
|
76
134
|
this.#chart = chart;
|
|
77
135
|
this.#engine = engine;
|
|
78
136
|
this.#originalBackground = originalBackground;
|
|
79
137
|
this.#interactiveRoot = interactiveRoot;
|
|
80
138
|
this.#chart.subscribeAction('onCrosshairChange', this.#handleCrosshair);
|
|
139
|
+
this.#installInteractionListeners();
|
|
81
140
|
this.#interactiveRoot.addEventListener('pointerleave', this.#handlePointerLeave);
|
|
82
141
|
}
|
|
83
142
|
static async create(container, value) {
|
|
@@ -103,6 +162,7 @@ export class TimeSeriesChartsAdapter {
|
|
|
103
162
|
pricePrecision: scene.series[0].precision,
|
|
104
163
|
volumePrecision: 0,
|
|
105
164
|
});
|
|
165
|
+
registerProjectOverlays(engine.registerOverlay);
|
|
106
166
|
chart.setPeriod(structuredClone(scene.period));
|
|
107
167
|
chart.setDataLoader(dataLoader(scene.data));
|
|
108
168
|
engine.registerIndicator(timeSeriesIndicatorTemplate);
|
|
@@ -147,11 +207,120 @@ export class TimeSeriesChartsAdapter {
|
|
|
147
207
|
throw adapterError('KLineCharts failed to initialize TimeSeriesScene.');
|
|
148
208
|
}
|
|
149
209
|
}
|
|
210
|
+
/** 显式 Workspace factory:时间序列场景 + DrawingDocument。 */
|
|
211
|
+
static async createWorkspace(container, value) {
|
|
212
|
+
const workspace = parseDrawableWorkspaceDocument(value);
|
|
213
|
+
if (workspace.scene.kind !== 'time-series') {
|
|
214
|
+
throw new TimeSeriesSceneError('TIME_SERIES_SCENE_SCHEMA_INVALID', '/scene/kind', 'TimeSeriesChartsAdapter requires a time-series Workspace Scene.');
|
|
215
|
+
}
|
|
216
|
+
const scene = workspace.scene.document;
|
|
217
|
+
const originalBackground = container.style.backgroundColor;
|
|
218
|
+
const engine = await import('klinecharts');
|
|
219
|
+
let chart = null;
|
|
220
|
+
try {
|
|
221
|
+
if (engine.version() !== scene.runtime.engineVersion) {
|
|
222
|
+
throw adapterError('KLineCharts engine version does not match the Scene.');
|
|
223
|
+
}
|
|
224
|
+
chart = engine.init(container, toKLineChartsTimeSeriesOptions(scene.chart));
|
|
225
|
+
if (chart === null) {
|
|
226
|
+
throw adapterError('KLineCharts returned null while initializing.');
|
|
227
|
+
}
|
|
228
|
+
const root = container.firstElementChild;
|
|
229
|
+
if (!(root instanceof HTMLElement)) {
|
|
230
|
+
throw adapterError('KLineCharts did not create an interactive root.');
|
|
231
|
+
}
|
|
232
|
+
root.style.touchAction = 'none';
|
|
233
|
+
chart.setSymbol({
|
|
234
|
+
ticker: '@baron1996/time-series-scene',
|
|
235
|
+
pricePrecision: scene.series[0].precision,
|
|
236
|
+
volumePrecision: 0,
|
|
237
|
+
});
|
|
238
|
+
registerProjectOverlays(engine.registerOverlay);
|
|
239
|
+
chart.setPeriod(structuredClone(scene.period));
|
|
240
|
+
chart.setDataLoader(dataLoader(scene.data));
|
|
241
|
+
engine.registerIndicator(timeSeriesIndicatorTemplate);
|
|
242
|
+
for (const series of scene.series) {
|
|
243
|
+
const expectedId = `baron_time_series_${series.id}`;
|
|
244
|
+
const createdId = chart.createIndicator(toTimeSeriesIndicatorCreate(series), true);
|
|
245
|
+
if (createdId !== expectedId) {
|
|
246
|
+
throw adapterError(`KLineCharts failed to create series ${series.id}.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
chart.setPaneOptions({
|
|
250
|
+
id: 'candle_pane',
|
|
251
|
+
height: 0,
|
|
252
|
+
minHeight: 0,
|
|
253
|
+
order: 0,
|
|
254
|
+
state: 'minimize',
|
|
255
|
+
dragEnabled: false,
|
|
256
|
+
});
|
|
257
|
+
chart.setPaneOptions({
|
|
258
|
+
id: TIME_SERIES_PANE_ID,
|
|
259
|
+
height: Math.max(container.clientHeight, 240),
|
|
260
|
+
minHeight: 120,
|
|
261
|
+
order: 1,
|
|
262
|
+
state: 'normal',
|
|
263
|
+
dragEnabled: false,
|
|
264
|
+
});
|
|
265
|
+
const adapter = new TimeSeriesChartsAdapter(container, scene, chart, engine, originalBackground, root);
|
|
266
|
+
adapter.#workspaceMode = true;
|
|
267
|
+
adapter.#updateAxisVisibility();
|
|
268
|
+
applyViewport(chart, scene.viewport);
|
|
269
|
+
container.style.backgroundColor = scene.chart.layout.backgroundColor;
|
|
270
|
+
adapter.#restoreWorkspaceDrawings(workspace.drawings.drawings.map((drawing) => timeSeriesSnapshotOfDrawing(drawing)));
|
|
271
|
+
return adapter;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (chart !== null) {
|
|
275
|
+
engine.dispose(container);
|
|
276
|
+
}
|
|
277
|
+
container.replaceChildren();
|
|
278
|
+
container.style.backgroundColor = originalBackground;
|
|
279
|
+
if (error instanceof TimeSeriesSceneError) {
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
throw adapterError('KLineCharts failed to initialize the Workspace Scene.');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
150
285
|
#assertActive() {
|
|
151
286
|
if (this.#disposed) {
|
|
152
287
|
throw adapterError('The Time Series Adapter has already been disposed.');
|
|
153
288
|
}
|
|
154
289
|
}
|
|
290
|
+
#emitPort(event) {
|
|
291
|
+
for (const listener of this.#portListeners) {
|
|
292
|
+
listener(structuredClone(event));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
#restoreWorkspaceDrawings(drawings) {
|
|
296
|
+
for (const overlay of this.#drawings) {
|
|
297
|
+
this.#chart.removeOverlay({ id: overlay.id });
|
|
298
|
+
}
|
|
299
|
+
this.#drawings = [];
|
|
300
|
+
this.#workspaceSources = new Map();
|
|
301
|
+
for (const snapshot of drawings) {
|
|
302
|
+
const drawing = timeSeriesDrawingFromSnapshot(snapshot);
|
|
303
|
+
this.#workspaceSources.set(drawing.id, drawing);
|
|
304
|
+
const overlay = drawingToSceneOverlay(drawing, TIME_SERIES_PANE_ID);
|
|
305
|
+
const result = this.#chart.createOverlay(toEngineOverlay(overlay, this.#overlayIdMap, `/drawings/${this.#drawings.length}`, this.#overlayCallbacks(overlay)));
|
|
306
|
+
if (result !== overlay.id) {
|
|
307
|
+
throw adapterError(`KLineCharts failed to restore Drawing ${overlay.id}.`);
|
|
308
|
+
}
|
|
309
|
+
this.#drawings.push(structuredClone(overlay));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
#fromPixelToData(point) {
|
|
313
|
+
const converted = this.#chart.convertFromPixel([point], {
|
|
314
|
+
paneId: TIME_SERIES_PANE_ID,
|
|
315
|
+
yAxisId: TIME_SERIES_Y_AXIS_ID,
|
|
316
|
+
absolute: true,
|
|
317
|
+
});
|
|
318
|
+
const value = converted[0];
|
|
319
|
+
return {
|
|
320
|
+
...(value?.timestamp !== undefined ? { timestamp: value.timestamp } : {}),
|
|
321
|
+
...(value?.value !== undefined ? { value: value.value } : {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
155
324
|
#updateAxisVisibility() {
|
|
156
325
|
const hasValues = hasVisibleFiniteValue(this.#scene);
|
|
157
326
|
this.#chart.overrideYAxis({
|
|
@@ -204,6 +373,567 @@ export class TimeSeriesChartsAdapter {
|
|
|
204
373
|
listener({ timestamp: null, values: null });
|
|
205
374
|
}
|
|
206
375
|
};
|
|
376
|
+
#engineOverlays() {
|
|
377
|
+
return this.#chart.getOverlays();
|
|
378
|
+
}
|
|
379
|
+
#selectOverlay(id) {
|
|
380
|
+
if (this.#workspaceMode && this.#selectedOverlayId !== id) {
|
|
381
|
+
this.#emitPort({
|
|
382
|
+
type: id === null ? 'deselected' : 'selected',
|
|
383
|
+
id: id ?? this.#selectedOverlayId ?? '',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
this.#selectedOverlayId = id;
|
|
387
|
+
}
|
|
388
|
+
#timeSeriesDimensionsForHit(overlay, hit) {
|
|
389
|
+
if (hit.target !== 'anchor') {
|
|
390
|
+
return { horizontal: true, vertical: true };
|
|
391
|
+
}
|
|
392
|
+
switch (overlay.type) {
|
|
393
|
+
case 'horizontalStraightLine':
|
|
394
|
+
case 'priceLine':
|
|
395
|
+
case 'simpleTag':
|
|
396
|
+
case 'horizontalRayLine':
|
|
397
|
+
case 'horizontalSegment':
|
|
398
|
+
return { horizontal: false, vertical: true };
|
|
399
|
+
case 'verticalStraightLine':
|
|
400
|
+
case 'verticalRayLine':
|
|
401
|
+
case 'verticalSegment':
|
|
402
|
+
return { horizontal: true, vertical: false };
|
|
403
|
+
default:
|
|
404
|
+
return { horizontal: true, vertical: true };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
#commitEngineOverlay(engineOverlay, source, kind) {
|
|
408
|
+
const existingIndex = this.#drawings.findIndex((overlay) => overlay.id === source.id);
|
|
409
|
+
const currentSource = existingIndex < 0 ? source : this.#drawings[existingIndex];
|
|
410
|
+
const overlay = fromEngineOverlay(engineOverlay, currentSource, this.#overlayIdMap, existingIndex < 0 ? `/drawings/${this.#drawings.length}` : `/drawings/${existingIndex}`, this.#pricePrecision);
|
|
411
|
+
const drawings = structuredClone(this.#drawings);
|
|
412
|
+
if (existingIndex < 0) {
|
|
413
|
+
drawings.push(overlay);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
drawings[existingIndex] = overlay;
|
|
417
|
+
}
|
|
418
|
+
this.#drawings = structuredClone(drawings);
|
|
419
|
+
this.#selectOverlay(overlay.id);
|
|
420
|
+
if (this.#workspaceMode) {
|
|
421
|
+
const drawing = this.#workspaceSources.get(overlay.id);
|
|
422
|
+
if (drawing !== undefined) {
|
|
423
|
+
const updated = sceneOverlayToDrawing(overlay, drawing);
|
|
424
|
+
this.#workspaceSources.set(overlay.id, updated);
|
|
425
|
+
this.#emitPort({
|
|
426
|
+
type: kind === 'created' ? 'created' : 'updated',
|
|
427
|
+
id: overlay.id,
|
|
428
|
+
drawing: timeSeriesSnapshotOfDrawing(updated),
|
|
429
|
+
editDimensions: structuredClone(this.#interactionDimensions),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
#safelyCommitEngineOverlay(engineOverlay, source, kind) {
|
|
435
|
+
try {
|
|
436
|
+
this.#commitEngineOverlay(engineOverlay, source, kind);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (error instanceof SceneError) {
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
throw adapterError(`KLineCharts failed to ${kind} time-series drawing ${source.id}.`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
#overlayCallbacks(source, drawing = false) {
|
|
446
|
+
return {
|
|
447
|
+
onDrawEnd: ({ overlay }) => {
|
|
448
|
+
if (drawing &&
|
|
449
|
+
this.#interactiveDrawing?.source.id === source.id) {
|
|
450
|
+
this.#interactiveDrawing = null;
|
|
451
|
+
}
|
|
452
|
+
this.#safelyCommitEngineOverlay(overlay, source, drawing ? 'created' : 'updated');
|
|
453
|
+
},
|
|
454
|
+
onPressedMoveStart: ({ overlay }) => {
|
|
455
|
+
this.#selectOverlay(overlay.id);
|
|
456
|
+
},
|
|
457
|
+
onPressedMoveEnd: ({ overlay }) => {
|
|
458
|
+
if (!isControlledInteractionOverlay(source)) {
|
|
459
|
+
this.#safelyCommitEngineOverlay(overlay, source, 'updated');
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
onSelected: ({ overlay }) => {
|
|
463
|
+
this.#selectOverlay(overlay.id);
|
|
464
|
+
},
|
|
465
|
+
onDeselected: (event) => {
|
|
466
|
+
const eventX = event.x;
|
|
467
|
+
const eventY = event.y;
|
|
468
|
+
const coordinate = typeof eventX === 'number' && Number.isFinite(eventX) &&
|
|
469
|
+
typeof eventY === 'number' && Number.isFinite(eventY)
|
|
470
|
+
? { x: eventX, y: eventY }
|
|
471
|
+
: undefined;
|
|
472
|
+
if (this.#selectedOverlayId === event.overlay.id &&
|
|
473
|
+
!shouldIgnoreStaleOverlayDeselection(this.#selectedOverlayId, event.overlay.id, coordinate, this.#overlayGeometries())) {
|
|
474
|
+
this.#selectOverlay(null);
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
onRemoved: ({ overlay }) => {
|
|
478
|
+
if (this.#interactiveDrawing?.source.id === overlay.id) {
|
|
479
|
+
this.#interactiveDrawing = null;
|
|
480
|
+
}
|
|
481
|
+
const index = this.#drawings.findIndex((candidate) => candidate.id === overlay.id);
|
|
482
|
+
if (index >= 0) {
|
|
483
|
+
this.#drawings = this.#drawings.filter((candidate) => candidate.id !== overlay.id);
|
|
484
|
+
if (this.#selectedOverlayId === overlay.id) {
|
|
485
|
+
this.#selectOverlay(null);
|
|
486
|
+
}
|
|
487
|
+
if (this.#workspaceMode) {
|
|
488
|
+
this.#workspaceSources.delete(overlay.id);
|
|
489
|
+
this.#emitPort({ type: 'removed', id: overlay.id });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
#completeInteractivePriceMeasurement(drawing, end) {
|
|
496
|
+
const source = drawing.source;
|
|
497
|
+
if (drawing.start === undefined) {
|
|
498
|
+
throw adapterError('Incomplete interactive price measurement anchors.');
|
|
499
|
+
}
|
|
500
|
+
const overlay = fromEngineOverlay({
|
|
501
|
+
...structuredClone(source),
|
|
502
|
+
id: source.id,
|
|
503
|
+
name: source.type,
|
|
504
|
+
paneId: requireMappedId(this.#overlayIdMap.paneToEngine, source.paneId, '/overlays/paneId'),
|
|
505
|
+
lock: source.locked,
|
|
506
|
+
visible: source.visible,
|
|
507
|
+
zLevel: source.zLevel,
|
|
508
|
+
mode: source.mode,
|
|
509
|
+
points: [
|
|
510
|
+
{
|
|
511
|
+
value: drawing.start.value,
|
|
512
|
+
timestamp: drawing.start.timestamp,
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
value: end.value,
|
|
516
|
+
timestamp: end.timestamp,
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
styles: toOverlayStyles(source.styles),
|
|
520
|
+
}, source, this.#overlayIdMap, `/drawings/${this.#drawings.length}`, this.#pricePrecision);
|
|
521
|
+
const result = overlay;
|
|
522
|
+
if (!this.#chart.removeOverlay({ id: result.id })) {
|
|
523
|
+
throw adapterError(`KLineCharts failed to replace in-progress Drawing ${result.id}.`);
|
|
524
|
+
}
|
|
525
|
+
const created = this.#chart.createOverlay(toEngineOverlay(result, this.#overlayIdMap, `/drawings/${this.#drawings.length}`, this.#overlayCallbacks(result)));
|
|
526
|
+
if (created !== result.id) {
|
|
527
|
+
throw adapterError(`KLineCharts failed to persist Drawing ${result.id}.`);
|
|
528
|
+
}
|
|
529
|
+
this.#drawings = [...this.#drawings, result];
|
|
530
|
+
this.#selectOverlay(result.id);
|
|
531
|
+
}
|
|
532
|
+
#toPixel(point, paneId) {
|
|
533
|
+
const converted = this.#chart.convertToPixel(point, {
|
|
534
|
+
paneId,
|
|
535
|
+
yAxisId: TIME_SERIES_Y_AXIS_ID,
|
|
536
|
+
absolute: true,
|
|
537
|
+
});
|
|
538
|
+
if (!Number.isFinite(converted.x) || !Number.isFinite(converted.y)) {
|
|
539
|
+
throw new SceneError('EXPORT_INVALID', '/overlays', 'KLineCharts returned a non-finite pixel coordinate.');
|
|
540
|
+
}
|
|
541
|
+
return { x: converted.x, y: converted.y };
|
|
542
|
+
}
|
|
543
|
+
#fromPixel(point, paneId) {
|
|
544
|
+
const converted = this.#chart.convertFromPixel([point], {
|
|
545
|
+
paneId,
|
|
546
|
+
yAxisId: TIME_SERIES_Y_AXIS_ID,
|
|
547
|
+
absolute: true,
|
|
548
|
+
});
|
|
549
|
+
const value = converted[0];
|
|
550
|
+
if (!Number.isFinite(value?.dataIndex) || !Number.isFinite(value?.value)) {
|
|
551
|
+
throw new SceneError('INVALID_REFERENCE', '/overlays', 'Pointer does not map to finite chart data.');
|
|
552
|
+
}
|
|
553
|
+
return { dataIndex: value.dataIndex, value: value.value };
|
|
554
|
+
}
|
|
555
|
+
#measurementAnchor(point, paneId, path) {
|
|
556
|
+
const converted = this.#chart.convertFromPixel([point], {
|
|
557
|
+
paneId,
|
|
558
|
+
yAxisId: TIME_SERIES_Y_AXIS_ID,
|
|
559
|
+
absolute: true,
|
|
560
|
+
});
|
|
561
|
+
const value = converted[0];
|
|
562
|
+
if (value === undefined ||
|
|
563
|
+
!Number.isSafeInteger(value.timestamp) ||
|
|
564
|
+
!this.#scene.data.some((bar) => bar.timestamp === value.timestamp)) {
|
|
565
|
+
throw new SceneError('INVALID_REFERENCE', path, 'Pointer does not map to a finite price and an embedded market-data timestamp.');
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
timestamp: value.timestamp,
|
|
569
|
+
value: normalizePriceValue(value.value, this.#pricePrecision, `${path}/value`),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
#overlayGeometries() {
|
|
573
|
+
const geometries = [];
|
|
574
|
+
for (let index = 0; index < this.#drawings.length; index++) {
|
|
575
|
+
const overlay = this.#drawings[index];
|
|
576
|
+
if (overlay === undefined || !overlay.visible) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (overlay.type === 'horizontalStraightLine') {
|
|
580
|
+
const anchor = overlay.anchor;
|
|
581
|
+
if (anchor === undefined || !('value' in anchor)) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const anchorPixel = this.#toPixel({ timestamp: this.#scene.data[0].timestamp, value: anchor.value }, overlay.paneId);
|
|
585
|
+
const paneMain = this.#chart.getDom(TIME_SERIES_PANE_ID, 'main');
|
|
586
|
+
const containerRect = this.#container.getBoundingClientRect();
|
|
587
|
+
const mainRect = paneMain?.getBoundingClientRect() ?? containerRect;
|
|
588
|
+
const start = { x: mainRect.left - containerRect.left, y: anchorPixel.y };
|
|
589
|
+
const end = { x: mainRect.right - containerRect.left, y: anchorPixel.y };
|
|
590
|
+
geometries.push({
|
|
591
|
+
overlayId: overlay.id,
|
|
592
|
+
sceneIndex: index,
|
|
593
|
+
zLevel: overlay.zLevel,
|
|
594
|
+
locked: overlay.locked,
|
|
595
|
+
anchors: [{ x: (start.x + end.x) / 2, y: anchorPixel.y }],
|
|
596
|
+
bodySegments: [[start, end]],
|
|
597
|
+
});
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (overlay.type === 'priceMeasurement' && overlay.start !== undefined && overlay.end !== undefined) {
|
|
601
|
+
const start = this.#toPixel(overlay.start, overlay.paneId);
|
|
602
|
+
const end = this.#toPixel(overlay.end, overlay.paneId);
|
|
603
|
+
geometries.push({
|
|
604
|
+
overlayId: overlay.id,
|
|
605
|
+
sceneIndex: index,
|
|
606
|
+
zLevel: overlay.zLevel,
|
|
607
|
+
locked: overlay.locked,
|
|
608
|
+
anchors: [start, end],
|
|
609
|
+
bodySegments: [[start, end]],
|
|
610
|
+
});
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (overlay.points !== undefined && overlay.points.length >= 2) {
|
|
614
|
+
const anchors = overlay.points.map((point) => this.#toPixel(point, overlay.paneId));
|
|
615
|
+
const bodySegments = [];
|
|
616
|
+
for (let pointIndex = 1; pointIndex < anchors.length; pointIndex++) {
|
|
617
|
+
bodySegments.push([anchors[pointIndex - 1], anchors[pointIndex]]);
|
|
618
|
+
}
|
|
619
|
+
geometries.push({
|
|
620
|
+
overlayId: overlay.id,
|
|
621
|
+
sceneIndex: index,
|
|
622
|
+
zLevel: overlay.zLevel,
|
|
623
|
+
locked: overlay.locked,
|
|
624
|
+
anchors,
|
|
625
|
+
bodySegments,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return geometries;
|
|
630
|
+
}
|
|
631
|
+
#handlePointerDown = (event) => {
|
|
632
|
+
if (this.#disposed ||
|
|
633
|
+
event.button !== 0 ||
|
|
634
|
+
this.#pointerInteraction !== undefined ||
|
|
635
|
+
!this.#mutationsEnabled) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const coordinate = (() => {
|
|
639
|
+
const rect = this.#container.getBoundingClientRect();
|
|
640
|
+
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
641
|
+
})();
|
|
642
|
+
const drawing = this.#interactiveDrawing;
|
|
643
|
+
if (drawing !== null) {
|
|
644
|
+
try {
|
|
645
|
+
const point = this.#measurementAnchor(coordinate, drawing.source.paneId, drawing.start === undefined ? '/drawings/start' : '/drawings/end');
|
|
646
|
+
if (drawing.start === undefined) {
|
|
647
|
+
drawing.start = point;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
event.stopImmediatePropagation();
|
|
651
|
+
this.#completeInteractivePriceMeasurement(drawing, point);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
if (error instanceof SceneError) {
|
|
656
|
+
event.preventDefault();
|
|
657
|
+
event.stopImmediatePropagation();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries());
|
|
664
|
+
if (hit === null) {
|
|
665
|
+
const selected = this.#drawings.find((overlay) => overlay.id === this.#selectedOverlayId);
|
|
666
|
+
if (selected !== undefined && isControlledInteractionOverlay(selected)) {
|
|
667
|
+
this.#selectOverlay(null);
|
|
668
|
+
}
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const before = this.#drawings.find((overlay) => overlay.id === hit.overlayId);
|
|
672
|
+
if (before === undefined) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
this.#selectOverlay(before.id);
|
|
676
|
+
if (!isControlledInteractionOverlay(before)) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
event.preventDefault();
|
|
680
|
+
event.stopImmediatePropagation();
|
|
681
|
+
if (hit.locked) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.#container.setPointerCapture(event.pointerId);
|
|
685
|
+
this.#interactionDimensions = this.#timeSeriesDimensionsForHit(before, hit);
|
|
686
|
+
this.#pointerInteraction = {
|
|
687
|
+
pointerId: event.pointerId,
|
|
688
|
+
originClient: coordinate,
|
|
689
|
+
originData: this.#fromPixel(coordinate, before.paneId),
|
|
690
|
+
hit,
|
|
691
|
+
before: structuredClone(before),
|
|
692
|
+
interactionId: `interaction-${this.#interactionSequence++}`,
|
|
693
|
+
started: false,
|
|
694
|
+
};
|
|
695
|
+
};
|
|
696
|
+
#handlePointerMove = (event) => {
|
|
697
|
+
const interaction = this.#pointerInteraction;
|
|
698
|
+
if (interaction === undefined || interaction.pointerId !== event.pointerId) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
event.stopImmediatePropagation();
|
|
703
|
+
const coordinate = (() => {
|
|
704
|
+
const rect = this.#container.getBoundingClientRect();
|
|
705
|
+
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
706
|
+
})();
|
|
707
|
+
if (!interaction.started &&
|
|
708
|
+
Math.hypot(coordinate.x - interaction.originClient.x, coordinate.y - interaction.originClient.y) < 0.5) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
interaction.started = true;
|
|
712
|
+
try {
|
|
713
|
+
const candidate = createDragCandidate(interaction.before, interaction.hit, interaction.originData, this.#fromPixel(coordinate, interaction.before.paneId), this.#scene.data.map((bar) => bar.timestamp), this.#pricePrecision);
|
|
714
|
+
const index = this.#drawings.findIndex((overlay) => overlay.id === candidate.id);
|
|
715
|
+
const drawings = structuredClone(this.#drawings);
|
|
716
|
+
drawings[index] = candidate;
|
|
717
|
+
if (index < 0 || !this.#chart.overrideOverlay(toEngineOverlay(candidate, this.#overlayIdMap, `/drawings/${index}`, this.#overlayCallbacks(candidate)))) {
|
|
718
|
+
throw adapterError(`KLineCharts failed to preview Drawing ${candidate.id}.`);
|
|
719
|
+
}
|
|
720
|
+
interaction.candidate = candidate;
|
|
721
|
+
this.#drawings = drawings;
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
if (error instanceof SceneError) {
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
#handlePointerUp = (event) => {
|
|
731
|
+
const interaction = this.#pointerInteraction;
|
|
732
|
+
if (interaction === undefined || interaction.pointerId !== event.pointerId) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
event.preventDefault();
|
|
736
|
+
event.stopImmediatePropagation();
|
|
737
|
+
this.#pointerInteraction = undefined;
|
|
738
|
+
this.#interactionDimensions = { horizontal: false, vertical: false };
|
|
739
|
+
if (!interaction.started) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (this.#container.hasPointerCapture(event.pointerId)) {
|
|
743
|
+
this.#container.releasePointerCapture(event.pointerId);
|
|
744
|
+
}
|
|
745
|
+
const overlay = interaction.candidate ?? interaction.before;
|
|
746
|
+
const index = this.#drawings.findIndex((candidate) => candidate.id === overlay.id);
|
|
747
|
+
if (index >= 0) {
|
|
748
|
+
const drawings = structuredClone(this.#drawings);
|
|
749
|
+
drawings[index] = overlay;
|
|
750
|
+
this.#drawings = drawings;
|
|
751
|
+
if (this.#workspaceMode) {
|
|
752
|
+
const source = this.#workspaceSources.get(overlay.id);
|
|
753
|
+
if (source !== undefined) {
|
|
754
|
+
const updated = sceneOverlayToDrawing(overlay, source);
|
|
755
|
+
this.#workspaceSources.set(overlay.id, updated);
|
|
756
|
+
this.#emitPort({
|
|
757
|
+
type: 'updated',
|
|
758
|
+
id: overlay.id,
|
|
759
|
+
drawing: timeSeriesSnapshotOfDrawing(updated),
|
|
760
|
+
editDimensions: structuredClone(this.#interactionDimensions),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
#handlePointerCancel = (event) => {
|
|
767
|
+
const interaction = this.#pointerInteraction;
|
|
768
|
+
if (interaction === undefined || interaction.pointerId !== event.pointerId) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
this.#pointerInteraction = undefined;
|
|
772
|
+
this.#interactionDimensions = { horizontal: false, vertical: false };
|
|
773
|
+
if (this.#container.hasPointerCapture(event.pointerId)) {
|
|
774
|
+
this.#container.releasePointerCapture(event.pointerId);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
#installInteractionListeners() {
|
|
778
|
+
this.#container.addEventListener('pointerdown', this.#handlePointerDown, true);
|
|
779
|
+
this.#container.addEventListener('pointermove', this.#handlePointerMove, true);
|
|
780
|
+
this.#container.addEventListener('pointerup', this.#handlePointerUp, true);
|
|
781
|
+
this.#container.addEventListener('pointercancel', this.#handlePointerCancel, true);
|
|
782
|
+
window.addEventListener('keydown', this.#handleKeyDown);
|
|
783
|
+
window.addEventListener('blur', this.#handleWindowBlur);
|
|
784
|
+
}
|
|
785
|
+
#removeInteractionListeners() {
|
|
786
|
+
this.#container.removeEventListener('pointerdown', this.#handlePointerDown, true);
|
|
787
|
+
this.#container.removeEventListener('pointermove', this.#handlePointerMove, true);
|
|
788
|
+
this.#container.removeEventListener('pointerup', this.#handlePointerUp, true);
|
|
789
|
+
this.#container.removeEventListener('pointercancel', this.#handlePointerCancel, true);
|
|
790
|
+
window.removeEventListener('keydown', this.#handleKeyDown);
|
|
791
|
+
window.removeEventListener('blur', this.#handleWindowBlur);
|
|
792
|
+
}
|
|
793
|
+
#handleKeyDown = (event) => {
|
|
794
|
+
if (event.key === 'Escape') {
|
|
795
|
+
const interaction = this.#pointerInteraction;
|
|
796
|
+
this.#pointerInteraction = undefined;
|
|
797
|
+
if (interaction !== undefined &&
|
|
798
|
+
this.#container.hasPointerCapture(interaction.pointerId)) {
|
|
799
|
+
this.#container.releasePointerCapture(interaction.pointerId);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
#handleWindowBlur = () => {
|
|
804
|
+
if (this.#pointerInteraction !== undefined) {
|
|
805
|
+
this.#pointerInteraction = undefined;
|
|
806
|
+
this.#selectedOverlayId = null;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
get sceneKind() {
|
|
810
|
+
return 'time-series';
|
|
811
|
+
}
|
|
812
|
+
restoreDrawings(drawings) {
|
|
813
|
+
this.#assertActive();
|
|
814
|
+
if (!this.#workspaceMode) {
|
|
815
|
+
throw adapterError('Drawing port operations require the Workspace factory.');
|
|
816
|
+
}
|
|
817
|
+
this.#restoreWorkspaceDrawings(drawings);
|
|
818
|
+
}
|
|
819
|
+
startDrawing(request) {
|
|
820
|
+
this.#assertActive();
|
|
821
|
+
const drawing = timeSeriesPlaceholderDrawing(request);
|
|
822
|
+
if (this.#workspaceSources.has(request.id)) {
|
|
823
|
+
throw new TimeSeriesSceneError('TIME_SERIES_SCENE_SCHEMA_INVALID', `/drawings/${request.id}`, `Drawing ${request.id} already exists.`);
|
|
824
|
+
}
|
|
825
|
+
this.#workspaceSources.set(request.id, drawing);
|
|
826
|
+
const overlay = drawingToSceneOverlay(drawing, TIME_SERIES_PANE_ID);
|
|
827
|
+
const result = this.#chart.createOverlay(toEngineOverlayDrawing({
|
|
828
|
+
...structuredClone(request),
|
|
829
|
+
...structuredClone(overlay),
|
|
830
|
+
}, this.#overlayIdMap, this.#overlayCallbacks(overlay, true)));
|
|
831
|
+
if (result !== request.id) {
|
|
832
|
+
this.#workspaceSources.delete(request.id);
|
|
833
|
+
throw adapterError(`KLineCharts failed to start Drawing ${request.id}.`);
|
|
834
|
+
}
|
|
835
|
+
return request.id;
|
|
836
|
+
}
|
|
837
|
+
listDrawings() {
|
|
838
|
+
this.#assertActive();
|
|
839
|
+
return Array.from(this.#workspaceSources.values(), (drawing) => timeSeriesSnapshotOfDrawing(drawing));
|
|
840
|
+
}
|
|
841
|
+
getDrawing(id) {
|
|
842
|
+
this.#assertActive();
|
|
843
|
+
const drawing = this.#workspaceSources.get(id);
|
|
844
|
+
return drawing === undefined ? undefined : timeSeriesSnapshotOfDrawing(drawing);
|
|
845
|
+
}
|
|
846
|
+
updateDrawingStyles(id, styles) {
|
|
847
|
+
this.#assertActive();
|
|
848
|
+
const source = this.#workspaceSources.get(id);
|
|
849
|
+
if (source === undefined) {
|
|
850
|
+
throw new TimeSeriesSceneError('TIME_SERIES_SCENE_SCHEMA_INVALID', `/drawings/${id}`, `Drawing ${id} does not exist.`);
|
|
851
|
+
}
|
|
852
|
+
if (!this.#chart.overrideOverlay({
|
|
853
|
+
id,
|
|
854
|
+
styles: toOverlayStyles(styles),
|
|
855
|
+
})) {
|
|
856
|
+
throw adapterError(`KLineCharts failed to update Drawing ${id} styles.`);
|
|
857
|
+
}
|
|
858
|
+
const updated = {
|
|
859
|
+
...structuredClone(source),
|
|
860
|
+
styles: structuredClone(styles),
|
|
861
|
+
};
|
|
862
|
+
this.#workspaceSources.set(id, updated);
|
|
863
|
+
this.#emitPort({
|
|
864
|
+
type: 'updated',
|
|
865
|
+
id,
|
|
866
|
+
drawing: timeSeriesSnapshotOfDrawing(updated),
|
|
867
|
+
editDimensions: { horizontal: false, vertical: false },
|
|
868
|
+
});
|
|
869
|
+
return timeSeriesSnapshotOfDrawing(updated);
|
|
870
|
+
}
|
|
871
|
+
updateDrawingText(id, text) {
|
|
872
|
+
this.#assertActive();
|
|
873
|
+
const source = this.#workspaceSources.get(id);
|
|
874
|
+
if (source === undefined) {
|
|
875
|
+
throw new TimeSeriesSceneError('TIME_SERIES_SCENE_SCHEMA_INVALID', `/drawings/${id}`, `Drawing ${id} does not exist.`);
|
|
876
|
+
}
|
|
877
|
+
if (!this.#chart.overrideOverlay({ id, extendData: text })) {
|
|
878
|
+
throw adapterError(`KLineCharts failed to update Drawing ${id} text.`);
|
|
879
|
+
}
|
|
880
|
+
const updated = timeSeriesWithDrawingText(structuredClone(source), text);
|
|
881
|
+
this.#workspaceSources.set(id, updated);
|
|
882
|
+
this.#emitPort({
|
|
883
|
+
type: 'updated',
|
|
884
|
+
id,
|
|
885
|
+
drawing: timeSeriesSnapshotOfDrawing(updated),
|
|
886
|
+
editDimensions: { horizontal: false, vertical: false },
|
|
887
|
+
});
|
|
888
|
+
return timeSeriesSnapshotOfDrawing(updated);
|
|
889
|
+
}
|
|
890
|
+
removeDrawing(id) {
|
|
891
|
+
this.#assertActive();
|
|
892
|
+
return this.#chart.removeOverlay({ id });
|
|
893
|
+
}
|
|
894
|
+
restoreDrawing(snapshot) {
|
|
895
|
+
this.#assertActive();
|
|
896
|
+
const drawing = timeSeriesDrawingFromSnapshot(snapshot);
|
|
897
|
+
const existing = this.#drawings.find((overlay) => overlay.id === drawing.id);
|
|
898
|
+
const overlay = drawingToSceneOverlay(drawing, TIME_SERIES_PANE_ID);
|
|
899
|
+
if (existing !== undefined) {
|
|
900
|
+
if (!this.#chart.overrideOverlay(toEngineOverlay(overlay, this.#overlayIdMap, `/drawings/${drawing.id}`, this.#overlayCallbacks(overlay)))) {
|
|
901
|
+
throw adapterError(`KLineCharts failed to restore Drawing ${drawing.id}.`);
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const result = this.#chart.createOverlay(toEngineOverlay(overlay, this.#overlayIdMap, `/drawings/${drawing.id}`, this.#overlayCallbacks(overlay)));
|
|
906
|
+
if (result !== drawing.id) {
|
|
907
|
+
throw adapterError(`KLineCharts failed to restore Drawing ${drawing.id}.`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
selectDrawing(id) {
|
|
911
|
+
this.#assertActive();
|
|
912
|
+
this.#selectOverlay(id);
|
|
913
|
+
}
|
|
914
|
+
hitTestDrawing(point) {
|
|
915
|
+
this.#assertActive();
|
|
916
|
+
const result = hitTestOverlayGeometries(point, this.#overlayGeometries());
|
|
917
|
+
return result === null ? null : result.overlayId;
|
|
918
|
+
}
|
|
919
|
+
projectToPixel(anchor, _paneRole) {
|
|
920
|
+
this.#assertActive();
|
|
921
|
+
return this.#toPixel(anchor, TIME_SERIES_PANE_ID);
|
|
922
|
+
}
|
|
923
|
+
unprojectFromPixel(point, _paneRole) {
|
|
924
|
+
this.#assertActive();
|
|
925
|
+
return this.#fromPixelToData(point);
|
|
926
|
+
}
|
|
927
|
+
setMutationsEnabled(enabled) {
|
|
928
|
+
this.#assertActive();
|
|
929
|
+
this.#mutationsEnabled = enabled;
|
|
930
|
+
}
|
|
931
|
+
subscribeDrawingEvents(listener) {
|
|
932
|
+
this.#portListeners.add(listener);
|
|
933
|
+
return () => {
|
|
934
|
+
this.#portListeners.delete(listener);
|
|
935
|
+
};
|
|
936
|
+
}
|
|
207
937
|
subscribeCrosshair(listener) {
|
|
208
938
|
this.#assertActive();
|
|
209
939
|
this.#crosshairListeners.add(listener);
|
|
@@ -281,6 +1011,44 @@ export class TimeSeriesChartsAdapter {
|
|
|
281
1011
|
: adapterError('KLineCharts failed to replace Time Series data.');
|
|
282
1012
|
}
|
|
283
1013
|
}
|
|
1014
|
+
/** 同结构场景替换:period/data/viewport 原子更新,series 结构必须一致。 */
|
|
1015
|
+
replaceScene(value) {
|
|
1016
|
+
this.#assertActive();
|
|
1017
|
+
const candidate = parseTimeSeriesScene(value);
|
|
1018
|
+
const seriesShape = (scene) => JSON.stringify(scene.series.map((series) => series.id));
|
|
1019
|
+
if (seriesShape(candidate) !== seriesShape(this.#scene)) {
|
|
1020
|
+
throw new TimeSeriesSceneError('TIME_SERIES_SCENE_SCHEMA_INVALID', '/series', 'Scene replacement requires identical series ids.');
|
|
1021
|
+
}
|
|
1022
|
+
const previous = this.#scene;
|
|
1023
|
+
const previousBackground = this.#container.style.backgroundColor;
|
|
1024
|
+
try {
|
|
1025
|
+
this.#chart.setPeriod(structuredClone(candidate.period));
|
|
1026
|
+
this.#chart.setDataLoader(dataLoader(candidate.data));
|
|
1027
|
+
this.#chart.resetData();
|
|
1028
|
+
this.#pointByTimestamp = new Map(candidate.data.map((point) => [point.timestamp, point]));
|
|
1029
|
+
this.#updateAxisVisibility();
|
|
1030
|
+
applyViewport(this.#chart, candidate.viewport);
|
|
1031
|
+
this.#scene = candidate;
|
|
1032
|
+
this.#container.style.backgroundColor =
|
|
1033
|
+
candidate.chart.layout.backgroundColor;
|
|
1034
|
+
return structuredClone(candidate);
|
|
1035
|
+
}
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
try {
|
|
1038
|
+
this.#chart.setPeriod(structuredClone(previous.period));
|
|
1039
|
+
this.#chart.setDataLoader(dataLoader(previous.data));
|
|
1040
|
+
this.#chart.resetData();
|
|
1041
|
+
this.#pointByTimestamp = new Map(previous.data.map((point) => [point.timestamp, point]));
|
|
1042
|
+
this.#updateAxisVisibility();
|
|
1043
|
+
applyViewport(this.#chart, previous.viewport);
|
|
1044
|
+
this.#container.style.backgroundColor = previousBackground;
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
// 回滚失败:保留原错误。
|
|
1048
|
+
}
|
|
1049
|
+
throw error;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
284
1052
|
exportScene() {
|
|
285
1053
|
this.#assertActive();
|
|
286
1054
|
return parseTimeSeriesScene(structuredClone(this.#scene));
|
|
@@ -294,8 +1062,128 @@ export class TimeSeriesChartsAdapter {
|
|
|
294
1062
|
this.#interactiveRoot.removeEventListener('pointerleave', this.#handlePointerLeave);
|
|
295
1063
|
this.#crosshairListeners.clear();
|
|
296
1064
|
this.#pointByTimestamp.clear();
|
|
1065
|
+
this.#portListeners.clear();
|
|
1066
|
+
this.#workspaceSources.clear();
|
|
1067
|
+
this.#drawings = [];
|
|
297
1068
|
this.#engine.dispose(this.#container);
|
|
298
1069
|
this.#container.replaceChildren();
|
|
299
1070
|
this.#container.style.backgroundColor = this.#originalBackground;
|
|
300
1071
|
}
|
|
301
1072
|
}
|
|
1073
|
+
function isControlledInteractionOverlay(overlay) {
|
|
1074
|
+
return overlay.type === 'horizontalStraightLine' || overlay.type === 'priceMeasurement';
|
|
1075
|
+
}
|
|
1076
|
+
function timeSeriesSnapshotOfDrawing(drawing) {
|
|
1077
|
+
return {
|
|
1078
|
+
id: drawing.id,
|
|
1079
|
+
type: drawing.type,
|
|
1080
|
+
target: structuredClone(drawing.target),
|
|
1081
|
+
geometry: structuredClone(drawing.geometry),
|
|
1082
|
+
styles: structuredClone(drawing.styles),
|
|
1083
|
+
locked: drawing.locked,
|
|
1084
|
+
visible: drawing.visible,
|
|
1085
|
+
zLevel: drawing.zLevel,
|
|
1086
|
+
mode: drawing.mode,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function timeSeriesDrawingFromSnapshot(snapshot) {
|
|
1090
|
+
return {
|
|
1091
|
+
id: snapshot.id,
|
|
1092
|
+
type: snapshot.type,
|
|
1093
|
+
target: structuredClone(snapshot.target),
|
|
1094
|
+
geometry: structuredClone(snapshot.geometry),
|
|
1095
|
+
styles: structuredClone(snapshot.styles),
|
|
1096
|
+
visible: snapshot.visible,
|
|
1097
|
+
locked: snapshot.locked,
|
|
1098
|
+
zLevel: snapshot.zLevel,
|
|
1099
|
+
mode: snapshot.mode,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function timeSeriesPlaceholderGeometry(type) {
|
|
1103
|
+
switch (type) {
|
|
1104
|
+
case 'horizontalStraightLine':
|
|
1105
|
+
case 'priceLine':
|
|
1106
|
+
return { value: 0 };
|
|
1107
|
+
case 'simpleTag':
|
|
1108
|
+
return { value: 0, text: '' };
|
|
1109
|
+
case 'verticalStraightLine':
|
|
1110
|
+
return { time: 0 };
|
|
1111
|
+
case 'horizontalRayLine':
|
|
1112
|
+
case 'horizontalSegment':
|
|
1113
|
+
return { value: 0, startTime: 0, endTime: 0 };
|
|
1114
|
+
case 'verticalRayLine':
|
|
1115
|
+
case 'verticalSegment':
|
|
1116
|
+
return { time: 0, startValue: 0, endValue: 0 };
|
|
1117
|
+
case 'rayLine':
|
|
1118
|
+
case 'segment':
|
|
1119
|
+
case 'straightLine':
|
|
1120
|
+
case 'fibonacciLine':
|
|
1121
|
+
return {
|
|
1122
|
+
points: [
|
|
1123
|
+
{ timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1124
|
+
{ timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1125
|
+
],
|
|
1126
|
+
};
|
|
1127
|
+
case 'priceChannelLine':
|
|
1128
|
+
case 'parallelStraightLine':
|
|
1129
|
+
return {
|
|
1130
|
+
points: [
|
|
1131
|
+
{ timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1132
|
+
{ timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1133
|
+
{ timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1134
|
+
],
|
|
1135
|
+
};
|
|
1136
|
+
case 'brush':
|
|
1137
|
+
return {
|
|
1138
|
+
points: [
|
|
1139
|
+
{ timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1140
|
+
{ timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1141
|
+
],
|
|
1142
|
+
};
|
|
1143
|
+
case 'simpleAnnotation':
|
|
1144
|
+
case 'callout':
|
|
1145
|
+
case 'text':
|
|
1146
|
+
return {
|
|
1147
|
+
point: { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1148
|
+
text: '',
|
|
1149
|
+
};
|
|
1150
|
+
case 'crossLine':
|
|
1151
|
+
return {
|
|
1152
|
+
point: { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1153
|
+
};
|
|
1154
|
+
case 'rectangle':
|
|
1155
|
+
case 'arrow':
|
|
1156
|
+
case 'priceMeasurement':
|
|
1157
|
+
return {
|
|
1158
|
+
start: { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1159
|
+
end: { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function timeSeriesPlaceholderDrawing(request) {
|
|
1164
|
+
return {
|
|
1165
|
+
id: request.id,
|
|
1166
|
+
type: request.type,
|
|
1167
|
+
target: structuredClone(request.target),
|
|
1168
|
+
geometry: timeSeriesPlaceholderGeometry(request.type),
|
|
1169
|
+
styles: structuredClone(request.styles),
|
|
1170
|
+
visible: true,
|
|
1171
|
+
locked: false,
|
|
1172
|
+
zLevel: 0,
|
|
1173
|
+
mode: 'normal',
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function timeSeriesWithDrawingText(drawing, text) {
|
|
1177
|
+
switch (drawing.type) {
|
|
1178
|
+
case 'simpleTag':
|
|
1179
|
+
case 'simpleAnnotation':
|
|
1180
|
+
case 'callout':
|
|
1181
|
+
case 'text':
|
|
1182
|
+
return {
|
|
1183
|
+
...structuredClone(drawing),
|
|
1184
|
+
geometry: { ...drawing.geometry, text },
|
|
1185
|
+
};
|
|
1186
|
+
default:
|
|
1187
|
+
return structuredClone(drawing);
|
|
1188
|
+
}
|
|
1189
|
+
}
|