@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
package/dist/workspace.cjs
CHANGED
|
@@ -677,6 +677,7 @@ function intervalMs(timeframe) {
|
|
|
677
677
|
"4h": 144e5,
|
|
678
678
|
"1d": 864e5,
|
|
679
679
|
"1w": 6048e5,
|
|
680
|
+
"1M": 2592e6,
|
|
680
681
|
"1": 6e4,
|
|
681
682
|
"5": 3e5,
|
|
682
683
|
"15": 9e5,
|
|
@@ -684,7 +685,8 @@ function intervalMs(timeframe) {
|
|
|
684
685
|
"60": 36e5,
|
|
685
686
|
"240": 144e5,
|
|
686
687
|
D: 864e5,
|
|
687
|
-
W: 6048e5
|
|
688
|
+
W: 6048e5,
|
|
689
|
+
M: 2592e6
|
|
688
690
|
};
|
|
689
691
|
return map[timeframe] ?? 36e5;
|
|
690
692
|
}
|
|
@@ -11187,6 +11189,111 @@ function roundFloat(n) {
|
|
|
11187
11189
|
return Math.round(n * 1e8) / 1e8;
|
|
11188
11190
|
}
|
|
11189
11191
|
|
|
11192
|
+
// src/core/drawings/types/Magnifier.ts
|
|
11193
|
+
var MAGNIFIER_TIMEFRAME_OPTIONS = [
|
|
11194
|
+
{ value: "auto", label: "Auto", ms: 0 },
|
|
11195
|
+
{ value: "1", label: "1m", ms: 6e4 },
|
|
11196
|
+
{ value: "5", label: "5m", ms: 3e5 },
|
|
11197
|
+
{ value: "15", label: "15m", ms: 9e5 },
|
|
11198
|
+
{ value: "30", label: "30m", ms: 18e5 },
|
|
11199
|
+
{ value: "60", label: "1h", ms: 36e5 },
|
|
11200
|
+
{ value: "240", label: "4h", ms: 144e5 },
|
|
11201
|
+
{ value: "D", label: "1D", ms: 864e5 }
|
|
11202
|
+
];
|
|
11203
|
+
function magnifierTimeframeLabel(value) {
|
|
11204
|
+
const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
|
|
11205
|
+
if (opt) return opt.label;
|
|
11206
|
+
const n = Number(value);
|
|
11207
|
+
if (Number.isFinite(n) && n > 0) {
|
|
11208
|
+
if (n % 1440 === 0) return `${n / 1440}D`;
|
|
11209
|
+
if (n % 60 === 0) return `${n / 60}h`;
|
|
11210
|
+
return `${n}m`;
|
|
11211
|
+
}
|
|
11212
|
+
return value;
|
|
11213
|
+
}
|
|
11214
|
+
function defaultMagnifierStyle() {
|
|
11215
|
+
return { timeframe: "auto", upColor: "", downColor: "" };
|
|
11216
|
+
}
|
|
11217
|
+
var Magnifier = class extends Drawing {
|
|
11218
|
+
constructor(init) {
|
|
11219
|
+
super(init);
|
|
11220
|
+
this.type = "magnifier";
|
|
11221
|
+
/** Pixel rect of the timeframe chip as painted last frame, caret included — the chip is
|
|
11222
|
+
* an interactive dropdown trigger, so the interaction layer needs the exact rect the
|
|
11223
|
+
* painter measured. Renderer-transient: never serialized, null while unpainted. */
|
|
11224
|
+
this.chipRect = null;
|
|
11225
|
+
if (!this.magnifier) this.magnifier = defaultMagnifierStyle();
|
|
11226
|
+
}
|
|
11227
|
+
anchorSchema() {
|
|
11228
|
+
return { min: 2, max: 2, slots: [{ role: "c1", free: "both" }, { role: "c2", free: "both" }] };
|
|
11229
|
+
}
|
|
11230
|
+
placementMode() {
|
|
11231
|
+
return "drag";
|
|
11232
|
+
}
|
|
11233
|
+
/** The pixel rectangle between the two corner anchors (painter + hit-test share it). */
|
|
11234
|
+
rect(proj) {
|
|
11235
|
+
const a = this.anchors[0];
|
|
11236
|
+
const b = this.anchors[1];
|
|
11237
|
+
if (!a || !b) return null;
|
|
11238
|
+
const ya = proj.yOf(a.price, this.paneId);
|
|
11239
|
+
const yb = proj.yOf(b.price, this.paneId);
|
|
11240
|
+
if (ya == null || yb == null) return null;
|
|
11241
|
+
return { x1: proj.xOf(a.time), y1: ya, x2: proj.xOf(b.time), y2: yb };
|
|
11242
|
+
}
|
|
11243
|
+
hitTest(px, py, proj, tol) {
|
|
11244
|
+
const r = this.rect(proj);
|
|
11245
|
+
if (!r) return false;
|
|
11246
|
+
if (pointInBox(px, py, r.x1, r.y1, r.x2, r.y2)) return true;
|
|
11247
|
+
const edges = [
|
|
11248
|
+
[r.x1, r.y1, r.x2, r.y1],
|
|
11249
|
+
[r.x2, r.y1, r.x2, r.y2],
|
|
11250
|
+
[r.x2, r.y2, r.x1, r.y2],
|
|
11251
|
+
[r.x1, r.y2, r.x1, r.y1]
|
|
11252
|
+
];
|
|
11253
|
+
return edges.some((e) => distToSegment(px, py, e[0], e[1], e[2], e[3]) <= tol);
|
|
11254
|
+
}
|
|
11255
|
+
handlePoints(proj) {
|
|
11256
|
+
const r = this.rect(proj);
|
|
11257
|
+
return r ? [[r.x1, r.y1], [r.x2, r.y2]] : [];
|
|
11258
|
+
}
|
|
11259
|
+
hitHandle(px, py, proj, tol) {
|
|
11260
|
+
return handleAt(px, py, this.handlePoints(proj), tol + 3);
|
|
11261
|
+
}
|
|
11262
|
+
bounds(proj) {
|
|
11263
|
+
const r = this.rect(proj);
|
|
11264
|
+
if (!r) return null;
|
|
11265
|
+
return { x: Math.min(r.x1, r.x2), y: Math.min(r.y1, r.y2), w: Math.abs(r.x2 - r.x1), h: Math.abs(r.y2 - r.y1) };
|
|
11266
|
+
}
|
|
11267
|
+
priceRange() {
|
|
11268
|
+
const a = this.anchors[0];
|
|
11269
|
+
const b = this.anchors[1];
|
|
11270
|
+
if (!a || !b) return null;
|
|
11271
|
+
return { min: Math.min(a.price, b.price), max: Math.max(a.price, b.price) };
|
|
11272
|
+
}
|
|
11273
|
+
schema() {
|
|
11274
|
+
return {
|
|
11275
|
+
fields: [
|
|
11276
|
+
{
|
|
11277
|
+
path: "magnifier.timeframe",
|
|
11278
|
+
label: "Timeframe",
|
|
11279
|
+
kind: "select",
|
|
11280
|
+
options: MAGNIFIER_TIMEFRAME_OPTIONS,
|
|
11281
|
+
group: "behavior"
|
|
11282
|
+
},
|
|
11283
|
+
...LINE_FIELDS.map((f) => ({ ...f, label: f.label.replace("Line", "Border") })),
|
|
11284
|
+
{ path: "magnifier.upColor", label: "Up candles", kind: "color", group: "fill" },
|
|
11285
|
+
{ path: "magnifier.downColor", label: "Down candles", kind: "color", group: "fill" }
|
|
11286
|
+
]
|
|
11287
|
+
};
|
|
11288
|
+
}
|
|
11289
|
+
writeProps() {
|
|
11290
|
+
return { ...this.magnifier };
|
|
11291
|
+
}
|
|
11292
|
+
readProps(props) {
|
|
11293
|
+
this.magnifier = { ...defaultMagnifierStyle(), ...props };
|
|
11294
|
+
}
|
|
11295
|
+
};
|
|
11296
|
+
|
|
11190
11297
|
// src/core/drawings/registry.ts
|
|
11191
11298
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
11192
11299
|
function registerDrawingType(meta) {
|
|
@@ -11846,6 +11953,22 @@ registerDrawingType({
|
|
|
11846
11953
|
defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
|
|
11847
11954
|
create: (init) => new PositionTool(init)
|
|
11848
11955
|
});
|
|
11956
|
+
var MAGNIFIER_ICON = svg24(
|
|
11957
|
+
'<circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.3 15.3 5.2 5.2"/><path d="M8 12.5v-3M10.5 13.5v-5.5M13 12v-2"/>'
|
|
11958
|
+
);
|
|
11959
|
+
registerDrawingType({
|
|
11960
|
+
type: "magnifier",
|
|
11961
|
+
group: "measure",
|
|
11962
|
+
label: "Magnifier",
|
|
11963
|
+
icon: MAGNIFIER_ICON,
|
|
11964
|
+
// An empty border color means the THEME's contrast ink (white on dark, black on
|
|
11965
|
+
// light), resolved at paint time so it follows theme switches; a user pick wins.
|
|
11966
|
+
defaultStyle: { lineColor: "", lineWidth: 1, lineStyle: "solid" },
|
|
11967
|
+
coversSeries: true,
|
|
11968
|
+
// the inset's backdrop must sit over the base candles it replaces
|
|
11969
|
+
placementHint: "Drag an area on the chart to view it at a lower timeframe",
|
|
11970
|
+
create: (init) => new Magnifier(init)
|
|
11971
|
+
});
|
|
11849
11972
|
var VWAP_ICON = svg24('<line x1="5" y1="3" x2="5" y2="21"/><path d="M5 16c4 0 5-9 8-9s3 5 8 3"/>');
|
|
11850
11973
|
registerDrawingType({
|
|
11851
11974
|
type: "anchoredvwap",
|
|
@@ -16051,7 +16174,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
|
|
|
16051
16174
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
16052
16175
|
var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
|
|
16053
16176
|
var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
|
|
16054
|
-
var MEASUREMENT_TYPES = ["position", "datepricerange"];
|
|
16177
|
+
var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
|
|
16055
16178
|
var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
|
|
16056
16179
|
var BRUSH_TYPES = ["freehand", "highlighter"];
|
|
16057
16180
|
var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
|
|
@@ -16187,7 +16310,7 @@ function buildToolbar(option) {
|
|
|
16187
16310
|
|
|
16188
16311
|
// src/core/drawings/DrawingController.ts
|
|
16189
16312
|
var DrawingController = class {
|
|
16190
|
-
constructor(renderer, events, option) {
|
|
16313
|
+
constructor(renderer, events, option, seriesGateway) {
|
|
16191
16314
|
this.events = events;
|
|
16192
16315
|
this.store = new DrawingStore();
|
|
16193
16316
|
this.history = new DrawingHistory();
|
|
@@ -16213,6 +16336,7 @@ var DrawingController = class {
|
|
|
16213
16336
|
const { definition, visible } = buildToolbar(option);
|
|
16214
16337
|
this.port.setToolbar(definition);
|
|
16215
16338
|
this.port.showToolbar(visible);
|
|
16339
|
+
if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
|
|
16216
16340
|
this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
|
|
16217
16341
|
this.subs.push(this.store.onChange(() => this.sync()));
|
|
16218
16342
|
}
|
|
@@ -16318,7 +16442,7 @@ var DrawingController = class {
|
|
|
16318
16442
|
style,
|
|
16319
16443
|
text: init.text,
|
|
16320
16444
|
props: init.props,
|
|
16321
|
-
zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
|
|
16445
|
+
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
16322
16446
|
});
|
|
16323
16447
|
if (!d) return null;
|
|
16324
16448
|
this.history.record(this.store.serialize());
|
|
@@ -16393,10 +16517,14 @@ var DrawingController = class {
|
|
|
16393
16517
|
* (falling back to just under the pane's top series where there is no price — a study
|
|
16394
16518
|
* pane). Half a key down never ties a series; drawings tying each other paint in insertion
|
|
16395
16519
|
* order, so consecutive new drawings still stack newest-in-front. Undefined without a
|
|
16396
|
-
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
16397
|
-
|
|
16520
|
+
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
16521
|
+
* A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
|
|
16522
|
+
* above the whole stack — under the candles its content would be buried. */
|
|
16523
|
+
startZ(type, paneId) {
|
|
16398
16524
|
const range = this.port?.stackRange?.(paneId);
|
|
16399
|
-
|
|
16525
|
+
if (!range) return void 0;
|
|
16526
|
+
if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
|
|
16527
|
+
return (range.price ?? range.front) - 0.5;
|
|
16400
16528
|
}
|
|
16401
16529
|
/** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
|
|
16402
16530
|
* `additive` toggles membership (matching shift-click) instead of replacing. */
|
|
@@ -16541,7 +16669,7 @@ var DrawingController = class {
|
|
|
16541
16669
|
const style = last ? { ...i.doc.style, ...last } : i.doc.style;
|
|
16542
16670
|
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
16543
16671
|
if (!d) return;
|
|
16544
|
-
if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
|
|
16672
|
+
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
16545
16673
|
this.history.record(before);
|
|
16546
16674
|
this.store.add(d);
|
|
16547
16675
|
this.captureStyle(d.id);
|
|
@@ -18073,6 +18201,180 @@ function presetToRange(preset, bars) {
|
|
|
18073
18201
|
return { from: Math.max(first, from), to: last };
|
|
18074
18202
|
}
|
|
18075
18203
|
|
|
18204
|
+
// src/core/engine/DrawingSeriesService.ts
|
|
18205
|
+
var MAX_BARS = 5e3;
|
|
18206
|
+
var PAD_FRAC = 0.25;
|
|
18207
|
+
var MAX_ENTRIES = 16;
|
|
18208
|
+
var RETRY_MS = 15e3;
|
|
18209
|
+
var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
|
|
18210
|
+
var DrawingSeriesService = class {
|
|
18211
|
+
constructor(deps) {
|
|
18212
|
+
this.deps = deps;
|
|
18213
|
+
/** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
|
|
18214
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
18215
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
18216
|
+
}
|
|
18217
|
+
seriesInRange(timeframe, from, to) {
|
|
18218
|
+
if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
|
|
18219
|
+
const resolved = this.resolveTimeframe(timeframe);
|
|
18220
|
+
if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
|
|
18221
|
+
const barMs = timeframeToMs(resolved);
|
|
18222
|
+
const lo = Math.min(from, to);
|
|
18223
|
+
const hi = Math.max(from, to);
|
|
18224
|
+
if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
|
|
18225
|
+
if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
|
|
18226
|
+
const key = `${this.deps.marketKey()}|${resolved}`;
|
|
18227
|
+
const entries = this.cache.get(key) ?? [];
|
|
18228
|
+
const covering = entries.find((e) => e.from <= lo && e.to >= hi);
|
|
18229
|
+
if (covering) {
|
|
18230
|
+
if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
|
|
18231
|
+
if (covering.failedAt > 0) {
|
|
18232
|
+
if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
|
|
18233
|
+
entries.splice(entries.indexOf(covering), 1);
|
|
18234
|
+
} else {
|
|
18235
|
+
this.maybeRefresh(key, covering, resolved, barMs, hi);
|
|
18236
|
+
return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
|
|
18237
|
+
}
|
|
18238
|
+
}
|
|
18239
|
+
this.fetchWindow(key, entries, resolved, lo, hi);
|
|
18240
|
+
return this.loading(entries, resolved, barMs, lo, hi);
|
|
18241
|
+
}
|
|
18242
|
+
onUpdate(listener) {
|
|
18243
|
+
this.listeners.add(listener);
|
|
18244
|
+
return () => this.listeners.delete(listener);
|
|
18245
|
+
}
|
|
18246
|
+
// ── internals ──
|
|
18247
|
+
/** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
|
|
18248
|
+
* step still below it); an explicit timeframe passes only when strictly finer. Failures
|
|
18249
|
+
* distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
|
|
18250
|
+
* at the finest offered step) so the consumer can word its notice honestly. */
|
|
18251
|
+
resolveTimeframe(timeframe) {
|
|
18252
|
+
const chartMs = timeframeToMs(this.deps.chartTimeframe());
|
|
18253
|
+
const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
|
|
18254
|
+
if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
|
|
18255
|
+
const tf = timeframe.trim() || "auto";
|
|
18256
|
+
if (tf === "auto") {
|
|
18257
|
+
for (const step of AUTO_STEPS) {
|
|
18258
|
+
if (timeframeToMs(step) <= chartMs / 4) return step;
|
|
18259
|
+
}
|
|
18260
|
+
return finest;
|
|
18261
|
+
}
|
|
18262
|
+
return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
|
|
18263
|
+
}
|
|
18264
|
+
/** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
|
|
18265
|
+
* windows — a widened window keeps painting what it already has while it fetches. */
|
|
18266
|
+
loading(entries, timeframe, barMs, lo, hi) {
|
|
18267
|
+
const partial = /* @__PURE__ */ new Map();
|
|
18268
|
+
for (const e of entries) {
|
|
18269
|
+
if (e.pending || e.failedAt > 0) continue;
|
|
18270
|
+
if (e.to < lo || e.from > hi) continue;
|
|
18271
|
+
for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
|
|
18272
|
+
}
|
|
18273
|
+
if (partial.size === 0) return { state: "loading", timeframe, barMs };
|
|
18274
|
+
const bars = [...partial.values()].sort((a, b) => a.time - b.time);
|
|
18275
|
+
return { state: "loading", timeframe, barMs, bars };
|
|
18276
|
+
}
|
|
18277
|
+
/** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
|
|
18278
|
+
* defers this one (a corner drag repaints per pointer move — kicking a window per
|
|
18279
|
+
* frame would spam the provider); when it lands, the next paint re-evaluates. */
|
|
18280
|
+
fetchWindow(key, entries, timeframe, lo, hi) {
|
|
18281
|
+
if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
|
|
18282
|
+
const pad = (hi - lo) * PAD_FRAC;
|
|
18283
|
+
const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
|
|
18284
|
+
entries.push(entry);
|
|
18285
|
+
this.cache.set(key, entries);
|
|
18286
|
+
this.evict();
|
|
18287
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
18288
|
+
entry.bars = bars;
|
|
18289
|
+
entry.fetchedAt = Date.now();
|
|
18290
|
+
entry.pending = false;
|
|
18291
|
+
this.absorbOverlaps(key, entry);
|
|
18292
|
+
this.fire();
|
|
18293
|
+
}).catch(() => {
|
|
18294
|
+
entry.pending = false;
|
|
18295
|
+
entry.failedAt = Date.now();
|
|
18296
|
+
this.fire();
|
|
18297
|
+
});
|
|
18298
|
+
}
|
|
18299
|
+
/** A window whose right edge reaches the newest fetched bar refreshes at most once per
|
|
18300
|
+
* bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
|
|
18301
|
+
maybeRefresh(key, entry, timeframe, barMs, hi) {
|
|
18302
|
+
if (entry.pending) return;
|
|
18303
|
+
const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
|
|
18304
|
+
if (hi < lastBar) return;
|
|
18305
|
+
if (Date.now() - entry.fetchedAt < barMs) return;
|
|
18306
|
+
entry.pending = true;
|
|
18307
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
18308
|
+
entry.bars = bars;
|
|
18309
|
+
entry.fetchedAt = Date.now();
|
|
18310
|
+
entry.pending = false;
|
|
18311
|
+
this.fire();
|
|
18312
|
+
}).catch(() => {
|
|
18313
|
+
entry.pending = false;
|
|
18314
|
+
entry.fetchedAt = Date.now();
|
|
18315
|
+
});
|
|
18316
|
+
}
|
|
18317
|
+
/** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
|
|
18318
|
+
* converges instead of accumulating slivers. */
|
|
18319
|
+
absorbOverlaps(key, entry) {
|
|
18320
|
+
const entries = this.cache.get(key);
|
|
18321
|
+
if (!entries) return;
|
|
18322
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
18323
|
+
const other = entries[i];
|
|
18324
|
+
if (other === entry || other.pending || other.failedAt > 0) continue;
|
|
18325
|
+
if (other.to < entry.from || other.from > entry.to) continue;
|
|
18326
|
+
const byTime = /* @__PURE__ */ new Map();
|
|
18327
|
+
for (const b of other.bars) byTime.set(b.time, b);
|
|
18328
|
+
for (const b of entry.bars) byTime.set(b.time, b);
|
|
18329
|
+
entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
18330
|
+
entry.from = Math.min(entry.from, other.from);
|
|
18331
|
+
entry.to = Math.max(entry.to, other.to);
|
|
18332
|
+
entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
|
|
18333
|
+
entries.splice(i, 1);
|
|
18334
|
+
}
|
|
18335
|
+
}
|
|
18336
|
+
/** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
|
|
18337
|
+
evict() {
|
|
18338
|
+
let total = 0;
|
|
18339
|
+
for (const entries of this.cache.values()) total += entries.length;
|
|
18340
|
+
while (total > MAX_ENTRIES) {
|
|
18341
|
+
let oldestKey = null;
|
|
18342
|
+
let oldestIdx = -1;
|
|
18343
|
+
let oldestAt = Infinity;
|
|
18344
|
+
for (const [key, entries2] of this.cache) {
|
|
18345
|
+
for (let i = 0; i < entries2.length; i += 1) {
|
|
18346
|
+
const e = entries2[i];
|
|
18347
|
+
if (e.pending) continue;
|
|
18348
|
+
const at = e.fetchedAt || e.failedAt;
|
|
18349
|
+
if (at < oldestAt) {
|
|
18350
|
+
oldestKey = key;
|
|
18351
|
+
oldestIdx = i;
|
|
18352
|
+
oldestAt = at;
|
|
18353
|
+
}
|
|
18354
|
+
}
|
|
18355
|
+
}
|
|
18356
|
+
if (oldestKey == null) return;
|
|
18357
|
+
const entries = this.cache.get(oldestKey);
|
|
18358
|
+
entries.splice(oldestIdx, 1);
|
|
18359
|
+
if (entries.length === 0) this.cache.delete(oldestKey);
|
|
18360
|
+
total -= 1;
|
|
18361
|
+
}
|
|
18362
|
+
}
|
|
18363
|
+
/** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
|
|
18364
|
+
slice(bars, lo, hi) {
|
|
18365
|
+
const out = [];
|
|
18366
|
+
for (const b of bars) {
|
|
18367
|
+
if (b.time < lo) continue;
|
|
18368
|
+
if (b.time > hi) break;
|
|
18369
|
+
out.push(b);
|
|
18370
|
+
}
|
|
18371
|
+
return out;
|
|
18372
|
+
}
|
|
18373
|
+
fire() {
|
|
18374
|
+
for (const l of [...this.listeners]) l();
|
|
18375
|
+
}
|
|
18376
|
+
};
|
|
18377
|
+
|
|
18076
18378
|
// src/core/price-styles/BarTransform.ts
|
|
18077
18379
|
function barTransformFor(style) {
|
|
18078
18380
|
return chartType(style)?.barTransform ?? null;
|
|
@@ -18091,7 +18393,6 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
18091
18393
|
var PREVIEW_BARS = 300;
|
|
18092
18394
|
var SINGLE_LOAD_BARS = 5e3;
|
|
18093
18395
|
var CHUNK_BARS = 1e4;
|
|
18094
|
-
var FIRST_PAINT_BARS = 100;
|
|
18095
18396
|
var GAP_FACTOR = 1.5;
|
|
18096
18397
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
18097
18398
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -18209,7 +18510,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18209
18510
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
18210
18511
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
18211
18512
|
this.barTransform = barTransformFor(initialStyle);
|
|
18212
|
-
|
|
18513
|
+
const drawingSeries = new DrawingSeriesService({
|
|
18514
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
18515
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
18516
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
18517
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
18518
|
+
});
|
|
18519
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
18213
18520
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
18214
18521
|
this.endLoad();
|
|
18215
18522
|
this.events.emit("data:unresolved", info);
|
|
@@ -18361,7 +18668,6 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18361
18668
|
let painted = false;
|
|
18362
18669
|
const paint = (bars, final) => {
|
|
18363
18670
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18364
|
-
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
18365
18671
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18366
18672
|
if (!painted && bars.length > 0) {
|
|
18367
18673
|
painted = true;
|
|
@@ -18380,10 +18686,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18380
18686
|
}
|
|
18381
18687
|
};
|
|
18382
18688
|
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
18383
|
-
this.feed.loadProgressive(
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18689
|
+
this.feed.loadProgressive(
|
|
18690
|
+
market,
|
|
18691
|
+
(bars) => {
|
|
18692
|
+
paint(bars, false);
|
|
18693
|
+
if (painted) signal(true);
|
|
18694
|
+
},
|
|
18695
|
+
{ signal: abort.signal }
|
|
18696
|
+
).then((full) => {
|
|
18387
18697
|
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18388
18698
|
if (full == null) return signal(false);
|
|
18389
18699
|
if (this.generation !== gen) return signal(true);
|
|
@@ -18470,7 +18780,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18470
18780
|
* an in-flight `setMarket` immediately (the config mutates before the load). */
|
|
18471
18781
|
marketSnapshot() {
|
|
18472
18782
|
const m = this.config.market;
|
|
18473
|
-
return {
|
|
18783
|
+
return {
|
|
18784
|
+
symbol: m.symbol,
|
|
18785
|
+
provider: parseSymbol(m.symbol ?? "").provider ?? void 0,
|
|
18786
|
+
timeframe: m.timeframe,
|
|
18787
|
+
bars: m.bars,
|
|
18788
|
+
session: m.session,
|
|
18789
|
+
offline: m.data !== void 0
|
|
18790
|
+
};
|
|
18474
18791
|
}
|
|
18475
18792
|
/**
|
|
18476
18793
|
* Switch the chart's market IN PLACE — no destroy/recreate. The renderer stays
|
|
@@ -18536,10 +18853,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18536
18853
|
if (depthOnly) {
|
|
18537
18854
|
this.extendDepth(gen, m.bars ?? 500);
|
|
18538
18855
|
} else {
|
|
18539
|
-
await Promise.race([
|
|
18540
|
-
this.loadMarket(gen, { firstLoad: false }),
|
|
18541
|
-
new Promise((resolve) => this.supersedeWaiters.push(resolve))
|
|
18542
|
-
]);
|
|
18856
|
+
await Promise.race([this.loadMarket(gen, { firstLoad: false }), new Promise((resolve) => this.supersedeWaiters.push(resolve))]);
|
|
18543
18857
|
}
|
|
18544
18858
|
if (this.generation !== gen) return;
|
|
18545
18859
|
} finally {
|
|
@@ -19202,8 +19516,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19202
19516
|
onModel: (model) => {
|
|
19203
19517
|
const first = !record.announced;
|
|
19204
19518
|
const cause = record.pendingCause ?? "history";
|
|
19519
|
+
if (!this.applyModel(id, model)) return;
|
|
19205
19520
|
record.pendingCause = void 0;
|
|
19206
|
-
this.applyModel(id, model);
|
|
19207
19521
|
this.emitContextChanged(id);
|
|
19208
19522
|
this.emitScriptRun(id, cause, first);
|
|
19209
19523
|
},
|
|
@@ -19496,10 +19810,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19496
19810
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
19497
19811
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
19498
19812
|
* (live tick / viewport re-run) value-patches.
|
|
19813
|
+
*
|
|
19814
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
19815
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
19816
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
19817
|
+
* stay unchanged.
|
|
19499
19818
|
*/
|
|
19500
19819
|
applyModel(id, model) {
|
|
19501
19820
|
const record = this.registry.get(id);
|
|
19502
|
-
if (!record || record.hidden) return;
|
|
19821
|
+
if (!record || record.hidden) return true;
|
|
19822
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
19503
19823
|
const handle = this.handles.get(id);
|
|
19504
19824
|
if (!record.renderHandle) {
|
|
19505
19825
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -19509,7 +19829,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19509
19829
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19510
19830
|
record.pendingStructural = false;
|
|
19511
19831
|
this.announce(record, handle);
|
|
19512
|
-
return;
|
|
19832
|
+
return true;
|
|
19513
19833
|
}
|
|
19514
19834
|
let paneId = record.model?.paneId ?? "price";
|
|
19515
19835
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -19537,6 +19857,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19537
19857
|
}
|
|
19538
19858
|
if (record.loading) this.setLoading(record, false);
|
|
19539
19859
|
this.announce(record, handle);
|
|
19860
|
+
return true;
|
|
19861
|
+
}
|
|
19862
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
19863
|
+
static modelHasOutput(model) {
|
|
19864
|
+
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;
|
|
19540
19865
|
}
|
|
19541
19866
|
routePane(id, model, options) {
|
|
19542
19867
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -25671,7 +25996,9 @@ var DrawingSceneRenderer = class {
|
|
|
25671
25996
|
const lo = Math.min(from, to);
|
|
25672
25997
|
const hi = Math.max(from, to);
|
|
25673
25998
|
const visible = (a, b, extend) => {
|
|
25674
|
-
if (extend
|
|
25999
|
+
if (extend === "both") return true;
|
|
26000
|
+
if (extend === "left") return Math.max(a, b) >= lo;
|
|
26001
|
+
if (extend === "right") return Math.min(a, b) <= hi;
|
|
25675
26002
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
25676
26003
|
};
|
|
25677
26004
|
let min = Infinity;
|
|
@@ -28145,6 +28472,17 @@ var DrawingPainter = class {
|
|
|
28145
28472
|
constructor() {
|
|
28146
28473
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
28147
28474
|
this.targets = {};
|
|
28475
|
+
/** The chart's active series LOOK — style + resolved series colors — pushed by the
|
|
28476
|
+
* controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
|
|
28477
|
+
* area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
|
|
28478
|
+
* bars; unknown/custom styles fall back to candles), and the colors default to the main
|
|
28479
|
+
* series' own so the inset reads as a finer copy of the chart. */
|
|
28480
|
+
this.seriesLook = {
|
|
28481
|
+
style: "candles",
|
|
28482
|
+
upColor: BULLISH,
|
|
28483
|
+
downColor: BEARISH,
|
|
28484
|
+
lineColor: BULLISH
|
|
28485
|
+
};
|
|
28148
28486
|
}
|
|
28149
28487
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
28150
28488
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -28189,6 +28527,8 @@ var DrawingPainter = class {
|
|
|
28189
28527
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
28190
28528
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
28191
28529
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
28530
|
+
} else if (ghost instanceof Magnifier) {
|
|
28531
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
28192
28532
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
28193
28533
|
ctx.globalAlpha = 1;
|
|
28194
28534
|
}
|
|
@@ -28322,6 +28662,10 @@ var DrawingPainter = class {
|
|
|
28322
28662
|
this.paintLabel(ctx, d, proj, theme);
|
|
28323
28663
|
return;
|
|
28324
28664
|
}
|
|
28665
|
+
if (d instanceof Magnifier) {
|
|
28666
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
28667
|
+
return;
|
|
28668
|
+
}
|
|
28325
28669
|
if (d instanceof PatternDrawing) {
|
|
28326
28670
|
this.paintPattern(ctx, d, proj, theme);
|
|
28327
28671
|
return;
|
|
@@ -28814,6 +29158,216 @@ var DrawingPainter = class {
|
|
|
28814
29158
|
ctx.textBaseline = "alphabetic";
|
|
28815
29159
|
}
|
|
28816
29160
|
}
|
|
29161
|
+
/** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
|
|
29162
|
+
* no series read, so dragging the area open never kicks a fetch per cursor move. */
|
|
29163
|
+
paintMagnifierGhost(ctx, d, proj, theme) {
|
|
29164
|
+
const r = d.rect(proj);
|
|
29165
|
+
if (!r) return;
|
|
29166
|
+
ctx.save();
|
|
29167
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29168
|
+
ctx.lineWidth = 1;
|
|
29169
|
+
ctx.setLineDash([4, 4]);
|
|
29170
|
+
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));
|
|
29171
|
+
ctx.restore();
|
|
29172
|
+
}
|
|
29173
|
+
/** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
|
|
29174
|
+
* market at a finer timeframe — candles at their true time/price positions, clipped to
|
|
29175
|
+
* the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
|
|
29176
|
+
* `unavailable` states paint a short notice instead). The lower-timeframe candles shift
|
|
29177
|
+
* half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
|
|
29178
|
+
* centered on its open time — subdivides in place. */
|
|
29179
|
+
paintMagnifier(ctx, d, proj, theme) {
|
|
29180
|
+
const r = d.rect(proj);
|
|
29181
|
+
const a = d.anchors[0];
|
|
29182
|
+
const b = d.anchors[1];
|
|
29183
|
+
if (!r || !a || !b) return;
|
|
29184
|
+
const x0 = Math.min(r.x1, r.x2);
|
|
29185
|
+
const x1 = Math.max(r.x1, r.x2);
|
|
29186
|
+
const y0 = Math.min(r.y1, r.y2);
|
|
29187
|
+
const y1 = Math.max(r.y1, r.y2);
|
|
29188
|
+
const w = x1 - x0;
|
|
29189
|
+
const h = y1 - y0;
|
|
29190
|
+
ctx.save();
|
|
29191
|
+
ctx.globalAlpha = 1;
|
|
29192
|
+
ctx.fillStyle = theme.background;
|
|
29193
|
+
ctx.fillRect(x0, y0, w, h);
|
|
29194
|
+
ctx.restore();
|
|
29195
|
+
const from = Math.min(a.time, b.time);
|
|
29196
|
+
const to = Math.max(a.time, b.time);
|
|
29197
|
+
const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
|
|
29198
|
+
const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
|
|
29199
|
+
const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
|
|
29200
|
+
let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
|
|
29201
|
+
if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
|
|
29202
|
+
const look = this.seriesLook;
|
|
29203
|
+
const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
|
|
29204
|
+
if (transform) seriesBars = transform.full(seriesBars);
|
|
29205
|
+
const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
|
|
29206
|
+
const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
|
|
29207
|
+
ctx.save();
|
|
29208
|
+
ctx.beginPath();
|
|
29209
|
+
ctx.rect(x0, y0, w, h);
|
|
29210
|
+
ctx.clip();
|
|
29211
|
+
if (mode === "line" || mode === "area") {
|
|
29212
|
+
this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
|
|
29213
|
+
} else {
|
|
29214
|
+
this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
|
|
29215
|
+
}
|
|
29216
|
+
ctx.restore();
|
|
29217
|
+
} else if (res) {
|
|
29218
|
+
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";
|
|
29219
|
+
this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
|
|
29220
|
+
}
|
|
29221
|
+
ctx.save();
|
|
29222
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29223
|
+
ctx.lineWidth = d.style.lineWidth || 1;
|
|
29224
|
+
ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
|
|
29225
|
+
ctx.strokeRect(x0, y0, w, h);
|
|
29226
|
+
ctx.restore();
|
|
29227
|
+
if (w > 44) {
|
|
29228
|
+
const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
|
|
29229
|
+
const chipH = 17;
|
|
29230
|
+
const gap = 4;
|
|
29231
|
+
const pane = proj.paneRect?.(d.paneId);
|
|
29232
|
+
const paneBottom = pane ? pane.top + pane.height : proj.height;
|
|
29233
|
+
const below = y1 + gap + chipH <= paneBottom;
|
|
29234
|
+
const chipY = below ? y1 + gap : y1 - gap - chipH;
|
|
29235
|
+
ctx.save();
|
|
29236
|
+
ctx.font = `10px ${theme.fontFamily}`;
|
|
29237
|
+
const tw = ctx.measureText(label).width;
|
|
29238
|
+
const caretW = 11;
|
|
29239
|
+
const chipW = tw + 12 + caretW;
|
|
29240
|
+
roundRect(ctx, x0, chipY, chipW, chipH, 3);
|
|
29241
|
+
ctx.fillStyle = theme.background;
|
|
29242
|
+
ctx.fill();
|
|
29243
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29244
|
+
ctx.lineWidth = 1;
|
|
29245
|
+
ctx.setLineDash([]);
|
|
29246
|
+
ctx.stroke();
|
|
29247
|
+
ctx.fillStyle = theme.textColor;
|
|
29248
|
+
ctx.textAlign = "left";
|
|
29249
|
+
ctx.textBaseline = "middle";
|
|
29250
|
+
ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
|
|
29251
|
+
const cxr = x0 + 6 + tw + 5;
|
|
29252
|
+
const cyr = chipY + chipH / 2;
|
|
29253
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
|
|
29254
|
+
ctx.lineWidth = 1.2;
|
|
29255
|
+
ctx.beginPath();
|
|
29256
|
+
ctx.moveTo(cxr, cyr - 1.5);
|
|
29257
|
+
ctx.lineTo(cxr + 2.5, cyr + 1.5);
|
|
29258
|
+
ctx.lineTo(cxr + 5, cyr - 1.5);
|
|
29259
|
+
ctx.stroke();
|
|
29260
|
+
ctx.restore();
|
|
29261
|
+
d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
|
|
29262
|
+
} else {
|
|
29263
|
+
d.chipRect = null;
|
|
29264
|
+
}
|
|
29265
|
+
}
|
|
29266
|
+
/** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
|
|
29267
|
+
* by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
|
|
29268
|
+
* one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
|
|
29269
|
+
paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
|
|
29270
|
+
ctx.setLineDash([]);
|
|
29271
|
+
ctx.lineWidth = 1;
|
|
29272
|
+
for (const bar of bars) {
|
|
29273
|
+
const cx0 = proj.xOf(bar.time) - halfPitch;
|
|
29274
|
+
const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
|
|
29275
|
+
if (cx1 < x0 || cx0 > x1) continue;
|
|
29276
|
+
const yHigh = proj.yOf(bar.high, d.paneId);
|
|
29277
|
+
const yLow = proj.yOf(bar.low, d.paneId);
|
|
29278
|
+
const yOpen = proj.yOf(bar.open, d.paneId);
|
|
29279
|
+
const yClose = proj.yOf(bar.close, d.paneId);
|
|
29280
|
+
if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
|
|
29281
|
+
const color = bar.close >= bar.open ? upColor : downColor;
|
|
29282
|
+
const cellW = cx1 - cx0;
|
|
29283
|
+
const cx = (cx0 + cx1) / 2;
|
|
29284
|
+
ctx.strokeStyle = color;
|
|
29285
|
+
ctx.beginPath();
|
|
29286
|
+
ctx.moveTo(cx, yHigh);
|
|
29287
|
+
ctx.lineTo(cx, yLow);
|
|
29288
|
+
ctx.stroke();
|
|
29289
|
+
if (cellW < 3) continue;
|
|
29290
|
+
if (mode === "bars") {
|
|
29291
|
+
const tick = Math.max(1, cellW * 0.35);
|
|
29292
|
+
ctx.beginPath();
|
|
29293
|
+
ctx.moveTo(cx - tick, yOpen);
|
|
29294
|
+
ctx.lineTo(cx, yOpen);
|
|
29295
|
+
ctx.moveTo(cx, yClose);
|
|
29296
|
+
ctx.lineTo(cx + tick, yClose);
|
|
29297
|
+
ctx.stroke();
|
|
29298
|
+
} else {
|
|
29299
|
+
const bw = Math.max(1, cellW * 0.7);
|
|
29300
|
+
ctx.fillStyle = color;
|
|
29301
|
+
ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
|
|
29302
|
+
}
|
|
29303
|
+
}
|
|
29304
|
+
}
|
|
29305
|
+
/** The magnifier's line/area rendering: a close polyline through each cell's center (same
|
|
29306
|
+
* half-chart-bar shift as the candles), with an optional translucent fill down to the
|
|
29307
|
+
* rectangle's bottom edge for the area style. Colored like the chart's own line series. */
|
|
29308
|
+
paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
|
|
29309
|
+
const pts = [];
|
|
29310
|
+
for (const bar of bars) {
|
|
29311
|
+
const y = proj.yOf(bar.close, d.paneId);
|
|
29312
|
+
if (y == null) continue;
|
|
29313
|
+
pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
|
|
29314
|
+
}
|
|
29315
|
+
if (pts.length < 2) return;
|
|
29316
|
+
if (area) {
|
|
29317
|
+
ctx.beginPath();
|
|
29318
|
+
ctx.moveTo(pts[0][0], yBottom);
|
|
29319
|
+
for (const [px, py] of pts) ctx.lineTo(px, py);
|
|
29320
|
+
ctx.lineTo(pts[pts.length - 1][0], yBottom);
|
|
29321
|
+
ctx.closePath();
|
|
29322
|
+
ctx.fillStyle = withAlpha(color, 0.15);
|
|
29323
|
+
ctx.fill();
|
|
29324
|
+
}
|
|
29325
|
+
ctx.setLineDash([]);
|
|
29326
|
+
ctx.lineWidth = 1.5;
|
|
29327
|
+
ctx.strokeStyle = color;
|
|
29328
|
+
ctx.beginPath();
|
|
29329
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
29330
|
+
for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
29331
|
+
ctx.stroke();
|
|
29332
|
+
}
|
|
29333
|
+
/** Centered muted notice inside the magnifier rect (loading / unavailable states). */
|
|
29334
|
+
paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
|
|
29335
|
+
if (w < 60 || h < 20) return;
|
|
29336
|
+
ctx.save();
|
|
29337
|
+
ctx.beginPath();
|
|
29338
|
+
ctx.rect(x0, y0, w, h);
|
|
29339
|
+
ctx.clip();
|
|
29340
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29341
|
+
ctx.fillStyle = withAlpha(theme.textColor, 0.55);
|
|
29342
|
+
ctx.textAlign = "center";
|
|
29343
|
+
ctx.textBaseline = "middle";
|
|
29344
|
+
ctx.fillText(text, x0 + w / 2, y0 + h / 2);
|
|
29345
|
+
ctx.restore();
|
|
29346
|
+
}
|
|
29347
|
+
/** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
|
|
29348
|
+
* "drag an area"). Painted by the drawings layer while the tool is armed and no placement
|
|
29349
|
+
* is in progress; chart-background fill so it reads as chrome over any content. */
|
|
29350
|
+
paintPlacementHint(ctx, text, theme, width, height) {
|
|
29351
|
+
ctx.save();
|
|
29352
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29353
|
+
const tw = ctx.measureText(text).width;
|
|
29354
|
+
const pillW = tw + 24;
|
|
29355
|
+
const pillH = 24;
|
|
29356
|
+
const x = (width - pillW) / 2;
|
|
29357
|
+
const y = height - pillH - 14;
|
|
29358
|
+
roundRect(ctx, x, y, pillW, pillH, pillH / 2);
|
|
29359
|
+
ctx.fillStyle = theme.background;
|
|
29360
|
+
ctx.fill();
|
|
29361
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29362
|
+
ctx.lineWidth = 1;
|
|
29363
|
+
ctx.setLineDash([]);
|
|
29364
|
+
ctx.stroke();
|
|
29365
|
+
ctx.fillStyle = theme.textColor;
|
|
29366
|
+
ctx.textAlign = "center";
|
|
29367
|
+
ctx.textBaseline = "middle";
|
|
29368
|
+
ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
|
|
29369
|
+
ctx.restore();
|
|
29370
|
+
}
|
|
28817
29371
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
28818
29372
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
28819
29373
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -30229,8 +30783,9 @@ function ensureStyles5() {
|
|
|
30229
30783
|
if (!existing) document.head.appendChild(s);
|
|
30230
30784
|
}
|
|
30231
30785
|
var DrawingSettingsPopup = class {
|
|
30232
|
-
constructor(host, theme) {
|
|
30786
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
30233
30787
|
this.host = host;
|
|
30788
|
+
this.chartBarMs = chartBarMs;
|
|
30234
30789
|
this.el = null;
|
|
30235
30790
|
this.tipEl = null;
|
|
30236
30791
|
// floating hover-label (above/below the toolbar)
|
|
@@ -30255,6 +30810,14 @@ var DrawingSettingsPopup = class {
|
|
|
30255
30810
|
this.theme = theme;
|
|
30256
30811
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
30257
30812
|
}
|
|
30813
|
+
/** The magnifier timeframe choices strictly below the chart's own bar duration
|
|
30814
|
+
* (`auto` rides along while at least one concrete lower step exists). */
|
|
30815
|
+
lowerTimeframeOptions() {
|
|
30816
|
+
const chartMs = this.chartBarMs();
|
|
30817
|
+
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
30818
|
+
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
30819
|
+
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
30820
|
+
}
|
|
30258
30821
|
setTheme(theme) {
|
|
30259
30822
|
this.theme = theme;
|
|
30260
30823
|
this.settingsDialog.setTheme(theme);
|
|
@@ -30297,7 +30860,20 @@ var DrawingSettingsPopup = class {
|
|
|
30297
30860
|
const sz = drawing.size ?? "normal";
|
|
30298
30861
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
30299
30862
|
}
|
|
30300
|
-
if (paths.has("
|
|
30863
|
+
if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
|
|
30864
|
+
const options = this.lowerTimeframeOptions();
|
|
30865
|
+
if (options.length > 0) {
|
|
30866
|
+
bar.appendChild(
|
|
30867
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
30868
|
+
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
30869
|
+
labelInTrigger: true
|
|
30870
|
+
})
|
|
30871
|
+
);
|
|
30872
|
+
}
|
|
30873
|
+
bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
|
|
30874
|
+
bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
|
|
30875
|
+
}
|
|
30876
|
+
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 })));
|
|
30301
30877
|
if (paths.has("style.lineWidth")) {
|
|
30302
30878
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
30303
30879
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -30619,6 +31195,7 @@ var DrawingSettingsPopup = class {
|
|
|
30619
31195
|
this.colorPop = null;
|
|
30620
31196
|
this.colorOwner = null;
|
|
30621
31197
|
}
|
|
31198
|
+
opts.onClose?.();
|
|
30622
31199
|
}
|
|
30623
31200
|
});
|
|
30624
31201
|
const el = pop.el;
|
|
@@ -30637,6 +31214,49 @@ var DrawingSettingsPopup = class {
|
|
|
30637
31214
|
pop.show();
|
|
30638
31215
|
return pop;
|
|
30639
31216
|
}
|
|
31217
|
+
/**
|
|
31218
|
+
* A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
|
|
31219
|
+
* canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
|
|
31220
|
+
* to position against, and removed again when the menu closes. Independent of the
|
|
31221
|
+
* quick toolbar — the chip works without selecting the drawing first.
|
|
31222
|
+
*/
|
|
31223
|
+
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
31224
|
+
ensureStyles5();
|
|
31225
|
+
closeOpenPopovers();
|
|
31226
|
+
const options = this.lowerTimeframeOptions();
|
|
31227
|
+
const anchor = document.createElement("div");
|
|
31228
|
+
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
31229
|
+
this.host.appendChild(anchor);
|
|
31230
|
+
this.menuPop = this.hostFloat(anchor, {
|
|
31231
|
+
zIndex: 26,
|
|
31232
|
+
padding: "4px",
|
|
31233
|
+
onClose: () => anchor.remove(),
|
|
31234
|
+
fill: (menu2, pop) => {
|
|
31235
|
+
if (options.length === 0) {
|
|
31236
|
+
const note = document.createElement("div");
|
|
31237
|
+
note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
|
|
31238
|
+
note.textContent = "No lower timeframe available";
|
|
31239
|
+
menu2.appendChild(note);
|
|
31240
|
+
return;
|
|
31241
|
+
}
|
|
31242
|
+
for (const o of options) {
|
|
31243
|
+
const item = document.createElement("button");
|
|
31244
|
+
item.type = "button";
|
|
31245
|
+
item.className = "vela-dpop-item";
|
|
31246
|
+
item.dataset.active = o.value === current ? "1" : "0";
|
|
31247
|
+
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;";
|
|
31248
|
+
item.textContent = o.label;
|
|
31249
|
+
item.addEventListener("click", (e) => {
|
|
31250
|
+
e.stopPropagation();
|
|
31251
|
+
pop.hide();
|
|
31252
|
+
onPick(o.value);
|
|
31253
|
+
});
|
|
31254
|
+
menu2.appendChild(item);
|
|
31255
|
+
}
|
|
31256
|
+
}
|
|
31257
|
+
});
|
|
31258
|
+
this.menuOwner = anchor;
|
|
31259
|
+
}
|
|
30640
31260
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
30641
31261
|
openActionMenu(anchor, rows) {
|
|
30642
31262
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -30747,10 +31367,13 @@ var DrawingSettingsPopup = class {
|
|
|
30747
31367
|
let cur = current;
|
|
30748
31368
|
const paint = (v) => {
|
|
30749
31369
|
b.replaceChildren();
|
|
30750
|
-
const
|
|
30751
|
-
|
|
30752
|
-
|
|
30753
|
-
|
|
31370
|
+
const glyph = render(v);
|
|
31371
|
+
if (glyph) {
|
|
31372
|
+
const ic = document.createElement("span");
|
|
31373
|
+
ic.style.cssText = "display:flex;";
|
|
31374
|
+
ic.innerHTML = sized(glyph);
|
|
31375
|
+
b.appendChild(ic);
|
|
31376
|
+
}
|
|
30754
31377
|
if (opts.label && opts.labelInTrigger) {
|
|
30755
31378
|
const tx = document.createElement("span");
|
|
30756
31379
|
tx.textContent = opts.label(v);
|
|
@@ -30792,10 +31415,13 @@ var DrawingSettingsPopup = class {
|
|
|
30792
31415
|
item.className = "vela-dpop-item";
|
|
30793
31416
|
item.dataset.active = active ? "1" : "0";
|
|
30794
31417
|
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;`;
|
|
30795
|
-
const
|
|
30796
|
-
|
|
30797
|
-
|
|
30798
|
-
|
|
31418
|
+
const glyph = render(v);
|
|
31419
|
+
if (glyph) {
|
|
31420
|
+
const ic = document.createElement("span");
|
|
31421
|
+
ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
|
|
31422
|
+
ic.innerHTML = sized(glyph, 18);
|
|
31423
|
+
item.appendChild(ic);
|
|
31424
|
+
}
|
|
30799
31425
|
if (label) {
|
|
30800
31426
|
const tx = document.createElement("span");
|
|
30801
31427
|
tx.textContent = label(v);
|
|
@@ -31147,6 +31773,9 @@ var UserDrawingController = class {
|
|
|
31147
31773
|
this.intentCb = null;
|
|
31148
31774
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
31149
31775
|
this.externalGhost = null;
|
|
31776
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
31777
|
+
this.seriesGw = null;
|
|
31778
|
+
this.seriesGwUnsub = null;
|
|
31150
31779
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
31151
31780
|
this.lastDraftKey = null;
|
|
31152
31781
|
this.measure = new MeasureOverlay();
|
|
@@ -31174,7 +31803,7 @@ var UserDrawingController = class {
|
|
|
31174
31803
|
this.textEditor = null;
|
|
31175
31804
|
this.painter = new DrawingPainter();
|
|
31176
31805
|
this.ctx = canvas.getContext("2d");
|
|
31177
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
31806
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
31178
31807
|
this.toolbar = new DrawingToolbar(
|
|
31179
31808
|
toolbarHost,
|
|
31180
31809
|
deps.theme(),
|
|
@@ -31232,6 +31861,22 @@ var UserDrawingController = class {
|
|
|
31232
31861
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
31233
31862
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
31234
31863
|
}
|
|
31864
|
+
/** Core push: the series gateway data-driven drawings read finer-timeframe bars
|
|
31865
|
+
* through (surfaced to them as `Projector.seriesInRange`). A landed background
|
|
31866
|
+
* fetch repaints both this layer and the interleave slices under the series. */
|
|
31867
|
+
setSeriesGateway(gateway) {
|
|
31868
|
+
this.seriesGwUnsub?.();
|
|
31869
|
+
this.seriesGw = gateway;
|
|
31870
|
+
this.seriesGwUnsub = gateway.onUpdate(() => {
|
|
31871
|
+
this.invalidateSlices();
|
|
31872
|
+
this.render();
|
|
31873
|
+
this.deps.requestDataPaint();
|
|
31874
|
+
});
|
|
31875
|
+
}
|
|
31876
|
+
/** The pushed series gateway, or null before the core provides one. */
|
|
31877
|
+
get seriesGateway() {
|
|
31878
|
+
return this.seriesGw;
|
|
31879
|
+
}
|
|
31235
31880
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
31236
31881
|
setExternalGhost(doc) {
|
|
31237
31882
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -31349,8 +31994,34 @@ var UserDrawingController = class {
|
|
|
31349
31994
|
/** Should the drawing layer win this press (vs pan)? */
|
|
31350
31995
|
claim(x, y) {
|
|
31351
31996
|
if (this.measureMode || this.eraserMode) return true;
|
|
31997
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
31352
31998
|
return this.interaction.claim(x, y);
|
|
31353
31999
|
}
|
|
32000
|
+
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
32001
|
+
* the chip's rect is what the painter measured last frame. */
|
|
32002
|
+
magnifierChipAt(x, y) {
|
|
32003
|
+
for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
|
|
32004
|
+
const d = this.drawings[i];
|
|
32005
|
+
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
32006
|
+
const r = d.chipRect;
|
|
32007
|
+
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
32008
|
+
}
|
|
32009
|
+
return null;
|
|
32010
|
+
}
|
|
32011
|
+
/** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
|
|
32012
|
+
* settings-popup edit (same intent, same undo step). */
|
|
32013
|
+
openMagnifierChipMenu(drawing) {
|
|
32014
|
+
const rect = drawing.chipRect;
|
|
32015
|
+
if (!rect) return;
|
|
32016
|
+
const id = drawing.id;
|
|
32017
|
+
this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
|
|
32018
|
+
const d = this.drawings.find((x) => x.id === id);
|
|
32019
|
+
if (!(d instanceof Magnifier)) return;
|
|
32020
|
+
d.applySettings({ "magnifier.timeframe": value });
|
|
32021
|
+
this.render();
|
|
32022
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32023
|
+
});
|
|
32024
|
+
}
|
|
31354
32025
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
31355
32026
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
31356
32027
|
deleteAt(x, y) {
|
|
@@ -31396,6 +32067,13 @@ var UserDrawingController = class {
|
|
|
31396
32067
|
this.render();
|
|
31397
32068
|
return;
|
|
31398
32069
|
}
|
|
32070
|
+
if (this.activeTool == null) {
|
|
32071
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
32072
|
+
if (chipOwner) {
|
|
32073
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
32074
|
+
return;
|
|
32075
|
+
}
|
|
32076
|
+
}
|
|
31399
32077
|
this.interaction.down(x, y, snap, shift);
|
|
31400
32078
|
}
|
|
31401
32079
|
pointerMove(x, y, snap = "off", shift = false) {
|
|
@@ -31614,6 +32292,7 @@ var UserDrawingController = class {
|
|
|
31614
32292
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
31615
32293
|
cursorAt(x, y) {
|
|
31616
32294
|
if (this.eraserMode) return "pointer";
|
|
32295
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
31617
32296
|
return this.interaction.cursorAt(x, y);
|
|
31618
32297
|
}
|
|
31619
32298
|
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
@@ -31792,6 +32471,7 @@ var UserDrawingController = class {
|
|
|
31792
32471
|
if (!sctx) continue;
|
|
31793
32472
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
31794
32473
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32474
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
31795
32475
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
31796
32476
|
const slices = out.get(paneId) ?? [];
|
|
31797
32477
|
slices.push({ beforeZ, canvas });
|
|
@@ -31819,6 +32499,7 @@ var UserDrawingController = class {
|
|
|
31819
32499
|
dragged: this.interaction.activeDragId(),
|
|
31820
32500
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
31821
32501
|
};
|
|
32502
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
31822
32503
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
31823
32504
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
31824
32505
|
this.layoutTextEditor();
|
|
@@ -31826,6 +32507,10 @@ var UserDrawingController = class {
|
|
|
31826
32507
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
31827
32508
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
31828
32509
|
this.emitDraft(ghost);
|
|
32510
|
+
if (this.activeTool && !ghost) {
|
|
32511
|
+
const hint = getDrawingType(this.activeTool)?.placementHint;
|
|
32512
|
+
if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
|
|
32513
|
+
}
|
|
31829
32514
|
const markers = this.interaction.placingMarkers(proj);
|
|
31830
32515
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
31831
32516
|
const m = this.interaction.snapMarker();
|
|
@@ -31837,6 +32522,9 @@ var UserDrawingController = class {
|
|
|
31837
32522
|
this.closeTextEditor();
|
|
31838
32523
|
this.popup.destroy();
|
|
31839
32524
|
this.toolbar.destroy();
|
|
32525
|
+
this.seriesGwUnsub?.();
|
|
32526
|
+
this.seriesGwUnsub = null;
|
|
32527
|
+
this.seriesGw = null;
|
|
31840
32528
|
this.intentCb = null;
|
|
31841
32529
|
this.drawings = [];
|
|
31842
32530
|
}
|
|
@@ -32234,7 +32922,7 @@ function mergeSlices(indicator, user) {
|
|
|
32234
32922
|
}
|
|
32235
32923
|
|
|
32236
32924
|
// src/renderers/native/drawings/Projector.ts
|
|
32237
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
32925
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
32238
32926
|
return {
|
|
32239
32927
|
xOf: (time) => coords.timeToX(time),
|
|
32240
32928
|
yOf: (price, paneId) => {
|
|
@@ -32255,6 +32943,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
|
32255
32943
|
},
|
|
32256
32944
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
32257
32945
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
32946
|
+
seriesInRange,
|
|
32258
32947
|
width: coords.width,
|
|
32259
32948
|
height: coords.height
|
|
32260
32949
|
};
|
|
@@ -34124,6 +34813,23 @@ var NativeRenderer = class {
|
|
|
34124
34813
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
34125
34814
|
priceZ: (paneId) => paneId === PRICE_PANE_ID2 ? this.scene.candleZ : null,
|
|
34126
34815
|
requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
|
|
34816
|
+
// The look the price series ACTUALLY paints with: candle colors resolved through
|
|
34817
|
+
// the per-style override, line/area colors through their configured styles — so
|
|
34818
|
+
// series-mirroring content (the magnifier inset) matches the chart exactly.
|
|
34819
|
+
seriesLook: () => {
|
|
34820
|
+
const st = this.scene.style;
|
|
34821
|
+
const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
|
|
34822
|
+
const barsUp = st.bars.upColor ?? this.theme.upColor;
|
|
34823
|
+
const barsDown = st.bars.downColor ?? this.theme.downColor;
|
|
34824
|
+
const style = this.scene.priceStyle;
|
|
34825
|
+
return {
|
|
34826
|
+
style,
|
|
34827
|
+
upColor: style === "bars" ? barsUp : paint.up,
|
|
34828
|
+
downColor: style === "bars" ? barsDown : paint.down,
|
|
34829
|
+
lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
|
|
34830
|
+
};
|
|
34831
|
+
},
|
|
34832
|
+
chartBarMs: () => this.coords.barInterval,
|
|
34127
34833
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
34128
34834
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
34129
34835
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -35517,7 +36223,8 @@ var NativeRenderer = class {
|
|
|
35517
36223
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
35518
36224
|
},
|
|
35519
36225
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
35520
|
-
(from, to) => this.barsInTimeRange(from, to)
|
|
36226
|
+
(from, to) => this.barsInTimeRange(from, to),
|
|
36227
|
+
this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
|
|
35521
36228
|
);
|
|
35522
36229
|
}
|
|
35523
36230
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|
|
@@ -36724,6 +37431,21 @@ function resolveElement(container) {
|
|
|
36724
37431
|
// src/core/options.ts
|
|
36725
37432
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
36726
37433
|
|
|
37434
|
+
// src/widget/statusline-model.ts
|
|
37435
|
+
function segmentVisibility(parts, chartHidden = false) {
|
|
37436
|
+
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
37437
|
+
}
|
|
37438
|
+
function statuslineMenuItems(parts, chartVisible) {
|
|
37439
|
+
return [
|
|
37440
|
+
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
37441
|
+
{ id: "part:name", label: "Symbol name", checked: parts.name },
|
|
37442
|
+
{ id: "part:market", label: "Market status", checked: parts.market },
|
|
37443
|
+
{ id: "part:ohlc", label: "OHLC values", checked: parts.ohlc },
|
|
37444
|
+
{ id: "part:change", label: "Bar change values", checked: parts.change },
|
|
37445
|
+
{ id: "chart", label: chartVisible ? "Hide chart" : "Show chart", separatorBefore: true }
|
|
37446
|
+
];
|
|
37447
|
+
}
|
|
37448
|
+
|
|
36727
37449
|
// src/widget/format.ts
|
|
36728
37450
|
function decimalsFor(ref) {
|
|
36729
37451
|
const a = Math.abs(ref);
|
|
@@ -36771,7 +37493,13 @@ var CSS21 = `
|
|
|
36771
37493
|
padding: 2px 7px;
|
|
36772
37494
|
margin-left: -7px;
|
|
36773
37495
|
}
|
|
36774
|
-
|
|
37496
|
+
/* Hovering opens the chip the same way a legend row opens: solid chart background
|
|
37497
|
+
* plus the same inset neutral outline the indicator rows wear (InputsUI's
|
|
37498
|
+
* setRowHighlighted) \u2014 the two columns read as one family. */
|
|
37499
|
+
.vela-statusline:hover { background: var(--vela-bg); box-shadow: inset 0 0 0 1px var(--vela-border); }
|
|
37500
|
+
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
37501
|
+
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
37502
|
+
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
36775
37503
|
.vela-statusline .vela-sl-avatar {
|
|
36776
37504
|
width: 18px;
|
|
36777
37505
|
height: 18px;
|
|
@@ -36796,6 +37524,24 @@ var CSS21 = `
|
|
|
36796
37524
|
* these are the pre-ink fallbacks only. */
|
|
36797
37525
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
36798
37526
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
37527
|
+
/* The show-chart eye \u2014 out only while the chart is hidden (syncParts drives display),
|
|
37528
|
+
* replacing the value readout it took away. Same footprint as a legend action button. */
|
|
37529
|
+
.vela-statusline .vela-sl-eye {
|
|
37530
|
+
align-self: center;
|
|
37531
|
+
align-items: center;
|
|
37532
|
+
justify-content: center;
|
|
37533
|
+
width: 18px;
|
|
37534
|
+
height: 18px;
|
|
37535
|
+
padding: 0;
|
|
37536
|
+
border: none;
|
|
37537
|
+
border-radius: 3px;
|
|
37538
|
+
background: none;
|
|
37539
|
+
color: var(--vela-fg-muted);
|
|
37540
|
+
cursor: pointer;
|
|
37541
|
+
line-height: 0;
|
|
37542
|
+
flex: none;
|
|
37543
|
+
}
|
|
37544
|
+
.vela-statusline .vela-sl-eye:hover { color: var(--vela-fg); background: color-mix(in srgb, var(--vela-fg) 12%, transparent); }
|
|
36799
37545
|
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
36800
37546
|
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
36801
37547
|
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
@@ -36905,7 +37651,12 @@ var Statusline = class {
|
|
|
36905
37651
|
constructor(host, symbol, iconFor) {
|
|
36906
37652
|
this.host = host;
|
|
36907
37653
|
this.iconFor = iconFor;
|
|
36908
|
-
this.parts = { name: true, market: true, ohlc: true, change: true };
|
|
37654
|
+
this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
|
|
37655
|
+
/** The right-click action menu — present once a host wires it via {@link attachMenu}. */
|
|
37656
|
+
this.menu = null;
|
|
37657
|
+
this.menuHooks = null;
|
|
37658
|
+
/** Mirror of the renderer's `candleVisible` — see {@link setChartHidden}. */
|
|
37659
|
+
this.chartHidden = false;
|
|
36909
37660
|
this.lastBar = null;
|
|
36910
37661
|
this.hoverBar = null;
|
|
36911
37662
|
this.unsubs = [];
|
|
@@ -36921,6 +37672,15 @@ var Statusline = class {
|
|
|
36921
37672
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
36922
37673
|
this.fitMode = false;
|
|
36923
37674
|
this.fitRO = null;
|
|
37675
|
+
this.onContextMenu = (e) => {
|
|
37676
|
+
if (!this.menu || !this.menuHooks) return;
|
|
37677
|
+
e.preventDefault();
|
|
37678
|
+
e.stopPropagation();
|
|
37679
|
+
const chartVisible = this.menuHooks.chartVisible();
|
|
37680
|
+
this.setChartHidden(!chartVisible);
|
|
37681
|
+
this.menu.setItems(statuslineMenuItems(this.parts, chartVisible));
|
|
37682
|
+
this.menu.openAt(e.clientX, e.clientY);
|
|
37683
|
+
};
|
|
36924
37684
|
const doc = host.ownerDocument;
|
|
36925
37685
|
injectStyles(STYLE_ID24, CSS21, doc);
|
|
36926
37686
|
host.classList.add("vela-has-statusline");
|
|
@@ -36947,9 +37707,21 @@ var Statusline = class {
|
|
|
36947
37707
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
36948
37708
|
this.changeEl = doc.createElement("span");
|
|
36949
37709
|
this.changeEl.className = "vela-sl-change";
|
|
36950
|
-
this.
|
|
37710
|
+
this.eyeEl = doc.createElement("button");
|
|
37711
|
+
this.eyeEl.type = "button";
|
|
37712
|
+
this.eyeEl.className = "vela-sl-eye";
|
|
37713
|
+
this.eyeEl.innerHTML = iconAt("eye-off", 14);
|
|
37714
|
+
this.eyeEl.setAttribute("aria-label", "Show chart");
|
|
37715
|
+
this.eyeEl.style.display = "none";
|
|
37716
|
+
this.eyeEl.addEventListener("click", (e) => {
|
|
37717
|
+
e.stopPropagation();
|
|
37718
|
+
this.menuHooks?.setChartVisible(true);
|
|
37719
|
+
this.setChartHidden(false);
|
|
37720
|
+
});
|
|
37721
|
+
this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl, this.eyeEl);
|
|
36951
37722
|
host.appendChild(this.el);
|
|
36952
37723
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
37724
|
+
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
36953
37725
|
this.setMarketStatus("open");
|
|
36954
37726
|
this.render();
|
|
36955
37727
|
}
|
|
@@ -36959,6 +37731,7 @@ var Statusline = class {
|
|
|
36959
37731
|
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
36960
37732
|
this.avatarEl.replaceWith(fresh);
|
|
36961
37733
|
this.avatarEl = fresh;
|
|
37734
|
+
this.syncParts();
|
|
36962
37735
|
this.fit();
|
|
36963
37736
|
}
|
|
36964
37737
|
/**
|
|
@@ -36986,20 +37759,25 @@ var Statusline = class {
|
|
|
36986
37759
|
}
|
|
36987
37760
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
36988
37761
|
syncParts() {
|
|
36989
|
-
|
|
36990
|
-
this.
|
|
36991
|
-
this.
|
|
36992
|
-
this.
|
|
37762
|
+
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
37763
|
+
this.avatarEl.style.display = seg.avatar ? "" : "none";
|
|
37764
|
+
this.symbolEl.style.display = seg.symbol ? "" : "none";
|
|
37765
|
+
this.metaEl.style.display = seg.meta ? "" : "none";
|
|
37766
|
+
this.marketEl.style.display = seg.market ? "" : "none";
|
|
37767
|
+
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
37768
|
+
this.changeEl.style.display = seg.change ? "" : "none";
|
|
37769
|
+
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
36993
37770
|
}
|
|
36994
37771
|
/** Hide overflowing segments until the row fits its max-width (fit mode only). */
|
|
36995
37772
|
fit() {
|
|
36996
37773
|
if (!this.fitMode) return;
|
|
36997
37774
|
this.syncParts();
|
|
37775
|
+
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
36998
37776
|
const order = [
|
|
36999
|
-
[this.ohlcEl,
|
|
37000
|
-
[this.changeEl,
|
|
37001
|
-
[this.metaEl,
|
|
37002
|
-
[this.marketEl,
|
|
37777
|
+
[this.ohlcEl, seg.ohlc],
|
|
37778
|
+
[this.changeEl, seg.change],
|
|
37779
|
+
[this.metaEl, seg.meta],
|
|
37780
|
+
[this.marketEl, seg.market]
|
|
37003
37781
|
];
|
|
37004
37782
|
for (const [el, shown] of order) {
|
|
37005
37783
|
if (this.el.scrollWidth <= this.el.clientWidth) break;
|
|
@@ -37020,7 +37798,6 @@ var Statusline = class {
|
|
|
37020
37798
|
this.readout = readout;
|
|
37021
37799
|
this.render();
|
|
37022
37800
|
}
|
|
37023
|
-
/** Show/hide one part (the settings dialog's Status line tab drives these). */
|
|
37024
37801
|
/** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
|
|
37025
37802
|
setMeta(timeframe, provider) {
|
|
37026
37803
|
this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
|
|
@@ -37039,6 +37816,9 @@ var Statusline = class {
|
|
|
37039
37816
|
});
|
|
37040
37817
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
37041
37818
|
}
|
|
37819
|
+
/** Show/hide one part — the settings dialog's Status line tab and the right-click
|
|
37820
|
+
* menu both drive these. 'name' owns the venue/timeframe meta too (see
|
|
37821
|
+
* {@link segmentVisibility}). */
|
|
37042
37822
|
setPartVisible(part, visible) {
|
|
37043
37823
|
this.parts[part] = visible;
|
|
37044
37824
|
this.syncParts();
|
|
@@ -37047,6 +37827,47 @@ var Statusline = class {
|
|
|
37047
37827
|
partVisible(part) {
|
|
37048
37828
|
return this.parts[part];
|
|
37049
37829
|
}
|
|
37830
|
+
/** Mirror the chart's (price series') visibility: dim the whole line like a hidden
|
|
37831
|
+
* indicator's legend row, drop the value readout (OHLC + bar change — values of a
|
|
37832
|
+
* series that isn't painted), and put the show-chart eye out in its place. The
|
|
37833
|
+
* parts config is untouched, so showing the chart restores the readout exactly as
|
|
37834
|
+
* configured. Idempotent; {@link render} re-syncs it from the live renderer, so
|
|
37835
|
+
* toggles made elsewhere (the object tree's eye) converge too. */
|
|
37836
|
+
setChartHidden(hidden) {
|
|
37837
|
+
if (hidden === this.chartHidden) return;
|
|
37838
|
+
this.chartHidden = hidden;
|
|
37839
|
+
this.el.classList.toggle("vela-sl-chart-hidden", hidden);
|
|
37840
|
+
this.syncParts();
|
|
37841
|
+
this.fit();
|
|
37842
|
+
}
|
|
37843
|
+
/** Wire the right-click action menu: one checkable toggle per part plus hide/show
|
|
37844
|
+
* for the chart itself. The menu is built once; later calls just swap the hooks. */
|
|
37845
|
+
attachMenu(hooks) {
|
|
37846
|
+
this.menuHooks = hooks;
|
|
37847
|
+
if (this.menu) return;
|
|
37848
|
+
this.menu = new Menu({
|
|
37849
|
+
host: this.host,
|
|
37850
|
+
items: [],
|
|
37851
|
+
placement: "bottom-start",
|
|
37852
|
+
// Pointer-anchored action menu — checked state reads as a leading ✓ (the
|
|
37853
|
+
// same shape as the chart's own context menu).
|
|
37854
|
+
checkmarks: true,
|
|
37855
|
+
onSelect: (id) => this.runMenuItem(id)
|
|
37856
|
+
});
|
|
37857
|
+
this.el.addEventListener("contextmenu", this.onContextMenu);
|
|
37858
|
+
}
|
|
37859
|
+
runMenuItem(id) {
|
|
37860
|
+
const hooks = this.menuHooks;
|
|
37861
|
+
if (!hooks) return;
|
|
37862
|
+
if (id.startsWith("part:")) {
|
|
37863
|
+
const part = id.slice("part:".length);
|
|
37864
|
+
hooks.setPart(part, !this.parts[part]);
|
|
37865
|
+
} else if (id === "chart") {
|
|
37866
|
+
const next = !hooks.chartVisible();
|
|
37867
|
+
hooks.setChartVisible(next);
|
|
37868
|
+
this.setChartHidden(!next);
|
|
37869
|
+
}
|
|
37870
|
+
}
|
|
37050
37871
|
/** (Re)bind to a chart instance — called after every widget rebuild. */
|
|
37051
37872
|
onChart(chart) {
|
|
37052
37873
|
this.detach();
|
|
@@ -37068,7 +37889,11 @@ var Statusline = class {
|
|
|
37068
37889
|
this.detach();
|
|
37069
37890
|
this.fitRO?.disconnect();
|
|
37070
37891
|
this.fitRO = null;
|
|
37892
|
+
this.el.removeEventListener("contextmenu", this.onContextMenu);
|
|
37893
|
+
this.menu?.destroy();
|
|
37894
|
+
this.menu = null;
|
|
37071
37895
|
this.marketTip.destroy();
|
|
37896
|
+
this.eyeTip.destroy();
|
|
37072
37897
|
this.marketBubble.destroy();
|
|
37073
37898
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
37074
37899
|
this.el.remove();
|
|
@@ -37078,6 +37903,7 @@ var Statusline = class {
|
|
|
37078
37903
|
this.unsubs = [];
|
|
37079
37904
|
}
|
|
37080
37905
|
render() {
|
|
37906
|
+
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
37081
37907
|
const bar = this.hoverBar ?? this.lastBar;
|
|
37082
37908
|
if (!bar) {
|
|
37083
37909
|
this.ohlcEl.replaceChildren();
|
|
@@ -37148,7 +37974,7 @@ var FETCH_BACK_MS = 4 * 864e5;
|
|
|
37148
37974
|
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
37149
37975
|
var MIN_TIMER_MS = 15e3;
|
|
37150
37976
|
var MAX_TIMER_MS = 36e5;
|
|
37151
|
-
var
|
|
37977
|
+
var RETRY_MS2 = 6e4;
|
|
37152
37978
|
var MarketStatusTracker = class {
|
|
37153
37979
|
constructor(onStatus) {
|
|
37154
37980
|
this.onStatus = onStatus;
|
|
@@ -37185,7 +38011,7 @@ var MarketStatusTracker = class {
|
|
|
37185
38011
|
]);
|
|
37186
38012
|
if (my !== this.epoch) return;
|
|
37187
38013
|
if (!regular || !extended) {
|
|
37188
|
-
this.arm(my, data, symbol,
|
|
38014
|
+
this.arm(my, data, symbol, RETRY_MS2);
|
|
37189
38015
|
return;
|
|
37190
38016
|
}
|
|
37191
38017
|
const w = { regular, extended };
|
|
@@ -37896,7 +38722,14 @@ var ChartCell = class {
|
|
|
37896
38722
|
this.destroyed = false;
|
|
37897
38723
|
this.appTheme = deps.theme;
|
|
37898
38724
|
const symbol = prefixedSymbol(seed);
|
|
37899
|
-
this.state = {
|
|
38725
|
+
this.state = {
|
|
38726
|
+
symbol,
|
|
38727
|
+
provider: parseSymbol(symbol ?? "").provider ?? void 0,
|
|
38728
|
+
timeframe: seed.timeframe,
|
|
38729
|
+
priceStyle: seed.priceStyle,
|
|
38730
|
+
bars: seed.bars,
|
|
38731
|
+
session: normalizeSession(seed.session)
|
|
38732
|
+
};
|
|
37900
38733
|
const doc = gridHost.ownerDocument;
|
|
37901
38734
|
this.host = doc.createElement("div");
|
|
37902
38735
|
this.host.className = "vela-cell";
|
|
@@ -37984,6 +38817,11 @@ var ChartCell = class {
|
|
|
37984
38817
|
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
37985
38818
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
37986
38819
|
this.statusline?.onChart(this.inner);
|
|
38820
|
+
this.statusline?.attachMenu({
|
|
38821
|
+
setPart: (part, visible) => this.setStatuslinePart(part, visible),
|
|
38822
|
+
chartVisible: () => this.inner?.renderer.get("candleVisible") !== false,
|
|
38823
|
+
setChartVisible: (visible) => this.inner?.renderer.set("candleVisible", visible)
|
|
38824
|
+
});
|
|
37987
38825
|
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
37988
38826
|
void this.inner.data.ready().then(() => {
|
|
37989
38827
|
if (this.inner && this.state.symbol) {
|
|
@@ -38191,7 +39029,7 @@ var ChartCell = class {
|
|
|
38191
39029
|
kind: "select",
|
|
38192
39030
|
label: "Bars to fetch",
|
|
38193
39031
|
id: "bars",
|
|
38194
|
-
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
39032
|
+
options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
|
|
38195
39033
|
get: () => String(this.state.bars ?? 1e3),
|
|
38196
39034
|
set: (v) => {
|
|
38197
39035
|
this.state.bars = Number(v);
|
|
@@ -38223,10 +39061,34 @@ var ChartCell = class {
|
|
|
38223
39061
|
id: "status-line",
|
|
38224
39062
|
rows: [
|
|
38225
39063
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
38226
|
-
{
|
|
38227
|
-
|
|
38228
|
-
|
|
38229
|
-
|
|
39064
|
+
{
|
|
39065
|
+
kind: "toggle",
|
|
39066
|
+
label: "Symbol name",
|
|
39067
|
+
id: "name",
|
|
39068
|
+
get: () => sl.partVisible("name"),
|
|
39069
|
+
set: (v) => this.setStatuslinePart("name", v)
|
|
39070
|
+
},
|
|
39071
|
+
{
|
|
39072
|
+
kind: "toggle",
|
|
39073
|
+
label: "Market status",
|
|
39074
|
+
id: "market",
|
|
39075
|
+
get: () => sl.partVisible("market"),
|
|
39076
|
+
set: (v) => this.setStatuslinePart("market", v)
|
|
39077
|
+
},
|
|
39078
|
+
{
|
|
39079
|
+
kind: "toggle",
|
|
39080
|
+
label: "OHLC values",
|
|
39081
|
+
id: "ohlc",
|
|
39082
|
+
get: () => sl.partVisible("ohlc"),
|
|
39083
|
+
set: (v) => this.setStatuslinePart("ohlc", v)
|
|
39084
|
+
},
|
|
39085
|
+
{
|
|
39086
|
+
kind: "toggle",
|
|
39087
|
+
label: "Bar change values",
|
|
39088
|
+
id: "change",
|
|
39089
|
+
get: () => sl.partVisible("change"),
|
|
39090
|
+
set: (v) => this.setStatuslinePart("change", v)
|
|
39091
|
+
},
|
|
38230
39092
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
38231
39093
|
{
|
|
38232
39094
|
kind: "toggle",
|
|
@@ -38279,7 +39141,7 @@ var ChartCell = class {
|
|
|
38279
39141
|
statusPrefs() {
|
|
38280
39142
|
const sl = this.statusline;
|
|
38281
39143
|
return {
|
|
38282
|
-
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
39144
|
+
parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
38283
39145
|
indicatorTitles: this.indicatorTitlesOn,
|
|
38284
39146
|
indicatorValues: this.indicatorValuesOn
|
|
38285
39147
|
};
|
|
@@ -38453,7 +39315,8 @@ var ChartCell = class {
|
|
|
38453
39315
|
if (this.manifest.length > 0) {
|
|
38454
39316
|
for (const item of led.manifest) {
|
|
38455
39317
|
const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
|
|
38456
|
-
if (entry)
|
|
39318
|
+
if (entry)
|
|
39319
|
+
this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
|
|
38457
39320
|
}
|
|
38458
39321
|
this.pendingManifestNames = null;
|
|
38459
39322
|
} else if (!this.deps.manifestSettled()) {
|
|
@@ -38502,7 +39365,10 @@ var ChartCell = class {
|
|
|
38502
39365
|
* a persistence handler's `restore` runs silently, a user-driven call records.
|
|
38503
39366
|
*/
|
|
38504
39367
|
addExternalIndicator(entry) {
|
|
38505
|
-
this.addManifestInstance(
|
|
39368
|
+
this.addManifestInstance(
|
|
39369
|
+
{ ...entry, enabled: true },
|
|
39370
|
+
{ external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} }
|
|
39371
|
+
);
|
|
38506
39372
|
}
|
|
38507
39373
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
38508
39374
|
addManifestInstance(entry, opts = {}) {
|
|
@@ -39014,7 +39880,7 @@ var SplitterLayer = class {
|
|
|
39014
39880
|
};
|
|
39015
39881
|
|
|
39016
39882
|
// src/workspace/VelaWorkspace.ts
|
|
39017
|
-
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
39883
|
+
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
|
|
39018
39884
|
var GAP_PX = 2;
|
|
39019
39885
|
var POOL_CAP = 16;
|
|
39020
39886
|
var TIME_AXIS_H4 = 22;
|