@luxalgo/vela 0.6.5 → 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-FQBT4NFM.js → chunk-JQFO2WMU.js} +2768 -2534
- package/dist/{chunk-BEJ57HP4.js → chunk-OEUGUXL7.js} +14 -1
- package/dist/{chunk-N6MBJHMV.js → chunk-TMXK2SJR.js} +5838 -3346
- 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 +2761 -2511
- package/dist/index.d.cts +40 -16
- package/dist/index.d.ts +40 -16
- 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-DO_DX0gp.d.cts → plugin-CN8U2__Q.d.cts} +6 -4
- package/dist/{plugin-Bcone6Vh.d.ts → plugin-WnvNNJOp.d.ts} +6 -4
- 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 +2762 -2512
- package/dist/vela.global.min.js +48 -48
- package/dist/widget.cjs +35560 -34095
- package/dist/widget.d.cts +34 -271
- package/dist/widget.d.ts +34 -271
- package/dist/widget.js +60 -1313
- package/dist/workspace.cjs +1438 -933
- 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 };
|
|
@@ -14212,10 +14227,11 @@ var MobileBar = class {
|
|
|
14212
14227
|
const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
|
|
14213
14228
|
this.actionsHost = doc.createElement("span");
|
|
14214
14229
|
this.actionsHost.className = "vela-mb-actions";
|
|
14215
|
-
const
|
|
14230
|
+
const onDrawings = opts.onDrawingsClick;
|
|
14231
|
+
const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
|
|
14216
14232
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
14217
14233
|
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
14218
|
-
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);
|
|
14219
14235
|
host.appendChild(this.el);
|
|
14220
14236
|
this.renderActions();
|
|
14221
14237
|
}
|
|
@@ -17030,6 +17046,35 @@ function rendererDefaults() {
|
|
|
17030
17046
|
return Object.fromEntries(defaults);
|
|
17031
17047
|
}
|
|
17032
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
|
+
|
|
17033
17078
|
// src/workspace/sync.ts
|
|
17034
17079
|
function syncTargets(originId, setting, cellIds) {
|
|
17035
17080
|
if (setting == null || setting === false) return [];
|
|
@@ -17089,8 +17134,18 @@ function sanitizeState(doc) {
|
|
|
17089
17134
|
if (tracks) out.trackSizes = tracks;
|
|
17090
17135
|
const panels2 = sanitizePanels(d.panels);
|
|
17091
17136
|
if (panels2) out.panels = panels2;
|
|
17137
|
+
const ext = sanitizeExt(d.ext);
|
|
17138
|
+
if (ext) out.ext = ext;
|
|
17092
17139
|
return out;
|
|
17093
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
|
+
}
|
|
17094
17149
|
function sanitizeCell(raw) {
|
|
17095
17150
|
if (raw == null || typeof raw !== "object") return null;
|
|
17096
17151
|
const c = raw;
|
|
@@ -17112,6 +17167,8 @@ function sanitizeCell(raw) {
|
|
|
17112
17167
|
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
17113
17168
|
out.indicators = { manifest, natives };
|
|
17114
17169
|
}
|
|
17170
|
+
const ext = sanitizeExt(c.ext);
|
|
17171
|
+
if (ext) out.ext = ext;
|
|
17115
17172
|
return out;
|
|
17116
17173
|
}
|
|
17117
17174
|
function sanitizeSync(raw) {
|
|
@@ -18444,7 +18501,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18444
18501
|
this.emitScriptRun(id, cause, first);
|
|
18445
18502
|
},
|
|
18446
18503
|
onAlert: (a) => {
|
|
18447
|
-
|
|
18504
|
+
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
18505
|
+
this.events.emit("alert", { ...a, indicator });
|
|
18448
18506
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
18449
18507
|
},
|
|
18450
18508
|
onWarning: (w) => this.events.emit("warning", w),
|
|
@@ -18492,6 +18550,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18492
18550
|
overlay: d.overlay,
|
|
18493
18551
|
paneHint: d.paneHint,
|
|
18494
18552
|
native: { type: record.native.type },
|
|
18553
|
+
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
18495
18554
|
series: out.series ?? [],
|
|
18496
18555
|
fills: out.fills ?? [],
|
|
18497
18556
|
backgrounds: out.backgrounds ?? [],
|
|
@@ -19086,6 +19145,24 @@ var RendererControl = class {
|
|
|
19086
19145
|
this.renderer.setSettingsSections?.(sections);
|
|
19087
19146
|
return this;
|
|
19088
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
|
+
}
|
|
19089
19166
|
/** Tell the renderer's own chrome which size class the host shell is in —
|
|
19090
19167
|
* `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
|
|
19091
19168
|
* Silent no-op on a renderer without adaptive chrome. */
|
|
@@ -21468,658 +21545,6 @@ function supportsWebGL2() {
|
|
|
21468
21545
|
return webgl2Probe;
|
|
21469
21546
|
}
|
|
21470
21547
|
|
|
21471
|
-
// src/renderers/native/chrome/ticks.ts
|
|
21472
|
-
function priceTicks(min, max, target = 6) {
|
|
21473
|
-
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
21474
|
-
const raw = (max - min) / Math.max(1, target);
|
|
21475
|
-
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
21476
|
-
const norm = raw / mag;
|
|
21477
|
-
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
21478
|
-
const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
|
|
21479
|
-
const out = [];
|
|
21480
|
-
const start = Math.ceil(min / step) * step;
|
|
21481
|
-
for (let v = start; v <= max + step * 1e-6; v += step) {
|
|
21482
|
-
out.push(Number(v.toFixed(decimals)));
|
|
21483
|
-
}
|
|
21484
|
-
return out;
|
|
21485
|
-
}
|
|
21486
|
-
function priceDecimals(min, max, target = 6) {
|
|
21487
|
-
if (!(max > min)) return 2;
|
|
21488
|
-
const raw = (max - min) / Math.max(1, target);
|
|
21489
|
-
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
21490
|
-
const norm = raw / mag;
|
|
21491
|
-
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
21492
|
-
return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
|
|
21493
|
-
}
|
|
21494
|
-
function logPriceTicks(min, max, target = 6) {
|
|
21495
|
-
if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
21496
|
-
if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
|
|
21497
|
-
const out = [];
|
|
21498
|
-
const startExp = Math.floor(Math.log10(min));
|
|
21499
|
-
const endExp = Math.ceil(Math.log10(max));
|
|
21500
|
-
for (let e = startExp; e <= endExp; e += 1) {
|
|
21501
|
-
for (const m of [1, 2, 5]) {
|
|
21502
|
-
const v = m * Math.pow(10, e);
|
|
21503
|
-
if (v >= min && v <= max) out.push(v);
|
|
21504
|
-
}
|
|
21505
|
-
}
|
|
21506
|
-
return out;
|
|
21507
|
-
}
|
|
21508
|
-
function valueDecimals(v) {
|
|
21509
|
-
const a = Math.abs(v);
|
|
21510
|
-
if (a >= 100) return 0;
|
|
21511
|
-
if (a >= 1) return 2;
|
|
21512
|
-
if (a >= 0.01) return 4;
|
|
21513
|
-
return 6;
|
|
21514
|
-
}
|
|
21515
|
-
function tickDecimals(tick) {
|
|
21516
|
-
if (!(tick > 0) || !Number.isFinite(tick)) return 2;
|
|
21517
|
-
for (let d = 0; d <= 8; d += 1) {
|
|
21518
|
-
if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
|
|
21519
|
-
}
|
|
21520
|
-
return 8;
|
|
21521
|
-
}
|
|
21522
|
-
function axisDecimals(scale, heightPx, mintick) {
|
|
21523
|
-
const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
|
|
21524
|
-
return d === 0 ? 2 : d;
|
|
21525
|
-
}
|
|
21526
|
-
function tickCount(paneHeightPx) {
|
|
21527
|
-
return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
|
|
21528
|
-
}
|
|
21529
|
-
function paneTicks(scale, heightPx) {
|
|
21530
|
-
return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
|
|
21531
|
-
}
|
|
21532
|
-
function toPct(price, baseline) {
|
|
21533
|
-
return (price / baseline - 1) * 100;
|
|
21534
|
-
}
|
|
21535
|
-
function toIndex(price, baseline) {
|
|
21536
|
-
return price / baseline * 100;
|
|
21537
|
-
}
|
|
21538
|
-
function formatPct(pct) {
|
|
21539
|
-
const sign = pct >= 0 ? "+" : "-";
|
|
21540
|
-
return `${sign}${Math.abs(pct).toFixed(2)}%`;
|
|
21541
|
-
}
|
|
21542
|
-
function formatIndex(idx) {
|
|
21543
|
-
return idx.toFixed(2);
|
|
21544
|
-
}
|
|
21545
|
-
function formatCompactValue(v) {
|
|
21546
|
-
const a = Math.abs(v);
|
|
21547
|
-
if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
|
|
21548
|
-
if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
|
|
21549
|
-
if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
|
|
21550
|
-
return trimZeros(v);
|
|
21551
|
-
}
|
|
21552
|
-
function trimZeros(v) {
|
|
21553
|
-
return Number(v.toFixed(2)).toString();
|
|
21554
|
-
}
|
|
21555
|
-
function paneAxisTicks(scale, heightPx, pct, mintick, format) {
|
|
21556
|
-
if (pct) {
|
|
21557
|
-
const { baseline, indexed } = pct;
|
|
21558
|
-
const lo = Math.min(scale.min, scale.max);
|
|
21559
|
-
const hi = Math.max(scale.min, scale.max);
|
|
21560
|
-
if (indexed) {
|
|
21561
|
-
const iLo = toIndex(lo, baseline);
|
|
21562
|
-
const iHi = toIndex(hi, baseline);
|
|
21563
|
-
return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
|
|
21564
|
-
}
|
|
21565
|
-
const pLo = toPct(lo, baseline);
|
|
21566
|
-
const pHi = toPct(hi, baseline);
|
|
21567
|
-
return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
|
|
21568
|
-
}
|
|
21569
|
-
if (format === "volume") {
|
|
21570
|
-
return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
|
|
21571
|
-
}
|
|
21572
|
-
return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
|
|
21573
|
-
}
|
|
21574
|
-
function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
|
|
21575
|
-
if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
|
|
21576
|
-
if (format === "volume") return formatCompactValue(value);
|
|
21577
|
-
return formatPriceLabel(scale, heightPx, value, mintick);
|
|
21578
|
-
}
|
|
21579
|
-
function formatPriceLabel(scale, heightPx, value, mintick) {
|
|
21580
|
-
const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
|
|
21581
|
-
if (wideLog) {
|
|
21582
|
-
const d = valueDecimals(value);
|
|
21583
|
-
return value.toFixed(d === 0 ? 2 : d);
|
|
21584
|
-
}
|
|
21585
|
-
return value.toFixed(axisDecimals(scale, heightPx, mintick));
|
|
21586
|
-
}
|
|
21587
|
-
var SEC = 1e3;
|
|
21588
|
-
var MIN = 60 * SEC;
|
|
21589
|
-
var HOUR = 60 * MIN;
|
|
21590
|
-
var DAY = 24 * HOUR;
|
|
21591
|
-
var WEEK = 7 * DAY;
|
|
21592
|
-
var MONTH = 30 * DAY;
|
|
21593
|
-
var YEAR = 365 * DAY;
|
|
21594
|
-
var STEP_LADDER = [
|
|
21595
|
-
SEC,
|
|
21596
|
-
5 * SEC,
|
|
21597
|
-
15 * SEC,
|
|
21598
|
-
30 * SEC,
|
|
21599
|
-
MIN,
|
|
21600
|
-
5 * MIN,
|
|
21601
|
-
15 * MIN,
|
|
21602
|
-
30 * MIN,
|
|
21603
|
-
HOUR,
|
|
21604
|
-
2 * HOUR,
|
|
21605
|
-
4 * HOUR,
|
|
21606
|
-
6 * HOUR,
|
|
21607
|
-
12 * HOUR,
|
|
21608
|
-
DAY,
|
|
21609
|
-
2 * DAY,
|
|
21610
|
-
WEEK,
|
|
21611
|
-
MONTH,
|
|
21612
|
-
3 * MONTH,
|
|
21613
|
-
YEAR
|
|
21614
|
-
];
|
|
21615
|
-
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
21616
|
-
var pad2 = (n) => n < 10 ? `0${n}` : String(n);
|
|
21617
|
-
function pickStep(targetMs) {
|
|
21618
|
-
for (const step of STEP_LADDER) if (step >= targetMs) return step;
|
|
21619
|
-
return STEP_LADDER[STEP_LADDER.length - 1];
|
|
21620
|
-
}
|
|
21621
|
-
function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
21622
|
-
const span = toMs - fromMs;
|
|
21623
|
-
if (!(span > 0)) return [];
|
|
21624
|
-
const step = pickStep(span / Math.max(1, target));
|
|
21625
|
-
const zFrom = fromMs + offsetMs;
|
|
21626
|
-
const zTo = toMs + offsetMs;
|
|
21627
|
-
const first = Math.ceil(zFrom / step) * step;
|
|
21628
|
-
const out = [];
|
|
21629
|
-
for (let zt = first; zt <= zTo; zt += step) {
|
|
21630
|
-
const t = zt - offsetMs;
|
|
21631
|
-
const d = new Date(zt);
|
|
21632
|
-
let label;
|
|
21633
|
-
let major = false;
|
|
21634
|
-
if (step < DAY) {
|
|
21635
|
-
const h = d.getUTCHours();
|
|
21636
|
-
const m = d.getUTCMinutes();
|
|
21637
|
-
if (h === 0 && m === 0) {
|
|
21638
|
-
label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
|
|
21639
|
-
major = true;
|
|
21640
|
-
} else {
|
|
21641
|
-
label = `${pad2(h)}:${pad2(m)}`;
|
|
21642
|
-
}
|
|
21643
|
-
} else if (step < YEAR) {
|
|
21644
|
-
label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
|
|
21645
|
-
if (d.getUTCDate() === 1) {
|
|
21646
|
-
label = MONTHS[d.getUTCMonth()];
|
|
21647
|
-
major = true;
|
|
21648
|
-
}
|
|
21649
|
-
} else {
|
|
21650
|
-
label = String(d.getUTCFullYear());
|
|
21651
|
-
major = true;
|
|
21652
|
-
}
|
|
21653
|
-
out.push({ time: t, label, major });
|
|
21654
|
-
}
|
|
21655
|
-
return out;
|
|
21656
|
-
}
|
|
21657
|
-
|
|
21658
|
-
// src/renderers/shared/trade-markers.ts
|
|
21659
|
-
var TRADE_LONG_COLOR = TRADE_LONG;
|
|
21660
|
-
var TRADE_SHORT_COLOR = TRADE_SHORT;
|
|
21661
|
-
var TRADE_EXIT_COLOR = TRADE_EXIT;
|
|
21662
|
-
function defaultTradeMarkersState() {
|
|
21663
|
-
return {
|
|
21664
|
-
visible: true,
|
|
21665
|
-
labels: true,
|
|
21666
|
-
qty: true,
|
|
21667
|
-
colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
|
|
21668
|
-
};
|
|
21669
|
-
}
|
|
21670
|
-
function mergeTradeMarkersState(base, patch) {
|
|
21671
|
-
const p = patch && typeof patch === "object" ? patch : {};
|
|
21672
|
-
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
21673
|
-
const bool = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
21674
|
-
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
21675
|
-
return {
|
|
21676
|
-
visible: bool(p.visible, base.visible),
|
|
21677
|
-
labels: bool(p.labels, base.labels),
|
|
21678
|
-
qty: bool(p.qty, base.qty),
|
|
21679
|
-
colors: {
|
|
21680
|
-
long: color(c.long, base.colors.long),
|
|
21681
|
-
short: color(c.short, base.colors.short),
|
|
21682
|
-
exit: color(c.exit, base.colors.exit)
|
|
21683
|
-
}
|
|
21684
|
-
};
|
|
21685
|
-
}
|
|
21686
|
-
var BAR_GAP = 10;
|
|
21687
|
-
var ARROW_W = 9;
|
|
21688
|
-
var HEAD_H = 6;
|
|
21689
|
-
var ARROW_H = 14;
|
|
21690
|
-
var STEM_W = 3;
|
|
21691
|
-
var CAP_H = 2;
|
|
21692
|
-
var CAP_GAP = 2;
|
|
21693
|
-
var TEXT_GAP = 3;
|
|
21694
|
-
var UNIT_GAP = 6;
|
|
21695
|
-
var TICK_W = 6;
|
|
21696
|
-
var TICK_H = 8;
|
|
21697
|
-
function lineHeightOf(fontSize) {
|
|
21698
|
-
return fontSize + 4;
|
|
21699
|
-
}
|
|
21700
|
-
function qtyText(exec) {
|
|
21701
|
-
if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
|
|
21702
|
-
const magnitude = Number(Math.abs(exec.qty).toFixed(8));
|
|
21703
|
-
return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
|
|
21704
|
-
}
|
|
21705
|
-
function unitOf(exec, state, lineH) {
|
|
21706
|
-
const lines = [];
|
|
21707
|
-
if (state.labels && exec.label) lines.push(exec.label);
|
|
21708
|
-
if (state.qty) {
|
|
21709
|
-
const q = qtyText(exec);
|
|
21710
|
-
if (q) lines.push(q);
|
|
21711
|
-
}
|
|
21712
|
-
return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
|
|
21713
|
-
}
|
|
21714
|
-
function stacksFor(trades, state, deps, from, to, lineH) {
|
|
21715
|
-
const byBar = /* @__PURE__ */ new Map();
|
|
21716
|
-
for (const exec of trades) {
|
|
21717
|
-
const logical = Math.round(deps.timeToLogical(exec.time));
|
|
21718
|
-
if (logical < from || logical > to) continue;
|
|
21719
|
-
let stack = byBar.get(logical);
|
|
21720
|
-
if (!stack) {
|
|
21721
|
-
stack = { logical, buys: [], sells: [] };
|
|
21722
|
-
byBar.set(logical, stack);
|
|
21723
|
-
}
|
|
21724
|
-
(exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
|
|
21725
|
-
}
|
|
21726
|
-
return [...byBar.values()];
|
|
21727
|
-
}
|
|
21728
|
-
function stackExtent(units) {
|
|
21729
|
-
if (units.length === 0) return 0;
|
|
21730
|
-
let px = BAR_GAP;
|
|
21731
|
-
for (const u of units) px += u.height;
|
|
21732
|
-
return px + (units.length - 1) * UNIT_GAP;
|
|
21733
|
-
}
|
|
21734
|
-
function tradesPriceHints(trades, state, deps, from, to, fontSize) {
|
|
21735
|
-
if (trades.length === 0) return null;
|
|
21736
|
-
const lineH = lineHeightOf(fontSize);
|
|
21737
|
-
let min = Infinity;
|
|
21738
|
-
let max = -Infinity;
|
|
21739
|
-
let abovePx = 0;
|
|
21740
|
-
let belowPx = 0;
|
|
21741
|
-
for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
|
|
21742
|
-
const bar = deps.barAt(stack.logical);
|
|
21743
|
-
if (!bar) continue;
|
|
21744
|
-
if (bar.low < min) min = bar.low;
|
|
21745
|
-
if (bar.high > max) max = bar.high;
|
|
21746
|
-
belowPx = Math.max(belowPx, stackExtent(stack.buys));
|
|
21747
|
-
abovePx = Math.max(abovePx, stackExtent(stack.sells));
|
|
21748
|
-
}
|
|
21749
|
-
if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
|
|
21750
|
-
return { min, max, abovePx, belowPx };
|
|
21751
|
-
}
|
|
21752
|
-
function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
|
|
21753
|
-
if (trades.length === 0) return;
|
|
21754
|
-
const lineH = lineHeightOf(text.fontSize);
|
|
21755
|
-
const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
|
|
21756
|
-
if (stacks.length === 0) return;
|
|
21757
|
-
ctx.save();
|
|
21758
|
-
ctx.font = `${text.fontSize}px ${text.fontFamily}`;
|
|
21759
|
-
ctx.textAlign = "center";
|
|
21760
|
-
ctx.textBaseline = "middle";
|
|
21761
|
-
for (const stack of stacks) {
|
|
21762
|
-
const x = xOf(stack.logical);
|
|
21763
|
-
if (x < -150 || x > width + 150) continue;
|
|
21764
|
-
const bar = deps.barAt(stack.logical);
|
|
21765
|
-
if (!bar) continue;
|
|
21766
|
-
const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
|
|
21767
|
-
const yTop = Math.min(yOf(bar.low), yOf(bar.high));
|
|
21768
|
-
let y = yBottom + BAR_GAP;
|
|
21769
|
-
for (const unit of stack.buys) {
|
|
21770
|
-
ctx.fillStyle = colorOf(unit.exec, state.colors);
|
|
21771
|
-
drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
|
|
21772
|
-
drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
|
|
21773
|
-
drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
|
|
21774
|
-
y += unit.height + UNIT_GAP;
|
|
21775
|
-
}
|
|
21776
|
-
y = yTop - BAR_GAP;
|
|
21777
|
-
for (const unit of stack.sells) {
|
|
21778
|
-
ctx.fillStyle = colorOf(unit.exec, state.colors);
|
|
21779
|
-
drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
|
|
21780
|
-
drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
|
|
21781
|
-
drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
|
|
21782
|
-
y -= unit.height + UNIT_GAP;
|
|
21783
|
-
}
|
|
21784
|
-
}
|
|
21785
|
-
ctx.restore();
|
|
21786
|
-
}
|
|
21787
|
-
function colorOf(exec, colors) {
|
|
21788
|
-
if (exec.kind === "exit") return colors.exit;
|
|
21789
|
-
return exec.side === "buy" ? colors.long : colors.short;
|
|
21790
|
-
}
|
|
21791
|
-
function drawArrowUp(ctx, x, yTip, capped) {
|
|
21792
|
-
ctx.beginPath();
|
|
21793
|
-
ctx.moveTo(x, yTip);
|
|
21794
|
-
ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
|
|
21795
|
-
ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
|
|
21796
|
-
ctx.closePath();
|
|
21797
|
-
ctx.fill();
|
|
21798
|
-
ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
|
|
21799
|
-
if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
|
|
21800
|
-
}
|
|
21801
|
-
function drawArrowDown(ctx, x, yTip, capped) {
|
|
21802
|
-
ctx.beginPath();
|
|
21803
|
-
ctx.moveTo(x, yTip);
|
|
21804
|
-
ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
|
|
21805
|
-
ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
|
|
21806
|
-
ctx.closePath();
|
|
21807
|
-
ctx.fill();
|
|
21808
|
-
ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
|
|
21809
|
-
if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
|
|
21810
|
-
}
|
|
21811
|
-
function drawFillTick(ctx, side, x, yFill, barHalfPx) {
|
|
21812
|
-
const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
|
|
21813
|
-
const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
|
|
21814
|
-
ctx.beginPath();
|
|
21815
|
-
ctx.moveTo(edge, yFill);
|
|
21816
|
-
ctx.lineTo(back, yFill - TICK_H / 2);
|
|
21817
|
-
ctx.lineTo(back, yFill + TICK_H / 2);
|
|
21818
|
-
ctx.closePath();
|
|
21819
|
-
ctx.fill();
|
|
21820
|
-
}
|
|
21821
|
-
function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
21822
|
-
if (lines.length === 0) return;
|
|
21823
|
-
ctx.fillStyle = color;
|
|
21824
|
-
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
21825
|
-
}
|
|
21826
|
-
|
|
21827
|
-
// src/renderers/native/core/SceneGraph.ts
|
|
21828
|
-
var SceneGraph = class {
|
|
21829
|
-
constructor() {
|
|
21830
|
-
this.panes = /* @__PURE__ */ new Map();
|
|
21831
|
-
this.indicators = /* @__PURE__ */ new Map();
|
|
21832
|
-
this.bars = [];
|
|
21833
|
-
/** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
|
|
21834
|
-
this.volumeLayer = null;
|
|
21835
|
-
/** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
|
|
21836
|
-
this.nativeData = /* @__PURE__ */ new Map();
|
|
21837
|
-
/** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
|
|
21838
|
-
this.nativePending = /* @__PURE__ */ new Map();
|
|
21839
|
-
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
21840
|
-
this.vpvrLayer = null;
|
|
21841
|
-
this.crosshair = null;
|
|
21842
|
-
/** How the base price series is drawn on the price pane (candles by default). */
|
|
21843
|
-
this.priceStyle = "candles";
|
|
21844
|
-
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
21845
|
-
this.basePainting = "candles";
|
|
21846
|
-
/** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
|
|
21847
|
-
* candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
|
|
21848
|
-
this.candleOverride = null;
|
|
21849
|
-
/** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
|
|
21850
|
-
* baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
|
|
21851
|
-
* range (resolved per frame via `baselinePriceFor`). */
|
|
21852
|
-
this.baselineValue = null;
|
|
21853
|
-
/** Draw the dashed horizontal line at the latest price (price pane). Independent
|
|
21854
|
-
* of the axis label chip (`showPriceLabel`) — either can show without the other. */
|
|
21855
|
-
this.showPriceLine = true;
|
|
21856
|
-
/** Draw the last-price label chip on the price axis. Independent of the line. */
|
|
21857
|
-
this.showPriceLabel = true;
|
|
21858
|
-
/** Draw the countdown-to-bar-close chip on the price axis. When the price label is
|
|
21859
|
-
* also shown, the two merge into one stacked block (countdown under the label);
|
|
21860
|
-
* when either shows alone it's centered on the latest price level. */
|
|
21861
|
-
this.showCountdown = true;
|
|
21862
|
-
/** Logarithmic price scale on the price pane. */
|
|
21863
|
-
this.logScale = false;
|
|
21864
|
-
/** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
|
|
21865
|
-
this.invertScale = false;
|
|
21866
|
-
/** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
|
|
21867
|
-
* price-axis decimals — the instrument's true precision instead of the zoom-derived
|
|
21868
|
-
* formula. Undefined until symbol metadata loads (the formula is the fallback). */
|
|
21869
|
-
this.priceMintick = void 0;
|
|
21870
|
-
/** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
|
|
21871
|
-
* vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
|
|
21872
|
-
this.scaleMode = "price";
|
|
21873
|
-
/** Reference price for percent mode (first visible bar's close); recomputed per frame. */
|
|
21874
|
-
this.percentBaseline = 0;
|
|
21875
|
-
/** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
|
|
21876
|
-
this.timezone = "UTC";
|
|
21877
|
-
/** Draw the background gridlines (price + time). Master toggle (`gridlines`
|
|
21878
|
-
* feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
|
|
21879
|
-
this.showGrid = true;
|
|
21880
|
-
/** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
|
|
21881
|
-
* border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
|
|
21882
|
-
* `applyConfig()`; every draw layer reads its knobs from here, falling back to
|
|
21883
|
-
* the theme for any value left at its inherit default. */
|
|
21884
|
-
this.style = defaultChartStyle();
|
|
21885
|
-
/** Draw the price/time axis tick labels. */
|
|
21886
|
-
this.showAxisLabels = true;
|
|
21887
|
-
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
21888
|
-
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
21889
|
-
this.tradeMarkers = defaultTradeMarkersState();
|
|
21890
|
-
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
21891
|
-
this.highlights = [];
|
|
21892
|
-
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
21893
|
-
this.sessionZones = null;
|
|
21894
|
-
/** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
|
|
21895
|
-
* Indicators with z below this draw BEHIND the candles; at/above draw in front.
|
|
21896
|
-
* Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
|
|
21897
|
-
* and user drawings (z ≥ 1 by default) on top of the price. */
|
|
21898
|
-
this.candleZ = 0;
|
|
21899
|
-
/** Hide the base price series (candles/bars/line/area) without removing it — overlay
|
|
21900
|
-
* indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
|
|
21901
|
-
this.candlesHidden = false;
|
|
21902
|
-
/** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
|
|
21903
|
-
* Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
|
|
21904
|
-
* so each indicator arrives behind the candles (and behind older indicators);
|
|
21905
|
-
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
21906
|
-
this.seriesZ = /* @__PURE__ */ new Map();
|
|
21907
|
-
/** Per-pane raster layers of user drawings interleaved into the series stack — each is a
|
|
21908
|
-
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
21909
|
-
* Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
|
|
21910
|
-
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
21911
|
-
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
21912
|
-
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
21913
|
-
* bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
|
|
21914
|
-
* the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
|
|
21915
|
-
this.anchorOffsets = /* @__PURE__ */ new Map();
|
|
21916
|
-
/** Per-indicator private price windows (merged indicators drawn on their own scale
|
|
21917
|
-
* column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
|
|
21918
|
-
* shares its pane's master scale. */
|
|
21919
|
-
this.indicatorScales = /* @__PURE__ */ new Map();
|
|
21920
|
-
/** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
|
|
21921
|
-
this.orderedCache = null;
|
|
21922
|
-
}
|
|
21923
|
-
/** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
|
|
21924
|
-
orderedPanes() {
|
|
21925
|
-
if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
|
|
21926
|
-
return this.orderedCache;
|
|
21927
|
-
}
|
|
21928
|
-
/** The session zones resolved into colored bands (pre/post-market washes from the
|
|
21929
|
-
* config's session colors) — consumed by the same painting path as {@link highlights}. */
|
|
21930
|
-
sessionHighlightBands() {
|
|
21931
|
-
if (!this.sessionZones) return [];
|
|
21932
|
-
const out = [];
|
|
21933
|
-
for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
|
|
21934
|
-
for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
|
|
21935
|
-
return out;
|
|
21936
|
-
}
|
|
21937
|
-
indicatorsForPane(paneId) {
|
|
21938
|
-
const out = [];
|
|
21939
|
-
for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
|
|
21940
|
-
return out;
|
|
21941
|
-
}
|
|
21942
|
-
/** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
|
|
21943
|
-
ownScaleIndicatorsForPane(paneId) {
|
|
21944
|
-
return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
|
|
21945
|
-
}
|
|
21946
|
-
/** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
|
|
21947
|
-
ensureIndicatorScale(id, seed) {
|
|
21948
|
-
let s = this.indicatorScales.get(id);
|
|
21949
|
-
if (!s) {
|
|
21950
|
-
const base = seed ?? { min: 0, max: 1 };
|
|
21951
|
-
s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
|
|
21952
|
-
this.indicatorScales.set(id, s);
|
|
21953
|
-
}
|
|
21954
|
-
return s;
|
|
21955
|
-
}
|
|
21956
|
-
dropIndicatorScale(id) {
|
|
21957
|
-
this.indicatorScales.delete(id);
|
|
21958
|
-
}
|
|
21959
|
-
/** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
|
|
21960
|
-
scaleFor(model, pane) {
|
|
21961
|
-
if (model.ownScale === true) {
|
|
21962
|
-
const s = this.indicatorScales.get(model.id);
|
|
21963
|
-
if (s) return s.scale;
|
|
21964
|
-
}
|
|
21965
|
-
return pane.scale;
|
|
21966
|
-
}
|
|
21967
|
-
/** Apply a new top-to-bottom pane order (ids not present are ignored). */
|
|
21968
|
-
orderPanes(orderedIds) {
|
|
21969
|
-
orderedIds.forEach((id, i) => {
|
|
21970
|
-
const pane = this.panes.get(id);
|
|
21971
|
-
if (pane) pane.order = i;
|
|
21972
|
-
});
|
|
21973
|
-
this.orderedCache = null;
|
|
21974
|
-
}
|
|
21975
|
-
/** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
|
|
21976
|
-
* so equal-z models keep their insertion order (the default). */
|
|
21977
|
-
orderedIndicatorsForPane(paneId) {
|
|
21978
|
-
return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
|
|
21979
|
-
}
|
|
21980
|
-
/** The foreground draw-order key of an indicator (0 when never assigned). */
|
|
21981
|
-
zOf(id) {
|
|
21982
|
-
return this.seriesZ.get(id) ?? 0;
|
|
21983
|
-
}
|
|
21984
|
-
/** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
|
|
21985
|
-
offsetOf(id) {
|
|
21986
|
-
return this.anchorOffsets.get(id) ?? 0;
|
|
21987
|
-
}
|
|
21988
|
-
/** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
|
|
21989
|
-
* over a suffix) — readers skip its leading chart bars. Negative: the model starts
|
|
21990
|
-
* BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
|
|
21991
|
-
* model's own leading points, `points[i - off]` reaching further in. Storing only the
|
|
21992
|
-
* positive case silently pinned such a model at index 0, i.e. drew it shifted. */
|
|
21993
|
-
setAnchorOffset(id, offset) {
|
|
21994
|
-
if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
|
|
21995
|
-
else this.anchorOffsets.delete(id);
|
|
21996
|
-
}
|
|
21997
|
-
forgetAnchorOffset(id) {
|
|
21998
|
-
this.anchorOffsets.delete(id);
|
|
21999
|
-
}
|
|
22000
|
-
/** Resolve the baseline reference price for the given pane window: the explicit
|
|
22001
|
-
* `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
|
|
22002
|
-
* at that fraction of the pane height. Interpolated in the same space the pane renders
|
|
22003
|
-
* in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
|
|
22004
|
-
* height — matching `CoordinateSystem.yToPrice`. */
|
|
22005
|
-
baselinePriceFor(scale) {
|
|
22006
|
-
if (this.baselineValue != null) return this.baselineValue;
|
|
22007
|
-
const t = this.style.baseline.baselineLevel / 100;
|
|
22008
|
-
if (scale.log && scale.min > 0 && scale.max > scale.min) {
|
|
22009
|
-
const lo = Math.log(scale.min);
|
|
22010
|
-
return Math.exp(lo + t * (Math.log(scale.max) - lo));
|
|
22011
|
-
}
|
|
22012
|
-
return scale.min + (scale.max - scale.min) * t;
|
|
22013
|
-
}
|
|
22014
|
-
/** Assign a default z on mount: the current bottom of the stack, so a new indicator
|
|
22015
|
-
* paints behind the candles and behind every indicator already there — the price stays
|
|
22016
|
-
* the top of the pile until the user restacks it. No-op if the indicator already has one. */
|
|
22017
|
-
assignIndicatorZ(id) {
|
|
22018
|
-
if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
|
|
22019
|
-
}
|
|
22020
|
-
/** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
|
|
22021
|
-
* data canvas by default): top of the stack, so the recorded order tells the truth from
|
|
22022
|
-
* the first frame. Keeps an existing key, so a restored stack survives the remount. */
|
|
22023
|
-
assignIndicatorZTop(id) {
|
|
22024
|
-
if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
|
|
22025
|
-
}
|
|
22026
|
-
forgetIndicatorZ(id) {
|
|
22027
|
-
this.seriesZ.delete(id);
|
|
22028
|
-
}
|
|
22029
|
-
setIndicatorZ(id, z) {
|
|
22030
|
-
this.seriesZ.set(id, z);
|
|
22031
|
-
}
|
|
22032
|
-
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
|
|
22033
|
-
indicatorZOrder() {
|
|
22034
|
-
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
|
|
22035
|
-
}
|
|
22036
|
-
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
22037
|
-
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
22038
|
-
seriesBoundaries(paneId) {
|
|
22039
|
-
const keys = /* @__PURE__ */ new Set();
|
|
22040
|
-
if (paneId === "price") keys.add(this.candleZ);
|
|
22041
|
-
for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
|
|
22042
|
-
return [...keys].sort((a, b) => a - b);
|
|
22043
|
-
}
|
|
22044
|
-
/** Raise an indicator above every other layer (other indicators AND the candles). */
|
|
22045
|
-
bringIndicatorToFront(id) {
|
|
22046
|
-
this.seriesZ.set(id, this.topZ() + 1);
|
|
22047
|
-
}
|
|
22048
|
-
/** Drop an indicator below every other layer (other indicators AND the candles). */
|
|
22049
|
-
sendIndicatorToBack(id) {
|
|
22050
|
-
this.seriesZ.set(id, this.bottomZ() - 1);
|
|
22051
|
-
}
|
|
22052
|
-
topZ() {
|
|
22053
|
-
let max = this.candleZ;
|
|
22054
|
-
for (const z of this.seriesZ.values()) if (z > max) max = z;
|
|
22055
|
-
return max;
|
|
22056
|
-
}
|
|
22057
|
-
bottomZ() {
|
|
22058
|
-
let min = this.candleZ;
|
|
22059
|
-
for (const z of this.seriesZ.values()) if (z < min) min = z;
|
|
22060
|
-
return min;
|
|
22061
|
-
}
|
|
22062
|
-
ensurePane(id, kind, order, heightWeight) {
|
|
22063
|
-
this.orderedCache = null;
|
|
22064
|
-
const existing = this.panes.get(id);
|
|
22065
|
-
if (existing) {
|
|
22066
|
-
existing.order = order;
|
|
22067
|
-
existing.heightWeight = heightWeight;
|
|
22068
|
-
existing.kind = kind;
|
|
22069
|
-
return existing;
|
|
22070
|
-
}
|
|
22071
|
-
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 };
|
|
22072
|
-
this.panes.set(id, pane);
|
|
22073
|
-
return pane;
|
|
22074
|
-
}
|
|
22075
|
-
removePane(id) {
|
|
22076
|
-
this.panes.delete(id);
|
|
22077
|
-
this.orderedCache = null;
|
|
22078
|
-
}
|
|
22079
|
-
};
|
|
22080
|
-
function paneScaleMode(scene, pane) {
|
|
22081
|
-
return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
|
|
22082
|
-
}
|
|
22083
|
-
function paneLogScale(scene, pane) {
|
|
22084
|
-
return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
|
|
22085
|
-
}
|
|
22086
|
-
function paneInvert(scene, pane) {
|
|
22087
|
-
return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
|
|
22088
|
-
}
|
|
22089
|
-
function percentScaleFor(scene, pane) {
|
|
22090
|
-
const mode = paneScaleMode(scene, pane);
|
|
22091
|
-
if (mode !== "percent" && mode !== "indexed") return void 0;
|
|
22092
|
-
const baseline = pane.percentBaseline;
|
|
22093
|
-
if (!Number.isFinite(baseline) || baseline === 0) return void 0;
|
|
22094
|
-
return { baseline, indexed: mode === "indexed" };
|
|
22095
|
-
}
|
|
22096
|
-
|
|
22097
|
-
// src/renderers/native/chrome/tz.ts
|
|
22098
|
-
function tzOffsetMs(ms, timeZone) {
|
|
22099
|
-
if (!timeZone || timeZone === "UTC") return 0;
|
|
22100
|
-
try {
|
|
22101
|
-
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
22102
|
-
timeZone,
|
|
22103
|
-
hourCycle: "h23",
|
|
22104
|
-
year: "numeric",
|
|
22105
|
-
month: "2-digit",
|
|
22106
|
-
day: "2-digit",
|
|
22107
|
-
hour: "2-digit",
|
|
22108
|
-
minute: "2-digit",
|
|
22109
|
-
second: "2-digit"
|
|
22110
|
-
});
|
|
22111
|
-
const parts = dtf.formatToParts(new Date(ms));
|
|
22112
|
-
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
22113
|
-
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
22114
|
-
return asUTC - ms;
|
|
22115
|
-
} catch {
|
|
22116
|
-
return 0;
|
|
22117
|
-
}
|
|
22118
|
-
}
|
|
22119
|
-
function zonedDate(ms, timeZone) {
|
|
22120
|
-
return new Date(ms + tzOffsetMs(ms, timeZone));
|
|
22121
|
-
}
|
|
22122
|
-
|
|
22123
21548
|
// src/renderers/native/backend/candle-lod.ts
|
|
22124
21549
|
var CANDLE_BODY_MIN_SPACING = 3;
|
|
22125
21550
|
var CANDLE_WICK_W = 1.5;
|
|
@@ -22409,11 +21834,6 @@ var DASH = {
|
|
|
22409
21834
|
dashed: [6, 4],
|
|
22410
21835
|
dotted: [2, 3]
|
|
22411
21836
|
};
|
|
22412
|
-
function crispHairline(cssCenter, dpr) {
|
|
22413
|
-
const w = Math.max(1, Math.round(dpr));
|
|
22414
|
-
const edge = Math.round(cssCenter * dpr - w / 2);
|
|
22415
|
-
return { pos: edge / dpr, size: w / dpr };
|
|
22416
|
-
}
|
|
22417
21837
|
function joinSegments(hw) {
|
|
22418
21838
|
return Math.max(6, Math.min(20, Math.round(hw * 4)));
|
|
22419
21839
|
}
|
|
@@ -22423,7 +21843,6 @@ var WebGL2Backend = class {
|
|
|
22423
21843
|
this.modelAlpha = 1;
|
|
22424
21844
|
this.candleBodyAlpha = 1;
|
|
22425
21845
|
this.candleStructureAlpha = 1;
|
|
22426
|
-
this.gridAlpha = 1;
|
|
22427
21846
|
this.candleBodyScale = 1;
|
|
22428
21847
|
/** Set by the renderer; invoked after a context restore to request a repaint. */
|
|
22429
21848
|
this.onNeedsRedraw = null;
|
|
@@ -22653,11 +22072,7 @@ var WebGL2Backend = class {
|
|
|
22653
22072
|
}
|
|
22654
22073
|
};
|
|
22655
22074
|
b.alpha = 1;
|
|
22656
|
-
if (pane.collapsed) {
|
|
22657
|
-
this.emitGrid(b, coords, theme, dataW, pane, scene);
|
|
22658
|
-
} else {
|
|
22659
|
-
this.emitHighlights(b, scene, pane, coords);
|
|
22660
|
-
this.emitGrid(b, coords, theme, dataW, pane, scene);
|
|
22075
|
+
if (!pane.collapsed) {
|
|
22661
22076
|
const models = scene.orderedIndicatorsForPane(pane.id);
|
|
22662
22077
|
const isPrice = pane.kind === "price";
|
|
22663
22078
|
const effPane = (m) => {
|
|
@@ -22842,49 +22257,6 @@ var WebGL2Backend = class {
|
|
|
22842
22257
|
this.screenProgram = null;
|
|
22843
22258
|
}
|
|
22844
22259
|
// ── geometry emit (mirrors Canvas2dBackend, in CSS-px space) ──
|
|
22845
|
-
emitGrid(b, coords, theme, dataW, pane, scene) {
|
|
22846
|
-
const top = pane.bounds.top;
|
|
22847
|
-
const bot = pane.bounds.top + pane.bounds.height;
|
|
22848
|
-
const { gridVert, gridHorz } = scene.style;
|
|
22849
|
-
const dpr = coords.dpr;
|
|
22850
|
-
b.alpha = this.gridAlpha;
|
|
22851
|
-
if (scene.showGrid && gridVert.visible && !pane.collapsed) {
|
|
22852
|
-
const grid = parseColor(gridVert.color ?? theme.gridColor);
|
|
22853
|
-
const tr = coords.visibleTimeRange();
|
|
22854
|
-
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
22855
|
-
for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
|
|
22856
|
-
const x = coords.timeToX(tick.time);
|
|
22857
|
-
if (x < 0 || x > dataW) continue;
|
|
22858
|
-
const g = crispHairline(x, dpr);
|
|
22859
|
-
b.rect(g.pos, top, g.size, bot - top, grid);
|
|
22860
|
-
}
|
|
22861
|
-
}
|
|
22862
|
-
if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
|
|
22863
|
-
const grid = parseColor(gridHorz.color ?? theme.gridColor);
|
|
22864
|
-
const pct = percentScaleFor(scene, pane);
|
|
22865
|
-
for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
|
|
22866
|
-
const y = coords.priceToY(t.price, pane.scale, pane.bounds);
|
|
22867
|
-
if (y < top || y > bot) continue;
|
|
22868
|
-
const g = crispHairline(y, dpr);
|
|
22869
|
-
b.rect(0, g.pos, dataW, g.size, grid);
|
|
22870
|
-
}
|
|
22871
|
-
}
|
|
22872
|
-
}
|
|
22873
|
-
/** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height).
|
|
22874
|
-
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
22875
|
-
emitHighlights(b, scene, pane, coords) {
|
|
22876
|
-
const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
|
|
22877
|
-
if (bands.length === 0) return;
|
|
22878
|
-
for (const band of bands) {
|
|
22879
|
-
const x1 = coords.timeToX(band.from);
|
|
22880
|
-
const x2 = coords.timeToX(band.to);
|
|
22881
|
-
if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
|
|
22882
|
-
const cx = Math.max(0, x1);
|
|
22883
|
-
const cw = Math.min(coords.width, x2) - cx;
|
|
22884
|
-
if (cw <= 0) continue;
|
|
22885
|
-
b.rect(cx, pane.bounds.top, cw, pane.bounds.height, parseColor(band.color));
|
|
22886
|
-
}
|
|
22887
|
-
}
|
|
22888
22260
|
emitBackground(b, bg, pane, coords) {
|
|
22889
22261
|
const x1 = coords.timeToX(bg.from);
|
|
22890
22262
|
const x2 = coords.timeToX(bg.to);
|
|
@@ -24207,6 +23579,445 @@ var KeyboardController = class {
|
|
|
24207
23579
|
}
|
|
24208
23580
|
};
|
|
24209
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
|
+
|
|
24210
24021
|
// src/renderers/native/backend/Canvas2dBackend.ts
|
|
24211
24022
|
var Canvas2dBackend = class {
|
|
24212
24023
|
constructor() {
|
|
@@ -24214,7 +24025,6 @@ var Canvas2dBackend = class {
|
|
|
24214
24025
|
this.modelAlpha = 1;
|
|
24215
24026
|
this.candleBodyAlpha = 1;
|
|
24216
24027
|
this.candleStructureAlpha = 1;
|
|
24217
|
-
this.gridAlpha = 1;
|
|
24218
24028
|
this.candleBodyScale = 1;
|
|
24219
24029
|
this.canvas = null;
|
|
24220
24030
|
this.ctx = null;
|
|
@@ -24241,8 +24051,6 @@ var Canvas2dBackend = class {
|
|
|
24241
24051
|
if (i1 < i0) return;
|
|
24242
24052
|
const barColorMap = mergeBarColors2(scene.indicators);
|
|
24243
24053
|
const panes = scene.orderedPanes();
|
|
24244
|
-
this.drawHighlights(ctx, scene, coords);
|
|
24245
|
-
this.drawGrid(ctx, scene, coords, theme, dataW);
|
|
24246
24054
|
for (const pane of panes) {
|
|
24247
24055
|
if (pane.collapsed) continue;
|
|
24248
24056
|
const models = scene.orderedIndicatorsForPane(pane.id);
|
|
@@ -24849,22 +24657,6 @@ var Canvas2dBackend = class {
|
|
|
24849
24657
|
ctx.fillStyle = bg.color;
|
|
24850
24658
|
ctx.fillRect(x1, pane.bounds.top, x2 - x1, pane.bounds.height);
|
|
24851
24659
|
}
|
|
24852
|
-
/** Renderer-owned session highlight bands: full-height (all panes), behind grid + data.
|
|
24853
|
-
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
24854
|
-
drawHighlights(ctx, scene, coords) {
|
|
24855
|
-
const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
|
|
24856
|
-
if (bands.length === 0) return;
|
|
24857
|
-
for (const band of bands) {
|
|
24858
|
-
const x1 = coords.timeToX(band.from);
|
|
24859
|
-
const x2 = coords.timeToX(band.to);
|
|
24860
|
-
if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
|
|
24861
|
-
const cx = Math.max(0, x1);
|
|
24862
|
-
const cw = Math.min(coords.width, x2) - cx;
|
|
24863
|
-
if (cw <= 0) continue;
|
|
24864
|
-
ctx.fillStyle = band.color;
|
|
24865
|
-
ctx.fillRect(cx, 0, cw, coords.height);
|
|
24866
|
-
}
|
|
24867
|
-
}
|
|
24868
24660
|
drawHline(ctx, pl, pane, coords, dataW, theme) {
|
|
24869
24661
|
const y = Math.round(coords.priceToY(pl.price, pane.scale, pane.bounds)) + 0.5;
|
|
24870
24662
|
if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) return;
|
|
@@ -24877,46 +24669,6 @@ var Canvas2dBackend = class {
|
|
|
24877
24669
|
ctx.stroke();
|
|
24878
24670
|
setDash(ctx, "solid");
|
|
24879
24671
|
}
|
|
24880
|
-
// ── grid (L0, behind data) ── vert/horz gate on `scene.showGrid` AND their own
|
|
24881
|
-
// per-axis visibility (style); each uses its own color. Pane separators are drawn on the
|
|
24882
|
-
// chrome layer (full-width, above the data) so series never overpaint them.
|
|
24883
|
-
drawGrid(ctx, scene, coords, theme, dataW) {
|
|
24884
|
-
const panes = scene.orderedPanes();
|
|
24885
|
-
const { gridVert, gridHorz } = scene.style;
|
|
24886
|
-
const vertColor = gridVert.color ?? theme.gridColor;
|
|
24887
|
-
const horzColor = gridHorz.color ?? theme.gridColor;
|
|
24888
|
-
ctx.lineWidth = 1;
|
|
24889
|
-
if (scene.showGrid && gridVert.visible) {
|
|
24890
|
-
ctx.globalAlpha = this.gridAlpha;
|
|
24891
|
-
ctx.strokeStyle = vertColor;
|
|
24892
|
-
const tr = coords.visibleTimeRange();
|
|
24893
|
-
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
24894
|
-
ctx.beginPath();
|
|
24895
|
-
for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
|
|
24896
|
-
const x = Math.round(coords.timeToX(tick.time)) + 0.5;
|
|
24897
|
-
if (x < 0 || x > dataW) continue;
|
|
24898
|
-
ctx.moveTo(x, 0);
|
|
24899
|
-
ctx.lineTo(x, coords.height);
|
|
24900
|
-
}
|
|
24901
|
-
ctx.stroke();
|
|
24902
|
-
}
|
|
24903
|
-
for (const pane of panes) {
|
|
24904
|
-
if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
|
|
24905
|
-
ctx.globalAlpha = this.gridAlpha;
|
|
24906
|
-
ctx.strokeStyle = horzColor;
|
|
24907
|
-
const pct = percentScaleFor(scene, pane);
|
|
24908
|
-
ctx.beginPath();
|
|
24909
|
-
for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct)) {
|
|
24910
|
-
const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
|
|
24911
|
-
if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
|
|
24912
|
-
ctx.moveTo(0, y);
|
|
24913
|
-
ctx.lineTo(dataW, y);
|
|
24914
|
-
}
|
|
24915
|
-
ctx.stroke();
|
|
24916
|
-
}
|
|
24917
|
-
}
|
|
24918
|
-
ctx.globalAlpha = 1;
|
|
24919
|
-
}
|
|
24920
24672
|
};
|
|
24921
24673
|
function setDash(ctx, style) {
|
|
24922
24674
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
@@ -25628,6 +25380,221 @@ var DrawingSceneRenderer = class {
|
|
|
25628
25380
|
}
|
|
25629
25381
|
};
|
|
25630
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
|
+
|
|
25631
25598
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
25632
25599
|
var ChromeRenderer = class {
|
|
25633
25600
|
constructor() {
|
|
@@ -25807,6 +25774,13 @@ var ChromeRenderer = class {
|
|
|
25807
25774
|
if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
|
|
25808
25775
|
ctx.fillText(t.label, dataW + 6, y);
|
|
25809
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
|
+
}
|
|
25810
25784
|
}
|
|
25811
25785
|
ctx.textAlign = "start";
|
|
25812
25786
|
}
|
|
@@ -26048,7 +26022,7 @@ var CrosshairRenderer = class {
|
|
|
26048
26022
|
}
|
|
26049
26023
|
}
|
|
26050
26024
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
26051
|
-
if (pane) {
|
|
26025
|
+
if (pane && pane.axisFormat !== "none") {
|
|
26052
26026
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
26053
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);
|
|
26054
26028
|
}
|
|
@@ -26096,7 +26070,7 @@ var CrosshairRenderer = class {
|
|
|
26096
26070
|
break;
|
|
26097
26071
|
}
|
|
26098
26072
|
}
|
|
26099
|
-
if (pane) {
|
|
26073
|
+
if (pane && pane.axisFormat !== "none") {
|
|
26100
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);
|
|
26101
26075
|
}
|
|
26102
26076
|
}
|
|
@@ -26139,6 +26113,156 @@ function formatStamp(ms, timeZone) {
|
|
|
26139
26113
|
return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
26140
26114
|
}
|
|
26141
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
|
+
|
|
26142
26266
|
// src/renderers/native/chrome/SettingsDialog.ts
|
|
26143
26267
|
var BUILTIN_STYLE_LABELS2 = {
|
|
26144
26268
|
candles: "Candles",
|
|
@@ -26257,12 +26381,21 @@ var SettingsDialog = class {
|
|
|
26257
26381
|
this.activeSection = null;
|
|
26258
26382
|
/** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
|
|
26259
26383
|
this.mobileLayout = false;
|
|
26384
|
+
/** The visibility policy: setting ids hidden by the host (subtree semantics). */
|
|
26385
|
+
this.hiddenSettings = /* @__PURE__ */ new Set();
|
|
26260
26386
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
26261
26387
|
}
|
|
26262
26388
|
/** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
|
|
26263
26389
|
setHostSections(sections) {
|
|
26264
26390
|
this.hostSections = sections;
|
|
26265
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
|
+
}
|
|
26266
26399
|
/** Configure the Canvas → Theme row (see {@link themeControl}); null hides the row. */
|
|
26267
26400
|
setThemeControl(current, onSelect) {
|
|
26268
26401
|
this.themeControl = { current, onSelect };
|
|
@@ -26330,8 +26463,12 @@ var SettingsDialog = class {
|
|
|
26330
26463
|
}
|
|
26331
26464
|
const body = document.createElement("div");
|
|
26332
26465
|
body.style.cssText = "display:flex;flex-direction:column;gap:0;";
|
|
26333
|
-
|
|
26334
|
-
|
|
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"));
|
|
26335
26472
|
const groups = {};
|
|
26336
26473
|
const showActive = (style) => {
|
|
26337
26474
|
const active = style === "heikinashi" ? "candles" : style;
|
|
@@ -26340,64 +26477,64 @@ var SettingsDialog = class {
|
|
|
26340
26477
|
}
|
|
26341
26478
|
};
|
|
26342
26479
|
body.append(
|
|
26343
|
-
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) => {
|
|
26344
26481
|
this.emit({ series: { style: v } });
|
|
26345
26482
|
showActive(v);
|
|
26346
26483
|
this.syncTypeTabs?.(v);
|
|
26347
|
-
})
|
|
26484
|
+
}), "symbol.type")
|
|
26348
26485
|
);
|
|
26349
|
-
const candles = this.group();
|
|
26486
|
+
const candles = sid(this.group(), "symbol.style.candles");
|
|
26350
26487
|
candles.append(this.sectionTitle("Candles"));
|
|
26351
|
-
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 } }), [
|
|
26352
26489
|
this.swatch(config.candles.upColor, (v) => this.emit({ candles: { upColor: v } })),
|
|
26353
26490
|
this.swatch(config.candles.downColor, (v) => this.emit({ candles: { downColor: v } }))
|
|
26354
|
-
]));
|
|
26355
|
-
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 } }), [
|
|
26356
26493
|
this.swatch(config.candles.borderUpColor, (v) => this.emit({ candles: { borderUpColor: v } })),
|
|
26357
26494
|
this.swatch(config.candles.borderDownColor, (v) => this.emit({ candles: { borderDownColor: v } }))
|
|
26358
|
-
]));
|
|
26359
|
-
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 } }), [
|
|
26360
26497
|
this.swatch(config.candles.wickUpColor, (v) => this.emit({ candles: { wickUpColor: v } })),
|
|
26361
26498
|
this.swatch(config.candles.wickDownColor, (v) => this.emit({ candles: { wickDownColor: v } }))
|
|
26362
|
-
]));
|
|
26363
|
-
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"));
|
|
26364
26501
|
groups.candles = candles;
|
|
26365
26502
|
body.append(candles);
|
|
26366
|
-
const bars = this.group();
|
|
26503
|
+
const bars = sid(this.group(), "symbol.style.bars");
|
|
26367
26504
|
bars.append(this.sectionTitle("Bars"));
|
|
26368
|
-
bars.append(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })));
|
|
26369
|
-
bars.append(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })));
|
|
26370
|
-
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"));
|
|
26371
26508
|
groups.bars = bars;
|
|
26372
26509
|
body.append(bars);
|
|
26373
|
-
const line = this.group();
|
|
26510
|
+
const line = sid(this.group(), "symbol.style.line");
|
|
26374
26511
|
line.append(this.sectionTitle("Line"));
|
|
26375
|
-
line.append(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })));
|
|
26376
|
-
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"));
|
|
26377
26514
|
groups.line = line;
|
|
26378
26515
|
body.append(line);
|
|
26379
|
-
const area = this.group();
|
|
26516
|
+
const area = sid(this.group(), "symbol.style.area");
|
|
26380
26517
|
area.append(this.sectionTitle("Area"));
|
|
26381
|
-
area.append(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })));
|
|
26382
|
-
area.append(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })));
|
|
26383
|
-
area.append(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })));
|
|
26384
|
-
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"));
|
|
26385
26522
|
groups.area = area;
|
|
26386
26523
|
body.append(area);
|
|
26387
|
-
const baseline = this.group();
|
|
26524
|
+
const baseline = sid(this.group(), "symbol.style.baseline");
|
|
26388
26525
|
baseline.append(this.sectionTitle("Baseline"));
|
|
26389
|
-
baseline.append(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]));
|
|
26390
|
-
baseline.append(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]));
|
|
26391
|
-
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", [
|
|
26392
26529
|
this.swatch(config.baseline.topFillColor, (v) => this.emit({ baseline: { topFillColor: v } })),
|
|
26393
26530
|
this.swatch(config.baseline.topFillColor2, (v) => this.emit({ baseline: { topFillColor2: v } }))
|
|
26394
|
-
]));
|
|
26395
|
-
baseline.append(this.rowWith("Fill bottom area", [
|
|
26531
|
+
]), "symbol.style.baseline.fill-top"));
|
|
26532
|
+
baseline.append(sid(this.rowWith("Fill bottom area", [
|
|
26396
26533
|
this.swatch(config.baseline.bottomFillColor2, (v) => this.emit({ baseline: { bottomFillColor2: v } })),
|
|
26397
26534
|
this.swatch(config.baseline.bottomFillColor, (v) => this.emit({ baseline: { bottomFillColor: v } }))
|
|
26398
|
-
]));
|
|
26399
|
-
baseline.append(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })));
|
|
26400
|
-
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"));
|
|
26401
26538
|
groups.baseline = baseline;
|
|
26402
26539
|
body.append(baseline);
|
|
26403
26540
|
for (const def of chartTypes()) {
|
|
@@ -26405,32 +26542,36 @@ var SettingsDialog = class {
|
|
|
26405
26542
|
const bag = config.chartTypes[def.id] ?? {};
|
|
26406
26543
|
const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
|
|
26407
26544
|
const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
|
|
26408
|
-
const g = this.group();
|
|
26545
|
+
const g = sid(this.group(), `symbol.style.${def.id}`);
|
|
26409
26546
|
g.append(this.sectionTitle("Candles"));
|
|
26410
|
-
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), [
|
|
26411
26548
|
this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
|
|
26412
26549
|
this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
|
|
26413
|
-
]));
|
|
26414
|
-
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), [
|
|
26415
26552
|
this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
|
|
26416
26553
|
this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
|
|
26417
|
-
]));
|
|
26418
|
-
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), [
|
|
26419
26556
|
this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
|
|
26420
26557
|
this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
|
|
26421
|
-
]));
|
|
26422
|
-
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`));
|
|
26423
26560
|
groups[def.id] = g;
|
|
26424
26561
|
body.append(g);
|
|
26425
26562
|
}
|
|
26426
26563
|
showActive(config.series.style);
|
|
26427
|
-
body.append(this.sectionTitle("Time zone"));
|
|
26428
|
-
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"));
|
|
26429
26566
|
const renderHostSections = (placement) => {
|
|
26430
26567
|
for (const hs of this.hostSections) {
|
|
26431
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;
|
|
26432
26573
|
body.append(placement === "symbol" ? this.sectionTitle(hs.title) : this.section(hs.title));
|
|
26433
|
-
for (const hr of
|
|
26574
|
+
for (const hr of rows) {
|
|
26434
26575
|
if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
|
|
26435
26576
|
else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
|
|
26436
26577
|
else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
|
|
@@ -26443,53 +26584,69 @@ var SettingsDialog = class {
|
|
|
26443
26584
|
const typeSettings = def.settings;
|
|
26444
26585
|
if (!typeSettings) continue;
|
|
26445
26586
|
if ((typeSettings.placement ?? "end") !== placement) continue;
|
|
26587
|
+
if (settingsIdHidden(`type:${def.id}`, this.hiddenSettings)) continue;
|
|
26446
26588
|
this.chartTypeSection(def.id, typeSettings, config, body);
|
|
26447
26589
|
}
|
|
26448
26590
|
};
|
|
26449
26591
|
renderHostSections("symbol");
|
|
26450
26592
|
renderChartTypeSections("after-symbol");
|
|
26451
26593
|
renderHostSections("after-symbol");
|
|
26452
|
-
body.append(this.section("Scales and lines"));
|
|
26453
|
-
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"));
|
|
26454
26596
|
body.append(
|
|
26455
|
-
this.selectRowLabeled(
|
|
26597
|
+
sid(this.selectRowLabeled(
|
|
26456
26598
|
"Mode",
|
|
26457
26599
|
config.priceScale.log ? "log" : config.priceScale.mode,
|
|
26458
26600
|
[["price", "Regular"], ["percent", "Percent"], ["indexed", "Indexed to 100"], ["log", "Logarithmic"]],
|
|
26459
26601
|
(v) => this.emit({ priceScale: v === "log" ? { mode: "price", log: true } : { mode: v, log: false } })
|
|
26460
|
-
)
|
|
26602
|
+
), "scales.price-scale.mode")
|
|
26461
26603
|
);
|
|
26462
|
-
body.append(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })));
|
|
26463
|
-
body.append(this.separator());
|
|
26464
|
-
body.append(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })));
|
|
26465
|
-
body.append(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })));
|
|
26466
|
-
body.append(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })));
|
|
26467
|
-
body.append(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })));
|
|
26468
|
-
body.append(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })));
|
|
26469
|
-
body.append(this.sectionTitle("Crosshair"));
|
|
26470
|
-
body.append(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })));
|
|
26471
|
-
body.append(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })));
|
|
26472
|
-
body.append(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })));
|
|
26473
|
-
body.append(this.section("Canvas"));
|
|
26474
|
-
body.append(this.sectionTitle("Background & text"));
|
|
26475
|
-
body.append(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })));
|
|
26476
|
-
body.append(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })));
|
|
26477
|
-
body.append(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })));
|
|
26478
|
-
body.append(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })));
|
|
26479
|
-
body.append(this.sectionTitle("Grid"));
|
|
26480
|
-
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 } } }), [
|
|
26481
26623
|
this.swatch(config.grid.vertLines.color, (v) => this.emit({ grid: { vertLines: { color: v } } }))
|
|
26482
|
-
]));
|
|
26483
|
-
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 } } }), [
|
|
26484
26626
|
this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
|
|
26485
|
-
]));
|
|
26627
|
+
]), "canvas.grid.horizontal"));
|
|
26486
26628
|
if (this.themeControl) {
|
|
26487
26629
|
const tc = this.themeControl;
|
|
26488
|
-
body.append(this.sectionTitle("Theme"));
|
|
26489
|
-
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"));
|
|
26490
26632
|
}
|
|
26491
26633
|
renderChartTypeSections("end");
|
|
26492
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
|
+
}
|
|
26493
26650
|
const shell = document.createElement("div");
|
|
26494
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;";
|
|
26495
26652
|
const rail = document.createElement("div");
|
|
@@ -26527,6 +26684,9 @@ var SettingsDialog = class {
|
|
|
26527
26684
|
}
|
|
26528
26685
|
if (current) current.appendChild(child);
|
|
26529
26686
|
}
|
|
26687
|
+
for (let i = panes.length - 1; i >= 0; i--) {
|
|
26688
|
+
if (panes[i].el.childElementCount === 0) panes.splice(i, 1);
|
|
26689
|
+
}
|
|
26530
26690
|
this.syncTypeTabs = (active) => {
|
|
26531
26691
|
let hidActive = false;
|
|
26532
26692
|
panes.forEach((pn, idx) => {
|
|
@@ -26666,19 +26826,25 @@ var SettingsDialog = class {
|
|
|
26666
26826
|
this.emitType(typeId, key, v);
|
|
26667
26827
|
for (const r of refreshers) r();
|
|
26668
26828
|
};
|
|
26829
|
+
const scope = `type:${typeId}`;
|
|
26669
26830
|
if (section.instances && section.instances.length > 0) {
|
|
26670
|
-
|
|
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));
|
|
26671
26833
|
} else if (section.rows) {
|
|
26672
|
-
|
|
26673
|
-
|
|
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);
|
|
26674
26837
|
}
|
|
26675
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;
|
|
26676
26842
|
const subMarker = this.section(sub.title);
|
|
26677
26843
|
subMarker.dataset.sdStyle = typeId;
|
|
26678
26844
|
subMarker.dataset.sdVisibility = section.visibility ?? "active";
|
|
26679
26845
|
subMarker.dataset.sdSub = "1";
|
|
26680
26846
|
body.append(subMarker);
|
|
26681
|
-
body.append(this.groupedRows(`${typeId}/${sub.title}`,
|
|
26847
|
+
body.append(this.groupedRows(`${typeId}/${sub.title}`, rows, bag, put, refreshers, sub.enableKey));
|
|
26682
26848
|
}
|
|
26683
26849
|
for (const r of refreshers) r();
|
|
26684
26850
|
}
|
|
@@ -30968,6 +31134,94 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
|
|
|
30968
31134
|
return { above, below: combinedWeight - above };
|
|
30969
31135
|
}
|
|
30970
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
|
+
|
|
30971
31225
|
// src/renderers/native/volume/paintVolume.ts
|
|
30972
31226
|
var VOLUME_FILL_ALPHA = 0.5;
|
|
30973
31227
|
function paintVolume(ctx, bars, geom, colors) {
|
|
@@ -31443,6 +31697,7 @@ var NativeRenderer = class {
|
|
|
31443
31697
|
// px reserved on the left for the docked drawings toolbar (0 when hidden)
|
|
31444
31698
|
this.mountContainer = null;
|
|
31445
31699
|
this.userDrawings = null;
|
|
31700
|
+
this.backdropRenderer = new BackdropRenderer();
|
|
31446
31701
|
this.volumeRenderer = new VolumeRenderer();
|
|
31447
31702
|
/** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
|
|
31448
31703
|
this.extLayers = [];
|
|
@@ -31570,6 +31825,9 @@ var NativeRenderer = class {
|
|
|
31570
31825
|
// bar under the crosshair; null when off a bar
|
|
31571
31826
|
// ── settings dialog (rich, serializable config — item 15) ──
|
|
31572
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 = [];
|
|
31573
31831
|
/** Where modal dialogs mount — a HOST override (multi-chart shells pass their root
|
|
31574
31832
|
* so dialogs center globally instead of clipping inside one cell). Null = the plot. */
|
|
31575
31833
|
this.dialogHost = null;
|
|
@@ -32242,6 +32500,7 @@ var NativeRenderer = class {
|
|
|
32242
32500
|
}
|
|
32243
32501
|
this.settingsDialog.setTheme(this.theme);
|
|
32244
32502
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
32503
|
+
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
32245
32504
|
this.syncThemeControl();
|
|
32246
32505
|
this.settingsDialog.toggle(
|
|
32247
32506
|
this.getConfig(),
|
|
@@ -32489,6 +32748,8 @@ var NativeRenderer = class {
|
|
|
32489
32748
|
this.wrapper = document.createElement("div");
|
|
32490
32749
|
Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair", userSelect: "none", webkitUserSelect: "none" });
|
|
32491
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" });
|
|
32492
32753
|
this.volumeCanvas = document.createElement("canvas");
|
|
32493
32754
|
Object.assign(this.volumeCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
|
|
32494
32755
|
this.dataCanvas = this.createGeometryBackend();
|
|
@@ -32511,7 +32772,7 @@ var NativeRenderer = class {
|
|
|
32511
32772
|
});
|
|
32512
32773
|
const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
|
|
32513
32774
|
const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
|
|
32514
|
-
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);
|
|
32515
32776
|
this.layerOrderSig = "";
|
|
32516
32777
|
this.wrapper.appendChild(this.plot);
|
|
32517
32778
|
this.factoryConfig = this.getConfig();
|
|
@@ -32521,6 +32782,7 @@ var NativeRenderer = class {
|
|
|
32521
32782
|
this.wrapper.appendChild(this.attributionEl);
|
|
32522
32783
|
container.appendChild(this.wrapper);
|
|
32523
32784
|
this.applyBackground();
|
|
32785
|
+
this.backdropRenderer.mount(this.backdropCanvas);
|
|
32524
32786
|
this.volumeRenderer.mount(this.volumeCanvas);
|
|
32525
32787
|
this.vpvrRenderer.mount(this.vpvrCanvas);
|
|
32526
32788
|
for (const l of this.extLayers) l.instance.mount(l.canvas);
|
|
@@ -32810,6 +33072,7 @@ var NativeRenderer = class {
|
|
|
32810
33072
|
this.scrollButton = null;
|
|
32811
33073
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
32812
33074
|
this.extLayers = [];
|
|
33075
|
+
this.backdropRenderer.destroy();
|
|
32813
33076
|
this.volumeRenderer.destroy();
|
|
32814
33077
|
this.vpvrRenderer.destroy();
|
|
32815
33078
|
this.backend.destroy();
|
|
@@ -33189,6 +33452,13 @@ var NativeRenderer = class {
|
|
|
33189
33452
|
this.hostSettingsSections = sections;
|
|
33190
33453
|
this.settingsDialog?.setHostSections(sections);
|
|
33191
33454
|
}
|
|
33455
|
+
setSettingsVisibility(policy) {
|
|
33456
|
+
this.hiddenSettings = [...policy.hidden ?? []];
|
|
33457
|
+
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
33458
|
+
}
|
|
33459
|
+
listSettingsIds() {
|
|
33460
|
+
return settingsIdCatalog(this.hostSettingsSections);
|
|
33461
|
+
}
|
|
33192
33462
|
onChartTypeSettingsChange(cb) {
|
|
33193
33463
|
this.chartTypeSettingsCbs.add(cb);
|
|
33194
33464
|
return () => this.chartTypeSettingsCbs.delete(cb);
|
|
@@ -33895,7 +34165,7 @@ var NativeRenderer = class {
|
|
|
33895
34165
|
this.backend.modelAlpha = this.modelAlpha;
|
|
33896
34166
|
this.backend.candleBodyAlpha = this.candleBodyAlpha;
|
|
33897
34167
|
this.backend.candleStructureAlpha = this.candleStructureAlpha;
|
|
33898
|
-
|
|
34168
|
+
let gridAlpha = 1;
|
|
33899
34169
|
const candleBodyScale = 1;
|
|
33900
34170
|
this.backend.candleBodyScale = candleBodyScale;
|
|
33901
34171
|
const pane = this.scene.panes.get(PRICE_PANE_ID2);
|
|
@@ -33934,7 +34204,7 @@ var NativeRenderer = class {
|
|
|
33934
34204
|
if (folded) {
|
|
33935
34205
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
33936
34206
|
if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
|
|
33937
|
-
if (folded.gridAlpha != null)
|
|
34207
|
+
if (folded.gridAlpha != null) gridAlpha = clamp012(folded.gridAlpha);
|
|
33938
34208
|
}
|
|
33939
34209
|
}
|
|
33940
34210
|
const li = this.bars.length - 1;
|
|
@@ -33942,6 +34212,7 @@ var NativeRenderer = class {
|
|
|
33942
34212
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
33943
34213
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
33944
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);
|
|
33945
34216
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
33946
34217
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
33947
34218
|
this.userDrawings?.render();
|
|
@@ -34084,6 +34355,7 @@ var NativeRenderer = class {
|
|
|
34084
34355
|
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34085
34356
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
34086
34357
|
pane.axisFormat = void 0;
|
|
34358
|
+
pane.axisBands = void 0;
|
|
34087
34359
|
if (this.volumeOwnsPane(pane, masterModels)) {
|
|
34088
34360
|
const maxVol = this.maxVisibleVolume(i0, i1);
|
|
34089
34361
|
if (maxVol > 0) {
|
|
@@ -34093,6 +34365,11 @@ var NativeRenderer = class {
|
|
|
34093
34365
|
} else if (this.layerNativesOwnPane(pane, masterModels)) {
|
|
34094
34366
|
pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34095
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
|
+
}
|
|
34096
34373
|
}
|
|
34097
34374
|
if (pane === pricePane && this.scene.tradeMarkers.visible && pane.bounds.height > 0) {
|
|
34098
34375
|
const th = this.tradesScaleHints(vr);
|
|
@@ -34213,11 +34490,11 @@ var NativeRenderer = class {
|
|
|
34213
34490
|
);
|
|
34214
34491
|
return { below: below.map((id) => byId.get(id)), above: above.map((id) => byId.get(id)) };
|
|
34215
34492
|
}
|
|
34216
|
-
/** The full canvas pile in paint order (layers + data/volume/vpvr) — what
|
|
34217
|
-
* stacking and the screenshot compositor must both follow. */
|
|
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. */
|
|
34218
34495
|
canvasPile() {
|
|
34219
34496
|
const { below, above } = this.orderedLayerCanvases();
|
|
34220
|
-
return [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
|
|
34497
|
+
return [this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
|
|
34221
34498
|
}
|
|
34222
34499
|
/** Re-slot the SDK layer canvases in the plot when the computed order changed (a z
|
|
34223
34500
|
* write, a restored config, an indicator mount/remove/restack). Runs at the top of
|
|
@@ -34534,6 +34811,8 @@ var NativeRenderer = class {
|
|
|
34534
34811
|
const ph = h;
|
|
34535
34812
|
this.dataCanvas.width = Math.round(pw * dpr);
|
|
34536
34813
|
this.dataCanvas.height = Math.round(ph * dpr);
|
|
34814
|
+
this.backdropCanvas.width = this.dataCanvas.width;
|
|
34815
|
+
this.backdropCanvas.height = this.dataCanvas.height;
|
|
34537
34816
|
this.volumeCanvas.width = this.dataCanvas.width;
|
|
34538
34817
|
this.volumeCanvas.height = this.dataCanvas.height;
|
|
34539
34818
|
for (const l of this.extLayers) {
|
|
@@ -34675,35 +34954,6 @@ function normalizeBars(bars) {
|
|
|
34675
34954
|
return out;
|
|
34676
34955
|
}
|
|
34677
34956
|
|
|
34678
|
-
// src/core/price-styles/heikin-ashi.ts
|
|
34679
|
-
function heikinAshiNext(raw, prevHa) {
|
|
34680
|
-
const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
|
|
34681
|
-
const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
|
|
34682
|
-
return {
|
|
34683
|
-
time: raw.time,
|
|
34684
|
-
open: haOpen,
|
|
34685
|
-
high: Math.max(raw.high, haOpen, haClose),
|
|
34686
|
-
low: Math.min(raw.low, haOpen, haClose),
|
|
34687
|
-
close: haClose,
|
|
34688
|
-
...raw.volume != null ? { volume: raw.volume } : {}
|
|
34689
|
-
};
|
|
34690
|
-
}
|
|
34691
|
-
function heikinAshiFull(raw) {
|
|
34692
|
-
const out = new Array(raw.length);
|
|
34693
|
-
let prev2;
|
|
34694
|
-
for (let i = 0; i < raw.length; i += 1) {
|
|
34695
|
-
prev2 = heikinAshiNext(raw[i], prev2);
|
|
34696
|
-
out[i] = prev2;
|
|
34697
|
-
}
|
|
34698
|
-
return out;
|
|
34699
|
-
}
|
|
34700
|
-
|
|
34701
|
-
// src/chart-types/builtins.ts
|
|
34702
|
-
var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
|
|
34703
|
-
function registerBuiltinChartTypes() {
|
|
34704
|
-
registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
|
|
34705
|
-
}
|
|
34706
|
-
|
|
34707
34957
|
// src/core/native-indicators/volume/VolumeIndicator.ts
|
|
34708
34958
|
var DEFAULT_UP = BULLISH;
|
|
34709
34959
|
var DEFAULT_DOWN = BEARISH;
|
|
@@ -34900,6 +35150,7 @@ var Vela = class {
|
|
|
34900
35150
|
// the volume indicator is on by default
|
|
34901
35151
|
};
|
|
34902
35152
|
this.rendererControl = new RendererControl(renderer);
|
|
35153
|
+
if (options.settings) this.rendererControl.setSettingsVisibility(options.settings);
|
|
34903
35154
|
this.dataControl = new DataControl(feed);
|
|
34904
35155
|
this.orchestrator = new EngineOrchestrator(element, renderer, feed, engines, config, this.dataControl);
|
|
34905
35156
|
const defaults2 = rendererDefaults();
|
|
@@ -36030,8 +36281,8 @@ function seedDefaults(opts) {
|
|
|
36030
36281
|
};
|
|
36031
36282
|
}
|
|
36032
36283
|
function cellChartDefaults(opts) {
|
|
36033
|
-
const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings } = opts;
|
|
36034
|
-
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 };
|
|
36035
36286
|
}
|
|
36036
36287
|
function cellDrawings(opt) {
|
|
36037
36288
|
if (opt === false) return false;
|
|
@@ -36044,7 +36295,11 @@ var ChartCell = class {
|
|
|
36044
36295
|
this.deps = deps;
|
|
36045
36296
|
/** This cell's unified app+drawings undo timeline (the shared Ctrl+Z routes here). */
|
|
36046
36297
|
this.history = new WidgetHistory(() => this.inner);
|
|
36047
|
-
/** 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`). */
|
|
36048
36303
|
this.instances = [];
|
|
36049
36304
|
/** The native-indicator catalog with this cell's live supported/present flags. */
|
|
36050
36305
|
this.nativeCatalog = [];
|
|
@@ -36069,6 +36324,14 @@ var ChartCell = class {
|
|
|
36069
36324
|
this.presentNatives = [];
|
|
36070
36325
|
this.rangeBars = 0;
|
|
36071
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 = {};
|
|
36072
36335
|
/** Indicator titles (this cell's in-chart legend rows) shown. */
|
|
36073
36336
|
this.indicatorTitlesOn = true;
|
|
36074
36337
|
/** Plot values beside this cell's legend titles shown. */
|
|
@@ -36135,9 +36398,16 @@ var ChartCell = class {
|
|
|
36135
36398
|
this.pendingManifestNames = [...seed.indicators.manifest];
|
|
36136
36399
|
}
|
|
36137
36400
|
this.volumeIntent = seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume;
|
|
36401
|
+
this.extState = { ...seed.ext ?? {} };
|
|
36138
36402
|
this.inner.on("load:end", () => {
|
|
36139
36403
|
this.volumeMayBePending = false;
|
|
36140
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
|
+
});
|
|
36141
36411
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
36142
36412
|
this.inner.on("load:end", () => this.watermark?.setLoading(false));
|
|
36143
36413
|
const tz = deps.timezone();
|
|
@@ -36298,11 +36568,13 @@ var ChartCell = class {
|
|
|
36298
36568
|
const eth = "Extended hours (ETH)";
|
|
36299
36569
|
const sessionSection = {
|
|
36300
36570
|
title: "Trading session",
|
|
36571
|
+
id: "trading-session",
|
|
36301
36572
|
placement: "symbol",
|
|
36302
36573
|
rows: [
|
|
36303
36574
|
{
|
|
36304
36575
|
kind: "select",
|
|
36305
36576
|
label: "Session",
|
|
36577
|
+
id: "session",
|
|
36306
36578
|
options: [rth, eth],
|
|
36307
36579
|
get: () => this.session === "extended" ? eth : rth,
|
|
36308
36580
|
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
@@ -36310,12 +36582,14 @@ var ChartCell = class {
|
|
|
36310
36582
|
{
|
|
36311
36583
|
kind: "color",
|
|
36312
36584
|
label: "Pre-market",
|
|
36585
|
+
id: "premarket-color",
|
|
36313
36586
|
get: () => this.sessionShadeColor("premarketColor"),
|
|
36314
36587
|
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
36315
36588
|
},
|
|
36316
36589
|
{
|
|
36317
36590
|
kind: "color",
|
|
36318
36591
|
label: "Post-market",
|
|
36592
|
+
id: "postmarket-color",
|
|
36319
36593
|
get: () => this.sessionShadeColor("postmarketColor"),
|
|
36320
36594
|
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
36321
36595
|
}
|
|
@@ -36323,11 +36597,13 @@ var ChartCell = class {
|
|
|
36323
36597
|
};
|
|
36324
36598
|
const advanced = {
|
|
36325
36599
|
title: "Advanced",
|
|
36600
|
+
id: "advanced",
|
|
36326
36601
|
placement: "end",
|
|
36327
36602
|
rows: [
|
|
36328
36603
|
{
|
|
36329
36604
|
kind: "select",
|
|
36330
36605
|
label: "Bars to fetch",
|
|
36606
|
+
id: "bars",
|
|
36331
36607
|
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
36332
36608
|
get: () => String(this.state.bars ?? 1e3),
|
|
36333
36609
|
set: (v) => {
|
|
@@ -36340,11 +36616,13 @@ var ChartCell = class {
|
|
|
36340
36616
|
};
|
|
36341
36617
|
const watermarkSection = {
|
|
36342
36618
|
title: "Watermark",
|
|
36619
|
+
id: "watermark",
|
|
36343
36620
|
placement: "symbol",
|
|
36344
36621
|
rows: [
|
|
36345
36622
|
{
|
|
36346
36623
|
kind: "toggle",
|
|
36347
36624
|
label: "Symbol watermark",
|
|
36625
|
+
id: "visible",
|
|
36348
36626
|
get: () => this.watermarkOn,
|
|
36349
36627
|
set: (v) => this.setWatermarkVisible(v)
|
|
36350
36628
|
}
|
|
@@ -36355,22 +36633,25 @@ var ChartCell = class {
|
|
|
36355
36633
|
const sl = this.statusline;
|
|
36356
36634
|
sections.push({
|
|
36357
36635
|
title: "Status line",
|
|
36636
|
+
id: "status-line",
|
|
36358
36637
|
rows: [
|
|
36359
|
-
{ kind: "heading", label: "Status line" },
|
|
36360
|
-
{ kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
|
|
36361
|
-
{ kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
|
|
36362
|
-
{ kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
|
|
36363
|
-
{ kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
|
|
36364
|
-
{ 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" },
|
|
36365
36644
|
{
|
|
36366
36645
|
kind: "toggle",
|
|
36367
36646
|
label: "Titles",
|
|
36647
|
+
id: "indicator-titles",
|
|
36368
36648
|
get: () => this.indicatorTitlesOn,
|
|
36369
36649
|
set: (v) => this.setIndicatorTitlesVisible(v)
|
|
36370
36650
|
},
|
|
36371
36651
|
{
|
|
36372
36652
|
kind: "toggle",
|
|
36373
36653
|
label: "Values",
|
|
36654
|
+
id: "indicator-values",
|
|
36374
36655
|
get: () => this.indicatorValuesOn,
|
|
36375
36656
|
set: (v) => this.setIndicatorValuesVisible(v)
|
|
36376
36657
|
}
|
|
@@ -36423,6 +36704,7 @@ var ChartCell = class {
|
|
|
36423
36704
|
/** Switch this cell's market in place (the chart instance survives). */
|
|
36424
36705
|
setSymbol(symbol) {
|
|
36425
36706
|
if (!this.inner || symbol === this.symbol) return;
|
|
36707
|
+
this.unresolvedToasted = null;
|
|
36426
36708
|
void this.inner.setMarket({ symbol });
|
|
36427
36709
|
}
|
|
36428
36710
|
setTimeframe(timeframe) {
|
|
@@ -36513,6 +36795,41 @@ var ChartCell = class {
|
|
|
36513
36795
|
for (const entry of list) if (entry.enabled) this.addManifestInstance(entry, { record: false });
|
|
36514
36796
|
}
|
|
36515
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
|
+
}
|
|
36516
36833
|
/** The picker's library rows: supported natives first, then the manifest. */
|
|
36517
36834
|
libraryRows() {
|
|
36518
36835
|
return [
|
|
@@ -36542,10 +36859,20 @@ var ChartCell = class {
|
|
|
36542
36859
|
if (index < present.length) this.removeNative(present[index].type);
|
|
36543
36860
|
else this.removeInstance(index - present.length);
|
|
36544
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
|
+
}
|
|
36545
36872
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
36546
36873
|
addManifestInstance(entry, opts = {}) {
|
|
36547
36874
|
if (this.destroyed) return;
|
|
36548
|
-
const it = { entry, handle: this.addToChart(entry) };
|
|
36875
|
+
const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
|
|
36549
36876
|
this.instances.push(it);
|
|
36550
36877
|
this.deps.onIndicatorsChanged(this.id);
|
|
36551
36878
|
if (opts.record === false) return;
|
|
@@ -36626,12 +36953,97 @@ var ChartCell = class {
|
|
|
36626
36953
|
return null;
|
|
36627
36954
|
}
|
|
36628
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
|
+
}
|
|
36629
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
|
+
}
|
|
36630
37041
|
/** Snapshot everything the pool needs to restore this slot later. The market fields
|
|
36631
37042
|
* come from the LIVE config (`chart.market`) — the requested identity — so a switch
|
|
36632
37043
|
* still loading when the snapshot is taken (persist-on-close) is not lost. */
|
|
36633
37044
|
dehydrate() {
|
|
36634
37045
|
const live = this.inner?.market;
|
|
37046
|
+
const ext = this.inner ? this.dehydrateExt() : Object.keys(this.extState).length > 0 ? { ...this.extState } : void 0;
|
|
36635
37047
|
return {
|
|
36636
37048
|
...this.state,
|
|
36637
37049
|
...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
|
|
@@ -36644,14 +37056,17 @@ var ChartCell = class {
|
|
|
36644
37056
|
// Natives from the chart's SYNC registry read — an async catalog mirror here
|
|
36645
37057
|
// lost unload-time saves, and the old empty-set fallbacks resurrected removed
|
|
36646
37058
|
// indicators. Manifest names fall back to the restored ledger only until the
|
|
36647
|
-
// 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.
|
|
36648
37062
|
indicators: indicatorLedger({
|
|
36649
37063
|
present: this.inner ? this.inner.presentNativeIndicators() : [],
|
|
36650
|
-
instanceNames: this.instances.map((it) => it.entry.name),
|
|
37064
|
+
instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
|
|
36651
37065
|
pendingManifest: this.pendingManifestNames,
|
|
36652
37066
|
manifestSettled: this.deps.manifestSettled(),
|
|
36653
37067
|
volumePending: this.volumeMayBePending && this.volumeIntent
|
|
36654
|
-
})
|
|
37068
|
+
}),
|
|
37069
|
+
...ext ? { ext } : {}
|
|
36655
37070
|
};
|
|
36656
37071
|
}
|
|
36657
37072
|
destroy() {
|
|
@@ -36688,6 +37103,9 @@ function buildContext(host) {
|
|
|
36688
37103
|
togglePanel: (id, open2) => host.togglePanel(id, open2),
|
|
36689
37104
|
host: host.root,
|
|
36690
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(),
|
|
36691
37109
|
cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
|
|
36692
37110
|
activeCellId: active?.id ?? "",
|
|
36693
37111
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
@@ -37049,6 +37467,10 @@ var VelaWorkspace = class {
|
|
|
37049
37467
|
/** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
|
|
37050
37468
|
this.priceScalePressY = 0;
|
|
37051
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 = {};
|
|
37052
37474
|
/** The single grid-wide attribution mark — re-inked on a live theme swap. */
|
|
37053
37475
|
this.attributionMark = null;
|
|
37054
37476
|
this.onRootKeydown = (ev) => this.routeTyping(ev);
|
|
@@ -37063,6 +37485,7 @@ var VelaWorkspace = class {
|
|
|
37063
37485
|
state: () => ({ ...this.syncOpts })
|
|
37064
37486
|
};
|
|
37065
37487
|
registerBuiltinLayouts();
|
|
37488
|
+
registerBuiltinChartTypes();
|
|
37066
37489
|
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
37067
37490
|
if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
|
|
37068
37491
|
this.opts = opts;
|
|
@@ -37086,8 +37509,12 @@ var VelaWorkspace = class {
|
|
|
37086
37509
|
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
|
|
37087
37510
|
this.applySyncSetting(kind, sync?.[kind]);
|
|
37088
37511
|
}
|
|
37089
|
-
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);
|
|
37090
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 };
|
|
37091
37518
|
if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
|
|
37092
37519
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
37093
37520
|
const bootActive = boot?.activeCellId ?? null;
|
|
@@ -37141,7 +37568,9 @@ var VelaWorkspace = class {
|
|
|
37141
37568
|
onTimeframe: (tf) => this.setActiveTimeframe(tf),
|
|
37142
37569
|
onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
|
|
37143
37570
|
onPriceStyle: (style) => this.active.setPriceStyle(style),
|
|
37144
|
-
|
|
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 : {
|
|
37145
37574
|
current: this.def.id,
|
|
37146
37575
|
// The picker composes dynamic layouts on its grid canvas; registered
|
|
37147
37576
|
// presets the canvas cannot express (bespoke plugin areas) list as rows.
|
|
@@ -37166,8 +37595,11 @@ var VelaWorkspace = class {
|
|
|
37166
37595
|
});
|
|
37167
37596
|
const main = doc.createElement("div");
|
|
37168
37597
|
main.className = "vela-ws-main";
|
|
37598
|
+
const toolbar = buildToolbar(opts.drawings);
|
|
37599
|
+
this.drawingsEnabled = opts.drawings !== false;
|
|
37600
|
+
this.toolbarDef = toolbar.definition;
|
|
37169
37601
|
let toolbarHost = null;
|
|
37170
|
-
if (opts.drawingToolbar !== false) {
|
|
37602
|
+
if (this.drawingsEnabled && toolbar.visible && opts.drawingToolbar !== false) {
|
|
37171
37603
|
toolbarHost = doc.createElement("div");
|
|
37172
37604
|
toolbarHost.className = "vela-ws-toolbar";
|
|
37173
37605
|
main.appendChild(toolbarHost);
|
|
@@ -37186,7 +37618,7 @@ var VelaWorkspace = class {
|
|
|
37186
37618
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
37187
37619
|
this.dock.refresh();
|
|
37188
37620
|
this.root.appendChild(main);
|
|
37189
|
-
this.
|
|
37621
|
+
this.toastHost = new Toast(this.gridEl);
|
|
37190
37622
|
const attribution = rendererDefaults().attribution;
|
|
37191
37623
|
if (attribution !== false) {
|
|
37192
37624
|
const background = resolveTheme(opts.theme).background;
|
|
@@ -37232,7 +37664,7 @@ var VelaWorkspace = class {
|
|
|
37232
37664
|
}
|
|
37233
37665
|
}
|
|
37234
37666
|
) : null;
|
|
37235
|
-
this.drawToolbar?.setDefinition(
|
|
37667
|
+
this.drawToolbar?.setDefinition(this.toolbarDef);
|
|
37236
37668
|
this.drawToolbar?.setVisible(true);
|
|
37237
37669
|
this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
|
|
37238
37670
|
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
@@ -37254,11 +37686,11 @@ var VelaWorkspace = class {
|
|
|
37254
37686
|
onTimeframeClick: () => this.openTimeframeDrawer(),
|
|
37255
37687
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
37256
37688
|
getContext: () => this.context(),
|
|
37257
|
-
onDrawingsClick: () => this.openDrawingsDrawer(),
|
|
37689
|
+
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
37258
37690
|
onMoreClick: () => this.openMoreDrawer(),
|
|
37259
37691
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
37260
37692
|
}) : null;
|
|
37261
|
-
this.drawingPill = new DrawingPill(this.gridEl);
|
|
37693
|
+
this.drawingPill = this.drawingsEnabled ? new DrawingPill(this.gridEl) : null;
|
|
37262
37694
|
hostEl.appendChild(this.root);
|
|
37263
37695
|
this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
|
|
37264
37696
|
this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
|
|
@@ -37297,6 +37729,7 @@ var VelaWorkspace = class {
|
|
|
37297
37729
|
this.manifestSettled = true;
|
|
37298
37730
|
}
|
|
37299
37731
|
this.mountAttachments();
|
|
37732
|
+
this.restoreGlobalExt();
|
|
37300
37733
|
}
|
|
37301
37734
|
// ── access ──────────────────────────────────────────────────
|
|
37302
37735
|
/** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
|
|
@@ -37358,7 +37791,8 @@ var VelaWorkspace = class {
|
|
|
37358
37791
|
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
37359
37792
|
togglePanel: (id, open2) => this.dock.toggle(id, open2),
|
|
37360
37793
|
root: this.root,
|
|
37361
|
-
toast: (message, kind) => this.
|
|
37794
|
+
toast: (message, kind) => this.toastHost.show(message, kind),
|
|
37795
|
+
stateDirty: () => this.markStateDirty()
|
|
37362
37796
|
});
|
|
37363
37797
|
}
|
|
37364
37798
|
/** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
|
|
@@ -37397,22 +37831,35 @@ var VelaWorkspace = class {
|
|
|
37397
37831
|
if (this.trackSizes.size > 0) state.trackSizes = Object.fromEntries([...this.trackSizes].map(([k, v]) => [k, { ...v }]));
|
|
37398
37832
|
const panels2 = this.dock.getState();
|
|
37399
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;
|
|
37400
37846
|
return state;
|
|
37401
37847
|
}
|
|
37402
37848
|
/**
|
|
37403
37849
|
* Restore a state document produced by {@link getState} (untrusted-safe: malformed
|
|
37404
|
-
* fields are dropped).
|
|
37405
|
-
* layout,
|
|
37406
|
-
*
|
|
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).
|
|
37407
37858
|
*/
|
|
37408
37859
|
applyState(state) {
|
|
37409
37860
|
if (this.destroyed) return;
|
|
37410
37861
|
const st = sanitizeState(state);
|
|
37411
37862
|
if (!st) return;
|
|
37412
|
-
if (st.timezone) {
|
|
37413
|
-
this.timezone = st.timezone;
|
|
37414
|
-
this.bottombar?.setTimezone(st.timezone);
|
|
37415
|
-
}
|
|
37416
37863
|
if (st.favorites) this.favs = [...st.favorites];
|
|
37417
37864
|
if (st.timeframeFavorites) {
|
|
37418
37865
|
this.tfFavs = [...st.timeframeFavorites];
|
|
@@ -37422,6 +37869,36 @@ var VelaWorkspace = class {
|
|
|
37422
37869
|
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
37423
37870
|
this.trackSizes.clear();
|
|
37424
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
|
+
}
|
|
37425
37902
|
for (const [id, cell] of [...this.cellsById]) {
|
|
37426
37903
|
cell.destroy();
|
|
37427
37904
|
this.cellsById.delete(id);
|
|
@@ -37431,7 +37908,7 @@ var VelaWorkspace = class {
|
|
|
37431
37908
|
this.drawingLinks.clear();
|
|
37432
37909
|
for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
|
|
37433
37910
|
this.order = st.charts.map((c) => c.id);
|
|
37434
|
-
const def = ensureLayout(st.layout);
|
|
37911
|
+
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
37435
37912
|
if (def) this.def = def;
|
|
37436
37913
|
this.cellBackend = this.backendFor(this.def);
|
|
37437
37914
|
this.applyGrid();
|
|
@@ -37443,8 +37920,24 @@ var VelaWorkspace = class {
|
|
|
37443
37920
|
else this.setActiveCell(nextActive);
|
|
37444
37921
|
this.refreshRetention();
|
|
37445
37922
|
this.events.emit("layout:changed", { layout: this.def.id });
|
|
37923
|
+
this.extState = { ...st.ext ?? {} };
|
|
37924
|
+
this.restoreGlobalExt();
|
|
37446
37925
|
this.markStateDirty();
|
|
37447
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
|
+
}
|
|
37448
37941
|
/** Set the workspace-global display timezone — applied to EVERY cell. */
|
|
37449
37942
|
setTimezone(zone) {
|
|
37450
37943
|
this.timezone = zone;
|
|
@@ -37480,6 +37973,7 @@ var VelaWorkspace = class {
|
|
|
37480
37973
|
*/
|
|
37481
37974
|
setLayout(layout) {
|
|
37482
37975
|
if (this.destroyed) return;
|
|
37976
|
+
if (this.monoLayout) return;
|
|
37483
37977
|
const next = this.resolveLayout(layout);
|
|
37484
37978
|
const nextBackend = this.backendFor(next);
|
|
37485
37979
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -37509,6 +38003,12 @@ var VelaWorkspace = class {
|
|
|
37509
38003
|
resize() {
|
|
37510
38004
|
this.splitters.layout();
|
|
37511
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
|
+
}
|
|
37512
38012
|
destroy() {
|
|
37513
38013
|
if (this.destroyed) return;
|
|
37514
38014
|
this.persistNow();
|
|
@@ -37534,7 +38034,7 @@ var VelaWorkspace = class {
|
|
|
37534
38034
|
this.topbar.destroy();
|
|
37535
38035
|
this.bottombar?.destroy();
|
|
37536
38036
|
this.mobileBar?.destroy();
|
|
37537
|
-
this.drawingPill
|
|
38037
|
+
this.drawingPill?.destroy();
|
|
37538
38038
|
this.tfDrawer?.destroy();
|
|
37539
38039
|
this.drawingsDrawer?.destroy();
|
|
37540
38040
|
this.moreDrawer?.destroy();
|
|
@@ -37548,10 +38048,10 @@ var VelaWorkspace = class {
|
|
|
37548
38048
|
this.indicatorPicker?.destroy();
|
|
37549
38049
|
this.tfQuick.destroy();
|
|
37550
38050
|
this.shortcutsHelp?.destroy();
|
|
37551
|
-
this.
|
|
38051
|
+
this.toastHost.destroy();
|
|
37552
38052
|
this.alertsMenu?.destroy();
|
|
37553
38053
|
this.glider.stop();
|
|
37554
|
-
sharedBarStore.retain(/* @__PURE__ */ new Set());
|
|
38054
|
+
sharedBarStore.retain(/* @__PURE__ */ new Set(), this);
|
|
37555
38055
|
this.root.remove();
|
|
37556
38056
|
this.events.clear();
|
|
37557
38057
|
}
|
|
@@ -37569,7 +38069,7 @@ var VelaWorkspace = class {
|
|
|
37569
38069
|
this.mobileBar?.renderActions();
|
|
37570
38070
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
37571
38071
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
37572
|
-
this.drawingPill
|
|
38072
|
+
this.drawingPill?.onChart(cell.chart);
|
|
37573
38073
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
37574
38074
|
this.historyUnsub?.();
|
|
37575
38075
|
this.historyUnsub = cell.history.onChange(pushHistory);
|
|
@@ -37695,7 +38195,8 @@ var VelaWorkspace = class {
|
|
|
37695
38195
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
37696
38196
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
37697
38197
|
onStateDirty: () => this.markStateDirty(),
|
|
37698
|
-
manifestSettled: () => this.manifestSettled
|
|
38198
|
+
manifestSettled: () => this.manifestSettled,
|
|
38199
|
+
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
37699
38200
|
});
|
|
37700
38201
|
cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
37701
38202
|
this.cellsById.set(id, cell);
|
|
@@ -37704,6 +38205,7 @@ var VelaWorkspace = class {
|
|
|
37704
38205
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
37705
38206
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
37706
38207
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
38208
|
+
cell.restorePersistedExt();
|
|
37707
38209
|
this.events.emit("cell:created", { id });
|
|
37708
38210
|
}
|
|
37709
38211
|
for (const [i] of this.def.cells.entries()) {
|
|
@@ -37715,12 +38217,13 @@ var VelaWorkspace = class {
|
|
|
37715
38217
|
* cell's whole life, so these live and die with the cell). */
|
|
37716
38218
|
wireCell(cell) {
|
|
37717
38219
|
const chart = cell.chart;
|
|
37718
|
-
chart.on("indicator:error", ({ error }) => this.
|
|
38220
|
+
chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
|
|
37719
38221
|
chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
|
|
37720
38222
|
chart.on("alert", (alert) => {
|
|
37721
|
-
|
|
37722
|
-
|
|
37723
|
-
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);
|
|
37724
38227
|
this.topbar.setAlertCount(this.alerts.length);
|
|
37725
38228
|
});
|
|
37726
38229
|
chart.on("drawing:favorites", ({ favorites }) => {
|
|
@@ -37981,6 +38484,7 @@ var VelaWorkspace = class {
|
|
|
37981
38484
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
37982
38485
|
this.objectTree.setSymbol(cell.symbol);
|
|
37983
38486
|
this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
|
|
38487
|
+
this.bottombar?.setActiveRange(cell.activeRangeId);
|
|
37984
38488
|
}
|
|
37985
38489
|
/** Trigger ② — a cell's price style changed: the topbar button/menu only if active.
|
|
37986
38490
|
* Reads the cell back (not the requested style) so the button reflects what the
|
|
@@ -38065,7 +38569,7 @@ var VelaWorkspace = class {
|
|
|
38065
38569
|
openDrawingsDrawer() {
|
|
38066
38570
|
this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
|
|
38067
38571
|
host: this.root,
|
|
38068
|
-
toolbar: () =>
|
|
38572
|
+
toolbar: () => this.toolbarDef,
|
|
38069
38573
|
// the shared static toolbar's definition (see constructor)
|
|
38070
38574
|
currentTool: () => this.active.chart.drawings.getTool(),
|
|
38071
38575
|
isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
|
|
@@ -38088,14 +38592,15 @@ var VelaWorkspace = class {
|
|
|
38088
38592
|
onPriceStyle: (id) => this.active.setPriceStyle(id),
|
|
38089
38593
|
panels: () => [...this.dock.list()],
|
|
38090
38594
|
onTogglePanel: (id) => this.dock.toggle(id),
|
|
38091
|
-
alerts: () => this.alerts.map((a) => ({ title:
|
|
38595
|
+
alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
|
|
38092
38596
|
// Left-aligned actions have their own bottom-bar stop — only the rest
|
|
38093
38597
|
// lands in the drawer, or every left action would appear twice.
|
|
38094
38598
|
actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
|
|
38095
38599
|
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
38096
38600
|
// non-canvas presets and the sync switches — relocated into the kebab
|
|
38097
|
-
// drawer (the topbar is hidden on mobile). Same reads as the topbar block
|
|
38098
|
-
|
|
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 : {
|
|
38099
38604
|
shape: () => layoutShape(this.def),
|
|
38100
38605
|
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
|
|
38101
38606
|
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
@@ -38138,7 +38643,7 @@ var VelaWorkspace = class {
|
|
|
38138
38643
|
this.alertsMenu?.destroy();
|
|
38139
38644
|
const items = this.alerts.length ? this.alerts.map((a, i) => ({
|
|
38140
38645
|
id: String(i),
|
|
38141
|
-
label:
|
|
38646
|
+
label: `${a.source} \xB7 ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
|
|
38142
38647
|
})) : [{ id: "none", label: "No alerts yet", disabled: true }];
|
|
38143
38648
|
this.alertsMenu = new Menu({
|
|
38144
38649
|
host: this.root,
|
|
@@ -38259,7 +38764,7 @@ var VelaWorkspace = class {
|
|
|
38259
38764
|
if (!raw) continue;
|
|
38260
38765
|
symbols.add(this.feed.resolveSymbol(raw)?.ticker ?? raw);
|
|
38261
38766
|
}
|
|
38262
|
-
sharedBarStore.retain(symbols);
|
|
38767
|
+
sharedBarStore.retain(symbols, this);
|
|
38263
38768
|
}
|
|
38264
38769
|
};
|
|
38265
38770
|
|