@luxalgo/vela 0.6.11 → 0.6.13
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-BBf-jc6W.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
- package/dist/{DataProvider-p0TEyhlX.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
- package/dist/{chunk-MTLJKZDZ.js → chunk-F4M24ANM.js} +122 -1
- package/dist/{chunk-IO3NYSQV.js → chunk-G52XAKZY.js} +184 -25
- package/dist/{chunk-73PEA4MU.js → chunk-JKGA36ZM.js} +635 -49
- package/dist/{contributions-Bbe2R-mQ.d.ts → contributions-37nni40G.d.ts} +6 -4
- package/dist/{contributions-CO01zWve.d.cts → contributions-lPojhTxI.d.cts} +6 -4
- package/dist/index.cjs +744 -37
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/{options-yp7sA96q.d.cts → options-CYS5Wmlx.d.cts} +71 -2
- package/dist/{options-yp7sA96q.d.ts → options-CYS5Wmlx.d.ts} +71 -2
- package/dist/{plugin-CkkH8QnX.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
- package/dist/{plugin-DwxjM3Ni.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
- package/dist/plugin.cjs +110 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +1 -1
- package/dist/providers/binance.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-CHPDuKNp.d.ts → statusline-model-CiJm-riV.d.cts} +7 -85
- package/dist/{statusline-DTHZUFqK.d.cts → statusline-model-D-q_OOx9.d.ts} +7 -85
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +744 -37
- package/dist/vela.global.min.js +52 -52
- package/dist/widget.cjs +927 -61
- package/dist/widget.d.cts +118 -7
- package/dist/widget.d.ts +118 -7
- package/dist/widget.js +4 -4
- package/dist/workspace.cjs +927 -61
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -5
- package/dist/workspace.js +3 -3
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -8699,7 +8706,9 @@ var DrawingSceneRenderer = class {
|
|
|
8699
8706
|
const lo = Math.min(from, to);
|
|
8700
8707
|
const hi = Math.max(from, to);
|
|
8701
8708
|
const visible = (a, b, extend) => {
|
|
8702
|
-
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;
|
|
8703
8712
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
8704
8713
|
};
|
|
8705
8714
|
let min = Infinity;
|
|
@@ -11154,6 +11163,18 @@ function timezoneOptions(current) {
|
|
|
11154
11163
|
return [[normalized, tzMenuLabel(normalized, normalized)], ...options];
|
|
11155
11164
|
}
|
|
11156
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
|
+
|
|
11157
11178
|
// src/renderers/native/drawings/DrawingPainter.ts
|
|
11158
11179
|
var HANDLE_RADIUS = 4.5;
|
|
11159
11180
|
var HANDLE_BORDER = DEFAULT_DRAWING_COLOR;
|
|
@@ -11173,6 +11194,17 @@ var DrawingPainter = class {
|
|
|
11173
11194
|
constructor() {
|
|
11174
11195
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
11175
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
|
+
};
|
|
11176
11208
|
}
|
|
11177
11209
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
11178
11210
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -11217,6 +11249,8 @@ var DrawingPainter = class {
|
|
|
11217
11249
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
11218
11250
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
11219
11251
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
11252
|
+
} else if (ghost instanceof Magnifier) {
|
|
11253
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
11220
11254
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
11221
11255
|
ctx.globalAlpha = 1;
|
|
11222
11256
|
}
|
|
@@ -11350,6 +11384,10 @@ var DrawingPainter = class {
|
|
|
11350
11384
|
this.paintLabel(ctx, d, proj, theme);
|
|
11351
11385
|
return;
|
|
11352
11386
|
}
|
|
11387
|
+
if (d instanceof Magnifier) {
|
|
11388
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
11389
|
+
return;
|
|
11390
|
+
}
|
|
11353
11391
|
if (d instanceof PatternDrawing) {
|
|
11354
11392
|
this.paintPattern(ctx, d, proj, theme);
|
|
11355
11393
|
return;
|
|
@@ -11842,6 +11880,216 @@ var DrawingPainter = class {
|
|
|
11842
11880
|
ctx.textBaseline = "alphabetic";
|
|
11843
11881
|
}
|
|
11844
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
|
+
}
|
|
11845
12093
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
11846
12094
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
11847
12095
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -13257,8 +13505,9 @@ function ensureStyles4() {
|
|
|
13257
13505
|
if (!existing) document.head.appendChild(s);
|
|
13258
13506
|
}
|
|
13259
13507
|
var DrawingSettingsPopup = class {
|
|
13260
|
-
constructor(host, theme) {
|
|
13508
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
13261
13509
|
this.host = host;
|
|
13510
|
+
this.chartBarMs = chartBarMs;
|
|
13262
13511
|
this.el = null;
|
|
13263
13512
|
this.tipEl = null;
|
|
13264
13513
|
// floating hover-label (above/below the toolbar)
|
|
@@ -13283,6 +13532,14 @@ var DrawingSettingsPopup = class {
|
|
|
13283
13532
|
this.theme = theme;
|
|
13284
13533
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
13285
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
|
+
}
|
|
13286
13543
|
setTheme(theme) {
|
|
13287
13544
|
this.theme = theme;
|
|
13288
13545
|
this.settingsDialog.setTheme(theme);
|
|
@@ -13325,7 +13582,20 @@ var DrawingSettingsPopup = class {
|
|
|
13325
13582
|
const sz = drawing.size ?? "normal";
|
|
13326
13583
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
13327
13584
|
}
|
|
13328
|
-
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 })));
|
|
13329
13599
|
if (paths.has("style.lineWidth")) {
|
|
13330
13600
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
13331
13601
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -13647,6 +13917,7 @@ var DrawingSettingsPopup = class {
|
|
|
13647
13917
|
this.colorPop = null;
|
|
13648
13918
|
this.colorOwner = null;
|
|
13649
13919
|
}
|
|
13920
|
+
opts.onClose?.();
|
|
13650
13921
|
}
|
|
13651
13922
|
});
|
|
13652
13923
|
const el = pop.el;
|
|
@@ -13665,6 +13936,49 @@ var DrawingSettingsPopup = class {
|
|
|
13665
13936
|
pop.show();
|
|
13666
13937
|
return pop;
|
|
13667
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
|
+
}
|
|
13668
13982
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
13669
13983
|
openActionMenu(anchor, rows) {
|
|
13670
13984
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -13775,10 +14089,13 @@ var DrawingSettingsPopup = class {
|
|
|
13775
14089
|
let cur = current;
|
|
13776
14090
|
const paint = (v) => {
|
|
13777
14091
|
b.replaceChildren();
|
|
13778
|
-
const
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
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
|
+
}
|
|
13782
14099
|
if (opts.label && opts.labelInTrigger) {
|
|
13783
14100
|
const tx = document.createElement("span");
|
|
13784
14101
|
tx.textContent = opts.label(v);
|
|
@@ -13820,10 +14137,13 @@ var DrawingSettingsPopup = class {
|
|
|
13820
14137
|
item.className = "vela-dpop-item";
|
|
13821
14138
|
item.dataset.active = active ? "1" : "0";
|
|
13822
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;`;
|
|
13823
|
-
const
|
|
13824
|
-
|
|
13825
|
-
|
|
13826
|
-
|
|
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
|
+
}
|
|
13827
14147
|
if (label) {
|
|
13828
14148
|
const tx = document.createElement("span");
|
|
13829
14149
|
tx.textContent = label(v);
|
|
@@ -14175,6 +14495,9 @@ var UserDrawingController = class {
|
|
|
14175
14495
|
this.intentCb = null;
|
|
14176
14496
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
14177
14497
|
this.externalGhost = null;
|
|
14498
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
14499
|
+
this.seriesGw = null;
|
|
14500
|
+
this.seriesGwUnsub = null;
|
|
14178
14501
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
14179
14502
|
this.lastDraftKey = null;
|
|
14180
14503
|
this.measure = new MeasureOverlay();
|
|
@@ -14202,7 +14525,7 @@ var UserDrawingController = class {
|
|
|
14202
14525
|
this.textEditor = null;
|
|
14203
14526
|
this.painter = new DrawingPainter();
|
|
14204
14527
|
this.ctx = canvas.getContext("2d");
|
|
14205
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
14528
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
14206
14529
|
this.toolbar = new DrawingToolbar(
|
|
14207
14530
|
toolbarHost,
|
|
14208
14531
|
deps.theme(),
|
|
@@ -14260,6 +14583,22 @@ var UserDrawingController = class {
|
|
|
14260
14583
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
14261
14584
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
14262
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
|
+
}
|
|
14263
14602
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
14264
14603
|
setExternalGhost(doc) {
|
|
14265
14604
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -14377,8 +14716,34 @@ var UserDrawingController = class {
|
|
|
14377
14716
|
/** Should the drawing layer win this press (vs pan)? */
|
|
14378
14717
|
claim(x, y) {
|
|
14379
14718
|
if (this.measureMode || this.eraserMode) return true;
|
|
14719
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
14380
14720
|
return this.interaction.claim(x, y);
|
|
14381
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
|
+
}
|
|
14382
14747
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
14383
14748
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
14384
14749
|
deleteAt(x, y) {
|
|
@@ -14424,6 +14789,13 @@ var UserDrawingController = class {
|
|
|
14424
14789
|
this.render();
|
|
14425
14790
|
return;
|
|
14426
14791
|
}
|
|
14792
|
+
if (this.activeTool == null) {
|
|
14793
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
14794
|
+
if (chipOwner) {
|
|
14795
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
14796
|
+
return;
|
|
14797
|
+
}
|
|
14798
|
+
}
|
|
14427
14799
|
this.interaction.down(x, y, snap, shift);
|
|
14428
14800
|
}
|
|
14429
14801
|
pointerMove(x, y, snap = "off", shift = false) {
|
|
@@ -14642,6 +15014,7 @@ var UserDrawingController = class {
|
|
|
14642
15014
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
14643
15015
|
cursorAt(x, y) {
|
|
14644
15016
|
if (this.eraserMode) return "pointer";
|
|
15017
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
14645
15018
|
return this.interaction.cursorAt(x, y);
|
|
14646
15019
|
}
|
|
14647
15020
|
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
@@ -14820,6 +15193,7 @@ var UserDrawingController = class {
|
|
|
14820
15193
|
if (!sctx) continue;
|
|
14821
15194
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
14822
15195
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
15196
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
14823
15197
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
14824
15198
|
const slices = out.get(paneId) ?? [];
|
|
14825
15199
|
slices.push({ beforeZ, canvas });
|
|
@@ -14847,6 +15221,7 @@ var UserDrawingController = class {
|
|
|
14847
15221
|
dragged: this.interaction.activeDragId(),
|
|
14848
15222
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
14849
15223
|
};
|
|
15224
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
14850
15225
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
14851
15226
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
14852
15227
|
this.layoutTextEditor();
|
|
@@ -14854,6 +15229,10 @@ var UserDrawingController = class {
|
|
|
14854
15229
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
14855
15230
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
14856
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
|
+
}
|
|
14857
15236
|
const markers = this.interaction.placingMarkers(proj);
|
|
14858
15237
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
14859
15238
|
const m = this.interaction.snapMarker();
|
|
@@ -14865,6 +15244,9 @@ var UserDrawingController = class {
|
|
|
14865
15244
|
this.closeTextEditor();
|
|
14866
15245
|
this.popup.destroy();
|
|
14867
15246
|
this.toolbar.destroy();
|
|
15247
|
+
this.seriesGwUnsub?.();
|
|
15248
|
+
this.seriesGwUnsub = null;
|
|
15249
|
+
this.seriesGw = null;
|
|
14868
15250
|
this.intentCb = null;
|
|
14869
15251
|
this.drawings = [];
|
|
14870
15252
|
}
|
|
@@ -15262,7 +15644,7 @@ function mergeSlices(indicator, user) {
|
|
|
15262
15644
|
}
|
|
15263
15645
|
|
|
15264
15646
|
// src/renderers/native/drawings/Projector.ts
|
|
15265
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
15647
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
15266
15648
|
return {
|
|
15267
15649
|
xOf: (time) => coords.timeToX(time),
|
|
15268
15650
|
yOf: (price, paneId) => {
|
|
@@ -15283,6 +15665,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
|
15283
15665
|
},
|
|
15284
15666
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
15285
15667
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
15668
|
+
seriesInRange,
|
|
15286
15669
|
width: coords.width,
|
|
15287
15670
|
height: coords.height
|
|
15288
15671
|
};
|
|
@@ -17260,6 +17643,23 @@ var NativeRenderer = class {
|
|
|
17260
17643
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
17261
17644
|
priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
|
|
17262
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,
|
|
17263
17663
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
17264
17664
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
17265
17665
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -18653,7 +19053,8 @@ var NativeRenderer = class {
|
|
|
18653
19053
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
18654
19054
|
},
|
|
18655
19055
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
18656
|
-
(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
|
|
18657
19058
|
);
|
|
18658
19059
|
}
|
|
18659
19060
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|
|
@@ -19574,17 +19975,179 @@ function presetToRange(preset, bars) {
|
|
|
19574
19975
|
return { from: Math.max(first, from), to: last };
|
|
19575
19976
|
}
|
|
19576
19977
|
|
|
19577
|
-
// src/core/
|
|
19578
|
-
|
|
19579
|
-
|
|
19580
|
-
|
|
19581
|
-
|
|
19582
|
-
|
|
19583
|
-
|
|
19584
|
-
|
|
19585
|
-
|
|
19586
|
-
|
|
19587
|
-
|
|
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
|
+
};
|
|
19588
20151
|
|
|
19589
20152
|
// src/core/engine/EngineOrchestrator.ts
|
|
19590
20153
|
var VIEWPORT_DEBOUNCE_MS = 150;
|
|
@@ -19592,7 +20155,6 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
19592
20155
|
var PREVIEW_BARS = 300;
|
|
19593
20156
|
var SINGLE_LOAD_BARS = 5e3;
|
|
19594
20157
|
var CHUNK_BARS = 1e4;
|
|
19595
|
-
var FIRST_PAINT_BARS = 100;
|
|
19596
20158
|
var GAP_FACTOR = 1.5;
|
|
19597
20159
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
19598
20160
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -19710,7 +20272,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19710
20272
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
19711
20273
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
19712
20274
|
this.barTransform = barTransformFor(initialStyle);
|
|
19713
|
-
|
|
20275
|
+
const drawingSeries = new DrawingSeriesService({
|
|
20276
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
20277
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
20278
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
20279
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
20280
|
+
});
|
|
20281
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
19714
20282
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
19715
20283
|
this.endLoad();
|
|
19716
20284
|
this.events.emit("data:unresolved", info);
|
|
@@ -19862,7 +20430,6 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19862
20430
|
let painted = false;
|
|
19863
20431
|
const paint = (bars, final) => {
|
|
19864
20432
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
19865
|
-
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
19866
20433
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
19867
20434
|
if (!painted && bars.length > 0) {
|
|
19868
20435
|
painted = true;
|
|
@@ -19881,10 +20448,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19881
20448
|
}
|
|
19882
20449
|
};
|
|
19883
20450
|
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
19884
|
-
this.feed.loadProgressive(
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
20451
|
+
this.feed.loadProgressive(
|
|
20452
|
+
market,
|
|
20453
|
+
(bars) => {
|
|
20454
|
+
paint(bars, false);
|
|
20455
|
+
if (painted) signal(true);
|
|
20456
|
+
},
|
|
20457
|
+
{ signal: abort.signal }
|
|
20458
|
+
).then((full) => {
|
|
19888
20459
|
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
19889
20460
|
if (full == null) return signal(false);
|
|
19890
20461
|
if (this.generation !== gen) return signal(true);
|
|
@@ -19971,7 +20542,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19971
20542
|
* an in-flight `setMarket` immediately (the config mutates before the load). */
|
|
19972
20543
|
marketSnapshot() {
|
|
19973
20544
|
const m = this.config.market;
|
|
19974
|
-
return {
|
|
20545
|
+
return {
|
|
20546
|
+
symbol: m.symbol,
|
|
20547
|
+
provider: parseSymbol(m.symbol ?? "").provider ?? void 0,
|
|
20548
|
+
timeframe: m.timeframe,
|
|
20549
|
+
bars: m.bars,
|
|
20550
|
+
session: m.session,
|
|
20551
|
+
offline: m.data !== void 0
|
|
20552
|
+
};
|
|
19975
20553
|
}
|
|
19976
20554
|
/**
|
|
19977
20555
|
* Switch the chart's market IN PLACE — no destroy/recreate. The renderer stays
|
|
@@ -20037,10 +20615,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20037
20615
|
if (depthOnly) {
|
|
20038
20616
|
this.extendDepth(gen, m.bars ?? 500);
|
|
20039
20617
|
} else {
|
|
20040
|
-
await Promise.race([
|
|
20041
|
-
this.loadMarket(gen, { firstLoad: false }),
|
|
20042
|
-
new Promise((resolve) => this.supersedeWaiters.push(resolve))
|
|
20043
|
-
]);
|
|
20618
|
+
await Promise.race([this.loadMarket(gen, { firstLoad: false }), new Promise((resolve) => this.supersedeWaiters.push(resolve))]);
|
|
20044
20619
|
}
|
|
20045
20620
|
if (this.generation !== gen) return;
|
|
20046
20621
|
} finally {
|
|
@@ -20703,8 +21278,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20703
21278
|
onModel: (model) => {
|
|
20704
21279
|
const first = !record.announced;
|
|
20705
21280
|
const cause = record.pendingCause ?? "history";
|
|
21281
|
+
if (!this.applyModel(id, model)) return;
|
|
20706
21282
|
record.pendingCause = void 0;
|
|
20707
|
-
this.applyModel(id, model);
|
|
20708
21283
|
this.emitContextChanged(id);
|
|
20709
21284
|
this.emitScriptRun(id, cause, first);
|
|
20710
21285
|
},
|
|
@@ -20997,10 +21572,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20997
21572
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
20998
21573
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
20999
21574
|
* (live tick / viewport re-run) value-patches.
|
|
21575
|
+
*
|
|
21576
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
21577
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
21578
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
21579
|
+
* stay unchanged.
|
|
21000
21580
|
*/
|
|
21001
21581
|
applyModel(id, model) {
|
|
21002
21582
|
const record = this.registry.get(id);
|
|
21003
|
-
if (!record || record.hidden) return;
|
|
21583
|
+
if (!record || record.hidden) return true;
|
|
21584
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
21004
21585
|
const handle = this.handles.get(id);
|
|
21005
21586
|
if (!record.renderHandle) {
|
|
21006
21587
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -21010,7 +21591,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21010
21591
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
21011
21592
|
record.pendingStructural = false;
|
|
21012
21593
|
this.announce(record, handle);
|
|
21013
|
-
return;
|
|
21594
|
+
return true;
|
|
21014
21595
|
}
|
|
21015
21596
|
let paneId = record.model?.paneId ?? "price";
|
|
21016
21597
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -21038,6 +21619,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21038
21619
|
}
|
|
21039
21620
|
if (record.loading) this.setLoading(record, false);
|
|
21040
21621
|
this.announce(record, handle);
|
|
21622
|
+
return true;
|
|
21623
|
+
}
|
|
21624
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
21625
|
+
static modelHasOutput(model) {
|
|
21626
|
+
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;
|
|
21041
21627
|
}
|
|
21042
21628
|
routePane(id, model, options) {
|
|
21043
21629
|
if (options.pane === "new") return `pane-${id}`;
|