@coderyo/renderer-lite 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +129 -4
- package/dist/index.js +647 -114
- package/dist/index.js.map +1 -1
- package/package.json +16 -15
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
HistogramSeries as HistogramSeries2,
|
|
7
7
|
LineSeries as LineSeries2
|
|
8
8
|
} from "lightweight-charts";
|
|
9
|
+
import { intervalMs as intervalMs2 } from "@coderyo/data";
|
|
9
10
|
import { lodDecimateBars } from "@coderyo/series";
|
|
10
11
|
|
|
11
12
|
// src/chart-grid.ts
|
|
@@ -17,6 +18,12 @@ function gridOptions(showGrid, dark) {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// src/pane-orchestrator.ts
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_INDICATOR_CONFIG as DEFAULT_INDICATOR_CONFIG2,
|
|
24
|
+
hasVisibleIndicatorPanes as hasVisibleIndicatorPanes2
|
|
25
|
+
} from "@coderyo/indicators";
|
|
26
|
+
|
|
20
27
|
// src/indicator-panes.ts
|
|
21
28
|
import {
|
|
22
29
|
ColorType,
|
|
@@ -26,6 +33,7 @@ import {
|
|
|
26
33
|
} from "lightweight-charts";
|
|
27
34
|
import {
|
|
28
35
|
DEFAULT_INDICATOR_CONFIG,
|
|
36
|
+
hasVisibleIndicatorPanes,
|
|
29
37
|
boll,
|
|
30
38
|
kdj,
|
|
31
39
|
macd,
|
|
@@ -33,6 +41,68 @@ import {
|
|
|
33
41
|
sma,
|
|
34
42
|
ema
|
|
35
43
|
} from "@coderyo/indicators";
|
|
44
|
+
|
|
45
|
+
// src/pane-resize.ts
|
|
46
|
+
function attachPaneResizer(topPane, bottomPane, opts = {}) {
|
|
47
|
+
const minTop = opts.minTopPx ?? 120;
|
|
48
|
+
const minBottom = opts.minBottomPx ?? 60;
|
|
49
|
+
const parent = topPane.parentElement;
|
|
50
|
+
if (!parent) return () => {
|
|
51
|
+
};
|
|
52
|
+
let dragging = false;
|
|
53
|
+
const handle = document.createElement("div");
|
|
54
|
+
handle.dataset.paneResizer = "1";
|
|
55
|
+
handle.style.cssText = "height:6px;cursor:row-resize;background:#30363d;flex-shrink:0;touch-action:none;z-index:5;";
|
|
56
|
+
handle.onmouseenter = () => {
|
|
57
|
+
handle.style.background = "#388bfd";
|
|
58
|
+
};
|
|
59
|
+
handle.onmouseleave = () => {
|
|
60
|
+
if (!dragging) handle.style.background = "#30363d";
|
|
61
|
+
};
|
|
62
|
+
bottomPane.insertAdjacentElement("beforebegin", handle);
|
|
63
|
+
const saved = opts.storageKey ? localStorage.getItem(opts.storageKey) : null;
|
|
64
|
+
if (saved) {
|
|
65
|
+
const ratio = Number(saved);
|
|
66
|
+
if (Number.isFinite(ratio) && ratio >= 0.15 && ratio <= 0.85) {
|
|
67
|
+
topPane.style.flex = `${ratio * 10}`;
|
|
68
|
+
bottomPane.style.flex = `${(1 - ratio) * 10}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const onMove = (clientY) => {
|
|
72
|
+
const rect = parent.getBoundingClientRect();
|
|
73
|
+
const y = clientY - rect.top;
|
|
74
|
+
const ratio = Math.min(0.85, Math.max(0.15, y / rect.height));
|
|
75
|
+
const topPx = ratio * rect.height;
|
|
76
|
+
const bottomPx = rect.height - topPx - handle.offsetHeight;
|
|
77
|
+
if (topPx < minTop || bottomPx < minBottom) return;
|
|
78
|
+
topPane.style.flex = `${ratio * 10}`;
|
|
79
|
+
bottomPane.style.flex = `${(1 - ratio) * 10}`;
|
|
80
|
+
if (opts.storageKey) localStorage.setItem(opts.storageKey, String(ratio));
|
|
81
|
+
};
|
|
82
|
+
const stop = () => {
|
|
83
|
+
const wasDragging = dragging;
|
|
84
|
+
dragging = false;
|
|
85
|
+
document.body.style.cursor = "";
|
|
86
|
+
handle.style.background = "#30363d";
|
|
87
|
+
if (wasDragging) window.dispatchEvent(new CustomEvent("tradview:pane-resize"));
|
|
88
|
+
};
|
|
89
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
90
|
+
dragging = true;
|
|
91
|
+
handle.setPointerCapture(e.pointerId);
|
|
92
|
+
document.body.style.cursor = "row-resize";
|
|
93
|
+
});
|
|
94
|
+
handle.addEventListener("pointermove", (e) => {
|
|
95
|
+
if (dragging) {
|
|
96
|
+
onMove(e.clientY);
|
|
97
|
+
window.dispatchEvent(new CustomEvent("tradview:pane-resize"));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
handle.addEventListener("pointerup", stop);
|
|
101
|
+
handle.addEventListener("pointercancel", stop);
|
|
102
|
+
return () => handle.remove();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/indicator-panes.ts
|
|
36
106
|
function barsForSource(bars, source) {
|
|
37
107
|
if (source === "close") return bars;
|
|
38
108
|
return bars.map((b) => ({ ...b, c: (b.h + b.l + b.c) / 3 }));
|
|
@@ -40,6 +110,21 @@ function barsForSource(bars, source) {
|
|
|
40
110
|
function toUtcSeconds(tMs) {
|
|
41
111
|
return Math.floor(tMs / 1e3);
|
|
42
112
|
}
|
|
113
|
+
function detectIndicatorBarMutation(prevTimes, bars) {
|
|
114
|
+
if (prevTimes.length === 0 || bars.length < prevTimes.length) return "full";
|
|
115
|
+
const prefixLen = Math.min(prevTimes.length, bars.length);
|
|
116
|
+
for (let i = 0; i < prefixLen - 1; i++) {
|
|
117
|
+
if (bars[i].t !== prevTimes[i]) return "full";
|
|
118
|
+
}
|
|
119
|
+
if (bars.length === prevTimes.length) {
|
|
120
|
+
return bars.length > 0 && bars[bars.length - 1].t === prevTimes[prevTimes.length - 1] ? "tail-update" : "full";
|
|
121
|
+
}
|
|
122
|
+
if (bars.length - prevTimes.length > 8) return "full";
|
|
123
|
+
for (let i = 0; i < prevTimes.length; i++) {
|
|
124
|
+
if (bars[i].t !== prevTimes[i]) return "full";
|
|
125
|
+
}
|
|
126
|
+
return "tail-append";
|
|
127
|
+
}
|
|
43
128
|
function lineData(bars, values) {
|
|
44
129
|
const out = [];
|
|
45
130
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -69,19 +154,18 @@ var IndicatorPaneStack = class {
|
|
|
69
154
|
this.dark = o.theme !== "light";
|
|
70
155
|
this.showGrid = o.showGrid ?? false;
|
|
71
156
|
this.config = o.config ?? DEFAULT_INDICATOR_CONFIG;
|
|
157
|
+
this.onConfigChange = o.onConfigChange;
|
|
72
158
|
this.root.style.display = "flex";
|
|
73
159
|
this.root.style.flexDirection = "column";
|
|
74
160
|
this.root.style.flex = "2";
|
|
75
161
|
this.root.style.minHeight = "0";
|
|
76
162
|
this.root.style.overflow = "hidden";
|
|
77
|
-
const macdPane = this.createPaneWrap("MACD");
|
|
78
|
-
const rsiPane = this.createPaneWrap("RSI");
|
|
79
|
-
const kdjPane = this.createPaneWrap("KDJ");
|
|
163
|
+
const macdPane = this.createPaneWrap("MACD", "macd");
|
|
164
|
+
const rsiPane = this.createPaneWrap("RSI", "rsi");
|
|
165
|
+
const kdjPane = this.createPaneWrap("KDJ", "kdj");
|
|
80
166
|
this.macdWrap = macdPane.wrap;
|
|
81
167
|
this.rsiWrap = rsiPane.wrap;
|
|
82
168
|
this.kdjWrap = kdjPane.wrap;
|
|
83
|
-
this.root.append(macdPane.wrap, rsiPane.wrap, kdjPane.wrap);
|
|
84
|
-
this.applyPaneVisibility();
|
|
85
169
|
const layout = this.layoutForTheme(this.dark);
|
|
86
170
|
const grid = gridOptions(this.showGrid, this.dark);
|
|
87
171
|
this.macdChart = createChart(macdPane.el, { layout, grid, autoSize: true });
|
|
@@ -97,6 +181,7 @@ var IndicatorPaneStack = class {
|
|
|
97
181
|
this.kdjK = this.kdjChart.addSeries(LineSeries, { color: "#42a5f5", lineWidth: 1 });
|
|
98
182
|
this.kdjD = this.kdjChart.addSeries(LineSeries, { color: "#ffa726", lineWidth: 1 });
|
|
99
183
|
this.kdjJ = this.kdjChart.addSeries(LineSeries, { color: "#ef5350", lineWidth: 1 });
|
|
184
|
+
this.applyPaneVisibility();
|
|
100
185
|
}
|
|
101
186
|
root;
|
|
102
187
|
macdChart;
|
|
@@ -115,14 +200,50 @@ var IndicatorPaneStack = class {
|
|
|
115
200
|
macdWrap;
|
|
116
201
|
rsiWrap;
|
|
117
202
|
kdjWrap;
|
|
203
|
+
detachResizers = [];
|
|
204
|
+
lastBarTimes = [];
|
|
205
|
+
onConfigChange;
|
|
118
206
|
setConfig(config) {
|
|
119
207
|
this.config = config;
|
|
208
|
+
this.lastBarTimes = [];
|
|
209
|
+
this.applyPaneVisibility();
|
|
210
|
+
if (!this.config.showMacd && !this.config.showRsi && !this.config.showKdj) {
|
|
211
|
+
this.clearBars();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
closePane(id) {
|
|
215
|
+
const patch = id === "macd" ? { showMacd: false } : id === "rsi" ? { showRsi: false } : { showKdj: false };
|
|
216
|
+
this.config = { ...this.config, ...patch };
|
|
120
217
|
this.applyPaneVisibility();
|
|
218
|
+
this.onConfigChange?.(this.config);
|
|
121
219
|
}
|
|
122
220
|
applyPaneVisibility() {
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
this.
|
|
221
|
+
const anyVisible = this.config.showMacd || this.config.showRsi || this.config.showKdj;
|
|
222
|
+
this.root.style.display = anyVisible ? "flex" : "none";
|
|
223
|
+
this.rebuildPaneLayout();
|
|
224
|
+
}
|
|
225
|
+
/** Rebuild flex children and drag handles only between visible panes. */
|
|
226
|
+
rebuildPaneLayout() {
|
|
227
|
+
for (const detach of this.detachResizers) detach();
|
|
228
|
+
this.detachResizers.length = 0;
|
|
229
|
+
this.root.querySelectorAll("[data-pane-resizer]").forEach((el) => el.remove());
|
|
230
|
+
const panes = [];
|
|
231
|
+
if (this.config.showMacd) panes.push({ id: "macd", el: this.macdWrap });
|
|
232
|
+
if (this.config.showRsi) panes.push({ id: "rsi", el: this.rsiWrap });
|
|
233
|
+
if (this.config.showKdj) panes.push({ id: "kdj", el: this.kdjWrap });
|
|
234
|
+
this.root.replaceChildren(...panes.map((p) => p.el));
|
|
235
|
+
for (let i = 0; i < panes.length - 1; i++) {
|
|
236
|
+
const top = panes[i];
|
|
237
|
+
const bottom = panes[i + 1];
|
|
238
|
+
this.detachResizers.push(
|
|
239
|
+
attachPaneResizer(top.el, bottom.el, {
|
|
240
|
+
storageKey: `tradview:pane:${top.id}-${bottom.id}`,
|
|
241
|
+
minTopPx: 72,
|
|
242
|
+
minBottomPx: 72
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
this.resize();
|
|
126
247
|
}
|
|
127
248
|
clearBars() {
|
|
128
249
|
this.macdLine.setData([]);
|
|
@@ -132,22 +253,87 @@ var IndicatorPaneStack = class {
|
|
|
132
253
|
this.kdjK.setData([]);
|
|
133
254
|
this.kdjD.setData([]);
|
|
134
255
|
this.kdjJ.setData([]);
|
|
256
|
+
this.lastBarTimes = [];
|
|
257
|
+
}
|
|
258
|
+
warmupLookback() {
|
|
259
|
+
const c = this.config;
|
|
260
|
+
return Math.max(
|
|
261
|
+
c.macdSlow + c.macdSignal,
|
|
262
|
+
c.rsiPeriod,
|
|
263
|
+
c.kdjPeriod + c.kdjKSmooth + c.kdjDSmooth
|
|
264
|
+
) + 5;
|
|
265
|
+
}
|
|
266
|
+
detectBarMutation(bars) {
|
|
267
|
+
return detectIndicatorBarMutation(this.lastBarTimes, bars);
|
|
268
|
+
}
|
|
269
|
+
pushSeriesUpdates(series, bars, values, fromIndex) {
|
|
270
|
+
for (let i = fromIndex; i < bars.length; i++) {
|
|
271
|
+
const v = values[i];
|
|
272
|
+
if (v == null) continue;
|
|
273
|
+
series.update({ time: toUtcSeconds(bars[i].t), value: v });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
pushHistUpdates(series, bars, values, fromIndex) {
|
|
277
|
+
for (let i = fromIndex; i < bars.length; i++) {
|
|
278
|
+
const v = values[i];
|
|
279
|
+
if (v == null) continue;
|
|
280
|
+
series.update({
|
|
281
|
+
time: toUtcSeconds(bars[i].t),
|
|
282
|
+
value: v,
|
|
283
|
+
color: v >= 0 ? "#26a69a88" : "#ef535088"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
135
286
|
}
|
|
136
287
|
setBars(bars) {
|
|
137
|
-
if (bars.length === 0)
|
|
288
|
+
if (bars.length === 0) {
|
|
289
|
+
this.clearBars();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const needMacd = this.config.showMacd;
|
|
293
|
+
const needRsi = this.config.showRsi;
|
|
294
|
+
const needKdj = this.config.showKdj;
|
|
295
|
+
if (!hasVisibleIndicatorPanes(this.config)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const mutation = this.detectBarMutation(bars);
|
|
299
|
+
this.lastBarTimes = bars.map((b) => b.t);
|
|
138
300
|
const src = barsForSource(bars, this.config.source);
|
|
139
|
-
const m = macd(src, this.config.macdFast, this.config.macdSlow, this.config.macdSignal);
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
301
|
+
const m = needMacd ? macd(src, this.config.macdFast, this.config.macdSlow, this.config.macdSignal) : null;
|
|
302
|
+
const r = needRsi ? rsi(src, this.config.rsiPeriod) : null;
|
|
303
|
+
const k = needKdj ? kdj(src, this.config.kdjPeriod, this.config.kdjKSmooth, this.config.kdjDSmooth) : null;
|
|
304
|
+
if (mutation === "full") {
|
|
305
|
+
if (needMacd && m) {
|
|
306
|
+
this.macdLine.setData(lineData(bars, m.macd));
|
|
307
|
+
this.macdSignal.setData(lineData(bars, m.signal));
|
|
308
|
+
this.macdHist.setData(histData(bars, m.histogram));
|
|
309
|
+
this.macdChart.timeScale().fitContent();
|
|
310
|
+
}
|
|
311
|
+
if (needRsi && r) {
|
|
312
|
+
this.rsiLine.setData(lineData(bars, r));
|
|
313
|
+
this.rsiChart.timeScale().fitContent();
|
|
314
|
+
}
|
|
315
|
+
if (needKdj && k) {
|
|
316
|
+
this.kdjK.setData(lineData(bars, k.k));
|
|
317
|
+
this.kdjD.setData(lineData(bars, k.d));
|
|
318
|
+
this.kdjJ.setData(lineData(bars, k.j));
|
|
319
|
+
this.kdjChart.timeScale().fitContent();
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
const from = mutation === "tail-update" ? Math.max(0, bars.length - 1) : Math.max(0, bars.length - this.warmupLookback());
|
|
323
|
+
if (needMacd && m) {
|
|
324
|
+
this.pushSeriesUpdates(this.macdLine, bars, m.macd, from);
|
|
325
|
+
this.pushSeriesUpdates(this.macdSignal, bars, m.signal, from);
|
|
326
|
+
this.pushHistUpdates(this.macdHist, bars, m.histogram, from);
|
|
327
|
+
}
|
|
328
|
+
if (needRsi && r) {
|
|
329
|
+
this.pushSeriesUpdates(this.rsiLine, bars, r, from);
|
|
330
|
+
}
|
|
331
|
+
if (needKdj && k) {
|
|
332
|
+
this.pushSeriesUpdates(this.kdjK, bars, k.k, from);
|
|
333
|
+
this.pushSeriesUpdates(this.kdjD, bars, k.d, from);
|
|
334
|
+
this.pushSeriesUpdates(this.kdjJ, bars, k.j, from);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
151
337
|
this.resize();
|
|
152
338
|
}
|
|
153
339
|
setTheme(theme) {
|
|
@@ -176,11 +362,14 @@ var IndicatorPaneStack = class {
|
|
|
176
362
|
this.kdjChart.timeScale().scrollToRealTime();
|
|
177
363
|
}
|
|
178
364
|
resize() {
|
|
179
|
-
|
|
180
|
-
{
|
|
181
|
-
{
|
|
182
|
-
{
|
|
183
|
-
]
|
|
365
|
+
const panes = [
|
|
366
|
+
{ show: this.config.showMacd, chart: this.macdChart },
|
|
367
|
+
{ show: this.config.showRsi, chart: this.rsiChart },
|
|
368
|
+
{ show: this.config.showKdj, chart: this.kdjChart }
|
|
369
|
+
];
|
|
370
|
+
for (const { show, chart } of panes) {
|
|
371
|
+
if (!show || !chart) continue;
|
|
372
|
+
const el = chart.chartElement()?.parentElement;
|
|
184
373
|
if (!el) continue;
|
|
185
374
|
const w = el.clientWidth;
|
|
186
375
|
const h = el.clientHeight;
|
|
@@ -188,20 +377,39 @@ var IndicatorPaneStack = class {
|
|
|
188
377
|
}
|
|
189
378
|
}
|
|
190
379
|
destroy() {
|
|
380
|
+
for (const detach of this.detachResizers) detach();
|
|
381
|
+
this.detachResizers.length = 0;
|
|
191
382
|
this.macdChart.remove();
|
|
192
383
|
this.rsiChart.remove();
|
|
193
384
|
this.kdjChart.remove();
|
|
194
385
|
this.root.replaceChildren();
|
|
195
386
|
}
|
|
196
|
-
createPaneWrap(label) {
|
|
387
|
+
createPaneWrap(label, paneId) {
|
|
197
388
|
const wrap = document.createElement("div");
|
|
389
|
+
wrap.className = `tv-indicator-pane tv-indicator-pane--${paneId}`;
|
|
390
|
+
wrap.dataset.paneId = paneId;
|
|
198
391
|
wrap.style.cssText = "flex:1;min-height:72px;width:100%;position:relative;border-top:1px solid #30363d;";
|
|
199
392
|
const tag = document.createElement("span");
|
|
200
393
|
tag.textContent = label;
|
|
201
394
|
tag.style.cssText = "position:absolute;left:6px;top:4px;z-index:2;font-size:10px;color:#8b949e;pointer-events:none;";
|
|
395
|
+
const closeBtn = document.createElement("button");
|
|
396
|
+
closeBtn.type = "button";
|
|
397
|
+
closeBtn.textContent = "\xD7";
|
|
398
|
+
closeBtn.title = `\u95DC\u9589 ${label}`;
|
|
399
|
+
closeBtn.setAttribute("aria-label", `Close ${label}`);
|
|
400
|
+
closeBtn.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;";
|
|
401
|
+
closeBtn.onmouseenter = () => {
|
|
402
|
+
closeBtn.style.color = "#e6edf3";
|
|
403
|
+
closeBtn.style.borderColor = "#484f58";
|
|
404
|
+
};
|
|
405
|
+
closeBtn.onmouseleave = () => {
|
|
406
|
+
closeBtn.style.color = "#8b949e";
|
|
407
|
+
closeBtn.style.borderColor = "#30363d";
|
|
408
|
+
};
|
|
409
|
+
closeBtn.onclick = () => this.closePane(paneId);
|
|
202
410
|
const el = document.createElement("div");
|
|
203
411
|
el.style.cssText = "width:100%;height:100%;";
|
|
204
|
-
wrap.append(tag, el);
|
|
412
|
+
wrap.append(tag, closeBtn, el);
|
|
205
413
|
return { wrap, el };
|
|
206
414
|
}
|
|
207
415
|
layoutForTheme(dark) {
|
|
@@ -210,6 +418,10 @@ var IndicatorPaneStack = class {
|
|
|
210
418
|
textColor: dark ? "#e6edf3" : "#24292f"
|
|
211
419
|
};
|
|
212
420
|
}
|
|
421
|
+
/** LWC instances for sync-group reassignment. */
|
|
422
|
+
getCharts() {
|
|
423
|
+
return [this.macdChart, this.rsiChart, this.kdjChart];
|
|
424
|
+
}
|
|
213
425
|
};
|
|
214
426
|
function maOverlayLine(bars, period = 20, source = "close") {
|
|
215
427
|
const src = barsForSource(bars, source);
|
|
@@ -233,53 +445,6 @@ function bollOverlayLines(bars, period, mult, source = "close") {
|
|
|
233
445
|
};
|
|
234
446
|
}
|
|
235
447
|
|
|
236
|
-
// src/pane-resize.ts
|
|
237
|
-
function attachPaneResizer(topPane, bottomPane, opts = {}) {
|
|
238
|
-
const minTop = opts.minTopPx ?? 120;
|
|
239
|
-
const minBottom = opts.minBottomPx ?? 60;
|
|
240
|
-
const parent = topPane.parentElement;
|
|
241
|
-
if (!parent) return () => {
|
|
242
|
-
};
|
|
243
|
-
const handle = document.createElement("div");
|
|
244
|
-
handle.style.cssText = "height:4px;cursor:row-resize;background:#30363d;flex-shrink:0;touch-action:none;";
|
|
245
|
-
bottomPane.insertAdjacentElement("beforebegin", handle);
|
|
246
|
-
const saved = opts.storageKey ? localStorage.getItem(opts.storageKey) : null;
|
|
247
|
-
if (saved) {
|
|
248
|
-
const ratio = Number(saved);
|
|
249
|
-
if (Number.isFinite(ratio) && ratio > 0 && ratio < 1) {
|
|
250
|
-
topPane.style.flex = `${ratio * 10}`;
|
|
251
|
-
bottomPane.style.flex = `${(1 - ratio) * 10}`;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
let dragging = false;
|
|
255
|
-
const onMove = (clientY) => {
|
|
256
|
-
const rect = parent.getBoundingClientRect();
|
|
257
|
-
const y = clientY - rect.top;
|
|
258
|
-
const ratio = Math.min(0.85, Math.max(0.15, y / rect.height));
|
|
259
|
-
const topPx = ratio * rect.height;
|
|
260
|
-
const bottomPx = rect.height - topPx - handle.offsetHeight;
|
|
261
|
-
if (topPx < minTop || bottomPx < minBottom) return;
|
|
262
|
-
topPane.style.flex = `${ratio * 10}`;
|
|
263
|
-
bottomPane.style.flex = `${(1 - ratio) * 10}`;
|
|
264
|
-
if (opts.storageKey) localStorage.setItem(opts.storageKey, String(ratio));
|
|
265
|
-
};
|
|
266
|
-
const stop = () => {
|
|
267
|
-
dragging = false;
|
|
268
|
-
document.body.style.cursor = "";
|
|
269
|
-
};
|
|
270
|
-
handle.addEventListener("pointerdown", (e) => {
|
|
271
|
-
dragging = true;
|
|
272
|
-
handle.setPointerCapture(e.pointerId);
|
|
273
|
-
document.body.style.cursor = "row-resize";
|
|
274
|
-
});
|
|
275
|
-
handle.addEventListener("pointermove", (e) => {
|
|
276
|
-
if (dragging) onMove(e.clientY);
|
|
277
|
-
});
|
|
278
|
-
handle.addEventListener("pointerup", stop);
|
|
279
|
-
handle.addEventListener("pointercancel", stop);
|
|
280
|
-
return () => handle.remove();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
448
|
// src/time-scale-bus.ts
|
|
284
449
|
var TimeScaleBus = class {
|
|
285
450
|
charts = [];
|
|
@@ -291,6 +456,7 @@ var TimeScaleBus = class {
|
|
|
291
456
|
if (this.charts.includes(chart)) return;
|
|
292
457
|
this.charts.push(chart);
|
|
293
458
|
chart.timeScale().subscribeVisibleLogicalRangeChange((range) => {
|
|
459
|
+
if (!this.charts.includes(chart)) return;
|
|
294
460
|
if (this.syncing || !range) return;
|
|
295
461
|
const tr = chart.timeScale().getVisibleRange();
|
|
296
462
|
if (tr && typeof tr.from === "number" && typeof tr.to === "number") {
|
|
@@ -300,6 +466,10 @@ var TimeScaleBus = class {
|
|
|
300
466
|
this.syncFrom(chart, range);
|
|
301
467
|
});
|
|
302
468
|
}
|
|
469
|
+
unregister(chart) {
|
|
470
|
+
const i = this.charts.indexOf(chart);
|
|
471
|
+
if (i >= 0) this.charts.splice(i, 1);
|
|
472
|
+
}
|
|
303
473
|
subscribeTransform(listener) {
|
|
304
474
|
this.listeners.add(listener);
|
|
305
475
|
return () => this.listeners.delete(listener);
|
|
@@ -368,6 +538,101 @@ var TimeScaleBus = class {
|
|
|
368
538
|
}
|
|
369
539
|
};
|
|
370
540
|
|
|
541
|
+
// src/time-scale-bus-registry.ts
|
|
542
|
+
function normalizeSyncGroupId(id) {
|
|
543
|
+
if (id == null) return null;
|
|
544
|
+
const trimmed = String(id).trim();
|
|
545
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
546
|
+
}
|
|
547
|
+
var INDEP_PREFIX = "@independent:";
|
|
548
|
+
function independentBusKey(pane) {
|
|
549
|
+
return `${INDEP_PREFIX}${pane}`;
|
|
550
|
+
}
|
|
551
|
+
function resolveBusMapKey(groupId, pane) {
|
|
552
|
+
const norm = normalizeSyncGroupId(groupId);
|
|
553
|
+
return norm ?? independentBusKey(pane);
|
|
554
|
+
}
|
|
555
|
+
var TimeScaleBusRegistry = class {
|
|
556
|
+
buses = /* @__PURE__ */ new Map();
|
|
557
|
+
paneKeys = /* @__PURE__ */ new Map();
|
|
558
|
+
activeKey;
|
|
559
|
+
constructor() {
|
|
560
|
+
for (const pane of ["main", "volume", "indicator"]) {
|
|
561
|
+
const key = independentBusKey(pane);
|
|
562
|
+
this.paneKeys.set(pane, key);
|
|
563
|
+
this.getOrCreateBus(key);
|
|
564
|
+
}
|
|
565
|
+
this.activeKey = this.paneKeys.get("main");
|
|
566
|
+
}
|
|
567
|
+
getOrCreateBus(key) {
|
|
568
|
+
let bus = this.buses.get(key);
|
|
569
|
+
if (!bus) {
|
|
570
|
+
bus = new TimeScaleBus();
|
|
571
|
+
this.buses.set(key, bus);
|
|
572
|
+
}
|
|
573
|
+
return bus;
|
|
574
|
+
}
|
|
575
|
+
getBusKeyForPane(pane) {
|
|
576
|
+
return this.paneKeys.get(pane);
|
|
577
|
+
}
|
|
578
|
+
getBusForPane(pane) {
|
|
579
|
+
return this.getOrCreateBus(this.getBusKeyForPane(pane));
|
|
580
|
+
}
|
|
581
|
+
/** Active bus for IChart viewport APIs (follows last-focused chart pane). */
|
|
582
|
+
get activeBus() {
|
|
583
|
+
return this.getOrCreateBus(this.activeKey);
|
|
584
|
+
}
|
|
585
|
+
getActiveBusKey() {
|
|
586
|
+
return this.activeKey;
|
|
587
|
+
}
|
|
588
|
+
setActivePane(pane) {
|
|
589
|
+
this.activeKey = this.paneKeys.get(pane);
|
|
590
|
+
}
|
|
591
|
+
setPaneSyncGroup(pane, groupId) {
|
|
592
|
+
const nextKey = resolveBusMapKey(groupId, pane);
|
|
593
|
+
const prevKey = this.paneKeys.get(pane);
|
|
594
|
+
this.paneKeys.set(pane, nextKey);
|
|
595
|
+
this.getOrCreateBus(nextKey);
|
|
596
|
+
if (this.activeKey === prevKey) this.activeKey = nextKey;
|
|
597
|
+
return prevKey;
|
|
598
|
+
}
|
|
599
|
+
forEachBus(fn) {
|
|
600
|
+
for (const [key, bus] of this.buses) fn(key, bus);
|
|
601
|
+
}
|
|
602
|
+
moveChart(chart, fromKey, toKey, copyRange = true) {
|
|
603
|
+
if (fromKey === toKey) return;
|
|
604
|
+
const from = this.getOrCreateBus(fromKey);
|
|
605
|
+
const to = this.getOrCreateBus(toKey);
|
|
606
|
+
const range = copyRange ? from.getVisibleRange() : null;
|
|
607
|
+
from.unregister(chart);
|
|
608
|
+
to.register(chart);
|
|
609
|
+
if (range) to.setVisibleTimeRange(range);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/viewport-fit.ts
|
|
614
|
+
import { intervalMs } from "@coderyo/data";
|
|
615
|
+
function defaultBarSpacingForInterval(interval) {
|
|
616
|
+
const ms = intervalMs(interval);
|
|
617
|
+
if (ms <= 1e3) return 3;
|
|
618
|
+
if (ms <= 5e3) return 4;
|
|
619
|
+
if (ms <= 15e3) return 5;
|
|
620
|
+
if (ms <= 3e4) return 6;
|
|
621
|
+
if (ms <= 6e4) return 7;
|
|
622
|
+
if (ms <= 3e5) return 8;
|
|
623
|
+
if (ms <= 9e5) return 9;
|
|
624
|
+
if (ms <= 36e5) return 10;
|
|
625
|
+
if (ms <= 144e5) return 11;
|
|
626
|
+
return 12;
|
|
627
|
+
}
|
|
628
|
+
function resolveBarSpacingForInterval(interval, overrides) {
|
|
629
|
+
const custom = overrides?.[interval];
|
|
630
|
+
if (custom != null && Number.isFinite(custom) && custom > 0) {
|
|
631
|
+
return Math.min(24, Math.max(2, custom));
|
|
632
|
+
}
|
|
633
|
+
return defaultBarSpacingForInterval(interval);
|
|
634
|
+
}
|
|
635
|
+
|
|
371
636
|
// src/bar-smooth-animator.ts
|
|
372
637
|
function lerp(a, b, t) {
|
|
373
638
|
return a + (b - a) * t;
|
|
@@ -425,6 +690,13 @@ var BarSmoothAnimator = class {
|
|
|
425
690
|
};
|
|
426
691
|
|
|
427
692
|
// src/pane-orchestrator.ts
|
|
693
|
+
function isLayeredPaneMount(opts) {
|
|
694
|
+
return !!opts.volumeMount;
|
|
695
|
+
}
|
|
696
|
+
function shouldResizeChartPane(focus, pane) {
|
|
697
|
+
if (!focus) return true;
|
|
698
|
+
return focus.has(pane);
|
|
699
|
+
}
|
|
428
700
|
function toUtcSeconds2(tMs) {
|
|
429
701
|
return Math.floor(tMs / 1e3);
|
|
430
702
|
}
|
|
@@ -435,7 +707,18 @@ function barToVolume(b) {
|
|
|
435
707
|
return { time: toUtcSeconds2(b.t), value: b.v ?? 0 };
|
|
436
708
|
}
|
|
437
709
|
var PaneOrchestrator = class {
|
|
438
|
-
|
|
710
|
+
busRegistry = new TimeScaleBusRegistry();
|
|
711
|
+
/** Active sync group bus (last-focused pane); used by ChartController viewport APIs. */
|
|
712
|
+
get bus() {
|
|
713
|
+
return this.busRegistry.activeBus;
|
|
714
|
+
}
|
|
715
|
+
layeredPanes;
|
|
716
|
+
mainEl;
|
|
717
|
+
volWrap;
|
|
718
|
+
detachMainVolResizer = () => {
|
|
719
|
+
};
|
|
720
|
+
resizeFocusPanes = null;
|
|
721
|
+
listenPaneResizeEvents;
|
|
439
722
|
mainChart;
|
|
440
723
|
volumeChart;
|
|
441
724
|
mainSeries;
|
|
@@ -458,24 +741,70 @@ var PaneOrchestrator = class {
|
|
|
458
741
|
barTimesOrdered = [];
|
|
459
742
|
didInitialFit = false;
|
|
460
743
|
skipNextInitialFit = false;
|
|
744
|
+
/** setBars ran before the pane had layout size; refit on first real resize. */
|
|
745
|
+
pendingViewportFit = false;
|
|
746
|
+
pendingViewportBars = null;
|
|
461
747
|
indicatorConfig = null;
|
|
748
|
+
onIndicatorConfigChange;
|
|
749
|
+
currentInterval = "1h";
|
|
750
|
+
autoBarSpacingOnInterval = true;
|
|
751
|
+
barSpacingByInterval;
|
|
462
752
|
priceLine = null;
|
|
463
753
|
barAnimator = null;
|
|
464
754
|
smoothPriceDurationMs = 150;
|
|
465
755
|
constructor(opts) {
|
|
466
756
|
this.maxRenderPoints = opts.maxRenderPoints ?? 4e3;
|
|
757
|
+
this.listenPaneResizeEvents = opts.listenPaneResizeEvents !== false;
|
|
467
758
|
this.dark = opts.theme !== "light";
|
|
468
759
|
this.showGrid = opts.showGrid ?? false;
|
|
469
760
|
const layout = this.layoutForTheme(this.dark);
|
|
470
761
|
const grid = gridOptions(this.showGrid, this.dark);
|
|
471
|
-
|
|
472
|
-
mainEl.style.cssText = "flex:7;min-height:120px;width:100%;position:relative;";
|
|
762
|
+
this.layeredPanes = !!opts.volumeMount;
|
|
473
763
|
const volEl = document.createElement("div");
|
|
474
|
-
volEl.style.cssText = "
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
764
|
+
volEl.style.cssText = "width:100%;height:100%;min-height:0;position:relative;";
|
|
765
|
+
if (this.layeredPanes) {
|
|
766
|
+
this.mainEl = opts.container;
|
|
767
|
+
this.mainEl.style.cssText = "width:100%;height:100%;min-height:80px;position:relative;overflow:hidden;";
|
|
768
|
+
this.volWrap = opts.volumeMount;
|
|
769
|
+
this.volWrap.dataset.paneId = "volume";
|
|
770
|
+
this.volWrap.className = "tv-volume-pane tv-volume-pane--layered";
|
|
771
|
+
this.volWrap.replaceChildren();
|
|
772
|
+
this.volWrap.style.cssText = "width:100%;height:100%;min-height:48px;position:relative;overflow:hidden;box-sizing:border-box;";
|
|
773
|
+
const volTag = document.createElement("span");
|
|
774
|
+
volTag.textContent = "Volume";
|
|
775
|
+
volTag.style.cssText = "position:absolute;left:6px;top:4px;z-index:2;font-size:10px;color:#8b949e;pointer-events:none;";
|
|
776
|
+
const volClose = document.createElement("button");
|
|
777
|
+
volClose.type = "button";
|
|
778
|
+
volClose.textContent = "\xD7";
|
|
779
|
+
volClose.title = "\u95DC\u9589\u6210\u4EA4\u91CF";
|
|
780
|
+
volClose.setAttribute("aria-label", "Close volume");
|
|
781
|
+
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;";
|
|
782
|
+
volClose.onclick = () => this.closeVolumePane();
|
|
783
|
+
this.volWrap.append(volTag, volClose, volEl);
|
|
784
|
+
} else {
|
|
785
|
+
this.mainEl = document.createElement("div");
|
|
786
|
+
this.mainEl.style.cssText = "flex:7;min-height:120px;width:100%;position:relative;";
|
|
787
|
+
volEl.style.cssText = "flex:1;min-height:0;width:100%;height:100%;position:relative;";
|
|
788
|
+
this.volWrap = document.createElement("div");
|
|
789
|
+
this.volWrap.dataset.paneId = "volume";
|
|
790
|
+
this.volWrap.className = "tv-volume-pane";
|
|
791
|
+
this.volWrap.style.cssText = "flex:2;min-height:48px;width:100%;position:relative;display:flex;flex-direction:column;border-top:1px solid #30363d;";
|
|
792
|
+
const volTag = document.createElement("span");
|
|
793
|
+
volTag.textContent = "Volume";
|
|
794
|
+
volTag.style.cssText = "position:absolute;left:6px;top:4px;z-index:2;font-size:10px;color:#8b949e;pointer-events:none;";
|
|
795
|
+
const volClose = document.createElement("button");
|
|
796
|
+
volClose.type = "button";
|
|
797
|
+
volClose.textContent = "\xD7";
|
|
798
|
+
volClose.title = "\u95DC\u9589\u6210\u4EA4\u91CF";
|
|
799
|
+
volClose.setAttribute("aria-label", "Close volume");
|
|
800
|
+
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;";
|
|
801
|
+
volClose.onclick = () => this.closeVolumePane();
|
|
802
|
+
this.volWrap.append(volTag, volClose, volEl);
|
|
803
|
+
opts.container.style.cssText = "display:flex;flex-direction:column;height:100%;width:100%;min-height:200px;overflow:hidden;";
|
|
804
|
+
opts.container.append(this.mainEl, this.volWrap);
|
|
805
|
+
this.rebuildMainVolumeResizer();
|
|
806
|
+
}
|
|
807
|
+
this.mainChart = createChart2(this.mainEl, { layout, grid, autoSize: true });
|
|
479
808
|
this.volumeChart = createChart2(volEl, {
|
|
480
809
|
layout,
|
|
481
810
|
grid,
|
|
@@ -531,15 +860,25 @@ var PaneOrchestrator = class {
|
|
|
531
860
|
lineWidth: 1,
|
|
532
861
|
title: "VolMA5"
|
|
533
862
|
});
|
|
534
|
-
this.
|
|
535
|
-
this.
|
|
863
|
+
this.busRegistry.getBusForPane("main").register(this.mainChart);
|
|
864
|
+
this.busRegistry.getBusForPane("volume").register(this.volumeChart);
|
|
536
865
|
this.indicatorRoot = opts.indicatorRoot;
|
|
537
866
|
this.indicatorConfig = opts.indicatorConfig ?? null;
|
|
867
|
+
this.onIndicatorConfigChange = opts.onIndicatorConfigChange;
|
|
868
|
+
this.autoBarSpacingOnInterval = opts.autoBarSpacingOnInterval ?? true;
|
|
869
|
+
this.barSpacingByInterval = opts.barSpacingByInterval;
|
|
538
870
|
this.pinePlots = opts.pinePlots ?? null;
|
|
539
871
|
this.indicators = this.createIndicatorStack();
|
|
540
|
-
this.initOverlay(mainEl);
|
|
872
|
+
this.initOverlay(this.mainEl);
|
|
541
873
|
this.setSmoothPriceUpdate(opts.smoothPriceUpdate ?? false, opts.smoothPriceDurationMs);
|
|
874
|
+
this.applyVolumeVisibility();
|
|
875
|
+
if (opts.listenPaneResizeEvents !== false) {
|
|
876
|
+
window.addEventListener("tradview:pane-resize", this.onPaneResize);
|
|
877
|
+
}
|
|
542
878
|
}
|
|
879
|
+
onPaneResize = () => {
|
|
880
|
+
this.resize();
|
|
881
|
+
};
|
|
543
882
|
setSmoothPriceUpdate(enabled, durationMs = 150) {
|
|
544
883
|
this.smoothPriceDurationMs = durationMs;
|
|
545
884
|
if (enabled) {
|
|
@@ -573,12 +912,16 @@ var PaneOrchestrator = class {
|
|
|
573
912
|
applyLastBarToSeries(bar) {
|
|
574
913
|
this.barByTime.set(bar.t, bar);
|
|
575
914
|
this.mainSeries.update(barToCandle(bar));
|
|
576
|
-
this.
|
|
915
|
+
if (this.isVolumeVisible()) {
|
|
916
|
+
this.volumeSeries.update(barToVolume(bar));
|
|
917
|
+
}
|
|
577
918
|
this.ensurePriceLine(bar.c);
|
|
578
919
|
if (this.indicatorConfig) {
|
|
579
920
|
const bars = [...this.barByTime.values()].sort((a, b) => a.t - b.t);
|
|
580
921
|
this.applyMainOverlays(bars);
|
|
581
|
-
this.
|
|
922
|
+
if (hasVisibleIndicatorPanes2(this.indicatorConfig)) {
|
|
923
|
+
this.indicators?.setBars(bars);
|
|
924
|
+
}
|
|
582
925
|
}
|
|
583
926
|
}
|
|
584
927
|
ensurePriceLine(price) {
|
|
@@ -595,6 +938,9 @@ var PaneOrchestrator = class {
|
|
|
595
938
|
this.priceLine.applyOptions({ price });
|
|
596
939
|
}
|
|
597
940
|
}
|
|
941
|
+
setIntervalContext(interval) {
|
|
942
|
+
this.currentInterval = interval;
|
|
943
|
+
}
|
|
598
944
|
setTheme(theme) {
|
|
599
945
|
this.dark = theme === "dark";
|
|
600
946
|
const layout = this.layoutForTheme(this.dark);
|
|
@@ -606,17 +952,19 @@ var PaneOrchestrator = class {
|
|
|
606
952
|
setIndicatorConfig(config) {
|
|
607
953
|
this.indicatorConfig = config;
|
|
608
954
|
if (!config) {
|
|
609
|
-
this.
|
|
955
|
+
this.teardownIndicatorStack();
|
|
610
956
|
this.maSeries.setData([]);
|
|
611
957
|
this.emaSeries.setData([]);
|
|
612
958
|
this.bollUpper.setData([]);
|
|
613
959
|
this.bollMiddle.setData([]);
|
|
614
960
|
this.bollLower.setData([]);
|
|
615
961
|
this.volMaSeries.setData([]);
|
|
962
|
+
this.volumeSeries.setData([]);
|
|
616
963
|
this.emaSeries.applyOptions({ visible: false });
|
|
617
964
|
this.bollUpper.applyOptions({ visible: false });
|
|
618
965
|
this.bollMiddle.applyOptions({ visible: false });
|
|
619
966
|
this.bollLower.applyOptions({ visible: false });
|
|
967
|
+
this.applyVolumeVisibility();
|
|
620
968
|
return;
|
|
621
969
|
}
|
|
622
970
|
if (!this.indicators) this.indicators = this.createIndicatorStack();
|
|
@@ -624,20 +972,112 @@ var PaneOrchestrator = class {
|
|
|
624
972
|
this.indicators?.setConfig(config);
|
|
625
973
|
if (bars.length > 0) {
|
|
626
974
|
this.applyMainOverlays(bars);
|
|
627
|
-
|
|
975
|
+
if (hasVisibleIndicatorPanes2(config)) {
|
|
976
|
+
this.indicators?.setBars(bars);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
this.applyVolumeVisibility();
|
|
980
|
+
}
|
|
981
|
+
isVolumeVisible() {
|
|
982
|
+
return this.indicatorConfig?.showVolume ?? true;
|
|
983
|
+
}
|
|
984
|
+
closeVolumePane() {
|
|
985
|
+
if (!this.indicatorConfig) {
|
|
986
|
+
this.indicatorConfig = { ...DEFAULT_INDICATOR_CONFIG2, showVolume: false };
|
|
987
|
+
} else {
|
|
988
|
+
this.indicatorConfig = { ...this.indicatorConfig, showVolume: false };
|
|
989
|
+
}
|
|
990
|
+
this.applyVolumeVisibility();
|
|
991
|
+
this.onIndicatorConfigChange?.(this.indicatorConfig);
|
|
992
|
+
}
|
|
993
|
+
applyVolumeVisibility() {
|
|
994
|
+
const show = this.isVolumeVisible();
|
|
995
|
+
this.volWrap.style.display = show ? "" : "none";
|
|
996
|
+
this.rebuildMainVolumeResizer();
|
|
997
|
+
if (!show) {
|
|
998
|
+
this.volumeSeries.setData([]);
|
|
999
|
+
this.volMaSeries.setData([]);
|
|
628
1000
|
}
|
|
1001
|
+
this.syncChartSize();
|
|
1002
|
+
}
|
|
1003
|
+
/** Assign per-pane sync group ids (`''` / omit = independent). Re-registers LWC charts on group change. */
|
|
1004
|
+
setPaneSyncGroups(patch) {
|
|
1005
|
+
const apply = (pane, groupId) => {
|
|
1006
|
+
const prevKey = this.busRegistry.setPaneSyncGroup(pane, groupId);
|
|
1007
|
+
const nextKey = this.busRegistry.getBusKeyForPane(pane);
|
|
1008
|
+
if (pane === "main") {
|
|
1009
|
+
this.busRegistry.moveChart(this.mainChart, prevKey, nextKey);
|
|
1010
|
+
} else if (pane === "volume") {
|
|
1011
|
+
this.busRegistry.moveChart(this.volumeChart, prevKey, nextKey);
|
|
1012
|
+
} else if (this.indicators) {
|
|
1013
|
+
for (const chart of this.indicators.getCharts()) {
|
|
1014
|
+
this.busRegistry.moveChart(chart, prevKey, nextKey);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
if (patch.main !== void 0) apply("main", patch.main);
|
|
1019
|
+
if (patch.volume !== void 0) apply("volume", patch.volume);
|
|
1020
|
+
if (patch.indicator !== void 0) apply("indicator", patch.indicator);
|
|
1021
|
+
}
|
|
1022
|
+
setActiveSyncPane(pane) {
|
|
1023
|
+
const key = pane === "volume" ? "volume" : pane === "indicator" ? "indicator" : "main";
|
|
1024
|
+
this.busRegistry.setActivePane(key);
|
|
1025
|
+
}
|
|
1026
|
+
/** P2: when set, only these panes get LWC resize (panes in the same sync group still share TimeScaleBus). */
|
|
1027
|
+
setResizeFocusPanes(panes) {
|
|
1028
|
+
this.resizeFocusPanes = panes?.length ? new Set(panes) : null;
|
|
1029
|
+
this.resize();
|
|
1030
|
+
}
|
|
1031
|
+
/** Current resize focus (null = all panes). @internal — not part of public package API. */
|
|
1032
|
+
getResizeFocusPanes() {
|
|
1033
|
+
return this.resizeFocusPanes ? [...this.resizeFocusPanes] : null;
|
|
1034
|
+
}
|
|
1035
|
+
shouldResizePane(pane) {
|
|
1036
|
+
return shouldResizeChartPane(this.resizeFocusPanes, pane);
|
|
1037
|
+
}
|
|
1038
|
+
rebuildMainVolumeResizer() {
|
|
1039
|
+
if (this.layeredPanes) return;
|
|
1040
|
+
this.detachMainVolResizer();
|
|
1041
|
+
const parent = this.mainEl.parentElement;
|
|
1042
|
+
parent?.querySelectorAll(":scope > [data-pane-resizer]").forEach((el) => el.remove());
|
|
1043
|
+
if (!this.isVolumeVisible()) {
|
|
1044
|
+
this.mainEl.style.flex = "1";
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
this.mainEl.style.flex = "";
|
|
1048
|
+
this.volWrap.style.flex = "";
|
|
1049
|
+
this.detachMainVolResizer = attachPaneResizer(this.mainEl, this.volWrap, {
|
|
1050
|
+
storageKey: "tradview:pane:main-volume",
|
|
1051
|
+
minTopPx: 120,
|
|
1052
|
+
minBottomPx: 48
|
|
1053
|
+
});
|
|
629
1054
|
}
|
|
630
1055
|
setPinePlots(plots) {
|
|
631
1056
|
this.pinePlots = plots;
|
|
632
1057
|
const bars = [...this.barByTime.values()].sort((a, b) => a.t - b.t);
|
|
633
1058
|
this.syncPinePlotSeries(bars);
|
|
634
1059
|
}
|
|
1060
|
+
teardownIndicatorStack() {
|
|
1061
|
+
this.indicators?.destroy();
|
|
1062
|
+
this.indicators = null;
|
|
1063
|
+
}
|
|
635
1064
|
applyMainOverlays(bars) {
|
|
636
1065
|
const cfg = this.indicatorConfig;
|
|
637
1066
|
if (!cfg) return;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1067
|
+
if (cfg.showMa) {
|
|
1068
|
+
this.maSeries.applyOptions({ visible: true, title: `MA${cfg.maPeriod}` });
|
|
1069
|
+
this.maSeries.setData(maOverlayLine(bars, cfg.maPeriod, cfg.source));
|
|
1070
|
+
} else {
|
|
1071
|
+
this.maSeries.applyOptions({ visible: false });
|
|
1072
|
+
this.maSeries.setData([]);
|
|
1073
|
+
}
|
|
1074
|
+
if (cfg.showVolMa) {
|
|
1075
|
+
this.volMaSeries.applyOptions({ visible: true });
|
|
1076
|
+
this.volMaSeries.setData(volMaOverlayLine(bars, cfg.volMaPeriod));
|
|
1077
|
+
} else {
|
|
1078
|
+
this.volMaSeries.applyOptions({ visible: false });
|
|
1079
|
+
this.volMaSeries.setData([]);
|
|
1080
|
+
}
|
|
641
1081
|
if (cfg.showEma) {
|
|
642
1082
|
this.emaSeries.applyOptions({ visible: true, title: `EMA${cfg.emaPeriod}` });
|
|
643
1083
|
this.emaSeries.setData(emaOverlayLine(bars, cfg.emaPeriod, cfg.source));
|
|
@@ -712,10 +1152,16 @@ var PaneOrchestrator = class {
|
|
|
712
1152
|
vols.push(barToVolume(b));
|
|
713
1153
|
}
|
|
714
1154
|
this.mainSeries.setData(candles);
|
|
715
|
-
this.
|
|
1155
|
+
if (this.isVolumeVisible()) {
|
|
1156
|
+
this.volumeSeries.setData(vols);
|
|
1157
|
+
} else {
|
|
1158
|
+
this.volumeSeries.setData([]);
|
|
1159
|
+
}
|
|
716
1160
|
if (this.indicatorConfig) {
|
|
717
1161
|
this.applyMainOverlays(renderBars);
|
|
718
|
-
this.
|
|
1162
|
+
if (hasVisibleIndicatorPanes2(this.indicatorConfig)) {
|
|
1163
|
+
this.indicators?.setBars(renderBars);
|
|
1164
|
+
}
|
|
719
1165
|
} else {
|
|
720
1166
|
this.maSeries.setData([]);
|
|
721
1167
|
this.emaSeries.setData([]);
|
|
@@ -727,15 +1173,35 @@ var PaneOrchestrator = class {
|
|
|
727
1173
|
}
|
|
728
1174
|
if (renderBars.length > 0) {
|
|
729
1175
|
this.syncChartSize();
|
|
730
|
-
|
|
731
|
-
this.mainChart.timeScale().fitContent();
|
|
732
|
-
this.volumeChart.timeScale().fitContent();
|
|
733
|
-
this.indicators?.fitContent();
|
|
734
|
-
this.didInitialFit = true;
|
|
735
|
-
}
|
|
1176
|
+
this.tryInitialViewportFit(renderBars);
|
|
736
1177
|
this.skipNextInitialFit = false;
|
|
737
1178
|
}
|
|
738
1179
|
}
|
|
1180
|
+
mainPaneHasSize() {
|
|
1181
|
+
const el = this.mainChart.chartElement().parentElement;
|
|
1182
|
+
return (el?.clientWidth ?? 0) > 0 && (el?.clientHeight ?? 0) > 0;
|
|
1183
|
+
}
|
|
1184
|
+
tryInitialViewportFit(renderBars) {
|
|
1185
|
+
if (this.didInitialFit || this.skipNextInitialFit) return;
|
|
1186
|
+
if (!this.mainPaneHasSize()) {
|
|
1187
|
+
this.pendingViewportFit = true;
|
|
1188
|
+
this.pendingViewportBars = renderBars;
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
this.applyViewAfterDataReload(renderBars);
|
|
1192
|
+
this.didInitialFit = true;
|
|
1193
|
+
this.pendingViewportFit = false;
|
|
1194
|
+
this.pendingViewportBars = null;
|
|
1195
|
+
}
|
|
1196
|
+
flushPendingViewportFit() {
|
|
1197
|
+
if (!this.pendingViewportFit || this.skipNextInitialFit || this.didInitialFit) return;
|
|
1198
|
+
const bars = this.pendingViewportBars ?? [...this.barByTime.values()].sort((a, b) => a.t - b.t);
|
|
1199
|
+
if (bars.length === 0 || !this.mainPaneHasSize()) return;
|
|
1200
|
+
this.applyViewAfterDataReload(bars);
|
|
1201
|
+
this.didInitialFit = true;
|
|
1202
|
+
this.pendingViewportFit = false;
|
|
1203
|
+
this.pendingViewportBars = null;
|
|
1204
|
+
}
|
|
739
1205
|
subscribeCrosshair(listener) {
|
|
740
1206
|
const handler = (param) => {
|
|
741
1207
|
if (param.time == null || !param.point) {
|
|
@@ -772,17 +1238,56 @@ var PaneOrchestrator = class {
|
|
|
772
1238
|
}
|
|
773
1239
|
createIndicatorStack() {
|
|
774
1240
|
if (!this.indicatorRoot || !this.indicatorConfig) return null;
|
|
775
|
-
return new IndicatorPaneStack(this.indicatorRoot, this.
|
|
1241
|
+
return new IndicatorPaneStack(this.indicatorRoot, this.busRegistry.getBusForPane("indicator"), {
|
|
776
1242
|
theme: this.dark ? "dark" : "light",
|
|
777
1243
|
showGrid: this.showGrid,
|
|
778
|
-
config: this.indicatorConfig
|
|
1244
|
+
config: this.indicatorConfig,
|
|
1245
|
+
onConfigChange: (config) => {
|
|
1246
|
+
this.indicatorConfig = config;
|
|
1247
|
+
this.onIndicatorConfigChange?.(config);
|
|
1248
|
+
}
|
|
779
1249
|
});
|
|
780
1250
|
}
|
|
1251
|
+
/** After symbol/interval reload: bar spacing for interval + show integrator-loaded span. */
|
|
1252
|
+
applyIntervalBarSpacing(interval) {
|
|
1253
|
+
if (!this.autoBarSpacingOnInterval) return;
|
|
1254
|
+
const iv = interval ?? this.currentInterval;
|
|
1255
|
+
const spacing = resolveBarSpacingForInterval(iv, this.barSpacingByInterval);
|
|
1256
|
+
this.busRegistry.forEachBus((_, bus) => bus.setBarSpacing(spacing));
|
|
1257
|
+
}
|
|
1258
|
+
setBarSpacingPolicy(opts) {
|
|
1259
|
+
if (opts.autoBarSpacingOnInterval !== void 0) {
|
|
1260
|
+
this.autoBarSpacingOnInterval = opts.autoBarSpacingOnInterval;
|
|
1261
|
+
}
|
|
1262
|
+
if (opts.barSpacingByInterval !== void 0) {
|
|
1263
|
+
this.barSpacingByInterval = opts.barSpacingByInterval;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/** Sync time scale to loaded bars (integrator history); adjust bar spacing only, not bar count. */
|
|
1267
|
+
applyViewAfterDataReload(renderBars) {
|
|
1268
|
+
if (this.autoBarSpacingOnInterval) {
|
|
1269
|
+
this.applyIntervalBarSpacing();
|
|
1270
|
+
} else {
|
|
1271
|
+
this.mainChart.timeScale().fitContent();
|
|
1272
|
+
if (this.isVolumeVisible()) this.volumeChart.timeScale().fitContent();
|
|
1273
|
+
this.indicators?.fitContent();
|
|
1274
|
+
}
|
|
1275
|
+
if (renderBars.length > 0) {
|
|
1276
|
+
const fromMs = renderBars[0].t;
|
|
1277
|
+
const toMs = renderBars[renderBars.length - 1].t + intervalMs2(this.currentInterval);
|
|
1278
|
+
this.bus.setVisibleTimeRange({ fromMs, toMs });
|
|
1279
|
+
}
|
|
1280
|
+
this.scrollToRealtime();
|
|
1281
|
+
}
|
|
781
1282
|
resetViewState() {
|
|
782
1283
|
this.didInitialFit = false;
|
|
783
1284
|
this.skipNextInitialFit = false;
|
|
784
|
-
this.
|
|
785
|
-
this.
|
|
1285
|
+
this.pendingViewportFit = false;
|
|
1286
|
+
this.pendingViewportBars = null;
|
|
1287
|
+
this.busRegistry.forEachBus((_, bus) => {
|
|
1288
|
+
bus.visibleFromMs = 0;
|
|
1289
|
+
bus.visibleToMs = 0;
|
|
1290
|
+
});
|
|
786
1291
|
}
|
|
787
1292
|
/** Skip the next automatic fitContent after setBars (used by reloadHistory). */
|
|
788
1293
|
preserveViewportOnNextSetBars() {
|
|
@@ -810,10 +1315,10 @@ var PaneOrchestrator = class {
|
|
|
810
1315
|
if (nearest == null) return;
|
|
811
1316
|
const i = this.barTimesOrdered.indexOf(nearest.t);
|
|
812
1317
|
if (i < 0) return;
|
|
813
|
-
this.
|
|
1318
|
+
this.busRegistry.getBusForPane("main").scrollToLogicalPosition(i, (animationMs ?? 0) > 0);
|
|
814
1319
|
return;
|
|
815
1320
|
}
|
|
816
|
-
this.
|
|
1321
|
+
this.busRegistry.getBusForPane("main").scrollToLogicalPosition(idx, (animationMs ?? 0) > 0);
|
|
817
1322
|
}
|
|
818
1323
|
/** Clear series while symbol/interval data reloads (avoids overlapping candles). */
|
|
819
1324
|
clearBars() {
|
|
@@ -828,13 +1333,13 @@ var PaneOrchestrator = class {
|
|
|
828
1333
|
}
|
|
829
1334
|
fitContent() {
|
|
830
1335
|
this.mainChart.timeScale().fitContent();
|
|
831
|
-
this.volumeChart.timeScale().fitContent();
|
|
1336
|
+
if (this.isVolumeVisible()) this.volumeChart.timeScale().fitContent();
|
|
832
1337
|
this.indicators?.fitContent();
|
|
833
1338
|
this.didInitialFit = true;
|
|
834
1339
|
}
|
|
835
1340
|
scrollToRealtime() {
|
|
836
1341
|
this.mainChart.timeScale().scrollToRealTime();
|
|
837
|
-
this.volumeChart.timeScale().scrollToRealTime();
|
|
1342
|
+
if (this.isVolumeVisible()) this.volumeChart.timeScale().scrollToRealTime();
|
|
838
1343
|
this.indicators?.scrollToRealtime();
|
|
839
1344
|
}
|
|
840
1345
|
setLogScale(enabled) {
|
|
@@ -843,7 +1348,18 @@ var PaneOrchestrator = class {
|
|
|
843
1348
|
resize() {
|
|
844
1349
|
this.syncChartSize();
|
|
845
1350
|
this.syncOverlaySize();
|
|
1351
|
+
if (this.shouldResizePane("indicator")) this.indicators?.resize();
|
|
1352
|
+
this.flushPendingViewportFit();
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Resize every LWC pane once for viewport fit; does **not** change {@link resizeFocusPanes}.
|
|
1356
|
+
* @internal Used by ChartController data refresh paths.
|
|
1357
|
+
*/
|
|
1358
|
+
resizeAllPanes() {
|
|
1359
|
+
this.syncChartSize({ allPanes: true });
|
|
1360
|
+
this.syncOverlaySize();
|
|
846
1361
|
this.indicators?.resize();
|
|
1362
|
+
this.flushPendingViewportFit();
|
|
847
1363
|
}
|
|
848
1364
|
getOverlayCanvas() {
|
|
849
1365
|
return this.overlayCanvas;
|
|
@@ -868,6 +1384,10 @@ var PaneOrchestrator = class {
|
|
|
868
1384
|
return p ?? null;
|
|
869
1385
|
}
|
|
870
1386
|
destroy() {
|
|
1387
|
+
if (this.listenPaneResizeEvents) {
|
|
1388
|
+
window.removeEventListener("tradview:pane-resize", this.onPaneResize);
|
|
1389
|
+
}
|
|
1390
|
+
this.detachMainVolResizer();
|
|
871
1391
|
this.barAnimator?.cancel();
|
|
872
1392
|
if (this.priceLine) {
|
|
873
1393
|
this.mainSeries.removePriceLine(this.priceLine);
|
|
@@ -885,8 +1405,10 @@ var PaneOrchestrator = class {
|
|
|
885
1405
|
parent.appendChild(canvas);
|
|
886
1406
|
this.overlayCanvas = canvas;
|
|
887
1407
|
this.syncOverlaySize();
|
|
888
|
-
this.
|
|
889
|
-
|
|
1408
|
+
this.busRegistry.forEachBus((_, bus) => {
|
|
1409
|
+
bus.subscribeTransform(() => {
|
|
1410
|
+
this.syncOverlaySize();
|
|
1411
|
+
});
|
|
890
1412
|
});
|
|
891
1413
|
}
|
|
892
1414
|
/** Let drawing overlay receive clicks; cursor mode keeps pan/zoom on LWC. */
|
|
@@ -903,15 +1425,16 @@ var PaneOrchestrator = class {
|
|
|
903
1425
|
this.overlayCanvas.width = rect.width * devicePixelRatio;
|
|
904
1426
|
this.overlayCanvas.height = rect.height * devicePixelRatio;
|
|
905
1427
|
}
|
|
906
|
-
syncChartSize() {
|
|
1428
|
+
syncChartSize(opts) {
|
|
1429
|
+
const all = opts?.allPanes === true;
|
|
907
1430
|
const mainEl = this.mainChart.chartElement().parentElement;
|
|
908
1431
|
const volEl = this.volumeChart.chartElement().parentElement;
|
|
909
|
-
if (mainEl) {
|
|
1432
|
+
if (mainEl && (all || this.shouldResizePane("main"))) {
|
|
910
1433
|
const w = mainEl.clientWidth;
|
|
911
1434
|
const h = mainEl.clientHeight;
|
|
912
1435
|
if (w > 0 && h > 0) this.mainChart.resize(w, h);
|
|
913
1436
|
}
|
|
914
|
-
if (volEl) {
|
|
1437
|
+
if (this.isVolumeVisible() && volEl && (all || this.shouldResizePane("volume"))) {
|
|
915
1438
|
const w = volEl.clientWidth;
|
|
916
1439
|
const h = volEl.clientHeight;
|
|
917
1440
|
if (w > 0 && h > 0) this.volumeChart.resize(w, h);
|
|
@@ -928,9 +1451,19 @@ export {
|
|
|
928
1451
|
IndicatorPaneStack,
|
|
929
1452
|
PaneOrchestrator,
|
|
930
1453
|
TimeScaleBus,
|
|
1454
|
+
TimeScaleBusRegistry,
|
|
1455
|
+
attachPaneResizer,
|
|
931
1456
|
bollOverlayLines,
|
|
1457
|
+
defaultBarSpacingForInterval,
|
|
1458
|
+
detectIndicatorBarMutation,
|
|
932
1459
|
emaOverlayLine,
|
|
1460
|
+
independentBusKey,
|
|
1461
|
+
isLayeredPaneMount,
|
|
933
1462
|
maOverlayLine,
|
|
1463
|
+
normalizeSyncGroupId,
|
|
1464
|
+
resolveBarSpacingForInterval,
|
|
1465
|
+
resolveBusMapKey,
|
|
1466
|
+
shouldResizeChartPane,
|
|
934
1467
|
volMaOverlayLine
|
|
935
1468
|
};
|
|
936
1469
|
//# sourceMappingURL=index.js.map
|