traderead-charts 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.
@@ -0,0 +1,2243 @@
1
+ /*!
2
+ * TradeRead Charts v1.0.0
3
+ * Copyright (c) 2026 Marcel Todea / TradeRead — traderead.ai
4
+ * Original work, written from first principles.
5
+ * TradeRead Community License 1.0 (see LICENSE.md): free to use, including
6
+ * commercially - the TradeRead mark stays visible.
7
+ */
8
+ (function (global) {
9
+ "use strict";
10
+
11
+ var DEFAULTS = {
12
+ background: "#0d1117",
13
+ textColor: "#8b949e",
14
+ gridColor: "rgba(139,148,158,0.12)",
15
+ separatorColor: "rgba(139,148,158,0.25)",
16
+ upColor: "#00d97e",
17
+ downColor: "#f85149",
18
+ wickUp: "#00d97e",
19
+ wickDown: "#f85149",
20
+ volumeUp: "rgba(0,217,126,0.35)",
21
+ volumeDown: "rgba(248,81,73,0.35)",
22
+ crosshair: "rgba(139,148,158,0.45)",
23
+ tagBg: "#2a2e39",
24
+ tagText: "#e6edf3",
25
+ font: "11px -apple-system, 'Segoe UI', system-ui, sans-serif",
26
+ priceAxisWidth: 64,
27
+ timeAxisHeight: 24,
28
+ barSpacing: 8,
29
+ minBarSpacing: 1.5,
30
+ maxBarSpacing: 60,
31
+ rightPadBars: 5,
32
+ volumeHeightPct: 0.18,
33
+ autoScalePadPct: 0.08,
34
+ oscPaneHeight: 110, // default px height of an oscillator pane
35
+ minPricePaneFrac: 0.45, // price pane never shrinks below this share
36
+ onFullscreenToggle: null, // host override: an iframe cannot fullscreen itself on iOS
37
+ logoHref: null, // override the badge link (default carries utm_source=widget)
38
+ logo: true,
39
+ ui: true,
40
+ maxIndicators: 0, // 0 = unlimited (Pro); Standard builds pass 4 // the bundled rail + indicator panel + type switcher + camera
41
+ };
42
+
43
+ function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }
44
+ function isNum(v) { return typeof v === "number" && isFinite(v); }
45
+
46
+ function niceStep(span, n) {
47
+ var raw = span / Math.max(1, n);
48
+ var mag = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10));
49
+ var norm = raw / mag;
50
+ var step = norm >= 5 ? 10 : norm >= 2.5 ? 5 : norm >= 2 ? 2.5 : norm >= 1 ? 2 : 1;
51
+ return step * mag;
52
+ }
53
+ function stepDecimals(step) {
54
+ if (step >= 1) return step % 1 === 0 ? 0 : 2;
55
+ var d = 0, s = step;
56
+ while (s < 1 && d < 10) { s *= 10; d++; }
57
+ return d + (Math.round(s) !== s ? 1 : 0);
58
+ }
59
+ function fmtPrice(v, dec) {
60
+ if (Math.abs(v) < Math.pow(10, -(dec + 3))) v = 0; // float dust must not read "-0"
61
+ return v.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec });
62
+ }
63
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
64
+ function pad2(n) { return n < 10 ? "0" + n : "" + n; }
65
+
66
+ // A pane owns a horizontal slice of the plot and its own value scale.
67
+ // Pane 0 is the price pane (candles, volume band, overlay lines); every
68
+ // further pane is an oscillator: lines, histograms and fixed guides.
69
+ function Pane(chart, kind) {
70
+ this.chart = chart;
71
+ this.kind = kind; // "price" | "osc"
72
+ this.hPx = kind === "osc" ? chart.opt.oscPaneHeight : null;
73
+ this.lines = {}; // id → {byTime, color, width}
74
+ this.hists = {}; // id → {byTime, pos, neg} (osc only)
75
+ this.guides = []; // {value, color} (osc only)
76
+ this.y0 = 0; this.h = 0; // set by layout
77
+ this.min = 0; this.max = 1; // set per paint
78
+ }
79
+ Pane.prototype.toY = function (v) {
80
+ var usable = this.kind === "price" ? this.h * (1 - this.chart.opt.volumeHeightPct * 0.35) : this.h - 8;
81
+ var off = this.kind === "price" ? 0 : 4;
82
+ return this.y0 + off + (this.max - v) / (this.max - this.min) * usable;
83
+ };
84
+ Pane.prototype.toValue = function (y) {
85
+ var usable = this.kind === "price" ? this.h * (1 - this.chart.opt.volumeHeightPct * 0.35) : this.h - 8;
86
+ var off = this.kind === "price" ? 0 : 4;
87
+ return this.max - ((y - this.y0 - off) / usable) * (this.max - this.min);
88
+ };
89
+ Pane.prototype.computeScale = function (lo, hi) {
90
+ var min = Infinity, max = -Infinity, i, b, v, id;
91
+ var bars = this.chart.bars;
92
+ if (this.kind === "price") {
93
+ for (i = lo; i <= hi; i++) {
94
+ b = bars[i]; if (!b) continue;
95
+ if (b.low < min) min = b.low;
96
+ if (b.high > max) max = b.high;
97
+ }
98
+ }
99
+ // The price pane scales from BARS alone: overlays ride the frame. An
100
+ // EMA200 far from price once pinned the candles to the top of the pane
101
+ // (owner, 28 Aug: "TV e central") — that lesson is now structural.
102
+ if (this.kind !== "price") for (id in this.lines) {
103
+ var ln = this.lines[id];
104
+ for (i = lo; i <= hi; i++) {
105
+ b = bars[i]; if (!b) continue;
106
+ v = ln.byTime.get(b.time);
107
+ if (isNum(v)) { if (v < min) min = v; if (v > max) max = v; }
108
+ }
109
+ }
110
+ for (id in this.hists) {
111
+ var hs = this.hists[id];
112
+ for (i = lo; i <= hi; i++) {
113
+ b = bars[i]; if (!b) continue;
114
+ v = hs.byTime.get(b.time);
115
+ if (isNum(v)) { if (v < min) min = v; if (v > max) max = v; if (0 < min) min = 0; if (0 > max) max = 0; }
116
+ }
117
+ }
118
+ for (i = 0; i < this.guides.length; i++) {
119
+ v = this.guides[i].value;
120
+ if (v < min) min = v;
121
+ if (v > max) max = v;
122
+ }
123
+ if (!isFinite(min) || !isFinite(max)) { min = 0; max = 1; }
124
+ if (min === max) { min -= 0.5; max += 0.5; }
125
+ var pad = (max - min) * this.chart.opt.autoScalePadPct;
126
+ min -= pad; max += pad;
127
+ if (this.kind === "price" && this.chart._vZoom !== 1) {
128
+ var mid = (min + max) / 2, halfR = (max - min) / 2 * this.chart._vZoom;
129
+ min = mid - halfR; max = mid + halfR;
130
+ }
131
+ if (this.kind === "price" && this.chart._vOff) {
132
+ var sh = (max - min) * this.chart._vOff;
133
+ min += sh; max += sh;
134
+ }
135
+ this.min = min; this.max = max;
136
+ };
137
+
138
+ function Chart(container, options) {
139
+ var self = this;
140
+ this.el = container;
141
+ this.opt = Object.assign({}, DEFAULTS, options || {});
142
+ this.bars = [];
143
+ this.panes = [new Pane(this, "price")];
144
+ this.priceLines = [];
145
+ this.drawings = []; // {id, type, t1,p1, t2,p2, text?, size?} in (time, price) space
146
+ this.tool = null; // null = cursor (select/move); tools mirror the production rail
147
+ this._draft = null;
148
+ this._selected = null;
149
+ this._dragDraw = null; // {d, handle:"p2"|null, t0, v0, orig}
150
+ this._persistKey = null;
151
+ this._saveT = null;
152
+ this._sepDrag = null; // {i, y0, h0} while a pane separator is dragged
153
+ this._vZoom = 1; // vertical stretch of the price pane (wheel on the axis)
154
+ this._vOff = 0; // vertical pan of the price pane, in range fractions
155
+ this._axisDrag = null; // {axis:"price"|"time", ...} while an axis is being dragged
156
+ this.indicators = []; // built-in indicator list (see addIndicator)
157
+ this._indColor = 0;
158
+ this.chartType = "candles"; // "candles" | "bars" | "line" | "area" | "baseline"
159
+ this.markers = []; // {time, position:"above"|"below", shape, color, text}
160
+ this.barSpacing = this.opt.barSpacing;
161
+ this.rightIndex = 0;
162
+ this.crosshairCb = null;
163
+ this._cross = null;
164
+
165
+ container.style.position = container.style.position || "relative";
166
+ container.style.background = this.opt.background;
167
+ container.style.overflow = "hidden";
168
+
169
+ this.canvas = document.createElement("canvas");
170
+ this.drawCanvas = document.createElement("canvas");
171
+ this.overlay = document.createElement("canvas");
172
+ [this.canvas, this.drawCanvas, this.overlay].forEach(function (c) {
173
+ c.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;display:block;";
174
+ container.appendChild(c);
175
+ });
176
+ this.ctx = this.canvas.getContext("2d");
177
+ this.dctx = this.drawCanvas.getContext("2d");
178
+ this.octx = this.overlay.getContext("2d");
179
+
180
+ if (this.opt.logo) this._mountLogo();
181
+ if (this.opt.ui) this._mountUI();
182
+
183
+ this._ro = new ResizeObserver(function () { self._resize(); });
184
+ this._ro.observe(container);
185
+ this._resize();
186
+ this._bind();
187
+ }
188
+
189
+
190
+ // ── Bundled UI: the plus over the bare engine ────────────────────────
191
+ // ui: true (default) mounts the production-style rail: drawing tools,
192
+ // the TradingView-style indicator panel (many EMAs, SMA, add/remove,
193
+ // editable params), the chart-type switcher and a PNG camera.
194
+ var SVGI = function (inner) {
195
+ return '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">' + inner + "</svg>";
196
+ };
197
+ var ICONS = {
198
+ cursor: SVGI('<path d="M4 2 L12 8.5 L8.2 9.3 L6.2 13.5 Z" fill="currentColor" stroke="none"/>'),
199
+ trend: SVGI('<line x1="3" y1="13" x2="13" y2="3"/><circle cx="3" cy="13" r="1.4" fill="currentColor" stroke="none"/><circle cx="13" cy="3" r="1.4" fill="currentColor" stroke="none"/>'),
200
+ hline: SVGI('<line x1="2" y1="8" x2="14" y2="8"/><circle cx="8" cy="8" r="1.4" fill="currentColor" stroke="none"/>'),
201
+ rect: SVGI('<rect x="3" y="4.5" width="10" height="7" rx="1"/>'),
202
+ fib: SVGI('<line x1="2.5" y1="4" x2="13.5" y2="4"/><line x1="2.5" y1="8" x2="13.5" y2="8" opacity="0.55"/><line x1="2.5" y1="12" x2="13.5" y2="12"/>'),
203
+ text: SVGI('<path d="M4 4 H12 M8 4 V12.5" />'),
204
+ trash: SVGI('<line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/>'),
205
+ ind: SVGI('<path d="M2.5 11 L6 7 L9 9.5 L13.5 4"/><circle cx="13.5" cy="4" r="1.3" fill="currentColor" stroke="none"/>'),
206
+ camera: SVGI('<rect x="2.5" y="5" width="11" height="8" rx="1.5"/><path d="M6 5 L7 3.2 H9 L10 5"/><circle cx="8" cy="9" r="2.2"/>'),
207
+ candles: SVGI('<line x1="5" y1="2.5" x2="5" y2="13.5"/><rect x="3.4" y="5" width="3.2" height="5" fill="currentColor" stroke="none" rx="0.6"/><line x1="11" y1="2.5" x2="11" y2="13.5"/><rect x="9.4" y="4" width="3.2" height="6.5" fill="currentColor" stroke="none" rx="0.6"/>'),
208
+ bars: SVGI('<line x1="5" y1="3" x2="5" y2="13"/><line x1="3.5" y1="6" x2="5" y2="6"/><line x1="5" y1="10" x2="6.5" y2="10"/><line x1="11" y1="3" x2="11" y2="13"/><line x1="9.5" y1="7" x2="11" y2="7"/><line x1="11" y1="11" x2="12.5" y2="11"/>'),
209
+ line: SVGI('<path d="M2.5 11 L6 7 L9 9.5 L13.5 4"/>'),
210
+ area: SVGI('<path d="M2.5 11 L6 7 L9 9.5 L13.5 4 V13 H2.5 Z" fill="currentColor" opacity="0.35" stroke="none"/><path d="M2.5 11 L6 7 L9 9.5 L13.5 4"/>'),
211
+ baseline: SVGI('<line x1="2" y1="8" x2="14" y2="8" stroke-dasharray="2 2"/><path d="M2.5 10.5 L6 5.5 L9 9 L13.5 4"/>'),
212
+ hollow: SVGI('<line x1="5" y1="2.5" x2="5" y2="13.5"/><rect x="3.4" y="5" width="3.2" height="5" rx="0.6"/><line x1="11" y1="2.5" x2="11" y2="13.5"/><rect x="9.4" y="4" width="3.2" height="6.5" fill="currentColor" stroke="none" rx="0.6"/>'),
213
+ heikin: SVGI('<rect x="2.8" y="6" width="3.4" height="6" rx="1.6"/><rect x="9.8" y="3.5" width="3.4" height="6" rx="1.6" fill="currentColor" stroke="none"/>'),
214
+ share: SVGI('<path d="M8 10 V2.8 M5.5 5 L8 2.5 L10.5 5"/><path d="M4 8.5 H3 V13.5 H13 V8.5 H12"/>'),
215
+ fit: SVGI('<path d="M6 2.5 V6 H2.5 M13.5 6 H10 V2.5 M10 13.5 V10 H13.5 M2.5 10 H6 V13.5"/>'),
216
+ };
217
+ var TYPE_LIST = [
218
+ ["candles", "Candles"], ["hollow", "Hollow candles"], ["heikin", "Heikin Ashi"],
219
+ ["bars", "OHLC bars"], ["line", "Line"], ["area", "Area"], ["baseline", "Baseline"],
220
+ ];
221
+
222
+ Chart.prototype._mountUI = function () {
223
+ var self = this, o = this.opt;
224
+ var rail = document.createElement("div");
225
+ rail.className = "trc-rail";
226
+ rail.style.cssText = "position:absolute;left:8px;top:10px;z-index:6;display:flex;flex-direction:column;gap:3px;" +
227
+ "background:" + o.background + ";border:1px solid " + o.separatorColor + ";border-radius:10px;padding:4px;" +
228
+ // a phone-height frame is shorter than the tool column: the rail
229
+ // scrolls so the bottom buttons (camera, share, fit) stay reachable
230
+ "overflow-y:auto;overflow-x:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;flex-shrink:0;";
231
+ if (!document.getElementById("trc-style")) {
232
+ var st = document.createElement("style");
233
+ st.id = "trc-style";
234
+ st.textContent = ".trc-rail::-webkit-scrollbar{display:none}";
235
+ document.head.appendChild(st);
236
+ }
237
+ function btn(icon, tip, fn) {
238
+ var b = document.createElement("button");
239
+ b.type = "button"; b.innerHTML = icon; b.title = tip;
240
+ b.style.cssText = "border:none;background:none;color:" + o.textColor + ";width:26px;height:26px;flex-shrink:0;" +
241
+ "border-radius:7px;cursor:pointer;font:700 13px -apple-system,'Segoe UI',sans-serif;line-height:1;";
242
+ b.addEventListener("click", function () { fn(b); });
243
+ rail.appendChild(b);
244
+ return b;
245
+ }
246
+ function mark(active) {
247
+ rail.querySelectorAll("button").forEach(function (b) {
248
+ b.style.background = b === active ? "rgba(99,102,241,0.3)" : "none";
249
+ });
250
+ }
251
+ var toolBtns = {};
252
+ [[ICONS.cursor, "Select / move", null], [ICONS.trend, "Trend line", "trend"], [ICONS.hline, "Horizontal line", "hline"],
253
+ [ICONS.rect, "Rectangle", "rect"], [ICONS.fib, "Fibonacci retracement", "fib"], [ICONS.text, "Text", "text"]].forEach(function (t) {
254
+ toolBtns[t[2] || "cursor"] = btn(t[0], t[1], function (b) { self.setTool(t[2]); mark(t[2] ? b : toolBtns.cursor); });
255
+ });
256
+ mark(toolBtns.cursor);
257
+ this.onToolDone(function () { mark(toolBtns.cursor); });
258
+ btn(ICONS.trash, "Delete selected (Del)", function () { self.deleteSelected(); });
259
+ var sep = document.createElement("div");
260
+ sep.style.cssText = "height:1px;background:" + o.separatorColor + ";margin:3px 2px;flex-shrink:0;";
261
+ rail.appendChild(sep);
262
+ this._indBtn = btn(ICONS.ind, "Indicators", function () { self._toggleIndPanel(); });
263
+ var typeBtn = btn(ICONS.candles, "Chart type", function () { self._toggleTypeMenu(typeBtn); });
264
+ this._typeBtn = typeBtn;
265
+ btn(ICONS.camera, "Screenshot (PNG)", function () { self.snapshot(); });
266
+ btn(ICONS.share, "Share chart", function (b) { self.share(b); });
267
+ btn(ICONS.fit, "Fit chart", function () { self.fit(); });
268
+ this.el.appendChild(rail);
269
+ this._rail = rail;
270
+
271
+ // fullscreen, bottom-right (owner, 7 Sep 2026)
272
+ var fsIcon = SVGI('<path d="M2.5 6 V2.5 H6 M10 2.5 H13.5 V6 M13.5 10 V13.5 H10 M6 13.5 H2.5 V10"/>');
273
+ var fsExit = SVGI('<path d="M6 2.5 V6 H2.5 M13.5 6 H10 V2.5 M10 13.5 V10 H13.5 M2.5 10 H6 V13.5"/>');
274
+ var fsBtn = document.createElement("button");
275
+ fsBtn.type = "button"; fsBtn.title = "Fullscreen"; fsBtn.innerHTML = fsIcon;
276
+ // visibly a button, not axis furniture (owner: nobody would find the
277
+ // faint one): accent tint, larger hit area, brightens on hover
278
+ fsBtn.style.cssText = "position:absolute;right:" + (o.priceAxisWidth + 10) + "px;bottom:" + (o.timeAxisHeight + 10) + "px;z-index:6;" +
279
+ "width:34px;height:34px;border:1px solid rgba(129,140,248,0.55);border-radius:10px;" +
280
+ "background:rgba(99,102,241,0.22);color:" + o.tagText + ";cursor:pointer;" +
281
+ "display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(0,0,0,0.25);";
282
+ fsBtn.addEventListener("mouseenter", function () { fsBtn.style.background = "rgba(99,102,241,0.4)"; });
283
+ fsBtn.addEventListener("mouseleave", function () { fsBtn.style.background = "rgba(99,102,241,0.22)"; });
284
+ this._fsIcons = { on: fsExit, off: fsIcon };
285
+ fsBtn.addEventListener("click", function () {
286
+ // a host override wins: inside an iframe the embedding page must
287
+ // maximize the frame (iOS has no element-fullscreen API at all)
288
+ if (o.onFullscreenToggle) { o.onFullscreenToggle(); return; }
289
+ var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
290
+ if (fsEl === self.el) (document.exitFullscreen || document.webkitExitFullscreen).call(document);
291
+ else if (self.el.requestFullscreen) self.el.requestFullscreen();
292
+ else if (self.el.webkitRequestFullscreen) self.el.webkitRequestFullscreen();
293
+ else self._toggleFakeFullscreen();
294
+ });
295
+ document.addEventListener("fullscreenchange", function () {
296
+ self.setMaximized(document.fullscreenElement === self.el);
297
+ });
298
+ this.el.appendChild(fsBtn);
299
+ this._fsBtn = fsBtn;
300
+ this._fitLogo();
301
+ };
302
+
303
+ // The host (or a native fullscreenchange) reports the state; the button
304
+ // swaps its icon to match.
305
+ Chart.prototype.setMaximized = function (on) {
306
+ if (this._fsBtn && this._fsIcons) this._fsBtn.innerHTML = on ? this._fsIcons.on : this._fsIcons.off;
307
+ };
308
+ // iOS Safari fallback: pin the container to the viewport. Restoring puts
309
+ // back the exact inline style the element had.
310
+ Chart.prototype._toggleFakeFullscreen = function () {
311
+ if (this._fakeFS != null) {
312
+ this.el.style.cssText = this._fakeFS;
313
+ this._fakeFS = null;
314
+ } else {
315
+ this._fakeFS = this.el.style.cssText;
316
+ this.el.style.cssText += ";position:fixed;top:0;left:0;width:100vw;height:100vh;margin:0;z-index:2147483000;background:" + (this.opt.background || "#0d1117") + ";";
317
+ }
318
+ this.setMaximized(this._fakeFS != null);
319
+ this._resize();
320
+ };
321
+ // Menus close on any tap outside them. The anchor button is exempt — its
322
+ // own toggle handles that tap, otherwise close-then-reopen double-fires.
323
+ Chart.prototype._armOutsideClose = function (box, anchorBtn, close) {
324
+ var h = function (e) {
325
+ if (!box.isConnected) { document.removeEventListener("pointerdown", h, true); return; }
326
+ if (box.contains(e.target) || (anchorBtn && anchorBtn.contains(e.target))) return;
327
+ document.removeEventListener("pointerdown", h, true);
328
+ close();
329
+ };
330
+ document.addEventListener("pointerdown", h, true);
331
+ };
332
+
333
+ // A picker list, not a blind cycle: seven types, current one checked.
334
+
335
+ // ── Symbol search ────────────────────────────────────────────────────
336
+ // The selector ships with the widget; the DATA behind it is pluggable:
337
+ // symbolSearch: {
338
+ // search: function (query, cb) { cb([{symbol, display, name, exchange}]) },
339
+ // onSelect: function (item) { ... host loads bars ... },
340
+ // }
341
+ // Matches by ticker or by name; several results = the dropdown decides.
342
+ Chart.prototype._toggleSearch = function () {
343
+ var self = this, o = this.opt;
344
+ var cfg = o.symbolSearch;
345
+ if (!cfg || !cfg.search) return;
346
+ if (this._searchBox) { this._searchBox.remove(); this._searchBox = null; return; }
347
+ var wrap = document.createElement("div");
348
+ wrap.className = "trc-search";
349
+ wrap.style.cssText = "position:absolute;inset:0;z-index:22;display:flex;align-items:flex-start;justify-content:center;" +
350
+ "background:rgba(0,0,0,0.5);padding-top:48px;";
351
+ var box = document.createElement("div");
352
+ box.style.cssText = "width:min(420px,92%);background:" + o.background + ";border:1px solid " + o.separatorColor + ";" +
353
+ "border-radius:14px;padding:10px;box-shadow:0 12px 40px rgba(0,0,0,0.4);";
354
+ var inp = document.createElement("input");
355
+ inp.type = "text";
356
+ inp.placeholder = "Search ticker or name…";
357
+ inp.style.cssText = "width:100%;box-sizing:border-box;background:none;border:1px solid " + o.separatorColor + ";" +
358
+ "border-radius:9px;color:" + o.tagText + ";font:600 14px -apple-system,'Segoe UI',sans-serif;padding:9px 12px;outline:none;";
359
+ var list = document.createElement("div");
360
+ list.style.cssText = "margin-top:8px;max-height:300px;overflow:auto;";
361
+ box.appendChild(inp); box.appendChild(list);
362
+ wrap.appendChild(box);
363
+ wrap.addEventListener("click", function (e) { if (e.target === wrap) close(); });
364
+ this.el.appendChild(wrap);
365
+ this._searchBox = wrap;
366
+ inp.focus();
367
+
368
+ var items = [], active = -1, timer = null;
369
+ function close() { wrap.remove(); self._searchBox = null; }
370
+ function paint() {
371
+ list.innerHTML = "";
372
+ items.forEach(function (it, i) {
373
+ var row = document.createElement("button");
374
+ row.type = "button";
375
+ row.style.cssText = "display:flex;align-items:center;gap:10px;width:100%;text-align:left;border:none;" +
376
+ "background:" + (i === active ? "rgba(99,102,241,0.25)" : "none") + ";color:" + o.tagText + ";" +
377
+ "font:600 13px -apple-system,'Segoe UI',sans-serif;border-radius:8px;padding:8px 10px;cursor:pointer;";
378
+ row.innerHTML = '<span style="min-width:64px;font-weight:800;">' + (it.display || it.symbol) + "</span>" +
379
+ '<span style="flex:1;color:' + o.textColor + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + (it.name || "") + "</span>" +
380
+ (it.exchange ? '<span style="font-size:10px;font-weight:800;color:' + o.textColor + ';border:1px solid ' + o.separatorColor + ';border-radius:5px;padding:1px 5px;">' + it.exchange + "</span>" : "");
381
+ row.addEventListener("click", function () { pick(it); });
382
+ row.addEventListener("mouseenter", function () { active = i; paint(); });
383
+ list.appendChild(row);
384
+ });
385
+ if (!items.length && inp.value.trim()) {
386
+ list.innerHTML = '<div style="color:' + o.textColor + ';font:12px -apple-system,sans-serif;padding:10px;">No matches.</div>';
387
+ }
388
+ }
389
+ function pick(it) {
390
+ close();
391
+ if (cfg.onSelect) cfg.onSelect(it);
392
+ }
393
+ inp.addEventListener("input", function () {
394
+ clearTimeout(timer);
395
+ timer = setTimeout(function () {
396
+ var q = inp.value.trim();
397
+ if (!q) { items = []; active = -1; paint(); return; }
398
+ cfg.search(q, function (res) { items = res || []; active = items.length ? 0 : -1; paint(); });
399
+ }, 160);
400
+ });
401
+ inp.addEventListener("keydown", function (e) {
402
+ if (e.key === "Escape") close();
403
+ else if (e.key === "ArrowDown") { active = Math.min(active + 1, items.length - 1); paint(); e.preventDefault(); }
404
+ else if (e.key === "ArrowUp") { active = Math.max(active - 1, 0); paint(); e.preventDefault(); }
405
+ else if (e.key === "Enter" && active >= 0 && items[active]) pick(items[active]);
406
+ });
407
+ };
408
+
409
+ // A picker list, not a blind cycle: seven types, current one checked.
410
+ Chart.prototype._toggleTypeMenu = function (anchorBtn) {
411
+ var self = this, o = this.opt;
412
+ if (this._typeMenu) { this._typeMenu.remove(); this._typeMenu = null; return; }
413
+ var m = document.createElement("div");
414
+ m.className = "trc-type-menu";
415
+ m.style.cssText = "position:absolute;left:44px;top:" + (anchorBtn ? anchorBtn.offsetTop + 4 : 40) + "px;z-index:8;" +
416
+ "background:" + o.background + ";border:1px solid " + o.separatorColor + ";border-radius:12px;padding:6px;" +
417
+ "color:" + o.textColor + ";font:12px -apple-system,'Segoe UI',sans-serif;box-shadow:0 8px 30px rgba(0,0,0,0.35);";
418
+ var fitMenu = function () {
419
+ var h = m.offsetHeight || 0, maxTop = self._plotH() - h - 6;
420
+ var top = parseInt(m.style.top, 10) || 0;
421
+ if (top > maxTop) m.style.top = Math.max(6, maxTop) + "px";
422
+ };
423
+ TYPE_LIST.forEach(function (t) {
424
+ var row = document.createElement("button");
425
+ row.type = "button";
426
+ row.style.cssText = "display:flex;align-items:center;gap:9px;width:100%;text-align:left;border:none;" +
427
+ "background:" + (self.chartType === t[0] ? "rgba(99,102,241,0.25)" : "none") + ";color:inherit;font:inherit;" +
428
+ "border-radius:8px;padding:6px 10px;cursor:pointer;white-space:nowrap;";
429
+ row.innerHTML = ICONS[t[0]] + "<span>" + t[1] + "</span>";
430
+ row.addEventListener("click", function () {
431
+ self.setChartType(t[0]);
432
+ if (self._typeBtn) { self._typeBtn.innerHTML = ICONS[t[0]]; self._typeBtn.title = "Chart type: " + t[1]; }
433
+ m.remove();
434
+ self._typeMenu = null;
435
+ });
436
+ m.appendChild(row);
437
+ });
438
+ this.el.appendChild(m);
439
+ fitMenu();
440
+ this._typeMenu = m;
441
+ this._armOutsideClose(m, this._typeBtn, function () { m.remove(); self._typeMenu = null; });
442
+ };
443
+
444
+ Chart.prototype._toggleIndPanel = function () {
445
+ var self = this, o = this.opt;
446
+ if (this._indPanel) { this._indPanel.remove(); this._indPanel = null; return; }
447
+ var p = document.createElement("div");
448
+ p.className = "trc-ind-panel";
449
+ p.style.cssText = "position:absolute;left:44px;top:10px;z-index:7;min-width:230px;max-height:70%;overflow:auto;" +
450
+ "background:" + o.background + ";border:1px solid " + o.separatorColor + ";border-radius:12px;padding:10px;" +
451
+ "color:" + o.textColor + ";font:12px -apple-system,'Segoe UI',sans-serif;box-shadow:0 8px 30px rgba(0,0,0,0.35);";
452
+ function repaint() {
453
+ var h = '<div style="font-weight:800;margin-bottom:8px;">Indicators</div>';
454
+ var list = self.getIndicators();
455
+ if (!list.length) h += '<div style="opacity:0.6;margin-bottom:8px;">None active.</div>';
456
+ list.forEach(function (ind) {
457
+ var def = IND_DEFS[ind.kind];
458
+ h += '<div style="display:flex;align-items:center;gap:6px;padding:4px 0;" data-row="' + ind.id + '">' +
459
+ '<input type="color" data-color="' + ind.id + '" value="' + (ind.color || "#8b949e") + '" title="Line color" ' +
460
+ 'style="width:18px;height:18px;padding:0;border:none;border-radius:4px;background:none;cursor:pointer;">' +
461
+ '<input type="text" data-label="' + ind.id + '" value="' + (ind.label || "") + '" placeholder="' + def.label + '" title="Rename" ' +
462
+ 'style="flex:1;min-width:60px;background:none;border:1px solid transparent;border-radius:5px;color:inherit;font:inherit;padding:1px 4px;" ' +
463
+ 'onfocus="this.style.borderColor=\'rgba(139,148,158,0.4)\'" onblur="this.style.borderColor=\'transparent\'">';
464
+ Object.keys(ind.params).forEach(function (k) {
465
+ h += '<input data-ind="' + ind.id + '" data-k="' + k + '" type="number" step="any" value="' + ind.params[k] + '" ' +
466
+ 'style="width:42px;background:none;border:1px solid ' + o.separatorColor + ';border-radius:5px;color:inherit;font:inherit;padding:1px 3px;">';
467
+ });
468
+ h += '<button data-del="' + ind.id + '" style="border:none;background:none;color:inherit;cursor:pointer;opacity:0.7;font:inherit;">✕</button></div>';
469
+ });
470
+ h += '<div style="border-top:1px solid ' + o.separatorColor + ';margin:8px 0;"></div>';
471
+ if (self.opt.maxIndicators > 0 && list.length >= self.opt.maxIndicators) {
472
+ h += '<div style="color:' + o.textColor + ';font-size:11px;margin:4px 0;">Standard: max ' + self.opt.maxIndicators + ' indicators — Pro is unlimited.</div>';
473
+ }
474
+ Object.keys(IND_DEFS).forEach(function (kind) {
475
+ h += '<button data-add="' + kind + '" style="border:1px solid ' + o.separatorColor + ';background:none;color:inherit;' +
476
+ 'cursor:pointer;font:inherit;border-radius:7px;padding:3px 8px;margin:2px 3px 2px 0;">+ ' + IND_DEFS[kind].label + '</button>';
477
+ });
478
+ p.innerHTML = h;
479
+ p.querySelectorAll("[data-add]").forEach(function (b) {
480
+ b.addEventListener("click", function () { self.addIndicator(b.getAttribute("data-add")); repaint(); });
481
+ });
482
+ p.querySelectorAll("[data-del]").forEach(function (b) {
483
+ b.addEventListener("click", function () { self.removeIndicator(b.getAttribute("data-del")); repaint(); });
484
+ });
485
+ p.querySelectorAll("input[data-color]").forEach(function (inp) {
486
+ inp.addEventListener("change", function () {
487
+ self.setIndicatorStyle(inp.getAttribute("data-color"), { color: inp.value });
488
+ });
489
+ });
490
+ p.querySelectorAll("input[data-label]").forEach(function (inp) {
491
+ inp.addEventListener("change", function () {
492
+ self.setIndicatorStyle(inp.getAttribute("data-label"), { label: inp.value.trim() });
493
+ });
494
+ });
495
+ p.querySelectorAll("input[data-ind]").forEach(function (inp) {
496
+ inp.addEventListener("change", function () {
497
+ var patch = {};
498
+ patch[inp.getAttribute("data-k")] = parseFloat(inp.value);
499
+ self.updateIndicator(inp.getAttribute("data-ind"), patch);
500
+ });
501
+ });
502
+ }
503
+ repaint();
504
+ this.el.appendChild(p);
505
+ var hP = p.offsetHeight || 0, maxTopP = this._plotH() - hP - 6;
506
+ if (10 > maxTopP) p.style.top = Math.max(6, maxTopP) + "px";
507
+ this._indPanel = p;
508
+ this._armOutsideClose(p, this._indBtn, function () { p.remove(); self._indPanel = null; });
509
+ };
510
+
511
+ Chart.prototype._composite = function () {
512
+ var out = document.createElement("canvas");
513
+ out.width = this.canvas.width; out.height = this.canvas.height;
514
+ var c = out.getContext("2d");
515
+ var dpr = out.width / Math.max(1, this.w);
516
+ c.fillStyle = this.opt.background;
517
+ c.fillRect(0, 0, out.width, out.height);
518
+ c.drawImage(this.canvas, 0, 0);
519
+ c.drawImage(this.drawCanvas, 0, 0);
520
+ // header + mark are DOM, invisible to canvases — draw them into the file
521
+ c.setTransform(dpr, 0, 0, dpr, 0, 0);
522
+ var inf = this._symbolInfo || {};
523
+ var n = this.bars.length, last = n ? this.bars[n - 1] : null;
524
+ if (inf.ticker || last) {
525
+ c.font = "800 15px -apple-system, 'Segoe UI', sans-serif";
526
+ var x = 12;
527
+ if (inf.ticker) { c.fillStyle = this.opt.tagText; c.fillText(inf.ticker, x, 22); x += c.measureText(inf.ticker).width + 8; }
528
+ if (inf.tf) { c.font = "700 11px -apple-system, sans-serif"; c.fillStyle = this.opt.textColor; c.fillText(inf.tf, x, 22); x += c.measureText(inf.tf).width + 8; }
529
+ if (last) {
530
+ c.font = "800 15px -apple-system, sans-serif";
531
+ c.fillStyle = last.close >= last.open ? this.opt.upColor : this.opt.downColor;
532
+ c.fillText(fmtPrice(last.close, last.close >= 1000 ? 2 : 4), x, 22);
533
+ }
534
+ }
535
+ c.font = "800 12px -apple-system, sans-serif";
536
+ c.fillStyle = this.opt.textColor;
537
+ c.save(); c.globalAlpha = 0.75;
538
+ c.fillText("TradeRead", 12, this._plotH() - 8);
539
+ c.restore();
540
+ return out;
541
+ };
542
+ // Share: the native sheet where it exists (phones), clipboard otherwise —
543
+ // same intent as the production widget's share button.
544
+ Chart.prototype.share = function (btnEl) {
545
+ var self = this;
546
+ this._composite().toBlob(function (blob) {
547
+ var file = new File([blob], "traderead-chart.png", { type: "image/png" });
548
+ if (navigator.canShare && navigator.canShare({ files: [file] })) {
549
+ navigator.share({ files: [file], title: "TradeRead chart" }).catch(function () {});
550
+ } else if (navigator.clipboard && window.ClipboardItem) {
551
+ navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]).then(function () {
552
+ if (btnEl) { var t = btnEl.title; btnEl.title = "Copied to clipboard"; setTimeout(function () { btnEl.title = t; }, 1500); }
553
+ }).catch(function () { self.snapshot(); });
554
+ } else self.snapshot();
555
+ });
556
+ };
557
+
558
+ // PNG snapshot. Nothing downloads by itself (owner, 7 Sep 2026): the
559
+ // camera opens a preview with explicit Download / Copy / Close actions.
560
+ Chart.prototype.snapshot = function (filename) {
561
+ var self = this, o = this.opt;
562
+ var out = this._composite();
563
+ if (this._snapModal) this._snapModal.remove();
564
+ var wrap = document.createElement("div");
565
+ wrap.className = "trc-snap";
566
+ wrap.style.cssText = "position:absolute;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;" +
567
+ "background:rgba(0,0,0,0.55);";
568
+ var box = document.createElement("div");
569
+ box.style.cssText = "max-width:86%;max-height:86%;display:flex;flex-direction:column;gap:10px;" +
570
+ "background:" + o.background + ";border:1px solid " + o.separatorColor + ";border-radius:14px;padding:12px;";
571
+ var img = document.createElement("img");
572
+ img.src = out.toDataURL("image/png");
573
+ img.style.cssText = "max-width:100%;max-height:60vh;border-radius:8px;display:block;";
574
+ var row = document.createElement("div");
575
+ row.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
576
+ function act(label, fn) {
577
+ var b = document.createElement("button");
578
+ b.type = "button"; b.textContent = label;
579
+ b.style.cssText = "border:1px solid " + o.separatorColor + ";background:none;color:" + o.tagText + ";" +
580
+ "font:700 12px -apple-system,'Segoe UI',sans-serif;border-radius:8px;padding:7px 14px;cursor:pointer;";
581
+ b.addEventListener("click", fn);
582
+ row.appendChild(b);
583
+ return b;
584
+ }
585
+ act("Download", function () {
586
+ var a = document.createElement("a");
587
+ a.download = filename || "traderead-chart.png";
588
+ a.href = img.src;
589
+ a.click();
590
+ });
591
+ if (navigator.clipboard && window.ClipboardItem) {
592
+ var copyBtn = act("Copy", function () {
593
+ out.toBlob(function (blob) {
594
+ navigator.clipboard.write([new ClipboardItem({ "image/png": blob })])
595
+ .then(function () { copyBtn.textContent = "Copied ✓"; setTimeout(function () { copyBtn.textContent = "Copy"; }, 1500); })
596
+ .catch(function () { copyBtn.textContent = "Copy failed"; });
597
+ });
598
+ });
599
+ }
600
+ act("Close", function () { wrap.remove(); self._snapModal = null; });
601
+ wrap.addEventListener("click", function (e) { if (e.target === wrap) { wrap.remove(); self._snapModal = null; } });
602
+ box.appendChild(img); box.appendChild(row); wrap.appendChild(box);
603
+ this.el.appendChild(wrap);
604
+ this._snapModal = wrap;
605
+ };
606
+
607
+ Chart.prototype._mountLogo = function () {
608
+ // The mark is a LINK to traderead.ai — that is the licence's attribution
609
+ // in its working form, so it stays clickable (owner, 7 Sep 2026).
610
+ var d = document.createElement("a");
611
+ d.className = "trc-logo";
612
+ // utm_source=widget: clicks from third-party embeds arrive referrer-less
613
+ // (or under the embedder's domain) — the tag is the only attribution.
614
+ d.href = this.opt.logoHref || "https://traderead.ai/?utm_source=widget";
615
+ d.target = "_blank";
616
+ d.rel = "noopener";
617
+ // A rounded plate separates the mark from whatever sits behind it — on
618
+ // the volume bars the candle icon camouflaged into the candles (owner,
619
+ // 7 Sep 2026). Plate tone follows the theme's background luminance.
620
+ var bg = String(this.opt.background || "#0d1117");
621
+ var lum = 0;
622
+ try {
623
+ var hx = bg.replace("#", "");
624
+ lum = (parseInt(hx.substr(0, 2), 16) * 0.299 + parseInt(hx.substr(2, 2), 16) * 0.587 + parseInt(hx.substr(4, 2), 16) * 0.114);
625
+ } catch (e) {}
626
+ var lightBg = lum > 128;
627
+ d.style.cssText = "position:absolute;left:8px;bottom:" + (this.opt.timeAxisHeight + 8) +
628
+ "px;z-index:3;display:flex;align-items:center;font:800 24px -apple-system,'Segoe UI',sans-serif;" +
629
+ "border-radius:12px;" +
630
+ "background:" + (lightBg ? "rgba(255,255,255,0.72)" : "rgba(13,17,23,0.66)") + ";" +
631
+ "-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);" +
632
+ "color:" + (lightBg ? "rgba(19,23,34,0.9)" : "rgba(205,214,223,0.95)") + ";" +
633
+ "text-decoration:none;cursor:pointer;user-select:none;";
634
+ d.innerHTML = '<svg width="30" height="30" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">' +
635
+ '<line x1="5" y1="3" x2="5" y2="7" stroke="#ef4444" stroke-width="1.5" stroke-linecap="round"/><rect x="2.5" y="7" width="5" height="8" rx="1" fill="#ef4444"/>' +
636
+ '<line x1="5" y1="15" x2="5" y2="20" stroke="#ef4444" stroke-width="1.5" stroke-linecap="round"/><line x1="14" y1="2" x2="14" y2="6" stroke="#22c55e" stroke-width="1.5" stroke-linecap="round"/>' +
637
+ '<rect x="11.5" y="6" width="5" height="13" rx="1" fill="#22c55e"/><line x1="14" y1="19" x2="14" y2="24" stroke="#22c55e" stroke-width="1.5" stroke-linecap="round"/>' +
638
+ '<line x1="23" y1="6" x2="23" y2="10" stroke="#ef4444" stroke-width="1.5" stroke-linecap="round"/><rect x="20.5" y="10" width="5" height="6" rx="1" fill="#ef4444"/>' +
639
+ '<line x1="23" y1="16" x2="23" y2="21" stroke="#ef4444" stroke-width="1.5" stroke-linecap="round"/></svg><span>TradeRead</span>';
640
+ this.el.appendChild(d);
641
+ this._logoEl = d;
642
+ this._fitLogo();
643
+ };
644
+
645
+ // Small embeds (the homepage widget is ~500px tall) get a compact mark:
646
+ // full size there overlapped the bottom oscillator pane (owner, 7 Sep 2026).
647
+ Chart.prototype._fitLogo = function () {
648
+ var d = this._logoEl;
649
+ if (!d) return;
650
+ var small = (this.h || this.el.clientHeight || 600) < 560;
651
+ d.style.fontSize = small ? "14px" : "24px";
652
+ d.style.gap = small ? "6px" : "10px";
653
+ d.style.padding = small ? "3px 9px 3px 7px" : "6px 14px 6px 10px";
654
+ d.style.borderRadius = small ? "9px" : "12px";
655
+ var svg = d.querySelector("svg");
656
+ if (svg) {
657
+ svg.setAttribute("width", small ? "18" : "30");
658
+ svg.setAttribute("height", small ? "18" : "30");
659
+ }
660
+ // the drawing rail shares the left column; in short frames it can reach
661
+ // the logo's row — slide the plate right of the rail instead of under it
662
+ var left = 8;
663
+ var rail = this.el.querySelector(".trc-rail");
664
+ if (rail) {
665
+ rail.style.maxHeight = Math.max(96, (this.h || this.el.clientHeight || 600) - this.opt.timeAxisHeight - 20) + "px";
666
+ var railBottom = rail.offsetTop + rail.offsetHeight;
667
+ var logoH = small ? 24 : 42;
668
+ var logoTop = (this.h || this.el.clientHeight || 600) - this.opt.timeAxisHeight - 8 - logoH;
669
+ if (railBottom + 4 > logoTop) left = rail.offsetLeft + rail.offsetWidth + 6;
670
+ }
671
+ d.style.left = left + "px";
672
+ };
673
+
674
+ Chart.prototype._resize = function () {
675
+ var dpr = window.devicePixelRatio || 1;
676
+ this.w = this.el.clientWidth;
677
+ this.h = this.el.clientHeight;
678
+ [this.canvas, this.drawCanvas, this.overlay].forEach(function (c) {
679
+ c.width = Math.round(this.w * dpr);
680
+ c.height = Math.round(this.h * dpr);
681
+ }, this);
682
+ this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
683
+ this.dctx.setTransform(dpr, 0, 0, dpr, 0, 0);
684
+ this.octx.setTransform(dpr, 0, 0, dpr, 0, 0);
685
+ this._fitLogo();
686
+ this._paint();
687
+ };
688
+
689
+ // ── Layout: stack the panes inside the plot column ───────────────────
690
+ Chart.prototype._plotW = function () { return this.w - this.opt.priceAxisWidth; };
691
+ Chart.prototype._plotH = function () { return this.h - this.opt.timeAxisHeight; };
692
+ Chart.prototype._layout = function () {
693
+ var H = this._plotH();
694
+ var fixed = 0, i;
695
+ for (i = 1; i < this.panes.length; i++) fixed += this.panes[i].hPx;
696
+ // The price pane defends its share: squeeze osc panes proportionally
697
+ // when there are too many for the frame.
698
+ var minPrice = H * this.opt.minPricePaneFrac;
699
+ var scale = fixed > 0 && H - fixed < minPrice ? (H - minPrice) / fixed : 1;
700
+ var y = 0;
701
+ this.panes[0].y0 = 0;
702
+ this.panes[0].h = H - Math.round(fixed * scale);
703
+ y = this.panes[0].h;
704
+ for (i = 1; i < this.panes.length; i++) {
705
+ this.panes[i].y0 = y;
706
+ this.panes[i].h = Math.round(this.panes[i].hPx * scale);
707
+ y += this.panes[i].h;
708
+ }
709
+ };
710
+ // ±4px around an oscillator pane's top edge grabs the separator.
711
+ Chart.prototype._separatorAt = function (y) {
712
+ for (var i = 1; i < this.panes.length; i++) {
713
+ if (Math.abs(y - this.panes[i].y0) <= 4) return i;
714
+ }
715
+ return null;
716
+ };
717
+ Chart.prototype._paneAt = function (y) {
718
+ for (var i = 0; i < this.panes.length; i++) {
719
+ var pn = this.panes[i];
720
+ if (y >= pn.y0 && y < pn.y0 + pn.h) return pn;
721
+ }
722
+ return null;
723
+ };
724
+
725
+ // Drawings anchor to (time, price): the production widget's lazy history
726
+ // PREPENDS bars and shifts every index — a lesson already paid for once.
727
+ Chart.prototype.timeToIndex = function (t) {
728
+ var b = this.bars, n = b.length;
729
+ if (!n) return 0;
730
+ var sec = this._barSec || 3600;
731
+ if (t <= b[0].time) return (t - b[0].time) / sec;
732
+ if (t >= b[n - 1].time) return n - 1 + (t - b[n - 1].time) / sec;
733
+ var lo = 0, hi = n - 1;
734
+ while (hi - lo > 1) { var m = (lo + hi) >> 1; if (b[m].time <= t) lo = m; else hi = m; }
735
+ return lo + (t - b[lo].time) / Math.max(1, b[hi].time - b[lo].time);
736
+ };
737
+ Chart.prototype.indexToTime = function (i) {
738
+ var b = this.bars, n = b.length;
739
+ if (!n) return 0;
740
+ var sec = this._barSec || 3600;
741
+ var r = Math.round(i);
742
+ if (r >= 0 && r < n) return b[r].time + (i - r) * sec;
743
+ if (r < 0) return b[0].time + i * sec;
744
+ return b[n - 1].time + (i - (n - 1)) * sec;
745
+ };
746
+ Chart.prototype.timeToX = function (t) { return this.indexToX(this.timeToIndex(t)); };
747
+
748
+ Chart.prototype.indexToX = function (i) {
749
+ return this._plotW() - (this.rightIndex + this.opt.rightPadBars - i) * this.barSpacing;
750
+ };
751
+ Chart.prototype.xToIndex = function (x) {
752
+ return this.rightIndex + this.opt.rightPadBars - (this._plotW() - x) / this.barSpacing;
753
+ };
754
+ Chart.prototype._visibleRange = function () {
755
+ var lo = Math.floor(this.xToIndex(0)), hi = Math.ceil(this.xToIndex(this._plotW()));
756
+ return [clamp(lo, 0, this.bars.length - 1), clamp(hi, 0, this.bars.length - 1)];
757
+ };
758
+ // Back-compat shorthands for the price pane
759
+ Chart.prototype.priceToY = function (v) { return this.panes[0].toY(v); };
760
+ Chart.prototype.yToPrice = function (y) { return this.panes[0].toValue(y); };
761
+
762
+ // ── Data API ─────────────────────────────────────────────────────────
763
+ // An explicit bar length beats the inferred one wherever sessions have
764
+ // gaps (stocks after a weekend). "1M"/"1Y" pass their calendar strings.
765
+ Chart.prototype.setTimeframe = function (spec) {
766
+ this._tfSpec = spec;
767
+ if (typeof spec === "number") this._barSecFixed = spec;
768
+ else if (spec === "1M") this._barSecFixed = 2629800;
769
+ else if (spec === "1Y") this._barSecFixed = 31557600;
770
+ else this._barSecFixed = null;
771
+ };
772
+ Chart.prototype.setData = function (bars) {
773
+ this.bars = (bars || []).slice().sort(function (a, b) { return a.time - b.time; });
774
+ this.rightIndex = this.bars.length - 1;
775
+ // The label alphabet depends on the bar size: intraday alternates
776
+ // day-marks with clock times, daily and up never shows a clock.
777
+ var n = this.bars.length;
778
+ this._barSec = this._barSecFixed || (n > 1 ? Math.max(1, Math.round((this.bars[n - 1].time - this.bars[0].time) / (n - 1))) : 3600);
779
+ this._ha = null;
780
+ if (this.indicators.length) this._applyIndicators();
781
+ else this._paint();
782
+ };
783
+ Chart.prototype.update = function (bar) {
784
+ var n = this.bars.length;
785
+ if (n && this.bars[n - 1].time === bar.time) this.bars[n - 1] = bar;
786
+ else {
787
+ var atRight = Math.abs(this.rightIndex - (n - 1)) < 2;
788
+ this.bars.push(bar);
789
+ if (atRight) this.rightIndex = this.bars.length - 1;
790
+ }
791
+ this._ha = null;
792
+ this._paintHeader();
793
+ if (this.indicators.length) this._applyIndicators();
794
+ else this._paint();
795
+ };
796
+ // Live tick with the bucket roll built in: when the clock crosses the bar
797
+ // boundary a NEW candle opens at the last close. On the production site,
798
+ // candles once froze at their close because only "update the last bar"
799
+ // existed — that class of bug ends here, inside the library.
800
+ Chart.prototype.tick = function (price, volume, nowSec) {
801
+ var n = this.bars.length;
802
+ if (!n || !isNum(price)) return;
803
+ var sec = this._barSec || 3600;
804
+ var now = isNum(nowSec) ? nowSec : Math.floor(Date.now() / 1000);
805
+ var last = this.bars[n - 1];
806
+ var bucket = Math.floor(now / sec) * sec;
807
+ if (bucket > last.time) {
808
+ this.update({ time: bucket, open: last.close, high: Math.max(last.close, price),
809
+ low: Math.min(last.close, price), close: price, volume: volume || 0 });
810
+ } else {
811
+ this.update({ time: last.time, open: last.open, high: Math.max(last.high, price),
812
+ low: Math.min(last.low, price), close: price, volume: (last.volume || 0) + (volume || 0) });
813
+ }
814
+ this._paintHeader();
815
+ };
816
+ function toByTime(data) {
817
+ var m = new Map();
818
+ (data || []).forEach(function (d) { m.set(d.time, d.value); });
819
+ return m;
820
+ }
821
+ Chart.prototype.addLine = function (id, data, color, width, paneIndex) {
822
+ var pn = this._ensurePane(paneIndex || 0);
823
+ pn.lines[id] = { byTime: toByTime(data), color: color || "#58a6ff", width: width || 1.4 };
824
+ this._paint();
825
+ };
826
+ Chart.prototype.addHistogram = function (id, data, paneIndex, posColor, negColor) {
827
+ var pn = this._ensurePane(paneIndex);
828
+ pn.hists[id] = { byTime: toByTime(data), pos: posColor || "rgba(0,217,126,0.55)", neg: negColor || "rgba(248,81,73,0.55)" };
829
+ this._paint();
830
+ };
831
+ Chart.prototype.addGuide = function (paneIndex, value, color) {
832
+ this._ensurePane(paneIndex).guides.push({ value: value, color: color || "rgba(139,148,158,0.4)" });
833
+ this._paint();
834
+ };
835
+ Chart.prototype._ensurePane = function (i) {
836
+ if (!i) return this.panes[0];
837
+ while (this.panes.length <= i) this.panes.push(new Pane(this, "osc"));
838
+ return this.panes[i];
839
+ };
840
+ Chart.prototype.setPaneHeight = function (i, px) {
841
+ if (this.panes[i] && this.panes[i].kind === "osc") { this.panes[i].hPx = px; this._paint(); }
842
+ };
843
+ Chart.prototype.removeOscPanes = function () { this.panes = [this.panes[0]]; this._paint(); };
844
+ Chart.prototype.removeLine = function (id, paneIndex) {
845
+ var pn = this.panes[paneIndex || 0];
846
+ if (pn) { delete pn.lines[id]; delete pn.hists[id]; }
847
+ this._paint();
848
+ };
849
+
850
+ // ── Built-in indicators ──────────────────────────────────────────────
851
+ // The library ships batteries included: the caller can still push raw
852
+ // series, but add/remove/configure of the standard set is one call —
853
+ // and the bundled panel (ui: true) gives end users the TradingView-style
854
+ // workflow the production widget has: many EMAs, SMA, delete, params.
855
+ var IND_PALETTE = ["#00d97e", "#f0b429", "#58a6ff", "#f85149", "#c084fc", "#22d3ee", "#fb923c", "#a3e635"];
856
+ var IND_DEFS = {
857
+ ema: { label: "EMA", overlay: true, params: { p: 20 } },
858
+ sma: { label: "SMA", overlay: true, params: { p: 50 } },
859
+ vwap: { label: "VWAP", overlay: true, params: {} },
860
+ bb: { label: "Bollinger", overlay: true, params: { p: 20, mult: 2 } },
861
+ rsi: { label: "RSI", overlay: false, params: { p: 14 } },
862
+ macd: { label: "MACD", overlay: false, params: { f: 12, s: 26, sig: 9 } },
863
+ stoch: { label: "Stochastic", overlay: false, params: { k: 14, d: 3, s: 3 } },
864
+ };
865
+
866
+ function calcEMA(bars, period, key) {
867
+ var k = 2 / (period + 1), out = [], prev = null;
868
+ for (var i = 0; i < bars.length; i++) {
869
+ var v = key ? bars[i][key] : bars[i].close;
870
+ prev = prev === null ? v : v * k + prev * (1 - k);
871
+ out.push({ time: bars[i].time, value: prev });
872
+ }
873
+ return out;
874
+ }
875
+ function calcSMA(bars, period) {
876
+ var out = [], sum = 0;
877
+ for (var i = 0; i < bars.length; i++) {
878
+ sum += bars[i].close;
879
+ if (i >= period) sum -= bars[i - period].close;
880
+ if (i >= period - 1) out.push({ time: bars[i].time, value: sum / period });
881
+ }
882
+ return out;
883
+ }
884
+ function calcVWAP(bars) {
885
+ var out = [], pv = 0, vol = 0, day = null;
886
+ for (var i = 0; i < bars.length; i++) {
887
+ var b = bars[i], d = Math.floor(b.time / 86400);
888
+ if (d !== day) { day = d; pv = 0; vol = 0; } // session reset at the UTC day open
889
+ var typ = (b.high + b.low + b.close) / 3;
890
+ pv += typ * (b.volume || 1); vol += (b.volume || 1);
891
+ out.push({ time: b.time, value: pv / vol });
892
+ }
893
+ return out;
894
+ }
895
+ function calcBB(bars, period, mult) {
896
+ var mid = [], up = [], lo = [], win = [];
897
+ for (var i = 0; i < bars.length; i++) {
898
+ win.push(bars[i].close);
899
+ if (win.length > period) win.shift();
900
+ if (win.length === period) {
901
+ var m = 0, j;
902
+ for (j = 0; j < period; j++) m += win[j];
903
+ m /= period;
904
+ var s2 = 0;
905
+ for (j = 0; j < period; j++) s2 += (win[j] - m) * (win[j] - m);
906
+ var sd = Math.sqrt(s2 / period);
907
+ mid.push({ time: bars[i].time, value: m });
908
+ up.push({ time: bars[i].time, value: m + mult * sd });
909
+ lo.push({ time: bars[i].time, value: m - mult * sd });
910
+ }
911
+ }
912
+ return { mid: mid, up: up, lo: lo };
913
+ }
914
+ function calcRSI(bars, period) { // Wilder smoothing
915
+ var out = [], gain = 0, loss = 0;
916
+ for (var i = 1; i < bars.length; i++) {
917
+ var ch = bars[i].close - bars[i - 1].close;
918
+ if (i <= period) { gain += Math.max(ch, 0); loss += Math.max(-ch, 0); }
919
+ if (i === period) { gain /= period; loss /= period; }
920
+ else if (i > period) {
921
+ gain = (gain * (period - 1) + Math.max(ch, 0)) / period;
922
+ loss = (loss * (period - 1) + Math.max(-ch, 0)) / period;
923
+ }
924
+ if (i >= period) out.push({ time: bars[i].time, value: loss === 0 ? 100 : 100 - 100 / (1 + gain / loss) });
925
+ }
926
+ return out;
927
+ }
928
+ function emaOfSeries(values, period) {
929
+ var k = 2 / (period + 1), out = [], prev = null;
930
+ for (var i = 0; i < values.length; i++) {
931
+ prev = prev === null ? values[i].value : values[i].value * k + prev * (1 - k);
932
+ out.push({ time: values[i].time, value: prev });
933
+ }
934
+ return out;
935
+ }
936
+ function calcMACD(bars, f, sl, sig) {
937
+ var closes = [];
938
+ for (var i = 0; i < bars.length; i++) closes.push({ time: bars[i].time, value: bars[i].close });
939
+ var ef = emaOfSeries(closes, f), es = emaOfSeries(closes, sl);
940
+ var line = [];
941
+ for (i = sl - 1; i < closes.length; i++) line.push({ time: closes[i].time, value: ef[i].value - es[i].value });
942
+ var signal = emaOfSeries(line, sig);
943
+ var hist = [];
944
+ for (i = 0; i < line.length; i++) hist.push({ time: line[i].time, value: line[i].value - signal[i].value });
945
+ return { line: line, signal: signal, hist: hist };
946
+ }
947
+ function smaOfSeries(values, period) {
948
+ var out = [], sum = 0;
949
+ for (var i = 0; i < values.length; i++) {
950
+ sum += values[i].value;
951
+ if (i >= period) sum -= values[i - period].value;
952
+ if (i >= period - 1) out.push({ time: values[i].time, value: sum / period });
953
+ }
954
+ return out;
955
+ }
956
+ function calcStoch(bars, kP, dP, smooth) {
957
+ var raw = [];
958
+ for (var i = kP - 1; i < bars.length; i++) {
959
+ var hh = -Infinity, ll = Infinity;
960
+ for (var j = i - kP + 1; j <= i; j++) { if (bars[j].high > hh) hh = bars[j].high; if (bars[j].low < ll) ll = bars[j].low; }
961
+ raw.push({ time: bars[i].time, value: hh === ll ? 50 : (bars[i].close - ll) / (hh - ll) * 100 });
962
+ }
963
+ var k = smooth > 1 ? smaOfSeries(raw, smooth) : raw;
964
+ return { k: k, d: smaOfSeries(k, dP) };
965
+ }
966
+
967
+ Chart.prototype.addIndicator = function (kind, params, color) {
968
+ var def = IND_DEFS[kind];
969
+ if (!def) return null;
970
+ if (this.opt.maxIndicators > 0 && this.indicators.length >= this.opt.maxIndicators) {
971
+ if (this.opt.onLimit) this.opt.onLimit("indicators", this.opt.maxIndicators);
972
+ return null;
973
+ }
974
+ var ind = {
975
+ id: "i" + Math.round(performance.now() * 1000) + "_" + this.indicators.length,
976
+ kind: kind,
977
+ params: Object.assign({}, def.params, params || {}),
978
+ color: color || IND_PALETTE[this._indColor++ % IND_PALETTE.length],
979
+ };
980
+ this.indicators.push(ind);
981
+ this._applyIndicators();
982
+ this._persistInd();
983
+ return ind.id;
984
+ };
985
+ Chart.prototype.removeIndicator = function (id) {
986
+ this.indicators = this.indicators.filter(function (x) { return x.id !== id; });
987
+ this._applyIndicators();
988
+ this._persistInd();
989
+ };
990
+ Chart.prototype.updateIndicator = function (id, params) {
991
+ for (var i = 0; i < this.indicators.length; i++) {
992
+ if (this.indicators[i].id === id) Object.assign(this.indicators[i].params, params || {});
993
+ }
994
+ this._applyIndicators();
995
+ this._persistInd();
996
+ };
997
+ Chart.prototype.setIndicatorStyle = function (id, style) {
998
+ for (var i = 0; i < this.indicators.length; i++) {
999
+ if (this.indicators[i].id === id) {
1000
+ if (style && style.color) this.indicators[i].color = style.color;
1001
+ if (style && "label" in style) this.indicators[i].label = style.label || undefined;
1002
+ }
1003
+ }
1004
+ this._applyIndicators();
1005
+ this._persistInd();
1006
+ };
1007
+ Chart.prototype.getIndicators = function () { return this.indicators.slice(); };
1008
+ // Per-bar markers: entries, exits, signals. Sorted once; drawn above the
1009
+ // bar's high or below its low with a small gap that scales with spacing.
1010
+ // "BTC · 1H · 79,822 −0.02%" — the render header the production widget
1011
+ // shows; price and change refresh with every setData/update.
1012
+ Chart.prototype.setSymbolInfo = function (info) {
1013
+ this._symbolInfo = info || null;
1014
+ if (!this._header) {
1015
+ var h = document.createElement("div");
1016
+ h.className = "trc-header";
1017
+ var clickable = !!(this.opt.symbolSearch && this.opt.symbolSearch.search);
1018
+ h.style.cssText = "position:absolute;left:" + (this.opt.ui ? 56 : 12) + "px;top:10px;z-index:4;display:flex;align-items:baseline;gap:8px;" +
1019
+ "font:800 15px -apple-system,'Segoe UI',sans-serif;user-select:none;" +
1020
+ (clickable ? "cursor:pointer;" : "pointer-events:none;");
1021
+ if (clickable) {
1022
+ var self2 = this;
1023
+ h.title = "Search ticker or name";
1024
+ h.addEventListener("click", function () { self2._toggleSearch(); });
1025
+ }
1026
+ this.el.appendChild(h);
1027
+ this._header = h;
1028
+ }
1029
+ this._paintHeader();
1030
+ };
1031
+ Chart.prototype._paintHeader = function () {
1032
+ if (!this._header) return;
1033
+ var o = this.opt, inf = this._symbolInfo || {};
1034
+ var n = this.bars.length;
1035
+ var last = n ? this.bars[n - 1] : null;
1036
+ var prev = n > 1 ? this.bars[n - 2] : null;
1037
+ var px = last ? fmtPrice(last.close, last.close >= 1000 ? 2 : 4) : "";
1038
+ // change is measured from the previous SESSION close when the host gives
1039
+ // us one (quote-screen semantics), else from the previous bar
1040
+ var base = isNum(this._prevCloseOverride) ? this._prevCloseOverride : (prev ? prev.close : null);
1041
+ var chg = last && base ? (last.close - base) / base * 100 : null;
1042
+ var up = chg !== null && chg >= 0;
1043
+ this._header.innerHTML =
1044
+ '<span style="color:' + (o.tagText || "#e6edf3") + ';font-size:17px;">' + (inf.ticker || "") + "</span>" +
1045
+ (inf.tf ? '<span style="color:' + o.textColor + ';font-weight:700;font-size:12px;">' + inf.tf + "</span>" : "") +
1046
+ (px ? '<span style="color:' + (up ? o.upColor : o.downColor) + ';">' + px + "</span>" : "") +
1047
+ (chg !== null ? '<span style="color:' + (up ? o.upColor : o.downColor) + ';font-weight:700;font-size:12px;">' +
1048
+ (up ? "+" : "") + chg.toFixed(2) + "%</span>" : "") +
1049
+ (inf.title ? '<span style="color:' + o.textColor + ';font-weight:600;font-size:12px;">' + inf.title + "</span>" : "");
1050
+ };
1051
+
1052
+ // The indicator legend with LIVE numbers: label in the series color plus
1053
+ // the value under the crosshair (or the last bar when the cursor is away).
1054
+ Chart.prototype._paintLegend = function (barTime) {
1055
+ if (!this.indicators.length) { if (this._legend) this._legend.innerHTML = ""; return; }
1056
+ if (!this._legend) {
1057
+ var lg = document.createElement("div");
1058
+ lg.className = "trc-legend";
1059
+ lg.style.cssText = "position:absolute;left:" + (this.opt.ui ? 56 : 12) + "px;top:34px;right:80px;z-index:4;display:flex;flex-wrap:wrap;gap:2px 12px;" +
1060
+ "font:700 " + ((this.h || 600) < 560 ? 10 : 11) + "px -apple-system,'Segoe UI',sans-serif;pointer-events:none;user-select:none;";
1061
+ this.el.appendChild(lg);
1062
+ this._legend = lg;
1063
+ }
1064
+ var t = barTime;
1065
+ if (t === undefined || t === null) {
1066
+ var n = this.bars.length;
1067
+ t = n ? this.bars[n - 1].time : null;
1068
+ }
1069
+ var html = "";
1070
+ for (var i = 0; i < this.indicators.length; i++) {
1071
+ var ind = this.indicators[i], def = IND_DEFS[ind.kind];
1072
+ var label = ind.label || (def.label + (ind.params.p ? " " + ind.params.p : ind.kind === "macd" ? " " + ind.params.f + "/" + ind.params.s + "/" + ind.params.sig : ind.kind === "stoch" ? " " + ind.params.k + "/" + ind.params.d + "/" + ind.params.s : ""));
1073
+ var val = "";
1074
+ if (t !== null && ind._legendRef) {
1075
+ var v = ind._legendRef.byTime.get(t);
1076
+ if (isNum(v)) val = " " + fmtPrice(v, v >= 1000 ? 2 : v >= 10 ? 2 : 4);
1077
+ }
1078
+ html += '<span style="color:' + (ind.color || "#8b949e") + ';white-space:nowrap;">' + label + val + "</span>";
1079
+ }
1080
+ this._legend.innerHTML = html;
1081
+ };
1082
+
1083
+ Chart.prototype.setMarkers = function (list) {
1084
+ this.markers = (list || []).slice().sort(function (a, b) { return a.time - b.time; });
1085
+ this._paint();
1086
+ };
1087
+ Chart.prototype._heikin = function () {
1088
+ if (this._ha) return this._ha;
1089
+ var out = [], po = null, pc = null;
1090
+ for (var i = 0; i < this.bars.length; i++) {
1091
+ var b = this.bars[i];
1092
+ var hc = (b.open + b.high + b.low + b.close) / 4;
1093
+ var ho = po === null ? (b.open + b.close) / 2 : (po + pc) / 2;
1094
+ out.push({ time: b.time, open: ho, close: hc,
1095
+ high: Math.max(b.high, ho, hc), low: Math.min(b.low, ho, hc), volume: b.volume });
1096
+ po = ho; pc = hc;
1097
+ }
1098
+ this._ha = out;
1099
+ return out;
1100
+ };
1101
+ Chart.prototype.setChartType = function (t) {
1102
+ if (["candles", "hollow", "heikin", "bars", "line", "area", "baseline"].indexOf(t) < 0) return;
1103
+ this.chartType = t;
1104
+ this._paint();
1105
+ };
1106
+ Chart.prototype._persistInd = function () {
1107
+ if (!this._persistKey) return;
1108
+ try { localStorage.setItem(this._persistKey + ":ind", JSON.stringify(this.indicators)); } catch (e) {}
1109
+ };
1110
+
1111
+ // Recompute everything the indicator list implies: overlay lines on the
1112
+ // price pane, one oscillator pane per oscillator, guides included.
1113
+ Chart.prototype._applyIndicators = function () {
1114
+ var self = this;
1115
+ // wipe indicator-owned series (ids prefixed "@") and every osc pane
1116
+ for (var pi = 0; pi < this.panes.length; pi++) {
1117
+ var pn = this.panes[pi], id;
1118
+ for (id in pn.lines) if (id.charAt(0) === "@") delete pn.lines[id];
1119
+ for (id in pn.hists) if (id.charAt(0) === "@") delete pn.hists[id];
1120
+ }
1121
+ this.panes = [this.panes[0]];
1122
+ this.panes[0].guides = [];
1123
+ var bars = this.bars, pane = 1;
1124
+ this.indicators.forEach(function (ind) {
1125
+ var P = ind.params, key = "@" + ind.id;
1126
+ var defL = IND_DEFS[ind.kind];
1127
+ var indLabel = ind.label || (defL.label + (P.p ? " " + P.p
1128
+ : ind.kind === "macd" ? " " + (P.f || 12) + "/" + (P.s || 26) + "/" + (P.sig || 9)
1129
+ : ind.kind === "stoch" ? " " + (P.k || 14) + "/" + (P.d || 3) + "/" + (P.s || 3) : ""));
1130
+ if (ind.kind === "ema") self.addLine(key, calcEMA(bars, P.p || 20), ind.color, 1.4);
1131
+ else if (ind.kind === "sma") self.addLine(key, calcSMA(bars, P.p || 50), ind.color, 1.4);
1132
+ else if (ind.kind === "vwap") self.addLine(key, calcVWAP(bars), ind.color, 1.4);
1133
+ else if (ind.kind === "bb") {
1134
+ var bb = calcBB(bars, P.p || 20, P.mult || 2);
1135
+ self.addLine(key + "u", bb.up, ind.color, 1);
1136
+ self.addLine(key + "m", bb.mid, ind.color, 1);
1137
+ self.addLine(key + "l", bb.lo, ind.color, 1);
1138
+ } else if (ind.kind === "rsi") {
1139
+ self.addLine(key, calcRSI(bars, P.p || 14), ind.color, 1.4, pane);
1140
+ self.addGuide(pane, 70, "rgba(248,81,73,0.5)");
1141
+ self.addGuide(pane, 30, "rgba(0,217,126,0.5)");
1142
+ self.panes[pane].title = indLabel;
1143
+ self.panes[pane].titleColor = ind.color;
1144
+ pane++;
1145
+ } else if (ind.kind === "macd") {
1146
+ var m = calcMACD(bars, P.f || 12, P.s || 26, P.sig || 9);
1147
+ self.addHistogram(key + "h", m.hist, pane);
1148
+ self.addLine(key, m.line, "#58a6ff", 1.2, pane);
1149
+ self.addLine(key + "s", m.signal, "#f0b429", 1.2, pane);
1150
+ self.addGuide(pane, 0, "rgba(139,148,158,0.4)");
1151
+ self.panes[pane].title = indLabel;
1152
+ self.panes[pane].titleColor = ind.color;
1153
+ pane++;
1154
+ } else if (ind.kind === "stoch") {
1155
+ var st = calcStoch(bars, P.k || 14, P.d || 3, P.s || 3);
1156
+ self.addLine(key, st.k, "#58a6ff", 1.2, pane);
1157
+ self.addLine(key + "d", st.d, "#f0b429", 1.2, pane);
1158
+ self.addGuide(pane, 80, "rgba(248,81,73,0.5)");
1159
+ self.addGuide(pane, 20, "rgba(0,217,126,0.5)");
1160
+ self.panes[pane].title = indLabel;
1161
+ self.panes[pane].titleColor = ind.color;
1162
+ pane++;
1163
+ }
1164
+ // legend reference: the indicator's primary line, wherever it landed
1165
+ var refKey = "@" + ind.id;
1166
+ ind._legendRef = null;
1167
+ for (var pi3 = 0; pi3 < self.panes.length; pi3++) {
1168
+ if (self.panes[pi3].lines[refKey]) { ind._legendRef = self.panes[pi3].lines[refKey]; break; }
1169
+ if (self.panes[pi3].lines[refKey + "m"]) { ind._legendRef = self.panes[pi3].lines[refKey + "m"]; break; }
1170
+ }
1171
+ });
1172
+ this._paintLegend(null);
1173
+ this._paint();
1174
+ };
1175
+
1176
+
1177
+ Chart.prototype.addPriceLine = function (price, color, label) {
1178
+ this.priceLines.push({ price: price, color: color || "#8b949e", label: label });
1179
+ this._paint();
1180
+ };
1181
+ Chart.prototype.clearPriceLines = function () { this.priceLines = []; this._paint(); };
1182
+ Chart.prototype.onCrosshair = function (cb) { this.crosshairCb = cb; };
1183
+ // cb({firstVisible, lastVisible, barsLeft}) after every pan/zoom; fires at
1184
+ // most once per paint. barsLeft small = time to prepend history.
1185
+ Chart.prototype.onVisibleRangeChange = function (cb) { this._rangeCb = cb; };
1186
+ // Older bars arrive in front; the visible frame must not move. Drawings
1187
+ // and markers anchor to TIME, so only rightIndex needs the shift — the
1188
+ // exact bug class the production lazy-loader taught us.
1189
+ Chart.prototype.prependBars = function (older) {
1190
+ if (!older || !older.length) return;
1191
+ var cut = this.bars.length ? this.bars[0].time : Infinity;
1192
+ var add = older.filter(function (b) { return b.time < cut; }).sort(function (a, b) { return a.time - b.time; });
1193
+ if (!add.length) return;
1194
+ this.bars = add.concat(this.bars);
1195
+ this.rightIndex += add.length;
1196
+ this._ha = null;
1197
+ if (this.indicators.length) this._applyIndicators();
1198
+ else this._paint();
1199
+ };
1200
+ // ── Drawings ─────────────────────────────────────────────────────────
1201
+ // The production widget's full set, ported (it is TradeRead code): trend,
1202
+ // hline, rect, fib retracement, text; cursor selects, moves and resizes;
1203
+ // Delete removes the selection; persistence is debounced and strips the
1204
+ // cached text width, and a persist key makes saving per-symbol.
1205
+ Chart.prototype.setTool = function (t) {
1206
+ this.tool = t || null;
1207
+ this.el.style.cursor = t ? "crosshair" : "";
1208
+ this._touchLock();
1209
+ };
1210
+ // While a tool is armed or a shape is being dragged, the chart owns the
1211
+ // touch: without touch-action none the browser steals the gesture, fires
1212
+ // a cancel and kills the line mid-draw (paid for once in production).
1213
+ Chart.prototype._touchLock = function () {
1214
+ this.overlay.style.touchAction = (this.tool || this._draft || this._dragDraw || this._sepDrag || this._axisDrag) ? "none" : "";
1215
+ };
1216
+ Chart.prototype.onToolDone = function (cb) { this._toolDoneCb = cb; };
1217
+ Chart.prototype.clearDrawings = function () { this.drawings = []; this._draft = null; this._selected = null; this._persist(); this._paintDrawings(); };
1218
+ // The style bar floats NEXT TO the selected drawing (the corner chip went
1219
+ // unseen — owner, 7 Sep 2026): color + delete, tracking the shape as it
1220
+ // moves, clamped inside the plot.
1221
+ Chart.prototype._syncStyleChip = function () {
1222
+ var self = this, o = this.opt;
1223
+ var d = null;
1224
+ for (var i = 0; i < this.drawings.length; i++) if (this.drawings[i].id === this._selected) d = this.drawings[i];
1225
+ if (!d) { if (this._styleChip) { this._styleChip.remove(); this._styleChip = null; } return; }
1226
+ if (!this._styleChip) {
1227
+ var chip = document.createElement("div");
1228
+ chip.className = "trc-style";
1229
+ chip.style.cssText = "position:absolute;z-index:9;display:flex;align-items:center;gap:6px;" +
1230
+ "background:" + o.background + ";border:1px solid " + o.separatorColor + ";" +
1231
+ "border-radius:10px;padding:5px 7px;box-shadow:0 6px 20px rgba(0,0,0,0.35);";
1232
+ chip.innerHTML =
1233
+ '<input type="color" title="Color" style="width:22px;height:22px;padding:0;border:none;border-radius:6px;background:none;cursor:pointer;">' +
1234
+ '<button type="button" data-act="del" title="Delete drawing (Del)" style="border:none;background:none;color:' + o.downColor + ';cursor:pointer;width:22px;height:22px;font:700 13px -apple-system,sans-serif;line-height:1;">' +
1235
+ SVGI('<path d="M3 4.5 H13 M6.5 4.5 V3 H9.5 V4.5 M4.5 4.5 L5.2 13 H10.8 L11.5 4.5 M7 7 V10.5 M9 7 V10.5"/>') + "</button>";
1236
+ chip.querySelector("input").addEventListener("input", function () {
1237
+ var sel2 = null;
1238
+ for (var j = 0; j < self.drawings.length; j++) if (self.drawings[j].id === self._selected) sel2 = self.drawings[j];
1239
+ if (sel2) { sel2.color = this.value; self._persist(); self._paintDrawings(); }
1240
+ });
1241
+ chip.querySelector('[data-act="del"]').addEventListener("click", function () { self.deleteSelected(); });
1242
+ this.el.appendChild(chip);
1243
+ this._styleChip = chip;
1244
+ }
1245
+ this._styleChip.querySelector("input").value = d.color || DRAW_COLOR;
1246
+ this._placeStyleChip(d);
1247
+ };
1248
+ Chart.prototype._placeStyleChip = function (d) {
1249
+ if (!this._styleChip) return;
1250
+ var pp = this.panes[0];
1251
+ var xs = [], ys = [];
1252
+ if (isNum(d.t1)) { xs.push(this.timeToX(d.t1)); }
1253
+ if (isNum(d.t2)) { xs.push(this.timeToX(d.t2)); }
1254
+ if (isNum(d.p1)) { ys.push(pp.toY(d.p1)); }
1255
+ if (isNum(d.p2)) { ys.push(pp.toY(d.p2)); }
1256
+ if (d.type === "hline") { xs = [this._plotW() / 2]; }
1257
+ var x = xs.length ? Math.min.apply(null, xs) : 20;
1258
+ var y = ys.length ? Math.min.apply(null, ys) : 20;
1259
+ var w = this._styleChip.offsetWidth || 64, h = this._styleChip.offsetHeight || 34;
1260
+ this._styleChip.style.left = clamp(x, 46, this._plotW() - w - 6) + "px";
1261
+ this._styleChip.style.top = clamp(y - h - 10, 4, this._plotH() - h - 4) + "px";
1262
+ };
1263
+
1264
+ Chart.prototype.deleteSelected = function () {
1265
+ if (!this._selected) return;
1266
+ var id = this._selected;
1267
+ this.drawings = this.drawings.filter(function (d) { return d.id !== id; });
1268
+ this._selected = null;
1269
+ this._persist();
1270
+ this._syncStyleChip();
1271
+ this._paintDrawings();
1272
+ };
1273
+ Chart.prototype._toPoints = function (d) {
1274
+ // The site (server sanitizer + index.html opt-in row) speaks a
1275
+ // points-based shape; emit it alongside our t/p fields so both read it.
1276
+ var pts = [];
1277
+ if (isNum(d.t1)) pts.push({ time: d.t1, price: d.p1 });
1278
+ if (isNum(d.t2)) pts.push({ time: d.t2, price: d.p2 });
1279
+ if (d.type === "hline") pts = [{ time: d.t1 || null, price: d.p1 }];
1280
+ return pts;
1281
+ };
1282
+ Chart.prototype.serializeDrawings = function () {
1283
+ var self = this;
1284
+ var arr = this.drawings.map(function (d) {
1285
+ var o = { id: d.id, type: d.type, points: self._toPoints(d) };
1286
+ if (isNum(d.t1)) o.t1 = d.t1; if (isNum(d.p1)) o.p1 = d.p1;
1287
+ if (isNum(d.t2)) o.t2 = d.t2; if (isNum(d.p2)) o.p2 = d.p2;
1288
+ if (d.color) o.color = d.color;
1289
+ if (d.text) o.text = d.text;
1290
+ if (isNum(d.size)) o.size = d.size;
1291
+ return o;
1292
+ });
1293
+ return JSON.stringify(arr);
1294
+ };
1295
+ Chart.prototype.loadDrawings = function (json) {
1296
+ var list;
1297
+ try { list = (typeof json === "string" ? JSON.parse(json) : json) || []; } catch (e) { list = []; }
1298
+ // accept the site's points-based shape and hydrate our t/p anchors
1299
+ this.drawings = list.map(function (d) {
1300
+ if (!isNum(d.t1) && d.points && d.points.length) {
1301
+ d.t1 = d.points[0].time; d.p1 = d.points[0].price;
1302
+ if (d.points[1]) { d.t2 = d.points[1].time; d.p2 = d.points[1].price; }
1303
+ }
1304
+ return d;
1305
+ });
1306
+ this._selected = null;
1307
+ this._syncStyleChip();
1308
+ this._paintDrawings();
1309
+ };
1310
+ Chart.prototype.setPersistKey = function (key) {
1311
+ this._persistKey = key || null;
1312
+ if (key) {
1313
+ try { this.loadDrawings(localStorage.getItem(key)); } catch (e) { this.loadDrawings([]); }
1314
+ try {
1315
+ var ind = JSON.parse(localStorage.getItem(key + ":ind"));
1316
+ if (Array.isArray(ind)) { this.indicators = ind; this._applyIndicators(); }
1317
+ } catch (e) {}
1318
+ }
1319
+ };
1320
+ Chart.prototype._persist = function () {
1321
+ var self = this;
1322
+ if (!this._persistKey) return;
1323
+ clearTimeout(this._saveT);
1324
+ this._saveT = setTimeout(function () {
1325
+ try { localStorage.setItem(self._persistKey, self.serializeDrawings()); } catch (e) {}
1326
+ }, 300);
1327
+ };
1328
+
1329
+ var FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
1330
+ var DRAW_COLOR = "#818cf8";
1331
+
1332
+ Chart.prototype._paintDrawings = function () {
1333
+ var c = this.dctx, W = this._plotW(), pp = this.panes[0], self = this;
1334
+ if (this._selected && this._styleChip) {
1335
+ for (var si = 0; si < this.drawings.length; si++) {
1336
+ if (this.drawings[si].id === this._selected) { this._placeStyleChip(this.drawings[si]); break; }
1337
+ }
1338
+ }
1339
+ c.clearRect(0, 0, this.w, this.h);
1340
+ var dec = stepDecimals(niceStep(pp.max - pp.min, 8));
1341
+ var list = this._draft ? this.drawings.concat([this._draft]) : this.drawings;
1342
+ for (var k = 0; k < list.length; k++) {
1343
+ var d = list[k];
1344
+ var sel = d.id && d.id === this._selected;
1345
+ var dc = d.color || DRAW_COLOR;
1346
+ c.strokeStyle = dc;
1347
+ c.fillStyle = dc;
1348
+ c.lineWidth = sel ? 2 : 1.4;
1349
+ var x1 = isNum(d.t1) ? this.timeToX(d.t1) : null, y1 = isNum(d.p1) ? pp.toY(d.p1) : null;
1350
+ var x2 = isNum(d.t2) ? this.timeToX(d.t2) : null, y2 = isNum(d.p2) ? pp.toY(d.p2) : null;
1351
+ if (d.type === "hline") {
1352
+ if (y1 === null || y1 < pp.y0 || y1 > pp.y0 + pp.h) continue;
1353
+ c.beginPath(); c.moveTo(0, Math.round(y1) + 0.5); c.lineTo(W, Math.round(y1) + 0.5); c.stroke();
1354
+ } else if (d.type === "trend") {
1355
+ c.beginPath(); c.moveTo(x1, y1); c.lineTo(x2, y2); c.stroke();
1356
+ if (sel) { c.fillRect(x1 - 3, y1 - 3, 6, 6); c.fillRect(x2 - 3, y2 - 3, 6, 6); }
1357
+ } else if (d.type === "rect") {
1358
+ c.save(); c.globalAlpha = 0.12;
1359
+ c.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
1360
+ c.restore();
1361
+ c.strokeRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
1362
+ if (sel) c.fillRect(x2 - 3, y2 - 3, 6, 6);
1363
+ } else if (d.type === "fib") {
1364
+ var xa = Math.min(x1, x2), xb = Math.max(x1, x2);
1365
+ c.font = "10px -apple-system, sans-serif";
1366
+ for (var f = 0; f < FIB_LEVELS.length; f++) {
1367
+ var lv = FIB_LEVELS[f];
1368
+ var price = d.p2 - (d.p2 - d.p1) * lv;
1369
+ var fy = pp.toY(price);
1370
+ c.globalAlpha = lv === 0 || lv === 1 ? 0.9 : 0.55;
1371
+ c.beginPath(); c.moveTo(xa, Math.round(fy) + 0.5); c.lineTo(xb, Math.round(fy) + 0.5); c.stroke();
1372
+ c.fillText(lv.toFixed(3) + " " + fmtPrice(price, dec), xb + 5, fy + 3);
1373
+ c.globalAlpha = 1;
1374
+ }
1375
+ if (sel) c.fillRect(x2 - 3, y2 - 3, 6, 6);
1376
+ } else if (d.type === "text") {
1377
+ var fs = d.size || 12;
1378
+ c.font = "600 " + fs + "px -apple-system, sans-serif";
1379
+ d._tw = c.measureText(d.text || "").width; // cached for hit-testing, never persisted
1380
+ c.fillText(d.text || "", x1, y1);
1381
+ if (sel) {
1382
+ c.save(); c.globalAlpha = 0.35;
1383
+ c.strokeRect(x1 - 3, y1 - fs - 2, d._tw + 6, fs + 8);
1384
+ c.restore();
1385
+ c.fillRect(x1 + d._tw + 3, y1 - 3, 6, 6); // size grip: drag down = bigger
1386
+ }
1387
+ }
1388
+ }
1389
+ };
1390
+
1391
+ function distToSeg(x, y, a, b) {
1392
+ var dx = b.x - a.x, dy = b.y - a.y;
1393
+ var len2 = dx * dx + dy * dy;
1394
+ var t = len2 ? clamp(((x - a.x) * dx + (y - a.y) * dy) / len2, 0, 1) : 0;
1395
+ var px = a.x + t * dx, py = a.y + t * dy;
1396
+ return Math.sqrt((x - px) * (x - px) + (y - py) * (y - py));
1397
+ }
1398
+
1399
+ // Topmost hit wins (reverse order); a selected shape's handle beats its body.
1400
+ Chart.prototype._hitTest = function (x, y) {
1401
+ var pp = this.panes[0];
1402
+ for (var k = this.drawings.length - 1; k >= 0; k--) {
1403
+ var d = this.drawings[k];
1404
+ var x1 = isNum(d.t1) ? this.timeToX(d.t1) : null, y1 = isNum(d.p1) ? pp.toY(d.p1) : null;
1405
+ var x2 = isNum(d.t2) ? this.timeToX(d.t2) : null, y2 = isNum(d.p2) ? pp.toY(d.p2) : null;
1406
+ if (d.id === this._selected && x2 !== null && Math.abs(x - x2) < 7 && Math.abs(y - y2) < 7 &&
1407
+ (d.type === "rect" || d.type === "fib" || d.type === "trend")) return { d: d, handle: "p2" };
1408
+ if (d.id === this._selected && d.type === "text") {
1409
+ var fsz = d.size || 12, tw2 = d._tw || (d.text || "").length * fsz * 0.62;
1410
+ if (Math.abs(x - (x1 + tw2 + 6)) < 8 && Math.abs(y - y1) < 8) return { d: d, handle: "size" };
1411
+ }
1412
+ if (d.id === this._selected && d.type === "trend" && Math.abs(x - x1) < 7 && Math.abs(y - y1) < 7) return { d: d, handle: "p1" };
1413
+ if (d.type === "hline") { if (Math.abs(y - y1) < 6) return { d: d, handle: null }; }
1414
+ else if (d.type === "trend") { if (distToSeg(x, y, { x: x1, y: y1 }, { x: x2, y: y2 }) < 6) return { d: d, handle: null }; }
1415
+ else if (d.type === "rect" || d.type === "fib") {
1416
+ if (x >= Math.min(x1, x2) - 4 && x <= Math.max(x1, x2) + 4 &&
1417
+ y >= Math.min(y1, y2) - 4 && y <= Math.max(y1, y2) + 4) return { d: d, handle: null };
1418
+ } else if (d.type === "text") {
1419
+ var fs = d.size || 12, tw = d._tw || (d.text || "").length * fs * 0.62;
1420
+ if (x >= x1 - 4 && x <= x1 + tw + 4 && y >= y1 - fs - 4 && y <= y1 + 6) return { d: d, handle: null };
1421
+ }
1422
+ }
1423
+ return null;
1424
+ };
1425
+ Chart.prototype.applyOptions = function (o) {
1426
+ Object.assign(this.opt, o || {});
1427
+ this.el.style.background = this.opt.background;
1428
+ // the mounted chrome carries colors from mount time — remount it in the
1429
+ // new palette (open panels close; their state is one click away)
1430
+ if (this._rail) {
1431
+ ["_rail", "_indPanel", "_typeMenu", "_styleChip", "_searchBox", "_fsBtn"].forEach(function (k) {
1432
+ if (this[k]) { this[k].remove(); this[k] = null; }
1433
+ }, this);
1434
+ this._mountUI();
1435
+ this._syncStyleChip();
1436
+ }
1437
+ this._paintHeader();
1438
+ this._paint();
1439
+ };
1440
+ Chart.prototype.scrollToRealtime = function () { this.rightIndex = this.bars.length - 1; this._paint(); };
1441
+ // Fit: back to the default frame — bar spacing, right edge and vertical
1442
+ // zoom all reset (the production widget's refit, whole-chart edition).
1443
+ Chart.prototype.fit = function () {
1444
+ this.barSpacing = this.opt.barSpacing;
1445
+ this.rightIndex = this.bars.length - 1;
1446
+ this._vZoom = 1;
1447
+ this._vOff = 0;
1448
+ this._paint();
1449
+ };
1450
+ Chart.prototype.remove = function () { this._ro.disconnect(); this.el.innerHTML = ""; };
1451
+
1452
+ // ── Painting ─────────────────────────────────────────────────────────
1453
+ Chart.prototype._paint = function () {
1454
+ var c = this.ctx, o = this.opt, W = this._plotW(), H = this._plotH();
1455
+ if (!W || !H) return;
1456
+ this._layout();
1457
+ var rng = this._visibleRange(), lo = rng[0], hi = rng[1];
1458
+ for (var pi = 0; pi < this.panes.length; pi++) this.panes[pi].computeScale(lo, hi);
1459
+
1460
+ c.clearRect(0, 0, this.w, this.h);
1461
+ c.fillStyle = o.background;
1462
+ c.fillRect(0, 0, this.w, this.h);
1463
+ c.font = o.font;
1464
+
1465
+ var i, b, x, y, v;
1466
+
1467
+ // time grid first (spans all panes)
1468
+ var barStep = Math.max(1, Math.round(90 / this.barSpacing));
1469
+ var lastDay = null;
1470
+ c.textBaseline = "top";
1471
+ for (i = lo - (lo % barStep); i <= hi; i += barStep) {
1472
+ b = this.bars[i]; if (!b) continue;
1473
+ x = this.indexToX(i);
1474
+ if (x < 0 || x > W) continue;
1475
+ c.strokeStyle = o.gridColor;
1476
+ c.beginPath(); c.moveTo(Math.round(x) + 0.5, 0); c.lineTo(Math.round(x) + 0.5, H); c.stroke();
1477
+ var d = new Date(b.time * 1000);
1478
+ var daily = (this._barSec || 3600) >= 86400;
1479
+ var key = daily ? (d.getUTCFullYear() + "-" + d.getUTCMonth()) : (d.getUTCMonth() + "-" + d.getUTCDate());
1480
+ var label;
1481
+ if (daily) {
1482
+ label = key !== lastDay ? MONTHS[d.getUTCMonth()] + (d.getUTCMonth() === 0 ? " " + d.getUTCFullYear() : "") : "" + d.getUTCDate();
1483
+ } else {
1484
+ label = key !== lastDay
1485
+ ? (d.getUTCDate() === 1 ? MONTHS[d.getUTCMonth()] : d.getUTCDate() + " " + MONTHS[d.getUTCMonth()])
1486
+ : pad2(d.getUTCHours()) + ":" + pad2(d.getUTCMinutes());
1487
+ }
1488
+ lastDay = key;
1489
+ c.fillStyle = o.textColor;
1490
+ c.fillText(label, x - c.measureText(label).width / 2, H + 7);
1491
+ }
1492
+
1493
+ // ── price pane ──
1494
+ var pp = this.panes[0];
1495
+ var span = pp.max - pp.min;
1496
+ var step = niceStep(span, Math.max(3, Math.round(pp.h / 55)));
1497
+ var dec = stepDecimals(step);
1498
+ var first = Math.ceil(pp.min / step) * step;
1499
+ c.textBaseline = "middle";
1500
+ for (v = first; v <= pp.max; v += step) {
1501
+ y = pp.toY(v);
1502
+ if (y < pp.y0 + 4 || y > pp.y0 + pp.h - 4) continue;
1503
+ c.strokeStyle = o.gridColor; c.lineWidth = 1;
1504
+ c.beginPath(); c.moveTo(0, Math.round(y) + 0.5); c.lineTo(W, Math.round(y) + 0.5); c.stroke();
1505
+ c.fillStyle = o.textColor;
1506
+ c.fillText(fmtPrice(v, dec), W + 8, y);
1507
+ }
1508
+
1509
+ var volTop = pp.h * (1 - o.volumeHeightPct);
1510
+ var maxVol = 0;
1511
+ for (i = lo; i <= hi; i++) { b = this.bars[i]; if (b && b.volume > maxVol) maxVol = b.volume; }
1512
+ if (maxVol > 0) {
1513
+ var bw = Math.max(1, this.barSpacing * 0.7);
1514
+ for (i = lo; i <= hi; i++) {
1515
+ b = this.bars[i]; if (!b || !b.volume) continue;
1516
+ var vx = this.indexToX(i);
1517
+ var vh = (b.volume / maxVol) * (pp.h - volTop - 2);
1518
+ c.fillStyle = b.close >= b.open ? o.volumeUp : o.volumeDown;
1519
+ c.fillRect(vx - bw / 2, pp.y0 + pp.h - vh, bw, vh);
1520
+ }
1521
+ }
1522
+
1523
+ var half = Math.max(0.5, this.barSpacing * 0.35);
1524
+ var ct = this.chartType;
1525
+ if (ct === "baseline") {
1526
+ // The base sits at the middle of the VISIBLE scale (the TradingView
1527
+ // default): half the frame is always up-territory and half down, so
1528
+ // baseline never degenerates into a plain area on a trending window
1529
+ // (owner's catch, 7 Sep 2026).
1530
+ var baseV = (pp.min + pp.max) / 2;
1531
+ var baseY = clamp(pp.toY(baseV), pp.y0, pp.y0 + pp.h);
1532
+ var seg = [];
1533
+ for (i = lo; i <= hi; i++) {
1534
+ b = this.bars[i]; if (!b) continue;
1535
+ seg.push([this.indexToX(i), pp.toY(b.close)]);
1536
+ }
1537
+ if (seg.length > 1) {
1538
+ var passes = [
1539
+ { clipY0: pp.y0, clipH: baseY - pp.y0, stroke: o.upColor, fillA: "rgba(0,217,126,0.22)", fillB: "rgba(0,217,126,0)" },
1540
+ { clipY0: baseY, clipH: pp.y0 + pp.h - baseY, stroke: o.downColor, fillA: "rgba(248,81,73,0)", fillB: "rgba(248,81,73,0.22)" },
1541
+ ];
1542
+ for (var pi2 = 0; pi2 < passes.length; pi2++) {
1543
+ var ps = passes[pi2];
1544
+ if (ps.clipH <= 0) continue;
1545
+ c.save();
1546
+ c.beginPath(); c.rect(0, ps.clipY0, W, ps.clipH); c.clip();
1547
+ c.beginPath();
1548
+ c.moveTo(seg[0][0], seg[0][1]);
1549
+ for (i = 1; i < seg.length; i++) c.lineTo(seg[i][0], seg[i][1]);
1550
+ var grB = c.createLinearGradient(0, pp.y0, 0, pp.y0 + pp.h);
1551
+ grB.addColorStop(0, ps.fillA);
1552
+ grB.addColorStop(1, ps.fillB);
1553
+ c.lineTo(seg[seg.length - 1][0], baseY); c.lineTo(seg[0][0], baseY); c.closePath();
1554
+ c.fillStyle = grB;
1555
+ c.fill();
1556
+ c.beginPath();
1557
+ c.moveTo(seg[0][0], seg[0][1]);
1558
+ for (i = 1; i < seg.length; i++) c.lineTo(seg[i][0], seg[i][1]);
1559
+ c.strokeStyle = ps.stroke; c.lineWidth = 1.6;
1560
+ c.stroke();
1561
+ c.restore();
1562
+ }
1563
+ c.strokeStyle = o.textColor; c.lineWidth = 1; c.setLineDash([3, 3]);
1564
+ c.beginPath(); c.moveTo(0, Math.round(baseY) + 0.5); c.lineTo(W, Math.round(baseY) + 0.5); c.stroke();
1565
+ c.setLineDash([]);
1566
+ }
1567
+ } else if (ct === "line" || ct === "area") {
1568
+ c.save();
1569
+ c.beginPath(); c.rect(0, pp.y0, W, pp.h); c.clip();
1570
+ c.strokeStyle = o.upColor; c.lineWidth = 1.6;
1571
+ c.beginPath();
1572
+ var startedM = false, firstX = null, lastX = null;
1573
+ for (i = lo; i <= hi; i++) {
1574
+ b = this.bars[i]; if (!b) continue;
1575
+ var mx = this.indexToX(i), my = pp.toY(b.close);
1576
+ if (!startedM) { c.moveTo(mx, my); startedM = true; firstX = mx; } else c.lineTo(mx, my);
1577
+ lastX = mx;
1578
+ }
1579
+ c.stroke();
1580
+ if (ct === "area" && startedM) {
1581
+ var gr = c.createLinearGradient(0, pp.y0, 0, pp.y0 + pp.h);
1582
+ gr.addColorStop(0, "rgba(0,217,126,0.25)");
1583
+ gr.addColorStop(1, "rgba(0,217,126,0)");
1584
+ c.lineTo(lastX, pp.y0 + pp.h); c.lineTo(firstX, pp.y0 + pp.h); c.closePath();
1585
+ c.fillStyle = gr;
1586
+ c.fill();
1587
+ }
1588
+ c.restore();
1589
+ } else if (ct === "bars") {
1590
+ for (i = lo; i <= hi; i++) {
1591
+ b = this.bars[i]; if (!b) continue;
1592
+ var bx2 = this.indexToX(i);
1593
+ if (bx2 < -this.barSpacing || bx2 > W + this.barSpacing) continue;
1594
+ var bup = b.close >= b.open;
1595
+ c.strokeStyle = bup ? o.upColor : o.downColor;
1596
+ c.lineWidth = 1;
1597
+ var xR = Math.round(bx2) + 0.5;
1598
+ c.beginPath();
1599
+ c.moveTo(xR, pp.toY(b.high)); c.lineTo(xR, pp.toY(b.low));
1600
+ c.moveTo(xR - half, pp.toY(b.open)); c.lineTo(xR, pp.toY(b.open));
1601
+ c.moveTo(xR, pp.toY(b.close)); c.lineTo(xR + half, pp.toY(b.close));
1602
+ c.stroke();
1603
+ }
1604
+ } else {
1605
+ var hollow = ct === "hollow";
1606
+ var src = ct === "heikin" ? this._heikin() : this.bars;
1607
+ for (i = lo; i <= hi; i++) {
1608
+ b = src[i]; if (!b) continue;
1609
+ var cx = this.indexToX(i);
1610
+ if (cx < -this.barSpacing || cx > W + this.barSpacing) continue;
1611
+ var up = b.close >= b.open;
1612
+ var yO = pp.toY(b.open), yC = pp.toY(b.close);
1613
+ var yH = pp.toY(b.high), yL = pp.toY(b.low);
1614
+ c.strokeStyle = up ? o.wickUp : o.wickDown;
1615
+ c.lineWidth = 1;
1616
+ c.beginPath(); c.moveTo(Math.round(cx) + 0.5, yH); c.lineTo(Math.round(cx) + 0.5, yL); c.stroke();
1617
+ var bodyY = Math.min(yO, yC), bodyH = Math.max(1, Math.abs(yC - yO));
1618
+ if (hollow && up) {
1619
+ c.strokeStyle = o.upColor;
1620
+ c.strokeRect(cx - half + 0.5, bodyY + 0.5, half * 2 - 1, bodyH);
1621
+ } else {
1622
+ c.fillStyle = up ? o.upColor : o.downColor;
1623
+ c.fillRect(cx - half, bodyY, half * 2, bodyH);
1624
+ }
1625
+ }
1626
+ }
1627
+
1628
+ // ── every pane: histograms, guides, lines (price pane has only lines) ──
1629
+ for (pi = 0; pi < this.panes.length; pi++) {
1630
+ var pn = this.panes[pi];
1631
+ c.save();
1632
+ c.beginPath(); c.rect(0, pn.y0, W, pn.h); c.clip();
1633
+
1634
+ var id;
1635
+ for (id in pn.hists) {
1636
+ var hs = pn.hists[id];
1637
+ var zeroY = pn.toY(0);
1638
+ var hw = Math.max(1, this.barSpacing * 0.55);
1639
+ for (i = lo; i <= hi; i++) {
1640
+ b = this.bars[i]; if (!b) continue;
1641
+ v = hs.byTime.get(b.time);
1642
+ if (!isNum(v)) continue;
1643
+ var hx = this.indexToX(i);
1644
+ var hy = pn.toY(v);
1645
+ c.fillStyle = v >= 0 ? hs.pos : hs.neg;
1646
+ c.fillRect(hx - hw / 2, Math.min(zeroY, hy), hw, Math.max(1, Math.abs(zeroY - hy)));
1647
+ }
1648
+ }
1649
+ for (i = 0; i < pn.guides.length; i++) {
1650
+ var g = pn.guides[i];
1651
+ var gy = pn.toY(g.value);
1652
+ c.strokeStyle = g.color; c.lineWidth = 1; c.setLineDash([4, 3]);
1653
+ c.beginPath(); c.moveTo(0, Math.round(gy) + 0.5); c.lineTo(W, Math.round(gy) + 0.5); c.stroke();
1654
+ c.setLineDash([]);
1655
+ }
1656
+ for (id in pn.lines) {
1657
+ var ln = pn.lines[id];
1658
+ c.strokeStyle = ln.color; c.lineWidth = ln.width;
1659
+ c.beginPath();
1660
+ var started = false;
1661
+ for (i = lo; i <= hi; i++) {
1662
+ b = this.bars[i]; if (!b) continue;
1663
+ v = ln.byTime.get(b.time);
1664
+ if (!isNum(v)) { started = false; continue; }
1665
+ var lx = this.indexToX(i), ly = pn.toY(v);
1666
+ if (!started) { c.moveTo(lx, ly); started = true; } else c.lineTo(lx, ly);
1667
+ }
1668
+ c.stroke();
1669
+ }
1670
+ c.restore();
1671
+
1672
+ // last-value tags on the axis, one per oscillator line — the numbers
1673
+ // the production widget shows at the scale (owner, 7 Sep 2026)
1674
+ if (pn.kind === "osc") {
1675
+ c.textBaseline = "middle";
1676
+ for (id in pn.lines) {
1677
+ var lnT = pn.lines[id];
1678
+ var lastB = this.bars[hi];
1679
+ var lastV = lastB ? lnT.byTime.get(lastB.time) : undefined;
1680
+ if (!isNum(lastV)) continue;
1681
+ var lyT = pn.toY(lastV);
1682
+ if (lyT < pn.y0 + 2 || lyT > pn.y0 + pn.h - 2) continue;
1683
+ var decT = stepDecimals(niceStep(pn.max - pn.min, 8));
1684
+ var txtT = fmtPrice(lastV, decT);
1685
+ c.fillStyle = lnT.color;
1686
+ c.fillRect(W, lyT - 8, o.priceAxisWidth, 16);
1687
+ c.fillStyle = "#0d1117";
1688
+ c.font = o.font;
1689
+ c.fillText(txtT, W + 5, lyT);
1690
+ }
1691
+ }
1692
+ // osc pane: right-axis labels for its own scale (top & bottom values)
1693
+ if (pn.kind === "osc") {
1694
+ var oStep = niceStep(pn.max - pn.min, Math.max(2, Math.round(pn.h / 45)));
1695
+ var oDec = stepDecimals(oStep);
1696
+ var oFirst = Math.ceil(pn.min / oStep) * oStep;
1697
+ c.textBaseline = "middle"; c.fillStyle = o.textColor;
1698
+ for (v = oFirst; v <= pn.max; v += oStep) {
1699
+ y = pn.toY(v);
1700
+ if (y < pn.y0 + 8 || y > pn.y0 + pn.h - 8) continue;
1701
+ c.fillText(fmtPrice(v, oDec), W + 8, y);
1702
+ }
1703
+ // separator above the pane
1704
+ c.strokeStyle = o.separatorColor;
1705
+ c.beginPath(); c.moveTo(0, pn.y0 + 0.5); c.lineTo(this.w, pn.y0 + 0.5); c.stroke();
1706
+ // the pane says what it is (owner, 7 Sep 2026)
1707
+ if (pn.title) {
1708
+ c.font = "800 10.5px -apple-system, 'Segoe UI', sans-serif";
1709
+ c.fillStyle = pn.titleColor || o.textColor;
1710
+ c.save(); c.globalAlpha = 0.9;
1711
+ // the tool rail overlays the left edge; titles start clear of it
1712
+ c.fillText(pn.title, o.ui ? 52 : 8, pn.y0 + 13);
1713
+ c.restore();
1714
+ c.font = o.font;
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+ // price lines + last-price tag (price pane only)
1720
+ var last = this.bars[this.bars.length - 1];
1721
+ var tags = this.priceLines.slice();
1722
+ if (last) tags.push({ price: last.close, color: last.close >= last.open ? o.upColor : o.downColor, _last: true });
1723
+ for (i = 0; i < tags.length; i++) {
1724
+ var t = tags[i];
1725
+ var ty = pp.toY(t.price);
1726
+ if (ty < pp.y0 || ty > pp.y0 + pp.h) continue;
1727
+ c.strokeStyle = t.color; c.lineWidth = 1; c.setLineDash([4, 3]);
1728
+ c.beginPath(); c.moveTo(0, Math.round(ty) + 0.5); c.lineTo(W, Math.round(ty) + 0.5); c.stroke();
1729
+ c.setLineDash([]);
1730
+ var txt = fmtPrice(t.price, dec);
1731
+ c.fillStyle = t._last ? t.color : o.tagBg;
1732
+ c.fillRect(W, ty - 9, o.priceAxisWidth, 18);
1733
+ c.fillStyle = t._last ? "#0d1117" : o.tagText;
1734
+ c.textBaseline = "middle";
1735
+ c.fillText(txt, W + 5, ty);
1736
+ }
1737
+
1738
+ // markers
1739
+ if (this.markers.length) {
1740
+ c.save();
1741
+ c.beginPath(); c.rect(0, pp.y0, W, pp.h); c.clip();
1742
+ var msz = clamp(this.barSpacing * 0.5, 4, 9);
1743
+ c.font = "700 10px -apple-system, sans-serif";
1744
+ for (i = 0; i < this.markers.length; i++) {
1745
+ var mk = this.markers[i];
1746
+ var mi = Math.round(this.timeToIndex(mk.time));
1747
+ b = this.bars[mi];
1748
+ if (!b || mi < lo || mi > hi) continue;
1749
+ var mx2 = this.indexToX(mi);
1750
+ var above = mk.position !== "below";
1751
+ var my2 = above ? pp.toY(b.high) - 7 : pp.toY(b.low) + 7;
1752
+ c.fillStyle = mk.color || (above ? o.downColor : o.upColor);
1753
+ c.beginPath();
1754
+ if (mk.shape === "circle") {
1755
+ c.arc(mx2, my2, msz * 0.55, 0, Math.PI * 2);
1756
+ } else if (above) { // arrow pointing down at the bar
1757
+ c.moveTo(mx2, my2); c.lineTo(mx2 - msz * 0.6, my2 - msz); c.lineTo(mx2 + msz * 0.6, my2 - msz);
1758
+ } else { // arrow pointing up at the bar
1759
+ c.moveTo(mx2, my2); c.lineTo(mx2 - msz * 0.6, my2 + msz); c.lineTo(mx2 + msz * 0.6, my2 + msz);
1760
+ }
1761
+ c.closePath(); c.fill();
1762
+ if (mk.text) {
1763
+ var mtw = c.measureText(mk.text).width;
1764
+ c.fillText(mk.text, mx2 - mtw / 2, above ? my2 - msz - 4 : my2 + msz + 12);
1765
+ }
1766
+ }
1767
+ c.restore();
1768
+ }
1769
+
1770
+ c.strokeStyle = o.gridColor;
1771
+ c.beginPath(); c.moveTo(W + 0.5, 0); c.lineTo(W + 0.5, this.h); c.stroke();
1772
+ c.beginPath(); c.moveTo(0, H + 0.5); c.lineTo(this.w, H + 0.5); c.stroke();
1773
+
1774
+ this._paintDrawings();
1775
+ this._paintCross();
1776
+ if (this._rangeCb) {
1777
+ var vr = this._visibleRange();
1778
+ this._rangeCb({ firstVisible: vr[0], lastVisible: vr[1], barsLeft: vr[0] });
1779
+ }
1780
+ };
1781
+
1782
+ Chart.prototype._paintCross = function () {
1783
+ var c = this.octx, o = this.opt, W = this._plotW(), H = this._plotH();
1784
+ c.clearRect(0, 0, this.w, this.h);
1785
+ if (!this._cross) return;
1786
+ var x = this._cross.x, y = this._cross.y;
1787
+ if (x > W || y > H) return;
1788
+ var i = clamp(Math.round(this.xToIndex(x)), 0, this.bars.length - 1);
1789
+ var b = this.bars[i];
1790
+ var bx = this.indexToX(i);
1791
+ c.strokeStyle = o.crosshair; c.lineWidth = 1; c.setLineDash([4, 3]);
1792
+ c.beginPath(); c.moveTo(Math.round(bx) + 0.5, 0); c.lineTo(Math.round(bx) + 0.5, H); c.stroke();
1793
+ c.beginPath(); c.moveTo(0, Math.round(y) + 0.5); c.lineTo(W, Math.round(y) + 0.5); c.stroke();
1794
+ c.setLineDash([]);
1795
+ c.font = o.font; c.textBaseline = "middle";
1796
+ // the y tag speaks the scale of the pane under the cursor
1797
+ var pn = this._paneAt(y) || this.panes[0];
1798
+ var val = pn.toValue(y);
1799
+ var dec = stepDecimals(niceStep(pn.max - pn.min, 8));
1800
+ var py = fmtPrice(val, dec);
1801
+ c.fillStyle = o.tagBg;
1802
+ c.fillRect(W, y - 9, o.priceAxisWidth, 18);
1803
+ c.fillStyle = o.tagText;
1804
+ c.fillText(py, W + 5, y);
1805
+ if (b) {
1806
+ var d = new Date(b.time * 1000);
1807
+ var xt = (this._barSec || 3600) >= 86400
1808
+ ? d.getUTCDate() + " " + MONTHS[d.getUTCMonth()] + " " + d.getUTCFullYear()
1809
+ : d.getUTCDate() + " " + MONTHS[d.getUTCMonth()] + " " + pad2(d.getUTCHours()) + ":" + pad2(d.getUTCMinutes());
1810
+ var tw = c.measureText(xt).width + 12;
1811
+ c.fillStyle = o.tagBg;
1812
+ c.fillRect(clamp(bx - tw / 2, 0, W - tw), H, tw, o.timeAxisHeight - 2);
1813
+ c.fillStyle = o.tagText;
1814
+ c.fillText(xt, clamp(bx - tw / 2, 0, W - tw) + 6, H + o.timeAxisHeight / 2 - 1);
1815
+ }
1816
+ this._paintLegend(b ? b.time : null);
1817
+ if (this.crosshairCb) this.crosshairCb(b ? { bar: b, index: i, price: this.panes[0].toValue(y), paneValue: val } : null);
1818
+ };
1819
+
1820
+ Chart.prototype._bind = function () {
1821
+ var self = this, el = this.overlay;
1822
+ var drag = null, pinch = null;
1823
+ // Momentum: the pan keeps gliding after release, decaying each frame —
1824
+ // the "thrown" feel every charting tool has. Any new input kills it.
1825
+ var glide = { v: 0, raf: 0, lastX: 0, lastT: 0 };
1826
+ function stopGlide() { if (glide.raf) cancelAnimationFrame(glide.raf); glide.raf = 0; glide.v = 0; }
1827
+ function startGlide() {
1828
+ if (Math.abs(glide.v) < 0.05) return;
1829
+ var last = performance.now();
1830
+ function step(now) {
1831
+ var dt = now - last; last = now;
1832
+ self.rightIndex = clamp(self.rightIndex - glide.v * dt / self.barSpacing,
1833
+ 0, self.bars.length - 1 + self.opt.rightPadBars * 2);
1834
+ self._paint();
1835
+ glide.v *= Math.pow(0.94, dt / 16.7);
1836
+ if (Math.abs(glide.v) >= 0.05) glide.raf = requestAnimationFrame(step);
1837
+ else glide.raf = 0;
1838
+ }
1839
+ glide.raf = requestAnimationFrame(step);
1840
+ }
1841
+
1842
+ el.addEventListener("mousemove", function (e) {
1843
+ var r = el.getBoundingClientRect();
1844
+ self._cross = { x: e.clientX - r.left, y: e.clientY - r.top };
1845
+ if (self._draft) {
1846
+ self._draft.t2 = self.indexToTime(self.xToIndex(self._cross.x));
1847
+ self._draft.p2 = self.panes[0].toValue(self._cross.y);
1848
+ self._paintDrawings();
1849
+ self._paintCross();
1850
+ return;
1851
+ }
1852
+ if (self._axisDrag) {
1853
+ var ad = self._axisDrag;
1854
+ if (ad.axis === "price") self._vZoom = clamp(ad.z0 * Math.exp((self._cross.y - ad.y0) * 0.004), 0.15, 8);
1855
+ else self.barSpacing = clamp(ad.s0 * Math.exp((self._cross.x - ad.x0) * 0.004), self.opt.minBarSpacing, self.opt.maxBarSpacing);
1856
+ self._paint();
1857
+ return;
1858
+ }
1859
+ if (self._sepDrag) {
1860
+ var sd = self._sepDrag;
1861
+ // dragging DOWN moves the pane's top edge down = the pane shrinks
1862
+ self.panes[sd.i].hPx = clamp(sd.h0 - (self._cross.y - sd.y0), 42, self._plotH() * 0.6);
1863
+ self._paint();
1864
+ return;
1865
+ }
1866
+ if (!self.tool && !drag && !self._draft && !self._dragDraw) {
1867
+ el.style.cursor = self._separatorAt(self._cross.y) !== null ? "ns-resize" : "";
1868
+ }
1869
+ if (self._dragDraw) {
1870
+ var dd = self._dragDraw, ppm = self.panes[0];
1871
+ var tNow = self.indexToTime(self.xToIndex(self._cross.x)), vNow = ppm.toValue(self._cross.y);
1872
+ if (dd.handle === "size") {
1873
+ if (dd._fs0 === undefined) { dd._fs0 = dd.d.size || 12; dd._ys = self._cross.y; }
1874
+ dd.d.size = clamp(Math.round(dd._fs0 + (self._cross.y - dd._ys) * 0.25), 8, 64);
1875
+ }
1876
+ else if (dd.handle === "p2") { dd.d.t2 = tNow; dd.d.p2 = vNow; }
1877
+ else if (dd.handle === "p1") { dd.d.t1 = tNow; dd.d.p1 = vNow; }
1878
+ else {
1879
+ var dt = tNow - dd.t0, dv = vNow - dd.v0;
1880
+ dd.t0 = tNow; dd.v0 = vNow;
1881
+ if (isNum(dd.d.t1)) dd.d.t1 += dt;
1882
+ if (isNum(dd.d.t2)) dd.d.t2 += dt;
1883
+ if (isNum(dd.d.p1)) dd.d.p1 += dv;
1884
+ if (isNum(dd.d.p2)) dd.d.p2 += dv;
1885
+ }
1886
+ self._paintDrawings();
1887
+ self._paintCross();
1888
+ return;
1889
+ }
1890
+ if (drag) {
1891
+ var nowT = performance.now(), nowX = e.clientX - r.left;
1892
+ if (glide.lastT) {
1893
+ var inst = (nowX - glide.lastX) / Math.max(1, nowT - glide.lastT);
1894
+ glide.v = glide.v * 0.75 + inst * 0.25; // smoothed px/ms
1895
+ }
1896
+ glide.lastX = nowX; glide.lastT = nowT;
1897
+ self.rightIndex = drag.right0 + (drag.x0 - nowX) / self.barSpacing;
1898
+ self.rightIndex = clamp(self.rightIndex, 0, self.bars.length - 1 + self.opt.rightPadBars * 2);
1899
+ if (drag.vpan) {
1900
+ var us = self.panes[0].h * (1 - self.opt.volumeHeightPct * 0.35) || 1;
1901
+ self._vOff = drag.off0 + (e.clientY - r.top - drag.y0) / us;
1902
+ }
1903
+ self._paint();
1904
+ } else self._paintCross();
1905
+ });
1906
+ el.addEventListener("mouseleave", function () { self._cross = null; drag = null; self._axisDrag = null; self._paintCross(); self._paintLegend(null); if (self.crosshairCb) self.crosshairCb(null); });
1907
+ function newId() { return "d" + Math.round(performance.now() * 1000) + "_" + self.drawings.length; }
1908
+ el.addEventListener("mousedown", function (e) {
1909
+ var r = el.getBoundingClientRect();
1910
+ var x = e.clientX - r.left, y = e.clientY - r.top;
1911
+ var pp = self.panes[0];
1912
+ // dragging the price axis stretches the scale, the time axis stretches
1913
+ // bar spacing — the same gestures TradingView trains everyone on
1914
+ if (x > self._plotW()) { stopGlide(); self._axisDrag = { axis: "price", y0: y, z0: self._vZoom }; e.preventDefault(); return; }
1915
+ if (y > self._plotH()) { stopGlide(); self._axisDrag = { axis: "time", x0: x, s0: self.barSpacing }; e.preventDefault(); return; }
1916
+ if (self.tool && self._paneAt(y) === pp) {
1917
+ var t0 = self.indexToTime(self.xToIndex(x)), v0 = pp.toValue(y);
1918
+ if (self.tool === "hline") {
1919
+ self.drawings.push({ id: newId(), type: "hline", t1: t0, p1: v0 });
1920
+ self.setTool(null);
1921
+ if (self._toolDoneCb) self._toolDoneCb();
1922
+ self._persist();
1923
+ self._paintDrawings();
1924
+ } else if (self.tool === "text") {
1925
+ var ask = self.opt.textPrompt || function (initial, cb) { cb(window.prompt("Text:", initial || "")); };
1926
+ ask("", function (txt) {
1927
+ if (txt) {
1928
+ self.drawings.push({ id: newId(), type: "text", t1: t0, p1: v0, text: txt, size: 12 });
1929
+ self._persist();
1930
+ self._paintDrawings();
1931
+ }
1932
+ });
1933
+ self.setTool(null);
1934
+ if (self._toolDoneCb) self._toolDoneCb();
1935
+ } else if (self._draft) {
1936
+ // second click of the two-click flow: commit where the preview is
1937
+ self._draft.t2 = t0; self._draft.p2 = v0;
1938
+ self.drawings.push(self._draft);
1939
+ self._selected = self._draft.id;
1940
+ self._draft = null;
1941
+ self.setTool(null);
1942
+ if (self._toolDoneCb) self._toolDoneCb();
1943
+ self._persist();
1944
+ self._syncStyleChip();
1945
+ self._paintDrawings();
1946
+ } else {
1947
+ self._draft = { id: newId(), type: self.tool, t1: t0, p1: v0, t2: t0, p2: v0, _x0: x, _y0: y };
1948
+ }
1949
+ e.preventDefault();
1950
+ return;
1951
+ }
1952
+ // separator drag: resize the oscillator pane under it
1953
+ var sep = self._separatorAt(y);
1954
+ if (sep !== null) {
1955
+ self._sepDrag = { i: sep, y0: y, h0: self.panes[sep].hPx };
1956
+ e.preventDefault();
1957
+ return;
1958
+ }
1959
+ // cursor: select, grab a handle, or start moving the whole shape
1960
+ var hit = self._hitTest(x, y);
1961
+ if (hit) {
1962
+ self._selected = hit.d.id;
1963
+ self._dragDraw = { d: hit.d, handle: hit.handle,
1964
+ t0: self.indexToTime(self.xToIndex(x)), v0: pp.toValue(y) };
1965
+ self._syncStyleChip();
1966
+ self._paintDrawings();
1967
+ e.preventDefault();
1968
+ return;
1969
+ }
1970
+ if (self._selected) { self._selected = null; self._syncStyleChip(); self._paintDrawings(); }
1971
+ stopGlide();
1972
+ glide.lastX = x; glide.lastT = 0;
1973
+ drag = { x0: x, right0: self.rightIndex, y0: y, off0: self._vOff, vpan: self._paneAt(y) === pp };
1974
+ e.preventDefault();
1975
+ });
1976
+ window.addEventListener("mouseup", function () {
1977
+ if (drag) startGlide();
1978
+ drag = null;
1979
+ self._axisDrag = null;
1980
+ if (self._draft) {
1981
+ var f = self._draft;
1982
+ var movedPx = self._cross ? Math.abs(self._cross.x - f._x0) + Math.abs(self._cross.y - f._y0) : 0;
1983
+ // released after a real drag → classic drag-to-draw commit;
1984
+ // released in place → the draft stays armed for the second click
1985
+ if (movedPx > 6) {
1986
+ delete f._x0; delete f._y0;
1987
+ self.drawings.push(f);
1988
+ self._selected = f.id;
1989
+ self._draft = null;
1990
+ self.setTool(null);
1991
+ if (self._toolDoneCb) self._toolDoneCb();
1992
+ self._persist();
1993
+ self._syncStyleChip();
1994
+ self._paintDrawings();
1995
+ }
1996
+ }
1997
+ if (self._dragDraw) { self._dragDraw = null; self._persist(); }
1998
+ self._sepDrag = null;
1999
+ });
2000
+ window.addEventListener("keydown", function (e) {
2001
+ if (e.key === "Escape" && self._draft) {
2002
+ self._draft = null;
2003
+ self.setTool(null);
2004
+ if (self._toolDoneCb) self._toolDoneCb();
2005
+ self._paintDrawings();
2006
+ }
2007
+ });
2008
+ // Delete removes the selection — but never while typing in a form field.
2009
+ window.addEventListener("keydown", function (e) {
2010
+ if ((e.key === "Delete" || e.key === "Backspace") && self._selected) {
2011
+ var tag = (document.activeElement && document.activeElement.tagName) || "";
2012
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
2013
+ e.preventDefault();
2014
+ self.deleteSelected();
2015
+ }
2016
+ });
2017
+ el.addEventListener("dblclick", function (e) {
2018
+ var r = el.getBoundingClientRect();
2019
+ if (e.clientX - r.left > self._plotW()) { self._vZoom = 1; self._vOff = 0; self._paint(); return; }
2020
+ var hit = self._hitTest(e.clientX - r.left, e.clientY - r.top);
2021
+ if (hit && hit.d.type === "text") {
2022
+ var ask = self.opt.textPrompt || function (initial, cb) { cb(window.prompt("Text:", initial || "")); };
2023
+ ask(hit.d.text || "", function (txt) {
2024
+ if (txt !== null && txt !== undefined && txt !== "") { hit.d.text = txt; self._persist(); self._paintDrawings(); }
2025
+ });
2026
+ return;
2027
+ }
2028
+ self.scrollToRealtime();
2029
+ });
2030
+
2031
+ el.addEventListener("wheel", function (e) {
2032
+ e.preventDefault();
2033
+ stopGlide();
2034
+ var r = el.getBoundingClientRect();
2035
+ var x = e.clientX - r.left;
2036
+ // over the price axis the wheel stretches the vertical scale — the
2037
+ // grid gets denser or sparser, TV-style; dblclick there resets
2038
+ if (x > self._plotW()) {
2039
+ self._vZoom = clamp(self._vZoom * Math.exp(e.deltaY * 0.0015), 0.15, 8);
2040
+ self._paint();
2041
+ return;
2042
+ }
2043
+ // A Mac trackpad swipes horizontally with deltaX: that is a PAN, the
2044
+ // way every charting tool treats it. Vertical wheel stays zoom.
2045
+ if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
2046
+ self.rightIndex = clamp(self.rightIndex + e.deltaX / self.barSpacing,
2047
+ 0, self.bars.length - 1 + self.opt.rightPadBars * 2);
2048
+ self._paint();
2049
+ return;
2050
+ }
2051
+ var anchor = self.xToIndex(x);
2052
+ var k = Math.exp(-e.deltaY * 0.0015);
2053
+ self.barSpacing = clamp(self.barSpacing * k, self.opt.minBarSpacing, self.opt.maxBarSpacing);
2054
+ self.rightIndex = anchor - (self.xToIndex(x) - self.rightIndex);
2055
+ self._paint();
2056
+ }, { passive: false });
2057
+
2058
+ el.addEventListener("touchstart", function (e) {
2059
+ var r = el.getBoundingClientRect();
2060
+ if (e.touches.length === 1) {
2061
+ var tx = e.touches[0].clientX - r.left, ty = e.touches[0].clientY - r.top;
2062
+ var pp = self.panes[0];
2063
+ if (tx > self._plotW()) { stopGlide(); self._axisDrag = { axis: "price", y0: ty, z0: self._vZoom }; self._touchLock(); return; }
2064
+ if (ty > self._plotH()) { stopGlide(); self._axisDrag = { axis: "time", x0: tx, s0: self.barSpacing }; self._touchLock(); return; }
2065
+ if (self.tool && self._paneAt(ty) === pp) {
2066
+ var t0 = self.indexToTime(self.xToIndex(tx)), v0 = pp.toValue(ty);
2067
+ if (self.tool === "hline") {
2068
+ self.drawings.push({ id: "d" + Math.round(performance.now() * 1000), type: "hline", t1: t0, p1: v0 });
2069
+ self.setTool(null);
2070
+ if (self._toolDoneCb) self._toolDoneCb();
2071
+ self._persist(); self._paintDrawings();
2072
+ } else if (self.tool === "text") {
2073
+ var ask = self.opt.textPrompt || function (initial, cb) { cb(window.prompt("Text:", initial || "")); };
2074
+ ask("", function (txt) {
2075
+ if (txt) { self.drawings.push({ id: "d" + Math.round(performance.now() * 1000), type: "text", t1: t0, p1: v0, text: txt, size: 12 }); self._persist(); self._paintDrawings(); }
2076
+ });
2077
+ self.setTool(null);
2078
+ if (self._toolDoneCb) self._toolDoneCb();
2079
+ } else {
2080
+ self._draft = { id: "d" + Math.round(performance.now() * 1000), type: self.tool, t1: t0, p1: v0, t2: t0, p2: v0 };
2081
+ self._touchLock();
2082
+ }
2083
+ return;
2084
+ }
2085
+ var sep = self._separatorAt(ty);
2086
+ if (sep !== null) { self._sepDrag = { i: sep, y0: ty, h0: self.panes[sep].hPx }; self._touchLock(); return; }
2087
+ var hit = self._hitTest(tx, ty);
2088
+ if (hit) {
2089
+ self._selected = hit.d.id;
2090
+ self._dragDraw = { d: hit.d, handle: hit.handle, t0: self.indexToTime(self.xToIndex(tx)), v0: pp.toValue(ty) };
2091
+ self._touchLock();
2092
+ self._syncStyleChip();
2093
+ self._paintDrawings();
2094
+ return;
2095
+ }
2096
+ stopGlide();
2097
+ glide.lastX = tx; glide.lastT = 0;
2098
+ drag = { x0: tx, right0: self.rightIndex, y0: ty, off0: self._vOff, vpan: self._paneAt(ty) === pp };
2099
+ } else if (e.touches.length === 2) {
2100
+ // a second finger means navigation: commit whatever was in flight
2101
+ if (self._draft) { self.drawings.push(self._draft); self._selected = self._draft.id; self._draft = null; self._persist(); self._paintDrawings(); }
2102
+ self._dragDraw = null; self._sepDrag = null; drag = null;
2103
+ self._touchLock();
2104
+ var dx = e.touches[0].clientX - e.touches[1].clientX;
2105
+ pinch = { d0: Math.abs(dx) || 1, spacing0: self.barSpacing };
2106
+ }
2107
+ }, { passive: true });
2108
+ el.addEventListener("touchmove", function (e) {
2109
+ var r = el.getBoundingClientRect();
2110
+ if (self._axisDrag && e.touches.length === 1) {
2111
+ var aax = e.touches[0].clientX - r.left, aay = e.touches[0].clientY - r.top;
2112
+ var ad2 = self._axisDrag;
2113
+ if (ad2.axis === "price") self._vZoom = clamp(ad2.z0 * Math.exp((aay - ad2.y0) * 0.004), 0.15, 8);
2114
+ else self.barSpacing = clamp(ad2.s0 * Math.exp((aax - ad2.x0) * 0.004), self.opt.minBarSpacing, self.opt.maxBarSpacing);
2115
+ self._paint();
2116
+ e.preventDefault();
2117
+ return;
2118
+ }
2119
+ if (e.touches.length === 1 && (self._draft || self._dragDraw || self._sepDrag)) {
2120
+ var tx = e.touches[0].clientX - r.left, ty = e.touches[0].clientY - r.top;
2121
+ var pp = self.panes[0];
2122
+ if (self._draft) {
2123
+ self._draft.t2 = self.indexToTime(self.xToIndex(tx));
2124
+ self._draft.p2 = pp.toValue(ty);
2125
+ self._paintDrawings();
2126
+ } else if (self._sepDrag) {
2127
+ var sd = self._sepDrag;
2128
+ self.panes[sd.i].hPx = clamp(sd.h0 - (ty - sd.y0), 42, self._plotH() * 0.6);
2129
+ self._paint();
2130
+ } else {
2131
+ var dd = self._dragDraw;
2132
+ var tNow = self.indexToTime(self.xToIndex(tx)), vNow = pp.toValue(ty);
2133
+ if (dd.handle === "p2") { dd.d.t2 = tNow; dd.d.p2 = vNow; }
2134
+ else if (dd.handle === "p1") { dd.d.t1 = tNow; dd.d.p1 = vNow; }
2135
+ else {
2136
+ var dt = tNow - dd.t0, dv = vNow - dd.v0;
2137
+ dd.t0 = tNow; dd.v0 = vNow;
2138
+ if (isNum(dd.d.t1)) dd.d.t1 += dt;
2139
+ if (isNum(dd.d.t2)) dd.d.t2 += dt;
2140
+ if (isNum(dd.d.p1)) dd.d.p1 += dv;
2141
+ if (isNum(dd.d.p2)) dd.d.p2 += dv;
2142
+ }
2143
+ self._paintDrawings();
2144
+ }
2145
+ e.preventDefault();
2146
+ return;
2147
+ }
2148
+ if (pinch && e.touches.length === 2) {
2149
+ var d = Math.abs(e.touches[0].clientX - e.touches[1].clientX) || 1;
2150
+ self.barSpacing = clamp(pinch.spacing0 * d / pinch.d0, self.opt.minBarSpacing, self.opt.maxBarSpacing);
2151
+ self._paint();
2152
+ } else if (drag && e.touches.length === 1) {
2153
+ var tNowT = performance.now(), tNowX = e.touches[0].clientX - r.left;
2154
+ if (glide.lastT) {
2155
+ var tInst = (tNowX - glide.lastX) / Math.max(1, tNowT - glide.lastT);
2156
+ glide.v = glide.v * 0.75 + tInst * 0.25;
2157
+ }
2158
+ glide.lastX = tNowX; glide.lastT = tNowT;
2159
+ self.rightIndex = drag.right0 + (drag.x0 - tNowX) / self.barSpacing;
2160
+ self.rightIndex = clamp(self.rightIndex, 0, self.bars.length - 1 + self.opt.rightPadBars * 2);
2161
+ if (drag.vpan) {
2162
+ var us2 = self.panes[0].h * (1 - self.opt.volumeHeightPct * 0.35) || 1;
2163
+ self._vOff = drag.off0 + (e.touches[0].clientY - r.top - drag.y0) / us2;
2164
+ }
2165
+ self._paint();
2166
+ e.preventDefault();
2167
+ }
2168
+ }, { passive: false });
2169
+ function endTouch(e) {
2170
+ if (e.touches.length) return;
2171
+ if (drag) startGlide();
2172
+ drag = null; pinch = null; self._axisDrag = null;
2173
+ if (self._draft) {
2174
+ var f = self._draft;
2175
+ if (f.t1 !== f.t2 || f.p1 !== f.p2) { self.drawings.push(f); self._selected = f.id; self._persist(); }
2176
+ self._draft = null;
2177
+ self.setTool(null);
2178
+ if (self._toolDoneCb) self._toolDoneCb();
2179
+ self._paintDrawings();
2180
+ }
2181
+ if (self._dragDraw) { self._dragDraw = null; self._persist(); }
2182
+ self._sepDrag = null;
2183
+ self._touchLock();
2184
+ }
2185
+ el.addEventListener("touchend", endTouch, { passive: true });
2186
+ // a browser-fired cancel must COMMIT the in-flight shape, not destroy it
2187
+ el.addEventListener("touchcancel", endTouch, { passive: true });
2188
+ };
2189
+
2190
+ // Ready-made palettes; pass one to createChart or applyOptions. Colors are
2191
+ // read at paint time, so switching themes live is one applyOptions call.
2192
+ var THEMES = {
2193
+ dark: {}, // DEFAULTS already are the dark look
2194
+ light: {
2195
+ background: "#ffffff",
2196
+ textColor: "#5f6b7c",
2197
+ gridColor: "rgba(42,46,57,0.10)",
2198
+ separatorColor: "rgba(42,46,57,0.28)",
2199
+ crosshair: "rgba(95,107,124,0.45)",
2200
+ tagBg: "#e0e3eb",
2201
+ tagText: "#131722",
2202
+ volumeUp: "rgba(8,153,129,0.30)",
2203
+ volumeDown: "rgba(242,54,69,0.30)",
2204
+ upColor: "#089981", downColor: "#f23645",
2205
+ wickUp: "#089981", wickDown: "#f23645",
2206
+ },
2207
+ };
2208
+
2209
+ // Resample finer bars into a coarser frame: any fixed step in seconds, or
2210
+ // the calendar frames "1M"/"1Y" (months and years have no fixed length).
2211
+ // This is what makes custom timeframes provider-independent: a source that
2212
+ // only serves 1m can still feed a 2m/3m/1M/1Y chart.
2213
+ function resample(bars, spec) {
2214
+ if (!bars || !bars.length) return [];
2215
+ function bucketOf(t) {
2216
+ if (spec === "1M") { var d = new Date(t * 1000); return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1) / 1000; }
2217
+ if (spec === "1Y") { var y = new Date(t * 1000); return Date.UTC(y.getUTCFullYear(), 0, 1) / 1000; }
2218
+ return Math.floor(t / spec) * spec;
2219
+ }
2220
+ var out = [], cur = null;
2221
+ for (var i = 0; i < bars.length; i++) {
2222
+ var b = bars[i], bk = bucketOf(b.time);
2223
+ if (!cur || cur.time !== bk) {
2224
+ if (cur) out.push(cur);
2225
+ cur = { time: bk, open: b.open, high: b.high, low: b.low, close: b.close, volume: b.volume || 0 };
2226
+ } else {
2227
+ if (b.high > cur.high) cur.high = b.high;
2228
+ if (b.low < cur.low) cur.low = b.low;
2229
+ cur.close = b.close;
2230
+ cur.volume += b.volume || 0;
2231
+ }
2232
+ }
2233
+ if (cur) out.push(cur);
2234
+ return out;
2235
+ }
2236
+
2237
+ global.TRCharts = {
2238
+ version: "1.0.0",
2239
+ themes: THEMES,
2240
+ resample: resample,
2241
+ createChart: function (el, options) { return new Chart(el, options); },
2242
+ };
2243
+ })(typeof window !== "undefined" ? window : this);