@baron1996/klinecharts-adapter 0.2.3 → 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.
Files changed (47) hide show
  1. package/dist/adapter.d.ts +27 -2
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js +638 -26
  4. package/dist/conversion/chart-options.d.ts +3 -1
  5. package/dist/conversion/chart-options.d.ts.map +1 -1
  6. package/dist/conversion/chart-options.js +71 -0
  7. package/dist/conversion/overlays.d.ts +2 -1
  8. package/dist/conversion/overlays.d.ts.map +1 -1
  9. package/dist/conversion/overlays.js +1 -1
  10. package/dist/conversion/price.d.ts.map +1 -1
  11. package/dist/conversion/price.js +10 -39
  12. package/dist/drawing/engine-port.d.ts +71 -0
  13. package/dist/drawing/engine-port.d.ts.map +1 -0
  14. package/dist/drawing/engine-port.js +1 -0
  15. package/dist/drawing/interaction-normalization.d.ts +18 -0
  16. package/dist/drawing/interaction-normalization.d.ts.map +1 -0
  17. package/dist/drawing/interaction-normalization.js +61 -0
  18. package/dist/drawing/kline-projection-policy.d.ts +8 -0
  19. package/dist/drawing/kline-projection-policy.d.ts.map +1 -0
  20. package/dist/drawing/kline-projection-policy.js +69 -0
  21. package/dist/drawing/overlay-conversion.d.ts +9 -0
  22. package/dist/drawing/overlay-conversion.d.ts.map +1 -0
  23. package/dist/drawing/overlay-conversion.js +245 -0
  24. package/dist/drawing/projection-policy.d.ts +34 -0
  25. package/dist/drawing/projection-policy.d.ts.map +1 -0
  26. package/dist/drawing/projection-policy.js +10 -0
  27. package/dist/drawing/time-series-projection-policy.d.ts +6 -0
  28. package/dist/drawing/time-series-projection-policy.d.ts.map +1 -0
  29. package/dist/drawing/time-series-projection-policy.js +28 -0
  30. package/dist/extensions/price-measurement.d.ts +1 -1
  31. package/dist/extensions/price-measurement.d.ts.map +1 -1
  32. package/dist/extensions/price-measurement.js +8 -2
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +8 -0
  36. package/dist/main-series-presentation.d.ts +35 -0
  37. package/dist/main-series-presentation.d.ts.map +1 -0
  38. package/dist/main-series-presentation.js +52 -0
  39. package/dist/time-series/adapter.d.ts +41 -0
  40. package/dist/time-series/adapter.d.ts.map +1 -0
  41. package/dist/time-series/adapter.js +1189 -0
  42. package/dist/time-series/indicator.d.ts +15 -0
  43. package/dist/time-series/indicator.d.ts.map +1 -0
  44. package/dist/time-series/indicator.js +56 -0
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +2 -2
@@ -0,0 +1,1189 @@
1
+ import { parseDrawableWorkspaceDocument, parseTimeSeriesScene, TimeSeriesSceneError, SceneError, } from '@baron1996/kline-scene-schema';
2
+ import { toKLineChartsTimeSeriesOptions } from '../conversion/chart-options.js';
3
+ import { registerProjectOverlays } from '../extensions/register.js';
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';
11
+ import { TIME_SERIES_INDICATOR_NAME, TIME_SERIES_PANE_ID, TIME_SERIES_Y_AXIS_ID, timeSeriesIndicatorTemplate, toTimeSeriesIndicatorCreate, } from './indicator.js';
12
+ function carrierData(data) {
13
+ return data.map(({ timestamp, values }) => ({
14
+ timestamp,
15
+ open: 0,
16
+ high: 0,
17
+ low: 0,
18
+ close: 0,
19
+ volume: 0,
20
+ __baronTimeSeriesValues: normalizeValues(values),
21
+ }));
22
+ }
23
+ function normalizeValues(values) {
24
+ const normalized = {};
25
+ for (const [seriesId, value] of Object.entries(values)) {
26
+ if (value === undefined) {
27
+ throw adapterError(`Time Series value ${seriesId} is undefined.`);
28
+ }
29
+ normalized[seriesId] = value;
30
+ }
31
+ return normalized;
32
+ }
33
+ function dataLoader(data) {
34
+ const snapshot = carrierData(data);
35
+ return {
36
+ getBars({ type, callback }) {
37
+ callback(type === 'init' ? structuredClone(snapshot) : [], {
38
+ forward: false,
39
+ backward: false,
40
+ });
41
+ },
42
+ };
43
+ }
44
+ function hasVisibleFiniteValue(scene) {
45
+ const visible = new Set(scene.series.filter((series) => series.visible).map((series) => series.id));
46
+ return scene.data.some((point) => Object.entries(point.values).some(([id, value]) => visible.has(id) && value !== null));
47
+ }
48
+ function adapterError(message) {
49
+ return new TimeSeriesSceneError('TIME_SERIES_ADAPTER_FAILED', '/runtime/adapter', message);
50
+ }
51
+ function dataError(error) {
52
+ const issues = error.issues.map((issue) => ({
53
+ ...issue,
54
+ code: 'TIME_SERIES_DATA_INVALID',
55
+ path: issue.path.startsWith('/data') ? issue.path : '/data',
56
+ }));
57
+ return new TimeSeriesSceneError('TIME_SERIES_DATA_INVALID', '/data', 'Time Series replacement data is invalid.', issues);
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
+ }
80
+ /** TimeSeriesScene 与 KLineCharts 之间的唯一受控边界。 */
81
+ export class TimeSeriesChartsAdapter {
82
+ /** KLineCharts 实例,只能在 Adapter 内使用。 */
83
+ #chart;
84
+ /** 引擎模块,用于版本检查和精确销毁。 */
85
+ #engine;
86
+ /** 当前可导出的规范化 Scene。 */
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;
98
+ /** 按时间戳索引的原始点,保证十字线查询为 O(1)。 */
99
+ #pointByTimestamp;
100
+ /** Adapter 独占的图表容器。 */
101
+ #container;
102
+ /** KLineCharts 交互根节点,用于补全离开绘图区事件。 */
103
+ #interactiveRoot;
104
+ /** 创建前容器背景,销毁后恢复。 */
105
+ #originalBackground;
106
+ /** 十字线纯数据监听器。 */
107
+ #crosshairListeners = new Set();
108
+ /** 当前受控拖拽会话。 */
109
+ #pointerInteraction;
110
+ /** 内部确定性交互序号。 */
111
+ #interactionSequence = 0;
112
+ /** 防止重复销毁底层引擎。 */
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
+ };
127
+ constructor(container, scene, chart, engine, originalBackground, interactiveRoot) {
128
+ this.#container = container;
129
+ this.#scene = scene;
130
+ this.#overlayIdMap = createTimeSeriesOverlayIdMap();
131
+ this.#drawings = [];
132
+ this.#pricePrecision = scene.series[0]?.precision ?? 0;
133
+ this.#pointByTimestamp = new Map(scene.data.map((point) => [point.timestamp, point]));
134
+ this.#chart = chart;
135
+ this.#engine = engine;
136
+ this.#originalBackground = originalBackground;
137
+ this.#interactiveRoot = interactiveRoot;
138
+ this.#chart.subscribeAction('onCrosshairChange', this.#handleCrosshair);
139
+ this.#installInteractionListeners();
140
+ this.#interactiveRoot.addEventListener('pointerleave', this.#handlePointerLeave);
141
+ }
142
+ static async create(container, value) {
143
+ const scene = parseTimeSeriesScene(value);
144
+ const originalBackground = container.style.backgroundColor;
145
+ const engine = await import('klinecharts');
146
+ let chart = null;
147
+ try {
148
+ if (engine.version() !== scene.runtime.engineVersion) {
149
+ throw adapterError('KLineCharts engine version does not match the Scene.');
150
+ }
151
+ chart = engine.init(container, toKLineChartsTimeSeriesOptions(scene.chart));
152
+ if (chart === null) {
153
+ throw adapterError('KLineCharts returned null while initializing.');
154
+ }
155
+ const root = container.firstElementChild;
156
+ if (!(root instanceof HTMLElement)) {
157
+ throw adapterError('KLineCharts did not create an interactive root.');
158
+ }
159
+ root.style.touchAction = 'none';
160
+ chart.setSymbol({
161
+ ticker: '@baron1996/time-series-scene',
162
+ pricePrecision: scene.series[0].precision,
163
+ volumePrecision: 0,
164
+ });
165
+ registerProjectOverlays(engine.registerOverlay);
166
+ chart.setPeriod(structuredClone(scene.period));
167
+ chart.setDataLoader(dataLoader(scene.data));
168
+ engine.registerIndicator(timeSeriesIndicatorTemplate);
169
+ for (const series of scene.series) {
170
+ const expectedId = `baron_time_series_${series.id}`;
171
+ const createdId = chart.createIndicator(toTimeSeriesIndicatorCreate(series), true);
172
+ if (createdId !== expectedId) {
173
+ throw adapterError(`KLineCharts failed to create series ${series.id}.`);
174
+ }
175
+ }
176
+ chart.setPaneOptions({
177
+ id: 'candle_pane',
178
+ height: 0,
179
+ minHeight: 0,
180
+ order: 0,
181
+ state: 'minimize',
182
+ dragEnabled: false,
183
+ });
184
+ chart.setPaneOptions({
185
+ id: TIME_SERIES_PANE_ID,
186
+ height: Math.max(container.clientHeight, 240),
187
+ minHeight: 120,
188
+ order: 1,
189
+ state: 'normal',
190
+ dragEnabled: false,
191
+ });
192
+ const adapter = new TimeSeriesChartsAdapter(container, scene, chart, engine, originalBackground, root);
193
+ adapter.#updateAxisVisibility();
194
+ applyViewport(chart, scene.viewport);
195
+ container.style.backgroundColor = scene.chart.layout.backgroundColor;
196
+ return adapter;
197
+ }
198
+ catch (error) {
199
+ if (chart !== null) {
200
+ engine.dispose(container);
201
+ }
202
+ container.replaceChildren();
203
+ container.style.backgroundColor = originalBackground;
204
+ if (error instanceof TimeSeriesSceneError) {
205
+ throw error;
206
+ }
207
+ throw adapterError('KLineCharts failed to initialize TimeSeriesScene.');
208
+ }
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
+ }
285
+ #assertActive() {
286
+ if (this.#disposed) {
287
+ throw adapterError('The Time Series Adapter has already been disposed.');
288
+ }
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
+ }
324
+ #updateAxisVisibility() {
325
+ const hasValues = hasVisibleFiniteValue(this.#scene);
326
+ this.#chart.overrideYAxis({
327
+ id: TIME_SERIES_Y_AXIS_ID,
328
+ paneId: TIME_SERIES_PANE_ID,
329
+ name: 'normal',
330
+ position: 'right',
331
+ inside: false,
332
+ scrollZoomEnabled: false,
333
+ gap: { top: 0.12, bottom: 0.08 },
334
+ needWidget: hasValues,
335
+ createTicks: ({ defaultTicks }) => hasValues ? defaultTicks : [],
336
+ });
337
+ }
338
+ #handleCrosshair = (value) => {
339
+ if (this.#disposed) {
340
+ return;
341
+ }
342
+ let timestamp = null;
343
+ if (value !== null && typeof value === 'object') {
344
+ if ('timestamp' in value && typeof value.timestamp === 'number') {
345
+ timestamp = value.timestamp;
346
+ }
347
+ else if ('x' in value && typeof value.x === 'number') {
348
+ const converted = this.#chart.convertFromPixel([{ x: value.x, y: 0 }], {
349
+ paneId: TIME_SERIES_PANE_ID,
350
+ yAxisId: TIME_SERIES_Y_AXIS_ID,
351
+ });
352
+ const point = Array.isArray(converted) ? converted[0] : converted;
353
+ timestamp = typeof point?.timestamp === 'number'
354
+ ? point.timestamp
355
+ : null;
356
+ }
357
+ }
358
+ const point = timestamp === null
359
+ ? undefined
360
+ : this.#pointByTimestamp.get(timestamp);
361
+ const event = point === undefined
362
+ ? { timestamp: null, values: null }
363
+ : { timestamp: point.timestamp, values: normalizeValues(point.values) };
364
+ for (const listener of this.#crosshairListeners) {
365
+ listener(structuredClone(event));
366
+ }
367
+ };
368
+ #handlePointerLeave = () => {
369
+ if (this.#disposed) {
370
+ return;
371
+ }
372
+ for (const listener of this.#crosshairListeners) {
373
+ listener({ timestamp: null, values: null });
374
+ }
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
+ }
937
+ subscribeCrosshair(listener) {
938
+ this.#assertActive();
939
+ this.#crosshairListeners.add(listener);
940
+ return () => this.#crosshairListeners.delete(listener);
941
+ }
942
+ setSeriesVisible(seriesId, visible) {
943
+ this.#assertActive();
944
+ const index = this.#scene.series.findIndex((series) => series.id === seriesId);
945
+ if (index < 0) {
946
+ throw new TimeSeriesSceneError('TIME_SERIES_UNKNOWN_SERIES', '/series', `Unknown time series: ${seriesId}.`);
947
+ }
948
+ const series = structuredClone(this.#scene.series);
949
+ series[index] = { ...series[index], visible };
950
+ const candidate = parseTimeSeriesScene({
951
+ ...structuredClone(this.#scene),
952
+ series,
953
+ });
954
+ const indicatorId = `baron_time_series_${seriesId}`;
955
+ const matches = this.#chart.getIndicators({ id: indicatorId });
956
+ if (matches.length !== 1) {
957
+ throw adapterError(`KLineCharts retained ${matches.length} indicators for series ${seriesId}.`);
958
+ }
959
+ this.#chart.overrideIndicator({
960
+ id: indicatorId,
961
+ name: TIME_SERIES_INDICATOR_NAME,
962
+ visible,
963
+ });
964
+ const afterVisible = this.#chart.getIndicators({ id: indicatorId })[0]?.visible;
965
+ if (afterVisible !== visible) {
966
+ throw adapterError(`KLineCharts failed to update series ${seriesId}.`);
967
+ }
968
+ this.#scene = candidate;
969
+ this.#updateAxisVisibility();
970
+ return structuredClone(candidate);
971
+ }
972
+ replaceData(data) {
973
+ this.#assertActive();
974
+ const last = data.at(-1);
975
+ let candidate;
976
+ try {
977
+ candidate = parseTimeSeriesScene({
978
+ ...structuredClone(this.#scene),
979
+ data: structuredClone(data),
980
+ viewport: {
981
+ ...structuredClone(this.#scene.viewport),
982
+ anchorTimestamp: last?.timestamp,
983
+ },
984
+ });
985
+ }
986
+ catch (error) {
987
+ throw error instanceof TimeSeriesSceneError
988
+ ? dataError(error)
989
+ : adapterError('Time Series replacement data validation failed.');
990
+ }
991
+ const previous = this.#scene;
992
+ const previousPointByTimestamp = this.#pointByTimestamp;
993
+ const candidatePointByTimestamp = new Map(candidate.data.map((point) => [point.timestamp, point]));
994
+ try {
995
+ this.#chart.setDataLoader(dataLoader(candidate.data));
996
+ this.#chart.resetData();
997
+ this.#scene = candidate;
998
+ this.#pointByTimestamp = candidatePointByTimestamp;
999
+ this.#updateAxisVisibility();
1000
+ applyViewport(this.#chart, candidate.viewport);
1001
+ return structuredClone(candidate);
1002
+ }
1003
+ catch (error) {
1004
+ this.#chart.setDataLoader(dataLoader(previous.data));
1005
+ this.#chart.resetData();
1006
+ this.#scene = previous;
1007
+ this.#pointByTimestamp = previousPointByTimestamp;
1008
+ this.#updateAxisVisibility();
1009
+ throw error instanceof TimeSeriesSceneError
1010
+ ? error
1011
+ : adapterError('KLineCharts failed to replace Time Series data.');
1012
+ }
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
+ }
1052
+ exportScene() {
1053
+ this.#assertActive();
1054
+ return parseTimeSeriesScene(structuredClone(this.#scene));
1055
+ }
1056
+ dispose() {
1057
+ if (this.#disposed) {
1058
+ return;
1059
+ }
1060
+ this.#disposed = true;
1061
+ this.#chart.unsubscribeAction('onCrosshairChange', this.#handleCrosshair);
1062
+ this.#interactiveRoot.removeEventListener('pointerleave', this.#handlePointerLeave);
1063
+ this.#crosshairListeners.clear();
1064
+ this.#pointByTimestamp.clear();
1065
+ this.#portListeners.clear();
1066
+ this.#workspaceSources.clear();
1067
+ this.#drawings = [];
1068
+ this.#engine.dispose(this.#container);
1069
+ this.#container.replaceChildren();
1070
+ this.#container.style.backgroundColor = this.#originalBackground;
1071
+ }
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
+ }