@luxalgo/vela 0.6.5 → 0.6.7
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-BmHkcwvJ.d.ts → DataProvider-BSLlBpB9.d.ts} +11 -1
- package/dist/{DataProvider-DrN7ZIou.d.cts → DataProvider-BsQM2WNH.d.cts} +11 -1
- package/dist/{history-BmyjaVir.d.cts → bottombar-Br4rzx_R.d.ts} +158 -129
- package/dist/{history-CvOtsjT5.d.ts → bottombar-Div5zibZ.d.cts} +158 -129
- package/dist/{chunk-FQBT4NFM.js → chunk-6WFKTAT4.js} +2797 -2527
- package/dist/{chunk-BEJ57HP4.js → chunk-L3I2CCYO.js} +62 -1
- package/dist/chunk-W4EJWLEO.js +12 -0
- package/dist/{chunk-N6MBJHMV.js → chunk-WVP4XV5A.js} +6050 -3402
- package/dist/{contributions-DatoAzMx.d.ts → contributions-Hv4_cOJo.d.cts} +152 -5
- package/dist/{contributions-2bEmCF9S.d.cts → contributions-Z12BrWCy.d.ts} +152 -5
- package/dist/index.cjs +2798 -2484
- package/dist/index.d.cts +44 -16
- package/dist/index.d.ts +44 -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-Bcone6Vh.d.ts → plugin-9koBjc5H.d.ts} +6 -4
- package/dist/{plugin-DO_DX0gp.d.cts → plugin-BXhDHTYn.d.cts} +6 -4
- package/dist/plugin.cjs +44 -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.cjs +16 -0
- package/dist/providers/binance.d.cts +5 -2
- package/dist/providers/binance.d.ts +5 -2
- package/dist/providers/binance.js +7 -0
- package/dist/providers/coinbase.cjs +16 -0
- package/dist/providers/coinbase.d.cts +5 -2
- package/dist/providers/coinbase.d.ts +5 -2
- package/dist/providers/coinbase.js +7 -0
- package/dist/providers/hyperliquid.cjs +16 -0
- package/dist/providers/hyperliquid.d.cts +5 -2
- package/dist/providers/hyperliquid.d.ts +5 -2
- package/dist/providers/hyperliquid.js +7 -0
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +2824 -2484
- package/dist/vela.global.min.js +48 -48
- package/dist/widget.cjs +35791 -34070
- package/dist/widget.d.cts +80 -275
- package/dist/widget.d.ts +80 -275
- package/dist/widget.js +61 -1313
- package/dist/workspace.cjs +1700 -995
- package/dist/workspace.d.cts +120 -15
- package/dist/workspace.d.ts +120 -15
- package/dist/workspace.js +7 -2281
- package/package.json +1 -1
package/dist/workspace.cjs
CHANGED
|
@@ -303,6 +303,28 @@ var ProviderRegistry = class {
|
|
|
303
303
|
const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
|
|
304
304
|
return `${d?.prefix ?? resolved.provider}:${d?.ticker ?? resolved.ticker}`;
|
|
305
305
|
}
|
|
306
|
+
/** The icon URL for a DESCRIPTOR — routed to its OWNING provider's resolver (the
|
|
307
|
+
* provider owns the knowledge of where its asset class's icons live). A missing
|
|
308
|
+
* provider, an absent resolver, or a resolver that throws all mean "no icon" —
|
|
309
|
+
* the shells' initials badge takes over, never an error. */
|
|
310
|
+
symbolIconOf(d) {
|
|
311
|
+
const name = d.provider ? normName(d.provider) : [...this.entries.keys()][0];
|
|
312
|
+
const entry = name !== void 0 ? this.entries.get(name) : void 0;
|
|
313
|
+
try {
|
|
314
|
+
return entry?.provider.resolveSymbolIcon?.(d) ?? void 0;
|
|
315
|
+
} catch {
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** The icon URL for a raw SYMBOL string (the statusline/object-tree path): resolve
|
|
320
|
+
* to the owning provider, find its descriptor, route to the resolver. Before the
|
|
321
|
+
* index settles (or for a lenient sole-provider resolution) a minimal synthetic
|
|
322
|
+
* descriptor is offered — crypto resolvers can answer from the ticker alone. */
|
|
323
|
+
symbolIcon(resolved) {
|
|
324
|
+
if (!resolved) return void 0;
|
|
325
|
+
const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
|
|
326
|
+
return this.symbolIconOf({ ...d ?? { ticker: resolved.ticker }, provider: d?.provider ?? resolved.provider });
|
|
327
|
+
}
|
|
306
328
|
/** Indexed symbols for one provider (or all, concatenated) — for autocomplete. */
|
|
307
329
|
symbolsOf(rawName) {
|
|
308
330
|
if (rawName != null) return this.entries.get(normName(rawName))?.descriptors ?? [];
|
|
@@ -337,8 +359,12 @@ var BarStore = class {
|
|
|
337
359
|
this.series = /* @__PURE__ */ new Map();
|
|
338
360
|
/** Earliest bar-open time fetched for a series — what the cache actually covers. */
|
|
339
361
|
this.coveredFrom = /* @__PURE__ */ new Map();
|
|
340
|
-
/** Symbols protected from the current-symbol purge (multi-chart cells)
|
|
362
|
+
/** Symbols protected from the current-symbol purge (multi-chart cells) — the UNION
|
|
363
|
+
* of every owner's declaration. Empty = legacy single-chart behavior. */
|
|
341
364
|
this.retained = /* @__PURE__ */ new Set();
|
|
365
|
+
/** Per-owner declarations behind {@link retained} — several shells on one page must
|
|
366
|
+
* not clobber (or, on destroy, evict) each other's protected symbols. */
|
|
367
|
+
this.retainedByOwner = /* @__PURE__ */ new Map();
|
|
342
368
|
}
|
|
343
369
|
get(key) {
|
|
344
370
|
return this.series.get(key);
|
|
@@ -382,14 +408,21 @@ var BarStore = class {
|
|
|
382
408
|
* Declare the set of symbols a multi-chart workspace is displaying (CANONICAL
|
|
383
409
|
* tickers, post-registry resolution — `chart.data.resolve(sym).ticker`). These
|
|
384
410
|
* 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
|
-
*
|
|
411
|
+
* stop evicting each other's history. Replaces the previous set FOR THAT OWNER
|
|
412
|
+
* (pass the shell instance as `owner`; several shells on one page keep separate
|
|
413
|
+
* declarations, the effective set is their union) and purges anything now outside
|
|
414
|
+
* the union ∪ {currentSymbol} immediately. An empty set releases the owner's
|
|
415
|
+
* declaration — with no owners left, the legacy single-chart policy is back.
|
|
416
|
+
* Note: SECONDARY symbols a script fetches (`request.security` cross-symbol) are
|
|
417
|
+
* not in this set and still drop on cross-cell loads — correctness is unaffected
|
|
418
|
+
* (they re-fetch on demand).
|
|
390
419
|
*/
|
|
391
|
-
retain(symbols) {
|
|
392
|
-
|
|
420
|
+
retain(symbols, owner = "default") {
|
|
421
|
+
if (symbols.size === 0) this.retainedByOwner.delete(owner);
|
|
422
|
+
else this.retainedByOwner.set(owner, new Set(symbols));
|
|
423
|
+
const union = /* @__PURE__ */ new Set();
|
|
424
|
+
for (const set of this.retainedByOwner.values()) for (const s of set) union.add(s);
|
|
425
|
+
this.retained = union;
|
|
393
426
|
this.purgeOutside(this.currentSymbol);
|
|
394
427
|
}
|
|
395
428
|
/** Drop every series whose symbol is neither `current` nor retained. */
|
|
@@ -644,6 +677,14 @@ var MultiProviderFeed = class {
|
|
|
644
677
|
providerInstance(name) {
|
|
645
678
|
return this.registry.get(name);
|
|
646
679
|
}
|
|
680
|
+
/** The icon URL for a DESCRIPTOR — its owning provider's `resolveSymbolIcon` (picker rows). */
|
|
681
|
+
symbolIconOf(d) {
|
|
682
|
+
return this.registry.symbolIconOf(d);
|
|
683
|
+
}
|
|
684
|
+
/** The icon URL for a raw SYMBOL string — resolve, then route (statusline, object tree). */
|
|
685
|
+
symbolIcon(raw) {
|
|
686
|
+
return this.registry.symbolIcon(this.resolveSymbol(raw));
|
|
687
|
+
}
|
|
647
688
|
symbols(name) {
|
|
648
689
|
return this.registry.symbolsOf(name);
|
|
649
690
|
}
|
|
@@ -4543,6 +4584,29 @@ function tickerModifierIds() {
|
|
|
4543
4584
|
return [...registry2.values()].filter((d) => d.tickerModifier ?? d.barTransform != null).map((d) => d.id);
|
|
4544
4585
|
}
|
|
4545
4586
|
|
|
4587
|
+
// src/widget/topbar-composition.ts
|
|
4588
|
+
var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
|
|
4589
|
+
var TOPBAR_DEFAULT_LEFT = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo"];
|
|
4590
|
+
var TOPBAR_DEFAULT_RIGHT = ["actions", "alerts", "panels", "screenshot"];
|
|
4591
|
+
function resolveTopbarComposition(opt) {
|
|
4592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4593
|
+
const take = (list) => {
|
|
4594
|
+
const out = [];
|
|
4595
|
+
const local = /* @__PURE__ */ new Set();
|
|
4596
|
+
for (const id of list) {
|
|
4597
|
+
if (!id || local.has(id) || id !== "actions" && seen.has(id)) continue;
|
|
4598
|
+
local.add(id);
|
|
4599
|
+
seen.add(id);
|
|
4600
|
+
out.push(id);
|
|
4601
|
+
}
|
|
4602
|
+
return out;
|
|
4603
|
+
};
|
|
4604
|
+
return { left: take(opt?.left ?? TOPBAR_DEFAULT_LEFT), right: take(opt?.right ?? TOPBAR_DEFAULT_RIGHT) };
|
|
4605
|
+
}
|
|
4606
|
+
function topbarHas(comp, id) {
|
|
4607
|
+
return comp.left.includes(id) || comp.right.includes(id);
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4546
4610
|
// src/widget/contributions.ts
|
|
4547
4611
|
var registry3 = /* @__PURE__ */ new Map();
|
|
4548
4612
|
var attachments = /* @__PURE__ */ new Map();
|
|
@@ -4554,6 +4618,13 @@ var DEFAULT_PANEL_ORDER = 100;
|
|
|
4554
4618
|
function sidePanels() {
|
|
4555
4619
|
return [...panels.values()].sort((a, b) => (a.order ?? DEFAULT_PANEL_ORDER) - (b.order ?? DEFAULT_PANEL_ORDER));
|
|
4556
4620
|
}
|
|
4621
|
+
var OVERRIDABLE_TOPBAR_IDS = ["indicators", "screenshot"];
|
|
4622
|
+
var overridableSet = new Set(OVERRIDABLE_TOPBAR_IDS);
|
|
4623
|
+
new Set(TOPBAR_BUILTIN_IDS);
|
|
4624
|
+
function topbarActionOverride(id) {
|
|
4625
|
+
if (!overridableSet.has(id)) return void 0;
|
|
4626
|
+
return registry3.get(id);
|
|
4627
|
+
}
|
|
4557
4628
|
function widgetActions(target, ctx) {
|
|
4558
4629
|
const list = [...registry3.values()].filter((d) => d.target === target);
|
|
4559
4630
|
list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
@@ -4571,6 +4642,10 @@ function legendActionsProviderFor(chart, context) {
|
|
|
4571
4642
|
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
4643
|
};
|
|
4573
4644
|
}
|
|
4645
|
+
var stateHandlers = /* @__PURE__ */ new Map();
|
|
4646
|
+
function statePersistenceHandlers(scope) {
|
|
4647
|
+
return [...stateHandlers.values()].filter((h) => h.scope === scope);
|
|
4648
|
+
}
|
|
4574
4649
|
var defaultEngines = /* @__PURE__ */ new Map();
|
|
4575
4650
|
function resolveEngines(overrides) {
|
|
4576
4651
|
return { ...Object.fromEntries(defaultEngines), ...overrides };
|
|
@@ -5053,9 +5128,14 @@ var CSS3 = `
|
|
|
5053
5128
|
align-items: center;
|
|
5054
5129
|
justify-content: center;
|
|
5055
5130
|
}
|
|
5056
|
-
|
|
5131
|
+
/* The right side of the bar \u2014 whatever the composition puts there rides this one
|
|
5132
|
+
auto-margin push (the flow-actions host used to carry it; composition can omit it). */
|
|
5133
|
+
.vela-topbar-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
5134
|
+
.vela-widget-actions { display: inline-flex; gap: var(--vela-space-1); }
|
|
5057
5135
|
/* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
|
|
5058
5136
|
.vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
5137
|
+
/* One PINNED contributed action's slot (a composition entry naming the action's id). */
|
|
5138
|
+
.vela-widget-action-pin { display: inline-flex; align-items: center; }
|
|
5059
5139
|
/* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
|
|
5060
5140
|
without disturbing the tools around it. */
|
|
5061
5141
|
.vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
@@ -5114,6 +5194,12 @@ var Topbar = class {
|
|
|
5114
5194
|
this.tooltips = [];
|
|
5115
5195
|
this.panelBtns = /* @__PURE__ */ new Map();
|
|
5116
5196
|
this.panelTooltips = [];
|
|
5197
|
+
/** Pinned contributed-action slots, by action id (composition entries that name one,
|
|
5198
|
+
* plus built-in slots taken over by an override). */
|
|
5199
|
+
this.pinned = /* @__PURE__ */ new Map();
|
|
5200
|
+
/** Overrides that LEFT their native slot (default side + a declared `order`) — they
|
|
5201
|
+
* render through the flow cluster like ordinary actions. */
|
|
5202
|
+
this.flowingOverrides = /* @__PURE__ */ new Set();
|
|
5117
5203
|
this.onHairlineSync = () => {
|
|
5118
5204
|
if (this.hairlineRaf) return;
|
|
5119
5205
|
const win = this.el.ownerDocument.defaultView;
|
|
@@ -5128,6 +5214,8 @@ var Topbar = class {
|
|
|
5128
5214
|
this.host = host;
|
|
5129
5215
|
this.timeframe = opts.timeframe;
|
|
5130
5216
|
this.priceStyle = opts.priceStyle;
|
|
5217
|
+
this.comp = resolveTopbarComposition(opts.composition);
|
|
5218
|
+
const vis = (id) => topbarHas(this.comp, id);
|
|
5131
5219
|
const doc = host.ownerDocument;
|
|
5132
5220
|
injectStyles(STYLE_ID2, CSS3, doc);
|
|
5133
5221
|
this.el = doc.createElement("div");
|
|
@@ -5150,7 +5238,7 @@ var Topbar = class {
|
|
|
5150
5238
|
this.styleButton.className = "vela-widget-style";
|
|
5151
5239
|
this.renderStyleButton(doc);
|
|
5152
5240
|
let indicatorsBtn = null;
|
|
5153
|
-
if (opts.onIndicatorsClick) {
|
|
5241
|
+
if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
|
|
5154
5242
|
indicatorsBtn = doc.createElement("button");
|
|
5155
5243
|
indicatorsBtn.className = "vela-widget-indicators";
|
|
5156
5244
|
indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
|
|
@@ -5163,18 +5251,23 @@ var Topbar = class {
|
|
|
5163
5251
|
this.panelsHost = doc.createElement("span");
|
|
5164
5252
|
this.panelsHost.className = "vela-widget-panels";
|
|
5165
5253
|
const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
|
|
5166
|
-
|
|
5167
|
-
|
|
5254
|
+
if (vis("undo-redo")) {
|
|
5255
|
+
this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
|
|
5256
|
+
this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
|
|
5257
|
+
} else {
|
|
5258
|
+
this.undoBtn = doc.createElement("button");
|
|
5259
|
+
this.redoBtn = doc.createElement("button");
|
|
5260
|
+
}
|
|
5168
5261
|
this.setHistoryState(false, false);
|
|
5169
|
-
const screenshotBtn = tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick);
|
|
5170
|
-
this.alertsBtn = tool("vela-widget-alerts", "bell", "Alerts");
|
|
5262
|
+
const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
|
|
5263
|
+
this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
|
|
5171
5264
|
this.alertsBtn.style.position = "relative";
|
|
5172
5265
|
if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
|
|
5173
5266
|
this.alertsBadge = doc.createElement("span");
|
|
5174
5267
|
this.alertsBadge.className = "vela-alerts-badge";
|
|
5175
5268
|
this.alertsBadge.style.display = "none";
|
|
5176
5269
|
this.alertsBtn.appendChild(this.alertsBadge);
|
|
5177
|
-
if (opts.layout) {
|
|
5270
|
+
if (opts.layout && vis("layout")) {
|
|
5178
5271
|
this.layoutId = opts.layout.current;
|
|
5179
5272
|
this.layoutButton = doc.createElement("button");
|
|
5180
5273
|
this.layoutButton.className = "vela-widget-style";
|
|
@@ -5185,12 +5278,65 @@ var Topbar = class {
|
|
|
5185
5278
|
d.className = "vela-sep";
|
|
5186
5279
|
return d;
|
|
5187
5280
|
};
|
|
5188
|
-
const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
|
|
5189
|
-
if (this.layoutButton) leading.push(this.layoutButton, sep());
|
|
5190
|
-
if (indicatorsBtn) leading.push(indicatorsBtn, sep());
|
|
5191
5281
|
this.leftActionsSep = sep();
|
|
5192
5282
|
this.leftActionsSep.hidden = true;
|
|
5193
|
-
|
|
5283
|
+
const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
|
|
5284
|
+
const pinSlot = (id, left) => {
|
|
5285
|
+
const slot = doc.createElement("span");
|
|
5286
|
+
slot.className = "vela-widget-action-pin";
|
|
5287
|
+
this.pinned.set(id, { host: slot, left });
|
|
5288
|
+
return [slot];
|
|
5289
|
+
};
|
|
5290
|
+
const overridden = (id, left) => {
|
|
5291
|
+
const ov = topbarActionOverride(id);
|
|
5292
|
+
if (!ov) return null;
|
|
5293
|
+
const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
|
|
5294
|
+
if (!sideDeclared && ov.order !== void 0) {
|
|
5295
|
+
this.flowingOverrides.add(id);
|
|
5296
|
+
return [];
|
|
5297
|
+
}
|
|
5298
|
+
return pinSlot(id, left);
|
|
5299
|
+
};
|
|
5300
|
+
const elementsFor = (id, left) => {
|
|
5301
|
+
switch (id) {
|
|
5302
|
+
case "symbol":
|
|
5303
|
+
return [this.symbolEl];
|
|
5304
|
+
case "timeframes":
|
|
5305
|
+
return [tfGroup];
|
|
5306
|
+
case "style":
|
|
5307
|
+
return [this.styleButton];
|
|
5308
|
+
case "layout":
|
|
5309
|
+
return this.layoutButton ? [this.layoutButton] : [];
|
|
5310
|
+
case "indicators":
|
|
5311
|
+
return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
|
|
5312
|
+
case "actions":
|
|
5313
|
+
return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
|
|
5314
|
+
case "undo-redo":
|
|
5315
|
+
return [this.undoBtn, this.redoBtn];
|
|
5316
|
+
case "alerts":
|
|
5317
|
+
return [this.alertsBtn];
|
|
5318
|
+
case "panels":
|
|
5319
|
+
return [this.panelsHost];
|
|
5320
|
+
case "screenshot":
|
|
5321
|
+
return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
|
|
5322
|
+
default:
|
|
5323
|
+
return pinSlot(id, left);
|
|
5324
|
+
}
|
|
5325
|
+
};
|
|
5326
|
+
const sideEls = (list, left) => {
|
|
5327
|
+
const out = [];
|
|
5328
|
+
for (const [i, id] of list.entries()) {
|
|
5329
|
+
const els = elementsFor(id, left);
|
|
5330
|
+
if (els.length === 0) continue;
|
|
5331
|
+
out.push(...els);
|
|
5332
|
+
if (primaries.has(id) && i < list.length - 1) out.push(sep());
|
|
5333
|
+
}
|
|
5334
|
+
return out;
|
|
5335
|
+
};
|
|
5336
|
+
const right = doc.createElement("span");
|
|
5337
|
+
right.className = "vela-topbar-right";
|
|
5338
|
+
right.append(...sideEls(this.comp.right, false));
|
|
5339
|
+
this.el.append(...sideEls(this.comp.left, true), right);
|
|
5194
5340
|
host.appendChild(this.el);
|
|
5195
5341
|
this.renderTfChips();
|
|
5196
5342
|
this.onHairlineSync();
|
|
@@ -5307,14 +5453,24 @@ var Topbar = class {
|
|
|
5307
5453
|
else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
|
|
5308
5454
|
this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
|
|
5309
5455
|
}
|
|
5310
|
-
/** Re-project the contributed topbar actions (call after registrations change).
|
|
5456
|
+
/** Re-project the contributed topbar actions (call after registrations change).
|
|
5457
|
+
* An action PINNED by the composition renders into its named slot (list position
|
|
5458
|
+
* wins over `align`/`order`); the rest flow into the side's `actions` slot — or
|
|
5459
|
+
* not at all when an explicit list omits it (the list is the side's contract). */
|
|
5311
5460
|
renderActions() {
|
|
5312
5461
|
const ctx = this.opts.getContext?.();
|
|
5313
5462
|
this.actionsHost.replaceChildren();
|
|
5314
5463
|
this.leftActionsHost.replaceChildren();
|
|
5464
|
+
for (const pin of this.pinned.values()) pin.host.replaceChildren();
|
|
5315
5465
|
const doc = this.actionsHost.ownerDocument;
|
|
5466
|
+
const flowLeft = this.comp.left.includes("actions");
|
|
5467
|
+
const flowRight = this.comp.right.includes("actions");
|
|
5468
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
5316
5469
|
for (const action of widgetActions("topbar", ctx)) {
|
|
5317
|
-
const
|
|
5470
|
+
const pin = this.pinned.get(action.id);
|
|
5471
|
+
if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
|
|
5472
|
+
const left = pin ? pin.left : action.align === "left";
|
|
5473
|
+
if (!pin && !(left ? flowLeft : flowRight)) continue;
|
|
5318
5474
|
const b = doc.createElement("button");
|
|
5319
5475
|
b.className = left ? "vela-widget-action-left" : "vela-widget-action";
|
|
5320
5476
|
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
@@ -5323,7 +5479,7 @@ var Topbar = class {
|
|
|
5323
5479
|
const c = this.opts.getContext?.();
|
|
5324
5480
|
if (c) action.run(c);
|
|
5325
5481
|
});
|
|
5326
|
-
(left ? this.leftActionsHost : this.actionsHost).appendChild(b);
|
|
5482
|
+
(pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
|
|
5327
5483
|
}
|
|
5328
5484
|
this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
|
|
5329
5485
|
this.onHairlineSync();
|
|
@@ -5716,41 +5872,39 @@ var Bottombar = class {
|
|
|
5716
5872
|
}
|
|
5717
5873
|
};
|
|
5718
5874
|
|
|
5719
|
-
// src/
|
|
5720
|
-
var iconFailed = /* @__PURE__ */ new Set();
|
|
5721
|
-
function initialsOf(name) {
|
|
5722
|
-
return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
|
|
5723
|
-
}
|
|
5724
|
-
function cryptoIconUrl(base) {
|
|
5725
|
-
return `https://crypto-icons.ledger.com/${encodeURIComponent(base.toUpperCase())}.png`;
|
|
5726
|
-
}
|
|
5875
|
+
// src/data/symbol-base.ts
|
|
5727
5876
|
function baseOf(d) {
|
|
5728
5877
|
const fromDesc = d.description?.split("/")[0]?.trim();
|
|
5729
5878
|
if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
|
|
5730
5879
|
return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
|
|
5731
5880
|
}
|
|
5732
|
-
|
|
5881
|
+
|
|
5882
|
+
// src/widget/symbol-icon.ts
|
|
5883
|
+
var iconFailed = /* @__PURE__ */ new Set();
|
|
5884
|
+
function initialsOf(name) {
|
|
5885
|
+
return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
|
|
5886
|
+
}
|
|
5887
|
+
function tickerIconEl(doc, base, name, className, iconUrl) {
|
|
5733
5888
|
const wrap = doc.createElement("span");
|
|
5734
5889
|
wrap.className = className;
|
|
5735
|
-
const key = base.toUpperCase();
|
|
5736
5890
|
const fallback = () => {
|
|
5737
5891
|
wrap.replaceChildren();
|
|
5738
5892
|
wrap.style.background = categoricalColor(name);
|
|
5739
5893
|
wrap.textContent = initialsOf(base || name);
|
|
5740
5894
|
};
|
|
5741
|
-
if (!
|
|
5895
|
+
if (!iconUrl || iconFailed.has(iconUrl)) {
|
|
5742
5896
|
fallback();
|
|
5743
5897
|
return wrap;
|
|
5744
5898
|
}
|
|
5745
5899
|
const img = doc.createElement("img");
|
|
5746
5900
|
img.alt = "";
|
|
5747
5901
|
img.crossOrigin = "anonymous";
|
|
5748
|
-
img.src =
|
|
5902
|
+
img.src = iconUrl;
|
|
5749
5903
|
img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
|
|
5750
5904
|
img.addEventListener(
|
|
5751
5905
|
"error",
|
|
5752
5906
|
() => {
|
|
5753
|
-
iconFailed.add(
|
|
5907
|
+
iconFailed.add(iconUrl);
|
|
5754
5908
|
fallback();
|
|
5755
5909
|
},
|
|
5756
5910
|
{ once: true }
|
|
@@ -12003,11 +12157,14 @@ var MenuBuilder = class {
|
|
|
12003
12157
|
}
|
|
12004
12158
|
};
|
|
12005
12159
|
var ObjectTree = class extends SidePanel {
|
|
12006
|
-
constructor(host) {
|
|
12160
|
+
constructor(host, iconFor) {
|
|
12007
12161
|
super(host, "Object tree", "vela-ot");
|
|
12162
|
+
this.iconFor = iconFor;
|
|
12008
12163
|
this.chart = null;
|
|
12009
12164
|
this.selectedDrawing = null;
|
|
12010
12165
|
this.symbolName = "";
|
|
12166
|
+
/** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
|
|
12167
|
+
this.symbolRaw = "";
|
|
12011
12168
|
/** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
|
|
12012
12169
|
* Kept per chart because a workspace points this one panel at whichever chart is active, and
|
|
12013
12170
|
* each chart's bundles have to survive the switch. */
|
|
@@ -12044,6 +12201,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
12044
12201
|
if (open2) this.refresh();
|
|
12045
12202
|
}
|
|
12046
12203
|
setSymbol(symbol) {
|
|
12204
|
+
this.symbolRaw = symbol;
|
|
12047
12205
|
this.symbolName = parseSymbol(symbol).ticker;
|
|
12048
12206
|
}
|
|
12049
12207
|
/** (Re)bind to a chart instance — called after every widget rebuild. */
|
|
@@ -12244,7 +12402,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
12244
12402
|
const { chart } = pass;
|
|
12245
12403
|
if (row.kind === "price") {
|
|
12246
12404
|
const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
|
|
12247
|
-
const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar");
|
|
12405
|
+
const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
|
|
12248
12406
|
const el2 = this.row(icon2, row.label, row.visible, [
|
|
12249
12407
|
{
|
|
12250
12408
|
icon: row.visible ? "eye" : "eye-off",
|
|
@@ -13274,8 +13432,6 @@ var PanelDock = class {
|
|
|
13274
13432
|
for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
|
|
13275
13433
|
}
|
|
13276
13434
|
};
|
|
13277
|
-
|
|
13278
|
-
// src/widget/symbol-picker.ts
|
|
13279
13435
|
var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
13280
13436
|
function parseQuery(raw, venues) {
|
|
13281
13437
|
const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
|
|
@@ -13290,17 +13446,18 @@ function onlyOne(matches) {
|
|
|
13290
13446
|
return matches.length === 1 ? matches[0] : null;
|
|
13291
13447
|
}
|
|
13292
13448
|
var venueOf = (s) => s.prefix ?? s.provider;
|
|
13293
|
-
function filterSymbols(list, query, limit = 100) {
|
|
13449
|
+
function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
|
|
13294
13450
|
const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
|
|
13295
13451
|
const { scope, term } = parseQuery(query, venues);
|
|
13296
13452
|
const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
|
|
13297
13453
|
const q = term.toUpperCase();
|
|
13298
13454
|
if (!q) {
|
|
13299
13455
|
if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
|
|
13456
|
+
if (top === false) return pool.slice(0, limit);
|
|
13300
13457
|
const byTicker = new Map(pool.map((s) => [s.ticker.toUpperCase(), s]));
|
|
13301
|
-
const
|
|
13302
|
-
const rest = pool.filter((s) => !
|
|
13303
|
-
return [...
|
|
13458
|
+
const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
|
|
13459
|
+
const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
|
|
13460
|
+
return [...pinned, ...rest].slice(0, limit);
|
|
13304
13461
|
}
|
|
13305
13462
|
const qLower = term.toLowerCase();
|
|
13306
13463
|
const prefix = [];
|
|
@@ -13410,12 +13567,16 @@ var CSS8 = `
|
|
|
13410
13567
|
var PAGE = 100;
|
|
13411
13568
|
var SymbolPicker = class {
|
|
13412
13569
|
constructor(opts) {
|
|
13570
|
+
this.opts = opts;
|
|
13413
13571
|
this.source = () => [];
|
|
13414
13572
|
this.rows = [];
|
|
13415
13573
|
this.highlighted = 0;
|
|
13416
13574
|
this.seed = "";
|
|
13417
13575
|
this.activeTab = "All";
|
|
13418
13576
|
this.visible = PAGE;
|
|
13577
|
+
/** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
|
|
13578
|
+
this.ranked = null;
|
|
13579
|
+
this.ranking = false;
|
|
13419
13580
|
const doc = (opts.host ?? document.body).ownerDocument;
|
|
13420
13581
|
injectStyles(STYLE_ID7, CSS8, doc);
|
|
13421
13582
|
this.input = doc.createElement("input");
|
|
@@ -13513,10 +13674,19 @@ var SymbolPicker = class {
|
|
|
13513
13674
|
} else delete el.dataset.highlighted;
|
|
13514
13675
|
});
|
|
13515
13676
|
}
|
|
13677
|
+
/** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
|
|
13678
|
+
* the hook runs when the pool CHANGES (an index lands or refreshes), never per
|
|
13679
|
+
* keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
|
|
13680
|
+
* in between, the raw pool before the first resolve). */
|
|
13681
|
+
pool() {
|
|
13682
|
+
const raw = this.source();
|
|
13683
|
+
return raw;
|
|
13684
|
+
}
|
|
13516
13685
|
computeRows() {
|
|
13517
13686
|
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
13518
|
-
const
|
|
13519
|
-
|
|
13687
|
+
const all = this.pool();
|
|
13688
|
+
const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
13689
|
+
return filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
|
|
13520
13690
|
}
|
|
13521
13691
|
refresh() {
|
|
13522
13692
|
const doc = this.list.ownerDocument;
|
|
@@ -13547,7 +13717,7 @@ var SymbolPicker = class {
|
|
|
13547
13717
|
row.dataset.ticker = s.ticker;
|
|
13548
13718
|
const venue = s.prefix ?? s.provider;
|
|
13549
13719
|
if (venue) row.dataset.venue = venue;
|
|
13550
|
-
const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
|
|
13720
|
+
const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
|
|
13551
13721
|
const main = doc.createElement("span");
|
|
13552
13722
|
main.className = "vela-sp-main";
|
|
13553
13723
|
const t = doc.createElement("span");
|
|
@@ -14212,10 +14382,11 @@ var MobileBar = class {
|
|
|
14212
14382
|
const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
|
|
14213
14383
|
this.actionsHost = doc.createElement("span");
|
|
14214
14384
|
this.actionsHost.className = "vela-mb-actions";
|
|
14215
|
-
const
|
|
14385
|
+
const onDrawings = opts.onDrawingsClick;
|
|
14386
|
+
const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
|
|
14216
14387
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
14217
14388
|
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);
|
|
14389
|
+
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
|
|
14219
14390
|
host.appendChild(this.el);
|
|
14220
14391
|
this.renderActions();
|
|
14221
14392
|
}
|
|
@@ -14227,7 +14398,8 @@ var MobileBar = class {
|
|
|
14227
14398
|
if (!ctx) return;
|
|
14228
14399
|
const doc = this.el.ownerDocument;
|
|
14229
14400
|
this.actionsHost.replaceChildren();
|
|
14230
|
-
|
|
14401
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
14402
|
+
for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
|
|
14231
14403
|
const b = doc.createElement("button");
|
|
14232
14404
|
b.className = "vela-mb-item";
|
|
14233
14405
|
b.setAttribute("aria-label", action.label);
|
|
@@ -14756,10 +14928,10 @@ var MoreDrawer = class {
|
|
|
14756
14928
|
});
|
|
14757
14929
|
actions.appendChild(b);
|
|
14758
14930
|
};
|
|
14759
|
-
action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
|
|
14760
|
-
action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
|
|
14761
|
-
action("camera", "Screenshot", true, this.opts.onScreenshot);
|
|
14762
|
-
this.drawer.body.appendChild(actions);
|
|
14931
|
+
if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
|
|
14932
|
+
if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
|
|
14933
|
+
if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
|
|
14934
|
+
if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
|
|
14763
14935
|
const list = doc.createElement("div");
|
|
14764
14936
|
list.className = "vela-md-list";
|
|
14765
14937
|
const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
|
|
@@ -14780,8 +14952,10 @@ var MoreDrawer = class {
|
|
|
14780
14952
|
})
|
|
14781
14953
|
);
|
|
14782
14954
|
}
|
|
14783
|
-
|
|
14784
|
-
|
|
14955
|
+
if (this.opts.alerts) {
|
|
14956
|
+
const alertCount = this.opts.alerts().length;
|
|
14957
|
+
list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
|
|
14958
|
+
}
|
|
14785
14959
|
for (const act of this.opts.actions()) {
|
|
14786
14960
|
list.appendChild(
|
|
14787
14961
|
this.row(doc, act.label, {
|
|
@@ -14862,7 +15036,7 @@ var MoreDrawer = class {
|
|
|
14862
15036
|
this.drawer.body.appendChild(list);
|
|
14863
15037
|
}
|
|
14864
15038
|
renderAlerts(doc) {
|
|
14865
|
-
const alerts = this.opts.alerts();
|
|
15039
|
+
const alerts = this.opts.alerts?.() ?? [];
|
|
14866
15040
|
if (alerts.length === 0) {
|
|
14867
15041
|
const empty = doc.createElement("div");
|
|
14868
15042
|
empty.className = "vela-md-empty";
|
|
@@ -17030,6 +17204,35 @@ function rendererDefaults() {
|
|
|
17030
17204
|
return Object.fromEntries(defaults);
|
|
17031
17205
|
}
|
|
17032
17206
|
|
|
17207
|
+
// src/core/price-styles/heikin-ashi.ts
|
|
17208
|
+
function heikinAshiNext(raw, prevHa) {
|
|
17209
|
+
const haClose = (raw.open + raw.high + raw.low + raw.close) / 4;
|
|
17210
|
+
const haOpen = prevHa ? (prevHa.open + prevHa.close) / 2 : (raw.open + raw.close) / 2;
|
|
17211
|
+
return {
|
|
17212
|
+
time: raw.time,
|
|
17213
|
+
open: haOpen,
|
|
17214
|
+
high: Math.max(raw.high, haOpen, haClose),
|
|
17215
|
+
low: Math.min(raw.low, haOpen, haClose),
|
|
17216
|
+
close: haClose,
|
|
17217
|
+
...raw.volume != null ? { volume: raw.volume } : {}
|
|
17218
|
+
};
|
|
17219
|
+
}
|
|
17220
|
+
function heikinAshiFull(raw) {
|
|
17221
|
+
const out = new Array(raw.length);
|
|
17222
|
+
let prev2;
|
|
17223
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
17224
|
+
prev2 = heikinAshiNext(raw[i], prev2);
|
|
17225
|
+
out[i] = prev2;
|
|
17226
|
+
}
|
|
17227
|
+
return out;
|
|
17228
|
+
}
|
|
17229
|
+
|
|
17230
|
+
// src/chart-types/builtins.ts
|
|
17231
|
+
var HEIKIN_ASHI = { full: heikinAshiFull, next: heikinAshiNext };
|
|
17232
|
+
function registerBuiltinChartTypes() {
|
|
17233
|
+
registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
|
|
17234
|
+
}
|
|
17235
|
+
|
|
17033
17236
|
// src/workspace/sync.ts
|
|
17034
17237
|
function syncTargets(originId, setting, cellIds) {
|
|
17035
17238
|
if (setting == null || setting === false) return [];
|
|
@@ -17089,8 +17292,18 @@ function sanitizeState(doc) {
|
|
|
17089
17292
|
if (tracks) out.trackSizes = tracks;
|
|
17090
17293
|
const panels2 = sanitizePanels(d.panels);
|
|
17091
17294
|
if (panels2) out.panels = panels2;
|
|
17295
|
+
const ext = sanitizeExt(d.ext);
|
|
17296
|
+
if (ext) out.ext = ext;
|
|
17092
17297
|
return out;
|
|
17093
17298
|
}
|
|
17299
|
+
function sanitizeExt(raw) {
|
|
17300
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
17301
|
+
const out = {};
|
|
17302
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
17303
|
+
if (key.length > 0 && value !== void 0) out[key] = value;
|
|
17304
|
+
}
|
|
17305
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
17306
|
+
}
|
|
17094
17307
|
function sanitizeCell(raw) {
|
|
17095
17308
|
if (raw == null || typeof raw !== "object") return null;
|
|
17096
17309
|
const c = raw;
|
|
@@ -17112,6 +17325,8 @@ function sanitizeCell(raw) {
|
|
|
17112
17325
|
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
17113
17326
|
out.indicators = { manifest, natives };
|
|
17114
17327
|
}
|
|
17328
|
+
const ext = sanitizeExt(c.ext);
|
|
17329
|
+
if (ext) out.ext = ext;
|
|
17115
17330
|
return out;
|
|
17116
17331
|
}
|
|
17117
17332
|
function sanitizeSync(raw) {
|
|
@@ -18444,7 +18659,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18444
18659
|
this.emitScriptRun(id, cause, first);
|
|
18445
18660
|
},
|
|
18446
18661
|
onAlert: (a) => {
|
|
18447
|
-
|
|
18662
|
+
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
18663
|
+
this.events.emit("alert", { ...a, indicator });
|
|
18448
18664
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
18449
18665
|
},
|
|
18450
18666
|
onWarning: (w) => this.events.emit("warning", w),
|
|
@@ -18492,6 +18708,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18492
18708
|
overlay: d.overlay,
|
|
18493
18709
|
paneHint: d.paneHint,
|
|
18494
18710
|
native: { type: record.native.type },
|
|
18711
|
+
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
18495
18712
|
series: out.series ?? [],
|
|
18496
18713
|
fills: out.fills ?? [],
|
|
18497
18714
|
backgrounds: out.backgrounds ?? [],
|
|
@@ -19086,6 +19303,24 @@ var RendererControl = class {
|
|
|
19086
19303
|
this.renderer.setSettingsSections?.(sections);
|
|
19087
19304
|
return this;
|
|
19088
19305
|
}
|
|
19306
|
+
/**
|
|
19307
|
+
* Set the settings-dialog visibility policy: `hidden` lists setting ids to hide —
|
|
19308
|
+
* a tab (`'canvas'`), a group (`'canvas.grid'`), or a single row
|
|
19309
|
+
* (`'canvas.grid.vertical'`); an id hides its whole subtree, and a tab with nothing
|
|
19310
|
+
* left disappears from the rail. Presentation-only: hidden values keep being stored
|
|
19311
|
+
* and applied (e.g. hide `'advanced'` while forcing the widget's `bars` option).
|
|
19312
|
+
* Seeded from `VelaOptions.settings`; silent no-op without a settings dialog.
|
|
19313
|
+
*/
|
|
19314
|
+
setSettingsVisibility(policy) {
|
|
19315
|
+
this.renderer.setSettingsVisibility?.(policy);
|
|
19316
|
+
return this;
|
|
19317
|
+
}
|
|
19318
|
+
/** Every addressable setting id of this chart (built-in tabs/groups/rows, chart-type
|
|
19319
|
+
* sections, host sections) — enumerate these to build a `hidden` list instead of
|
|
19320
|
+
* reading contributor source. Empty on a renderer without a settings dialog. */
|
|
19321
|
+
listSettingsIds() {
|
|
19322
|
+
return this.renderer.listSettingsIds?.() ?? [];
|
|
19323
|
+
}
|
|
19089
19324
|
/** Tell the renderer's own chrome which size class the host shell is in —
|
|
19090
19325
|
* `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
|
|
19091
19326
|
* Silent no-op on a renderer without adaptive chrome. */
|
|
@@ -19200,6 +19435,12 @@ var DataControl = class {
|
|
|
19200
19435
|
symbols(provider) {
|
|
19201
19436
|
return this.registry?.symbols(provider) ?? [];
|
|
19202
19437
|
}
|
|
19438
|
+
/** The icon URL for `symbol` — its owning provider's `resolveSymbolIcon`, routed
|
|
19439
|
+
* through resolution. Undefined while unresolvable, when the provider declares no
|
|
19440
|
+
* resolver, or on a custom `deps.dataFeed` — the shells then show initials. */
|
|
19441
|
+
symbolIcon(symbol) {
|
|
19442
|
+
return this.registry?.symbolIcon(symbol);
|
|
19443
|
+
}
|
|
19203
19444
|
/** Per-symbol metadata (Pine `syminfo.*`), resolved through the owning provider. */
|
|
19204
19445
|
symbolInfo(symbol) {
|
|
19205
19446
|
return this.registry?.symbolInfoFor(symbol) ?? Promise.resolve(void 0);
|
|
@@ -21468,658 +21709,6 @@ function supportsWebGL2() {
|
|
|
21468
21709
|
return webgl2Probe;
|
|
21469
21710
|
}
|
|
21470
21711
|
|
|
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
21712
|
// src/renderers/native/backend/candle-lod.ts
|
|
22124
21713
|
var CANDLE_BODY_MIN_SPACING = 3;
|
|
22125
21714
|
var CANDLE_WICK_W = 1.5;
|
|
@@ -22409,11 +21998,6 @@ var DASH = {
|
|
|
22409
21998
|
dashed: [6, 4],
|
|
22410
21999
|
dotted: [2, 3]
|
|
22411
22000
|
};
|
|
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
22001
|
function joinSegments(hw) {
|
|
22418
22002
|
return Math.max(6, Math.min(20, Math.round(hw * 4)));
|
|
22419
22003
|
}
|
|
@@ -22423,7 +22007,6 @@ var WebGL2Backend = class {
|
|
|
22423
22007
|
this.modelAlpha = 1;
|
|
22424
22008
|
this.candleBodyAlpha = 1;
|
|
22425
22009
|
this.candleStructureAlpha = 1;
|
|
22426
|
-
this.gridAlpha = 1;
|
|
22427
22010
|
this.candleBodyScale = 1;
|
|
22428
22011
|
/** Set by the renderer; invoked after a context restore to request a repaint. */
|
|
22429
22012
|
this.onNeedsRedraw = null;
|
|
@@ -22653,11 +22236,7 @@ var WebGL2Backend = class {
|
|
|
22653
22236
|
}
|
|
22654
22237
|
};
|
|
22655
22238
|
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);
|
|
22239
|
+
if (!pane.collapsed) {
|
|
22661
22240
|
const models = scene.orderedIndicatorsForPane(pane.id);
|
|
22662
22241
|
const isPrice = pane.kind === "price";
|
|
22663
22242
|
const effPane = (m) => {
|
|
@@ -22842,49 +22421,6 @@ var WebGL2Backend = class {
|
|
|
22842
22421
|
this.screenProgram = null;
|
|
22843
22422
|
}
|
|
22844
22423
|
// ── 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
22424
|
emitBackground(b, bg, pane, coords) {
|
|
22889
22425
|
const x1 = coords.timeToX(bg.from);
|
|
22890
22426
|
const x2 = coords.timeToX(bg.to);
|
|
@@ -24207,6 +23743,445 @@ var KeyboardController = class {
|
|
|
24207
23743
|
}
|
|
24208
23744
|
};
|
|
24209
23745
|
|
|
23746
|
+
// src/renderers/shared/trade-markers.ts
|
|
23747
|
+
var TRADE_LONG_COLOR = TRADE_LONG;
|
|
23748
|
+
var TRADE_SHORT_COLOR = TRADE_SHORT;
|
|
23749
|
+
var TRADE_EXIT_COLOR = TRADE_EXIT;
|
|
23750
|
+
function defaultTradeMarkersState() {
|
|
23751
|
+
return {
|
|
23752
|
+
visible: true,
|
|
23753
|
+
labels: true,
|
|
23754
|
+
qty: true,
|
|
23755
|
+
colors: { long: TRADE_LONG_COLOR, short: TRADE_SHORT_COLOR, exit: TRADE_EXIT_COLOR }
|
|
23756
|
+
};
|
|
23757
|
+
}
|
|
23758
|
+
function mergeTradeMarkersState(base, patch) {
|
|
23759
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
23760
|
+
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
23761
|
+
const bool = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
23762
|
+
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
23763
|
+
return {
|
|
23764
|
+
visible: bool(p.visible, base.visible),
|
|
23765
|
+
labels: bool(p.labels, base.labels),
|
|
23766
|
+
qty: bool(p.qty, base.qty),
|
|
23767
|
+
colors: {
|
|
23768
|
+
long: color(c.long, base.colors.long),
|
|
23769
|
+
short: color(c.short, base.colors.short),
|
|
23770
|
+
exit: color(c.exit, base.colors.exit)
|
|
23771
|
+
}
|
|
23772
|
+
};
|
|
23773
|
+
}
|
|
23774
|
+
var BAR_GAP = 10;
|
|
23775
|
+
var ARROW_W = 9;
|
|
23776
|
+
var HEAD_H = 6;
|
|
23777
|
+
var ARROW_H = 14;
|
|
23778
|
+
var STEM_W = 3;
|
|
23779
|
+
var CAP_H = 2;
|
|
23780
|
+
var CAP_GAP = 2;
|
|
23781
|
+
var TEXT_GAP = 3;
|
|
23782
|
+
var UNIT_GAP = 6;
|
|
23783
|
+
var TICK_W = 6;
|
|
23784
|
+
var TICK_H = 8;
|
|
23785
|
+
function lineHeightOf(fontSize) {
|
|
23786
|
+
return fontSize + 4;
|
|
23787
|
+
}
|
|
23788
|
+
function qtyText(exec) {
|
|
23789
|
+
if (exec.qty == null || !Number.isFinite(exec.qty)) return null;
|
|
23790
|
+
const magnitude = Number(Math.abs(exec.qty).toFixed(8));
|
|
23791
|
+
return `${exec.side === "buy" ? "+" : "-"}${magnitude}`;
|
|
23792
|
+
}
|
|
23793
|
+
function unitOf(exec, state, lineH) {
|
|
23794
|
+
const lines = [];
|
|
23795
|
+
if (state.labels && exec.label) lines.push(exec.label);
|
|
23796
|
+
if (state.qty) {
|
|
23797
|
+
const q = qtyText(exec);
|
|
23798
|
+
if (q) lines.push(q);
|
|
23799
|
+
}
|
|
23800
|
+
return { exec, lines, height: ARROW_H + (lines.length ? TEXT_GAP + lines.length * lineH : 0) };
|
|
23801
|
+
}
|
|
23802
|
+
function stacksFor(trades, state, deps, from, to, lineH) {
|
|
23803
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
23804
|
+
for (const exec of trades) {
|
|
23805
|
+
const logical = Math.round(deps.timeToLogical(exec.time));
|
|
23806
|
+
if (logical < from || logical > to) continue;
|
|
23807
|
+
let stack = byBar.get(logical);
|
|
23808
|
+
if (!stack) {
|
|
23809
|
+
stack = { logical, buys: [], sells: [] };
|
|
23810
|
+
byBar.set(logical, stack);
|
|
23811
|
+
}
|
|
23812
|
+
(exec.side === "buy" ? stack.buys : stack.sells).push(unitOf(exec, state, lineH));
|
|
23813
|
+
}
|
|
23814
|
+
return [...byBar.values()];
|
|
23815
|
+
}
|
|
23816
|
+
function stackExtent(units) {
|
|
23817
|
+
if (units.length === 0) return 0;
|
|
23818
|
+
let px = BAR_GAP;
|
|
23819
|
+
for (const u of units) px += u.height;
|
|
23820
|
+
return px + (units.length - 1) * UNIT_GAP;
|
|
23821
|
+
}
|
|
23822
|
+
function tradesPriceHints(trades, state, deps, from, to, fontSize) {
|
|
23823
|
+
if (trades.length === 0) return null;
|
|
23824
|
+
const lineH = lineHeightOf(fontSize);
|
|
23825
|
+
let min = Infinity;
|
|
23826
|
+
let max = -Infinity;
|
|
23827
|
+
let abovePx = 0;
|
|
23828
|
+
let belowPx = 0;
|
|
23829
|
+
for (const stack of stacksFor(trades, state, deps, Math.floor(from), Math.ceil(to), lineH)) {
|
|
23830
|
+
const bar = deps.barAt(stack.logical);
|
|
23831
|
+
if (!bar) continue;
|
|
23832
|
+
if (bar.low < min) min = bar.low;
|
|
23833
|
+
if (bar.high > max) max = bar.high;
|
|
23834
|
+
belowPx = Math.max(belowPx, stackExtent(stack.buys));
|
|
23835
|
+
abovePx = Math.max(abovePx, stackExtent(stack.sells));
|
|
23836
|
+
}
|
|
23837
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
|
|
23838
|
+
return { min, max, abovePx, belowPx };
|
|
23839
|
+
}
|
|
23840
|
+
function renderTradeMarkers(ctx, trades, state, deps, xOf, yOf, text, width, barHalfPx) {
|
|
23841
|
+
if (trades.length === 0) return;
|
|
23842
|
+
const lineH = lineHeightOf(text.fontSize);
|
|
23843
|
+
const stacks = stacksFor(trades, state, deps, -Infinity, Infinity, lineH);
|
|
23844
|
+
if (stacks.length === 0) return;
|
|
23845
|
+
ctx.save();
|
|
23846
|
+
ctx.font = `${text.fontSize}px ${text.fontFamily}`;
|
|
23847
|
+
ctx.textAlign = "center";
|
|
23848
|
+
ctx.textBaseline = "middle";
|
|
23849
|
+
for (const stack of stacks) {
|
|
23850
|
+
const x = xOf(stack.logical);
|
|
23851
|
+
if (x < -150 || x > width + 150) continue;
|
|
23852
|
+
const bar = deps.barAt(stack.logical);
|
|
23853
|
+
if (!bar) continue;
|
|
23854
|
+
const yBottom = Math.max(yOf(bar.low), yOf(bar.high));
|
|
23855
|
+
const yTop = Math.min(yOf(bar.low), yOf(bar.high));
|
|
23856
|
+
let y = yBottom + BAR_GAP;
|
|
23857
|
+
for (const unit of stack.buys) {
|
|
23858
|
+
ctx.fillStyle = colorOf(unit.exec, state.colors);
|
|
23859
|
+
drawArrowUp(ctx, x, y, unit.exec.kind === "exit");
|
|
23860
|
+
drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
|
|
23861
|
+
drawTextLines(ctx, unit.lines, x, y + ARROW_H + TEXT_GAP + lineH / 2, lineH, text.color);
|
|
23862
|
+
y += unit.height + UNIT_GAP;
|
|
23863
|
+
}
|
|
23864
|
+
y = yTop - BAR_GAP;
|
|
23865
|
+
for (const unit of stack.sells) {
|
|
23866
|
+
ctx.fillStyle = colorOf(unit.exec, state.colors);
|
|
23867
|
+
drawArrowDown(ctx, x, y, unit.exec.kind === "exit");
|
|
23868
|
+
drawFillTick(ctx, unit.exec.side, x, yOf(unit.exec.price), barHalfPx);
|
|
23869
|
+
drawTextLines(ctx, unit.lines, x, y - ARROW_H - TEXT_GAP - lineH / 2, -lineH, text.color);
|
|
23870
|
+
y -= unit.height + UNIT_GAP;
|
|
23871
|
+
}
|
|
23872
|
+
}
|
|
23873
|
+
ctx.restore();
|
|
23874
|
+
}
|
|
23875
|
+
function colorOf(exec, colors) {
|
|
23876
|
+
if (exec.kind === "exit") return colors.exit;
|
|
23877
|
+
return exec.side === "buy" ? colors.long : colors.short;
|
|
23878
|
+
}
|
|
23879
|
+
function drawArrowUp(ctx, x, yTip, capped) {
|
|
23880
|
+
ctx.beginPath();
|
|
23881
|
+
ctx.moveTo(x, yTip);
|
|
23882
|
+
ctx.lineTo(x - ARROW_W / 2, yTip + HEAD_H);
|
|
23883
|
+
ctx.lineTo(x + ARROW_W / 2, yTip + HEAD_H);
|
|
23884
|
+
ctx.closePath();
|
|
23885
|
+
ctx.fill();
|
|
23886
|
+
ctx.fillRect(x - STEM_W / 2, yTip + HEAD_H, STEM_W, ARROW_H - HEAD_H);
|
|
23887
|
+
if (capped) ctx.fillRect(x - ARROW_W / 2, yTip - CAP_GAP - CAP_H, ARROW_W, CAP_H);
|
|
23888
|
+
}
|
|
23889
|
+
function drawArrowDown(ctx, x, yTip, capped) {
|
|
23890
|
+
ctx.beginPath();
|
|
23891
|
+
ctx.moveTo(x, yTip);
|
|
23892
|
+
ctx.lineTo(x - ARROW_W / 2, yTip - HEAD_H);
|
|
23893
|
+
ctx.lineTo(x + ARROW_W / 2, yTip - HEAD_H);
|
|
23894
|
+
ctx.closePath();
|
|
23895
|
+
ctx.fill();
|
|
23896
|
+
ctx.fillRect(x - STEM_W / 2, yTip - ARROW_H, STEM_W, ARROW_H - HEAD_H);
|
|
23897
|
+
if (capped) ctx.fillRect(x - ARROW_W / 2, yTip + CAP_GAP, ARROW_W, CAP_H);
|
|
23898
|
+
}
|
|
23899
|
+
function drawFillTick(ctx, side, x, yFill, barHalfPx) {
|
|
23900
|
+
const edge = side === "buy" ? x - barHalfPx : x + barHalfPx;
|
|
23901
|
+
const back = side === "buy" ? edge - TICK_W : edge + TICK_W;
|
|
23902
|
+
ctx.beginPath();
|
|
23903
|
+
ctx.moveTo(edge, yFill);
|
|
23904
|
+
ctx.lineTo(back, yFill - TICK_H / 2);
|
|
23905
|
+
ctx.lineTo(back, yFill + TICK_H / 2);
|
|
23906
|
+
ctx.closePath();
|
|
23907
|
+
ctx.fill();
|
|
23908
|
+
}
|
|
23909
|
+
function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
23910
|
+
if (lines.length === 0) return;
|
|
23911
|
+
ctx.fillStyle = color;
|
|
23912
|
+
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
23913
|
+
}
|
|
23914
|
+
|
|
23915
|
+
// src/renderers/native/core/SceneGraph.ts
|
|
23916
|
+
var SceneGraph = class {
|
|
23917
|
+
constructor() {
|
|
23918
|
+
this.panes = /* @__PURE__ */ new Map();
|
|
23919
|
+
this.indicators = /* @__PURE__ */ new Map();
|
|
23920
|
+
this.bars = [];
|
|
23921
|
+
/** Volume-layer config pushed by the volume native indicator (null ⇒ layer off). Ephemeral. */
|
|
23922
|
+
this.volumeLayer = null;
|
|
23923
|
+
/** Generic native-data channels for SDK renderer layers (`setNativeData(id, …)`). Ephemeral. */
|
|
23924
|
+
this.nativeData = /* @__PURE__ */ new Map();
|
|
23925
|
+
/** Loading ranges per channel (`setNativeData(id + '-pending', …)`). Ephemeral. */
|
|
23926
|
+
this.nativePending = /* @__PURE__ */ new Map();
|
|
23927
|
+
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
23928
|
+
this.vpvrLayer = null;
|
|
23929
|
+
this.crosshair = null;
|
|
23930
|
+
/** How the base price series is drawn on the price pane (candles by default). */
|
|
23931
|
+
this.priceStyle = "candles";
|
|
23932
|
+
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
23933
|
+
this.basePainting = "candles";
|
|
23934
|
+
/** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
|
|
23935
|
+
* candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
|
|
23936
|
+
this.candleOverride = null;
|
|
23937
|
+
/** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
|
|
23938
|
+
* baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
|
|
23939
|
+
* range (resolved per frame via `baselinePriceFor`). */
|
|
23940
|
+
this.baselineValue = null;
|
|
23941
|
+
/** Draw the dashed horizontal line at the latest price (price pane). Independent
|
|
23942
|
+
* of the axis label chip (`showPriceLabel`) — either can show without the other. */
|
|
23943
|
+
this.showPriceLine = true;
|
|
23944
|
+
/** Draw the last-price label chip on the price axis. Independent of the line. */
|
|
23945
|
+
this.showPriceLabel = true;
|
|
23946
|
+
/** Draw the countdown-to-bar-close chip on the price axis. When the price label is
|
|
23947
|
+
* also shown, the two merge into one stacked block (countdown under the label);
|
|
23948
|
+
* when either shows alone it's centered on the latest price level. */
|
|
23949
|
+
this.showCountdown = true;
|
|
23950
|
+
/** Logarithmic price scale on the price pane. */
|
|
23951
|
+
this.logScale = false;
|
|
23952
|
+
/** Inverted price axis on the price pane (high at the bottom). Study panes carry their own. */
|
|
23953
|
+
this.invertScale = false;
|
|
23954
|
+
/** Exchange tick size for the active symbol (e.g. 0.01), when known. Drives the
|
|
23955
|
+
* price-axis decimals — the instrument's true precision instead of the zoom-derived
|
|
23956
|
+
* formula. Undefined until symbol metadata loads (the formula is the fallback). */
|
|
23957
|
+
this.priceMintick = void 0;
|
|
23958
|
+
/** Price-axis mode on the price pane: `'price'` (absolute) or `'percent'` (change
|
|
23959
|
+
* vs `percentBaseline`). Gridlines, axis labels and crosshair chip all follow it. */
|
|
23960
|
+
this.scaleMode = "price";
|
|
23961
|
+
/** Reference price for percent mode (first visible bar's close); recomputed per frame. */
|
|
23962
|
+
this.percentBaseline = 0;
|
|
23963
|
+
/** IANA time zone for the time axis + crosshair/data-window stamps (`'UTC'` default). */
|
|
23964
|
+
this.timezone = "UTC";
|
|
23965
|
+
/** Draw the background gridlines (price + time). Master toggle (`gridlines`
|
|
23966
|
+
* feature); per-axis visibility + colors live in `style.gridVert`/`gridHorz`. */
|
|
23967
|
+
this.showGrid = true;
|
|
23968
|
+
/** Comprehensive cosmetic config (item 15): grid colors, crosshair, candle
|
|
23969
|
+
* border/wick, fonts, separators. Serialized via the renderer's `getConfig()`/
|
|
23970
|
+
* `applyConfig()`; every draw layer reads its knobs from here, falling back to
|
|
23971
|
+
* the theme for any value left at its inherit default. */
|
|
23972
|
+
this.style = defaultChartStyle();
|
|
23973
|
+
/** Draw the price/time axis tick labels. */
|
|
23974
|
+
this.showAxisLabels = true;
|
|
23975
|
+
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
23976
|
+
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
23977
|
+
this.tradeMarkers = defaultTradeMarkersState();
|
|
23978
|
+
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
23979
|
+
this.highlights = [];
|
|
23980
|
+
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
23981
|
+
this.sessionZones = null;
|
|
23982
|
+
/** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
|
|
23983
|
+
* Indicators with z below this draw BEHIND the candles; at/above draw in front.
|
|
23984
|
+
* Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
|
|
23985
|
+
* and user drawings (z ≥ 1 by default) on top of the price. */
|
|
23986
|
+
this.candleZ = 0;
|
|
23987
|
+
/** Hide the base price series (candles/bars/line/area) without removing it — overlay
|
|
23988
|
+
* indicators keep drawing and the pane autoscales to them. Toggled from the object tree. */
|
|
23989
|
+
this.candlesHidden = false;
|
|
23990
|
+
/** Per-indicator foreground draw-order key (series layer), keyed by indicator id.
|
|
23991
|
+
* Higher = drawn later (in front). Assigned on mount to the current BOTTOM of the stack,
|
|
23992
|
+
* so each indicator arrives behind the candles (and behind older indicators);
|
|
23993
|
+
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
23994
|
+
this.seriesZ = /* @__PURE__ */ new Map();
|
|
23995
|
+
/** Per-pane raster layers of user drawings interleaved into the series stack — each is a
|
|
23996
|
+
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
23997
|
+
* Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
|
|
23998
|
+
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
23999
|
+
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
24000
|
+
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
24001
|
+
* bar. Only nonzero for models computed over a SUFFIX of the bars (whole-chart models,
|
|
24002
|
+
* the norm, aren't stored). Recomputed by the renderer on setBars + mount/patch. */
|
|
24003
|
+
this.anchorOffsets = /* @__PURE__ */ new Map();
|
|
24004
|
+
/** Per-indicator private price windows (merged indicators drawn on their own scale
|
|
24005
|
+
* column). Populated per frame for models flagged `ownScale`; absent ⇒ the model
|
|
24006
|
+
* shares its pane's master scale. */
|
|
24007
|
+
this.indicatorScales = /* @__PURE__ */ new Map();
|
|
24008
|
+
/** Cached sort of `panes` by order; invalidated on add/remove/reorder. */
|
|
24009
|
+
this.orderedCache = null;
|
|
24010
|
+
}
|
|
24011
|
+
/** Panes sorted top-to-bottom by `order`. Cached — callers must NOT mutate the array. */
|
|
24012
|
+
orderedPanes() {
|
|
24013
|
+
if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
|
|
24014
|
+
return this.orderedCache;
|
|
24015
|
+
}
|
|
24016
|
+
/** The session zones resolved into colored bands (pre/post-market washes from the
|
|
24017
|
+
* config's session colors) — consumed by the same painting path as {@link highlights}. */
|
|
24018
|
+
sessionHighlightBands() {
|
|
24019
|
+
if (!this.sessionZones) return [];
|
|
24020
|
+
const out = [];
|
|
24021
|
+
for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
|
|
24022
|
+
for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
|
|
24023
|
+
return out;
|
|
24024
|
+
}
|
|
24025
|
+
indicatorsForPane(paneId) {
|
|
24026
|
+
const out = [];
|
|
24027
|
+
for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
|
|
24028
|
+
return out;
|
|
24029
|
+
}
|
|
24030
|
+
/** Merged (own-scale) indicators on a pane, ordered by z — one axis column each. */
|
|
24031
|
+
ownScaleIndicatorsForPane(paneId) {
|
|
24032
|
+
return this.orderedIndicatorsForPane(paneId).filter((m) => m.ownScale === true);
|
|
24033
|
+
}
|
|
24034
|
+
/** Ensure a merged indicator has a private scale slot (seeded from the pane if given). */
|
|
24035
|
+
ensureIndicatorScale(id, seed) {
|
|
24036
|
+
let s = this.indicatorScales.get(id);
|
|
24037
|
+
if (!s) {
|
|
24038
|
+
const base = seed ?? { min: 0, max: 1 };
|
|
24039
|
+
s = { scale: { ...base }, scaleTarget: { ...base }, initialized: false, manualScale: null };
|
|
24040
|
+
this.indicatorScales.set(id, s);
|
|
24041
|
+
}
|
|
24042
|
+
return s;
|
|
24043
|
+
}
|
|
24044
|
+
dropIndicatorScale(id) {
|
|
24045
|
+
this.indicatorScales.delete(id);
|
|
24046
|
+
}
|
|
24047
|
+
/** The price window a model renders on: its own scale when merged (`ownScale`), else the pane's. */
|
|
24048
|
+
scaleFor(model, pane) {
|
|
24049
|
+
if (model.ownScale === true) {
|
|
24050
|
+
const s = this.indicatorScales.get(model.id);
|
|
24051
|
+
if (s) return s.scale;
|
|
24052
|
+
}
|
|
24053
|
+
return pane.scale;
|
|
24054
|
+
}
|
|
24055
|
+
/** Apply a new top-to-bottom pane order (ids not present are ignored). */
|
|
24056
|
+
orderPanes(orderedIds) {
|
|
24057
|
+
orderedIds.forEach((id, i) => {
|
|
24058
|
+
const pane = this.panes.get(id);
|
|
24059
|
+
if (pane) pane.order = i;
|
|
24060
|
+
});
|
|
24061
|
+
this.orderedCache = null;
|
|
24062
|
+
}
|
|
24063
|
+
/** Indicators on a pane sorted by foreground z (ascending). Array#sort is stable,
|
|
24064
|
+
* so equal-z models keep their insertion order (the default). */
|
|
24065
|
+
orderedIndicatorsForPane(paneId) {
|
|
24066
|
+
return this.indicatorsForPane(paneId).sort((a, b) => this.zOf(a.id) - this.zOf(b.id));
|
|
24067
|
+
}
|
|
24068
|
+
/** The foreground draw-order key of an indicator (0 when never assigned). */
|
|
24069
|
+
zOf(id) {
|
|
24070
|
+
return this.seriesZ.get(id) ?? 0;
|
|
24071
|
+
}
|
|
24072
|
+
/** The model's index offset: chart bar index its index-aligned payloads count from (0 = whole-chart). */
|
|
24073
|
+
offsetOf(id) {
|
|
24074
|
+
return this.anchorOffsets.get(id) ?? 0;
|
|
24075
|
+
}
|
|
24076
|
+
/** Offsets are SIGNED. Positive: the model starts after the chart's first bar (it ran
|
|
24077
|
+
* over a suffix) — readers skip its leading chart bars. Negative: the model starts
|
|
24078
|
+
* BEFORE it (the chart's head moved forward under a mounted model) — readers skip the
|
|
24079
|
+
* model's own leading points, `points[i - off]` reaching further in. Storing only the
|
|
24080
|
+
* positive case silently pinned such a model at index 0, i.e. drew it shifted. */
|
|
24081
|
+
setAnchorOffset(id, offset) {
|
|
24082
|
+
if (offset !== 0 && Number.isFinite(offset)) this.anchorOffsets.set(id, offset);
|
|
24083
|
+
else this.anchorOffsets.delete(id);
|
|
24084
|
+
}
|
|
24085
|
+
forgetAnchorOffset(id) {
|
|
24086
|
+
this.anchorOffsets.delete(id);
|
|
24087
|
+
}
|
|
24088
|
+
/** Resolve the baseline reference price for the given pane window: the explicit
|
|
24089
|
+
* `baselineValue` when set, else `style.baseline.baselineLevel` as the price that sits
|
|
24090
|
+
* at that fraction of the pane height. Interpolated in the same space the pane renders
|
|
24091
|
+
* in (log when `scale.log`, else linear) so `level%` always lands at `level%` of the
|
|
24092
|
+
* height — matching `CoordinateSystem.yToPrice`. */
|
|
24093
|
+
baselinePriceFor(scale) {
|
|
24094
|
+
if (this.baselineValue != null) return this.baselineValue;
|
|
24095
|
+
const t = this.style.baseline.baselineLevel / 100;
|
|
24096
|
+
if (scale.log && scale.min > 0 && scale.max > scale.min) {
|
|
24097
|
+
const lo = Math.log(scale.min);
|
|
24098
|
+
return Math.exp(lo + t * (Math.log(scale.max) - lo));
|
|
24099
|
+
}
|
|
24100
|
+
return scale.min + (scale.max - scale.min) * t;
|
|
24101
|
+
}
|
|
24102
|
+
/** Assign a default z on mount: the current bottom of the stack, so a new indicator
|
|
24103
|
+
* paints behind the candles and behind every indicator already there — the price stays
|
|
24104
|
+
* the top of the pile until the user restacks it. No-op if the indicator already has one. */
|
|
24105
|
+
assignIndicatorZ(id) {
|
|
24106
|
+
if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
|
|
24107
|
+
}
|
|
24108
|
+
/** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
|
|
24109
|
+
* data canvas by default): top of the stack, so the recorded order tells the truth from
|
|
24110
|
+
* the first frame. Keeps an existing key, so a restored stack survives the remount. */
|
|
24111
|
+
assignIndicatorZTop(id) {
|
|
24112
|
+
if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
|
|
24113
|
+
}
|
|
24114
|
+
forgetIndicatorZ(id) {
|
|
24115
|
+
this.seriesZ.delete(id);
|
|
24116
|
+
}
|
|
24117
|
+
setIndicatorZ(id, z) {
|
|
24118
|
+
this.seriesZ.set(id, z);
|
|
24119
|
+
}
|
|
24120
|
+
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
|
|
24121
|
+
indicatorZOrder() {
|
|
24122
|
+
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
|
|
24123
|
+
}
|
|
24124
|
+
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
24125
|
+
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
24126
|
+
seriesBoundaries(paneId) {
|
|
24127
|
+
const keys = /* @__PURE__ */ new Set();
|
|
24128
|
+
if (paneId === "price") keys.add(this.candleZ);
|
|
24129
|
+
for (const m of this.indicatorsForPane(paneId)) keys.add(this.zOf(m.id));
|
|
24130
|
+
return [...keys].sort((a, b) => a - b);
|
|
24131
|
+
}
|
|
24132
|
+
/** Raise an indicator above every other layer (other indicators AND the candles). */
|
|
24133
|
+
bringIndicatorToFront(id) {
|
|
24134
|
+
this.seriesZ.set(id, this.topZ() + 1);
|
|
24135
|
+
}
|
|
24136
|
+
/** Drop an indicator below every other layer (other indicators AND the candles). */
|
|
24137
|
+
sendIndicatorToBack(id) {
|
|
24138
|
+
this.seriesZ.set(id, this.bottomZ() - 1);
|
|
24139
|
+
}
|
|
24140
|
+
topZ() {
|
|
24141
|
+
let max = this.candleZ;
|
|
24142
|
+
for (const z of this.seriesZ.values()) if (z > max) max = z;
|
|
24143
|
+
return max;
|
|
24144
|
+
}
|
|
24145
|
+
bottomZ() {
|
|
24146
|
+
let min = this.candleZ;
|
|
24147
|
+
for (const z of this.seriesZ.values()) if (z < min) min = z;
|
|
24148
|
+
return min;
|
|
24149
|
+
}
|
|
24150
|
+
ensurePane(id, kind, order, heightWeight) {
|
|
24151
|
+
this.orderedCache = null;
|
|
24152
|
+
const existing = this.panes.get(id);
|
|
24153
|
+
if (existing) {
|
|
24154
|
+
existing.order = order;
|
|
24155
|
+
existing.heightWeight = heightWeight;
|
|
24156
|
+
existing.kind = kind;
|
|
24157
|
+
return existing;
|
|
24158
|
+
}
|
|
24159
|
+
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 };
|
|
24160
|
+
this.panes.set(id, pane);
|
|
24161
|
+
return pane;
|
|
24162
|
+
}
|
|
24163
|
+
removePane(id) {
|
|
24164
|
+
this.panes.delete(id);
|
|
24165
|
+
this.orderedCache = null;
|
|
24166
|
+
}
|
|
24167
|
+
};
|
|
24168
|
+
function paneScaleMode(scene, pane) {
|
|
24169
|
+
return pane.kind === "price" ? scene.scaleMode : pane.scaleMode ?? "price";
|
|
24170
|
+
}
|
|
24171
|
+
function paneLogScale(scene, pane) {
|
|
24172
|
+
return pane.kind === "price" ? scene.logScale : pane.logScale ?? false;
|
|
24173
|
+
}
|
|
24174
|
+
function paneInvert(scene, pane) {
|
|
24175
|
+
return pane.kind === "price" ? scene.invertScale : pane.invert ?? false;
|
|
24176
|
+
}
|
|
24177
|
+
function percentScaleFor(scene, pane) {
|
|
24178
|
+
const mode = paneScaleMode(scene, pane);
|
|
24179
|
+
if (mode !== "percent" && mode !== "indexed") return void 0;
|
|
24180
|
+
const baseline = pane.percentBaseline;
|
|
24181
|
+
if (!Number.isFinite(baseline) || baseline === 0) return void 0;
|
|
24182
|
+
return { baseline, indexed: mode === "indexed" };
|
|
24183
|
+
}
|
|
24184
|
+
|
|
24210
24185
|
// src/renderers/native/backend/Canvas2dBackend.ts
|
|
24211
24186
|
var Canvas2dBackend = class {
|
|
24212
24187
|
constructor() {
|
|
@@ -24214,7 +24189,6 @@ var Canvas2dBackend = class {
|
|
|
24214
24189
|
this.modelAlpha = 1;
|
|
24215
24190
|
this.candleBodyAlpha = 1;
|
|
24216
24191
|
this.candleStructureAlpha = 1;
|
|
24217
|
-
this.gridAlpha = 1;
|
|
24218
24192
|
this.candleBodyScale = 1;
|
|
24219
24193
|
this.canvas = null;
|
|
24220
24194
|
this.ctx = null;
|
|
@@ -24241,8 +24215,6 @@ var Canvas2dBackend = class {
|
|
|
24241
24215
|
if (i1 < i0) return;
|
|
24242
24216
|
const barColorMap = mergeBarColors2(scene.indicators);
|
|
24243
24217
|
const panes = scene.orderedPanes();
|
|
24244
|
-
this.drawHighlights(ctx, scene, coords);
|
|
24245
|
-
this.drawGrid(ctx, scene, coords, theme, dataW);
|
|
24246
24218
|
for (const pane of panes) {
|
|
24247
24219
|
if (pane.collapsed) continue;
|
|
24248
24220
|
const models = scene.orderedIndicatorsForPane(pane.id);
|
|
@@ -24849,22 +24821,6 @@ var Canvas2dBackend = class {
|
|
|
24849
24821
|
ctx.fillStyle = bg.color;
|
|
24850
24822
|
ctx.fillRect(x1, pane.bounds.top, x2 - x1, pane.bounds.height);
|
|
24851
24823
|
}
|
|
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
24824
|
drawHline(ctx, pl, pane, coords, dataW, theme) {
|
|
24869
24825
|
const y = Math.round(coords.priceToY(pl.price, pane.scale, pane.bounds)) + 0.5;
|
|
24870
24826
|
if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) return;
|
|
@@ -24877,46 +24833,6 @@ var Canvas2dBackend = class {
|
|
|
24877
24833
|
ctx.stroke();
|
|
24878
24834
|
setDash(ctx, "solid");
|
|
24879
24835
|
}
|
|
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
24836
|
};
|
|
24921
24837
|
function setDash(ctx, style) {
|
|
24922
24838
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
@@ -25628,6 +25544,221 @@ var DrawingSceneRenderer = class {
|
|
|
25628
25544
|
}
|
|
25629
25545
|
};
|
|
25630
25546
|
|
|
25547
|
+
// src/renderers/native/chrome/ticks.ts
|
|
25548
|
+
function priceTicks(min, max, target = 6) {
|
|
25549
|
+
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
25550
|
+
const raw = (max - min) / Math.max(1, target);
|
|
25551
|
+
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
25552
|
+
const norm = raw / mag;
|
|
25553
|
+
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
25554
|
+
const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
|
|
25555
|
+
const out = [];
|
|
25556
|
+
const start = Math.ceil(min / step) * step;
|
|
25557
|
+
for (let v = start; v <= max + step * 1e-6; v += step) {
|
|
25558
|
+
out.push(Number(v.toFixed(decimals)));
|
|
25559
|
+
}
|
|
25560
|
+
return out;
|
|
25561
|
+
}
|
|
25562
|
+
function priceDecimals(min, max, target = 6) {
|
|
25563
|
+
if (!(max > min)) return 2;
|
|
25564
|
+
const raw = (max - min) / Math.max(1, target);
|
|
25565
|
+
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
25566
|
+
const norm = raw / mag;
|
|
25567
|
+
const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
|
|
25568
|
+
return Math.max(0, Math.min(8, -Math.floor(Math.log10(step)) + 1));
|
|
25569
|
+
}
|
|
25570
|
+
function logPriceTicks(min, max, target = 6) {
|
|
25571
|
+
if (min <= 0 || !(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
25572
|
+
if (Math.log10(max) - Math.log10(min) < 1.1) return priceTicks(min, max, target);
|
|
25573
|
+
const out = [];
|
|
25574
|
+
const startExp = Math.floor(Math.log10(min));
|
|
25575
|
+
const endExp = Math.ceil(Math.log10(max));
|
|
25576
|
+
for (let e = startExp; e <= endExp; e += 1) {
|
|
25577
|
+
for (const m of [1, 2, 5]) {
|
|
25578
|
+
const v = m * Math.pow(10, e);
|
|
25579
|
+
if (v >= min && v <= max) out.push(v);
|
|
25580
|
+
}
|
|
25581
|
+
}
|
|
25582
|
+
return out;
|
|
25583
|
+
}
|
|
25584
|
+
function valueDecimals(v) {
|
|
25585
|
+
const a = Math.abs(v);
|
|
25586
|
+
if (a >= 100) return 0;
|
|
25587
|
+
if (a >= 1) return 2;
|
|
25588
|
+
if (a >= 0.01) return 4;
|
|
25589
|
+
return 6;
|
|
25590
|
+
}
|
|
25591
|
+
function tickDecimals(tick) {
|
|
25592
|
+
if (!(tick > 0) || !Number.isFinite(tick)) return 2;
|
|
25593
|
+
for (let d = 0; d <= 8; d += 1) {
|
|
25594
|
+
if (Math.abs(Number(tick.toFixed(d)) - tick) <= tick * 1e-6) return d;
|
|
25595
|
+
}
|
|
25596
|
+
return 8;
|
|
25597
|
+
}
|
|
25598
|
+
function axisDecimals(scale, heightPx, mintick) {
|
|
25599
|
+
const d = mintick != null && mintick > 0 ? tickDecimals(mintick) : priceDecimals(scale.min, scale.max, tickCount(heightPx));
|
|
25600
|
+
return d === 0 ? 2 : d;
|
|
25601
|
+
}
|
|
25602
|
+
function tickCount(paneHeightPx) {
|
|
25603
|
+
return Math.max(2, Math.min(16, Math.round(paneHeightPx / 50)));
|
|
25604
|
+
}
|
|
25605
|
+
function paneTicks(scale, heightPx) {
|
|
25606
|
+
return scale.log ? logPriceTicks(scale.min, scale.max, tickCount(heightPx)) : priceTicks(scale.min, scale.max, tickCount(heightPx));
|
|
25607
|
+
}
|
|
25608
|
+
function toPct(price, baseline) {
|
|
25609
|
+
return (price / baseline - 1) * 100;
|
|
25610
|
+
}
|
|
25611
|
+
function toIndex(price, baseline) {
|
|
25612
|
+
return price / baseline * 100;
|
|
25613
|
+
}
|
|
25614
|
+
function formatPct(pct) {
|
|
25615
|
+
const sign = pct >= 0 ? "+" : "-";
|
|
25616
|
+
return `${sign}${Math.abs(pct).toFixed(2)}%`;
|
|
25617
|
+
}
|
|
25618
|
+
function formatIndex(idx) {
|
|
25619
|
+
return idx.toFixed(2);
|
|
25620
|
+
}
|
|
25621
|
+
function formatCompactValue(v) {
|
|
25622
|
+
const a = Math.abs(v);
|
|
25623
|
+
if (a >= 1e9) return `${trimZeros(v / 1e9)}B`;
|
|
25624
|
+
if (a >= 1e6) return `${trimZeros(v / 1e6)}M`;
|
|
25625
|
+
if (a >= 1e3) return `${trimZeros(v / 1e3)}K`;
|
|
25626
|
+
return trimZeros(v);
|
|
25627
|
+
}
|
|
25628
|
+
function trimZeros(v) {
|
|
25629
|
+
return Number(v.toFixed(2)).toString();
|
|
25630
|
+
}
|
|
25631
|
+
function paneAxisTicks(scale, heightPx, pct, mintick, format) {
|
|
25632
|
+
if (format === "none") return [];
|
|
25633
|
+
if (pct) {
|
|
25634
|
+
const { baseline, indexed } = pct;
|
|
25635
|
+
const lo = Math.min(scale.min, scale.max);
|
|
25636
|
+
const hi = Math.max(scale.min, scale.max);
|
|
25637
|
+
if (indexed) {
|
|
25638
|
+
const iLo = toIndex(lo, baseline);
|
|
25639
|
+
const iHi = toIndex(hi, baseline);
|
|
25640
|
+
return priceTicks(iLo, iHi, tickCount(heightPx)).map((idx) => ({ price: baseline * idx / 100, label: formatIndex(idx) }));
|
|
25641
|
+
}
|
|
25642
|
+
const pLo = toPct(lo, baseline);
|
|
25643
|
+
const pHi = toPct(hi, baseline);
|
|
25644
|
+
return priceTicks(pLo, pHi, tickCount(heightPx)).map((p) => ({ price: baseline * (1 + p / 100), label: formatPct(p) }));
|
|
25645
|
+
}
|
|
25646
|
+
if (format === "volume") {
|
|
25647
|
+
return paneTicks(scale, heightPx).map((price) => ({ price, label: formatCompactValue(price) }));
|
|
25648
|
+
}
|
|
25649
|
+
return paneTicks(scale, heightPx).map((price) => ({ price, label: formatPriceLabel(scale, heightPx, price, mintick) }));
|
|
25650
|
+
}
|
|
25651
|
+
function formatAxisValue(scale, heightPx, value, pct, mintick, format) {
|
|
25652
|
+
if (format === "none") return "";
|
|
25653
|
+
if (pct) return pct.indexed ? formatIndex(toIndex(value, pct.baseline)) : formatPct(toPct(value, pct.baseline));
|
|
25654
|
+
if (format === "volume") return formatCompactValue(value);
|
|
25655
|
+
return formatPriceLabel(scale, heightPx, value, mintick);
|
|
25656
|
+
}
|
|
25657
|
+
function formatPriceLabel(scale, heightPx, value, mintick) {
|
|
25658
|
+
const wideLog = scale.log && Math.log10(scale.max) - Math.log10(scale.min) >= 1.1;
|
|
25659
|
+
if (wideLog) {
|
|
25660
|
+
const d = valueDecimals(value);
|
|
25661
|
+
return value.toFixed(d === 0 ? 2 : d);
|
|
25662
|
+
}
|
|
25663
|
+
return value.toFixed(axisDecimals(scale, heightPx, mintick));
|
|
25664
|
+
}
|
|
25665
|
+
var SEC = 1e3;
|
|
25666
|
+
var MIN = 60 * SEC;
|
|
25667
|
+
var HOUR = 60 * MIN;
|
|
25668
|
+
var DAY = 24 * HOUR;
|
|
25669
|
+
var WEEK = 7 * DAY;
|
|
25670
|
+
var MONTH = 30 * DAY;
|
|
25671
|
+
var YEAR = 365 * DAY;
|
|
25672
|
+
var STEP_LADDER = [
|
|
25673
|
+
SEC,
|
|
25674
|
+
5 * SEC,
|
|
25675
|
+
15 * SEC,
|
|
25676
|
+
30 * SEC,
|
|
25677
|
+
MIN,
|
|
25678
|
+
5 * MIN,
|
|
25679
|
+
15 * MIN,
|
|
25680
|
+
30 * MIN,
|
|
25681
|
+
HOUR,
|
|
25682
|
+
2 * HOUR,
|
|
25683
|
+
4 * HOUR,
|
|
25684
|
+
6 * HOUR,
|
|
25685
|
+
12 * HOUR,
|
|
25686
|
+
DAY,
|
|
25687
|
+
2 * DAY,
|
|
25688
|
+
WEEK,
|
|
25689
|
+
MONTH,
|
|
25690
|
+
3 * MONTH,
|
|
25691
|
+
YEAR
|
|
25692
|
+
];
|
|
25693
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
25694
|
+
var pad2 = (n) => n < 10 ? `0${n}` : String(n);
|
|
25695
|
+
function pickStep(targetMs) {
|
|
25696
|
+
for (const step of STEP_LADDER) if (step >= targetMs) return step;
|
|
25697
|
+
return STEP_LADDER[STEP_LADDER.length - 1];
|
|
25698
|
+
}
|
|
25699
|
+
function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
25700
|
+
const span = toMs - fromMs;
|
|
25701
|
+
if (!(span > 0)) return [];
|
|
25702
|
+
const step = pickStep(span / Math.max(1, target));
|
|
25703
|
+
const zFrom = fromMs + offsetMs;
|
|
25704
|
+
const zTo = toMs + offsetMs;
|
|
25705
|
+
const first = Math.ceil(zFrom / step) * step;
|
|
25706
|
+
const out = [];
|
|
25707
|
+
for (let zt = first; zt <= zTo; zt += step) {
|
|
25708
|
+
const t = zt - offsetMs;
|
|
25709
|
+
const d = new Date(zt);
|
|
25710
|
+
let label;
|
|
25711
|
+
let major = false;
|
|
25712
|
+
if (step < DAY) {
|
|
25713
|
+
const h = d.getUTCHours();
|
|
25714
|
+
const m = d.getUTCMinutes();
|
|
25715
|
+
if (h === 0 && m === 0) {
|
|
25716
|
+
label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
|
|
25717
|
+
major = true;
|
|
25718
|
+
} else {
|
|
25719
|
+
label = `${pad2(h)}:${pad2(m)}`;
|
|
25720
|
+
}
|
|
25721
|
+
} else if (step < YEAR) {
|
|
25722
|
+
label = `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;
|
|
25723
|
+
if (d.getUTCDate() === 1) {
|
|
25724
|
+
label = MONTHS[d.getUTCMonth()];
|
|
25725
|
+
major = true;
|
|
25726
|
+
}
|
|
25727
|
+
} else {
|
|
25728
|
+
label = String(d.getUTCFullYear());
|
|
25729
|
+
major = true;
|
|
25730
|
+
}
|
|
25731
|
+
out.push({ time: t, label, major });
|
|
25732
|
+
}
|
|
25733
|
+
return out;
|
|
25734
|
+
}
|
|
25735
|
+
|
|
25736
|
+
// src/renderers/native/chrome/tz.ts
|
|
25737
|
+
function tzOffsetMs(ms, timeZone) {
|
|
25738
|
+
if (!timeZone || timeZone === "UTC") return 0;
|
|
25739
|
+
try {
|
|
25740
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
25741
|
+
timeZone,
|
|
25742
|
+
hourCycle: "h23",
|
|
25743
|
+
year: "numeric",
|
|
25744
|
+
month: "2-digit",
|
|
25745
|
+
day: "2-digit",
|
|
25746
|
+
hour: "2-digit",
|
|
25747
|
+
minute: "2-digit",
|
|
25748
|
+
second: "2-digit"
|
|
25749
|
+
});
|
|
25750
|
+
const parts = dtf.formatToParts(new Date(ms));
|
|
25751
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
25752
|
+
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
25753
|
+
return asUTC - ms;
|
|
25754
|
+
} catch {
|
|
25755
|
+
return 0;
|
|
25756
|
+
}
|
|
25757
|
+
}
|
|
25758
|
+
function zonedDate(ms, timeZone) {
|
|
25759
|
+
return new Date(ms + tzOffsetMs(ms, timeZone));
|
|
25760
|
+
}
|
|
25761
|
+
|
|
25631
25762
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
25632
25763
|
var ChromeRenderer = class {
|
|
25633
25764
|
constructor() {
|
|
@@ -25807,6 +25938,13 @@ var ChromeRenderer = class {
|
|
|
25807
25938
|
if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
|
|
25808
25939
|
ctx.fillText(t.label, dataW + 6, y);
|
|
25809
25940
|
}
|
|
25941
|
+
if (pane.axisBands) {
|
|
25942
|
+
for (const b of pane.axisBands) {
|
|
25943
|
+
const y = pane.bounds.top + b.frac * pane.bounds.height;
|
|
25944
|
+
if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
|
|
25945
|
+
ctx.fillText(b.label, dataW + 6, y);
|
|
25946
|
+
}
|
|
25947
|
+
}
|
|
25810
25948
|
}
|
|
25811
25949
|
ctx.textAlign = "start";
|
|
25812
25950
|
}
|
|
@@ -26048,7 +26186,7 @@ var CrosshairRenderer = class {
|
|
|
26048
26186
|
}
|
|
26049
26187
|
}
|
|
26050
26188
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
26051
|
-
if (pane) {
|
|
26189
|
+
if (pane && pane.axisFormat !== "none") {
|
|
26052
26190
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
26053
26191
|
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
26192
|
}
|
|
@@ -26096,7 +26234,7 @@ var CrosshairRenderer = class {
|
|
|
26096
26234
|
break;
|
|
26097
26235
|
}
|
|
26098
26236
|
}
|
|
26099
|
-
if (pane) {
|
|
26237
|
+
if (pane && pane.axisFormat !== "none") {
|
|
26100
26238
|
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
26239
|
}
|
|
26102
26240
|
}
|
|
@@ -26139,6 +26277,156 @@ function formatStamp(ms, timeZone) {
|
|
|
26139
26277
|
return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
26140
26278
|
}
|
|
26141
26279
|
|
|
26280
|
+
// src/renderers/native/chrome/settings-visibility.ts
|
|
26281
|
+
function settingsIdSlug(label) {
|
|
26282
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
26283
|
+
}
|
|
26284
|
+
function settingsIdHidden(id, hidden) {
|
|
26285
|
+
if (hidden.size === 0) return false;
|
|
26286
|
+
let path = id;
|
|
26287
|
+
for (; ; ) {
|
|
26288
|
+
if (hidden.has(path)) return true;
|
|
26289
|
+
const cut = path.lastIndexOf(".");
|
|
26290
|
+
if (cut < 0) return false;
|
|
26291
|
+
path = path.slice(0, cut);
|
|
26292
|
+
}
|
|
26293
|
+
}
|
|
26294
|
+
function settingsRowId(r) {
|
|
26295
|
+
if (r.kind === "heading" || r.kind === "header") return settingsIdSlug(r.label);
|
|
26296
|
+
const n = normalizeSettingsRow(r);
|
|
26297
|
+
if (n.toggle) return n.toggle.key;
|
|
26298
|
+
for (const c of n.controls) {
|
|
26299
|
+
if (c.kind !== "hint") return c.key;
|
|
26300
|
+
}
|
|
26301
|
+
return settingsIdSlug(n.label);
|
|
26302
|
+
}
|
|
26303
|
+
function filterHiddenRows(rows, scope, hidden) {
|
|
26304
|
+
if (hidden.size === 0) return [...rows];
|
|
26305
|
+
const out = [];
|
|
26306
|
+
let skipGroup = false;
|
|
26307
|
+
let skipSub = false;
|
|
26308
|
+
for (const r of rows) {
|
|
26309
|
+
const id = `${scope}.${settingsRowId(r)}`;
|
|
26310
|
+
if (r.kind === "heading") {
|
|
26311
|
+
skipGroup = settingsIdHidden(id, hidden);
|
|
26312
|
+
skipSub = false;
|
|
26313
|
+
if (!skipGroup) out.push(r);
|
|
26314
|
+
continue;
|
|
26315
|
+
}
|
|
26316
|
+
if (r.kind === "header") {
|
|
26317
|
+
if (skipGroup) continue;
|
|
26318
|
+
skipSub = settingsIdHidden(id, hidden);
|
|
26319
|
+
if (!skipSub) out.push(r);
|
|
26320
|
+
continue;
|
|
26321
|
+
}
|
|
26322
|
+
if (skipGroup || skipSub || settingsIdHidden(id, hidden)) continue;
|
|
26323
|
+
out.push(r);
|
|
26324
|
+
}
|
|
26325
|
+
return out;
|
|
26326
|
+
}
|
|
26327
|
+
function hostSectionId(s) {
|
|
26328
|
+
return s.id ?? settingsIdSlug(s.title);
|
|
26329
|
+
}
|
|
26330
|
+
function hostRowId(r) {
|
|
26331
|
+
return r.id ?? settingsIdSlug(r.label);
|
|
26332
|
+
}
|
|
26333
|
+
function filterHiddenHostRows(rows, scope, hidden) {
|
|
26334
|
+
if (hidden.size === 0) return [...rows];
|
|
26335
|
+
const out = [];
|
|
26336
|
+
let skipGroup = false;
|
|
26337
|
+
for (const r of rows) {
|
|
26338
|
+
const id = `${scope}.${hostRowId(r)}`;
|
|
26339
|
+
if (r.kind === "heading") {
|
|
26340
|
+
skipGroup = settingsIdHidden(id, hidden);
|
|
26341
|
+
if (!skipGroup) out.push(r);
|
|
26342
|
+
continue;
|
|
26343
|
+
}
|
|
26344
|
+
if (skipGroup || settingsIdHidden(id, hidden)) continue;
|
|
26345
|
+
out.push(r);
|
|
26346
|
+
}
|
|
26347
|
+
return out;
|
|
26348
|
+
}
|
|
26349
|
+
var BUILTIN_SETTINGS_IDS = [
|
|
26350
|
+
"symbol",
|
|
26351
|
+
"symbol.type",
|
|
26352
|
+
"symbol.style.candles",
|
|
26353
|
+
"symbol.style.candles.body",
|
|
26354
|
+
"symbol.style.candles.borders",
|
|
26355
|
+
"symbol.style.candles.wick",
|
|
26356
|
+
"symbol.style.candles.spacing",
|
|
26357
|
+
"symbol.style.bars",
|
|
26358
|
+
"symbol.style.bars.up-color",
|
|
26359
|
+
"symbol.style.bars.down-color",
|
|
26360
|
+
"symbol.style.bars.spacing",
|
|
26361
|
+
"symbol.style.line",
|
|
26362
|
+
"symbol.style.line.color",
|
|
26363
|
+
"symbol.style.line.width",
|
|
26364
|
+
"symbol.style.area",
|
|
26365
|
+
"symbol.style.area.line-color",
|
|
26366
|
+
"symbol.style.area.width",
|
|
26367
|
+
"symbol.style.area.top-fill",
|
|
26368
|
+
"symbol.style.area.bottom-fill",
|
|
26369
|
+
"symbol.style.baseline",
|
|
26370
|
+
"symbol.style.baseline.top-line",
|
|
26371
|
+
"symbol.style.baseline.bottom-line",
|
|
26372
|
+
"symbol.style.baseline.fill-top",
|
|
26373
|
+
"symbol.style.baseline.fill-bottom",
|
|
26374
|
+
"symbol.style.baseline.base-level",
|
|
26375
|
+
"symbol.style.baseline.width",
|
|
26376
|
+
"symbol.timezone",
|
|
26377
|
+
"scales",
|
|
26378
|
+
"scales.price-scale",
|
|
26379
|
+
"scales.price-scale.mode",
|
|
26380
|
+
"scales.price-scale.invert",
|
|
26381
|
+
"scales.price-scale.last-price-line",
|
|
26382
|
+
"scales.price-scale.last-price-label",
|
|
26383
|
+
"scales.price-scale.countdown",
|
|
26384
|
+
"scales.price-scale.axis-labels",
|
|
26385
|
+
"scales.price-scale.border-color",
|
|
26386
|
+
"scales.crosshair",
|
|
26387
|
+
"scales.crosshair.color",
|
|
26388
|
+
"scales.crosshair.width",
|
|
26389
|
+
"scales.crosshair.style",
|
|
26390
|
+
"canvas",
|
|
26391
|
+
"canvas.background",
|
|
26392
|
+
"canvas.background.color",
|
|
26393
|
+
"canvas.background.text-color",
|
|
26394
|
+
"canvas.background.text-size",
|
|
26395
|
+
"canvas.background.pane-separator",
|
|
26396
|
+
"canvas.grid",
|
|
26397
|
+
"canvas.grid.vertical",
|
|
26398
|
+
"canvas.grid.horizontal",
|
|
26399
|
+
"canvas.theme"
|
|
26400
|
+
];
|
|
26401
|
+
function settingsIdCatalog(hostSections) {
|
|
26402
|
+
const ids = new Set(BUILTIN_SETTINGS_IDS);
|
|
26403
|
+
for (const def of chartTypes()) {
|
|
26404
|
+
if (hasOwnCandlePaint(def.id)) {
|
|
26405
|
+
const style = `symbol.style.${def.id}`;
|
|
26406
|
+
for (const leaf of ["", ".body", ".borders", ".wick", ".spacing"]) ids.add(style + leaf);
|
|
26407
|
+
}
|
|
26408
|
+
const section = def.settings;
|
|
26409
|
+
if (!section) continue;
|
|
26410
|
+
const scope = `type:${def.id}`;
|
|
26411
|
+
ids.add(scope);
|
|
26412
|
+
const addRows = (rows) => {
|
|
26413
|
+
for (const r of rows) ids.add(`${scope}.${settingsRowId(r)}`);
|
|
26414
|
+
};
|
|
26415
|
+
if (section.rows) addRows(section.rows);
|
|
26416
|
+
for (const inst of section.instances ?? []) addRows(inst.rows);
|
|
26417
|
+
for (const sub of section.subsections ?? []) {
|
|
26418
|
+
ids.add(`${scope}.${settingsIdSlug(sub.title)}`);
|
|
26419
|
+
addRows(sub.rows);
|
|
26420
|
+
}
|
|
26421
|
+
}
|
|
26422
|
+
for (const hs of hostSections) {
|
|
26423
|
+
const scope = hostSectionId(hs);
|
|
26424
|
+
ids.add(scope);
|
|
26425
|
+
for (const r of hs.rows) ids.add(`${scope}.${hostRowId(r)}`);
|
|
26426
|
+
}
|
|
26427
|
+
return [...ids];
|
|
26428
|
+
}
|
|
26429
|
+
|
|
26142
26430
|
// src/renderers/native/chrome/SettingsDialog.ts
|
|
26143
26431
|
var BUILTIN_STYLE_LABELS2 = {
|
|
26144
26432
|
candles: "Candles",
|
|
@@ -26257,12 +26545,21 @@ var SettingsDialog = class {
|
|
|
26257
26545
|
this.activeSection = null;
|
|
26258
26546
|
/** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
|
|
26259
26547
|
this.mobileLayout = false;
|
|
26548
|
+
/** The visibility policy: setting ids hidden by the host (subtree semantics). */
|
|
26549
|
+
this.hiddenSettings = /* @__PURE__ */ new Set();
|
|
26260
26550
|
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
26261
26551
|
}
|
|
26262
26552
|
/** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
|
|
26263
26553
|
setHostSections(sections) {
|
|
26264
26554
|
this.hostSections = sections;
|
|
26265
26555
|
}
|
|
26556
|
+
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
26557
|
+
setHiddenSettings(ids) {
|
|
26558
|
+
const next = new Set(ids);
|
|
26559
|
+
const same = next.size === this.hiddenSettings.size && [...next].every((id) => this.hiddenSettings.has(id));
|
|
26560
|
+
this.hiddenSettings = next;
|
|
26561
|
+
if (!same) this.reopenIfLive();
|
|
26562
|
+
}
|
|
26266
26563
|
/** Configure the Canvas → Theme row (see {@link themeControl}); null hides the row. */
|
|
26267
26564
|
setThemeControl(current, onSelect) {
|
|
26268
26565
|
this.themeControl = { current, onSelect };
|
|
@@ -26330,8 +26627,12 @@ var SettingsDialog = class {
|
|
|
26330
26627
|
}
|
|
26331
26628
|
const body = document.createElement("div");
|
|
26332
26629
|
body.style.cssText = "display:flex;flex-direction:column;gap:0;";
|
|
26333
|
-
|
|
26334
|
-
|
|
26630
|
+
const sid = (el, id) => {
|
|
26631
|
+
el.dataset.sdId = id;
|
|
26632
|
+
return el;
|
|
26633
|
+
};
|
|
26634
|
+
body.append(sid(this.section("Symbol"), "symbol"));
|
|
26635
|
+
body.append(sid(this.sectionTitle("Chart type"), "symbol.type"));
|
|
26335
26636
|
const groups = {};
|
|
26336
26637
|
const showActive = (style) => {
|
|
26337
26638
|
const active = style === "heikinashi" ? "candles" : style;
|
|
@@ -26340,64 +26641,64 @@ var SettingsDialog = class {
|
|
|
26340
26641
|
}
|
|
26341
26642
|
};
|
|
26342
26643
|
body.append(
|
|
26343
|
-
this.selectRowLabeled("Type", config.series.style, priceStyleIds().map((id) => [id, styleLabel(id)]), (v) => {
|
|
26644
|
+
sid(this.selectRowLabeled("Type", config.series.style, priceStyleIds().map((id) => [id, styleLabel(id)]), (v) => {
|
|
26344
26645
|
this.emit({ series: { style: v } });
|
|
26345
26646
|
showActive(v);
|
|
26346
26647
|
this.syncTypeTabs?.(v);
|
|
26347
|
-
})
|
|
26648
|
+
}), "symbol.type")
|
|
26348
26649
|
);
|
|
26349
|
-
const candles = this.group();
|
|
26650
|
+
const candles = sid(this.group(), "symbol.style.candles");
|
|
26350
26651
|
candles.append(this.sectionTitle("Candles"));
|
|
26351
|
-
candles.append(this.toggleRow("Body", config.candles.bodyVisible, (v) => this.emit({ candles: { bodyVisible: v } }), [
|
|
26652
|
+
candles.append(sid(this.toggleRow("Body", config.candles.bodyVisible, (v) => this.emit({ candles: { bodyVisible: v } }), [
|
|
26352
26653
|
this.swatch(config.candles.upColor, (v) => this.emit({ candles: { upColor: v } })),
|
|
26353
26654
|
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 } }), [
|
|
26655
|
+
]), "symbol.style.candles.body"));
|
|
26656
|
+
candles.append(sid(this.toggleRow("Borders", config.candles.borderVisible, (v) => this.emit({ candles: { borderVisible: v } }), [
|
|
26356
26657
|
this.swatch(config.candles.borderUpColor, (v) => this.emit({ candles: { borderUpColor: v } })),
|
|
26357
26658
|
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 } }), [
|
|
26659
|
+
]), "symbol.style.candles.borders"));
|
|
26660
|
+
candles.append(sid(this.toggleRow("Wick", config.candles.wickVisible, (v) => this.emit({ candles: { wickVisible: v } }), [
|
|
26360
26661
|
this.swatch(config.candles.wickUpColor, (v) => this.emit({ candles: { wickUpColor: v } })),
|
|
26361
26662
|
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 } })));
|
|
26663
|
+
]), "symbol.style.candles.wick"));
|
|
26664
|
+
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
26665
|
groups.candles = candles;
|
|
26365
26666
|
body.append(candles);
|
|
26366
|
-
const bars = this.group();
|
|
26667
|
+
const bars = sid(this.group(), "symbol.style.bars");
|
|
26367
26668
|
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 } })));
|
|
26669
|
+
bars.append(sid(this.colorRow("Color Up", config.bars.upColor, (v) => this.emit({ bars: { upColor: v } })), "symbol.style.bars.up-color"));
|
|
26670
|
+
bars.append(sid(this.colorRow("Color Down", config.bars.downColor, (v) => this.emit({ bars: { downColor: v } })), "symbol.style.bars.down-color"));
|
|
26671
|
+
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
26672
|
groups.bars = bars;
|
|
26372
26673
|
body.append(bars);
|
|
26373
|
-
const line = this.group();
|
|
26674
|
+
const line = sid(this.group(), "symbol.style.line");
|
|
26374
26675
|
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 } })));
|
|
26676
|
+
line.append(sid(this.colorRow("Color", config.line.color, (v) => this.emit({ line: { color: v } })), "symbol.style.line.color"));
|
|
26677
|
+
line.append(sid(this.numberRow("Width", config.line.width, 1, 10, 1, (v) => this.emit({ line: { width: v } })), "symbol.style.line.width"));
|
|
26377
26678
|
groups.line = line;
|
|
26378
26679
|
body.append(line);
|
|
26379
|
-
const area = this.group();
|
|
26680
|
+
const area = sid(this.group(), "symbol.style.area");
|
|
26380
26681
|
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 } })));
|
|
26682
|
+
area.append(sid(this.colorRow("Line color", config.area.lineColor, (v) => this.emit({ area: { lineColor: v } })), "symbol.style.area.line-color"));
|
|
26683
|
+
area.append(sid(this.numberRow("Width", config.area.width, 1, 10, 1, (v) => this.emit({ area: { width: v } })), "symbol.style.area.width"));
|
|
26684
|
+
area.append(sid(this.colorRow("Top fill", config.area.topColor, (v) => this.emit({ area: { topColor: v } })), "symbol.style.area.top-fill"));
|
|
26685
|
+
area.append(sid(this.colorRow("Bottom fill", config.area.bottomColor, (v) => this.emit({ area: { bottomColor: v } })), "symbol.style.area.bottom-fill"));
|
|
26385
26686
|
groups.area = area;
|
|
26386
26687
|
body.append(area);
|
|
26387
|
-
const baseline = this.group();
|
|
26688
|
+
const baseline = sid(this.group(), "symbol.style.baseline");
|
|
26388
26689
|
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", [
|
|
26690
|
+
baseline.append(sid(this.rowWith("Top line", [this.swatch(config.baseline.topLineColor, (v) => this.emit({ baseline: { topLineColor: v } }))]), "symbol.style.baseline.top-line"));
|
|
26691
|
+
baseline.append(sid(this.rowWith("Bottom line", [this.swatch(config.baseline.bottomLineColor, (v) => this.emit({ baseline: { bottomLineColor: v } }))]), "symbol.style.baseline.bottom-line"));
|
|
26692
|
+
baseline.append(sid(this.rowWith("Fill top area", [
|
|
26392
26693
|
this.swatch(config.baseline.topFillColor, (v) => this.emit({ baseline: { topFillColor: v } })),
|
|
26393
26694
|
this.swatch(config.baseline.topFillColor2, (v) => this.emit({ baseline: { topFillColor2: v } }))
|
|
26394
|
-
]));
|
|
26395
|
-
baseline.append(this.rowWith("Fill bottom area", [
|
|
26695
|
+
]), "symbol.style.baseline.fill-top"));
|
|
26696
|
+
baseline.append(sid(this.rowWith("Fill bottom area", [
|
|
26396
26697
|
this.swatch(config.baseline.bottomFillColor2, (v) => this.emit({ baseline: { bottomFillColor2: v } })),
|
|
26397
26698
|
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 } })));
|
|
26699
|
+
]), "symbol.style.baseline.fill-bottom"));
|
|
26700
|
+
baseline.append(sid(this.numberRow("Base level %", config.baseline.baselineLevel, 0, 100, 1, (v) => this.emit({ baseline: { baselineLevel: v } })), "symbol.style.baseline.base-level"));
|
|
26701
|
+
baseline.append(sid(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })), "symbol.style.baseline.width"));
|
|
26401
26702
|
groups.baseline = baseline;
|
|
26402
26703
|
body.append(baseline);
|
|
26403
26704
|
for (const def of chartTypes()) {
|
|
@@ -26405,32 +26706,36 @@ var SettingsDialog = class {
|
|
|
26405
26706
|
const bag = config.chartTypes[def.id] ?? {};
|
|
26406
26707
|
const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
|
|
26407
26708
|
const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
|
|
26408
|
-
const g = this.group();
|
|
26709
|
+
const g = sid(this.group(), `symbol.style.${def.id}`);
|
|
26409
26710
|
g.append(this.sectionTitle("Candles"));
|
|
26410
|
-
g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
|
|
26711
|
+
g.append(sid(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
|
|
26411
26712
|
this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
|
|
26412
26713
|
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), [
|
|
26714
|
+
]), `symbol.style.${def.id}.body`));
|
|
26715
|
+
g.append(sid(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
|
|
26415
26716
|
this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
|
|
26416
26717
|
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), [
|
|
26718
|
+
]), `symbol.style.${def.id}.borders`));
|
|
26719
|
+
g.append(sid(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
|
|
26419
26720
|
this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
|
|
26420
26721
|
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 } })));
|
|
26722
|
+
]), `symbol.style.${def.id}.wick`));
|
|
26723
|
+
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
26724
|
groups[def.id] = g;
|
|
26424
26725
|
body.append(g);
|
|
26425
26726
|
}
|
|
26426
26727
|
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 } })));
|
|
26728
|
+
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
26729
|
+
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
26429
26730
|
const renderHostSections = (placement) => {
|
|
26430
26731
|
for (const hs of this.hostSections) {
|
|
26431
26732
|
if ((hs.placement ?? "after-symbol") !== placement) continue;
|
|
26733
|
+
const scope = hostSectionId(hs);
|
|
26734
|
+
if (settingsIdHidden(scope, this.hiddenSettings)) continue;
|
|
26735
|
+
const rows = filterHiddenHostRows(hs.rows, scope, this.hiddenSettings);
|
|
26736
|
+
if (rows.length === 0) continue;
|
|
26432
26737
|
body.append(placement === "symbol" ? this.sectionTitle(hs.title) : this.section(hs.title));
|
|
26433
|
-
for (const hr of
|
|
26738
|
+
for (const hr of rows) {
|
|
26434
26739
|
if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
|
|
26435
26740
|
else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
|
|
26436
26741
|
else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
|
|
@@ -26443,53 +26748,69 @@ var SettingsDialog = class {
|
|
|
26443
26748
|
const typeSettings = def.settings;
|
|
26444
26749
|
if (!typeSettings) continue;
|
|
26445
26750
|
if ((typeSettings.placement ?? "end") !== placement) continue;
|
|
26751
|
+
if (settingsIdHidden(`type:${def.id}`, this.hiddenSettings)) continue;
|
|
26446
26752
|
this.chartTypeSection(def.id, typeSettings, config, body);
|
|
26447
26753
|
}
|
|
26448
26754
|
};
|
|
26449
26755
|
renderHostSections("symbol");
|
|
26450
26756
|
renderChartTypeSections("after-symbol");
|
|
26451
26757
|
renderHostSections("after-symbol");
|
|
26452
|
-
body.append(this.section("Scales and lines"));
|
|
26453
|
-
body.append(this.sectionTitle("Price scale"));
|
|
26758
|
+
body.append(sid(this.section("Scales and lines"), "scales"));
|
|
26759
|
+
body.append(sid(this.sectionTitle("Price scale"), "scales.price-scale"));
|
|
26454
26760
|
body.append(
|
|
26455
|
-
this.selectRowLabeled(
|
|
26761
|
+
sid(this.selectRowLabeled(
|
|
26456
26762
|
"Mode",
|
|
26457
26763
|
config.priceScale.log ? "log" : config.priceScale.mode,
|
|
26458
26764
|
[["price", "Regular"], ["percent", "Percent"], ["indexed", "Indexed to 100"], ["log", "Logarithmic"]],
|
|
26459
26765
|
(v) => this.emit({ priceScale: v === "log" ? { mode: "price", log: true } : { mode: v, log: false } })
|
|
26460
|
-
)
|
|
26766
|
+
), "scales.price-scale.mode")
|
|
26461
26767
|
);
|
|
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 } } }), [
|
|
26768
|
+
body.append(sid(this.boolRow("Invert scale", config.priceScale.invert, (v) => this.emit({ priceScale: { invert: v } })), "scales.price-scale.invert"));
|
|
26769
|
+
body.append(sid(this.separator(), "scales.price-scale"));
|
|
26770
|
+
body.append(sid(this.boolRow("Last Price Line", config.priceScale.currentPriceLine, (v) => this.emit({ priceScale: { currentPriceLine: v } })), "scales.price-scale.last-price-line"));
|
|
26771
|
+
body.append(sid(this.boolRow("Last price label", config.priceScale.priceLabel, (v) => this.emit({ priceScale: { priceLabel: v } })), "scales.price-scale.last-price-label"));
|
|
26772
|
+
body.append(sid(this.boolRow("Countdown to bar close", config.priceScale.countdown, (v) => this.emit({ priceScale: { countdown: v } })), "scales.price-scale.countdown"));
|
|
26773
|
+
body.append(sid(this.boolRow("Axis labels", config.priceScale.labelsVisible, (v) => this.emit({ priceScale: { labelsVisible: v } })), "scales.price-scale.axis-labels"));
|
|
26774
|
+
body.append(sid(this.colorRow("Scale border color", config.priceScale.borderColor, (v) => this.emit({ priceScale: { borderColor: v } })), "scales.price-scale.border-color"));
|
|
26775
|
+
body.append(sid(this.sectionTitle("Crosshair"), "scales.crosshair"));
|
|
26776
|
+
body.append(sid(this.colorRow("Color", config.crosshair.color, (v) => this.emit({ crosshair: { color: v } })), "scales.crosshair.color"));
|
|
26777
|
+
body.append(sid(this.numberRow("Width", config.crosshair.width, 0.5, 8, 0.5, (v) => this.emit({ crosshair: { width: v } })), "scales.crosshair.width"));
|
|
26778
|
+
body.append(sid(this.selectRowLabeled("Style", config.crosshair.style, [["solid", "Solid"], ["dashed", "Dashed"], ["dotted", "Dotted"]], (v) => this.emit({ crosshair: { style: v } })), "scales.crosshair.style"));
|
|
26779
|
+
body.append(sid(this.section("Canvas"), "canvas"));
|
|
26780
|
+
body.append(sid(this.sectionTitle("Background & text"), "canvas.background"));
|
|
26781
|
+
body.append(sid(this.colorRow("Background", config.layout.background, (v) => this.emit({ layout: { background: v } })), "canvas.background.color"));
|
|
26782
|
+
body.append(sid(this.colorRow("Text color", config.layout.textColor, (v) => this.emit({ layout: { textColor: v } })), "canvas.background.text-color"));
|
|
26783
|
+
body.append(sid(this.numberRow("Text size", config.layout.fontSize, 6, 32, 1, (v) => this.emit({ layout: { fontSize: v } })), "canvas.background.text-size"));
|
|
26784
|
+
body.append(sid(this.colorRow("Pane separator color", config.panes.separatorColor, (v) => this.emit({ panes: { separatorColor: v } })), "canvas.background.pane-separator"));
|
|
26785
|
+
body.append(sid(this.sectionTitle("Grid"), "canvas.grid"));
|
|
26786
|
+
body.append(sid(this.toggleRow("Vertical", config.grid.vertLines.visible, (v) => this.emit({ grid: { vertLines: { visible: v } } }), [
|
|
26481
26787
|
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 } } }), [
|
|
26788
|
+
]), "canvas.grid.vertical"));
|
|
26789
|
+
body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
|
|
26484
26790
|
this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
|
|
26485
|
-
]));
|
|
26791
|
+
]), "canvas.grid.horizontal"));
|
|
26486
26792
|
if (this.themeControl) {
|
|
26487
26793
|
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")));
|
|
26794
|
+
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
26795
|
+
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
26490
26796
|
}
|
|
26491
26797
|
renderChartTypeSections("end");
|
|
26492
26798
|
renderHostSections("end");
|
|
26799
|
+
if (this.hiddenSettings.size > 0) {
|
|
26800
|
+
let skipTab = false;
|
|
26801
|
+
for (const child of [...body.children]) {
|
|
26802
|
+
if (child.dataset.sdTab !== void 0) {
|
|
26803
|
+
skipTab = child.dataset.sdId !== void 0 && settingsIdHidden(child.dataset.sdId, this.hiddenSettings);
|
|
26804
|
+
}
|
|
26805
|
+
if (skipTab || child.dataset.sdId !== void 0 && settingsIdHidden(child.dataset.sdId, this.hiddenSettings)) {
|
|
26806
|
+
child.remove();
|
|
26807
|
+
continue;
|
|
26808
|
+
}
|
|
26809
|
+
for (const el of [...child.querySelectorAll("[data-sd-id]")]) {
|
|
26810
|
+
if (settingsIdHidden(el.dataset.sdId, this.hiddenSettings)) el.remove();
|
|
26811
|
+
}
|
|
26812
|
+
}
|
|
26813
|
+
}
|
|
26493
26814
|
const shell = document.createElement("div");
|
|
26494
26815
|
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
26816
|
const rail = document.createElement("div");
|
|
@@ -26527,6 +26848,9 @@ var SettingsDialog = class {
|
|
|
26527
26848
|
}
|
|
26528
26849
|
if (current) current.appendChild(child);
|
|
26529
26850
|
}
|
|
26851
|
+
for (let i = panes.length - 1; i >= 0; i--) {
|
|
26852
|
+
if (panes[i].el.childElementCount === 0) panes.splice(i, 1);
|
|
26853
|
+
}
|
|
26530
26854
|
this.syncTypeTabs = (active) => {
|
|
26531
26855
|
let hidActive = false;
|
|
26532
26856
|
panes.forEach((pn, idx) => {
|
|
@@ -26666,19 +26990,25 @@ var SettingsDialog = class {
|
|
|
26666
26990
|
this.emitType(typeId, key, v);
|
|
26667
26991
|
for (const r of refreshers) r();
|
|
26668
26992
|
};
|
|
26993
|
+
const scope = `type:${typeId}`;
|
|
26669
26994
|
if (section.instances && section.instances.length > 0) {
|
|
26670
|
-
|
|
26995
|
+
const instances = section.instances.map((inst) => ({ ...inst, rows: filterHiddenRows(inst.rows, scope, this.hiddenSettings) }));
|
|
26996
|
+
body.append(this.instancesBlock(typeId, instances, bag, put, refreshers));
|
|
26671
26997
|
} else if (section.rows) {
|
|
26672
|
-
|
|
26673
|
-
|
|
26998
|
+
const rows = filterHiddenRows(section.rows, scope, this.hiddenSettings);
|
|
26999
|
+
if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, rows, bag, put, refreshers));
|
|
27000
|
+
else this.flatTypeRows(rows, bag, put, refreshers, body);
|
|
26674
27001
|
}
|
|
26675
27002
|
for (const sub of section.subsections ?? []) {
|
|
27003
|
+
if (settingsIdHidden(`${scope}.${settingsIdSlug(sub.title)}`, this.hiddenSettings)) continue;
|
|
27004
|
+
const rows = filterHiddenRows(sub.rows, scope, this.hiddenSettings);
|
|
27005
|
+
if (rows.length === 0) continue;
|
|
26676
27006
|
const subMarker = this.section(sub.title);
|
|
26677
27007
|
subMarker.dataset.sdStyle = typeId;
|
|
26678
27008
|
subMarker.dataset.sdVisibility = section.visibility ?? "active";
|
|
26679
27009
|
subMarker.dataset.sdSub = "1";
|
|
26680
27010
|
body.append(subMarker);
|
|
26681
|
-
body.append(this.groupedRows(`${typeId}/${sub.title}`,
|
|
27011
|
+
body.append(this.groupedRows(`${typeId}/${sub.title}`, rows, bag, put, refreshers, sub.enableKey));
|
|
26682
27012
|
}
|
|
26683
27013
|
for (const r of refreshers) r();
|
|
26684
27014
|
}
|
|
@@ -30968,6 +31298,94 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
|
|
|
30968
31298
|
return { above, below: combinedWeight - above };
|
|
30969
31299
|
}
|
|
30970
31300
|
|
|
31301
|
+
// src/renderers/native/backdrop/BackdropRenderer.ts
|
|
31302
|
+
var BackdropRenderer = class {
|
|
31303
|
+
constructor() {
|
|
31304
|
+
this.canvas = null;
|
|
31305
|
+
this.ctx = null;
|
|
31306
|
+
}
|
|
31307
|
+
mount(canvas) {
|
|
31308
|
+
this.canvas = canvas;
|
|
31309
|
+
this.ctx = canvas.getContext("2d");
|
|
31310
|
+
}
|
|
31311
|
+
destroy() {
|
|
31312
|
+
this.canvas = null;
|
|
31313
|
+
this.ctx = null;
|
|
31314
|
+
}
|
|
31315
|
+
/** Paint one frame: highlight bands first, gridlines on top (the order they had inside
|
|
31316
|
+
* the data canvas). `gridAlpha` fades the gridlines as a reveal-under layer opens. */
|
|
31317
|
+
render(scene, coords, theme, gridAlpha) {
|
|
31318
|
+
const ctx = this.ctx;
|
|
31319
|
+
const canvas = this.canvas;
|
|
31320
|
+
if (!ctx || !canvas) return;
|
|
31321
|
+
const dpr = coords.dpr;
|
|
31322
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
31323
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
31324
|
+
const n = coords.barCount;
|
|
31325
|
+
if (n === 0) return;
|
|
31326
|
+
const vr = coords.visibleLogicalRange();
|
|
31327
|
+
if (Math.min(n - 1, Math.ceil(vr.to)) < Math.max(0, Math.floor(vr.from))) return;
|
|
31328
|
+
this.drawHighlights(ctx, scene, coords);
|
|
31329
|
+
this.drawGrid(ctx, scene, coords, theme, coords.width, gridAlpha);
|
|
31330
|
+
}
|
|
31331
|
+
/** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
|
|
31332
|
+
* Session-zone washes (pre/post-market) paint first, host highlights on top. */
|
|
31333
|
+
drawHighlights(ctx, scene, coords) {
|
|
31334
|
+
const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
|
|
31335
|
+
if (bands.length === 0) return;
|
|
31336
|
+
for (const band of bands) {
|
|
31337
|
+
const x1 = coords.timeToX(band.from);
|
|
31338
|
+
const x2 = coords.timeToX(band.to);
|
|
31339
|
+
if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
|
|
31340
|
+
const cx = Math.max(0, x1);
|
|
31341
|
+
const cw = Math.min(coords.width, x2) - cx;
|
|
31342
|
+
if (cw <= 0) continue;
|
|
31343
|
+
ctx.fillStyle = band.color;
|
|
31344
|
+
ctx.fillRect(cx, 0, cw, coords.height);
|
|
31345
|
+
}
|
|
31346
|
+
}
|
|
31347
|
+
// ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
|
|
31348
|
+
// (style); each uses its own color. Pane separators are drawn on the chrome layer
|
|
31349
|
+
// (full-width, above the data) so series never overpaint them.
|
|
31350
|
+
drawGrid(ctx, scene, coords, theme, dataW, gridAlpha) {
|
|
31351
|
+
const panes = scene.orderedPanes();
|
|
31352
|
+
const { gridVert, gridHorz } = scene.style;
|
|
31353
|
+
const vertColor = gridVert.color ?? theme.gridColor;
|
|
31354
|
+
const horzColor = gridHorz.color ?? theme.gridColor;
|
|
31355
|
+
ctx.lineWidth = 1;
|
|
31356
|
+
if (scene.showGrid && gridVert.visible) {
|
|
31357
|
+
ctx.globalAlpha = gridAlpha;
|
|
31358
|
+
ctx.strokeStyle = vertColor;
|
|
31359
|
+
const tr = coords.visibleTimeRange();
|
|
31360
|
+
const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
|
|
31361
|
+
ctx.beginPath();
|
|
31362
|
+
for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
|
|
31363
|
+
const x = Math.round(coords.timeToX(tick.time)) + 0.5;
|
|
31364
|
+
if (x < 0 || x > dataW) continue;
|
|
31365
|
+
ctx.moveTo(x, 0);
|
|
31366
|
+
ctx.lineTo(x, coords.height);
|
|
31367
|
+
}
|
|
31368
|
+
ctx.stroke();
|
|
31369
|
+
}
|
|
31370
|
+
for (const pane of panes) {
|
|
31371
|
+
if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
|
|
31372
|
+
ctx.globalAlpha = gridAlpha;
|
|
31373
|
+
ctx.strokeStyle = horzColor;
|
|
31374
|
+
const pct = percentScaleFor(scene, pane);
|
|
31375
|
+
ctx.beginPath();
|
|
31376
|
+
for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
|
|
31377
|
+
const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
|
|
31378
|
+
if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
|
|
31379
|
+
ctx.moveTo(0, y);
|
|
31380
|
+
ctx.lineTo(dataW, y);
|
|
31381
|
+
}
|
|
31382
|
+
ctx.stroke();
|
|
31383
|
+
}
|
|
31384
|
+
}
|
|
31385
|
+
ctx.globalAlpha = 1;
|
|
31386
|
+
}
|
|
31387
|
+
};
|
|
31388
|
+
|
|
30971
31389
|
// src/renderers/native/volume/paintVolume.ts
|
|
30972
31390
|
var VOLUME_FILL_ALPHA = 0.5;
|
|
30973
31391
|
function paintVolume(ctx, bars, geom, colors) {
|
|
@@ -31443,6 +31861,7 @@ var NativeRenderer = class {
|
|
|
31443
31861
|
// px reserved on the left for the docked drawings toolbar (0 when hidden)
|
|
31444
31862
|
this.mountContainer = null;
|
|
31445
31863
|
this.userDrawings = null;
|
|
31864
|
+
this.backdropRenderer = new BackdropRenderer();
|
|
31446
31865
|
this.volumeRenderer = new VolumeRenderer();
|
|
31447
31866
|
/** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
|
|
31448
31867
|
this.extLayers = [];
|
|
@@ -31570,6 +31989,9 @@ var NativeRenderer = class {
|
|
|
31570
31989
|
// bar under the crosshair; null when off a bar
|
|
31571
31990
|
// ── settings dialog (rich, serializable config — item 15) ──
|
|
31572
31991
|
this.settingsDialog = null;
|
|
31992
|
+
/** The host's visibility policy (setting ids hidden from the dialog) — instance
|
|
31993
|
+
* state, never part of the persisted config. */
|
|
31994
|
+
this.hiddenSettings = [];
|
|
31573
31995
|
/** Where modal dialogs mount — a HOST override (multi-chart shells pass their root
|
|
31574
31996
|
* so dialogs center globally instead of clipping inside one cell). Null = the plot. */
|
|
31575
31997
|
this.dialogHost = null;
|
|
@@ -32242,6 +32664,7 @@ var NativeRenderer = class {
|
|
|
32242
32664
|
}
|
|
32243
32665
|
this.settingsDialog.setTheme(this.theme);
|
|
32244
32666
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
32667
|
+
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
32245
32668
|
this.syncThemeControl();
|
|
32246
32669
|
this.settingsDialog.toggle(
|
|
32247
32670
|
this.getConfig(),
|
|
@@ -32489,6 +32912,8 @@ var NativeRenderer = class {
|
|
|
32489
32912
|
this.wrapper = document.createElement("div");
|
|
32490
32913
|
Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair", userSelect: "none", webkitUserSelect: "none" });
|
|
32491
32914
|
applyChromeTokens(this.wrapper, this.chromeTheme());
|
|
32915
|
+
this.backdropCanvas = document.createElement("canvas");
|
|
32916
|
+
Object.assign(this.backdropCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
|
|
32492
32917
|
this.volumeCanvas = document.createElement("canvas");
|
|
32493
32918
|
Object.assign(this.volumeCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
|
|
32494
32919
|
this.dataCanvas = this.createGeometryBackend();
|
|
@@ -32511,7 +32936,7 @@ var NativeRenderer = class {
|
|
|
32511
32936
|
});
|
|
32512
32937
|
const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
|
|
32513
32938
|
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);
|
|
32939
|
+
this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
|
|
32515
32940
|
this.layerOrderSig = "";
|
|
32516
32941
|
this.wrapper.appendChild(this.plot);
|
|
32517
32942
|
this.factoryConfig = this.getConfig();
|
|
@@ -32521,6 +32946,7 @@ var NativeRenderer = class {
|
|
|
32521
32946
|
this.wrapper.appendChild(this.attributionEl);
|
|
32522
32947
|
container.appendChild(this.wrapper);
|
|
32523
32948
|
this.applyBackground();
|
|
32949
|
+
this.backdropRenderer.mount(this.backdropCanvas);
|
|
32524
32950
|
this.volumeRenderer.mount(this.volumeCanvas);
|
|
32525
32951
|
this.vpvrRenderer.mount(this.vpvrCanvas);
|
|
32526
32952
|
for (const l of this.extLayers) l.instance.mount(l.canvas);
|
|
@@ -32810,6 +33236,7 @@ var NativeRenderer = class {
|
|
|
32810
33236
|
this.scrollButton = null;
|
|
32811
33237
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
32812
33238
|
this.extLayers = [];
|
|
33239
|
+
this.backdropRenderer.destroy();
|
|
32813
33240
|
this.volumeRenderer.destroy();
|
|
32814
33241
|
this.vpvrRenderer.destroy();
|
|
32815
33242
|
this.backend.destroy();
|
|
@@ -33189,6 +33616,13 @@ var NativeRenderer = class {
|
|
|
33189
33616
|
this.hostSettingsSections = sections;
|
|
33190
33617
|
this.settingsDialog?.setHostSections(sections);
|
|
33191
33618
|
}
|
|
33619
|
+
setSettingsVisibility(policy) {
|
|
33620
|
+
this.hiddenSettings = [...policy.hidden ?? []];
|
|
33621
|
+
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
33622
|
+
}
|
|
33623
|
+
listSettingsIds() {
|
|
33624
|
+
return settingsIdCatalog(this.hostSettingsSections);
|
|
33625
|
+
}
|
|
33192
33626
|
onChartTypeSettingsChange(cb) {
|
|
33193
33627
|
this.chartTypeSettingsCbs.add(cb);
|
|
33194
33628
|
return () => this.chartTypeSettingsCbs.delete(cb);
|
|
@@ -33895,7 +34329,7 @@ var NativeRenderer = class {
|
|
|
33895
34329
|
this.backend.modelAlpha = this.modelAlpha;
|
|
33896
34330
|
this.backend.candleBodyAlpha = this.candleBodyAlpha;
|
|
33897
34331
|
this.backend.candleStructureAlpha = this.candleStructureAlpha;
|
|
33898
|
-
|
|
34332
|
+
let gridAlpha = 1;
|
|
33899
34333
|
const candleBodyScale = 1;
|
|
33900
34334
|
this.backend.candleBodyScale = candleBodyScale;
|
|
33901
34335
|
const pane = this.scene.panes.get(PRICE_PANE_ID2);
|
|
@@ -33934,7 +34368,7 @@ var NativeRenderer = class {
|
|
|
33934
34368
|
if (folded) {
|
|
33935
34369
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
33936
34370
|
if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
|
|
33937
|
-
if (folded.gridAlpha != null)
|
|
34371
|
+
if (folded.gridAlpha != null) gridAlpha = clamp012(folded.gridAlpha);
|
|
33938
34372
|
}
|
|
33939
34373
|
}
|
|
33940
34374
|
const li = this.bars.length - 1;
|
|
@@ -33942,6 +34376,7 @@ var NativeRenderer = class {
|
|
|
33942
34376
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
33943
34377
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
33944
34378
|
this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
|
|
34379
|
+
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
33945
34380
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
33946
34381
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
33947
34382
|
this.userDrawings?.render();
|
|
@@ -34084,6 +34519,7 @@ var NativeRenderer = class {
|
|
|
34084
34519
|
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34085
34520
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
34086
34521
|
pane.axisFormat = void 0;
|
|
34522
|
+
pane.axisBands = void 0;
|
|
34087
34523
|
if (this.volumeOwnsPane(pane, masterModels)) {
|
|
34088
34524
|
const maxVol = this.maxVisibleVolume(i0, i1);
|
|
34089
34525
|
if (maxVol > 0) {
|
|
@@ -34093,6 +34529,11 @@ var NativeRenderer = class {
|
|
|
34093
34529
|
} else if (this.layerNativesOwnPane(pane, masterModels)) {
|
|
34094
34530
|
pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34095
34531
|
pane.percentBaseline = this.bars[i0]?.close ?? 0;
|
|
34532
|
+
if (masterModels.every((m) => m.paneAxis != null)) {
|
|
34533
|
+
pane.axisFormat = "none";
|
|
34534
|
+
const banded = masterModels.find((m) => typeof m.paneAxis === "object");
|
|
34535
|
+
pane.axisBands = banded ? banded.paneAxis.bands : void 0;
|
|
34536
|
+
}
|
|
34096
34537
|
}
|
|
34097
34538
|
if (pane === pricePane && this.scene.tradeMarkers.visible && pane.bounds.height > 0) {
|
|
34098
34539
|
const th = this.tradesScaleHints(vr);
|
|
@@ -34213,11 +34654,11 @@ var NativeRenderer = class {
|
|
|
34213
34654
|
);
|
|
34214
34655
|
return { below: below.map((id) => byId.get(id)), above: above.map((id) => byId.get(id)) };
|
|
34215
34656
|
}
|
|
34216
|
-
/** The full canvas pile in paint order (layers + data/volume/vpvr) — what
|
|
34217
|
-
* stacking and the screenshot compositor must both follow. */
|
|
34657
|
+
/** The full canvas pile in paint order (backdrop + layers + data/volume/vpvr) — what
|
|
34658
|
+
* the DOM stacking and the screenshot compositor must both follow. */
|
|
34218
34659
|
canvasPile() {
|
|
34219
34660
|
const { below, above } = this.orderedLayerCanvases();
|
|
34220
|
-
return [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
|
|
34661
|
+
return [this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
|
|
34221
34662
|
}
|
|
34222
34663
|
/** Re-slot the SDK layer canvases in the plot when the computed order changed (a z
|
|
34223
34664
|
* write, a restored config, an indicator mount/remove/restack). Runs at the top of
|
|
@@ -34534,6 +34975,8 @@ var NativeRenderer = class {
|
|
|
34534
34975
|
const ph = h;
|
|
34535
34976
|
this.dataCanvas.width = Math.round(pw * dpr);
|
|
34536
34977
|
this.dataCanvas.height = Math.round(ph * dpr);
|
|
34978
|
+
this.backdropCanvas.width = this.dataCanvas.width;
|
|
34979
|
+
this.backdropCanvas.height = this.dataCanvas.height;
|
|
34537
34980
|
this.volumeCanvas.width = this.dataCanvas.width;
|
|
34538
34981
|
this.volumeCanvas.height = this.dataCanvas.height;
|
|
34539
34982
|
for (const l of this.extLayers) {
|
|
@@ -34675,35 +35118,6 @@ function normalizeBars(bars) {
|
|
|
34675
35118
|
return out;
|
|
34676
35119
|
}
|
|
34677
35120
|
|
|
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
35121
|
// src/core/native-indicators/volume/VolumeIndicator.ts
|
|
34708
35122
|
var DEFAULT_UP = BULLISH;
|
|
34709
35123
|
var DEFAULT_DOWN = BEARISH;
|
|
@@ -34900,6 +35314,7 @@ var Vela = class {
|
|
|
34900
35314
|
// the volume indicator is on by default
|
|
34901
35315
|
};
|
|
34902
35316
|
this.rendererControl = new RendererControl(renderer);
|
|
35317
|
+
if (options.settings) this.rendererControl.setSettingsVisibility(options.settings);
|
|
34903
35318
|
this.dataControl = new DataControl(feed);
|
|
34904
35319
|
this.orchestrator = new EngineOrchestrator(element, renderer, feed, engines, config, this.dataControl);
|
|
34905
35320
|
const defaults2 = rendererDefaults();
|
|
@@ -35374,8 +35789,9 @@ function statuslineInkOf(renderer, priceStyle) {
|
|
|
35374
35789
|
}
|
|
35375
35790
|
}
|
|
35376
35791
|
var Statusline = class {
|
|
35377
|
-
constructor(host, symbol) {
|
|
35792
|
+
constructor(host, symbol, iconFor) {
|
|
35378
35793
|
this.host = host;
|
|
35794
|
+
this.iconFor = iconFor;
|
|
35379
35795
|
this.parts = { name: true, market: true, ohlc: true, change: true };
|
|
35380
35796
|
this.lastBar = null;
|
|
35381
35797
|
this.hoverBar = null;
|
|
@@ -35399,7 +35815,7 @@ var Statusline = class {
|
|
|
35399
35815
|
this.el.className = "vela-statusline";
|
|
35400
35816
|
this.el.dataset.velaScreenshot = "1";
|
|
35401
35817
|
const ticker = parseSymbol(symbol).ticker;
|
|
35402
|
-
this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
|
|
35818
|
+
this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
35403
35819
|
this.symbolEl = doc.createElement("span");
|
|
35404
35820
|
this.symbolEl.className = "vela-sl-symbol";
|
|
35405
35821
|
this.symbolEl.textContent = ticker;
|
|
@@ -35420,7 +35836,7 @@ var Statusline = class {
|
|
|
35420
35836
|
setSymbol(symbol) {
|
|
35421
35837
|
const ticker = parseSymbol(symbol).ticker;
|
|
35422
35838
|
this.symbolEl.textContent = ticker;
|
|
35423
|
-
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
|
|
35839
|
+
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
35424
35840
|
this.avatarEl.replaceWith(fresh);
|
|
35425
35841
|
this.avatarEl = fresh;
|
|
35426
35842
|
this.fit();
|
|
@@ -36030,8 +36446,8 @@ function seedDefaults(opts) {
|
|
|
36030
36446
|
};
|
|
36031
36447
|
}
|
|
36032
36448
|
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 };
|
|
36449
|
+
const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
|
|
36450
|
+
return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings };
|
|
36035
36451
|
}
|
|
36036
36452
|
function cellDrawings(opt) {
|
|
36037
36453
|
if (opt === false) return false;
|
|
@@ -36044,7 +36460,11 @@ var ChartCell = class {
|
|
|
36044
36460
|
this.deps = deps;
|
|
36045
36461
|
/** This cell's unified app+drawings undo timeline (the shared Ctrl+Z routes here). */
|
|
36046
36462
|
this.history = new WidgetHistory(() => this.inner);
|
|
36047
|
-
/** Live manifest-indicator instances on this cell (the SAME entry may repeat).
|
|
36463
|
+
/** Live manifest-indicator instances on this cell (the SAME entry may repeat).
|
|
36464
|
+
* `external` marks instances added through the public seam (`ctx.addIndicator`)
|
|
36465
|
+
* rather than the shell manifest — they share the undo/redo and picker plumbing
|
|
36466
|
+
* but stay OUT of the persisted ledger (their names would never resolve against
|
|
36467
|
+
* the manifest); persisting them is their plugin's job (`registerStatePersistence`). */
|
|
36048
36468
|
this.instances = [];
|
|
36049
36469
|
/** The native-indicator catalog with this cell's live supported/present flags. */
|
|
36050
36470
|
this.nativeCatalog = [];
|
|
@@ -36069,6 +36489,14 @@ var ChartCell = class {
|
|
|
36069
36489
|
this.presentNatives = [];
|
|
36070
36490
|
this.rangeBars = 0;
|
|
36071
36491
|
this.pendingRange = null;
|
|
36492
|
+
/** Last symbol we toasted "no provider serves this" for — once per symbol (the
|
|
36493
|
+
* core re-reports on every provider-index settle). */
|
|
36494
|
+
this.unresolvedToasted = null;
|
|
36495
|
+
/** The cell's third-party state bag (`ext` of the persisted per-chart state) —
|
|
36496
|
+
* seeded from the boot/restored document, refreshed by handler `serialize` calls at
|
|
36497
|
+
* dehydrate time. Entries with no registered handler this session ride along
|
|
36498
|
+
* verbatim, so a document never loses a plugin's state in the plugin's absence. */
|
|
36499
|
+
this.extState = {};
|
|
36072
36500
|
/** Indicator titles (this cell's in-chart legend rows) shown. */
|
|
36073
36501
|
this.indicatorTitlesOn = true;
|
|
36074
36502
|
/** Plot values beside this cell's legend titles shown. */
|
|
@@ -36135,9 +36563,16 @@ var ChartCell = class {
|
|
|
36135
36563
|
this.pendingManifestNames = [...seed.indicators.manifest];
|
|
36136
36564
|
}
|
|
36137
36565
|
this.volumeIntent = seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume;
|
|
36566
|
+
this.extState = { ...seed.ext ?? {} };
|
|
36138
36567
|
this.inner.on("load:end", () => {
|
|
36139
36568
|
this.volumeMayBePending = false;
|
|
36140
36569
|
});
|
|
36570
|
+
this.inner.on("data:unresolved", ({ symbol: symbol2, providers }) => {
|
|
36571
|
+
if (this.unresolvedToasted === symbol2) return;
|
|
36572
|
+
this.unresolvedToasted = symbol2;
|
|
36573
|
+
const list = providers.length > 0 ? providers.join(", ") : "none";
|
|
36574
|
+
this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
|
|
36575
|
+
});
|
|
36141
36576
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
36142
36577
|
this.inner.on("load:end", () => this.watermark?.setLoading(false));
|
|
36143
36578
|
const tz = deps.timezone();
|
|
@@ -36149,12 +36584,15 @@ var ChartCell = class {
|
|
|
36149
36584
|
this.watermarkOn = seed.watermark ?? deps.watermark;
|
|
36150
36585
|
this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
|
|
36151
36586
|
if (!this.watermarkOn) this.watermark?.setVisible(false);
|
|
36152
|
-
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
|
|
36587
|
+
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
36153
36588
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
36154
36589
|
this.statusline?.onChart(this.inner);
|
|
36155
36590
|
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
36156
36591
|
void this.inner.data.ready().then(() => {
|
|
36157
|
-
if (this.inner && this.state.symbol)
|
|
36592
|
+
if (this.inner && this.state.symbol) {
|
|
36593
|
+
this.statusline?.setSymbol(this.state.symbol);
|
|
36594
|
+
this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
|
|
36595
|
+
}
|
|
36158
36596
|
this.refreshSessionAvailable();
|
|
36159
36597
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
36160
36598
|
});
|
|
@@ -36298,11 +36736,13 @@ var ChartCell = class {
|
|
|
36298
36736
|
const eth = "Extended hours (ETH)";
|
|
36299
36737
|
const sessionSection = {
|
|
36300
36738
|
title: "Trading session",
|
|
36739
|
+
id: "trading-session",
|
|
36301
36740
|
placement: "symbol",
|
|
36302
36741
|
rows: [
|
|
36303
36742
|
{
|
|
36304
36743
|
kind: "select",
|
|
36305
36744
|
label: "Session",
|
|
36745
|
+
id: "session",
|
|
36306
36746
|
options: [rth, eth],
|
|
36307
36747
|
get: () => this.session === "extended" ? eth : rth,
|
|
36308
36748
|
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
@@ -36310,12 +36750,14 @@ var ChartCell = class {
|
|
|
36310
36750
|
{
|
|
36311
36751
|
kind: "color",
|
|
36312
36752
|
label: "Pre-market",
|
|
36753
|
+
id: "premarket-color",
|
|
36313
36754
|
get: () => this.sessionShadeColor("premarketColor"),
|
|
36314
36755
|
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
36315
36756
|
},
|
|
36316
36757
|
{
|
|
36317
36758
|
kind: "color",
|
|
36318
36759
|
label: "Post-market",
|
|
36760
|
+
id: "postmarket-color",
|
|
36319
36761
|
get: () => this.sessionShadeColor("postmarketColor"),
|
|
36320
36762
|
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
36321
36763
|
}
|
|
@@ -36323,11 +36765,13 @@ var ChartCell = class {
|
|
|
36323
36765
|
};
|
|
36324
36766
|
const advanced = {
|
|
36325
36767
|
title: "Advanced",
|
|
36768
|
+
id: "advanced",
|
|
36326
36769
|
placement: "end",
|
|
36327
36770
|
rows: [
|
|
36328
36771
|
{
|
|
36329
36772
|
kind: "select",
|
|
36330
36773
|
label: "Bars to fetch",
|
|
36774
|
+
id: "bars",
|
|
36331
36775
|
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
36332
36776
|
get: () => String(this.state.bars ?? 1e3),
|
|
36333
36777
|
set: (v) => {
|
|
@@ -36340,11 +36784,13 @@ var ChartCell = class {
|
|
|
36340
36784
|
};
|
|
36341
36785
|
const watermarkSection = {
|
|
36342
36786
|
title: "Watermark",
|
|
36787
|
+
id: "watermark",
|
|
36343
36788
|
placement: "symbol",
|
|
36344
36789
|
rows: [
|
|
36345
36790
|
{
|
|
36346
36791
|
kind: "toggle",
|
|
36347
36792
|
label: "Symbol watermark",
|
|
36793
|
+
id: "visible",
|
|
36348
36794
|
get: () => this.watermarkOn,
|
|
36349
36795
|
set: (v) => this.setWatermarkVisible(v)
|
|
36350
36796
|
}
|
|
@@ -36355,22 +36801,25 @@ var ChartCell = class {
|
|
|
36355
36801
|
const sl = this.statusline;
|
|
36356
36802
|
sections.push({
|
|
36357
36803
|
title: "Status line",
|
|
36804
|
+
id: "status-line",
|
|
36358
36805
|
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" },
|
|
36806
|
+
{ kind: "heading", label: "Status line", id: "parts" },
|
|
36807
|
+
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
|
|
36808
|
+
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
|
|
36809
|
+
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
|
|
36810
|
+
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
|
|
36811
|
+
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
36365
36812
|
{
|
|
36366
36813
|
kind: "toggle",
|
|
36367
36814
|
label: "Titles",
|
|
36815
|
+
id: "indicator-titles",
|
|
36368
36816
|
get: () => this.indicatorTitlesOn,
|
|
36369
36817
|
set: (v) => this.setIndicatorTitlesVisible(v)
|
|
36370
36818
|
},
|
|
36371
36819
|
{
|
|
36372
36820
|
kind: "toggle",
|
|
36373
36821
|
label: "Values",
|
|
36822
|
+
id: "indicator-values",
|
|
36374
36823
|
get: () => this.indicatorValuesOn,
|
|
36375
36824
|
set: (v) => this.setIndicatorValuesVisible(v)
|
|
36376
36825
|
}
|
|
@@ -36423,6 +36872,7 @@ var ChartCell = class {
|
|
|
36423
36872
|
/** Switch this cell's market in place (the chart instance survives). */
|
|
36424
36873
|
setSymbol(symbol) {
|
|
36425
36874
|
if (!this.inner || symbol === this.symbol) return;
|
|
36875
|
+
this.unresolvedToasted = null;
|
|
36426
36876
|
void this.inner.setMarket({ symbol });
|
|
36427
36877
|
}
|
|
36428
36878
|
setTimeframe(timeframe) {
|
|
@@ -36513,6 +36963,41 @@ var ChartCell = class {
|
|
|
36513
36963
|
for (const entry of list) if (entry.enabled) this.addManifestInstance(entry, { record: false });
|
|
36514
36964
|
}
|
|
36515
36965
|
}
|
|
36966
|
+
/**
|
|
36967
|
+
* Replace the indicator ledger: natives converge to the listed set (volume
|
|
36968
|
+
* included — removing it sticks, the core's auto-add respects the opt-out), and
|
|
36969
|
+
* manifest instances are re-created by name, held until the shared manifest
|
|
36970
|
+
* resolves. Convergence is state application, not user edits — nothing enters the
|
|
36971
|
+
* undo timeline.
|
|
36972
|
+
*/
|
|
36973
|
+
applyIndicatorLedger(led) {
|
|
36974
|
+
const chart = this.inner;
|
|
36975
|
+
if (!chart) return;
|
|
36976
|
+
this.volumeIntent = led.natives.includes("volume");
|
|
36977
|
+
this.history.silently(() => {
|
|
36978
|
+
const present = chart.presentNativeIndicators();
|
|
36979
|
+
for (const type of led.natives) {
|
|
36980
|
+
if (!present.includes(type)) chart.addNativeIndicator(type);
|
|
36981
|
+
}
|
|
36982
|
+
for (const type of present) {
|
|
36983
|
+
if (!led.natives.includes(type)) chart.addNativeIndicator(type).remove();
|
|
36984
|
+
}
|
|
36985
|
+
for (const it of [...this.instances]) this.dropInstance(it);
|
|
36986
|
+
if (this.manifest.length > 0) {
|
|
36987
|
+
for (const name of led.manifest) {
|
|
36988
|
+
const entry = this.manifest.find((e) => e.name === name);
|
|
36989
|
+
if (entry) this.addManifestInstance(entry, { record: false });
|
|
36990
|
+
}
|
|
36991
|
+
this.pendingManifestNames = null;
|
|
36992
|
+
} else if (!this.deps.manifestSettled()) {
|
|
36993
|
+
this.pendingManifestNames = [...led.manifest];
|
|
36994
|
+
} else {
|
|
36995
|
+
this.pendingManifestNames = null;
|
|
36996
|
+
}
|
|
36997
|
+
});
|
|
36998
|
+
this.syncPresentNatives();
|
|
36999
|
+
this.refreshNativeCatalog();
|
|
37000
|
+
}
|
|
36516
37001
|
/** The picker's library rows: supported natives first, then the manifest. */
|
|
36517
37002
|
libraryRows() {
|
|
36518
37003
|
return [
|
|
@@ -36542,10 +37027,20 @@ var ChartCell = class {
|
|
|
36542
37027
|
if (index < present.length) this.removeNative(present[index].type);
|
|
36543
37028
|
else this.removeInstance(index - present.length);
|
|
36544
37029
|
}
|
|
37030
|
+
/**
|
|
37031
|
+
* Add a script indicator through the PUBLIC seam (`ctx.addIndicator`) — same undo/
|
|
37032
|
+
* redo and picker plumbing as a manifest entry, but flagged `external` so the
|
|
37033
|
+
* persisted ledger never records a name the manifest can't resolve (the plugin owns
|
|
37034
|
+
* persistence via `registerStatePersistence`). Recording follows the ambient mute:
|
|
37035
|
+
* a persistence handler's `restore` runs silently, a user-driven call records.
|
|
37036
|
+
*/
|
|
37037
|
+
addExternalIndicator(entry) {
|
|
37038
|
+
this.addManifestInstance({ ...entry, enabled: true }, { external: true });
|
|
37039
|
+
}
|
|
36545
37040
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
36546
37041
|
addManifestInstance(entry, opts = {}) {
|
|
36547
37042
|
if (this.destroyed) return;
|
|
36548
|
-
const it = { entry, handle: this.addToChart(entry) };
|
|
37043
|
+
const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
|
|
36549
37044
|
this.instances.push(it);
|
|
36550
37045
|
this.deps.onIndicatorsChanged(this.id);
|
|
36551
37046
|
if (opts.record === false) return;
|
|
@@ -36626,12 +37121,97 @@ var ChartCell = class {
|
|
|
36626
37121
|
return null;
|
|
36627
37122
|
}
|
|
36628
37123
|
}
|
|
37124
|
+
// ── third-party state (the `ext` seam) ──
|
|
37125
|
+
/** The cell-bound surface persistence handlers work against (built per call — the
|
|
37126
|
+
* widget-context rule; nothing here may be cached by a handler). Its add methods
|
|
37127
|
+
* are ALWAYS muted — a `restore` that fetches before adding escapes the sync mute
|
|
37128
|
+
* of {@link restorePersistedExt}, and a state application must never enter the
|
|
37129
|
+
* undo timeline, however late its continuation lands. */
|
|
37130
|
+
stateContext() {
|
|
37131
|
+
return {
|
|
37132
|
+
cellId: this.id,
|
|
37133
|
+
chart: this.chart,
|
|
37134
|
+
addIndicator: (entry) => this.history.silently(() => this.addExternalIndicator(entry)),
|
|
37135
|
+
addNativeIndicator: (type) => this.history.silently(() => this.addNative(type))
|
|
37136
|
+
};
|
|
37137
|
+
}
|
|
37138
|
+
/**
|
|
37139
|
+
* Run the registered cell-scope `restore` handlers against the cell's restored
|
|
37140
|
+
* `ext` bag — the workspace calls this AFTER the core state is in place (chart
|
|
37141
|
+
* alive and wired, indicator ledger converged). Muted: nothing a restore does
|
|
37142
|
+
* enters the undo timeline. Handlers only see keys the document carries; a failing
|
|
37143
|
+
* handler is contained (one broken plugin must not take the cell down).
|
|
37144
|
+
*/
|
|
37145
|
+
restorePersistedExt() {
|
|
37146
|
+
if (this.destroyed) return;
|
|
37147
|
+
for (const h of statePersistenceHandlers("cell")) {
|
|
37148
|
+
if (!(h.key in this.extState)) continue;
|
|
37149
|
+
try {
|
|
37150
|
+
this.history.silently(() => h.restore(this.extState[h.key], this.stateContext()));
|
|
37151
|
+
} catch (err) {
|
|
37152
|
+
console.warn(`[vela] state persistence "${h.key}" restore failed:`, err);
|
|
37153
|
+
}
|
|
37154
|
+
}
|
|
37155
|
+
}
|
|
37156
|
+
/** Assemble the cell's `ext` bag: fresh handler snapshots merged OVER the preserved
|
|
37157
|
+
* entries — a key with no handler this session rides along verbatim; a registered
|
|
37158
|
+
* handler returning `undefined` withdraws its entry. */
|
|
37159
|
+
dehydrateExt() {
|
|
37160
|
+
const ext = { ...this.extState };
|
|
37161
|
+
for (const h of statePersistenceHandlers("cell")) {
|
|
37162
|
+
try {
|
|
37163
|
+
const value = h.serialize(this.stateContext());
|
|
37164
|
+
if (value === void 0) delete ext[h.key];
|
|
37165
|
+
else ext[h.key] = value;
|
|
37166
|
+
} catch (err) {
|
|
37167
|
+
console.warn(`[vela] state persistence "${h.key}" serialize failed:`, err);
|
|
37168
|
+
}
|
|
37169
|
+
}
|
|
37170
|
+
this.extState = ext;
|
|
37171
|
+
return Object.keys(ext).length > 0 ? ext : void 0;
|
|
37172
|
+
}
|
|
36629
37173
|
// ── lifecycle ──
|
|
37174
|
+
/**
|
|
37175
|
+
* Apply a restored cell state IN PLACE — the chart instance survives (the market
|
|
37176
|
+
* switches via `setMarket`) while cosmetics, renderer config, drawings, and the
|
|
37177
|
+
* indicator ledger converge to the document. The workspace takes this path when a
|
|
37178
|
+
* state document lands on a grid of the same shape (async-storage boot, host
|
|
37179
|
+
* `applyState`), so chart references, indicator handles, event subscriptions, and
|
|
37180
|
+
* the cell host all stay valid.
|
|
37181
|
+
*/
|
|
37182
|
+
rehydrate(cs) {
|
|
37183
|
+
if (!this.inner || this.destroyed) return;
|
|
37184
|
+
if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
|
|
37185
|
+
if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
|
|
37186
|
+
if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
|
|
37187
|
+
if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
|
|
37188
|
+
if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
|
|
37189
|
+
if (cs.drawings != null) this.inner.drawings.fromJSON(cs.drawings);
|
|
37190
|
+
if (cs.indicators) this.applyIndicatorLedger(cs.indicators);
|
|
37191
|
+
this.extState = { ...cs.ext ?? {} };
|
|
37192
|
+
this.restorePersistedExt();
|
|
37193
|
+
const symbol = prefixedSymbol(cs);
|
|
37194
|
+
const session = normalizeSession(cs.session) ?? "regular";
|
|
37195
|
+
const bars = typeof cs.bars === "number" && Number.isFinite(cs.bars) && cs.bars > 0 ? cs.bars : 0;
|
|
37196
|
+
const next = {};
|
|
37197
|
+
if (symbol && symbol !== this.symbol) next.symbol = symbol;
|
|
37198
|
+
if (cs.timeframe && cs.timeframe !== this.timeframe) next.timeframe = cs.timeframe;
|
|
37199
|
+
if (session !== this.session) {
|
|
37200
|
+
this.state.session = session;
|
|
37201
|
+
next.session = session;
|
|
37202
|
+
}
|
|
37203
|
+
if (bars > 0 && bars !== this.state.bars) {
|
|
37204
|
+
this.state.bars = bars;
|
|
37205
|
+
next.bars = Math.max(bars, this.rangeBars);
|
|
37206
|
+
}
|
|
37207
|
+
if (Object.keys(next).length > 0) void this.inner.setMarket(next);
|
|
37208
|
+
}
|
|
36630
37209
|
/** Snapshot everything the pool needs to restore this slot later. The market fields
|
|
36631
37210
|
* come from the LIVE config (`chart.market`) — the requested identity — so a switch
|
|
36632
37211
|
* still loading when the snapshot is taken (persist-on-close) is not lost. */
|
|
36633
37212
|
dehydrate() {
|
|
36634
37213
|
const live = this.inner?.market;
|
|
37214
|
+
const ext = this.inner ? this.dehydrateExt() : Object.keys(this.extState).length > 0 ? { ...this.extState } : void 0;
|
|
36635
37215
|
return {
|
|
36636
37216
|
...this.state,
|
|
36637
37217
|
...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
|
|
@@ -36644,14 +37224,17 @@ var ChartCell = class {
|
|
|
36644
37224
|
// Natives from the chart's SYNC registry read — an async catalog mirror here
|
|
36645
37225
|
// lost unload-time saves, and the old empty-set fallbacks resurrected removed
|
|
36646
37226
|
// indicators. Manifest names fall back to the restored ledger only until the
|
|
36647
|
-
// shared manifest settles. See {@link indicatorLedger}.
|
|
37227
|
+
// shared manifest settles. See {@link indicatorLedger}. External instances
|
|
37228
|
+
// (`ctx.addIndicator`) stay out: their names would never resolve against the
|
|
37229
|
+
// manifest — their plugin persists them via the `ext` seam instead.
|
|
36648
37230
|
indicators: indicatorLedger({
|
|
36649
37231
|
present: this.inner ? this.inner.presentNativeIndicators() : [],
|
|
36650
|
-
instanceNames: this.instances.map((it) => it.entry.name),
|
|
37232
|
+
instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
|
|
36651
37233
|
pendingManifest: this.pendingManifestNames,
|
|
36652
37234
|
manifestSettled: this.deps.manifestSettled(),
|
|
36653
37235
|
volumePending: this.volumeMayBePending && this.volumeIntent
|
|
36654
|
-
})
|
|
37236
|
+
}),
|
|
37237
|
+
...ext ? { ext } : {}
|
|
36655
37238
|
};
|
|
36656
37239
|
}
|
|
36657
37240
|
destroy() {
|
|
@@ -36688,6 +37271,9 @@ function buildContext(host) {
|
|
|
36688
37271
|
togglePanel: (id, open2) => host.togglePanel(id, open2),
|
|
36689
37272
|
host: host.root,
|
|
36690
37273
|
toast: (message, kind) => host.toast(message, kind),
|
|
37274
|
+
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
37275
|
+
addNativeIndicator: (type) => host.active()?.addNative(type),
|
|
37276
|
+
stateChanged: () => host.stateDirty(),
|
|
36691
37277
|
cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
|
|
36692
37278
|
activeCellId: active?.id ?? "",
|
|
36693
37279
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
@@ -37049,6 +37635,10 @@ var VelaWorkspace = class {
|
|
|
37049
37635
|
/** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
|
|
37050
37636
|
this.priceScalePressY = 0;
|
|
37051
37637
|
this.attachmentDisposers = /* @__PURE__ */ new Map();
|
|
37638
|
+
/** The document-level third-party state bag (`state.ext`) — seeded from the restored
|
|
37639
|
+
* document, refreshed by global-scope handler `serialize` calls at snapshot time.
|
|
37640
|
+
* Entries with no registered handler this session ride along verbatim. */
|
|
37641
|
+
this.extState = {};
|
|
37052
37642
|
/** The single grid-wide attribution mark — re-inked on a live theme swap. */
|
|
37053
37643
|
this.attributionMark = null;
|
|
37054
37644
|
this.onRootKeydown = (ev) => this.routeTyping(ev);
|
|
@@ -37063,6 +37653,7 @@ var VelaWorkspace = class {
|
|
|
37063
37653
|
state: () => ({ ...this.syncOpts })
|
|
37064
37654
|
};
|
|
37065
37655
|
registerBuiltinLayouts();
|
|
37656
|
+
registerBuiltinChartTypes();
|
|
37066
37657
|
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
37067
37658
|
if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
|
|
37068
37659
|
this.opts = opts;
|
|
@@ -37086,8 +37677,15 @@ var VelaWorkspace = class {
|
|
|
37086
37677
|
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
|
|
37087
37678
|
this.applySyncSetting(kind, sync?.[kind]);
|
|
37088
37679
|
}
|
|
37089
|
-
this.
|
|
37680
|
+
this.monoLayout = opts.layout === false;
|
|
37681
|
+
const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
|
|
37682
|
+
this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
|
|
37683
|
+
this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
|
|
37684
|
+
this.topbarComp = resolveTopbarComposition(opts.topbar);
|
|
37685
|
+
this.indicatorsOverride = topbarActionOverride("indicators");
|
|
37686
|
+
this.screenshotOverride = topbarActionOverride("screenshot");
|
|
37090
37687
|
if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
|
|
37688
|
+
if (boot?.ext) this.extState = { ...boot.ext };
|
|
37091
37689
|
if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
|
|
37092
37690
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
37093
37691
|
const bootActive = boot?.activeCellId ?? null;
|
|
@@ -37102,6 +37700,8 @@ var VelaWorkspace = class {
|
|
|
37102
37700
|
});
|
|
37103
37701
|
this.symbolPicker = new SymbolPicker({
|
|
37104
37702
|
host: this.root,
|
|
37703
|
+
// Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
|
|
37704
|
+
iconFor: (d) => this.feed.symbolIconOf(d),
|
|
37105
37705
|
onSelect: (ticker) => this.active.setSymbol(ticker),
|
|
37106
37706
|
onOpenChange: (open2) => {
|
|
37107
37707
|
if (open2) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
|
|
@@ -37109,7 +37709,7 @@ var VelaWorkspace = class {
|
|
|
37109
37709
|
}
|
|
37110
37710
|
});
|
|
37111
37711
|
this.symbolPicker.setSource(() => this.feed.symbols());
|
|
37112
|
-
this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
|
|
37712
|
+
this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
|
|
37113
37713
|
host: this.root,
|
|
37114
37714
|
library: () => this.active.libraryRows(),
|
|
37115
37715
|
onChart: () => this.active.onChartRows(),
|
|
@@ -37128,6 +37728,9 @@ var VelaWorkspace = class {
|
|
|
37128
37728
|
});
|
|
37129
37729
|
this.topbar = new Topbar(this.root, {
|
|
37130
37730
|
symbol: "",
|
|
37731
|
+
// RAW option, not the resolved lists — the bar distinguishes a host-declared
|
|
37732
|
+
// side (list is law) from a default one (an override's `order` may flow).
|
|
37733
|
+
composition: opts.topbar,
|
|
37131
37734
|
onSymbolClick: () => this.symbolPicker.open(),
|
|
37132
37735
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
37133
37736
|
onUndoClick: () => this.active.history.undo(),
|
|
@@ -37141,7 +37744,9 @@ var VelaWorkspace = class {
|
|
|
37141
37744
|
onTimeframe: (tf) => this.setActiveTimeframe(tf),
|
|
37142
37745
|
onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
|
|
37143
37746
|
onPriceStyle: (style) => this.active.setPriceStyle(style),
|
|
37144
|
-
|
|
37747
|
+
// Single-chart mode: no layout block at all — the topbar renders no layout
|
|
37748
|
+
// button and no sync switches (see TopbarOptions.layout).
|
|
37749
|
+
layout: this.monoLayout ? void 0 : {
|
|
37145
37750
|
current: this.def.id,
|
|
37146
37751
|
// The picker composes dynamic layouts on its grid canvas; registered
|
|
37147
37752
|
// presets the canvas cannot express (bespoke plugin areas) list as rows.
|
|
@@ -37166,8 +37771,11 @@ var VelaWorkspace = class {
|
|
|
37166
37771
|
});
|
|
37167
37772
|
const main = doc.createElement("div");
|
|
37168
37773
|
main.className = "vela-ws-main";
|
|
37774
|
+
const toolbar = buildToolbar(opts.drawings);
|
|
37775
|
+
this.drawingsEnabled = opts.drawings !== false;
|
|
37776
|
+
this.toolbarDef = toolbar.definition;
|
|
37169
37777
|
let toolbarHost = null;
|
|
37170
|
-
if (opts.drawingToolbar !== false) {
|
|
37778
|
+
if (this.drawingsEnabled && toolbar.visible && opts.drawingToolbar !== false) {
|
|
37171
37779
|
toolbarHost = doc.createElement("div");
|
|
37172
37780
|
toolbarHost.className = "vela-ws-toolbar";
|
|
37173
37781
|
main.appendChild(toolbarHost);
|
|
@@ -37180,13 +37788,13 @@ var VelaWorkspace = class {
|
|
|
37180
37788
|
context: () => this.context(),
|
|
37181
37789
|
changed: () => this.markStateDirty()
|
|
37182
37790
|
});
|
|
37183
|
-
this.objectTree = new ObjectTree(main);
|
|
37791
|
+
this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
|
|
37184
37792
|
this.dataWindow = new DataWindow(main);
|
|
37185
37793
|
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
37186
37794
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
37187
37795
|
this.dock.refresh();
|
|
37188
37796
|
this.root.appendChild(main);
|
|
37189
|
-
this.
|
|
37797
|
+
this.toastHost = new Toast(this.gridEl);
|
|
37190
37798
|
const attribution = rendererDefaults().attribution;
|
|
37191
37799
|
if (attribution !== false) {
|
|
37192
37800
|
const background = resolveTheme(opts.theme).background;
|
|
@@ -37232,7 +37840,7 @@ var VelaWorkspace = class {
|
|
|
37232
37840
|
}
|
|
37233
37841
|
}
|
|
37234
37842
|
) : null;
|
|
37235
|
-
this.drawToolbar?.setDefinition(
|
|
37843
|
+
this.drawToolbar?.setDefinition(this.toolbarDef);
|
|
37236
37844
|
this.drawToolbar?.setVisible(true);
|
|
37237
37845
|
this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
|
|
37238
37846
|
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
@@ -37252,13 +37860,15 @@ var VelaWorkspace = class {
|
|
|
37252
37860
|
timeframe: "60",
|
|
37253
37861
|
onSymbolClick: () => this.symbolPicker.open(),
|
|
37254
37862
|
onTimeframeClick: () => this.openTimeframeDrawer(),
|
|
37255
|
-
|
|
37863
|
+
// Same visibility truth as the desktop bar: composition-hidden
|
|
37864
|
+
// indicators lose their mobile stop too; an override takes it over.
|
|
37865
|
+
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
37256
37866
|
getContext: () => this.context(),
|
|
37257
|
-
onDrawingsClick: () => this.openDrawingsDrawer(),
|
|
37867
|
+
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
37258
37868
|
onMoreClick: () => this.openMoreDrawer(),
|
|
37259
37869
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
37260
37870
|
}) : null;
|
|
37261
|
-
this.drawingPill = new DrawingPill(this.gridEl);
|
|
37871
|
+
this.drawingPill = this.drawingsEnabled ? new DrawingPill(this.gridEl) : null;
|
|
37262
37872
|
hostEl.appendChild(this.root);
|
|
37263
37873
|
this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
|
|
37264
37874
|
this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
|
|
@@ -37297,6 +37907,7 @@ var VelaWorkspace = class {
|
|
|
37297
37907
|
this.manifestSettled = true;
|
|
37298
37908
|
}
|
|
37299
37909
|
this.mountAttachments();
|
|
37910
|
+
this.restoreGlobalExt();
|
|
37300
37911
|
}
|
|
37301
37912
|
// ── access ──────────────────────────────────────────────────
|
|
37302
37913
|
/** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
|
|
@@ -37358,7 +37969,8 @@ var VelaWorkspace = class {
|
|
|
37358
37969
|
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
37359
37970
|
togglePanel: (id, open2) => this.dock.toggle(id, open2),
|
|
37360
37971
|
root: this.root,
|
|
37361
|
-
toast: (message, kind) => this.
|
|
37972
|
+
toast: (message, kind) => this.toastHost.show(message, kind),
|
|
37973
|
+
stateDirty: () => this.markStateDirty()
|
|
37362
37974
|
});
|
|
37363
37975
|
}
|
|
37364
37976
|
/** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
|
|
@@ -37397,22 +38009,35 @@ var VelaWorkspace = class {
|
|
|
37397
38009
|
if (this.trackSizes.size > 0) state.trackSizes = Object.fromEntries([...this.trackSizes].map(([k, v]) => [k, { ...v }]));
|
|
37398
38010
|
const panels2 = this.dock.getState();
|
|
37399
38011
|
if (panels2) state.panels = panels2;
|
|
38012
|
+
const ext = { ...this.extState };
|
|
38013
|
+
for (const h of statePersistenceHandlers("global")) {
|
|
38014
|
+
try {
|
|
38015
|
+
const value = h.serialize(this.context());
|
|
38016
|
+
if (value === void 0) delete ext[h.key];
|
|
38017
|
+
else ext[h.key] = value;
|
|
38018
|
+
} catch (err) {
|
|
38019
|
+
console.warn(`[vela] state persistence "${h.key}" serialize failed:`, err);
|
|
38020
|
+
}
|
|
38021
|
+
}
|
|
38022
|
+
this.extState = ext;
|
|
38023
|
+
if (Object.keys(ext).length > 0) state.ext = ext;
|
|
37400
38024
|
return state;
|
|
37401
38025
|
}
|
|
37402
38026
|
/**
|
|
37403
38027
|
* Restore a state document produced by {@link getState} (untrusted-safe: malformed
|
|
37404
|
-
* fields are dropped).
|
|
37405
|
-
* layout,
|
|
37406
|
-
*
|
|
38028
|
+
* fields are dropped). When the document matches the live grid one-to-one — same
|
|
38029
|
+
* layout, same ordered slot identities — it is applied IN PLACE: every chart
|
|
38030
|
+
* instance survives (markets switch via `setMarket`), so chart references,
|
|
38031
|
+
* indicator handles, and event subscriptions stay valid. Any structural difference
|
|
38032
|
+
* (layout, slot count, renamed ids) falls back to the full rebuild: prefs, sync
|
|
38033
|
+
* links, layout, and every slot are replaced, current cells rebuilt from the
|
|
38034
|
+
* document. A layout id that is not registered keeps the current grid (register
|
|
38035
|
+
* custom layouts first).
|
|
37407
38036
|
*/
|
|
37408
38037
|
applyState(state) {
|
|
37409
38038
|
if (this.destroyed) return;
|
|
37410
38039
|
const st = sanitizeState(state);
|
|
37411
38040
|
if (!st) return;
|
|
37412
|
-
if (st.timezone) {
|
|
37413
|
-
this.timezone = st.timezone;
|
|
37414
|
-
this.bottombar?.setTimezone(st.timezone);
|
|
37415
|
-
}
|
|
37416
38041
|
if (st.favorites) this.favs = [...st.favorites];
|
|
37417
38042
|
if (st.timeframeFavorites) {
|
|
37418
38043
|
this.tfFavs = [...st.timeframeFavorites];
|
|
@@ -37422,6 +38047,36 @@ var VelaWorkspace = class {
|
|
|
37422
38047
|
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
37423
38048
|
this.trackSizes.clear();
|
|
37424
38049
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
38050
|
+
const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
|
|
38051
|
+
const liveCount = this.def.cells.length;
|
|
38052
|
+
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]));
|
|
38053
|
+
if (inPlace) {
|
|
38054
|
+
if (st.favorites) {
|
|
38055
|
+
for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
|
|
38056
|
+
}
|
|
38057
|
+
this.drawingLinks.clear();
|
|
38058
|
+
for (const [i] of this.def.cells.entries()) {
|
|
38059
|
+
const { id, ...cs } = st.charts[i];
|
|
38060
|
+
this.cellsById.get(id)?.rehydrate(cs);
|
|
38061
|
+
}
|
|
38062
|
+
if (st.timezone) this.setTimezone(st.timezone);
|
|
38063
|
+
this.pool.clear();
|
|
38064
|
+
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
38065
|
+
this.order = st.charts.map((c) => c.id);
|
|
38066
|
+
this.applyGrid();
|
|
38067
|
+
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
38068
|
+
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
38069
|
+
else this.setActiveCell(nextActive2);
|
|
38070
|
+
this.refreshRetention();
|
|
38071
|
+
this.extState = { ...st.ext ?? {} };
|
|
38072
|
+
this.restoreGlobalExt();
|
|
38073
|
+
this.markStateDirty();
|
|
38074
|
+
return;
|
|
38075
|
+
}
|
|
38076
|
+
if (st.timezone) {
|
|
38077
|
+
this.timezone = st.timezone;
|
|
38078
|
+
this.bottombar?.setTimezone(st.timezone);
|
|
38079
|
+
}
|
|
37425
38080
|
for (const [id, cell] of [...this.cellsById]) {
|
|
37426
38081
|
cell.destroy();
|
|
37427
38082
|
this.cellsById.delete(id);
|
|
@@ -37431,7 +38086,7 @@ var VelaWorkspace = class {
|
|
|
37431
38086
|
this.drawingLinks.clear();
|
|
37432
38087
|
for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
|
|
37433
38088
|
this.order = st.charts.map((c) => c.id);
|
|
37434
|
-
const def = ensureLayout(st.layout);
|
|
38089
|
+
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
37435
38090
|
if (def) this.def = def;
|
|
37436
38091
|
this.cellBackend = this.backendFor(this.def);
|
|
37437
38092
|
this.applyGrid();
|
|
@@ -37443,8 +38098,24 @@ var VelaWorkspace = class {
|
|
|
37443
38098
|
else this.setActiveCell(nextActive);
|
|
37444
38099
|
this.refreshRetention();
|
|
37445
38100
|
this.events.emit("layout:changed", { layout: this.def.id });
|
|
38101
|
+
this.extState = { ...st.ext ?? {} };
|
|
38102
|
+
this.restoreGlobalExt();
|
|
37446
38103
|
this.markStateDirty();
|
|
37447
38104
|
}
|
|
38105
|
+
/** Run the registered global-scope `restore` handlers against the document-level
|
|
38106
|
+
* `ext` bag — keys present in the document only; a failing handler is contained.
|
|
38107
|
+
* Handlers whose restore touches chart content should be `scope: 'cell'` instead
|
|
38108
|
+
* (those run inside the cell's history-mute). */
|
|
38109
|
+
restoreGlobalExt() {
|
|
38110
|
+
for (const h of statePersistenceHandlers("global")) {
|
|
38111
|
+
if (!(h.key in this.extState)) continue;
|
|
38112
|
+
try {
|
|
38113
|
+
h.restore(this.extState[h.key], this.context());
|
|
38114
|
+
} catch (err) {
|
|
38115
|
+
console.warn(`[vela] state persistence "${h.key}" restore failed:`, err);
|
|
38116
|
+
}
|
|
38117
|
+
}
|
|
38118
|
+
}
|
|
37448
38119
|
/** Set the workspace-global display timezone — applied to EVERY cell. */
|
|
37449
38120
|
setTimezone(zone) {
|
|
37450
38121
|
this.timezone = zone;
|
|
@@ -37480,6 +38151,7 @@ var VelaWorkspace = class {
|
|
|
37480
38151
|
*/
|
|
37481
38152
|
setLayout(layout) {
|
|
37482
38153
|
if (this.destroyed) return;
|
|
38154
|
+
if (this.monoLayout) return;
|
|
37483
38155
|
const next = this.resolveLayout(layout);
|
|
37484
38156
|
const nextBackend = this.backendFor(next);
|
|
37485
38157
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -37509,6 +38181,12 @@ var VelaWorkspace = class {
|
|
|
37509
38181
|
resize() {
|
|
37510
38182
|
this.splitters.layout();
|
|
37511
38183
|
}
|
|
38184
|
+
/** Show a toast over the grid — the same surface the shell's own notices use
|
|
38185
|
+
* (alerts, script errors) and the one contributions reach via `ctx.toast`. */
|
|
38186
|
+
toast(message, kind = "info", durationMs = 3e3) {
|
|
38187
|
+
if (this.destroyed) return;
|
|
38188
|
+
this.toastHost.show(message, kind, durationMs);
|
|
38189
|
+
}
|
|
37512
38190
|
destroy() {
|
|
37513
38191
|
if (this.destroyed) return;
|
|
37514
38192
|
this.persistNow();
|
|
@@ -37534,7 +38212,7 @@ var VelaWorkspace = class {
|
|
|
37534
38212
|
this.topbar.destroy();
|
|
37535
38213
|
this.bottombar?.destroy();
|
|
37536
38214
|
this.mobileBar?.destroy();
|
|
37537
|
-
this.drawingPill
|
|
38215
|
+
this.drawingPill?.destroy();
|
|
37538
38216
|
this.tfDrawer?.destroy();
|
|
37539
38217
|
this.drawingsDrawer?.destroy();
|
|
37540
38218
|
this.moreDrawer?.destroy();
|
|
@@ -37548,10 +38226,10 @@ var VelaWorkspace = class {
|
|
|
37548
38226
|
this.indicatorPicker?.destroy();
|
|
37549
38227
|
this.tfQuick.destroy();
|
|
37550
38228
|
this.shortcutsHelp?.destroy();
|
|
37551
|
-
this.
|
|
38229
|
+
this.toastHost.destroy();
|
|
37552
38230
|
this.alertsMenu?.destroy();
|
|
37553
38231
|
this.glider.stop();
|
|
37554
|
-
sharedBarStore.retain(/* @__PURE__ */ new Set());
|
|
38232
|
+
sharedBarStore.retain(/* @__PURE__ */ new Set(), this);
|
|
37555
38233
|
this.root.remove();
|
|
37556
38234
|
this.events.clear();
|
|
37557
38235
|
}
|
|
@@ -37569,7 +38247,7 @@ var VelaWorkspace = class {
|
|
|
37569
38247
|
this.mobileBar?.renderActions();
|
|
37570
38248
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
37571
38249
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
37572
|
-
this.drawingPill
|
|
38250
|
+
this.drawingPill?.onChart(cell.chart);
|
|
37573
38251
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
37574
38252
|
this.historyUnsub?.();
|
|
37575
38253
|
this.historyUnsub = cell.history.onChange(pushHistory);
|
|
@@ -37695,7 +38373,8 @@ var VelaWorkspace = class {
|
|
|
37695
38373
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
37696
38374
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
37697
38375
|
onStateDirty: () => this.markStateDirty(),
|
|
37698
|
-
manifestSettled: () => this.manifestSettled
|
|
38376
|
+
manifestSettled: () => this.manifestSettled,
|
|
38377
|
+
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
37699
38378
|
});
|
|
37700
38379
|
cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
37701
38380
|
this.cellsById.set(id, cell);
|
|
@@ -37704,6 +38383,7 @@ var VelaWorkspace = class {
|
|
|
37704
38383
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
37705
38384
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
37706
38385
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
38386
|
+
cell.restorePersistedExt();
|
|
37707
38387
|
this.events.emit("cell:created", { id });
|
|
37708
38388
|
}
|
|
37709
38389
|
for (const [i] of this.def.cells.entries()) {
|
|
@@ -37715,12 +38395,13 @@ var VelaWorkspace = class {
|
|
|
37715
38395
|
* cell's whole life, so these live and die with the cell). */
|
|
37716
38396
|
wireCell(cell) {
|
|
37717
38397
|
const chart = cell.chart;
|
|
37718
|
-
chart.on("indicator:error", ({ error }) => this.
|
|
38398
|
+
chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
|
|
37719
38399
|
chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
|
|
37720
38400
|
chart.on("alert", (alert) => {
|
|
37721
|
-
|
|
37722
|
-
|
|
37723
|
-
this.
|
|
38401
|
+
const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
|
|
38402
|
+
this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
|
|
38403
|
+
if (this.alerts.length > this.alertCap) this.alerts.pop();
|
|
38404
|
+
this.toastHost.show(`${source} \u2014 ${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
|
|
37724
38405
|
this.topbar.setAlertCount(this.alerts.length);
|
|
37725
38406
|
});
|
|
37726
38407
|
chart.on("drawing:favorites", ({ favorites }) => {
|
|
@@ -37981,6 +38662,7 @@ var VelaWorkspace = class {
|
|
|
37981
38662
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
37982
38663
|
this.objectTree.setSymbol(cell.symbol);
|
|
37983
38664
|
this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
|
|
38665
|
+
this.bottombar?.setActiveRange(cell.activeRangeId);
|
|
37984
38666
|
}
|
|
37985
38667
|
/** Trigger ② — a cell's price style changed: the topbar button/menu only if active.
|
|
37986
38668
|
* Reads the cell back (not the requested style) so the button reflects what the
|
|
@@ -38065,7 +38747,7 @@ var VelaWorkspace = class {
|
|
|
38065
38747
|
openDrawingsDrawer() {
|
|
38066
38748
|
this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
|
|
38067
38749
|
host: this.root,
|
|
38068
|
-
toolbar: () =>
|
|
38750
|
+
toolbar: () => this.toolbarDef,
|
|
38069
38751
|
// the shared static toolbar's definition (see constructor)
|
|
38070
38752
|
currentTool: () => this.active.chart.drawings.getTool(),
|
|
38071
38753
|
isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
|
|
@@ -38076,26 +38758,32 @@ var VelaWorkspace = class {
|
|
|
38076
38758
|
this.drawingsDrawer.open();
|
|
38077
38759
|
}
|
|
38078
38760
|
openMoreDrawer() {
|
|
38761
|
+
const has = (id) => topbarHas(this.topbarComp, id);
|
|
38079
38762
|
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
38080
38763
|
host: this.root,
|
|
38081
|
-
onUndo: () => this.active.history.undo(),
|
|
38082
|
-
|
|
38083
|
-
onScreenshot: () => this.active.downloadScreenshot(),
|
|
38764
|
+
...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
|
|
38765
|
+
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
|
|
38084
38766
|
canUndo: () => this.active.history.canUndo,
|
|
38085
38767
|
canRedo: () => this.active.history.canRedo,
|
|
38086
38768
|
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
38087
38769
|
priceStyle: () => this.active.priceStyle,
|
|
38088
38770
|
onPriceStyle: (id) => this.active.setPriceStyle(id),
|
|
38089
|
-
panels: () => [...this.dock.list()],
|
|
38771
|
+
panels: () => has("panels") ? [...this.dock.list()] : [],
|
|
38090
38772
|
onTogglePanel: (id) => this.dock.toggle(id),
|
|
38091
|
-
alerts: () => this.alerts.map((a) => ({ title:
|
|
38773
|
+
...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
|
|
38092
38774
|
// Left-aligned actions have their own bottom-bar stop — only the rest
|
|
38093
|
-
// lands in the drawer, or every left action would appear twice.
|
|
38094
|
-
actions
|
|
38775
|
+
// lands in the drawer, or every left action would appear twice. Built-in-id
|
|
38776
|
+
// actions are slot OVERRIDES: they reach the drawer through the slot's own
|
|
38777
|
+
// routed button (screenshot) or stop (indicators), never as an extra row.
|
|
38778
|
+
actions: () => {
|
|
38779
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
38780
|
+
return widgetActions("topbar", this.context()).filter((a) => a.align !== "left" && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
|
|
38781
|
+
},
|
|
38095
38782
|
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
38096
38783
|
// 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
|
-
layout
|
|
38784
|
+
// drawer (the topbar is hidden on mobile). Same reads as the topbar block;
|
|
38785
|
+
// single-chart mode and a composition without 'layout' omit it here too.
|
|
38786
|
+
layout: this.monoLayout || !has("layout") ? void 0 : {
|
|
38099
38787
|
shape: () => layoutShape(this.def),
|
|
38100
38788
|
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
|
|
38101
38789
|
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
@@ -38138,7 +38826,7 @@ var VelaWorkspace = class {
|
|
|
38138
38826
|
this.alertsMenu?.destroy();
|
|
38139
38827
|
const items = this.alerts.length ? this.alerts.map((a, i) => ({
|
|
38140
38828
|
id: String(i),
|
|
38141
|
-
label:
|
|
38829
|
+
label: `${a.source} \xB7 ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
|
|
38142
38830
|
})) : [{ id: "none", label: "No alerts yet", disabled: true }];
|
|
38143
38831
|
this.alertsMenu = new Menu({
|
|
38144
38832
|
host: this.root,
|
|
@@ -38162,9 +38850,23 @@ var VelaWorkspace = class {
|
|
|
38162
38850
|
}
|
|
38163
38851
|
}
|
|
38164
38852
|
}
|
|
38853
|
+
/** Invoke a slot override the way its button would: fresh context, `when` respected. */
|
|
38854
|
+
runOverride(action) {
|
|
38855
|
+
const ctx = this.context();
|
|
38856
|
+
if (!action.when || action.when(ctx)) action.run(ctx);
|
|
38857
|
+
}
|
|
38165
38858
|
/** The default shortcut set — every binding acts on the ACTIVE cell. */
|
|
38166
38859
|
registerDefaultKeys() {
|
|
38167
|
-
this.
|
|
38860
|
+
if (topbarHas(this.topbarComp, "screenshot")) {
|
|
38861
|
+
const ov = this.screenshotOverride;
|
|
38862
|
+
this.keymap.register({
|
|
38863
|
+
id: "chart.screenshot",
|
|
38864
|
+
keys: "mod+alt+s",
|
|
38865
|
+
label: ov ? ov.label : "Download a chart screenshot",
|
|
38866
|
+
category: "Chart",
|
|
38867
|
+
run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
|
|
38868
|
+
});
|
|
38869
|
+
}
|
|
38168
38870
|
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
|
|
38169
38871
|
this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.active.chart.renderer.set("logScale", !this.active.chart.renderer.get("logScale")) });
|
|
38170
38872
|
this.keymap.register({
|
|
@@ -38204,7 +38906,10 @@ var VelaWorkspace = class {
|
|
|
38204
38906
|
this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
|
|
38205
38907
|
this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
|
|
38206
38908
|
this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
|
|
38207
|
-
|
|
38909
|
+
const indOv = this.indicatorsOverride;
|
|
38910
|
+
if (indOv && topbarHas(this.topbarComp, "indicators")) {
|
|
38911
|
+
this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
|
|
38912
|
+
} else if (this.indicatorPicker) {
|
|
38208
38913
|
this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
|
|
38209
38914
|
}
|
|
38210
38915
|
this.keymap.register({
|
|
@@ -38259,7 +38964,7 @@ var VelaWorkspace = class {
|
|
|
38259
38964
|
if (!raw) continue;
|
|
38260
38965
|
symbols.add(this.feed.resolveSymbol(raw)?.ticker ?? raw);
|
|
38261
38966
|
}
|
|
38262
|
-
sharedBarStore.retain(symbols);
|
|
38967
|
+
sharedBarStore.retain(symbols, this);
|
|
38263
38968
|
}
|
|
38264
38969
|
};
|
|
38265
38970
|
|