@luxalgo/vela 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -46
- package/dist/{DataProvider-CWmp31dA.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
- package/dist/{DataProvider-l_eLMly_.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
- package/dist/{chunk-4KZVZ7QQ.js → chunk-F4M24ANM.js} +130 -1
- package/dist/{chunk-NMTQXNT4.js → chunk-IZV3CW5N.js} +245 -43
- package/dist/{chunk-FFOP37FQ.js → chunk-KV7FDWSL.js} +748 -70
- package/dist/{contributions-BCz6Dr6a.d.ts → contributions-37nni40G.d.ts} +20 -4
- package/dist/{contributions-D9vTzm5p.d.cts → contributions-lPojhTxI.d.cts} +20 -4
- package/dist/index.cjs +866 -58
- package/dist/index.d.cts +12 -6
- package/dist/index.d.ts +12 -6
- package/dist/index.js +3 -3
- package/dist/{options-DSqHsQyN.d.cts → options-CYS5Wmlx.d.cts} +78 -2
- package/dist/{options-DSqHsQyN.d.ts → options-CYS5Wmlx.d.ts} +78 -2
- package/dist/{plugin-Bt8hLR8Z.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
- package/dist/{plugin-CH7pfMrE.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
- package/dist/plugin.cjs +119 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-5K7y4Ya2.d.ts → statusline-model-CiJm-riV.d.cts} +17 -87
- package/dist/{statusline-TYLQmoeh.d.cts → statusline-model-D-q_OOx9.d.ts} +17 -87
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +866 -58
- package/dist/vela.global.min.js +52 -52
- package/dist/widget.cjs +1109 -100
- package/dist/widget.d.cts +118 -7
- package/dist/widget.d.ts +118 -7
- package/dist/widget.js +4 -4
- package/dist/workspace.cjs +1109 -100
- package/dist/workspace.d.cts +20 -11
- package/dist/workspace.d.ts +20 -11
- package/dist/workspace.js +3 -3
- package/package.json +1 -1
- /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
package/dist/widget.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
|
}
|
|
@@ -11241,6 +11243,111 @@ function roundFloat(n) {
|
|
|
11241
11243
|
return Math.round(n * 1e8) / 1e8;
|
|
11242
11244
|
}
|
|
11243
11245
|
|
|
11246
|
+
// src/core/drawings/types/Magnifier.ts
|
|
11247
|
+
var MAGNIFIER_TIMEFRAME_OPTIONS = [
|
|
11248
|
+
{ value: "auto", label: "Auto", ms: 0 },
|
|
11249
|
+
{ value: "1", label: "1m", ms: 6e4 },
|
|
11250
|
+
{ value: "5", label: "5m", ms: 3e5 },
|
|
11251
|
+
{ value: "15", label: "15m", ms: 9e5 },
|
|
11252
|
+
{ value: "30", label: "30m", ms: 18e5 },
|
|
11253
|
+
{ value: "60", label: "1h", ms: 36e5 },
|
|
11254
|
+
{ value: "240", label: "4h", ms: 144e5 },
|
|
11255
|
+
{ value: "D", label: "1D", ms: 864e5 }
|
|
11256
|
+
];
|
|
11257
|
+
function magnifierTimeframeLabel(value) {
|
|
11258
|
+
const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
|
|
11259
|
+
if (opt) return opt.label;
|
|
11260
|
+
const n = Number(value);
|
|
11261
|
+
if (Number.isFinite(n) && n > 0) {
|
|
11262
|
+
if (n % 1440 === 0) return `${n / 1440}D`;
|
|
11263
|
+
if (n % 60 === 0) return `${n / 60}h`;
|
|
11264
|
+
return `${n}m`;
|
|
11265
|
+
}
|
|
11266
|
+
return value;
|
|
11267
|
+
}
|
|
11268
|
+
function defaultMagnifierStyle() {
|
|
11269
|
+
return { timeframe: "auto", upColor: "", downColor: "" };
|
|
11270
|
+
}
|
|
11271
|
+
var Magnifier = class extends Drawing {
|
|
11272
|
+
constructor(init) {
|
|
11273
|
+
super(init);
|
|
11274
|
+
this.type = "magnifier";
|
|
11275
|
+
/** Pixel rect of the timeframe chip as painted last frame, caret included — the chip is
|
|
11276
|
+
* an interactive dropdown trigger, so the interaction layer needs the exact rect the
|
|
11277
|
+
* painter measured. Renderer-transient: never serialized, null while unpainted. */
|
|
11278
|
+
this.chipRect = null;
|
|
11279
|
+
if (!this.magnifier) this.magnifier = defaultMagnifierStyle();
|
|
11280
|
+
}
|
|
11281
|
+
anchorSchema() {
|
|
11282
|
+
return { min: 2, max: 2, slots: [{ role: "c1", free: "both" }, { role: "c2", free: "both" }] };
|
|
11283
|
+
}
|
|
11284
|
+
placementMode() {
|
|
11285
|
+
return "drag";
|
|
11286
|
+
}
|
|
11287
|
+
/** The pixel rectangle between the two corner anchors (painter + hit-test share it). */
|
|
11288
|
+
rect(proj) {
|
|
11289
|
+
const a = this.anchors[0];
|
|
11290
|
+
const b = this.anchors[1];
|
|
11291
|
+
if (!a || !b) return null;
|
|
11292
|
+
const ya = proj.yOf(a.price, this.paneId);
|
|
11293
|
+
const yb = proj.yOf(b.price, this.paneId);
|
|
11294
|
+
if (ya == null || yb == null) return null;
|
|
11295
|
+
return { x1: proj.xOf(a.time), y1: ya, x2: proj.xOf(b.time), y2: yb };
|
|
11296
|
+
}
|
|
11297
|
+
hitTest(px, py, proj, tol) {
|
|
11298
|
+
const r = this.rect(proj);
|
|
11299
|
+
if (!r) return false;
|
|
11300
|
+
if (pointInBox(px, py, r.x1, r.y1, r.x2, r.y2)) return true;
|
|
11301
|
+
const edges = [
|
|
11302
|
+
[r.x1, r.y1, r.x2, r.y1],
|
|
11303
|
+
[r.x2, r.y1, r.x2, r.y2],
|
|
11304
|
+
[r.x2, r.y2, r.x1, r.y2],
|
|
11305
|
+
[r.x1, r.y2, r.x1, r.y1]
|
|
11306
|
+
];
|
|
11307
|
+
return edges.some((e) => distToSegment(px, py, e[0], e[1], e[2], e[3]) <= tol);
|
|
11308
|
+
}
|
|
11309
|
+
handlePoints(proj) {
|
|
11310
|
+
const r = this.rect(proj);
|
|
11311
|
+
return r ? [[r.x1, r.y1], [r.x2, r.y2]] : [];
|
|
11312
|
+
}
|
|
11313
|
+
hitHandle(px, py, proj, tol) {
|
|
11314
|
+
return handleAt(px, py, this.handlePoints(proj), tol + 3);
|
|
11315
|
+
}
|
|
11316
|
+
bounds(proj) {
|
|
11317
|
+
const r = this.rect(proj);
|
|
11318
|
+
if (!r) return null;
|
|
11319
|
+
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) };
|
|
11320
|
+
}
|
|
11321
|
+
priceRange() {
|
|
11322
|
+
const a = this.anchors[0];
|
|
11323
|
+
const b = this.anchors[1];
|
|
11324
|
+
if (!a || !b) return null;
|
|
11325
|
+
return { min: Math.min(a.price, b.price), max: Math.max(a.price, b.price) };
|
|
11326
|
+
}
|
|
11327
|
+
schema() {
|
|
11328
|
+
return {
|
|
11329
|
+
fields: [
|
|
11330
|
+
{
|
|
11331
|
+
path: "magnifier.timeframe",
|
|
11332
|
+
label: "Timeframe",
|
|
11333
|
+
kind: "select",
|
|
11334
|
+
options: MAGNIFIER_TIMEFRAME_OPTIONS,
|
|
11335
|
+
group: "behavior"
|
|
11336
|
+
},
|
|
11337
|
+
...LINE_FIELDS.map((f) => ({ ...f, label: f.label.replace("Line", "Border") })),
|
|
11338
|
+
{ path: "magnifier.upColor", label: "Up candles", kind: "color", group: "fill" },
|
|
11339
|
+
{ path: "magnifier.downColor", label: "Down candles", kind: "color", group: "fill" }
|
|
11340
|
+
]
|
|
11341
|
+
};
|
|
11342
|
+
}
|
|
11343
|
+
writeProps() {
|
|
11344
|
+
return { ...this.magnifier };
|
|
11345
|
+
}
|
|
11346
|
+
readProps(props) {
|
|
11347
|
+
this.magnifier = { ...defaultMagnifierStyle(), ...props };
|
|
11348
|
+
}
|
|
11349
|
+
};
|
|
11350
|
+
|
|
11244
11351
|
// src/core/drawings/registry.ts
|
|
11245
11352
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
11246
11353
|
function registerDrawingType(meta) {
|
|
@@ -11900,6 +12007,22 @@ registerDrawingType({
|
|
|
11900
12007
|
defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
|
|
11901
12008
|
create: (init) => new PositionTool(init)
|
|
11902
12009
|
});
|
|
12010
|
+
var MAGNIFIER_ICON = svg24(
|
|
12011
|
+
'<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"/>'
|
|
12012
|
+
);
|
|
12013
|
+
registerDrawingType({
|
|
12014
|
+
type: "magnifier",
|
|
12015
|
+
group: "measure",
|
|
12016
|
+
label: "Magnifier",
|
|
12017
|
+
icon: MAGNIFIER_ICON,
|
|
12018
|
+
// An empty border color means the THEME's contrast ink (white on dark, black on
|
|
12019
|
+
// light), resolved at paint time so it follows theme switches; a user pick wins.
|
|
12020
|
+
defaultStyle: { lineColor: "", lineWidth: 1, lineStyle: "solid" },
|
|
12021
|
+
coversSeries: true,
|
|
12022
|
+
// the inset's backdrop must sit over the base candles it replaces
|
|
12023
|
+
placementHint: "Drag an area on the chart to view it at a lower timeframe",
|
|
12024
|
+
create: (init) => new Magnifier(init)
|
|
12025
|
+
});
|
|
11903
12026
|
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"/>');
|
|
11904
12027
|
registerDrawingType({
|
|
11905
12028
|
type: "anchoredvwap",
|
|
@@ -16133,7 +16256,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
|
|
|
16133
16256
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
16134
16257
|
var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
|
|
16135
16258
|
var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
|
|
16136
|
-
var MEASUREMENT_TYPES = ["position", "datepricerange"];
|
|
16259
|
+
var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
|
|
16137
16260
|
var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
|
|
16138
16261
|
var BRUSH_TYPES = ["freehand", "highlighter"];
|
|
16139
16262
|
var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
|
|
@@ -16269,7 +16392,7 @@ function buildToolbar(option) {
|
|
|
16269
16392
|
|
|
16270
16393
|
// src/core/drawings/DrawingController.ts
|
|
16271
16394
|
var DrawingController = class {
|
|
16272
|
-
constructor(renderer, events, option) {
|
|
16395
|
+
constructor(renderer, events, option, seriesGateway) {
|
|
16273
16396
|
this.events = events;
|
|
16274
16397
|
this.store = new DrawingStore();
|
|
16275
16398
|
this.history = new DrawingHistory();
|
|
@@ -16295,6 +16418,7 @@ var DrawingController = class {
|
|
|
16295
16418
|
const { definition, visible } = buildToolbar(option);
|
|
16296
16419
|
this.port.setToolbar(definition);
|
|
16297
16420
|
this.port.showToolbar(visible);
|
|
16421
|
+
if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
|
|
16298
16422
|
this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
|
|
16299
16423
|
this.subs.push(this.store.onChange(() => this.sync()));
|
|
16300
16424
|
}
|
|
@@ -16400,7 +16524,7 @@ var DrawingController = class {
|
|
|
16400
16524
|
style,
|
|
16401
16525
|
text: init.text,
|
|
16402
16526
|
props: init.props,
|
|
16403
|
-
zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
|
|
16527
|
+
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
16404
16528
|
});
|
|
16405
16529
|
if (!d) return null;
|
|
16406
16530
|
this.history.record(this.store.serialize());
|
|
@@ -16475,10 +16599,14 @@ var DrawingController = class {
|
|
|
16475
16599
|
* (falling back to just under the pane's top series where there is no price — a study
|
|
16476
16600
|
* pane). Half a key down never ties a series; drawings tying each other paint in insertion
|
|
16477
16601
|
* order, so consecutive new drawings still stack newest-in-front. Undefined without a
|
|
16478
|
-
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
16479
|
-
|
|
16602
|
+
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
16603
|
+
* A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
|
|
16604
|
+
* above the whole stack — under the candles its content would be buried. */
|
|
16605
|
+
startZ(type, paneId) {
|
|
16480
16606
|
const range = this.port?.stackRange?.(paneId);
|
|
16481
|
-
|
|
16607
|
+
if (!range) return void 0;
|
|
16608
|
+
if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
|
|
16609
|
+
return (range.price ?? range.front) - 0.5;
|
|
16482
16610
|
}
|
|
16483
16611
|
/** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
|
|
16484
16612
|
* `additive` toggles membership (matching shift-click) instead of replacing. */
|
|
@@ -16623,7 +16751,7 @@ var DrawingController = class {
|
|
|
16623
16751
|
const style = last ? { ...i.doc.style, ...last } : i.doc.style;
|
|
16624
16752
|
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
16625
16753
|
if (!d) return;
|
|
16626
|
-
if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
|
|
16754
|
+
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
16627
16755
|
this.history.record(before);
|
|
16628
16756
|
this.store.add(d);
|
|
16629
16757
|
this.captureStyle(d.id);
|
|
@@ -16880,11 +17008,12 @@ async function resolveIndicators(config, fetchImpl = fetch) {
|
|
|
16880
17008
|
}
|
|
16881
17009
|
return out;
|
|
16882
17010
|
}
|
|
17011
|
+
var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
|
|
16883
17012
|
function indicatorLedger(i) {
|
|
16884
17013
|
const natives = [...i.present];
|
|
16885
17014
|
if (i.volumePending && !natives.includes("volume")) natives.push("volume");
|
|
16886
17015
|
return {
|
|
16887
|
-
manifest: i.manifestSettled ? [...i.
|
|
17016
|
+
manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
|
|
16888
17017
|
natives
|
|
16889
17018
|
};
|
|
16890
17019
|
}
|
|
@@ -17818,7 +17947,17 @@ function sanitizeCell(raw) {
|
|
|
17818
17947
|
if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
|
|
17819
17948
|
const ind = c.indicators;
|
|
17820
17949
|
if (ind != null && typeof ind === "object") {
|
|
17821
|
-
const manifest = Array.isArray(ind.manifest) ? ind.manifest.
|
|
17950
|
+
const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
|
|
17951
|
+
if (typeof n === "string") return [n];
|
|
17952
|
+
if (n != null && typeof n === "object" && typeof n.name === "string") {
|
|
17953
|
+
const e = n;
|
|
17954
|
+
const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
17955
|
+
const inputs = bag(e.inputs);
|
|
17956
|
+
const props = bag(e.props);
|
|
17957
|
+
return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
|
|
17958
|
+
}
|
|
17959
|
+
return [];
|
|
17960
|
+
}) : [];
|
|
17822
17961
|
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
17823
17962
|
out.indicators = { manifest, natives };
|
|
17824
17963
|
}
|
|
@@ -18003,6 +18142,12 @@ var IndicatorHandleImpl = class {
|
|
|
18003
18142
|
get visible() {
|
|
18004
18143
|
return this.visibleState;
|
|
18005
18144
|
}
|
|
18145
|
+
inputValues() {
|
|
18146
|
+
return this.controller.inputValuesOf(this.id);
|
|
18147
|
+
}
|
|
18148
|
+
propValues() {
|
|
18149
|
+
return this.controller.propValuesOf(this.id);
|
|
18150
|
+
}
|
|
18006
18151
|
setInput(key, value) {
|
|
18007
18152
|
this.controller.applyInputs(this.id, { [key]: value });
|
|
18008
18153
|
}
|
|
@@ -18124,6 +18269,180 @@ function presetToRange(preset, bars) {
|
|
|
18124
18269
|
return { from: Math.max(first, from), to: last };
|
|
18125
18270
|
}
|
|
18126
18271
|
|
|
18272
|
+
// src/core/engine/DrawingSeriesService.ts
|
|
18273
|
+
var MAX_BARS = 5e3;
|
|
18274
|
+
var PAD_FRAC = 0.25;
|
|
18275
|
+
var MAX_ENTRIES = 16;
|
|
18276
|
+
var RETRY_MS = 15e3;
|
|
18277
|
+
var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
|
|
18278
|
+
var DrawingSeriesService = class {
|
|
18279
|
+
constructor(deps) {
|
|
18280
|
+
this.deps = deps;
|
|
18281
|
+
/** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
|
|
18282
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
18283
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
18284
|
+
}
|
|
18285
|
+
seriesInRange(timeframe, from, to) {
|
|
18286
|
+
if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
|
|
18287
|
+
const resolved = this.resolveTimeframe(timeframe);
|
|
18288
|
+
if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
|
|
18289
|
+
const barMs = timeframeToMs(resolved);
|
|
18290
|
+
const lo = Math.min(from, to);
|
|
18291
|
+
const hi = Math.max(from, to);
|
|
18292
|
+
if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
|
|
18293
|
+
if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
|
|
18294
|
+
const key = `${this.deps.marketKey()}|${resolved}`;
|
|
18295
|
+
const entries = this.cache.get(key) ?? [];
|
|
18296
|
+
const covering = entries.find((e) => e.from <= lo && e.to >= hi);
|
|
18297
|
+
if (covering) {
|
|
18298
|
+
if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
|
|
18299
|
+
if (covering.failedAt > 0) {
|
|
18300
|
+
if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
|
|
18301
|
+
entries.splice(entries.indexOf(covering), 1);
|
|
18302
|
+
} else {
|
|
18303
|
+
this.maybeRefresh(key, covering, resolved, barMs, hi);
|
|
18304
|
+
return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
|
|
18305
|
+
}
|
|
18306
|
+
}
|
|
18307
|
+
this.fetchWindow(key, entries, resolved, lo, hi);
|
|
18308
|
+
return this.loading(entries, resolved, barMs, lo, hi);
|
|
18309
|
+
}
|
|
18310
|
+
onUpdate(listener) {
|
|
18311
|
+
this.listeners.add(listener);
|
|
18312
|
+
return () => this.listeners.delete(listener);
|
|
18313
|
+
}
|
|
18314
|
+
// ── internals ──
|
|
18315
|
+
/** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
|
|
18316
|
+
* step still below it); an explicit timeframe passes only when strictly finer. Failures
|
|
18317
|
+
* distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
|
|
18318
|
+
* at the finest offered step) so the consumer can word its notice honestly. */
|
|
18319
|
+
resolveTimeframe(timeframe) {
|
|
18320
|
+
const chartMs = timeframeToMs(this.deps.chartTimeframe());
|
|
18321
|
+
const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
|
|
18322
|
+
if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
|
|
18323
|
+
const tf = timeframe.trim() || "auto";
|
|
18324
|
+
if (tf === "auto") {
|
|
18325
|
+
for (const step of AUTO_STEPS) {
|
|
18326
|
+
if (timeframeToMs(step) <= chartMs / 4) return step;
|
|
18327
|
+
}
|
|
18328
|
+
return finest;
|
|
18329
|
+
}
|
|
18330
|
+
return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
|
|
18331
|
+
}
|
|
18332
|
+
/** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
|
|
18333
|
+
* windows — a widened window keeps painting what it already has while it fetches. */
|
|
18334
|
+
loading(entries, timeframe, barMs, lo, hi) {
|
|
18335
|
+
const partial = /* @__PURE__ */ new Map();
|
|
18336
|
+
for (const e of entries) {
|
|
18337
|
+
if (e.pending || e.failedAt > 0) continue;
|
|
18338
|
+
if (e.to < lo || e.from > hi) continue;
|
|
18339
|
+
for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
|
|
18340
|
+
}
|
|
18341
|
+
if (partial.size === 0) return { state: "loading", timeframe, barMs };
|
|
18342
|
+
const bars = [...partial.values()].sort((a, b) => a.time - b.time);
|
|
18343
|
+
return { state: "loading", timeframe, barMs, bars };
|
|
18344
|
+
}
|
|
18345
|
+
/** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
|
|
18346
|
+
* defers this one (a corner drag repaints per pointer move — kicking a window per
|
|
18347
|
+
* frame would spam the provider); when it lands, the next paint re-evaluates. */
|
|
18348
|
+
fetchWindow(key, entries, timeframe, lo, hi) {
|
|
18349
|
+
if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
|
|
18350
|
+
const pad = (hi - lo) * PAD_FRAC;
|
|
18351
|
+
const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
|
|
18352
|
+
entries.push(entry);
|
|
18353
|
+
this.cache.set(key, entries);
|
|
18354
|
+
this.evict();
|
|
18355
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
18356
|
+
entry.bars = bars;
|
|
18357
|
+
entry.fetchedAt = Date.now();
|
|
18358
|
+
entry.pending = false;
|
|
18359
|
+
this.absorbOverlaps(key, entry);
|
|
18360
|
+
this.fire();
|
|
18361
|
+
}).catch(() => {
|
|
18362
|
+
entry.pending = false;
|
|
18363
|
+
entry.failedAt = Date.now();
|
|
18364
|
+
this.fire();
|
|
18365
|
+
});
|
|
18366
|
+
}
|
|
18367
|
+
/** A window whose right edge reaches the newest fetched bar refreshes at most once per
|
|
18368
|
+
* bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
|
|
18369
|
+
maybeRefresh(key, entry, timeframe, barMs, hi) {
|
|
18370
|
+
if (entry.pending) return;
|
|
18371
|
+
const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
|
|
18372
|
+
if (hi < lastBar) return;
|
|
18373
|
+
if (Date.now() - entry.fetchedAt < barMs) return;
|
|
18374
|
+
entry.pending = true;
|
|
18375
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
18376
|
+
entry.bars = bars;
|
|
18377
|
+
entry.fetchedAt = Date.now();
|
|
18378
|
+
entry.pending = false;
|
|
18379
|
+
this.fire();
|
|
18380
|
+
}).catch(() => {
|
|
18381
|
+
entry.pending = false;
|
|
18382
|
+
entry.fetchedAt = Date.now();
|
|
18383
|
+
});
|
|
18384
|
+
}
|
|
18385
|
+
/** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
|
|
18386
|
+
* converges instead of accumulating slivers. */
|
|
18387
|
+
absorbOverlaps(key, entry) {
|
|
18388
|
+
const entries = this.cache.get(key);
|
|
18389
|
+
if (!entries) return;
|
|
18390
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
18391
|
+
const other = entries[i];
|
|
18392
|
+
if (other === entry || other.pending || other.failedAt > 0) continue;
|
|
18393
|
+
if (other.to < entry.from || other.from > entry.to) continue;
|
|
18394
|
+
const byTime = /* @__PURE__ */ new Map();
|
|
18395
|
+
for (const b of other.bars) byTime.set(b.time, b);
|
|
18396
|
+
for (const b of entry.bars) byTime.set(b.time, b);
|
|
18397
|
+
entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
18398
|
+
entry.from = Math.min(entry.from, other.from);
|
|
18399
|
+
entry.to = Math.max(entry.to, other.to);
|
|
18400
|
+
entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
|
|
18401
|
+
entries.splice(i, 1);
|
|
18402
|
+
}
|
|
18403
|
+
}
|
|
18404
|
+
/** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
|
|
18405
|
+
evict() {
|
|
18406
|
+
let total = 0;
|
|
18407
|
+
for (const entries of this.cache.values()) total += entries.length;
|
|
18408
|
+
while (total > MAX_ENTRIES) {
|
|
18409
|
+
let oldestKey = null;
|
|
18410
|
+
let oldestIdx = -1;
|
|
18411
|
+
let oldestAt = Infinity;
|
|
18412
|
+
for (const [key, entries2] of this.cache) {
|
|
18413
|
+
for (let i = 0; i < entries2.length; i += 1) {
|
|
18414
|
+
const e = entries2[i];
|
|
18415
|
+
if (e.pending) continue;
|
|
18416
|
+
const at = e.fetchedAt || e.failedAt;
|
|
18417
|
+
if (at < oldestAt) {
|
|
18418
|
+
oldestKey = key;
|
|
18419
|
+
oldestIdx = i;
|
|
18420
|
+
oldestAt = at;
|
|
18421
|
+
}
|
|
18422
|
+
}
|
|
18423
|
+
}
|
|
18424
|
+
if (oldestKey == null) return;
|
|
18425
|
+
const entries = this.cache.get(oldestKey);
|
|
18426
|
+
entries.splice(oldestIdx, 1);
|
|
18427
|
+
if (entries.length === 0) this.cache.delete(oldestKey);
|
|
18428
|
+
total -= 1;
|
|
18429
|
+
}
|
|
18430
|
+
}
|
|
18431
|
+
/** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
|
|
18432
|
+
slice(bars, lo, hi) {
|
|
18433
|
+
const out = [];
|
|
18434
|
+
for (const b of bars) {
|
|
18435
|
+
if (b.time < lo) continue;
|
|
18436
|
+
if (b.time > hi) break;
|
|
18437
|
+
out.push(b);
|
|
18438
|
+
}
|
|
18439
|
+
return out;
|
|
18440
|
+
}
|
|
18441
|
+
fire() {
|
|
18442
|
+
for (const l of [...this.listeners]) l();
|
|
18443
|
+
}
|
|
18444
|
+
};
|
|
18445
|
+
|
|
18127
18446
|
// src/core/price-styles/BarTransform.ts
|
|
18128
18447
|
function barTransformFor(style) {
|
|
18129
18448
|
return chartType(style)?.barTransform ?? null;
|
|
@@ -18142,6 +18461,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
18142
18461
|
var PREVIEW_BARS = 300;
|
|
18143
18462
|
var SINGLE_LOAD_BARS = 5e3;
|
|
18144
18463
|
var CHUNK_BARS = 1e4;
|
|
18464
|
+
var FIRST_PAINT_BARS = 100;
|
|
18145
18465
|
var GAP_FACTOR = 1.5;
|
|
18146
18466
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
18147
18467
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -18259,7 +18579,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18259
18579
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
18260
18580
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
18261
18581
|
this.barTransform = barTransformFor(initialStyle);
|
|
18262
|
-
|
|
18582
|
+
const drawingSeries = new DrawingSeriesService({
|
|
18583
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
18584
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
18585
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
18586
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
18587
|
+
});
|
|
18588
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
18263
18589
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
18264
18590
|
this.endLoad();
|
|
18265
18591
|
this.events.emit("data:unresolved", info);
|
|
@@ -18411,6 +18737,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18411
18737
|
let painted = false;
|
|
18412
18738
|
const paint = (bars, final) => {
|
|
18413
18739
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18740
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
18414
18741
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18415
18742
|
if (!painted && bars.length > 0) {
|
|
18416
18743
|
painted = true;
|
|
@@ -19041,6 +19368,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19041
19368
|
record.pendingCause = "inputs";
|
|
19042
19369
|
if (record.session) record.session.update(record.inputValues);
|
|
19043
19370
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
19371
|
+
this.events.emit("indicator:inputs", { id });
|
|
19372
|
+
}
|
|
19373
|
+
/** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
|
|
19374
|
+
inputValuesOf(id) {
|
|
19375
|
+
return { ...this.registry.get(id)?.inputValues };
|
|
19376
|
+
}
|
|
19377
|
+
/** IndicatorController: the CURRENT declaration-prop overrides. */
|
|
19378
|
+
propValuesOf(id) {
|
|
19379
|
+
return { ...this.registry.get(id)?.propValues };
|
|
19044
19380
|
}
|
|
19045
19381
|
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
19046
19382
|
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
@@ -19054,6 +19390,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19054
19390
|
if (!record.hidden) this.setLoading(record, true);
|
|
19055
19391
|
record.pendingCause = "inputs";
|
|
19056
19392
|
record.session.update(record.inputValues, record.propValues);
|
|
19393
|
+
this.events.emit("indicator:inputs", { id });
|
|
19057
19394
|
}
|
|
19058
19395
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
19059
19396
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
@@ -19241,8 +19578,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19241
19578
|
onModel: (model) => {
|
|
19242
19579
|
const first = !record.announced;
|
|
19243
19580
|
const cause = record.pendingCause ?? "history";
|
|
19581
|
+
if (!this.applyModel(id, model)) return;
|
|
19244
19582
|
record.pendingCause = void 0;
|
|
19245
|
-
this.applyModel(id, model);
|
|
19246
19583
|
this.emitContextChanged(id);
|
|
19247
19584
|
this.emitScriptRun(id, cause, first);
|
|
19248
19585
|
},
|
|
@@ -19535,10 +19872,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19535
19872
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
19536
19873
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
19537
19874
|
* (live tick / viewport re-run) value-patches.
|
|
19875
|
+
*
|
|
19876
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
19877
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
19878
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
19879
|
+
* stay unchanged.
|
|
19538
19880
|
*/
|
|
19539
19881
|
applyModel(id, model) {
|
|
19540
19882
|
const record = this.registry.get(id);
|
|
19541
|
-
if (!record || record.hidden) return;
|
|
19883
|
+
if (!record || record.hidden) return true;
|
|
19884
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
19542
19885
|
const handle = this.handles.get(id);
|
|
19543
19886
|
if (!record.renderHandle) {
|
|
19544
19887
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -19548,7 +19891,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19548
19891
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19549
19892
|
record.pendingStructural = false;
|
|
19550
19893
|
this.announce(record, handle);
|
|
19551
|
-
return;
|
|
19894
|
+
return true;
|
|
19552
19895
|
}
|
|
19553
19896
|
let paneId = record.model?.paneId ?? "price";
|
|
19554
19897
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -19576,6 +19919,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19576
19919
|
}
|
|
19577
19920
|
if (record.loading) this.setLoading(record, false);
|
|
19578
19921
|
this.announce(record, handle);
|
|
19922
|
+
return true;
|
|
19923
|
+
}
|
|
19924
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
19925
|
+
static modelHasOutput(model) {
|
|
19926
|
+
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;
|
|
19579
19927
|
}
|
|
19580
19928
|
routePane(id, model, options) {
|
|
19581
19929
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -20275,6 +20623,14 @@ function inputVisible(when, values) {
|
|
|
20275
20623
|
const conds = Array.isArray(when) ? when : [when];
|
|
20276
20624
|
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
|
|
20277
20625
|
}
|
|
20626
|
+
function inputDeltas(schema, values) {
|
|
20627
|
+
const out = {};
|
|
20628
|
+
for (const s of schema) {
|
|
20629
|
+
const v = values[s.key];
|
|
20630
|
+
if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
|
|
20631
|
+
}
|
|
20632
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
20633
|
+
}
|
|
20278
20634
|
|
|
20279
20635
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
20280
20636
|
var PROPS_TAB = "Properties";
|
|
@@ -23862,6 +24218,7 @@ var DOUBLE_TAP_MS = 350;
|
|
|
23862
24218
|
var DOUBLE_TAP_SLOP = 30;
|
|
23863
24219
|
var TIME_SCALE_K = 4e-3;
|
|
23864
24220
|
var WHEEL_ZOOM_K = 4e-3;
|
|
24221
|
+
var WHEEL_PRICE_DRAG_PX = 0.25;
|
|
23865
24222
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
23866
24223
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
23867
24224
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -23992,7 +24349,7 @@ var InputController = class {
|
|
|
23992
24349
|
this.capture(e.pointerId);
|
|
23993
24350
|
return;
|
|
23994
24351
|
}
|
|
23995
|
-
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
|
|
24352
|
+
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
23996
24353
|
this.region = "drawing";
|
|
23997
24354
|
this.capture(e.pointerId);
|
|
23998
24355
|
return;
|
|
@@ -24106,7 +24463,7 @@ var InputController = class {
|
|
|
24106
24463
|
const wasTouch = e.pointerType === "touch";
|
|
24107
24464
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
24108
24465
|
if (this.dragging && this.region === "drawing") {
|
|
24109
|
-
this.deps.drawingsPointerUp?.(x, y);
|
|
24466
|
+
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
24110
24467
|
} else if (tapRelease && this.region === "data") {
|
|
24111
24468
|
this.deps.onClick(x, y);
|
|
24112
24469
|
} else if (this.dragging && this.region === "data") {
|
|
@@ -24127,7 +24484,7 @@ var InputController = class {
|
|
|
24127
24484
|
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
24128
24485
|
this.cancelLongPress();
|
|
24129
24486
|
if (!this.dragging) return;
|
|
24130
|
-
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
24487
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
24131
24488
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
24132
24489
|
this.endGesture(e);
|
|
24133
24490
|
};
|
|
@@ -24143,6 +24500,12 @@ var InputController = class {
|
|
|
24143
24500
|
this.onWheel = (e) => {
|
|
24144
24501
|
e.preventDefault();
|
|
24145
24502
|
this.deps.drawingsClearTransient?.();
|
|
24503
|
+
const { x, y } = this.local(e);
|
|
24504
|
+
if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
|
|
24505
|
+
this.deps.beginPriceScale(x, y);
|
|
24506
|
+
this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
|
|
24507
|
+
return;
|
|
24508
|
+
}
|
|
24146
24509
|
const coords = this.deps.getCoords();
|
|
24147
24510
|
const vp = coords.getViewport();
|
|
24148
24511
|
const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
|
|
@@ -24150,9 +24513,8 @@ var InputController = class {
|
|
|
24150
24513
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
|
|
24151
24514
|
return;
|
|
24152
24515
|
}
|
|
24153
|
-
const cursorX = this.local(e).x;
|
|
24154
24516
|
const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
|
|
24155
|
-
const anchor = wheelZoomAnchor(coords,
|
|
24517
|
+
const anchor = wheelZoomAnchor(coords, x, rightEdge);
|
|
24156
24518
|
const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
|
|
24157
24519
|
this.deps.zoomTo(target, anchor.logical, anchor.x);
|
|
24158
24520
|
};
|
|
@@ -25634,6 +25996,15 @@ function drawingSetEmpty(s) {
|
|
|
25634
25996
|
function fontSizePx(size) {
|
|
25635
25997
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
25636
25998
|
}
|
|
25999
|
+
function lineCoversWindow(a, b, extend, lo, hi) {
|
|
26000
|
+
const minX = Math.min(a, b);
|
|
26001
|
+
const maxX = Math.max(a, b);
|
|
26002
|
+
if (a === b) return a >= lo && a <= hi;
|
|
26003
|
+
if (extend === "both") return true;
|
|
26004
|
+
if (extend === "left") return maxX >= lo;
|
|
26005
|
+
if (extend === "right") return minX <= hi;
|
|
26006
|
+
return maxX >= lo && minX <= hi;
|
|
26007
|
+
}
|
|
25637
26008
|
var DrawingSceneRenderer = class {
|
|
25638
26009
|
constructor(deps, set = EMPTY_DRAWING_SET) {
|
|
25639
26010
|
this.deps = deps;
|
|
@@ -25687,7 +26058,9 @@ var DrawingSceneRenderer = class {
|
|
|
25687
26058
|
const lo = Math.min(from, to);
|
|
25688
26059
|
const hi = Math.max(from, to);
|
|
25689
26060
|
const visible = (a, b, extend) => {
|
|
25690
|
-
if (extend
|
|
26061
|
+
if (extend === "both") return true;
|
|
26062
|
+
if (extend === "left") return Math.max(a, b) >= lo;
|
|
26063
|
+
if (extend === "right") return Math.min(a, b) <= hi;
|
|
25691
26064
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
25692
26065
|
};
|
|
25693
26066
|
let min = Infinity;
|
|
@@ -25698,7 +26071,7 @@ var DrawingSceneRenderer = class {
|
|
|
25698
26071
|
};
|
|
25699
26072
|
for (const ln of this.set.lines) {
|
|
25700
26073
|
if (ln.invisible) continue;
|
|
25701
|
-
if (!
|
|
26074
|
+
if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
|
|
25702
26075
|
fold(ln.y1);
|
|
25703
26076
|
fold(ln.y2);
|
|
25704
26077
|
}
|
|
@@ -28161,6 +28534,17 @@ var DrawingPainter = class {
|
|
|
28161
28534
|
constructor() {
|
|
28162
28535
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
28163
28536
|
this.targets = {};
|
|
28537
|
+
/** The chart's active series LOOK — style + resolved series colors — pushed by the
|
|
28538
|
+
* controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
|
|
28539
|
+
* area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
|
|
28540
|
+
* bars; unknown/custom styles fall back to candles), and the colors default to the main
|
|
28541
|
+
* series' own so the inset reads as a finer copy of the chart. */
|
|
28542
|
+
this.seriesLook = {
|
|
28543
|
+
style: "candles",
|
|
28544
|
+
upColor: BULLISH,
|
|
28545
|
+
downColor: BEARISH,
|
|
28546
|
+
lineColor: BULLISH
|
|
28547
|
+
};
|
|
28164
28548
|
}
|
|
28165
28549
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
28166
28550
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -28205,6 +28589,8 @@ var DrawingPainter = class {
|
|
|
28205
28589
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
28206
28590
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
28207
28591
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
28592
|
+
} else if (ghost instanceof Magnifier) {
|
|
28593
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
28208
28594
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
28209
28595
|
ctx.globalAlpha = 1;
|
|
28210
28596
|
}
|
|
@@ -28338,6 +28724,10 @@ var DrawingPainter = class {
|
|
|
28338
28724
|
this.paintLabel(ctx, d, proj, theme);
|
|
28339
28725
|
return;
|
|
28340
28726
|
}
|
|
28727
|
+
if (d instanceof Magnifier) {
|
|
28728
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
28729
|
+
return;
|
|
28730
|
+
}
|
|
28341
28731
|
if (d instanceof PatternDrawing) {
|
|
28342
28732
|
this.paintPattern(ctx, d, proj, theme);
|
|
28343
28733
|
return;
|
|
@@ -28830,6 +29220,216 @@ var DrawingPainter = class {
|
|
|
28830
29220
|
ctx.textBaseline = "alphabetic";
|
|
28831
29221
|
}
|
|
28832
29222
|
}
|
|
29223
|
+
/** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
|
|
29224
|
+
* no series read, so dragging the area open never kicks a fetch per cursor move. */
|
|
29225
|
+
paintMagnifierGhost(ctx, d, proj, theme) {
|
|
29226
|
+
const r = d.rect(proj);
|
|
29227
|
+
if (!r) return;
|
|
29228
|
+
ctx.save();
|
|
29229
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29230
|
+
ctx.lineWidth = 1;
|
|
29231
|
+
ctx.setLineDash([4, 4]);
|
|
29232
|
+
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));
|
|
29233
|
+
ctx.restore();
|
|
29234
|
+
}
|
|
29235
|
+
/** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
|
|
29236
|
+
* market at a finer timeframe — candles at their true time/price positions, clipped to
|
|
29237
|
+
* the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
|
|
29238
|
+
* `unavailable` states paint a short notice instead). The lower-timeframe candles shift
|
|
29239
|
+
* half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
|
|
29240
|
+
* centered on its open time — subdivides in place. */
|
|
29241
|
+
paintMagnifier(ctx, d, proj, theme) {
|
|
29242
|
+
const r = d.rect(proj);
|
|
29243
|
+
const a = d.anchors[0];
|
|
29244
|
+
const b = d.anchors[1];
|
|
29245
|
+
if (!r || !a || !b) return;
|
|
29246
|
+
const x0 = Math.min(r.x1, r.x2);
|
|
29247
|
+
const x1 = Math.max(r.x1, r.x2);
|
|
29248
|
+
const y0 = Math.min(r.y1, r.y2);
|
|
29249
|
+
const y1 = Math.max(r.y1, r.y2);
|
|
29250
|
+
const w = x1 - x0;
|
|
29251
|
+
const h = y1 - y0;
|
|
29252
|
+
ctx.save();
|
|
29253
|
+
ctx.globalAlpha = 1;
|
|
29254
|
+
ctx.fillStyle = theme.background;
|
|
29255
|
+
ctx.fillRect(x0, y0, w, h);
|
|
29256
|
+
ctx.restore();
|
|
29257
|
+
const from = Math.min(a.time, b.time);
|
|
29258
|
+
const to = Math.max(a.time, b.time);
|
|
29259
|
+
const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
|
|
29260
|
+
const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
|
|
29261
|
+
const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
|
|
29262
|
+
let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
|
|
29263
|
+
if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
|
|
29264
|
+
const look = this.seriesLook;
|
|
29265
|
+
const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
|
|
29266
|
+
if (transform) seriesBars = transform.full(seriesBars);
|
|
29267
|
+
const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
|
|
29268
|
+
const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
|
|
29269
|
+
ctx.save();
|
|
29270
|
+
ctx.beginPath();
|
|
29271
|
+
ctx.rect(x0, y0, w, h);
|
|
29272
|
+
ctx.clip();
|
|
29273
|
+
if (mode === "line" || mode === "area") {
|
|
29274
|
+
this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
|
|
29275
|
+
} else {
|
|
29276
|
+
this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
|
|
29277
|
+
}
|
|
29278
|
+
ctx.restore();
|
|
29279
|
+
} else if (res) {
|
|
29280
|
+
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";
|
|
29281
|
+
this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
|
|
29282
|
+
}
|
|
29283
|
+
ctx.save();
|
|
29284
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29285
|
+
ctx.lineWidth = d.style.lineWidth || 1;
|
|
29286
|
+
ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
|
|
29287
|
+
ctx.strokeRect(x0, y0, w, h);
|
|
29288
|
+
ctx.restore();
|
|
29289
|
+
if (w > 44) {
|
|
29290
|
+
const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
|
|
29291
|
+
const chipH = 17;
|
|
29292
|
+
const gap = 4;
|
|
29293
|
+
const pane = proj.paneRect?.(d.paneId);
|
|
29294
|
+
const paneBottom = pane ? pane.top + pane.height : proj.height;
|
|
29295
|
+
const below = y1 + gap + chipH <= paneBottom;
|
|
29296
|
+
const chipY = below ? y1 + gap : y1 - gap - chipH;
|
|
29297
|
+
ctx.save();
|
|
29298
|
+
ctx.font = `10px ${theme.fontFamily}`;
|
|
29299
|
+
const tw = ctx.measureText(label).width;
|
|
29300
|
+
const caretW = 11;
|
|
29301
|
+
const chipW = tw + 12 + caretW;
|
|
29302
|
+
roundRect(ctx, x0, chipY, chipW, chipH, 3);
|
|
29303
|
+
ctx.fillStyle = theme.background;
|
|
29304
|
+
ctx.fill();
|
|
29305
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29306
|
+
ctx.lineWidth = 1;
|
|
29307
|
+
ctx.setLineDash([]);
|
|
29308
|
+
ctx.stroke();
|
|
29309
|
+
ctx.fillStyle = theme.textColor;
|
|
29310
|
+
ctx.textAlign = "left";
|
|
29311
|
+
ctx.textBaseline = "middle";
|
|
29312
|
+
ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
|
|
29313
|
+
const cxr = x0 + 6 + tw + 5;
|
|
29314
|
+
const cyr = chipY + chipH / 2;
|
|
29315
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
|
|
29316
|
+
ctx.lineWidth = 1.2;
|
|
29317
|
+
ctx.beginPath();
|
|
29318
|
+
ctx.moveTo(cxr, cyr - 1.5);
|
|
29319
|
+
ctx.lineTo(cxr + 2.5, cyr + 1.5);
|
|
29320
|
+
ctx.lineTo(cxr + 5, cyr - 1.5);
|
|
29321
|
+
ctx.stroke();
|
|
29322
|
+
ctx.restore();
|
|
29323
|
+
d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
|
|
29324
|
+
} else {
|
|
29325
|
+
d.chipRect = null;
|
|
29326
|
+
}
|
|
29327
|
+
}
|
|
29328
|
+
/** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
|
|
29329
|
+
* by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
|
|
29330
|
+
* one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
|
|
29331
|
+
paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
|
|
29332
|
+
ctx.setLineDash([]);
|
|
29333
|
+
ctx.lineWidth = 1;
|
|
29334
|
+
for (const bar of bars) {
|
|
29335
|
+
const cx0 = proj.xOf(bar.time) - halfPitch;
|
|
29336
|
+
const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
|
|
29337
|
+
if (cx1 < x0 || cx0 > x1) continue;
|
|
29338
|
+
const yHigh = proj.yOf(bar.high, d.paneId);
|
|
29339
|
+
const yLow = proj.yOf(bar.low, d.paneId);
|
|
29340
|
+
const yOpen = proj.yOf(bar.open, d.paneId);
|
|
29341
|
+
const yClose = proj.yOf(bar.close, d.paneId);
|
|
29342
|
+
if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
|
|
29343
|
+
const color = bar.close >= bar.open ? upColor : downColor;
|
|
29344
|
+
const cellW = cx1 - cx0;
|
|
29345
|
+
const cx = (cx0 + cx1) / 2;
|
|
29346
|
+
ctx.strokeStyle = color;
|
|
29347
|
+
ctx.beginPath();
|
|
29348
|
+
ctx.moveTo(cx, yHigh);
|
|
29349
|
+
ctx.lineTo(cx, yLow);
|
|
29350
|
+
ctx.stroke();
|
|
29351
|
+
if (cellW < 3) continue;
|
|
29352
|
+
if (mode === "bars") {
|
|
29353
|
+
const tick = Math.max(1, cellW * 0.35);
|
|
29354
|
+
ctx.beginPath();
|
|
29355
|
+
ctx.moveTo(cx - tick, yOpen);
|
|
29356
|
+
ctx.lineTo(cx, yOpen);
|
|
29357
|
+
ctx.moveTo(cx, yClose);
|
|
29358
|
+
ctx.lineTo(cx + tick, yClose);
|
|
29359
|
+
ctx.stroke();
|
|
29360
|
+
} else {
|
|
29361
|
+
const bw = Math.max(1, cellW * 0.7);
|
|
29362
|
+
ctx.fillStyle = color;
|
|
29363
|
+
ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
|
|
29364
|
+
}
|
|
29365
|
+
}
|
|
29366
|
+
}
|
|
29367
|
+
/** The magnifier's line/area rendering: a close polyline through each cell's center (same
|
|
29368
|
+
* half-chart-bar shift as the candles), with an optional translucent fill down to the
|
|
29369
|
+
* rectangle's bottom edge for the area style. Colored like the chart's own line series. */
|
|
29370
|
+
paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
|
|
29371
|
+
const pts = [];
|
|
29372
|
+
for (const bar of bars) {
|
|
29373
|
+
const y = proj.yOf(bar.close, d.paneId);
|
|
29374
|
+
if (y == null) continue;
|
|
29375
|
+
pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
|
|
29376
|
+
}
|
|
29377
|
+
if (pts.length < 2) return;
|
|
29378
|
+
if (area) {
|
|
29379
|
+
ctx.beginPath();
|
|
29380
|
+
ctx.moveTo(pts[0][0], yBottom);
|
|
29381
|
+
for (const [px, py] of pts) ctx.lineTo(px, py);
|
|
29382
|
+
ctx.lineTo(pts[pts.length - 1][0], yBottom);
|
|
29383
|
+
ctx.closePath();
|
|
29384
|
+
ctx.fillStyle = withAlpha(color, 0.15);
|
|
29385
|
+
ctx.fill();
|
|
29386
|
+
}
|
|
29387
|
+
ctx.setLineDash([]);
|
|
29388
|
+
ctx.lineWidth = 1.5;
|
|
29389
|
+
ctx.strokeStyle = color;
|
|
29390
|
+
ctx.beginPath();
|
|
29391
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
29392
|
+
for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
29393
|
+
ctx.stroke();
|
|
29394
|
+
}
|
|
29395
|
+
/** Centered muted notice inside the magnifier rect (loading / unavailable states). */
|
|
29396
|
+
paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
|
|
29397
|
+
if (w < 60 || h < 20) return;
|
|
29398
|
+
ctx.save();
|
|
29399
|
+
ctx.beginPath();
|
|
29400
|
+
ctx.rect(x0, y0, w, h);
|
|
29401
|
+
ctx.clip();
|
|
29402
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29403
|
+
ctx.fillStyle = withAlpha(theme.textColor, 0.55);
|
|
29404
|
+
ctx.textAlign = "center";
|
|
29405
|
+
ctx.textBaseline = "middle";
|
|
29406
|
+
ctx.fillText(text, x0 + w / 2, y0 + h / 2);
|
|
29407
|
+
ctx.restore();
|
|
29408
|
+
}
|
|
29409
|
+
/** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
|
|
29410
|
+
* "drag an area"). Painted by the drawings layer while the tool is armed and no placement
|
|
29411
|
+
* is in progress; chart-background fill so it reads as chrome over any content. */
|
|
29412
|
+
paintPlacementHint(ctx, text, theme, width, height) {
|
|
29413
|
+
ctx.save();
|
|
29414
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29415
|
+
const tw = ctx.measureText(text).width;
|
|
29416
|
+
const pillW = tw + 24;
|
|
29417
|
+
const pillH = 24;
|
|
29418
|
+
const x = (width - pillW) / 2;
|
|
29419
|
+
const y = height - pillH - 14;
|
|
29420
|
+
roundRect(ctx, x, y, pillW, pillH, pillH / 2);
|
|
29421
|
+
ctx.fillStyle = theme.background;
|
|
29422
|
+
ctx.fill();
|
|
29423
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29424
|
+
ctx.lineWidth = 1;
|
|
29425
|
+
ctx.setLineDash([]);
|
|
29426
|
+
ctx.stroke();
|
|
29427
|
+
ctx.fillStyle = theme.textColor;
|
|
29428
|
+
ctx.textAlign = "center";
|
|
29429
|
+
ctx.textBaseline = "middle";
|
|
29430
|
+
ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
|
|
29431
|
+
ctx.restore();
|
|
29432
|
+
}
|
|
28833
29433
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
28834
29434
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
28835
29435
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -29548,6 +30148,22 @@ var DrawingInteraction = class {
|
|
|
29548
30148
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
29549
30149
|
return snapped;
|
|
29550
30150
|
}
|
|
30151
|
+
/**
|
|
30152
|
+
* Resolve a cursor pixel through the magnet and return the snapped pixel — the same
|
|
30153
|
+
* conversion drawing placement uses. Updates the snap-ring marker. The measure
|
|
30154
|
+
* ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
|
|
30155
|
+
*/
|
|
30156
|
+
snapCursor(x, y, mode) {
|
|
30157
|
+
const proj = this.deps.projector();
|
|
30158
|
+
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
30159
|
+
const point = this.resolve(x, y, paneId, mode);
|
|
30160
|
+
const sy = proj.yOf(point.price, paneId);
|
|
30161
|
+
return { x: proj.xOf(point.time), y: sy ?? y };
|
|
30162
|
+
}
|
|
30163
|
+
/** Drop the snap-ring marker (a transient mode ended without going through `up`). */
|
|
30164
|
+
clearSnapMarker() {
|
|
30165
|
+
this.snapAt = null;
|
|
30166
|
+
}
|
|
29551
30167
|
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
29552
30168
|
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
29553
30169
|
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
@@ -30229,8 +30845,9 @@ function ensureStyles5() {
|
|
|
30229
30845
|
if (!existing) document.head.appendChild(s);
|
|
30230
30846
|
}
|
|
30231
30847
|
var DrawingSettingsPopup = class {
|
|
30232
|
-
constructor(host, theme) {
|
|
30848
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
30233
30849
|
this.host = host;
|
|
30850
|
+
this.chartBarMs = chartBarMs;
|
|
30234
30851
|
this.el = null;
|
|
30235
30852
|
this.tipEl = null;
|
|
30236
30853
|
// floating hover-label (above/below the toolbar)
|
|
@@ -30255,6 +30872,14 @@ var DrawingSettingsPopup = class {
|
|
|
30255
30872
|
this.theme = theme;
|
|
30256
30873
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
30257
30874
|
}
|
|
30875
|
+
/** The magnifier timeframe choices strictly below the chart's own bar duration
|
|
30876
|
+
* (`auto` rides along while at least one concrete lower step exists). */
|
|
30877
|
+
lowerTimeframeOptions() {
|
|
30878
|
+
const chartMs = this.chartBarMs();
|
|
30879
|
+
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
30880
|
+
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
30881
|
+
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
30882
|
+
}
|
|
30258
30883
|
setTheme(theme) {
|
|
30259
30884
|
this.theme = theme;
|
|
30260
30885
|
this.settingsDialog.setTheme(theme);
|
|
@@ -30297,7 +30922,20 @@ var DrawingSettingsPopup = class {
|
|
|
30297
30922
|
const sz = drawing.size ?? "normal";
|
|
30298
30923
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
30299
30924
|
}
|
|
30300
|
-
if (paths.has("
|
|
30925
|
+
if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
|
|
30926
|
+
const options = this.lowerTimeframeOptions();
|
|
30927
|
+
if (options.length > 0) {
|
|
30928
|
+
bar.appendChild(
|
|
30929
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
30930
|
+
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
30931
|
+
labelInTrigger: true
|
|
30932
|
+
})
|
|
30933
|
+
);
|
|
30934
|
+
}
|
|
30935
|
+
bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
|
|
30936
|
+
bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
|
|
30937
|
+
}
|
|
30938
|
+
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
30939
|
if (paths.has("style.lineWidth")) {
|
|
30302
30940
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
30303
30941
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -30619,6 +31257,7 @@ var DrawingSettingsPopup = class {
|
|
|
30619
31257
|
this.colorPop = null;
|
|
30620
31258
|
this.colorOwner = null;
|
|
30621
31259
|
}
|
|
31260
|
+
opts.onClose?.();
|
|
30622
31261
|
}
|
|
30623
31262
|
});
|
|
30624
31263
|
const el = pop.el;
|
|
@@ -30637,6 +31276,49 @@ var DrawingSettingsPopup = class {
|
|
|
30637
31276
|
pop.show();
|
|
30638
31277
|
return pop;
|
|
30639
31278
|
}
|
|
31279
|
+
/**
|
|
31280
|
+
* A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
|
|
31281
|
+
* canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
|
|
31282
|
+
* to position against, and removed again when the menu closes. Independent of the
|
|
31283
|
+
* quick toolbar — the chip works without selecting the drawing first.
|
|
31284
|
+
*/
|
|
31285
|
+
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
31286
|
+
ensureStyles5();
|
|
31287
|
+
closeOpenPopovers();
|
|
31288
|
+
const options = this.lowerTimeframeOptions();
|
|
31289
|
+
const anchor = document.createElement("div");
|
|
31290
|
+
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
31291
|
+
this.host.appendChild(anchor);
|
|
31292
|
+
this.menuPop = this.hostFloat(anchor, {
|
|
31293
|
+
zIndex: 26,
|
|
31294
|
+
padding: "4px",
|
|
31295
|
+
onClose: () => anchor.remove(),
|
|
31296
|
+
fill: (menu2, pop) => {
|
|
31297
|
+
if (options.length === 0) {
|
|
31298
|
+
const note = document.createElement("div");
|
|
31299
|
+
note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
|
|
31300
|
+
note.textContent = "No lower timeframe available";
|
|
31301
|
+
menu2.appendChild(note);
|
|
31302
|
+
return;
|
|
31303
|
+
}
|
|
31304
|
+
for (const o of options) {
|
|
31305
|
+
const item = document.createElement("button");
|
|
31306
|
+
item.type = "button";
|
|
31307
|
+
item.className = "vela-dpop-item";
|
|
31308
|
+
item.dataset.active = o.value === current ? "1" : "0";
|
|
31309
|
+
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;";
|
|
31310
|
+
item.textContent = o.label;
|
|
31311
|
+
item.addEventListener("click", (e) => {
|
|
31312
|
+
e.stopPropagation();
|
|
31313
|
+
pop.hide();
|
|
31314
|
+
onPick(o.value);
|
|
31315
|
+
});
|
|
31316
|
+
menu2.appendChild(item);
|
|
31317
|
+
}
|
|
31318
|
+
}
|
|
31319
|
+
});
|
|
31320
|
+
this.menuOwner = anchor;
|
|
31321
|
+
}
|
|
30640
31322
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
30641
31323
|
openActionMenu(anchor, rows) {
|
|
30642
31324
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -30747,10 +31429,13 @@ var DrawingSettingsPopup = class {
|
|
|
30747
31429
|
let cur = current;
|
|
30748
31430
|
const paint = (v) => {
|
|
30749
31431
|
b.replaceChildren();
|
|
30750
|
-
const
|
|
30751
|
-
|
|
30752
|
-
|
|
30753
|
-
|
|
31432
|
+
const glyph = render(v);
|
|
31433
|
+
if (glyph) {
|
|
31434
|
+
const ic = document.createElement("span");
|
|
31435
|
+
ic.style.cssText = "display:flex;";
|
|
31436
|
+
ic.innerHTML = sized(glyph);
|
|
31437
|
+
b.appendChild(ic);
|
|
31438
|
+
}
|
|
30754
31439
|
if (opts.label && opts.labelInTrigger) {
|
|
30755
31440
|
const tx = document.createElement("span");
|
|
30756
31441
|
tx.textContent = opts.label(v);
|
|
@@ -30792,10 +31477,13 @@ var DrawingSettingsPopup = class {
|
|
|
30792
31477
|
item.className = "vela-dpop-item";
|
|
30793
31478
|
item.dataset.active = active ? "1" : "0";
|
|
30794
31479
|
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
|
-
|
|
31480
|
+
const glyph = render(v);
|
|
31481
|
+
if (glyph) {
|
|
31482
|
+
const ic = document.createElement("span");
|
|
31483
|
+
ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
|
|
31484
|
+
ic.innerHTML = sized(glyph, 18);
|
|
31485
|
+
item.appendChild(ic);
|
|
31486
|
+
}
|
|
30799
31487
|
if (label) {
|
|
30800
31488
|
const tx = document.createElement("span");
|
|
30801
31489
|
tx.textContent = label(v);
|
|
@@ -30953,30 +31641,39 @@ var MeasureOverlay = class {
|
|
|
30953
31641
|
isFinished() {
|
|
30954
31642
|
return this.state === "finished";
|
|
30955
31643
|
}
|
|
30956
|
-
/** A press: begin the measurement, or finish it on the second click.
|
|
30957
|
-
|
|
31644
|
+
/** A press: begin the measurement, or finish it on the second click.
|
|
31645
|
+
* `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
|
|
31646
|
+
* graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
|
|
31647
|
+
down(x, y, gx = x, gy = y) {
|
|
30958
31648
|
if (this.state === "measuring") {
|
|
30959
|
-
this.end = { x, y };
|
|
31649
|
+
this.end = { x: gx, y: gy };
|
|
30960
31650
|
this.state = "finished";
|
|
30961
31651
|
return;
|
|
30962
31652
|
}
|
|
30963
|
-
this.start = { x, y };
|
|
30964
|
-
this.end = { x, y };
|
|
31653
|
+
this.start = { x: gx, y: gy };
|
|
31654
|
+
this.end = { x: gx, y: gy };
|
|
30965
31655
|
this.pressX = x;
|
|
30966
31656
|
this.pressY = y;
|
|
30967
31657
|
this.state = "measuring";
|
|
30968
31658
|
}
|
|
31659
|
+
/** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
|
|
30969
31660
|
move(x, y) {
|
|
30970
31661
|
if (this.state === "measuring") this.end = { x, y };
|
|
30971
31662
|
}
|
|
30972
31663
|
/** A release: finish if the press was actually dragged (press-drag-release), else wait
|
|
30973
|
-
* for the second click (click-move-click).
|
|
30974
|
-
|
|
31664
|
+
* for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
|
|
31665
|
+
* are the graphic end (magnet-snapped when the magnet is on). */
|
|
31666
|
+
up(x, y, gx = x, gy = y) {
|
|
30975
31667
|
if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP4) {
|
|
30976
|
-
this.end = { x, y };
|
|
31668
|
+
this.end = { x: gx, y: gy };
|
|
30977
31669
|
this.state = "finished";
|
|
30978
31670
|
}
|
|
30979
31671
|
}
|
|
31672
|
+
/** Current graphic endpoints in media pixels, or null when idle. */
|
|
31673
|
+
points() {
|
|
31674
|
+
if (!this.start || !this.end) return null;
|
|
31675
|
+
return { start: this.start, end: this.end };
|
|
31676
|
+
}
|
|
30980
31677
|
clear() {
|
|
30981
31678
|
this.state = "idle";
|
|
30982
31679
|
this.start = null;
|
|
@@ -31138,6 +31835,9 @@ var UserDrawingController = class {
|
|
|
31138
31835
|
this.intentCb = null;
|
|
31139
31836
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
31140
31837
|
this.externalGhost = null;
|
|
31838
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
31839
|
+
this.seriesGw = null;
|
|
31840
|
+
this.seriesGwUnsub = null;
|
|
31141
31841
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
31142
31842
|
this.lastDraftKey = null;
|
|
31143
31843
|
this.measure = new MeasureOverlay();
|
|
@@ -31165,7 +31865,7 @@ var UserDrawingController = class {
|
|
|
31165
31865
|
this.textEditor = null;
|
|
31166
31866
|
this.painter = new DrawingPainter();
|
|
31167
31867
|
this.ctx = canvas.getContext("2d");
|
|
31168
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
31868
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
31169
31869
|
this.toolbar = new DrawingToolbar(
|
|
31170
31870
|
toolbarHost,
|
|
31171
31871
|
deps.theme(),
|
|
@@ -31223,6 +31923,22 @@ var UserDrawingController = class {
|
|
|
31223
31923
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
31224
31924
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
31225
31925
|
}
|
|
31926
|
+
/** Core push: the series gateway data-driven drawings read finer-timeframe bars
|
|
31927
|
+
* through (surfaced to them as `Projector.seriesInRange`). A landed background
|
|
31928
|
+
* fetch repaints both this layer and the interleave slices under the series. */
|
|
31929
|
+
setSeriesGateway(gateway) {
|
|
31930
|
+
this.seriesGwUnsub?.();
|
|
31931
|
+
this.seriesGw = gateway;
|
|
31932
|
+
this.seriesGwUnsub = gateway.onUpdate(() => {
|
|
31933
|
+
this.invalidateSlices();
|
|
31934
|
+
this.render();
|
|
31935
|
+
this.deps.requestDataPaint();
|
|
31936
|
+
});
|
|
31937
|
+
}
|
|
31938
|
+
/** The pushed series gateway, or null before the core provides one. */
|
|
31939
|
+
get seriesGateway() {
|
|
31940
|
+
return this.seriesGw;
|
|
31941
|
+
}
|
|
31226
31942
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
31227
31943
|
setExternalGhost(doc) {
|
|
31228
31944
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -31340,8 +32056,34 @@ var UserDrawingController = class {
|
|
|
31340
32056
|
/** Should the drawing layer win this press (vs pan)? */
|
|
31341
32057
|
claim(x, y) {
|
|
31342
32058
|
if (this.measureMode || this.eraserMode) return true;
|
|
32059
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
31343
32060
|
return this.interaction.claim(x, y);
|
|
31344
32061
|
}
|
|
32062
|
+
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
32063
|
+
* the chip's rect is what the painter measured last frame. */
|
|
32064
|
+
magnifierChipAt(x, y) {
|
|
32065
|
+
for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
|
|
32066
|
+
const d = this.drawings[i];
|
|
32067
|
+
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
32068
|
+
const r = d.chipRect;
|
|
32069
|
+
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
32070
|
+
}
|
|
32071
|
+
return null;
|
|
32072
|
+
}
|
|
32073
|
+
/** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
|
|
32074
|
+
* settings-popup edit (same intent, same undo step). */
|
|
32075
|
+
openMagnifierChipMenu(drawing) {
|
|
32076
|
+
const rect = drawing.chipRect;
|
|
32077
|
+
if (!rect) return;
|
|
32078
|
+
const id = drawing.id;
|
|
32079
|
+
this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
|
|
32080
|
+
const d = this.drawings.find((x) => x.id === id);
|
|
32081
|
+
if (!(d instanceof Magnifier)) return;
|
|
32082
|
+
d.applySettings({ "magnifier.timeframe": value });
|
|
32083
|
+
this.render();
|
|
32084
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32085
|
+
});
|
|
32086
|
+
}
|
|
31345
32087
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
31346
32088
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
31347
32089
|
deleteAt(x, y) {
|
|
@@ -31353,11 +32095,13 @@ var UserDrawingController = class {
|
|
|
31353
32095
|
}
|
|
31354
32096
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
31355
32097
|
* gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
|
|
31356
|
-
*
|
|
31357
|
-
|
|
32098
|
+
* `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
|
|
32099
|
+
* false when a mode/tool is already active (the normal press path owns it). */
|
|
32100
|
+
beginMeasureAt(x, y, snap = "off") {
|
|
31358
32101
|
if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
|
|
31359
32102
|
this.withModeIntent(() => this.toggleMeasure());
|
|
31360
|
-
this.
|
|
32103
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
32104
|
+
this.measure.down(x, y, g.x, g.y);
|
|
31361
32105
|
this.render();
|
|
31362
32106
|
return true;
|
|
31363
32107
|
}
|
|
@@ -31379,11 +32123,19 @@ var UserDrawingController = class {
|
|
|
31379
32123
|
return;
|
|
31380
32124
|
}
|
|
31381
32125
|
if (this.measureMode) {
|
|
31382
|
-
this.
|
|
32126
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
32127
|
+
this.measure.down(x, y, g.x, g.y);
|
|
31383
32128
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
31384
32129
|
this.render();
|
|
31385
32130
|
return;
|
|
31386
32131
|
}
|
|
32132
|
+
if (this.activeTool == null) {
|
|
32133
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
32134
|
+
if (chipOwner) {
|
|
32135
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
32136
|
+
return;
|
|
32137
|
+
}
|
|
32138
|
+
}
|
|
31387
32139
|
this.interaction.down(x, y, snap, shift);
|
|
31388
32140
|
}
|
|
31389
32141
|
pointerMove(x, y, snap = "off", shift = false) {
|
|
@@ -31392,7 +32144,8 @@ var UserDrawingController = class {
|
|
|
31392
32144
|
return;
|
|
31393
32145
|
}
|
|
31394
32146
|
if (this.measureMode) {
|
|
31395
|
-
this.
|
|
32147
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
32148
|
+
this.measure.move(g.x, g.y);
|
|
31396
32149
|
this.render();
|
|
31397
32150
|
return;
|
|
31398
32151
|
}
|
|
@@ -31410,13 +32163,14 @@ var UserDrawingController = class {
|
|
|
31410
32163
|
this.render();
|
|
31411
32164
|
}
|
|
31412
32165
|
}
|
|
31413
|
-
pointerUp(x, y) {
|
|
32166
|
+
pointerUp(x, y, snap = "off") {
|
|
31414
32167
|
if (this.eraserMode) {
|
|
31415
32168
|
this.erasing = false;
|
|
31416
32169
|
return;
|
|
31417
32170
|
}
|
|
31418
32171
|
if (this.measureMode) {
|
|
31419
|
-
this.
|
|
32172
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
32173
|
+
this.measure.up(x, y, g.x, g.y);
|
|
31420
32174
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
31421
32175
|
this.render();
|
|
31422
32176
|
return;
|
|
@@ -31461,6 +32215,7 @@ var UserDrawingController = class {
|
|
|
31461
32215
|
/** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
|
|
31462
32216
|
exitMeasure(clearGraphic = true) {
|
|
31463
32217
|
this.measureMode = false;
|
|
32218
|
+
this.interaction.clearSnapMarker();
|
|
31464
32219
|
if (clearGraphic) this.measure.clear();
|
|
31465
32220
|
this.toolbar.setMeasureActive(false);
|
|
31466
32221
|
this.render();
|
|
@@ -31599,17 +32354,31 @@ var UserDrawingController = class {
|
|
|
31599
32354
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
31600
32355
|
cursorAt(x, y) {
|
|
31601
32356
|
if (this.eraserMode) return "pointer";
|
|
32357
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
31602
32358
|
return this.interaction.cursorAt(x, y);
|
|
31603
32359
|
}
|
|
31604
|
-
/** Right-click
|
|
31605
|
-
*
|
|
31606
|
-
*
|
|
31607
|
-
*
|
|
32360
|
+
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
32361
|
+
* placement or measurement, and also plain-disarms an armed-but-idle drawing
|
|
32362
|
+
* tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
|
|
32363
|
+
* stay-in-drawing-mode (where Escape would leave a drawing tool armed).
|
|
32364
|
+
* Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
|
|
32365
|
+
* whether the press was consumed; false lets the host's context menu open
|
|
32366
|
+
* normally. */
|
|
31608
32367
|
cancelPlacement() {
|
|
31609
|
-
if (
|
|
31610
|
-
|
|
31611
|
-
|
|
31612
|
-
|
|
32368
|
+
if (this.measureMode || this.eraserMode) {
|
|
32369
|
+
this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
|
|
32370
|
+
return true;
|
|
32371
|
+
}
|
|
32372
|
+
if (this.interaction.isPlacing()) {
|
|
32373
|
+
this.interaction.cancel();
|
|
32374
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
32375
|
+
return true;
|
|
32376
|
+
}
|
|
32377
|
+
if (this.activeTool != null) {
|
|
32378
|
+
this.emit({ kind: "arm", type: null });
|
|
32379
|
+
return true;
|
|
32380
|
+
}
|
|
32381
|
+
return false;
|
|
31613
32382
|
}
|
|
31614
32383
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
31615
32384
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
@@ -31635,6 +32404,10 @@ var UserDrawingController = class {
|
|
|
31635
32404
|
return true;
|
|
31636
32405
|
}
|
|
31637
32406
|
if (this.interaction.cancel()) return true;
|
|
32407
|
+
if (this.measureMode) {
|
|
32408
|
+
this.withModeIntent(() => this.exitMeasure());
|
|
32409
|
+
return true;
|
|
32410
|
+
}
|
|
31638
32411
|
if (this.selectedIds.size) {
|
|
31639
32412
|
this.clearSelection();
|
|
31640
32413
|
return true;
|
|
@@ -31760,6 +32533,7 @@ var UserDrawingController = class {
|
|
|
31760
32533
|
if (!sctx) continue;
|
|
31761
32534
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
31762
32535
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32536
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
31763
32537
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
31764
32538
|
const slices = out.get(paneId) ?? [];
|
|
31765
32539
|
slices.push({ beforeZ, canvas });
|
|
@@ -31787,6 +32561,7 @@ var UserDrawingController = class {
|
|
|
31787
32561
|
dragged: this.interaction.activeDragId(),
|
|
31788
32562
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
31789
32563
|
};
|
|
32564
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
31790
32565
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
31791
32566
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
31792
32567
|
this.layoutTextEditor();
|
|
@@ -31794,6 +32569,10 @@ var UserDrawingController = class {
|
|
|
31794
32569
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
31795
32570
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
31796
32571
|
this.emitDraft(ghost);
|
|
32572
|
+
if (this.activeTool && !ghost) {
|
|
32573
|
+
const hint = getDrawingType(this.activeTool)?.placementHint;
|
|
32574
|
+
if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
|
|
32575
|
+
}
|
|
31797
32576
|
const markers = this.interaction.placingMarkers(proj);
|
|
31798
32577
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
31799
32578
|
const m = this.interaction.snapMarker();
|
|
@@ -31805,6 +32584,9 @@ var UserDrawingController = class {
|
|
|
31805
32584
|
this.closeTextEditor();
|
|
31806
32585
|
this.popup.destroy();
|
|
31807
32586
|
this.toolbar.destroy();
|
|
32587
|
+
this.seriesGwUnsub?.();
|
|
32588
|
+
this.seriesGwUnsub = null;
|
|
32589
|
+
this.seriesGw = null;
|
|
31808
32590
|
this.intentCb = null;
|
|
31809
32591
|
this.drawings = [];
|
|
31810
32592
|
}
|
|
@@ -32202,7 +32984,7 @@ function mergeSlices(indicator, user) {
|
|
|
32202
32984
|
}
|
|
32203
32985
|
|
|
32204
32986
|
// src/renderers/native/drawings/Projector.ts
|
|
32205
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
32987
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
32206
32988
|
return {
|
|
32207
32989
|
xOf: (time) => coords.timeToX(time),
|
|
32208
32990
|
yOf: (price, paneId) => {
|
|
@@ -32223,6 +33005,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
|
32223
33005
|
},
|
|
32224
33006
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
32225
33007
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
33008
|
+
seriesInRange,
|
|
32226
33009
|
width: coords.width,
|
|
32227
33010
|
height: coords.height
|
|
32228
33011
|
};
|
|
@@ -34059,13 +34842,13 @@ var NativeRenderer = class {
|
|
|
34059
34842
|
resetView: () => this.resetView(),
|
|
34060
34843
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
34061
34844
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
34062
|
-
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
34845
|
+
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
34063
34846
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
34064
34847
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
34065
34848
|
drawingsSnapMode: () => this.snapMode,
|
|
34066
34849
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
34067
34850
|
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
34068
|
-
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
34851
|
+
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
34069
34852
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
34070
34853
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
34071
34854
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
@@ -34092,6 +34875,23 @@ var NativeRenderer = class {
|
|
|
34092
34875
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
34093
34876
|
priceZ: (paneId) => paneId === PRICE_PANE_ID2 ? this.scene.candleZ : null,
|
|
34094
34877
|
requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
|
|
34878
|
+
// The look the price series ACTUALLY paints with: candle colors resolved through
|
|
34879
|
+
// the per-style override, line/area colors through their configured styles — so
|
|
34880
|
+
// series-mirroring content (the magnifier inset) matches the chart exactly.
|
|
34881
|
+
seriesLook: () => {
|
|
34882
|
+
const st = this.scene.style;
|
|
34883
|
+
const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
|
|
34884
|
+
const barsUp = st.bars.upColor ?? this.theme.upColor;
|
|
34885
|
+
const barsDown = st.bars.downColor ?? this.theme.downColor;
|
|
34886
|
+
const style = this.scene.priceStyle;
|
|
34887
|
+
return {
|
|
34888
|
+
style,
|
|
34889
|
+
upColor: style === "bars" ? barsUp : paint.up,
|
|
34890
|
+
downColor: style === "bars" ? barsDown : paint.down,
|
|
34891
|
+
lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
|
|
34892
|
+
};
|
|
34893
|
+
},
|
|
34894
|
+
chartBarMs: () => this.coords.barInterval,
|
|
34095
34895
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
34096
34896
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
34097
34897
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -34380,7 +35180,8 @@ var NativeRenderer = class {
|
|
|
34380
35180
|
}
|
|
34381
35181
|
const skipFit = opts?.preserveView === true && this.didInitialFit;
|
|
34382
35182
|
if (this.coords.width > 0 && !skipFit) {
|
|
34383
|
-
this.
|
|
35183
|
+
if (this.didInitialFit) this.reframeKeepZoom();
|
|
35184
|
+
else this.fitContent();
|
|
34384
35185
|
this.didInitialFit = true;
|
|
34385
35186
|
}
|
|
34386
35187
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
@@ -35028,6 +35829,7 @@ var NativeRenderer = class {
|
|
|
35028
35829
|
this.scaleDragHeight = res.height;
|
|
35029
35830
|
this.scaleDragStart = { ...res.holder.scale };
|
|
35030
35831
|
res.holder.manualScale = { ...res.holder.scale };
|
|
35832
|
+
this.axisScaleButtons?.reposition();
|
|
35031
35833
|
this.scheduler.invalidate(4 /* Full */);
|
|
35032
35834
|
}
|
|
35033
35835
|
/** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
|
|
@@ -35059,6 +35861,7 @@ var NativeRenderer = class {
|
|
|
35059
35861
|
const res = this.resolveScaleHolder(x, y);
|
|
35060
35862
|
if (!res) return;
|
|
35061
35863
|
res.holder.manualScale = null;
|
|
35864
|
+
this.axisScaleButtons?.reposition();
|
|
35062
35865
|
this.scheduler.invalidate(4 /* Full */);
|
|
35063
35866
|
}
|
|
35064
35867
|
/**
|
|
@@ -35482,7 +36285,8 @@ var NativeRenderer = class {
|
|
|
35482
36285
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
35483
36286
|
},
|
|
35484
36287
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
35485
|
-
(from, to) => this.barsInTimeRange(from, to)
|
|
36288
|
+
(from, to) => this.barsInTimeRange(from, to),
|
|
36289
|
+
this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
|
|
35486
36290
|
);
|
|
35487
36291
|
}
|
|
35488
36292
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|
|
@@ -35696,6 +36500,20 @@ var NativeRenderer = class {
|
|
|
35696
36500
|
this.coords.setViewport(v);
|
|
35697
36501
|
this.targetBarSpacing = v.barSpacing;
|
|
35698
36502
|
}
|
|
36503
|
+
/** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
|
|
36504
|
+
* zoom (bar spacing), re-anchor the newest bars at the default right offset.
|
|
36505
|
+
* `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
|
|
36506
|
+
* head may still be backfilling toward the previous depth, and raising the spacing
|
|
36507
|
+
* to its temporary bar count would lose the zoom this exists to keep. */
|
|
36508
|
+
reframeKeepZoom() {
|
|
36509
|
+
this.animator?.stop();
|
|
36510
|
+
this.panVelocity = 0;
|
|
36511
|
+
for (const pane of this.scene.panes.values()) pane.manualScale = null;
|
|
36512
|
+
for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
|
|
36513
|
+
const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
|
|
36514
|
+
this.coords.setViewport(v);
|
|
36515
|
+
this.targetBarSpacing = v.barSpacing;
|
|
36516
|
+
}
|
|
35699
36517
|
paneBoundsFor(paneId) {
|
|
35700
36518
|
const p = this.scene.panes.get(paneId);
|
|
35701
36519
|
return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
|
|
@@ -36675,6 +37493,21 @@ function resolveElement(container) {
|
|
|
36675
37493
|
// src/core/options.ts
|
|
36676
37494
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
36677
37495
|
|
|
37496
|
+
// src/widget/statusline-model.ts
|
|
37497
|
+
function segmentVisibility(parts, chartHidden = false) {
|
|
37498
|
+
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
37499
|
+
}
|
|
37500
|
+
function statuslineMenuItems(parts, chartVisible) {
|
|
37501
|
+
return [
|
|
37502
|
+
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
37503
|
+
{ id: "part:name", label: "Symbol name", checked: parts.name },
|
|
37504
|
+
{ id: "part:market", label: "Market status", checked: parts.market },
|
|
37505
|
+
{ id: "part:ohlc", label: "OHLC values", checked: parts.ohlc },
|
|
37506
|
+
{ id: "part:change", label: "Bar change values", checked: parts.change },
|
|
37507
|
+
{ id: "chart", label: chartVisible ? "Hide chart" : "Show chart", separatorBefore: true }
|
|
37508
|
+
];
|
|
37509
|
+
}
|
|
37510
|
+
|
|
36678
37511
|
// src/widget/format.ts
|
|
36679
37512
|
function decimalsFor(ref) {
|
|
36680
37513
|
const a = Math.abs(ref);
|
|
@@ -36722,7 +37555,13 @@ var CSS21 = `
|
|
|
36722
37555
|
padding: 2px 7px;
|
|
36723
37556
|
margin-left: -7px;
|
|
36724
37557
|
}
|
|
36725
|
-
|
|
37558
|
+
/* Hovering opens the chip the same way a legend row opens: solid chart background
|
|
37559
|
+
* plus the same inset neutral outline the indicator rows wear (InputsUI's
|
|
37560
|
+
* setRowHighlighted) \u2014 the two columns read as one family. */
|
|
37561
|
+
.vela-statusline:hover { background: var(--vela-bg); box-shadow: inset 0 0 0 1px var(--vela-border); }
|
|
37562
|
+
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
37563
|
+
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
37564
|
+
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
36726
37565
|
.vela-statusline .vela-sl-avatar {
|
|
36727
37566
|
width: 18px;
|
|
36728
37567
|
height: 18px;
|
|
@@ -36747,6 +37586,24 @@ var CSS21 = `
|
|
|
36747
37586
|
* these are the pre-ink fallbacks only. */
|
|
36748
37587
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
36749
37588
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
37589
|
+
/* The show-chart eye \u2014 out only while the chart is hidden (syncParts drives display),
|
|
37590
|
+
* replacing the value readout it took away. Same footprint as a legend action button. */
|
|
37591
|
+
.vela-statusline .vela-sl-eye {
|
|
37592
|
+
align-self: center;
|
|
37593
|
+
align-items: center;
|
|
37594
|
+
justify-content: center;
|
|
37595
|
+
width: 18px;
|
|
37596
|
+
height: 18px;
|
|
37597
|
+
padding: 0;
|
|
37598
|
+
border: none;
|
|
37599
|
+
border-radius: 3px;
|
|
37600
|
+
background: none;
|
|
37601
|
+
color: var(--vela-fg-muted);
|
|
37602
|
+
cursor: pointer;
|
|
37603
|
+
line-height: 0;
|
|
37604
|
+
flex: none;
|
|
37605
|
+
}
|
|
37606
|
+
.vela-statusline .vela-sl-eye:hover { color: var(--vela-fg); background: color-mix(in srgb, var(--vela-fg) 12%, transparent); }
|
|
36750
37607
|
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
36751
37608
|
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
36752
37609
|
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
@@ -36856,7 +37713,12 @@ var Statusline = class {
|
|
|
36856
37713
|
constructor(host, symbol, iconFor) {
|
|
36857
37714
|
this.host = host;
|
|
36858
37715
|
this.iconFor = iconFor;
|
|
36859
|
-
this.parts = { name: true, market: true, ohlc: true, change: true };
|
|
37716
|
+
this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
|
|
37717
|
+
/** The right-click action menu — present once a host wires it via {@link attachMenu}. */
|
|
37718
|
+
this.menu = null;
|
|
37719
|
+
this.menuHooks = null;
|
|
37720
|
+
/** Mirror of the renderer's `candleVisible` — see {@link setChartHidden}. */
|
|
37721
|
+
this.chartHidden = false;
|
|
36860
37722
|
this.lastBar = null;
|
|
36861
37723
|
this.hoverBar = null;
|
|
36862
37724
|
this.unsubs = [];
|
|
@@ -36872,6 +37734,15 @@ var Statusline = class {
|
|
|
36872
37734
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
36873
37735
|
this.fitMode = false;
|
|
36874
37736
|
this.fitRO = null;
|
|
37737
|
+
this.onContextMenu = (e) => {
|
|
37738
|
+
if (!this.menu || !this.menuHooks) return;
|
|
37739
|
+
e.preventDefault();
|
|
37740
|
+
e.stopPropagation();
|
|
37741
|
+
const chartVisible = this.menuHooks.chartVisible();
|
|
37742
|
+
this.setChartHidden(!chartVisible);
|
|
37743
|
+
this.menu.setItems(statuslineMenuItems(this.parts, chartVisible));
|
|
37744
|
+
this.menu.openAt(e.clientX, e.clientY);
|
|
37745
|
+
};
|
|
36875
37746
|
const doc = host.ownerDocument;
|
|
36876
37747
|
injectStyles(STYLE_ID24, CSS21, doc);
|
|
36877
37748
|
host.classList.add("vela-has-statusline");
|
|
@@ -36898,9 +37769,21 @@ var Statusline = class {
|
|
|
36898
37769
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
36899
37770
|
this.changeEl = doc.createElement("span");
|
|
36900
37771
|
this.changeEl.className = "vela-sl-change";
|
|
36901
|
-
this.
|
|
37772
|
+
this.eyeEl = doc.createElement("button");
|
|
37773
|
+
this.eyeEl.type = "button";
|
|
37774
|
+
this.eyeEl.className = "vela-sl-eye";
|
|
37775
|
+
this.eyeEl.innerHTML = iconAt("eye-off", 14);
|
|
37776
|
+
this.eyeEl.setAttribute("aria-label", "Show chart");
|
|
37777
|
+
this.eyeEl.style.display = "none";
|
|
37778
|
+
this.eyeEl.addEventListener("click", (e) => {
|
|
37779
|
+
e.stopPropagation();
|
|
37780
|
+
this.menuHooks?.setChartVisible(true);
|
|
37781
|
+
this.setChartHidden(false);
|
|
37782
|
+
});
|
|
37783
|
+
this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl, this.eyeEl);
|
|
36902
37784
|
host.appendChild(this.el);
|
|
36903
37785
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
37786
|
+
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
36904
37787
|
this.setMarketStatus("open");
|
|
36905
37788
|
this.render();
|
|
36906
37789
|
}
|
|
@@ -36910,6 +37793,7 @@ var Statusline = class {
|
|
|
36910
37793
|
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
36911
37794
|
this.avatarEl.replaceWith(fresh);
|
|
36912
37795
|
this.avatarEl = fresh;
|
|
37796
|
+
this.syncParts();
|
|
36913
37797
|
this.fit();
|
|
36914
37798
|
}
|
|
36915
37799
|
/**
|
|
@@ -36937,20 +37821,25 @@ var Statusline = class {
|
|
|
36937
37821
|
}
|
|
36938
37822
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
36939
37823
|
syncParts() {
|
|
36940
|
-
|
|
36941
|
-
this.
|
|
36942
|
-
this.
|
|
36943
|
-
this.
|
|
37824
|
+
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
37825
|
+
this.avatarEl.style.display = seg.avatar ? "" : "none";
|
|
37826
|
+
this.symbolEl.style.display = seg.symbol ? "" : "none";
|
|
37827
|
+
this.metaEl.style.display = seg.meta ? "" : "none";
|
|
37828
|
+
this.marketEl.style.display = seg.market ? "" : "none";
|
|
37829
|
+
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
37830
|
+
this.changeEl.style.display = seg.change ? "" : "none";
|
|
37831
|
+
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
36944
37832
|
}
|
|
36945
37833
|
/** Hide overflowing segments until the row fits its max-width (fit mode only). */
|
|
36946
37834
|
fit() {
|
|
36947
37835
|
if (!this.fitMode) return;
|
|
36948
37836
|
this.syncParts();
|
|
37837
|
+
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
36949
37838
|
const order = [
|
|
36950
|
-
[this.ohlcEl,
|
|
36951
|
-
[this.changeEl,
|
|
36952
|
-
[this.metaEl,
|
|
36953
|
-
[this.marketEl,
|
|
37839
|
+
[this.ohlcEl, seg.ohlc],
|
|
37840
|
+
[this.changeEl, seg.change],
|
|
37841
|
+
[this.metaEl, seg.meta],
|
|
37842
|
+
[this.marketEl, seg.market]
|
|
36954
37843
|
];
|
|
36955
37844
|
for (const [el, shown] of order) {
|
|
36956
37845
|
if (this.el.scrollWidth <= this.el.clientWidth) break;
|
|
@@ -36971,7 +37860,6 @@ var Statusline = class {
|
|
|
36971
37860
|
this.readout = readout;
|
|
36972
37861
|
this.render();
|
|
36973
37862
|
}
|
|
36974
|
-
/** Show/hide one part (the settings dialog's Status line tab drives these). */
|
|
36975
37863
|
/** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
|
|
36976
37864
|
setMeta(timeframe, provider) {
|
|
36977
37865
|
this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
|
|
@@ -36990,6 +37878,9 @@ var Statusline = class {
|
|
|
36990
37878
|
});
|
|
36991
37879
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
36992
37880
|
}
|
|
37881
|
+
/** Show/hide one part — the settings dialog's Status line tab and the right-click
|
|
37882
|
+
* menu both drive these. 'name' owns the venue/timeframe meta too (see
|
|
37883
|
+
* {@link segmentVisibility}). */
|
|
36993
37884
|
setPartVisible(part, visible) {
|
|
36994
37885
|
this.parts[part] = visible;
|
|
36995
37886
|
this.syncParts();
|
|
@@ -36998,6 +37889,47 @@ var Statusline = class {
|
|
|
36998
37889
|
partVisible(part) {
|
|
36999
37890
|
return this.parts[part];
|
|
37000
37891
|
}
|
|
37892
|
+
/** Mirror the chart's (price series') visibility: dim the whole line like a hidden
|
|
37893
|
+
* indicator's legend row, drop the value readout (OHLC + bar change — values of a
|
|
37894
|
+
* series that isn't painted), and put the show-chart eye out in its place. The
|
|
37895
|
+
* parts config is untouched, so showing the chart restores the readout exactly as
|
|
37896
|
+
* configured. Idempotent; {@link render} re-syncs it from the live renderer, so
|
|
37897
|
+
* toggles made elsewhere (the object tree's eye) converge too. */
|
|
37898
|
+
setChartHidden(hidden) {
|
|
37899
|
+
if (hidden === this.chartHidden) return;
|
|
37900
|
+
this.chartHidden = hidden;
|
|
37901
|
+
this.el.classList.toggle("vela-sl-chart-hidden", hidden);
|
|
37902
|
+
this.syncParts();
|
|
37903
|
+
this.fit();
|
|
37904
|
+
}
|
|
37905
|
+
/** Wire the right-click action menu: one checkable toggle per part plus hide/show
|
|
37906
|
+
* for the chart itself. The menu is built once; later calls just swap the hooks. */
|
|
37907
|
+
attachMenu(hooks) {
|
|
37908
|
+
this.menuHooks = hooks;
|
|
37909
|
+
if (this.menu) return;
|
|
37910
|
+
this.menu = new Menu({
|
|
37911
|
+
host: this.host,
|
|
37912
|
+
items: [],
|
|
37913
|
+
placement: "bottom-start",
|
|
37914
|
+
// Pointer-anchored action menu — checked state reads as a leading ✓ (the
|
|
37915
|
+
// same shape as the chart's own context menu).
|
|
37916
|
+
checkmarks: true,
|
|
37917
|
+
onSelect: (id) => this.runMenuItem(id)
|
|
37918
|
+
});
|
|
37919
|
+
this.el.addEventListener("contextmenu", this.onContextMenu);
|
|
37920
|
+
}
|
|
37921
|
+
runMenuItem(id) {
|
|
37922
|
+
const hooks = this.menuHooks;
|
|
37923
|
+
if (!hooks) return;
|
|
37924
|
+
if (id.startsWith("part:")) {
|
|
37925
|
+
const part = id.slice("part:".length);
|
|
37926
|
+
hooks.setPart(part, !this.parts[part]);
|
|
37927
|
+
} else if (id === "chart") {
|
|
37928
|
+
const next = !hooks.chartVisible();
|
|
37929
|
+
hooks.setChartVisible(next);
|
|
37930
|
+
this.setChartHidden(!next);
|
|
37931
|
+
}
|
|
37932
|
+
}
|
|
37001
37933
|
/** (Re)bind to a chart instance — called after every widget rebuild. */
|
|
37002
37934
|
onChart(chart) {
|
|
37003
37935
|
this.detach();
|
|
@@ -37019,7 +37951,11 @@ var Statusline = class {
|
|
|
37019
37951
|
this.detach();
|
|
37020
37952
|
this.fitRO?.disconnect();
|
|
37021
37953
|
this.fitRO = null;
|
|
37954
|
+
this.el.removeEventListener("contextmenu", this.onContextMenu);
|
|
37955
|
+
this.menu?.destroy();
|
|
37956
|
+
this.menu = null;
|
|
37022
37957
|
this.marketTip.destroy();
|
|
37958
|
+
this.eyeTip.destroy();
|
|
37023
37959
|
this.marketBubble.destroy();
|
|
37024
37960
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
37025
37961
|
this.el.remove();
|
|
@@ -37029,6 +37965,7 @@ var Statusline = class {
|
|
|
37029
37965
|
this.unsubs = [];
|
|
37030
37966
|
}
|
|
37031
37967
|
render() {
|
|
37968
|
+
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
37032
37969
|
const bar = this.hoverBar ?? this.lastBar;
|
|
37033
37970
|
if (!bar) {
|
|
37034
37971
|
this.ohlcEl.replaceChildren();
|
|
@@ -37099,7 +38036,7 @@ var FETCH_BACK_MS = 4 * 864e5;
|
|
|
37099
38036
|
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
37100
38037
|
var MIN_TIMER_MS = 15e3;
|
|
37101
38038
|
var MAX_TIMER_MS = 36e5;
|
|
37102
|
-
var
|
|
38039
|
+
var RETRY_MS2 = 6e4;
|
|
37103
38040
|
var MarketStatusTracker = class {
|
|
37104
38041
|
constructor(onStatus) {
|
|
37105
38042
|
this.onStatus = onStatus;
|
|
@@ -37136,7 +38073,7 @@ var MarketStatusTracker = class {
|
|
|
37136
38073
|
]);
|
|
37137
38074
|
if (my !== this.epoch) return;
|
|
37138
38075
|
if (!regular || !extended) {
|
|
37139
|
-
this.arm(my, data, symbol,
|
|
38076
|
+
this.arm(my, data, symbol, RETRY_MS2);
|
|
37140
38077
|
return;
|
|
37141
38078
|
}
|
|
37142
38079
|
const w = { regular, extended };
|
|
@@ -37397,6 +38334,7 @@ var TIME_AXIS_H2 = 22;
|
|
|
37397
38334
|
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
37398
38335
|
var CLUSTER_H2 = 24;
|
|
37399
38336
|
var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
|
|
38337
|
+
var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
|
|
37400
38338
|
var STYLE_ID26 = "vela-cell-controls";
|
|
37401
38339
|
var CSS23 = `
|
|
37402
38340
|
.vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
|
|
@@ -37406,8 +38344,11 @@ var CSS23 = `
|
|
|
37406
38344
|
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
37407
38345
|
.vela-cc-grip:active{cursor:grabbing;}
|
|
37408
38346
|
`;
|
|
37409
|
-
function
|
|
37410
|
-
|
|
38347
|
+
function plotCenterX(width, toolbarGutter = 0, scaleGutter = 0) {
|
|
38348
|
+
return (width + toolbarGutter - scaleGutter) / 2;
|
|
38349
|
+
}
|
|
38350
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX, gutters = {}) {
|
|
38351
|
+
const cx = plotCenterX(width, gutters.toolbar ?? 0, gutters.scale ?? 0);
|
|
37411
38352
|
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
|
|
37412
38353
|
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
37413
38354
|
}
|
|
@@ -37426,7 +38367,7 @@ var CellControls = class {
|
|
|
37426
38367
|
if (this.suspended) return;
|
|
37427
38368
|
if (this.dragging) return;
|
|
37428
38369
|
const rect = this.host.getBoundingClientRect();
|
|
37429
|
-
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
|
|
38370
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, CELL_CONTROLS_PROXIMITY_PX, this.hostGutters()));
|
|
37430
38371
|
};
|
|
37431
38372
|
this.onHostLeave = () => {
|
|
37432
38373
|
if (this.dragging) return;
|
|
@@ -37437,7 +38378,7 @@ var CellControls = class {
|
|
|
37437
38378
|
this.root = host.ownerDocument.createElement("div");
|
|
37438
38379
|
Object.assign(this.root.style, {
|
|
37439
38380
|
position: "absolute",
|
|
37440
|
-
left:
|
|
38381
|
+
left: CLUSTER_LEFT_CSS,
|
|
37441
38382
|
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
37442
38383
|
transform: "translateX(-50%)",
|
|
37443
38384
|
zIndex: "6",
|
|
@@ -37537,6 +38478,16 @@ var CellControls = class {
|
|
|
37537
38478
|
this.suspended = on;
|
|
37538
38479
|
if (on) this.setNear(false);
|
|
37539
38480
|
}
|
|
38481
|
+
/** Live renderer gutters on the cell host (0 when unpublished — a test stub). */
|
|
38482
|
+
hostGutters() {
|
|
38483
|
+
const view = this.host.ownerDocument.defaultView;
|
|
38484
|
+
if (!view) return { toolbar: 0, scale: 0 };
|
|
38485
|
+
const cs = view.getComputedStyle(this.host);
|
|
38486
|
+
return {
|
|
38487
|
+
toolbar: Number.parseFloat(cs.getPropertyValue("--vela-toolbar-gutter")) || 0,
|
|
38488
|
+
scale: Number.parseFloat(cs.getPropertyValue("--vela-scale-gutter")) || 0
|
|
38489
|
+
};
|
|
38490
|
+
}
|
|
37540
38491
|
setNear(near) {
|
|
37541
38492
|
if (near === this.near) return;
|
|
37542
38493
|
this.near = near;
|
|
@@ -37777,6 +38728,12 @@ function cellDrawings(opt) {
|
|
|
37777
38728
|
if (opt === true || opt == null) return { toolbar: false };
|
|
37778
38729
|
return { ...opt, toolbar: false };
|
|
37779
38730
|
}
|
|
38731
|
+
function instanceDeltas(handle) {
|
|
38732
|
+
if (!handle) return void 0;
|
|
38733
|
+
const inputs = inputDeltas(handle.inputs, handle.inputValues());
|
|
38734
|
+
const props = inputDeltas(handle.props, handle.propValues());
|
|
38735
|
+
return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
|
|
38736
|
+
}
|
|
37780
38737
|
var ChartCell = class {
|
|
37781
38738
|
constructor(id, gridHost, seed, deps) {
|
|
37782
38739
|
this.id = id;
|
|
@@ -37827,7 +38784,14 @@ var ChartCell = class {
|
|
|
37827
38784
|
this.destroyed = false;
|
|
37828
38785
|
this.appTheme = deps.theme;
|
|
37829
38786
|
const symbol = prefixedSymbol(seed);
|
|
37830
|
-
this.state = {
|
|
38787
|
+
this.state = {
|
|
38788
|
+
symbol,
|
|
38789
|
+
provider: parseSymbol(symbol ?? "").provider ?? void 0,
|
|
38790
|
+
timeframe: seed.timeframe,
|
|
38791
|
+
priceStyle: seed.priceStyle,
|
|
38792
|
+
bars: seed.bars,
|
|
38793
|
+
session: normalizeSession(seed.session)
|
|
38794
|
+
};
|
|
37831
38795
|
const doc = gridHost.ownerDocument;
|
|
37832
38796
|
this.host = doc.createElement("div");
|
|
37833
38797
|
this.host.className = "vela-cell";
|
|
@@ -37915,6 +38879,11 @@ var ChartCell = class {
|
|
|
37915
38879
|
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
37916
38880
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
37917
38881
|
this.statusline?.onChart(this.inner);
|
|
38882
|
+
this.statusline?.attachMenu({
|
|
38883
|
+
setPart: (part, visible) => this.setStatuslinePart(part, visible),
|
|
38884
|
+
chartVisible: () => this.inner?.renderer.get("candleVisible") !== false,
|
|
38885
|
+
setChartVisible: (visible) => this.inner?.renderer.set("candleVisible", visible)
|
|
38886
|
+
});
|
|
37918
38887
|
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
37919
38888
|
void this.inner.data.ready().then(() => {
|
|
37920
38889
|
if (this.inner && this.state.symbol) {
|
|
@@ -37952,6 +38921,7 @@ var ChartCell = class {
|
|
|
37952
38921
|
this.syncPresentNatives();
|
|
37953
38922
|
this.refreshNativeCatalog();
|
|
37954
38923
|
});
|
|
38924
|
+
this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
|
|
37955
38925
|
this.inner.on("indicator:removed", ({ id: id2 }) => {
|
|
37956
38926
|
if (this.destroyed) return;
|
|
37957
38927
|
const idx = this.instances.findIndex((it) => it.handle?.id === id2);
|
|
@@ -37960,7 +38930,7 @@ var ChartCell = class {
|
|
|
37960
38930
|
this.instances.splice(idx, 1);
|
|
37961
38931
|
this.history.push({
|
|
37962
38932
|
undo: () => {
|
|
37963
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
38933
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
37964
38934
|
this.instances.push(snapshot);
|
|
37965
38935
|
this.deps.onIndicatorsChanged(this.id);
|
|
37966
38936
|
},
|
|
@@ -38121,7 +39091,7 @@ var ChartCell = class {
|
|
|
38121
39091
|
kind: "select",
|
|
38122
39092
|
label: "Bars to fetch",
|
|
38123
39093
|
id: "bars",
|
|
38124
|
-
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
39094
|
+
options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
|
|
38125
39095
|
get: () => String(this.state.bars ?? 1e3),
|
|
38126
39096
|
set: (v) => {
|
|
38127
39097
|
this.state.bars = Number(v);
|
|
@@ -38153,10 +39123,34 @@ var ChartCell = class {
|
|
|
38153
39123
|
id: "status-line",
|
|
38154
39124
|
rows: [
|
|
38155
39125
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
38156
|
-
{
|
|
38157
|
-
|
|
38158
|
-
|
|
38159
|
-
|
|
39126
|
+
{
|
|
39127
|
+
kind: "toggle",
|
|
39128
|
+
label: "Symbol name",
|
|
39129
|
+
id: "name",
|
|
39130
|
+
get: () => sl.partVisible("name"),
|
|
39131
|
+
set: (v) => this.setStatuslinePart("name", v)
|
|
39132
|
+
},
|
|
39133
|
+
{
|
|
39134
|
+
kind: "toggle",
|
|
39135
|
+
label: "Market status",
|
|
39136
|
+
id: "market",
|
|
39137
|
+
get: () => sl.partVisible("market"),
|
|
39138
|
+
set: (v) => this.setStatuslinePart("market", v)
|
|
39139
|
+
},
|
|
39140
|
+
{
|
|
39141
|
+
kind: "toggle",
|
|
39142
|
+
label: "OHLC values",
|
|
39143
|
+
id: "ohlc",
|
|
39144
|
+
get: () => sl.partVisible("ohlc"),
|
|
39145
|
+
set: (v) => this.setStatuslinePart("ohlc", v)
|
|
39146
|
+
},
|
|
39147
|
+
{
|
|
39148
|
+
kind: "toggle",
|
|
39149
|
+
label: "Bar change values",
|
|
39150
|
+
id: "change",
|
|
39151
|
+
get: () => sl.partVisible("change"),
|
|
39152
|
+
set: (v) => this.setStatuslinePart("change", v)
|
|
39153
|
+
},
|
|
38160
39154
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
38161
39155
|
{
|
|
38162
39156
|
kind: "toggle",
|
|
@@ -38209,7 +39203,7 @@ var ChartCell = class {
|
|
|
38209
39203
|
statusPrefs() {
|
|
38210
39204
|
const sl = this.statusline;
|
|
38211
39205
|
return {
|
|
38212
|
-
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
39206
|
+
parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
38213
39207
|
indicatorTitles: this.indicatorTitlesOn,
|
|
38214
39208
|
indicatorValues: this.indicatorValuesOn
|
|
38215
39209
|
};
|
|
@@ -38349,9 +39343,9 @@ var ChartCell = class {
|
|
|
38349
39343
|
this.manifest = list;
|
|
38350
39344
|
if (this.pendingManifestNames) {
|
|
38351
39345
|
if (list.length === 0) return;
|
|
38352
|
-
for (const
|
|
38353
|
-
const entry = list.find((e) => e.name ===
|
|
38354
|
-
if (entry) this.addManifestInstance(entry, { record: false });
|
|
39346
|
+
for (const led of this.pendingManifestNames) {
|
|
39347
|
+
const entry = list.find((e) => e.name === ledgerEntryName(led));
|
|
39348
|
+
if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
|
|
38355
39349
|
}
|
|
38356
39350
|
this.pendingManifestNames = null;
|
|
38357
39351
|
return;
|
|
@@ -38381,9 +39375,10 @@ var ChartCell = class {
|
|
|
38381
39375
|
}
|
|
38382
39376
|
for (const it of [...this.instances]) this.dropInstance(it);
|
|
38383
39377
|
if (this.manifest.length > 0) {
|
|
38384
|
-
for (const
|
|
38385
|
-
const entry = this.manifest.find((e) => e.name ===
|
|
38386
|
-
if (entry)
|
|
39378
|
+
for (const item of led.manifest) {
|
|
39379
|
+
const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
|
|
39380
|
+
if (entry)
|
|
39381
|
+
this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
|
|
38387
39382
|
}
|
|
38388
39383
|
this.pendingManifestNames = null;
|
|
38389
39384
|
} else if (!this.deps.manifestSettled()) {
|
|
@@ -38432,12 +39427,16 @@ var ChartCell = class {
|
|
|
38432
39427
|
* a persistence handler's `restore` runs silently, a user-driven call records.
|
|
38433
39428
|
*/
|
|
38434
39429
|
addExternalIndicator(entry) {
|
|
38435
|
-
this.addManifestInstance(
|
|
39430
|
+
this.addManifestInstance(
|
|
39431
|
+
{ ...entry, enabled: true },
|
|
39432
|
+
{ external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} }
|
|
39433
|
+
);
|
|
38436
39434
|
}
|
|
38437
39435
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
38438
39436
|
addManifestInstance(entry, opts = {}) {
|
|
38439
39437
|
if (this.destroyed) return;
|
|
38440
|
-
const
|
|
39438
|
+
const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
|
|
39439
|
+
const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
|
|
38441
39440
|
this.instances.push(it);
|
|
38442
39441
|
this.deps.onIndicatorsChanged(this.id);
|
|
38443
39442
|
if (opts.record === false) return;
|
|
@@ -38445,7 +39444,7 @@ var ChartCell = class {
|
|
|
38445
39444
|
this.history.push({
|
|
38446
39445
|
undo: () => this.dropInstance(snapshot),
|
|
38447
39446
|
redo: () => {
|
|
38448
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
39447
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
38449
39448
|
this.instances.push(snapshot);
|
|
38450
39449
|
this.deps.onIndicatorsChanged(this.id);
|
|
38451
39450
|
}
|
|
@@ -38458,7 +39457,7 @@ var ChartCell = class {
|
|
|
38458
39457
|
const snapshot = it;
|
|
38459
39458
|
this.history.push({
|
|
38460
39459
|
undo: () => {
|
|
38461
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
39460
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
38462
39461
|
this.instances.push(snapshot);
|
|
38463
39462
|
this.deps.onIndicatorsChanged(this.id);
|
|
38464
39463
|
},
|
|
@@ -38468,6 +39467,9 @@ var ChartCell = class {
|
|
|
38468
39467
|
dropInstance(it) {
|
|
38469
39468
|
const idx = this.instances.indexOf(it);
|
|
38470
39469
|
if (idx >= 0) this.instances.splice(idx, 1);
|
|
39470
|
+
const captured = instanceDeltas(it.handle);
|
|
39471
|
+
if (captured) it.values = captured;
|
|
39472
|
+
else delete it.values;
|
|
38471
39473
|
try {
|
|
38472
39474
|
it.handle?.remove();
|
|
38473
39475
|
} catch {
|
|
@@ -38510,9 +39512,13 @@ var ChartCell = class {
|
|
|
38510
39512
|
this.deps.onIndicatorsChanged(this.id);
|
|
38511
39513
|
});
|
|
38512
39514
|
}
|
|
38513
|
-
addToChart(entry) {
|
|
39515
|
+
addToChart(entry, values) {
|
|
38514
39516
|
try {
|
|
38515
|
-
return this.inner?.addIndicator(entry.script,
|
|
39517
|
+
return this.inner?.addIndicator(entry.script, {
|
|
39518
|
+
...entry.language !== void 0 ? { language: entry.language } : {},
|
|
39519
|
+
...values?.inputs ? { inputs: values.inputs } : {},
|
|
39520
|
+
...values?.props ? { props: values.props } : {}
|
|
39521
|
+
}) ?? null;
|
|
38516
39522
|
} catch (err) {
|
|
38517
39523
|
console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
|
|
38518
39524
|
return null;
|
|
@@ -38626,7 +39632,10 @@ var ChartCell = class {
|
|
|
38626
39632
|
// manifest — their plugin persists them via the `ext` seam instead.
|
|
38627
39633
|
indicators: indicatorLedger({
|
|
38628
39634
|
present: this.inner ? this.inner.presentNativeIndicators() : [],
|
|
38629
|
-
|
|
39635
|
+
instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
|
|
39636
|
+
const d = it.handle ? instanceDeltas(it.handle) : it.values;
|
|
39637
|
+
return d ? { name: it.entry.name, ...d } : it.entry.name;
|
|
39638
|
+
}),
|
|
38630
39639
|
pendingManifest: this.pendingManifestNames,
|
|
38631
39640
|
manifestSettled: this.deps.manifestSettled(),
|
|
38632
39641
|
volumePending: this.volumeMayBePending && this.volumeIntent
|
|
@@ -38927,7 +39936,7 @@ var SplitterLayer = class {
|
|
|
38927
39936
|
};
|
|
38928
39937
|
|
|
38929
39938
|
// src/workspace/VelaWorkspace.ts
|
|
38930
|
-
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
39939
|
+
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
|
|
38931
39940
|
var GAP_PX = 2;
|
|
38932
39941
|
var POOL_CAP = 16;
|
|
38933
39942
|
var TIME_AXIS_H4 = 22;
|