@luxalgo/vela 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -46
- package/dist/{DataProvider-CWmp31dA.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
- package/dist/{DataProvider-l_eLMly_.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
- package/dist/{chunk-4KZVZ7QQ.js → chunk-F4M24ANM.js} +130 -1
- package/dist/{chunk-NMTQXNT4.js → chunk-IZV3CW5N.js} +245 -43
- package/dist/{chunk-FFOP37FQ.js → chunk-KV7FDWSL.js} +748 -70
- package/dist/{contributions-BCz6Dr6a.d.ts → contributions-37nni40G.d.ts} +20 -4
- package/dist/{contributions-D9vTzm5p.d.cts → contributions-lPojhTxI.d.cts} +20 -4
- package/dist/index.cjs +866 -58
- package/dist/index.d.cts +12 -6
- package/dist/index.d.ts +12 -6
- package/dist/index.js +3 -3
- package/dist/{options-DSqHsQyN.d.cts → options-CYS5Wmlx.d.cts} +78 -2
- package/dist/{options-DSqHsQyN.d.ts → options-CYS5Wmlx.d.ts} +78 -2
- package/dist/{plugin-Bt8hLR8Z.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
- package/dist/{plugin-CH7pfMrE.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
- package/dist/plugin.cjs +119 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- 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-5K7y4Ya2.d.ts → statusline-model-CiJm-riV.d.cts} +17 -87
- package/dist/{statusline-TYLQmoeh.d.cts → statusline-model-D-q_OOx9.d.ts} +17 -87
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +866 -58
- package/dist/vela.global.min.js +52 -52
- package/dist/widget.cjs +1109 -100
- package/dist/widget.d.cts +118 -7
- package/dist/widget.d.ts +118 -7
- package/dist/widget.js +4 -4
- package/dist/workspace.cjs +1109 -100
- package/dist/workspace.d.cts +20 -11
- package/dist/workspace.d.ts +20 -11
- package/dist/workspace.js +3 -3
- package/package.json +1 -1
- /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-
|
|
1
|
+
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, 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 } from './chunk-F4M24ANM.js';
|
|
2
2
|
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-MHY7MVXH.js';
|
|
3
3
|
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BEARISH, BULLISH, isDarkColor, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-KG4YT3TI.js';
|
|
4
4
|
|
|
@@ -62,7 +62,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
|
|
|
62
62
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
63
63
|
var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
|
|
64
64
|
var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
|
|
65
|
-
var MEASUREMENT_TYPES = ["position", "datepricerange"];
|
|
65
|
+
var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
|
|
66
66
|
var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
|
|
67
67
|
var BRUSH_TYPES = ["freehand", "highlighter"];
|
|
68
68
|
var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
|
|
@@ -1138,6 +1138,7 @@ function intervalMs(timeframe) {
|
|
|
1138
1138
|
"4h": 144e5,
|
|
1139
1139
|
"1d": 864e5,
|
|
1140
1140
|
"1w": 6048e5,
|
|
1141
|
+
"1M": 2592e6,
|
|
1141
1142
|
"1": 6e4,
|
|
1142
1143
|
"5": 3e5,
|
|
1143
1144
|
"15": 9e5,
|
|
@@ -1145,7 +1146,8 @@ function intervalMs(timeframe) {
|
|
|
1145
1146
|
"60": 36e5,
|
|
1146
1147
|
"240": 144e5,
|
|
1147
1148
|
D: 864e5,
|
|
1148
|
-
W: 6048e5
|
|
1149
|
+
W: 6048e5,
|
|
1150
|
+
M: 2592e6
|
|
1149
1151
|
};
|
|
1150
1152
|
return map[timeframe] ?? 36e5;
|
|
1151
1153
|
}
|
|
@@ -1859,7 +1861,7 @@ var DrawingHistory = class {
|
|
|
1859
1861
|
|
|
1860
1862
|
// src/core/drawings/DrawingController.ts
|
|
1861
1863
|
var DrawingController = class {
|
|
1862
|
-
constructor(renderer, events, option) {
|
|
1864
|
+
constructor(renderer, events, option, seriesGateway) {
|
|
1863
1865
|
this.events = events;
|
|
1864
1866
|
this.store = new DrawingStore();
|
|
1865
1867
|
this.history = new DrawingHistory();
|
|
@@ -1885,6 +1887,7 @@ var DrawingController = class {
|
|
|
1885
1887
|
const { definition, visible } = buildToolbar(option);
|
|
1886
1888
|
this.port.setToolbar(definition);
|
|
1887
1889
|
this.port.showToolbar(visible);
|
|
1890
|
+
if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
|
|
1888
1891
|
this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
|
|
1889
1892
|
this.subs.push(this.store.onChange(() => this.sync()));
|
|
1890
1893
|
}
|
|
@@ -1990,7 +1993,7 @@ var DrawingController = class {
|
|
|
1990
1993
|
style,
|
|
1991
1994
|
text: init.text,
|
|
1992
1995
|
props: init.props,
|
|
1993
|
-
zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
|
|
1996
|
+
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
1994
1997
|
});
|
|
1995
1998
|
if (!d) return null;
|
|
1996
1999
|
this.history.record(this.store.serialize());
|
|
@@ -2065,10 +2068,14 @@ var DrawingController = class {
|
|
|
2065
2068
|
* (falling back to just under the pane's top series where there is no price — a study
|
|
2066
2069
|
* pane). Half a key down never ties a series; drawings tying each other paint in insertion
|
|
2067
2070
|
* order, so consecutive new drawings still stack newest-in-front. Undefined without a
|
|
2068
|
-
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
2069
|
-
|
|
2071
|
+
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
2072
|
+
* A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
|
|
2073
|
+
* above the whole stack — under the candles its content would be buried. */
|
|
2074
|
+
startZ(type, paneId) {
|
|
2070
2075
|
const range = this.port?.stackRange?.(paneId);
|
|
2071
|
-
|
|
2076
|
+
if (!range) return void 0;
|
|
2077
|
+
if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
|
|
2078
|
+
return (range.price ?? range.front) - 0.5;
|
|
2072
2079
|
}
|
|
2073
2080
|
/** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
|
|
2074
2081
|
* `additive` toggles membership (matching shift-click) instead of replacing. */
|
|
@@ -2213,7 +2220,7 @@ var DrawingController = class {
|
|
|
2213
2220
|
const style = last ? { ...i.doc.style, ...last } : i.doc.style;
|
|
2214
2221
|
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
2215
2222
|
if (!d) return;
|
|
2216
|
-
if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
|
|
2223
|
+
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
2217
2224
|
this.history.record(before);
|
|
2218
2225
|
this.store.add(d);
|
|
2219
2226
|
this.captureStyle(d.id);
|
|
@@ -6859,6 +6866,7 @@ var DOUBLE_TAP_MS = 350;
|
|
|
6859
6866
|
var DOUBLE_TAP_SLOP = 30;
|
|
6860
6867
|
var TIME_SCALE_K = 4e-3;
|
|
6861
6868
|
var WHEEL_ZOOM_K = 4e-3;
|
|
6869
|
+
var WHEEL_PRICE_DRAG_PX = 0.25;
|
|
6862
6870
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
6863
6871
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
6864
6872
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -6989,7 +6997,7 @@ var InputController = class {
|
|
|
6989
6997
|
this.capture(e.pointerId);
|
|
6990
6998
|
return;
|
|
6991
6999
|
}
|
|
6992
|
-
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
|
|
7000
|
+
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
6993
7001
|
this.region = "drawing";
|
|
6994
7002
|
this.capture(e.pointerId);
|
|
6995
7003
|
return;
|
|
@@ -7103,7 +7111,7 @@ var InputController = class {
|
|
|
7103
7111
|
const wasTouch = e.pointerType === "touch";
|
|
7104
7112
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
7105
7113
|
if (this.dragging && this.region === "drawing") {
|
|
7106
|
-
this.deps.drawingsPointerUp?.(x, y);
|
|
7114
|
+
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
7107
7115
|
} else if (tapRelease && this.region === "data") {
|
|
7108
7116
|
this.deps.onClick(x, y);
|
|
7109
7117
|
} else if (this.dragging && this.region === "data") {
|
|
@@ -7124,7 +7132,7 @@ var InputController = class {
|
|
|
7124
7132
|
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
7125
7133
|
this.cancelLongPress();
|
|
7126
7134
|
if (!this.dragging) return;
|
|
7127
|
-
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
7135
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
7128
7136
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
7129
7137
|
this.endGesture(e);
|
|
7130
7138
|
};
|
|
@@ -7140,6 +7148,12 @@ var InputController = class {
|
|
|
7140
7148
|
this.onWheel = (e) => {
|
|
7141
7149
|
e.preventDefault();
|
|
7142
7150
|
this.deps.drawingsClearTransient?.();
|
|
7151
|
+
const { x, y } = this.local(e);
|
|
7152
|
+
if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
|
|
7153
|
+
this.deps.beginPriceScale(x, y);
|
|
7154
|
+
this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
|
|
7155
|
+
return;
|
|
7156
|
+
}
|
|
7143
7157
|
const coords = this.deps.getCoords();
|
|
7144
7158
|
const vp = coords.getViewport();
|
|
7145
7159
|
const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
|
|
@@ -7147,9 +7161,8 @@ var InputController = class {
|
|
|
7147
7161
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
|
|
7148
7162
|
return;
|
|
7149
7163
|
}
|
|
7150
|
-
const cursorX = this.local(e).x;
|
|
7151
7164
|
const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
|
|
7152
|
-
const anchor = wheelZoomAnchor(coords,
|
|
7165
|
+
const anchor = wheelZoomAnchor(coords, x, rightEdge);
|
|
7153
7166
|
const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
|
|
7154
7167
|
this.deps.zoomTo(target, anchor.logical, anchor.x);
|
|
7155
7168
|
};
|
|
@@ -8631,6 +8644,15 @@ function drawingSetEmpty(s) {
|
|
|
8631
8644
|
function fontSizePx(size) {
|
|
8632
8645
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
8633
8646
|
}
|
|
8647
|
+
function lineCoversWindow(a, b, extend, lo, hi) {
|
|
8648
|
+
const minX = Math.min(a, b);
|
|
8649
|
+
const maxX = Math.max(a, b);
|
|
8650
|
+
if (a === b) return a >= lo && a <= hi;
|
|
8651
|
+
if (extend === "both") return true;
|
|
8652
|
+
if (extend === "left") return maxX >= lo;
|
|
8653
|
+
if (extend === "right") return minX <= hi;
|
|
8654
|
+
return maxX >= lo && minX <= hi;
|
|
8655
|
+
}
|
|
8634
8656
|
var DrawingSceneRenderer = class {
|
|
8635
8657
|
constructor(deps, set = EMPTY_DRAWING_SET) {
|
|
8636
8658
|
this.deps = deps;
|
|
@@ -8684,7 +8706,9 @@ var DrawingSceneRenderer = class {
|
|
|
8684
8706
|
const lo = Math.min(from, to);
|
|
8685
8707
|
const hi = Math.max(from, to);
|
|
8686
8708
|
const visible = (a, b, extend) => {
|
|
8687
|
-
if (extend
|
|
8709
|
+
if (extend === "both") return true;
|
|
8710
|
+
if (extend === "left") return Math.max(a, b) >= lo;
|
|
8711
|
+
if (extend === "right") return Math.min(a, b) <= hi;
|
|
8688
8712
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
8689
8713
|
};
|
|
8690
8714
|
let min = Infinity;
|
|
@@ -8695,7 +8719,7 @@ var DrawingSceneRenderer = class {
|
|
|
8695
8719
|
};
|
|
8696
8720
|
for (const ln of this.set.lines) {
|
|
8697
8721
|
if (ln.invisible) continue;
|
|
8698
|
-
if (!
|
|
8722
|
+
if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
|
|
8699
8723
|
fold(ln.y1);
|
|
8700
8724
|
fold(ln.y2);
|
|
8701
8725
|
}
|
|
@@ -11139,6 +11163,18 @@ function timezoneOptions(current) {
|
|
|
11139
11163
|
return [[normalized, tzMenuLabel(normalized, normalized)], ...options];
|
|
11140
11164
|
}
|
|
11141
11165
|
|
|
11166
|
+
// src/core/price-styles/BarTransform.ts
|
|
11167
|
+
function barTransformFor(style) {
|
|
11168
|
+
return chartType(style)?.barTransform ?? null;
|
|
11169
|
+
}
|
|
11170
|
+
function parseExtendedTicker(symbol) {
|
|
11171
|
+
const at = symbol.lastIndexOf(";");
|
|
11172
|
+
if (at <= 0 || at === symbol.length - 1) return { symbol, modifier: null, transform: null };
|
|
11173
|
+
const modifier = symbol.slice(at + 1).toLowerCase();
|
|
11174
|
+
if (modifier !== "standard" && !tickerModifierIds().includes(modifier)) return { symbol, modifier: null, transform: null };
|
|
11175
|
+
return { symbol: symbol.slice(0, at), modifier, transform: barTransformFor(modifier) };
|
|
11176
|
+
}
|
|
11177
|
+
|
|
11142
11178
|
// src/renderers/native/drawings/DrawingPainter.ts
|
|
11143
11179
|
var HANDLE_RADIUS = 4.5;
|
|
11144
11180
|
var HANDLE_BORDER = DEFAULT_DRAWING_COLOR;
|
|
@@ -11158,6 +11194,17 @@ var DrawingPainter = class {
|
|
|
11158
11194
|
constructor() {
|
|
11159
11195
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
11160
11196
|
this.targets = {};
|
|
11197
|
+
/** The chart's active series LOOK — style + resolved series colors — pushed by the
|
|
11198
|
+
* controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
|
|
11199
|
+
* area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
|
|
11200
|
+
* bars; unknown/custom styles fall back to candles), and the colors default to the main
|
|
11201
|
+
* series' own so the inset reads as a finer copy of the chart. */
|
|
11202
|
+
this.seriesLook = {
|
|
11203
|
+
style: "candles",
|
|
11204
|
+
upColor: BULLISH,
|
|
11205
|
+
downColor: BEARISH,
|
|
11206
|
+
lineColor: BULLISH
|
|
11207
|
+
};
|
|
11161
11208
|
}
|
|
11162
11209
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
11163
11210
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -11202,6 +11249,8 @@ var DrawingPainter = class {
|
|
|
11202
11249
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
11203
11250
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
11204
11251
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
11252
|
+
} else if (ghost instanceof Magnifier) {
|
|
11253
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
11205
11254
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
11206
11255
|
ctx.globalAlpha = 1;
|
|
11207
11256
|
}
|
|
@@ -11335,6 +11384,10 @@ var DrawingPainter = class {
|
|
|
11335
11384
|
this.paintLabel(ctx, d, proj, theme);
|
|
11336
11385
|
return;
|
|
11337
11386
|
}
|
|
11387
|
+
if (d instanceof Magnifier) {
|
|
11388
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
11389
|
+
return;
|
|
11390
|
+
}
|
|
11338
11391
|
if (d instanceof PatternDrawing) {
|
|
11339
11392
|
this.paintPattern(ctx, d, proj, theme);
|
|
11340
11393
|
return;
|
|
@@ -11827,6 +11880,216 @@ var DrawingPainter = class {
|
|
|
11827
11880
|
ctx.textBaseline = "alphabetic";
|
|
11828
11881
|
}
|
|
11829
11882
|
}
|
|
11883
|
+
/** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
|
|
11884
|
+
* no series read, so dragging the area open never kicks a fetch per cursor move. */
|
|
11885
|
+
paintMagnifierGhost(ctx, d, proj, theme) {
|
|
11886
|
+
const r = d.rect(proj);
|
|
11887
|
+
if (!r) return;
|
|
11888
|
+
ctx.save();
|
|
11889
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
11890
|
+
ctx.lineWidth = 1;
|
|
11891
|
+
ctx.setLineDash([4, 4]);
|
|
11892
|
+
ctx.strokeRect(Math.min(r.x1, r.x2), Math.min(r.y1, r.y2), Math.abs(r.x2 - r.x1), Math.abs(r.y2 - r.y1));
|
|
11893
|
+
ctx.restore();
|
|
11894
|
+
}
|
|
11895
|
+
/** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
|
|
11896
|
+
* market at a finer timeframe — candles at their true time/price positions, clipped to
|
|
11897
|
+
* the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
|
|
11898
|
+
* `unavailable` states paint a short notice instead). The lower-timeframe candles shift
|
|
11899
|
+
* half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
|
|
11900
|
+
* centered on its open time — subdivides in place. */
|
|
11901
|
+
paintMagnifier(ctx, d, proj, theme) {
|
|
11902
|
+
const r = d.rect(proj);
|
|
11903
|
+
const a = d.anchors[0];
|
|
11904
|
+
const b = d.anchors[1];
|
|
11905
|
+
if (!r || !a || !b) return;
|
|
11906
|
+
const x0 = Math.min(r.x1, r.x2);
|
|
11907
|
+
const x1 = Math.max(r.x1, r.x2);
|
|
11908
|
+
const y0 = Math.min(r.y1, r.y2);
|
|
11909
|
+
const y1 = Math.max(r.y1, r.y2);
|
|
11910
|
+
const w = x1 - x0;
|
|
11911
|
+
const h = y1 - y0;
|
|
11912
|
+
ctx.save();
|
|
11913
|
+
ctx.globalAlpha = 1;
|
|
11914
|
+
ctx.fillStyle = theme.background;
|
|
11915
|
+
ctx.fillRect(x0, y0, w, h);
|
|
11916
|
+
ctx.restore();
|
|
11917
|
+
const from = Math.min(a.time, b.time);
|
|
11918
|
+
const to = Math.max(a.time, b.time);
|
|
11919
|
+
const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
|
|
11920
|
+
const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
|
|
11921
|
+
const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
|
|
11922
|
+
let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
|
|
11923
|
+
if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
|
|
11924
|
+
const look = this.seriesLook;
|
|
11925
|
+
const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
|
|
11926
|
+
if (transform) seriesBars = transform.full(seriesBars);
|
|
11927
|
+
const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
|
|
11928
|
+
const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
|
|
11929
|
+
ctx.save();
|
|
11930
|
+
ctx.beginPath();
|
|
11931
|
+
ctx.rect(x0, y0, w, h);
|
|
11932
|
+
ctx.clip();
|
|
11933
|
+
if (mode === "line" || mode === "area") {
|
|
11934
|
+
this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
|
|
11935
|
+
} else {
|
|
11936
|
+
this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
|
|
11937
|
+
}
|
|
11938
|
+
ctx.restore();
|
|
11939
|
+
} else if (res) {
|
|
11940
|
+
const notice = res.state === "loading" ? "Loading\u2026" : res.state === "ready" ? "No lower-timeframe data" : res.reason === "too-wide" ? "Area too wide for this timeframe" : res.reason === "none-lower" ? "No lower timeframe available" : res.reason === "not-lower" ? "Pick a timeframe below the chart" : "No data source";
|
|
11941
|
+
this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
|
|
11942
|
+
}
|
|
11943
|
+
ctx.save();
|
|
11944
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
11945
|
+
ctx.lineWidth = d.style.lineWidth || 1;
|
|
11946
|
+
ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
|
|
11947
|
+
ctx.strokeRect(x0, y0, w, h);
|
|
11948
|
+
ctx.restore();
|
|
11949
|
+
if (w > 44) {
|
|
11950
|
+
const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
|
|
11951
|
+
const chipH = 17;
|
|
11952
|
+
const gap = 4;
|
|
11953
|
+
const pane = proj.paneRect?.(d.paneId);
|
|
11954
|
+
const paneBottom = pane ? pane.top + pane.height : proj.height;
|
|
11955
|
+
const below = y1 + gap + chipH <= paneBottom;
|
|
11956
|
+
const chipY = below ? y1 + gap : y1 - gap - chipH;
|
|
11957
|
+
ctx.save();
|
|
11958
|
+
ctx.font = `10px ${theme.fontFamily}`;
|
|
11959
|
+
const tw = ctx.measureText(label).width;
|
|
11960
|
+
const caretW = 11;
|
|
11961
|
+
const chipW = tw + 12 + caretW;
|
|
11962
|
+
roundRect(ctx, x0, chipY, chipW, chipH, 3);
|
|
11963
|
+
ctx.fillStyle = theme.background;
|
|
11964
|
+
ctx.fill();
|
|
11965
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
11966
|
+
ctx.lineWidth = 1;
|
|
11967
|
+
ctx.setLineDash([]);
|
|
11968
|
+
ctx.stroke();
|
|
11969
|
+
ctx.fillStyle = theme.textColor;
|
|
11970
|
+
ctx.textAlign = "left";
|
|
11971
|
+
ctx.textBaseline = "middle";
|
|
11972
|
+
ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
|
|
11973
|
+
const cxr = x0 + 6 + tw + 5;
|
|
11974
|
+
const cyr = chipY + chipH / 2;
|
|
11975
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
|
|
11976
|
+
ctx.lineWidth = 1.2;
|
|
11977
|
+
ctx.beginPath();
|
|
11978
|
+
ctx.moveTo(cxr, cyr - 1.5);
|
|
11979
|
+
ctx.lineTo(cxr + 2.5, cyr + 1.5);
|
|
11980
|
+
ctx.lineTo(cxr + 5, cyr - 1.5);
|
|
11981
|
+
ctx.stroke();
|
|
11982
|
+
ctx.restore();
|
|
11983
|
+
d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
|
|
11984
|
+
} else {
|
|
11985
|
+
d.chipRect = null;
|
|
11986
|
+
}
|
|
11987
|
+
}
|
|
11988
|
+
/** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
|
|
11989
|
+
* by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
|
|
11990
|
+
* one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
|
|
11991
|
+
paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
|
|
11992
|
+
ctx.setLineDash([]);
|
|
11993
|
+
ctx.lineWidth = 1;
|
|
11994
|
+
for (const bar of bars) {
|
|
11995
|
+
const cx0 = proj.xOf(bar.time) - halfPitch;
|
|
11996
|
+
const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
|
|
11997
|
+
if (cx1 < x0 || cx0 > x1) continue;
|
|
11998
|
+
const yHigh = proj.yOf(bar.high, d.paneId);
|
|
11999
|
+
const yLow = proj.yOf(bar.low, d.paneId);
|
|
12000
|
+
const yOpen = proj.yOf(bar.open, d.paneId);
|
|
12001
|
+
const yClose = proj.yOf(bar.close, d.paneId);
|
|
12002
|
+
if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
|
|
12003
|
+
const color = bar.close >= bar.open ? upColor : downColor;
|
|
12004
|
+
const cellW = cx1 - cx0;
|
|
12005
|
+
const cx = (cx0 + cx1) / 2;
|
|
12006
|
+
ctx.strokeStyle = color;
|
|
12007
|
+
ctx.beginPath();
|
|
12008
|
+
ctx.moveTo(cx, yHigh);
|
|
12009
|
+
ctx.lineTo(cx, yLow);
|
|
12010
|
+
ctx.stroke();
|
|
12011
|
+
if (cellW < 3) continue;
|
|
12012
|
+
if (mode === "bars") {
|
|
12013
|
+
const tick = Math.max(1, cellW * 0.35);
|
|
12014
|
+
ctx.beginPath();
|
|
12015
|
+
ctx.moveTo(cx - tick, yOpen);
|
|
12016
|
+
ctx.lineTo(cx, yOpen);
|
|
12017
|
+
ctx.moveTo(cx, yClose);
|
|
12018
|
+
ctx.lineTo(cx + tick, yClose);
|
|
12019
|
+
ctx.stroke();
|
|
12020
|
+
} else {
|
|
12021
|
+
const bw = Math.max(1, cellW * 0.7);
|
|
12022
|
+
ctx.fillStyle = color;
|
|
12023
|
+
ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
|
|
12024
|
+
}
|
|
12025
|
+
}
|
|
12026
|
+
}
|
|
12027
|
+
/** The magnifier's line/area rendering: a close polyline through each cell's center (same
|
|
12028
|
+
* half-chart-bar shift as the candles), with an optional translucent fill down to the
|
|
12029
|
+
* rectangle's bottom edge for the area style. Colored like the chart's own line series. */
|
|
12030
|
+
paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
|
|
12031
|
+
const pts = [];
|
|
12032
|
+
for (const bar of bars) {
|
|
12033
|
+
const y = proj.yOf(bar.close, d.paneId);
|
|
12034
|
+
if (y == null) continue;
|
|
12035
|
+
pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
|
|
12036
|
+
}
|
|
12037
|
+
if (pts.length < 2) return;
|
|
12038
|
+
if (area) {
|
|
12039
|
+
ctx.beginPath();
|
|
12040
|
+
ctx.moveTo(pts[0][0], yBottom);
|
|
12041
|
+
for (const [px, py] of pts) ctx.lineTo(px, py);
|
|
12042
|
+
ctx.lineTo(pts[pts.length - 1][0], yBottom);
|
|
12043
|
+
ctx.closePath();
|
|
12044
|
+
ctx.fillStyle = withAlpha(color, 0.15);
|
|
12045
|
+
ctx.fill();
|
|
12046
|
+
}
|
|
12047
|
+
ctx.setLineDash([]);
|
|
12048
|
+
ctx.lineWidth = 1.5;
|
|
12049
|
+
ctx.strokeStyle = color;
|
|
12050
|
+
ctx.beginPath();
|
|
12051
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
12052
|
+
for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
12053
|
+
ctx.stroke();
|
|
12054
|
+
}
|
|
12055
|
+
/** Centered muted notice inside the magnifier rect (loading / unavailable states). */
|
|
12056
|
+
paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
|
|
12057
|
+
if (w < 60 || h < 20) return;
|
|
12058
|
+
ctx.save();
|
|
12059
|
+
ctx.beginPath();
|
|
12060
|
+
ctx.rect(x0, y0, w, h);
|
|
12061
|
+
ctx.clip();
|
|
12062
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
12063
|
+
ctx.fillStyle = withAlpha(theme.textColor, 0.55);
|
|
12064
|
+
ctx.textAlign = "center";
|
|
12065
|
+
ctx.textBaseline = "middle";
|
|
12066
|
+
ctx.fillText(text, x0 + w / 2, y0 + h / 2);
|
|
12067
|
+
ctx.restore();
|
|
12068
|
+
}
|
|
12069
|
+
/** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
|
|
12070
|
+
* "drag an area"). Painted by the drawings layer while the tool is armed and no placement
|
|
12071
|
+
* is in progress; chart-background fill so it reads as chrome over any content. */
|
|
12072
|
+
paintPlacementHint(ctx, text, theme, width, height) {
|
|
12073
|
+
ctx.save();
|
|
12074
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
12075
|
+
const tw = ctx.measureText(text).width;
|
|
12076
|
+
const pillW = tw + 24;
|
|
12077
|
+
const pillH = 24;
|
|
12078
|
+
const x = (width - pillW) / 2;
|
|
12079
|
+
const y = height - pillH - 14;
|
|
12080
|
+
roundRect(ctx, x, y, pillW, pillH, pillH / 2);
|
|
12081
|
+
ctx.fillStyle = theme.background;
|
|
12082
|
+
ctx.fill();
|
|
12083
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
12084
|
+
ctx.lineWidth = 1;
|
|
12085
|
+
ctx.setLineDash([]);
|
|
12086
|
+
ctx.stroke();
|
|
12087
|
+
ctx.fillStyle = theme.textColor;
|
|
12088
|
+
ctx.textAlign = "center";
|
|
12089
|
+
ctx.textBaseline = "middle";
|
|
12090
|
+
ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
|
|
12091
|
+
ctx.restore();
|
|
12092
|
+
}
|
|
11830
12093
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
11831
12094
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
11832
12095
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -12545,6 +12808,22 @@ var DrawingInteraction = class {
|
|
|
12545
12808
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
12546
12809
|
return snapped;
|
|
12547
12810
|
}
|
|
12811
|
+
/**
|
|
12812
|
+
* Resolve a cursor pixel through the magnet and return the snapped pixel — the same
|
|
12813
|
+
* conversion drawing placement uses. Updates the snap-ring marker. The measure
|
|
12814
|
+
* ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
|
|
12815
|
+
*/
|
|
12816
|
+
snapCursor(x, y, mode) {
|
|
12817
|
+
const proj = this.deps.projector();
|
|
12818
|
+
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
12819
|
+
const point = this.resolve(x, y, paneId, mode);
|
|
12820
|
+
const sy = proj.yOf(point.price, paneId);
|
|
12821
|
+
return { x: proj.xOf(point.time), y: sy ?? y };
|
|
12822
|
+
}
|
|
12823
|
+
/** Drop the snap-ring marker (a transient mode ended without going through `up`). */
|
|
12824
|
+
clearSnapMarker() {
|
|
12825
|
+
this.snapAt = null;
|
|
12826
|
+
}
|
|
12548
12827
|
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
12549
12828
|
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
12550
12829
|
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
@@ -13226,8 +13505,9 @@ function ensureStyles4() {
|
|
|
13226
13505
|
if (!existing) document.head.appendChild(s);
|
|
13227
13506
|
}
|
|
13228
13507
|
var DrawingSettingsPopup = class {
|
|
13229
|
-
constructor(host, theme) {
|
|
13508
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
13230
13509
|
this.host = host;
|
|
13510
|
+
this.chartBarMs = chartBarMs;
|
|
13231
13511
|
this.el = null;
|
|
13232
13512
|
this.tipEl = null;
|
|
13233
13513
|
// floating hover-label (above/below the toolbar)
|
|
@@ -13252,6 +13532,14 @@ var DrawingSettingsPopup = class {
|
|
|
13252
13532
|
this.theme = theme;
|
|
13253
13533
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
13254
13534
|
}
|
|
13535
|
+
/** The magnifier timeframe choices strictly below the chart's own bar duration
|
|
13536
|
+
* (`auto` rides along while at least one concrete lower step exists). */
|
|
13537
|
+
lowerTimeframeOptions() {
|
|
13538
|
+
const chartMs = this.chartBarMs();
|
|
13539
|
+
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
13540
|
+
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
13541
|
+
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
13542
|
+
}
|
|
13255
13543
|
setTheme(theme) {
|
|
13256
13544
|
this.theme = theme;
|
|
13257
13545
|
this.settingsDialog.setTheme(theme);
|
|
@@ -13294,7 +13582,20 @@ var DrawingSettingsPopup = class {
|
|
|
13294
13582
|
const sz = drawing.size ?? "normal";
|
|
13295
13583
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
13296
13584
|
}
|
|
13297
|
-
if (paths.has("
|
|
13585
|
+
if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
|
|
13586
|
+
const options = this.lowerTimeframeOptions();
|
|
13587
|
+
if (options.length > 0) {
|
|
13588
|
+
bar.appendChild(
|
|
13589
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
13590
|
+
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
13591
|
+
labelInTrigger: true
|
|
13592
|
+
})
|
|
13593
|
+
);
|
|
13594
|
+
}
|
|
13595
|
+
bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
|
|
13596
|
+
bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
|
|
13597
|
+
}
|
|
13598
|
+
if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || (drawing instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), (v) => actions.patch({ "style.lineColor": v })));
|
|
13298
13599
|
if (paths.has("style.lineWidth")) {
|
|
13299
13600
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
13300
13601
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -13616,6 +13917,7 @@ var DrawingSettingsPopup = class {
|
|
|
13616
13917
|
this.colorPop = null;
|
|
13617
13918
|
this.colorOwner = null;
|
|
13618
13919
|
}
|
|
13920
|
+
opts.onClose?.();
|
|
13619
13921
|
}
|
|
13620
13922
|
});
|
|
13621
13923
|
const el = pop.el;
|
|
@@ -13634,6 +13936,49 @@ var DrawingSettingsPopup = class {
|
|
|
13634
13936
|
pop.show();
|
|
13635
13937
|
return pop;
|
|
13636
13938
|
}
|
|
13939
|
+
/**
|
|
13940
|
+
* A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
|
|
13941
|
+
* canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
|
|
13942
|
+
* to position against, and removed again when the menu closes. Independent of the
|
|
13943
|
+
* quick toolbar — the chip works without selecting the drawing first.
|
|
13944
|
+
*/
|
|
13945
|
+
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
13946
|
+
ensureStyles4();
|
|
13947
|
+
closeOpenPopovers();
|
|
13948
|
+
const options = this.lowerTimeframeOptions();
|
|
13949
|
+
const anchor = document.createElement("div");
|
|
13950
|
+
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
13951
|
+
this.host.appendChild(anchor);
|
|
13952
|
+
this.menuPop = this.hostFloat(anchor, {
|
|
13953
|
+
zIndex: 26,
|
|
13954
|
+
padding: "4px",
|
|
13955
|
+
onClose: () => anchor.remove(),
|
|
13956
|
+
fill: (menu, pop) => {
|
|
13957
|
+
if (options.length === 0) {
|
|
13958
|
+
const note = document.createElement("div");
|
|
13959
|
+
note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
|
|
13960
|
+
note.textContent = "No lower timeframe available";
|
|
13961
|
+
menu.appendChild(note);
|
|
13962
|
+
return;
|
|
13963
|
+
}
|
|
13964
|
+
for (const o of options) {
|
|
13965
|
+
const item = document.createElement("button");
|
|
13966
|
+
item.type = "button";
|
|
13967
|
+
item.className = "vela-dpop-item";
|
|
13968
|
+
item.dataset.active = o.value === current ? "1" : "0";
|
|
13969
|
+
item.style.cssText = "display:flex;align-items:center;min-width:88px;padding:5px 10px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;font-variant-numeric:tabular-nums;";
|
|
13970
|
+
item.textContent = o.label;
|
|
13971
|
+
item.addEventListener("click", (e) => {
|
|
13972
|
+
e.stopPropagation();
|
|
13973
|
+
pop.hide();
|
|
13974
|
+
onPick(o.value);
|
|
13975
|
+
});
|
|
13976
|
+
menu.appendChild(item);
|
|
13977
|
+
}
|
|
13978
|
+
}
|
|
13979
|
+
});
|
|
13980
|
+
this.menuOwner = anchor;
|
|
13981
|
+
}
|
|
13637
13982
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
13638
13983
|
openActionMenu(anchor, rows) {
|
|
13639
13984
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -13744,10 +14089,13 @@ var DrawingSettingsPopup = class {
|
|
|
13744
14089
|
let cur = current;
|
|
13745
14090
|
const paint = (v) => {
|
|
13746
14091
|
b.replaceChildren();
|
|
13747
|
-
const
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
14092
|
+
const glyph = render(v);
|
|
14093
|
+
if (glyph) {
|
|
14094
|
+
const ic = document.createElement("span");
|
|
14095
|
+
ic.style.cssText = "display:flex;";
|
|
14096
|
+
ic.innerHTML = sized(glyph);
|
|
14097
|
+
b.appendChild(ic);
|
|
14098
|
+
}
|
|
13751
14099
|
if (opts.label && opts.labelInTrigger) {
|
|
13752
14100
|
const tx = document.createElement("span");
|
|
13753
14101
|
tx.textContent = opts.label(v);
|
|
@@ -13789,10 +14137,13 @@ var DrawingSettingsPopup = class {
|
|
|
13789
14137
|
item.className = "vela-dpop-item";
|
|
13790
14138
|
item.dataset.active = active ? "1" : "0";
|
|
13791
14139
|
item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
|
|
13792
|
-
const
|
|
13793
|
-
|
|
13794
|
-
|
|
13795
|
-
|
|
14140
|
+
const glyph = render(v);
|
|
14141
|
+
if (glyph) {
|
|
14142
|
+
const ic = document.createElement("span");
|
|
14143
|
+
ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
|
|
14144
|
+
ic.innerHTML = sized(glyph, 18);
|
|
14145
|
+
item.appendChild(ic);
|
|
14146
|
+
}
|
|
13796
14147
|
if (label) {
|
|
13797
14148
|
const tx = document.createElement("span");
|
|
13798
14149
|
tx.textContent = label(v);
|
|
@@ -13950,30 +14301,39 @@ var MeasureOverlay = class {
|
|
|
13950
14301
|
isFinished() {
|
|
13951
14302
|
return this.state === "finished";
|
|
13952
14303
|
}
|
|
13953
|
-
/** A press: begin the measurement, or finish it on the second click.
|
|
13954
|
-
|
|
14304
|
+
/** A press: begin the measurement, or finish it on the second click.
|
|
14305
|
+
* `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
|
|
14306
|
+
* graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
|
|
14307
|
+
down(x, y, gx = x, gy = y) {
|
|
13955
14308
|
if (this.state === "measuring") {
|
|
13956
|
-
this.end = { x, y };
|
|
14309
|
+
this.end = { x: gx, y: gy };
|
|
13957
14310
|
this.state = "finished";
|
|
13958
14311
|
return;
|
|
13959
14312
|
}
|
|
13960
|
-
this.start = { x, y };
|
|
13961
|
-
this.end = { x, y };
|
|
14313
|
+
this.start = { x: gx, y: gy };
|
|
14314
|
+
this.end = { x: gx, y: gy };
|
|
13962
14315
|
this.pressX = x;
|
|
13963
14316
|
this.pressY = y;
|
|
13964
14317
|
this.state = "measuring";
|
|
13965
14318
|
}
|
|
14319
|
+
/** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
|
|
13966
14320
|
move(x, y) {
|
|
13967
14321
|
if (this.state === "measuring") this.end = { x, y };
|
|
13968
14322
|
}
|
|
13969
14323
|
/** A release: finish if the press was actually dragged (press-drag-release), else wait
|
|
13970
|
-
* for the second click (click-move-click).
|
|
13971
|
-
|
|
14324
|
+
* for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
|
|
14325
|
+
* are the graphic end (magnet-snapped when the magnet is on). */
|
|
14326
|
+
up(x, y, gx = x, gy = y) {
|
|
13972
14327
|
if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP3) {
|
|
13973
|
-
this.end = { x, y };
|
|
14328
|
+
this.end = { x: gx, y: gy };
|
|
13974
14329
|
this.state = "finished";
|
|
13975
14330
|
}
|
|
13976
14331
|
}
|
|
14332
|
+
/** Current graphic endpoints in media pixels, or null when idle. */
|
|
14333
|
+
points() {
|
|
14334
|
+
if (!this.start || !this.end) return null;
|
|
14335
|
+
return { start: this.start, end: this.end };
|
|
14336
|
+
}
|
|
13977
14337
|
clear() {
|
|
13978
14338
|
this.state = "idle";
|
|
13979
14339
|
this.start = null;
|
|
@@ -14135,6 +14495,9 @@ var UserDrawingController = class {
|
|
|
14135
14495
|
this.intentCb = null;
|
|
14136
14496
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
14137
14497
|
this.externalGhost = null;
|
|
14498
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
14499
|
+
this.seriesGw = null;
|
|
14500
|
+
this.seriesGwUnsub = null;
|
|
14138
14501
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
14139
14502
|
this.lastDraftKey = null;
|
|
14140
14503
|
this.measure = new MeasureOverlay();
|
|
@@ -14162,7 +14525,7 @@ var UserDrawingController = class {
|
|
|
14162
14525
|
this.textEditor = null;
|
|
14163
14526
|
this.painter = new DrawingPainter();
|
|
14164
14527
|
this.ctx = canvas.getContext("2d");
|
|
14165
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
14528
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
14166
14529
|
this.toolbar = new DrawingToolbar(
|
|
14167
14530
|
toolbarHost,
|
|
14168
14531
|
deps.theme(),
|
|
@@ -14220,6 +14583,22 @@ var UserDrawingController = class {
|
|
|
14220
14583
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
14221
14584
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
14222
14585
|
}
|
|
14586
|
+
/** Core push: the series gateway data-driven drawings read finer-timeframe bars
|
|
14587
|
+
* through (surfaced to them as `Projector.seriesInRange`). A landed background
|
|
14588
|
+
* fetch repaints both this layer and the interleave slices under the series. */
|
|
14589
|
+
setSeriesGateway(gateway) {
|
|
14590
|
+
this.seriesGwUnsub?.();
|
|
14591
|
+
this.seriesGw = gateway;
|
|
14592
|
+
this.seriesGwUnsub = gateway.onUpdate(() => {
|
|
14593
|
+
this.invalidateSlices();
|
|
14594
|
+
this.render();
|
|
14595
|
+
this.deps.requestDataPaint();
|
|
14596
|
+
});
|
|
14597
|
+
}
|
|
14598
|
+
/** The pushed series gateway, or null before the core provides one. */
|
|
14599
|
+
get seriesGateway() {
|
|
14600
|
+
return this.seriesGw;
|
|
14601
|
+
}
|
|
14223
14602
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
14224
14603
|
setExternalGhost(doc) {
|
|
14225
14604
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -14337,8 +14716,34 @@ var UserDrawingController = class {
|
|
|
14337
14716
|
/** Should the drawing layer win this press (vs pan)? */
|
|
14338
14717
|
claim(x, y) {
|
|
14339
14718
|
if (this.measureMode || this.eraserMode) return true;
|
|
14719
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
14340
14720
|
return this.interaction.claim(x, y);
|
|
14341
14721
|
}
|
|
14722
|
+
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
14723
|
+
* the chip's rect is what the painter measured last frame. */
|
|
14724
|
+
magnifierChipAt(x, y) {
|
|
14725
|
+
for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
|
|
14726
|
+
const d = this.drawings[i];
|
|
14727
|
+
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
14728
|
+
const r = d.chipRect;
|
|
14729
|
+
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
14730
|
+
}
|
|
14731
|
+
return null;
|
|
14732
|
+
}
|
|
14733
|
+
/** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
|
|
14734
|
+
* settings-popup edit (same intent, same undo step). */
|
|
14735
|
+
openMagnifierChipMenu(drawing) {
|
|
14736
|
+
const rect = drawing.chipRect;
|
|
14737
|
+
if (!rect) return;
|
|
14738
|
+
const id = drawing.id;
|
|
14739
|
+
this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
|
|
14740
|
+
const d = this.drawings.find((x) => x.id === id);
|
|
14741
|
+
if (!(d instanceof Magnifier)) return;
|
|
14742
|
+
d.applySettings({ "magnifier.timeframe": value });
|
|
14743
|
+
this.render();
|
|
14744
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
14745
|
+
});
|
|
14746
|
+
}
|
|
14342
14747
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
14343
14748
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
14344
14749
|
deleteAt(x, y) {
|
|
@@ -14350,11 +14755,13 @@ var UserDrawingController = class {
|
|
|
14350
14755
|
}
|
|
14351
14756
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
14352
14757
|
* gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
|
|
14353
|
-
*
|
|
14354
|
-
|
|
14758
|
+
* `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
|
|
14759
|
+
* false when a mode/tool is already active (the normal press path owns it). */
|
|
14760
|
+
beginMeasureAt(x, y, snap = "off") {
|
|
14355
14761
|
if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
|
|
14356
14762
|
this.withModeIntent(() => this.toggleMeasure());
|
|
14357
|
-
this.
|
|
14763
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
14764
|
+
this.measure.down(x, y, g.x, g.y);
|
|
14358
14765
|
this.render();
|
|
14359
14766
|
return true;
|
|
14360
14767
|
}
|
|
@@ -14376,11 +14783,19 @@ var UserDrawingController = class {
|
|
|
14376
14783
|
return;
|
|
14377
14784
|
}
|
|
14378
14785
|
if (this.measureMode) {
|
|
14379
|
-
this.
|
|
14786
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
14787
|
+
this.measure.down(x, y, g.x, g.y);
|
|
14380
14788
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
14381
14789
|
this.render();
|
|
14382
14790
|
return;
|
|
14383
14791
|
}
|
|
14792
|
+
if (this.activeTool == null) {
|
|
14793
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
14794
|
+
if (chipOwner) {
|
|
14795
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
14796
|
+
return;
|
|
14797
|
+
}
|
|
14798
|
+
}
|
|
14384
14799
|
this.interaction.down(x, y, snap, shift);
|
|
14385
14800
|
}
|
|
14386
14801
|
pointerMove(x, y, snap = "off", shift = false) {
|
|
@@ -14389,7 +14804,8 @@ var UserDrawingController = class {
|
|
|
14389
14804
|
return;
|
|
14390
14805
|
}
|
|
14391
14806
|
if (this.measureMode) {
|
|
14392
|
-
this.
|
|
14807
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
14808
|
+
this.measure.move(g.x, g.y);
|
|
14393
14809
|
this.render();
|
|
14394
14810
|
return;
|
|
14395
14811
|
}
|
|
@@ -14407,13 +14823,14 @@ var UserDrawingController = class {
|
|
|
14407
14823
|
this.render();
|
|
14408
14824
|
}
|
|
14409
14825
|
}
|
|
14410
|
-
pointerUp(x, y) {
|
|
14826
|
+
pointerUp(x, y, snap = "off") {
|
|
14411
14827
|
if (this.eraserMode) {
|
|
14412
14828
|
this.erasing = false;
|
|
14413
14829
|
return;
|
|
14414
14830
|
}
|
|
14415
14831
|
if (this.measureMode) {
|
|
14416
|
-
this.
|
|
14832
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
14833
|
+
this.measure.up(x, y, g.x, g.y);
|
|
14417
14834
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
14418
14835
|
this.render();
|
|
14419
14836
|
return;
|
|
@@ -14458,6 +14875,7 @@ var UserDrawingController = class {
|
|
|
14458
14875
|
/** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
|
|
14459
14876
|
exitMeasure(clearGraphic = true) {
|
|
14460
14877
|
this.measureMode = false;
|
|
14878
|
+
this.interaction.clearSnapMarker();
|
|
14461
14879
|
if (clearGraphic) this.measure.clear();
|
|
14462
14880
|
this.toolbar.setMeasureActive(false);
|
|
14463
14881
|
this.render();
|
|
@@ -14596,17 +15014,31 @@ var UserDrawingController = class {
|
|
|
14596
15014
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
14597
15015
|
cursorAt(x, y) {
|
|
14598
15016
|
if (this.eraserMode) return "pointer";
|
|
15017
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
14599
15018
|
return this.interaction.cursorAt(x, y);
|
|
14600
15019
|
}
|
|
14601
|
-
/** Right-click
|
|
14602
|
-
*
|
|
14603
|
-
*
|
|
14604
|
-
*
|
|
15020
|
+
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
15021
|
+
* placement or measurement, and also plain-disarms an armed-but-idle drawing
|
|
15022
|
+
* tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
|
|
15023
|
+
* stay-in-drawing-mode (where Escape would leave a drawing tool armed).
|
|
15024
|
+
* Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
|
|
15025
|
+
* whether the press was consumed; false lets the host's context menu open
|
|
15026
|
+
* normally. */
|
|
14605
15027
|
cancelPlacement() {
|
|
14606
|
-
if (
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
15028
|
+
if (this.measureMode || this.eraserMode) {
|
|
15029
|
+
this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
|
|
15030
|
+
return true;
|
|
15031
|
+
}
|
|
15032
|
+
if (this.interaction.isPlacing()) {
|
|
15033
|
+
this.interaction.cancel();
|
|
15034
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
15035
|
+
return true;
|
|
15036
|
+
}
|
|
15037
|
+
if (this.activeTool != null) {
|
|
15038
|
+
this.emit({ kind: "arm", type: null });
|
|
15039
|
+
return true;
|
|
15040
|
+
}
|
|
15041
|
+
return false;
|
|
14610
15042
|
}
|
|
14611
15043
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
14612
15044
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
@@ -14632,6 +15064,10 @@ var UserDrawingController = class {
|
|
|
14632
15064
|
return true;
|
|
14633
15065
|
}
|
|
14634
15066
|
if (this.interaction.cancel()) return true;
|
|
15067
|
+
if (this.measureMode) {
|
|
15068
|
+
this.withModeIntent(() => this.exitMeasure());
|
|
15069
|
+
return true;
|
|
15070
|
+
}
|
|
14635
15071
|
if (this.selectedIds.size) {
|
|
14636
15072
|
this.clearSelection();
|
|
14637
15073
|
return true;
|
|
@@ -14757,6 +15193,7 @@ var UserDrawingController = class {
|
|
|
14757
15193
|
if (!sctx) continue;
|
|
14758
15194
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
14759
15195
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
15196
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
14760
15197
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
14761
15198
|
const slices = out.get(paneId) ?? [];
|
|
14762
15199
|
slices.push({ beforeZ, canvas });
|
|
@@ -14784,6 +15221,7 @@ var UserDrawingController = class {
|
|
|
14784
15221
|
dragged: this.interaction.activeDragId(),
|
|
14785
15222
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
14786
15223
|
};
|
|
15224
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
14787
15225
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
14788
15226
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
14789
15227
|
this.layoutTextEditor();
|
|
@@ -14791,6 +15229,10 @@ var UserDrawingController = class {
|
|
|
14791
15229
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
14792
15230
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
14793
15231
|
this.emitDraft(ghost);
|
|
15232
|
+
if (this.activeTool && !ghost) {
|
|
15233
|
+
const hint = getDrawingType(this.activeTool)?.placementHint;
|
|
15234
|
+
if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
|
|
15235
|
+
}
|
|
14794
15236
|
const markers = this.interaction.placingMarkers(proj);
|
|
14795
15237
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
14796
15238
|
const m = this.interaction.snapMarker();
|
|
@@ -14802,6 +15244,9 @@ var UserDrawingController = class {
|
|
|
14802
15244
|
this.closeTextEditor();
|
|
14803
15245
|
this.popup.destroy();
|
|
14804
15246
|
this.toolbar.destroy();
|
|
15247
|
+
this.seriesGwUnsub?.();
|
|
15248
|
+
this.seriesGwUnsub = null;
|
|
15249
|
+
this.seriesGw = null;
|
|
14805
15250
|
this.intentCb = null;
|
|
14806
15251
|
this.drawings = [];
|
|
14807
15252
|
}
|
|
@@ -15199,7 +15644,7 @@ function mergeSlices(indicator, user) {
|
|
|
15199
15644
|
}
|
|
15200
15645
|
|
|
15201
15646
|
// src/renderers/native/drawings/Projector.ts
|
|
15202
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
15647
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
15203
15648
|
return {
|
|
15204
15649
|
xOf: (time) => coords.timeToX(time),
|
|
15205
15650
|
yOf: (price, paneId) => {
|
|
@@ -15220,6 +15665,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
|
15220
15665
|
},
|
|
15221
15666
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
15222
15667
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
15668
|
+
seriesInRange,
|
|
15223
15669
|
width: coords.width,
|
|
15224
15670
|
height: coords.height
|
|
15225
15671
|
};
|
|
@@ -17164,13 +17610,13 @@ var NativeRenderer = class {
|
|
|
17164
17610
|
resetView: () => this.resetView(),
|
|
17165
17611
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
17166
17612
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
17167
|
-
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
17613
|
+
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
17168
17614
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
17169
17615
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
17170
17616
|
drawingsSnapMode: () => this.snapMode,
|
|
17171
17617
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
17172
17618
|
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
17173
|
-
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
17619
|
+
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
17174
17620
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
17175
17621
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
17176
17622
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
@@ -17197,6 +17643,23 @@ var NativeRenderer = class {
|
|
|
17197
17643
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
17198
17644
|
priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
|
|
17199
17645
|
requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
|
|
17646
|
+
// The look the price series ACTUALLY paints with: candle colors resolved through
|
|
17647
|
+
// the per-style override, line/area colors through their configured styles — so
|
|
17648
|
+
// series-mirroring content (the magnifier inset) matches the chart exactly.
|
|
17649
|
+
seriesLook: () => {
|
|
17650
|
+
const st = this.scene.style;
|
|
17651
|
+
const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
|
|
17652
|
+
const barsUp = st.bars.upColor ?? this.theme.upColor;
|
|
17653
|
+
const barsDown = st.bars.downColor ?? this.theme.downColor;
|
|
17654
|
+
const style = this.scene.priceStyle;
|
|
17655
|
+
return {
|
|
17656
|
+
style,
|
|
17657
|
+
upColor: style === "bars" ? barsUp : paint.up,
|
|
17658
|
+
downColor: style === "bars" ? barsDown : paint.down,
|
|
17659
|
+
lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
|
|
17660
|
+
};
|
|
17661
|
+
},
|
|
17662
|
+
chartBarMs: () => this.coords.barInterval,
|
|
17200
17663
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
17201
17664
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
17202
17665
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -17485,7 +17948,8 @@ var NativeRenderer = class {
|
|
|
17485
17948
|
}
|
|
17486
17949
|
const skipFit = opts?.preserveView === true && this.didInitialFit;
|
|
17487
17950
|
if (this.coords.width > 0 && !skipFit) {
|
|
17488
|
-
this.
|
|
17951
|
+
if (this.didInitialFit) this.reframeKeepZoom();
|
|
17952
|
+
else this.fitContent();
|
|
17489
17953
|
this.didInitialFit = true;
|
|
17490
17954
|
}
|
|
17491
17955
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
@@ -18133,6 +18597,7 @@ var NativeRenderer = class {
|
|
|
18133
18597
|
this.scaleDragHeight = res.height;
|
|
18134
18598
|
this.scaleDragStart = { ...res.holder.scale };
|
|
18135
18599
|
res.holder.manualScale = { ...res.holder.scale };
|
|
18600
|
+
this.axisScaleButtons?.reposition();
|
|
18136
18601
|
this.scheduler.invalidate(4 /* Full */);
|
|
18137
18602
|
}
|
|
18138
18603
|
/** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
|
|
@@ -18164,6 +18629,7 @@ var NativeRenderer = class {
|
|
|
18164
18629
|
const res = this.resolveScaleHolder(x, y);
|
|
18165
18630
|
if (!res) return;
|
|
18166
18631
|
res.holder.manualScale = null;
|
|
18632
|
+
this.axisScaleButtons?.reposition();
|
|
18167
18633
|
this.scheduler.invalidate(4 /* Full */);
|
|
18168
18634
|
}
|
|
18169
18635
|
/**
|
|
@@ -18587,7 +19053,8 @@ var NativeRenderer = class {
|
|
|
18587
19053
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
18588
19054
|
},
|
|
18589
19055
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
18590
|
-
(from, to) => this.barsInTimeRange(from, to)
|
|
19056
|
+
(from, to) => this.barsInTimeRange(from, to),
|
|
19057
|
+
this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
|
|
18591
19058
|
);
|
|
18592
19059
|
}
|
|
18593
19060
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|
|
@@ -18801,6 +19268,20 @@ var NativeRenderer = class {
|
|
|
18801
19268
|
this.coords.setViewport(v);
|
|
18802
19269
|
this.targetBarSpacing = v.barSpacing;
|
|
18803
19270
|
}
|
|
19271
|
+
/** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
|
|
19272
|
+
* zoom (bar spacing), re-anchor the newest bars at the default right offset.
|
|
19273
|
+
* `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
|
|
19274
|
+
* head may still be backfilling toward the previous depth, and raising the spacing
|
|
19275
|
+
* to its temporary bar count would lose the zoom this exists to keep. */
|
|
19276
|
+
reframeKeepZoom() {
|
|
19277
|
+
this.animator?.stop();
|
|
19278
|
+
this.panVelocity = 0;
|
|
19279
|
+
for (const pane of this.scene.panes.values()) pane.manualScale = null;
|
|
19280
|
+
for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
|
|
19281
|
+
const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
|
|
19282
|
+
this.coords.setViewport(v);
|
|
19283
|
+
this.targetBarSpacing = v.barSpacing;
|
|
19284
|
+
}
|
|
18804
19285
|
paneBoundsFor(paneId) {
|
|
18805
19286
|
const p = this.scene.panes.get(paneId);
|
|
18806
19287
|
return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
|
|
@@ -19367,6 +19848,12 @@ var IndicatorHandleImpl = class {
|
|
|
19367
19848
|
get visible() {
|
|
19368
19849
|
return this.visibleState;
|
|
19369
19850
|
}
|
|
19851
|
+
inputValues() {
|
|
19852
|
+
return this.controller.inputValuesOf(this.id);
|
|
19853
|
+
}
|
|
19854
|
+
propValues() {
|
|
19855
|
+
return this.controller.propValuesOf(this.id);
|
|
19856
|
+
}
|
|
19370
19857
|
setInput(key, value) {
|
|
19371
19858
|
this.controller.applyInputs(this.id, { [key]: value });
|
|
19372
19859
|
}
|
|
@@ -19488,17 +19975,179 @@ function presetToRange(preset, bars) {
|
|
|
19488
19975
|
return { from: Math.max(first, from), to: last };
|
|
19489
19976
|
}
|
|
19490
19977
|
|
|
19491
|
-
// src/core/
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
19496
|
-
|
|
19497
|
-
|
|
19498
|
-
|
|
19499
|
-
|
|
19500
|
-
|
|
19501
|
-
|
|
19978
|
+
// src/core/engine/DrawingSeriesService.ts
|
|
19979
|
+
var MAX_BARS = 5e3;
|
|
19980
|
+
var PAD_FRAC = 0.25;
|
|
19981
|
+
var MAX_ENTRIES = 16;
|
|
19982
|
+
var RETRY_MS = 15e3;
|
|
19983
|
+
var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
|
|
19984
|
+
var DrawingSeriesService = class {
|
|
19985
|
+
constructor(deps) {
|
|
19986
|
+
this.deps = deps;
|
|
19987
|
+
/** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
|
|
19988
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
19989
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
19990
|
+
}
|
|
19991
|
+
seriesInRange(timeframe, from, to) {
|
|
19992
|
+
if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
|
|
19993
|
+
const resolved = this.resolveTimeframe(timeframe);
|
|
19994
|
+
if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
|
|
19995
|
+
const barMs = timeframeToMs(resolved);
|
|
19996
|
+
const lo = Math.min(from, to);
|
|
19997
|
+
const hi = Math.max(from, to);
|
|
19998
|
+
if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
|
|
19999
|
+
if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
|
|
20000
|
+
const key = `${this.deps.marketKey()}|${resolved}`;
|
|
20001
|
+
const entries = this.cache.get(key) ?? [];
|
|
20002
|
+
const covering = entries.find((e) => e.from <= lo && e.to >= hi);
|
|
20003
|
+
if (covering) {
|
|
20004
|
+
if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
|
|
20005
|
+
if (covering.failedAt > 0) {
|
|
20006
|
+
if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
|
|
20007
|
+
entries.splice(entries.indexOf(covering), 1);
|
|
20008
|
+
} else {
|
|
20009
|
+
this.maybeRefresh(key, covering, resolved, barMs, hi);
|
|
20010
|
+
return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
|
|
20011
|
+
}
|
|
20012
|
+
}
|
|
20013
|
+
this.fetchWindow(key, entries, resolved, lo, hi);
|
|
20014
|
+
return this.loading(entries, resolved, barMs, lo, hi);
|
|
20015
|
+
}
|
|
20016
|
+
onUpdate(listener) {
|
|
20017
|
+
this.listeners.add(listener);
|
|
20018
|
+
return () => this.listeners.delete(listener);
|
|
20019
|
+
}
|
|
20020
|
+
// ── internals ──
|
|
20021
|
+
/** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
|
|
20022
|
+
* step still below it); an explicit timeframe passes only when strictly finer. Failures
|
|
20023
|
+
* distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
|
|
20024
|
+
* at the finest offered step) so the consumer can word its notice honestly. */
|
|
20025
|
+
resolveTimeframe(timeframe) {
|
|
20026
|
+
const chartMs = timeframeToMs(this.deps.chartTimeframe());
|
|
20027
|
+
const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
|
|
20028
|
+
if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
|
|
20029
|
+
const tf = timeframe.trim() || "auto";
|
|
20030
|
+
if (tf === "auto") {
|
|
20031
|
+
for (const step of AUTO_STEPS) {
|
|
20032
|
+
if (timeframeToMs(step) <= chartMs / 4) return step;
|
|
20033
|
+
}
|
|
20034
|
+
return finest;
|
|
20035
|
+
}
|
|
20036
|
+
return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
|
|
20037
|
+
}
|
|
20038
|
+
/** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
|
|
20039
|
+
* windows — a widened window keeps painting what it already has while it fetches. */
|
|
20040
|
+
loading(entries, timeframe, barMs, lo, hi) {
|
|
20041
|
+
const partial = /* @__PURE__ */ new Map();
|
|
20042
|
+
for (const e of entries) {
|
|
20043
|
+
if (e.pending || e.failedAt > 0) continue;
|
|
20044
|
+
if (e.to < lo || e.from > hi) continue;
|
|
20045
|
+
for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
|
|
20046
|
+
}
|
|
20047
|
+
if (partial.size === 0) return { state: "loading", timeframe, barMs };
|
|
20048
|
+
const bars = [...partial.values()].sort((a, b) => a.time - b.time);
|
|
20049
|
+
return { state: "loading", timeframe, barMs, bars };
|
|
20050
|
+
}
|
|
20051
|
+
/** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
|
|
20052
|
+
* defers this one (a corner drag repaints per pointer move — kicking a window per
|
|
20053
|
+
* frame would spam the provider); when it lands, the next paint re-evaluates. */
|
|
20054
|
+
fetchWindow(key, entries, timeframe, lo, hi) {
|
|
20055
|
+
if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
|
|
20056
|
+
const pad = (hi - lo) * PAD_FRAC;
|
|
20057
|
+
const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
|
|
20058
|
+
entries.push(entry);
|
|
20059
|
+
this.cache.set(key, entries);
|
|
20060
|
+
this.evict();
|
|
20061
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
20062
|
+
entry.bars = bars;
|
|
20063
|
+
entry.fetchedAt = Date.now();
|
|
20064
|
+
entry.pending = false;
|
|
20065
|
+
this.absorbOverlaps(key, entry);
|
|
20066
|
+
this.fire();
|
|
20067
|
+
}).catch(() => {
|
|
20068
|
+
entry.pending = false;
|
|
20069
|
+
entry.failedAt = Date.now();
|
|
20070
|
+
this.fire();
|
|
20071
|
+
});
|
|
20072
|
+
}
|
|
20073
|
+
/** A window whose right edge reaches the newest fetched bar refreshes at most once per
|
|
20074
|
+
* bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
|
|
20075
|
+
maybeRefresh(key, entry, timeframe, barMs, hi) {
|
|
20076
|
+
if (entry.pending) return;
|
|
20077
|
+
const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
|
|
20078
|
+
if (hi < lastBar) return;
|
|
20079
|
+
if (Date.now() - entry.fetchedAt < barMs) return;
|
|
20080
|
+
entry.pending = true;
|
|
20081
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
20082
|
+
entry.bars = bars;
|
|
20083
|
+
entry.fetchedAt = Date.now();
|
|
20084
|
+
entry.pending = false;
|
|
20085
|
+
this.fire();
|
|
20086
|
+
}).catch(() => {
|
|
20087
|
+
entry.pending = false;
|
|
20088
|
+
entry.fetchedAt = Date.now();
|
|
20089
|
+
});
|
|
20090
|
+
}
|
|
20091
|
+
/** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
|
|
20092
|
+
* converges instead of accumulating slivers. */
|
|
20093
|
+
absorbOverlaps(key, entry) {
|
|
20094
|
+
const entries = this.cache.get(key);
|
|
20095
|
+
if (!entries) return;
|
|
20096
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
20097
|
+
const other = entries[i];
|
|
20098
|
+
if (other === entry || other.pending || other.failedAt > 0) continue;
|
|
20099
|
+
if (other.to < entry.from || other.from > entry.to) continue;
|
|
20100
|
+
const byTime = /* @__PURE__ */ new Map();
|
|
20101
|
+
for (const b of other.bars) byTime.set(b.time, b);
|
|
20102
|
+
for (const b of entry.bars) byTime.set(b.time, b);
|
|
20103
|
+
entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
20104
|
+
entry.from = Math.min(entry.from, other.from);
|
|
20105
|
+
entry.to = Math.max(entry.to, other.to);
|
|
20106
|
+
entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
|
|
20107
|
+
entries.splice(i, 1);
|
|
20108
|
+
}
|
|
20109
|
+
}
|
|
20110
|
+
/** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
|
|
20111
|
+
evict() {
|
|
20112
|
+
let total = 0;
|
|
20113
|
+
for (const entries of this.cache.values()) total += entries.length;
|
|
20114
|
+
while (total > MAX_ENTRIES) {
|
|
20115
|
+
let oldestKey = null;
|
|
20116
|
+
let oldestIdx = -1;
|
|
20117
|
+
let oldestAt = Infinity;
|
|
20118
|
+
for (const [key, entries2] of this.cache) {
|
|
20119
|
+
for (let i = 0; i < entries2.length; i += 1) {
|
|
20120
|
+
const e = entries2[i];
|
|
20121
|
+
if (e.pending) continue;
|
|
20122
|
+
const at = e.fetchedAt || e.failedAt;
|
|
20123
|
+
if (at < oldestAt) {
|
|
20124
|
+
oldestKey = key;
|
|
20125
|
+
oldestIdx = i;
|
|
20126
|
+
oldestAt = at;
|
|
20127
|
+
}
|
|
20128
|
+
}
|
|
20129
|
+
}
|
|
20130
|
+
if (oldestKey == null) return;
|
|
20131
|
+
const entries = this.cache.get(oldestKey);
|
|
20132
|
+
entries.splice(oldestIdx, 1);
|
|
20133
|
+
if (entries.length === 0) this.cache.delete(oldestKey);
|
|
20134
|
+
total -= 1;
|
|
20135
|
+
}
|
|
20136
|
+
}
|
|
20137
|
+
/** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
|
|
20138
|
+
slice(bars, lo, hi) {
|
|
20139
|
+
const out = [];
|
|
20140
|
+
for (const b of bars) {
|
|
20141
|
+
if (b.time < lo) continue;
|
|
20142
|
+
if (b.time > hi) break;
|
|
20143
|
+
out.push(b);
|
|
20144
|
+
}
|
|
20145
|
+
return out;
|
|
20146
|
+
}
|
|
20147
|
+
fire() {
|
|
20148
|
+
for (const l of [...this.listeners]) l();
|
|
20149
|
+
}
|
|
20150
|
+
};
|
|
19502
20151
|
|
|
19503
20152
|
// src/core/engine/EngineOrchestrator.ts
|
|
19504
20153
|
var VIEWPORT_DEBOUNCE_MS = 150;
|
|
@@ -19506,6 +20155,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
19506
20155
|
var PREVIEW_BARS = 300;
|
|
19507
20156
|
var SINGLE_LOAD_BARS = 5e3;
|
|
19508
20157
|
var CHUNK_BARS = 1e4;
|
|
20158
|
+
var FIRST_PAINT_BARS = 100;
|
|
19509
20159
|
var GAP_FACTOR = 1.5;
|
|
19510
20160
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
19511
20161
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -19623,7 +20273,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19623
20273
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
19624
20274
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
19625
20275
|
this.barTransform = barTransformFor(initialStyle);
|
|
19626
|
-
|
|
20276
|
+
const drawingSeries = new DrawingSeriesService({
|
|
20277
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
20278
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
20279
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
20280
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
20281
|
+
});
|
|
20282
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
19627
20283
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
19628
20284
|
this.endLoad();
|
|
19629
20285
|
this.events.emit("data:unresolved", info);
|
|
@@ -19775,6 +20431,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19775
20431
|
let painted = false;
|
|
19776
20432
|
const paint = (bars, final) => {
|
|
19777
20433
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
20434
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
19778
20435
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
19779
20436
|
if (!painted && bars.length > 0) {
|
|
19780
20437
|
painted = true;
|
|
@@ -20405,6 +21062,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20405
21062
|
record.pendingCause = "inputs";
|
|
20406
21063
|
if (record.session) record.session.update(record.inputValues);
|
|
20407
21064
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
21065
|
+
this.events.emit("indicator:inputs", { id });
|
|
21066
|
+
}
|
|
21067
|
+
/** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
|
|
21068
|
+
inputValuesOf(id) {
|
|
21069
|
+
return { ...this.registry.get(id)?.inputValues };
|
|
21070
|
+
}
|
|
21071
|
+
/** IndicatorController: the CURRENT declaration-prop overrides. */
|
|
21072
|
+
propValuesOf(id) {
|
|
21073
|
+
return { ...this.registry.get(id)?.propValues };
|
|
20408
21074
|
}
|
|
20409
21075
|
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
20410
21076
|
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
@@ -20418,6 +21084,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20418
21084
|
if (!record.hidden) this.setLoading(record, true);
|
|
20419
21085
|
record.pendingCause = "inputs";
|
|
20420
21086
|
record.session.update(record.inputValues, record.propValues);
|
|
21087
|
+
this.events.emit("indicator:inputs", { id });
|
|
20421
21088
|
}
|
|
20422
21089
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
20423
21090
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
@@ -20605,8 +21272,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20605
21272
|
onModel: (model) => {
|
|
20606
21273
|
const first = !record.announced;
|
|
20607
21274
|
const cause = record.pendingCause ?? "history";
|
|
21275
|
+
if (!this.applyModel(id, model)) return;
|
|
20608
21276
|
record.pendingCause = void 0;
|
|
20609
|
-
this.applyModel(id, model);
|
|
20610
21277
|
this.emitContextChanged(id);
|
|
20611
21278
|
this.emitScriptRun(id, cause, first);
|
|
20612
21279
|
},
|
|
@@ -20899,10 +21566,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20899
21566
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
20900
21567
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
20901
21568
|
* (live tick / viewport re-run) value-patches.
|
|
21569
|
+
*
|
|
21570
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
21571
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
21572
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
21573
|
+
* stay unchanged.
|
|
20902
21574
|
*/
|
|
20903
21575
|
applyModel(id, model) {
|
|
20904
21576
|
const record = this.registry.get(id);
|
|
20905
|
-
if (!record || record.hidden) return;
|
|
21577
|
+
if (!record || record.hidden) return true;
|
|
21578
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
20906
21579
|
const handle = this.handles.get(id);
|
|
20907
21580
|
if (!record.renderHandle) {
|
|
20908
21581
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -20912,7 +21585,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20912
21585
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
20913
21586
|
record.pendingStructural = false;
|
|
20914
21587
|
this.announce(record, handle);
|
|
20915
|
-
return;
|
|
21588
|
+
return true;
|
|
20916
21589
|
}
|
|
20917
21590
|
let paneId = record.model?.paneId ?? "price";
|
|
20918
21591
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -20940,6 +21613,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20940
21613
|
}
|
|
20941
21614
|
if (record.loading) this.setLoading(record, false);
|
|
20942
21615
|
this.announce(record, handle);
|
|
21616
|
+
return true;
|
|
21617
|
+
}
|
|
21618
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
21619
|
+
static modelHasOutput(model) {
|
|
21620
|
+
return model.series.length > 0 || model.fills.length > 0 || model.backgrounds.length > 0 || model.priceLines.length > 0 || (model.lines?.length ?? 0) > 0 || (model.boxes?.length ?? 0) > 0 || (model.labels?.length ?? 0) > 0 || (model.polylines?.length ?? 0) > 0 || (model.linefills?.length ?? 0) > 0 || (model.tables?.length ?? 0) > 0 || (model.barColors?.length ?? 0) > 0 || (model.trades?.length ?? 0) > 0;
|
|
20943
21621
|
}
|
|
20944
21622
|
routePane(id, model, options) {
|
|
20945
21623
|
if (options.pane === "new") return `pane-${id}`;
|