@baron1996/klinecharts-adapter 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +6 -0
  2. package/dist/adapter.d.ts +40 -1
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js +408 -22
  5. package/dist/conversion/overlays.d.ts +4 -1
  6. package/dist/conversion/overlays.d.ts.map +1 -1
  7. package/dist/conversion/overlays.js +26 -20
  8. package/dist/conversion/panes.d.ts +3 -1
  9. package/dist/conversion/panes.d.ts.map +1 -1
  10. package/dist/conversion/panes.js +7 -1
  11. package/dist/conversion/price.d.ts +6 -0
  12. package/dist/conversion/price.d.ts.map +1 -0
  13. package/dist/conversion/price.js +48 -0
  14. package/dist/engine.d.ts.map +1 -1
  15. package/dist/engine.js +3 -3
  16. package/dist/extensions/price-measurement.d.ts +13 -0
  17. package/dist/extensions/price-measurement.d.ts.map +1 -0
  18. package/dist/extensions/price-measurement.js +64 -0
  19. package/dist/extensions/register.d.ts.map +1 -1
  20. package/dist/extensions/register.js +2 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/interaction/dragging.d.ts +12 -0
  25. package/dist/interaction/dragging.d.ts.map +1 -0
  26. package/dist/interaction/dragging.js +71 -0
  27. package/dist/interaction/hit-testing.d.ts +26 -0
  28. package/dist/interaction/hit-testing.d.ts.map +1 -0
  29. package/dist/interaction/hit-testing.js +86 -0
  30. package/dist/interaction/selection-arbitration.d.ts +6 -0
  31. package/dist/interaction/selection-arbitration.d.ts.map +1 -0
  32. package/dist/interaction/selection-arbitration.js +10 -0
  33. package/dist/registry/overlays.d.ts +2 -2
  34. package/dist/registry/overlays.d.ts.map +1 -1
  35. package/dist/registry/overlays.js +1 -0
  36. package/dist/version.d.ts +3 -2
  37. package/dist/version.d.ts.map +1 -1
  38. package/dist/version.js +3 -2
  39. package/package.json +2 -2
package/README.md CHANGED
@@ -8,3 +8,9 @@ npm install @baron1996/klinecharts-adapter
8
8
 
9
9
  The adapter loads only scene-owned static data, maps supported panes, indicators and
10
10
  overlays, and owns chart lifecycle and touch gestures. KLineCharts is the only engine.
11
+
12
+ At the engine-to-Scene boundary, every price-bearing Overlay coordinate is normalized
13
+ to `scene.symbol.pricePrecision` (validated as `0..16`). Normalization uses the finite
14
+ JavaScript Number's decimal representation, rounds to nearest with exact halfway values
15
+ away from zero, rejects non-finite input/output, and canonicalizes negative zero to
16
+ positive zero. Timestamp coordinates are never changed by this rule.
package/dist/adapter.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { ChartScene, SceneIndicator, SceneOverlay } from '@baron1996/kline-scene-schema';
2
2
  import { SceneError } from '@baron1996/kline-scene-schema';
3
3
  import { type OverlayDrawingSource } from './conversion/overlays.js';
4
+ import { type OverlayHitResult, type PixelCoordinate } from './interaction/hit-testing.js';
5
+ export type PriceScale = 'linear' | 'logarithmic';
6
+ export type AdapterDragTarget = 'body' | 'anchor';
7
+ export type AdapterDragCancelReason = 'escape' | 'pointer-cancel' | 'window-blur' | 'destroy' | 'validation-error';
4
8
  export interface AdapterIndicatorSnapshot {
5
9
  readonly id: string;
6
10
  readonly name: SceneIndicator['name'];
@@ -9,6 +13,7 @@ export interface AdapterIndicatorSnapshot {
9
13
  }
10
14
  export interface AdapterSnapshot {
11
15
  readonly engineVersion: string;
16
+ readonly runtimeVersion: ChartScene['runtime']['runtimeVersion'];
12
17
  readonly dataCount: number;
13
18
  readonly paneIds: readonly string[];
14
19
  readonly indicators: readonly AdapterIndicatorSnapshot[];
@@ -16,19 +21,45 @@ export interface AdapterSnapshot {
16
21
  readonly barSpace: number;
17
22
  readonly rightOffsetDistance: number;
18
23
  }
24
+ interface AdapterDragEventIdentity {
25
+ readonly interactionId: string;
26
+ readonly overlayId: string;
27
+ readonly target: AdapterDragTarget;
28
+ readonly anchorIndex: number | null;
29
+ readonly before: SceneOverlay;
30
+ }
19
31
  export type AdapterSceneEvent = {
20
32
  readonly type: 'overlay-created';
21
33
  readonly overlay: SceneOverlay;
22
34
  } | {
23
35
  readonly type: 'overlay-updated';
24
36
  readonly overlay: SceneOverlay;
37
+ } | {
38
+ readonly type: 'overlay-style-changed';
39
+ readonly before: SceneOverlay;
40
+ readonly overlay: SceneOverlay;
25
41
  } | {
26
42
  readonly type: 'overlay-removed';
27
43
  readonly id: string;
44
+ } | {
45
+ readonly type: 'overlay-selection-changed';
46
+ readonly previousId: string | null;
47
+ readonly id: string | null;
28
48
  } | {
29
49
  readonly type: 'overlay-selected';
30
50
  readonly id: string;
31
- } | {
51
+ } | ({
52
+ readonly type: 'overlay-drag-started';
53
+ } & AdapterDragEventIdentity) | ({
54
+ readonly type: 'overlay-dragging';
55
+ readonly candidate: SceneOverlay;
56
+ } & AdapterDragEventIdentity) | ({
57
+ readonly type: 'overlay-drag-committed';
58
+ readonly overlay: SceneOverlay;
59
+ } & AdapterDragEventIdentity) | ({
60
+ readonly type: 'overlay-drag-cancelled';
61
+ readonly reason: AdapterDragCancelReason;
62
+ } & AdapterDragEventIdentity) | {
32
63
  readonly type: 'scene-error';
33
64
  readonly issues: readonly SceneError['issues'][number][];
34
65
  };
@@ -45,8 +76,15 @@ export declare class KLineChartsSceneAdapter {
45
76
  static create(container: HTMLElement, value: unknown): Promise<KLineChartsSceneAdapter>;
46
77
  subscribe(listener: AdapterSceneEventListener): () => void;
47
78
  exportScene(): ChartScene;
79
+ setPriceScale(scale: PriceScale): Promise<ChartScene>;
80
+ projectPoint(point: {
81
+ readonly timestamp: number;
82
+ readonly value: number;
83
+ }, paneId?: string): PixelCoordinate;
84
+ hitTestOverlay(point: PixelCoordinate): OverlayHitResult | null;
48
85
  addOverlay(value: SceneOverlay): SceneOverlay;
49
86
  updateOverlay(value: SceneOverlay): SceneOverlay;
87
+ updateOverlayStyles(id: string, styles: SceneOverlay['styles']): SceneOverlay;
50
88
  removeOverlay(id: string): boolean;
51
89
  startOverlayDrawing(request: OverlayDrawingRequest): string;
52
90
  getOverlay(id: string): SceneOverlay | undefined;
@@ -54,4 +92,5 @@ export declare class KLineChartsSceneAdapter {
54
92
  inspect(): AdapterSnapshot;
55
93
  dispose(): void;
56
94
  }
95
+ export {};
57
96
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,UAAU,EACV,cAAc,EACd,YAAY,EACZ,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAEN,UAAU,EACV,MAAM,+BAA+B,CAAC;AAUvC,OAAO,EAIN,KAAK,oBAAoB,EAGzB,MAAM,0BAA0B,CAAC;AAGlC,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACzD,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,MAAM,iBAAiB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,CAAA;CAAE,CAAC;AAE9F,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAE3E,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;CAAG;AAEtE;;;GAGG;AACH,qBAAa,uBAAuB;;IAkBnC,OAAO;WAea,MAAM,CACzB,SAAS,EAAE,WAAW,EACtB,KAAK,EAAE,OAAO,GACZ,OAAO,CAAC,uBAAuB,CAAC;IAyH5B,SAAS,CAAC,QAAQ,EAAE,yBAAyB,GAAG,MAAM,IAAI;IAQ1D,WAAW,IAAI,UAAU;IAoBzB,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY;IA4B7C,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY;IA6BhD,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAuBlC,mBAAmB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM;IAyB3D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAIhD,YAAY,IAAI,SAAS,YAAY,EAAE;IAIvC,OAAO,IAAI,eAAe;IA8B1B,OAAO,IAAI,IAAI;CAUtB"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -2,9 +2,28 @@ import { parseChartScene, SceneError, } from '@baron1996/kline-scene-schema';
2
2
  import { createEngine } from './engine.js';
3
3
  import { registerProjectOverlays } from './extensions/register.js';
4
4
  import { createEngineIdMap, } from './conversion/id-map.js';
5
- import { applyPanes } from './conversion/panes.js';
5
+ import { applyPanes, overrideSceneYAxis } from './conversion/panes.js';
6
6
  import { createSceneOverlays, fromEngineOverlay, toEngineOverlay, toEngineOverlayDrawing, } from './conversion/overlays.js';
7
7
  import { applyViewport } from './conversion/viewport.js';
8
+ import { createDragCandidate, } from './interaction/dragging.js';
9
+ import { hitTestOverlayGeometries, } from './interaction/hit-testing.js';
10
+ import { shouldIgnoreStaleOverlayDeselection } from './interaction/selection-arbitration.js';
11
+ function promoteSceneToM2(scene, scale) {
12
+ const candidate = structuredClone(scene);
13
+ candidate.runtime.runtimeVersion = '0.2.0';
14
+ for (const pane of candidate.panes) {
15
+ for (const axis of pane.yAxes) {
16
+ axis.scale =
17
+ pane.kind === 'candle' && axis.role === 'primary' && scale !== undefined
18
+ ? scale
19
+ : axis.scale ?? 'linear';
20
+ }
21
+ }
22
+ return candidate;
23
+ }
24
+ function isControlledInteractionOverlay(overlay) {
25
+ return overlay.type === 'horizontalStraightLine' || overlay.type === 'priceMeasurement';
26
+ }
8
27
  /**
9
28
  * ChartScene 与 KLineCharts 之间的唯一边界。
10
29
  * 引擎对象和内部 ID 永不从该类的公共接口泄露。
@@ -16,7 +35,7 @@ export class KLineChartsSceneAdapter {
16
35
  #engine;
17
36
  /** 场景 ID 与引擎内部 ID 的双向映射。 */
18
37
  #idMap;
19
- /** 当前可导出的规范化场景。 */
38
+ /** 当前最后一次成功提交、可导出的规范化场景。 */
20
39
  #scene;
21
40
  /** 当前引擎容器。 */
22
41
  #container;
@@ -26,6 +45,12 @@ export class KLineChartsSceneAdapter {
26
45
  #disposed = false;
27
46
  /** 仅传递纯场景数据的事件订阅者。 */
28
47
  #listeners = new Set();
48
+ /** 当前选择状态,null 表示明确未选择。 */
49
+ #selectedOverlayId = null;
50
+ /** 当前受控拖动事务;progress 永不写入 #scene。 */
51
+ #pointerInteraction;
52
+ /** 确定性 opaque 交互 ID 序号。 */
53
+ #interactionSequence = 0;
29
54
  constructor(container, scene, handle, idMap, originalBackground) {
30
55
  this.#container = container;
31
56
  this.#scene = scene;
@@ -33,6 +58,7 @@ export class KLineChartsSceneAdapter {
33
58
  this.#engine = handle.module;
34
59
  this.#idMap = idMap;
35
60
  this.#originalBackground = originalBackground;
61
+ this.#installInteractionListeners();
36
62
  }
37
63
  static async create(container, value) {
38
64
  const scene = parseChartScene(value);
@@ -75,11 +101,22 @@ export class KLineChartsSceneAdapter {
75
101
  listener(structuredClone(event));
76
102
  }
77
103
  }
104
+ #selectOverlay(id) {
105
+ const previousId = this.#selectedOverlayId;
106
+ if (previousId === id) {
107
+ return;
108
+ }
109
+ this.#selectedOverlayId = id;
110
+ this.#emit({ type: 'overlay-selection-changed', previousId, id });
111
+ if (id !== null) {
112
+ this.#emit({ type: 'overlay-selected', id });
113
+ }
114
+ }
78
115
  #commitEngineOverlay(engineOverlay, source, kind) {
79
116
  const existingIndex = this.#scene.overlays.findIndex((overlay) => overlay.id === source.id);
80
117
  const path = existingIndex < 0 ? `/overlays/${this.#scene.overlays.length}` : `/overlays/${existingIndex}`;
81
118
  const currentSource = existingIndex < 0 ? source : this.#scene.overlays[existingIndex];
82
- const overlay = fromEngineOverlay(engineOverlay, currentSource, this.#idMap, path);
119
+ const overlay = fromEngineOverlay(engineOverlay, currentSource, this.#idMap, path, this.#scene.symbol.pricePrecision);
83
120
  const overlays = structuredClone(this.#scene.overlays);
84
121
  if (existingIndex < 0) {
85
122
  overlays.push(overlay);
@@ -101,11 +138,28 @@ export class KLineChartsSceneAdapter {
101
138
  onDrawEnd: ({ overlay }) => {
102
139
  this.#safelyCommitEngineOverlay(overlay, source, drawing ? 'created' : 'updated');
103
140
  },
141
+ onPressedMoveStart: ({ overlay }) => {
142
+ this.#selectOverlay(overlay.id);
143
+ },
104
144
  onPressedMoveEnd: ({ overlay }) => {
105
- this.#safelyCommitEngineOverlay(overlay, source, 'updated');
145
+ if (!isControlledInteractionOverlay(source)) {
146
+ this.#safelyCommitEngineOverlay(overlay, source, 'updated');
147
+ }
106
148
  },
107
149
  onSelected: ({ overlay }) => {
108
- this.#emit({ type: 'overlay-selected', id: overlay.id });
150
+ this.#selectOverlay(overlay.id);
151
+ },
152
+ onDeselected: (event) => {
153
+ const eventX = event.x;
154
+ const eventY = event.y;
155
+ const coordinate = typeof eventX === 'number' && Number.isFinite(eventX) &&
156
+ typeof eventY === 'number' && Number.isFinite(eventY)
157
+ ? { x: eventX, y: eventY }
158
+ : undefined;
159
+ if (this.#selectedOverlayId === event.overlay.id &&
160
+ !shouldIgnoreStaleOverlayDeselection(this.#selectedOverlayId, event.overlay.id, coordinate, this.#overlayGeometries())) {
161
+ this.#selectOverlay(null);
162
+ }
109
163
  },
110
164
  onRemoved: ({ overlay }) => {
111
165
  if (this.#scene.overlays.some((candidate) => candidate.id === overlay.id)) {
@@ -113,6 +167,9 @@ export class KLineChartsSceneAdapter {
113
167
  ...structuredClone(this.#scene),
114
168
  overlays: this.#scene.overlays.filter((candidate) => candidate.id !== overlay.id),
115
169
  });
170
+ if (this.#selectedOverlayId === overlay.id) {
171
+ this.#selectOverlay(null);
172
+ }
116
173
  this.#emit({ type: 'overlay-removed', id: overlay.id });
117
174
  }
118
175
  },
@@ -130,6 +187,272 @@ export class KLineChartsSceneAdapter {
130
187
  throw error;
131
188
  }
132
189
  }
190
+ #installInteractionListeners() {
191
+ this.#container.addEventListener('pointerdown', this.#handlePointerDown, true);
192
+ this.#container.addEventListener('pointermove', this.#handlePointerMove, true);
193
+ this.#container.addEventListener('pointerup', this.#handlePointerUp, true);
194
+ this.#container.addEventListener('pointercancel', this.#handlePointerCancel, true);
195
+ window.addEventListener('keydown', this.#handleKeyDown);
196
+ window.addEventListener('blur', this.#handleWindowBlur);
197
+ }
198
+ #removeInteractionListeners() {
199
+ this.#container.removeEventListener('pointerdown', this.#handlePointerDown, true);
200
+ this.#container.removeEventListener('pointermove', this.#handlePointerMove, true);
201
+ this.#container.removeEventListener('pointerup', this.#handlePointerUp, true);
202
+ this.#container.removeEventListener('pointercancel', this.#handlePointerCancel, true);
203
+ window.removeEventListener('keydown', this.#handleKeyDown);
204
+ window.removeEventListener('blur', this.#handleWindowBlur);
205
+ }
206
+ #pointerCoordinate(event) {
207
+ const rect = this.#container.getBoundingClientRect();
208
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
209
+ }
210
+ #primaryAxisFilter(paneId) {
211
+ const pane = this.#scene.panes.find((candidate) => candidate.id === paneId);
212
+ const axis = pane?.yAxes.find((candidate) => candidate.role === 'primary');
213
+ const enginePaneId = this.#idMap.paneToEngine.get(paneId);
214
+ const engineAxisId = axis === undefined ? undefined : this.#idMap.yAxisToEngine.get(axis.id);
215
+ if (enginePaneId === undefined || engineAxisId === undefined) {
216
+ throw new SceneError('INVALID_REFERENCE', '/panes', 'Overlay Pane or primary Y-axis is unmapped.');
217
+ }
218
+ return { paneId: enginePaneId, yAxisId: engineAxisId, absolute: true };
219
+ }
220
+ #toPixel(point, paneId) {
221
+ const converted = this.#chart.convertToPixel(point, this.#primaryAxisFilter(paneId));
222
+ if (!Number.isFinite(converted.x) || !Number.isFinite(converted.y)) {
223
+ throw new SceneError('EXPORT_INVALID', '/overlays', 'KLineCharts returned a non-finite pixel coordinate.');
224
+ }
225
+ return { x: converted.x, y: converted.y };
226
+ }
227
+ #fromPixel(point, paneId) {
228
+ const converted = this.#chart.convertFromPixel([point], this.#primaryAxisFilter(paneId));
229
+ const value = converted[0];
230
+ if (!Number.isFinite(value?.dataIndex) || !Number.isFinite(value?.value)) {
231
+ throw new SceneError('INVALID_REFERENCE', '/overlays', 'Pointer does not map to finite chart data.');
232
+ }
233
+ return { dataIndex: value.dataIndex, value: value.value };
234
+ }
235
+ #overlayGeometries() {
236
+ const geometries = [];
237
+ for (let sceneIndex = 0; sceneIndex < this.#scene.overlays.length; sceneIndex++) {
238
+ const overlay = this.#scene.overlays[sceneIndex];
239
+ if (overlay === undefined || !overlay.visible) {
240
+ continue;
241
+ }
242
+ if (overlay.type === 'horizontalStraightLine') {
243
+ const anchor = overlay.anchor;
244
+ if (anchor === undefined || !('value' in anchor)) {
245
+ continue;
246
+ }
247
+ const paneFilter = this.#primaryAxisFilter(overlay.paneId);
248
+ const paneMain = this.#chart.getDom(paneFilter.paneId, 'main');
249
+ const containerRect = this.#container.getBoundingClientRect();
250
+ const mainRect = paneMain?.getBoundingClientRect() ?? containerRect;
251
+ const projected = this.#toPixel({ timestamp: this.#scene.data[0].timestamp, value: anchor.value }, overlay.paneId);
252
+ const start = { x: mainRect.left - containerRect.left, y: projected.y };
253
+ const end = { x: mainRect.right - containerRect.left, y: projected.y };
254
+ geometries.push({
255
+ overlayId: overlay.id,
256
+ sceneIndex,
257
+ zLevel: overlay.zLevel,
258
+ locked: overlay.locked,
259
+ anchors: [{ x: (start.x + end.x) / 2, y: projected.y }],
260
+ bodySegments: [[start, end]],
261
+ });
262
+ continue;
263
+ }
264
+ if (overlay.type === 'priceMeasurement' && overlay.start !== undefined && overlay.end !== undefined) {
265
+ const start = this.#toPixel(overlay.start, overlay.paneId);
266
+ const end = this.#toPixel(overlay.end, overlay.paneId);
267
+ geometries.push({
268
+ overlayId: overlay.id,
269
+ sceneIndex,
270
+ zLevel: overlay.zLevel,
271
+ locked: overlay.locked,
272
+ anchors: [start, end],
273
+ bodySegments: [[start, end]],
274
+ });
275
+ continue;
276
+ }
277
+ if (overlay.points !== undefined && overlay.points.length >= 2) {
278
+ const anchors = overlay.points.map((point) => this.#toPixel(point, overlay.paneId));
279
+ const bodySegments = [];
280
+ for (let pointIndex = 1; pointIndex < anchors.length; pointIndex++) {
281
+ bodySegments.push([anchors[pointIndex - 1], anchors[pointIndex]]);
282
+ }
283
+ geometries.push({
284
+ overlayId: overlay.id,
285
+ sceneIndex,
286
+ zLevel: overlay.zLevel,
287
+ locked: overlay.locked,
288
+ anchors,
289
+ bodySegments,
290
+ });
291
+ }
292
+ }
293
+ return geometries;
294
+ }
295
+ #interactionIdentity(interaction) {
296
+ return {
297
+ interactionId: interaction.interactionId,
298
+ overlayId: interaction.hit.overlayId,
299
+ target: interaction.hit.target,
300
+ anchorIndex: interaction.hit.anchorIndex,
301
+ before: structuredClone(interaction.before),
302
+ };
303
+ }
304
+ #stopPointerCapture(interaction) {
305
+ if (this.#container.hasPointerCapture(interaction.pointerId)) {
306
+ this.#container.releasePointerCapture(interaction.pointerId);
307
+ }
308
+ }
309
+ #cancelInteraction(reason, error) {
310
+ const interaction = this.#pointerInteraction;
311
+ if (interaction === undefined) {
312
+ return;
313
+ }
314
+ this.#pointerInteraction = undefined;
315
+ this.#stopPointerCapture(interaction);
316
+ if (!interaction.started) {
317
+ return;
318
+ }
319
+ const index = this.#scene.overlays.findIndex((overlay) => overlay.id === interaction.before.id);
320
+ if (index < 0 ||
321
+ !this.#chart.overrideOverlay(toEngineOverlay(interaction.before, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(interaction.before)))) {
322
+ throw new SceneError('RUNTIME_INIT_FAILED', '/overlays', `KLineCharts failed to restore Overlay ${interaction.before.id}.`);
323
+ }
324
+ this.#emit({
325
+ type: 'overlay-drag-cancelled',
326
+ ...this.#interactionIdentity(interaction),
327
+ reason,
328
+ });
329
+ if (reason === 'validation-error' && error !== undefined) {
330
+ this.#emit({ type: 'scene-error', issues: structuredClone(error.issues) });
331
+ }
332
+ }
333
+ #handlePointerDown = (event) => {
334
+ if (this.#disposed || event.button !== 0 || this.#pointerInteraction !== undefined) {
335
+ return;
336
+ }
337
+ const coordinate = this.#pointerCoordinate(event);
338
+ const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries());
339
+ if (hit === null) {
340
+ const selected = this.#scene.overlays.find((overlay) => overlay.id === this.#selectedOverlayId);
341
+ if (selected !== undefined && isControlledInteractionOverlay(selected)) {
342
+ this.#selectOverlay(null);
343
+ }
344
+ return;
345
+ }
346
+ const before = this.#scene.overlays.find((overlay) => overlay.id === hit.overlayId);
347
+ if (before === undefined) {
348
+ return;
349
+ }
350
+ this.#selectOverlay(before.id);
351
+ if (!isControlledInteractionOverlay(before)) {
352
+ return;
353
+ }
354
+ event.preventDefault();
355
+ event.stopImmediatePropagation();
356
+ if (hit.locked) {
357
+ return;
358
+ }
359
+ this.#container.setPointerCapture(event.pointerId);
360
+ this.#pointerInteraction = {
361
+ pointerId: event.pointerId,
362
+ originClient: coordinate,
363
+ originData: this.#fromPixel(coordinate, before.paneId),
364
+ hit,
365
+ before: structuredClone(before),
366
+ interactionId: `interaction-${this.#interactionSequence++}`,
367
+ started: false,
368
+ };
369
+ };
370
+ #handlePointerMove = (event) => {
371
+ const interaction = this.#pointerInteraction;
372
+ if (interaction === undefined || interaction.pointerId !== event.pointerId) {
373
+ return;
374
+ }
375
+ event.preventDefault();
376
+ event.stopImmediatePropagation();
377
+ const coordinate = this.#pointerCoordinate(event);
378
+ if (!interaction.started &&
379
+ Math.hypot(coordinate.x - interaction.originClient.x, coordinate.y - interaction.originClient.y) < 0.5) {
380
+ return;
381
+ }
382
+ if (!interaction.started) {
383
+ interaction.started = true;
384
+ this.#emit({
385
+ type: 'overlay-drag-started',
386
+ ...this.#interactionIdentity(interaction),
387
+ });
388
+ }
389
+ try {
390
+ const candidate = createDragCandidate(interaction.before, interaction.hit, interaction.originData, this.#fromPixel(coordinate, interaction.before.paneId), this.#scene.data.map((bar) => bar.timestamp), this.#scene.symbol.pricePrecision);
391
+ const index = this.#scene.overlays.findIndex((overlay) => overlay.id === candidate.id);
392
+ const overlays = structuredClone(this.#scene.overlays);
393
+ overlays[index] = candidate;
394
+ const parsed = parseChartScene({ ...structuredClone(this.#scene), overlays });
395
+ const normalized = parsed.overlays[index];
396
+ if (!this.#chart.overrideOverlay(toEngineOverlay(normalized, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(normalized)))) {
397
+ throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to preview Overlay ${normalized.id}.`);
398
+ }
399
+ interaction.candidate = normalized;
400
+ this.#emit({
401
+ type: 'overlay-dragging',
402
+ ...this.#interactionIdentity(interaction),
403
+ candidate: normalized,
404
+ });
405
+ }
406
+ catch (error) {
407
+ if (error instanceof SceneError) {
408
+ this.#cancelInteraction('validation-error', error);
409
+ return;
410
+ }
411
+ throw error;
412
+ }
413
+ };
414
+ #handlePointerUp = (event) => {
415
+ const interaction = this.#pointerInteraction;
416
+ if (interaction === undefined || interaction.pointerId !== event.pointerId) {
417
+ return;
418
+ }
419
+ event.preventDefault();
420
+ event.stopImmediatePropagation();
421
+ this.#pointerInteraction = undefined;
422
+ this.#stopPointerCapture(interaction);
423
+ if (!interaction.started) {
424
+ return;
425
+ }
426
+ const overlay = interaction.candidate ?? interaction.before;
427
+ const index = this.#scene.overlays.findIndex((candidate) => candidate.id === overlay.id);
428
+ const overlays = structuredClone(this.#scene.overlays);
429
+ overlays[index] = overlay;
430
+ this.#scene = parseChartScene({ ...structuredClone(this.#scene), overlays });
431
+ const committed = this.#scene.overlays[index];
432
+ this.#emit({
433
+ type: 'overlay-drag-committed',
434
+ ...this.#interactionIdentity(interaction),
435
+ overlay: committed,
436
+ });
437
+ this.#emit({ type: 'overlay-updated', overlay: committed });
438
+ };
439
+ #handlePointerCancel = (event) => {
440
+ if (this.#pointerInteraction?.pointerId === event.pointerId) {
441
+ event.preventDefault();
442
+ event.stopImmediatePropagation();
443
+ this.#cancelInteraction('pointer-cancel');
444
+ }
445
+ };
446
+ #handleKeyDown = (event) => {
447
+ if (event.key === 'Escape' && this.#pointerInteraction !== undefined) {
448
+ this.#cancelInteraction('escape');
449
+ }
450
+ };
451
+ #handleWindowBlur = () => {
452
+ if (this.#pointerInteraction !== undefined) {
453
+ this.#cancelInteraction('window-blur');
454
+ }
455
+ };
133
456
  subscribe(listener) {
134
457
  this.#assertActive();
135
458
  this.#listeners.add(listener);
@@ -139,24 +462,60 @@ export class KLineChartsSceneAdapter {
139
462
  }
140
463
  exportScene() {
141
464
  this.#assertActive();
142
- const engines = new Map(this.#engineOverlays().map((overlay) => [overlay.id, overlay]));
143
- const overlays = this.#scene.overlays.map((source, index) => {
144
- const engine = engines.get(source.id);
145
- if (engine === undefined) {
146
- throw new SceneError('EXPORT_INVALID', `/overlays/${index}`, `KLineCharts lost Overlay ${source.id}.`);
465
+ for (let index = 0; index < this.#scene.overlays.length; index++) {
466
+ const overlay = this.#scene.overlays[index];
467
+ if (!this.#engineOverlays().some((engine) => engine.id === overlay.id)) {
468
+ throw new SceneError('EXPORT_INVALID', `/overlays/${index}`, `KLineCharts lost Overlay ${overlay.id}.`);
147
469
  }
148
- return fromEngineOverlay(engine, source, this.#idMap, `/overlays/${index}`);
149
- });
150
- return parseChartScene({
151
- ...structuredClone(this.#scene),
152
- overlays,
153
- });
470
+ }
471
+ return parseChartScene(structuredClone(this.#scene));
472
+ }
473
+ async setPriceScale(scale) {
474
+ this.#assertActive();
475
+ const candidate = parseChartScene(promoteSceneToM2(this.#scene, scale));
476
+ const paneIndex = candidate.panes.findIndex((pane) => pane.kind === 'candle');
477
+ const pane = candidate.panes[paneIndex];
478
+ const axisIndex = pane.yAxes.findIndex((axis) => axis.role === 'primary');
479
+ const axis = pane.yAxes[axisIndex];
480
+ const previousPane = this.#scene.panes[paneIndex];
481
+ const previousAxis = previousPane.yAxes[axisIndex];
482
+ const path = `/panes/${paneIndex}/yAxes/${axisIndex}`;
483
+ try {
484
+ overrideSceneYAxis(this.#chart, this.#idMap, axis, pane.id, path);
485
+ // KLineCharts batches Y-axis recreation in a microtask; await that formal
486
+ // layout boundary before making the upgraded Scene externally visible.
487
+ await Promise.resolve();
488
+ const reference = candidate.data[0];
489
+ this.#toPixel({ timestamp: reference.timestamp, value: reference.close }, pane.id);
490
+ }
491
+ catch (error) {
492
+ overrideSceneYAxis(this.#chart, this.#idMap, previousAxis, previousPane.id, path);
493
+ await Promise.resolve();
494
+ if (error instanceof SceneError) {
495
+ throw error;
496
+ }
497
+ throw new SceneError('RUNTIME_INIT_FAILED', `${path}/scale`, 'KLineCharts failed to apply the requested price scale atomically.');
498
+ }
499
+ this.#scene = candidate;
500
+ return structuredClone(candidate);
501
+ }
502
+ projectPoint(point, paneId) {
503
+ this.#assertActive();
504
+ const targetPane = paneId ?? this.#scene.panes.find((pane) => pane.kind === 'candle').id;
505
+ return this.#toPixel(point, targetPane);
506
+ }
507
+ hitTestOverlay(point) {
508
+ this.#assertActive();
509
+ return hitTestOverlayGeometries(point, this.#overlayGeometries());
154
510
  }
155
511
  addOverlay(value) {
156
512
  this.#assertActive();
513
+ const baseScene = value.type === 'priceMeasurement'
514
+ ? promoteSceneToM2(this.#scene)
515
+ : structuredClone(this.#scene);
157
516
  const candidate = parseChartScene({
158
- ...structuredClone(this.#scene),
159
- overlays: [...this.#scene.overlays, structuredClone(value)],
517
+ ...baseScene,
518
+ overlays: [...baseScene.overlays, structuredClone(value)],
160
519
  });
161
520
  const index = candidate.overlays.length - 1;
162
521
  const overlay = candidate.overlays[index];
@@ -168,12 +527,13 @@ export class KLineChartsSceneAdapter {
168
527
  this.#emit({ type: 'overlay-created', overlay });
169
528
  return structuredClone(overlay);
170
529
  }
171
- updateOverlay(value) {
530
+ #updateOverlay(value, styleChange) {
172
531
  this.#assertActive();
173
532
  const index = this.#scene.overlays.findIndex((overlay) => overlay.id === value.id);
174
533
  if (index < 0) {
175
534
  throw new SceneError('INVALID_REFERENCE', '/overlays', `Overlay ${value.id} does not exist.`);
176
535
  }
536
+ const before = structuredClone(this.#scene.overlays[index]);
177
537
  const overlays = structuredClone(this.#scene.overlays);
178
538
  overlays[index] = structuredClone(value);
179
539
  const candidate = parseChartScene({
@@ -181,13 +541,26 @@ export class KLineChartsSceneAdapter {
181
541
  overlays,
182
542
  });
183
543
  const overlay = candidate.overlays[index];
184
- if (!this.#chart.overrideOverlay(toEngineOverlay(overlay, this.#idMap, `/overlays/${index}`))) {
544
+ if (!this.#chart.overrideOverlay(toEngineOverlay(overlay, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(overlay)))) {
185
545
  throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to update Overlay ${overlay.id}.`);
186
546
  }
187
547
  this.#scene = candidate;
548
+ if (styleChange) {
549
+ this.#emit({ type: 'overlay-style-changed', before, overlay });
550
+ }
188
551
  this.#emit({ type: 'overlay-updated', overlay });
189
552
  return structuredClone(overlay);
190
553
  }
554
+ updateOverlay(value) {
555
+ return this.#updateOverlay(value, false);
556
+ }
557
+ updateOverlayStyles(id, styles) {
558
+ const overlay = this.getOverlay(id);
559
+ if (overlay === undefined) {
560
+ throw new SceneError('INVALID_REFERENCE', '/overlays', `Overlay ${id} does not exist.`);
561
+ }
562
+ return this.#updateOverlay({ ...overlay, styles: structuredClone(styles) }, true);
563
+ }
191
564
  removeOverlay(id) {
192
565
  this.#assertActive();
193
566
  const index = this.#scene.overlays.findIndex((overlay) => overlay.id === id);
@@ -202,6 +575,9 @@ export class KLineChartsSceneAdapter {
202
575
  ...structuredClone(this.#scene),
203
576
  overlays: this.#scene.overlays.filter((overlay) => overlay.id !== id),
204
577
  });
578
+ if (this.#selectedOverlayId === id) {
579
+ this.#selectOverlay(null);
580
+ }
205
581
  this.#emit({ type: 'overlay-removed', id });
206
582
  }
207
583
  return true;
@@ -212,17 +588,24 @@ export class KLineChartsSceneAdapter {
212
588
  this.#engineOverlays().some((overlay) => overlay.id === request.id)) {
213
589
  throw new SceneError('DUPLICATE_ID', '/overlays/id', `Overlay ${request.id} already exists.`);
214
590
  }
591
+ const candidate = request.type === 'priceMeasurement'
592
+ ? parseChartScene(promoteSceneToM2(this.#scene))
593
+ : this.#scene;
215
594
  const result = this.#chart.createOverlay(toEngineOverlayDrawing(structuredClone(request), this.#idMap, this.#overlayCallbacks(structuredClone(request), true)));
216
595
  if (result !== request.id) {
217
596
  throw new SceneError('RUNTIME_INIT_FAILED', '/overlays', `KLineCharts failed to start drawing Overlay ${request.id}.`);
218
597
  }
598
+ this.#scene = candidate;
219
599
  return request.id;
220
600
  }
221
601
  getOverlay(id) {
222
- return this.exportScene().overlays.find((overlay) => overlay.id === id);
602
+ this.#assertActive();
603
+ const overlay = this.#scene.overlays.find((candidate) => candidate.id === id);
604
+ return overlay === undefined ? undefined : structuredClone(overlay);
223
605
  }
224
606
  listOverlays() {
225
- return this.exportScene().overlays;
607
+ this.#assertActive();
608
+ return structuredClone(this.#scene.overlays);
226
609
  }
227
610
  inspect() {
228
611
  this.#assertActive();
@@ -241,6 +624,7 @@ export class KLineChartsSceneAdapter {
241
624
  });
242
625
  return {
243
626
  engineVersion: this.#engine.version(),
627
+ runtimeVersion: this.#scene.runtime.runtimeVersion,
244
628
  dataCount: this.#chart.getDataList().length,
245
629
  paneIds: this.#scene.panes.map((pane) => pane.id),
246
630
  indicators,
@@ -253,7 +637,9 @@ export class KLineChartsSceneAdapter {
253
637
  if (this.#disposed) {
254
638
  return;
255
639
  }
640
+ this.#cancelInteraction('destroy');
256
641
  this.#disposed = true;
642
+ this.#removeInteractionListeners();
257
643
  this.#listeners.clear();
258
644
  this.#engine.dispose(this.#container);
259
645
  this.#container.replaceChildren();