@luxalgo/vela 0.6.9 → 0.6.11
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/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-BBf-jc6W.d.ts} +49 -1
- package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-p0TEyhlX.d.cts} +49 -1
- package/dist/{chunk-6WDDVMBJ.js → chunk-73PEA4MU.js} +707 -353
- package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
- package/dist/{chunk-STHSKXOR.js → chunk-IO3NYSQV.js} +650 -157
- package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
- package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
- package/dist/{chunk-EQCHJZOT.js → chunk-MTLJKZDZ.js} +10 -2
- package/dist/{contributions-D7PVZO2i.d.ts → contributions-Bbe2R-mQ.d.ts} +20 -6
- package/dist/{contributions-C1U2Krwg.d.cts → contributions-CO01zWve.d.cts} +20 -6
- package/dist/index.cjs +719 -350
- package/dist/index.d.cts +37 -10
- package/dist/index.d.ts +37 -10
- package/dist/index.js +5 -5
- package/dist/{options-FM0peknS.d.ts → options-yp7sA96q.d.cts} +14 -7
- package/dist/{options-FM0peknS.d.cts → options-yp7sA96q.d.ts} +14 -7
- package/dist/{plugin-DfVqBz9p.d.cts → plugin-CkkH8QnX.d.cts} +3 -3
- package/dist/{plugin-7bkF32Rk.d.ts → plugin-DwxjM3Ni.d.ts} +3 -3
- package/dist/plugin.cjs +12 -1
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +3 -3
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-DOPiT6I6.d.cts → statusline-CHPDuKNp.d.ts} +13 -5
- package/dist/{statusline-zdF4eZLr.d.ts → statusline-DTHZUFqK.d.cts} +13 -5
- package/dist/ui.cjs +7 -1
- package/dist/ui.d.cts +3 -1
- package/dist/ui.d.ts +3 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +719 -350
- package/dist/vela.global.min.js +51 -49
- package/dist/widget.cjs +1249 -388
- package/dist/widget.d.cts +17 -6
- package/dist/widget.d.ts +17 -6
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1249 -388
- package/dist/workspace.d.cts +103 -10
- package/dist/workspace.d.ts +103 -10
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
- /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-
|
|
2
|
-
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-
|
|
3
|
-
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BEARISH, BULLISH, isDarkColor, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-
|
|
1
|
+
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-MTLJKZDZ.js';
|
|
2
|
+
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-MHY7MVXH.js';
|
|
3
|
+
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BEARISH, BULLISH, isDarkColor, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-KG4YT3TI.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -696,6 +696,23 @@ var BarStore = class {
|
|
|
696
696
|
};
|
|
697
697
|
var sharedBarStore = new BarStore();
|
|
698
698
|
|
|
699
|
+
// src/data/symbol-groups.ts
|
|
700
|
+
function isGroupRow(d) {
|
|
701
|
+
return d.group != null && d.ticker === d.group;
|
|
702
|
+
}
|
|
703
|
+
function groupKeyOf(d) {
|
|
704
|
+
return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
|
|
705
|
+
}
|
|
706
|
+
function groupMembers(pool, groupRow) {
|
|
707
|
+
const key = groupKeyOf(groupRow);
|
|
708
|
+
return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
|
|
709
|
+
}
|
|
710
|
+
function defaultMemberOf(pool, groupRow) {
|
|
711
|
+
const members = groupMembers(pool, groupRow);
|
|
712
|
+
const defaults = members.filter((m) => m.default);
|
|
713
|
+
return (defaults.length === 1 ? defaults[0] : members[0]) ?? null;
|
|
714
|
+
}
|
|
715
|
+
|
|
699
716
|
// src/data/ProviderRegistry.ts
|
|
700
717
|
var normName = (n) => n.trim().toLowerCase();
|
|
701
718
|
var normTicker = (t) => t.trim().toUpperCase();
|
|
@@ -785,17 +802,24 @@ var ProviderRegistry = class {
|
|
|
785
802
|
resolve(raw, opts = {}) {
|
|
786
803
|
const { provider, ticker } = this.parse(raw);
|
|
787
804
|
if (provider) {
|
|
788
|
-
if (this.entries.has(provider))
|
|
805
|
+
if (this.entries.has(provider)) {
|
|
806
|
+
const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
|
|
807
|
+
return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
|
|
808
|
+
}
|
|
789
809
|
const key = `${provider}:${normTicker(ticker)}`;
|
|
790
810
|
for (const name of this.candidateOrder(opts.default)) {
|
|
791
811
|
const d = this.entries.get(name).prefixIndex?.get(key);
|
|
792
|
-
if (d) return { provider: name, ticker:
|
|
812
|
+
if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
|
|
793
813
|
}
|
|
794
814
|
return null;
|
|
795
815
|
}
|
|
796
816
|
const norm = normTicker(ticker);
|
|
797
817
|
for (const name of this.candidateOrder(opts.default)) {
|
|
798
|
-
|
|
818
|
+
const e = this.entries.get(name);
|
|
819
|
+
if (e.index?.has(norm)) {
|
|
820
|
+
const d = e.byTicker?.get(norm);
|
|
821
|
+
return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
|
|
822
|
+
}
|
|
799
823
|
}
|
|
800
824
|
if (opts.lenient && this.entries.size === 1) {
|
|
801
825
|
const only = this.names()[0];
|
|
@@ -804,6 +828,13 @@ var ProviderRegistry = class {
|
|
|
804
828
|
}
|
|
805
829
|
return null;
|
|
806
830
|
}
|
|
831
|
+
/** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
|
|
832
|
+
* served), which loads its default member (single `default`, else first listed). A
|
|
833
|
+
* memberless group keeps its own ticker and fails downstream like any unknown symbol. */
|
|
834
|
+
loadableTicker(name, d) {
|
|
835
|
+
if (!isGroupRow(d)) return d.ticker;
|
|
836
|
+
return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
|
|
837
|
+
}
|
|
807
838
|
/**
|
|
808
839
|
* Resolve now if possible, else resolve later — re-attempting after each provider
|
|
809
840
|
* settles. The promise stays pending until some registered provider can serve the
|
|
@@ -947,6 +978,28 @@ var CachingDataFeed = class {
|
|
|
947
978
|
this.store.merge(key, dropForming(bars));
|
|
948
979
|
return bars;
|
|
949
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* Progressive twin of {@link load}: a COLD load streams through the inner feed's
|
|
983
|
+
* progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
|
|
984
|
+
* `load` caches — while a cache-covered load answers once through `load` itself (a
|
|
985
|
+
* warm chart has nothing to stream). Falls back to `load` wholesale when the inner
|
|
986
|
+
* feed lacks the capability, so callers may prefer this method unconditionally.
|
|
987
|
+
*/
|
|
988
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
989
|
+
if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
|
|
990
|
+
if (!this.inner.loadProgressive) return null;
|
|
991
|
+
const symbol = cfg.symbol ?? "TEST";
|
|
992
|
+
const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
|
|
993
|
+
const cached = this.store.get(key);
|
|
994
|
+
const n = cfg.bars ?? 500;
|
|
995
|
+
const lastCached = cached?.[cached.length - 1];
|
|
996
|
+
if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
|
|
997
|
+
this.store.retainSymbol(symbol);
|
|
998
|
+
const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
|
|
999
|
+
if (bars == null) return null;
|
|
1000
|
+
this.store.merge(key, dropForming(bars));
|
|
1001
|
+
return bars;
|
|
1002
|
+
}
|
|
950
1003
|
/**
|
|
951
1004
|
* Cache-backed ranged fetch — the gateway engines use for secondary series
|
|
952
1005
|
* (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
|
|
@@ -1192,6 +1245,17 @@ var MultiProviderFeed = class {
|
|
|
1192
1245
|
this.prefetchSymbolInfo(resolved);
|
|
1193
1246
|
return this.cache.load(canonical(cfg, resolved));
|
|
1194
1247
|
}
|
|
1248
|
+
/** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
|
|
1249
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
1250
|
+
if (cfg.data && cfg.data.length > 0) {
|
|
1251
|
+
this.liveBars = cfg.data.map((b) => ({ ...b }));
|
|
1252
|
+
return cfg.data;
|
|
1253
|
+
}
|
|
1254
|
+
const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
|
|
1255
|
+
this.primaryProvider = resolved.provider;
|
|
1256
|
+
this.prefetchSymbolInfo(resolved);
|
|
1257
|
+
return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
|
|
1258
|
+
}
|
|
1195
1259
|
/**
|
|
1196
1260
|
* Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
|
|
1197
1261
|
* warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
|
|
@@ -1279,6 +1343,18 @@ var RegistryFetchFeed = class {
|
|
|
1279
1343
|
if (!provider) return Promise.resolve([]);
|
|
1280
1344
|
return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
|
|
1281
1345
|
}
|
|
1346
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
1347
|
+
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
1348
|
+
const provider = this.registry.get(name ?? "");
|
|
1349
|
+
if (!provider) return [];
|
|
1350
|
+
if (!provider.getBarsProgressive) return null;
|
|
1351
|
+
try {
|
|
1352
|
+
return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
|
|
1353
|
+
} catch (e) {
|
|
1354
|
+
console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
1355
|
+
return [];
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1282
1358
|
loadRange(cfg, range) {
|
|
1283
1359
|
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
1284
1360
|
const provider = this.registry.get(name ?? "");
|
|
@@ -2426,7 +2502,7 @@ var DrawingToolbar = class {
|
|
|
2426
2502
|
this.root.replaceChildren();
|
|
2427
2503
|
this.groupCells.clear();
|
|
2428
2504
|
this.groupIcons.clear();
|
|
2429
|
-
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.
|
|
2505
|
+
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
|
|
2430
2506
|
this.root.appendChild(this.cursorBtn);
|
|
2431
2507
|
if (this.def.groups.length > 0) this.root.appendChild(this.divider());
|
|
2432
2508
|
for (const g of this.def.groups) {
|
|
@@ -2530,6 +2606,14 @@ var DrawingToolbar = class {
|
|
|
2530
2606
|
this.magnetIcon = icon2;
|
|
2531
2607
|
return cell;
|
|
2532
2608
|
}
|
|
2609
|
+
/** Cursor returns to select/idle: an active measure/eraser mode exits through its own
|
|
2610
|
+
* toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
|
|
2611
|
+
* null arm as a no-op side effect of entering those modes), then the tool disarms. */
|
|
2612
|
+
onCursorClick() {
|
|
2613
|
+
if (this.measureActive) this.onMeasure();
|
|
2614
|
+
if (this.eraserActive) this.onEraser();
|
|
2615
|
+
this.onArm(null);
|
|
2616
|
+
}
|
|
2533
2617
|
/** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
|
|
2534
2618
|
onGroupIconClick(group) {
|
|
2535
2619
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
@@ -3819,6 +3903,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
|
|
|
3819
3903
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
|
|
3820
3904
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
|
|
3821
3905
|
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
|
|
3906
|
+
function legendCalloutsDisplay(open, hasCallouts) {
|
|
3907
|
+
return !open && hasCallouts ? "inline-flex" : "none";
|
|
3908
|
+
}
|
|
3822
3909
|
var InputsUI = class {
|
|
3823
3910
|
constructor(container, theme, paneBoundsOf) {
|
|
3824
3911
|
this.container = container;
|
|
@@ -4022,7 +4109,7 @@ var InputsUI = class {
|
|
|
4022
4109
|
row.callouts = [];
|
|
4023
4110
|
row.calloutsEl.replaceChildren();
|
|
4024
4111
|
const views = this.legendCallouts?.(row.id) ?? [];
|
|
4025
|
-
row.calloutsEl.style.display = views.length > 0
|
|
4112
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
|
|
4026
4113
|
for (const view of views) {
|
|
4027
4114
|
const bubble = new CalloutBubble({
|
|
4028
4115
|
icon: view.icon,
|
|
@@ -4506,11 +4593,11 @@ var InputsUI = class {
|
|
|
4506
4593
|
row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
|
|
4507
4594
|
if (open) {
|
|
4508
4595
|
row.el.appendChild(row.statusEl);
|
|
4509
|
-
row.
|
|
4596
|
+
for (const bubble of row.callouts) bubble.hidePanel();
|
|
4510
4597
|
} else {
|
|
4511
4598
|
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
4512
|
-
row.el.insertBefore(row.calloutsEl, row.statusEl);
|
|
4513
4599
|
}
|
|
4600
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(open, row.callouts.length > 0);
|
|
4514
4601
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
4515
4602
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
4516
4603
|
if (child === row.extrasEl) {
|
|
@@ -4600,7 +4687,7 @@ var ICONS = {
|
|
|
4600
4687
|
};
|
|
4601
4688
|
var STYLE_ID2 = "vela-pane-controls";
|
|
4602
4689
|
var ICON_PX = 12;
|
|
4603
|
-
var CLUSTER_PILL = "rgba(0,0,0,0.
|
|
4690
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
4604
4691
|
function ensureStyles2() {
|
|
4605
4692
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
4606
4693
|
const st = document.createElement("style");
|
|
@@ -4620,8 +4707,12 @@ var PaneControls = class {
|
|
|
4620
4707
|
this.deps = deps;
|
|
4621
4708
|
this.clusters = /* @__PURE__ */ new Map();
|
|
4622
4709
|
this.hoverPaneId = null;
|
|
4710
|
+
/** Mobile: hover clusters are meaningless without a cursor — suppressed; a
|
|
4711
|
+
* collapsed pane's standalone expand chip stays (the only way back up). */
|
|
4712
|
+
this.suspended = false;
|
|
4623
4713
|
/** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
|
|
4624
4714
|
this.onPlotMove = (e) => {
|
|
4715
|
+
if (this.suspended) return;
|
|
4625
4716
|
const rect = this.plot.getBoundingClientRect();
|
|
4626
4717
|
const y = e.clientY - rect.top;
|
|
4627
4718
|
let hit = null;
|
|
@@ -4693,7 +4784,12 @@ var PaneControls = class {
|
|
|
4693
4784
|
}
|
|
4694
4785
|
if (p.count > 1) {
|
|
4695
4786
|
cluster.appendChild(
|
|
4696
|
-
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
4787
|
+
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
4788
|
+
role: "maximize",
|
|
4789
|
+
// Same inverse-chip treatment as the collapsed pane's expand toggle: the
|
|
4790
|
+
// maximized state must read as an active state, not just a swapped glyph.
|
|
4791
|
+
selected: p.maximized
|
|
4792
|
+
})
|
|
4697
4793
|
);
|
|
4698
4794
|
}
|
|
4699
4795
|
}
|
|
@@ -4730,17 +4826,18 @@ var PaneControls = class {
|
|
|
4730
4826
|
}
|
|
4731
4827
|
const hovered = id === this.hoverPaneId;
|
|
4732
4828
|
const hasButtons = cluster.children.length > 0;
|
|
4733
|
-
const
|
|
4829
|
+
const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
|
|
4830
|
+
const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
|
|
4734
4831
|
cluster.style.right = `${rightPx}px`;
|
|
4735
4832
|
cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
|
|
4736
4833
|
cluster.style.display = visible ? "flex" : "none";
|
|
4737
4834
|
if (!visible) continue;
|
|
4738
|
-
const
|
|
4739
|
-
cluster.style.background =
|
|
4835
|
+
const soloChip = stateChipRole != null && !hovered;
|
|
4836
|
+
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
4740
4837
|
for (const child of cluster.children) {
|
|
4741
4838
|
const btn2 = child;
|
|
4742
4839
|
btn2.style.display = "inline-flex";
|
|
4743
|
-
btn2.style.visibility =
|
|
4840
|
+
btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
4744
4841
|
}
|
|
4745
4842
|
}
|
|
4746
4843
|
}
|
|
@@ -4750,6 +4847,14 @@ var PaneControls = class {
|
|
|
4750
4847
|
this.hoverPaneId = paneId;
|
|
4751
4848
|
this.reposition();
|
|
4752
4849
|
}
|
|
4850
|
+
/** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
|
|
4851
|
+
* chrome covers maximize), while collapsed panes keep their expand chips. */
|
|
4852
|
+
setSuspended(on) {
|
|
4853
|
+
if (on === this.suspended) return;
|
|
4854
|
+
this.suspended = on;
|
|
4855
|
+
if (on) this.hoverPaneId = null;
|
|
4856
|
+
this.reposition();
|
|
4857
|
+
}
|
|
4753
4858
|
destroy() {
|
|
4754
4859
|
this.plot.removeEventListener("pointermove", this.onPlotMove);
|
|
4755
4860
|
this.plot.removeEventListener("pointerleave", this.onPlotLeave);
|
|
@@ -4900,160 +5005,6 @@ var AxisScaleButtons = class {
|
|
|
4900
5005
|
}
|
|
4901
5006
|
};
|
|
4902
5007
|
|
|
4903
|
-
// src/renderers/shared/TableOverlay.ts
|
|
4904
|
-
var SIZE_PX = {
|
|
4905
|
-
auto: 13,
|
|
4906
|
-
tiny: 10,
|
|
4907
|
-
small: 11,
|
|
4908
|
-
normal: 13,
|
|
4909
|
-
large: 16,
|
|
4910
|
-
huge: 20
|
|
4911
|
-
};
|
|
4912
|
-
function fontPxOf(size) {
|
|
4913
|
-
if (typeof size === "number") return size > 0 ? size : SIZE_PX.auto;
|
|
4914
|
-
return SIZE_PX[size] ?? SIZE_PX.auto;
|
|
4915
|
-
}
|
|
4916
|
-
function tableHasContent(t) {
|
|
4917
|
-
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
4918
|
-
}
|
|
4919
|
-
function mergeRenderPlan(t) {
|
|
4920
|
-
const span = /* @__PURE__ */ new Map();
|
|
4921
|
-
const omit = /* @__PURE__ */ new Set();
|
|
4922
|
-
for (const m of t.merges) {
|
|
4923
|
-
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
4924
|
-
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
4925
|
-
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
4926
|
-
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
4927
|
-
}
|
|
4928
|
-
}
|
|
4929
|
-
}
|
|
4930
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
4931
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
4932
|
-
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
4933
|
-
}
|
|
4934
|
-
}
|
|
4935
|
-
for (const key of span.keys()) omit.delete(key);
|
|
4936
|
-
return { span, omit };
|
|
4937
|
-
}
|
|
4938
|
-
var TableOverlay = class {
|
|
4939
|
-
constructor(container, theme, paneBounds) {
|
|
4940
|
-
this.container = container;
|
|
4941
|
-
this.theme = theme;
|
|
4942
|
-
this.paneBounds = paneBounds;
|
|
4943
|
-
this.lastTables = [];
|
|
4944
|
-
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
4945
|
-
this.root = document.createElement("div");
|
|
4946
|
-
Object.assign(this.root.style, {
|
|
4947
|
-
position: "absolute",
|
|
4948
|
-
inset: "0",
|
|
4949
|
-
pointerEvents: "none",
|
|
4950
|
-
overflow: "hidden",
|
|
4951
|
-
zIndex: "3"
|
|
4952
|
-
});
|
|
4953
|
-
container.appendChild(this.root);
|
|
4954
|
-
}
|
|
4955
|
-
update(tables) {
|
|
4956
|
-
this.lastTables = tables;
|
|
4957
|
-
this.root.replaceChildren();
|
|
4958
|
-
for (const t of tables) {
|
|
4959
|
-
if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
|
|
4960
|
-
}
|
|
4961
|
-
}
|
|
4962
|
-
/** Re-render at the current pane geometry — after layout settles or on resize. */
|
|
4963
|
-
reposition() {
|
|
4964
|
-
if (this.root.isConnected) this.update(this.lastTables);
|
|
4965
|
-
}
|
|
4966
|
-
/** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
|
|
4967
|
-
* series content they DON'T vanish with an emptied chart — the loading state hides them. */
|
|
4968
|
-
setVisible(visible) {
|
|
4969
|
-
this.root.style.display = visible ? "" : "none";
|
|
4970
|
-
}
|
|
4971
|
-
destroy() {
|
|
4972
|
-
this.root.remove();
|
|
4973
|
-
}
|
|
4974
|
-
renderTable(t) {
|
|
4975
|
-
const b = this.paneBounds(t.paneId);
|
|
4976
|
-
const wrap = document.createElement("div");
|
|
4977
|
-
wrap.style.position = "absolute";
|
|
4978
|
-
if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
|
|
4979
|
-
this.anchor(wrap, t.position, b);
|
|
4980
|
-
const table = document.createElement("table");
|
|
4981
|
-
Object.assign(table.style, {
|
|
4982
|
-
borderCollapse: "collapse",
|
|
4983
|
-
background: t.bgColor ?? "transparent",
|
|
4984
|
-
fontFamily: this.theme.fontFamily || "sans-serif",
|
|
4985
|
-
border: "none",
|
|
4986
|
-
tableLayout: "auto",
|
|
4987
|
-
// Re-enable pointer events on the table only (root is none) so cell tooltips work.
|
|
4988
|
-
pointerEvents: "auto"
|
|
4989
|
-
});
|
|
4990
|
-
const { span, omit } = mergeRenderPlan(t);
|
|
4991
|
-
const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
|
|
4992
|
-
const paneH = b.height;
|
|
4993
|
-
const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
|
|
4994
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
4995
|
-
const tr = document.createElement("tr");
|
|
4996
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
4997
|
-
if (omit.has(`${r}:${c}`)) continue;
|
|
4998
|
-
const cell = t.cells[r]?.[c] ?? null;
|
|
4999
|
-
const td = document.createElement("td");
|
|
5000
|
-
if (cell === null) {
|
|
5001
|
-
Object.assign(td.style, { padding: "0", border: "none" });
|
|
5002
|
-
tr.appendChild(td);
|
|
5003
|
-
continue;
|
|
5004
|
-
}
|
|
5005
|
-
const sp = span.get(`${r}:${c}`);
|
|
5006
|
-
if (sp) {
|
|
5007
|
-
if (sp.cs > 1) td.colSpan = sp.cs;
|
|
5008
|
-
if (sp.rs > 1) td.rowSpan = sp.rs;
|
|
5009
|
-
}
|
|
5010
|
-
Object.assign(td.style, {
|
|
5011
|
-
border: cellBorder,
|
|
5012
|
-
padding: "2px 6px",
|
|
5013
|
-
background: cell.bgColor ?? "transparent",
|
|
5014
|
-
color: cell.textColor ?? this.theme.textColor,
|
|
5015
|
-
textAlign: cell.hAlign,
|
|
5016
|
-
verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
|
|
5017
|
-
fontSize: `${fontPxOf(cell.textSize)}px`,
|
|
5018
|
-
fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
|
|
5019
|
-
fontWeight: cell.bold ? "bold" : "normal",
|
|
5020
|
-
fontStyle: cell.italic ? "italic" : "normal",
|
|
5021
|
-
// Pine cell text never wraps; `\n` still breaks lines. Wrapping
|
|
5022
|
-
// used to collapse unicode sparklines and ━━━ dividers.
|
|
5023
|
-
whiteSpace: "pre"
|
|
5024
|
-
});
|
|
5025
|
-
if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
|
|
5026
|
-
if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
|
|
5027
|
-
if (cell.tooltip) td.title = cell.tooltip;
|
|
5028
|
-
td.textContent = cell.text ?? "";
|
|
5029
|
-
tr.appendChild(td);
|
|
5030
|
-
}
|
|
5031
|
-
table.appendChild(tr);
|
|
5032
|
-
}
|
|
5033
|
-
wrap.appendChild(table);
|
|
5034
|
-
return wrap;
|
|
5035
|
-
}
|
|
5036
|
-
/**
|
|
5037
|
-
* Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
|
|
5038
|
-
* (not the whole chart), inset past the right price axis so it never overlaps
|
|
5039
|
-
* the Y-axis labels.
|
|
5040
|
-
*/
|
|
5041
|
-
anchor(el, position, b) {
|
|
5042
|
-
const m = 6;
|
|
5043
|
-
const containerH = this.root.clientHeight || this.container.clientHeight;
|
|
5044
|
-
const paneBottom = b.top + b.height;
|
|
5045
|
-
if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
|
|
5046
|
-
else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
|
|
5047
|
-
else el.style.top = `${b.top + b.height / 2}px`;
|
|
5048
|
-
if (position.endsWith("left")) el.style.left = `${m}px`;
|
|
5049
|
-
else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
|
|
5050
|
-
else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
|
|
5051
|
-
const tx = position.endsWith("center") ? "-50%" : "0";
|
|
5052
|
-
const ty = position.startsWith("middle") ? "-50%" : "0";
|
|
5053
|
-
if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
|
|
5054
|
-
}
|
|
5055
|
-
};
|
|
5056
|
-
|
|
5057
5008
|
// src/renderers/native/capabilities.ts
|
|
5058
5009
|
var NATIVE_CAPABILITIES = {
|
|
5059
5010
|
panes: true,
|
|
@@ -5074,7 +5025,7 @@ var NATIVE_CAPABILITIES = {
|
|
|
5074
5025
|
drawingDepth: true,
|
|
5075
5026
|
// drawings share the series' z space (backend-composited interleave layers)
|
|
5076
5027
|
tables: true,
|
|
5077
|
-
//
|
|
5028
|
+
// canvas-painted into the owning indicator's interleave slice
|
|
5078
5029
|
trades: true,
|
|
5079
5030
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
5080
5031
|
inputsUI: true
|
|
@@ -5105,6 +5056,9 @@ function candleTier(spacing) {
|
|
|
5105
5056
|
if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
|
|
5106
5057
|
return "full";
|
|
5107
5058
|
}
|
|
5059
|
+
function snapY(yCss, dpr) {
|
|
5060
|
+
return Math.round(yCss * dpr) / dpr;
|
|
5061
|
+
}
|
|
5108
5062
|
function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
5109
5063
|
const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
|
|
5110
5064
|
const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
|
|
@@ -5911,11 +5865,9 @@ var WebGL2Backend = class {
|
|
|
5911
5865
|
};
|
|
5912
5866
|
b.alpha = this.modelAlpha;
|
|
5913
5867
|
for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
|
|
5914
|
-
for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
|
|
5915
5868
|
if (isPrice) {
|
|
5916
5869
|
for (const m of scene.indicators.values()) {
|
|
5917
5870
|
for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
|
|
5918
|
-
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
5919
5871
|
}
|
|
5920
5872
|
}
|
|
5921
5873
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
@@ -5931,6 +5883,7 @@ var WebGL2Backend = class {
|
|
|
5931
5883
|
b.alpha = this.modelAlpha;
|
|
5932
5884
|
const off = scene.offsetOf(m.id);
|
|
5933
5885
|
const mp = effPane(m);
|
|
5886
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
|
|
5934
5887
|
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
5935
5888
|
}
|
|
5936
5889
|
if (drawCandles && !candleDrawn) {
|
|
@@ -5938,14 +5891,18 @@ var WebGL2Backend = class {
|
|
|
5938
5891
|
b.alpha = this.candleStructureAlpha;
|
|
5939
5892
|
this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
5940
5893
|
}
|
|
5941
|
-
drawSlicesUpTo(Infinity);
|
|
5942
5894
|
b.alpha = this.modelAlpha;
|
|
5943
5895
|
if (isPrice) {
|
|
5896
|
+
for (const m of scene.indicators.values()) {
|
|
5897
|
+
const off = scene.offsetOf(m.id);
|
|
5898
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
|
|
5899
|
+
}
|
|
5944
5900
|
for (const m of scene.indicators.values()) {
|
|
5945
5901
|
const off = scene.offsetOf(m.id);
|
|
5946
5902
|
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
5947
5903
|
}
|
|
5948
5904
|
}
|
|
5905
|
+
drawSlicesUpTo(Infinity);
|
|
5949
5906
|
for (const m of models) {
|
|
5950
5907
|
const mp = effPane(m);
|
|
5951
5908
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -6340,12 +6297,12 @@ var WebGL2Backend = class {
|
|
|
6340
6297
|
if (drawBody) {
|
|
6341
6298
|
const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
|
|
6342
6299
|
const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
|
|
6343
|
-
bodyTop = Math.min(oY, cY);
|
|
6344
|
-
bodyH = Math.max(1, Math.
|
|
6300
|
+
bodyTop = snapY(Math.min(oY, cY), coords.dpr);
|
|
6301
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
|
|
6345
6302
|
}
|
|
6346
6303
|
if (cs.wickVisible) {
|
|
6347
|
-
const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
|
|
6348
|
-
const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
|
|
6304
|
+
const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
|
|
6305
|
+
const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
|
|
6349
6306
|
b.alpha = this.candleStructureAlpha;
|
|
6350
6307
|
const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
|
|
6351
6308
|
if (drawBody) {
|
|
@@ -6902,6 +6859,7 @@ var DOUBLE_TAP_MS = 350;
|
|
|
6902
6859
|
var DOUBLE_TAP_SLOP = 30;
|
|
6903
6860
|
var TIME_SCALE_K = 4e-3;
|
|
6904
6861
|
var WHEEL_ZOOM_K = 4e-3;
|
|
6862
|
+
var WHEEL_PRICE_DRAG_PX = 0.25;
|
|
6905
6863
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
6906
6864
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
6907
6865
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -7032,7 +6990,7 @@ var InputController = class {
|
|
|
7032
6990
|
this.capture(e.pointerId);
|
|
7033
6991
|
return;
|
|
7034
6992
|
}
|
|
7035
|
-
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
|
|
6993
|
+
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
7036
6994
|
this.region = "drawing";
|
|
7037
6995
|
this.capture(e.pointerId);
|
|
7038
6996
|
return;
|
|
@@ -7146,7 +7104,7 @@ var InputController = class {
|
|
|
7146
7104
|
const wasTouch = e.pointerType === "touch";
|
|
7147
7105
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
7148
7106
|
if (this.dragging && this.region === "drawing") {
|
|
7149
|
-
this.deps.drawingsPointerUp?.(x, y);
|
|
7107
|
+
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
7150
7108
|
} else if (tapRelease && this.region === "data") {
|
|
7151
7109
|
this.deps.onClick(x, y);
|
|
7152
7110
|
} else if (this.dragging && this.region === "data") {
|
|
@@ -7167,7 +7125,7 @@ var InputController = class {
|
|
|
7167
7125
|
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
7168
7126
|
this.cancelLongPress();
|
|
7169
7127
|
if (!this.dragging) return;
|
|
7170
|
-
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
7128
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
7171
7129
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
7172
7130
|
this.endGesture(e);
|
|
7173
7131
|
};
|
|
@@ -7183,6 +7141,12 @@ var InputController = class {
|
|
|
7183
7141
|
this.onWheel = (e) => {
|
|
7184
7142
|
e.preventDefault();
|
|
7185
7143
|
this.deps.drawingsClearTransient?.();
|
|
7144
|
+
const { x, y } = this.local(e);
|
|
7145
|
+
if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
|
|
7146
|
+
this.deps.beginPriceScale(x, y);
|
|
7147
|
+
this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
|
|
7148
|
+
return;
|
|
7149
|
+
}
|
|
7186
7150
|
const coords = this.deps.getCoords();
|
|
7187
7151
|
const vp = coords.getViewport();
|
|
7188
7152
|
const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
|
|
@@ -7190,9 +7154,8 @@ var InputController = class {
|
|
|
7190
7154
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
|
|
7191
7155
|
return;
|
|
7192
7156
|
}
|
|
7193
|
-
const cursorX = this.local(e).x;
|
|
7194
7157
|
const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
|
|
7195
|
-
const anchor = wheelZoomAnchor(coords,
|
|
7158
|
+
const anchor = wheelZoomAnchor(coords, x, rightEdge);
|
|
7196
7159
|
const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
|
|
7197
7160
|
this.deps.zoomTo(target, anchor.logical, anchor.x);
|
|
7198
7161
|
};
|
|
@@ -7678,9 +7641,10 @@ var SceneGraph = class {
|
|
|
7678
7641
|
* so each indicator arrives behind the candles (and behind older indicators);
|
|
7679
7642
|
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
7680
7643
|
this.seriesZ = /* @__PURE__ */ new Map();
|
|
7681
|
-
/** Per-pane raster layers of
|
|
7644
|
+
/** Per-pane raster layers of drawings interleaved into the series stack (each
|
|
7645
|
+
* indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
|
|
7682
7646
|
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
7683
|
-
* Rebuilt by the renderer per data frame
|
|
7647
|
+
* Rebuilt by the renderer per data frame. */
|
|
7684
7648
|
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
7685
7649
|
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
7686
7650
|
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
@@ -7915,11 +7879,9 @@ var Canvas2dBackend = class {
|
|
|
7915
7879
|
};
|
|
7916
7880
|
ctx.globalAlpha = this.modelAlpha;
|
|
7917
7881
|
for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
|
|
7918
|
-
for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
|
|
7919
7882
|
if (isPrice) {
|
|
7920
7883
|
for (const m of scene.indicators.values()) {
|
|
7921
7884
|
for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
|
|
7922
|
-
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
7923
7885
|
}
|
|
7924
7886
|
}
|
|
7925
7887
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
@@ -7943,6 +7905,7 @@ var Canvas2dBackend = class {
|
|
|
7943
7905
|
ctx.globalAlpha = this.modelAlpha;
|
|
7944
7906
|
const off = scene.offsetOf(m.id);
|
|
7945
7907
|
const mp = effPane(m);
|
|
7908
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
|
|
7946
7909
|
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
7947
7910
|
}
|
|
7948
7911
|
if (drawCandles && !candleDrawn) {
|
|
@@ -7950,14 +7913,18 @@ var Canvas2dBackend = class {
|
|
|
7950
7913
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
7951
7914
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
7952
7915
|
}
|
|
7953
|
-
drawSlicesUpTo(Infinity);
|
|
7954
7916
|
if (isPrice) {
|
|
7955
7917
|
ctx.globalAlpha = this.modelAlpha;
|
|
7918
|
+
for (const m of scene.indicators.values()) {
|
|
7919
|
+
const off = scene.offsetOf(m.id);
|
|
7920
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
|
|
7921
|
+
}
|
|
7956
7922
|
for (const m of scene.indicators.values()) {
|
|
7957
7923
|
const off = scene.offsetOf(m.id);
|
|
7958
7924
|
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
7959
7925
|
}
|
|
7960
7926
|
}
|
|
7927
|
+
drawSlicesUpTo(Infinity);
|
|
7961
7928
|
ctx.globalAlpha = this.modelAlpha;
|
|
7962
7929
|
for (const m of models) {
|
|
7963
7930
|
const mp = effPane(m);
|
|
@@ -8201,13 +8168,13 @@ var Canvas2dBackend = class {
|
|
|
8201
8168
|
if (drawBody) {
|
|
8202
8169
|
const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
|
|
8203
8170
|
const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
|
|
8204
|
-
top = Math.min(oY, cY);
|
|
8205
|
-
bodyH = Math.max(1, Math.
|
|
8171
|
+
top = snapY(Math.min(oY, cY), coords.dpr);
|
|
8172
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
|
|
8206
8173
|
}
|
|
8207
8174
|
if (cs.wickVisible) {
|
|
8208
8175
|
const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
|
|
8209
|
-
const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
|
|
8210
|
-
const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
|
|
8176
|
+
const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
|
|
8177
|
+
const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
|
|
8211
8178
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
8212
8179
|
ctx.strokeStyle = wick;
|
|
8213
8180
|
ctx.lineWidth = g.wickW;
|
|
@@ -8654,9 +8621,31 @@ function autoFontSize(lines, boxW, boxH, bold) {
|
|
|
8654
8621
|
|
|
8655
8622
|
// src/renderers/shared/DrawingSceneRenderer.ts
|
|
8656
8623
|
var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
|
|
8624
|
+
function modelDrawingSet(m, overlay) {
|
|
8625
|
+
const want = (d) => Boolean(d.overlay) === overlay;
|
|
8626
|
+
return {
|
|
8627
|
+
lines: (m.lines ?? []).filter(want),
|
|
8628
|
+
boxes: (m.boxes ?? []).filter(want),
|
|
8629
|
+
labels: (m.labels ?? []).filter(want),
|
|
8630
|
+
polylines: (m.polylines ?? []).filter(want),
|
|
8631
|
+
linefills: (m.linefills ?? []).filter(want)
|
|
8632
|
+
};
|
|
8633
|
+
}
|
|
8634
|
+
function drawingSetEmpty(s) {
|
|
8635
|
+
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
8636
|
+
}
|
|
8657
8637
|
function fontSizePx(size) {
|
|
8658
8638
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
8659
8639
|
}
|
|
8640
|
+
function lineCoversWindow(a, b, extend, lo, hi) {
|
|
8641
|
+
const minX = Math.min(a, b);
|
|
8642
|
+
const maxX = Math.max(a, b);
|
|
8643
|
+
if (a === b) return a >= lo && a <= hi;
|
|
8644
|
+
if (extend === "both") return true;
|
|
8645
|
+
if (extend === "left") return maxX >= lo;
|
|
8646
|
+
if (extend === "right") return minX <= hi;
|
|
8647
|
+
return maxX >= lo && minX <= hi;
|
|
8648
|
+
}
|
|
8660
8649
|
var DrawingSceneRenderer = class {
|
|
8661
8650
|
constructor(deps, set = EMPTY_DRAWING_SET) {
|
|
8662
8651
|
this.deps = deps;
|
|
@@ -8721,7 +8710,7 @@ var DrawingSceneRenderer = class {
|
|
|
8721
8710
|
};
|
|
8722
8711
|
for (const ln of this.set.lines) {
|
|
8723
8712
|
if (ln.invisible) continue;
|
|
8724
|
-
if (!
|
|
8713
|
+
if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
|
|
8725
8714
|
fold(ln.y1);
|
|
8726
8715
|
fold(ln.y2);
|
|
8727
8716
|
}
|
|
@@ -9506,10 +9495,8 @@ var ChromeRenderer = class {
|
|
|
9506
9495
|
this.ctx = null;
|
|
9507
9496
|
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
9508
9497
|
this.axisTextColor = DARK_THEME.textColor;
|
|
9509
|
-
// Shared Pine-drawing renderer
|
|
9498
|
+
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
9510
9499
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
9511
|
-
// Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
|
|
9512
|
-
this.labelTips = [];
|
|
9513
9500
|
}
|
|
9514
9501
|
mount(canvas) {
|
|
9515
9502
|
this.canvas = canvas;
|
|
@@ -9533,8 +9520,8 @@ var ChromeRenderer = class {
|
|
|
9533
9520
|
*/
|
|
9534
9521
|
paneDrawingsRange(ownModels, scene, isPricePane, vr) {
|
|
9535
9522
|
let dr = null;
|
|
9536
|
-
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(
|
|
9537
|
-
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(
|
|
9523
|
+
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
|
|
9524
|
+
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
|
|
9538
9525
|
return dr;
|
|
9539
9526
|
}
|
|
9540
9527
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
@@ -9551,7 +9538,6 @@ var ChromeRenderer = class {
|
|
|
9551
9538
|
const dataW = coords.width;
|
|
9552
9539
|
const dataH = coords.height;
|
|
9553
9540
|
this.axisTextColor = surface?.textColor ?? theme.textColor;
|
|
9554
|
-
this.labelTips = [];
|
|
9555
9541
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
9556
9542
|
ctx.clearRect(0, 0, fullW, fullH);
|
|
9557
9543
|
if (surface && (fullW > dataW || fullH > dataH)) {
|
|
@@ -9567,17 +9553,6 @@ var ChromeRenderer = class {
|
|
|
9567
9553
|
return;
|
|
9568
9554
|
}
|
|
9569
9555
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
9570
|
-
for (const pane of panes) {
|
|
9571
|
-
if (pane.collapsed) continue;
|
|
9572
|
-
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
9573
|
-
const sc = scene.scaleFor(m, pane);
|
|
9574
|
-
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
9575
|
-
this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
|
|
9576
|
-
}
|
|
9577
|
-
}
|
|
9578
|
-
if (pricePane) {
|
|
9579
|
-
for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
|
|
9580
|
-
}
|
|
9581
9556
|
if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
|
|
9582
9557
|
for (const m of scene.indicators.values()) {
|
|
9583
9558
|
if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
|
|
@@ -9593,25 +9568,6 @@ var ChromeRenderer = class {
|
|
|
9593
9568
|
this.canvas = null;
|
|
9594
9569
|
this.ctx = null;
|
|
9595
9570
|
}
|
|
9596
|
-
// ── Pine-drawing helpers (own vs force_overlay routing) ──
|
|
9597
|
-
ownDrawings(m) {
|
|
9598
|
-
return {
|
|
9599
|
-
lines: (m.lines ?? []).filter((d) => !d.overlay),
|
|
9600
|
-
boxes: (m.boxes ?? []).filter((d) => !d.overlay),
|
|
9601
|
-
labels: (m.labels ?? []).filter((d) => !d.overlay),
|
|
9602
|
-
polylines: (m.polylines ?? []).filter((d) => !d.overlay),
|
|
9603
|
-
linefills: (m.linefills ?? []).filter((d) => !d.overlay)
|
|
9604
|
-
};
|
|
9605
|
-
}
|
|
9606
|
-
overlayDrawings(m) {
|
|
9607
|
-
return {
|
|
9608
|
-
lines: (m.lines ?? []).filter((d) => d.overlay),
|
|
9609
|
-
boxes: (m.boxes ?? []).filter((d) => d.overlay),
|
|
9610
|
-
labels: (m.labels ?? []).filter((d) => d.overlay),
|
|
9611
|
-
polylines: (m.polylines ?? []).filter((d) => d.overlay),
|
|
9612
|
-
linefills: (m.linefills ?? []).filter((d) => d.overlay)
|
|
9613
|
-
};
|
|
9614
|
-
}
|
|
9615
9571
|
drawingsRange(set, vr, indexOffset = 0) {
|
|
9616
9572
|
this.drawScene.setSet(set, indexOffset);
|
|
9617
9573
|
if (this.drawScene.isEmpty()) return null;
|
|
@@ -9645,34 +9601,6 @@ var ChromeRenderer = class {
|
|
|
9645
9601
|
);
|
|
9646
9602
|
ctx.restore();
|
|
9647
9603
|
}
|
|
9648
|
-
renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
|
|
9649
|
-
this.drawScene.setSet(set, indexOffset);
|
|
9650
|
-
if (this.drawScene.isEmpty()) return;
|
|
9651
|
-
ctx.save();
|
|
9652
|
-
ctx.translate(0, pane.bounds.top);
|
|
9653
|
-
ctx.beginPath();
|
|
9654
|
-
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
9655
|
-
ctx.clip();
|
|
9656
|
-
this.drawScene.render(
|
|
9657
|
-
ctx,
|
|
9658
|
-
dataW,
|
|
9659
|
-
pane.bounds.height,
|
|
9660
|
-
(l) => coords.logicalToX(l),
|
|
9661
|
-
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
9662
|
-
);
|
|
9663
|
-
ctx.restore();
|
|
9664
|
-
for (const r of this.drawScene.labelTipRegions()) {
|
|
9665
|
-
this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
9666
|
-
}
|
|
9667
|
-
}
|
|
9668
|
-
/** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
|
|
9669
|
-
labelTooltipAt(x, y) {
|
|
9670
|
-
for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
|
|
9671
|
-
const r = this.labelTips[i];
|
|
9672
|
-
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
9673
|
-
}
|
|
9674
|
-
return null;
|
|
9675
|
-
}
|
|
9676
9604
|
// ── axes ──
|
|
9677
9605
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
9678
9606
|
ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
|
|
@@ -12632,6 +12560,22 @@ var DrawingInteraction = class {
|
|
|
12632
12560
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
12633
12561
|
return snapped;
|
|
12634
12562
|
}
|
|
12563
|
+
/**
|
|
12564
|
+
* Resolve a cursor pixel through the magnet and return the snapped pixel — the same
|
|
12565
|
+
* conversion drawing placement uses. Updates the snap-ring marker. The measure
|
|
12566
|
+
* ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
|
|
12567
|
+
*/
|
|
12568
|
+
snapCursor(x, y, mode) {
|
|
12569
|
+
const proj = this.deps.projector();
|
|
12570
|
+
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
12571
|
+
const point = this.resolve(x, y, paneId, mode);
|
|
12572
|
+
const sy = proj.yOf(point.price, paneId);
|
|
12573
|
+
return { x: proj.xOf(point.time), y: sy ?? y };
|
|
12574
|
+
}
|
|
12575
|
+
/** Drop the snap-ring marker (a transient mode ended without going through `up`). */
|
|
12576
|
+
clearSnapMarker() {
|
|
12577
|
+
this.snapAt = null;
|
|
12578
|
+
}
|
|
12635
12579
|
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
12636
12580
|
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
12637
12581
|
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
@@ -13991,17 +13935,17 @@ function glyphIcon(glyph) {
|
|
|
13991
13935
|
return textGlyph(String(glyph), 15);
|
|
13992
13936
|
}
|
|
13993
13937
|
function stampSizeIcon(size) {
|
|
13994
|
-
return textGlyph("\u25CF", (
|
|
13938
|
+
return textGlyph("\u25CF", (SIZE_PX[String(size)] ?? 13) + 4);
|
|
13995
13939
|
}
|
|
13996
13940
|
function sizeIcon(size) {
|
|
13997
13941
|
return textGlyph(String(size).charAt(0).toUpperCase(), 15);
|
|
13998
13942
|
}
|
|
13999
|
-
var
|
|
13943
|
+
var SIZE_PX = { small: 10, normal: 13, large: 16, huge: 20 };
|
|
14000
13944
|
function numbersSizeIcon(size) {
|
|
14001
|
-
return textGlyph("12", (
|
|
13945
|
+
return textGlyph("12", (SIZE_PX[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
|
|
14002
13946
|
}
|
|
14003
13947
|
function labelSizeIcon(size) {
|
|
14004
|
-
return textGlyph("T", (
|
|
13948
|
+
return textGlyph("T", (SIZE_PX[String(size)] ?? 13) + 2);
|
|
14005
13949
|
}
|
|
14006
13950
|
function capitalize(s) {
|
|
14007
13951
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -14037,30 +13981,39 @@ var MeasureOverlay = class {
|
|
|
14037
13981
|
isFinished() {
|
|
14038
13982
|
return this.state === "finished";
|
|
14039
13983
|
}
|
|
14040
|
-
/** A press: begin the measurement, or finish it on the second click.
|
|
14041
|
-
|
|
13984
|
+
/** A press: begin the measurement, or finish it on the second click.
|
|
13985
|
+
* `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
|
|
13986
|
+
* graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
|
|
13987
|
+
down(x, y, gx = x, gy = y) {
|
|
14042
13988
|
if (this.state === "measuring") {
|
|
14043
|
-
this.end = { x, y };
|
|
13989
|
+
this.end = { x: gx, y: gy };
|
|
14044
13990
|
this.state = "finished";
|
|
14045
13991
|
return;
|
|
14046
13992
|
}
|
|
14047
|
-
this.start = { x, y };
|
|
14048
|
-
this.end = { x, y };
|
|
13993
|
+
this.start = { x: gx, y: gy };
|
|
13994
|
+
this.end = { x: gx, y: gy };
|
|
14049
13995
|
this.pressX = x;
|
|
14050
13996
|
this.pressY = y;
|
|
14051
13997
|
this.state = "measuring";
|
|
14052
13998
|
}
|
|
13999
|
+
/** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
|
|
14053
14000
|
move(x, y) {
|
|
14054
14001
|
if (this.state === "measuring") this.end = { x, y };
|
|
14055
14002
|
}
|
|
14056
14003
|
/** A release: finish if the press was actually dragged (press-drag-release), else wait
|
|
14057
|
-
* for the second click (click-move-click).
|
|
14058
|
-
|
|
14004
|
+
* for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
|
|
14005
|
+
* are the graphic end (magnet-snapped when the magnet is on). */
|
|
14006
|
+
up(x, y, gx = x, gy = y) {
|
|
14059
14007
|
if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP3) {
|
|
14060
|
-
this.end = { x, y };
|
|
14008
|
+
this.end = { x: gx, y: gy };
|
|
14061
14009
|
this.state = "finished";
|
|
14062
14010
|
}
|
|
14063
14011
|
}
|
|
14012
|
+
/** Current graphic endpoints in media pixels, or null when idle. */
|
|
14013
|
+
points() {
|
|
14014
|
+
if (!this.start || !this.end) return null;
|
|
14015
|
+
return { start: this.start, end: this.end };
|
|
14016
|
+
}
|
|
14064
14017
|
clear() {
|
|
14065
14018
|
this.state = "idle";
|
|
14066
14019
|
this.start = null;
|
|
@@ -14437,11 +14390,13 @@ var UserDrawingController = class {
|
|
|
14437
14390
|
}
|
|
14438
14391
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
14439
14392
|
* gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
|
|
14440
|
-
*
|
|
14441
|
-
|
|
14393
|
+
* `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
|
|
14394
|
+
* false when a mode/tool is already active (the normal press path owns it). */
|
|
14395
|
+
beginMeasureAt(x, y, snap = "off") {
|
|
14442
14396
|
if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
|
|
14443
14397
|
this.withModeIntent(() => this.toggleMeasure());
|
|
14444
|
-
this.
|
|
14398
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
14399
|
+
this.measure.down(x, y, g.x, g.y);
|
|
14445
14400
|
this.render();
|
|
14446
14401
|
return true;
|
|
14447
14402
|
}
|
|
@@ -14463,7 +14418,8 @@ var UserDrawingController = class {
|
|
|
14463
14418
|
return;
|
|
14464
14419
|
}
|
|
14465
14420
|
if (this.measureMode) {
|
|
14466
|
-
this.
|
|
14421
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
14422
|
+
this.measure.down(x, y, g.x, g.y);
|
|
14467
14423
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
14468
14424
|
this.render();
|
|
14469
14425
|
return;
|
|
@@ -14476,7 +14432,8 @@ var UserDrawingController = class {
|
|
|
14476
14432
|
return;
|
|
14477
14433
|
}
|
|
14478
14434
|
if (this.measureMode) {
|
|
14479
|
-
this.
|
|
14435
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
14436
|
+
this.measure.move(g.x, g.y);
|
|
14480
14437
|
this.render();
|
|
14481
14438
|
return;
|
|
14482
14439
|
}
|
|
@@ -14494,13 +14451,14 @@ var UserDrawingController = class {
|
|
|
14494
14451
|
this.render();
|
|
14495
14452
|
}
|
|
14496
14453
|
}
|
|
14497
|
-
pointerUp(x, y) {
|
|
14454
|
+
pointerUp(x, y, snap = "off") {
|
|
14498
14455
|
if (this.eraserMode) {
|
|
14499
14456
|
this.erasing = false;
|
|
14500
14457
|
return;
|
|
14501
14458
|
}
|
|
14502
14459
|
if (this.measureMode) {
|
|
14503
|
-
this.
|
|
14460
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
14461
|
+
this.measure.up(x, y, g.x, g.y);
|
|
14504
14462
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
14505
14463
|
this.render();
|
|
14506
14464
|
return;
|
|
@@ -14545,6 +14503,7 @@ var UserDrawingController = class {
|
|
|
14545
14503
|
/** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
|
|
14546
14504
|
exitMeasure(clearGraphic = true) {
|
|
14547
14505
|
this.measureMode = false;
|
|
14506
|
+
this.interaction.clearSnapMarker();
|
|
14548
14507
|
if (clearGraphic) this.measure.clear();
|
|
14549
14508
|
this.toolbar.setMeasureActive(false);
|
|
14550
14509
|
this.render();
|
|
@@ -14685,15 +14644,28 @@ var UserDrawingController = class {
|
|
|
14685
14644
|
if (this.eraserMode) return "pointer";
|
|
14686
14645
|
return this.interaction.cursorAt(x, y);
|
|
14687
14646
|
}
|
|
14688
|
-
/** Right-click
|
|
14689
|
-
*
|
|
14690
|
-
*
|
|
14691
|
-
*
|
|
14647
|
+
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
14648
|
+
* placement or measurement, and also plain-disarms an armed-but-idle drawing
|
|
14649
|
+
* tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
|
|
14650
|
+
* stay-in-drawing-mode (where Escape would leave a drawing tool armed).
|
|
14651
|
+
* Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
|
|
14652
|
+
* whether the press was consumed; false lets the host's context menu open
|
|
14653
|
+
* normally. */
|
|
14692
14654
|
cancelPlacement() {
|
|
14693
|
-
if (
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14655
|
+
if (this.measureMode || this.eraserMode) {
|
|
14656
|
+
this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
|
|
14657
|
+
return true;
|
|
14658
|
+
}
|
|
14659
|
+
if (this.interaction.isPlacing()) {
|
|
14660
|
+
this.interaction.cancel();
|
|
14661
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
14662
|
+
return true;
|
|
14663
|
+
}
|
|
14664
|
+
if (this.activeTool != null) {
|
|
14665
|
+
this.emit({ kind: "arm", type: null });
|
|
14666
|
+
return true;
|
|
14667
|
+
}
|
|
14668
|
+
return false;
|
|
14697
14669
|
}
|
|
14698
14670
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
14699
14671
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
@@ -14719,6 +14691,10 @@ var UserDrawingController = class {
|
|
|
14719
14691
|
return true;
|
|
14720
14692
|
}
|
|
14721
14693
|
if (this.interaction.cancel()) return true;
|
|
14694
|
+
if (this.measureMode) {
|
|
14695
|
+
this.withModeIntent(() => this.exitMeasure());
|
|
14696
|
+
return true;
|
|
14697
|
+
}
|
|
14722
14698
|
if (this.selectedIds.size) {
|
|
14723
14699
|
this.clearSelection();
|
|
14724
14700
|
return true;
|
|
@@ -14976,6 +14952,315 @@ var UserDrawingController = class {
|
|
|
14976
14952
|
}
|
|
14977
14953
|
};
|
|
14978
14954
|
|
|
14955
|
+
// src/renderers/shared/TableOverlay.ts
|
|
14956
|
+
var SIZE_PX2 = {
|
|
14957
|
+
auto: 13,
|
|
14958
|
+
tiny: 10,
|
|
14959
|
+
small: 11,
|
|
14960
|
+
normal: 13,
|
|
14961
|
+
large: 16,
|
|
14962
|
+
huge: 20
|
|
14963
|
+
};
|
|
14964
|
+
function fontPxOf(size) {
|
|
14965
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX2.auto;
|
|
14966
|
+
return SIZE_PX2[size] ?? SIZE_PX2.auto;
|
|
14967
|
+
}
|
|
14968
|
+
function tableHasContent(t) {
|
|
14969
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
14970
|
+
}
|
|
14971
|
+
function mergeRenderPlan(t) {
|
|
14972
|
+
const span = /* @__PURE__ */ new Map();
|
|
14973
|
+
const omit = /* @__PURE__ */ new Set();
|
|
14974
|
+
for (const m of t.merges) {
|
|
14975
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
14976
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
14977
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
14978
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
14979
|
+
}
|
|
14980
|
+
}
|
|
14981
|
+
}
|
|
14982
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
14983
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
14984
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
14985
|
+
}
|
|
14986
|
+
}
|
|
14987
|
+
for (const key of span.keys()) omit.delete(key);
|
|
14988
|
+
return { span, omit };
|
|
14989
|
+
}
|
|
14990
|
+
|
|
14991
|
+
// src/renderers/shared/TableCanvasRenderer.ts
|
|
14992
|
+
var PAD_X = 6;
|
|
14993
|
+
var PAD_Y = 2;
|
|
14994
|
+
var MARGIN = 6;
|
|
14995
|
+
var LINE_HEIGHT = 1.2;
|
|
14996
|
+
function paintTable(ctx, t, args, tips) {
|
|
14997
|
+
if (!tableHasContent(t)) return;
|
|
14998
|
+
const layout = layoutTable(ctx, t, args);
|
|
14999
|
+
if (!layout || layout.w <= 0 || layout.h <= 0) return;
|
|
15000
|
+
const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
|
|
15001
|
+
const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
|
|
15002
|
+
const x0 = x + fw;
|
|
15003
|
+
const y0 = y + fw;
|
|
15004
|
+
if (t.bgColor) {
|
|
15005
|
+
ctx.fillStyle = t.bgColor;
|
|
15006
|
+
ctx.fillRect(x0, y0, layout.w, layout.h);
|
|
15007
|
+
}
|
|
15008
|
+
if (fw > 0 && t.frameColor) {
|
|
15009
|
+
ctx.strokeStyle = t.frameColor;
|
|
15010
|
+
ctx.lineWidth = fw;
|
|
15011
|
+
ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
|
|
15012
|
+
}
|
|
15013
|
+
const colX = [0];
|
|
15014
|
+
for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
|
|
15015
|
+
const rowY = [0];
|
|
15016
|
+
for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
|
|
15017
|
+
const prevBaseline = ctx.textBaseline;
|
|
15018
|
+
const prevAlign = ctx.textAlign;
|
|
15019
|
+
ctx.textBaseline = "middle";
|
|
15020
|
+
for (const box of layout.boxes) {
|
|
15021
|
+
const rx = x0 + colX[box.c];
|
|
15022
|
+
const ry = y0 + rowY[box.r];
|
|
15023
|
+
const rw = colX[box.c + box.cs] - colX[box.c];
|
|
15024
|
+
const rh = rowY[box.r + box.rs] - rowY[box.r];
|
|
15025
|
+
const cell = box.cell;
|
|
15026
|
+
if (cell.bgColor) {
|
|
15027
|
+
ctx.fillStyle = cell.bgColor;
|
|
15028
|
+
ctx.fillRect(rx, ry, rw, rh);
|
|
15029
|
+
}
|
|
15030
|
+
const text = cell.text ?? "";
|
|
15031
|
+
if (text.length > 0) {
|
|
15032
|
+
const px = fontPxOf(cell.textSize);
|
|
15033
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
15034
|
+
ctx.fillStyle = cell.textColor ?? args.theme.textColor;
|
|
15035
|
+
const lines = text.split("\n");
|
|
15036
|
+
const blockH = lines.length * px * LINE_HEIGHT;
|
|
15037
|
+
const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
|
|
15038
|
+
const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
|
|
15039
|
+
ctx.textAlign = cell.hAlign;
|
|
15040
|
+
lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
|
|
15041
|
+
}
|
|
15042
|
+
if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
|
|
15043
|
+
}
|
|
15044
|
+
ctx.textBaseline = prevBaseline;
|
|
15045
|
+
ctx.textAlign = prevAlign;
|
|
15046
|
+
if (t.borderColor && t.borderWidth > 0) {
|
|
15047
|
+
ctx.strokeStyle = t.borderColor;
|
|
15048
|
+
ctx.lineWidth = t.borderWidth;
|
|
15049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15050
|
+
ctx.beginPath();
|
|
15051
|
+
const edge = (ax, ay, bx, by) => {
|
|
15052
|
+
const key = `${ax},${ay},${bx},${by}`;
|
|
15053
|
+
if (seen.has(key)) return;
|
|
15054
|
+
seen.add(key);
|
|
15055
|
+
ctx.moveTo(ax, ay);
|
|
15056
|
+
ctx.lineTo(bx, by);
|
|
15057
|
+
};
|
|
15058
|
+
for (const box of layout.boxes) {
|
|
15059
|
+
const l = Math.round(x0 + colX[box.c]);
|
|
15060
|
+
const r = Math.round(x0 + colX[box.c + box.cs]);
|
|
15061
|
+
const tp = Math.round(y0 + rowY[box.r]);
|
|
15062
|
+
const bt = Math.round(y0 + rowY[box.r + box.rs]);
|
|
15063
|
+
edge(l, tp, r, tp);
|
|
15064
|
+
edge(l, bt, r, bt);
|
|
15065
|
+
edge(l, tp, l, bt);
|
|
15066
|
+
edge(r, tp, r, bt);
|
|
15067
|
+
}
|
|
15068
|
+
ctx.stroke();
|
|
15069
|
+
}
|
|
15070
|
+
}
|
|
15071
|
+
function layoutTable(ctx, t, args) {
|
|
15072
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
15073
|
+
const colW = new Array(t.columns).fill(0);
|
|
15074
|
+
const rowH = new Array(t.rows).fill(0);
|
|
15075
|
+
const boxes = [];
|
|
15076
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
15077
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
15078
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
15079
|
+
const cell = t.cells[r]?.[c];
|
|
15080
|
+
if (cell == null) continue;
|
|
15081
|
+
const sp = span.get(`${r}:${c}`);
|
|
15082
|
+
boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
|
|
15083
|
+
}
|
|
15084
|
+
}
|
|
15085
|
+
if (boxes.length === 0) return null;
|
|
15086
|
+
const sizeOf = (cell) => {
|
|
15087
|
+
const px = fontPxOf(cell.textSize);
|
|
15088
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
15089
|
+
const lines = (cell.text ?? "").split("\n");
|
|
15090
|
+
let maxW = 0;
|
|
15091
|
+
for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
|
|
15092
|
+
let w2 = Math.ceil(maxW) + 2 * PAD_X;
|
|
15093
|
+
let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
|
|
15094
|
+
if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
|
|
15095
|
+
if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
|
|
15096
|
+
return { w: w2, h: h2 };
|
|
15097
|
+
};
|
|
15098
|
+
const spanning = [];
|
|
15099
|
+
for (const box of boxes) {
|
|
15100
|
+
const { w: w2, h: h2 } = sizeOf(box.cell);
|
|
15101
|
+
if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
|
|
15102
|
+
if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
|
|
15103
|
+
if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
|
|
15104
|
+
}
|
|
15105
|
+
for (const { box, w: w2, h: h2 } of spanning) {
|
|
15106
|
+
if (box.cs > 1) {
|
|
15107
|
+
let sum = 0;
|
|
15108
|
+
for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
|
|
15109
|
+
if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
|
|
15110
|
+
}
|
|
15111
|
+
if (box.rs > 1) {
|
|
15112
|
+
let sum = 0;
|
|
15113
|
+
for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
|
|
15114
|
+
if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
|
|
15115
|
+
}
|
|
15116
|
+
}
|
|
15117
|
+
let w = 0;
|
|
15118
|
+
for (const cw of colW) w += cw;
|
|
15119
|
+
let h = 0;
|
|
15120
|
+
for (const rh of rowH) h += rh;
|
|
15121
|
+
return { colW, rowH, w, h, boxes };
|
|
15122
|
+
}
|
|
15123
|
+
function anchorOrigin(position, totalW, totalH, args) {
|
|
15124
|
+
let y;
|
|
15125
|
+
if (position.startsWith("top")) y = MARGIN;
|
|
15126
|
+
else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
|
|
15127
|
+
else y = args.paneHeight / 2 - totalH / 2;
|
|
15128
|
+
let x;
|
|
15129
|
+
if (position.endsWith("left")) x = MARGIN;
|
|
15130
|
+
else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
|
|
15131
|
+
else x = args.plotWidth / 2 - totalW / 2;
|
|
15132
|
+
return { x, y };
|
|
15133
|
+
}
|
|
15134
|
+
function cellFont(cell, px, theme) {
|
|
15135
|
+
const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
|
|
15136
|
+
return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
|
|
15137
|
+
}
|
|
15138
|
+
|
|
15139
|
+
// src/renderers/native/drawings/IndicatorDrawingSlices.ts
|
|
15140
|
+
function indicatorSliceKey(z, boundaries) {
|
|
15141
|
+
return boundaries.find((b) => b > z) ?? Infinity;
|
|
15142
|
+
}
|
|
15143
|
+
var IndicatorDrawingSlices = class {
|
|
15144
|
+
constructor() {
|
|
15145
|
+
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
15146
|
+
/** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
|
|
15147
|
+
this.sliceCache = /* @__PURE__ */ new Map();
|
|
15148
|
+
/** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
|
|
15149
|
+
this.tips = [];
|
|
15150
|
+
}
|
|
15151
|
+
/**
|
|
15152
|
+
* Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
|
|
15153
|
+
* canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
|
|
15154
|
+
* Runs from the renderer's data paint, just before the backend composites the scene.
|
|
15155
|
+
*/
|
|
15156
|
+
prepare(scene, coords, theme, ref) {
|
|
15157
|
+
this.tips = [];
|
|
15158
|
+
const out = /* @__PURE__ */ new Map();
|
|
15159
|
+
if (ref.width === 0 || ref.height === 0) {
|
|
15160
|
+
this.sliceCache.clear();
|
|
15161
|
+
return out;
|
|
15162
|
+
}
|
|
15163
|
+
this.drawScene.setDeps({
|
|
15164
|
+
timeToLogical: (ms) => coords.timeToLogical(ms),
|
|
15165
|
+
barAt: (logical) => {
|
|
15166
|
+
const b = scene.bars[Math.round(logical)];
|
|
15167
|
+
return b ? { high: b.high, low: b.low } : null;
|
|
15168
|
+
},
|
|
15169
|
+
theme
|
|
15170
|
+
});
|
|
15171
|
+
const dpr = coords.dpr;
|
|
15172
|
+
const dataW = coords.width;
|
|
15173
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
15174
|
+
const add = (paneId, beforeZ, entry) => {
|
|
15175
|
+
const key = `${paneId}|${beforeZ}`;
|
|
15176
|
+
const bucket = buckets.get(key);
|
|
15177
|
+
if (bucket) bucket.entries.push(entry);
|
|
15178
|
+
else buckets.set(key, { paneId, beforeZ, entries: [entry] });
|
|
15179
|
+
};
|
|
15180
|
+
for (const pane of scene.orderedPanes()) {
|
|
15181
|
+
if (pane.collapsed) continue;
|
|
15182
|
+
const boundaries = scene.seriesBoundaries(pane.id);
|
|
15183
|
+
for (const m of scene.orderedIndicatorsForPane(pane.id)) {
|
|
15184
|
+
const set = modelDrawingSet(m, false);
|
|
15185
|
+
const tables = (m.tables ?? []).filter((t) => !t.overlay);
|
|
15186
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
15187
|
+
const sc = scene.scaleFor(m, pane);
|
|
15188
|
+
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
15189
|
+
const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
|
|
15190
|
+
add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
|
|
15191
|
+
}
|
|
15192
|
+
if (pane.kind === "price") {
|
|
15193
|
+
for (const m of scene.indicators.values()) {
|
|
15194
|
+
const set = modelDrawingSet(m, true);
|
|
15195
|
+
const tables = (m.tables ?? []).filter((t) => t.overlay === true);
|
|
15196
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
15197
|
+
add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
|
|
15198
|
+
}
|
|
15199
|
+
}
|
|
15200
|
+
}
|
|
15201
|
+
for (const [key, { paneId, beforeZ, entries }] of buckets) {
|
|
15202
|
+
let canvas = this.sliceCache.get(key);
|
|
15203
|
+
if (!canvas) {
|
|
15204
|
+
canvas = document.createElement("canvas");
|
|
15205
|
+
this.sliceCache.set(key, canvas);
|
|
15206
|
+
}
|
|
15207
|
+
if (canvas.width !== ref.width || canvas.height !== ref.height) {
|
|
15208
|
+
canvas.width = ref.width;
|
|
15209
|
+
canvas.height = ref.height;
|
|
15210
|
+
}
|
|
15211
|
+
const ctx = canvas.getContext("2d");
|
|
15212
|
+
if (!ctx) continue;
|
|
15213
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
15214
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
15215
|
+
for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
|
|
15216
|
+
const slices = out.get(paneId) ?? [];
|
|
15217
|
+
slices.push({ beforeZ, canvas });
|
|
15218
|
+
out.set(paneId, slices);
|
|
15219
|
+
}
|
|
15220
|
+
for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
|
|
15221
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
15222
|
+
return out;
|
|
15223
|
+
}
|
|
15224
|
+
paintEntry(ctx, e, coords, dataW, theme) {
|
|
15225
|
+
const { pane } = e;
|
|
15226
|
+
const paneTips = [];
|
|
15227
|
+
ctx.save();
|
|
15228
|
+
ctx.translate(0, pane.bounds.top);
|
|
15229
|
+
ctx.beginPath();
|
|
15230
|
+
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
15231
|
+
ctx.clip();
|
|
15232
|
+
this.drawScene.setSet(e.set, e.indexOffset);
|
|
15233
|
+
this.drawScene.render(
|
|
15234
|
+
ctx,
|
|
15235
|
+
dataW,
|
|
15236
|
+
pane.bounds.height,
|
|
15237
|
+
(l) => coords.logicalToX(l),
|
|
15238
|
+
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
15239
|
+
);
|
|
15240
|
+
paneTips.push(...this.drawScene.labelTipRegions());
|
|
15241
|
+
for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
|
|
15242
|
+
ctx.restore();
|
|
15243
|
+
for (const r of paneTips) {
|
|
15244
|
+
this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
15245
|
+
}
|
|
15246
|
+
}
|
|
15247
|
+
/** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
|
|
15248
|
+
labelTooltipAt(x, y) {
|
|
15249
|
+
for (let i = this.tips.length - 1; i >= 0; i -= 1) {
|
|
15250
|
+
const r = this.tips[i];
|
|
15251
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
15252
|
+
}
|
|
15253
|
+
return null;
|
|
15254
|
+
}
|
|
15255
|
+
};
|
|
15256
|
+
function mergeSlices(indicator, user) {
|
|
15257
|
+
const out = /* @__PURE__ */ new Map();
|
|
15258
|
+
for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
|
|
15259
|
+
for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
|
|
15260
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
15261
|
+
return out;
|
|
15262
|
+
}
|
|
15263
|
+
|
|
14979
15264
|
// src/renderers/native/drawings/Projector.ts
|
|
14980
15265
|
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
14981
15266
|
return {
|
|
@@ -15829,6 +16114,11 @@ var NativeRenderer = class {
|
|
|
15829
16114
|
this.vpvrRenderer = new VpvrRenderer();
|
|
15830
16115
|
this.resizeObserver = null;
|
|
15831
16116
|
this.dprMedia = null;
|
|
16117
|
+
/** Plot size in INTEGER device px, as last reported by the resize observer's
|
|
16118
|
+
* device-pixel-content-box — the browser's own statement of how many device pixels
|
|
16119
|
+
* it paints the plot into. `null` until the first report or where the box type is
|
|
16120
|
+
* unsupported (WebKit); syncSize then falls back to rounding the client rect. */
|
|
16121
|
+
this.plotDeviceSize = null;
|
|
15832
16122
|
this.coords = new CoordinateSystem();
|
|
15833
16123
|
this.scene = new SceneGraph();
|
|
15834
16124
|
// chosen at mount (WebGL2 if available, else canvas2d)
|
|
@@ -15836,6 +16126,8 @@ var NativeRenderer = class {
|
|
|
15836
16126
|
this.glowAmount = 0;
|
|
15837
16127
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
15838
16128
|
this.chrome = new ChromeRenderer();
|
|
16129
|
+
/** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
|
|
16130
|
+
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
15839
16131
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
15840
16132
|
this.labelTooltip = null;
|
|
15841
16133
|
this.crosshairLayer = new CrosshairRenderer();
|
|
@@ -15984,7 +16276,6 @@ var NativeRenderer = class {
|
|
|
15984
16276
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
15985
16277
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
15986
16278
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
15987
|
-
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
15988
16279
|
this.name = "native";
|
|
15989
16280
|
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
15990
16281
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
@@ -16298,7 +16589,6 @@ var NativeRenderer = class {
|
|
|
16298
16589
|
* hidden) on clear so a re-show picks up the current theme.
|
|
16299
16590
|
*/
|
|
16300
16591
|
setLoading(loading) {
|
|
16301
|
-
for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
|
|
16302
16592
|
if (!loading || !this.wrapper) {
|
|
16303
16593
|
this.loadingEl?.remove();
|
|
16304
16594
|
this.loadingEl = null;
|
|
@@ -16937,13 +17227,13 @@ var NativeRenderer = class {
|
|
|
16937
17227
|
resetView: () => this.resetView(),
|
|
16938
17228
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
16939
17229
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
16940
|
-
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
17230
|
+
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
16941
17231
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
16942
17232
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
16943
17233
|
drawingsSnapMode: () => this.snapMode,
|
|
16944
17234
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
16945
17235
|
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
16946
|
-
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
17236
|
+
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
16947
17237
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
16948
17238
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
16949
17239
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
@@ -16960,7 +17250,7 @@ var NativeRenderer = class {
|
|
|
16960
17250
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
16961
17251
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
16962
17252
|
theme: () => this.chromeTheme(),
|
|
16963
|
-
lookup: (x, y) => this.
|
|
17253
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
16964
17254
|
});
|
|
16965
17255
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
16966
17256
|
projector: () => this.drawingProjector(),
|
|
@@ -17045,6 +17335,7 @@ var NativeRenderer = class {
|
|
|
17045
17335
|
this.emitPaneAction({ type: "maximize", paneId, maximized });
|
|
17046
17336
|
}
|
|
17047
17337
|
});
|
|
17338
|
+
this.paneControls.setSuspended(this.layoutMode === "mobile");
|
|
17048
17339
|
this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
|
|
17049
17340
|
panes: () => this.axisScaleViews(),
|
|
17050
17341
|
rightAxis: () => this.rightAxisW,
|
|
@@ -17054,8 +17345,19 @@ var NativeRenderer = class {
|
|
|
17054
17345
|
if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
|
|
17055
17346
|
}
|
|
17056
17347
|
});
|
|
17057
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
17348
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
17349
|
+
for (const e of entries) {
|
|
17350
|
+
if (e.target !== this.plot) continue;
|
|
17351
|
+
const s = e.devicePixelContentBoxSize?.[0];
|
|
17352
|
+
if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
|
|
17353
|
+
}
|
|
17354
|
+
this.resize();
|
|
17355
|
+
});
|
|
17058
17356
|
this.resizeObserver.observe(this.wrapper);
|
|
17357
|
+
try {
|
|
17358
|
+
this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
|
|
17359
|
+
} catch {
|
|
17360
|
+
}
|
|
17059
17361
|
this.watchDpr();
|
|
17060
17362
|
this.syncSize();
|
|
17061
17363
|
}
|
|
@@ -17180,8 +17482,6 @@ var NativeRenderer = class {
|
|
|
17180
17482
|
this.inputsUI?.destroy();
|
|
17181
17483
|
this.paneControls?.destroy();
|
|
17182
17484
|
this.axisScaleButtons?.destroy();
|
|
17183
|
-
for (const overlay of this.tableOverlays.values()) overlay.destroy();
|
|
17184
|
-
this.tableOverlays.clear();
|
|
17185
17485
|
this.resizeObserver?.disconnect();
|
|
17186
17486
|
this.resizeObserver = null;
|
|
17187
17487
|
this.dprMedia?.removeEventListener("change", this.onDprChange);
|
|
@@ -17210,6 +17510,7 @@ var NativeRenderer = class {
|
|
|
17210
17510
|
this.attributionEl = null;
|
|
17211
17511
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
17212
17512
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
17513
|
+
this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
|
|
17213
17514
|
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
17214
17515
|
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
17215
17516
|
this.mountContainer = null;
|
|
@@ -17247,7 +17548,8 @@ var NativeRenderer = class {
|
|
|
17247
17548
|
}
|
|
17248
17549
|
const skipFit = opts?.preserveView === true && this.didInitialFit;
|
|
17249
17550
|
if (this.coords.width > 0 && !skipFit) {
|
|
17250
|
-
this.
|
|
17551
|
+
if (this.didInitialFit) this.reframeKeepZoom();
|
|
17552
|
+
else this.fitContent();
|
|
17251
17553
|
this.didInitialFit = true;
|
|
17252
17554
|
}
|
|
17253
17555
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
@@ -17302,7 +17604,6 @@ var NativeRenderer = class {
|
|
|
17302
17604
|
ensurePane(pane) {
|
|
17303
17605
|
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
|
|
17304
17606
|
this.layoutPanes();
|
|
17305
|
-
this.repositionTables();
|
|
17306
17607
|
this.paneControls?.refresh();
|
|
17307
17608
|
this.scheduler.invalidate(4 /* Full */);
|
|
17308
17609
|
}
|
|
@@ -17324,17 +17625,14 @@ var NativeRenderer = class {
|
|
|
17324
17625
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
17325
17626
|
this.inputsUI.setPane(handle.id, paneId);
|
|
17326
17627
|
this.refreshAnchorOffset(model);
|
|
17327
|
-
this.syncTables(model);
|
|
17328
17628
|
this.refreshAxisWidth();
|
|
17329
17629
|
this.layoutPanes();
|
|
17330
|
-
this.repositionTables();
|
|
17331
17630
|
this.paneControls?.refresh();
|
|
17332
17631
|
this.scheduler.invalidate(4 /* Full */);
|
|
17333
17632
|
}
|
|
17334
17633
|
orderPanes(orderedIds) {
|
|
17335
17634
|
this.scene.orderPanes(orderedIds);
|
|
17336
17635
|
this.layoutPanes();
|
|
17337
|
-
this.repositionTables();
|
|
17338
17636
|
this.paneControls?.refresh();
|
|
17339
17637
|
this.scheduler.invalidate(4 /* Full */);
|
|
17340
17638
|
}
|
|
@@ -17343,7 +17641,6 @@ var NativeRenderer = class {
|
|
|
17343
17641
|
if (!pane || pane.collapsed === collapsed) return;
|
|
17344
17642
|
pane.collapsed = collapsed;
|
|
17345
17643
|
this.layoutPanes();
|
|
17346
|
-
this.repositionTables();
|
|
17347
17644
|
this.paneControls?.refresh();
|
|
17348
17645
|
this.scheduler.invalidate(4 /* Full */);
|
|
17349
17646
|
}
|
|
@@ -17351,7 +17648,6 @@ var NativeRenderer = class {
|
|
|
17351
17648
|
if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
|
|
17352
17649
|
this.maximizedPaneId = paneId;
|
|
17353
17650
|
this.layoutPanes();
|
|
17354
|
-
this.repositionTables();
|
|
17355
17651
|
this.paneControls?.refresh();
|
|
17356
17652
|
this.scheduler.invalidate(4 /* Full */);
|
|
17357
17653
|
}
|
|
@@ -17438,7 +17734,6 @@ var NativeRenderer = class {
|
|
|
17438
17734
|
native: !!model.native,
|
|
17439
17735
|
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
17440
17736
|
});
|
|
17441
|
-
this.syncTables(model);
|
|
17442
17737
|
if (model.native?.type === "volume") {
|
|
17443
17738
|
this.volumeActive = true;
|
|
17444
17739
|
this.volumeHidden = false;
|
|
@@ -17462,7 +17757,6 @@ var NativeRenderer = class {
|
|
|
17462
17757
|
}
|
|
17463
17758
|
}
|
|
17464
17759
|
applyPatch(model, patch);
|
|
17465
|
-
this.syncTables(model);
|
|
17466
17760
|
this.scheduler.invalidate(3 /* Light */);
|
|
17467
17761
|
}
|
|
17468
17762
|
removeIndicator(handle) {
|
|
@@ -17481,8 +17775,6 @@ var NativeRenderer = class {
|
|
|
17481
17775
|
this.scene.forgetAnchorOffset(handle.id);
|
|
17482
17776
|
this.scene.dropIndicatorScale(handle.id);
|
|
17483
17777
|
this.inputsUI.remove(handle.id);
|
|
17484
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
17485
|
-
this.tableOverlays.delete(handle.id);
|
|
17486
17778
|
this.refreshAxisWidth();
|
|
17487
17779
|
this.paneControls?.refresh();
|
|
17488
17780
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -17526,8 +17818,6 @@ var NativeRenderer = class {
|
|
|
17526
17818
|
}
|
|
17527
17819
|
if (!visible) {
|
|
17528
17820
|
this.scene.indicators.delete(handle.id);
|
|
17529
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
17530
|
-
this.tableOverlays.delete(handle.id);
|
|
17531
17821
|
}
|
|
17532
17822
|
this.inputsUI.setVisible(handle.id, visible);
|
|
17533
17823
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -17573,6 +17863,7 @@ var NativeRenderer = class {
|
|
|
17573
17863
|
this.userDrawings?.setLayoutMode(mode);
|
|
17574
17864
|
this.settingsDialog?.setLayoutMode(mode);
|
|
17575
17865
|
this.inputsUI?.setLayoutMode(mode);
|
|
17866
|
+
this.paneControls?.setSuspended(mode === "mobile");
|
|
17576
17867
|
if (this.scrollButton) {
|
|
17577
17868
|
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
17578
17869
|
this.scrollButton.style.width = `${px}px`;
|
|
@@ -17906,6 +18197,7 @@ var NativeRenderer = class {
|
|
|
17906
18197
|
this.scaleDragHeight = res.height;
|
|
17907
18198
|
this.scaleDragStart = { ...res.holder.scale };
|
|
17908
18199
|
res.holder.manualScale = { ...res.holder.scale };
|
|
18200
|
+
this.axisScaleButtons?.reposition();
|
|
17909
18201
|
this.scheduler.invalidate(4 /* Full */);
|
|
17910
18202
|
}
|
|
17911
18203
|
/** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
|
|
@@ -17937,6 +18229,7 @@ var NativeRenderer = class {
|
|
|
17937
18229
|
const res = this.resolveScaleHolder(x, y);
|
|
17938
18230
|
if (!res) return;
|
|
17939
18231
|
res.holder.manualScale = null;
|
|
18232
|
+
this.axisScaleButtons?.reposition();
|
|
17940
18233
|
this.scheduler.invalidate(4 /* Full */);
|
|
17941
18234
|
}
|
|
17942
18235
|
/**
|
|
@@ -17968,7 +18261,6 @@ var NativeRenderer = class {
|
|
|
17968
18261
|
/** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
|
|
17969
18262
|
afterPaneLayoutChange() {
|
|
17970
18263
|
this.layoutPanes();
|
|
17971
|
-
this.repositionTables();
|
|
17972
18264
|
this.paneControls?.refresh();
|
|
17973
18265
|
this.scheduler.invalidate(4 /* Full */);
|
|
17974
18266
|
}
|
|
@@ -18049,7 +18341,6 @@ var NativeRenderer = class {
|
|
|
18049
18341
|
above.heightWeight = next.above;
|
|
18050
18342
|
below.heightWeight = next.below;
|
|
18051
18343
|
this.layoutPanes();
|
|
18052
|
-
this.repositionTables();
|
|
18053
18344
|
this.scheduler.invalidate(4 /* Full */);
|
|
18054
18345
|
}
|
|
18055
18346
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
@@ -18064,7 +18355,6 @@ var NativeRenderer = class {
|
|
|
18064
18355
|
above.heightWeight = half;
|
|
18065
18356
|
below.heightWeight = half;
|
|
18066
18357
|
this.layoutPanes();
|
|
18067
|
-
this.repositionTables();
|
|
18068
18358
|
this.scheduler.invalidate(4 /* Full */);
|
|
18069
18359
|
}
|
|
18070
18360
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -18343,7 +18633,10 @@ var NativeRenderer = class {
|
|
|
18343
18633
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
18344
18634
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
18345
18635
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
18346
|
-
this.scene.drawingSlices =
|
|
18636
|
+
this.scene.drawingSlices = mergeSlices(
|
|
18637
|
+
this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
|
|
18638
|
+
this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
|
|
18639
|
+
);
|
|
18347
18640
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
18348
18641
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
18349
18642
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
@@ -18574,6 +18867,20 @@ var NativeRenderer = class {
|
|
|
18574
18867
|
this.coords.setViewport(v);
|
|
18575
18868
|
this.targetBarSpacing = v.barSpacing;
|
|
18576
18869
|
}
|
|
18870
|
+
/** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
|
|
18871
|
+
* zoom (bar spacing), re-anchor the newest bars at the default right offset.
|
|
18872
|
+
* `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
|
|
18873
|
+
* head may still be backfilling toward the previous depth, and raising the spacing
|
|
18874
|
+
* to its temporary bar count would lose the zoom this exists to keep. */
|
|
18875
|
+
reframeKeepZoom() {
|
|
18876
|
+
this.animator?.stop();
|
|
18877
|
+
this.panVelocity = 0;
|
|
18878
|
+
for (const pane of this.scene.panes.values()) pane.manualScale = null;
|
|
18879
|
+
for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
|
|
18880
|
+
const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
|
|
18881
|
+
this.coords.setViewport(v);
|
|
18882
|
+
this.targetBarSpacing = v.barSpacing;
|
|
18883
|
+
}
|
|
18577
18884
|
paneBoundsFor(paneId) {
|
|
18578
18885
|
const p = this.scene.panes.get(paneId);
|
|
18579
18886
|
return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
|
|
@@ -18781,27 +19088,6 @@ var NativeRenderer = class {
|
|
|
18781
19088
|
}
|
|
18782
19089
|
return maxVol;
|
|
18783
19090
|
}
|
|
18784
|
-
/** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
|
|
18785
|
-
syncTables(model) {
|
|
18786
|
-
const tables = model.tables ?? [];
|
|
18787
|
-
let overlay = this.tableOverlays.get(model.id);
|
|
18788
|
-
if (tables.length === 0) {
|
|
18789
|
-
if (overlay) {
|
|
18790
|
-
overlay.destroy();
|
|
18791
|
-
this.tableOverlays.delete(model.id);
|
|
18792
|
-
}
|
|
18793
|
-
return;
|
|
18794
|
-
}
|
|
18795
|
-
if (!overlay) {
|
|
18796
|
-
overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
|
|
18797
|
-
overlay.setVisible(this.loadingEl === null);
|
|
18798
|
-
this.tableOverlays.set(model.id, overlay);
|
|
18799
|
-
}
|
|
18800
|
-
overlay.update(tables);
|
|
18801
|
-
}
|
|
18802
|
-
repositionTables() {
|
|
18803
|
-
for (const overlay of this.tableOverlays.values()) overlay.reposition();
|
|
18804
|
-
}
|
|
18805
19091
|
layoutPanes() {
|
|
18806
19092
|
const panes = this.scene.orderedPanes();
|
|
18807
19093
|
const dataHeight = this.coords.height;
|
|
@@ -18852,6 +19138,7 @@ var NativeRenderer = class {
|
|
|
18852
19138
|
const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
|
|
18853
19139
|
const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
|
|
18854
19140
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
|
|
19141
|
+
this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
|
|
18855
19142
|
this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
|
|
18856
19143
|
if (this.scrollButton) {
|
|
18857
19144
|
this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
|
|
@@ -18944,30 +19231,33 @@ var NativeRenderer = class {
|
|
|
18944
19231
|
if (w <= 0 || h <= 0) return;
|
|
18945
19232
|
const dpr = window.devicePixelRatio || 1;
|
|
18946
19233
|
this.plot.style.left = `${this.toolbarGutter}px`;
|
|
18947
|
-
const
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
this.
|
|
18964
|
-
this.
|
|
18965
|
-
this.
|
|
18966
|
-
|
|
19234
|
+
const rect = this.plot.getBoundingClientRect();
|
|
19235
|
+
let bw = Math.max(1, Math.round(rect.width * dpr));
|
|
19236
|
+
let bh = Math.max(1, Math.round(rect.height * dpr));
|
|
19237
|
+
const dev = this.plotDeviceSize;
|
|
19238
|
+
if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
|
|
19239
|
+
bw = Math.max(1, dev.width);
|
|
19240
|
+
bh = Math.max(1, dev.height);
|
|
19241
|
+
}
|
|
19242
|
+
const pw = bw / dpr;
|
|
19243
|
+
const ph = bh / dpr;
|
|
19244
|
+
const size = (canvas) => {
|
|
19245
|
+
canvas.width = bw;
|
|
19246
|
+
canvas.height = bh;
|
|
19247
|
+
canvas.style.width = `${pw}px`;
|
|
19248
|
+
canvas.style.height = `${ph}px`;
|
|
19249
|
+
};
|
|
19250
|
+
size(this.dataCanvas);
|
|
19251
|
+
size(this.backdropCanvas);
|
|
19252
|
+
size(this.volumeCanvas);
|
|
19253
|
+
for (const l of this.extLayers) size(l.canvas);
|
|
19254
|
+
size(this.vpvrCanvas);
|
|
19255
|
+
size(this.chromeCanvas);
|
|
19256
|
+
size(this.drawingsCanvas);
|
|
19257
|
+
size(this.cursorCanvas);
|
|
18967
19258
|
this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
|
|
18968
19259
|
this.scene.crosshair = null;
|
|
18969
19260
|
this.layoutPanes();
|
|
18970
|
-
this.repositionTables();
|
|
18971
19261
|
this.userDrawings?.onResize();
|
|
18972
19262
|
if (!this.didInitialFit && this.coords.barCount > 0) {
|
|
18973
19263
|
this.fitContent();
|
|
@@ -19157,6 +19447,12 @@ var IndicatorHandleImpl = class {
|
|
|
19157
19447
|
get visible() {
|
|
19158
19448
|
return this.visibleState;
|
|
19159
19449
|
}
|
|
19450
|
+
inputValues() {
|
|
19451
|
+
return this.controller.inputValuesOf(this.id);
|
|
19452
|
+
}
|
|
19453
|
+
propValues() {
|
|
19454
|
+
return this.controller.propValuesOf(this.id);
|
|
19455
|
+
}
|
|
19160
19456
|
setInput(key, value) {
|
|
19161
19457
|
this.controller.applyInputs(this.id, { [key]: value });
|
|
19162
19458
|
}
|
|
@@ -19296,6 +19592,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
19296
19592
|
var PREVIEW_BARS = 300;
|
|
19297
19593
|
var SINGLE_LOAD_BARS = 5e3;
|
|
19298
19594
|
var CHUNK_BARS = 1e4;
|
|
19595
|
+
var FIRST_PAINT_BARS = 100;
|
|
19299
19596
|
var GAP_FACTOR = 1.5;
|
|
19300
19597
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
19301
19598
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -19359,6 +19656,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19359
19656
|
/** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
|
|
19360
19657
|
* bumped by init(), setMarket() and destroy(). */
|
|
19361
19658
|
this.generation = 0;
|
|
19659
|
+
/** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
|
|
19660
|
+
* abandoned stream left polling to its own budget starves the browser's per-host
|
|
19661
|
+
* connection pool, and the NEXT symbol's very first fetch with it (measured). */
|
|
19662
|
+
this.progressiveAbort = null;
|
|
19362
19663
|
/** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
|
|
19363
19664
|
this.supersedeWaiters = [];
|
|
19364
19665
|
/** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
|
|
@@ -19501,6 +19802,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19501
19802
|
* superseded setMarket awaiters so their promises resolve instead of hanging. */
|
|
19502
19803
|
bumpGeneration() {
|
|
19503
19804
|
const gen = ++this.generation;
|
|
19805
|
+
this.progressiveAbort?.abort();
|
|
19806
|
+
this.progressiveAbort = null;
|
|
19504
19807
|
for (const w of this.supersedeWaiters.splice(0)) w();
|
|
19505
19808
|
return gen;
|
|
19506
19809
|
}
|
|
@@ -19554,7 +19857,48 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19554
19857
|
const requested = market.bars ?? 500;
|
|
19555
19858
|
const initialRange = market.visibleRange;
|
|
19556
19859
|
const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
|
|
19557
|
-
|
|
19860
|
+
let progressiveServed = false;
|
|
19861
|
+
if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
|
|
19862
|
+
let painted = false;
|
|
19863
|
+
const paint = (bars, final) => {
|
|
19864
|
+
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
19865
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
19866
|
+
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
19867
|
+
if (!painted && bars.length > 0) {
|
|
19868
|
+
painted = true;
|
|
19869
|
+
if (opts.firstLoad) this.activateBarLayers();
|
|
19870
|
+
if (!final) this.historyState = "backfill";
|
|
19871
|
+
}
|
|
19872
|
+
};
|
|
19873
|
+
const abort = new AbortController();
|
|
19874
|
+
this.progressiveAbort = abort;
|
|
19875
|
+
progressiveServed = await new Promise((firstPaint) => {
|
|
19876
|
+
let signaled = false;
|
|
19877
|
+
const signal = (served) => {
|
|
19878
|
+
if (!signaled) {
|
|
19879
|
+
signaled = true;
|
|
19880
|
+
firstPaint(served);
|
|
19881
|
+
}
|
|
19882
|
+
};
|
|
19883
|
+
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
19884
|
+
this.feed.loadProgressive(market, (bars) => {
|
|
19885
|
+
paint(bars, false);
|
|
19886
|
+
if (painted) signal(true);
|
|
19887
|
+
}, { signal: abort.signal }).then((full) => {
|
|
19888
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
19889
|
+
if (full == null) return signal(false);
|
|
19890
|
+
if (this.generation !== gen) return signal(true);
|
|
19891
|
+
paint(full, true);
|
|
19892
|
+
this.completeHistory(full.length >= requested ? "depth" : "genesis");
|
|
19893
|
+
signal(true);
|
|
19894
|
+
}).catch(() => {
|
|
19895
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
19896
|
+
if (this.generation === gen) this.completeHistory("aborted");
|
|
19897
|
+
signal(true);
|
|
19898
|
+
});
|
|
19899
|
+
});
|
|
19900
|
+
}
|
|
19901
|
+
if (progressiveServed) ; else if (deep && this.feed.loadRange) {
|
|
19558
19902
|
const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
|
|
19559
19903
|
if (this.generation !== gen) return;
|
|
19560
19904
|
this.setBarSeries(head);
|
|
@@ -20149,6 +20493,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20149
20493
|
record.pendingCause = "inputs";
|
|
20150
20494
|
if (record.session) record.session.update(record.inputValues);
|
|
20151
20495
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
20496
|
+
this.events.emit("indicator:inputs", { id });
|
|
20497
|
+
}
|
|
20498
|
+
/** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
|
|
20499
|
+
inputValuesOf(id) {
|
|
20500
|
+
return { ...this.registry.get(id)?.inputValues };
|
|
20501
|
+
}
|
|
20502
|
+
/** IndicatorController: the CURRENT declaration-prop overrides. */
|
|
20503
|
+
propValuesOf(id) {
|
|
20504
|
+
return { ...this.registry.get(id)?.propValues };
|
|
20152
20505
|
}
|
|
20153
20506
|
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
20154
20507
|
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
@@ -20162,6 +20515,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20162
20515
|
if (!record.hidden) this.setLoading(record, true);
|
|
20163
20516
|
record.pendingCause = "inputs";
|
|
20164
20517
|
record.session.update(record.inputValues, record.propValues);
|
|
20518
|
+
this.events.emit("indicator:inputs", { id });
|
|
20165
20519
|
}
|
|
20166
20520
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
20167
20521
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
@@ -21374,4 +21728,4 @@ function resolveElement(container) {
|
|
|
21374
21728
|
return element;
|
|
21375
21729
|
}
|
|
21376
21730
|
|
|
21377
|
-
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|
|
21731
|
+
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|