@luxalgo/vela 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/dist/{DataProvider-DrN7ZIou.d.cts → DataProvider-DDUYw2qP.d.cts} +1 -1
- package/dist/{DataProvider-BmHkcwvJ.d.ts → DataProvider-JJq7M2it.d.ts} +1 -1
- package/dist/{history-CvOtsjT5.d.ts → bottombar-COnL2Sk4.d.ts} +110 -127
- package/dist/{history-BmyjaVir.d.cts → bottombar-DeDXbF_K.d.cts} +110 -127
- package/dist/{chunk-6FA4TDUF.js → chunk-JQFO2WMU.js} +2666 -2333
- package/dist/{chunk-BEJ57HP4.js → chunk-OEUGUXL7.js} +14 -1
- package/dist/{chunk-3QX6QOCX.js → chunk-TMXK2SJR.js} +5852 -3340
- package/dist/{contributions-2bEmCF9S.d.cts → contributions-BqCIsbaP.d.ts} +108 -4
- package/dist/{contributions-DatoAzMx.d.ts → contributions-DNMqrAuw.d.cts} +108 -4
- package/dist/index.cjs +2681 -2332
- package/dist/index.d.cts +65 -14
- package/dist/index.d.ts +65 -14
- package/dist/index.js +2 -2
- package/dist/{options-YdiaaVjt.d.cts → options-BaVTMXaO.d.cts} +48 -2
- package/dist/{options-YdiaaVjt.d.ts → options-BaVTMXaO.d.ts} +48 -2
- package/dist/{plugin-CkiStRaI.d.cts → plugin-CN8U2__Q.d.cts} +11 -5
- package/dist/{plugin-BeCMF1VQ.d.ts → plugin-WnvNNJOp.d.ts} +11 -5
- package/dist/plugin.cjs +16 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +1 -1
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +2661 -2312
- package/dist/vela.global.min.js +48 -48
- package/dist/widget.cjs +35658 -34074
- package/dist/widget.d.cts +39 -271
- package/dist/widget.d.ts +39 -271
- package/dist/widget.js +60 -1313
- package/dist/workspace.cjs +1584 -960
- package/dist/workspace.d.cts +111 -15
- package/dist/workspace.d.ts +111 -15
- package/dist/workspace.js +6 -2281
- package/package.json +1 -1
package/dist/workspace.cjs
CHANGED
|
@@ -337,8 +337,12 @@ var BarStore = class {
|
|
|
337
337
|
this.series = /* @__PURE__ */ new Map();
|
|
338
338
|
/** Earliest bar-open time fetched for a series — what the cache actually covers. */
|
|
339
339
|
this.coveredFrom = /* @__PURE__ */ new Map();
|
|
340
|
-
/** Symbols protected from the current-symbol purge (multi-chart cells)
|
|
340
|
+
/** Symbols protected from the current-symbol purge (multi-chart cells) — the UNION
|
|
341
|
+
* of every owner's declaration. Empty = legacy single-chart behavior. */
|
|
341
342
|
this.retained = /* @__PURE__ */ new Set();
|
|
343
|
+
/** Per-owner declarations behind {@link retained} — several shells on one page must
|
|
344
|
+
* not clobber (or, on destroy, evict) each other's protected symbols. */
|
|
345
|
+
this.retainedByOwner = /* @__PURE__ */ new Map();
|
|
342
346
|
}
|
|
343
347
|
get(key) {
|
|
344
348
|
return this.series.get(key);
|
|
@@ -382,14 +386,21 @@ var BarStore = class {
|
|
|
382
386
|
* Declare the set of symbols a multi-chart workspace is displaying (CANONICAL
|
|
383
387
|
* tickers, post-registry resolution — `chart.data.resolve(sym).ticker`). These
|
|
384
388
|
* survive every {@link retainSymbol} purge, so cells loading different symbols
|
|
385
|
-
* stop evicting each other's history. Replaces the previous set
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
389
|
+
* stop evicting each other's history. Replaces the previous set FOR THAT OWNER
|
|
390
|
+
* (pass the shell instance as `owner`; several shells on one page keep separate
|
|
391
|
+
* declarations, the effective set is their union) and purges anything now outside
|
|
392
|
+
* the union ∪ {currentSymbol} immediately. An empty set releases the owner's
|
|
393
|
+
* declaration — with no owners left, the legacy single-chart policy is back.
|
|
394
|
+
* Note: SECONDARY symbols a script fetches (`request.security` cross-symbol) are
|
|
395
|
+
* not in this set and still drop on cross-cell loads — correctness is unaffected
|
|
396
|
+
* (they re-fetch on demand).
|
|
390
397
|
*/
|
|
391
|
-
retain(symbols) {
|
|
392
|
-
|
|
398
|
+
retain(symbols, owner = "default") {
|
|
399
|
+
if (symbols.size === 0) this.retainedByOwner.delete(owner);
|
|
400
|
+
else this.retainedByOwner.set(owner, new Set(symbols));
|
|
401
|
+
const union = /* @__PURE__ */ new Set();
|
|
402
|
+
for (const set of this.retainedByOwner.values()) for (const s of set) union.add(s);
|
|
403
|
+
this.retained = union;
|
|
393
404
|
this.purgeOutside(this.currentSymbol);
|
|
394
405
|
}
|
|
395
406
|
/** Drop every series whose symbol is neither `current` nor retained. */
|
|
@@ -4571,6 +4582,10 @@ function legendActionsProviderFor(chart, context) {
|
|
|
4571
4582
|
return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
|
|
4572
4583
|
};
|
|
4573
4584
|
}
|
|
4585
|
+
var stateHandlers = /* @__PURE__ */ new Map();
|
|
4586
|
+
function statePersistenceHandlers(scope) {
|
|
4587
|
+
return [...stateHandlers.values()].filter((h) => h.scope === scope);
|
|
4588
|
+
}
|
|
4574
4589
|
var defaultEngines = /* @__PURE__ */ new Map();
|
|
4575
4590
|
function resolveEngines(overrides) {
|
|
4576
4591
|
return { ...Object.fromEntries(defaultEngines), ...overrides };
|
|
@@ -13407,6 +13422,7 @@ var CSS8 = `
|
|
|
13407
13422
|
.vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
|
|
13408
13423
|
.vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
|
|
13409
13424
|
`;
|
|
13425
|
+
var PAGE = 100;
|
|
13410
13426
|
var SymbolPicker = class {
|
|
13411
13427
|
constructor(opts) {
|
|
13412
13428
|
this.source = () => [];
|
|
@@ -13414,6 +13430,7 @@ var SymbolPicker = class {
|
|
|
13414
13430
|
this.highlighted = 0;
|
|
13415
13431
|
this.seed = "";
|
|
13416
13432
|
this.activeTab = "All";
|
|
13433
|
+
this.visible = PAGE;
|
|
13417
13434
|
const doc = (opts.host ?? document.body).ownerDocument;
|
|
13418
13435
|
injectStyles(STYLE_ID7, CSS8, doc);
|
|
13419
13436
|
this.input = doc.createElement("input");
|
|
@@ -13440,6 +13457,12 @@ var SymbolPicker = class {
|
|
|
13440
13457
|
}
|
|
13441
13458
|
this.list = doc.createElement("div");
|
|
13442
13459
|
this.list.className = "vela-sp-list";
|
|
13460
|
+
this.list.addEventListener("scroll", () => {
|
|
13461
|
+
if (this.rows.length < this.visible) return;
|
|
13462
|
+
if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
|
|
13463
|
+
this.visible += PAGE;
|
|
13464
|
+
this.grow();
|
|
13465
|
+
});
|
|
13443
13466
|
this.dialog = new Dialog({
|
|
13444
13467
|
title: "Symbol Search",
|
|
13445
13468
|
host: opts.host,
|
|
@@ -13505,11 +13528,15 @@ var SymbolPicker = class {
|
|
|
13505
13528
|
} else delete el.dataset.highlighted;
|
|
13506
13529
|
});
|
|
13507
13530
|
}
|
|
13508
|
-
|
|
13509
|
-
const doc = this.list.ownerDocument;
|
|
13531
|
+
computeRows() {
|
|
13510
13532
|
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
13511
13533
|
const pool = this.activeTab === "All" ? this.source() : this.source().filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
13512
|
-
|
|
13534
|
+
return filterSymbols(pool, this.input.value, this.visible);
|
|
13535
|
+
}
|
|
13536
|
+
refresh() {
|
|
13537
|
+
const doc = this.list.ownerDocument;
|
|
13538
|
+
this.visible = PAGE;
|
|
13539
|
+
this.rows = this.computeRows();
|
|
13513
13540
|
this.highlighted = 0;
|
|
13514
13541
|
this.list.replaceChildren();
|
|
13515
13542
|
if (!this.rows.length) {
|
|
@@ -13519,34 +13546,42 @@ var SymbolPicker = class {
|
|
|
13519
13546
|
this.list.appendChild(empty);
|
|
13520
13547
|
return;
|
|
13521
13548
|
}
|
|
13522
|
-
for (const s of this.rows)
|
|
13523
|
-
const row = doc.createElement("div");
|
|
13524
|
-
row.className = "vela-sp-row";
|
|
13525
|
-
row.dataset.ticker = s.ticker;
|
|
13526
|
-
const venue = s.prefix ?? s.provider;
|
|
13527
|
-
if (venue) row.dataset.venue = venue;
|
|
13528
|
-
const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
|
|
13529
|
-
const main = doc.createElement("span");
|
|
13530
|
-
main.className = "vela-sp-main";
|
|
13531
|
-
const t = doc.createElement("span");
|
|
13532
|
-
t.className = "vela-sp-ticker";
|
|
13533
|
-
t.textContent = s.ticker;
|
|
13534
|
-
const d = doc.createElement("span");
|
|
13535
|
-
d.className = "vela-sp-desc";
|
|
13536
|
-
d.textContent = s.description ?? (s.type ?? "");
|
|
13537
|
-
main.append(t, d);
|
|
13538
|
-
row.append(av, main);
|
|
13539
|
-
if (venue) {
|
|
13540
|
-
const badge = doc.createElement("span");
|
|
13541
|
-
badge.className = "vela-sp-badge";
|
|
13542
|
-
badge.dataset.p = s.provider ?? venue;
|
|
13543
|
-
badge.textContent = venue;
|
|
13544
|
-
row.appendChild(badge);
|
|
13545
|
-
}
|
|
13546
|
-
this.list.appendChild(row);
|
|
13547
|
-
}
|
|
13549
|
+
for (const s of this.rows) this.list.appendChild(this.rowEl(s));
|
|
13548
13550
|
this.renderHighlight();
|
|
13549
13551
|
}
|
|
13552
|
+
/** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
|
|
13553
|
+
grow() {
|
|
13554
|
+
const already = this.rows.length;
|
|
13555
|
+
this.rows = this.computeRows();
|
|
13556
|
+
for (const s of this.rows.slice(already)) this.list.appendChild(this.rowEl(s));
|
|
13557
|
+
}
|
|
13558
|
+
rowEl(s) {
|
|
13559
|
+
const doc = this.list.ownerDocument;
|
|
13560
|
+
const row = doc.createElement("div");
|
|
13561
|
+
row.className = "vela-sp-row";
|
|
13562
|
+
row.dataset.ticker = s.ticker;
|
|
13563
|
+
const venue = s.prefix ?? s.provider;
|
|
13564
|
+
if (venue) row.dataset.venue = venue;
|
|
13565
|
+
const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
|
|
13566
|
+
const main = doc.createElement("span");
|
|
13567
|
+
main.className = "vela-sp-main";
|
|
13568
|
+
const t = doc.createElement("span");
|
|
13569
|
+
t.className = "vela-sp-ticker";
|
|
13570
|
+
t.textContent = s.ticker;
|
|
13571
|
+
const d = doc.createElement("span");
|
|
13572
|
+
d.className = "vela-sp-desc";
|
|
13573
|
+
d.textContent = s.description ?? (s.type ?? "");
|
|
13574
|
+
main.append(t, d);
|
|
13575
|
+
row.append(av, main);
|
|
13576
|
+
if (venue) {
|
|
13577
|
+
const badge = doc.createElement("span");
|
|
13578
|
+
badge.className = "vela-sp-badge";
|
|
13579
|
+
badge.dataset.p = s.provider ?? venue;
|
|
13580
|
+
badge.textContent = venue;
|
|
13581
|
+
row.appendChild(badge);
|
|
13582
|
+
}
|
|
13583
|
+
return row;
|
|
13584
|
+
}
|
|
13550
13585
|
};
|
|
13551
13586
|
|
|
13552
13587
|
// src/widget/indicator-picker.ts
|
|
@@ -14192,10 +14227,11 @@ var MobileBar = class {
|
|
|
14192
14227
|
const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
|
|
14193
14228
|
this.actionsHost = doc.createElement("span");
|
|
14194
14229
|
this.actionsHost.className = "vela-mb-actions";
|
|
14195
|
-
const
|
|
14230
|
+
const onDrawings = opts.onDrawingsClick;
|
|
14231
|
+
const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
|
|
14196
14232
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
14197
14233
|
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
14198
|
-
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, drawings, more, settings);
|
|
14234
|
+
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
|
|
14199
14235
|
host.appendChild(this.el);
|
|
14200
14236
|
this.renderActions();
|
|
14201
14237
|
}
|
|
@@ -17010,6 +17046,35 @@ function rendererDefaults() {
|
|
|
17010
17046
|
return Object.fromEntries(defaults);
|
|
17011
17047
|
}
|
|
17012
17048
|
|
|
17049
|
+
// src/core/price-styles/heikin-ashi.ts
|
|
17050
|
+
function heikinAshiNext(raw, prevHa) {
|
|
17051
|
+
const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
|
|
17052
|
+
const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
|
|
17053
|
+
return {
|
|
17054
|
+
time: raw.time,
|
|
17055
|
+
open: haOpen,
|
|
17056
|
+
high: Math.max(raw.high, haOpen, haClose),
|
|
17057
|
+
low: Math.min(raw.low, haOpen, haClose),
|
|
17058
|
+
close: haClose,
|
|
17059
|
+
...raw.volume != null ? { volume: raw.volume } : {}
|
|
17060
|
+
};
|
|
17061
|
+
}
|
|
17062
|
+
function heikinAshiFull(raw) {
|
|
17063
|
+
const out = new Array(raw.length);
|
|
17064
|
+
let prev2;
|
|
17065
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
17066
|
+
prev2 = heikinAshiNext(raw[i], prev2);
|
|
17067
|
+
out[i] = prev2;
|
|
17068
|
+
}
|
|
17069
|
+
return out;
|
|
17070
|
+
}
|
|
17071
|
+
|
|
17072
|
+
// src/chart-types/builtins.ts
|
|
17073
|
+
var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
|
|
17074
|
+
function registerBuiltinChartTypes() {
|
|
17075
|
+
registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
|
|
17076
|
+
}
|
|
17077
|
+
|
|
17013
17078
|
// src/workspace/sync.ts
|
|
17014
17079
|
function syncTargets(originId, setting, cellIds) {
|
|
17015
17080
|
if (setting == null || setting === false) return [];
|
|
@@ -17069,8 +17134,18 @@ function sanitizeState(doc) {
|
|
|
17069
17134
|
if (tracks) out.trackSizes = tracks;
|
|
17070
17135
|
const panels2 = sanitizePanels(d.panels);
|
|
17071
17136
|
if (panels2) out.panels = panels2;
|
|
17137
|
+
const ext = sanitizeExt(d.ext);
|
|
17138
|
+
if (ext) out.ext = ext;
|
|
17072
17139
|
return out;
|
|
17073
17140
|
}
|
|
17141
|
+
function sanitizeExt(raw) {
|
|
17142
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
17143
|
+
const out = {};
|
|
17144
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
17145
|
+
if (key.length > 0 && value !== void 0) out[key] = value;
|
|
17146
|
+
}
|
|
17147
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
17148
|
+
}
|
|
17074
17149
|
function sanitizeCell(raw) {
|
|
17075
17150
|
if (raw == null || typeof raw !== "object") return null;
|
|
17076
17151
|
const c = raw;
|
|
@@ -17092,6 +17167,8 @@ function sanitizeCell(raw) {
|
|
|
17092
17167
|
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
17093
17168
|
out.indicators = { manifest, natives };
|
|
17094
17169
|
}
|
|
17170
|
+
const ext = sanitizeExt(c.ext);
|
|
17171
|
+
if (ext) out.ext = ext;
|
|
17095
17172
|
return out;
|
|
17096
17173
|
}
|
|
17097
17174
|
function sanitizeSync(raw) {
|
|
@@ -18424,7 +18501,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18424
18501
|
this.emitScriptRun(id, cause, first);
|
|
18425
18502
|
},
|
|
18426
18503
|
onAlert: (a) => {
|
|
18427
|
-
|
|
18504
|
+
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
18505
|
+
this.events.emit("alert", { ...a, indicator });
|
|
18428
18506
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
18429
18507
|
},
|
|
18430
18508
|
onWarning: (w) => this.events.emit("warning", w),
|
|
@@ -18472,6 +18550,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18472
18550
|
overlay: d.overlay,
|
|
18473
18551
|
paneHint: d.paneHint,
|
|
18474
18552
|
native: { type: record.native.type },
|
|
18553
|
+
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
18475
18554
|
series: out.series ?? [],
|
|
18476
18555
|
fills: out.fills ?? [],
|
|
18477
18556
|
backgrounds: out.backgrounds ?? [],
|
|
@@ -19066,6 +19145,24 @@ var RendererControl = class {
|
|
|
19066
19145
|
this.renderer.setSettingsSections?.(sections);
|
|
19067
19146
|
return this;
|
|
19068
19147
|
}
|
|
19148
|
+
/**
|
|
19149
|
+
* Set the settings-dialog visibility policy: `hidden` lists setting ids to hide —
|
|
19150
|
+
* a tab (`'canvas'`), a group (`'canvas.grid'`), or a single row
|
|
19151
|
+
* (`'canvas.grid.vertical'`); an id hides its whole subtree, and a tab with nothing
|
|
19152
|
+
* left disappears from the rail. Presentation-only: hidden values keep being stored
|
|
19153
|
+
* and applied (e.g. hide `'advanced'` while forcing the widget's `bars` option).
|
|
19154
|
+
* Seeded from `VelaOptions.settings`; silent no-op without a settings dialog.
|
|
19155
|
+
*/
|
|
19156
|
+
setSettingsVisibility(policy) {
|
|
19157
|
+
this.renderer.setSettingsVisibility?.(policy);
|
|
19158
|
+
return this;
|
|
19159
|
+
}
|
|
19160
|
+
/** Every addressable setting id of this chart (built-in tabs/groups/rows, chart-type
|
|
19161
|
+
* sections, host sections) — enumerate these to build a `hidden` list instead of
|
|
19162
|
+
* reading contributor source. Empty on a renderer without a settings dialog. */
|
|
19163
|
+
listSettingsIds() {
|
|
19164
|
+
return this.renderer.listSettingsIds?.() ?? [];
|
|
19165
|
+
}
|
|
19069
19166
|
/** Tell the renderer's own chrome which size class the host shell is in —
|
|
19070
19167
|
* `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
|
|
19071
19168
|
* Silent no-op on a renderer without adaptive chrome. */
|
|
@@ -21448,652 +21545,6 @@ function supportsWebGL2() {
|
|
|
21448
21545
|
return webgl2Probe;
|
|
21449
21546
|
}
|
|
21450
21547
|
|
|
21451
|
-
// src/renderers/native/chrome/ticks.ts
|
|
21452
|
-
function priceTicks(min, max, target = 6) {
|
|
21453
|
-
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
21454
|
-
const raw = (max - min) / Math.max(1, target);
|
|
21455
|
-
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
21456
|
-
const norm = raw / mag;
|
|
21457
|
-
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
21458
|
-
const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
|
|
21459
|
-
const out = [];
|
|
21460
|
-
const start = Math.ceil(min / step) * step;
|
|
21461
|
-
for (let v = start; v <= max + step * 1e-6; v += step) {
|
|
21462
|
-
out.push(Number(v.toFixed(decimals)));
|
|
21463
|
-
}
|
|
21464
|
-
return out;
|
|
21465
|
-
}
|
|
21466
|
-
function priceDecimals(min, max, target = 6) {
|
|
21467
|
-
if (!(max > min)) return 2;
|
|
21468
|
-
const raw = (max - min) / Math.max(1, target);
|
|
21469
|
-
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
21470
|
-
const norm = raw / mag;
|
|
21471
|
-
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
21472
|
-
return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
|
|
21473
|
-
}
|
|
21474
|
-
function logPriceTicks(min, max, target = 6) {
|
|
21475
|
-
if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
21476
|
-
if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
|
|
21477
|
-
const out = [];
|
|
21478
|
-
const startExp = Math.floor(Math.log10(min));
|
|
21479
|
-
const endExp = Math.ceil(Math.log10(max));
|
|
21480
|
-
for (let e = startExp; e <= endExp; e += 1) {
|
|
21481
|
-
for (const m of [1, 2, 5]) {
|
|
21482
|
-
const v = m * Math.pow(10, e);
|
|
21483
|
-
if (v >= min && v <= max) out.push(v);
|
|
21484
|
-
}
|
|
21485
|
-
}
|
|
21486
|
-
return out;
|
|
21487
|
-
}
|
|
21488
|
-
function valueDecimals(v) {
|
|
21489
|
-
const a = Math.abs(v);
|
|
21490
|
-
if (a >= 100) return 0;
|
|
21491
|
-
if (a >= 1) return 2;
|
|
21492
|
-
if (a >= 0.01) return 4;
|
|
21493
|
-
return 6;
|
|
21494
|
-
}
|
|
21495
|
-
function tickDecimals(tick) {
|
|
21496
|
-
if (!(tick > 0) || !Number.isFinite(tick)) return 2;
|
|
21497
|
-
for (let d = 0; d <= 8; d += 1) {
|
|
21498
|
-
if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
|
|
21499
|
-
}
|
|
21500
|
-
return 8;
|
|
21501
|
-
}
|
|
21502
|
-
function axisDecimals(scale, heightPx, mintick) {
|
|
21503
|
-
const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
|
|
21504
|
-
return d === 0 ? 2 : d;
|
|
21505
|
-
}
|
|
21506
|
-
function tickCount(paneHeightPx) {
|
|
21507
|
-
return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
|
|
21508
|
-
}
|
|
21509
|
-
function paneTicks(scale, heightPx) {
|
|
21510
|
-
return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
|
|
21511
|
-
}
|
|
21512
|
-
function toPct(price, baseline) {
|
|
21513
|
-
return (price / baseline - 1) * 100;
|
|
21514
|
-
}
|
|
21515
|
-
function toIndex(price, baseline) {
|
|
21516
|
-
return price / baseline * 100;
|
|
21517
|
-
}
|
|
21518
|
-
function formatPct(pct) {
|
|
21519
|
-
const sign = pct >= 0 ? "+" : "-";
|
|
21520
|
-
return `${sign}${Math.abs(pct).toFixed(2)}%`;
|
|
21521
|
-
}
|
|
21522
|
-
function formatIndex(idx) {
|
|
21523
|
-
return idx.toFixed(2);
|
|
21524
|
-
}
|
|
21525
|
-
function formatCompactValue(v) {
|
|
21526
|
-
const a = Math.abs(v);
|
|
21527
|
-
if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
|
|
21528
|
-
if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
|
|
21529
|
-
if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
|
|
21530
|
-
return trimZeros(v);
|
|
21531
|
-
}
|
|
21532
|
-
function trimZeros(v) {
|
|
21533
|
-
return Number(v.toFixed(2)).toString();
|
|
21534
|
-
}
|
|
21535
|
-
function paneAxisTicks(scale, heightPx, pct, mintick, format) {
|
|
21536
|
-
if (pct) {
|
|
21537
|
-
const { baseline, indexed } = pct;
|
|
21538
|
-
const lo = Math.min(scale.min, scale.max);
|
|
21539
|
-
const hi = Math.max(scale.min, scale.max);
|
|
21540
|
-
if (indexed) {
|
|
21541
|
-
const iLo = toIndex(lo, baseline);
|
|
21542
|
-
const iHi = toIndex(hi, baseline);
|
|
21543
|
-
return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
|
|
21544
|
-
}
|
|
21545
|
-
const pLo = toPct(lo, baseline);
|
|
21546
|
-
const pHi = toPct(hi, baseline);
|
|
21547
|
-
return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
|
|
21548
|
-
}
|
|
21549
|
-
if (format === "volume") {
|
|
21550
|
-
return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
|
|
21551
|
-
}
|
|
21552
|
-
return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
|
|
21553
|
-
}
|
|
21554
|
-
function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
|
|
21555
|
-
if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
|
|
21556
|
-
if (format === "volume") return formatCompactValue(value);
|
|
21557
|
-
return formatPriceLabel(scale, heightPx, value, mintick);
|
|
21558
|
-
}
|
|
21559
|
-
function formatPriceLabel(scale, heightPx, value, mintick) {
|
|
21560
|
-
const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
|
|
21561
|
-
if (wideLog) {
|
|
21562
|
-
const d = valueDecimals(value);
|
|
21563
|
-
return value.toFixed(d === 0 ? 2 : d);
|
|
21564
|
-
}
|
|
21565
|
-
return value.toFixed(axisDecimals(scale, heightPx, mintick));
|
|
21566
|
-
}
|
|
21567
|
-
var SEC = 1e3;
|
|
21568
|
-
var MIN = 60 * SEC;
|
|
21569
|
-
var HOUR = 60 * MIN;
|
|
21570
|
-
var DAY = 24 * HOUR;
|
|
21571
|
-
var WEEK = 7 * DAY;
|
|
21572
|
-
var MONTH = 30 * DAY;
|
|
21573
|
-
var YEAR = 365 * DAY;
|
|
21574
|
-
var STEP_LADDER = [
|
|
21575
|
-
SEC,
|
|
21576
|
-
5 * SEC,
|
|
21577
|
-
15 * SEC,
|
|
21578
|
-
30 * SEC,
|
|
21579
|
-
MIN,
|
|
21580
|
-
5 * MIN,
|
|
21581
|
-
15 * MIN,
|
|
21582
|
-
30 * MIN,
|
|
21583
|
-
HOUR,
|
|
21584
|
-
2 * HOUR,
|
|
21585
|
-
4 * HOUR,
|
|
21586
|
-
6 * HOUR,
|
|
21587
|
-
12 * HOUR,
|
|
21588
|
-
DAY,
|
|
21589
|
-
2 * DAY,
|
|
21590
|
-
WEEK,
|
|
21591
|
-
MONTH,
|
|
21592
|
-
3 * MONTH,
|
|
21593
|
-
YEAR
|
|
21594
|
-
];
|
|
21595
|
-
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
21596
|
-
var pad2 = (n) => n < 10 ? `0${n}` : String(n);
|
|
21597
|
-
function pickStep(targetMs) {
|
|
21598
|
-
for (const step of STEP_LADDER) if (step >= targetMs) return step;
|
|
21599
|
-
return STEP_LADDER[STEP_LADDER.length - 1];
|
|
21600
|
-
}
|
|
21601
|
-
function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
21602
|
-
const span = toMs - fromMs;
|
|
21603
|
-
if (!(span > 0)) return [];
|
|
21604
|
-
const step = pickStep(span / Math.max(1, target));
|
|
21605
|
-
const zFrom = fromMs + offsetMs;
|
|
21606
|
-
const zTo = toMs + offsetMs;
|
|
21607
|
-
const first = Math.ceil(zFrom / step) * step;
|
|
21608
|
-
const out = [];
|
|
21609
|
-
for (let zt = first; zt <= zTo; zt += step) {
|
|
21610
|
-
const t = zt - offsetMs;
|
|
21611
|
-
const d = new Date(zt);
|
|
21612
|
-
let label;
|
|
21613
|
-
let major = false;
|
|
21614
|
-
if (step < DAY) {
|
|
21615
|
-
const h = d.getUTCHours();
|
|
21616
|
-
const m = d.getUTCMinutes();
|
|
21617
|
-
if (h === 0 && m === 0) {
|
|
21618
|
-
label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
|
|
21619
|
-
major = true;
|
|
21620
|
-
} else {
|
|
21621
|
-
label = `${pad2(h)}:${pad2(m)}`;
|
|
21622
|
-
}
|
|
21623
|
-
} else if (step < YEAR) {
|
|
21624
|
-
label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
|
|
21625
|
-
if (d.getUTCDate() === 1) {
|
|
21626
|
-
label = MONTHS[d.getUTCMonth()];
|
|
21627
|
-
major = true;
|
|
21628
|
-
}
|
|
21629
|
-
} else {
|
|
21630
|
-
label = String(d.getUTCFullYear());
|
|
21631
|
-
major = true;
|
|
21632
|
-
}
|
|
21633
|
-
out.push({ time: t, label, major });
|
|
21634
|
-
}
|
|
21635
|
-
return out;
|
|
21636
|
-
}
|
|
21637
|
-
|
|
21638
|
-
// src/renderers/shared/trade-markers.ts
|
|
21639
|
-
var TRADE_LONG_COLOR = TRADE_LONG;
|
|
21640
|
-
var TRADE_SHORT_COLOR = TRADE_SHORT;
|
|
21641
|
-
var TRADE_EXIT_COLOR = TRADE_EXIT;
|
|
21642
|
-
function defaultTradeMarkersState() {
|
|
21643
|
-
return {
|
|
21644
|
-
visible: true,
|
|
21645
|
-
labels: true,
|
|
21646
|
-
qty: true,
|
|
21647
|
-
colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
|
|
21648
|
-
};
|
|
21649
|
-
}
|
|
21650
|
-
function mergeTradeMarkersState(base, patch) {
|
|
21651
|
-
const p = patch && typeof patch === "object" ? patch : {};
|
|
21652
|
-
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
21653
|
-
const bool = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
21654
|
-
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
21655
|
-
return {
|
|
21656
|
-
visible: bool(p.visible, base.visible),
|
|
21657
|
-
labels: bool(p.labels, base.labels),
|
|
21658
|
-
qty: bool(p.qty, base.qty),
|
|
21659
|
-
colors: {
|
|
21660
|
-
long: color(c.long, base.colors.long),
|
|
21661
|
-
short: color(c.short, base.colors.short),
|
|
21662
|
-
exit: color(c.exit, base.colors.exit)
|
|
21663
|
-
}
|
|
21664
|
-
};
|
|
21665
|
-
}
|
|
21666
|
-
var BAR_GAP = 10;
|
|
21667
|
-
var ARROW_W = 9;
|
|
21668
|
-
var HEAD_H = 6;
|
|
21669
|
-
var ARROW_H = 14;
|
|
21670
|
-
var STEM_W = 3;
|
|
21671
|
-
var CAP_H = 2;
|
|
21672
|
-
var CAP_GAP = 2;
|
|
21673
|
-
var TEXT_GAP = 3;
|
|
21674
|
-
var UNIT_GAP = 6;
|
|
21675
|
-
var TICK_W = 6;
|
|
21676
|
-
var TICK_H = 8;
|
|
21677
|
-
function lineHeightOf(fontSize) {
|
|
21678
|
-
return fontSize + 4;
|
|
21679
|
-
}
|
|
21680
|
-
function qtyText(exec) {
|
|
21681
|
-
if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
|
|
21682
|
-
const magnitude = Number(Math.abs(exec.qty).toFixed(8));
|
|
21683
|
-
return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
|
|
21684
|
-
}
|
|
21685
|
-
function unitOf(exec, state, lineH) {
|
|
21686
|
-
const lines = [];
|
|
21687
|
-
if (state.labels && exec.label) lines.push(exec.label);
|
|
21688
|
-
if (state.qty) {
|
|
21689
|
-
const q = qtyText(exec);
|
|
21690
|
-
if (q) lines.push(q);
|
|
21691
|
-
}
|
|
21692
|
-
return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
|
|
21693
|
-
}
|
|
21694
|
-
function stacksFor(trades, state, deps, from, to, lineH) {
|
|
21695
|
-
const byBar = /* @__PURE__ */ new Map();
|
|
21696
|
-
for (const exec of trades) {
|
|
21697
|
-
const logical = Math.round(deps.timeToLogical(exec.time));
|
|
21698
|
-
if (logical < from || logical > to) continue;
|
|
21699
|
-
let stack = byBar.get(logical);
|
|
21700
|
-
if (!stack) {
|
|
21701
|
-
stack = { logical, buys: [], sells: [] };
|
|
21702
|
-
byBar.set(logical, stack);
|
|
21703
|
-
}
|
|
21704
|
-
(exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
|
|
21705
|
-
}
|
|
21706
|
-
return [...byBar.values()];
|
|
21707
|
-
}
|
|
21708
|
-
function stackExtent(units) {
|
|
21709
|
-
if (units.length === 0) return 0;
|
|
21710
|
-
let px = BAR_GAP;
|
|
21711
|
-
for (const u of units) px += u.height;
|
|
21712
|
-
return px + (units.length - 1) * UNIT_GAP;
|
|
21713
|
-
}
|
|
21714
|
-
function tradesPriceHints(trades, state, deps, from, to, fontSize) {
|
|
21715
|
-
if (trades.length === 0) return null;
|
|
21716
|
-
const lineH = lineHeightOf(fontSize);
|
|
21717
|
-
let min = Infinity;
|
|
21718
|
-
let max = -Infinity;
|
|
21719
|
-
let abovePx = 0;
|
|
21720
|
-
let belowPx = 0;
|
|
21721
|
-
for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
|
|
21722
|
-
const bar = deps.barAt(stack.logical);
|
|
21723
|
-
if (!bar) continue;
|
|
21724
|
-
if (bar.low < min) min = bar.low;
|
|
21725
|
-
if (bar.high > max) max = bar.high;
|
|
21726
|
-
belowPx = Math.max(belowPx, stackExtent(stack.buys));
|
|
21727
|
-
abovePx = Math.max(abovePx, stackExtent(stack.sells));
|
|
21728
|
-
}
|
|
21729
|
-
if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
|
|
21730
|
-
return { min, max, abovePx, belowPx };
|
|
21731
|
-
}
|
|
21732
|
-
function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
|
|
21733
|
-
if (trades.length === 0) return;
|
|
21734
|
-
const lineH = lineHeightOf(text.fontSize);
|
|
21735
|
-
const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
|
|
21736
|
-
if (stacks.length === 0) return;
|
|
21737
|
-
ctx.save();
|
|
21738
|
-
ctx.font = `${text.fontSize}px ${text.fontFamily}`;
|
|
21739
|
-
ctx.textAlign = "center";
|
|
21740
|
-
ctx.textBaseline = "middle";
|
|
21741
|
-
for (const stack of stacks) {
|
|
21742
|
-
const x = xOf(stack.logical);
|
|
21743
|
-
if (x < -150 || x > width + 150) continue;
|
|
21744
|
-
const bar = deps.barAt(stack.logical);
|
|
21745
|
-
if (!bar) continue;
|
|
21746
|
-
const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
|
|
21747
|
-
const yTop = Math.min(yOf(bar.low), yOf(bar.high));
|
|
21748
|
-
let y = yBottom + BAR_GAP;
|
|
21749
|
-
for (const unit of stack.buys) {
|
|
21750
|
-
ctx.fillStyle = colorOf(unit.exec, state.colors);
|
|
21751
|
-
drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
|
|
21752
|
-
drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
|
|
21753
|
-
drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
|
|
21754
|
-
y += unit.height + UNIT_GAP;
|
|
21755
|
-
}
|
|
21756
|
-
y = yTop - BAR_GAP;
|
|
21757
|
-
for (const unit of stack.sells) {
|
|
21758
|
-
ctx.fillStyle = colorOf(unit.exec, state.colors);
|
|
21759
|
-
drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
|
|
21760
|
-
drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
|
|
21761
|
-
drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
|
|
21762
|
-
y -= unit.height + UNIT_GAP;
|
|
21763
|
-
}
|
|
21764
|
-
}
|
|
21765
|
-
ctx.restore();
|
|
21766
|
-
}
|
|
21767
|
-
function colorOf(exec, colors) {
|
|
21768
|
-
if (exec.kind === "exit") return colors.exit;
|
|
21769
|
-
return exec.side === "buy" ? colors.long : colors.short;
|
|
21770
|
-
}
|
|
21771
|
-
function drawArrowUp(ctx, x, yTip, capped) {
|
|
21772
|
-
ctx.beginPath();
|
|
21773
|
-
ctx.moveTo(x, yTip);
|
|
21774
|
-
ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
|
|
21775
|
-
ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
|
|
21776
|
-
ctx.closePath();
|
|
21777
|
-
ctx.fill();
|
|
21778
|
-
ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
|
|
21779
|
-
if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
|
|
21780
|
-
}
|
|
21781
|
-
function drawArrowDown(ctx, x, yTip, capped) {
|
|
21782
|
-
ctx.beginPath();
|
|
21783
|
-
ctx.moveTo(x, yTip);
|
|
21784
|
-
ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
|
|
21785
|
-
ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
|
|
21786
|
-
ctx.closePath();
|
|
21787
|
-
ctx.fill();
|
|
21788
|
-
ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
|
|
21789
|
-
if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
|
|
21790
|
-
}
|
|
21791
|
-
function drawFillTick(ctx, side, x, yFill, barHalfPx) {
|
|
21792
|
-
const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
|
|
21793
|
-
const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
|
|
21794
|
-
ctx.beginPath();
|
|
21795
|
-
ctx.moveTo(edge, yFill);
|
|
21796
|
-
ctx.lineTo(back, yFill - TICK_H / 2);
|
|
21797
|
-
ctx.lineTo(back, yFill + TICK_H / 2);
|
|
21798
|
-
ctx.closePath();
|
|
21799
|
-
ctx.fill();
|
|
21800
|
-
}
|
|
21801
|
-
function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
21802
|
-
if (lines.length === 0) return;
|
|
21803
|
-
ctx.fillStyle = color;
|
|
21804
|
-
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
21805
|
-
}
|
|
21806
|
-
|
|
21807
|
-
// src/renderers/native/core/SceneGraph.ts
|
|
21808
|
-
var SceneGraph = class {
|
|
21809
|
-
constructor() {
|
|
21810
|
-
this.panes = /* @__PURE__ */ new Map();
|
|
21811
|
-
this.indicators = /* @__PURE__ */ new Map();
|
|
21812
|
-
this.bars = [];
|
|
21813
|
-
/** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
|
|
21814
|
-
this.volumeLayer = null;
|
|
21815
|
-
/** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
|
|
21816
|
-
this.nativeData = /* @__PURE__ */ new Map();
|
|
21817
|
-
/** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
|
|
21818
|
-
this.nativePending = /* @__PURE__ */ new Map();
|
|
21819
|
-
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
21820
|
-
this.vpvrLayer = null;
|
|
21821
|
-
this.crosshair = null;
|
|
21822
|
-
/** How the base price series is drawn on the price pane (candles by default). */
|
|
21823
|
-
this.priceStyle = "candles";
|
|
21824
|
-
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
21825
|
-
this.basePainting = "candles";
|
|
21826
|
-
/** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
|
|
21827
|
-
* candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
|
|
21828
|
-
this.candleOverride = null;
|
|
21829
|
-
/** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
|
|
21830
|
-
* baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
|
|
21831
|
-
* range (resolved per frame via `baselinePriceFor`). */
|
|
21832
|
-
this.baselineValue = null;
|
|
21833
|
-
/** Draw the dashed horizontal line at the latest price (price pane). Independent
|
|
21834
|
-
* of the axis label chip (`showPriceLabel`) — either can show without the other. */
|
|
21835
|
-
this.showPriceLine = true;
|
|
21836
|
-
/** Draw the last-price label chip on the price axis. Independent of the line. */
|
|
21837
|
-
this.showPriceLabel = true;
|
|
21838
|
-
/** Draw the countdown-to-bar-close chip on the price axis. When the price label is
|
|
21839
|
-
* also shown, the two merge into one stacked block (countdown under the label);
|
|
21840
|
-
* when either shows alone it's centered on the latest price level. */
|
|
21841
|
-
this.showCountdown = true;
|
|
21842
|
-
/** Logarithmic price scale on the price pane. */
|
|
21843
|
-
this.logScale = false;
|
|
21844
|
-
/** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
|
|
21845
|
-
this.invertScale = false;
|
|
21846
|
-
/** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
|
|
21847
|
-
* price-axis decimals — the instrument's true precision instead of the zoom-derived
|
|
21848
|
-
* formula. Undefined until symbol metadata loads (the formula is the fallback). */
|
|
21849
|
-
this.priceMintick = void 0;
|
|
21850
|
-
/** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
|
|
21851
|
-
* vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
|
|
21852
|
-
this.scaleMode = "price";
|
|
21853
|
-
/** Reference price for percent mode (first visible bar's close); recomputed per frame. */
|
|
21854
|
-
this.percentBaseline = 0;
|
|
21855
|
-
/** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
|
|
21856
|
-
this.timezone = "UTC";
|
|
21857
|
-
/** Draw the background gridlines (price + time). Master toggle (`gridlines`
|
|
21858
|
-
* feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
|
|
21859
|
-
this.showGrid = true;
|
|
21860
|
-
/** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
|
|
21861
|
-
* border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
|
|
21862
|
-
* `applyConfig()`; every draw layer reads its knobs from here, falling back to
|
|
21863
|
-
* the theme for any value left at its inherit default. */
|
|
21864
|
-
this.style = defaultChartStyle();
|
|
21865
|
-
/** Draw the price/time axis tick labels. */
|
|
21866
|
-
this.showAxisLabels = true;
|
|
21867
|
-
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
21868
|
-
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
21869
|
-
this.tradeMarkers = defaultTradeMarkersState();
|
|
21870
|
-
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
21871
|
-
this.highlights = [];
|
|
21872
|
-
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
21873
|
-
this.sessionZones = null;
|
|
21874
|
-
/** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
|
|
21875
|
-
* Indicators with z below this draw BEHIND the candles; at/above draw in front.
|
|
21876
|
-
* Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
|
|
21877
|
-
* and user drawings (z ≥ 1 by default) on top of the price. */
|
|
21878
|
-
this.candleZ = 0;
|
|
21879
|
-
/** Hide the base price series (candles/bars/line/area) without removing it — overlay
|
|
21880
|
-
* indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
|
|
21881
|
-
this.candlesHidden = false;
|
|
21882
|
-
/** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
|
|
21883
|
-
* Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
|
|
21884
|
-
* so each indicator arrives behind the candles (and behind older indicators);
|
|
21885
|
-
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
21886
|
-
this.seriesZ = /* @__PURE__ */ new Map();
|
|
21887
|
-
/** Per-pane raster layers of user drawings interleaved into the series stack — each is a
|
|
21888
|
-
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
21889
|
-
* Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
|
|
21890
|
-
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
21891
|
-
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
21892
|
-
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
21893
|
-
* bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
|
|
21894
|
-
* the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
|
|
21895
|
-
this.anchorOffsets = /* @__PURE__ */ new Map();
|
|
21896
|
-
/** Per-indicator private price windows (merged indicators drawn on their own scale
|
|
21897
|
-
* column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
|
|
21898
|
-
* shares its pane's master scale. */
|
|
21899
|
-
this.indicatorScales = /* @__PURE__ */ new Map();
|
|
21900
|
-
/** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
|
|
21901
|
-
this.orderedCache = null;
|
|
21902
|
-
}
|
|
21903
|
-
/** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
|
|
21904
|
-
orderedPanes() {
|
|
21905
|
-
if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
|
|
21906
|
-
return this.orderedCache;
|
|
21907
|
-
}
|
|
21908
|
-
/** The session zones resolved into colored bands (pre/post-market washes from the
|
|
21909
|
-
* config's session colors) — consumed by the same painting path as {@link highlights}. */
|
|
21910
|
-
sessionHighlightBands() {
|
|
21911
|
-
if (!this.sessionZones) return [];
|
|
21912
|
-
const out = [];
|
|
21913
|
-
for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
|
|
21914
|
-
for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
|
|
21915
|
-
return out;
|
|
21916
|
-
}
|
|
21917
|
-
indicatorsForPane(paneId) {
|
|
21918
|
-
const out = [];
|
|
21919
|
-
for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
|
|
21920
|
-
return out;
|
|
21921
|
-
}
|
|
21922
|
-
/** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
|
|
21923
|
-
ownScaleIndicatorsForPane(paneId) {
|
|
21924
|
-
return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
|
|
21925
|
-
}
|
|
21926
|
-
/** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
|
|
21927
|
-
ensureIndicatorScale(id, seed) {
|
|
21928
|
-
let s = this.indicatorScales.get(id);
|
|
21929
|
-
if (!s) {
|
|
21930
|
-
const base = seed ?? { min: 0, max: 1 };
|
|
21931
|
-
s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
|
|
21932
|
-
this.indicatorScales.set(id, s);
|
|
21933
|
-
}
|
|
21934
|
-
return s;
|
|
21935
|
-
}
|
|
21936
|
-
dropIndicatorScale(id) {
|
|
21937
|
-
this.indicatorScales.delete(id);
|
|
21938
|
-
}
|
|
21939
|
-
/** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
|
|
21940
|
-
scaleFor(model, pane) {
|
|
21941
|
-
if (model.ownScale === true) {
|
|
21942
|
-
const s = this.indicatorScales.get(model.id);
|
|
21943
|
-
if (s) return s.scale;
|
|
21944
|
-
}
|
|
21945
|
-
return pane.scale;
|
|
21946
|
-
}
|
|
21947
|
-
/** Apply a new top-to-bottom pane order (ids not present are ignored). */
|
|
21948
|
-
orderPanes(orderedIds) {
|
|
21949
|
-
orderedIds.forEach((id, i) => {
|
|
21950
|
-
const pane = this.panes.get(id);
|
|
21951
|
-
if (pane) pane.order = i;
|
|
21952
|
-
});
|
|
21953
|
-
this.orderedCache = null;
|
|
21954
|
-
}
|
|
21955
|
-
/** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
|
|
21956
|
-
* so equal-z models keep their insertion order (the default). */
|
|
21957
|
-
orderedIndicatorsForPane(paneId) {
|
|
21958
|
-
return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
|
|
21959
|
-
}
|
|
21960
|
-
/** The foreground draw-order key of an indicator (0 when never assigned). */
|
|
21961
|
-
zOf(id) {
|
|
21962
|
-
return this.seriesZ.get(id) ?? 0;
|
|
21963
|
-
}
|
|
21964
|
-
/** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
|
|
21965
|
-
offsetOf(id) {
|
|
21966
|
-
return this.anchorOffsets.get(id) ?? 0;
|
|
21967
|
-
}
|
|
21968
|
-
/** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
|
|
21969
|
-
* over a suffix) — readers skip its leading chart bars. Negative: the model starts
|
|
21970
|
-
* BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
|
|
21971
|
-
* model's own leading points, `points[i - off]` reaching further in. Storing only the
|
|
21972
|
-
* positive case silently pinned such a model at index 0, i.e. drew it shifted. */
|
|
21973
|
-
setAnchorOffset(id, offset) {
|
|
21974
|
-
if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
|
|
21975
|
-
else this.anchorOffsets.delete(id);
|
|
21976
|
-
}
|
|
21977
|
-
forgetAnchorOffset(id) {
|
|
21978
|
-
this.anchorOffsets.delete(id);
|
|
21979
|
-
}
|
|
21980
|
-
/** Resolve the baseline reference price for the given pane window: the explicit
|
|
21981
|
-
* `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
|
|
21982
|
-
* at that fraction of the pane height. Interpolated in the same space the pane renders
|
|
21983
|
-
* in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
|
|
21984
|
-
* height — matching `CoordinateSystem.yToPrice`. */
|
|
21985
|
-
baselinePriceFor(scale) {
|
|
21986
|
-
if (this.baselineValue != null) return this.baselineValue;
|
|
21987
|
-
const t = this.style.baseline.baselineLevel / 100;
|
|
21988
|
-
if (scale.log && scale.min > 0 && scale.max > scale.min) {
|
|
21989
|
-
const lo = Math.log(scale.min);
|
|
21990
|
-
return Math.exp(lo + t * (Math.log(scale.max) - lo));
|
|
21991
|
-
}
|
|
21992
|
-
return scale.min + (scale.max - scale.min) * t;
|
|
21993
|
-
}
|
|
21994
|
-
/** Assign a default z on mount: the current bottom of the stack, so a new indicator
|
|
21995
|
-
* paints behind the candles and behind every indicator already there — the price stays
|
|
21996
|
-
* the top of the pile until the user restacks it. No-op if the indicator already has one. */
|
|
21997
|
-
assignIndicatorZ(id) {
|
|
21998
|
-
if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
|
|
21999
|
-
}
|
|
22000
|
-
forgetIndicatorZ(id) {
|
|
22001
|
-
this.seriesZ.delete(id);
|
|
22002
|
-
}
|
|
22003
|
-
setIndicatorZ(id, z) {
|
|
22004
|
-
this.seriesZ.set(id, z);
|
|
22005
|
-
}
|
|
22006
|
-
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
|
|
22007
|
-
indicatorZOrder() {
|
|
22008
|
-
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
|
|
22009
|
-
}
|
|
22010
|
-
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
22011
|
-
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
22012
|
-
seriesBoundaries(paneId) {
|
|
22013
|
-
const keys = /* @__PURE__ */ new Set();
|
|
22014
|
-
if (paneId === "price") keys.add(this.candleZ);
|
|
22015
|
-
for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
|
|
22016
|
-
return [...keys].sort((a, b) => a - b);
|
|
22017
|
-
}
|
|
22018
|
-
/** Raise an indicator above every other layer (other indicators AND the candles). */
|
|
22019
|
-
bringIndicatorToFront(id) {
|
|
22020
|
-
this.seriesZ.set(id, this.topZ() + 1);
|
|
22021
|
-
}
|
|
22022
|
-
/** Drop an indicator below every other layer (other indicators AND the candles). */
|
|
22023
|
-
sendIndicatorToBack(id) {
|
|
22024
|
-
this.seriesZ.set(id, this.bottomZ() - 1);
|
|
22025
|
-
}
|
|
22026
|
-
topZ() {
|
|
22027
|
-
let max = this.candleZ;
|
|
22028
|
-
for (const z of this.seriesZ.values()) if (z > max) max = z;
|
|
22029
|
-
return max;
|
|
22030
|
-
}
|
|
22031
|
-
bottomZ() {
|
|
22032
|
-
let min = this.candleZ;
|
|
22033
|
-
for (const z of this.seriesZ.values()) if (z < min) min = z;
|
|
22034
|
-
return min;
|
|
22035
|
-
}
|
|
22036
|
-
ensurePane(id, kind, order, heightWeight) {
|
|
22037
|
-
this.orderedCache = null;
|
|
22038
|
-
const existing = this.panes.get(id);
|
|
22039
|
-
if (existing) {
|
|
22040
|
-
existing.order = order;
|
|
22041
|
-
existing.heightWeight = heightWeight;
|
|
22042
|
-
existing.kind = kind;
|
|
22043
|
-
return existing;
|
|
22044
|
-
}
|
|
22045
|
-
const pane = { id, kind, order, heightWeight, bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
|
|
22046
|
-
this.panes.set(id, pane);
|
|
22047
|
-
return pane;
|
|
22048
|
-
}
|
|
22049
|
-
removePane(id) {
|
|
22050
|
-
this.panes.delete(id);
|
|
22051
|
-
this.orderedCache = null;
|
|
22052
|
-
}
|
|
22053
|
-
};
|
|
22054
|
-
function paneScaleMode(scene, pane) {
|
|
22055
|
-
return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
|
|
22056
|
-
}
|
|
22057
|
-
function paneLogScale(scene, pane) {
|
|
22058
|
-
return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
|
|
22059
|
-
}
|
|
22060
|
-
function paneInvert(scene, pane) {
|
|
22061
|
-
return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
|
|
22062
|
-
}
|
|
22063
|
-
function percentScaleFor(scene, pane) {
|
|
22064
|
-
const mode = paneScaleMode(scene, pane);
|
|
22065
|
-
if (mode !== "percent" && mode !== "indexed") return void 0;
|
|
22066
|
-
const baseline = pane.percentBaseline;
|
|
22067
|
-
if (!Number.isFinite(baseline) || baseline === 0) return void 0;
|
|
22068
|
-
return { baseline, indexed: mode === "indexed" };
|
|
22069
|
-
}
|
|
22070
|
-
|
|
22071
|
-
// src/renderers/native/chrome/tz.ts
|
|
22072
|
-
function tzOffsetMs(ms, timeZone) {
|
|
22073
|
-
if (!timeZone || timeZone === "UTC") return 0;
|
|
22074
|
-
try {
|
|
22075
|
-
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
22076
|
-
timeZone,
|
|
22077
|
-
hourCycle: "h23",
|
|
22078
|
-
year: "numeric",
|
|
22079
|
-
month: "2-digit",
|
|
22080
|
-
day: "2-digit",
|
|
22081
|
-
hour: "2-digit",
|
|
22082
|
-
minute: "2-digit",
|
|
22083
|
-
second: "2-digit"
|
|
22084
|
-
});
|
|
22085
|
-
const parts = dtf.formatToParts(new Date(ms));
|
|
22086
|
-
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
22087
|
-
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
22088
|
-
return asUTC - ms;
|
|
22089
|
-
} catch {
|
|
22090
|
-
return 0;
|
|
22091
|
-
}
|
|
22092
|
-
}
|
|
22093
|
-
function zonedDate(ms, timeZone) {
|
|
22094
|
-
return new Date(ms + tzOffsetMs(ms, timeZone));
|
|
22095
|
-
}
|
|
22096
|
-
|
|
22097
21548
|
// src/renderers/native/backend/candle-lod.ts
|
|
22098
21549
|
var CANDLE_BODY_MIN_SPACING = 3;
|
|
22099
21550
|
var CANDLE_WICK_W = 1.5;
|
|
@@ -22383,11 +21834,6 @@ var DASH = {
|
|
|
22383
21834
|
dashed: [6, 4],
|
|
22384
21835
|
dotted: [2, 3]
|
|
22385
21836
|
};
|
|
22386
|
-
function crispHairline(cssCenter, dpr) {
|
|
22387
|
-
const w = Math.max(1, Math.round(dpr));
|
|
22388
|
-
const edge = Math.round(cssCenter * dpr - w / 2);
|
|
22389
|
-
return { pos: edge / dpr, size: w / dpr };
|
|
22390
|
-
}
|
|
22391
21837
|
function joinSegments(hw) {
|
|
22392
21838
|
return Math.max(6, Math.min(20, Math.round(hw * 4)));
|
|
22393
21839
|
}
|
|
@@ -22397,7 +21843,6 @@ var WebGL2Backend = class {
|
|
|
22397
21843
|
this.modelAlpha = 1;
|
|
22398
21844
|
this.candleBodyAlpha = 1;
|
|
22399
21845
|
this.candleStructureAlpha = 1;
|
|
22400
|
-
this.gridAlpha = 1;
|
|
22401
21846
|
this.candleBodyScale = 1;
|
|
22402
21847
|
/** Set by the renderer; invoked after a context restore to request a repaint. */
|
|
22403
21848
|
this.onNeedsRedraw = null;
|
|
@@ -22627,11 +22072,7 @@ var WebGL2Backend = class {
|
|
|
22627
22072
|
}
|
|
22628
22073
|
};
|
|
22629
22074
|
b.alpha = 1;
|
|
22630
|
-
if (pane.collapsed) {
|
|
22631
|
-
this.emitGrid(b, coords, theme, dataW, pane, scene);
|
|
22632
|
-
} else {
|
|
22633
|
-
this.emitHighlights(b, scene, pane, coords);
|
|
22634
|
-
this.emitGrid(b, coords, theme, dataW, pane, scene);
|
|
22075
|
+
if (!pane.collapsed) {
|
|
22635
22076
|
const models = scene.orderedIndicatorsForPane(pane.id);
|
|
22636
22077
|
const isPrice = pane.kind === "price";
|
|
22637
22078
|
const effPane = (m) => {
|
|
@@ -22816,49 +22257,6 @@ var WebGL2Backend = class {
|
|
|
22816
22257
|
this.screenProgram = null;
|
|
22817
22258
|
}
|
|
22818
22259
|
// ── geometry emit (mirrors Canvas2dBackend, in CSS-px space) ──
|
|
22819
|
-
emitGrid(b, coords, theme, dataW, pane, scene) {
|
|
22820
|
-
const top = pane.bounds.top;
|
|
22821
|
-
const bot = pane.bounds.top + pane.bounds.height;
|
|
22822
|
-
const { gridVert, gridHorz } = scene.style;
|
|
22823
|
-
const dpr = coords.dpr;
|
|
22824
|
-
b.alpha = this.gridAlpha;
|
|
22825
|
-
if (scene.showGrid && gridVert.visible && !pane.collapsed) {
|
|
22826
|
-
const grid = parseColor(gridVert.color ?? theme.gridColor);
|
|
22827
|
-
const tr = coords.visibleTimeRange();
|
|
22828
|
-
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
22829
|
-
for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
|
|
22830
|
-
const x = coords.timeToX(tick.time);
|
|
22831
|
-
if (x < 0 || x > dataW) continue;
|
|
22832
|
-
const g = crispHairline(x, dpr);
|
|
22833
|
-
b.rect(g.pos, top, g.size, bot - top, grid);
|
|
22834
|
-
}
|
|
22835
|
-
}
|
|
22836
|
-
if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
|
|
22837
|
-
const grid = parseColor(gridHorz.color ?? theme.gridColor);
|
|
22838
|
-
const pct = percentScaleFor(scene, pane);
|
|
22839
|
-
for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
|
|
22840
|
-
const y = coords.priceToY(t.price, pane.scale, pane.bounds);
|
|
22841
|
-
if (y < top || y > bot) continue;
|
|
22842
|
-
const g = crispHairline(y, dpr);
|
|
22843
|
-
b.rect(0, g.pos, dataW, g.size, grid);
|
|
22844
|
-
}
|
|
22845
|
-
}
|
|
22846
|
-
}
|
|
22847
|
-
/** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height).
|
|
22848
|
-
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
22849
|
-
emitHighlights(b, scene, pane, coords) {
|
|
22850
|
-
const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
|
|
22851
|
-
if (bands.length === 0) return;
|
|
22852
|
-
for (const band of bands) {
|
|
22853
|
-
const x1 = coords.timeToX(band.from);
|
|
22854
|
-
const x2 = coords.timeToX(band.to);
|
|
22855
|
-
if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
|
|
22856
|
-
const cx = Math.max(0, x1);
|
|
22857
|
-
const cw = Math.min(coords.width, x2) - cx;
|
|
22858
|
-
if (cw <= 0) continue;
|
|
22859
|
-
b.rect(cx, pane.bounds.top, cw, pane.bounds.height, parseColor(band.color));
|
|
22860
|
-
}
|
|
22861
|
-
}
|
|
22862
22260
|
emitBackground(b, bg, pane, coords) {
|
|
22863
22261
|
const x1 = coords.timeToX(bg.from);
|
|
22864
22262
|
const x2 = coords.timeToX(bg.to);
|
|
@@ -24181,6 +23579,445 @@ var KeyboardController = class {
|
|
|
24181
23579
|
}
|
|
24182
23580
|
};
|
|
24183
23581
|
|
|
23582
|
+
// src/renderers/shared/trade-markers.ts
|
|
23583
|
+
var TRADE_LONG_COLOR = TRADE_LONG;
|
|
23584
|
+
var TRADE_SHORT_COLOR = TRADE_SHORT;
|
|
23585
|
+
var TRADE_EXIT_COLOR = TRADE_EXIT;
|
|
23586
|
+
function defaultTradeMarkersState() {
|
|
23587
|
+
return {
|
|
23588
|
+
visible: true,
|
|
23589
|
+
labels: true,
|
|
23590
|
+
qty: true,
|
|
23591
|
+
colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
|
|
23592
|
+
};
|
|
23593
|
+
}
|
|
23594
|
+
function mergeTradeMarkersState(base, patch) {
|
|
23595
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
23596
|
+
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
23597
|
+
const bool = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
23598
|
+
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
23599
|
+
return {
|
|
23600
|
+
visible: bool(p.visible, base.visible),
|
|
23601
|
+
labels: bool(p.labels, base.labels),
|
|
23602
|
+
qty: bool(p.qty, base.qty),
|
|
23603
|
+
colors: {
|
|
23604
|
+
long: color(c.long, base.colors.long),
|
|
23605
|
+
short: color(c.short, base.colors.short),
|
|
23606
|
+
exit: color(c.exit, base.colors.exit)
|
|
23607
|
+
}
|
|
23608
|
+
};
|
|
23609
|
+
}
|
|
23610
|
+
var BAR_GAP = 10;
|
|
23611
|
+
var ARROW_W = 9;
|
|
23612
|
+
var HEAD_H = 6;
|
|
23613
|
+
var ARROW_H = 14;
|
|
23614
|
+
var STEM_W = 3;
|
|
23615
|
+
var CAP_H = 2;
|
|
23616
|
+
var CAP_GAP = 2;
|
|
23617
|
+
var TEXT_GAP = 3;
|
|
23618
|
+
var UNIT_GAP = 6;
|
|
23619
|
+
var TICK_W = 6;
|
|
23620
|
+
var TICK_H = 8;
|
|
23621
|
+
function lineHeightOf(fontSize) {
|
|
23622
|
+
return fontSize + 4;
|
|
23623
|
+
}
|
|
23624
|
+
function qtyText(exec) {
|
|
23625
|
+
if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
|
|
23626
|
+
const magnitude = Number(Math.abs(exec.qty).toFixed(8));
|
|
23627
|
+
return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
|
|
23628
|
+
}
|
|
23629
|
+
function unitOf(exec, state, lineH) {
|
|
23630
|
+
const lines = [];
|
|
23631
|
+
if (state.labels && exec.label) lines.push(exec.label);
|
|
23632
|
+
if (state.qty) {
|
|
23633
|
+
const q = qtyText(exec);
|
|
23634
|
+
if (q) lines.push(q);
|
|
23635
|
+
}
|
|
23636
|
+
return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
|
|
23637
|
+
}
|
|
23638
|
+
function stacksFor(trades, state, deps, from, to, lineH) {
|
|
23639
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
23640
|
+
for (const exec of trades) {
|
|
23641
|
+
const logical = Math.round(deps.timeToLogical(exec.time));
|
|
23642
|
+
if (logical < from || logical > to) continue;
|
|
23643
|
+
let stack = byBar.get(logical);
|
|
23644
|
+
if (!stack) {
|
|
23645
|
+
stack = { logical, buys: [], sells: [] };
|
|
23646
|
+
byBar.set(logical, stack);
|
|
23647
|
+
}
|
|
23648
|
+
(exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
|
|
23649
|
+
}
|
|
23650
|
+
return [...byBar.values()];
|
|
23651
|
+
}
|
|
23652
|
+
function stackExtent(units) {
|
|
23653
|
+
if (units.length === 0) return 0;
|
|
23654
|
+
let px = BAR_GAP;
|
|
23655
|
+
for (const u of units) px += u.height;
|
|
23656
|
+
return px + (units.length - 1) * UNIT_GAP;
|
|
23657
|
+
}
|
|
23658
|
+
function tradesPriceHints(trades, state, deps, from, to, fontSize) {
|
|
23659
|
+
if (trades.length === 0) return null;
|
|
23660
|
+
const lineH = lineHeightOf(fontSize);
|
|
23661
|
+
let min = Infinity;
|
|
23662
|
+
let max = -Infinity;
|
|
23663
|
+
let abovePx = 0;
|
|
23664
|
+
let belowPx = 0;
|
|
23665
|
+
for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
|
|
23666
|
+
const bar = deps.barAt(stack.logical);
|
|
23667
|
+
if (!bar) continue;
|
|
23668
|
+
if (bar.low < min) min = bar.low;
|
|
23669
|
+
if (bar.high > max) max = bar.high;
|
|
23670
|
+
belowPx = Math.max(belowPx, stackExtent(stack.buys));
|
|
23671
|
+
abovePx = Math.max(abovePx, stackExtent(stack.sells));
|
|
23672
|
+
}
|
|
23673
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
|
|
23674
|
+
return { min, max, abovePx, belowPx };
|
|
23675
|
+
}
|
|
23676
|
+
function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
|
|
23677
|
+
if (trades.length === 0) return;
|
|
23678
|
+
const lineH = lineHeightOf(text.fontSize);
|
|
23679
|
+
const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
|
|
23680
|
+
if (stacks.length === 0) return;
|
|
23681
|
+
ctx.save();
|
|
23682
|
+
ctx.font = `${text.fontSize}px ${text.fontFamily}`;
|
|
23683
|
+
ctx.textAlign = "center";
|
|
23684
|
+
ctx.textBaseline = "middle";
|
|
23685
|
+
for (const stack of stacks) {
|
|
23686
|
+
const x = xOf(stack.logical);
|
|
23687
|
+
if (x < -150 || x > width + 150) continue;
|
|
23688
|
+
const bar = deps.barAt(stack.logical);
|
|
23689
|
+
if (!bar) continue;
|
|
23690
|
+
const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
|
|
23691
|
+
const yTop = Math.min(yOf(bar.low), yOf(bar.high));
|
|
23692
|
+
let y = yBottom + BAR_GAP;
|
|
23693
|
+
for (const unit of stack.buys) {
|
|
23694
|
+
ctx.fillStyle = colorOf(unit.exec, state.colors);
|
|
23695
|
+
drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
|
|
23696
|
+
drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
|
|
23697
|
+
drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
|
|
23698
|
+
y += unit.height + UNIT_GAP;
|
|
23699
|
+
}
|
|
23700
|
+
y = yTop - BAR_GAP;
|
|
23701
|
+
for (const unit of stack.sells) {
|
|
23702
|
+
ctx.fillStyle = colorOf(unit.exec, state.colors);
|
|
23703
|
+
drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
|
|
23704
|
+
drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
|
|
23705
|
+
drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
|
|
23706
|
+
y -= unit.height + UNIT_GAP;
|
|
23707
|
+
}
|
|
23708
|
+
}
|
|
23709
|
+
ctx.restore();
|
|
23710
|
+
}
|
|
23711
|
+
function colorOf(exec, colors) {
|
|
23712
|
+
if (exec.kind === "exit") return colors.exit;
|
|
23713
|
+
return exec.side === "buy" ? colors.long : colors.short;
|
|
23714
|
+
}
|
|
23715
|
+
function drawArrowUp(ctx, x, yTip, capped) {
|
|
23716
|
+
ctx.beginPath();
|
|
23717
|
+
ctx.moveTo(x, yTip);
|
|
23718
|
+
ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
|
|
23719
|
+
ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
|
|
23720
|
+
ctx.closePath();
|
|
23721
|
+
ctx.fill();
|
|
23722
|
+
ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
|
|
23723
|
+
if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
|
|
23724
|
+
}
|
|
23725
|
+
function drawArrowDown(ctx, x, yTip, capped) {
|
|
23726
|
+
ctx.beginPath();
|
|
23727
|
+
ctx.moveTo(x, yTip);
|
|
23728
|
+
ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
|
|
23729
|
+
ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
|
|
23730
|
+
ctx.closePath();
|
|
23731
|
+
ctx.fill();
|
|
23732
|
+
ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
|
|
23733
|
+
if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
|
|
23734
|
+
}
|
|
23735
|
+
function drawFillTick(ctx, side, x, yFill, barHalfPx) {
|
|
23736
|
+
const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
|
|
23737
|
+
const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
|
|
23738
|
+
ctx.beginPath();
|
|
23739
|
+
ctx.moveTo(edge, yFill);
|
|
23740
|
+
ctx.lineTo(back, yFill - TICK_H / 2);
|
|
23741
|
+
ctx.lineTo(back, yFill + TICK_H / 2);
|
|
23742
|
+
ctx.closePath();
|
|
23743
|
+
ctx.fill();
|
|
23744
|
+
}
|
|
23745
|
+
function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
23746
|
+
if (lines.length === 0) return;
|
|
23747
|
+
ctx.fillStyle = color;
|
|
23748
|
+
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
23749
|
+
}
|
|
23750
|
+
|
|
23751
|
+
// src/renderers/native/core/SceneGraph.ts
|
|
23752
|
+
var SceneGraph = class {
|
|
23753
|
+
constructor() {
|
|
23754
|
+
this.panes = /* @__PURE__ */ new Map();
|
|
23755
|
+
this.indicators = /* @__PURE__ */ new Map();
|
|
23756
|
+
this.bars = [];
|
|
23757
|
+
/** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
|
|
23758
|
+
this.volumeLayer = null;
|
|
23759
|
+
/** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
|
|
23760
|
+
this.nativeData = /* @__PURE__ */ new Map();
|
|
23761
|
+
/** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
|
|
23762
|
+
this.nativePending = /* @__PURE__ */ new Map();
|
|
23763
|
+
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
23764
|
+
this.vpvrLayer = null;
|
|
23765
|
+
this.crosshair = null;
|
|
23766
|
+
/** How the base price series is drawn on the price pane (candles by default). */
|
|
23767
|
+
this.priceStyle = "candles";
|
|
23768
|
+
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
23769
|
+
this.basePainting = "candles";
|
|
23770
|
+
/** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
|
|
23771
|
+
* candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
|
|
23772
|
+
this.candleOverride = null;
|
|
23773
|
+
/** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
|
|
23774
|
+
* baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
|
|
23775
|
+
* range (resolved per frame via `baselinePriceFor`). */
|
|
23776
|
+
this.baselineValue = null;
|
|
23777
|
+
/** Draw the dashed horizontal line at the latest price (price pane). Independent
|
|
23778
|
+
* of the axis label chip (`showPriceLabel`) — either can show without the other. */
|
|
23779
|
+
this.showPriceLine = true;
|
|
23780
|
+
/** Draw the last-price label chip on the price axis. Independent of the line. */
|
|
23781
|
+
this.showPriceLabel = true;
|
|
23782
|
+
/** Draw the countdown-to-bar-close chip on the price axis. When the price label is
|
|
23783
|
+
* also shown, the two merge into one stacked block (countdown under the label);
|
|
23784
|
+
* when either shows alone it's centered on the latest price level. */
|
|
23785
|
+
this.showCountdown = true;
|
|
23786
|
+
/** Logarithmic price scale on the price pane. */
|
|
23787
|
+
this.logScale = false;
|
|
23788
|
+
/** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
|
|
23789
|
+
this.invertScale = false;
|
|
23790
|
+
/** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
|
|
23791
|
+
* price-axis decimals — the instrument's true precision instead of the zoom-derived
|
|
23792
|
+
* formula. Undefined until symbol metadata loads (the formula is the fallback). */
|
|
23793
|
+
this.priceMintick = void 0;
|
|
23794
|
+
/** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
|
|
23795
|
+
* vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
|
|
23796
|
+
this.scaleMode = "price";
|
|
23797
|
+
/** Reference price for percent mode (first visible bar's close); recomputed per frame. */
|
|
23798
|
+
this.percentBaseline = 0;
|
|
23799
|
+
/** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
|
|
23800
|
+
this.timezone = "UTC";
|
|
23801
|
+
/** Draw the background gridlines (price + time). Master toggle (`gridlines`
|
|
23802
|
+
* feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
|
|
23803
|
+
this.showGrid = true;
|
|
23804
|
+
/** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
|
|
23805
|
+
* border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
|
|
23806
|
+
* `applyConfig()`; every draw layer reads its knobs from here, falling back to
|
|
23807
|
+
* the theme for any value left at its inherit default. */
|
|
23808
|
+
this.style = defaultChartStyle();
|
|
23809
|
+
/** Draw the price/time axis tick labels. */
|
|
23810
|
+
this.showAxisLabels = true;
|
|
23811
|
+
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
23812
|
+
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
23813
|
+
this.tradeMarkers = defaultTradeMarkersState();
|
|
23814
|
+
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
23815
|
+
this.highlights = [];
|
|
23816
|
+
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
23817
|
+
this.sessionZones = null;
|
|
23818
|
+
/** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
|
|
23819
|
+
* Indicators with z below this draw BEHIND the candles; at/above draw in front.
|
|
23820
|
+
* Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
|
|
23821
|
+
* and user drawings (z ≥ 1 by default) on top of the price. */
|
|
23822
|
+
this.candleZ = 0;
|
|
23823
|
+
/** Hide the base price series (candles/bars/line/area) without removing it — overlay
|
|
23824
|
+
* indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
|
|
23825
|
+
this.candlesHidden = false;
|
|
23826
|
+
/** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
|
|
23827
|
+
* Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
|
|
23828
|
+
* so each indicator arrives behind the candles (and behind older indicators);
|
|
23829
|
+
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
23830
|
+
this.seriesZ = /* @__PURE__ */ new Map();
|
|
23831
|
+
/** Per-pane raster layers of user drawings interleaved into the series stack — each is a
|
|
23832
|
+
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
23833
|
+
* Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
|
|
23834
|
+
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
23835
|
+
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
23836
|
+
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
23837
|
+
* bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
|
|
23838
|
+
* the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
|
|
23839
|
+
this.anchorOffsets = /* @__PURE__ */ new Map();
|
|
23840
|
+
/** Per-indicator private price windows (merged indicators drawn on their own scale
|
|
23841
|
+
* column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
|
|
23842
|
+
* shares its pane's master scale. */
|
|
23843
|
+
this.indicatorScales = /* @__PURE__ */ new Map();
|
|
23844
|
+
/** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
|
|
23845
|
+
this.orderedCache = null;
|
|
23846
|
+
}
|
|
23847
|
+
/** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
|
|
23848
|
+
orderedPanes() {
|
|
23849
|
+
if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
|
|
23850
|
+
return this.orderedCache;
|
|
23851
|
+
}
|
|
23852
|
+
/** The session zones resolved into colored bands (pre/post-market washes from the
|
|
23853
|
+
* config's session colors) — consumed by the same painting path as {@link highlights}. */
|
|
23854
|
+
sessionHighlightBands() {
|
|
23855
|
+
if (!this.sessionZones) return [];
|
|
23856
|
+
const out = [];
|
|
23857
|
+
for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
|
|
23858
|
+
for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
|
|
23859
|
+
return out;
|
|
23860
|
+
}
|
|
23861
|
+
indicatorsForPane(paneId) {
|
|
23862
|
+
const out = [];
|
|
23863
|
+
for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
|
|
23864
|
+
return out;
|
|
23865
|
+
}
|
|
23866
|
+
/** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
|
|
23867
|
+
ownScaleIndicatorsForPane(paneId) {
|
|
23868
|
+
return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
|
|
23869
|
+
}
|
|
23870
|
+
/** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
|
|
23871
|
+
ensureIndicatorScale(id, seed) {
|
|
23872
|
+
let s = this.indicatorScales.get(id);
|
|
23873
|
+
if (!s) {
|
|
23874
|
+
const base = seed ?? { min: 0, max: 1 };
|
|
23875
|
+
s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
|
|
23876
|
+
this.indicatorScales.set(id, s);
|
|
23877
|
+
}
|
|
23878
|
+
return s;
|
|
23879
|
+
}
|
|
23880
|
+
dropIndicatorScale(id) {
|
|
23881
|
+
this.indicatorScales.delete(id);
|
|
23882
|
+
}
|
|
23883
|
+
/** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
|
|
23884
|
+
scaleFor(model, pane) {
|
|
23885
|
+
if (model.ownScale === true) {
|
|
23886
|
+
const s = this.indicatorScales.get(model.id);
|
|
23887
|
+
if (s) return s.scale;
|
|
23888
|
+
}
|
|
23889
|
+
return pane.scale;
|
|
23890
|
+
}
|
|
23891
|
+
/** Apply a new top-to-bottom pane order (ids not present are ignored). */
|
|
23892
|
+
orderPanes(orderedIds) {
|
|
23893
|
+
orderedIds.forEach((id, i) => {
|
|
23894
|
+
const pane = this.panes.get(id);
|
|
23895
|
+
if (pane) pane.order = i;
|
|
23896
|
+
});
|
|
23897
|
+
this.orderedCache = null;
|
|
23898
|
+
}
|
|
23899
|
+
/** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
|
|
23900
|
+
* so equal-z models keep their insertion order (the default). */
|
|
23901
|
+
orderedIndicatorsForPane(paneId) {
|
|
23902
|
+
return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
|
|
23903
|
+
}
|
|
23904
|
+
/** The foreground draw-order key of an indicator (0 when never assigned). */
|
|
23905
|
+
zOf(id) {
|
|
23906
|
+
return this.seriesZ.get(id) ?? 0;
|
|
23907
|
+
}
|
|
23908
|
+
/** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
|
|
23909
|
+
offsetOf(id) {
|
|
23910
|
+
return this.anchorOffsets.get(id) ?? 0;
|
|
23911
|
+
}
|
|
23912
|
+
/** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
|
|
23913
|
+
* over a suffix) — readers skip its leading chart bars. Negative: the model starts
|
|
23914
|
+
* BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
|
|
23915
|
+
* model's own leading points, `points[i - off]` reaching further in. Storing only the
|
|
23916
|
+
* positive case silently pinned such a model at index 0, i.e. drew it shifted. */
|
|
23917
|
+
setAnchorOffset(id, offset) {
|
|
23918
|
+
if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
|
|
23919
|
+
else this.anchorOffsets.delete(id);
|
|
23920
|
+
}
|
|
23921
|
+
forgetAnchorOffset(id) {
|
|
23922
|
+
this.anchorOffsets.delete(id);
|
|
23923
|
+
}
|
|
23924
|
+
/** Resolve the baseline reference price for the given pane window: the explicit
|
|
23925
|
+
* `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
|
|
23926
|
+
* at that fraction of the pane height. Interpolated in the same space the pane renders
|
|
23927
|
+
* in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
|
|
23928
|
+
* height — matching `CoordinateSystem.yToPrice`. */
|
|
23929
|
+
baselinePriceFor(scale) {
|
|
23930
|
+
if (this.baselineValue != null) return this.baselineValue;
|
|
23931
|
+
const t = this.style.baseline.baselineLevel / 100;
|
|
23932
|
+
if (scale.log && scale.min > 0 && scale.max > scale.min) {
|
|
23933
|
+
const lo = Math.log(scale.min);
|
|
23934
|
+
return Math.exp(lo + t * (Math.log(scale.max) - lo));
|
|
23935
|
+
}
|
|
23936
|
+
return scale.min + (scale.max - scale.min) * t;
|
|
23937
|
+
}
|
|
23938
|
+
/** Assign a default z on mount: the current bottom of the stack, so a new indicator
|
|
23939
|
+
* paints behind the candles and behind every indicator already there — the price stays
|
|
23940
|
+
* the top of the pile until the user restacks it. No-op if the indicator already has one. */
|
|
23941
|
+
assignIndicatorZ(id) {
|
|
23942
|
+
if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
|
|
23943
|
+
}
|
|
23944
|
+
/** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
|
|
23945
|
+
* data canvas by default): top of the stack, so the recorded order tells the truth from
|
|
23946
|
+
* the first frame. Keeps an existing key, so a restored stack survives the remount. */
|
|
23947
|
+
assignIndicatorZTop(id) {
|
|
23948
|
+
if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
|
|
23949
|
+
}
|
|
23950
|
+
forgetIndicatorZ(id) {
|
|
23951
|
+
this.seriesZ.delete(id);
|
|
23952
|
+
}
|
|
23953
|
+
setIndicatorZ(id, z) {
|
|
23954
|
+
this.seriesZ.set(id, z);
|
|
23955
|
+
}
|
|
23956
|
+
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
|
|
23957
|
+
indicatorZOrder() {
|
|
23958
|
+
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
|
|
23959
|
+
}
|
|
23960
|
+
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
23961
|
+
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
23962
|
+
seriesBoundaries(paneId) {
|
|
23963
|
+
const keys = /* @__PURE__ */ new Set();
|
|
23964
|
+
if (paneId === "price") keys.add(this.candleZ);
|
|
23965
|
+
for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
|
|
23966
|
+
return [...keys].sort((a, b) => a - b);
|
|
23967
|
+
}
|
|
23968
|
+
/** Raise an indicator above every other layer (other indicators AND the candles). */
|
|
23969
|
+
bringIndicatorToFront(id) {
|
|
23970
|
+
this.seriesZ.set(id, this.topZ() + 1);
|
|
23971
|
+
}
|
|
23972
|
+
/** Drop an indicator below every other layer (other indicators AND the candles). */
|
|
23973
|
+
sendIndicatorToBack(id) {
|
|
23974
|
+
this.seriesZ.set(id, this.bottomZ() - 1);
|
|
23975
|
+
}
|
|
23976
|
+
topZ() {
|
|
23977
|
+
let max = this.candleZ;
|
|
23978
|
+
for (const z of this.seriesZ.values()) if (z > max) max = z;
|
|
23979
|
+
return max;
|
|
23980
|
+
}
|
|
23981
|
+
bottomZ() {
|
|
23982
|
+
let min = this.candleZ;
|
|
23983
|
+
for (const z of this.seriesZ.values()) if (z < min) min = z;
|
|
23984
|
+
return min;
|
|
23985
|
+
}
|
|
23986
|
+
ensurePane(id, kind, order, heightWeight) {
|
|
23987
|
+
this.orderedCache = null;
|
|
23988
|
+
const existing = this.panes.get(id);
|
|
23989
|
+
if (existing) {
|
|
23990
|
+
existing.order = order;
|
|
23991
|
+
existing.heightWeight = heightWeight;
|
|
23992
|
+
existing.kind = kind;
|
|
23993
|
+
return existing;
|
|
23994
|
+
}
|
|
23995
|
+
const pane = { id, kind, order, heightWeight, bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
|
|
23996
|
+
this.panes.set(id, pane);
|
|
23997
|
+
return pane;
|
|
23998
|
+
}
|
|
23999
|
+
removePane(id) {
|
|
24000
|
+
this.panes.delete(id);
|
|
24001
|
+
this.orderedCache = null;
|
|
24002
|
+
}
|
|
24003
|
+
};
|
|
24004
|
+
function paneScaleMode(scene, pane) {
|
|
24005
|
+
return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
|
|
24006
|
+
}
|
|
24007
|
+
function paneLogScale(scene, pane) {
|
|
24008
|
+
return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
|
|
24009
|
+
}
|
|
24010
|
+
function paneInvert(scene, pane) {
|
|
24011
|
+
return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
|
|
24012
|
+
}
|
|
24013
|
+
function percentScaleFor(scene, pane) {
|
|
24014
|
+
const mode = paneScaleMode(scene, pane);
|
|
24015
|
+
if (mode !== "percent" && mode !== "indexed") return void 0;
|
|
24016
|
+
const baseline = pane.percentBaseline;
|
|
24017
|
+
if (!Number.isFinite(baseline) || baseline === 0) return void 0;
|
|
24018
|
+
return { baseline, indexed: mode === "indexed" };
|
|
24019
|
+
}
|
|
24020
|
+
|
|
24184
24021
|
// src/renderers/native/backend/Canvas2dBackend.ts
|
|
24185
24022
|
var Canvas2dBackend = class {
|
|
24186
24023
|
constructor() {
|
|
@@ -24188,7 +24025,6 @@ var Canvas2dBackend = class {
|
|
|
24188
24025
|
this.modelAlpha = 1;
|
|
24189
24026
|
this.candleBodyAlpha = 1;
|
|
24190
24027
|
this.candleStructureAlpha = 1;
|
|
24191
|
-
this.gridAlpha = 1;
|
|
24192
24028
|
this.candleBodyScale = 1;
|
|
24193
24029
|
this.canvas = null;
|
|
24194
24030
|
this.ctx = null;
|
|
@@ -24215,8 +24051,6 @@ var Canvas2dBackend = class {
|
|
|
24215
24051
|
if (i1 < i0) return;
|
|
24216
24052
|
const barColorMap = mergeBarColors2(scene.indicators);
|
|
24217
24053
|
const panes = scene.orderedPanes();
|
|
24218
|
-
this.drawHighlights(ctx, scene, coords);
|
|
24219
|
-
this.drawGrid(ctx, scene, coords, theme, dataW);
|
|
24220
24054
|
for (const pane of panes) {
|
|
24221
24055
|
if (pane.collapsed) continue;
|
|
24222
24056
|
const models = scene.orderedIndicatorsForPane(pane.id);
|
|
@@ -24823,22 +24657,6 @@ var Canvas2dBackend = class {
|
|
|
24823
24657
|
ctx.fillStyle = bg.color;
|
|
24824
24658
|
ctx.fillRect(x1, pane.bounds.top, x2 - x1, pane.bounds.height);
|
|
24825
24659
|
}
|
|
24826
|
-
/** Renderer-owned session highlight bands: full-height (all panes), behind grid + data.
|
|
24827
|
-
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
24828
|
-
drawHighlights(ctx, scene, coords) {
|
|
24829
|
-
const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
|
|
24830
|
-
if (bands.length === 0) return;
|
|
24831
|
-
for (const band of bands) {
|
|
24832
|
-
const x1 = coords.timeToX(band.from);
|
|
24833
|
-
const x2 = coords.timeToX(band.to);
|
|
24834
|
-
if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
|
|
24835
|
-
const cx = Math.max(0, x1);
|
|
24836
|
-
const cw = Math.min(coords.width, x2) - cx;
|
|
24837
|
-
if (cw <= 0) continue;
|
|
24838
|
-
ctx.fillStyle = band.color;
|
|
24839
|
-
ctx.fillRect(cx, 0, cw, coords.height);
|
|
24840
|
-
}
|
|
24841
|
-
}
|
|
24842
24660
|
drawHline(ctx, pl, pane, coords, dataW, theme) {
|
|
24843
24661
|
const y = Math.round(coords.priceToY(pl.price, pane.scale, pane.bounds)) + 0.5;
|
|
24844
24662
|
if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) return;
|
|
@@ -24851,46 +24669,6 @@ var Canvas2dBackend = class {
|
|
|
24851
24669
|
ctx.stroke();
|
|
24852
24670
|
setDash(ctx, "solid");
|
|
24853
24671
|
}
|
|
24854
|
-
// ── grid (L0, behind data) ── vert/horz gate on `scene.showGrid` AND their own
|
|
24855
|
-
// per-axis visibility (style); each uses its own color. Pane separators are drawn on the
|
|
24856
|
-
// chrome layer (full-width, above the data) so series never overpaint them.
|
|
24857
|
-
drawGrid(ctx, scene, coords, theme, dataW) {
|
|
24858
|
-
const panes = scene.orderedPanes();
|
|
24859
|
-
const { gridVert, gridHorz } = scene.style;
|
|
24860
|
-
const vertColor = gridVert.color ?? theme.gridColor;
|
|
24861
|
-
const horzColor = gridHorz.color ?? theme.gridColor;
|
|
24862
|
-
ctx.lineWidth = 1;
|
|
24863
|
-
if (scene.showGrid && gridVert.visible) {
|
|
24864
|
-
ctx.globalAlpha = this.gridAlpha;
|
|
24865
|
-
ctx.strokeStyle = vertColor;
|
|
24866
|
-
const tr = coords.visibleTimeRange();
|
|
24867
|
-
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
24868
|
-
ctx.beginPath();
|
|
24869
|
-
for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
|
|
24870
|
-
const x = Math.round(coords.timeToX(tick.time)) + 0.5;
|
|
24871
|
-
if (x < 0 || x > dataW) continue;
|
|
24872
|
-
ctx.moveTo(x, 0);
|
|
24873
|
-
ctx.lineTo(x, coords.height);
|
|
24874
|
-
}
|
|
24875
|
-
ctx.stroke();
|
|
24876
|
-
}
|
|
24877
|
-
for (const pane of panes) {
|
|
24878
|
-
if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
|
|
24879
|
-
ctx.globalAlpha = this.gridAlpha;
|
|
24880
|
-
ctx.strokeStyle = horzColor;
|
|
24881
|
-
const pct = percentScaleFor(scene, pane);
|
|
24882
|
-
ctx.beginPath();
|
|
24883
|
-
for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
|
|
24884
|
-
const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
|
|
24885
|
-
if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
|
|
24886
|
-
ctx.moveTo(0, y);
|
|
24887
|
-
ctx.lineTo(dataW, y);
|
|
24888
|
-
}
|
|
24889
|
-
ctx.stroke();
|
|
24890
|
-
}
|
|
24891
|
-
}
|
|
24892
|
-
ctx.globalAlpha = 1;
|
|
24893
|
-
}
|
|
24894
24672
|
};
|
|
24895
24673
|
function setDash(ctx, style) {
|
|
24896
24674
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
@@ -25602,6 +25380,221 @@ var DrawingSceneRenderer = class {
|
|
|
25602
25380
|
}
|
|
25603
25381
|
};
|
|
25604
25382
|
|
|
25383
|
+
// src/renderers/native/chrome/ticks.ts
|
|
25384
|
+
function priceTicks(min, max, target = 6) {
|
|
25385
|
+
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
25386
|
+
const raw = (max - min) / Math.max(1, target);
|
|
25387
|
+
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
25388
|
+
const norm = raw / mag;
|
|
25389
|
+
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
25390
|
+
const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
|
|
25391
|
+
const out = [];
|
|
25392
|
+
const start = Math.ceil(min / step) * step;
|
|
25393
|
+
for (let v = start; v <= max + step * 1e-6; v += step) {
|
|
25394
|
+
out.push(Number(v.toFixed(decimals)));
|
|
25395
|
+
}
|
|
25396
|
+
return out;
|
|
25397
|
+
}
|
|
25398
|
+
function priceDecimals(min, max, target = 6) {
|
|
25399
|
+
if (!(max > min)) return 2;
|
|
25400
|
+
const raw = (max - min) / Math.max(1, target);
|
|
25401
|
+
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
25402
|
+
const norm = raw / mag;
|
|
25403
|
+
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
25404
|
+
return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
|
|
25405
|
+
}
|
|
25406
|
+
function logPriceTicks(min, max, target = 6) {
|
|
25407
|
+
if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
25408
|
+
if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
|
|
25409
|
+
const out = [];
|
|
25410
|
+
const startExp = Math.floor(Math.log10(min));
|
|
25411
|
+
const endExp = Math.ceil(Math.log10(max));
|
|
25412
|
+
for (let e = startExp; e <= endExp; e += 1) {
|
|
25413
|
+
for (const m of [1, 2, 5]) {
|
|
25414
|
+
const v = m * Math.pow(10, e);
|
|
25415
|
+
if (v >= min && v <= max) out.push(v);
|
|
25416
|
+
}
|
|
25417
|
+
}
|
|
25418
|
+
return out;
|
|
25419
|
+
}
|
|
25420
|
+
function valueDecimals(v) {
|
|
25421
|
+
const a = Math.abs(v);
|
|
25422
|
+
if (a >= 100) return 0;
|
|
25423
|
+
if (a >= 1) return 2;
|
|
25424
|
+
if (a >= 0.01) return 4;
|
|
25425
|
+
return 6;
|
|
25426
|
+
}
|
|
25427
|
+
function tickDecimals(tick) {
|
|
25428
|
+
if (!(tick > 0) || !Number.isFinite(tick)) return 2;
|
|
25429
|
+
for (let d = 0; d <= 8; d += 1) {
|
|
25430
|
+
if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
|
|
25431
|
+
}
|
|
25432
|
+
return 8;
|
|
25433
|
+
}
|
|
25434
|
+
function axisDecimals(scale, heightPx, mintick) {
|
|
25435
|
+
const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
|
|
25436
|
+
return d === 0 ? 2 : d;
|
|
25437
|
+
}
|
|
25438
|
+
function tickCount(paneHeightPx) {
|
|
25439
|
+
return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
|
|
25440
|
+
}
|
|
25441
|
+
function paneTicks(scale, heightPx) {
|
|
25442
|
+
return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
|
|
25443
|
+
}
|
|
25444
|
+
function toPct(price, baseline) {
|
|
25445
|
+
return (price / baseline - 1) * 100;
|
|
25446
|
+
}
|
|
25447
|
+
function toIndex(price, baseline) {
|
|
25448
|
+
return price / baseline * 100;
|
|
25449
|
+
}
|
|
25450
|
+
function formatPct(pct) {
|
|
25451
|
+
const sign = pct >= 0 ? "+" : "-";
|
|
25452
|
+
return `${sign}${Math.abs(pct).toFixed(2)}%`;
|
|
25453
|
+
}
|
|
25454
|
+
function formatIndex(idx) {
|
|
25455
|
+
return idx.toFixed(2);
|
|
25456
|
+
}
|
|
25457
|
+
function formatCompactValue(v) {
|
|
25458
|
+
const a = Math.abs(v);
|
|
25459
|
+
if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
|
|
25460
|
+
if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
|
|
25461
|
+
if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
|
|
25462
|
+
return trimZeros(v);
|
|
25463
|
+
}
|
|
25464
|
+
function trimZeros(v) {
|
|
25465
|
+
return Number(v.toFixed(2)).toString();
|
|
25466
|
+
}
|
|
25467
|
+
function paneAxisTicks(scale, heightPx, pct, mintick, format) {
|
|
25468
|
+
if (format === "none") return [];
|
|
25469
|
+
if (pct) {
|
|
25470
|
+
const { baseline, indexed } = pct;
|
|
25471
|
+
const lo = Math.min(scale.min, scale.max);
|
|
25472
|
+
const hi = Math.max(scale.min, scale.max);
|
|
25473
|
+
if (indexed) {
|
|
25474
|
+
const iLo = toIndex(lo, baseline);
|
|
25475
|
+
const iHi = toIndex(hi, baseline);
|
|
25476
|
+
return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
|
|
25477
|
+
}
|
|
25478
|
+
const pLo = toPct(lo, baseline);
|
|
25479
|
+
const pHi = toPct(hi, baseline);
|
|
25480
|
+
return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
|
|
25481
|
+
}
|
|
25482
|
+
if (format === "volume") {
|
|
25483
|
+
return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
|
|
25484
|
+
}
|
|
25485
|
+
return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
|
|
25486
|
+
}
|
|
25487
|
+
function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
|
|
25488
|
+
if (format === "none") return "";
|
|
25489
|
+
if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
|
|
25490
|
+
if (format === "volume") return formatCompactValue(value);
|
|
25491
|
+
return formatPriceLabel(scale, heightPx, value, mintick);
|
|
25492
|
+
}
|
|
25493
|
+
function formatPriceLabel(scale, heightPx, value, mintick) {
|
|
25494
|
+
const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
|
|
25495
|
+
if (wideLog) {
|
|
25496
|
+
const d = valueDecimals(value);
|
|
25497
|
+
return value.toFixed(d === 0 ? 2 : d);
|
|
25498
|
+
}
|
|
25499
|
+
return value.toFixed(axisDecimals(scale, heightPx, mintick));
|
|
25500
|
+
}
|
|
25501
|
+
var SEC = 1e3;
|
|
25502
|
+
var MIN = 60 * SEC;
|
|
25503
|
+
var HOUR = 60 * MIN;
|
|
25504
|
+
var DAY = 24 * HOUR;
|
|
25505
|
+
var WEEK = 7 * DAY;
|
|
25506
|
+
var MONTH = 30 * DAY;
|
|
25507
|
+
var YEAR = 365 * DAY;
|
|
25508
|
+
var STEP_LADDER = [
|
|
25509
|
+
SEC,
|
|
25510
|
+
5 * SEC,
|
|
25511
|
+
15 * SEC,
|
|
25512
|
+
30 * SEC,
|
|
25513
|
+
MIN,
|
|
25514
|
+
5 * MIN,
|
|
25515
|
+
15 * MIN,
|
|
25516
|
+
30 * MIN,
|
|
25517
|
+
HOUR,
|
|
25518
|
+
2 * HOUR,
|
|
25519
|
+
4 * HOUR,
|
|
25520
|
+
6 * HOUR,
|
|
25521
|
+
12 * HOUR,
|
|
25522
|
+
DAY,
|
|
25523
|
+
2 * DAY,
|
|
25524
|
+
WEEK,
|
|
25525
|
+
MONTH,
|
|
25526
|
+
3 * MONTH,
|
|
25527
|
+
YEAR
|
|
25528
|
+
];
|
|
25529
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
25530
|
+
var pad2 = (n) => n < 10 ? `0${n}` : String(n);
|
|
25531
|
+
function pickStep(targetMs) {
|
|
25532
|
+
for (const step of STEP_LADDER) if (step >= targetMs) return step;
|
|
25533
|
+
return STEP_LADDER[STEP_LADDER.length - 1];
|
|
25534
|
+
}
|
|
25535
|
+
function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
25536
|
+
const span = toMs - fromMs;
|
|
25537
|
+
if (!(span > 0)) return [];
|
|
25538
|
+
const step = pickStep(span / Math.max(1, target));
|
|
25539
|
+
const zFrom = fromMs + offsetMs;
|
|
25540
|
+
const zTo = toMs + offsetMs;
|
|
25541
|
+
const first = Math.ceil(zFrom / step) * step;
|
|
25542
|
+
const out = [];
|
|
25543
|
+
for (let zt = first; zt <= zTo; zt += step) {
|
|
25544
|
+
const t = zt - offsetMs;
|
|
25545
|
+
const d = new Date(zt);
|
|
25546
|
+
let label;
|
|
25547
|
+
let major = false;
|
|
25548
|
+
if (step < DAY) {
|
|
25549
|
+
const h = d.getUTCHours();
|
|
25550
|
+
const m = d.getUTCMinutes();
|
|
25551
|
+
if (h === 0 && m === 0) {
|
|
25552
|
+
label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
|
|
25553
|
+
major = true;
|
|
25554
|
+
} else {
|
|
25555
|
+
label = `${pad2(h)}:${pad2(m)}`;
|
|
25556
|
+
}
|
|
25557
|
+
} else if (step < YEAR) {
|
|
25558
|
+
label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
|
|
25559
|
+
if (d.getUTCDate() === 1) {
|
|
25560
|
+
label = MONTHS[d.getUTCMonth()];
|
|
25561
|
+
major = true;
|
|
25562
|
+
}
|
|
25563
|
+
} else {
|
|
25564
|
+
label = String(d.getUTCFullYear());
|
|
25565
|
+
major = true;
|
|
25566
|
+
}
|
|
25567
|
+
out.push({ time: t, label, major });
|
|
25568
|
+
}
|
|
25569
|
+
return out;
|
|
25570
|
+
}
|
|
25571
|
+
|
|
25572
|
+
// src/renderers/native/chrome/tz.ts
|
|
25573
|
+
function tzOffsetMs(ms, timeZone) {
|
|
25574
|
+
if (!timeZone || timeZone === "UTC") return 0;
|
|
25575
|
+
try {
|
|
25576
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
25577
|
+
timeZone,
|
|
25578
|
+
hourCycle: "h23",
|
|
25579
|
+
year: "numeric",
|
|
25580
|
+
month: "2-digit",
|
|
25581
|
+
day: "2-digit",
|
|
25582
|
+
hour: "2-digit",
|
|
25583
|
+
minute: "2-digit",
|
|
25584
|
+
second: "2-digit"
|
|
25585
|
+
});
|
|
25586
|
+
const parts = dtf.formatToParts(new Date(ms));
|
|
25587
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
25588
|
+
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
25589
|
+
return asUTC - ms;
|
|
25590
|
+
} catch {
|
|
25591
|
+
return 0;
|
|
25592
|
+
}
|
|
25593
|
+
}
|
|
25594
|
+
function zonedDate(ms, timeZone) {
|
|
25595
|
+
return new Date(ms + tzOffsetMs(ms, timeZone));
|
|
25596
|
+
}
|
|
25597
|
+
|
|
25605
25598
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
25606
25599
|
var ChromeRenderer = class {
|
|
25607
25600
|
constructor() {
|
|
@@ -25781,6 +25774,13 @@ var ChromeRenderer = class {
|
|
|
25781
25774
|
if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
|
|
25782
25775
|
ctx.fillText(t.label, dataW + 6, y);
|
|
25783
25776
|
}
|
|
25777
|
+
if (pane.axisBands) {
|
|
25778
|
+
for (const b of pane.axisBands) {
|
|
25779
|
+
const y = pane.bounds.top + b.frac * pane.bounds.height;
|
|
25780
|
+
if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
|
|
25781
|
+
ctx.fillText(b.label, dataW + 6, y);
|
|
25782
|
+
}
|
|
25783
|
+
}
|
|
25784
25784
|
}
|
|
25785
25785
|
ctx.textAlign = "start";
|
|
25786
25786
|
}
|
|
@@ -26022,7 +26022,7 @@ var CrosshairRenderer = class {
|
|
|
26022
26022
|
}
|
|
26023
26023
|
}
|
|
26024
26024
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
26025
|
-
if (pane) {
|
|
26025
|
+
if (pane && pane.axisFormat !== "none") {
|
|
26026
26026
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
26027
26027
|
this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
|
|
26028
26028
|
}
|
|
@@ -26070,7 +26070,7 @@ var CrosshairRenderer = class {
|
|
|
26070
26070
|
break;
|
|
26071
26071
|
}
|
|
26072
26072
|
}
|
|
26073
|
-
if (pane) {
|
|
26073
|
+
if (pane && pane.axisFormat !== "none") {
|
|
26074
26074
|
this.chip(ctx, dataW + 1, ext.y, formatAxisValue(pane.scale, pane.bounds.height, ext.price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
|
|
26075
26075
|
}
|
|
26076
26076
|
}
|
|
@@ -26113,6 +26113,156 @@ function formatStamp(ms, timeZone) {
|
|
|
26113
26113
|
return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
26114
26114
|
}
|
|
26115
26115
|
|
|
26116
|
+
// src/renderers/native/chrome/settings-visibility.ts
|
|
26117
|
+
function settingsIdSlug(label) {
|
|
26118
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
26119
|
+
}
|
|
26120
|
+
function settingsIdHidden(id, hidden) {
|
|
26121
|
+
if (hidden.size === 0) return false;
|
|
26122
|
+
let path = id;
|
|
26123
|
+
for (; ; ) {
|
|
26124
|
+
if (hidden.has(path)) return true;
|
|
26125
|
+
const cut = path.lastIndexOf(".");
|
|
26126
|
+
if (cut < 0) return false;
|
|
26127
|
+
path = path.slice(0, cut);
|
|
26128
|
+
}
|
|
26129
|
+
}
|
|
26130
|
+
function settingsRowId(r) {
|
|
26131
|
+
if (r.kind === "heading" || r.kind === "header") return settingsIdSlug(r.label);
|
|
26132
|
+
const n = normalizeSettingsRow(r);
|
|
26133
|
+
if (n.toggle) return n.toggle.key;
|
|
26134
|
+
for (const c of n.controls) {
|
|
26135
|
+
if (c.kind !== "hint") return c.key;
|
|
26136
|
+
}
|
|
26137
|
+
return settingsIdSlug(n.label);
|
|
26138
|
+
}
|
|
26139
|
+
function filterHiddenRows(rows, scope, hidden) {
|
|
26140
|
+
if (hidden.size === 0) return [...rows];
|
|
26141
|
+
const out = [];
|
|
26142
|
+
let skipGroup = false;
|
|
26143
|
+
let skipSub = false;
|
|
26144
|
+
for (const r of rows) {
|
|
26145
|
+
const id = `${scope}.${settingsRowId(r)}`;
|
|
26146
|
+
if (r.kind === "heading") {
|
|
26147
|
+
skipGroup = settingsIdHidden(id, hidden);
|
|
26148
|
+
skipSub = false;
|
|
26149
|
+
if (!skipGroup) out.push(r);
|
|
26150
|
+
continue;
|
|
26151
|
+
}
|
|
26152
|
+
if (r.kind === "header") {
|
|
26153
|
+
if (skipGroup) continue;
|
|
26154
|
+
skipSub = settingsIdHidden(id, hidden);
|
|
26155
|
+
if (!skipSub) out.push(r);
|
|
26156
|
+
continue;
|
|
26157
|
+
}
|
|
26158
|
+
if (skipGroup || skipSub || settingsIdHidden(id, hidden)) continue;
|
|
26159
|
+
out.push(r);
|
|
26160
|
+
}
|
|
26161
|
+
return out;
|
|
26162
|
+
}
|
|
26163
|
+
function hostSectionId(s) {
|
|
26164
|
+
return s.id ?? settingsIdSlug(s.title);
|
|
26165
|
+
}
|
|
26166
|
+
function hostRowId(r) {
|
|
26167
|
+
return r.id ?? settingsIdSlug(r.label);
|
|
26168
|
+
}
|
|
26169
|
+
function filterHiddenHostRows(rows, scope, hidden) {
|
|
26170
|
+
if (hidden.size === 0) return [...rows];
|
|
26171
|
+
const out = [];
|
|
26172
|
+
let skipGroup = false;
|
|
26173
|
+
for (const r of rows) {
|
|
26174
|
+
const id = `${scope}.${hostRowId(r)}`;
|
|
26175
|
+
if (r.kind === "heading") {
|
|
26176
|
+
skipGroup = settingsIdHidden(id, hidden);
|
|
26177
|
+
if (!skipGroup) out.push(r);
|
|
26178
|
+
continue;
|
|
26179
|
+
}
|
|
26180
|
+
if (skipGroup || settingsIdHidden(id, hidden)) continue;
|
|
26181
|
+
out.push(r);
|
|
26182
|
+
}
|
|
26183
|
+
return out;
|
|
26184
|
+
}
|
|
26185
|
+
var BUILTIN_SETTINGS_IDS = [
|
|
26186
|
+
"symbol",
|
|
26187
|
+
"symbol.type",
|
|
26188
|
+
"symbol.style.candles",
|
|
26189
|
+
"symbol.style.candles.body",
|
|
26190
|
+
"symbol.style.candles.borders",
|
|
26191
|
+
"symbol.style.candles.wick",
|
|
26192
|
+
"symbol.style.candles.spacing",
|
|
26193
|
+
"symbol.style.bars",
|
|
26194
|
+
"symbol.style.bars.up-color",
|
|
26195
|
+
"symbol.style.bars.down-color",
|
|
26196
|
+
"symbol.style.bars.spacing",
|
|
26197
|
+
"symbol.style.line",
|
|
26198
|
+
"symbol.style.line.color",
|
|
26199
|
+
"symbol.style.line.width",
|
|
26200
|
+
"symbol.style.area",
|
|
26201
|
+
"symbol.style.area.line-color",
|
|
26202
|
+
"symbol.style.area.width",
|
|
26203
|
+
"symbol.style.area.top-fill",
|
|
26204
|
+
"symbol.style.area.bottom-fill",
|
|
26205
|
+
"symbol.style.baseline",
|
|
26206
|
+
"symbol.style.baseline.top-line",
|
|
26207
|
+
"symbol.style.baseline.bottom-line",
|
|
26208
|
+
"symbol.style.baseline.fill-top",
|
|
26209
|
+
"symbol.style.baseline.fill-bottom",
|
|
26210
|
+
"symbol.style.baseline.base-level",
|
|
26211
|
+
"symbol.style.baseline.width",
|
|
26212
|
+
"symbol.timezone",
|
|
26213
|
+
"scales",
|
|
26214
|
+
"scales.price-scale",
|
|
26215
|
+
"scales.price-scale.mode",
|
|
26216
|
+
"scales.price-scale.invert",
|
|
26217
|
+
"scales.price-scale.last-price-line",
|
|
26218
|
+
"scales.price-scale.last-price-label",
|
|
26219
|
+
"scales.price-scale.countdown",
|
|
26220
|
+
"scales.price-scale.axis-labels",
|
|
26221
|
+
"scales.price-scale.border-color",
|
|
26222
|
+
"scales.crosshair",
|
|
26223
|
+
"scales.crosshair.color",
|
|
26224
|
+
"scales.crosshair.width",
|
|
26225
|
+
"scales.crosshair.style",
|
|
26226
|
+
"canvas",
|
|
26227
|
+
"canvas.background",
|
|
26228
|
+
"canvas.background.color",
|
|
26229
|
+
"canvas.background.text-color",
|
|
26230
|
+
"canvas.background.text-size",
|
|
26231
|
+
"canvas.background.pane-separator",
|
|
26232
|
+
"canvas.grid",
|
|
26233
|
+
"canvas.grid.vertical",
|
|
26234
|
+
"canvas.grid.horizontal",
|
|
26235
|
+
"canvas.theme"
|
|
26236
|
+
];
|
|
26237
|
+
function settingsIdCatalog(hostSections) {
|
|
26238
|
+
const ids = new Set(BUILTIN_SETTINGS_IDS);
|
|
26239
|
+
for (const def of chartTypes()) {
|
|
26240
|
+
if (hasOwnCandlePaint(def.id)) {
|
|
26241
|
+
const style = `symbol.style.${def.id}`;
|
|
26242
|
+
for (const leaf of ["", ".body", ".borders", ".wick", ".spacing"]) ids.add(style + leaf);
|
|
26243
|
+
}
|
|
26244
|
+
const section = def.settings;
|
|
26245
|
+
if (!section) continue;
|
|
26246
|
+
const scope = `type:${def.id}`;
|
|
26247
|
+
ids.add(scope);
|
|
26248
|
+
const addRows = (rows) => {
|
|
26249
|
+
for (const r of rows) ids.add(`${scope}.${settingsRowId(r)}`);
|
|
26250
|
+
};
|
|
26251
|
+
if (section.rows) addRows(section.rows);
|
|
26252
|
+
for (const inst of section.instances ?? []) addRows(inst.rows);
|
|
26253
|
+
for (const sub of section.subsections ?? []) {
|
|
26254
|
+
ids.add(`${scope}.${settingsIdSlug(sub.title)}`);
|
|
26255
|
+
addRows(sub.rows);
|
|
26256
|
+
}
|
|
26257
|
+
}
|
|
26258
|
+
for (const hs of hostSections) {
|
|
26259
|
+
const scope = hostSectionId(hs);
|
|
26260
|
+
ids.add(scope);
|
|
26261
|
+
for (const r of hs.rows) ids.add(`${scope}.${hostRowId(r)}`);
|
|
26262
|
+
}
|
|
26263
|
+
return [...ids];
|
|
26264
|
+
}
|
|
26265
|
+
|
|
26116
26266
|
// src/renderers/native/chrome/SettingsDialog.ts
|
|
26117
26267
|
var BUILTIN_STYLE_LABELS2 = {
|
|
26118
26268
|
candles: "Candles",
|
|
@@ -26231,12 +26381,21 @@ var SettingsDialog = class {
|
|
|
26231
26381
|
this.activeSection = null;
|
|
26232
26382
|
/** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
|
|
26233
26383
|
this.mobileLayout = false;
|
|
26384
|
+
/** The visibility policy: setting ids hidden by the host (subtree semantics). */
|
|
26385
|
+
this.hiddenSettings = /* @__PURE__ */ new Set();
|
|
26234
26386
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
26235
26387
|
}
|
|
26236
26388
|
/** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
|
|
26237
26389
|
setHostSections(sections) {
|
|
26238
26390
|
this.hostSections = sections;
|
|
26239
26391
|
}
|
|
26392
|
+
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
26393
|
+
setHiddenSettings(ids) {
|
|
26394
|
+
const next = new Set(ids);
|
|
26395
|
+
const same = next.size === this.hiddenSettings.size && [...next].every((id) => this.hiddenSettings.has(id));
|
|
26396
|
+
this.hiddenSettings = next;
|
|
26397
|
+
if (!same) this.reopenIfLive();
|
|
26398
|
+
}
|
|
26240
26399
|
/** Configure the Canvas → Theme row (see {@link themeControl}); null hides the row. */
|
|
26241
26400
|
setThemeControl(current, onSelect) {
|
|
26242
26401
|
this.themeControl = { current, onSelect };
|
|
@@ -26304,8 +26463,12 @@ var SettingsDialog = class {
|
|
|
26304
26463
|
}
|
|
26305
26464
|
const body = document.createElement("div");
|
|
26306
26465
|
body.style.cssText = "display:flex;flex-direction:column;gap:0;";
|
|
26307
|
-
|
|
26308
|
-
|
|
26466
|
+
const sid = (el, id) => {
|
|
26467
|
+
el.dataset.sdId = id;
|
|
26468
|
+
return el;
|
|
26469
|
+
};
|
|
26470
|
+
body.append(sid(this.section("Symbol"), "symbol"));
|
|
26471
|
+
body.append(sid(this.sectionTitle("Chart type"), "symbol.type"));
|
|
26309
26472
|
const groups = {};
|
|
26310
26473
|
const showActive = (style) => {
|
|
26311
26474
|
const active = style === "heikinashi" ? "candles" : style;
|
|
@@ -26314,64 +26477,64 @@ var SettingsDialog = class {
|
|
|
26314
26477
|
}
|
|
26315
26478
|
};
|
|
26316
26479
|
body.append(
|
|
26317
|
-
this.selectRowLabeled("Type", config.series.style, priceStyleIds().map((id) => [id, styleLabel(id)]), (v) => {
|
|
26480
|
+
sid(this.selectRowLabeled("Type", config.series.style, priceStyleIds().map((id) => [id, styleLabel(id)]), (v) => {
|
|
26318
26481
|
this.emit({ series: { style: v } });
|
|
26319
26482
|
showActive(v);
|
|
26320
26483
|
this.syncTypeTabs?.(v);
|
|
26321
|
-
})
|
|
26484
|
+
}), "symbol.type")
|
|
26322
26485
|
);
|
|
26323
|
-
const candles = this.group();
|
|
26486
|
+
const candles = sid(this.group(), "symbol.style.candles");
|
|
26324
26487
|
candles.append(this.sectionTitle("Candles"));
|
|
26325
|
-
candles.append(this.toggleRow("Body", config.candles.bodyVisible, (v) => this.emit({ candles: { bodyVisible: v } }), [
|
|
26488
|
+
candles.append(sid(this.toggleRow("Body", config.candles.bodyVisible, (v) => this.emit({ candles: { bodyVisible: v } }), [
|
|
26326
26489
|
this.swatch(config.candles.upColor, (v) => this.emit({ candles: { upColor: v } })),
|
|
26327
26490
|
this.swatch(config.candles.downColor, (v) => this.emit({ candles: { downColor: v } }))
|
|
26328
|
-
]));
|
|
26329
|
-
candles.append(this.toggleRow("Borders", config.candles.borderVisible, (v) => this.emit({ candles: { borderVisible: v } }), [
|
|
26491
|
+
]), "symbol.style.candles.body"));
|
|
26492
|
+
candles.append(sid(this.toggleRow("Borders", config.candles.borderVisible, (v) => this.emit({ candles: { borderVisible: v } }), [
|
|
26330
26493
|
this.swatch(config.candles.borderUpColor, (v) => this.emit({ candles: { borderUpColor: v } })),
|
|
26331
26494
|
this.swatch(config.candles.borderDownColor, (v) => this.emit({ candles: { borderDownColor: v } }))
|
|
26332
|
-
]));
|
|
26333
|
-
candles.append(this.toggleRow("Wick", config.candles.wickVisible, (v) => this.emit({ candles: { wickVisible: v } }), [
|
|
26495
|
+
]), "symbol.style.candles.borders"));
|
|
26496
|
+
candles.append(sid(this.toggleRow("Wick", config.candles.wickVisible, (v) => this.emit({ candles: { wickVisible: v } }), [
|
|
26334
26497
|
this.swatch(config.candles.wickUpColor, (v) => this.emit({ candles: { wickUpColor: v } })),
|
|
26335
26498
|
this.swatch(config.candles.wickDownColor, (v) => this.emit({ candles: { wickDownColor: v } }))
|
|
26336
|
-
]));
|
|
26337
|
-
candles.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
|
|
26499
|
+
]), "symbol.style.candles.wick"));
|
|
26500
|
+
candles.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), "symbol.style.candles.spacing"));
|
|
26338
26501
|
groups.candles = candles;
|
|
26339
26502
|
body.append(candles);
|
|
26340
|
-
const bars = this.group();
|
|
26503
|
+
const bars = sid(this.group(), "symbol.style.bars");
|
|
26341
26504
|
bars.append(this.sectionTitle("Bars"));
|
|
26342
|
-
bars.append(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })));
|
|
26343
|
-
bars.append(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })));
|
|
26344
|
-
bars.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
|
|
26505
|
+
bars.append(sid(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })), "symbol.style.bars.up-color"));
|
|
26506
|
+
bars.append(sid(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })), "symbol.style.bars.down-color"));
|
|
26507
|
+
bars.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), "symbol.style.bars.spacing"));
|
|
26345
26508
|
groups.bars = bars;
|
|
26346
26509
|
body.append(bars);
|
|
26347
|
-
const line = this.group();
|
|
26510
|
+
const line = sid(this.group(), "symbol.style.line");
|
|
26348
26511
|
line.append(this.sectionTitle("Line"));
|
|
26349
|
-
line.append(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })));
|
|
26350
|
-
line.append(this.numberRow("Width", config.line.width, 1, 10, 1, (v) => this.emit({ line: { width: v } })));
|
|
26512
|
+
line.append(sid(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })), "symbol.style.line.color"));
|
|
26513
|
+
line.append(sid(this.numberRow("Width", config.line.width, 1, 10, 1, (v) => this.emit({ line: { width: v } })), "symbol.style.line.width"));
|
|
26351
26514
|
groups.line = line;
|
|
26352
26515
|
body.append(line);
|
|
26353
|
-
const area = this.group();
|
|
26516
|
+
const area = sid(this.group(), "symbol.style.area");
|
|
26354
26517
|
area.append(this.sectionTitle("Area"));
|
|
26355
|
-
area.append(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })));
|
|
26356
|
-
area.append(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })));
|
|
26357
|
-
area.append(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })));
|
|
26358
|
-
area.append(this.colorRow("Bottom fill", config.area.bottomColor, (v) => this.emit({ area: { bottomColor: v } })));
|
|
26518
|
+
area.append(sid(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })), "symbol.style.area.line-color"));
|
|
26519
|
+
area.append(sid(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })), "symbol.style.area.width"));
|
|
26520
|
+
area.append(sid(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })), "symbol.style.area.top-fill"));
|
|
26521
|
+
area.append(sid(this.colorRow("Bottom fill", config.area.bottomColor, (v) => this.emit({ area: { bottomColor: v } })), "symbol.style.area.bottom-fill"));
|
|
26359
26522
|
groups.area = area;
|
|
26360
26523
|
body.append(area);
|
|
26361
|
-
const baseline = this.group();
|
|
26524
|
+
const baseline = sid(this.group(), "symbol.style.baseline");
|
|
26362
26525
|
baseline.append(this.sectionTitle("Baseline"));
|
|
26363
|
-
baseline.append(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]));
|
|
26364
|
-
baseline.append(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]));
|
|
26365
|
-
baseline.append(this.rowWith("Fill top area", [
|
|
26526
|
+
baseline.append(sid(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]), "symbol.style.baseline.top-line"));
|
|
26527
|
+
baseline.append(sid(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]), "symbol.style.baseline.bottom-line"));
|
|
26528
|
+
baseline.append(sid(this.rowWith("Fill top area", [
|
|
26366
26529
|
this.swatch(config.baseline.topFillColor, (v) => this.emit({ baseline: { topFillColor: v } })),
|
|
26367
26530
|
this.swatch(config.baseline.topFillColor2, (v) => this.emit({ baseline: { topFillColor2: v } }))
|
|
26368
|
-
]));
|
|
26369
|
-
baseline.append(this.rowWith("Fill bottom area", [
|
|
26531
|
+
]), "symbol.style.baseline.fill-top"));
|
|
26532
|
+
baseline.append(sid(this.rowWith("Fill bottom area", [
|
|
26370
26533
|
this.swatch(config.baseline.bottomFillColor2, (v) => this.emit({ baseline: { bottomFillColor2: v } })),
|
|
26371
26534
|
this.swatch(config.baseline.bottomFillColor, (v) => this.emit({ baseline: { bottomFillColor: v } }))
|
|
26372
|
-
]));
|
|
26373
|
-
baseline.append(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })));
|
|
26374
|
-
baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
|
|
26535
|
+
]), "symbol.style.baseline.fill-bottom"));
|
|
26536
|
+
baseline.append(sid(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })), "symbol.style.baseline.base-level"));
|
|
26537
|
+
baseline.append(sid(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })), "symbol.style.baseline.width"));
|
|
26375
26538
|
groups.baseline = baseline;
|
|
26376
26539
|
body.append(baseline);
|
|
26377
26540
|
for (const def of chartTypes()) {
|
|
@@ -26379,32 +26542,36 @@ var SettingsDialog = class {
|
|
|
26379
26542
|
const bag = config.chartTypes[def.id] ?? {};
|
|
26380
26543
|
const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
|
|
26381
26544
|
const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
|
|
26382
|
-
const g = this.group();
|
|
26545
|
+
const g = sid(this.group(), `symbol.style.${def.id}`);
|
|
26383
26546
|
g.append(this.sectionTitle("Candles"));
|
|
26384
|
-
g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
|
|
26547
|
+
g.append(sid(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
|
|
26385
26548
|
this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
|
|
26386
26549
|
this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
|
|
26387
|
-
]));
|
|
26388
|
-
g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
|
|
26550
|
+
]), `symbol.style.${def.id}.body`));
|
|
26551
|
+
g.append(sid(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
|
|
26389
26552
|
this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
|
|
26390
26553
|
this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
|
|
26391
|
-
]));
|
|
26392
|
-
g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
|
|
26554
|
+
]), `symbol.style.${def.id}.borders`));
|
|
26555
|
+
g.append(sid(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
|
|
26393
26556
|
this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
|
|
26394
26557
|
this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
|
|
26395
|
-
]));
|
|
26396
|
-
g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
|
|
26558
|
+
]), `symbol.style.${def.id}.wick`));
|
|
26559
|
+
g.append(sid(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })), `symbol.style.${def.id}.spacing`));
|
|
26397
26560
|
groups[def.id] = g;
|
|
26398
26561
|
body.append(g);
|
|
26399
26562
|
}
|
|
26400
26563
|
showActive(config.series.style);
|
|
26401
|
-
body.append(this.sectionTitle("Time zone"));
|
|
26402
|
-
body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
|
|
26564
|
+
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
26565
|
+
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
26403
26566
|
const renderHostSections = (placement) => {
|
|
26404
26567
|
for (const hs of this.hostSections) {
|
|
26405
26568
|
if ((hs.placement ?? "after-symbol") !== placement) continue;
|
|
26569
|
+
const scope = hostSectionId(hs);
|
|
26570
|
+
if (settingsIdHidden(scope, this.hiddenSettings)) continue;
|
|
26571
|
+
const rows = filterHiddenHostRows(hs.rows, scope, this.hiddenSettings);
|
|
26572
|
+
if (rows.length === 0) continue;
|
|
26406
26573
|
body.append(placement === "symbol" ? this.sectionTitle(hs.title) : this.section(hs.title));
|
|
26407
|
-
for (const hr of
|
|
26574
|
+
for (const hr of rows) {
|
|
26408
26575
|
if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
|
|
26409
26576
|
else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
|
|
26410
26577
|
else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
|
|
@@ -26417,53 +26584,69 @@ var SettingsDialog = class {
|
|
|
26417
26584
|
const typeSettings = def.settings;
|
|
26418
26585
|
if (!typeSettings) continue;
|
|
26419
26586
|
if ((typeSettings.placement ?? "end") !== placement) continue;
|
|
26587
|
+
if (settingsIdHidden(`type:${def.id}`, this.hiddenSettings)) continue;
|
|
26420
26588
|
this.chartTypeSection(def.id, typeSettings, config, body);
|
|
26421
26589
|
}
|
|
26422
26590
|
};
|
|
26423
26591
|
renderHostSections("symbol");
|
|
26424
26592
|
renderChartTypeSections("after-symbol");
|
|
26425
26593
|
renderHostSections("after-symbol");
|
|
26426
|
-
body.append(this.section("Scales and lines"));
|
|
26427
|
-
body.append(this.sectionTitle("Price scale"));
|
|
26594
|
+
body.append(sid(this.section("Scales and lines"), "scales"));
|
|
26595
|
+
body.append(sid(this.sectionTitle("Price scale"), "scales.price-scale"));
|
|
26428
26596
|
body.append(
|
|
26429
|
-
this.selectRowLabeled(
|
|
26597
|
+
sid(this.selectRowLabeled(
|
|
26430
26598
|
"Mode",
|
|
26431
26599
|
config.priceScale.log ? "log" : config.priceScale.mode,
|
|
26432
26600
|
[["price", "Regular"], ["percent", "Percent"], ["indexed", "Indexed to 100"], ["log", "Logarithmic"]],
|
|
26433
26601
|
(v) => this.emit({ priceScale: v === "log" ? { mode: "price", log: true } : { mode: v, log: false } })
|
|
26434
|
-
)
|
|
26602
|
+
), "scales.price-scale.mode")
|
|
26435
26603
|
);
|
|
26436
|
-
body.append(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })));
|
|
26437
|
-
body.append(this.separator());
|
|
26438
|
-
body.append(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })));
|
|
26439
|
-
body.append(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })));
|
|
26440
|
-
body.append(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })));
|
|
26441
|
-
body.append(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })));
|
|
26442
|
-
body.append(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })));
|
|
26443
|
-
body.append(this.sectionTitle("Crosshair"));
|
|
26444
|
-
body.append(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })));
|
|
26445
|
-
body.append(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })));
|
|
26446
|
-
body.append(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })));
|
|
26447
|
-
body.append(this.section("Canvas"));
|
|
26448
|
-
body.append(this.sectionTitle("Background & text"));
|
|
26449
|
-
body.append(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })));
|
|
26450
|
-
body.append(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })));
|
|
26451
|
-
body.append(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })));
|
|
26452
|
-
body.append(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })));
|
|
26453
|
-
body.append(this.sectionTitle("Grid"));
|
|
26454
|
-
body.append(this.toggleRow("Vertical", config.grid.vertLines.visible, (v) => this.emit({ grid: { vertLines: { visible: v } } }), [
|
|
26604
|
+
body.append(sid(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })), "scales.price-scale.invert"));
|
|
26605
|
+
body.append(sid(this.separator(), "scales.price-scale"));
|
|
26606
|
+
body.append(sid(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })), "scales.price-scale.last-price-line"));
|
|
26607
|
+
body.append(sid(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })), "scales.price-scale.last-price-label"));
|
|
26608
|
+
body.append(sid(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })), "scales.price-scale.countdown"));
|
|
26609
|
+
body.append(sid(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })), "scales.price-scale.axis-labels"));
|
|
26610
|
+
body.append(sid(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })), "scales.price-scale.border-color"));
|
|
26611
|
+
body.append(sid(this.sectionTitle("Crosshair"), "scales.crosshair"));
|
|
26612
|
+
body.append(sid(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })), "scales.crosshair.color"));
|
|
26613
|
+
body.append(sid(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })), "scales.crosshair.width"));
|
|
26614
|
+
body.append(sid(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })), "scales.crosshair.style"));
|
|
26615
|
+
body.append(sid(this.section("Canvas"), "canvas"));
|
|
26616
|
+
body.append(sid(this.sectionTitle("Background & text"), "canvas.background"));
|
|
26617
|
+
body.append(sid(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })), "canvas.background.color"));
|
|
26618
|
+
body.append(sid(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })), "canvas.background.text-color"));
|
|
26619
|
+
body.append(sid(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })), "canvas.background.text-size"));
|
|
26620
|
+
body.append(sid(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })), "canvas.background.pane-separator"));
|
|
26621
|
+
body.append(sid(this.sectionTitle("Grid"), "canvas.grid"));
|
|
26622
|
+
body.append(sid(this.toggleRow("Vertical", config.grid.vertLines.visible, (v) => this.emit({ grid: { vertLines: { visible: v } } }), [
|
|
26455
26623
|
this.swatch(config.grid.vertLines.color, (v) => this.emit({ grid: { vertLines: { color: v } } }))
|
|
26456
|
-
]));
|
|
26457
|
-
body.append(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
|
|
26624
|
+
]), "canvas.grid.vertical"));
|
|
26625
|
+
body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
|
|
26458
26626
|
this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
|
|
26459
|
-
]));
|
|
26627
|
+
]), "canvas.grid.horizontal"));
|
|
26460
26628
|
if (this.themeControl) {
|
|
26461
26629
|
const tc = this.themeControl;
|
|
26462
|
-
body.append(this.sectionTitle("Theme"));
|
|
26463
|
-
body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
|
|
26630
|
+
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
26631
|
+
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
26464
26632
|
}
|
|
26465
26633
|
renderChartTypeSections("end");
|
|
26466
26634
|
renderHostSections("end");
|
|
26635
|
+
if (this.hiddenSettings.size > 0) {
|
|
26636
|
+
let skipTab = false;
|
|
26637
|
+
for (const child of [...body.children]) {
|
|
26638
|
+
if (child.dataset.sdTab !== void 0) {
|
|
26639
|
+
skipTab = child.dataset.sdId !== void 0 && settingsIdHidden(child.dataset.sdId, this.hiddenSettings);
|
|
26640
|
+
}
|
|
26641
|
+
if (skipTab || child.dataset.sdId !== void 0 && settingsIdHidden(child.dataset.sdId, this.hiddenSettings)) {
|
|
26642
|
+
child.remove();
|
|
26643
|
+
continue;
|
|
26644
|
+
}
|
|
26645
|
+
for (const el of [...child.querySelectorAll("[data-sd-id]")]) {
|
|
26646
|
+
if (settingsIdHidden(el.dataset.sdId, this.hiddenSettings)) el.remove();
|
|
26647
|
+
}
|
|
26648
|
+
}
|
|
26649
|
+
}
|
|
26467
26650
|
const shell = document.createElement("div");
|
|
26468
26651
|
shell.style.cssText = mobile ? "display:flex;min-height:0;flex:1 1 auto;position:relative;overflow:hidden;" : "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
|
|
26469
26652
|
const rail = document.createElement("div");
|
|
@@ -26501,6 +26684,9 @@ var SettingsDialog = class {
|
|
|
26501
26684
|
}
|
|
26502
26685
|
if (current) current.appendChild(child);
|
|
26503
26686
|
}
|
|
26687
|
+
for (let i = panes.length - 1; i >= 0; i--) {
|
|
26688
|
+
if (panes[i].el.childElementCount === 0) panes.splice(i, 1);
|
|
26689
|
+
}
|
|
26504
26690
|
this.syncTypeTabs = (active) => {
|
|
26505
26691
|
let hidActive = false;
|
|
26506
26692
|
panes.forEach((pn, idx) => {
|
|
@@ -26640,19 +26826,25 @@ var SettingsDialog = class {
|
|
|
26640
26826
|
this.emitType(typeId, key, v);
|
|
26641
26827
|
for (const r of refreshers) r();
|
|
26642
26828
|
};
|
|
26829
|
+
const scope = `type:${typeId}`;
|
|
26643
26830
|
if (section.instances && section.instances.length > 0) {
|
|
26644
|
-
|
|
26831
|
+
const instances = section.instances.map((inst) => ({ ...inst, rows: filterHiddenRows(inst.rows, scope, this.hiddenSettings) }));
|
|
26832
|
+
body.append(this.instancesBlock(typeId, instances, bag, put, refreshers));
|
|
26645
26833
|
} else if (section.rows) {
|
|
26646
|
-
|
|
26647
|
-
|
|
26834
|
+
const rows = filterHiddenRows(section.rows, scope, this.hiddenSettings);
|
|
26835
|
+
if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, rows, bag, put, refreshers));
|
|
26836
|
+
else this.flatTypeRows(rows, bag, put, refreshers, body);
|
|
26648
26837
|
}
|
|
26649
26838
|
for (const sub of section.subsections ?? []) {
|
|
26839
|
+
if (settingsIdHidden(`${scope}.${settingsIdSlug(sub.title)}`, this.hiddenSettings)) continue;
|
|
26840
|
+
const rows = filterHiddenRows(sub.rows, scope, this.hiddenSettings);
|
|
26841
|
+
if (rows.length === 0) continue;
|
|
26650
26842
|
const subMarker = this.section(sub.title);
|
|
26651
26843
|
subMarker.dataset.sdStyle = typeId;
|
|
26652
26844
|
subMarker.dataset.sdVisibility = section.visibility ?? "active";
|
|
26653
26845
|
subMarker.dataset.sdSub = "1";
|
|
26654
26846
|
body.append(subMarker);
|
|
26655
|
-
body.append(this.groupedRows(`${typeId}/${sub.title}`,
|
|
26847
|
+
body.append(this.groupedRows(`${typeId}/${sub.title}`, rows, bag, put, refreshers, sub.enableKey));
|
|
26656
26848
|
}
|
|
26657
26849
|
for (const r of refreshers) r();
|
|
26658
26850
|
}
|
|
@@ -30942,6 +31134,94 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
|
|
|
30942
31134
|
return { above, below: combinedWeight - above };
|
|
30943
31135
|
}
|
|
30944
31136
|
|
|
31137
|
+
// src/renderers/native/backdrop/BackdropRenderer.ts
|
|
31138
|
+
var BackdropRenderer = class {
|
|
31139
|
+
constructor() {
|
|
31140
|
+
this.canvas = null;
|
|
31141
|
+
this.ctx = null;
|
|
31142
|
+
}
|
|
31143
|
+
mount(canvas) {
|
|
31144
|
+
this.canvas = canvas;
|
|
31145
|
+
this.ctx = canvas.getContext("2d");
|
|
31146
|
+
}
|
|
31147
|
+
destroy() {
|
|
31148
|
+
this.canvas = null;
|
|
31149
|
+
this.ctx = null;
|
|
31150
|
+
}
|
|
31151
|
+
/** Paint one frame: highlight bands first, gridlines on top (the order they had inside
|
|
31152
|
+
* the data canvas). `gridAlpha` fades the gridlines as a reveal-under layer opens. */
|
|
31153
|
+
render(scene, coords, theme, gridAlpha) {
|
|
31154
|
+
const ctx = this.ctx;
|
|
31155
|
+
const canvas = this.canvas;
|
|
31156
|
+
if (!ctx || !canvas) return;
|
|
31157
|
+
const dpr = coords.dpr;
|
|
31158
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
31159
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
31160
|
+
const n = coords.barCount;
|
|
31161
|
+
if (n === 0) return;
|
|
31162
|
+
const vr = coords.visibleLogicalRange();
|
|
31163
|
+
if (Math.min(n - 1, Math.ceil(vr.to)) < Math.max(0, Math.floor(vr.from))) return;
|
|
31164
|
+
this.drawHighlights(ctx, scene, coords);
|
|
31165
|
+
this.drawGrid(ctx, scene, coords, theme, coords.width, gridAlpha);
|
|
31166
|
+
}
|
|
31167
|
+
/** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
|
|
31168
|
+
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
31169
|
+
drawHighlights(ctx, scene, coords) {
|
|
31170
|
+
const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
|
|
31171
|
+
if (bands.length === 0) return;
|
|
31172
|
+
for (const band of bands) {
|
|
31173
|
+
const x1 = coords.timeToX(band.from);
|
|
31174
|
+
const x2 = coords.timeToX(band.to);
|
|
31175
|
+
if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
|
|
31176
|
+
const cx = Math.max(0, x1);
|
|
31177
|
+
const cw = Math.min(coords.width, x2) - cx;
|
|
31178
|
+
if (cw <= 0) continue;
|
|
31179
|
+
ctx.fillStyle = band.color;
|
|
31180
|
+
ctx.fillRect(cx, 0, cw, coords.height);
|
|
31181
|
+
}
|
|
31182
|
+
}
|
|
31183
|
+
// ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
|
|
31184
|
+
// (style); each uses its own color. Pane separators are drawn on the chrome layer
|
|
31185
|
+
// (full-width, above the data) so series never overpaint them.
|
|
31186
|
+
drawGrid(ctx, scene, coords, theme, dataW, gridAlpha) {
|
|
31187
|
+
const panes = scene.orderedPanes();
|
|
31188
|
+
const { gridVert, gridHorz } = scene.style;
|
|
31189
|
+
const vertColor = gridVert.color ?? theme.gridColor;
|
|
31190
|
+
const horzColor = gridHorz.color ?? theme.gridColor;
|
|
31191
|
+
ctx.lineWidth = 1;
|
|
31192
|
+
if (scene.showGrid && gridVert.visible) {
|
|
31193
|
+
ctx.globalAlpha = gridAlpha;
|
|
31194
|
+
ctx.strokeStyle = vertColor;
|
|
31195
|
+
const tr = coords.visibleTimeRange();
|
|
31196
|
+
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
31197
|
+
ctx.beginPath();
|
|
31198
|
+
for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
|
|
31199
|
+
const x = Math.round(coords.timeToX(tick.time)) + 0.5;
|
|
31200
|
+
if (x < 0 || x > dataW) continue;
|
|
31201
|
+
ctx.moveTo(x, 0);
|
|
31202
|
+
ctx.lineTo(x, coords.height);
|
|
31203
|
+
}
|
|
31204
|
+
ctx.stroke();
|
|
31205
|
+
}
|
|
31206
|
+
for (const pane of panes) {
|
|
31207
|
+
if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
|
|
31208
|
+
ctx.globalAlpha = gridAlpha;
|
|
31209
|
+
ctx.strokeStyle = horzColor;
|
|
31210
|
+
const pct = percentScaleFor(scene, pane);
|
|
31211
|
+
ctx.beginPath();
|
|
31212
|
+
for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
|
|
31213
|
+
const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
|
|
31214
|
+
if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
|
|
31215
|
+
ctx.moveTo(0, y);
|
|
31216
|
+
ctx.lineTo(dataW, y);
|
|
31217
|
+
}
|
|
31218
|
+
ctx.stroke();
|
|
31219
|
+
}
|
|
31220
|
+
}
|
|
31221
|
+
ctx.globalAlpha = 1;
|
|
31222
|
+
}
|
|
31223
|
+
};
|
|
31224
|
+
|
|
30945
31225
|
// src/renderers/native/volume/paintVolume.ts
|
|
30946
31226
|
var VOLUME_FILL_ALPHA = 0.5;
|
|
30947
31227
|
function paintVolume(ctx, bars, geom, colors) {
|
|
@@ -31035,6 +31315,19 @@ function rendererLayers() {
|
|
|
31035
31315
|
return [...registry4.values()];
|
|
31036
31316
|
}
|
|
31037
31317
|
|
|
31318
|
+
// src/renderers/native/core/layerStacking.ts
|
|
31319
|
+
function stackLayers(entries, candleZ) {
|
|
31320
|
+
const keyed = entries.map((e) => ({
|
|
31321
|
+
id: e.id,
|
|
31322
|
+
key: e.ownerZ ?? (e.placement === "below-data" ? -Infinity : candleZ)
|
|
31323
|
+
}));
|
|
31324
|
+
keyed.sort((a, b) => a.key - b.key);
|
|
31325
|
+
const below = [];
|
|
31326
|
+
const above = [];
|
|
31327
|
+
for (const k of keyed) (k.key < candleZ ? below : above).push(k.id);
|
|
31328
|
+
return { below, above };
|
|
31329
|
+
}
|
|
31330
|
+
|
|
31038
31331
|
// src/renderers/shared/dom-raster.ts
|
|
31039
31332
|
function hasInk(color) {
|
|
31040
31333
|
if (!color || color === "transparent") return false;
|
|
@@ -31404,9 +31697,12 @@ var NativeRenderer = class {
|
|
|
31404
31697
|
// px reserved on the left for the docked drawings toolbar (0 when hidden)
|
|
31405
31698
|
this.mountContainer = null;
|
|
31406
31699
|
this.userDrawings = null;
|
|
31700
|
+
this.backdropRenderer = new BackdropRenderer();
|
|
31407
31701
|
this.volumeRenderer = new VolumeRenderer();
|
|
31408
31702
|
/** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
|
|
31409
31703
|
this.extLayers = [];
|
|
31704
|
+
/** Last applied layer-canvas order (ids below + above the data canvas) — re-slotted only on change. */
|
|
31705
|
+
this.layerOrderSig = "";
|
|
31410
31706
|
// The attribution mark (see chrome/AttributionMark + the NOTICE file): default-on;
|
|
31411
31707
|
// disabling requires an equivalent visible attribution elsewhere in the host UI.
|
|
31412
31708
|
this.attributionEl = null;
|
|
@@ -31529,6 +31825,9 @@ var NativeRenderer = class {
|
|
|
31529
31825
|
// bar under the crosshair; null when off a bar
|
|
31530
31826
|
// ── settings dialog (rich, serializable config — item 15) ──
|
|
31531
31827
|
this.settingsDialog = null;
|
|
31828
|
+
/** The host's visibility policy (setting ids hidden from the dialog) — instance
|
|
31829
|
+
* state, never part of the persisted config. */
|
|
31830
|
+
this.hiddenSettings = [];
|
|
31532
31831
|
/** Where modal dialogs mount — a HOST override (multi-chart shells pass their root
|
|
31533
31832
|
* so dialogs center globally instead of clipping inside one cell). Null = the plot. */
|
|
31534
31833
|
this.dialogHost = null;
|
|
@@ -32201,6 +32500,7 @@ var NativeRenderer = class {
|
|
|
32201
32500
|
}
|
|
32202
32501
|
this.settingsDialog.setTheme(this.theme);
|
|
32203
32502
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
32503
|
+
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
32204
32504
|
this.syncThemeControl();
|
|
32205
32505
|
this.settingsDialog.toggle(
|
|
32206
32506
|
this.getConfig(),
|
|
@@ -32337,9 +32637,7 @@ var NativeRenderer = class {
|
|
|
32337
32637
|
if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
|
|
32338
32638
|
}
|
|
32339
32639
|
}
|
|
32340
|
-
const
|
|
32341
|
-
const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
|
|
32342
|
-
for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
|
|
32640
|
+
for (const canvas of [...this.canvasPile(), this.chromeCanvas, this.drawingsCanvas]) {
|
|
32343
32641
|
if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
|
|
32344
32642
|
}
|
|
32345
32643
|
if (frame) {
|
|
@@ -32450,6 +32748,8 @@ var NativeRenderer = class {
|
|
|
32450
32748
|
this.wrapper = document.createElement("div");
|
|
32451
32749
|
Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair", userSelect: "none", webkitUserSelect: "none" });
|
|
32452
32750
|
applyChromeTokens(this.wrapper, this.chromeTheme());
|
|
32751
|
+
this.backdropCanvas = document.createElement("canvas");
|
|
32752
|
+
Object.assign(this.backdropCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
|
|
32453
32753
|
this.volumeCanvas = document.createElement("canvas");
|
|
32454
32754
|
Object.assign(this.volumeCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
|
|
32455
32755
|
this.dataCanvas = this.createGeometryBackend();
|
|
@@ -32472,7 +32772,8 @@ var NativeRenderer = class {
|
|
|
32472
32772
|
});
|
|
32473
32773
|
const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
|
|
32474
32774
|
const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
|
|
32475
|
-
this.plot.append(...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
|
|
32775
|
+
this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
|
|
32776
|
+
this.layerOrderSig = "";
|
|
32476
32777
|
this.wrapper.appendChild(this.plot);
|
|
32477
32778
|
this.factoryConfig = this.getConfig();
|
|
32478
32779
|
this.attributionEl = this.buildAttributionEl();
|
|
@@ -32481,6 +32782,7 @@ var NativeRenderer = class {
|
|
|
32481
32782
|
this.wrapper.appendChild(this.attributionEl);
|
|
32482
32783
|
container.appendChild(this.wrapper);
|
|
32483
32784
|
this.applyBackground();
|
|
32785
|
+
this.backdropRenderer.mount(this.backdropCanvas);
|
|
32484
32786
|
this.volumeRenderer.mount(this.volumeCanvas);
|
|
32485
32787
|
this.vpvrRenderer.mount(this.vpvrCanvas);
|
|
32486
32788
|
for (const l of this.extLayers) l.instance.mount(l.canvas);
|
|
@@ -32770,6 +33072,7 @@ var NativeRenderer = class {
|
|
|
32770
33072
|
this.scrollButton = null;
|
|
32771
33073
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
32772
33074
|
this.extLayers = [];
|
|
33075
|
+
this.backdropRenderer.destroy();
|
|
32773
33076
|
this.volumeRenderer.destroy();
|
|
32774
33077
|
this.vpvrRenderer.destroy();
|
|
32775
33078
|
this.backend.destroy();
|
|
@@ -33001,7 +33304,8 @@ var NativeRenderer = class {
|
|
|
33001
33304
|
mountIndicator(model) {
|
|
33002
33305
|
this.scene.indicators.set(model.id, model);
|
|
33003
33306
|
this.refreshAnchorOffset(model);
|
|
33004
|
-
this.scene.
|
|
33307
|
+
if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
|
|
33308
|
+
else this.scene.assignIndicatorZ(model.id);
|
|
33005
33309
|
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
33006
33310
|
native: !!model.native,
|
|
33007
33311
|
...model.shorttitle ? { settingsTitle: model.title } : {}
|
|
@@ -33148,6 +33452,13 @@ var NativeRenderer = class {
|
|
|
33148
33452
|
this.hostSettingsSections = sections;
|
|
33149
33453
|
this.settingsDialog?.setHostSections(sections);
|
|
33150
33454
|
}
|
|
33455
|
+
setSettingsVisibility(policy) {
|
|
33456
|
+
this.hiddenSettings = [...policy.hidden ?? []];
|
|
33457
|
+
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
33458
|
+
}
|
|
33459
|
+
listSettingsIds() {
|
|
33460
|
+
return settingsIdCatalog(this.hostSettingsSections);
|
|
33461
|
+
}
|
|
33151
33462
|
onChartTypeSettingsChange(cb) {
|
|
33152
33463
|
this.chartTypeSettingsCbs.add(cb);
|
|
33153
33464
|
return () => this.chartTypeSettingsCbs.delete(cb);
|
|
@@ -33820,10 +34131,17 @@ var NativeRenderer = class {
|
|
|
33820
34131
|
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
33821
34132
|
for (const l of this.extLayers) {
|
|
33822
34133
|
if (!l.def.repaintOnCursor) continue;
|
|
33823
|
-
|
|
34134
|
+
const lp = this.layerPane(l.def.id) ?? pane;
|
|
34135
|
+
if (lp.collapsed) continue;
|
|
34136
|
+
l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
|
|
33824
34137
|
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
33825
34138
|
}
|
|
33826
34139
|
}
|
|
34140
|
+
/** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
|
|
34141
|
+
clearLayerCanvas(canvas) {
|
|
34142
|
+
if (canvas.width === 0 || canvas.height === 0) return;
|
|
34143
|
+
canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
|
|
34144
|
+
}
|
|
33827
34145
|
/** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
|
|
33828
34146
|
extLayerArgs(id, scale, bounds, nowMs) {
|
|
33829
34147
|
return {
|
|
@@ -33842,11 +34160,12 @@ var NativeRenderer = class {
|
|
|
33842
34160
|
}
|
|
33843
34161
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
33844
34162
|
paintData() {
|
|
34163
|
+
this.syncLayerCanvasOrder();
|
|
33845
34164
|
this.stampScaleInvert();
|
|
33846
34165
|
this.backend.modelAlpha = this.modelAlpha;
|
|
33847
34166
|
this.backend.candleBodyAlpha = this.candleBodyAlpha;
|
|
33848
34167
|
this.backend.candleStructureAlpha = this.candleStructureAlpha;
|
|
33849
|
-
|
|
34168
|
+
let gridAlpha = 1;
|
|
33850
34169
|
const candleBodyScale = 1;
|
|
33851
34170
|
this.backend.candleBodyScale = candleBodyScale;
|
|
33852
34171
|
const pane = this.scene.panes.get(PRICE_PANE_ID2);
|
|
@@ -33872,15 +34191,20 @@ var NativeRenderer = class {
|
|
|
33872
34191
|
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
33873
34192
|
let folded = null;
|
|
33874
34193
|
for (const l of this.extLayers) {
|
|
33875
|
-
const
|
|
34194
|
+
const lp = this.layerPane(l.def.id) ?? pane;
|
|
34195
|
+
if (lp.collapsed) {
|
|
34196
|
+
this.clearLayerCanvas(l.canvas);
|
|
34197
|
+
continue;
|
|
34198
|
+
}
|
|
34199
|
+
const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
|
|
33876
34200
|
l.instance.render(args);
|
|
33877
|
-
folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
34201
|
+
if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
33878
34202
|
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
33879
34203
|
}
|
|
33880
34204
|
if (folded) {
|
|
33881
34205
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
33882
34206
|
if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
|
|
33883
|
-
if (folded.gridAlpha != null)
|
|
34207
|
+
if (folded.gridAlpha != null) gridAlpha = clamp012(folded.gridAlpha);
|
|
33884
34208
|
}
|
|
33885
34209
|
}
|
|
33886
34210
|
const li = this.bars.length - 1;
|
|
@@ -33888,6 +34212,7 @@ var NativeRenderer = class {
|
|
|
33888
34212
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
33889
34213
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
33890
34214
|
this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
|
|
34215
|
+
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
33891
34216
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
33892
34217
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
33893
34218
|
this.userDrawings?.render();
|
|
@@ -34030,12 +34355,21 @@ var NativeRenderer = class {
|
|
|
34030
34355
|
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34031
34356
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
34032
34357
|
pane.axisFormat = void 0;
|
|
34358
|
+
pane.axisBands = void 0;
|
|
34033
34359
|
if (this.volumeOwnsPane(pane, masterModels)) {
|
|
34034
34360
|
const maxVol = this.maxVisibleVolume(i0, i1);
|
|
34035
34361
|
if (maxVol > 0) {
|
|
34036
34362
|
pane.scaleTarget = { min: 0, max: maxVol / VOLUME_PANE_FILL_FRAC };
|
|
34037
34363
|
pane.axisFormat = "volume";
|
|
34038
34364
|
}
|
|
34365
|
+
} else if (this.layerNativesOwnPane(pane, masterModels)) {
|
|
34366
|
+
pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34367
|
+
pane.percentBaseline = this.bars[i0]?.close ?? 0;
|
|
34368
|
+
if (masterModels.every((m) => m.paneAxis != null)) {
|
|
34369
|
+
pane.axisFormat = "none";
|
|
34370
|
+
const banded = masterModels.find((m) => typeof m.paneAxis === "object");
|
|
34371
|
+
pane.axisBands = banded ? banded.paneAxis.bands : void 0;
|
|
34372
|
+
}
|
|
34039
34373
|
}
|
|
34040
34374
|
if (pane === pricePane && this.scene.tradeMarkers.visible && pane.bounds.height > 0) {
|
|
34041
34375
|
const th = this.tradesScaleHints(vr);
|
|
@@ -34122,6 +34456,59 @@ var NativeRenderer = class {
|
|
|
34122
34456
|
}
|
|
34123
34457
|
return null;
|
|
34124
34458
|
}
|
|
34459
|
+
/** The mounted native indicator that OWNS an SDK layer — the one whose type equals the
|
|
34460
|
+
* layer id (the id doubles as the data channel, so the pairing is the SDK's own
|
|
34461
|
+
* contract). Null for chart-type channels and while the owner is hidden (a hidden
|
|
34462
|
+
* indicator leaves the scene; its cleared data channel paints nothing anyway). */
|
|
34463
|
+
layerOwner(layerId) {
|
|
34464
|
+
for (const m of this.scene.indicators.values()) {
|
|
34465
|
+
if (m.native?.type === layerId) return m;
|
|
34466
|
+
}
|
|
34467
|
+
return null;
|
|
34468
|
+
}
|
|
34469
|
+
/** The pane an SDK layer paints on: its owner's pane, else the price pane. */
|
|
34470
|
+
layerPane(layerId) {
|
|
34471
|
+
const owner = this.layerOwner(layerId);
|
|
34472
|
+
const paneId = owner ? owner.paneId ?? PRICE_PANE_ID2 : PRICE_PANE_ID2;
|
|
34473
|
+
return this.scene.panes.get(paneId) ?? null;
|
|
34474
|
+
}
|
|
34475
|
+
/** The SDK layer canvases split around the data canvas, each side back-to-front:
|
|
34476
|
+
* owned layers by their owner's z key against the candles' (an indicator restacked
|
|
34477
|
+
* below the candles takes its layer canvas along), unowned by declared placement. */
|
|
34478
|
+
orderedLayerCanvases() {
|
|
34479
|
+
const byId = new Map(this.extLayers.map((l) => [l.def.id, l.canvas]));
|
|
34480
|
+
const { below, above } = stackLayers(
|
|
34481
|
+
this.extLayers.map((l) => {
|
|
34482
|
+
const owner = this.layerOwner(l.def.id);
|
|
34483
|
+
return {
|
|
34484
|
+
id: l.def.id,
|
|
34485
|
+
placement: l.def.placement === "below-data" ? "below-data" : "above-data",
|
|
34486
|
+
ownerZ: owner ? this.scene.zOf(owner.id) : null
|
|
34487
|
+
};
|
|
34488
|
+
}),
|
|
34489
|
+
this.scene.candleZ
|
|
34490
|
+
);
|
|
34491
|
+
return { below: below.map((id) => byId.get(id)), above: above.map((id) => byId.get(id)) };
|
|
34492
|
+
}
|
|
34493
|
+
/** The full canvas pile in paint order (backdrop + layers + data/volume/vpvr) — what
|
|
34494
|
+
* the DOM stacking and the screenshot compositor must both follow. */
|
|
34495
|
+
canvasPile() {
|
|
34496
|
+
const { below, above } = this.orderedLayerCanvases();
|
|
34497
|
+
return [this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
|
|
34498
|
+
}
|
|
34499
|
+
/** Re-slot the SDK layer canvases in the plot when the computed order changed (a z
|
|
34500
|
+
* write, a restored config, an indicator mount/remove/restack). Runs at the top of
|
|
34501
|
+
* every data frame; a no-op when the signature is unchanged. Re-inserting an
|
|
34502
|
+
* absolutely-positioned, pointer-transparent canvas repaints nothing by itself. */
|
|
34503
|
+
syncLayerCanvasOrder() {
|
|
34504
|
+
if (this.extLayers.length === 0 || !this.plot) return;
|
|
34505
|
+
const { below, above } = this.orderedLayerCanvases();
|
|
34506
|
+
const sig = [...below.map((c) => this.extLayers.find((l) => l.canvas === c).def.id), "|", ...above.map((c) => this.extLayers.find((l) => l.canvas === c).def.id)].join(",");
|
|
34507
|
+
if (sig === this.layerOrderSig) return;
|
|
34508
|
+
this.layerOrderSig = sig;
|
|
34509
|
+
for (const c of below) this.plot.insertBefore(c, this.dataCanvas);
|
|
34510
|
+
for (const c of above) this.plot.insertBefore(c, this.chromeCanvas);
|
|
34511
|
+
}
|
|
34125
34512
|
/** True when an active volume layer is this study pane's ONLY content — so its scale should
|
|
34126
34513
|
* come from volume, not the empty {0,1} placeholder. (In the price pane, or alongside a real
|
|
34127
34514
|
* series, volume stays a bottom overlay and the master scale wins.) */
|
|
@@ -34131,6 +34518,15 @@ var NativeRenderer = class {
|
|
|
34131
34518
|
if (this.nativeLayerPane("volume") !== pane) return false;
|
|
34132
34519
|
return masterModels.every((m) => m.native?.type === "volume");
|
|
34133
34520
|
}
|
|
34521
|
+
/** True when this study pane's master content is only SDK-layer natives (series-less
|
|
34522
|
+
* models whose type names a mounted layer) — its scale then follows the visible bars
|
|
34523
|
+
* (see the call site). Any real master series takes over the scale as usual. */
|
|
34524
|
+
layerNativesOwnPane(pane, masterModels) {
|
|
34525
|
+
if (pane.kind === "price" || masterModels.length === 0) return false;
|
|
34526
|
+
return masterModels.every(
|
|
34527
|
+
(m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type)
|
|
34528
|
+
);
|
|
34529
|
+
}
|
|
34134
34530
|
/** Per-pane scale state for a host UI (e.g. a price-axis context menu): the pane's pixel
|
|
34135
34531
|
* band (`top`/`height`, so a click y maps to a pane) plus its current axis `mode`/`log`.
|
|
34136
34532
|
* Top-to-bottom order. Every pane is independent — the price pane from the scene setting,
|
|
@@ -34415,6 +34811,8 @@ var NativeRenderer = class {
|
|
|
34415
34811
|
const ph = h;
|
|
34416
34812
|
this.dataCanvas.width = Math.round(pw * dpr);
|
|
34417
34813
|
this.dataCanvas.height = Math.round(ph * dpr);
|
|
34814
|
+
this.backdropCanvas.width = this.dataCanvas.width;
|
|
34815
|
+
this.backdropCanvas.height = this.dataCanvas.height;
|
|
34418
34816
|
this.volumeCanvas.width = this.dataCanvas.width;
|
|
34419
34817
|
this.volumeCanvas.height = this.dataCanvas.height;
|
|
34420
34818
|
for (const l of this.extLayers) {
|
|
@@ -34556,35 +34954,6 @@ function normalizeBars(bars) {
|
|
|
34556
34954
|
return out;
|
|
34557
34955
|
}
|
|
34558
34956
|
|
|
34559
|
-
// src/core/price-styles/heikin-ashi.ts
|
|
34560
|
-
function heikinAshiNext(raw, prevHa) {
|
|
34561
|
-
const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
|
|
34562
|
-
const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
|
|
34563
|
-
return {
|
|
34564
|
-
time: raw.time,
|
|
34565
|
-
open: haOpen,
|
|
34566
|
-
high: Math.max(raw.high, haOpen, haClose),
|
|
34567
|
-
low: Math.min(raw.low, haOpen, haClose),
|
|
34568
|
-
close: haClose,
|
|
34569
|
-
...raw.volume != null ? { volume: raw.volume } : {}
|
|
34570
|
-
};
|
|
34571
|
-
}
|
|
34572
|
-
function heikinAshiFull(raw) {
|
|
34573
|
-
const out = new Array(raw.length);
|
|
34574
|
-
let prev2;
|
|
34575
|
-
for (let i = 0; i < raw.length; i += 1) {
|
|
34576
|
-
prev2 = heikinAshiNext(raw[i], prev2);
|
|
34577
|
-
out[i] = prev2;
|
|
34578
|
-
}
|
|
34579
|
-
return out;
|
|
34580
|
-
}
|
|
34581
|
-
|
|
34582
|
-
// src/chart-types/builtins.ts
|
|
34583
|
-
var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
|
|
34584
|
-
function registerBuiltinChartTypes() {
|
|
34585
|
-
registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
|
|
34586
|
-
}
|
|
34587
|
-
|
|
34588
34957
|
// src/core/native-indicators/volume/VolumeIndicator.ts
|
|
34589
34958
|
var DEFAULT_UP = BULLISH;
|
|
34590
34959
|
var DEFAULT_DOWN = BEARISH;
|
|
@@ -34781,6 +35150,7 @@ var Vela = class {
|
|
|
34781
35150
|
// the volume indicator is on by default
|
|
34782
35151
|
};
|
|
34783
35152
|
this.rendererControl = new RendererControl(renderer);
|
|
35153
|
+
if (options.settings) this.rendererControl.setSettingsVisibility(options.settings);
|
|
34784
35154
|
this.dataControl = new DataControl(feed);
|
|
34785
35155
|
this.orchestrator = new EngineOrchestrator(element, renderer, feed, engines, config, this.dataControl);
|
|
34786
35156
|
const defaults2 = rendererDefaults();
|
|
@@ -35911,8 +36281,8 @@ function seedDefaults(opts) {
|
|
|
35911
36281
|
};
|
|
35912
36282
|
}
|
|
35913
36283
|
function cellChartDefaults(opts) {
|
|
35914
|
-
const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings } = opts;
|
|
35915
|
-
return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings };
|
|
36284
|
+
const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
|
|
36285
|
+
return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings };
|
|
35916
36286
|
}
|
|
35917
36287
|
function cellDrawings(opt) {
|
|
35918
36288
|
if (opt === false) return false;
|
|
@@ -35925,7 +36295,11 @@ var ChartCell = class {
|
|
|
35925
36295
|
this.deps = deps;
|
|
35926
36296
|
/** This cell's unified app+drawings undo timeline (the shared Ctrl+Z routes here). */
|
|
35927
36297
|
this.history = new WidgetHistory(() => this.inner);
|
|
35928
|
-
/** Live manifest-indicator instances on this cell (the SAME entry may repeat).
|
|
36298
|
+
/** Live manifest-indicator instances on this cell (the SAME entry may repeat).
|
|
36299
|
+
* `external` marks instances added through the public seam (`ctx.addIndicator`)
|
|
36300
|
+
* rather than the shell manifest — they share the undo/redo and picker plumbing
|
|
36301
|
+
* but stay OUT of the persisted ledger (their names would never resolve against
|
|
36302
|
+
* the manifest); persisting them is their plugin's job (`registerStatePersistence`). */
|
|
35929
36303
|
this.instances = [];
|
|
35930
36304
|
/** The native-indicator catalog with this cell's live supported/present flags. */
|
|
35931
36305
|
this.nativeCatalog = [];
|
|
@@ -35950,6 +36324,14 @@ var ChartCell = class {
|
|
|
35950
36324
|
this.presentNatives = [];
|
|
35951
36325
|
this.rangeBars = 0;
|
|
35952
36326
|
this.pendingRange = null;
|
|
36327
|
+
/** Last symbol we toasted "no provider serves this" for — once per symbol (the
|
|
36328
|
+
* core re-reports on every provider-index settle). */
|
|
36329
|
+
this.unresolvedToasted = null;
|
|
36330
|
+
/** The cell's third-party state bag (`ext` of the persisted per-chart state) —
|
|
36331
|
+
* seeded from the boot/restored document, refreshed by handler `serialize` calls at
|
|
36332
|
+
* dehydrate time. Entries with no registered handler this session ride along
|
|
36333
|
+
* verbatim, so a document never loses a plugin's state in the plugin's absence. */
|
|
36334
|
+
this.extState = {};
|
|
35953
36335
|
/** Indicator titles (this cell's in-chart legend rows) shown. */
|
|
35954
36336
|
this.indicatorTitlesOn = true;
|
|
35955
36337
|
/** Plot values beside this cell's legend titles shown. */
|
|
@@ -36016,9 +36398,16 @@ var ChartCell = class {
|
|
|
36016
36398
|
this.pendingManifestNames = [...seed.indicators.manifest];
|
|
36017
36399
|
}
|
|
36018
36400
|
this.volumeIntent = seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume;
|
|
36401
|
+
this.extState = { ...seed.ext ?? {} };
|
|
36019
36402
|
this.inner.on("load:end", () => {
|
|
36020
36403
|
this.volumeMayBePending = false;
|
|
36021
36404
|
});
|
|
36405
|
+
this.inner.on("data:unresolved", ({ symbol: symbol2, providers }) => {
|
|
36406
|
+
if (this.unresolvedToasted === symbol2) return;
|
|
36407
|
+
this.unresolvedToasted = symbol2;
|
|
36408
|
+
const list = providers.length > 0 ? providers.join(", ") : "none";
|
|
36409
|
+
this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
|
|
36410
|
+
});
|
|
36022
36411
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
36023
36412
|
this.inner.on("load:end", () => this.watermark?.setLoading(false));
|
|
36024
36413
|
const tz = deps.timezone();
|
|
@@ -36179,11 +36568,13 @@ var ChartCell = class {
|
|
|
36179
36568
|
const eth = "Extended hours (ETH)";
|
|
36180
36569
|
const sessionSection = {
|
|
36181
36570
|
title: "Trading session",
|
|
36571
|
+
id: "trading-session",
|
|
36182
36572
|
placement: "symbol",
|
|
36183
36573
|
rows: [
|
|
36184
36574
|
{
|
|
36185
36575
|
kind: "select",
|
|
36186
36576
|
label: "Session",
|
|
36577
|
+
id: "session",
|
|
36187
36578
|
options: [rth, eth],
|
|
36188
36579
|
get: () => this.session === "extended" ? eth : rth,
|
|
36189
36580
|
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
@@ -36191,12 +36582,14 @@ var ChartCell = class {
|
|
|
36191
36582
|
{
|
|
36192
36583
|
kind: "color",
|
|
36193
36584
|
label: "Pre-market",
|
|
36585
|
+
id: "premarket-color",
|
|
36194
36586
|
get: () => this.sessionShadeColor("premarketColor"),
|
|
36195
36587
|
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
36196
36588
|
},
|
|
36197
36589
|
{
|
|
36198
36590
|
kind: "color",
|
|
36199
36591
|
label: "Post-market",
|
|
36592
|
+
id: "postmarket-color",
|
|
36200
36593
|
get: () => this.sessionShadeColor("postmarketColor"),
|
|
36201
36594
|
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
36202
36595
|
}
|
|
@@ -36204,11 +36597,13 @@ var ChartCell = class {
|
|
|
36204
36597
|
};
|
|
36205
36598
|
const advanced = {
|
|
36206
36599
|
title: "Advanced",
|
|
36600
|
+
id: "advanced",
|
|
36207
36601
|
placement: "end",
|
|
36208
36602
|
rows: [
|
|
36209
36603
|
{
|
|
36210
36604
|
kind: "select",
|
|
36211
36605
|
label: "Bars to fetch",
|
|
36606
|
+
id: "bars",
|
|
36212
36607
|
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
36213
36608
|
get: () => String(this.state.bars ?? 1e3),
|
|
36214
36609
|
set: (v) => {
|
|
@@ -36221,11 +36616,13 @@ var ChartCell = class {
|
|
|
36221
36616
|
};
|
|
36222
36617
|
const watermarkSection = {
|
|
36223
36618
|
title: "Watermark",
|
|
36619
|
+
id: "watermark",
|
|
36224
36620
|
placement: "symbol",
|
|
36225
36621
|
rows: [
|
|
36226
36622
|
{
|
|
36227
36623
|
kind: "toggle",
|
|
36228
36624
|
label: "Symbol watermark",
|
|
36625
|
+
id: "visible",
|
|
36229
36626
|
get: () => this.watermarkOn,
|
|
36230
36627
|
set: (v) => this.setWatermarkVisible(v)
|
|
36231
36628
|
}
|
|
@@ -36236,22 +36633,25 @@ var ChartCell = class {
|
|
|
36236
36633
|
const sl = this.statusline;
|
|
36237
36634
|
sections.push({
|
|
36238
36635
|
title: "Status line",
|
|
36636
|
+
id: "status-line",
|
|
36239
36637
|
rows: [
|
|
36240
|
-
{ kind: "heading", label: "Status line" },
|
|
36241
|
-
{ kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
|
|
36242
|
-
{ kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
|
|
36243
|
-
{ kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
|
|
36244
|
-
{ kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
|
|
36245
|
-
{ kind: "heading", label: "Indicators" },
|
|
36638
|
+
{ kind: "heading", label: "Status line", id: "parts" },
|
|
36639
|
+
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
|
|
36640
|
+
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
|
|
36641
|
+
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
|
|
36642
|
+
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
|
|
36643
|
+
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
36246
36644
|
{
|
|
36247
36645
|
kind: "toggle",
|
|
36248
36646
|
label: "Titles",
|
|
36647
|
+
id: "indicator-titles",
|
|
36249
36648
|
get: () => this.indicatorTitlesOn,
|
|
36250
36649
|
set: (v) => this.setIndicatorTitlesVisible(v)
|
|
36251
36650
|
},
|
|
36252
36651
|
{
|
|
36253
36652
|
kind: "toggle",
|
|
36254
36653
|
label: "Values",
|
|
36654
|
+
id: "indicator-values",
|
|
36255
36655
|
get: () => this.indicatorValuesOn,
|
|
36256
36656
|
set: (v) => this.setIndicatorValuesVisible(v)
|
|
36257
36657
|
}
|
|
@@ -36304,6 +36704,7 @@ var ChartCell = class {
|
|
|
36304
36704
|
/** Switch this cell's market in place (the chart instance survives). */
|
|
36305
36705
|
setSymbol(symbol) {
|
|
36306
36706
|
if (!this.inner || symbol === this.symbol) return;
|
|
36707
|
+
this.unresolvedToasted = null;
|
|
36307
36708
|
void this.inner.setMarket({ symbol });
|
|
36308
36709
|
}
|
|
36309
36710
|
setTimeframe(timeframe) {
|
|
@@ -36394,6 +36795,41 @@ var ChartCell = class {
|
|
|
36394
36795
|
for (const entry of list) if (entry.enabled) this.addManifestInstance(entry, { record: false });
|
|
36395
36796
|
}
|
|
36396
36797
|
}
|
|
36798
|
+
/**
|
|
36799
|
+
* Replace the indicator ledger: natives converge to the listed set (volume
|
|
36800
|
+
* included — removing it sticks, the core's auto-add respects the opt-out), and
|
|
36801
|
+
* manifest instances are re-created by name, held until the shared manifest
|
|
36802
|
+
* resolves. Convergence is state application, not user edits — nothing enters the
|
|
36803
|
+
* undo timeline.
|
|
36804
|
+
*/
|
|
36805
|
+
applyIndicatorLedger(led) {
|
|
36806
|
+
const chart = this.inner;
|
|
36807
|
+
if (!chart) return;
|
|
36808
|
+
this.volumeIntent = led.natives.includes("volume");
|
|
36809
|
+
this.history.silently(() => {
|
|
36810
|
+
const present = chart.presentNativeIndicators();
|
|
36811
|
+
for (const type of led.natives) {
|
|
36812
|
+
if (!present.includes(type)) chart.addNativeIndicator(type);
|
|
36813
|
+
}
|
|
36814
|
+
for (const type of present) {
|
|
36815
|
+
if (!led.natives.includes(type)) chart.addNativeIndicator(type).remove();
|
|
36816
|
+
}
|
|
36817
|
+
for (const it of [...this.instances]) this.dropInstance(it);
|
|
36818
|
+
if (this.manifest.length > 0) {
|
|
36819
|
+
for (const name of led.manifest) {
|
|
36820
|
+
const entry = this.manifest.find((e) => e.name === name);
|
|
36821
|
+
if (entry) this.addManifestInstance(entry, { record: false });
|
|
36822
|
+
}
|
|
36823
|
+
this.pendingManifestNames = null;
|
|
36824
|
+
} else if (!this.deps.manifestSettled()) {
|
|
36825
|
+
this.pendingManifestNames = [...led.manifest];
|
|
36826
|
+
} else {
|
|
36827
|
+
this.pendingManifestNames = null;
|
|
36828
|
+
}
|
|
36829
|
+
});
|
|
36830
|
+
this.syncPresentNatives();
|
|
36831
|
+
this.refreshNativeCatalog();
|
|
36832
|
+
}
|
|
36397
36833
|
/** The picker's library rows: supported natives first, then the manifest. */
|
|
36398
36834
|
libraryRows() {
|
|
36399
36835
|
return [
|
|
@@ -36423,10 +36859,20 @@ var ChartCell = class {
|
|
|
36423
36859
|
if (index < present.length) this.removeNative(present[index].type);
|
|
36424
36860
|
else this.removeInstance(index - present.length);
|
|
36425
36861
|
}
|
|
36862
|
+
/**
|
|
36863
|
+
* Add a script indicator through the PUBLIC seam (`ctx.addIndicator`) — same undo/
|
|
36864
|
+
* redo and picker plumbing as a manifest entry, but flagged `external` so the
|
|
36865
|
+
* persisted ledger never records a name the manifest can't resolve (the plugin owns
|
|
36866
|
+
* persistence via `registerStatePersistence`). Recording follows the ambient mute:
|
|
36867
|
+
* a persistence handler's `restore` runs silently, a user-driven call records.
|
|
36868
|
+
*/
|
|
36869
|
+
addExternalIndicator(entry) {
|
|
36870
|
+
this.addManifestInstance({ ...entry, enabled: true }, { external: true });
|
|
36871
|
+
}
|
|
36426
36872
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
36427
36873
|
addManifestInstance(entry, opts = {}) {
|
|
36428
36874
|
if (this.destroyed) return;
|
|
36429
|
-
const it = { entry, handle: this.addToChart(entry) };
|
|
36875
|
+
const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
|
|
36430
36876
|
this.instances.push(it);
|
|
36431
36877
|
this.deps.onIndicatorsChanged(this.id);
|
|
36432
36878
|
if (opts.record === false) return;
|
|
@@ -36507,12 +36953,97 @@ var ChartCell = class {
|
|
|
36507
36953
|
return null;
|
|
36508
36954
|
}
|
|
36509
36955
|
}
|
|
36956
|
+
// ── third-party state (the `ext` seam) ──
|
|
36957
|
+
/** The cell-bound surface persistence handlers work against (built per call — the
|
|
36958
|
+
* widget-context rule; nothing here may be cached by a handler). Its add methods
|
|
36959
|
+
* are ALWAYS muted — a `restore` that fetches before adding escapes the sync mute
|
|
36960
|
+
* of {@link restorePersistedExt}, and a state application must never enter the
|
|
36961
|
+
* undo timeline, however late its continuation lands. */
|
|
36962
|
+
stateContext() {
|
|
36963
|
+
return {
|
|
36964
|
+
cellId: this.id,
|
|
36965
|
+
chart: this.chart,
|
|
36966
|
+
addIndicator: (entry) => this.history.silently(() => this.addExternalIndicator(entry)),
|
|
36967
|
+
addNativeIndicator: (type) => this.history.silently(() => this.addNative(type))
|
|
36968
|
+
};
|
|
36969
|
+
}
|
|
36970
|
+
/**
|
|
36971
|
+
* Run the registered cell-scope `restore` handlers against the cell's restored
|
|
36972
|
+
* `ext` bag — the workspace calls this AFTER the core state is in place (chart
|
|
36973
|
+
* alive and wired, indicator ledger converged). Muted: nothing a restore does
|
|
36974
|
+
* enters the undo timeline. Handlers only see keys the document carries; a failing
|
|
36975
|
+
* handler is contained (one broken plugin must not take the cell down).
|
|
36976
|
+
*/
|
|
36977
|
+
restorePersistedExt() {
|
|
36978
|
+
if (this.destroyed) return;
|
|
36979
|
+
for (const h of statePersistenceHandlers("cell")) {
|
|
36980
|
+
if (!(h.key in this.extState)) continue;
|
|
36981
|
+
try {
|
|
36982
|
+
this.history.silently(() => h.restore(this.extState[h.key], this.stateContext()));
|
|
36983
|
+
} catch (err) {
|
|
36984
|
+
console.warn(`[vela] state persistence "${h.key}" restore failed:`, err);
|
|
36985
|
+
}
|
|
36986
|
+
}
|
|
36987
|
+
}
|
|
36988
|
+
/** Assemble the cell's `ext` bag: fresh handler snapshots merged OVER the preserved
|
|
36989
|
+
* entries — a key with no handler this session rides along verbatim; a registered
|
|
36990
|
+
* handler returning `undefined` withdraws its entry. */
|
|
36991
|
+
dehydrateExt() {
|
|
36992
|
+
const ext = { ...this.extState };
|
|
36993
|
+
for (const h of statePersistenceHandlers("cell")) {
|
|
36994
|
+
try {
|
|
36995
|
+
const value = h.serialize(this.stateContext());
|
|
36996
|
+
if (value === void 0) delete ext[h.key];
|
|
36997
|
+
else ext[h.key] = value;
|
|
36998
|
+
} catch (err) {
|
|
36999
|
+
console.warn(`[vela] state persistence "${h.key}" serialize failed:`, err);
|
|
37000
|
+
}
|
|
37001
|
+
}
|
|
37002
|
+
this.extState = ext;
|
|
37003
|
+
return Object.keys(ext).length > 0 ? ext : void 0;
|
|
37004
|
+
}
|
|
36510
37005
|
// ── lifecycle ──
|
|
37006
|
+
/**
|
|
37007
|
+
* Apply a restored cell state IN PLACE — the chart instance survives (the market
|
|
37008
|
+
* switches via `setMarket`) while cosmetics, renderer config, drawings, and the
|
|
37009
|
+
* indicator ledger converge to the document. The workspace takes this path when a
|
|
37010
|
+
* state document lands on a grid of the same shape (async-storage boot, host
|
|
37011
|
+
* `applyState`), so chart references, indicator handles, event subscriptions, and
|
|
37012
|
+
* the cell host all stay valid.
|
|
37013
|
+
*/
|
|
37014
|
+
rehydrate(cs) {
|
|
37015
|
+
if (!this.inner || this.destroyed) return;
|
|
37016
|
+
if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
|
|
37017
|
+
if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
|
|
37018
|
+
if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
|
|
37019
|
+
if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
|
|
37020
|
+
if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
|
|
37021
|
+
if (cs.drawings != null) this.inner.drawings.fromJSON(cs.drawings);
|
|
37022
|
+
if (cs.indicators) this.applyIndicatorLedger(cs.indicators);
|
|
37023
|
+
this.extState = { ...cs.ext ?? {} };
|
|
37024
|
+
this.restorePersistedExt();
|
|
37025
|
+
const symbol = prefixedSymbol(cs);
|
|
37026
|
+
const session = normalizeSession(cs.session) ?? "regular";
|
|
37027
|
+
const bars = typeof cs.bars === "number" && Number.isFinite(cs.bars) && cs.bars > 0 ? cs.bars : 0;
|
|
37028
|
+
const next = {};
|
|
37029
|
+
if (symbol && symbol !== this.symbol) next.symbol = symbol;
|
|
37030
|
+
if (cs.timeframe && cs.timeframe !== this.timeframe) next.timeframe = cs.timeframe;
|
|
37031
|
+
if (session !== this.session) {
|
|
37032
|
+
this.state.session = session;
|
|
37033
|
+
next.session = session;
|
|
37034
|
+
}
|
|
37035
|
+
if (bars > 0 && bars !== this.state.bars) {
|
|
37036
|
+
this.state.bars = bars;
|
|
37037
|
+
next.bars = Math.max(bars, this.rangeBars);
|
|
37038
|
+
}
|
|
37039
|
+
if (Object.keys(next).length > 0) void this.inner.setMarket(next);
|
|
37040
|
+
}
|
|
36511
37041
|
/** Snapshot everything the pool needs to restore this slot later. The market fields
|
|
36512
37042
|
* come from the LIVE config (`chart.market`) — the requested identity — so a switch
|
|
36513
37043
|
* still loading when the snapshot is taken (persist-on-close) is not lost. */
|
|
36514
37044
|
dehydrate() {
|
|
36515
37045
|
const live = this.inner?.market;
|
|
37046
|
+
const ext = this.inner ? this.dehydrateExt() : Object.keys(this.extState).length > 0 ? { ...this.extState } : void 0;
|
|
36516
37047
|
return {
|
|
36517
37048
|
...this.state,
|
|
36518
37049
|
...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
|
|
@@ -36525,14 +37056,17 @@ var ChartCell = class {
|
|
|
36525
37056
|
// Natives from the chart's SYNC registry read — an async catalog mirror here
|
|
36526
37057
|
// lost unload-time saves, and the old empty-set fallbacks resurrected removed
|
|
36527
37058
|
// indicators. Manifest names fall back to the restored ledger only until the
|
|
36528
|
-
// shared manifest settles. See {@link indicatorLedger}.
|
|
37059
|
+
// shared manifest settles. See {@link indicatorLedger}. External instances
|
|
37060
|
+
// (`ctx.addIndicator`) stay out: their names would never resolve against the
|
|
37061
|
+
// manifest — their plugin persists them via the `ext` seam instead.
|
|
36529
37062
|
indicators: indicatorLedger({
|
|
36530
37063
|
present: this.inner ? this.inner.presentNativeIndicators() : [],
|
|
36531
|
-
instanceNames: this.instances.map((it) => it.entry.name),
|
|
37064
|
+
instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
|
|
36532
37065
|
pendingManifest: this.pendingManifestNames,
|
|
36533
37066
|
manifestSettled: this.deps.manifestSettled(),
|
|
36534
37067
|
volumePending: this.volumeMayBePending && this.volumeIntent
|
|
36535
|
-
})
|
|
37068
|
+
}),
|
|
37069
|
+
...ext ? { ext } : {}
|
|
36536
37070
|
};
|
|
36537
37071
|
}
|
|
36538
37072
|
destroy() {
|
|
@@ -36569,6 +37103,9 @@ function buildContext(host) {
|
|
|
36569
37103
|
togglePanel: (id, open2) => host.togglePanel(id, open2),
|
|
36570
37104
|
host: host.root,
|
|
36571
37105
|
toast: (message, kind) => host.toast(message, kind),
|
|
37106
|
+
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
37107
|
+
addNativeIndicator: (type) => host.active()?.addNative(type),
|
|
37108
|
+
stateChanged: () => host.stateDirty(),
|
|
36572
37109
|
cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
|
|
36573
37110
|
activeCellId: active?.id ?? "",
|
|
36574
37111
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
@@ -36930,6 +37467,10 @@ var VelaWorkspace = class {
|
|
|
36930
37467
|
/** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
|
|
36931
37468
|
this.priceScalePressY = 0;
|
|
36932
37469
|
this.attachmentDisposers = /* @__PURE__ */ new Map();
|
|
37470
|
+
/** The document-level third-party state bag (`state.ext`) — seeded from the restored
|
|
37471
|
+
* document, refreshed by global-scope handler `serialize` calls at snapshot time.
|
|
37472
|
+
* Entries with no registered handler this session ride along verbatim. */
|
|
37473
|
+
this.extState = {};
|
|
36933
37474
|
/** The single grid-wide attribution mark — re-inked on a live theme swap. */
|
|
36934
37475
|
this.attributionMark = null;
|
|
36935
37476
|
this.onRootKeydown = (ev) => this.routeTyping(ev);
|
|
@@ -36944,6 +37485,7 @@ var VelaWorkspace = class {
|
|
|
36944
37485
|
state: () => ({ ...this.syncOpts })
|
|
36945
37486
|
};
|
|
36946
37487
|
registerBuiltinLayouts();
|
|
37488
|
+
registerBuiltinChartTypes();
|
|
36947
37489
|
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
36948
37490
|
if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
|
|
36949
37491
|
this.opts = opts;
|
|
@@ -36967,8 +37509,12 @@ var VelaWorkspace = class {
|
|
|
36967
37509
|
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
|
|
36968
37510
|
this.applySyncSetting(kind, sync?.[kind]);
|
|
36969
37511
|
}
|
|
36970
|
-
this.
|
|
37512
|
+
this.monoLayout = opts.layout === false;
|
|
37513
|
+
const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
|
|
37514
|
+
this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
|
|
37515
|
+
this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
|
|
36971
37516
|
if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
|
|
37517
|
+
if (boot?.ext) this.extState = { ...boot.ext };
|
|
36972
37518
|
if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
|
|
36973
37519
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
36974
37520
|
const bootActive = boot?.activeCellId ?? null;
|
|
@@ -37022,7 +37568,9 @@ var VelaWorkspace = class {
|
|
|
37022
37568
|
onTimeframe: (tf) => this.setActiveTimeframe(tf),
|
|
37023
37569
|
onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
|
|
37024
37570
|
onPriceStyle: (style) => this.active.setPriceStyle(style),
|
|
37025
|
-
|
|
37571
|
+
// Single-chart mode: no layout block at all — the topbar renders no layout
|
|
37572
|
+
// button and no sync switches (see TopbarOptions.layout).
|
|
37573
|
+
layout: this.monoLayout ? void 0 : {
|
|
37026
37574
|
current: this.def.id,
|
|
37027
37575
|
// The picker composes dynamic layouts on its grid canvas; registered
|
|
37028
37576
|
// presets the canvas cannot express (bespoke plugin areas) list as rows.
|
|
@@ -37047,8 +37595,11 @@ var VelaWorkspace = class {
|
|
|
37047
37595
|
});
|
|
37048
37596
|
const main = doc.createElement("div");
|
|
37049
37597
|
main.className = "vela-ws-main";
|
|
37598
|
+
const toolbar = buildToolbar(opts.drawings);
|
|
37599
|
+
this.drawingsEnabled = opts.drawings !== false;
|
|
37600
|
+
this.toolbarDef = toolbar.definition;
|
|
37050
37601
|
let toolbarHost = null;
|
|
37051
|
-
if (opts.drawingToolbar !== false) {
|
|
37602
|
+
if (this.drawingsEnabled && toolbar.visible && opts.drawingToolbar !== false) {
|
|
37052
37603
|
toolbarHost = doc.createElement("div");
|
|
37053
37604
|
toolbarHost.className = "vela-ws-toolbar";
|
|
37054
37605
|
main.appendChild(toolbarHost);
|
|
@@ -37067,7 +37618,7 @@ var VelaWorkspace = class {
|
|
|
37067
37618
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
37068
37619
|
this.dock.refresh();
|
|
37069
37620
|
this.root.appendChild(main);
|
|
37070
|
-
this.
|
|
37621
|
+
this.toastHost = new Toast(this.gridEl);
|
|
37071
37622
|
const attribution = rendererDefaults().attribution;
|
|
37072
37623
|
if (attribution !== false) {
|
|
37073
37624
|
const background = resolveTheme(opts.theme).background;
|
|
@@ -37113,7 +37664,7 @@ var VelaWorkspace = class {
|
|
|
37113
37664
|
}
|
|
37114
37665
|
}
|
|
37115
37666
|
) : null;
|
|
37116
|
-
this.drawToolbar?.setDefinition(
|
|
37667
|
+
this.drawToolbar?.setDefinition(this.toolbarDef);
|
|
37117
37668
|
this.drawToolbar?.setVisible(true);
|
|
37118
37669
|
this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
|
|
37119
37670
|
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
@@ -37135,11 +37686,11 @@ var VelaWorkspace = class {
|
|
|
37135
37686
|
onTimeframeClick: () => this.openTimeframeDrawer(),
|
|
37136
37687
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
37137
37688
|
getContext: () => this.context(),
|
|
37138
|
-
onDrawingsClick: () => this.openDrawingsDrawer(),
|
|
37689
|
+
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
37139
37690
|
onMoreClick: () => this.openMoreDrawer(),
|
|
37140
37691
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
37141
37692
|
}) : null;
|
|
37142
|
-
this.drawingPill = new DrawingPill(this.gridEl);
|
|
37693
|
+
this.drawingPill = this.drawingsEnabled ? new DrawingPill(this.gridEl) : null;
|
|
37143
37694
|
hostEl.appendChild(this.root);
|
|
37144
37695
|
this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
|
|
37145
37696
|
this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
|
|
@@ -37178,6 +37729,7 @@ var VelaWorkspace = class {
|
|
|
37178
37729
|
this.manifestSettled = true;
|
|
37179
37730
|
}
|
|
37180
37731
|
this.mountAttachments();
|
|
37732
|
+
this.restoreGlobalExt();
|
|
37181
37733
|
}
|
|
37182
37734
|
// ── access ──────────────────────────────────────────────────
|
|
37183
37735
|
/** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
|
|
@@ -37239,7 +37791,8 @@ var VelaWorkspace = class {
|
|
|
37239
37791
|
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
37240
37792
|
togglePanel: (id, open2) => this.dock.toggle(id, open2),
|
|
37241
37793
|
root: this.root,
|
|
37242
|
-
toast: (message, kind) => this.
|
|
37794
|
+
toast: (message, kind) => this.toastHost.show(message, kind),
|
|
37795
|
+
stateDirty: () => this.markStateDirty()
|
|
37243
37796
|
});
|
|
37244
37797
|
}
|
|
37245
37798
|
/** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
|
|
@@ -37278,22 +37831,35 @@ var VelaWorkspace = class {
|
|
|
37278
37831
|
if (this.trackSizes.size > 0) state.trackSizes = Object.fromEntries([...this.trackSizes].map(([k, v]) => [k, { ...v }]));
|
|
37279
37832
|
const panels2 = this.dock.getState();
|
|
37280
37833
|
if (panels2) state.panels = panels2;
|
|
37834
|
+
const ext = { ...this.extState };
|
|
37835
|
+
for (const h of statePersistenceHandlers("global")) {
|
|
37836
|
+
try {
|
|
37837
|
+
const value = h.serialize(this.context());
|
|
37838
|
+
if (value === void 0) delete ext[h.key];
|
|
37839
|
+
else ext[h.key] = value;
|
|
37840
|
+
} catch (err) {
|
|
37841
|
+
console.warn(`[vela] state persistence "${h.key}" serialize failed:`, err);
|
|
37842
|
+
}
|
|
37843
|
+
}
|
|
37844
|
+
this.extState = ext;
|
|
37845
|
+
if (Object.keys(ext).length > 0) state.ext = ext;
|
|
37281
37846
|
return state;
|
|
37282
37847
|
}
|
|
37283
37848
|
/**
|
|
37284
37849
|
* Restore a state document produced by {@link getState} (untrusted-safe: malformed
|
|
37285
|
-
* fields are dropped).
|
|
37286
|
-
* layout,
|
|
37287
|
-
*
|
|
37850
|
+
* fields are dropped). When the document matches the live grid one-to-one — same
|
|
37851
|
+
* layout, same ordered slot identities — it is applied IN PLACE: every chart
|
|
37852
|
+
* instance survives (markets switch via `setMarket`), so chart references,
|
|
37853
|
+
* indicator handles, and event subscriptions stay valid. Any structural difference
|
|
37854
|
+
* (layout, slot count, renamed ids) falls back to the full rebuild: prefs, sync
|
|
37855
|
+
* links, layout, and every slot are replaced, current cells rebuilt from the
|
|
37856
|
+
* document. A layout id that is not registered keeps the current grid (register
|
|
37857
|
+
* custom layouts first).
|
|
37288
37858
|
*/
|
|
37289
37859
|
applyState(state) {
|
|
37290
37860
|
if (this.destroyed) return;
|
|
37291
37861
|
const st = sanitizeState(state);
|
|
37292
37862
|
if (!st) return;
|
|
37293
|
-
if (st.timezone) {
|
|
37294
|
-
this.timezone = st.timezone;
|
|
37295
|
-
this.bottombar?.setTimezone(st.timezone);
|
|
37296
|
-
}
|
|
37297
37863
|
if (st.favorites) this.favs = [...st.favorites];
|
|
37298
37864
|
if (st.timeframeFavorites) {
|
|
37299
37865
|
this.tfFavs = [...st.timeframeFavorites];
|
|
@@ -37303,6 +37869,36 @@ var VelaWorkspace = class {
|
|
|
37303
37869
|
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
37304
37870
|
this.trackSizes.clear();
|
|
37305
37871
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
37872
|
+
const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
|
|
37873
|
+
const liveCount = this.def.cells.length;
|
|
37874
|
+
const inPlace = targetDef.id === this.def.id && st.charts.length >= liveCount && this.order.length >= liveCount && this.def.cells.every((_, i) => st.charts[i].id === this.order[i] && this.cellsById.has(this.order[i]));
|
|
37875
|
+
if (inPlace) {
|
|
37876
|
+
if (st.favorites) {
|
|
37877
|
+
for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
|
|
37878
|
+
}
|
|
37879
|
+
this.drawingLinks.clear();
|
|
37880
|
+
for (const [i] of this.def.cells.entries()) {
|
|
37881
|
+
const { id, ...cs } = st.charts[i];
|
|
37882
|
+
this.cellsById.get(id)?.rehydrate(cs);
|
|
37883
|
+
}
|
|
37884
|
+
if (st.timezone) this.setTimezone(st.timezone);
|
|
37885
|
+
this.pool.clear();
|
|
37886
|
+
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
37887
|
+
this.order = st.charts.map((c) => c.id);
|
|
37888
|
+
this.applyGrid();
|
|
37889
|
+
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
37890
|
+
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
37891
|
+
else this.setActiveCell(nextActive2);
|
|
37892
|
+
this.refreshRetention();
|
|
37893
|
+
this.extState = { ...st.ext ?? {} };
|
|
37894
|
+
this.restoreGlobalExt();
|
|
37895
|
+
this.markStateDirty();
|
|
37896
|
+
return;
|
|
37897
|
+
}
|
|
37898
|
+
if (st.timezone) {
|
|
37899
|
+
this.timezone = st.timezone;
|
|
37900
|
+
this.bottombar?.setTimezone(st.timezone);
|
|
37901
|
+
}
|
|
37306
37902
|
for (const [id, cell] of [...this.cellsById]) {
|
|
37307
37903
|
cell.destroy();
|
|
37308
37904
|
this.cellsById.delete(id);
|
|
@@ -37312,7 +37908,7 @@ var VelaWorkspace = class {
|
|
|
37312
37908
|
this.drawingLinks.clear();
|
|
37313
37909
|
for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
|
|
37314
37910
|
this.order = st.charts.map((c) => c.id);
|
|
37315
|
-
const def = ensureLayout(st.layout);
|
|
37911
|
+
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
37316
37912
|
if (def) this.def = def;
|
|
37317
37913
|
this.cellBackend = this.backendFor(this.def);
|
|
37318
37914
|
this.applyGrid();
|
|
@@ -37324,8 +37920,24 @@ var VelaWorkspace = class {
|
|
|
37324
37920
|
else this.setActiveCell(nextActive);
|
|
37325
37921
|
this.refreshRetention();
|
|
37326
37922
|
this.events.emit("layout:changed", { layout: this.def.id });
|
|
37923
|
+
this.extState = { ...st.ext ?? {} };
|
|
37924
|
+
this.restoreGlobalExt();
|
|
37327
37925
|
this.markStateDirty();
|
|
37328
37926
|
}
|
|
37927
|
+
/** Run the registered global-scope `restore` handlers against the document-level
|
|
37928
|
+
* `ext` bag — keys present in the document only; a failing handler is contained.
|
|
37929
|
+
* Handlers whose restore touches chart content should be `scope: 'cell'` instead
|
|
37930
|
+
* (those run inside the cell's history-mute). */
|
|
37931
|
+
restoreGlobalExt() {
|
|
37932
|
+
for (const h of statePersistenceHandlers("global")) {
|
|
37933
|
+
if (!(h.key in this.extState)) continue;
|
|
37934
|
+
try {
|
|
37935
|
+
h.restore(this.extState[h.key], this.context());
|
|
37936
|
+
} catch (err) {
|
|
37937
|
+
console.warn(`[vela] state persistence "${h.key}" restore failed:`, err);
|
|
37938
|
+
}
|
|
37939
|
+
}
|
|
37940
|
+
}
|
|
37329
37941
|
/** Set the workspace-global display timezone — applied to EVERY cell. */
|
|
37330
37942
|
setTimezone(zone) {
|
|
37331
37943
|
this.timezone = zone;
|
|
@@ -37361,6 +37973,7 @@ var VelaWorkspace = class {
|
|
|
37361
37973
|
*/
|
|
37362
37974
|
setLayout(layout) {
|
|
37363
37975
|
if (this.destroyed) return;
|
|
37976
|
+
if (this.monoLayout) return;
|
|
37364
37977
|
const next = this.resolveLayout(layout);
|
|
37365
37978
|
const nextBackend = this.backendFor(next);
|
|
37366
37979
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -37390,6 +38003,12 @@ var VelaWorkspace = class {
|
|
|
37390
38003
|
resize() {
|
|
37391
38004
|
this.splitters.layout();
|
|
37392
38005
|
}
|
|
38006
|
+
/** Show a toast over the grid — the same surface the shell's own notices use
|
|
38007
|
+
* (alerts, script errors) and the one contributions reach via `ctx.toast`. */
|
|
38008
|
+
toast(message, kind = "info", durationMs = 3e3) {
|
|
38009
|
+
if (this.destroyed) return;
|
|
38010
|
+
this.toastHost.show(message, kind, durationMs);
|
|
38011
|
+
}
|
|
37393
38012
|
destroy() {
|
|
37394
38013
|
if (this.destroyed) return;
|
|
37395
38014
|
this.persistNow();
|
|
@@ -37415,7 +38034,7 @@ var VelaWorkspace = class {
|
|
|
37415
38034
|
this.topbar.destroy();
|
|
37416
38035
|
this.bottombar?.destroy();
|
|
37417
38036
|
this.mobileBar?.destroy();
|
|
37418
|
-
this.drawingPill
|
|
38037
|
+
this.drawingPill?.destroy();
|
|
37419
38038
|
this.tfDrawer?.destroy();
|
|
37420
38039
|
this.drawingsDrawer?.destroy();
|
|
37421
38040
|
this.moreDrawer?.destroy();
|
|
@@ -37429,10 +38048,10 @@ var VelaWorkspace = class {
|
|
|
37429
38048
|
this.indicatorPicker?.destroy();
|
|
37430
38049
|
this.tfQuick.destroy();
|
|
37431
38050
|
this.shortcutsHelp?.destroy();
|
|
37432
|
-
this.
|
|
38051
|
+
this.toastHost.destroy();
|
|
37433
38052
|
this.alertsMenu?.destroy();
|
|
37434
38053
|
this.glider.stop();
|
|
37435
|
-
sharedBarStore.retain(/* @__PURE__ */ new Set());
|
|
38054
|
+
sharedBarStore.retain(/* @__PURE__ */ new Set(), this);
|
|
37436
38055
|
this.root.remove();
|
|
37437
38056
|
this.events.clear();
|
|
37438
38057
|
}
|
|
@@ -37450,7 +38069,7 @@ var VelaWorkspace = class {
|
|
|
37450
38069
|
this.mobileBar?.renderActions();
|
|
37451
38070
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
37452
38071
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
37453
|
-
this.drawingPill
|
|
38072
|
+
this.drawingPill?.onChart(cell.chart);
|
|
37454
38073
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
37455
38074
|
this.historyUnsub?.();
|
|
37456
38075
|
this.historyUnsub = cell.history.onChange(pushHistory);
|
|
@@ -37576,7 +38195,8 @@ var VelaWorkspace = class {
|
|
|
37576
38195
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
37577
38196
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
37578
38197
|
onStateDirty: () => this.markStateDirty(),
|
|
37579
|
-
manifestSettled: () => this.manifestSettled
|
|
38198
|
+
manifestSettled: () => this.manifestSettled,
|
|
38199
|
+
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
37580
38200
|
});
|
|
37581
38201
|
cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
37582
38202
|
this.cellsById.set(id, cell);
|
|
@@ -37585,6 +38205,7 @@ var VelaWorkspace = class {
|
|
|
37585
38205
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
37586
38206
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
37587
38207
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
38208
|
+
cell.restorePersistedExt();
|
|
37588
38209
|
this.events.emit("cell:created", { id });
|
|
37589
38210
|
}
|
|
37590
38211
|
for (const [i] of this.def.cells.entries()) {
|
|
@@ -37596,12 +38217,13 @@ var VelaWorkspace = class {
|
|
|
37596
38217
|
* cell's whole life, so these live and die with the cell). */
|
|
37597
38218
|
wireCell(cell) {
|
|
37598
38219
|
const chart = cell.chart;
|
|
37599
|
-
chart.on("indicator:error", ({ error }) => this.
|
|
38220
|
+
chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
|
|
37600
38221
|
chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
|
|
37601
38222
|
chart.on("alert", (alert) => {
|
|
37602
|
-
|
|
37603
|
-
|
|
37604
|
-
this.
|
|
38223
|
+
const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
|
|
38224
|
+
this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
|
|
38225
|
+
if (this.alerts.length > this.alertCap) this.alerts.pop();
|
|
38226
|
+
this.toastHost.show(`${source} \u2014 ${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
|
|
37605
38227
|
this.topbar.setAlertCount(this.alerts.length);
|
|
37606
38228
|
});
|
|
37607
38229
|
chart.on("drawing:favorites", ({ favorites }) => {
|
|
@@ -37862,6 +38484,7 @@ var VelaWorkspace = class {
|
|
|
37862
38484
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
37863
38485
|
this.objectTree.setSymbol(cell.symbol);
|
|
37864
38486
|
this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
|
|
38487
|
+
this.bottombar?.setActiveRange(cell.activeRangeId);
|
|
37865
38488
|
}
|
|
37866
38489
|
/** Trigger ② — a cell's price style changed: the topbar button/menu only if active.
|
|
37867
38490
|
* Reads the cell back (not the requested style) so the button reflects what the
|
|
@@ -37946,7 +38569,7 @@ var VelaWorkspace = class {
|
|
|
37946
38569
|
openDrawingsDrawer() {
|
|
37947
38570
|
this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
|
|
37948
38571
|
host: this.root,
|
|
37949
|
-
toolbar: () =>
|
|
38572
|
+
toolbar: () => this.toolbarDef,
|
|
37950
38573
|
// the shared static toolbar's definition (see constructor)
|
|
37951
38574
|
currentTool: () => this.active.chart.drawings.getTool(),
|
|
37952
38575
|
isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
|
|
@@ -37969,14 +38592,15 @@ var VelaWorkspace = class {
|
|
|
37969
38592
|
onPriceStyle: (id) => this.active.setPriceStyle(id),
|
|
37970
38593
|
panels: () => [...this.dock.list()],
|
|
37971
38594
|
onTogglePanel: (id) => this.dock.toggle(id),
|
|
37972
|
-
alerts: () => this.alerts.map((a) => ({ title:
|
|
38595
|
+
alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
|
|
37973
38596
|
// Left-aligned actions have their own bottom-bar stop — only the rest
|
|
37974
38597
|
// lands in the drawer, or every left action would appear twice.
|
|
37975
38598
|
actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
|
|
37976
38599
|
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
37977
38600
|
// non-canvas presets and the sync switches — relocated into the kebab
|
|
37978
|
-
// drawer (the topbar is hidden on mobile). Same reads as the topbar block
|
|
37979
|
-
|
|
38601
|
+
// drawer (the topbar is hidden on mobile). Same reads as the topbar block;
|
|
38602
|
+
// single-chart mode omits it here too.
|
|
38603
|
+
layout: this.monoLayout ? void 0 : {
|
|
37980
38604
|
shape: () => layoutShape(this.def),
|
|
37981
38605
|
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
|
|
37982
38606
|
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
@@ -38019,7 +38643,7 @@ var VelaWorkspace = class {
|
|
|
38019
38643
|
this.alertsMenu?.destroy();
|
|
38020
38644
|
const items = this.alerts.length ? this.alerts.map((a, i) => ({
|
|
38021
38645
|
id: String(i),
|
|
38022
|
-
label:
|
|
38646
|
+
label: `${a.source} \xB7 ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
|
|
38023
38647
|
})) : [{ id: "none", label: "No alerts yet", disabled: true }];
|
|
38024
38648
|
this.alertsMenu = new Menu({
|
|
38025
38649
|
host: this.root,
|
|
@@ -38140,7 +38764,7 @@ var VelaWorkspace = class {
|
|
|
38140
38764
|
if (!raw) continue;
|
|
38141
38765
|
symbols.add(this.feed.resolveSymbol(raw)?.ticker ?? raw);
|
|
38142
38766
|
}
|
|
38143
|
-
sharedBarStore.retain(symbols);
|
|
38767
|
+
sharedBarStore.retain(symbols, this);
|
|
38144
38768
|
}
|
|
38145
38769
|
};
|
|
38146
38770
|
|