@coderyo/renderer-lite 1.0.3 → 1.1.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.
package/dist/index.js CHANGED
@@ -7,6 +7,106 @@ import {
7
7
  LineSeries as LineSeries2
8
8
  } from "lightweight-charts";
9
9
  import { intervalMs as intervalMs2 } from "@coderyo/data";
10
+
11
+ // src/time-scale-prepend.ts
12
+ function buildSliceTimes(sortedTimes, renderFromMs, renderToMs) {
13
+ return sortedTimes.filter((t) => t >= renderFromMs && t <= renderToMs);
14
+ }
15
+ function countPrependSliceDelta(beforeSlice, afterSlice) {
16
+ const before = new Set(beforeSlice);
17
+ let delta = 0;
18
+ for (const t of afterSlice) {
19
+ if (!before.has(t)) delta += 1;
20
+ }
21
+ return delta;
22
+ }
23
+ function logicalIndexToBarTimeMs(sliceTimes, logicalIndex) {
24
+ if (sliceTimes.length === 0) return null;
25
+ const idx = Math.min(Math.max(0, Math.floor(logicalIndex)), sliceTimes.length - 1);
26
+ return sliceTimes[idx] ?? null;
27
+ }
28
+ function deriveRenderRange(visibleFromMs, visibleToMs, sortedTimes, intervalMs3) {
29
+ if (visibleToMs <= visibleFromMs && sortedTimes.length > 0) {
30
+ const fromMs = sortedTimes[0];
31
+ const toMs = sortedTimes[sortedTimes.length - 1];
32
+ const bufferMs2 = intervalMs3 * 20;
33
+ return { renderFromMs: fromMs - bufferMs2, renderToMs: toMs + bufferMs2 };
34
+ }
35
+ const span = visibleToMs - visibleFromMs;
36
+ const bufferMs = Math.max(span * 0.1, intervalMs3 * 50);
37
+ return {
38
+ renderFromMs: visibleFromMs - bufferMs,
39
+ renderToMs: visibleToMs + bufferMs
40
+ };
41
+ }
42
+ function logicalRangeForVisibleWindow(sliceTimes, visibleFromMs, visibleToMs) {
43
+ if (sliceTimes.length === 0 || visibleToMs <= visibleFromMs) return null;
44
+ let fromIdx = 0;
45
+ let toIdx = sliceTimes.length - 1;
46
+ for (let i = 0; i < sliceTimes.length; i++) {
47
+ if (sliceTimes[i] >= visibleFromMs) {
48
+ fromIdx = i;
49
+ break;
50
+ }
51
+ }
52
+ for (let i = sliceTimes.length - 1; i >= 0; i--) {
53
+ if (sliceTimes[i] <= visibleToMs) {
54
+ toIdx = i;
55
+ break;
56
+ }
57
+ }
58
+ if (toIdx < fromIdx) return null;
59
+ return { from: fromIdx, to: toIdx };
60
+ }
61
+ function computePrependSliceDeltaForViewport(input) {
62
+ const { visibleFromMs, visibleToMs, intervalMs: intervalMs3, sortedTimesBefore, sortedTimesAfter } = input;
63
+ const renderBefore = deriveRenderRange(visibleFromMs, visibleToMs, sortedTimesBefore, intervalMs3);
64
+ const renderAfter = deriveRenderRange(visibleFromMs, visibleToMs, sortedTimesAfter, intervalMs3);
65
+ const beforeSlice = buildSliceTimes(
66
+ sortedTimesBefore,
67
+ renderBefore.renderFromMs,
68
+ renderBefore.renderToMs
69
+ );
70
+ const afterSlice = buildSliceTimes(
71
+ sortedTimesAfter,
72
+ renderAfter.renderFromMs,
73
+ renderAfter.renderToMs
74
+ );
75
+ return countPrependSliceDelta(beforeSlice, afterSlice);
76
+ }
77
+ function compensatePrependOnRegistry(opts) {
78
+ const { registry, sortedTimesBefore, sortedTimesAfter, intervalMs: intervalMs3, referenceChart } = opts;
79
+ registry.forEachBus((_, bus) => {
80
+ compensatePrependOnBus(bus, {
81
+ sortedTimesBefore,
82
+ sortedTimesAfter,
83
+ intervalMs: intervalMs3,
84
+ referenceChart
85
+ });
86
+ });
87
+ }
88
+ function compensatePrependOnBus(bus, opts) {
89
+ const range = bus.getVisibleRange();
90
+ if (!range) return;
91
+ const delta = computePrependSliceDeltaForViewport({
92
+ sortedTimesBefore: opts.sortedTimesBefore,
93
+ sortedTimesAfter: opts.sortedTimesAfter,
94
+ visibleFromMs: range.fromMs,
95
+ visibleToMs: range.toMs,
96
+ intervalMs: opts.intervalMs
97
+ });
98
+ if (delta <= 0) return;
99
+ const { renderFromMs, renderToMs } = deriveRenderRange(
100
+ range.fromMs,
101
+ range.toMs,
102
+ opts.sortedTimesAfter,
103
+ opts.intervalMs
104
+ );
105
+ const sliceAfter = buildSliceTimes(opts.sortedTimesAfter, renderFromMs, renderToMs);
106
+ bus.compensatePrependLogicalRange(delta, opts.referenceChart, sliceAfter);
107
+ }
108
+
109
+ // src/pane-orchestrator.ts
10
110
  import { lodDecimateBars } from "@coderyo/series";
11
111
 
12
112
  // src/chart-grid.ts
@@ -19,7 +119,10 @@ function gridOptions(showGrid, dark) {
19
119
  }
20
120
 
21
121
  // src/pane-orchestrator.ts
22
- import { hasVisibleIndicatorPanes as hasVisibleIndicatorPanes2 } from "@coderyo/indicators";
122
+ import {
123
+ DEFAULT_INDICATOR_CONFIG as DEFAULT_INDICATOR_CONFIG2,
124
+ hasVisibleIndicatorPanes as hasVisibleIndicatorPanes2
125
+ } from "@coderyo/indicators";
23
126
 
24
127
  // src/indicator-panes.ts
25
128
  import {
@@ -46,18 +149,25 @@ function attachPaneResizer(topPane, bottomPane, opts = {}) {
46
149
  const parent = topPane.parentElement;
47
150
  if (!parent) return () => {
48
151
  };
152
+ let dragging = false;
49
153
  const handle = document.createElement("div");
50
- handle.style.cssText = "height:4px;cursor:row-resize;background:#30363d;flex-shrink:0;touch-action:none;";
154
+ handle.dataset.paneResizer = "1";
155
+ handle.style.cssText = "height:6px;cursor:row-resize;background:#30363d;flex-shrink:0;touch-action:none;z-index:5;";
156
+ handle.onmouseenter = () => {
157
+ handle.style.background = "#388bfd";
158
+ };
159
+ handle.onmouseleave = () => {
160
+ if (!dragging) handle.style.background = "#30363d";
161
+ };
51
162
  bottomPane.insertAdjacentElement("beforebegin", handle);
52
163
  const saved = opts.storageKey ? localStorage.getItem(opts.storageKey) : null;
53
164
  if (saved) {
54
165
  const ratio = Number(saved);
55
- if (Number.isFinite(ratio) && ratio > 0 && ratio < 1) {
166
+ if (Number.isFinite(ratio) && ratio >= 0.15 && ratio <= 0.85) {
56
167
  topPane.style.flex = `${ratio * 10}`;
57
168
  bottomPane.style.flex = `${(1 - ratio) * 10}`;
58
169
  }
59
170
  }
60
- let dragging = false;
61
171
  const onMove = (clientY) => {
62
172
  const rect = parent.getBoundingClientRect();
63
173
  const y = clientY - rect.top;
@@ -70,8 +180,11 @@ function attachPaneResizer(topPane, bottomPane, opts = {}) {
70
180
  if (opts.storageKey) localStorage.setItem(opts.storageKey, String(ratio));
71
181
  };
72
182
  const stop = () => {
183
+ const wasDragging = dragging;
73
184
  dragging = false;
74
185
  document.body.style.cursor = "";
186
+ handle.style.background = "#30363d";
187
+ if (wasDragging) window.dispatchEvent(new CustomEvent("tradview:pane-resize"));
75
188
  };
76
189
  handle.addEventListener("pointerdown", (e) => {
77
190
  dragging = true;
@@ -79,7 +192,10 @@ function attachPaneResizer(topPane, bottomPane, opts = {}) {
79
192
  document.body.style.cursor = "row-resize";
80
193
  });
81
194
  handle.addEventListener("pointermove", (e) => {
82
- if (dragging) onMove(e.clientY);
195
+ if (dragging) {
196
+ onMove(e.clientY);
197
+ window.dispatchEvent(new CustomEvent("tradview:pane-resize"));
198
+ }
83
199
  });
84
200
  handle.addEventListener("pointerup", stop);
85
201
  handle.addEventListener("pointercancel", stop);
@@ -150,20 +266,6 @@ var IndicatorPaneStack = class {
150
266
  this.macdWrap = macdPane.wrap;
151
267
  this.rsiWrap = rsiPane.wrap;
152
268
  this.kdjWrap = kdjPane.wrap;
153
- this.root.append(macdPane.wrap, rsiPane.wrap, kdjPane.wrap);
154
- this.detachResizers.push(
155
- attachPaneResizer(macdPane.wrap, rsiPane.wrap, {
156
- storageKey: "tradview:pane:macd-rsi",
157
- minTopPx: 72,
158
- minBottomPx: 72
159
- }),
160
- attachPaneResizer(rsiPane.wrap, kdjPane.wrap, {
161
- storageKey: "tradview:pane:rsi-kdj",
162
- minTopPx: 72,
163
- minBottomPx: 72
164
- })
165
- );
166
- this.applyPaneVisibility();
167
269
  const layout = this.layoutForTheme(this.dark);
168
270
  const grid = gridOptions(this.showGrid, this.dark);
169
271
  this.macdChart = createChart(macdPane.el, { layout, grid, autoSize: true });
@@ -179,6 +281,7 @@ var IndicatorPaneStack = class {
179
281
  this.kdjK = this.kdjChart.addSeries(LineSeries, { color: "#42a5f5", lineWidth: 1 });
180
282
  this.kdjD = this.kdjChart.addSeries(LineSeries, { color: "#ffa726", lineWidth: 1 });
181
283
  this.kdjJ = this.kdjChart.addSeries(LineSeries, { color: "#ef5350", lineWidth: 1 });
284
+ this.applyPaneVisibility();
182
285
  }
183
286
  root;
184
287
  macdChart;
@@ -215,11 +318,32 @@ var IndicatorPaneStack = class {
215
318
  this.onConfigChange?.(this.config);
216
319
  }
217
320
  applyPaneVisibility() {
218
- this.macdWrap.style.display = this.config.showMacd ? "" : "none";
219
- this.rsiWrap.style.display = this.config.showRsi ? "" : "none";
220
- this.kdjWrap.style.display = this.config.showKdj ? "" : "none";
221
321
  const anyVisible = this.config.showMacd || this.config.showRsi || this.config.showKdj;
222
322
  this.root.style.display = anyVisible ? "flex" : "none";
323
+ this.rebuildPaneLayout();
324
+ }
325
+ /** Rebuild flex children and drag handles only between visible panes. */
326
+ rebuildPaneLayout() {
327
+ for (const detach of this.detachResizers) detach();
328
+ this.detachResizers.length = 0;
329
+ this.root.querySelectorAll("[data-pane-resizer]").forEach((el) => el.remove());
330
+ const panes = [];
331
+ if (this.config.showMacd) panes.push({ id: "macd", el: this.macdWrap });
332
+ if (this.config.showRsi) panes.push({ id: "rsi", el: this.rsiWrap });
333
+ if (this.config.showKdj) panes.push({ id: "kdj", el: this.kdjWrap });
334
+ this.root.replaceChildren(...panes.map((p) => p.el));
335
+ for (let i = 0; i < panes.length - 1; i++) {
336
+ const top = panes[i];
337
+ const bottom = panes[i + 1];
338
+ this.detachResizers.push(
339
+ attachPaneResizer(top.el, bottom.el, {
340
+ storageKey: `tradview:pane:${top.id}-${bottom.id}`,
341
+ minTopPx: 72,
342
+ minBottomPx: 72
343
+ })
344
+ );
345
+ }
346
+ this.resize();
223
347
  }
224
348
  clearBars() {
225
349
  this.macdLine.setData([]);
@@ -338,11 +462,14 @@ var IndicatorPaneStack = class {
338
462
  this.kdjChart.timeScale().scrollToRealTime();
339
463
  }
340
464
  resize() {
341
- for (const { chart, el } of [
342
- { chart: this.macdChart, el: this.macdChart.chartElement().parentElement },
343
- { chart: this.rsiChart, el: this.rsiChart.chartElement().parentElement },
344
- { chart: this.kdjChart, el: this.kdjChart.chartElement().parentElement }
345
- ]) {
465
+ const panes = [
466
+ { show: this.config.showMacd, chart: this.macdChart },
467
+ { show: this.config.showRsi, chart: this.rsiChart },
468
+ { show: this.config.showKdj, chart: this.kdjChart }
469
+ ];
470
+ for (const { show, chart } of panes) {
471
+ if (!show || !chart) continue;
472
+ const el = chart.chartElement()?.parentElement;
346
473
  if (!el) continue;
347
474
  const w = el.clientWidth;
348
475
  const h = el.clientHeight;
@@ -360,6 +487,7 @@ var IndicatorPaneStack = class {
360
487
  createPaneWrap(label, paneId) {
361
488
  const wrap = document.createElement("div");
362
489
  wrap.className = `tv-indicator-pane tv-indicator-pane--${paneId}`;
490
+ wrap.dataset.paneId = paneId;
363
491
  wrap.style.cssText = "flex:1;min-height:72px;width:100%;position:relative;border-top:1px solid #30363d;";
364
492
  const tag = document.createElement("span");
365
493
  tag.textContent = label;
@@ -390,6 +518,10 @@ var IndicatorPaneStack = class {
390
518
  textColor: dark ? "#e6edf3" : "#24292f"
391
519
  };
392
520
  }
521
+ /** LWC instances for sync-group reassignment. */
522
+ getCharts() {
523
+ return [this.macdChart, this.rsiChart, this.kdjChart];
524
+ }
393
525
  };
394
526
  function maOverlayLine(bars, period = 20, source = "close") {
395
527
  const src = barsForSource(bars, source);
@@ -424,6 +556,7 @@ var TimeScaleBus = class {
424
556
  if (this.charts.includes(chart)) return;
425
557
  this.charts.push(chart);
426
558
  chart.timeScale().subscribeVisibleLogicalRangeChange((range) => {
559
+ if (!this.charts.includes(chart)) return;
427
560
  if (this.syncing || !range) return;
428
561
  const tr = chart.timeScale().getVisibleRange();
429
562
  if (tr && typeof tr.from === "number" && typeof tr.to === "number") {
@@ -433,6 +566,10 @@ var TimeScaleBus = class {
433
566
  this.syncFrom(chart, range);
434
567
  });
435
568
  }
569
+ unregister(chart) {
570
+ const i = this.charts.indexOf(chart);
571
+ if (i >= 0) this.charts.splice(i, 1);
572
+ }
436
573
  subscribeTransform(listener) {
437
574
  this.listeners.add(listener);
438
575
  return () => this.listeners.delete(listener);
@@ -480,6 +617,35 @@ var TimeScaleBus = class {
480
617
  this.syncing = false;
481
618
  this.emit();
482
619
  }
620
+ /**
621
+ * DESIGN §10.4.1: after historical prepend, shift every pane's logical range by Δ
622
+ * so canonical `visibleFromMs` / `visibleToMs` (and crosshair time mapping) stay fixed.
623
+ */
624
+ compensatePrependLogicalRange(delta, referenceChart, sliceTimes) {
625
+ const d = Math.max(0, Math.floor(delta));
626
+ if (d === 0 || this.charts.length === 0) return;
627
+ const ref = referenceChart && this.charts.includes(referenceChart) ? referenceChart : this.charts[0];
628
+ const ts = ref.timeScale();
629
+ let current = typeof ts.getVisibleLogicalRange === "function" ? ts.getVisibleLogicalRange() : null;
630
+ if (!current && sliceTimes?.length && this.visibleToMs > this.visibleFromMs) {
631
+ current = logicalRangeForVisibleWindow(
632
+ sliceTimes,
633
+ this.visibleFromMs,
634
+ this.visibleToMs
635
+ );
636
+ }
637
+ if (!current) return;
638
+ const next = {
639
+ from: current.from + d,
640
+ to: current.to + d
641
+ };
642
+ this.syncing = true;
643
+ for (const chart of this.charts) {
644
+ chart.timeScale().setVisibleLogicalRange(next);
645
+ }
646
+ this.syncing = false;
647
+ this.emit();
648
+ }
483
649
  syncFrom(source, range) {
484
650
  this.syncing = true;
485
651
  for (const chart of this.charts) {
@@ -501,6 +667,78 @@ var TimeScaleBus = class {
501
667
  }
502
668
  };
503
669
 
670
+ // src/time-scale-bus-registry.ts
671
+ function normalizeSyncGroupId(id) {
672
+ if (id == null) return null;
673
+ const trimmed = String(id).trim();
674
+ return trimmed.length > 0 ? trimmed : null;
675
+ }
676
+ var INDEP_PREFIX = "@independent:";
677
+ function independentBusKey(pane) {
678
+ return `${INDEP_PREFIX}${pane}`;
679
+ }
680
+ function resolveBusMapKey(groupId, pane) {
681
+ const norm = normalizeSyncGroupId(groupId);
682
+ return norm ?? independentBusKey(pane);
683
+ }
684
+ var TimeScaleBusRegistry = class {
685
+ buses = /* @__PURE__ */ new Map();
686
+ paneKeys = /* @__PURE__ */ new Map();
687
+ activeKey;
688
+ constructor() {
689
+ for (const pane of ["main", "volume", "indicator"]) {
690
+ const key = independentBusKey(pane);
691
+ this.paneKeys.set(pane, key);
692
+ this.getOrCreateBus(key);
693
+ }
694
+ this.activeKey = this.paneKeys.get("main");
695
+ }
696
+ getOrCreateBus(key) {
697
+ let bus = this.buses.get(key);
698
+ if (!bus) {
699
+ bus = new TimeScaleBus();
700
+ this.buses.set(key, bus);
701
+ }
702
+ return bus;
703
+ }
704
+ getBusKeyForPane(pane) {
705
+ return this.paneKeys.get(pane);
706
+ }
707
+ getBusForPane(pane) {
708
+ return this.getOrCreateBus(this.getBusKeyForPane(pane));
709
+ }
710
+ /** Active bus for IChart viewport APIs (follows last-focused chart pane). */
711
+ get activeBus() {
712
+ return this.getOrCreateBus(this.activeKey);
713
+ }
714
+ getActiveBusKey() {
715
+ return this.activeKey;
716
+ }
717
+ setActivePane(pane) {
718
+ this.activeKey = this.paneKeys.get(pane);
719
+ }
720
+ setPaneSyncGroup(pane, groupId) {
721
+ const nextKey = resolveBusMapKey(groupId, pane);
722
+ const prevKey = this.paneKeys.get(pane);
723
+ this.paneKeys.set(pane, nextKey);
724
+ this.getOrCreateBus(nextKey);
725
+ if (this.activeKey === prevKey) this.activeKey = nextKey;
726
+ return prevKey;
727
+ }
728
+ forEachBus(fn) {
729
+ for (const [key, bus] of this.buses) fn(key, bus);
730
+ }
731
+ moveChart(chart, fromKey, toKey, copyRange = true) {
732
+ if (fromKey === toKey) return;
733
+ const from = this.getOrCreateBus(fromKey);
734
+ const to = this.getOrCreateBus(toKey);
735
+ const range = copyRange ? from.getVisibleRange() : null;
736
+ from.unregister(chart);
737
+ to.register(chart);
738
+ if (range) to.setVisibleTimeRange(range);
739
+ }
740
+ };
741
+
504
742
  // src/viewport-fit.ts
505
743
  import { intervalMs } from "@coderyo/data";
506
744
  function defaultBarSpacingForInterval(interval) {
@@ -581,6 +819,13 @@ var BarSmoothAnimator = class {
581
819
  };
582
820
 
583
821
  // src/pane-orchestrator.ts
822
+ function isLayeredPaneMount(opts) {
823
+ return !!opts.volumeMount;
824
+ }
825
+ function shouldResizeChartPane(focus, pane) {
826
+ if (!focus) return true;
827
+ return focus.has(pane);
828
+ }
584
829
  function toUtcSeconds2(tMs) {
585
830
  return Math.floor(tMs / 1e3);
586
831
  }
@@ -591,7 +836,18 @@ function barToVolume(b) {
591
836
  return { time: toUtcSeconds2(b.t), value: b.v ?? 0 };
592
837
  }
593
838
  var PaneOrchestrator = class {
594
- bus = new TimeScaleBus();
839
+ busRegistry = new TimeScaleBusRegistry();
840
+ /** Active sync group bus (last-focused pane); used by ChartController viewport APIs. */
841
+ get bus() {
842
+ return this.busRegistry.activeBus;
843
+ }
844
+ layeredPanes;
845
+ mainEl;
846
+ volWrap;
847
+ detachMainVolResizer = () => {
848
+ };
849
+ resizeFocusPanes = null;
850
+ listenPaneResizeEvents;
595
851
  mainChart;
596
852
  volumeChart;
597
853
  mainSeries;
@@ -614,6 +870,9 @@ var PaneOrchestrator = class {
614
870
  barTimesOrdered = [];
615
871
  didInitialFit = false;
616
872
  skipNextInitialFit = false;
873
+ /** setBars ran before the pane had layout size; refit on first real resize. */
874
+ pendingViewportFit = false;
875
+ pendingViewportBars = null;
617
876
  indicatorConfig = null;
618
877
  onIndicatorConfigChange;
619
878
  currentInterval = "1h";
@@ -624,18 +883,57 @@ var PaneOrchestrator = class {
624
883
  smoothPriceDurationMs = 150;
625
884
  constructor(opts) {
626
885
  this.maxRenderPoints = opts.maxRenderPoints ?? 4e3;
886
+ this.listenPaneResizeEvents = opts.listenPaneResizeEvents !== false;
627
887
  this.dark = opts.theme !== "light";
628
888
  this.showGrid = opts.showGrid ?? false;
629
889
  const layout = this.layoutForTheme(this.dark);
630
890
  const grid = gridOptions(this.showGrid, this.dark);
631
- const mainEl = document.createElement("div");
632
- mainEl.style.cssText = "flex:7;min-height:120px;width:100%;position:relative;";
891
+ this.layeredPanes = !!opts.volumeMount;
633
892
  const volEl = document.createElement("div");
634
- volEl.style.cssText = "flex:2;min-height:64px;width:100%;position:relative;";
635
- opts.container.style.cssText = "display:flex;flex-direction:column;height:100%;width:100%;min-height:240px;overflow:hidden;";
636
- opts.container.append(mainEl, volEl);
637
- attachPaneResizer(mainEl, volEl, { storageKey: "tradview:pane:main-volume" });
638
- this.mainChart = createChart2(mainEl, { layout, grid, autoSize: true });
893
+ volEl.style.cssText = "width:100%;height:100%;min-height:0;position:relative;";
894
+ if (this.layeredPanes) {
895
+ this.mainEl = opts.container;
896
+ this.mainEl.style.cssText = "width:100%;height:100%;min-height:80px;position:relative;overflow:hidden;";
897
+ this.volWrap = opts.volumeMount;
898
+ this.volWrap.dataset.paneId = "volume";
899
+ this.volWrap.className = "tv-volume-pane tv-volume-pane--layered";
900
+ this.volWrap.replaceChildren();
901
+ this.volWrap.style.cssText = "width:100%;height:100%;min-height:48px;position:relative;overflow:hidden;box-sizing:border-box;";
902
+ const volTag = document.createElement("span");
903
+ volTag.textContent = "Volume";
904
+ volTag.style.cssText = "position:absolute;left:6px;top:4px;z-index:2;font-size:10px;color:#8b949e;pointer-events:none;";
905
+ const volClose = document.createElement("button");
906
+ volClose.type = "button";
907
+ volClose.textContent = "\xD7";
908
+ volClose.title = "\u95DC\u9589\u6210\u4EA4\u91CF";
909
+ volClose.setAttribute("aria-label", "Close volume");
910
+ volClose.style.cssText = "position:absolute;right:6px;top:4px;z-index:3;width:22px;height:22px;padding:0;border:1px solid #30363d;border-radius:4px;background:#21262d;color:#8b949e;cursor:pointer;font-size:14px;line-height:1;";
911
+ volClose.onclick = () => this.closeVolumePane();
912
+ this.volWrap.append(volTag, volClose, volEl);
913
+ } else {
914
+ this.mainEl = document.createElement("div");
915
+ this.mainEl.style.cssText = "flex:7;min-height:120px;width:100%;position:relative;";
916
+ volEl.style.cssText = "flex:1;min-height:0;width:100%;height:100%;position:relative;";
917
+ this.volWrap = document.createElement("div");
918
+ this.volWrap.dataset.paneId = "volume";
919
+ this.volWrap.className = "tv-volume-pane";
920
+ this.volWrap.style.cssText = "flex:2;min-height:48px;width:100%;position:relative;display:flex;flex-direction:column;border-top:1px solid #30363d;";
921
+ const volTag = document.createElement("span");
922
+ volTag.textContent = "Volume";
923
+ volTag.style.cssText = "position:absolute;left:6px;top:4px;z-index:2;font-size:10px;color:#8b949e;pointer-events:none;";
924
+ const volClose = document.createElement("button");
925
+ volClose.type = "button";
926
+ volClose.textContent = "\xD7";
927
+ volClose.title = "\u95DC\u9589\u6210\u4EA4\u91CF";
928
+ volClose.setAttribute("aria-label", "Close volume");
929
+ volClose.style.cssText = "position:absolute;right:6px;top:4px;z-index:3;width:22px;height:22px;padding:0;border:1px solid #30363d;border-radius:4px;background:#21262d;color:#8b949e;cursor:pointer;font-size:14px;line-height:1;";
930
+ volClose.onclick = () => this.closeVolumePane();
931
+ this.volWrap.append(volTag, volClose, volEl);
932
+ opts.container.style.cssText = "display:flex;flex-direction:column;height:100%;width:100%;min-height:200px;overflow:hidden;";
933
+ opts.container.append(this.mainEl, this.volWrap);
934
+ this.rebuildMainVolumeResizer();
935
+ }
936
+ this.mainChart = createChart2(this.mainEl, { layout, grid, autoSize: true });
639
937
  this.volumeChart = createChart2(volEl, {
640
938
  layout,
641
939
  grid,
@@ -691,8 +989,8 @@ var PaneOrchestrator = class {
691
989
  lineWidth: 1,
692
990
  title: "VolMA5"
693
991
  });
694
- this.bus.register(this.mainChart);
695
- this.bus.register(this.volumeChart);
992
+ this.busRegistry.getBusForPane("main").register(this.mainChart);
993
+ this.busRegistry.getBusForPane("volume").register(this.volumeChart);
696
994
  this.indicatorRoot = opts.indicatorRoot;
697
995
  this.indicatorConfig = opts.indicatorConfig ?? null;
698
996
  this.onIndicatorConfigChange = opts.onIndicatorConfigChange;
@@ -700,9 +998,16 @@ var PaneOrchestrator = class {
700
998
  this.barSpacingByInterval = opts.barSpacingByInterval;
701
999
  this.pinePlots = opts.pinePlots ?? null;
702
1000
  this.indicators = this.createIndicatorStack();
703
- this.initOverlay(mainEl);
1001
+ this.initOverlay(this.mainEl);
704
1002
  this.setSmoothPriceUpdate(opts.smoothPriceUpdate ?? false, opts.smoothPriceDurationMs);
1003
+ this.applyVolumeVisibility();
1004
+ if (opts.listenPaneResizeEvents !== false) {
1005
+ window.addEventListener("tradview:pane-resize", this.onPaneResize);
1006
+ }
705
1007
  }
1008
+ onPaneResize = () => {
1009
+ this.resize();
1010
+ };
706
1011
  setSmoothPriceUpdate(enabled, durationMs = 150) {
707
1012
  this.smoothPriceDurationMs = durationMs;
708
1013
  if (enabled) {
@@ -736,7 +1041,9 @@ var PaneOrchestrator = class {
736
1041
  applyLastBarToSeries(bar) {
737
1042
  this.barByTime.set(bar.t, bar);
738
1043
  this.mainSeries.update(barToCandle(bar));
739
- this.volumeSeries.update(barToVolume(bar));
1044
+ if (this.isVolumeVisible()) {
1045
+ this.volumeSeries.update(barToVolume(bar));
1046
+ }
740
1047
  this.ensurePriceLine(bar.c);
741
1048
  if (this.indicatorConfig) {
742
1049
  const bars = [...this.barByTime.values()].sort((a, b) => a.t - b.t);
@@ -781,10 +1088,12 @@ var PaneOrchestrator = class {
781
1088
  this.bollMiddle.setData([]);
782
1089
  this.bollLower.setData([]);
783
1090
  this.volMaSeries.setData([]);
1091
+ this.volumeSeries.setData([]);
784
1092
  this.emaSeries.applyOptions({ visible: false });
785
1093
  this.bollUpper.applyOptions({ visible: false });
786
1094
  this.bollMiddle.applyOptions({ visible: false });
787
1095
  this.bollLower.applyOptions({ visible: false });
1096
+ this.applyVolumeVisibility();
788
1097
  return;
789
1098
  }
790
1099
  if (!this.indicators) this.indicators = this.createIndicatorStack();
@@ -796,6 +1105,81 @@ var PaneOrchestrator = class {
796
1105
  this.indicators?.setBars(bars);
797
1106
  }
798
1107
  }
1108
+ this.applyVolumeVisibility();
1109
+ }
1110
+ isVolumeVisible() {
1111
+ return this.indicatorConfig?.showVolume ?? true;
1112
+ }
1113
+ closeVolumePane() {
1114
+ if (!this.indicatorConfig) {
1115
+ this.indicatorConfig = { ...DEFAULT_INDICATOR_CONFIG2, showVolume: false };
1116
+ } else {
1117
+ this.indicatorConfig = { ...this.indicatorConfig, showVolume: false };
1118
+ }
1119
+ this.applyVolumeVisibility();
1120
+ this.onIndicatorConfigChange?.(this.indicatorConfig);
1121
+ }
1122
+ applyVolumeVisibility() {
1123
+ const show = this.isVolumeVisible();
1124
+ this.volWrap.style.display = show ? "" : "none";
1125
+ this.rebuildMainVolumeResizer();
1126
+ if (!show) {
1127
+ this.volumeSeries.setData([]);
1128
+ this.volMaSeries.setData([]);
1129
+ }
1130
+ this.syncChartSize();
1131
+ }
1132
+ /** Assign per-pane sync group ids (`''` / omit = independent). Re-registers LWC charts on group change. */
1133
+ setPaneSyncGroups(patch) {
1134
+ const apply = (pane, groupId) => {
1135
+ const prevKey = this.busRegistry.setPaneSyncGroup(pane, groupId);
1136
+ const nextKey = this.busRegistry.getBusKeyForPane(pane);
1137
+ if (pane === "main") {
1138
+ this.busRegistry.moveChart(this.mainChart, prevKey, nextKey);
1139
+ } else if (pane === "volume") {
1140
+ this.busRegistry.moveChart(this.volumeChart, prevKey, nextKey);
1141
+ } else if (this.indicators) {
1142
+ for (const chart of this.indicators.getCharts()) {
1143
+ this.busRegistry.moveChart(chart, prevKey, nextKey);
1144
+ }
1145
+ }
1146
+ };
1147
+ if (patch.main !== void 0) apply("main", patch.main);
1148
+ if (patch.volume !== void 0) apply("volume", patch.volume);
1149
+ if (patch.indicator !== void 0) apply("indicator", patch.indicator);
1150
+ }
1151
+ setActiveSyncPane(pane) {
1152
+ const key = pane === "volume" ? "volume" : pane === "indicator" ? "indicator" : "main";
1153
+ this.busRegistry.setActivePane(key);
1154
+ }
1155
+ /** P2: when set, only these panes get LWC resize (panes in the same sync group still share TimeScaleBus). */
1156
+ setResizeFocusPanes(panes) {
1157
+ this.resizeFocusPanes = panes?.length ? new Set(panes) : null;
1158
+ this.resize();
1159
+ }
1160
+ /** Current resize focus (null = all panes). @internal — not part of public package API. */
1161
+ getResizeFocusPanes() {
1162
+ return this.resizeFocusPanes ? [...this.resizeFocusPanes] : null;
1163
+ }
1164
+ shouldResizePane(pane) {
1165
+ return shouldResizeChartPane(this.resizeFocusPanes, pane);
1166
+ }
1167
+ rebuildMainVolumeResizer() {
1168
+ if (this.layeredPanes) return;
1169
+ this.detachMainVolResizer();
1170
+ const parent = this.mainEl.parentElement;
1171
+ parent?.querySelectorAll(":scope > [data-pane-resizer]").forEach((el) => el.remove());
1172
+ if (!this.isVolumeVisible()) {
1173
+ this.mainEl.style.flex = "1";
1174
+ return;
1175
+ }
1176
+ this.mainEl.style.flex = "";
1177
+ this.volWrap.style.flex = "";
1178
+ this.detachMainVolResizer = attachPaneResizer(this.mainEl, this.volWrap, {
1179
+ storageKey: "tradview:pane:main-volume",
1180
+ minTopPx: 120,
1181
+ minBottomPx: 48
1182
+ });
799
1183
  }
800
1184
  setPinePlots(plots) {
801
1185
  this.pinePlots = plots;
@@ -897,7 +1281,11 @@ var PaneOrchestrator = class {
897
1281
  vols.push(barToVolume(b));
898
1282
  }
899
1283
  this.mainSeries.setData(candles);
900
- this.volumeSeries.setData(vols);
1284
+ if (this.isVolumeVisible()) {
1285
+ this.volumeSeries.setData(vols);
1286
+ } else {
1287
+ this.volumeSeries.setData([]);
1288
+ }
901
1289
  if (this.indicatorConfig) {
902
1290
  this.applyMainOverlays(renderBars);
903
1291
  if (hasVisibleIndicatorPanes2(this.indicatorConfig)) {
@@ -914,13 +1302,35 @@ var PaneOrchestrator = class {
914
1302
  }
915
1303
  if (renderBars.length > 0) {
916
1304
  this.syncChartSize();
917
- if (!this.didInitialFit && !this.skipNextInitialFit) {
918
- this.applyViewAfterDataReload(renderBars);
919
- this.didInitialFit = true;
920
- }
1305
+ this.tryInitialViewportFit(renderBars);
921
1306
  this.skipNextInitialFit = false;
922
1307
  }
923
1308
  }
1309
+ mainPaneHasSize() {
1310
+ const el = this.mainChart.chartElement().parentElement;
1311
+ return (el?.clientWidth ?? 0) > 0 && (el?.clientHeight ?? 0) > 0;
1312
+ }
1313
+ tryInitialViewportFit(renderBars) {
1314
+ if (this.didInitialFit || this.skipNextInitialFit) return;
1315
+ if (!this.mainPaneHasSize()) {
1316
+ this.pendingViewportFit = true;
1317
+ this.pendingViewportBars = renderBars;
1318
+ return;
1319
+ }
1320
+ this.applyViewAfterDataReload(renderBars);
1321
+ this.didInitialFit = true;
1322
+ this.pendingViewportFit = false;
1323
+ this.pendingViewportBars = null;
1324
+ }
1325
+ flushPendingViewportFit() {
1326
+ if (!this.pendingViewportFit || this.skipNextInitialFit || this.didInitialFit) return;
1327
+ const bars = this.pendingViewportBars ?? [...this.barByTime.values()].sort((a, b) => a.t - b.t);
1328
+ if (bars.length === 0 || !this.mainPaneHasSize()) return;
1329
+ this.applyViewAfterDataReload(bars);
1330
+ this.didInitialFit = true;
1331
+ this.pendingViewportFit = false;
1332
+ this.pendingViewportBars = null;
1333
+ }
924
1334
  subscribeCrosshair(listener) {
925
1335
  const handler = (param) => {
926
1336
  if (param.time == null || !param.point) {
@@ -957,7 +1367,7 @@ var PaneOrchestrator = class {
957
1367
  }
958
1368
  createIndicatorStack() {
959
1369
  if (!this.indicatorRoot || !this.indicatorConfig) return null;
960
- return new IndicatorPaneStack(this.indicatorRoot, this.bus, {
1370
+ return new IndicatorPaneStack(this.indicatorRoot, this.busRegistry.getBusForPane("indicator"), {
961
1371
  theme: this.dark ? "dark" : "light",
962
1372
  showGrid: this.showGrid,
963
1373
  config: this.indicatorConfig,
@@ -972,7 +1382,7 @@ var PaneOrchestrator = class {
972
1382
  if (!this.autoBarSpacingOnInterval) return;
973
1383
  const iv = interval ?? this.currentInterval;
974
1384
  const spacing = resolveBarSpacingForInterval(iv, this.barSpacingByInterval);
975
- this.bus.setBarSpacing(spacing);
1385
+ this.busRegistry.forEachBus((_, bus) => bus.setBarSpacing(spacing));
976
1386
  }
977
1387
  setBarSpacingPolicy(opts) {
978
1388
  if (opts.autoBarSpacingOnInterval !== void 0) {
@@ -988,7 +1398,7 @@ var PaneOrchestrator = class {
988
1398
  this.applyIntervalBarSpacing();
989
1399
  } else {
990
1400
  this.mainChart.timeScale().fitContent();
991
- this.volumeChart.timeScale().fitContent();
1401
+ if (this.isVolumeVisible()) this.volumeChart.timeScale().fitContent();
992
1402
  this.indicators?.fitContent();
993
1403
  }
994
1404
  if (renderBars.length > 0) {
@@ -1001,14 +1411,31 @@ var PaneOrchestrator = class {
1001
1411
  resetViewState() {
1002
1412
  this.didInitialFit = false;
1003
1413
  this.skipNextInitialFit = false;
1004
- this.bus.visibleFromMs = 0;
1005
- this.bus.visibleToMs = 0;
1414
+ this.pendingViewportFit = false;
1415
+ this.pendingViewportBars = null;
1416
+ this.busRegistry.forEachBus((_, bus) => {
1417
+ bus.visibleFromMs = 0;
1418
+ bus.visibleToMs = 0;
1419
+ });
1006
1420
  }
1007
1421
  /** Skip the next automatic fitContent after setBars (used by reloadHistory). */
1008
1422
  preserveViewportOnNextSetBars() {
1009
1423
  this.skipNextInitialFit = true;
1010
1424
  this.didInitialFit = true;
1011
1425
  }
1426
+ /**
1427
+ * DESIGN §10.4.1: after `mergeBars(..., prepend)` shift logical ranges on every sync bus
1428
+ * so canonical ms viewport and crosshair `t` stay stable.
1429
+ */
1430
+ compensatePrependForBuses(sortedTimesBefore, sortedTimesAfter, interval) {
1431
+ compensatePrependOnRegistry({
1432
+ registry: this.busRegistry,
1433
+ sortedTimesBefore,
1434
+ sortedTimesAfter,
1435
+ intervalMs: intervalMs2(interval),
1436
+ referenceChart: this.mainChart
1437
+ });
1438
+ }
1012
1439
  getVisibleRange() {
1013
1440
  return this.bus.getVisibleRange();
1014
1441
  }
@@ -1030,10 +1457,10 @@ var PaneOrchestrator = class {
1030
1457
  if (nearest == null) return;
1031
1458
  const i = this.barTimesOrdered.indexOf(nearest.t);
1032
1459
  if (i < 0) return;
1033
- this.bus.scrollToLogicalPosition(i, (animationMs ?? 0) > 0);
1460
+ this.busRegistry.getBusForPane("main").scrollToLogicalPosition(i, (animationMs ?? 0) > 0);
1034
1461
  return;
1035
1462
  }
1036
- this.bus.scrollToLogicalPosition(idx, (animationMs ?? 0) > 0);
1463
+ this.busRegistry.getBusForPane("main").scrollToLogicalPosition(idx, (animationMs ?? 0) > 0);
1037
1464
  }
1038
1465
  /** Clear series while symbol/interval data reloads (avoids overlapping candles). */
1039
1466
  clearBars() {
@@ -1048,13 +1475,13 @@ var PaneOrchestrator = class {
1048
1475
  }
1049
1476
  fitContent() {
1050
1477
  this.mainChart.timeScale().fitContent();
1051
- this.volumeChart.timeScale().fitContent();
1478
+ if (this.isVolumeVisible()) this.volumeChart.timeScale().fitContent();
1052
1479
  this.indicators?.fitContent();
1053
1480
  this.didInitialFit = true;
1054
1481
  }
1055
1482
  scrollToRealtime() {
1056
1483
  this.mainChart.timeScale().scrollToRealTime();
1057
- this.volumeChart.timeScale().scrollToRealTime();
1484
+ if (this.isVolumeVisible()) this.volumeChart.timeScale().scrollToRealTime();
1058
1485
  this.indicators?.scrollToRealtime();
1059
1486
  }
1060
1487
  setLogScale(enabled) {
@@ -1063,7 +1490,18 @@ var PaneOrchestrator = class {
1063
1490
  resize() {
1064
1491
  this.syncChartSize();
1065
1492
  this.syncOverlaySize();
1493
+ if (this.shouldResizePane("indicator")) this.indicators?.resize();
1494
+ this.flushPendingViewportFit();
1495
+ }
1496
+ /**
1497
+ * Resize every LWC pane once for viewport fit; does **not** change {@link resizeFocusPanes}.
1498
+ * @internal Used by ChartController data refresh paths.
1499
+ */
1500
+ resizeAllPanes() {
1501
+ this.syncChartSize({ allPanes: true });
1502
+ this.syncOverlaySize();
1066
1503
  this.indicators?.resize();
1504
+ this.flushPendingViewportFit();
1067
1505
  }
1068
1506
  getOverlayCanvas() {
1069
1507
  return this.overlayCanvas;
@@ -1088,6 +1526,10 @@ var PaneOrchestrator = class {
1088
1526
  return p ?? null;
1089
1527
  }
1090
1528
  destroy() {
1529
+ if (this.listenPaneResizeEvents) {
1530
+ window.removeEventListener("tradview:pane-resize", this.onPaneResize);
1531
+ }
1532
+ this.detachMainVolResizer();
1091
1533
  this.barAnimator?.cancel();
1092
1534
  if (this.priceLine) {
1093
1535
  this.mainSeries.removePriceLine(this.priceLine);
@@ -1105,8 +1547,10 @@ var PaneOrchestrator = class {
1105
1547
  parent.appendChild(canvas);
1106
1548
  this.overlayCanvas = canvas;
1107
1549
  this.syncOverlaySize();
1108
- this.bus.subscribeTransform(() => {
1109
- this.syncOverlaySize();
1550
+ this.busRegistry.forEachBus((_, bus) => {
1551
+ bus.subscribeTransform(() => {
1552
+ this.syncOverlaySize();
1553
+ });
1110
1554
  });
1111
1555
  }
1112
1556
  /** Let drawing overlay receive clicks; cursor mode keeps pan/zoom on LWC. */
@@ -1123,15 +1567,16 @@ var PaneOrchestrator = class {
1123
1567
  this.overlayCanvas.width = rect.width * devicePixelRatio;
1124
1568
  this.overlayCanvas.height = rect.height * devicePixelRatio;
1125
1569
  }
1126
- syncChartSize() {
1570
+ syncChartSize(opts) {
1571
+ const all = opts?.allPanes === true;
1127
1572
  const mainEl = this.mainChart.chartElement().parentElement;
1128
1573
  const volEl = this.volumeChart.chartElement().parentElement;
1129
- if (mainEl) {
1574
+ if (mainEl && (all || this.shouldResizePane("main"))) {
1130
1575
  const w = mainEl.clientWidth;
1131
1576
  const h = mainEl.clientHeight;
1132
1577
  if (w > 0 && h > 0) this.mainChart.resize(w, h);
1133
1578
  }
1134
- if (volEl) {
1579
+ if (this.isVolumeVisible() && volEl && (all || this.shouldResizePane("volume"))) {
1135
1580
  const w = volEl.clientWidth;
1136
1581
  const h = volEl.clientHeight;
1137
1582
  if (w > 0 && h > 0) this.volumeChart.resize(w, h);
@@ -1148,12 +1593,27 @@ export {
1148
1593
  IndicatorPaneStack,
1149
1594
  PaneOrchestrator,
1150
1595
  TimeScaleBus,
1596
+ TimeScaleBusRegistry,
1597
+ attachPaneResizer,
1151
1598
  bollOverlayLines,
1599
+ buildSliceTimes,
1600
+ compensatePrependOnBus,
1601
+ compensatePrependOnRegistry,
1602
+ computePrependSliceDeltaForViewport,
1603
+ countPrependSliceDelta,
1152
1604
  defaultBarSpacingForInterval,
1605
+ deriveRenderRange,
1153
1606
  detectIndicatorBarMutation,
1154
1607
  emaOverlayLine,
1608
+ independentBusKey,
1609
+ isLayeredPaneMount,
1610
+ logicalIndexToBarTimeMs,
1611
+ logicalRangeForVisibleWindow,
1155
1612
  maOverlayLine,
1613
+ normalizeSyncGroupId,
1156
1614
  resolveBarSpacingForInterval,
1615
+ resolveBusMapKey,
1616
+ shouldResizeChartPane,
1157
1617
  volMaOverlayLine
1158
1618
  };
1159
1619
  //# sourceMappingURL=index.js.map