@luxalgo/vela 0.6.21 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-Cb-2lC5O.d.ts → DataProvider-Bu9E5Zh5.d.ts} +1 -1
- package/dist/{DataProvider-UtWw2CgJ.d.cts → DataProvider-Ck2qyS_9.d.cts} +1 -1
- package/dist/{chunk-BRPFYKTT.js → chunk-A347YL2P.js} +30 -7
- package/dist/{chunk-TRZQQUTR.js → chunk-A4G64KVF.js} +1442 -126
- package/dist/{chunk-T5Z5YUCF.js → chunk-CZRNTHD6.js} +24 -2
- package/dist/{chunk-XCBJU674.js → chunk-UHXG5J7C.js} +1 -1
- package/dist/{contributions-D8HdlKmp.d.cts → contributions-BjQ7hyIx.d.ts} +108 -3
- package/dist/{contributions-FqIWhN4p.d.ts → contributions-Cv2Mutvn.d.cts} +108 -3
- package/dist/index.cjs +1465 -125
- package/dist/index.d.cts +76 -20
- package/dist/index.d.ts +76 -20
- package/dist/index.js +2 -2
- package/dist/{options-BCRmYALw.d.cts → options-ex2_gtKp.d.cts} +200 -12
- package/dist/{options-BCRmYALw.d.ts → options-ex2_gtKp.d.ts} +200 -12
- package/dist/{plugin-B4mnNAeh.d.ts → plugin-CiyhU7pi.d.ts} +3 -3
- package/dist/{plugin-Ccupy_BV.d.cts → plugin-absNH7Yn.d.cts} +3 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- 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-model-CCP1zm_Z.d.ts → statusline-model-BXoU4Kua.d.ts} +8 -4
- package/dist/{statusline-model-E7CMw5pQ.d.cts → statusline-model-cRhPBLPO.d.cts} +8 -4
- package/dist/ui.cjs +24 -2
- package/dist/ui.d.cts +9 -3
- package/dist/ui.d.ts +9 -3
- package/dist/ui.js +2 -2
- package/dist/vela.global.js +1465 -125
- package/dist/vela.global.min.js +77 -53
- package/dist/widget.cjs +1490 -129
- package/dist/widget.d.cts +6 -6
- package/dist/widget.d.ts +6 -6
- package/dist/widget.js +5 -5
- package/dist/workspace.cjs +1490 -129
- package/dist/workspace.d.cts +8 -5
- package/dist/workspace.d.ts +8 -5
- package/dist/workspace.js +4 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, 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, stableSeriesId } from './chunk-IFJJPXSV.js';
|
|
2
|
-
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea,
|
|
3
|
-
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BULLISH, CATEGORICAL, SERIES_LINE, BEARISH, isDarkColor, INFO, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
|
|
2
|
+
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-CZRNTHD6.js';
|
|
3
|
+
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BULLISH, CATEGORICAL, SERIES_LINE, BEARISH, isDarkColor, INFO, injectStyles, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -608,6 +608,16 @@ var RendererControl = class {
|
|
|
608
608
|
this.renderer.setLayoutMode?.(mode);
|
|
609
609
|
return this;
|
|
610
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Drive the renderer's time-of-day chrome (the countdown-to-bar-close chip) from the
|
|
613
|
+
* host's own second pulse, so it ticks in step with a host clock display instead of
|
|
614
|
+
* on a separate timer that can read a different second. `null` hands the pulse back
|
|
615
|
+
* to the renderer. Silent no-op on a renderer without time-of-day chrome.
|
|
616
|
+
*/
|
|
617
|
+
setWallClock(clock) {
|
|
618
|
+
this.renderer.setWallClock?.(clock);
|
|
619
|
+
return this;
|
|
620
|
+
}
|
|
611
621
|
};
|
|
612
622
|
|
|
613
623
|
// src/data/BarStore.ts
|
|
@@ -1691,6 +1701,95 @@ var DrawingsControl = class {
|
|
|
1691
1701
|
}
|
|
1692
1702
|
};
|
|
1693
1703
|
|
|
1704
|
+
// src/core/MarksControl.ts
|
|
1705
|
+
var MarksControl = class {
|
|
1706
|
+
constructor(ctrl) {
|
|
1707
|
+
this.ctrl = ctrl;
|
|
1708
|
+
}
|
|
1709
|
+
/** Whether the active renderer paints timeline marks. */
|
|
1710
|
+
get supported() {
|
|
1711
|
+
return this.ctrl.supported;
|
|
1712
|
+
}
|
|
1713
|
+
/** Add one mark; an existing id is replaced in place. */
|
|
1714
|
+
add(mark) {
|
|
1715
|
+
this.ctrl.add(mark);
|
|
1716
|
+
return this;
|
|
1717
|
+
}
|
|
1718
|
+
/** Replace the whole set (a market switch). */
|
|
1719
|
+
set(marks) {
|
|
1720
|
+
this.ctrl.set(marks);
|
|
1721
|
+
return this;
|
|
1722
|
+
}
|
|
1723
|
+
remove(id) {
|
|
1724
|
+
this.ctrl.remove(id);
|
|
1725
|
+
return this;
|
|
1726
|
+
}
|
|
1727
|
+
clear() {
|
|
1728
|
+
this.ctrl.clear();
|
|
1729
|
+
return this;
|
|
1730
|
+
}
|
|
1731
|
+
/** Every mark, in insertion order. */
|
|
1732
|
+
all() {
|
|
1733
|
+
return this.ctrl.all();
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Define a visibility group's presentation: the label of its checkbox in chart
|
|
1737
|
+
* settings (the Events tab) and its default visibility. Marks may name a group
|
|
1738
|
+
* that was never defined — it then shows its capitalized id.
|
|
1739
|
+
*/
|
|
1740
|
+
defineGroup(group) {
|
|
1741
|
+
this.ctrl.defineGroup(group);
|
|
1742
|
+
return this;
|
|
1743
|
+
}
|
|
1744
|
+
/** The defined groups, in definition order. */
|
|
1745
|
+
groups() {
|
|
1746
|
+
return this.ctrl.groupDefinitions();
|
|
1747
|
+
}
|
|
1748
|
+
/** Show or hide one group's marks — the same switch as the settings checkbox, persisted with the chart's config. */
|
|
1749
|
+
setGroupVisible(id, visible = true) {
|
|
1750
|
+
this.ctrl.setGroupVisible(id, visible);
|
|
1751
|
+
return this;
|
|
1752
|
+
}
|
|
1753
|
+
/** A group's effective visibility (the user's choice, else the group's declared default). */
|
|
1754
|
+
isGroupVisible(id) {
|
|
1755
|
+
return this.ctrl.isGroupVisible(id);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// src/core/util/wall-clock.ts
|
|
1760
|
+
var BOUNDARY_SLACK_MS = 5;
|
|
1761
|
+
var SecondClock = class _SecondClock {
|
|
1762
|
+
constructor(now = () => Date.now()) {
|
|
1763
|
+
this.now = now;
|
|
1764
|
+
this.subs = /* @__PURE__ */ new Set();
|
|
1765
|
+
this.timer = null;
|
|
1766
|
+
}
|
|
1767
|
+
onTick(cb) {
|
|
1768
|
+
this.subs.add(cb);
|
|
1769
|
+
if (this.timer == null) this.arm();
|
|
1770
|
+
return () => {
|
|
1771
|
+
this.subs.delete(cb);
|
|
1772
|
+
if (this.subs.size === 0 && this.timer != null) {
|
|
1773
|
+
clearTimeout(this.timer);
|
|
1774
|
+
this.timer = null;
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
/** Milliseconds from `now` to just past the next second boundary. */
|
|
1779
|
+
static delayToNextSecond(now) {
|
|
1780
|
+
const intoSecond = (now % 1e3 + 1e3) % 1e3;
|
|
1781
|
+
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
1782
|
+
}
|
|
1783
|
+
arm() {
|
|
1784
|
+
this.timer = setTimeout(() => {
|
|
1785
|
+
this.timer = null;
|
|
1786
|
+
const now = this.now();
|
|
1787
|
+
for (const cb of this.subs) cb(now);
|
|
1788
|
+
if (this.subs.size > 0) this.arm();
|
|
1789
|
+
}, _SecondClock.delayToNextSecond(this.now()));
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1694
1793
|
// src/core/timezones.ts
|
|
1695
1794
|
var TIMEZONES = [
|
|
1696
1795
|
{ value: "Etc/UTC", label: "UTC" },
|
|
@@ -3026,19 +3125,48 @@ function arrowHitHtml(svg) {
|
|
|
3026
3125
|
|
|
3027
3126
|
// src/core/options.ts
|
|
3028
3127
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
3128
|
+
var ZOOM_EASE_DEFAULT_MS = 70;
|
|
3129
|
+
var PAN_INERTIA_DEFAULT_MS = 110;
|
|
3130
|
+
var SCROLL_EASE_DEFAULT_MS = 130;
|
|
3131
|
+
var AUTOSCALE_EASE_DEFAULT_MS = 80;
|
|
3029
3132
|
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
3030
|
-
var
|
|
3031
|
-
|
|
3032
|
-
|
|
3133
|
+
var INTRO_DURATION_DEFAULT_MS = 650;
|
|
3134
|
+
var ANIMATION_EASE_MAX_MS = 1e3;
|
|
3135
|
+
var LIVE_BAR_EASE_MAX_MS = ANIMATION_EASE_MAX_MS;
|
|
3136
|
+
var INTRO_DURATION_MAX_MS = 5e3;
|
|
3137
|
+
function resolveEaseMs(value, defaultMs, maxMs = ANIMATION_EASE_MAX_MS) {
|
|
3138
|
+
if (value === true) return defaultMs;
|
|
3033
3139
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
3034
|
-
return Math.min(value,
|
|
3140
|
+
return Math.min(value, maxMs);
|
|
3141
|
+
}
|
|
3142
|
+
function resolveLiveBarEaseMs(value) {
|
|
3143
|
+
return resolveEaseMs(value, LIVE_BAR_EASE_DEFAULT_MS, LIVE_BAR_EASE_MAX_MS);
|
|
3144
|
+
}
|
|
3145
|
+
function resolveIntro(value) {
|
|
3146
|
+
const off = { style: false, duration: 0 };
|
|
3147
|
+
if (value === true) return { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
3148
|
+
if (value === "settle" || value === "grow") return { style: value, duration: INTRO_DURATION_DEFAULT_MS };
|
|
3149
|
+
if (value && typeof value === "object") {
|
|
3150
|
+
const o = value;
|
|
3151
|
+
const d = o.duration;
|
|
3152
|
+
return {
|
|
3153
|
+
style: o.style === "grow" ? "grow" : "settle",
|
|
3154
|
+
duration: typeof d === "number" && Number.isFinite(d) && d > 0 ? Math.min(d, INTRO_DURATION_MAX_MS) : INTRO_DURATION_DEFAULT_MS
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
return off;
|
|
3035
3158
|
}
|
|
3036
3159
|
function resolveAnimations(animations) {
|
|
3037
|
-
if (
|
|
3160
|
+
if (animations === false) return { animZoom: 0, animPan: 0, animScroll: 0, animAutoscale: 0, animLiveBar: 0, animIntro: { style: false, duration: 0 } };
|
|
3161
|
+
const cfg = animations === true || animations == null ? {} : animations;
|
|
3162
|
+
const animPan = resolveEaseMs(cfg.pan ?? true, PAN_INERTIA_DEFAULT_MS);
|
|
3038
3163
|
return {
|
|
3039
|
-
animZoom:
|
|
3040
|
-
animPan
|
|
3041
|
-
|
|
3164
|
+
animZoom: resolveEaseMs(cfg.zoom ?? true, ZOOM_EASE_DEFAULT_MS),
|
|
3165
|
+
animPan,
|
|
3166
|
+
animScroll: cfg.scroll === void 0 ? animPan > 0 ? SCROLL_EASE_DEFAULT_MS : 0 : resolveEaseMs(cfg.scroll, SCROLL_EASE_DEFAULT_MS),
|
|
3167
|
+
animAutoscale: resolveEaseMs(cfg.autoscale ?? true, AUTOSCALE_EASE_DEFAULT_MS),
|
|
3168
|
+
animLiveBar: resolveLiveBarEaseMs(cfg.liveBar),
|
|
3169
|
+
animIntro: resolveIntro(cfg.intro ?? true)
|
|
3042
3170
|
};
|
|
3043
3171
|
}
|
|
3044
3172
|
|
|
@@ -5080,6 +5208,8 @@ var NATIVE_CAPABILITIES = {
|
|
|
5080
5208
|
// canvas-painted into the owning indicator's interleave slice
|
|
5081
5209
|
trades: true,
|
|
5082
5210
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
5211
|
+
timelineMarks: true,
|
|
5212
|
+
// host events on a lane above the time axis (glyphs + detail popup)
|
|
5083
5213
|
inputsUI: true
|
|
5084
5214
|
// reuses the DOM InputsUI
|
|
5085
5215
|
};
|
|
@@ -5352,9 +5482,12 @@ function mergeConfig(base, patch) {
|
|
|
5352
5482
|
const gh = asObject(grid.horzLines);
|
|
5353
5483
|
const cross = asObject(p.crosshair);
|
|
5354
5484
|
const ps = asObject(p.priceScale);
|
|
5485
|
+
const anim = asObject(p.animations);
|
|
5355
5486
|
const panes = asObject(p.panes);
|
|
5356
5487
|
const trades = asObject(p.trades);
|
|
5357
5488
|
const ts = asObject(p.timeScale);
|
|
5489
|
+
const marks = asObject(p.marks);
|
|
5490
|
+
const markGroups = asObject(marks.groups);
|
|
5358
5491
|
const candles = asObject(p.candles);
|
|
5359
5492
|
const bars = asObject(p.bars);
|
|
5360
5493
|
const line = asObject(p.line);
|
|
@@ -5401,6 +5534,12 @@ function mergeConfig(base, patch) {
|
|
|
5401
5534
|
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
5402
5535
|
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
5403
5536
|
},
|
|
5537
|
+
animations: {
|
|
5538
|
+
zoom: isBool(anim.zoom) ? anim.zoom : base.animations.zoom,
|
|
5539
|
+
pan: isBool(anim.pan) ? anim.pan : base.animations.pan,
|
|
5540
|
+
autoscale: isBool(anim.autoscale) ? anim.autoscale : base.animations.autoscale,
|
|
5541
|
+
intro: isBool(anim.intro) ? anim.intro : base.animations.intro
|
|
5542
|
+
},
|
|
5404
5543
|
panes: {
|
|
5405
5544
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
5406
5545
|
},
|
|
@@ -5415,6 +5554,15 @@ function mergeConfig(base, patch) {
|
|
|
5415
5554
|
timeScale: {
|
|
5416
5555
|
timezone: typeof ts.timezone === "string" && ts.timezone ? ts.timezone : base.timeScale.timezone
|
|
5417
5556
|
},
|
|
5557
|
+
marks: {
|
|
5558
|
+
visible: isBool(marks.visible) ? marks.visible : base.marks.visible,
|
|
5559
|
+
// Additive like `stacking.series`: a patch names only the groups it carries, so a
|
|
5560
|
+
// choice stored for a group the host has not registered yet survives verbatim.
|
|
5561
|
+
groups: {
|
|
5562
|
+
...base.marks.groups,
|
|
5563
|
+
...Object.fromEntries(Object.entries(markGroups).filter(([, v]) => isBool(v)))
|
|
5564
|
+
}
|
|
5565
|
+
},
|
|
5418
5566
|
candles: {
|
|
5419
5567
|
upColor: isColor(candles.upColor) ? candles.upColor : base.candles.upColor,
|
|
5420
5568
|
downColor: isColor(candles.downColor) ? candles.downColor : base.candles.downColor,
|
|
@@ -6933,6 +7081,31 @@ var Animator = class {
|
|
|
6933
7081
|
}
|
|
6934
7082
|
}
|
|
6935
7083
|
};
|
|
7084
|
+
var EaseSetting = class {
|
|
7085
|
+
/** `defaultMs` is what the on/off switch restores when nothing else was configured;
|
|
7086
|
+
* `initialMs` (default: `defaultMs`) is the starting value — 0 for a motion that
|
|
7087
|
+
* ships off. */
|
|
7088
|
+
constructor(defaultMs, initialMs = defaultMs) {
|
|
7089
|
+
this.onMs = defaultMs;
|
|
7090
|
+
this.ms = initialMs;
|
|
7091
|
+
}
|
|
7092
|
+
/** The active time-constant; 0 when off. */
|
|
7093
|
+
get tau() {
|
|
7094
|
+
return this.ms;
|
|
7095
|
+
}
|
|
7096
|
+
get on() {
|
|
7097
|
+
return this.ms > 0;
|
|
7098
|
+
}
|
|
7099
|
+
/** Set the time-constant (0 = off). A non-zero value becomes what `toggle(true)` restores. */
|
|
7100
|
+
set(ms) {
|
|
7101
|
+
this.ms = ms;
|
|
7102
|
+
if (ms > 0) this.onMs = ms;
|
|
7103
|
+
}
|
|
7104
|
+
/** On/off only — the duration stays the last one configured. */
|
|
7105
|
+
toggle(on) {
|
|
7106
|
+
this.ms = on ? this.onMs : 0;
|
|
7107
|
+
}
|
|
7108
|
+
};
|
|
6936
7109
|
function easeToward(current, target, dtMs, tauMs) {
|
|
6937
7110
|
if (tauMs <= 0) return target;
|
|
6938
7111
|
return current + (target - current) * (1 - Math.exp(-dtMs / tauMs));
|
|
@@ -7656,6 +7829,25 @@ function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
|
7656
7829
|
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
7657
7830
|
}
|
|
7658
7831
|
|
|
7832
|
+
// src/renderers/shared/marks-state.ts
|
|
7833
|
+
function defaultMarksState() {
|
|
7834
|
+
return { visible: true, groups: {} };
|
|
7835
|
+
}
|
|
7836
|
+
function mergeMarksState(base, patch) {
|
|
7837
|
+
if (typeof patch === "boolean") return { visible: patch, groups: { ...base.groups } };
|
|
7838
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
7839
|
+
const g = p.groups && typeof p.groups === "object" ? p.groups : {};
|
|
7840
|
+
const groups = { ...base.groups };
|
|
7841
|
+
for (const [id, v] of Object.entries(g)) if (typeof v === "boolean") groups[id] = v;
|
|
7842
|
+
return { visible: typeof p.visible === "boolean" ? p.visible : base.visible, groups };
|
|
7843
|
+
}
|
|
7844
|
+
function markGroupVisible(state, groupId, groups) {
|
|
7845
|
+
if (groupId === void 0) return true;
|
|
7846
|
+
const chosen = state.groups[groupId];
|
|
7847
|
+
if (typeof chosen === "boolean") return chosen;
|
|
7848
|
+
return groups.find((g) => g.id === groupId)?.visible !== false;
|
|
7849
|
+
}
|
|
7850
|
+
|
|
7659
7851
|
// src/renderers/native/core/SceneGraph.ts
|
|
7660
7852
|
var SceneGraph = class {
|
|
7661
7853
|
constructor() {
|
|
@@ -7719,6 +7911,20 @@ var SceneGraph = class {
|
|
|
7719
7911
|
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
7720
7912
|
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
7721
7913
|
this.tradeMarkers = defaultTradeMarkersState();
|
|
7914
|
+
/** Timeline-mark display (the `marks` feature): the lane's master toggle + per-group visibility. */
|
|
7915
|
+
this.marks = defaultMarksState();
|
|
7916
|
+
/** The host's timeline marks + group definitions (`setTimelineMarks`), painted on the lane above the time axis. */
|
|
7917
|
+
this.timelineMarks = [];
|
|
7918
|
+
this.markGroups = [];
|
|
7919
|
+
/** The mark stack (bar index) fanned out by hover or tap, if any. */
|
|
7920
|
+
this.marksExpandedStack = null;
|
|
7921
|
+
/** The lane glyph (cluster key) under the pointer — it pulses — and when the hover began (frame-clock ms). */
|
|
7922
|
+
this.marksHoverKey = null;
|
|
7923
|
+
this.marksHoverSince = 0;
|
|
7924
|
+
/** The cluster whose popup is open: its glyph paints filled ("active"). */
|
|
7925
|
+
this.marksActiveKey = null;
|
|
7926
|
+
/** A content-less click's brief filled flash — the cluster key and the frame-clock time it ends. */
|
|
7927
|
+
this.marksFlash = null;
|
|
7722
7928
|
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
7723
7929
|
this.highlights = [];
|
|
7724
7930
|
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
@@ -9567,6 +9773,315 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
9567
9773
|
return out;
|
|
9568
9774
|
}
|
|
9569
9775
|
|
|
9776
|
+
// src/renderers/native/chrome/countdown.ts
|
|
9777
|
+
function countdownText(barOpen, barMs, now) {
|
|
9778
|
+
if (!(barMs > 0)) return null;
|
|
9779
|
+
const remaining = barOpen + barMs - now;
|
|
9780
|
+
if (remaining <= 0) return null;
|
|
9781
|
+
return formatCountdown(remaining);
|
|
9782
|
+
}
|
|
9783
|
+
function formatCountdown(ms) {
|
|
9784
|
+
const total = Math.max(0, Math.ceil(ms / 1e3));
|
|
9785
|
+
const s = total % 60;
|
|
9786
|
+
const m = Math.floor(total / 60) % 60;
|
|
9787
|
+
const h = Math.floor(total / 3600);
|
|
9788
|
+
const pad = (v) => String(v).padStart(2, "0");
|
|
9789
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
9790
|
+
}
|
|
9791
|
+
|
|
9792
|
+
// src/renderers/native/chrome/contrast.ts
|
|
9793
|
+
function tagTextColor(bg, over) {
|
|
9794
|
+
const [r, g, b, a] = parseColor(bg);
|
|
9795
|
+
let R = r;
|
|
9796
|
+
let G = g;
|
|
9797
|
+
let B = b;
|
|
9798
|
+
if (a < 1) {
|
|
9799
|
+
const [or, og, ob] = parseColor(over);
|
|
9800
|
+
R = r * a + or * (1 - a);
|
|
9801
|
+
G = g * a + og * (1 - a);
|
|
9802
|
+
B = b * a + ob * (1 - a);
|
|
9803
|
+
}
|
|
9804
|
+
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
9805
|
+
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
9806
|
+
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
9807
|
+
}
|
|
9808
|
+
|
|
9809
|
+
// src/renderers/native/chrome/marks/layout.ts
|
|
9810
|
+
var MARK_GLYPH_PX = 16;
|
|
9811
|
+
var MARK_CLUSTER_PX = 20;
|
|
9812
|
+
var MARK_LANE_INSET = 4;
|
|
9813
|
+
var MARK_DECK_STEP = 3;
|
|
9814
|
+
var MARK_FAN_GAP = 4;
|
|
9815
|
+
var MARK_HIT_PAD = 3;
|
|
9816
|
+
var MARK_FAN_HOLD = 8;
|
|
9817
|
+
function snapMarkBar(time, barTimes, intervalMs2) {
|
|
9818
|
+
const n = barTimes.length;
|
|
9819
|
+
if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
|
|
9820
|
+
if (time < barTimes[0]) return null;
|
|
9821
|
+
const last = barTimes[n - 1];
|
|
9822
|
+
if (time >= last + intervalMs2) return n - 1 + Math.floor((time - last) / intervalMs2);
|
|
9823
|
+
let lo = 0;
|
|
9824
|
+
let hi = n - 1;
|
|
9825
|
+
while (lo < hi) {
|
|
9826
|
+
const mid = lo + hi + 1 >> 1;
|
|
9827
|
+
if (barTimes[mid] <= time) lo = mid;
|
|
9828
|
+
else hi = mid - 1;
|
|
9829
|
+
}
|
|
9830
|
+
if (time < barTimes[lo] + intervalMs2) return lo;
|
|
9831
|
+
return lo + 1;
|
|
9832
|
+
}
|
|
9833
|
+
function clusterMarks(marks, barTimes, intervalMs2, hidden) {
|
|
9834
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
9835
|
+
marks.forEach((m, seq) => {
|
|
9836
|
+
if (m.group !== void 0 && hidden(m.group)) return;
|
|
9837
|
+
const bar = snapMarkBar(m.time, barTimes, intervalMs2);
|
|
9838
|
+
if (bar === null) return;
|
|
9839
|
+
const key = `${bar}|${m.group ?? ""}`;
|
|
9840
|
+
let c = byKey.get(key);
|
|
9841
|
+
if (!c) {
|
|
9842
|
+
c = { key, bar, group: m.group, marks: [], seq: [] };
|
|
9843
|
+
byKey.set(key, c);
|
|
9844
|
+
}
|
|
9845
|
+
c.marks.push(m);
|
|
9846
|
+
c.seq.push(seq);
|
|
9847
|
+
});
|
|
9848
|
+
const out = [];
|
|
9849
|
+
for (const c of byKey.values()) {
|
|
9850
|
+
const order = c.marks.map((m, i) => ({ m, seq: c.seq[i] })).sort((a, b) => a.m.time - b.m.time || a.seq - b.seq);
|
|
9851
|
+
out.push({ key: c.key, bar: c.bar, group: c.group, marks: order.map((o) => o.m) });
|
|
9852
|
+
}
|
|
9853
|
+
return out;
|
|
9854
|
+
}
|
|
9855
|
+
function groupRank(groups, clusters) {
|
|
9856
|
+
const rank = /* @__PURE__ */ new Map();
|
|
9857
|
+
groups.forEach((g, i) => rank.set(g.id, i));
|
|
9858
|
+
for (const c of clusters) {
|
|
9859
|
+
if (c.group !== void 0 && !rank.has(c.group)) rank.set(c.group, rank.size);
|
|
9860
|
+
}
|
|
9861
|
+
return (group) => group === void 0 ? Number.MAX_SAFE_INTEGER : rank.get(group) ?? Number.MAX_SAFE_INTEGER - 1;
|
|
9862
|
+
}
|
|
9863
|
+
function layoutMarkLane(input) {
|
|
9864
|
+
const clusters = clusterMarks(input.marks, input.barTimes, input.intervalMs, input.hidden);
|
|
9865
|
+
const rankOf = groupRank(input.groups, clusters);
|
|
9866
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
9867
|
+
for (const c of clusters) {
|
|
9868
|
+
const list = byBar.get(c.bar);
|
|
9869
|
+
if (list) list.push(c);
|
|
9870
|
+
else byBar.set(c.bar, [c]);
|
|
9871
|
+
}
|
|
9872
|
+
const glyphs = [];
|
|
9873
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
9874
|
+
for (const [bar, list] of byBar) {
|
|
9875
|
+
const x = input.xOf(bar);
|
|
9876
|
+
if (!Number.isFinite(x) || x < -MARK_CLUSTER_PX || x > input.dataW + MARK_CLUSTER_PX) continue;
|
|
9877
|
+
list.sort((a, b) => rankOf(a.group) - rankOf(b.group));
|
|
9878
|
+
const multi = list.length > 1;
|
|
9879
|
+
const expanded = multi && input.expanded === bar;
|
|
9880
|
+
const decked = multi && !expanded;
|
|
9881
|
+
const placed = [];
|
|
9882
|
+
const deckSize = list[0].marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
9883
|
+
let bottom = input.axisY - MARK_LANE_INSET;
|
|
9884
|
+
list.forEach((cluster, depth) => {
|
|
9885
|
+
const size = decked ? deckSize : cluster.marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
9886
|
+
let y;
|
|
9887
|
+
if (expanded) {
|
|
9888
|
+
y = bottom - size / 2;
|
|
9889
|
+
bottom -= size + MARK_FAN_GAP;
|
|
9890
|
+
} else {
|
|
9891
|
+
y = input.axisY - MARK_LANE_INSET - size / 2 - depth * MARK_DECK_STEP;
|
|
9892
|
+
}
|
|
9893
|
+
placed.push({ cluster, x, y, size, stack: bar, depth, decked });
|
|
9894
|
+
});
|
|
9895
|
+
for (let i = placed.length - 1; i >= 0; i--) glyphs.push(placed[i]);
|
|
9896
|
+
stacks.set(bar, placed);
|
|
9897
|
+
}
|
|
9898
|
+
return { glyphs, stacks };
|
|
9899
|
+
}
|
|
9900
|
+
function markGlyphAt(layout, x, y) {
|
|
9901
|
+
for (let i = layout.glyphs.length - 1; i >= 0; i--) {
|
|
9902
|
+
const g = layout.glyphs[i];
|
|
9903
|
+
if (g.decked && g.depth !== 0) continue;
|
|
9904
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
9905
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return g;
|
|
9906
|
+
}
|
|
9907
|
+
return null;
|
|
9908
|
+
}
|
|
9909
|
+
function markStackAt(layout, x, y) {
|
|
9910
|
+
for (const [bar, placed] of layout.stacks) {
|
|
9911
|
+
for (const g of placed) {
|
|
9912
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
9913
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return bar;
|
|
9914
|
+
}
|
|
9915
|
+
if (placed.length > 1 && !placed[0].decked) {
|
|
9916
|
+
const top = placed[placed.length - 1];
|
|
9917
|
+
const base = placed[0];
|
|
9918
|
+
const r = Math.max(top.size, base.size) / 2 + MARK_HIT_PAD;
|
|
9919
|
+
if (Math.abs(x - base.x) <= r && y >= top.y - top.size / 2 - MARK_FAN_HOLD && y <= base.y + base.size / 2 + MARK_HIT_PAD) return bar;
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9922
|
+
return null;
|
|
9923
|
+
}
|
|
9924
|
+
function clusterTooltip(cluster, groups) {
|
|
9925
|
+
const first = cluster.marks[0];
|
|
9926
|
+
if (!first) return null;
|
|
9927
|
+
if (cluster.marks.length === 1) return first.tooltip ?? first.title ?? null;
|
|
9928
|
+
const label = cluster.group !== void 0 ? markGroupLabel(cluster.group, groups) : first.title ?? first.tooltip ?? "Marks";
|
|
9929
|
+
return `${label} \xB7 ${cluster.marks.length}`;
|
|
9930
|
+
}
|
|
9931
|
+
function markGroupLabel(groupId, groups) {
|
|
9932
|
+
const def = groups.find((g) => g.id === groupId);
|
|
9933
|
+
if (def) return def.label;
|
|
9934
|
+
return groupId.charAt(0).toUpperCase() + groupId.slice(1);
|
|
9935
|
+
}
|
|
9936
|
+
function effectiveMarkGroups(marks, groups) {
|
|
9937
|
+
const out = groups.map((g) => ({ ...g }));
|
|
9938
|
+
const seen = new Set(out.map((g) => g.id));
|
|
9939
|
+
for (const m of marks) {
|
|
9940
|
+
if (m.group === void 0 || seen.has(m.group)) continue;
|
|
9941
|
+
seen.add(m.group);
|
|
9942
|
+
out.push({ id: m.group, label: markGroupLabel(m.group, groups) });
|
|
9943
|
+
}
|
|
9944
|
+
return out;
|
|
9945
|
+
}
|
|
9946
|
+
|
|
9947
|
+
// src/renderers/native/chrome/marks/paint.ts
|
|
9948
|
+
var MARK_PULSE_MS = 360;
|
|
9949
|
+
var MARK_PULSE_AMPLITUDE = 0.1;
|
|
9950
|
+
var ACTIVE_INK = "#ffffff";
|
|
9951
|
+
function pulseScale(elapsedMs) {
|
|
9952
|
+
if (!(elapsedMs > 0) || elapsedMs >= MARK_PULSE_MS) return 1;
|
|
9953
|
+
return 1 + MARK_PULSE_AMPLITUDE * Math.sin(Math.PI * elapsedMs / MARK_PULSE_MS);
|
|
9954
|
+
}
|
|
9955
|
+
function paintMarkLane(ctx, layout, deps) {
|
|
9956
|
+
if (layout.glyphs.length === 0) return;
|
|
9957
|
+
ctx.save();
|
|
9958
|
+
ctx.setLineDash([]);
|
|
9959
|
+
ctx.textAlign = "center";
|
|
9960
|
+
ctx.textBaseline = "middle";
|
|
9961
|
+
for (const g of layout.glyphs) {
|
|
9962
|
+
const mark = g.cluster.marks[0];
|
|
9963
|
+
if (!mark) continue;
|
|
9964
|
+
const key = g.cluster.key;
|
|
9965
|
+
const color = mark.glyph.color;
|
|
9966
|
+
const active = key === deps.activeKey || key === deps.flashKey;
|
|
9967
|
+
const size = g.size * (key === deps.hoverKey ? pulseScale(deps.nowMs - deps.hoverSince) : 1);
|
|
9968
|
+
if (g.depth === 0) {
|
|
9969
|
+
const sx = Math.round(g.x) + 0.5;
|
|
9970
|
+
ctx.lineWidth = 1;
|
|
9971
|
+
ctx.strokeStyle = deps.stemColor;
|
|
9972
|
+
ctx.beginPath();
|
|
9973
|
+
ctx.moveTo(sx, g.y + g.size / 2);
|
|
9974
|
+
ctx.lineTo(sx, deps.axisY);
|
|
9975
|
+
ctx.stroke();
|
|
9976
|
+
}
|
|
9977
|
+
const shape = mark.glyph.shape ?? "circle";
|
|
9978
|
+
const center = traceShape(ctx, shape, g.x, g.y, size);
|
|
9979
|
+
ctx.lineWidth = 4;
|
|
9980
|
+
ctx.strokeStyle = deps.background;
|
|
9981
|
+
ctx.stroke();
|
|
9982
|
+
ctx.fillStyle = active ? color : deps.background;
|
|
9983
|
+
ctx.fill();
|
|
9984
|
+
ctx.lineWidth = 1.5;
|
|
9985
|
+
ctx.strokeStyle = color;
|
|
9986
|
+
ctx.stroke();
|
|
9987
|
+
const ink = active ? ACTIVE_INK : color;
|
|
9988
|
+
const symbolPx = Math.round(size * 0.62);
|
|
9989
|
+
if (mark.glyph.icon) {
|
|
9990
|
+
const img = deps.icons.get(mark.glyph.icon, ink, symbolPx, deps.dpr);
|
|
9991
|
+
if (img) ctx.drawImage(img, center.x - symbolPx / 2, center.y - symbolPx / 2, symbolPx, symbolPx);
|
|
9992
|
+
} else if (mark.glyph.letter) {
|
|
9993
|
+
ctx.fillStyle = ink;
|
|
9994
|
+
ctx.font = `600 ${Math.round(size * 0.58)}px ${deps.fontFamily}`;
|
|
9995
|
+
ctx.fillText(mark.glyph.letter.slice(0, 2), center.x, center.y + 0.5);
|
|
9996
|
+
}
|
|
9997
|
+
}
|
|
9998
|
+
ctx.restore();
|
|
9999
|
+
}
|
|
10000
|
+
function traceShape(ctx, shape, x, y, size) {
|
|
10001
|
+
const r = size / 2;
|
|
10002
|
+
ctx.beginPath();
|
|
10003
|
+
switch (shape) {
|
|
10004
|
+
case "square": {
|
|
10005
|
+
const c = Math.min(3, r / 2);
|
|
10006
|
+
roundedRect(ctx, x - r, y - r, size, size, c);
|
|
10007
|
+
return { x, y };
|
|
10008
|
+
}
|
|
10009
|
+
case "diamond":
|
|
10010
|
+
ctx.moveTo(x, y - r);
|
|
10011
|
+
ctx.lineTo(x + r, y);
|
|
10012
|
+
ctx.lineTo(x, y + r);
|
|
10013
|
+
ctx.lineTo(x - r, y);
|
|
10014
|
+
ctx.closePath();
|
|
10015
|
+
return { x, y };
|
|
10016
|
+
case "pin": {
|
|
10017
|
+
const hr = r * 0.82;
|
|
10018
|
+
const hy = y - r + hr;
|
|
10019
|
+
ctx.arc(x, hy, hr, Math.PI * 0.75, Math.PI * 0.25, false);
|
|
10020
|
+
ctx.lineTo(x, y + r);
|
|
10021
|
+
ctx.closePath();
|
|
10022
|
+
return { x, y: hy };
|
|
10023
|
+
}
|
|
10024
|
+
case "circle":
|
|
10025
|
+
default:
|
|
10026
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
10027
|
+
return { x, y };
|
|
10028
|
+
}
|
|
10029
|
+
}
|
|
10030
|
+
function roundedRect(ctx, x, y, w, h, radius) {
|
|
10031
|
+
ctx.moveTo(x + radius, y);
|
|
10032
|
+
ctx.lineTo(x + w - radius, y);
|
|
10033
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
|
10034
|
+
ctx.lineTo(x + w, y + h - radius);
|
|
10035
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
|
10036
|
+
ctx.lineTo(x + radius, y + h);
|
|
10037
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
|
10038
|
+
ctx.lineTo(x, y + radius);
|
|
10039
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
10040
|
+
ctx.closePath();
|
|
10041
|
+
}
|
|
10042
|
+
var MarkIconRaster = class {
|
|
10043
|
+
constructor(onReady) {
|
|
10044
|
+
this.onReady = onReady;
|
|
10045
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
10046
|
+
}
|
|
10047
|
+
get(icon2, ink, px, dpr) {
|
|
10048
|
+
const key = `${icon2}|${ink}|${px}|${dpr}`;
|
|
10049
|
+
if (this.cache.has(key)) {
|
|
10050
|
+
const img2 = this.cache.get(key);
|
|
10051
|
+
return img2 && img2.complete && img2.naturalWidth > 0 ? img2 : null;
|
|
10052
|
+
}
|
|
10053
|
+
const markup = iconMarkup(icon2);
|
|
10054
|
+
if (!markup || typeof document === "undefined" || typeof Image === "undefined" || typeof XMLSerializer === "undefined") {
|
|
10055
|
+
this.cache.set(key, null);
|
|
10056
|
+
return null;
|
|
10057
|
+
}
|
|
10058
|
+
const svg = standaloneSvg(markup, ink, Math.max(1, Math.ceil(px * dpr)));
|
|
10059
|
+
if (!svg) {
|
|
10060
|
+
this.cache.set(key, null);
|
|
10061
|
+
return null;
|
|
10062
|
+
}
|
|
10063
|
+
const img = new Image();
|
|
10064
|
+
img.onload = () => this.onReady();
|
|
10065
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
10066
|
+
this.cache.set(key, img);
|
|
10067
|
+
return null;
|
|
10068
|
+
}
|
|
10069
|
+
clear() {
|
|
10070
|
+
this.cache.clear();
|
|
10071
|
+
}
|
|
10072
|
+
};
|
|
10073
|
+
function standaloneSvg(markup, ink, px) {
|
|
10074
|
+
const tpl = document.createElement("template");
|
|
10075
|
+
tpl.innerHTML = markup;
|
|
10076
|
+
const svg = tpl.content.firstElementChild;
|
|
10077
|
+
if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
|
|
10078
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
10079
|
+
svg.setAttribute("width", String(px));
|
|
10080
|
+
svg.setAttribute("height", String(px));
|
|
10081
|
+
svg.setAttribute("color", ink);
|
|
10082
|
+
return new XMLSerializer().serializeToString(svg);
|
|
10083
|
+
}
|
|
10084
|
+
|
|
9570
10085
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
9571
10086
|
var ChromeRenderer = class {
|
|
9572
10087
|
constructor() {
|
|
@@ -9576,11 +10091,40 @@ var ChromeRenderer = class {
|
|
|
9576
10091
|
this.axisTextColor = DARK_THEME.textColor;
|
|
9577
10092
|
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
9578
10093
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
10094
|
+
/** The timeline-mark lane as laid out by the last frame — what hover/click hit-test against. */
|
|
10095
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
10096
|
+
/** Registry icons rasterized for the lane; the owner is asked for a chrome repaint when one lands. */
|
|
10097
|
+
this.markIcons = new MarkIconRaster(() => this.onMarkIconReady?.());
|
|
10098
|
+
this.onMarkIconReady = null;
|
|
10099
|
+
/** Bar open times of the current series, rebuilt only when the array or its length changes (a live tick keeps both). */
|
|
10100
|
+
this.barTimesSrc = null;
|
|
10101
|
+
this.barTimesCache = [];
|
|
9579
10102
|
}
|
|
9580
10103
|
mount(canvas) {
|
|
9581
10104
|
this.canvas = canvas;
|
|
9582
10105
|
this.ctx = canvas.getContext("2d");
|
|
9583
10106
|
}
|
|
10107
|
+
/** Where to ask for a chrome repaint when a lane icon finishes rasterizing. */
|
|
10108
|
+
setMarkIconReady(cb) {
|
|
10109
|
+
this.onMarkIconReady = cb;
|
|
10110
|
+
}
|
|
10111
|
+
/** The interactive mark glyph under a plot point (last frame's layout), or null. */
|
|
10112
|
+
markGlyphAt(x, y) {
|
|
10113
|
+
return markGlyphAt(this.markLayout, x, y);
|
|
10114
|
+
}
|
|
10115
|
+
/** The mark stack (bar index) whose glyphs — or the gaps of its fan — cover a plot point. */
|
|
10116
|
+
markStackAt(x, y) {
|
|
10117
|
+
return markStackAt(this.markLayout, x, y);
|
|
10118
|
+
}
|
|
10119
|
+
/** A glyph of the last frame by its cluster key — how an open popup follows its anchor. */
|
|
10120
|
+
markGlyphByKey(key) {
|
|
10121
|
+
return this.markLayout.glyphs.find((g) => g.cluster.key === key) ?? null;
|
|
10122
|
+
}
|
|
10123
|
+
/** Hover text of the mark glyph under a plot point, or null. */
|
|
10124
|
+
markTooltipAt(x, y, groups) {
|
|
10125
|
+
const g = this.markGlyphAt(x, y);
|
|
10126
|
+
return g ? clusterTooltip(g.cluster, groups) : null;
|
|
10127
|
+
}
|
|
9584
10128
|
/** Wire the drawing coordinate resolvers + theme (call once per frame before use). */
|
|
9585
10129
|
prepare(scene, coords, theme) {
|
|
9586
10130
|
this.drawScene.setDeps({
|
|
@@ -9629,6 +10173,7 @@ var ChromeRenderer = class {
|
|
|
9629
10173
|
const panes = scene.orderedPanes();
|
|
9630
10174
|
if (coords.barCount === 0) {
|
|
9631
10175
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
10176
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
9632
10177
|
return;
|
|
9633
10178
|
}
|
|
9634
10179
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -9642,6 +10187,46 @@ var ChromeRenderer = class {
|
|
|
9642
10187
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
9643
10188
|
this.drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane);
|
|
9644
10189
|
this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
|
|
10190
|
+
this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
|
|
10191
|
+
}
|
|
10192
|
+
/** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
|
|
10193
|
+
drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
|
|
10194
|
+
if (!scene.marks.visible || scene.timelineMarks.length === 0) {
|
|
10195
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
10196
|
+
return;
|
|
10197
|
+
}
|
|
10198
|
+
this.markLayout = layoutMarkLane({
|
|
10199
|
+
marks: scene.timelineMarks,
|
|
10200
|
+
groups: scene.markGroups,
|
|
10201
|
+
hidden: (groupId) => !markGroupVisible(scene.marks, groupId, scene.markGroups),
|
|
10202
|
+
barTimes: this.barTimes(scene),
|
|
10203
|
+
intervalMs: coords.barInterval,
|
|
10204
|
+
xOf: (bar) => coords.logicalToX(bar),
|
|
10205
|
+
axisY: dataH,
|
|
10206
|
+
dataW,
|
|
10207
|
+
expanded: scene.marksExpandedStack
|
|
10208
|
+
});
|
|
10209
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
10210
|
+
paintMarkLane(ctx, this.markLayout, {
|
|
10211
|
+
axisY: dataH,
|
|
10212
|
+
background: theme.background,
|
|
10213
|
+
stemColor: scene.style.borderColor ?? theme.borderColor,
|
|
10214
|
+
fontFamily: theme.fontFamily,
|
|
10215
|
+
dpr: coords.dpr,
|
|
10216
|
+
icons: this.markIcons,
|
|
10217
|
+
hoverKey: scene.marksHoverKey,
|
|
10218
|
+
hoverSince: scene.marksHoverSince,
|
|
10219
|
+
activeKey: scene.marksActiveKey,
|
|
10220
|
+
flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
|
|
10221
|
+
nowMs
|
|
10222
|
+
});
|
|
10223
|
+
}
|
|
10224
|
+
barTimes(scene) {
|
|
10225
|
+
if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
|
|
10226
|
+
this.barTimesSrc = scene.bars;
|
|
10227
|
+
this.barTimesCache = scene.bars.map((b) => b.time);
|
|
10228
|
+
}
|
|
10229
|
+
return this.barTimesCache;
|
|
9645
10230
|
}
|
|
9646
10231
|
destroy() {
|
|
9647
10232
|
this.canvas = null;
|
|
@@ -9757,7 +10342,8 @@ var ChromeRenderer = class {
|
|
|
9757
10342
|
* - the countdown-to-bar-close chip (`showCountdown`).
|
|
9758
10343
|
* When the label and countdown are both on they merge into one stacked block (countdown
|
|
9759
10344
|
* under the label, text flushed left); a lone label or countdown is centered on the
|
|
9760
|
-
* price level with centered text. The countdown
|
|
10345
|
+
* price level with centered text. The countdown repaints on the renderer's second pulse
|
|
10346
|
+
* and disappears once the bar has closed, until the next bar arrives.
|
|
9761
10347
|
*/
|
|
9762
10348
|
drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane) {
|
|
9763
10349
|
const n = scene.bars.length;
|
|
@@ -9777,17 +10363,16 @@ var ChromeRenderer = class {
|
|
|
9777
10363
|
ctx.stroke();
|
|
9778
10364
|
setDash2(ctx, "solid");
|
|
9779
10365
|
}
|
|
9780
|
-
const
|
|
9781
|
-
const showCountdown =
|
|
10366
|
+
const cdText = scene.showCountdown ? countdownText(last.time, coords.barInterval, Date.now()) : null;
|
|
10367
|
+
const showCountdown = cdText !== null;
|
|
9782
10368
|
const showLabel = scene.showPriceLabel;
|
|
9783
10369
|
if (!showLabel && !showCountdown) return;
|
|
9784
10370
|
const priceText = formatAxisValue(pricePane.scale, pricePane.bounds.height, last.close, percentScaleFor(scene, pricePane), scene.priceMintick);
|
|
9785
|
-
const cdText = showCountdown ? formatCountdown(last.time + interval - Date.now()) : "";
|
|
9786
10371
|
const PAD = 8;
|
|
9787
10372
|
const x = dataW + 1;
|
|
9788
10373
|
const textColor = tagTextColor(color, theme.background);
|
|
9789
10374
|
ctx.textBaseline = "middle";
|
|
9790
|
-
if (showLabel &&
|
|
10375
|
+
if (showLabel && cdText !== null) {
|
|
9791
10376
|
const w2 = Math.max(ctx.measureText(priceText).width, ctx.measureText(cdText).width) + PAD;
|
|
9792
10377
|
const top = y - 8;
|
|
9793
10378
|
const tx = x + PAD / 2;
|
|
@@ -9800,7 +10385,7 @@ var ChromeRenderer = class {
|
|
|
9800
10385
|
ctx.textAlign = "start";
|
|
9801
10386
|
return;
|
|
9802
10387
|
}
|
|
9803
|
-
const text = showLabel ? priceText : cdText;
|
|
10388
|
+
const text = showLabel ? priceText : cdText ?? "";
|
|
9804
10389
|
const w = ctx.measureText(text).width + PAD;
|
|
9805
10390
|
ctx.fillStyle = color;
|
|
9806
10391
|
ctx.fillRect(x, y - 8, w, 16);
|
|
@@ -9871,29 +10456,6 @@ function setDash2(ctx, style) {
|
|
|
9871
10456
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
9872
10457
|
else ctx.setLineDash([]);
|
|
9873
10458
|
}
|
|
9874
|
-
function tagTextColor(bg, over) {
|
|
9875
|
-
const [r, g, b, a] = parseColor(bg);
|
|
9876
|
-
let R = r;
|
|
9877
|
-
let G = g;
|
|
9878
|
-
let B = b;
|
|
9879
|
-
if (a < 1) {
|
|
9880
|
-
const [or, og, ob] = parseColor(over);
|
|
9881
|
-
R = r * a + or * (1 - a);
|
|
9882
|
-
G = g * a + og * (1 - a);
|
|
9883
|
-
B = b * a + ob * (1 - a);
|
|
9884
|
-
}
|
|
9885
|
-
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
9886
|
-
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
9887
|
-
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
9888
|
-
}
|
|
9889
|
-
function formatCountdown(ms) {
|
|
9890
|
-
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
9891
|
-
const s = total % 60;
|
|
9892
|
-
const m = Math.floor(total / 60) % 60;
|
|
9893
|
-
const h = Math.floor(total / 3600);
|
|
9894
|
-
const pad = (v) => String(v).padStart(2, "0");
|
|
9895
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
9896
|
-
}
|
|
9897
10459
|
|
|
9898
10460
|
// src/renderers/native/chrome/LabelTooltip.ts
|
|
9899
10461
|
var HOVER_DELAY_MS = 350;
|
|
@@ -10099,6 +10661,11 @@ function setDash3(ctx, style) {
|
|
|
10099
10661
|
function settingsIdSlug(label) {
|
|
10100
10662
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10101
10663
|
}
|
|
10664
|
+
var MARKS_SETTINGS_ID = "events";
|
|
10665
|
+
var MARKS_GROUPS_SETTINGS_ID = "events.groups";
|
|
10666
|
+
function markGroupSettingsId(groupId) {
|
|
10667
|
+
return `${MARKS_GROUPS_SETTINGS_ID}.${settingsIdSlug(groupId)}`;
|
|
10668
|
+
}
|
|
10102
10669
|
function settingsIdHidden(id, hidden) {
|
|
10103
10670
|
if (hidden.size === 0) return false;
|
|
10104
10671
|
let path = id;
|
|
@@ -10192,7 +10759,11 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
10192
10759
|
"symbol.style.baseline.base-level",
|
|
10193
10760
|
"symbol.style.baseline.width",
|
|
10194
10761
|
"symbol.animation",
|
|
10762
|
+
"symbol.animation.zoom",
|
|
10763
|
+
"symbol.animation.pan",
|
|
10764
|
+
"symbol.animation.autoscale",
|
|
10195
10765
|
"symbol.animation.price-changes",
|
|
10766
|
+
"symbol.animation.intro",
|
|
10196
10767
|
"symbol.timezone",
|
|
10197
10768
|
"scales",
|
|
10198
10769
|
"scales.price-scale",
|
|
@@ -10218,8 +10789,13 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
10218
10789
|
"canvas.grid.horizontal",
|
|
10219
10790
|
"canvas.theme"
|
|
10220
10791
|
];
|
|
10221
|
-
function settingsIdCatalog(hostSections) {
|
|
10792
|
+
function settingsIdCatalog(hostSections, markGroups = []) {
|
|
10222
10793
|
const ids = new Set(BUILTIN_SETTINGS_IDS);
|
|
10794
|
+
if (markGroups.length > 0) {
|
|
10795
|
+
ids.add(MARKS_SETTINGS_ID);
|
|
10796
|
+
ids.add(MARKS_GROUPS_SETTINGS_ID);
|
|
10797
|
+
for (const g of markGroups) ids.add(markGroupSettingsId(g.id));
|
|
10798
|
+
}
|
|
10223
10799
|
for (const def of chartTypes()) {
|
|
10224
10800
|
if (hasOwnCandlePaint(def.id)) {
|
|
10225
10801
|
const style = `symbol.style.${def.id}`;
|
|
@@ -10259,7 +10835,7 @@ function styleLabel(id) {
|
|
|
10259
10835
|
return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
|
|
10260
10836
|
}
|
|
10261
10837
|
var SD_STYLE_ID = "vela-settings-controls";
|
|
10262
|
-
var SD_STYLE_REV = "
|
|
10838
|
+
var SD_STYLE_REV = "6";
|
|
10263
10839
|
var SETTINGS_BORDER = "var(--vela-border)";
|
|
10264
10840
|
function ensureControlStyles() {
|
|
10265
10841
|
if (typeof document === "undefined") return;
|
|
@@ -10335,7 +10911,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
10335
10911
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
10336
10912
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
10337
10913
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
10338
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
10914
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
10915
|
+
/* The wrap rule above is for ROW LABELS only: a select's closed value must keep its
|
|
10916
|
+
single-line ellipsis, or a long option wraps to several lines inside the 34px
|
|
10917
|
+
trigger and spills over the rows around it. Three classes so it outranks the
|
|
10918
|
+
two-classes-plus-element selector above. The kit's fixed 100px column is a desktop
|
|
10919
|
+
alignment device; on mobile the trigger hugs its value instead (the grid's control
|
|
10920
|
+
column is max-content), capped so a long option still ellipsizes before the label. */
|
|
10921
|
+
.vela-sd-mobile .vela-select-trigger .vela-select-label{white-space:nowrap !important;}
|
|
10922
|
+
.vela-sd-mobile .vela-select:not([data-fill]){width:auto;min-width:100px;max-width:min(220px,55vw);}`;
|
|
10339
10923
|
if (!existing) document.head.appendChild(st);
|
|
10340
10924
|
}
|
|
10341
10925
|
var SettingsDialog = class {
|
|
@@ -10350,6 +10934,9 @@ var SettingsDialog = class {
|
|
|
10350
10934
|
this.config = null;
|
|
10351
10935
|
this.syncTypeTabs = null;
|
|
10352
10936
|
this.hostSections = [];
|
|
10937
|
+
/** The timeline-mark groups (defined + named by marks) — one checkbox each on the Events tab. */
|
|
10938
|
+
this.markGroups = [];
|
|
10939
|
+
this.markGroupVisible = () => true;
|
|
10353
10940
|
/** The Canvas → Theme row: current app theme + where a pick is raised. The row is a
|
|
10354
10941
|
* host callback, NOT a config patch — the app theme stays out of the persisted
|
|
10355
10942
|
* `ChartConfig`, so exported templates never carry it. */
|
|
@@ -10374,6 +10961,11 @@ var SettingsDialog = class {
|
|
|
10374
10961
|
setHostSections(sections) {
|
|
10375
10962
|
this.hostSections = sections;
|
|
10376
10963
|
}
|
|
10964
|
+
/** The timeline-mark groups and their current visibility — the Events tab's rows on next open. */
|
|
10965
|
+
setMarkGroups(groups, visible) {
|
|
10966
|
+
this.markGroups = groups;
|
|
10967
|
+
this.markGroupVisible = visible;
|
|
10968
|
+
}
|
|
10377
10969
|
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
10378
10970
|
setHiddenSettings(ids) {
|
|
10379
10971
|
const next = new Set(ids);
|
|
@@ -10547,12 +11139,36 @@ var SettingsDialog = class {
|
|
|
10547
11139
|
}
|
|
10548
11140
|
showActive(config.series.style);
|
|
10549
11141
|
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
11142
|
+
body.append(sid(this.boolRow(
|
|
11143
|
+
"Animate zoom",
|
|
11144
|
+
config.animations.zoom,
|
|
11145
|
+
(v) => this.emit({ animations: { zoom: v } }),
|
|
11146
|
+
this.hint("Glide the chart to each zoom step instead of jumping.")
|
|
11147
|
+
), "symbol.animation.zoom"));
|
|
11148
|
+
body.append(sid(this.boolRow(
|
|
11149
|
+
"Pan momentum",
|
|
11150
|
+
config.animations.pan,
|
|
11151
|
+
(v) => this.emit({ animations: { pan: v } }),
|
|
11152
|
+
this.hint("Keep gliding briefly after a drag release, and ease scroll-to-latest and keyboard pans.")
|
|
11153
|
+
), "symbol.animation.pan"));
|
|
11154
|
+
body.append(sid(this.boolRow(
|
|
11155
|
+
"Animate price scale",
|
|
11156
|
+
config.animations.autoscale,
|
|
11157
|
+
(v) => this.emit({ animations: { autoscale: v } }),
|
|
11158
|
+
this.hint("Glide the price scale to its new range while zooming or panning.")
|
|
11159
|
+
), "symbol.animation.autoscale"));
|
|
10550
11160
|
body.append(sid(this.boolRow(
|
|
10551
11161
|
"Animate price changes",
|
|
10552
11162
|
config.priceScale.animateLastPrice,
|
|
10553
11163
|
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
10554
11164
|
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
10555
11165
|
), "symbol.animation.price-changes"));
|
|
11166
|
+
body.append(sid(this.boolRow(
|
|
11167
|
+
"Reveal on load",
|
|
11168
|
+
config.animations.intro,
|
|
11169
|
+
(v) => this.emit({ animations: { intro: v } }),
|
|
11170
|
+
this.hint("Draw the candles in when a chart first loads. Takes effect on the next load.")
|
|
11171
|
+
), "symbol.animation.intro"));
|
|
10556
11172
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
10557
11173
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
10558
11174
|
const renderHostSections = (placement) => {
|
|
@@ -10622,6 +11238,13 @@ var SettingsDialog = class {
|
|
|
10622
11238
|
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
10623
11239
|
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
10624
11240
|
}
|
|
11241
|
+
if (this.markGroups.length > 0) {
|
|
11242
|
+
body.append(sid(this.section("Events"), MARKS_SETTINGS_ID));
|
|
11243
|
+
body.append(sid(this.sectionTitle("Visible events"), MARKS_GROUPS_SETTINGS_ID));
|
|
11244
|
+
for (const g of this.markGroups) {
|
|
11245
|
+
body.append(sid(this.boolRow(g.label, this.markGroupVisible(g.id), (v) => this.emit({ marks: { groups: { [g.id]: v } } })), markGroupSettingsId(g.id)));
|
|
11246
|
+
}
|
|
11247
|
+
}
|
|
10625
11248
|
renderChartTypeSections("end");
|
|
10626
11249
|
renderHostSections("end");
|
|
10627
11250
|
if (this.hiddenSettings.size > 0) {
|
|
@@ -16193,6 +16816,383 @@ function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
|
16193
16816
|
return { ...scale, min: scale.min - belowPx * perPx, max: scale.max + abovePx * perPx };
|
|
16194
16817
|
}
|
|
16195
16818
|
|
|
16819
|
+
// src/ui/sanitize-html.ts
|
|
16820
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
16821
|
+
"a",
|
|
16822
|
+
"abbr",
|
|
16823
|
+
"b",
|
|
16824
|
+
"blockquote",
|
|
16825
|
+
"br",
|
|
16826
|
+
"code",
|
|
16827
|
+
"dd",
|
|
16828
|
+
"del",
|
|
16829
|
+
"div",
|
|
16830
|
+
"dl",
|
|
16831
|
+
"dt",
|
|
16832
|
+
"em",
|
|
16833
|
+
"h1",
|
|
16834
|
+
"h2",
|
|
16835
|
+
"h3",
|
|
16836
|
+
"h4",
|
|
16837
|
+
"h5",
|
|
16838
|
+
"h6",
|
|
16839
|
+
"hr",
|
|
16840
|
+
"i",
|
|
16841
|
+
"img",
|
|
16842
|
+
"ins",
|
|
16843
|
+
"kbd",
|
|
16844
|
+
"li",
|
|
16845
|
+
"mark",
|
|
16846
|
+
"ol",
|
|
16847
|
+
"p",
|
|
16848
|
+
"pre",
|
|
16849
|
+
"q",
|
|
16850
|
+
"s",
|
|
16851
|
+
"small",
|
|
16852
|
+
"span",
|
|
16853
|
+
"strong",
|
|
16854
|
+
"sub",
|
|
16855
|
+
"sup",
|
|
16856
|
+
"table",
|
|
16857
|
+
"tbody",
|
|
16858
|
+
"td",
|
|
16859
|
+
"tfoot",
|
|
16860
|
+
"th",
|
|
16861
|
+
"thead",
|
|
16862
|
+
"tr",
|
|
16863
|
+
"u",
|
|
16864
|
+
"ul"
|
|
16865
|
+
]);
|
|
16866
|
+
var DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
16867
|
+
"script",
|
|
16868
|
+
"style",
|
|
16869
|
+
"iframe",
|
|
16870
|
+
"frame",
|
|
16871
|
+
"frameset",
|
|
16872
|
+
"object",
|
|
16873
|
+
"embed",
|
|
16874
|
+
"applet",
|
|
16875
|
+
"form",
|
|
16876
|
+
"input",
|
|
16877
|
+
"textarea",
|
|
16878
|
+
"button",
|
|
16879
|
+
"select",
|
|
16880
|
+
"option",
|
|
16881
|
+
"link",
|
|
16882
|
+
"meta",
|
|
16883
|
+
"base",
|
|
16884
|
+
"svg",
|
|
16885
|
+
"math",
|
|
16886
|
+
"template",
|
|
16887
|
+
"noscript",
|
|
16888
|
+
"audio",
|
|
16889
|
+
"video",
|
|
16890
|
+
"canvas",
|
|
16891
|
+
"dialog",
|
|
16892
|
+
"head",
|
|
16893
|
+
"title"
|
|
16894
|
+
]);
|
|
16895
|
+
var ALLOWED_ATTRS = {
|
|
16896
|
+
a: /* @__PURE__ */ new Set(["href"]),
|
|
16897
|
+
img: /* @__PURE__ */ new Set(["src", "alt", "width", "height"]),
|
|
16898
|
+
td: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
16899
|
+
th: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
16900
|
+
ol: /* @__PURE__ */ new Set(["start"])
|
|
16901
|
+
};
|
|
16902
|
+
var BLOCKED_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript", "data", "file", "blob"]);
|
|
16903
|
+
function tagDisposition(tag) {
|
|
16904
|
+
const t = tag.toLowerCase();
|
|
16905
|
+
if (DROPPED_TAGS.has(t)) return "drop";
|
|
16906
|
+
return ALLOWED_TAGS.has(t) ? "keep" : "unwrap";
|
|
16907
|
+
}
|
|
16908
|
+
function attributeAllowed(tag, name) {
|
|
16909
|
+
const n = name.toLowerCase();
|
|
16910
|
+
if (n.startsWith("on") || n === "style") return false;
|
|
16911
|
+
if (n === "title") return true;
|
|
16912
|
+
return ALLOWED_ATTRS[tag.toLowerCase()]?.has(n) ?? false;
|
|
16913
|
+
}
|
|
16914
|
+
function safeUrl(value, absoluteOnly = false) {
|
|
16915
|
+
let url = "";
|
|
16916
|
+
for (const ch of value) if (ch.charCodeAt(0) > 32) url += ch;
|
|
16917
|
+
const m = /^([a-z][a-z0-9+.-]*):/i.exec(url);
|
|
16918
|
+
const scheme = m ? m[1].toLowerCase() : null;
|
|
16919
|
+
if (scheme !== null && BLOCKED_SCHEMES.has(scheme)) return null;
|
|
16920
|
+
if (absoluteOnly && scheme !== "http" && scheme !== "https") return null;
|
|
16921
|
+
return url;
|
|
16922
|
+
}
|
|
16923
|
+
var ELEMENT_NODE = 1;
|
|
16924
|
+
var TEXT_NODE = 3;
|
|
16925
|
+
function sanitizeHtml(html, doc) {
|
|
16926
|
+
const tpl = doc.createElement("template");
|
|
16927
|
+
tpl.innerHTML = html;
|
|
16928
|
+
const out = doc.createDocumentFragment();
|
|
16929
|
+
copyChildren(tpl.content, out, doc);
|
|
16930
|
+
return out;
|
|
16931
|
+
}
|
|
16932
|
+
function copyChildren(from, to, doc) {
|
|
16933
|
+
for (const child of Array.from(from.childNodes)) {
|
|
16934
|
+
if (child.nodeType === TEXT_NODE) {
|
|
16935
|
+
to.appendChild(doc.createTextNode(child.textContent ?? ""));
|
|
16936
|
+
continue;
|
|
16937
|
+
}
|
|
16938
|
+
if (child.nodeType !== ELEMENT_NODE) continue;
|
|
16939
|
+
const el = child;
|
|
16940
|
+
const tag = el.tagName.toLowerCase();
|
|
16941
|
+
const disposition = tagDisposition(tag);
|
|
16942
|
+
if (disposition === "drop") continue;
|
|
16943
|
+
if (disposition === "unwrap") {
|
|
16944
|
+
copyChildren(el, to, doc);
|
|
16945
|
+
continue;
|
|
16946
|
+
}
|
|
16947
|
+
const clean = doc.createElement(tag);
|
|
16948
|
+
for (const attr of Array.from(el.attributes)) {
|
|
16949
|
+
const name = attr.name.toLowerCase();
|
|
16950
|
+
if (!attributeAllowed(tag, name)) continue;
|
|
16951
|
+
let value = attr.value;
|
|
16952
|
+
if (name === "href" || name === "src") {
|
|
16953
|
+
const safe = safeUrl(value, name === "src");
|
|
16954
|
+
if (safe === null) continue;
|
|
16955
|
+
value = safe;
|
|
16956
|
+
}
|
|
16957
|
+
clean.setAttribute(name, value);
|
|
16958
|
+
}
|
|
16959
|
+
if (tag === "a") {
|
|
16960
|
+
clean.setAttribute("target", "_blank");
|
|
16961
|
+
clean.setAttribute("rel", "noopener noreferrer");
|
|
16962
|
+
}
|
|
16963
|
+
copyChildren(el, clean, doc);
|
|
16964
|
+
to.appendChild(clean);
|
|
16965
|
+
}
|
|
16966
|
+
}
|
|
16967
|
+
|
|
16968
|
+
// src/renderers/native/chrome/marks/MarkPopover.ts
|
|
16969
|
+
var MARKS_STYLE_ID = "vela-marks-popover";
|
|
16970
|
+
var MARKS_CSS = `
|
|
16971
|
+
.vela-marks-panel { padding: 0; gap: 0; min-width: 220px; max-width: 320px; max-height: 320px; overflow-y: auto; overscroll-behavior: contain; }
|
|
16972
|
+
.vela-marks-section { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; }
|
|
16973
|
+
.vela-marks-section + .vela-marks-section { border-top: 1px solid var(--vela-border); }
|
|
16974
|
+
.vela-marks-field { display: flex; justify-content: space-between; gap: 16px; line-height: 1.45; }
|
|
16975
|
+
.vela-marks-field-label { color: var(--vela-fg-muted); }
|
|
16976
|
+
.vela-marks-field-value { color: var(--vela-fg-bright); text-align: right; font-variant-numeric: tabular-nums; }
|
|
16977
|
+
.vela-marks-html { color: var(--vela-fg); line-height: 1.45; overflow-wrap: anywhere; }
|
|
16978
|
+
.vela-marks-html p { margin: 0 0 6px; }
|
|
16979
|
+
.vela-marks-html p:last-child { margin-bottom: 0; }
|
|
16980
|
+
.vela-marks-html a { color: var(--vela-accent); }
|
|
16981
|
+
.vela-marks-html img { max-width: 100%; height: auto; }
|
|
16982
|
+
.vela-marks-html table { border-collapse: collapse; }
|
|
16983
|
+
.vela-marks-html td, .vela-marks-html th { padding: 2px 6px; border: 1px solid var(--vela-border); }
|
|
16984
|
+
.vela-marks-html pre, .vela-marks-html code { font-family: var(--vela-font-mono, monospace); font-size: 0.92em; }
|
|
16985
|
+
.vela-marks-loading, .vela-marks-error { color: var(--vela-fg-muted); font-style: italic; }
|
|
16986
|
+
`;
|
|
16987
|
+
var MarkPopover = class {
|
|
16988
|
+
constructor(deps) {
|
|
16989
|
+
this.deps = deps;
|
|
16990
|
+
this.pop = null;
|
|
16991
|
+
this.openKey = null;
|
|
16992
|
+
/** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
|
|
16993
|
+
this.generation = 0;
|
|
16994
|
+
const doc = deps.plot.ownerDocument;
|
|
16995
|
+
injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
|
|
16996
|
+
injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
|
|
16997
|
+
this.anchor = doc.createElement("div");
|
|
16998
|
+
this.anchor.className = "vela-marks-anchor";
|
|
16999
|
+
Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
|
|
17000
|
+
deps.plot.appendChild(this.anchor);
|
|
17001
|
+
}
|
|
17002
|
+
/** The cluster key the open popup belongs to, or null. */
|
|
17003
|
+
get key() {
|
|
17004
|
+
return this.openKey;
|
|
17005
|
+
}
|
|
17006
|
+
open(cluster, rect) {
|
|
17007
|
+
this.close();
|
|
17008
|
+
this.place(rect);
|
|
17009
|
+
const gen = ++this.generation;
|
|
17010
|
+
this.openKey = cluster.key;
|
|
17011
|
+
this.pop = new Popover({
|
|
17012
|
+
trigger: this.anchor,
|
|
17013
|
+
host: this.deps.host(),
|
|
17014
|
+
theme: this.deps.theme(),
|
|
17015
|
+
gap: 8,
|
|
17016
|
+
align: "center",
|
|
17017
|
+
// centered on the glyph
|
|
17018
|
+
fadeMs: 120,
|
|
17019
|
+
// a short, discreet fade in and out
|
|
17020
|
+
className: "vela-marks-pop",
|
|
17021
|
+
content: (body) => this.build(body, cluster, gen),
|
|
17022
|
+
onClose: () => {
|
|
17023
|
+
if (this.generation === gen) {
|
|
17024
|
+
this.openKey = null;
|
|
17025
|
+
this.pop = null;
|
|
17026
|
+
this.deps.onOpenChange?.(null);
|
|
17027
|
+
}
|
|
17028
|
+
}
|
|
17029
|
+
});
|
|
17030
|
+
this.pop.show();
|
|
17031
|
+
this.deps.onOpenChange?.(cluster.key);
|
|
17032
|
+
}
|
|
17033
|
+
/** Follow the anchor glyph after a repaint; `null` (glyph gone — hidden, scrolled off, marks replaced) closes. */
|
|
17034
|
+
track(rect) {
|
|
17035
|
+
if (!this.pop) return;
|
|
17036
|
+
if (!rect) {
|
|
17037
|
+
this.close();
|
|
17038
|
+
return;
|
|
17039
|
+
}
|
|
17040
|
+
this.place(rect);
|
|
17041
|
+
this.pop.reposition();
|
|
17042
|
+
}
|
|
17043
|
+
close() {
|
|
17044
|
+
const pop = this.pop;
|
|
17045
|
+
const wasOpen = this.openKey !== null;
|
|
17046
|
+
this.pop = null;
|
|
17047
|
+
this.openKey = null;
|
|
17048
|
+
this.generation++;
|
|
17049
|
+
pop?.destroy();
|
|
17050
|
+
if (wasOpen) this.deps.onOpenChange?.(null);
|
|
17051
|
+
}
|
|
17052
|
+
destroy() {
|
|
17053
|
+
this.close();
|
|
17054
|
+
this.anchor.remove();
|
|
17055
|
+
}
|
|
17056
|
+
place(rect) {
|
|
17057
|
+
Object.assign(this.anchor.style, {
|
|
17058
|
+
left: `${rect.x - rect.size / 2}px`,
|
|
17059
|
+
top: `${rect.y - rect.size / 2}px`,
|
|
17060
|
+
width: `${rect.size}px`,
|
|
17061
|
+
height: `${rect.size}px`
|
|
17062
|
+
});
|
|
17063
|
+
}
|
|
17064
|
+
build(body, cluster, gen) {
|
|
17065
|
+
const doc = body.ownerDocument;
|
|
17066
|
+
const root = doc.createElement("div");
|
|
17067
|
+
root.className = "vela-callout-panel vela-marks-panel";
|
|
17068
|
+
const pending = [];
|
|
17069
|
+
for (const mark of cluster.marks) {
|
|
17070
|
+
const section = doc.createElement("section");
|
|
17071
|
+
section.className = "vela-marks-section";
|
|
17072
|
+
if (mark.title) {
|
|
17073
|
+
const title = doc.createElement("div");
|
|
17074
|
+
title.className = "vela-callout-title";
|
|
17075
|
+
title.textContent = mark.title;
|
|
17076
|
+
section.appendChild(title);
|
|
17077
|
+
}
|
|
17078
|
+
const content = mark.content;
|
|
17079
|
+
if (typeof content === "function") {
|
|
17080
|
+
const slot = doc.createElement("div");
|
|
17081
|
+
slot.className = "vela-marks-loading";
|
|
17082
|
+
slot.textContent = "Loading\u2026";
|
|
17083
|
+
section.appendChild(slot);
|
|
17084
|
+
pending.push({ el: section, resolve: () => this.resolveLazy(content, slot, gen) });
|
|
17085
|
+
} else if (content !== void 0) {
|
|
17086
|
+
this.renderContent(section, content);
|
|
17087
|
+
}
|
|
17088
|
+
if (section.childElementCount > 0) root.appendChild(section);
|
|
17089
|
+
}
|
|
17090
|
+
body.appendChild(root);
|
|
17091
|
+
if (pending.length > 0) scheduleLazy(root, pending);
|
|
17092
|
+
}
|
|
17093
|
+
resolveLazy(source, slot, gen) {
|
|
17094
|
+
let result;
|
|
17095
|
+
try {
|
|
17096
|
+
result = Promise.resolve(source());
|
|
17097
|
+
} catch (err) {
|
|
17098
|
+
result = Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
17099
|
+
}
|
|
17100
|
+
void result.then(
|
|
17101
|
+
(content) => {
|
|
17102
|
+
if (gen !== this.generation) return;
|
|
17103
|
+
const section = slot.parentElement;
|
|
17104
|
+
if (!section) return;
|
|
17105
|
+
slot.remove();
|
|
17106
|
+
this.renderContent(section, content);
|
|
17107
|
+
this.pop?.reposition();
|
|
17108
|
+
},
|
|
17109
|
+
() => {
|
|
17110
|
+
if (gen !== this.generation) return;
|
|
17111
|
+
slot.className = "vela-marks-error";
|
|
17112
|
+
slot.textContent = "Couldn\u2019t load this entry.";
|
|
17113
|
+
}
|
|
17114
|
+
);
|
|
17115
|
+
}
|
|
17116
|
+
renderContent(section, content) {
|
|
17117
|
+
const doc = section.ownerDocument;
|
|
17118
|
+
if (!content || typeof content !== "object") return;
|
|
17119
|
+
if ("text" in content) {
|
|
17120
|
+
const text = doc.createElement("div");
|
|
17121
|
+
text.className = "vela-callout-text";
|
|
17122
|
+
text.textContent = String(content.text);
|
|
17123
|
+
section.appendChild(text);
|
|
17124
|
+
return;
|
|
17125
|
+
}
|
|
17126
|
+
if ("html" in content) {
|
|
17127
|
+
const html = doc.createElement("div");
|
|
17128
|
+
html.className = "vela-marks-html";
|
|
17129
|
+
html.appendChild(sanitizeHtml(String(content.html), doc));
|
|
17130
|
+
section.appendChild(html);
|
|
17131
|
+
return;
|
|
17132
|
+
}
|
|
17133
|
+
if ("panel" in content && content.panel && Array.isArray(content.panel.items)) {
|
|
17134
|
+
let actions = null;
|
|
17135
|
+
for (const item of content.panel.items) {
|
|
17136
|
+
if (!item || typeof item !== "object") continue;
|
|
17137
|
+
if (item.type === "button") {
|
|
17138
|
+
if (!actions) {
|
|
17139
|
+
actions = doc.createElement("div");
|
|
17140
|
+
actions.className = "vela-callout-actions";
|
|
17141
|
+
section.appendChild(actions);
|
|
17142
|
+
}
|
|
17143
|
+
const btn2 = doc.createElement("button");
|
|
17144
|
+
btn2.type = "button";
|
|
17145
|
+
btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
17146
|
+
btn2.textContent = item.label;
|
|
17147
|
+
btn2.addEventListener("click", () => {
|
|
17148
|
+
item.run();
|
|
17149
|
+
if (item.close !== false) this.close();
|
|
17150
|
+
});
|
|
17151
|
+
actions.appendChild(btn2);
|
|
17152
|
+
continue;
|
|
17153
|
+
}
|
|
17154
|
+
actions = null;
|
|
17155
|
+
if (item.type === "text") {
|
|
17156
|
+
const text = doc.createElement("div");
|
|
17157
|
+
text.className = "vela-callout-text";
|
|
17158
|
+
text.textContent = item.text;
|
|
17159
|
+
section.appendChild(text);
|
|
17160
|
+
} else if (item.type === "field") {
|
|
17161
|
+
const row = doc.createElement("div");
|
|
17162
|
+
row.className = "vela-marks-field";
|
|
17163
|
+
const label = doc.createElement("span");
|
|
17164
|
+
label.className = "vela-marks-field-label";
|
|
17165
|
+
label.textContent = item.label;
|
|
17166
|
+
const value = doc.createElement("span");
|
|
17167
|
+
value.className = "vela-marks-field-value";
|
|
17168
|
+
value.textContent = item.value;
|
|
17169
|
+
row.append(label, value);
|
|
17170
|
+
section.appendChild(row);
|
|
17171
|
+
}
|
|
17172
|
+
}
|
|
17173
|
+
}
|
|
17174
|
+
}
|
|
17175
|
+
};
|
|
17176
|
+
function scheduleLazy(scroller, pending) {
|
|
17177
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
17178
|
+
for (const p of pending) p.resolve();
|
|
17179
|
+
return;
|
|
17180
|
+
}
|
|
17181
|
+
const io = new IntersectionObserver(
|
|
17182
|
+
(entries) => {
|
|
17183
|
+
for (const e of entries) {
|
|
17184
|
+
if (!e.isIntersecting) continue;
|
|
17185
|
+
const p = pending.find((q) => q.el === e.target);
|
|
17186
|
+
if (!p) continue;
|
|
17187
|
+
io.unobserve(e.target);
|
|
17188
|
+
p.resolve();
|
|
17189
|
+
}
|
|
17190
|
+
},
|
|
17191
|
+
{ root: scroller }
|
|
17192
|
+
);
|
|
17193
|
+
for (const p of pending) io.observe(p.el);
|
|
17194
|
+
}
|
|
17195
|
+
|
|
16196
17196
|
// src/renderers/native/core/manualScale.ts
|
|
16197
17197
|
function rescaleAround(start, factor) {
|
|
16198
17198
|
if (start.log && start.min > 0 && start.max > start.min) {
|
|
@@ -16895,11 +17895,12 @@ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
|
16895
17895
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
16896
17896
|
var MIN_VISIBLE_BARS = 2;
|
|
16897
17897
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
16898
|
-
var
|
|
16899
|
-
var SCALE_TAU_MS = 80;
|
|
16900
|
-
var FLING_TAU_MS = 110;
|
|
16901
|
-
var SCROLL_TO_TAU_MS = 130;
|
|
17898
|
+
var INTRO_MODEL_FADE_MS = 350;
|
|
16902
17899
|
var FLING_STOP_PX = 0.02;
|
|
17900
|
+
var MARK_FLASH_MS = 220;
|
|
17901
|
+
function frameNow() {
|
|
17902
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
17903
|
+
}
|
|
16903
17904
|
var PRICE_SCALE_K = 4e-3;
|
|
16904
17905
|
var KEY_ZOOM_STEP = 0.2;
|
|
16905
17906
|
var SEPARATOR_HIT_PX = 4;
|
|
@@ -16948,9 +17949,20 @@ var NativeRenderer = class {
|
|
|
16948
17949
|
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
16949
17950
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
16950
17951
|
this.labelTooltip = null;
|
|
17952
|
+
/** The timeline-mark popup (a kit Popover anchored on a lane glyph); null before mount. */
|
|
17953
|
+
this.markPopover = null;
|
|
17954
|
+
/** How the fanned mark stack was opened: a hover folds when the pointer leaves, a tap only on a tap elsewhere. */
|
|
17955
|
+
this.marksExpandedBy = "hover";
|
|
17956
|
+
/** The rAF loop keeping the chrome repainting while a lane glyph pulses (hover) or flashes (click); null when idle. */
|
|
17957
|
+
this.markPulseRaf = null;
|
|
17958
|
+
this.markClickCbs = /* @__PURE__ */ new Set();
|
|
16951
17959
|
this.crosshairLayer = new CrosshairRenderer();
|
|
16952
|
-
/**
|
|
16953
|
-
|
|
17960
|
+
/** The second pulse the countdown-to-bar-close chip ticks on: the host's (`setWallClock`)
|
|
17961
|
+
* when one is wired, else the renderer's own second-aligned clock. */
|
|
17962
|
+
this.hostClock = null;
|
|
17963
|
+
this.ownClock = null;
|
|
17964
|
+
/** Live subscription to the pulse while the countdown is on; null when off. */
|
|
17965
|
+
this.countdownUnsub = null;
|
|
16954
17966
|
this.symbolPicker = null;
|
|
16955
17967
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
16956
17968
|
this.indicatorTitlesOn = true;
|
|
@@ -16968,19 +17980,25 @@ var NativeRenderer = class {
|
|
|
16968
17980
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
16969
17981
|
this.historyChordsEnabled = true;
|
|
16970
17982
|
this.liveRegion = null;
|
|
16971
|
-
// ── animation state
|
|
16972
|
-
|
|
16973
|
-
this.
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
17983
|
+
// ── animation state: one ease time-constant per motion (0 = off), each remembering the
|
|
17984
|
+
// host's duration so the config's on/off switches restore it (see EaseSetting) ──
|
|
17985
|
+
this.animZoom = new EaseSetting(ZOOM_EASE_DEFAULT_MS);
|
|
17986
|
+
// wheel-zoom glide
|
|
17987
|
+
this.animPan = new EaseSetting(PAN_INERTIA_DEFAULT_MS);
|
|
17988
|
+
// inertial-pan velocity decay
|
|
17989
|
+
this.animScroll = new EaseSetting(SCROLL_EASE_DEFAULT_MS);
|
|
17990
|
+
// scroll-to-latest / panBy glide
|
|
17991
|
+
this.animAutoscale = new EaseSetting(AUTOSCALE_EASE_DEFAULT_MS);
|
|
17992
|
+
// autoscale glide during zoom/fling
|
|
17993
|
+
this.animLiveBar = new EaseSetting(LIVE_BAR_EASE_DEFAULT_MS, 0);
|
|
17994
|
+
// forming-bar OHLC glide; ships off
|
|
16978
17995
|
// Brand default candles.
|
|
16979
17996
|
this.candleUp = BULLISH;
|
|
16980
17997
|
this.candleDown = BEARISH;
|
|
16981
17998
|
// ── intro reveal (plays once when candles first appear) ──
|
|
16982
|
-
this.
|
|
16983
|
-
|
|
17999
|
+
this.intro = { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
18000
|
+
this.introOnStyle = "settle";
|
|
18001
|
+
// the style the config's on/off switch restores
|
|
16984
18002
|
this.introPlayed = false;
|
|
16985
18003
|
this.introRaf = null;
|
|
16986
18004
|
/** The load affordance (three pulsing dots) — up while the host reports a bar load in
|
|
@@ -17099,7 +18117,7 @@ var NativeRenderer = class {
|
|
|
17099
18117
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
17100
18118
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
17101
18119
|
this.name = "native";
|
|
17102
|
-
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animLiveBar", "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"];
|
|
18120
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "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", "marks", "indicatorTitles", "indicatorValues"];
|
|
17103
18121
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
17104
18122
|
* so moving onto the button doesn't count as leaving). */
|
|
17105
18123
|
this.onScrollProximityMove = (e) => {
|
|
@@ -17131,9 +18149,12 @@ var NativeRenderer = class {
|
|
|
17131
18149
|
this.scene.showPriceLine = opts.currentPriceLine;
|
|
17132
18150
|
this.scene.logScale = opts.logScale;
|
|
17133
18151
|
this.backendMode = opts.nativeBackend;
|
|
17134
|
-
this.animZoom
|
|
17135
|
-
this.animPan
|
|
17136
|
-
this.
|
|
18152
|
+
this.animZoom.set(opts.animZoom);
|
|
18153
|
+
this.animPan.set(opts.animPan);
|
|
18154
|
+
this.animScroll.set(opts.animScroll);
|
|
18155
|
+
this.animAutoscale.set(opts.animAutoscale);
|
|
18156
|
+
this.animLiveBar.set(opts.animLiveBar);
|
|
18157
|
+
this.setIntro(opts.animIntro);
|
|
17137
18158
|
this.glowAmount = opts.glow;
|
|
17138
18159
|
this.candleUp = opts.upColor;
|
|
17139
18160
|
this.candleDown = opts.downColor;
|
|
@@ -17177,20 +18198,29 @@ var NativeRenderer = class {
|
|
|
17177
18198
|
if (this.backend && "glow" in this.backend) this.backend.glow = this.glowAmount;
|
|
17178
18199
|
break;
|
|
17179
18200
|
case "animZoom":
|
|
17180
|
-
this.animZoom
|
|
18201
|
+
this.animZoom.set(resolveEaseMs(value, ZOOM_EASE_DEFAULT_MS));
|
|
17181
18202
|
return;
|
|
17182
18203
|
// affects the next interaction only — nothing to repaint
|
|
17183
|
-
case "animPan":
|
|
17184
|
-
|
|
18204
|
+
case "animPan": {
|
|
18205
|
+
const ms = resolveEaseMs(value, PAN_INERTIA_DEFAULT_MS);
|
|
18206
|
+
this.animPan.set(ms);
|
|
18207
|
+
this.animScroll.toggle(ms > 0);
|
|
18208
|
+
return;
|
|
18209
|
+
}
|
|
18210
|
+
case "animScroll":
|
|
18211
|
+
this.animScroll.set(resolveEaseMs(value, SCROLL_EASE_DEFAULT_MS));
|
|
18212
|
+
return;
|
|
18213
|
+
case "animAutoscale":
|
|
18214
|
+
this.animAutoscale.set(resolveEaseMs(value, AUTOSCALE_EASE_DEFAULT_MS));
|
|
17185
18215
|
return;
|
|
18216
|
+
// a glide in flight finishes at the new rate (or snaps at 0)
|
|
17186
18217
|
case "animLiveBar":
|
|
17187
|
-
this.
|
|
18218
|
+
this.animLiveBar.set(resolveLiveBarEaseMs(value));
|
|
17188
18219
|
return;
|
|
17189
18220
|
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
17190
18221
|
case "intro": {
|
|
17191
|
-
|
|
17192
|
-
this.
|
|
17193
|
-
if (s) this.playIntro(s);
|
|
18222
|
+
this.setIntro(resolveIntro(value));
|
|
18223
|
+
if (this.intro.style) this.playIntro();
|
|
17194
18224
|
return;
|
|
17195
18225
|
}
|
|
17196
18226
|
case "zoomAnchor":
|
|
@@ -17262,6 +18292,10 @@ var NativeRenderer = class {
|
|
|
17262
18292
|
case "tradeMarkers":
|
|
17263
18293
|
this.scene.tradeMarkers = mergeTradeMarkersState(this.scene.tradeMarkers, value);
|
|
17264
18294
|
break;
|
|
18295
|
+
case "marks":
|
|
18296
|
+
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
18297
|
+
this.markPopover?.close();
|
|
18298
|
+
break;
|
|
17265
18299
|
case "keyboard":
|
|
17266
18300
|
this.setKeyboardEnabled(Boolean(value));
|
|
17267
18301
|
return;
|
|
@@ -17320,13 +18354,17 @@ var NativeRenderer = class {
|
|
|
17320
18354
|
case "glow":
|
|
17321
18355
|
return this.glowAmount;
|
|
17322
18356
|
case "animZoom":
|
|
17323
|
-
return this.animZoom;
|
|
18357
|
+
return this.animZoom.tau;
|
|
17324
18358
|
case "animPan":
|
|
17325
|
-
return this.animPan;
|
|
18359
|
+
return this.animPan.tau;
|
|
18360
|
+
case "animScroll":
|
|
18361
|
+
return this.animScroll.tau;
|
|
18362
|
+
case "animAutoscale":
|
|
18363
|
+
return this.animAutoscale.tau;
|
|
17326
18364
|
case "animLiveBar":
|
|
17327
|
-
return this.
|
|
18365
|
+
return this.animLiveBar.tau;
|
|
17328
18366
|
case "intro":
|
|
17329
|
-
return this.
|
|
18367
|
+
return this.intro.style;
|
|
17330
18368
|
case "zoomAnchor":
|
|
17331
18369
|
return this.zoomAnchorMode;
|
|
17332
18370
|
case "axisDrag":
|
|
@@ -17369,6 +18407,8 @@ var NativeRenderer = class {
|
|
|
17369
18407
|
}
|
|
17370
18408
|
case "tradeMarkers":
|
|
17371
18409
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
18410
|
+
case "marks":
|
|
18411
|
+
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
17372
18412
|
case "keyboard":
|
|
17373
18413
|
return this.keyboardEnabled;
|
|
17374
18414
|
case "historyChords":
|
|
@@ -17494,7 +18534,13 @@ var NativeRenderer = class {
|
|
|
17494
18534
|
currentPriceLine: this.scene.showPriceLine,
|
|
17495
18535
|
priceLabel: this.scene.showPriceLabel,
|
|
17496
18536
|
countdown: this.scene.showCountdown,
|
|
17497
|
-
animateLastPrice: this.
|
|
18537
|
+
animateLastPrice: this.animLiveBar.on
|
|
18538
|
+
},
|
|
18539
|
+
animations: {
|
|
18540
|
+
zoom: this.animZoom.on,
|
|
18541
|
+
pan: this.animPan.on,
|
|
18542
|
+
autoscale: this.animAutoscale.on,
|
|
18543
|
+
intro: this.intro.style !== false
|
|
17498
18544
|
},
|
|
17499
18545
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
17500
18546
|
trades: {
|
|
@@ -17506,6 +18552,7 @@ var NativeRenderer = class {
|
|
|
17506
18552
|
exitColor: this.scene.tradeMarkers.colors.exit
|
|
17507
18553
|
},
|
|
17508
18554
|
timeScale: { timezone: this.scene.timezone },
|
|
18555
|
+
marks: { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } },
|
|
17509
18556
|
candles: {
|
|
17510
18557
|
upColor: this.candleUp,
|
|
17511
18558
|
downColor: this.candleDown,
|
|
@@ -17601,7 +18648,12 @@ var NativeRenderer = class {
|
|
|
17601
18648
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
17602
18649
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
17603
18650
|
this.syncCountdownTimer();
|
|
17604
|
-
this.
|
|
18651
|
+
this.animLiveBar.toggle(next.priceScale.animateLastPrice);
|
|
18652
|
+
this.animZoom.toggle(next.animations.zoom);
|
|
18653
|
+
this.animPan.toggle(next.animations.pan);
|
|
18654
|
+
this.animScroll.toggle(next.animations.pan);
|
|
18655
|
+
this.animAutoscale.toggle(next.animations.autoscale);
|
|
18656
|
+
this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
|
|
17605
18657
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
17606
18658
|
this.scene.tradeMarkers = {
|
|
17607
18659
|
visible: next.trades.visible,
|
|
@@ -17610,6 +18662,7 @@ var NativeRenderer = class {
|
|
|
17610
18662
|
colors: { long: next.trades.longColor, short: next.trades.shortColor, exit: next.trades.exitColor }
|
|
17611
18663
|
};
|
|
17612
18664
|
this.scene.timezone = next.timeScale.timezone;
|
|
18665
|
+
this.scene.marks = { visible: next.marks.visible, groups: { ...next.marks.groups } };
|
|
17613
18666
|
this.candleUp = next.candles.upColor;
|
|
17614
18667
|
this.candleDown = next.candles.downColor;
|
|
17615
18668
|
s.candle = {
|
|
@@ -17742,6 +18795,7 @@ var NativeRenderer = class {
|
|
|
17742
18795
|
}
|
|
17743
18796
|
this.settingsDialog.setTheme(this.theme);
|
|
17744
18797
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
18798
|
+
this.settingsDialog.setMarkGroups(this.markGroupsInUse(), (id) => markGroupVisible(this.scene.marks, id, this.scene.markGroups));
|
|
17745
18799
|
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
17746
18800
|
this.syncThemeControl();
|
|
17747
18801
|
this.settingsDialog.toggle(
|
|
@@ -17832,10 +18886,10 @@ var NativeRenderer = class {
|
|
|
17832
18886
|
this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
|
|
17833
18887
|
}
|
|
17834
18888
|
/** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
|
|
17835
|
-
* instant when
|
|
18889
|
+
* instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
|
|
17836
18890
|
glideRightOffset(target) {
|
|
17837
18891
|
const vp = this.coords.getViewport();
|
|
17838
|
-
if (!this.
|
|
18892
|
+
if (!this.animScroll.on) {
|
|
17839
18893
|
this.applyViewport({ barSpacing: vp.barSpacing, rightOffset: target });
|
|
17840
18894
|
return;
|
|
17841
18895
|
}
|
|
@@ -17900,20 +18954,21 @@ var NativeRenderer = class {
|
|
|
17900
18954
|
* full size, eased, with a left→right stagger so the chart draws itself; `settle`
|
|
17901
18955
|
* adds an ease-out-back overshoot. Autoscale stays on the real bars so the frame
|
|
17902
18956
|
* never moves. Re-callable, so styles can be compared live from the console.
|
|
18957
|
+
* Style and sweep duration come from the resolved `intro` setting.
|
|
17903
18958
|
*/
|
|
17904
|
-
playIntro(
|
|
18959
|
+
playIntro() {
|
|
17905
18960
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
17906
18961
|
this.introRaf = null;
|
|
18962
|
+
const { style, duration } = this.intro;
|
|
17907
18963
|
const real = this.bars;
|
|
17908
18964
|
const n = real.length;
|
|
17909
|
-
if (n === 0) return;
|
|
18965
|
+
if (n === 0 || !style) return;
|
|
17910
18966
|
this.computeScales();
|
|
17911
18967
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
17912
18968
|
this.modelAlpha = 0;
|
|
17913
|
-
const DURATION = 650;
|
|
17914
18969
|
const start = performance.now();
|
|
17915
18970
|
const step = (now) => {
|
|
17916
|
-
const p = Math.min(1, (now - start) /
|
|
18971
|
+
const p = Math.min(1, (now - start) / duration);
|
|
17917
18972
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
17918
18973
|
this.paintData();
|
|
17919
18974
|
if (p < 1) {
|
|
@@ -17927,10 +18982,10 @@ var NativeRenderer = class {
|
|
|
17927
18982
|
}
|
|
17928
18983
|
/** After the candle reveal, fade the indicator models (series/fills/…) from hidden to full. */
|
|
17929
18984
|
fadeInModels() {
|
|
17930
|
-
const
|
|
18985
|
+
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
17931
18986
|
const start = performance.now();
|
|
17932
18987
|
const step = (now) => {
|
|
17933
|
-
this.modelAlpha = Math.min(1, (now - start) /
|
|
18988
|
+
this.modelAlpha = Math.min(1, (now - start) / fade);
|
|
17934
18989
|
this.paintData();
|
|
17935
18990
|
if (this.modelAlpha < 1) {
|
|
17936
18991
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -18045,7 +19100,8 @@ var NativeRenderer = class {
|
|
|
18045
19100
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
18046
19101
|
fling: (v) => this.fling(v),
|
|
18047
19102
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
18048
|
-
onClick: (x) => {
|
|
19103
|
+
onClick: (x, y) => {
|
|
19104
|
+
if (this.handleMarkClick(x, y)) return;
|
|
18049
19105
|
this.userDrawings?.deselect();
|
|
18050
19106
|
this.handleClick(x);
|
|
18051
19107
|
},
|
|
@@ -18074,7 +19130,7 @@ var NativeRenderer = class {
|
|
|
18074
19130
|
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
18075
19131
|
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
18076
19132
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
18077
|
-
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
19133
|
+
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
|
|
18078
19134
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
18079
19135
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
18080
19136
|
});
|
|
@@ -18090,8 +19146,18 @@ var NativeRenderer = class {
|
|
|
18090
19146
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
18091
19147
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
18092
19148
|
theme: () => this.chromeTheme(),
|
|
18093
|
-
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
19149
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y) ?? this.chrome.markTooltipAt(x, y, this.markGroupsInUse())
|
|
19150
|
+
});
|
|
19151
|
+
this.markPopover = new MarkPopover({
|
|
19152
|
+
plot: this.plot,
|
|
19153
|
+
host: () => this.dialogHost ?? this.plot,
|
|
19154
|
+
theme: () => this.chromeTheme(),
|
|
19155
|
+
onOpenChange: (key) => {
|
|
19156
|
+
this.scene.marksActiveKey = key;
|
|
19157
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
19158
|
+
}
|
|
18094
19159
|
});
|
|
19160
|
+
this.chrome.setMarkIconReady(() => this.scheduler?.invalidate(2 /* Chrome */));
|
|
18095
19161
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
18096
19162
|
projector: () => this.drawingProjector(),
|
|
18097
19163
|
dpr: () => this.coords.dpr,
|
|
@@ -18306,29 +19372,40 @@ var NativeRenderer = class {
|
|
|
18306
19372
|
resize() {
|
|
18307
19373
|
this.syncSize();
|
|
18308
19374
|
}
|
|
18309
|
-
/**
|
|
18310
|
-
|
|
19375
|
+
/** Drive the countdown chip from the host's second pulse (`null` → the renderer's own). */
|
|
19376
|
+
setWallClock(clock) {
|
|
19377
|
+
if (clock === this.hostClock) return;
|
|
19378
|
+
this.hostClock = clock;
|
|
19379
|
+
if (this.countdownUnsub != null) {
|
|
19380
|
+
this.countdownUnsub();
|
|
19381
|
+
this.countdownUnsub = null;
|
|
19382
|
+
}
|
|
19383
|
+
this.syncCountdownTimer();
|
|
19384
|
+
}
|
|
19385
|
+
/** Subscribe to the second pulse while the countdown chip is on (so it ticks); unsubscribe
|
|
19386
|
+
* otherwise. Chrome tier: only the chip's text moves — an idle chart must not recompute
|
|
18311
19387
|
* scales or repaint the geometry/volume/VPVR/SDK layers once a second (that cost
|
|
18312
19388
|
* multiplies by the cell count in a multi-chart workspace). */
|
|
18313
19389
|
syncCountdownTimer() {
|
|
18314
19390
|
if (this.scene.showCountdown) {
|
|
18315
|
-
if (this.
|
|
18316
|
-
this.
|
|
19391
|
+
if (this.countdownUnsub == null) {
|
|
19392
|
+
const clock = this.hostClock ?? (this.ownClock ?? (this.ownClock = new SecondClock()));
|
|
19393
|
+
this.countdownUnsub = clock.onTick(() => {
|
|
18317
19394
|
if (this.scene.showCountdown && this.scene.bars.length > 0) this.scheduler?.invalidate(2 /* Chrome */);
|
|
18318
|
-
}
|
|
19395
|
+
});
|
|
18319
19396
|
}
|
|
18320
|
-
} else if (this.
|
|
18321
|
-
|
|
18322
|
-
this.
|
|
19397
|
+
} else if (this.countdownUnsub != null) {
|
|
19398
|
+
this.countdownUnsub();
|
|
19399
|
+
this.countdownUnsub = null;
|
|
18323
19400
|
}
|
|
18324
19401
|
}
|
|
18325
19402
|
destroy() {
|
|
18326
19403
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
18327
19404
|
this.loadingEl?.remove();
|
|
18328
19405
|
this.loadingEl = null;
|
|
18329
|
-
if (this.
|
|
18330
|
-
|
|
18331
|
-
this.
|
|
19406
|
+
if (this.countdownUnsub != null) {
|
|
19407
|
+
this.countdownUnsub();
|
|
19408
|
+
this.countdownUnsub = null;
|
|
18332
19409
|
}
|
|
18333
19410
|
this.scheduler?.destroy();
|
|
18334
19411
|
this.animator?.stop();
|
|
@@ -18353,6 +19430,11 @@ var NativeRenderer = class {
|
|
|
18353
19430
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
18354
19431
|
this.labelTooltip?.destroy();
|
|
18355
19432
|
this.labelTooltip = null;
|
|
19433
|
+
this.markPopover?.destroy();
|
|
19434
|
+
this.markPopover = null;
|
|
19435
|
+
this.chrome.setMarkIconReady(null);
|
|
19436
|
+
if (this.markPulseRaf !== null) cancelAnimationFrame(this.markPulseRaf);
|
|
19437
|
+
this.markPulseRaf = null;
|
|
18356
19438
|
this.scrollButton?.remove();
|
|
18357
19439
|
this.scrollButton = null;
|
|
18358
19440
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -18411,8 +19493,8 @@ var NativeRenderer = class {
|
|
|
18411
19493
|
}
|
|
18412
19494
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
18413
19495
|
this.introPlayed = true;
|
|
18414
|
-
if (this.
|
|
18415
|
-
this.playIntro(
|
|
19496
|
+
if (this.intro.style) {
|
|
19497
|
+
this.playIntro();
|
|
18416
19498
|
return;
|
|
18417
19499
|
}
|
|
18418
19500
|
}
|
|
@@ -18423,7 +19505,7 @@ var NativeRenderer = class {
|
|
|
18423
19505
|
const last = this.bars[n - 1];
|
|
18424
19506
|
if (last && bar.time === last.time) {
|
|
18425
19507
|
this.bars[n - 1] = bar;
|
|
18426
|
-
if (this.
|
|
19508
|
+
if (!this.animLiveBar.on || this.liveEaseTime !== bar.time) {
|
|
18427
19509
|
this.syncLiveEase(bar);
|
|
18428
19510
|
} else {
|
|
18429
19511
|
this.animator.start();
|
|
@@ -18439,11 +19521,11 @@ var NativeRenderer = class {
|
|
|
18439
19521
|
this.scene.bars = this.bars;
|
|
18440
19522
|
this.scheduler.invalidate(4 /* Full */);
|
|
18441
19523
|
}
|
|
18442
|
-
/** Set the
|
|
18443
|
-
*
|
|
18444
|
-
|
|
18445
|
-
this.
|
|
18446
|
-
if (
|
|
19524
|
+
/** Set the reveal (style + duration). A non-off style is also remembered as what the
|
|
19525
|
+
* config's on/off toggle (`animations.intro`) switches back on to. */
|
|
19526
|
+
setIntro(next) {
|
|
19527
|
+
this.intro = next;
|
|
19528
|
+
if (next.style) this.introOnStyle = next.style;
|
|
18447
19529
|
}
|
|
18448
19530
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
18449
19531
|
syncLiveEase(bar) {
|
|
@@ -18457,7 +19539,7 @@ var NativeRenderer = class {
|
|
|
18457
19539
|
const target = this.bars[this.bars.length - 1];
|
|
18458
19540
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
18459
19541
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
18460
|
-
const tau = this.
|
|
19542
|
+
const tau = this.animLiveBar.tau;
|
|
18461
19543
|
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
18462
19544
|
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
18463
19545
|
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
@@ -18597,10 +19679,12 @@ var NativeRenderer = class {
|
|
|
18597
19679
|
this.refreshAnchorOffset(model);
|
|
18598
19680
|
if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
|
|
18599
19681
|
else this.scene.assignIndicatorZ(model.id);
|
|
18600
|
-
|
|
18601
|
-
|
|
18602
|
-
|
|
18603
|
-
|
|
19682
|
+
if (model.legend !== false) {
|
|
19683
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
19684
|
+
native: !!model.native,
|
|
19685
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
19686
|
+
});
|
|
19687
|
+
}
|
|
18604
19688
|
if (model.native?.type === "volume") {
|
|
18605
19689
|
this.volumeActive = true;
|
|
18606
19690
|
this.volumeHidden = false;
|
|
@@ -18747,7 +19831,7 @@ var NativeRenderer = class {
|
|
|
18747
19831
|
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
18748
19832
|
}
|
|
18749
19833
|
listSettingsIds() {
|
|
18750
|
-
return settingsIdCatalog(this.hostSettingsSections);
|
|
19834
|
+
return settingsIdCatalog(this.hostSettingsSections, this.markGroupsInUse());
|
|
18751
19835
|
}
|
|
18752
19836
|
onChartTypeSettingsChange(cb) {
|
|
18753
19837
|
this.chartTypeSettingsCbs.add(cb);
|
|
@@ -18773,6 +19857,22 @@ var NativeRenderer = class {
|
|
|
18773
19857
|
this.axisLongPressCbs.add(cb);
|
|
18774
19858
|
return () => this.axisLongPressCbs.delete(cb);
|
|
18775
19859
|
}
|
|
19860
|
+
// ── timeline marks (the `chart.marks` model; see the port) ──
|
|
19861
|
+
setTimelineMarks(marks, groups) {
|
|
19862
|
+
this.scene.timelineMarks = marks;
|
|
19863
|
+
this.scene.markGroups = groups;
|
|
19864
|
+
this.scene.marksExpandedStack = null;
|
|
19865
|
+
this.markPopover?.close();
|
|
19866
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
19867
|
+
}
|
|
19868
|
+
onMarkClick(cb) {
|
|
19869
|
+
this.markClickCbs.add(cb);
|
|
19870
|
+
return () => this.markClickCbs.delete(cb);
|
|
19871
|
+
}
|
|
19872
|
+
/** Every group the lane knows: the defined ones, then those marks name without a definition. */
|
|
19873
|
+
markGroupsInUse() {
|
|
19874
|
+
return effectiveMarkGroups(this.scene.timelineMarks, this.scene.markGroups);
|
|
19875
|
+
}
|
|
18776
19876
|
onViewportChange(cb) {
|
|
18777
19877
|
this.viewportCbs.add(cb);
|
|
18778
19878
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -18826,7 +19926,7 @@ var NativeRenderer = class {
|
|
|
18826
19926
|
this.zoomAnchorX = anchorX;
|
|
18827
19927
|
this.panVelocity = 0;
|
|
18828
19928
|
this.scrollTargetRO = null;
|
|
18829
|
-
if (!this.animZoom) {
|
|
19929
|
+
if (!this.animZoom.on) {
|
|
18830
19930
|
const v = this.clampViewport(barSpacing, this.anchoredRightOffset(barSpacing));
|
|
18831
19931
|
this.coords.setViewport(v);
|
|
18832
19932
|
this.targetBarSpacing = v.barSpacing;
|
|
@@ -18839,7 +19939,7 @@ var NativeRenderer = class {
|
|
|
18839
19939
|
}
|
|
18840
19940
|
/** Inertial pan: continue with a rightOffset velocity (logical units / ms) that decays. */
|
|
18841
19941
|
fling(velocity) {
|
|
18842
|
-
if (!this.animPan) return;
|
|
19942
|
+
if (!this.animPan.on) return;
|
|
18843
19943
|
this.scrollTargetRO = null;
|
|
18844
19944
|
this.panVelocity = velocity;
|
|
18845
19945
|
this.animator.start();
|
|
@@ -18879,7 +19979,7 @@ var NativeRenderer = class {
|
|
|
18879
19979
|
let active = false;
|
|
18880
19980
|
const tbs = this.targetBarSpacing;
|
|
18881
19981
|
if (Math.abs(barSpacing - tbs) > tbs * 1e-3) {
|
|
18882
|
-
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs,
|
|
19982
|
+
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, this.animZoom.tau));
|
|
18883
19983
|
rightOffset = this.anchoredRightOffset(barSpacing);
|
|
18884
19984
|
active = true;
|
|
18885
19985
|
} else if (barSpacing !== tbs) {
|
|
@@ -18889,13 +19989,14 @@ var NativeRenderer = class {
|
|
|
18889
19989
|
const stopVel = FLING_STOP_PX / Math.max(1e-6, barSpacing * this.coords.spacingScale);
|
|
18890
19990
|
if (Math.abs(this.panVelocity) > stopVel) {
|
|
18891
19991
|
rightOffset += this.panVelocity * dtMs;
|
|
18892
|
-
|
|
19992
|
+
const tau = this.animPan.tau;
|
|
19993
|
+
this.panVelocity = tau > 0 ? this.panVelocity * Math.exp(-dtMs / tau) : 0;
|
|
18893
19994
|
if (Math.abs(this.panVelocity) <= stopVel) this.panVelocity = 0;
|
|
18894
19995
|
else active = true;
|
|
18895
19996
|
}
|
|
18896
19997
|
if (this.scrollTargetRO != null) {
|
|
18897
19998
|
const target = this.scrollTargetRO;
|
|
18898
|
-
const next = easeToward(rightOffset, target, dtMs,
|
|
19999
|
+
const next = easeToward(rightOffset, target, dtMs, this.animScroll.tau);
|
|
18899
20000
|
if (Math.abs(next - target) < 1e-3) {
|
|
18900
20001
|
rightOffset = target;
|
|
18901
20002
|
this.scrollTargetRO = null;
|
|
@@ -18923,6 +20024,7 @@ var NativeRenderer = class {
|
|
|
18923
20024
|
* through Math.log), not a non-linear jump. */
|
|
18924
20025
|
easeScales(dtMs) {
|
|
18925
20026
|
let moving = false;
|
|
20027
|
+
const tau = this.animAutoscale.tau;
|
|
18926
20028
|
for (const pane of this.scene.panes.values()) {
|
|
18927
20029
|
const t = pane.scaleTarget;
|
|
18928
20030
|
const s = pane.scale;
|
|
@@ -18930,8 +20032,8 @@ var NativeRenderer = class {
|
|
|
18930
20032
|
const lt0 = Math.log(t.min);
|
|
18931
20033
|
const lt1 = Math.log(t.max);
|
|
18932
20034
|
const lspan = Math.max(1e-9, Math.abs(lt1 - lt0));
|
|
18933
|
-
const n0 = easeToward(Math.log(s.min), lt0, dtMs,
|
|
18934
|
-
const n1 = easeToward(Math.log(s.max), lt1, dtMs,
|
|
20035
|
+
const n0 = easeToward(Math.log(s.min), lt0, dtMs, tau);
|
|
20036
|
+
const n1 = easeToward(Math.log(s.max), lt1, dtMs, tau);
|
|
18935
20037
|
if (Math.abs(n0 - lt0) <= lspan * 1e-3 && Math.abs(n1 - lt1) <= lspan * 1e-3) {
|
|
18936
20038
|
pane.scale = { min: t.min, max: t.max, log: true };
|
|
18937
20039
|
} else {
|
|
@@ -18941,8 +20043,8 @@ var NativeRenderer = class {
|
|
|
18941
20043
|
continue;
|
|
18942
20044
|
}
|
|
18943
20045
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
18944
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
18945
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
20046
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
20047
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
18946
20048
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
18947
20049
|
nmin = t.min;
|
|
18948
20050
|
nmax = t.max;
|
|
@@ -18955,8 +20057,8 @@ var NativeRenderer = class {
|
|
|
18955
20057
|
const t = sl.scaleTarget;
|
|
18956
20058
|
const s = sl.scale;
|
|
18957
20059
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
18958
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
18959
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
20060
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
20061
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
18960
20062
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
18961
20063
|
nmin = t.min;
|
|
18962
20064
|
nmax = t.max;
|
|
@@ -18977,6 +20079,8 @@ var NativeRenderer = class {
|
|
|
18977
20079
|
this.scene.crosshair = null;
|
|
18978
20080
|
this.hoverSeparatorY = null;
|
|
18979
20081
|
this.lastPointer = null;
|
|
20082
|
+
if (this.marksExpandedBy === "hover") this.setMarksExpanded(null);
|
|
20083
|
+
this.setMarkHover(null);
|
|
18980
20084
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
18981
20085
|
this.hoverLogical = null;
|
|
18982
20086
|
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
@@ -18988,6 +20092,8 @@ var NativeRenderer = class {
|
|
|
18988
20092
|
this.lastPointer = inData ? { x, y } : null;
|
|
18989
20093
|
this.hoverSeparatorY = x >= 0 && y >= 0 && y <= this.coords.height ? this.separatorHoverY(y) : null;
|
|
18990
20094
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
20095
|
+
this.setMarksExpanded(inData ? this.chrome.markStackAt(x, y) : null);
|
|
20096
|
+
this.setMarkHover(inData ? this.chrome.markGlyphAt(x, y)?.cluster.key ?? null : null);
|
|
18991
20097
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
18992
20098
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
18993
20099
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
@@ -19017,6 +20123,76 @@ var NativeRenderer = class {
|
|
|
19017
20123
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
19018
20124
|
for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
|
|
19019
20125
|
}
|
|
20126
|
+
/**
|
|
20127
|
+
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
20128
|
+
* it changes. A stack whose glyph holds the open popup stays fanned — the pointer leaving
|
|
20129
|
+
* the plot for the popup must not bury the glyph under the deck (which would close it).
|
|
20130
|
+
*/
|
|
20131
|
+
setMarksExpanded(stack, by = "hover") {
|
|
20132
|
+
if (this.scene.marksExpandedStack === stack) return;
|
|
20133
|
+
if (stack === null && this.markPopover?.key) {
|
|
20134
|
+
const open = this.chrome.markGlyphByKey(this.markPopover.key);
|
|
20135
|
+
if (open && open.stack === this.scene.marksExpandedStack) return;
|
|
20136
|
+
}
|
|
20137
|
+
this.scene.marksExpandedStack = stack;
|
|
20138
|
+
this.marksExpandedBy = by;
|
|
20139
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
20140
|
+
}
|
|
20141
|
+
/**
|
|
20142
|
+
* A click on the mark lane: a collapsed deck fans out (the touch path — a mouse already
|
|
20143
|
+
* fanned it by hovering), a glyph reports its cluster (`onMarkClick`) and opens the popup
|
|
20144
|
+
* when any of its marks carries content. True when the click landed on the lane.
|
|
20145
|
+
*/
|
|
20146
|
+
handleMarkClick(x, y) {
|
|
20147
|
+
const glyph = this.chrome.markGlyphAt(x, y);
|
|
20148
|
+
if (!glyph) {
|
|
20149
|
+
if (this.scene.marksExpandedStack !== null && this.chrome.markStackAt(x, y) === null) this.setMarksExpanded(null);
|
|
20150
|
+
return false;
|
|
20151
|
+
}
|
|
20152
|
+
if (glyph.decked) {
|
|
20153
|
+
this.setMarksExpanded(glyph.stack, "tap");
|
|
20154
|
+
return true;
|
|
20155
|
+
}
|
|
20156
|
+
const marks = glyph.cluster.marks;
|
|
20157
|
+
const first = marks[0];
|
|
20158
|
+
const event = { id: first.id, ids: marks.map((m) => m.id), time: first.time, ...glyph.cluster.group !== void 0 ? { group: glyph.cluster.group } : {} };
|
|
20159
|
+
for (const cb of this.markClickCbs) cb(event);
|
|
20160
|
+
if (marks.some((m) => m.content !== void 0)) {
|
|
20161
|
+
this.markPopover?.open(glyph.cluster, { x: glyph.x, y: glyph.y, size: glyph.size });
|
|
20162
|
+
} else {
|
|
20163
|
+
this.scene.marksFlash = { key: glyph.cluster.key, until: frameNow() + MARK_FLASH_MS };
|
|
20164
|
+
this.syncMarkPulse();
|
|
20165
|
+
}
|
|
20166
|
+
return true;
|
|
20167
|
+
}
|
|
20168
|
+
/** After a chrome frame: keep the open mark popup on its glyph, or close it once the glyph is gone. */
|
|
20169
|
+
trackMarkPopover() {
|
|
20170
|
+
const key = this.markPopover?.key;
|
|
20171
|
+
if (!key) return;
|
|
20172
|
+
const g = this.chrome.markGlyphByKey(key);
|
|
20173
|
+
this.markPopover.track(g && !(g.decked && g.depth !== 0) ? { x: g.x, y: g.y, size: g.size } : null);
|
|
20174
|
+
}
|
|
20175
|
+
/** The lane glyph under the pointer — it swells once as the pointer lands (a rAF-driven chrome repaint for the pulse's duration). */
|
|
20176
|
+
setMarkHover(key) {
|
|
20177
|
+
if (this.scene.marksHoverKey === key) return;
|
|
20178
|
+
this.scene.marksHoverKey = key;
|
|
20179
|
+
this.scene.marksHoverSince = frameNow();
|
|
20180
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
20181
|
+
this.syncMarkPulse();
|
|
20182
|
+
}
|
|
20183
|
+
/** Run a chrome-tier repaint loop while a glyph's hover pulse or click flash plays; it stops itself once both are over. */
|
|
20184
|
+
syncMarkPulse() {
|
|
20185
|
+
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
20186
|
+
const tick = () => {
|
|
20187
|
+
this.markPulseRaf = null;
|
|
20188
|
+
const now = frameNow();
|
|
20189
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
|
|
20190
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
20191
|
+
const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
20192
|
+
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
20193
|
+
};
|
|
20194
|
+
this.markPulseRaf = requestAnimationFrame(tick);
|
|
20195
|
+
}
|
|
19020
20196
|
paneAtY(y) {
|
|
19021
20197
|
return this.paneNodeAtY(y);
|
|
19022
20198
|
}
|
|
@@ -19408,6 +20584,7 @@ var NativeRenderer = class {
|
|
|
19408
20584
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
19409
20585
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
19410
20586
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
20587
|
+
this.trackMarkPopover();
|
|
19411
20588
|
}
|
|
19412
20589
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
19413
20590
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
@@ -19423,7 +20600,7 @@ var NativeRenderer = class {
|
|
|
19423
20600
|
const lp = this.layerPane(l.def.id) ?? pane;
|
|
19424
20601
|
if (lp.collapsed) continue;
|
|
19425
20602
|
l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
|
|
19426
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
20603
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
19427
20604
|
}
|
|
19428
20605
|
}
|
|
19429
20606
|
/** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
|
|
@@ -19488,7 +20665,7 @@ var NativeRenderer = class {
|
|
|
19488
20665
|
const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
|
|
19489
20666
|
l.instance.render(args);
|
|
19490
20667
|
if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
19491
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
20668
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
19492
20669
|
}
|
|
19493
20670
|
if (folded) {
|
|
19494
20671
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
@@ -19507,6 +20684,7 @@ var NativeRenderer = class {
|
|
|
19507
20684
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
19508
20685
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
19509
20686
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
20687
|
+
this.trackMarkPopover();
|
|
19510
20688
|
this.userDrawings?.render();
|
|
19511
20689
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
19512
20690
|
this.paintedData = true;
|
|
@@ -20019,7 +21197,7 @@ var NativeRenderer = class {
|
|
|
20019
21197
|
const map2 = /* @__PURE__ */ new Map();
|
|
20020
21198
|
for (const pane of this.scene.panes.values()) {
|
|
20021
21199
|
if (!pane.collapsed) continue;
|
|
20022
|
-
const models = this.scene.orderedIndicatorsForPane(pane.id);
|
|
21200
|
+
const models = this.scene.orderedIndicatorsForPane(pane.id).filter((m) => m.legend !== false);
|
|
20023
21201
|
const merged = new Set(this.scene.ownScaleIndicatorsForPane(pane.id).map((m) => m.id));
|
|
20024
21202
|
const master = models.find((m) => !merged.has(m.id)) ?? models[0];
|
|
20025
21203
|
map2.set(pane.id, master?.id ?? null);
|
|
@@ -20443,6 +21621,93 @@ function presetToRange(preset, bars) {
|
|
|
20443
21621
|
return { from: Math.max(first, from), to: last };
|
|
20444
21622
|
}
|
|
20445
21623
|
|
|
21624
|
+
// src/core/marks/MarksController.ts
|
|
21625
|
+
var MarksController = class {
|
|
21626
|
+
constructor(renderer, events) {
|
|
21627
|
+
this.renderer = renderer;
|
|
21628
|
+
/** Insertion-ordered — the order a cluster falls back to for equal times. */
|
|
21629
|
+
this.marks = /* @__PURE__ */ new Map();
|
|
21630
|
+
this.groups = /* @__PURE__ */ new Map();
|
|
21631
|
+
this.subs = [];
|
|
21632
|
+
this.enabled = !!renderer.capabilities.timelineMarks && typeof renderer.setTimelineMarks === "function";
|
|
21633
|
+
if (this.enabled && renderer.onMarkClick) this.subs.push(renderer.onMarkClick((e) => events.emit("mark:click", e)));
|
|
21634
|
+
}
|
|
21635
|
+
/** Whether the active renderer paints timeline marks. */
|
|
21636
|
+
get supported() {
|
|
21637
|
+
return this.enabled;
|
|
21638
|
+
}
|
|
21639
|
+
/** Add (or replace, by id) one mark. */
|
|
21640
|
+
add(mark) {
|
|
21641
|
+
this.marks.set(mark.id, validateMark(mark));
|
|
21642
|
+
this.sync();
|
|
21643
|
+
}
|
|
21644
|
+
/** Replace the whole set — a market switch. */
|
|
21645
|
+
set(marks) {
|
|
21646
|
+
this.marks.clear();
|
|
21647
|
+
for (const m of marks) this.marks.set(m.id, validateMark(m));
|
|
21648
|
+
this.sync();
|
|
21649
|
+
}
|
|
21650
|
+
remove(id) {
|
|
21651
|
+
const had = this.marks.delete(id);
|
|
21652
|
+
if (had) this.sync();
|
|
21653
|
+
return had;
|
|
21654
|
+
}
|
|
21655
|
+
clear() {
|
|
21656
|
+
if (this.marks.size === 0) return;
|
|
21657
|
+
this.marks.clear();
|
|
21658
|
+
this.sync();
|
|
21659
|
+
}
|
|
21660
|
+
/** Every mark, in insertion order (shallow copies — mutating one changes nothing). */
|
|
21661
|
+
all() {
|
|
21662
|
+
return [...this.marks.values()].map((m) => ({ ...m, glyph: { ...m.glyph } }));
|
|
21663
|
+
}
|
|
21664
|
+
/** Define (or replace) a group's presentation — its settings label and default visibility. */
|
|
21665
|
+
defineGroup(group) {
|
|
21666
|
+
if (!group || typeof group.id !== "string" || group.id.length === 0) throw new Error("[vela] marks.defineGroup: `id` must be a non-empty string");
|
|
21667
|
+
if (typeof group.label !== "string") throw new Error(`[vela] marks.defineGroup: group "${group.id}" needs a string \`label\``);
|
|
21668
|
+
this.groups.set(group.id, { ...group });
|
|
21669
|
+
this.sync();
|
|
21670
|
+
}
|
|
21671
|
+
/** The defined groups, in definition order. */
|
|
21672
|
+
groupDefinitions() {
|
|
21673
|
+
return [...this.groups.values()].map((g) => ({ ...g }));
|
|
21674
|
+
}
|
|
21675
|
+
/**
|
|
21676
|
+
* Show or hide one group's marks. The choice lives in the renderer's cosmetic config
|
|
21677
|
+
* (the `marks` feature) — what the settings dialog's Events checkboxes edit and what
|
|
21678
|
+
* a persisted chart restores — so it warns + no-ops on a renderer without it.
|
|
21679
|
+
*/
|
|
21680
|
+
setGroupVisible(id, visible) {
|
|
21681
|
+
if (!this.renderer.features.includes("marks")) {
|
|
21682
|
+
console.warn(`[vela] renderer "${this.renderer.name}" does not paint timeline marks \u2014 setGroupVisible ignored.`);
|
|
21683
|
+
return;
|
|
21684
|
+
}
|
|
21685
|
+
this.renderer.applyFeature("marks", { groups: { [id]: visible } });
|
|
21686
|
+
}
|
|
21687
|
+
/** A group's effective visibility: the user's (persisted) choice, else the group's declared default, else visible. */
|
|
21688
|
+
isGroupVisible(id) {
|
|
21689
|
+
const state = this.renderer.readFeature("marks");
|
|
21690
|
+
const chosen = state?.groups?.[id];
|
|
21691
|
+
if (typeof chosen === "boolean") return chosen;
|
|
21692
|
+
return this.groups.get(id)?.visible !== false;
|
|
21693
|
+
}
|
|
21694
|
+
destroy() {
|
|
21695
|
+
for (const unsub of this.subs) unsub();
|
|
21696
|
+
this.subs.length = 0;
|
|
21697
|
+
}
|
|
21698
|
+
sync() {
|
|
21699
|
+
if (!this.enabled) return;
|
|
21700
|
+
this.renderer.setTimelineMarks([...this.marks.values()], [...this.groups.values()]);
|
|
21701
|
+
}
|
|
21702
|
+
};
|
|
21703
|
+
function validateMark(mark) {
|
|
21704
|
+
if (!mark || typeof mark !== "object") throw new Error("[vela] marks: a mark must be an object");
|
|
21705
|
+
if (typeof mark.id !== "string" || mark.id.length === 0) throw new Error("[vela] marks: `id` must be a non-empty string");
|
|
21706
|
+
if (typeof mark.time !== "number" || !Number.isFinite(mark.time)) throw new Error(`[vela] marks: mark "${mark.id}" needs a finite epoch-ms \`time\``);
|
|
21707
|
+
if (!mark.glyph || typeof mark.glyph !== "object" || typeof mark.glyph.color !== "string") throw new Error(`[vela] marks: mark "${mark.id}" needs a glyph with a \`color\``);
|
|
21708
|
+
return { ...mark, glyph: { ...mark.glyph } };
|
|
21709
|
+
}
|
|
21710
|
+
|
|
20446
21711
|
// src/core/engine/DrawingSeriesService.ts
|
|
20447
21712
|
var MAX_BARS = 5e3;
|
|
20448
21713
|
var PAD_FRAC = 0.25;
|
|
@@ -20748,6 +22013,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20748
22013
|
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
20749
22014
|
});
|
|
20750
22015
|
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
22016
|
+
this.marks = new MarksController(this.renderer, this.events);
|
|
20751
22017
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
20752
22018
|
this.endLoad();
|
|
20753
22019
|
this.events.emit("data:unresolved", info);
|
|
@@ -21612,6 +22878,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21612
22878
|
record.session = void 0;
|
|
21613
22879
|
record.native?.instance.suspend();
|
|
21614
22880
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
22881
|
+
else if (record.native) this.mountHiddenNativeRow(id, record);
|
|
21615
22882
|
} else {
|
|
21616
22883
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, true);
|
|
21617
22884
|
record.pendingStructural = true;
|
|
@@ -21699,6 +22966,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21699
22966
|
this.unresolvedUnsub = null;
|
|
21700
22967
|
this.feed.destroy?.();
|
|
21701
22968
|
this.drawings.destroy();
|
|
22969
|
+
this.marks.destroy();
|
|
21702
22970
|
this.renderer.destroy();
|
|
21703
22971
|
this.events.clear();
|
|
21704
22972
|
}
|
|
@@ -21812,6 +23080,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21812
23080
|
overlay: d.overlay,
|
|
21813
23081
|
paneHint: d.paneHint,
|
|
21814
23082
|
native: { type: record.native.type },
|
|
23083
|
+
...d.legend === false ? { legend: false } : {},
|
|
21815
23084
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
21816
23085
|
series: out.series ?? [],
|
|
21817
23086
|
fills: out.fills ?? [],
|
|
@@ -21834,9 +23103,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21834
23103
|
* over it in place (`pendingStructural`), clears the spinner, and only THEN fires
|
|
21835
23104
|
* `indicator:added`/`ready` — so event semantics and `inspect()` (which skips
|
|
21836
23105
|
* loading records) still mean "the indicator produced output".
|
|
23106
|
+
*
|
|
23107
|
+
* A HIDDEN record mounts too — dimmed, no spinner (its session never starts while
|
|
23108
|
+
* hidden, so nothing is computing and no model will ever arrive to mount the row
|
|
23109
|
+
* later). Without this an indicator ADDED hidden (a restored ledger/ext entry) had
|
|
23110
|
+
* no legend row at all: invisible AND unreachable — the eye that unhides it never
|
|
23111
|
+
* existed. The hidden mount announces immediately for the same reason: the "first
|
|
23112
|
+
* computed model" that normally announces cannot come until the indicator is shown,
|
|
23113
|
+
* and host UIs (object tree, landing watchers) must know it exists NOW.
|
|
21837
23114
|
*/
|
|
21838
23115
|
mountLoadingPlaceholder(id, record) {
|
|
21839
|
-
if (record.renderHandle ||
|
|
23116
|
+
if (record.renderHandle || !record.prepared) return;
|
|
21840
23117
|
const meta = record.prepared.meta;
|
|
21841
23118
|
const model = {
|
|
21842
23119
|
id,
|
|
@@ -21860,8 +23137,34 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21860
23137
|
this.ensurePaneFor(paneId);
|
|
21861
23138
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
21862
23139
|
record.pendingStructural = true;
|
|
23140
|
+
if (record.hidden) {
|
|
23141
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
23142
|
+
this.announce(record, this.handles.get(id));
|
|
23143
|
+
return;
|
|
23144
|
+
}
|
|
21863
23145
|
this.setLoading(record, true);
|
|
21864
23146
|
}
|
|
23147
|
+
/**
|
|
23148
|
+
* Mount the legend row for a NATIVE indicator that is being hidden BEFORE it ever
|
|
23149
|
+
* started (a restored-hidden ledger entry: `startNativeIndicator` bails on hidden
|
|
23150
|
+
* records, so no model — and therefore no row — would ever mount). The native
|
|
23151
|
+
* counterpart of {@link mountLoadingPlaceholder}'s hidden branch: an empty model
|
|
23152
|
+
* carries the title + inputs schema, the renderer marks the row hidden, and the
|
|
23153
|
+
* announce makes the indicator visible to host UIs. Showing later STARTS the
|
|
23154
|
+
* instance (the `started` flag path) and its first emit remounts over this row.
|
|
23155
|
+
*/
|
|
23156
|
+
mountHiddenNativeRow(id, record) {
|
|
23157
|
+
if (record.renderHandle || !record.native) return;
|
|
23158
|
+
const model = this.buildNativeModel(record, {});
|
|
23159
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
23160
|
+
this.placeModel(model, id, paneId);
|
|
23161
|
+
record.model = model;
|
|
23162
|
+
this.ensurePaneFor(paneId);
|
|
23163
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
23164
|
+
record.pendingStructural = true;
|
|
23165
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
23166
|
+
this.announce(record, this.handles.get(id));
|
|
23167
|
+
}
|
|
21865
23168
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
21866
23169
|
setLoading(record, loading) {
|
|
21867
23170
|
record.loading = loading;
|
|
@@ -21896,6 +23199,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21896
23199
|
for (const r of this.registry.all()) {
|
|
21897
23200
|
const model = r.model;
|
|
21898
23201
|
if (!model) continue;
|
|
23202
|
+
if (model.legend === false) continue;
|
|
21899
23203
|
const paneId = model.paneId ?? "price";
|
|
21900
23204
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
21901
23205
|
byPane.get(paneId).push({
|
|
@@ -24881,6 +26185,7 @@ var Vela = class {
|
|
|
24881
26185
|
if (Object.keys(defaults).length > 0) this.rendererControl.set(defaults);
|
|
24882
26186
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
24883
26187
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
26188
|
+
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
24884
26189
|
}
|
|
24885
26190
|
/**
|
|
24886
26191
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -25099,6 +26404,17 @@ var Vela = class {
|
|
|
25099
26404
|
get drawings() {
|
|
25100
26405
|
return this.drawingsControl;
|
|
25101
26406
|
}
|
|
26407
|
+
/**
|
|
26408
|
+
* The chart's timeline-marks control surface: host events pinned to a bar and shown
|
|
26409
|
+
* as glyphs on a lane above the time axis, each opening a popup on click —
|
|
26410
|
+
* `chart.marks.add({ id, time, glyph, title, content })`, `chart.marks.set(list)`,
|
|
26411
|
+
* `chart.marks.defineGroup({ id, label })`. Marks are data, not user state: re-supply
|
|
26412
|
+
* them on `market:changed`. On a renderer without the `timelineMarks` capability the
|
|
26413
|
+
* model still fills but nothing paints (`chart.marks.supported`).
|
|
26414
|
+
*/
|
|
26415
|
+
get marks() {
|
|
26416
|
+
return this.marksControl;
|
|
26417
|
+
}
|
|
25102
26418
|
on(event, handler) {
|
|
25103
26419
|
return this.orchestrator.events.on(event, handler);
|
|
25104
26420
|
}
|
|
@@ -25157,4 +26473,4 @@ function resolveElement(container) {
|
|
|
25157
26473
|
return element;
|
|
25158
26474
|
}
|
|
25159
26475
|
|
|
25160
|
-
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, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|
|
26476
|
+
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|