@luxalgo/vela 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +28 -0
- package/README.md +135 -0
- package/dist/DataProvider-DKNDHpNv.d.cts +134 -0
- package/dist/DataProvider-Dzd-Erlk.d.ts +134 -0
- package/dist/chunk-7UFX5ZIG.js +5976 -0
- package/dist/chunk-GYD2THPV.js +252 -0
- package/dist/chunk-KCCZNKH7.js +19137 -0
- package/dist/chunk-KM6LHB3Y.js +76 -0
- package/dist/chunk-OVQKKXLZ.js +3978 -0
- package/dist/chunk-Q3XQHLIH.js +7 -0
- package/dist/chunk-RHIDOUFL.js +698 -0
- package/dist/contributions-hX3EUyjG.d.ts +1528 -0
- package/dist/contributions-o1GRKPI_.d.cts +1528 -0
- package/dist/history-Dzxz-MQj.d.ts +298 -0
- package/dist/history-LJkz4-sS.d.cts +298 -0
- package/dist/icons-BZYbJXSV.d.cts +10 -0
- package/dist/icons-BZYbJXSV.d.ts +10 -0
- package/dist/index.cjs +25294 -0
- package/dist/index.d.cts +1189 -0
- package/dist/index.d.ts +1189 -0
- package/dist/index.js +5 -0
- package/dist/keymap-CGOz5F5f.d.cts +64 -0
- package/dist/keymap-CGOz5F5f.d.ts +64 -0
- package/dist/options-Q-576hIi.d.cts +1545 -0
- package/dist/options-Q-576hIi.d.ts +1545 -0
- package/dist/plugin-D94muTV-.d.cts +405 -0
- package/dist/plugin-aGUD1epn.d.ts +405 -0
- package/dist/plugin.cjs +5967 -0
- package/dist/plugin.d.cts +7 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +3 -0
- package/dist/providers/binance.cjs +390 -0
- package/dist/providers/binance.d.cts +62 -0
- package/dist/providers/binance.d.ts +62 -0
- package/dist/providers/binance.js +388 -0
- package/dist/providers/coinbase.cjs +462 -0
- package/dist/providers/coinbase.d.cts +59 -0
- package/dist/providers/coinbase.d.ts +59 -0
- package/dist/providers/coinbase.js +460 -0
- package/dist/providers/hyperliquid.cjs +361 -0
- package/dist/providers/hyperliquid.d.cts +54 -0
- package/dist/providers/hyperliquid.d.ts +54 -0
- package/dist/providers/hyperliquid.js +359 -0
- package/dist/side-panel-CT9ZwIGz.d.cts +63 -0
- package/dist/side-panel-CT9ZwIGz.d.ts +63 -0
- package/dist/ui.cjs +1030 -0
- package/dist/ui.d.cts +188 -0
- package/dist/ui.d.ts +188 -0
- package/dist/ui.js +3 -0
- package/dist/vela.global.js +26509 -0
- package/dist/vela.global.min.js +230 -0
- package/dist/widget.cjs +31178 -0
- package/dist/widget.d.cts +799 -0
- package/dist/widget.d.ts +799 -0
- package/dist/widget.js +1060 -0
- package/dist/workspace.cjs +31692 -0
- package/dist/workspace.d.cts +579 -0
- package/dist/workspace.d.ts +579 -0
- package/dist/workspace.js +1694 -0
- package/package.json +95 -0
package/dist/widget.js
ADDED
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
import { Glider, WidgetHistory, localStorageAdapter, decodeState, prefixedSymbol, SymbolPicker, IndicatorPicker, TimeframeQuick, Topbar, PanelDock, ObjectTree, DataWindow, ChartContextMenu, Toast, Watermark, Statusline, Bottombar, ZOOM_IN, ZOOM_OUT, PAN_FAST, ShortcutsHelp, indicatorLedger, sanitizeState, legacyWidgetState, parsePersisted, statuslineInkOf, toolShortcutHints, resolveIndicators, encodeState } from './chunk-OVQKKXLZ.js';
|
|
2
|
+
export { Bottombar, ChartContextMenu, DataWindow, IndicatorPicker, ObjectTree, PanelDock, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeQuick, Topbar, Watermark, dataWindowSections, decimalsFor, decodeState, encodeState, filterSymbols, fmtChange, fmtPrice, legacyWidgetState, loadPersisted, localStorageAdapter, parsePersisted, parseTimeframe, priceStyleLabel, resolveIndicators, sanitizeState, savePersisted, timeframeLabel, timeframeMs } from './chunk-OVQKKXLZ.js';
|
|
3
|
+
import { registerBuiltinChartTypes, resolveTheme, parseSymbol, Vela, normalizeTimezone } from './chunk-KCCZNKH7.js';
|
|
4
|
+
export { TIMEZONES, normalizeTimezone, tzButtonLabel, tzMenuLabel, tzOffset } from './chunk-KCCZNKH7.js';
|
|
5
|
+
import { widgetAttachments, legendActionsProviderFor, resolveEngines } from './chunk-7UFX5ZIG.js';
|
|
6
|
+
export { DEFAULT_PANEL_MAX_WIDTH, DEFAULT_PANEL_MIN_WIDTH, DEFAULT_PANEL_ORDER, DEFAULT_PANEL_WIDTH, SidePanel, clampPanelWidth, registerSidePanel, registerWidgetAction, registerWidgetAttachment, sidePanels, unregisterSidePanel, unregisterWidgetAction, unregisterWidgetAttachment, widgetActions, widgetAttachments } from './chunk-7UFX5ZIG.js';
|
|
7
|
+
import { ensureUIHost, KeymapManager, Menu, applyPlotOverlayTokens, isEditableTarget } from './chunk-RHIDOUFL.js';
|
|
8
|
+
import './chunk-KM6LHB3Y.js';
|
|
9
|
+
import { injectStyles } from './chunk-GYD2THPV.js';
|
|
10
|
+
|
|
11
|
+
// src/widget/url-state.ts
|
|
12
|
+
var PARAMS = [
|
|
13
|
+
["symbol", "symbol"],
|
|
14
|
+
["timeframe", "interval"],
|
|
15
|
+
["priceStyle", "style"],
|
|
16
|
+
["timezone", "tz"],
|
|
17
|
+
["bars", "bars"]
|
|
18
|
+
];
|
|
19
|
+
function readUrlState(search) {
|
|
20
|
+
const out = {};
|
|
21
|
+
try {
|
|
22
|
+
const q = new URLSearchParams(search);
|
|
23
|
+
for (const [key, param] of PARAMS) {
|
|
24
|
+
const v = q.get(param);
|
|
25
|
+
if (v) out[key] = v;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function writeUrlState(state) {
|
|
32
|
+
if (typeof history === "undefined" || typeof location === "undefined") return;
|
|
33
|
+
try {
|
|
34
|
+
const q = new URLSearchParams(location.search);
|
|
35
|
+
for (const [key, param] of PARAMS) {
|
|
36
|
+
const v = state[key];
|
|
37
|
+
if (v) q.set(param, v);
|
|
38
|
+
else q.delete(param);
|
|
39
|
+
}
|
|
40
|
+
const qs = q.toString();
|
|
41
|
+
history.replaceState(null, "", `${location.pathname}${qs ? `?${qs}` : ""}${location.hash}`);
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/widget/VelaWidget.ts
|
|
47
|
+
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
48
|
+
var WIDGET_STYLE_ID = "vela-widget";
|
|
49
|
+
var WIDGET_CSS = `
|
|
50
|
+
.vela-widget { display: flex; flex-direction: column; width: 100%; height: 100%; background: var(--vela-bg); }
|
|
51
|
+
.vela-widget-main { display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
52
|
+
.vela-widget-chart { position: relative; flex: 1 1 auto; min-width: 0; }
|
|
53
|
+
`;
|
|
54
|
+
var VelaWidget = class {
|
|
55
|
+
constructor(container, opts) {
|
|
56
|
+
this.shortcutsHelp = null;
|
|
57
|
+
this.inner = null;
|
|
58
|
+
/** The manifest library (loaded once). */
|
|
59
|
+
this.manifest = [];
|
|
60
|
+
/** Live instances — the SAME entry may be added several times. */
|
|
61
|
+
this.instances = [];
|
|
62
|
+
/** Native-indicator catalog of the CURRENT chart (refreshed per rebuild/change). */
|
|
63
|
+
this.nativeCatalog = [];
|
|
64
|
+
/** Present native types, mirrored SYNC from the chart (see syncPresentNatives). */
|
|
65
|
+
this.presentNatives = [];
|
|
66
|
+
this.openDialogs = 0;
|
|
67
|
+
this.onRootKeydown = (ev) => this.routeTyping(ev);
|
|
68
|
+
/** Indicator titles (the in-chart legend rows) shown — reapplied across rebuilds. */
|
|
69
|
+
this.indicatorTitlesOn = true;
|
|
70
|
+
this.pendingRange = null;
|
|
71
|
+
/** Symbol already reported as unservable (the core re-reports on every index settle). */
|
|
72
|
+
this.unresolvedToasted = null;
|
|
73
|
+
/** Extra fetch depth the ACTIVE range chip needs — a view concern, kept apart from the
|
|
74
|
+
* user's own `bars` setting so a preset never overwrites their preference. */
|
|
75
|
+
this.rangeBars = 0;
|
|
76
|
+
this.toast = null;
|
|
77
|
+
this.alerts = [];
|
|
78
|
+
this.alertsMenu = null;
|
|
79
|
+
this.glider = new Glider(() => this.inner);
|
|
80
|
+
/** Unified app+drawings undo timeline (Ctrl+Z / Ctrl+Y). Late-resolves the CURRENT
|
|
81
|
+
* inner chart so steps recorded before a rebuild never act on a destroyed instance. */
|
|
82
|
+
this.history = new WidgetHistory(() => this.inner);
|
|
83
|
+
this.lastCrossPrice = null;
|
|
84
|
+
this.lastCrossTime = null;
|
|
85
|
+
/** Renderer cosmetic template carried across rebuilds (and persisted when enabled). */
|
|
86
|
+
this.savedConfig = null;
|
|
87
|
+
/** User-drawings document carried across rebuilds (and persisted when enabled). */
|
|
88
|
+
this.savedDrawings = null;
|
|
89
|
+
/** A restored indicator ledger waiting for the manifest to resolve. */
|
|
90
|
+
this.pendingIndicators = null;
|
|
91
|
+
/** Volume presence decided by a RESTORED ledger (null = follow the option). */
|
|
92
|
+
this.ledgerVolume = null;
|
|
93
|
+
/** The manifest can no longer change the instance set: it resolved, or no `indicators`
|
|
94
|
+
* option exists so nothing will ever resolve. Gates the ledger's pending fallback —
|
|
95
|
+
* once settled, a live empty set means "the user removed everything" and persists so. */
|
|
96
|
+
this.manifestSettled = false;
|
|
97
|
+
/** The volume auto-add rides the first candles (`load:end`); until then the registry
|
|
98
|
+
* can't show it and the persisted ledger reports the INTENT instead. */
|
|
99
|
+
this.volumeMayBePending = true;
|
|
100
|
+
/** True when the boot state came from the LEGACY three-key layout — the first
|
|
101
|
+
* unified save then drops the old `:config`/`:drawings` sub-keys. */
|
|
102
|
+
this.legacyKeys = false;
|
|
103
|
+
this.stateTimer = null;
|
|
104
|
+
this.stateListeners = /* @__PURE__ */ new Set();
|
|
105
|
+
/** Favorite drawing-tool types — mirrored from chart events, reapplied on rebuilds. */
|
|
106
|
+
this.favs = [];
|
|
107
|
+
/** Mounted attachment disposers (by id), torn down at destroy. */
|
|
108
|
+
this.attachmentDisposers = /* @__PURE__ */ new Map();
|
|
109
|
+
this.onUnload = () => this.persistNow();
|
|
110
|
+
this.indicatorsPromise = null;
|
|
111
|
+
this.destroyed = false;
|
|
112
|
+
this.buildSeq = 0;
|
|
113
|
+
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
114
|
+
if (!hostEl) throw new Error(`VelaWidget: container not found: ${String(container)}`);
|
|
115
|
+
registerBuiltinChartTypes();
|
|
116
|
+
this.opts = opts;
|
|
117
|
+
this.storageKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-widget" : opts.persist;
|
|
118
|
+
this.storage = opts.storage ?? localStorageAdapter();
|
|
119
|
+
let boot = null;
|
|
120
|
+
if (this.storageKey !== null) {
|
|
121
|
+
const raw = this.storage.get(this.storageKey);
|
|
122
|
+
if (raw instanceof Promise) void this.bootAsync(raw);
|
|
123
|
+
else {
|
|
124
|
+
boot = raw != null ? decodeState(raw) : null;
|
|
125
|
+
if (!boot) boot = this.bootLegacySync(raw);
|
|
126
|
+
}
|
|
127
|
+
window.addEventListener("beforeunload", this.onUnload);
|
|
128
|
+
}
|
|
129
|
+
const bootCell = boot ? boot.charts.find((c) => c.id === "c1") ?? boot.charts[0] : void 0;
|
|
130
|
+
const fromUrl = opts.urlState ? readUrlState(typeof location !== "undefined" ? location.search : "") : {};
|
|
131
|
+
this.symbol = fromUrl.symbol ?? prefixedSymbol(bootCell) ?? opts.symbol ?? "";
|
|
132
|
+
this.timeframe = fromUrl.timeframe ?? bootCell?.timeframe ?? opts.timeframe ?? "60";
|
|
133
|
+
this.priceStyle = fromUrl.priceStyle ?? bootCell?.priceStyle ?? opts.priceStyle ?? "candles";
|
|
134
|
+
this.timezone = fromUrl.timezone ?? boot?.timezone ?? opts.timezone ?? "Etc/UTC";
|
|
135
|
+
this.bars = Number(fromUrl.bars ?? bootCell?.bars ?? opts.bars ?? 1e3);
|
|
136
|
+
this.watermarkOn = bootCell?.watermark !== void 0 ? bootCell.watermark : opts.watermark !== false;
|
|
137
|
+
this.indicatorTitlesOn = bootCell?.indicatorTitles ?? true;
|
|
138
|
+
this.favs = boot?.favorites ? [...boot.favorites] : [];
|
|
139
|
+
this.savedConfig = bootCell?.rendererConfig ?? null;
|
|
140
|
+
this.savedDrawings = bootCell?.drawings ?? null;
|
|
141
|
+
this.pendingIndicators = bootCell?.indicators ?? null;
|
|
142
|
+
this.ledgerVolume = bootCell?.indicators ? bootCell.indicators.natives.includes("volume") : null;
|
|
143
|
+
this.manifestSettled = opts.indicators === void 0;
|
|
144
|
+
const doc = hostEl.ownerDocument;
|
|
145
|
+
injectStyles(WIDGET_STYLE_ID, WIDGET_CSS, doc);
|
|
146
|
+
this.root = doc.createElement("div");
|
|
147
|
+
this.root.className = "vela-widget";
|
|
148
|
+
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
149
|
+
this.symbolPicker = new SymbolPicker({
|
|
150
|
+
host: this.root,
|
|
151
|
+
onSelect: (symbol) => this.setSymbol(symbol),
|
|
152
|
+
onOpenChange: (open) => {
|
|
153
|
+
if (open) this.inner?.renderer.closeDialogs();
|
|
154
|
+
this.trackDialog(open);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
this.indicatorPicker = new IndicatorPicker({
|
|
158
|
+
host: this.root,
|
|
159
|
+
library: () => [
|
|
160
|
+
...this.nativeCatalog.filter((n) => n.supported).map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
|
|
161
|
+
...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
|
|
162
|
+
],
|
|
163
|
+
onChart: () => [
|
|
164
|
+
...this.nativeCatalog.filter((n) => n.present).map((n) => ({ name: n.title, native: true, nativeType: n.type })),
|
|
165
|
+
...this.instances.map((it) => ({ name: it.entry.name, language: it.entry.language }))
|
|
166
|
+
],
|
|
167
|
+
onAdd: (i) => {
|
|
168
|
+
const natives = this.nativeCatalog.filter((n) => n.supported);
|
|
169
|
+
if (i < natives.length) this.addNative(natives[i].type);
|
|
170
|
+
else this.addInstance(i - natives.length);
|
|
171
|
+
},
|
|
172
|
+
onRemove: (i) => {
|
|
173
|
+
const present = this.nativeCatalog.filter((n) => n.present);
|
|
174
|
+
if (i < present.length) this.removeNative(present[i].type);
|
|
175
|
+
else this.removeInstance(i - present.length);
|
|
176
|
+
},
|
|
177
|
+
onOpenChange: (open) => this.trackDialog(open)
|
|
178
|
+
});
|
|
179
|
+
this.tfQuick = new TimeframeQuick({
|
|
180
|
+
host: this.root,
|
|
181
|
+
onApply: (tf) => this.setTimeframe(tf),
|
|
182
|
+
onOpenChange: (open) => this.trackDialog(open)
|
|
183
|
+
});
|
|
184
|
+
this.topbar = new Topbar(this.root, {
|
|
185
|
+
symbol: this.symbol,
|
|
186
|
+
onSymbolClick: () => this.symbolPicker.open(),
|
|
187
|
+
onIndicatorsClick: () => this.indicatorPicker.open(),
|
|
188
|
+
onUndoClick: () => this.history.undo(),
|
|
189
|
+
onRedoClick: () => this.history.redo(),
|
|
190
|
+
onScreenshotClick: () => this.downloadScreenshot(),
|
|
191
|
+
onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
|
|
192
|
+
timeframe: this.timeframe,
|
|
193
|
+
timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
194
|
+
priceStyle: this.priceStyle,
|
|
195
|
+
onTimeframe: (tf) => this.setTimeframe(tf),
|
|
196
|
+
onPriceStyle: (style) => this.setPriceStyle(style),
|
|
197
|
+
getContext: () => this.context()
|
|
198
|
+
});
|
|
199
|
+
this.history.onChange(() => this.topbar.setHistoryState(this.history.canUndo, this.history.canRedo));
|
|
200
|
+
const main = doc.createElement("div");
|
|
201
|
+
main.className = "vela-widget-main";
|
|
202
|
+
this.chartHost = doc.createElement("div");
|
|
203
|
+
this.chartHost.className = "vela-widget-chart";
|
|
204
|
+
main.appendChild(this.chartHost);
|
|
205
|
+
this.dock = new PanelDock(main, {
|
|
206
|
+
chrome: this.topbar,
|
|
207
|
+
context: () => this.context(),
|
|
208
|
+
changed: () => this.markStateDirty()
|
|
209
|
+
});
|
|
210
|
+
this.objectTree = new ObjectTree(main);
|
|
211
|
+
this.dataWindow = new DataWindow(main);
|
|
212
|
+
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
213
|
+
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
214
|
+
this.dock.refresh();
|
|
215
|
+
this.root.appendChild(main);
|
|
216
|
+
this.contextMenu = new ChartContextMenu(this.chartHost, {
|
|
217
|
+
resetView: () => {
|
|
218
|
+
this.inner?.renderer.set("autoScale", true);
|
|
219
|
+
this.inner?.setVisibleRangePreset("ALL");
|
|
220
|
+
},
|
|
221
|
+
timezone: () => this.timezone,
|
|
222
|
+
setTimezone: (zone) => this.setTimezone(zone),
|
|
223
|
+
getContext: () => this.context()
|
|
224
|
+
});
|
|
225
|
+
this.toast = new Toast(this.chartHost);
|
|
226
|
+
this.watermark = opts.watermark !== false ? new Watermark(this.chartHost, this.symbol, this.timeframe) : null;
|
|
227
|
+
this.watermark?.setVisible(this.watermarkOn);
|
|
228
|
+
this.statusline = opts.statusline !== false ? new Statusline(this.chartHost, this.symbol) : null;
|
|
229
|
+
this.statusline?.setMeta(this.timeframe, parseSymbol(this.symbol).provider ?? "");
|
|
230
|
+
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
231
|
+
timezone: this.timezone,
|
|
232
|
+
onRange: (preset) => this.applyRange(preset),
|
|
233
|
+
onTimezone: (zone) => this.setTimezone(zone),
|
|
234
|
+
onSettingsClick: () => this.inner?.renderer.openSettings()
|
|
235
|
+
}) : null;
|
|
236
|
+
hostEl.appendChild(this.root);
|
|
237
|
+
this.keymap = new KeymapManager();
|
|
238
|
+
this.keymap.attach(this.root);
|
|
239
|
+
this.keymap.register({
|
|
240
|
+
id: "chart.screenshot",
|
|
241
|
+
keys: "mod+alt+s",
|
|
242
|
+
label: "Download a chart screenshot",
|
|
243
|
+
category: "Chart",
|
|
244
|
+
run: () => this.downloadScreenshot()
|
|
245
|
+
});
|
|
246
|
+
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.inner?.setVisibleRangePreset("ALL") });
|
|
247
|
+
this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.inner?.renderer.set("logScale", !this.inner.renderer.get("logScale")) });
|
|
248
|
+
this.keymap.register({
|
|
249
|
+
id: "chart.toggle-percent",
|
|
250
|
+
keys: "alt+p",
|
|
251
|
+
label: "Toggle percent scale",
|
|
252
|
+
category: "Chart",
|
|
253
|
+
run: () => {
|
|
254
|
+
const mode = this.inner?.renderer.get("scaleMode");
|
|
255
|
+
this.inner?.renderer.set("scaleMode", mode === "percent" ? "price" : "percent");
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
this.keymap.register({ id: "drawings.trendline", keys: "alt+t", label: "Arm the trend line tool", category: "Drawings", run: () => this.inner?.drawings.setTool("trendline") });
|
|
259
|
+
this.keymap.register({
|
|
260
|
+
id: "drawings.hline-cursor",
|
|
261
|
+
keys: "alt+h",
|
|
262
|
+
label: "Horizontal line at the cursor price",
|
|
263
|
+
category: "Drawings",
|
|
264
|
+
run: () => {
|
|
265
|
+
if (this.lastCrossPrice != null && this.lastCrossTime != null)
|
|
266
|
+
this.inner?.drawings.add("hline", { anchors: [{ time: this.lastCrossTime, price: this.lastCrossPrice }] });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
this.keymap.register({
|
|
270
|
+
id: "drawings.vline-cursor",
|
|
271
|
+
keys: "alt+v",
|
|
272
|
+
label: "Vertical line at the cursor time",
|
|
273
|
+
category: "Drawings",
|
|
274
|
+
run: () => {
|
|
275
|
+
if (this.lastCrossTime != null && this.lastCrossPrice != null)
|
|
276
|
+
this.inner?.drawings.add("vline", { anchors: [{ time: this.lastCrossTime, price: this.lastCrossPrice }] });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
this.keymap.register({ id: "history.undo", keys: ["mod+z"], label: "Undo", category: "Edit", run: () => this.history.undo() });
|
|
280
|
+
this.keymap.register({ id: "history.redo", keys: ["mod+y", "mod+shift+z"], label: "Redo", category: "Edit", run: () => this.history.redo() });
|
|
281
|
+
this.keymap.register({ id: "view.zoom-in", keys: "mod+arrowup", label: "Zoom in", category: "Chart", run: () => this.glider.zoom(ZOOM_IN) });
|
|
282
|
+
this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
|
|
283
|
+
this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.inner?.panBy(-PAN_FAST) });
|
|
284
|
+
this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.inner?.panBy(PAN_FAST) });
|
|
285
|
+
this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker.open() });
|
|
286
|
+
this.keymap.register({
|
|
287
|
+
id: "help.shortcuts",
|
|
288
|
+
keys: "?",
|
|
289
|
+
label: "Show this shortcuts panel",
|
|
290
|
+
category: "Help",
|
|
291
|
+
run: () => {
|
|
292
|
+
this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open)));
|
|
293
|
+
this.shortcutsHelp.open();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
this.root.addEventListener("keydown", this.onRootKeydown);
|
|
297
|
+
this.root.tabIndex = -1;
|
|
298
|
+
this.rebuild();
|
|
299
|
+
this.mountAttachments();
|
|
300
|
+
if (opts.autofocus) this.inner?.renderer.focus();
|
|
301
|
+
}
|
|
302
|
+
/** Mount registered attachments not yet mounted on this widget (idempotent per id). */
|
|
303
|
+
mountAttachments() {
|
|
304
|
+
for (const att of widgetAttachments()) {
|
|
305
|
+
if (this.attachmentDisposers.has(att.id)) continue;
|
|
306
|
+
try {
|
|
307
|
+
this.attachmentDisposers.set(att.id, att.mount(this.context()));
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.warn(`[vela] widget attachment "${att.id}" failed to mount:`, err);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** The context handed to contributed actions (see `registerWidgetAction`). */
|
|
314
|
+
context() {
|
|
315
|
+
const self = this;
|
|
316
|
+
return {
|
|
317
|
+
get chart() {
|
|
318
|
+
return self.chart;
|
|
319
|
+
},
|
|
320
|
+
symbol: this.symbol,
|
|
321
|
+
timeframe: this.timeframe,
|
|
322
|
+
priceStyle: this.priceStyle,
|
|
323
|
+
setSymbol: (symbol) => this.setSymbol(symbol),
|
|
324
|
+
setTimeframe: (tf) => this.setTimeframe(tf),
|
|
325
|
+
setPriceStyle: (style) => this.setPriceStyle(style),
|
|
326
|
+
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
327
|
+
togglePanel: (id, open) => this.dock.toggle(id, open),
|
|
328
|
+
host: this.root,
|
|
329
|
+
toast: (message, kind) => this.toast?.show(message, kind)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/** The recent engine alerts (topbar bell). */
|
|
333
|
+
openAlertsMenu(anchor) {
|
|
334
|
+
this.alertsMenu?.destroy();
|
|
335
|
+
const items = this.alerts.length ? this.alerts.map((a, i) => ({
|
|
336
|
+
id: String(i),
|
|
337
|
+
label: `${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 70)
|
|
338
|
+
})) : [{ id: "none", label: "No alerts yet", disabled: true }];
|
|
339
|
+
this.alertsMenu = new Menu({ host: this.root, items, onSelect: () => {
|
|
340
|
+
} });
|
|
341
|
+
const r = anchor.getBoundingClientRect();
|
|
342
|
+
this.alertsMenu.openAt(r.left, r.bottom + 4);
|
|
343
|
+
}
|
|
344
|
+
/** Re-project contributed topbar actions and side panels (after late registrations). */
|
|
345
|
+
refreshActions() {
|
|
346
|
+
this.mountAttachments();
|
|
347
|
+
this.topbar.renderActions();
|
|
348
|
+
this.dock.refresh();
|
|
349
|
+
if (this.inner) this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => this.context()));
|
|
350
|
+
}
|
|
351
|
+
/** The inner headless chart of the CURRENT build — becomes a new instance after a
|
|
352
|
+
* symbol/timeframe change; don't cache it across awaits. */
|
|
353
|
+
get chart() {
|
|
354
|
+
if (!this.inner) throw new Error("VelaWidget is destroyed");
|
|
355
|
+
return this.inner;
|
|
356
|
+
}
|
|
357
|
+
/** Chrome that must follow the timeframe wherever it changes from (menu, quick entry,
|
|
358
|
+
* range chip, API). Keeping it in one place is what stopped the status line from
|
|
359
|
+
* drifting out of sync with the topbar. */
|
|
360
|
+
syncTimeframeChrome(tf) {
|
|
361
|
+
this.timeframe = tf;
|
|
362
|
+
this.topbar.setTimeframe(tf);
|
|
363
|
+
this.watermark?.update(this.symbol, tf);
|
|
364
|
+
this.statusline?.setMeta(tf, this.providerLabel());
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The provider name to show: the one that RESOLVED the symbol, not the one configured.
|
|
368
|
+
* They differ whenever the `provider` option is just a default (or names something that
|
|
369
|
+
* isn't registered) — the status line must not claim a venue that served nothing.
|
|
370
|
+
*/
|
|
371
|
+
providerLabel() {
|
|
372
|
+
const resolved = this.inner?.data.resolve(this.symbol)?.provider;
|
|
373
|
+
return resolved ?? parseSymbol(this.symbol).provider ?? "";
|
|
374
|
+
}
|
|
375
|
+
setTimeframe(tf) {
|
|
376
|
+
if (tf === this.timeframe || this.destroyed) return;
|
|
377
|
+
this.bottombar?.setActiveRange(null);
|
|
378
|
+
this.rangeBars = 0;
|
|
379
|
+
this.syncTimeframeChrome(tf);
|
|
380
|
+
this.markStateDirty();
|
|
381
|
+
void this.inner?.setMarket({ timeframe: tf, bars: this.bars });
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Switch the chart symbol in place. An `EXCHANGE:` prefix pins the venue (the
|
|
385
|
+
* symbol picker composes one from the row the user pointed at) — a bare ticker
|
|
386
|
+
* resolves against the registered providers in declaration order.
|
|
387
|
+
*/
|
|
388
|
+
setSymbol(symbol) {
|
|
389
|
+
if (this.destroyed) return;
|
|
390
|
+
if (symbol === this.symbol) return;
|
|
391
|
+
this.unresolvedToasted = null;
|
|
392
|
+
this.symbol = symbol;
|
|
393
|
+
this.topbar.setSymbol(symbol);
|
|
394
|
+
this.statusline?.setSymbol(symbol);
|
|
395
|
+
this.objectTree.setSymbol(symbol);
|
|
396
|
+
this.watermark?.update(symbol, this.timeframe);
|
|
397
|
+
this.markStateDirty();
|
|
398
|
+
void this.inner?.setMarket({ symbol });
|
|
399
|
+
this.statusline?.setMeta(this.timeframe, this.providerLabel());
|
|
400
|
+
}
|
|
401
|
+
// ── state surface (same triplet as the workspace: getState / applyState / state:changed) ──
|
|
402
|
+
/**
|
|
403
|
+
* Snapshot the COMPLETE widget state as the unified shell document — the SAME
|
|
404
|
+
* format `VelaWorkspace.getState()` returns, with a single `c1` cell: market,
|
|
405
|
+
* display prefs, the renderer config document, the user-drawings document, and
|
|
406
|
+
* the indicator ledger. This is what `persist` writes; hosts build custom flows
|
|
407
|
+
* on it (server snapshots, share links, templates) — and a saved widget state
|
|
408
|
+
* drops into a workspace slot as-is.
|
|
409
|
+
*/
|
|
410
|
+
getState() {
|
|
411
|
+
const cell = {};
|
|
412
|
+
const live = this.inner?.market;
|
|
413
|
+
const symbol = live?.symbol ?? this.symbol;
|
|
414
|
+
if (symbol) cell.symbol = symbol;
|
|
415
|
+
const provider = parseSymbol(symbol ?? "").provider ?? void 0;
|
|
416
|
+
if (provider) cell.provider = provider;
|
|
417
|
+
cell.timeframe = live?.timeframe ?? this.timeframe;
|
|
418
|
+
cell.priceStyle = this.priceStyle;
|
|
419
|
+
if (this.bars > 0) cell.bars = this.bars;
|
|
420
|
+
cell.watermark = this.watermarkOn;
|
|
421
|
+
cell.indicatorTitles = this.indicatorTitlesOn;
|
|
422
|
+
if (this.inner) {
|
|
423
|
+
cell.rendererConfig = this.inner.renderer.getConfig();
|
|
424
|
+
cell.drawings = this.inner.drawings.toJSON();
|
|
425
|
+
} else {
|
|
426
|
+
if (this.savedConfig != null) cell.rendererConfig = this.savedConfig;
|
|
427
|
+
if (this.savedDrawings != null) cell.drawings = this.savedDrawings;
|
|
428
|
+
}
|
|
429
|
+
cell.indicators = indicatorLedger({
|
|
430
|
+
present: this.inner ? this.inner.presentNativeIndicators() : this.pendingIndicators?.natives ?? [],
|
|
431
|
+
instanceNames: this.instances.map((it) => it.entry.name),
|
|
432
|
+
pendingManifest: this.pendingIndicators?.manifest ?? null,
|
|
433
|
+
manifestSettled: this.manifestSettled,
|
|
434
|
+
volumePending: this.volumeMayBePending && (this.ledgerVolume ?? this.opts.volume !== false)
|
|
435
|
+
});
|
|
436
|
+
const state = { version: 1, layout: "1", activeCellId: "c1", timezone: this.timezone, charts: [{ id: "c1", ...cell }] };
|
|
437
|
+
if (this.favs.length > 0) state.favorites = [...this.favs];
|
|
438
|
+
const panels = this.dock.getState();
|
|
439
|
+
if (panels) state.panels = panels;
|
|
440
|
+
return state;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Restore a state document produced by {@link getState} — or by a WORKSPACE (the
|
|
444
|
+
* first cell of a multi-cell document applies). Untrusted-safe: malformed fields
|
|
445
|
+
* are dropped. Applied IN PLACE: the chart instance survives (market switches via
|
|
446
|
+
* `setMarket`), config/drawings/indicators are replaced. With `urlState` enabled,
|
|
447
|
+
* URL params still win over the document's market/prefs fields.
|
|
448
|
+
*/
|
|
449
|
+
applyState(state) {
|
|
450
|
+
if (this.destroyed) return;
|
|
451
|
+
const st = sanitizeState(state);
|
|
452
|
+
if (!st) return;
|
|
453
|
+
const cell = st.charts.find((c) => c.id === "c1") ?? st.charts[0];
|
|
454
|
+
const fromUrl = this.opts.urlState ? readUrlState(typeof location !== "undefined" ? location.search : "") : {};
|
|
455
|
+
const tz = fromUrl.timezone ?? st.timezone;
|
|
456
|
+
if (tz && tz !== this.timezone) this.setTimezone(tz);
|
|
457
|
+
if (st.favorites) {
|
|
458
|
+
this.favs = [...st.favorites];
|
|
459
|
+
this.inner?.drawings.setFavorites(this.favs);
|
|
460
|
+
}
|
|
461
|
+
this.dock.applyState(st.panels);
|
|
462
|
+
if (cell) {
|
|
463
|
+
const style = fromUrl.priceStyle ?? cell.priceStyle;
|
|
464
|
+
if (style && style !== this.priceStyle) this.setPriceStyle(style);
|
|
465
|
+
if (cell.watermark !== void 0 && cell.watermark !== this.watermarkOn) this.setWatermarkVisible(cell.watermark);
|
|
466
|
+
if (cell.indicatorTitles !== void 0 && cell.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cell.indicatorTitles);
|
|
467
|
+
if (cell.rendererConfig != null) {
|
|
468
|
+
this.savedConfig = cell.rendererConfig;
|
|
469
|
+
this.inner?.renderer.applyConfig(cell.rendererConfig);
|
|
470
|
+
}
|
|
471
|
+
if (cell.drawings != null) {
|
|
472
|
+
this.savedDrawings = cell.drawings;
|
|
473
|
+
this.inner?.drawings.fromJSON(cell.drawings);
|
|
474
|
+
}
|
|
475
|
+
if (cell.indicators) this.applyIndicatorLedger(cell.indicators);
|
|
476
|
+
const symbol = fromUrl.symbol ?? prefixedSymbol(cell);
|
|
477
|
+
const timeframe = fromUrl.timeframe ?? cell.timeframe;
|
|
478
|
+
const bars = Number(fromUrl.bars ?? cell.bars ?? 0);
|
|
479
|
+
const next = {};
|
|
480
|
+
if (symbol && symbol !== this.symbol) next.symbol = symbol;
|
|
481
|
+
if (timeframe && timeframe !== this.timeframe) next.timeframe = timeframe;
|
|
482
|
+
if (bars > 0 && bars !== this.bars) {
|
|
483
|
+
this.bars = bars;
|
|
484
|
+
next.bars = Math.max(bars, this.rangeBars);
|
|
485
|
+
}
|
|
486
|
+
if (Object.keys(next).length > 0) void this.inner?.setMarket(next);
|
|
487
|
+
}
|
|
488
|
+
this.markStateDirty();
|
|
489
|
+
}
|
|
490
|
+
/** Subscribe to `state:changed` — the persistable state changed (debounced ~500ms);
|
|
491
|
+
* re-pull {@link getState}. Returns an unsubscribe function. */
|
|
492
|
+
on(event, handler) {
|
|
493
|
+
if (event !== "state:changed") return () => void 0;
|
|
494
|
+
this.stateListeners.add(handler);
|
|
495
|
+
return () => this.stateListeners.delete(handler);
|
|
496
|
+
}
|
|
497
|
+
/** Replace the indicator ledger: natives converge to the listed set (volume
|
|
498
|
+
* included — removing it sticks, the core's auto-add respects the opt-out),
|
|
499
|
+
* manifest instances are re-created by name — held until the manifest resolves. */
|
|
500
|
+
applyIndicatorLedger(led) {
|
|
501
|
+
const chart = this.inner;
|
|
502
|
+
if (!chart) return;
|
|
503
|
+
this.ledgerVolume = led.natives.includes("volume");
|
|
504
|
+
this.history.silently(() => {
|
|
505
|
+
for (const type of led.natives) {
|
|
506
|
+
if (!this.nativeCatalog.some((n) => n.type === type && n.present)) chart.addNativeIndicator(type);
|
|
507
|
+
}
|
|
508
|
+
for (const n of this.nativeCatalog) {
|
|
509
|
+
if (n.present && !led.natives.includes(n.type)) chart.addNativeIndicator(n.type).remove();
|
|
510
|
+
}
|
|
511
|
+
for (const it of [...this.instances]) this.dropInstance(it);
|
|
512
|
+
});
|
|
513
|
+
if (this.manifest.length > 0) {
|
|
514
|
+
for (const name of led.manifest) {
|
|
515
|
+
const entry = this.manifest.find((e) => e.name === name);
|
|
516
|
+
if (entry) this.instances.push({ entry, handle: this.addToChart(chart, entry) });
|
|
517
|
+
}
|
|
518
|
+
this.pendingIndicators = null;
|
|
519
|
+
} else if (!this.manifestSettled) {
|
|
520
|
+
this.pendingIndicators = led;
|
|
521
|
+
} else {
|
|
522
|
+
this.pendingIndicators = null;
|
|
523
|
+
}
|
|
524
|
+
this.refreshNativeCatalog();
|
|
525
|
+
this.syncIndicatorCount();
|
|
526
|
+
}
|
|
527
|
+
/** SYNC boot fallback: a LEGACY three-key payload read at construction. */
|
|
528
|
+
bootLegacySync(rawMain) {
|
|
529
|
+
if (this.storageKey === null) return null;
|
|
530
|
+
const cfg = this.storage.get(`${this.storageKey}:config`);
|
|
531
|
+
const drw = this.storage.get(`${this.storageKey}:drawings`);
|
|
532
|
+
if (cfg instanceof Promise || drw instanceof Promise) return null;
|
|
533
|
+
const doc = sanitizeState(legacyWidgetState(parsePersisted(rawMain), cfg, drw));
|
|
534
|
+
if (doc) this.legacyKeys = true;
|
|
535
|
+
return doc;
|
|
536
|
+
}
|
|
537
|
+
/** ASYNC boot: resolve the unified document (or migrate a legacy payload), then
|
|
538
|
+
* late-apply it — the widget built with option defaults in the meantime. */
|
|
539
|
+
async bootAsync(rawMain) {
|
|
540
|
+
let doc = null;
|
|
541
|
+
try {
|
|
542
|
+
const raw = await rawMain;
|
|
543
|
+
doc = raw != null ? decodeState(raw) : null;
|
|
544
|
+
if (!doc && this.storageKey !== null) {
|
|
545
|
+
const [cfg, drw] = await Promise.all([
|
|
546
|
+
Promise.resolve(this.storage.get(`${this.storageKey}:config`)),
|
|
547
|
+
Promise.resolve(this.storage.get(`${this.storageKey}:drawings`))
|
|
548
|
+
]);
|
|
549
|
+
doc = sanitizeState(legacyWidgetState(parsePersisted(raw), cfg, drw));
|
|
550
|
+
if (doc) this.legacyKeys = true;
|
|
551
|
+
}
|
|
552
|
+
} catch {
|
|
553
|
+
doc = null;
|
|
554
|
+
}
|
|
555
|
+
if (doc && !this.destroyed) this.applyState(doc);
|
|
556
|
+
}
|
|
557
|
+
/** Show/hide the symbol watermark behind the chart (persisted). */
|
|
558
|
+
setWatermarkVisible(visible) {
|
|
559
|
+
this.watermarkOn = visible;
|
|
560
|
+
this.watermark?.setVisible(visible);
|
|
561
|
+
this.markStateDirty();
|
|
562
|
+
}
|
|
563
|
+
/** Show/hide the indicator titles — the in-chart legend rows (persisted). */
|
|
564
|
+
setIndicatorTitlesVisible(visible) {
|
|
565
|
+
this.indicatorTitlesOn = visible;
|
|
566
|
+
this.inner?.renderer.set("indicatorTitles", visible);
|
|
567
|
+
this.markStateDirty();
|
|
568
|
+
}
|
|
569
|
+
/** Applied LIVE (renderer feature) — no rebuild; persists across rebuilds. */
|
|
570
|
+
setPriceStyle(style) {
|
|
571
|
+
this.priceStyle = style;
|
|
572
|
+
this.topbar.setPriceStyle(style);
|
|
573
|
+
this.inner?.renderer.set("priceStyle", style);
|
|
574
|
+
this.syncStatuslineColors();
|
|
575
|
+
this.markStateDirty();
|
|
576
|
+
}
|
|
577
|
+
/** OHLC/change ink in the status line follows the ACTIVE price style's configured
|
|
578
|
+
* colors and direction rule (candle bodies by close-vs-open, baseline by position
|
|
579
|
+
* against the live baseline price, …) instead of the fixed theme tokens. */
|
|
580
|
+
syncStatuslineColors() {
|
|
581
|
+
if (!this.statusline || !this.inner) return;
|
|
582
|
+
this.statusline.setDirectionColors(...statuslineInkOf(this.inner.renderer, this.priceStyle));
|
|
583
|
+
}
|
|
584
|
+
/** The widget shell (topbar, panels) keeps the app theme; the chart host's tokens
|
|
585
|
+
* re-derive from the LIVE plot surface (see {@link applyPlotOverlayTokens}). */
|
|
586
|
+
syncPlotOverlayTokens() {
|
|
587
|
+
applyPlotOverlayTokens(this.chartHost, resolveTheme(this.opts.theme), this.inner?.renderer.getConfig() ?? null);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Swap the widget theme at runtime — `'dark'`, `'light'`, or a full custom theme.
|
|
591
|
+
* Applied LIVE: the inner chart re-skins through `Vela.setTheme` and its
|
|
592
|
+
* `theme:changed` event re-tokens the widget chrome (topbar, menus, panels) in
|
|
593
|
+
* place; the choice also persists into rebuilds. Same switch the user reaches from
|
|
594
|
+
* chart settings → Canvas → Theme.
|
|
595
|
+
*/
|
|
596
|
+
setTheme(theme) {
|
|
597
|
+
if (this.destroyed) return;
|
|
598
|
+
this.opts.theme = theme;
|
|
599
|
+
this.inner?.setTheme(theme);
|
|
600
|
+
}
|
|
601
|
+
/** Applied LIVE (renderer feature) — no rebuild; persists across rebuilds. */
|
|
602
|
+
setTimezone(zone) {
|
|
603
|
+
this.timezone = zone;
|
|
604
|
+
this.bottombar?.setTimezone(zone);
|
|
605
|
+
this.inner?.renderer.set("timezone", zone);
|
|
606
|
+
this.markStateDirty();
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Range chip: switch to the preset's timeframe, fetch enough history for its window,
|
|
610
|
+
* and frame it once ready. The in-place `setMarket` loads the depth, so it also runs
|
|
611
|
+
* when only the DEPTH grows (same timeframe, deeper window).
|
|
612
|
+
*/
|
|
613
|
+
applyRange(preset) {
|
|
614
|
+
if (this.destroyed) return;
|
|
615
|
+
this.pendingRange = preset;
|
|
616
|
+
const tfChanged = preset.tf !== this.timeframe;
|
|
617
|
+
const deeper = preset.bars > Math.max(this.bars, this.rangeBars);
|
|
618
|
+
this.rangeBars = preset.bars;
|
|
619
|
+
if (tfChanged) this.syncTimeframeChrome(preset.tf);
|
|
620
|
+
if (tfChanged || deeper) {
|
|
621
|
+
void this.inner?.setMarket({ timeframe: preset.tf, bars: Math.max(this.bars, this.rangeBars), visibleRange: preset.preset }).then(() => {
|
|
622
|
+
if (!this.destroyed && this.pendingRange === preset) {
|
|
623
|
+
this.inner?.setVisibleRangePreset(preset.preset);
|
|
624
|
+
this.pendingRange = null;
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
} else {
|
|
628
|
+
this.inner?.setVisibleRangePreset(preset.preset);
|
|
629
|
+
this.pendingRange = null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
destroy() {
|
|
633
|
+
this.destroyed = true;
|
|
634
|
+
this.persistNow();
|
|
635
|
+
this.inner?.destroy();
|
|
636
|
+
this.inner = null;
|
|
637
|
+
for (const dispose of this.attachmentDisposers.values()) {
|
|
638
|
+
try {
|
|
639
|
+
dispose();
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
this.attachmentDisposers.clear();
|
|
644
|
+
window.removeEventListener("beforeunload", this.onUnload);
|
|
645
|
+
this.root.removeEventListener("keydown", this.onRootKeydown);
|
|
646
|
+
this.topbar.destroy();
|
|
647
|
+
this.dock.destroy();
|
|
648
|
+
this.objectTree.destroy();
|
|
649
|
+
this.dataWindow.destroy();
|
|
650
|
+
this.shortcutsHelp?.destroy();
|
|
651
|
+
this.contextMenu.destroy();
|
|
652
|
+
this.symbolPicker.destroy();
|
|
653
|
+
this.indicatorPicker.destroy();
|
|
654
|
+
this.tfQuick.destroy();
|
|
655
|
+
this.statusline?.destroy();
|
|
656
|
+
this.watermark?.destroy();
|
|
657
|
+
this.bottombar?.destroy();
|
|
658
|
+
this.toast?.destroy();
|
|
659
|
+
this.alertsMenu?.destroy();
|
|
660
|
+
this.glider.stop();
|
|
661
|
+
this.history.destroy();
|
|
662
|
+
this.keymap.destroy();
|
|
663
|
+
this.root.remove();
|
|
664
|
+
}
|
|
665
|
+
/** Destroy + recreate the inner chart with the current symbol/timeframe, then
|
|
666
|
+
* re-register providers/engines, rebind chrome, and re-add manifest indicators. */
|
|
667
|
+
rebuild() {
|
|
668
|
+
const seq = ++this.buildSeq;
|
|
669
|
+
if (this.inner) {
|
|
670
|
+
this.savedConfig = this.inner.renderer.getConfig();
|
|
671
|
+
this.savedDrawings = this.inner.drawings.toJSON();
|
|
672
|
+
}
|
|
673
|
+
this.inner?.destroy();
|
|
674
|
+
const {
|
|
675
|
+
providers,
|
|
676
|
+
engines,
|
|
677
|
+
indicators,
|
|
678
|
+
timeframes: _timeframes,
|
|
679
|
+
priceStyle: _priceStyle,
|
|
680
|
+
timezone: _timezone,
|
|
681
|
+
statusline: _statusline,
|
|
682
|
+
watermark: _watermark,
|
|
683
|
+
bottombar: _bottombar,
|
|
684
|
+
...chartOpts
|
|
685
|
+
} = this.opts;
|
|
686
|
+
const chart = new Vela(this.chartHost, {
|
|
687
|
+
...chartOpts,
|
|
688
|
+
symbol: this.symbol,
|
|
689
|
+
timeframe: this.timeframe,
|
|
690
|
+
bars: Math.max(this.bars, this.rangeBars),
|
|
691
|
+
volume: this.ledgerVolume ?? chartOpts.volume,
|
|
692
|
+
// A pending range chip frames the FIRST paint (no preview flash, no re-frame).
|
|
693
|
+
...this.pendingRange ? { visibleRange: this.pendingRange.preset } : {}
|
|
694
|
+
});
|
|
695
|
+
for (const [name, make] of Object.entries(providers ?? {})) chart.data.registerProvider(name, make());
|
|
696
|
+
for (const [language, make] of Object.entries(resolveEngines(engines))) chart.registerEngine(language, make());
|
|
697
|
+
this.inner = chart;
|
|
698
|
+
this.symbolPicker.setSource(() => chart.data.symbols());
|
|
699
|
+
this.objectTree.setSymbol(this.symbol);
|
|
700
|
+
chart.renderer.setLegendActions(legendActionsProviderFor(chart, () => this.context()));
|
|
701
|
+
if (chart.renderer.supports("historyChords")) chart.renderer.set("historyChords", false);
|
|
702
|
+
this.dock.onChart(chart);
|
|
703
|
+
this.contextMenu.onChart(chart);
|
|
704
|
+
this.refreshNativeCatalog();
|
|
705
|
+
chart.on("indicator:added", () => {
|
|
706
|
+
this.syncPresentNatives();
|
|
707
|
+
this.refreshNativeCatalog();
|
|
708
|
+
this.markStateDirty();
|
|
709
|
+
});
|
|
710
|
+
chart.on("indicator:removed", ({ id }) => {
|
|
711
|
+
const idx = this.instances.findIndex((it) => it.handle?.id === id);
|
|
712
|
+
if (idx >= 0) {
|
|
713
|
+
const snapshot = this.instances[idx];
|
|
714
|
+
this.instances.splice(idx, 1);
|
|
715
|
+
this.syncIndicatorCount();
|
|
716
|
+
this.history.push({
|
|
717
|
+
undo: () => {
|
|
718
|
+
snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
|
|
719
|
+
this.instances.push(snapshot);
|
|
720
|
+
this.syncIndicatorCount();
|
|
721
|
+
},
|
|
722
|
+
redo: () => this.dropInstance(snapshot)
|
|
723
|
+
});
|
|
724
|
+
} else {
|
|
725
|
+
const now = this.inner?.presentNativeIndicators() ?? [];
|
|
726
|
+
for (const type of this.presentNatives.filter((t) => !now.includes(t))) {
|
|
727
|
+
this.history.push({
|
|
728
|
+
undo: () => {
|
|
729
|
+
this.inner?.addNativeIndicator(type);
|
|
730
|
+
this.refreshNativeCatalog();
|
|
731
|
+
},
|
|
732
|
+
redo: () => {
|
|
733
|
+
this.inner?.addNativeIndicator(type).remove();
|
|
734
|
+
this.refreshNativeCatalog();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
this.syncPresentNatives();
|
|
740
|
+
this.refreshNativeCatalog();
|
|
741
|
+
this.markStateDirty();
|
|
742
|
+
});
|
|
743
|
+
if (this.pendingIndicators) {
|
|
744
|
+
for (const type of this.pendingIndicators.natives) chart.addNativeIndicator(type);
|
|
745
|
+
if (this.manifestSettled) this.pendingIndicators = null;
|
|
746
|
+
}
|
|
747
|
+
this.syncPresentNatives();
|
|
748
|
+
chart.on("load:end", () => {
|
|
749
|
+
this.volumeMayBePending = false;
|
|
750
|
+
});
|
|
751
|
+
chart.on("theme:changed", (t) => {
|
|
752
|
+
this.opts.theme = t;
|
|
753
|
+
ensureUIHost(this.root, t);
|
|
754
|
+
this.syncPlotOverlayTokens();
|
|
755
|
+
});
|
|
756
|
+
chart.on("market:changed", ({ symbol, timeframe }) => {
|
|
757
|
+
this.refreshNativeCatalog();
|
|
758
|
+
if (this.statusline) this.statusline.onChart(chart);
|
|
759
|
+
if (symbol !== this.symbol) {
|
|
760
|
+
this.symbol = symbol;
|
|
761
|
+
this.topbar.setSymbol(symbol);
|
|
762
|
+
this.statusline?.setSymbol(symbol);
|
|
763
|
+
this.objectTree.setSymbol(symbol);
|
|
764
|
+
this.watermark?.update(symbol, this.timeframe);
|
|
765
|
+
}
|
|
766
|
+
if (timeframe !== this.timeframe) this.syncTimeframeChrome(timeframe);
|
|
767
|
+
});
|
|
768
|
+
this.history.onChart(chart);
|
|
769
|
+
chart.on("alert", (alert) => {
|
|
770
|
+
this.alerts.unshift({ title: alert.title ?? "Alert", message: alert.message, time: alert.time });
|
|
771
|
+
if (this.alerts.length > 20) this.alerts.pop();
|
|
772
|
+
this.toast?.show(`${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
|
|
773
|
+
this.topbar.setAlertCount(this.alerts.length);
|
|
774
|
+
});
|
|
775
|
+
chart.on("indicator:error", ({ error }) => this.toast?.show(error.message, "error", 5e3));
|
|
776
|
+
chart.on("data:unresolved", ({ symbol, providers: providers2 }) => {
|
|
777
|
+
if (this.unresolvedToasted === symbol) return;
|
|
778
|
+
this.unresolvedToasted = symbol;
|
|
779
|
+
const list = providers2.length > 0 ? providers2.join(", ") : "none";
|
|
780
|
+
this.toast?.show(`No registered provider serves "${symbol}" (registered: ${list})`, "error", 6e3);
|
|
781
|
+
});
|
|
782
|
+
if (this.favs.length > 0) chart.drawings.setFavorites(this.favs);
|
|
783
|
+
chart.on("drawing:favorites", ({ favorites }) => {
|
|
784
|
+
this.favs = favorites;
|
|
785
|
+
this.markStateDirty();
|
|
786
|
+
});
|
|
787
|
+
if (chart.drawings.supported) chart.drawings.setToolShortcuts(toolShortcutHints(this.keymap));
|
|
788
|
+
chart.on("drawing:created", () => this.markStateDirty());
|
|
789
|
+
chart.on("drawing:edited", () => this.markStateDirty());
|
|
790
|
+
chart.on("drawing:removed", () => this.markStateDirty());
|
|
791
|
+
chart.renderer.onCrosshairMove((e) => {
|
|
792
|
+
this.lastCrossPrice = e.price;
|
|
793
|
+
this.lastCrossTime = e.time;
|
|
794
|
+
});
|
|
795
|
+
this.topbar.renderActions();
|
|
796
|
+
if (this.savedConfig != null) chart.renderer.applyConfig(this.savedConfig);
|
|
797
|
+
if (this.savedDrawings != null) chart.drawings.fromJSON(this.savedDrawings);
|
|
798
|
+
if (this.priceStyle !== "candles") chart.renderer.set("priceStyle", this.priceStyle);
|
|
799
|
+
if (this.timezone !== "Etc/UTC") chart.renderer.set("timezone", this.timezone);
|
|
800
|
+
if (!this.indicatorTitlesOn) chart.renderer.set("indicatorTitles", false);
|
|
801
|
+
chart.renderer.onConfigChanged(() => {
|
|
802
|
+
const zone = chart.renderer.get("timezone");
|
|
803
|
+
if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.timezone)) {
|
|
804
|
+
this.setTimezone(normalizeTimezone(zone));
|
|
805
|
+
}
|
|
806
|
+
this.syncStatuslineColors();
|
|
807
|
+
this.syncPlotOverlayTokens();
|
|
808
|
+
});
|
|
809
|
+
this.syncPlotOverlayTokens();
|
|
810
|
+
this.statusline?.onChart(chart);
|
|
811
|
+
this.syncStatuslineColors();
|
|
812
|
+
const advanced = {
|
|
813
|
+
title: "Advanced",
|
|
814
|
+
placement: "end",
|
|
815
|
+
rows: [
|
|
816
|
+
{
|
|
817
|
+
kind: "select",
|
|
818
|
+
label: "Bars to fetch",
|
|
819
|
+
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
820
|
+
get: () => String(this.bars),
|
|
821
|
+
set: (v) => {
|
|
822
|
+
this.bars = Number(v);
|
|
823
|
+
this.markStateDirty();
|
|
824
|
+
void this.inner?.setMarket({ bars: Math.max(this.bars, this.rangeBars) });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
]
|
|
828
|
+
};
|
|
829
|
+
const watermarkSection = {
|
|
830
|
+
title: "Watermark",
|
|
831
|
+
placement: "symbol",
|
|
832
|
+
rows: [
|
|
833
|
+
{
|
|
834
|
+
kind: "toggle",
|
|
835
|
+
label: "Symbol watermark",
|
|
836
|
+
get: () => this.watermarkOn,
|
|
837
|
+
set: (v) => this.setWatermarkVisible(v)
|
|
838
|
+
}
|
|
839
|
+
]
|
|
840
|
+
};
|
|
841
|
+
if (this.statusline) {
|
|
842
|
+
const sl = this.statusline;
|
|
843
|
+
chart.renderer.setSettingsSections([
|
|
844
|
+
{
|
|
845
|
+
title: "Status line",
|
|
846
|
+
rows: [
|
|
847
|
+
{ kind: "heading", label: "Status line" },
|
|
848
|
+
{ kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
|
|
849
|
+
{ kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
|
|
850
|
+
{ kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
|
|
851
|
+
{ kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
|
|
852
|
+
{ kind: "heading", label: "Indicators" },
|
|
853
|
+
{
|
|
854
|
+
kind: "toggle",
|
|
855
|
+
label: "Titles",
|
|
856
|
+
get: () => this.indicatorTitlesOn,
|
|
857
|
+
set: (v) => this.setIndicatorTitlesVisible(v)
|
|
858
|
+
}
|
|
859
|
+
]
|
|
860
|
+
},
|
|
861
|
+
advanced,
|
|
862
|
+
watermarkSection
|
|
863
|
+
]);
|
|
864
|
+
} else {
|
|
865
|
+
chart.renderer.setSettingsSections([advanced, watermarkSection]);
|
|
866
|
+
}
|
|
867
|
+
void chart.ready().then(() => {
|
|
868
|
+
if (this.buildSeq !== seq || this.destroyed) return;
|
|
869
|
+
if (this.pendingRange) {
|
|
870
|
+
chart.setVisibleRangePreset(this.pendingRange.preset);
|
|
871
|
+
this.pendingRange = null;
|
|
872
|
+
}
|
|
873
|
+
this.statusline?.setMeta(this.timeframe, this.providerLabel());
|
|
874
|
+
});
|
|
875
|
+
if (this.manifest.length || this.instances.length) {
|
|
876
|
+
for (const it of this.instances) it.handle = this.addToChart(chart, it.entry);
|
|
877
|
+
this.syncIndicatorCount();
|
|
878
|
+
} else if (indicators !== void 0) {
|
|
879
|
+
this.indicatorsPromise ?? (this.indicatorsPromise = resolveIndicators(indicators).then((list) => {
|
|
880
|
+
this.manifest = list;
|
|
881
|
+
const pending = this.pendingIndicators;
|
|
882
|
+
this.instances = pending ? pending.manifest.map((name) => list.find((e) => e.name === name)).filter((e) => e != null).map((entry) => ({ entry, handle: null })) : list.filter((e) => e.enabled).map((entry) => ({ entry, handle: null }));
|
|
883
|
+
if (pending) this.pendingIndicators = null;
|
|
884
|
+
this.manifestSettled = true;
|
|
885
|
+
return list;
|
|
886
|
+
}));
|
|
887
|
+
void this.indicatorsPromise.then(() => {
|
|
888
|
+
if (this.buildSeq !== seq || this.destroyed) return;
|
|
889
|
+
for (const it of this.instances) {
|
|
890
|
+
it.handle = this.addToChart(chart, it.entry);
|
|
891
|
+
}
|
|
892
|
+
this.syncIndicatorCount();
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/** Refresh the native catalog (supported/present flags) for the current chart. */
|
|
897
|
+
refreshNativeCatalog() {
|
|
898
|
+
const chart = this.inner;
|
|
899
|
+
if (!chart) return;
|
|
900
|
+
void chart.availableNativeIndicators().then((list) => {
|
|
901
|
+
if (this.inner !== chart || this.destroyed) return;
|
|
902
|
+
this.nativeCatalog = list.map((n) => ({ type: n.type, title: n.title, supported: n.supported, present: n.present, beta: n.beta }));
|
|
903
|
+
this.syncIndicatorCount();
|
|
904
|
+
this.indicatorPicker.sync();
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
/** Add a native indicator (single-instance per type — the core dedupes). */
|
|
908
|
+
addNative(type) {
|
|
909
|
+
this.inner?.addNativeIndicator(type);
|
|
910
|
+
this.syncPresentNatives();
|
|
911
|
+
this.refreshNativeCatalog();
|
|
912
|
+
this.history.push({
|
|
913
|
+
undo: () => {
|
|
914
|
+
this.inner?.addNativeIndicator(type).remove();
|
|
915
|
+
this.refreshNativeCatalog();
|
|
916
|
+
},
|
|
917
|
+
redo: () => {
|
|
918
|
+
this.inner?.addNativeIndicator(type);
|
|
919
|
+
this.refreshNativeCatalog();
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
removeNative(type) {
|
|
924
|
+
this.inner?.addNativeIndicator(type).remove();
|
|
925
|
+
this.refreshNativeCatalog();
|
|
926
|
+
}
|
|
927
|
+
/** Sync mirror of the chart's present native types — the removal handler diffs
|
|
928
|
+
* against it to identify (and record) whichever type was just removed. */
|
|
929
|
+
syncPresentNatives() {
|
|
930
|
+
this.presentNatives = this.inner?.presentNativeIndicators() ?? [];
|
|
931
|
+
}
|
|
932
|
+
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
933
|
+
addInstance(libraryIndex) {
|
|
934
|
+
const entry = this.manifest[libraryIndex];
|
|
935
|
+
if (!entry || this.destroyed) return;
|
|
936
|
+
const it = { entry, handle: this.inner ? this.addToChart(this.inner, entry) : null };
|
|
937
|
+
this.instances.push(it);
|
|
938
|
+
this.syncIndicatorCount();
|
|
939
|
+
const snapshot = it;
|
|
940
|
+
this.history.push({
|
|
941
|
+
undo: () => this.dropInstance(snapshot),
|
|
942
|
+
redo: () => {
|
|
943
|
+
snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
|
|
944
|
+
this.instances.push(snapshot);
|
|
945
|
+
this.syncIndicatorCount();
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
/** Remove one live instance (picker trash). */
|
|
950
|
+
removeInstance(instanceIndex) {
|
|
951
|
+
const it = this.instances[instanceIndex];
|
|
952
|
+
if (!it || this.destroyed) return;
|
|
953
|
+
this.dropInstance(it);
|
|
954
|
+
const snapshot = it;
|
|
955
|
+
this.history.push({
|
|
956
|
+
undo: () => {
|
|
957
|
+
snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
|
|
958
|
+
this.instances.push(snapshot);
|
|
959
|
+
this.syncIndicatorCount();
|
|
960
|
+
},
|
|
961
|
+
redo: () => this.dropInstance(snapshot)
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
dropInstance(it) {
|
|
965
|
+
const idx = this.instances.indexOf(it);
|
|
966
|
+
if (idx >= 0) this.instances.splice(idx, 1);
|
|
967
|
+
try {
|
|
968
|
+
it.handle?.remove();
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
it.handle = null;
|
|
972
|
+
this.syncIndicatorCount();
|
|
973
|
+
}
|
|
974
|
+
syncIndicatorCount() {
|
|
975
|
+
this.topbar.setIndicatorCount(this.instances.length + this.nativeCatalog.filter((n) => n.present).length);
|
|
976
|
+
}
|
|
977
|
+
addToChart(chart, ind) {
|
|
978
|
+
try {
|
|
979
|
+
return chart.addIndicator(ind.script, ind.language !== void 0 ? { language: ind.language } : void 0);
|
|
980
|
+
} catch (err) {
|
|
981
|
+
console.warn(`[vela] indicator "${ind.name}" failed to add:`, err);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
986
|
+
routeTyping(ev) {
|
|
987
|
+
if (this.destroyed || this.openDialogs > 0) return;
|
|
988
|
+
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
989
|
+
if (isEditableTarget(ev)) return;
|
|
990
|
+
const key = ev.key;
|
|
991
|
+
if (/^[a-zA-Z]$/.test(key)) {
|
|
992
|
+
ev.preventDefault();
|
|
993
|
+
this.symbolPicker.open(key.toUpperCase());
|
|
994
|
+
} else if (/^[0-9]$/.test(key)) {
|
|
995
|
+
ev.preventDefault();
|
|
996
|
+
this.tfQuick.open(key);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
trackDialog(open) {
|
|
1000
|
+
this.openDialogs = Math.max(0, this.openDialogs + (open ? 1 : -1));
|
|
1001
|
+
if (open) this.keymap.pushScope("dialog");
|
|
1002
|
+
else this.keymap.popScope("dialog");
|
|
1003
|
+
}
|
|
1004
|
+
downloadScreenshot() {
|
|
1005
|
+
const url = this.inner?.renderer.screenshot();
|
|
1006
|
+
if (!url) return;
|
|
1007
|
+
const a = this.root.ownerDocument.createElement("a");
|
|
1008
|
+
a.href = url;
|
|
1009
|
+
a.download = `${this.symbol || "chart"}-${this.timeframe}.png`;
|
|
1010
|
+
a.click();
|
|
1011
|
+
}
|
|
1012
|
+
/** The persistable state changed: debounce (~500ms), then notify `state:changed`
|
|
1013
|
+
* listeners and flush — the same cadence as the workspace. */
|
|
1014
|
+
markStateDirty() {
|
|
1015
|
+
if (this.destroyed) return;
|
|
1016
|
+
if (this.stateTimer != null) clearTimeout(this.stateTimer);
|
|
1017
|
+
this.stateTimer = setTimeout(() => {
|
|
1018
|
+
this.stateTimer = null;
|
|
1019
|
+
for (const listener of [...this.stateListeners]) listener();
|
|
1020
|
+
this.persistNow();
|
|
1021
|
+
}, 500);
|
|
1022
|
+
}
|
|
1023
|
+
/** Write the unified state document through the adapter (persist mode only) — also
|
|
1024
|
+
* the unload/destroy flush. The first save after a LEGACY boot rewrites the main
|
|
1025
|
+
* key in the unified format and drops the old `:config`/`:drawings` sub-keys. */
|
|
1026
|
+
persistNow() {
|
|
1027
|
+
if (this.stateTimer != null) {
|
|
1028
|
+
clearTimeout(this.stateTimer);
|
|
1029
|
+
this.stateTimer = null;
|
|
1030
|
+
}
|
|
1031
|
+
const state = this.getState();
|
|
1032
|
+
if (this.storageKey !== null) {
|
|
1033
|
+
try {
|
|
1034
|
+
void this.storage.set(this.storageKey, encodeState(state));
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
1037
|
+
if (this.legacyKeys) {
|
|
1038
|
+
this.legacyKeys = false;
|
|
1039
|
+
try {
|
|
1040
|
+
void this.storage.remove?.(`${this.storageKey}:config`);
|
|
1041
|
+
void this.storage.remove?.(`${this.storageKey}:drawings`);
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (this.opts.urlState) {
|
|
1047
|
+
writeUrlState({
|
|
1048
|
+
symbol: this.symbol,
|
|
1049
|
+
timeframe: this.timeframe,
|
|
1050
|
+
priceStyle: this.priceStyle,
|
|
1051
|
+
timezone: this.timezone,
|
|
1052
|
+
bars: String(this.bars),
|
|
1053
|
+
watermark: this.watermarkOn ? "1" : "0",
|
|
1054
|
+
favorites: this.favs.join(",")
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
export { VelaWidget, readUrlState, writeUrlState };
|