@baron1996/klinecharts-adapter 0.9.4 → 0.9.7

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.js CHANGED
@@ -8,12 +8,15 @@ import { createSceneOverlays, fromEngineOverlay, normalizeSceneOverlayPrices, to
8
8
  import { normalizePriceValue } from './conversion/price.js';
9
9
  import { applyViewport } from './conversion/viewport.js';
10
10
  import { createStaticDataLoader } from './static-data-loader.js';
11
+ import { assertGapAwareSceneSupported, engineDataForScene, GAP_AWARE_CANDLE_INDICATOR_ID, gapCount, installGapAwareMainSeries, isGapAwareScene, timelineItemOf, timelineSlotCount, } from './gap-aware-series.js';
11
12
  import { toOverlayStyles } from './conversion/overlays.js';
12
13
  import { drawingToSceneOverlay, sceneOverlayToDrawing, } from './drawing/overlay-conversion.js';
13
14
  import { MainSeriesPresentationError, presentationToSceneCandle, STANDARD_CLOSE_LINE_PRESENTATION, } from './main-series-presentation.js';
14
15
  import { createDragCandidate, } from './interaction/dragging.js';
15
- import { hitTestOverlayGeometries, } from './interaction/hit-testing.js';
16
+ import { DEFAULT_OVERLAY_MOUSE_HIT_TOLERANCE, DEFAULT_OVERLAY_TOUCH_HIT_TOLERANCE, hitTestOverlayGeometries, } from './interaction/hit-testing.js';
17
+ import { projectOverlayGeometry } from './interaction/overlay-geometry.js';
16
18
  import { shouldIgnoreStaleOverlayDeselection } from './interaction/selection-arbitration.js';
19
+ import { isTouchPrecisionTap, resolveTouchPrecisionCursor, TouchPrecisionDrawingGuide, } from './interaction/touch-precision-drawing.js';
17
20
  function promoteSceneToM2(scene, scale) {
18
21
  const candidate = structuredClone(scene);
19
22
  candidate.runtime.runtimeVersion = '0.2.0';
@@ -41,12 +44,19 @@ export class KLineChartsSceneAdapter {
41
44
  #engine;
42
45
  /** 引擎点击仲裁显式复位;每次绘制开始前调用,保证新绘制首击独立成单击。 */
43
46
  #resetClickArbitration;
47
+ /** 向引擎派发鼠标移动语义,供触摸虚拟光标复用原生绘制预览。 */
48
+ #dispatchEngineMouseMove;
49
+ /** 向引擎派发鼠标单击语义,供触摸轻点复用原生绘制落点。 */
50
+ #dispatchEngineMouseClick;
44
51
  /** 引擎十字线动作监听器集合。 */
45
52
  #crosshairListeners = new Set();
46
53
  /** 引擎十字线动作退订函数。 */
47
54
  #unsubscribeCrosshair = null;
48
55
  /** 引擎十字线动作的绑定处理函数。 */
49
56
  #handleCrosshairChange = (payload) => {
57
+ if (this.#chartInteractionSnapshot !== undefined) {
58
+ return;
59
+ }
50
60
  const event = (typeof payload === 'object' && payload !== null
51
61
  ? payload
52
62
  : {});
@@ -74,14 +84,16 @@ export class KLineChartsSceneAdapter {
74
84
  }
75
85
  const match = barIndex === null ? undefined : data[barIndex];
76
86
  const timestamp = match?.timestamp ?? null;
77
- const bar = match === undefined
87
+ const timelineItem = timelineItemOf(match);
88
+ const source = timelineItem?.kind === 'bar' ? timelineItem.bar : match;
89
+ const bar = source === undefined || timelineItem?.kind === 'gap'
78
90
  ? null
79
91
  : {
80
- open: match.open,
81
- high: match.high,
82
- low: match.low,
83
- close: match.close,
84
- volume: match.volume ?? null,
92
+ open: source.open,
93
+ high: source.high,
94
+ low: source.low,
95
+ close: source.close,
96
+ volume: source.volume ?? null,
85
97
  };
86
98
  const snapshot = { timestamp, bar };
87
99
  for (const listener of this.#crosshairListeners) {
@@ -102,12 +114,26 @@ export class KLineChartsSceneAdapter {
102
114
  #listeners = new Set();
103
115
  /** 当前选择状态,null 表示明确未选择。 */
104
116
  #selectedOverlayId = null;
117
+ /** 独占选择前的图表交互状态,用于无损恢复宿主原配置。 */
118
+ #chartInteractionSnapshot;
119
+ /** 点击空白退出编辑时被完整消费的 Pointer ID。 */
120
+ #deselectingPointerId;
105
121
  /** 当前受控拖动事务;progress 永不写入 #scene。 */
106
122
  #pointerInteraction;
107
123
  /** 当前交互式量度的首锚点;只用于补偿引擎丢弃快速第二击,不进入 Scene。 */
108
124
  #interactivePriceMeasurement;
109
125
  /** 当前引擎进行中的绘制 Overlay ID;非 null 时 pointerdown 只路由给新绘制,不做命中测试。 */
110
126
  #drawingInProgressId = null;
127
+ /** 宿主显式开启的 Drawing 输入策略;未配置时保持引擎原生行为。 */
128
+ #drawingInteraction;
129
+ /** 触摸精确绘制的通用提示与虚拟光标层。 */
130
+ #touchPrecisionGuide;
131
+ /** 当前允许切换到触摸精确交互的线段绘制。 */
132
+ #touchPrecisionDrawing;
133
+ /** 当前单指定位手势;拖动只更新光标,轻点才确认。 */
134
+ #touchPrecisionPointer;
135
+ /** 阻断触摸结束后浏览器补发的兼容鼠标事件。 */
136
+ #suppressCompatibilityMouseUntil = 0;
111
137
  /** 确定性 opaque 交互 ID 序号。 */
112
138
  #interactionSequence = 0;
113
139
  /** 显式 Workspace 模式;Legacy 与 Workspace 状态严格隔离。 */
@@ -135,14 +161,19 @@ export class KLineChartsSceneAdapter {
135
161
  horizontal: false,
136
162
  vertical: false,
137
163
  };
138
- constructor(container, scene, handle, idMap, originalBackground) {
164
+ constructor(container, scene, handle, idMap, originalBackground, drawingInteraction = {}) {
139
165
  this.#container = container;
140
166
  this.#scene = scene;
141
167
  this.#chart = handle.chart;
142
168
  this.#engine = handle.module;
143
169
  this.#resetClickArbitration = handle.resetClickArbitration;
170
+ this.#dispatchEngineMouseMove = handle.dispatchMouseMove;
171
+ this.#dispatchEngineMouseClick = handle.dispatchMouseClick;
144
172
  this.#idMap = idMap;
145
173
  this.#originalBackground = originalBackground;
174
+ this.#drawingInteraction = structuredClone(drawingInteraction);
175
+ this.#hitTolerance('mouse');
176
+ this.#hitTolerance('touch');
146
177
  this.#installInteractionListeners();
147
178
  this.#chart.subscribeAction('onCrosshairChange', this.#handleCrosshairChange);
148
179
  this.#unsubscribeCrosshair = () => {
@@ -156,6 +187,7 @@ export class KLineChartsSceneAdapter {
156
187
  let adapter;
157
188
  try {
158
189
  handle = await createEngine(container, scene);
190
+ installGapAwareMainSeries(scene, handle.chart, handle.module.registerIndicator);
159
191
  registerProjectOverlays(handle.module.registerOverlay);
160
192
  const idMap = createEngineIdMap(scene, handle.chart);
161
193
  applyPanes(scene, handle.chart, idMap);
@@ -193,10 +225,11 @@ export class KLineChartsSceneAdapter {
193
225
  ? {}
194
226
  : { displayTimezone: options.displayTimezone }),
195
227
  });
228
+ installGapAwareMainSeries(scene, handle.chart, handle.module.registerIndicator);
196
229
  registerProjectOverlays(handle.module.registerOverlay);
197
230
  const idMap = createEngineIdMap(scene, handle.chart);
198
231
  applyPanes(scene, handle.chart, idMap);
199
- adapter = new KLineChartsSceneAdapter(container, scene, handle, idMap, originalBackground);
232
+ adapter = new KLineChartsSceneAdapter(container, scene, handle, idMap, originalBackground, options?.drawingInteraction);
200
233
  applyViewport(handle.chart, scene.viewport);
201
234
  container.style.backgroundColor = scene.chart.layout.backgroundColor;
202
235
  adapter.#workspaceMode = true;
@@ -279,7 +312,7 @@ export class KLineChartsSceneAdapter {
279
312
  }
280
313
  }
281
314
  #historicalDataLoader(scene) {
282
- const snapshot = structuredClone(scene.data);
315
+ const snapshot = structuredClone(engineDataForScene(scene));
283
316
  return {
284
317
  getBars: ({ type, timestamp, callback }) => {
285
318
  if (type === 'init') {
@@ -359,6 +392,12 @@ export class KLineChartsSceneAdapter {
359
392
  if (previousId === id) {
360
393
  return;
361
394
  }
395
+ if (previousId === null && id !== null) {
396
+ this.#enterExclusiveSelection();
397
+ }
398
+ else if (previousId !== null && id === null) {
399
+ this.#leaveExclusiveSelection();
400
+ }
362
401
  this.#selectedOverlayId = id;
363
402
  if (this.#workspaceMode) {
364
403
  this.#emitPort({
@@ -372,7 +411,63 @@ export class KLineChartsSceneAdapter {
372
411
  this.#emit({ type: 'overlay-selected', id });
373
412
  }
374
413
  }
414
+ #enterExclusiveSelection() {
415
+ if (this.#drawingInteraction.exclusiveSelection !== true ||
416
+ this.#chartInteractionSnapshot !== undefined) {
417
+ return;
418
+ }
419
+ this.#chartInteractionSnapshot = {
420
+ scrollEnabled: this.#chart.isScrollEnabled(),
421
+ zoomEnabled: this.#chart.isZoomEnabled(),
422
+ crosshairVisible: this.#chart.getStyles().crosshair.show,
423
+ };
424
+ this.#chart.setScrollEnabled(false);
425
+ this.#chart.setZoomEnabled(false);
426
+ this.#chart.setStyles({ crosshair: { show: false } });
427
+ }
428
+ #leaveExclusiveSelection() {
429
+ const snapshot = this.#chartInteractionSnapshot;
430
+ if (snapshot === undefined) {
431
+ return;
432
+ }
433
+ this.#chartInteractionSnapshot = undefined;
434
+ this.#chart.setScrollEnabled(snapshot.scrollEnabled);
435
+ this.#chart.setZoomEnabled(snapshot.zoomEnabled);
436
+ this.#chart.setStyles({ crosshair: { show: snapshot.crosshairVisible } });
437
+ }
438
+ #hitTolerance(pointerType) {
439
+ const touch = pointerType === 'touch';
440
+ const defaults = touch
441
+ ? DEFAULT_OVERLAY_TOUCH_HIT_TOLERANCE
442
+ : DEFAULT_OVERLAY_MOUSE_HIT_TOLERANCE;
443
+ const configured = touch
444
+ ? this.#drawingInteraction.hitTolerance?.touch
445
+ : this.#drawingInteraction.hitTolerance?.mouse;
446
+ const body = configured?.body ?? defaults.body;
447
+ const anchor = configured?.anchor ?? defaults.anchor;
448
+ if (!Number.isFinite(body) || body < 0 || !Number.isFinite(anchor) || anchor < 0) {
449
+ throw new SceneError('RUNTIME_INIT_FAILED', '/drawingInteraction/hitTolerance', 'Drawing hit tolerance must contain finite non-negative CSS pixel values.');
450
+ }
451
+ return { body, anchor };
452
+ }
375
453
  #dimensionsForHit(overlay, hit) {
454
+ const constrainedType = overlay.type === 'horizontalStraightLine'
455
+ || overlay.type === 'priceLine'
456
+ || overlay.type === 'simpleTag'
457
+ || overlay.type === 'horizontalRayLine'
458
+ || overlay.type === 'horizontalSegment'
459
+ ? 'vertical'
460
+ : overlay.type === 'verticalStraightLine'
461
+ || overlay.type === 'verticalRayLine'
462
+ || overlay.type === 'verticalSegment'
463
+ ? 'horizontal'
464
+ : null;
465
+ if (constrainedType === 'vertical') {
466
+ return { horizontal: false, vertical: true };
467
+ }
468
+ if (constrainedType === 'horizontal') {
469
+ return { horizontal: true, vertical: false };
470
+ }
376
471
  if (hit.target !== 'anchor') {
377
472
  return { horizontal: true, vertical: true };
378
473
  }
@@ -430,6 +525,7 @@ export class KLineChartsSceneAdapter {
430
525
  preventDefault?.();
431
526
  },
432
527
  onDrawEnd: ({ overlay }) => {
528
+ this.#finishTouchPrecisionDrawing(overlay.id);
433
529
  if (drawing &&
434
530
  this.#interactivePriceMeasurement?.source.id === source.id) {
435
531
  this.#interactivePriceMeasurement = undefined;
@@ -463,6 +559,7 @@ export class KLineChartsSceneAdapter {
463
559
  }
464
560
  },
465
561
  onRemoved: ({ overlay }) => {
562
+ this.#finishTouchPrecisionDrawing(overlay.id);
466
563
  if (this.#interactivePriceMeasurement?.source.id === overlay.id) {
467
564
  this.#interactivePriceMeasurement = undefined;
468
565
  }
@@ -496,32 +593,191 @@ export class KLineChartsSceneAdapter {
496
593
  throw error;
497
594
  }
498
595
  }
596
+ #prepareTouchPrecisionDrawing(id, paneId) {
597
+ if (this.#drawingInteraction.touch !== 'precision-cursor') {
598
+ return;
599
+ }
600
+ this.#finishTouchPrecisionDrawing();
601
+ this.#touchPrecisionDrawing = {
602
+ id,
603
+ paneId,
604
+ phase: 'move-start',
605
+ };
606
+ const view = this.#container.ownerDocument.defaultView;
607
+ if ((view?.navigator.maxTouchPoints ?? 0) > 0 ||
608
+ view?.matchMedia('(any-pointer: coarse)').matches === true) {
609
+ const guide = this.#ensureTouchPrecisionGuide();
610
+ guide.setPhase('move-start');
611
+ guide.show();
612
+ }
613
+ }
614
+ #ensureTouchPrecisionGuide() {
615
+ this.#touchPrecisionGuide ??= new TouchPrecisionDrawingGuide(this.#container, () => this.#cancelTouchPrecisionDrawing(true));
616
+ return this.#touchPrecisionGuide;
617
+ }
618
+ #finishTouchPrecisionDrawing(id) {
619
+ const drawing = this.#touchPrecisionDrawing;
620
+ if (drawing === undefined || (id !== undefined && drawing.id !== id)) {
621
+ return;
622
+ }
623
+ this.#stopTouchPrecisionPointerCapture();
624
+ this.#touchPrecisionPointer = undefined;
625
+ this.#touchPrecisionDrawing = undefined;
626
+ this.#touchPrecisionGuide?.hide();
627
+ }
628
+ #cancelTouchPrecisionDrawing(emitRemoval) {
629
+ const drawing = this.#touchPrecisionDrawing;
630
+ if (drawing === undefined) {
631
+ return;
632
+ }
633
+ const id = drawing.id;
634
+ this.#finishTouchPrecisionDrawing(id);
635
+ this.#suppressCompatibilityMouseUntil = performance.now() + 800;
636
+ this.#chart.removeOverlay({ id });
637
+ if (this.#drawingInProgressId === id) {
638
+ this.#drawingInProgressId = null;
639
+ }
640
+ if (this.#workspaceMode) {
641
+ const removed = this.#workspaceSources.delete(id);
642
+ if (emitRemoval && removed) {
643
+ this.#emitPort({ type: 'removed', id });
644
+ }
645
+ }
646
+ }
647
+ #stopTouchPrecisionPointerCapture() {
648
+ const pointer = this.#touchPrecisionPointer;
649
+ if (pointer === undefined) {
650
+ return;
651
+ }
652
+ try {
653
+ if (this.#container.hasPointerCapture(pointer.pointerId)) {
654
+ this.#container.releasePointerCapture(pointer.pointerId);
655
+ }
656
+ }
657
+ catch {
658
+ // Synthetic browser tests and interrupted system gestures may not own capture.
659
+ }
660
+ }
661
+ #touchPrecisionMainElement(paneId) {
662
+ const filter = this.#primaryAxisFilter(paneId);
663
+ const main = this.#chart.getDom(filter.paneId, 'main');
664
+ if (!(main instanceof HTMLElement)) {
665
+ throw new SceneError('INVALID_REFERENCE', '/drawings/target', 'Touch Drawing target Pane has no interactive main element.');
666
+ }
667
+ return main;
668
+ }
669
+ #touchPrecisionBounds(paneId) {
670
+ const containerRect = this.#container.getBoundingClientRect();
671
+ const mainRect = this.#touchPrecisionMainElement(paneId).getBoundingClientRect();
672
+ return {
673
+ left: Math.max(0, mainRect.left - containerRect.left),
674
+ top: Math.max(0, mainRect.top - containerRect.top),
675
+ right: Math.min(containerRect.width, mainRect.right - containerRect.left),
676
+ bottom: Math.min(containerRect.height, mainRect.bottom - containerRect.top),
677
+ };
678
+ }
679
+ #dispatchTouchPrecisionMouseMove(point) {
680
+ const rect = this.#container.getBoundingClientRect();
681
+ this.#dispatchEngineMouseMove({
682
+ x: point.x,
683
+ y: point.y,
684
+ pageX: rect.left + point.x + window.scrollX,
685
+ pageY: rect.top + point.y + window.scrollY,
686
+ });
687
+ }
688
+ #dispatchTouchPrecisionClick(point) {
689
+ const rect = this.#container.getBoundingClientRect();
690
+ this.#resetClickArbitration();
691
+ this.#dispatchEngineMouseClick({
692
+ x: point.x,
693
+ y: point.y,
694
+ pageX: rect.left + point.x + window.scrollX,
695
+ pageY: rect.top + point.y + window.scrollY,
696
+ });
697
+ }
698
+ #updateTouchPrecisionCursor(drawing, pointer) {
699
+ const cursor = resolveTouchPrecisionCursor(pointer, this.#touchPrecisionBounds(drawing.paneId));
700
+ this.#touchPrecisionGuide?.updateCursor(cursor);
701
+ this.#dispatchTouchPrecisionMouseMove(cursor);
702
+ return cursor;
703
+ }
704
+ #isTouchPrecisionCancelTarget(target) {
705
+ return this.#touchPrecisionGuide?.ownsCancelTarget(target) ?? false;
706
+ }
499
707
  #installInteractionListeners() {
500
708
  this.#container.addEventListener('mousedown', this.#handleRightMouseDown, true);
709
+ this.#container.addEventListener('click', this.#handleCompatibilityClick, true);
501
710
  this.#container.addEventListener('contextmenu', this.#handleContextMenu, true);
502
711
  this.#container.addEventListener('pointerdown', this.#handlePointerDown, true);
503
712
  this.#container.addEventListener('pointermove', this.#handlePointerMove, true);
504
713
  this.#container.addEventListener('pointerup', this.#handlePointerUp, true);
505
714
  this.#container.addEventListener('pointercancel', this.#handlePointerCancel, true);
715
+ this.#container.addEventListener('touchstart', this.#handleCompatibilityTouch, {
716
+ capture: true,
717
+ passive: false,
718
+ });
719
+ this.#container.addEventListener('touchmove', this.#handleCompatibilityTouch, {
720
+ capture: true,
721
+ passive: false,
722
+ });
723
+ this.#container.addEventListener('touchend', this.#handleCompatibilityTouch, {
724
+ capture: true,
725
+ passive: false,
726
+ });
727
+ this.#container.addEventListener('touchcancel', this.#handleCompatibilityTouch, {
728
+ capture: true,
729
+ passive: false,
730
+ });
506
731
  window.addEventListener('keydown', this.#handleKeyDown);
507
732
  window.addEventListener('blur', this.#handleWindowBlur);
508
733
  }
509
734
  #removeInteractionListeners() {
510
735
  this.#container.removeEventListener('mousedown', this.#handleRightMouseDown, true);
736
+ this.#container.removeEventListener('click', this.#handleCompatibilityClick, true);
511
737
  this.#container.removeEventListener('contextmenu', this.#handleContextMenu, true);
512
738
  this.#container.removeEventListener('pointerdown', this.#handlePointerDown, true);
513
739
  this.#container.removeEventListener('pointermove', this.#handlePointerMove, true);
514
740
  this.#container.removeEventListener('pointerup', this.#handlePointerUp, true);
515
741
  this.#container.removeEventListener('pointercancel', this.#handlePointerCancel, true);
742
+ this.#container.removeEventListener('touchstart', this.#handleCompatibilityTouch, true);
743
+ this.#container.removeEventListener('touchmove', this.#handleCompatibilityTouch, true);
744
+ this.#container.removeEventListener('touchend', this.#handleCompatibilityTouch, true);
745
+ this.#container.removeEventListener('touchcancel', this.#handleCompatibilityTouch, true);
516
746
  window.removeEventListener('keydown', this.#handleKeyDown);
517
747
  window.removeEventListener('blur', this.#handleWindowBlur);
518
748
  }
519
749
  #handleRightMouseDown = (event) => {
750
+ if (event.button === 0 &&
751
+ event.isTrusted &&
752
+ performance.now() < this.#suppressCompatibilityMouseUntil &&
753
+ !this.#isTouchPrecisionCancelTarget(event.target)) {
754
+ event.preventDefault();
755
+ event.stopImmediatePropagation();
756
+ return;
757
+ }
520
758
  if (event.button === 2) {
521
759
  // 阻止事件到达 KLineCharts;不 preventDefault,保留浏览器原生右键菜单。
522
760
  event.stopPropagation();
523
761
  }
524
762
  };
763
+ #handleCompatibilityClick = (event) => {
764
+ if (event.isTrusted &&
765
+ performance.now() < this.#suppressCompatibilityMouseUntil &&
766
+ !this.#isTouchPrecisionCancelTarget(event.target)) {
767
+ event.preventDefault();
768
+ event.stopImmediatePropagation();
769
+ }
770
+ };
771
+ #handleCompatibilityTouch = (event) => {
772
+ if ((this.#touchPrecisionDrawing !== undefined ||
773
+ performance.now() < this.#suppressCompatibilityMouseUntil) &&
774
+ !this.#isTouchPrecisionCancelTarget(event.target)) {
775
+ if (event.cancelable) {
776
+ event.preventDefault();
777
+ }
778
+ event.stopImmediatePropagation();
779
+ }
780
+ };
525
781
  #handleContextMenu = (event) => {
526
782
  // KLineCharts 会 preventDefault。捕获阶段截断传播,但 Baron 本身不拦截菜单。
527
783
  event.stopPropagation();
@@ -598,60 +854,40 @@ export class KLineChartsSceneAdapter {
598
854
  #overlayGeometries() {
599
855
  const geometries = [];
600
856
  const active = this.#activeOverlays();
857
+ const containerRect = this.#container.getBoundingClientRect();
858
+ const canvas = this.#container.ownerDocument.createElement('canvas');
859
+ const textContext = canvas.getContext('2d');
601
860
  for (let sceneIndex = 0; sceneIndex < active.length; sceneIndex++) {
602
861
  const overlay = active[sceneIndex];
603
862
  if (overlay === undefined || !overlay.visible) {
604
863
  continue;
605
864
  }
606
- if (overlay.type === 'horizontalStraightLine') {
607
- const anchor = overlay.anchor;
608
- if (anchor === undefined || !('value' in anchor)) {
609
- continue;
610
- }
611
- const paneFilter = this.#primaryAxisFilter(overlay.paneId);
612
- const paneMain = this.#chart.getDom(paneFilter.paneId, 'main');
613
- const containerRect = this.#container.getBoundingClientRect();
614
- const mainRect = paneMain?.getBoundingClientRect() ?? containerRect;
615
- const projected = this.#toPixel({ timestamp: this.#scene.data[0].timestamp, value: anchor.value }, overlay.paneId);
616
- const start = { x: mainRect.left - containerRect.left, y: projected.y };
617
- const end = { x: mainRect.right - containerRect.left, y: projected.y };
618
- geometries.push({
619
- overlayId: overlay.id,
620
- sceneIndex,
621
- zLevel: overlay.zLevel,
622
- locked: overlay.locked,
623
- anchors: [{ x: (start.x + end.x) / 2, y: projected.y }],
624
- bodySegments: [[start, end]],
625
- });
626
- continue;
627
- }
628
- if (overlay.type === 'priceMeasurement' && overlay.start !== undefined && overlay.end !== undefined) {
629
- const start = this.#toPixel(overlay.start, overlay.paneId);
630
- const end = this.#toPixel(overlay.end, overlay.paneId);
631
- geometries.push({
632
- overlayId: overlay.id,
633
- sceneIndex,
634
- zLevel: overlay.zLevel,
635
- locked: overlay.locked,
636
- anchors: [start, end],
637
- bodySegments: [[start, end]],
638
- });
639
- continue;
640
- }
641
- if (overlay.points !== undefined && overlay.points.length >= 2) {
642
- const anchors = overlay.points.map((point) => this.#toPixel(point, overlay.paneId));
643
- const bodySegments = [];
644
- for (let pointIndex = 1; pointIndex < anchors.length; pointIndex++) {
645
- bodySegments.push([anchors[pointIndex - 1], anchors[pointIndex]]);
646
- }
647
- geometries.push({
648
- overlayId: overlay.id,
649
- sceneIndex,
650
- zLevel: overlay.zLevel,
651
- locked: overlay.locked,
652
- anchors,
653
- bodySegments,
654
- });
865
+ const paneFilter = this.#primaryAxisFilter(overlay.paneId);
866
+ const paneMain = this.#chart.getDom(paneFilter.paneId, 'main');
867
+ const mainRect = paneMain?.getBoundingClientRect() ?? containerRect;
868
+ const geometry = projectOverlayGeometry(overlay, sceneIndex, {
869
+ bounds: {
870
+ left: mainRect.left - containerRect.left,
871
+ top: mainRect.top - containerRect.top,
872
+ right: mainRect.right - containerRect.left,
873
+ bottom: mainRect.bottom - containerRect.top,
874
+ },
875
+ referenceTimestamp: this.#scene.data[0].timestamp,
876
+ referenceValue: this.#scene.data[0].close,
877
+ project: (point) => this.#toPixel(point, overlay.paneId),
878
+ measureText: (text, source) => {
879
+ const textStyle = source.styles.text;
880
+ if (textContext !== null) {
881
+ textContext.font = `${textStyle.weight} ${textStyle.size}px ${textStyle.family}`;
882
+ }
883
+ return {
884
+ width: Math.max(textStyle.size, textContext?.measureText(text).width ?? [...text].length * textStyle.size),
885
+ height: textStyle.size * 1.4,
886
+ };
887
+ },
888
+ });
889
+ if (geometry !== null) {
890
+ geometries.push(geometry);
655
891
  }
656
892
  }
657
893
  return geometries;
@@ -695,13 +931,102 @@ export class KLineChartsSceneAdapter {
695
931
  this.#emit({ type: 'scene-error', issues: structuredClone(error.issues) });
696
932
  }
697
933
  }
934
+ #routeTouchPrecisionPointerDown(event) {
935
+ const drawing = this.#touchPrecisionDrawing;
936
+ if (drawing === undefined || this.#isTouchPrecisionCancelTarget(event.target)) {
937
+ return false;
938
+ }
939
+ if (event.pointerType !== 'touch') {
940
+ this.#finishTouchPrecisionDrawing(drawing.id);
941
+ return false;
942
+ }
943
+ event.preventDefault();
944
+ event.stopImmediatePropagation();
945
+ if (this.#touchPrecisionPointer !== undefined) {
946
+ return true;
947
+ }
948
+ const guide = this.#ensureTouchPrecisionGuide();
949
+ guide.setPhase(drawing.phase);
950
+ guide.show();
951
+ const point = this.#pointerCoordinate(event);
952
+ this.#touchPrecisionPointer = {
953
+ pointerId: event.pointerId,
954
+ origin: point,
955
+ current: point,
956
+ };
957
+ try {
958
+ this.#container.setPointerCapture(event.pointerId);
959
+ }
960
+ catch {
961
+ // Synthetic PointerEvents do not own browser pointer capture.
962
+ }
963
+ this.#suppressCompatibilityMouseUntil = performance.now() + 800;
964
+ this.#updateTouchPrecisionCursor(drawing, point);
965
+ return true;
966
+ }
967
+ #routeTouchPrecisionPointerMove(event) {
968
+ const drawing = this.#touchPrecisionDrawing;
969
+ const pointer = this.#touchPrecisionPointer;
970
+ if (drawing === undefined ||
971
+ pointer === undefined ||
972
+ pointer.pointerId !== event.pointerId) {
973
+ return false;
974
+ }
975
+ event.preventDefault();
976
+ event.stopImmediatePropagation();
977
+ const point = this.#pointerCoordinate(event);
978
+ pointer.current = point;
979
+ this.#suppressCompatibilityMouseUntil = performance.now() + 800;
980
+ this.#updateTouchPrecisionCursor(drawing, point);
981
+ return true;
982
+ }
983
+ #routeTouchPrecisionPointerUp(event) {
984
+ const drawing = this.#touchPrecisionDrawing;
985
+ const pointer = this.#touchPrecisionPointer;
986
+ if (drawing === undefined ||
987
+ pointer === undefined ||
988
+ pointer.pointerId !== event.pointerId) {
989
+ return false;
990
+ }
991
+ event.preventDefault();
992
+ event.stopImmediatePropagation();
993
+ const point = this.#pointerCoordinate(event);
994
+ pointer.current = point;
995
+ const cursor = this.#updateTouchPrecisionCursor(drawing, point);
996
+ const tap = isTouchPrecisionTap(pointer.origin, pointer.current);
997
+ this.#stopTouchPrecisionPointerCapture();
998
+ this.#touchPrecisionPointer = undefined;
999
+ this.#suppressCompatibilityMouseUntil = performance.now() + 800;
1000
+ if (drawing.phase === 'move-start') {
1001
+ drawing.phase = 'confirm-start';
1002
+ this.#touchPrecisionGuide?.setPhase('confirm-start');
1003
+ return true;
1004
+ }
1005
+ if (!tap) {
1006
+ return true;
1007
+ }
1008
+ const phase = drawing.phase;
1009
+ this.#dispatchTouchPrecisionClick(cursor);
1010
+ const active = this.#touchPrecisionDrawing;
1011
+ if (phase === 'confirm-start' &&
1012
+ active !== undefined &&
1013
+ active.id === drawing.id) {
1014
+ active.phase = 'confirm-end';
1015
+ this.#touchPrecisionGuide?.setPhase('confirm-end');
1016
+ }
1017
+ return true;
1018
+ }
698
1019
  #handlePointerDown = (event) => {
699
1020
  if (this.#disposed ||
700
1021
  event.button !== 0 ||
701
1022
  this.#pointerInteraction !== undefined ||
1023
+ this.#deselectingPointerId !== undefined ||
702
1024
  !this.#mutationsEnabled) {
703
1025
  return;
704
1026
  }
1027
+ if (this.#routeTouchPrecisionPointerDown(event)) {
1028
+ return;
1029
+ }
705
1030
  const coordinate = this.#pointerCoordinate(event);
706
1031
  const measurement = this.#interactivePriceMeasurement;
707
1032
  if (measurement !== undefined) {
@@ -732,9 +1057,22 @@ export class KLineChartsSceneAdapter {
732
1057
  if (this.#drawingInProgressId !== null) {
733
1058
  return;
734
1059
  }
735
- const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries());
1060
+ const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries(), this.#hitTolerance(event.pointerType));
736
1061
  if (hit === null) {
737
1062
  const selected = this.#activeOverlays().find((overlay) => overlay.id === this.#selectedOverlayId);
1063
+ if (selected !== undefined &&
1064
+ this.#drawingInteraction.exclusiveSelection === true) {
1065
+ event.preventDefault();
1066
+ event.stopImmediatePropagation();
1067
+ this.#deselectingPointerId = event.pointerId;
1068
+ try {
1069
+ this.#container.setPointerCapture(event.pointerId);
1070
+ }
1071
+ catch {
1072
+ // Synthetic PointerEvents do not own browser pointer capture.
1073
+ }
1074
+ return;
1075
+ }
738
1076
  if (selected !== undefined && isControlledInteractionOverlay(selected)) {
739
1077
  this.#selectOverlay(null);
740
1078
  }
@@ -745,7 +1083,8 @@ export class KLineChartsSceneAdapter {
745
1083
  return;
746
1084
  }
747
1085
  this.#selectOverlay(before.id);
748
- if (!isControlledInteractionOverlay(before)) {
1086
+ if (!isControlledInteractionOverlay(before) &&
1087
+ this.#drawingInteraction.exclusiveSelection !== true) {
749
1088
  return;
750
1089
  }
751
1090
  event.preventDefault();
@@ -753,7 +1092,12 @@ export class KLineChartsSceneAdapter {
753
1092
  if (hit.locked) {
754
1093
  return;
755
1094
  }
756
- this.#container.setPointerCapture(event.pointerId);
1095
+ try {
1096
+ this.#container.setPointerCapture(event.pointerId);
1097
+ }
1098
+ catch {
1099
+ // Synthetic PointerEvents do not own browser pointer capture.
1100
+ }
757
1101
  this.#interactionDimensions = this.#dimensionsForHit(before, hit);
758
1102
  this.#pointerInteraction = {
759
1103
  pointerId: event.pointerId,
@@ -766,6 +1110,14 @@ export class KLineChartsSceneAdapter {
766
1110
  };
767
1111
  };
768
1112
  #handlePointerMove = (event) => {
1113
+ if (this.#routeTouchPrecisionPointerMove(event)) {
1114
+ return;
1115
+ }
1116
+ if (this.#deselectingPointerId === event.pointerId) {
1117
+ event.preventDefault();
1118
+ event.stopImmediatePropagation();
1119
+ return;
1120
+ }
769
1121
  const interaction = this.#pointerInteraction;
770
1122
  if (interaction === undefined || interaction.pointerId !== event.pointerId) {
771
1123
  return;
@@ -797,6 +1149,7 @@ export class KLineChartsSceneAdapter {
797
1149
  throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to preview Overlay ${normalized.id}.`);
798
1150
  }
799
1151
  if (this.#workspaceMode) {
1152
+ interaction.candidate = candidate;
800
1153
  this.#setActiveOverlays(overlays);
801
1154
  return;
802
1155
  }
@@ -816,6 +1169,19 @@ export class KLineChartsSceneAdapter {
816
1169
  }
817
1170
  };
818
1171
  #handlePointerUp = (event) => {
1172
+ if (this.#routeTouchPrecisionPointerUp(event)) {
1173
+ return;
1174
+ }
1175
+ if (this.#deselectingPointerId === event.pointerId) {
1176
+ event.preventDefault();
1177
+ event.stopImmediatePropagation();
1178
+ if (this.#container.hasPointerCapture(event.pointerId)) {
1179
+ this.#container.releasePointerCapture(event.pointerId);
1180
+ }
1181
+ this.#deselectingPointerId = undefined;
1182
+ this.#selectOverlay(null);
1183
+ return;
1184
+ }
819
1185
  const interaction = this.#pointerInteraction;
820
1186
  if (interaction === undefined || interaction.pointerId !== event.pointerId) {
821
1187
  return;
@@ -823,6 +1189,7 @@ export class KLineChartsSceneAdapter {
823
1189
  event.preventDefault();
824
1190
  event.stopImmediatePropagation();
825
1191
  this.#pointerInteraction = undefined;
1192
+ const interactionDimensions = this.#interactionDimensions;
826
1193
  this.#interactionDimensions = { horizontal: false, vertical: false };
827
1194
  this.#stopPointerCapture(interaction);
828
1195
  if (!interaction.started) {
@@ -841,7 +1208,7 @@ export class KLineChartsSceneAdapter {
841
1208
  type: 'updated',
842
1209
  id: overlay.id,
843
1210
  drawing: snapshotOfDrawing(sceneOverlayToDrawing(overlay, drawing)),
844
- editDimensions: structuredClone(this.#interactionDimensions),
1211
+ editDimensions: structuredClone(interactionDimensions),
845
1212
  });
846
1213
  }
847
1214
  return;
@@ -856,21 +1223,56 @@ export class KLineChartsSceneAdapter {
856
1223
  this.#emit({ type: 'overlay-updated', overlay: committed });
857
1224
  };
858
1225
  #handlePointerCancel = (event) => {
1226
+ if (this.#touchPrecisionPointer?.pointerId === event.pointerId) {
1227
+ event.preventDefault();
1228
+ event.stopImmediatePropagation();
1229
+ this.#cancelTouchPrecisionDrawing(true);
1230
+ return;
1231
+ }
859
1232
  if (this.#pointerInteraction?.pointerId === event.pointerId) {
860
1233
  event.preventDefault();
861
1234
  event.stopImmediatePropagation();
862
1235
  this.#cancelInteraction('pointer-cancel');
1236
+ return;
1237
+ }
1238
+ if (this.#deselectingPointerId === event.pointerId) {
1239
+ event.preventDefault();
1240
+ event.stopImmediatePropagation();
1241
+ if (this.#container.hasPointerCapture(event.pointerId)) {
1242
+ this.#container.releasePointerCapture(event.pointerId);
1243
+ }
1244
+ this.#deselectingPointerId = undefined;
1245
+ this.#selectOverlay(null);
863
1246
  }
864
1247
  };
865
1248
  #handleKeyDown = (event) => {
1249
+ if (event.key === 'Escape' && this.#touchPrecisionDrawing !== undefined) {
1250
+ this.#cancelTouchPrecisionDrawing(true);
1251
+ return;
1252
+ }
866
1253
  if (event.key === 'Escape' && this.#pointerInteraction !== undefined) {
867
1254
  this.#cancelInteraction('escape');
868
1255
  }
1256
+ if (event.key === 'Escape' &&
1257
+ this.#drawingInteraction.exclusiveSelection === true &&
1258
+ this.#selectedOverlayId !== null) {
1259
+ this.#selectOverlay(null);
1260
+ }
869
1261
  };
870
1262
  #handleWindowBlur = () => {
1263
+ if (this.#touchPrecisionDrawing !== undefined) {
1264
+ this.#cancelTouchPrecisionDrawing(true);
1265
+ }
871
1266
  if (this.#pointerInteraction !== undefined) {
872
1267
  this.#cancelInteraction('window-blur');
873
1268
  }
1269
+ if (this.#deselectingPointerId !== undefined) {
1270
+ if (this.#container.hasPointerCapture(this.#deselectingPointerId)) {
1271
+ this.#container.releasePointerCapture(this.#deselectingPointerId);
1272
+ }
1273
+ this.#deselectingPointerId = undefined;
1274
+ this.#selectOverlay(null);
1275
+ }
874
1276
  };
875
1277
  subscribe(listener) {
876
1278
  this.#assertActive();
@@ -922,6 +1324,10 @@ export class KLineChartsSceneAdapter {
922
1324
  replaceScene(value) {
923
1325
  this.#assertActive();
924
1326
  const candidate = parseChartScene(value);
1327
+ if (candidate.version !== this.#scene.version) {
1328
+ throw new SceneError('INVALID_REFERENCE', '/version', 'Scene replacement requires the same ChartScene contract version.');
1329
+ }
1330
+ assertGapAwareSceneSupported(candidate);
925
1331
  const paneShape = (scene) => JSON.stringify(scene.panes.map((pane) => ({
926
1332
  id: pane.id,
927
1333
  kind: pane.kind,
@@ -946,7 +1352,7 @@ export class KLineChartsSceneAdapter {
946
1352
  this.#historicalDataLoading = { hasMore: true };
947
1353
  }
948
1354
  this.#chart.setDataLoader(this.#historicalDataLoading === undefined
949
- ? createStaticDataLoader(candidate.data)
1355
+ ? createStaticDataLoader(engineDataForScene(candidate))
950
1356
  : this.#historicalDataLoader(candidate));
951
1357
  this.#chart.resetData();
952
1358
  applyViewport(this.#chart, candidate.viewport);
@@ -969,7 +1375,7 @@ export class KLineChartsSceneAdapter {
969
1375
  this.#historicalDataLoading = { hasMore: previousHistoricalHasMore };
970
1376
  }
971
1377
  this.#chart.setDataLoader(this.#historicalDataLoading === undefined
972
- ? createStaticDataLoader(previous.data)
1378
+ ? createStaticDataLoader(engineDataForScene(previous))
973
1379
  : this.#historicalDataLoader(previous));
974
1380
  this.#chart.resetData();
975
1381
  applyViewport(this.#chart, previous.viewport);
@@ -1080,6 +1486,9 @@ export class KLineChartsSceneAdapter {
1080
1486
  throw new SceneError('RUNTIME_INIT_FAILED', '/drawings', `KLineCharts failed to start Drawing ${request.id}.`);
1081
1487
  }
1082
1488
  this.#drawingInProgressId = request.id;
1489
+ if (request.type === 'segment') {
1490
+ this.#prepareTouchPrecisionDrawing(request.id, overlay.paneId);
1491
+ }
1083
1492
  return request.id;
1084
1493
  }
1085
1494
  listDrawings() {
@@ -1224,6 +1633,9 @@ export class KLineChartsSceneAdapter {
1224
1633
  applyMainSeriesPresentation(presentation) {
1225
1634
  this.#assertActive();
1226
1635
  this.#assertNotTerminated();
1636
+ if (isGapAwareScene(this.#scene)) {
1637
+ throw new MainSeriesPresentationError('MAIN_SERIES_PRESENTATION_UNSUPPORTED', '/chart/candle/type', 'ChartScene v2 main-series switching is disabled until every presentation is Gap-aware.');
1638
+ }
1227
1639
  if (presentation.type === 'area') {
1228
1640
  const expected = STANDARD_CLOSE_LINE_PRESENTATION;
1229
1641
  if (presentation.value !== expected.value ||
@@ -1513,7 +1925,9 @@ export class KLineChartsSceneAdapter {
1513
1925
  }
1514
1926
  inspect() {
1515
1927
  this.#assertActive();
1516
- const indicators = this.#chart.getIndicators().map((indicator) => {
1928
+ const indicators = this.#chart.getIndicators()
1929
+ .filter((indicator) => indicator.id !== GAP_AWARE_CANDLE_INDICATOR_ID)
1930
+ .map((indicator) => {
1517
1931
  const paneId = this.#idMap.paneFromEngine.get(indicator.paneId);
1518
1932
  const yAxisId = this.#idMap.yAxisFromEngine.get(indicator.yAxisId);
1519
1933
  if (paneId === undefined || yAxisId === undefined) {
@@ -1529,7 +1943,9 @@ export class KLineChartsSceneAdapter {
1529
1943
  return {
1530
1944
  engineVersion: this.#engine.version(),
1531
1945
  runtimeVersion: this.#scene.runtime.runtimeVersion,
1532
- dataCount: this.#chart.getDataList().length,
1946
+ dataCount: this.#scene.data.length,
1947
+ timelineSlotCount: timelineSlotCount(this.#scene),
1948
+ gapCount: gapCount(this.#scene),
1533
1949
  paneIds: this.#scene.panes.map((pane) => pane.id),
1534
1950
  indicators,
1535
1951
  overlays: this.exportScene().overlays,
@@ -1542,6 +1958,11 @@ export class KLineChartsSceneAdapter {
1542
1958
  return;
1543
1959
  }
1544
1960
  this.#cancelInteraction('destroy');
1961
+ this.#deselectingPointerId = undefined;
1962
+ this.#leaveExclusiveSelection();
1963
+ this.#cancelTouchPrecisionDrawing(false);
1964
+ this.#touchPrecisionGuide?.destroy();
1965
+ this.#touchPrecisionGuide = undefined;
1545
1966
  this.#interactivePriceMeasurement = undefined;
1546
1967
  this.#drawingInProgressId = null;
1547
1968
  this.#disposed = true;