@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/workspace.cjs
CHANGED
|
@@ -677,6 +677,7 @@ function intervalMs(timeframe) {
|
|
|
677
677
|
"4h": 144e5,
|
|
678
678
|
"1d": 864e5,
|
|
679
679
|
"1w": 6048e5,
|
|
680
|
+
"1M": 2592e6,
|
|
680
681
|
"1": 6e4,
|
|
681
682
|
"5": 3e5,
|
|
682
683
|
"15": 9e5,
|
|
@@ -684,7 +685,8 @@ function intervalMs(timeframe) {
|
|
|
684
685
|
"60": 36e5,
|
|
685
686
|
"240": 144e5,
|
|
686
687
|
D: 864e5,
|
|
687
|
-
W: 6048e5
|
|
688
|
+
W: 6048e5,
|
|
689
|
+
M: 2592e6
|
|
688
690
|
};
|
|
689
691
|
return map[timeframe] ?? 36e5;
|
|
690
692
|
}
|
|
@@ -11187,6 +11189,111 @@ function roundFloat(n) {
|
|
|
11187
11189
|
return Math.round(n * 1e8) / 1e8;
|
|
11188
11190
|
}
|
|
11189
11191
|
|
|
11192
|
+
// src/core/drawings/types/Magnifier.ts
|
|
11193
|
+
var MAGNIFIER_TIMEFRAME_OPTIONS = [
|
|
11194
|
+
{ value: "auto", label: "Auto", ms: 0 },
|
|
11195
|
+
{ value: "1", label: "1m", ms: 6e4 },
|
|
11196
|
+
{ value: "5", label: "5m", ms: 3e5 },
|
|
11197
|
+
{ value: "15", label: "15m", ms: 9e5 },
|
|
11198
|
+
{ value: "30", label: "30m", ms: 18e5 },
|
|
11199
|
+
{ value: "60", label: "1h", ms: 36e5 },
|
|
11200
|
+
{ value: "240", label: "4h", ms: 144e5 },
|
|
11201
|
+
{ value: "D", label: "1D", ms: 864e5 }
|
|
11202
|
+
];
|
|
11203
|
+
function magnifierTimeframeLabel(value) {
|
|
11204
|
+
const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
|
|
11205
|
+
if (opt) return opt.label;
|
|
11206
|
+
const n = Number(value);
|
|
11207
|
+
if (Number.isFinite(n) && n > 0) {
|
|
11208
|
+
if (n % 1440 === 0) return `${n / 1440}D`;
|
|
11209
|
+
if (n % 60 === 0) return `${n / 60}h`;
|
|
11210
|
+
return `${n}m`;
|
|
11211
|
+
}
|
|
11212
|
+
return value;
|
|
11213
|
+
}
|
|
11214
|
+
function defaultMagnifierStyle() {
|
|
11215
|
+
return { timeframe: "auto", upColor: "", downColor: "" };
|
|
11216
|
+
}
|
|
11217
|
+
var Magnifier = class extends Drawing {
|
|
11218
|
+
constructor(init) {
|
|
11219
|
+
super(init);
|
|
11220
|
+
this.type = "magnifier";
|
|
11221
|
+
/** Pixel rect of the timeframe chip as painted last frame, caret included — the chip is
|
|
11222
|
+
* an interactive dropdown trigger, so the interaction layer needs the exact rect the
|
|
11223
|
+
* painter measured. Renderer-transient: never serialized, null while unpainted. */
|
|
11224
|
+
this.chipRect = null;
|
|
11225
|
+
if (!this.magnifier) this.magnifier = defaultMagnifierStyle();
|
|
11226
|
+
}
|
|
11227
|
+
anchorSchema() {
|
|
11228
|
+
return { min: 2, max: 2, slots: [{ role: "c1", free: "both" }, { role: "c2", free: "both" }] };
|
|
11229
|
+
}
|
|
11230
|
+
placementMode() {
|
|
11231
|
+
return "drag";
|
|
11232
|
+
}
|
|
11233
|
+
/** The pixel rectangle between the two corner anchors (painter + hit-test share it). */
|
|
11234
|
+
rect(proj) {
|
|
11235
|
+
const a = this.anchors[0];
|
|
11236
|
+
const b = this.anchors[1];
|
|
11237
|
+
if (!a || !b) return null;
|
|
11238
|
+
const ya = proj.yOf(a.price, this.paneId);
|
|
11239
|
+
const yb = proj.yOf(b.price, this.paneId);
|
|
11240
|
+
if (ya == null || yb == null) return null;
|
|
11241
|
+
return { x1: proj.xOf(a.time), y1: ya, x2: proj.xOf(b.time), y2: yb };
|
|
11242
|
+
}
|
|
11243
|
+
hitTest(px, py, proj, tol) {
|
|
11244
|
+
const r = this.rect(proj);
|
|
11245
|
+
if (!r) return false;
|
|
11246
|
+
if (pointInBox(px, py, r.x1, r.y1, r.x2, r.y2)) return true;
|
|
11247
|
+
const edges = [
|
|
11248
|
+
[r.x1, r.y1, r.x2, r.y1],
|
|
11249
|
+
[r.x2, r.y1, r.x2, r.y2],
|
|
11250
|
+
[r.x2, r.y2, r.x1, r.y2],
|
|
11251
|
+
[r.x1, r.y2, r.x1, r.y1]
|
|
11252
|
+
];
|
|
11253
|
+
return edges.some((e) => distToSegment(px, py, e[0], e[1], e[2], e[3]) <= tol);
|
|
11254
|
+
}
|
|
11255
|
+
handlePoints(proj) {
|
|
11256
|
+
const r = this.rect(proj);
|
|
11257
|
+
return r ? [[r.x1, r.y1], [r.x2, r.y2]] : [];
|
|
11258
|
+
}
|
|
11259
|
+
hitHandle(px, py, proj, tol) {
|
|
11260
|
+
return handleAt(px, py, this.handlePoints(proj), tol + 3);
|
|
11261
|
+
}
|
|
11262
|
+
bounds(proj) {
|
|
11263
|
+
const r = this.rect(proj);
|
|
11264
|
+
if (!r) return null;
|
|
11265
|
+
return { x: Math.min(r.x1, r.x2), y: Math.min(r.y1, r.y2), w: Math.abs(r.x2 - r.x1), h: Math.abs(r.y2 - r.y1) };
|
|
11266
|
+
}
|
|
11267
|
+
priceRange() {
|
|
11268
|
+
const a = this.anchors[0];
|
|
11269
|
+
const b = this.anchors[1];
|
|
11270
|
+
if (!a || !b) return null;
|
|
11271
|
+
return { min: Math.min(a.price, b.price), max: Math.max(a.price, b.price) };
|
|
11272
|
+
}
|
|
11273
|
+
schema() {
|
|
11274
|
+
return {
|
|
11275
|
+
fields: [
|
|
11276
|
+
{
|
|
11277
|
+
path: "magnifier.timeframe",
|
|
11278
|
+
label: "Timeframe",
|
|
11279
|
+
kind: "select",
|
|
11280
|
+
options: MAGNIFIER_TIMEFRAME_OPTIONS,
|
|
11281
|
+
group: "behavior"
|
|
11282
|
+
},
|
|
11283
|
+
...LINE_FIELDS.map((f) => ({ ...f, label: f.label.replace("Line", "Border") })),
|
|
11284
|
+
{ path: "magnifier.upColor", label: "Up candles", kind: "color", group: "fill" },
|
|
11285
|
+
{ path: "magnifier.downColor", label: "Down candles", kind: "color", group: "fill" }
|
|
11286
|
+
]
|
|
11287
|
+
};
|
|
11288
|
+
}
|
|
11289
|
+
writeProps() {
|
|
11290
|
+
return { ...this.magnifier };
|
|
11291
|
+
}
|
|
11292
|
+
readProps(props) {
|
|
11293
|
+
this.magnifier = { ...defaultMagnifierStyle(), ...props };
|
|
11294
|
+
}
|
|
11295
|
+
};
|
|
11296
|
+
|
|
11190
11297
|
// src/core/drawings/registry.ts
|
|
11191
11298
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
11192
11299
|
function registerDrawingType(meta) {
|
|
@@ -11846,6 +11953,22 @@ registerDrawingType({
|
|
|
11846
11953
|
defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
|
|
11847
11954
|
create: (init) => new PositionTool(init)
|
|
11848
11955
|
});
|
|
11956
|
+
var MAGNIFIER_ICON = svg24(
|
|
11957
|
+
'<circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.3 15.3 5.2 5.2"/><path d="M8 12.5v-3M10.5 13.5v-5.5M13 12v-2"/>'
|
|
11958
|
+
);
|
|
11959
|
+
registerDrawingType({
|
|
11960
|
+
type: "magnifier",
|
|
11961
|
+
group: "measure",
|
|
11962
|
+
label: "Magnifier",
|
|
11963
|
+
icon: MAGNIFIER_ICON,
|
|
11964
|
+
// An empty border color means the THEME's contrast ink (white on dark, black on
|
|
11965
|
+
// light), resolved at paint time so it follows theme switches; a user pick wins.
|
|
11966
|
+
defaultStyle: { lineColor: "", lineWidth: 1, lineStyle: "solid" },
|
|
11967
|
+
coversSeries: true,
|
|
11968
|
+
// the inset's backdrop must sit over the base candles it replaces
|
|
11969
|
+
placementHint: "Drag an area on the chart to view it at a lower timeframe",
|
|
11970
|
+
create: (init) => new Magnifier(init)
|
|
11971
|
+
});
|
|
11849
11972
|
var VWAP_ICON = svg24('<line x1="5" y1="3" x2="5" y2="21"/><path d="M5 16c4 0 5-9 8-9s3 5 8 3"/>');
|
|
11850
11973
|
registerDrawingType({
|
|
11851
11974
|
type: "anchoredvwap",
|
|
@@ -16051,7 +16174,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
|
|
|
16051
16174
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
16052
16175
|
var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
|
|
16053
16176
|
var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
|
|
16054
|
-
var MEASUREMENT_TYPES = ["position", "datepricerange"];
|
|
16177
|
+
var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
|
|
16055
16178
|
var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
|
|
16056
16179
|
var BRUSH_TYPES = ["freehand", "highlighter"];
|
|
16057
16180
|
var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
|
|
@@ -16187,7 +16310,7 @@ function buildToolbar(option) {
|
|
|
16187
16310
|
|
|
16188
16311
|
// src/core/drawings/DrawingController.ts
|
|
16189
16312
|
var DrawingController = class {
|
|
16190
|
-
constructor(renderer, events, option) {
|
|
16313
|
+
constructor(renderer, events, option, seriesGateway) {
|
|
16191
16314
|
this.events = events;
|
|
16192
16315
|
this.store = new DrawingStore();
|
|
16193
16316
|
this.history = new DrawingHistory();
|
|
@@ -16213,6 +16336,7 @@ var DrawingController = class {
|
|
|
16213
16336
|
const { definition, visible } = buildToolbar(option);
|
|
16214
16337
|
this.port.setToolbar(definition);
|
|
16215
16338
|
this.port.showToolbar(visible);
|
|
16339
|
+
if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
|
|
16216
16340
|
this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
|
|
16217
16341
|
this.subs.push(this.store.onChange(() => this.sync()));
|
|
16218
16342
|
}
|
|
@@ -16318,7 +16442,7 @@ var DrawingController = class {
|
|
|
16318
16442
|
style,
|
|
16319
16443
|
text: init.text,
|
|
16320
16444
|
props: init.props,
|
|
16321
|
-
zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
|
|
16445
|
+
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
16322
16446
|
});
|
|
16323
16447
|
if (!d) return null;
|
|
16324
16448
|
this.history.record(this.store.serialize());
|
|
@@ -16393,10 +16517,14 @@ var DrawingController = class {
|
|
|
16393
16517
|
* (falling back to just under the pane's top series where there is no price — a study
|
|
16394
16518
|
* pane). Half a key down never ties a series; drawings tying each other paint in insertion
|
|
16395
16519
|
* order, so consecutive new drawings still stack newest-in-front. Undefined without a
|
|
16396
|
-
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
16397
|
-
|
|
16520
|
+
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
16521
|
+
* A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
|
|
16522
|
+
* above the whole stack — under the candles its content would be buried. */
|
|
16523
|
+
startZ(type, paneId) {
|
|
16398
16524
|
const range = this.port?.stackRange?.(paneId);
|
|
16399
|
-
|
|
16525
|
+
if (!range) return void 0;
|
|
16526
|
+
if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
|
|
16527
|
+
return (range.price ?? range.front) - 0.5;
|
|
16400
16528
|
}
|
|
16401
16529
|
/** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
|
|
16402
16530
|
* `additive` toggles membership (matching shift-click) instead of replacing. */
|
|
@@ -16541,7 +16669,7 @@ var DrawingController = class {
|
|
|
16541
16669
|
const style = last ? { ...i.doc.style, ...last } : i.doc.style;
|
|
16542
16670
|
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
16543
16671
|
if (!d) return;
|
|
16544
|
-
if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
|
|
16672
|
+
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
16545
16673
|
this.history.record(before);
|
|
16546
16674
|
this.store.add(d);
|
|
16547
16675
|
this.captureStyle(d.id);
|
|
@@ -16798,11 +16926,12 @@ async function resolveIndicators(config, fetchImpl = fetch) {
|
|
|
16798
16926
|
}
|
|
16799
16927
|
return out;
|
|
16800
16928
|
}
|
|
16929
|
+
var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
|
|
16801
16930
|
function indicatorLedger(i) {
|
|
16802
16931
|
const natives = [...i.present];
|
|
16803
16932
|
if (i.volumePending && !natives.includes("volume")) natives.push("volume");
|
|
16804
16933
|
return {
|
|
16805
|
-
manifest: i.manifestSettled ? [...i.
|
|
16934
|
+
manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
|
|
16806
16935
|
natives
|
|
16807
16936
|
};
|
|
16808
16937
|
}
|
|
@@ -17736,7 +17865,17 @@ function sanitizeCell(raw) {
|
|
|
17736
17865
|
if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
|
|
17737
17866
|
const ind = c.indicators;
|
|
17738
17867
|
if (ind != null && typeof ind === "object") {
|
|
17739
|
-
const manifest = Array.isArray(ind.manifest) ? ind.manifest.
|
|
17868
|
+
const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
|
|
17869
|
+
if (typeof n === "string") return [n];
|
|
17870
|
+
if (n != null && typeof n === "object" && typeof n.name === "string") {
|
|
17871
|
+
const e = n;
|
|
17872
|
+
const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
17873
|
+
const inputs = bag(e.inputs);
|
|
17874
|
+
const props = bag(e.props);
|
|
17875
|
+
return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
|
|
17876
|
+
}
|
|
17877
|
+
return [];
|
|
17878
|
+
}) : [];
|
|
17740
17879
|
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
17741
17880
|
out.indicators = { manifest, natives };
|
|
17742
17881
|
}
|
|
@@ -17935,6 +18074,12 @@ var IndicatorHandleImpl = class {
|
|
|
17935
18074
|
get visible() {
|
|
17936
18075
|
return this.visibleState;
|
|
17937
18076
|
}
|
|
18077
|
+
inputValues() {
|
|
18078
|
+
return this.controller.inputValuesOf(this.id);
|
|
18079
|
+
}
|
|
18080
|
+
propValues() {
|
|
18081
|
+
return this.controller.propValuesOf(this.id);
|
|
18082
|
+
}
|
|
17938
18083
|
setInput(key, value) {
|
|
17939
18084
|
this.controller.applyInputs(this.id, { [key]: value });
|
|
17940
18085
|
}
|
|
@@ -18056,6 +18201,180 @@ function presetToRange(preset, bars) {
|
|
|
18056
18201
|
return { from: Math.max(first, from), to: last };
|
|
18057
18202
|
}
|
|
18058
18203
|
|
|
18204
|
+
// src/core/engine/DrawingSeriesService.ts
|
|
18205
|
+
var MAX_BARS = 5e3;
|
|
18206
|
+
var PAD_FRAC = 0.25;
|
|
18207
|
+
var MAX_ENTRIES = 16;
|
|
18208
|
+
var RETRY_MS = 15e3;
|
|
18209
|
+
var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
|
|
18210
|
+
var DrawingSeriesService = class {
|
|
18211
|
+
constructor(deps) {
|
|
18212
|
+
this.deps = deps;
|
|
18213
|
+
/** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
|
|
18214
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
18215
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
18216
|
+
}
|
|
18217
|
+
seriesInRange(timeframe, from, to) {
|
|
18218
|
+
if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
|
|
18219
|
+
const resolved = this.resolveTimeframe(timeframe);
|
|
18220
|
+
if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
|
|
18221
|
+
const barMs = timeframeToMs(resolved);
|
|
18222
|
+
const lo = Math.min(from, to);
|
|
18223
|
+
const hi = Math.max(from, to);
|
|
18224
|
+
if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
|
|
18225
|
+
if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
|
|
18226
|
+
const key = `${this.deps.marketKey()}|${resolved}`;
|
|
18227
|
+
const entries = this.cache.get(key) ?? [];
|
|
18228
|
+
const covering = entries.find((e) => e.from <= lo && e.to >= hi);
|
|
18229
|
+
if (covering) {
|
|
18230
|
+
if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
|
|
18231
|
+
if (covering.failedAt > 0) {
|
|
18232
|
+
if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
|
|
18233
|
+
entries.splice(entries.indexOf(covering), 1);
|
|
18234
|
+
} else {
|
|
18235
|
+
this.maybeRefresh(key, covering, resolved, barMs, hi);
|
|
18236
|
+
return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
|
|
18237
|
+
}
|
|
18238
|
+
}
|
|
18239
|
+
this.fetchWindow(key, entries, resolved, lo, hi);
|
|
18240
|
+
return this.loading(entries, resolved, barMs, lo, hi);
|
|
18241
|
+
}
|
|
18242
|
+
onUpdate(listener) {
|
|
18243
|
+
this.listeners.add(listener);
|
|
18244
|
+
return () => this.listeners.delete(listener);
|
|
18245
|
+
}
|
|
18246
|
+
// ── internals ──
|
|
18247
|
+
/** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
|
|
18248
|
+
* step still below it); an explicit timeframe passes only when strictly finer. Failures
|
|
18249
|
+
* distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
|
|
18250
|
+
* at the finest offered step) so the consumer can word its notice honestly. */
|
|
18251
|
+
resolveTimeframe(timeframe) {
|
|
18252
|
+
const chartMs = timeframeToMs(this.deps.chartTimeframe());
|
|
18253
|
+
const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
|
|
18254
|
+
if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
|
|
18255
|
+
const tf = timeframe.trim() || "auto";
|
|
18256
|
+
if (tf === "auto") {
|
|
18257
|
+
for (const step of AUTO_STEPS) {
|
|
18258
|
+
if (timeframeToMs(step) <= chartMs / 4) return step;
|
|
18259
|
+
}
|
|
18260
|
+
return finest;
|
|
18261
|
+
}
|
|
18262
|
+
return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
|
|
18263
|
+
}
|
|
18264
|
+
/** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
|
|
18265
|
+
* windows — a widened window keeps painting what it already has while it fetches. */
|
|
18266
|
+
loading(entries, timeframe, barMs, lo, hi) {
|
|
18267
|
+
const partial = /* @__PURE__ */ new Map();
|
|
18268
|
+
for (const e of entries) {
|
|
18269
|
+
if (e.pending || e.failedAt > 0) continue;
|
|
18270
|
+
if (e.to < lo || e.from > hi) continue;
|
|
18271
|
+
for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
|
|
18272
|
+
}
|
|
18273
|
+
if (partial.size === 0) return { state: "loading", timeframe, barMs };
|
|
18274
|
+
const bars = [...partial.values()].sort((a, b) => a.time - b.time);
|
|
18275
|
+
return { state: "loading", timeframe, barMs, bars };
|
|
18276
|
+
}
|
|
18277
|
+
/** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
|
|
18278
|
+
* defers this one (a corner drag repaints per pointer move — kicking a window per
|
|
18279
|
+
* frame would spam the provider); when it lands, the next paint re-evaluates. */
|
|
18280
|
+
fetchWindow(key, entries, timeframe, lo, hi) {
|
|
18281
|
+
if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
|
|
18282
|
+
const pad = (hi - lo) * PAD_FRAC;
|
|
18283
|
+
const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
|
|
18284
|
+
entries.push(entry);
|
|
18285
|
+
this.cache.set(key, entries);
|
|
18286
|
+
this.evict();
|
|
18287
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
18288
|
+
entry.bars = bars;
|
|
18289
|
+
entry.fetchedAt = Date.now();
|
|
18290
|
+
entry.pending = false;
|
|
18291
|
+
this.absorbOverlaps(key, entry);
|
|
18292
|
+
this.fire();
|
|
18293
|
+
}).catch(() => {
|
|
18294
|
+
entry.pending = false;
|
|
18295
|
+
entry.failedAt = Date.now();
|
|
18296
|
+
this.fire();
|
|
18297
|
+
});
|
|
18298
|
+
}
|
|
18299
|
+
/** A window whose right edge reaches the newest fetched bar refreshes at most once per
|
|
18300
|
+
* bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
|
|
18301
|
+
maybeRefresh(key, entry, timeframe, barMs, hi) {
|
|
18302
|
+
if (entry.pending) return;
|
|
18303
|
+
const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
|
|
18304
|
+
if (hi < lastBar) return;
|
|
18305
|
+
if (Date.now() - entry.fetchedAt < barMs) return;
|
|
18306
|
+
entry.pending = true;
|
|
18307
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
18308
|
+
entry.bars = bars;
|
|
18309
|
+
entry.fetchedAt = Date.now();
|
|
18310
|
+
entry.pending = false;
|
|
18311
|
+
this.fire();
|
|
18312
|
+
}).catch(() => {
|
|
18313
|
+
entry.pending = false;
|
|
18314
|
+
entry.fetchedAt = Date.now();
|
|
18315
|
+
});
|
|
18316
|
+
}
|
|
18317
|
+
/** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
|
|
18318
|
+
* converges instead of accumulating slivers. */
|
|
18319
|
+
absorbOverlaps(key, entry) {
|
|
18320
|
+
const entries = this.cache.get(key);
|
|
18321
|
+
if (!entries) return;
|
|
18322
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
18323
|
+
const other = entries[i];
|
|
18324
|
+
if (other === entry || other.pending || other.failedAt > 0) continue;
|
|
18325
|
+
if (other.to < entry.from || other.from > entry.to) continue;
|
|
18326
|
+
const byTime = /* @__PURE__ */ new Map();
|
|
18327
|
+
for (const b of other.bars) byTime.set(b.time, b);
|
|
18328
|
+
for (const b of entry.bars) byTime.set(b.time, b);
|
|
18329
|
+
entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
18330
|
+
entry.from = Math.min(entry.from, other.from);
|
|
18331
|
+
entry.to = Math.max(entry.to, other.to);
|
|
18332
|
+
entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
|
|
18333
|
+
entries.splice(i, 1);
|
|
18334
|
+
}
|
|
18335
|
+
}
|
|
18336
|
+
/** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
|
|
18337
|
+
evict() {
|
|
18338
|
+
let total = 0;
|
|
18339
|
+
for (const entries of this.cache.values()) total += entries.length;
|
|
18340
|
+
while (total > MAX_ENTRIES) {
|
|
18341
|
+
let oldestKey = null;
|
|
18342
|
+
let oldestIdx = -1;
|
|
18343
|
+
let oldestAt = Infinity;
|
|
18344
|
+
for (const [key, entries2] of this.cache) {
|
|
18345
|
+
for (let i = 0; i < entries2.length; i += 1) {
|
|
18346
|
+
const e = entries2[i];
|
|
18347
|
+
if (e.pending) continue;
|
|
18348
|
+
const at = e.fetchedAt || e.failedAt;
|
|
18349
|
+
if (at < oldestAt) {
|
|
18350
|
+
oldestKey = key;
|
|
18351
|
+
oldestIdx = i;
|
|
18352
|
+
oldestAt = at;
|
|
18353
|
+
}
|
|
18354
|
+
}
|
|
18355
|
+
}
|
|
18356
|
+
if (oldestKey == null) return;
|
|
18357
|
+
const entries = this.cache.get(oldestKey);
|
|
18358
|
+
entries.splice(oldestIdx, 1);
|
|
18359
|
+
if (entries.length === 0) this.cache.delete(oldestKey);
|
|
18360
|
+
total -= 1;
|
|
18361
|
+
}
|
|
18362
|
+
}
|
|
18363
|
+
/** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
|
|
18364
|
+
slice(bars, lo, hi) {
|
|
18365
|
+
const out = [];
|
|
18366
|
+
for (const b of bars) {
|
|
18367
|
+
if (b.time < lo) continue;
|
|
18368
|
+
if (b.time > hi) break;
|
|
18369
|
+
out.push(b);
|
|
18370
|
+
}
|
|
18371
|
+
return out;
|
|
18372
|
+
}
|
|
18373
|
+
fire() {
|
|
18374
|
+
for (const l of [...this.listeners]) l();
|
|
18375
|
+
}
|
|
18376
|
+
};
|
|
18377
|
+
|
|
18059
18378
|
// src/core/price-styles/BarTransform.ts
|
|
18060
18379
|
function barTransformFor(style) {
|
|
18061
18380
|
return chartType(style)?.barTransform ?? null;
|
|
@@ -18074,6 +18393,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
18074
18393
|
var PREVIEW_BARS = 300;
|
|
18075
18394
|
var SINGLE_LOAD_BARS = 5e3;
|
|
18076
18395
|
var CHUNK_BARS = 1e4;
|
|
18396
|
+
var FIRST_PAINT_BARS = 100;
|
|
18077
18397
|
var GAP_FACTOR = 1.5;
|
|
18078
18398
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
18079
18399
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -18191,7 +18511,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18191
18511
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
18192
18512
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
18193
18513
|
this.barTransform = barTransformFor(initialStyle);
|
|
18194
|
-
|
|
18514
|
+
const drawingSeries = new DrawingSeriesService({
|
|
18515
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
18516
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
18517
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
18518
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
18519
|
+
});
|
|
18520
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
18195
18521
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
18196
18522
|
this.endLoad();
|
|
18197
18523
|
this.events.emit("data:unresolved", info);
|
|
@@ -18343,6 +18669,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18343
18669
|
let painted = false;
|
|
18344
18670
|
const paint = (bars, final) => {
|
|
18345
18671
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18672
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
18346
18673
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18347
18674
|
if (!painted && bars.length > 0) {
|
|
18348
18675
|
painted = true;
|
|
@@ -18973,6 +19300,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18973
19300
|
record.pendingCause = "inputs";
|
|
18974
19301
|
if (record.session) record.session.update(record.inputValues);
|
|
18975
19302
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
19303
|
+
this.events.emit("indicator:inputs", { id });
|
|
19304
|
+
}
|
|
19305
|
+
/** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
|
|
19306
|
+
inputValuesOf(id) {
|
|
19307
|
+
return { ...this.registry.get(id)?.inputValues };
|
|
19308
|
+
}
|
|
19309
|
+
/** IndicatorController: the CURRENT declaration-prop overrides. */
|
|
19310
|
+
propValuesOf(id) {
|
|
19311
|
+
return { ...this.registry.get(id)?.propValues };
|
|
18976
19312
|
}
|
|
18977
19313
|
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
18978
19314
|
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
@@ -18986,6 +19322,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18986
19322
|
if (!record.hidden) this.setLoading(record, true);
|
|
18987
19323
|
record.pendingCause = "inputs";
|
|
18988
19324
|
record.session.update(record.inputValues, record.propValues);
|
|
19325
|
+
this.events.emit("indicator:inputs", { id });
|
|
18989
19326
|
}
|
|
18990
19327
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
18991
19328
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
@@ -19173,8 +19510,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19173
19510
|
onModel: (model) => {
|
|
19174
19511
|
const first = !record.announced;
|
|
19175
19512
|
const cause = record.pendingCause ?? "history";
|
|
19513
|
+
if (!this.applyModel(id, model)) return;
|
|
19176
19514
|
record.pendingCause = void 0;
|
|
19177
|
-
this.applyModel(id, model);
|
|
19178
19515
|
this.emitContextChanged(id);
|
|
19179
19516
|
this.emitScriptRun(id, cause, first);
|
|
19180
19517
|
},
|
|
@@ -19467,10 +19804,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19467
19804
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
19468
19805
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
19469
19806
|
* (live tick / viewport re-run) value-patches.
|
|
19807
|
+
*
|
|
19808
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
19809
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
19810
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
19811
|
+
* stay unchanged.
|
|
19470
19812
|
*/
|
|
19471
19813
|
applyModel(id, model) {
|
|
19472
19814
|
const record = this.registry.get(id);
|
|
19473
|
-
if (!record || record.hidden) return;
|
|
19815
|
+
if (!record || record.hidden) return true;
|
|
19816
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
19474
19817
|
const handle = this.handles.get(id);
|
|
19475
19818
|
if (!record.renderHandle) {
|
|
19476
19819
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -19480,7 +19823,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19480
19823
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19481
19824
|
record.pendingStructural = false;
|
|
19482
19825
|
this.announce(record, handle);
|
|
19483
|
-
return;
|
|
19826
|
+
return true;
|
|
19484
19827
|
}
|
|
19485
19828
|
let paneId = record.model?.paneId ?? "price";
|
|
19486
19829
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -19508,6 +19851,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19508
19851
|
}
|
|
19509
19852
|
if (record.loading) this.setLoading(record, false);
|
|
19510
19853
|
this.announce(record, handle);
|
|
19854
|
+
return true;
|
|
19855
|
+
}
|
|
19856
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
19857
|
+
static modelHasOutput(model) {
|
|
19858
|
+
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;
|
|
19511
19859
|
}
|
|
19512
19860
|
routePane(id, model, options) {
|
|
19513
19861
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -20207,6 +20555,14 @@ function inputVisible(when, values) {
|
|
|
20207
20555
|
const conds = Array.isArray(when) ? when : [when];
|
|
20208
20556
|
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
|
|
20209
20557
|
}
|
|
20558
|
+
function inputDeltas(schema, values) {
|
|
20559
|
+
const out = {};
|
|
20560
|
+
for (const s of schema) {
|
|
20561
|
+
const v = values[s.key];
|
|
20562
|
+
if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
|
|
20563
|
+
}
|
|
20564
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
20565
|
+
}
|
|
20210
20566
|
|
|
20211
20567
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
20212
20568
|
var PROPS_TAB = "Properties";
|
|
@@ -23794,6 +24150,7 @@ var DOUBLE_TAP_MS = 350;
|
|
|
23794
24150
|
var DOUBLE_TAP_SLOP = 30;
|
|
23795
24151
|
var TIME_SCALE_K = 4e-3;
|
|
23796
24152
|
var WHEEL_ZOOM_K = 4e-3;
|
|
24153
|
+
var WHEEL_PRICE_DRAG_PX = 0.25;
|
|
23797
24154
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
23798
24155
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
23799
24156
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -23924,7 +24281,7 @@ var InputController = class {
|
|
|
23924
24281
|
this.capture(e.pointerId);
|
|
23925
24282
|
return;
|
|
23926
24283
|
}
|
|
23927
|
-
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
|
|
24284
|
+
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
23928
24285
|
this.region = "drawing";
|
|
23929
24286
|
this.capture(e.pointerId);
|
|
23930
24287
|
return;
|
|
@@ -24038,7 +24395,7 @@ var InputController = class {
|
|
|
24038
24395
|
const wasTouch = e.pointerType === "touch";
|
|
24039
24396
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
24040
24397
|
if (this.dragging && this.region === "drawing") {
|
|
24041
|
-
this.deps.drawingsPointerUp?.(x, y);
|
|
24398
|
+
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
24042
24399
|
} else if (tapRelease && this.region === "data") {
|
|
24043
24400
|
this.deps.onClick(x, y);
|
|
24044
24401
|
} else if (this.dragging && this.region === "data") {
|
|
@@ -24059,7 +24416,7 @@ var InputController = class {
|
|
|
24059
24416
|
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
24060
24417
|
this.cancelLongPress();
|
|
24061
24418
|
if (!this.dragging) return;
|
|
24062
|
-
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
24419
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
24063
24420
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
24064
24421
|
this.endGesture(e);
|
|
24065
24422
|
};
|
|
@@ -24075,6 +24432,12 @@ var InputController = class {
|
|
|
24075
24432
|
this.onWheel = (e) => {
|
|
24076
24433
|
e.preventDefault();
|
|
24077
24434
|
this.deps.drawingsClearTransient?.();
|
|
24435
|
+
const { x, y } = this.local(e);
|
|
24436
|
+
if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
|
|
24437
|
+
this.deps.beginPriceScale(x, y);
|
|
24438
|
+
this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
|
|
24439
|
+
return;
|
|
24440
|
+
}
|
|
24078
24441
|
const coords = this.deps.getCoords();
|
|
24079
24442
|
const vp = coords.getViewport();
|
|
24080
24443
|
const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
|
|
@@ -24082,9 +24445,8 @@ var InputController = class {
|
|
|
24082
24445
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
|
|
24083
24446
|
return;
|
|
24084
24447
|
}
|
|
24085
|
-
const cursorX = this.local(e).x;
|
|
24086
24448
|
const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
|
|
24087
|
-
const anchor = wheelZoomAnchor(coords,
|
|
24449
|
+
const anchor = wheelZoomAnchor(coords, x, rightEdge);
|
|
24088
24450
|
const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
|
|
24089
24451
|
this.deps.zoomTo(target, anchor.logical, anchor.x);
|
|
24090
24452
|
};
|
|
@@ -25566,6 +25928,15 @@ function drawingSetEmpty(s) {
|
|
|
25566
25928
|
function fontSizePx(size) {
|
|
25567
25929
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
25568
25930
|
}
|
|
25931
|
+
function lineCoversWindow(a, b, extend, lo, hi) {
|
|
25932
|
+
const minX = Math.min(a, b);
|
|
25933
|
+
const maxX = Math.max(a, b);
|
|
25934
|
+
if (a === b) return a >= lo && a <= hi;
|
|
25935
|
+
if (extend === "both") return true;
|
|
25936
|
+
if (extend === "left") return maxX >= lo;
|
|
25937
|
+
if (extend === "right") return minX <= hi;
|
|
25938
|
+
return maxX >= lo && minX <= hi;
|
|
25939
|
+
}
|
|
25569
25940
|
var DrawingSceneRenderer = class {
|
|
25570
25941
|
constructor(deps, set = EMPTY_DRAWING_SET) {
|
|
25571
25942
|
this.deps = deps;
|
|
@@ -25619,7 +25990,9 @@ var DrawingSceneRenderer = class {
|
|
|
25619
25990
|
const lo = Math.min(from, to);
|
|
25620
25991
|
const hi = Math.max(from, to);
|
|
25621
25992
|
const visible = (a, b, extend) => {
|
|
25622
|
-
if (extend
|
|
25993
|
+
if (extend === "both") return true;
|
|
25994
|
+
if (extend === "left") return Math.max(a, b) >= lo;
|
|
25995
|
+
if (extend === "right") return Math.min(a, b) <= hi;
|
|
25623
25996
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
25624
25997
|
};
|
|
25625
25998
|
let min = Infinity;
|
|
@@ -25630,7 +26003,7 @@ var DrawingSceneRenderer = class {
|
|
|
25630
26003
|
};
|
|
25631
26004
|
for (const ln of this.set.lines) {
|
|
25632
26005
|
if (ln.invisible) continue;
|
|
25633
|
-
if (!
|
|
26006
|
+
if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
|
|
25634
26007
|
fold(ln.y1);
|
|
25635
26008
|
fold(ln.y2);
|
|
25636
26009
|
}
|
|
@@ -28093,6 +28466,17 @@ var DrawingPainter = class {
|
|
|
28093
28466
|
constructor() {
|
|
28094
28467
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
28095
28468
|
this.targets = {};
|
|
28469
|
+
/** The chart's active series LOOK — style + resolved series colors — pushed by the
|
|
28470
|
+
* controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
|
|
28471
|
+
* area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
|
|
28472
|
+
* bars; unknown/custom styles fall back to candles), and the colors default to the main
|
|
28473
|
+
* series' own so the inset reads as a finer copy of the chart. */
|
|
28474
|
+
this.seriesLook = {
|
|
28475
|
+
style: "candles",
|
|
28476
|
+
upColor: BULLISH,
|
|
28477
|
+
downColor: BEARISH,
|
|
28478
|
+
lineColor: BULLISH
|
|
28479
|
+
};
|
|
28096
28480
|
}
|
|
28097
28481
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
28098
28482
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -28137,6 +28521,8 @@ var DrawingPainter = class {
|
|
|
28137
28521
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
28138
28522
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
28139
28523
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
28524
|
+
} else if (ghost instanceof Magnifier) {
|
|
28525
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
28140
28526
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
28141
28527
|
ctx.globalAlpha = 1;
|
|
28142
28528
|
}
|
|
@@ -28270,6 +28656,10 @@ var DrawingPainter = class {
|
|
|
28270
28656
|
this.paintLabel(ctx, d, proj, theme);
|
|
28271
28657
|
return;
|
|
28272
28658
|
}
|
|
28659
|
+
if (d instanceof Magnifier) {
|
|
28660
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
28661
|
+
return;
|
|
28662
|
+
}
|
|
28273
28663
|
if (d instanceof PatternDrawing) {
|
|
28274
28664
|
this.paintPattern(ctx, d, proj, theme);
|
|
28275
28665
|
return;
|
|
@@ -28762,6 +29152,216 @@ var DrawingPainter = class {
|
|
|
28762
29152
|
ctx.textBaseline = "alphabetic";
|
|
28763
29153
|
}
|
|
28764
29154
|
}
|
|
29155
|
+
/** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
|
|
29156
|
+
* no series read, so dragging the area open never kicks a fetch per cursor move. */
|
|
29157
|
+
paintMagnifierGhost(ctx, d, proj, theme) {
|
|
29158
|
+
const r = d.rect(proj);
|
|
29159
|
+
if (!r) return;
|
|
29160
|
+
ctx.save();
|
|
29161
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29162
|
+
ctx.lineWidth = 1;
|
|
29163
|
+
ctx.setLineDash([4, 4]);
|
|
29164
|
+
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));
|
|
29165
|
+
ctx.restore();
|
|
29166
|
+
}
|
|
29167
|
+
/** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
|
|
29168
|
+
* market at a finer timeframe — candles at their true time/price positions, clipped to
|
|
29169
|
+
* the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
|
|
29170
|
+
* `unavailable` states paint a short notice instead). The lower-timeframe candles shift
|
|
29171
|
+
* half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
|
|
29172
|
+
* centered on its open time — subdivides in place. */
|
|
29173
|
+
paintMagnifier(ctx, d, proj, theme) {
|
|
29174
|
+
const r = d.rect(proj);
|
|
29175
|
+
const a = d.anchors[0];
|
|
29176
|
+
const b = d.anchors[1];
|
|
29177
|
+
if (!r || !a || !b) return;
|
|
29178
|
+
const x0 = Math.min(r.x1, r.x2);
|
|
29179
|
+
const x1 = Math.max(r.x1, r.x2);
|
|
29180
|
+
const y0 = Math.min(r.y1, r.y2);
|
|
29181
|
+
const y1 = Math.max(r.y1, r.y2);
|
|
29182
|
+
const w = x1 - x0;
|
|
29183
|
+
const h = y1 - y0;
|
|
29184
|
+
ctx.save();
|
|
29185
|
+
ctx.globalAlpha = 1;
|
|
29186
|
+
ctx.fillStyle = theme.background;
|
|
29187
|
+
ctx.fillRect(x0, y0, w, h);
|
|
29188
|
+
ctx.restore();
|
|
29189
|
+
const from = Math.min(a.time, b.time);
|
|
29190
|
+
const to = Math.max(a.time, b.time);
|
|
29191
|
+
const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
|
|
29192
|
+
const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
|
|
29193
|
+
const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
|
|
29194
|
+
let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
|
|
29195
|
+
if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
|
|
29196
|
+
const look = this.seriesLook;
|
|
29197
|
+
const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
|
|
29198
|
+
if (transform) seriesBars = transform.full(seriesBars);
|
|
29199
|
+
const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
|
|
29200
|
+
const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
|
|
29201
|
+
ctx.save();
|
|
29202
|
+
ctx.beginPath();
|
|
29203
|
+
ctx.rect(x0, y0, w, h);
|
|
29204
|
+
ctx.clip();
|
|
29205
|
+
if (mode === "line" || mode === "area") {
|
|
29206
|
+
this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
|
|
29207
|
+
} else {
|
|
29208
|
+
this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
|
|
29209
|
+
}
|
|
29210
|
+
ctx.restore();
|
|
29211
|
+
} else if (res) {
|
|
29212
|
+
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";
|
|
29213
|
+
this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
|
|
29214
|
+
}
|
|
29215
|
+
ctx.save();
|
|
29216
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29217
|
+
ctx.lineWidth = d.style.lineWidth || 1;
|
|
29218
|
+
ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
|
|
29219
|
+
ctx.strokeRect(x0, y0, w, h);
|
|
29220
|
+
ctx.restore();
|
|
29221
|
+
if (w > 44) {
|
|
29222
|
+
const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
|
|
29223
|
+
const chipH = 17;
|
|
29224
|
+
const gap = 4;
|
|
29225
|
+
const pane = proj.paneRect?.(d.paneId);
|
|
29226
|
+
const paneBottom = pane ? pane.top + pane.height : proj.height;
|
|
29227
|
+
const below = y1 + gap + chipH <= paneBottom;
|
|
29228
|
+
const chipY = below ? y1 + gap : y1 - gap - chipH;
|
|
29229
|
+
ctx.save();
|
|
29230
|
+
ctx.font = `10px ${theme.fontFamily}`;
|
|
29231
|
+
const tw = ctx.measureText(label).width;
|
|
29232
|
+
const caretW = 11;
|
|
29233
|
+
const chipW = tw + 12 + caretW;
|
|
29234
|
+
roundRect(ctx, x0, chipY, chipW, chipH, 3);
|
|
29235
|
+
ctx.fillStyle = theme.background;
|
|
29236
|
+
ctx.fill();
|
|
29237
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29238
|
+
ctx.lineWidth = 1;
|
|
29239
|
+
ctx.setLineDash([]);
|
|
29240
|
+
ctx.stroke();
|
|
29241
|
+
ctx.fillStyle = theme.textColor;
|
|
29242
|
+
ctx.textAlign = "left";
|
|
29243
|
+
ctx.textBaseline = "middle";
|
|
29244
|
+
ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
|
|
29245
|
+
const cxr = x0 + 6 + tw + 5;
|
|
29246
|
+
const cyr = chipY + chipH / 2;
|
|
29247
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
|
|
29248
|
+
ctx.lineWidth = 1.2;
|
|
29249
|
+
ctx.beginPath();
|
|
29250
|
+
ctx.moveTo(cxr, cyr - 1.5);
|
|
29251
|
+
ctx.lineTo(cxr + 2.5, cyr + 1.5);
|
|
29252
|
+
ctx.lineTo(cxr + 5, cyr - 1.5);
|
|
29253
|
+
ctx.stroke();
|
|
29254
|
+
ctx.restore();
|
|
29255
|
+
d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
|
|
29256
|
+
} else {
|
|
29257
|
+
d.chipRect = null;
|
|
29258
|
+
}
|
|
29259
|
+
}
|
|
29260
|
+
/** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
|
|
29261
|
+
* by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
|
|
29262
|
+
* one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
|
|
29263
|
+
paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
|
|
29264
|
+
ctx.setLineDash([]);
|
|
29265
|
+
ctx.lineWidth = 1;
|
|
29266
|
+
for (const bar of bars) {
|
|
29267
|
+
const cx0 = proj.xOf(bar.time) - halfPitch;
|
|
29268
|
+
const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
|
|
29269
|
+
if (cx1 < x0 || cx0 > x1) continue;
|
|
29270
|
+
const yHigh = proj.yOf(bar.high, d.paneId);
|
|
29271
|
+
const yLow = proj.yOf(bar.low, d.paneId);
|
|
29272
|
+
const yOpen = proj.yOf(bar.open, d.paneId);
|
|
29273
|
+
const yClose = proj.yOf(bar.close, d.paneId);
|
|
29274
|
+
if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
|
|
29275
|
+
const color = bar.close >= bar.open ? upColor : downColor;
|
|
29276
|
+
const cellW = cx1 - cx0;
|
|
29277
|
+
const cx = (cx0 + cx1) / 2;
|
|
29278
|
+
ctx.strokeStyle = color;
|
|
29279
|
+
ctx.beginPath();
|
|
29280
|
+
ctx.moveTo(cx, yHigh);
|
|
29281
|
+
ctx.lineTo(cx, yLow);
|
|
29282
|
+
ctx.stroke();
|
|
29283
|
+
if (cellW < 3) continue;
|
|
29284
|
+
if (mode === "bars") {
|
|
29285
|
+
const tick = Math.max(1, cellW * 0.35);
|
|
29286
|
+
ctx.beginPath();
|
|
29287
|
+
ctx.moveTo(cx - tick, yOpen);
|
|
29288
|
+
ctx.lineTo(cx, yOpen);
|
|
29289
|
+
ctx.moveTo(cx, yClose);
|
|
29290
|
+
ctx.lineTo(cx + tick, yClose);
|
|
29291
|
+
ctx.stroke();
|
|
29292
|
+
} else {
|
|
29293
|
+
const bw = Math.max(1, cellW * 0.7);
|
|
29294
|
+
ctx.fillStyle = color;
|
|
29295
|
+
ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
|
|
29296
|
+
}
|
|
29297
|
+
}
|
|
29298
|
+
}
|
|
29299
|
+
/** The magnifier's line/area rendering: a close polyline through each cell's center (same
|
|
29300
|
+
* half-chart-bar shift as the candles), with an optional translucent fill down to the
|
|
29301
|
+
* rectangle's bottom edge for the area style. Colored like the chart's own line series. */
|
|
29302
|
+
paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
|
|
29303
|
+
const pts = [];
|
|
29304
|
+
for (const bar of bars) {
|
|
29305
|
+
const y = proj.yOf(bar.close, d.paneId);
|
|
29306
|
+
if (y == null) continue;
|
|
29307
|
+
pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
|
|
29308
|
+
}
|
|
29309
|
+
if (pts.length < 2) return;
|
|
29310
|
+
if (area) {
|
|
29311
|
+
ctx.beginPath();
|
|
29312
|
+
ctx.moveTo(pts[0][0], yBottom);
|
|
29313
|
+
for (const [px, py] of pts) ctx.lineTo(px, py);
|
|
29314
|
+
ctx.lineTo(pts[pts.length - 1][0], yBottom);
|
|
29315
|
+
ctx.closePath();
|
|
29316
|
+
ctx.fillStyle = withAlpha(color, 0.15);
|
|
29317
|
+
ctx.fill();
|
|
29318
|
+
}
|
|
29319
|
+
ctx.setLineDash([]);
|
|
29320
|
+
ctx.lineWidth = 1.5;
|
|
29321
|
+
ctx.strokeStyle = color;
|
|
29322
|
+
ctx.beginPath();
|
|
29323
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
29324
|
+
for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
29325
|
+
ctx.stroke();
|
|
29326
|
+
}
|
|
29327
|
+
/** Centered muted notice inside the magnifier rect (loading / unavailable states). */
|
|
29328
|
+
paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
|
|
29329
|
+
if (w < 60 || h < 20) return;
|
|
29330
|
+
ctx.save();
|
|
29331
|
+
ctx.beginPath();
|
|
29332
|
+
ctx.rect(x0, y0, w, h);
|
|
29333
|
+
ctx.clip();
|
|
29334
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29335
|
+
ctx.fillStyle = withAlpha(theme.textColor, 0.55);
|
|
29336
|
+
ctx.textAlign = "center";
|
|
29337
|
+
ctx.textBaseline = "middle";
|
|
29338
|
+
ctx.fillText(text, x0 + w / 2, y0 + h / 2);
|
|
29339
|
+
ctx.restore();
|
|
29340
|
+
}
|
|
29341
|
+
/** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
|
|
29342
|
+
* "drag an area"). Painted by the drawings layer while the tool is armed and no placement
|
|
29343
|
+
* is in progress; chart-background fill so it reads as chrome over any content. */
|
|
29344
|
+
paintPlacementHint(ctx, text, theme, width, height) {
|
|
29345
|
+
ctx.save();
|
|
29346
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29347
|
+
const tw = ctx.measureText(text).width;
|
|
29348
|
+
const pillW = tw + 24;
|
|
29349
|
+
const pillH = 24;
|
|
29350
|
+
const x = (width - pillW) / 2;
|
|
29351
|
+
const y = height - pillH - 14;
|
|
29352
|
+
roundRect(ctx, x, y, pillW, pillH, pillH / 2);
|
|
29353
|
+
ctx.fillStyle = theme.background;
|
|
29354
|
+
ctx.fill();
|
|
29355
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29356
|
+
ctx.lineWidth = 1;
|
|
29357
|
+
ctx.setLineDash([]);
|
|
29358
|
+
ctx.stroke();
|
|
29359
|
+
ctx.fillStyle = theme.textColor;
|
|
29360
|
+
ctx.textAlign = "center";
|
|
29361
|
+
ctx.textBaseline = "middle";
|
|
29362
|
+
ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
|
|
29363
|
+
ctx.restore();
|
|
29364
|
+
}
|
|
28765
29365
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
28766
29366
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
28767
29367
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -29480,6 +30080,22 @@ var DrawingInteraction = class {
|
|
|
29480
30080
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
29481
30081
|
return snapped;
|
|
29482
30082
|
}
|
|
30083
|
+
/**
|
|
30084
|
+
* Resolve a cursor pixel through the magnet and return the snapped pixel — the same
|
|
30085
|
+
* conversion drawing placement uses. Updates the snap-ring marker. The measure
|
|
30086
|
+
* ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
|
|
30087
|
+
*/
|
|
30088
|
+
snapCursor(x, y, mode) {
|
|
30089
|
+
const proj = this.deps.projector();
|
|
30090
|
+
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
30091
|
+
const point = this.resolve(x, y, paneId, mode);
|
|
30092
|
+
const sy = proj.yOf(point.price, paneId);
|
|
30093
|
+
return { x: proj.xOf(point.time), y: sy ?? y };
|
|
30094
|
+
}
|
|
30095
|
+
/** Drop the snap-ring marker (a transient mode ended without going through `up`). */
|
|
30096
|
+
clearSnapMarker() {
|
|
30097
|
+
this.snapAt = null;
|
|
30098
|
+
}
|
|
29483
30099
|
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
29484
30100
|
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
29485
30101
|
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
@@ -30161,8 +30777,9 @@ function ensureStyles5() {
|
|
|
30161
30777
|
if (!existing) document.head.appendChild(s);
|
|
30162
30778
|
}
|
|
30163
30779
|
var DrawingSettingsPopup = class {
|
|
30164
|
-
constructor(host, theme) {
|
|
30780
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
30165
30781
|
this.host = host;
|
|
30782
|
+
this.chartBarMs = chartBarMs;
|
|
30166
30783
|
this.el = null;
|
|
30167
30784
|
this.tipEl = null;
|
|
30168
30785
|
// floating hover-label (above/below the toolbar)
|
|
@@ -30187,6 +30804,14 @@ var DrawingSettingsPopup = class {
|
|
|
30187
30804
|
this.theme = theme;
|
|
30188
30805
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
30189
30806
|
}
|
|
30807
|
+
/** The magnifier timeframe choices strictly below the chart's own bar duration
|
|
30808
|
+
* (`auto` rides along while at least one concrete lower step exists). */
|
|
30809
|
+
lowerTimeframeOptions() {
|
|
30810
|
+
const chartMs = this.chartBarMs();
|
|
30811
|
+
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
30812
|
+
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
30813
|
+
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
30814
|
+
}
|
|
30190
30815
|
setTheme(theme) {
|
|
30191
30816
|
this.theme = theme;
|
|
30192
30817
|
this.settingsDialog.setTheme(theme);
|
|
@@ -30229,7 +30854,20 @@ var DrawingSettingsPopup = class {
|
|
|
30229
30854
|
const sz = drawing.size ?? "normal";
|
|
30230
30855
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
30231
30856
|
}
|
|
30232
|
-
if (paths.has("
|
|
30857
|
+
if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
|
|
30858
|
+
const options = this.lowerTimeframeOptions();
|
|
30859
|
+
if (options.length > 0) {
|
|
30860
|
+
bar.appendChild(
|
|
30861
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
30862
|
+
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
30863
|
+
labelInTrigger: true
|
|
30864
|
+
})
|
|
30865
|
+
);
|
|
30866
|
+
}
|
|
30867
|
+
bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
|
|
30868
|
+
bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
|
|
30869
|
+
}
|
|
30870
|
+
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 })));
|
|
30233
30871
|
if (paths.has("style.lineWidth")) {
|
|
30234
30872
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
30235
30873
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -30551,6 +31189,7 @@ var DrawingSettingsPopup = class {
|
|
|
30551
31189
|
this.colorPop = null;
|
|
30552
31190
|
this.colorOwner = null;
|
|
30553
31191
|
}
|
|
31192
|
+
opts.onClose?.();
|
|
30554
31193
|
}
|
|
30555
31194
|
});
|
|
30556
31195
|
const el = pop.el;
|
|
@@ -30569,6 +31208,49 @@ var DrawingSettingsPopup = class {
|
|
|
30569
31208
|
pop.show();
|
|
30570
31209
|
return pop;
|
|
30571
31210
|
}
|
|
31211
|
+
/**
|
|
31212
|
+
* A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
|
|
31213
|
+
* canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
|
|
31214
|
+
* to position against, and removed again when the menu closes. Independent of the
|
|
31215
|
+
* quick toolbar — the chip works without selecting the drawing first.
|
|
31216
|
+
*/
|
|
31217
|
+
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
31218
|
+
ensureStyles5();
|
|
31219
|
+
closeOpenPopovers();
|
|
31220
|
+
const options = this.lowerTimeframeOptions();
|
|
31221
|
+
const anchor = document.createElement("div");
|
|
31222
|
+
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
31223
|
+
this.host.appendChild(anchor);
|
|
31224
|
+
this.menuPop = this.hostFloat(anchor, {
|
|
31225
|
+
zIndex: 26,
|
|
31226
|
+
padding: "4px",
|
|
31227
|
+
onClose: () => anchor.remove(),
|
|
31228
|
+
fill: (menu2, pop) => {
|
|
31229
|
+
if (options.length === 0) {
|
|
31230
|
+
const note = document.createElement("div");
|
|
31231
|
+
note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
|
|
31232
|
+
note.textContent = "No lower timeframe available";
|
|
31233
|
+
menu2.appendChild(note);
|
|
31234
|
+
return;
|
|
31235
|
+
}
|
|
31236
|
+
for (const o of options) {
|
|
31237
|
+
const item = document.createElement("button");
|
|
31238
|
+
item.type = "button";
|
|
31239
|
+
item.className = "vela-dpop-item";
|
|
31240
|
+
item.dataset.active = o.value === current ? "1" : "0";
|
|
31241
|
+
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;";
|
|
31242
|
+
item.textContent = o.label;
|
|
31243
|
+
item.addEventListener("click", (e) => {
|
|
31244
|
+
e.stopPropagation();
|
|
31245
|
+
pop.hide();
|
|
31246
|
+
onPick(o.value);
|
|
31247
|
+
});
|
|
31248
|
+
menu2.appendChild(item);
|
|
31249
|
+
}
|
|
31250
|
+
}
|
|
31251
|
+
});
|
|
31252
|
+
this.menuOwner = anchor;
|
|
31253
|
+
}
|
|
30572
31254
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
30573
31255
|
openActionMenu(anchor, rows) {
|
|
30574
31256
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -30679,10 +31361,13 @@ var DrawingSettingsPopup = class {
|
|
|
30679
31361
|
let cur = current;
|
|
30680
31362
|
const paint = (v) => {
|
|
30681
31363
|
b.replaceChildren();
|
|
30682
|
-
const
|
|
30683
|
-
|
|
30684
|
-
|
|
30685
|
-
|
|
31364
|
+
const glyph = render(v);
|
|
31365
|
+
if (glyph) {
|
|
31366
|
+
const ic = document.createElement("span");
|
|
31367
|
+
ic.style.cssText = "display:flex;";
|
|
31368
|
+
ic.innerHTML = sized(glyph);
|
|
31369
|
+
b.appendChild(ic);
|
|
31370
|
+
}
|
|
30686
31371
|
if (opts.label && opts.labelInTrigger) {
|
|
30687
31372
|
const tx = document.createElement("span");
|
|
30688
31373
|
tx.textContent = opts.label(v);
|
|
@@ -30724,10 +31409,13 @@ var DrawingSettingsPopup = class {
|
|
|
30724
31409
|
item.className = "vela-dpop-item";
|
|
30725
31410
|
item.dataset.active = active ? "1" : "0";
|
|
30726
31411
|
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;`;
|
|
30727
|
-
const
|
|
30728
|
-
|
|
30729
|
-
|
|
30730
|
-
|
|
31412
|
+
const glyph = render(v);
|
|
31413
|
+
if (glyph) {
|
|
31414
|
+
const ic = document.createElement("span");
|
|
31415
|
+
ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
|
|
31416
|
+
ic.innerHTML = sized(glyph, 18);
|
|
31417
|
+
item.appendChild(ic);
|
|
31418
|
+
}
|
|
30731
31419
|
if (label) {
|
|
30732
31420
|
const tx = document.createElement("span");
|
|
30733
31421
|
tx.textContent = label(v);
|
|
@@ -30885,30 +31573,39 @@ var MeasureOverlay = class {
|
|
|
30885
31573
|
isFinished() {
|
|
30886
31574
|
return this.state === "finished";
|
|
30887
31575
|
}
|
|
30888
|
-
/** A press: begin the measurement, or finish it on the second click.
|
|
30889
|
-
|
|
31576
|
+
/** A press: begin the measurement, or finish it on the second click.
|
|
31577
|
+
* `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
|
|
31578
|
+
* graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
|
|
31579
|
+
down(x, y, gx = x, gy = y) {
|
|
30890
31580
|
if (this.state === "measuring") {
|
|
30891
|
-
this.end = { x, y };
|
|
31581
|
+
this.end = { x: gx, y: gy };
|
|
30892
31582
|
this.state = "finished";
|
|
30893
31583
|
return;
|
|
30894
31584
|
}
|
|
30895
|
-
this.start = { x, y };
|
|
30896
|
-
this.end = { x, y };
|
|
31585
|
+
this.start = { x: gx, y: gy };
|
|
31586
|
+
this.end = { x: gx, y: gy };
|
|
30897
31587
|
this.pressX = x;
|
|
30898
31588
|
this.pressY = y;
|
|
30899
31589
|
this.state = "measuring";
|
|
30900
31590
|
}
|
|
31591
|
+
/** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
|
|
30901
31592
|
move(x, y) {
|
|
30902
31593
|
if (this.state === "measuring") this.end = { x, y };
|
|
30903
31594
|
}
|
|
30904
31595
|
/** A release: finish if the press was actually dragged (press-drag-release), else wait
|
|
30905
|
-
* for the second click (click-move-click).
|
|
30906
|
-
|
|
31596
|
+
* for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
|
|
31597
|
+
* are the graphic end (magnet-snapped when the magnet is on). */
|
|
31598
|
+
up(x, y, gx = x, gy = y) {
|
|
30907
31599
|
if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP4) {
|
|
30908
|
-
this.end = { x, y };
|
|
31600
|
+
this.end = { x: gx, y: gy };
|
|
30909
31601
|
this.state = "finished";
|
|
30910
31602
|
}
|
|
30911
31603
|
}
|
|
31604
|
+
/** Current graphic endpoints in media pixels, or null when idle. */
|
|
31605
|
+
points() {
|
|
31606
|
+
if (!this.start || !this.end) return null;
|
|
31607
|
+
return { start: this.start, end: this.end };
|
|
31608
|
+
}
|
|
30912
31609
|
clear() {
|
|
30913
31610
|
this.state = "idle";
|
|
30914
31611
|
this.start = null;
|
|
@@ -31070,6 +31767,9 @@ var UserDrawingController = class {
|
|
|
31070
31767
|
this.intentCb = null;
|
|
31071
31768
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
31072
31769
|
this.externalGhost = null;
|
|
31770
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
31771
|
+
this.seriesGw = null;
|
|
31772
|
+
this.seriesGwUnsub = null;
|
|
31073
31773
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
31074
31774
|
this.lastDraftKey = null;
|
|
31075
31775
|
this.measure = new MeasureOverlay();
|
|
@@ -31097,7 +31797,7 @@ var UserDrawingController = class {
|
|
|
31097
31797
|
this.textEditor = null;
|
|
31098
31798
|
this.painter = new DrawingPainter();
|
|
31099
31799
|
this.ctx = canvas.getContext("2d");
|
|
31100
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
31800
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
31101
31801
|
this.toolbar = new DrawingToolbar(
|
|
31102
31802
|
toolbarHost,
|
|
31103
31803
|
deps.theme(),
|
|
@@ -31155,6 +31855,22 @@ var UserDrawingController = class {
|
|
|
31155
31855
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
31156
31856
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
31157
31857
|
}
|
|
31858
|
+
/** Core push: the series gateway data-driven drawings read finer-timeframe bars
|
|
31859
|
+
* through (surfaced to them as `Projector.seriesInRange`). A landed background
|
|
31860
|
+
* fetch repaints both this layer and the interleave slices under the series. */
|
|
31861
|
+
setSeriesGateway(gateway) {
|
|
31862
|
+
this.seriesGwUnsub?.();
|
|
31863
|
+
this.seriesGw = gateway;
|
|
31864
|
+
this.seriesGwUnsub = gateway.onUpdate(() => {
|
|
31865
|
+
this.invalidateSlices();
|
|
31866
|
+
this.render();
|
|
31867
|
+
this.deps.requestDataPaint();
|
|
31868
|
+
});
|
|
31869
|
+
}
|
|
31870
|
+
/** The pushed series gateway, or null before the core provides one. */
|
|
31871
|
+
get seriesGateway() {
|
|
31872
|
+
return this.seriesGw;
|
|
31873
|
+
}
|
|
31158
31874
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
31159
31875
|
setExternalGhost(doc) {
|
|
31160
31876
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -31272,8 +31988,34 @@ var UserDrawingController = class {
|
|
|
31272
31988
|
/** Should the drawing layer win this press (vs pan)? */
|
|
31273
31989
|
claim(x, y) {
|
|
31274
31990
|
if (this.measureMode || this.eraserMode) return true;
|
|
31991
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
31275
31992
|
return this.interaction.claim(x, y);
|
|
31276
31993
|
}
|
|
31994
|
+
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
31995
|
+
* the chip's rect is what the painter measured last frame. */
|
|
31996
|
+
magnifierChipAt(x, y) {
|
|
31997
|
+
for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
|
|
31998
|
+
const d = this.drawings[i];
|
|
31999
|
+
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
32000
|
+
const r = d.chipRect;
|
|
32001
|
+
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
32002
|
+
}
|
|
32003
|
+
return null;
|
|
32004
|
+
}
|
|
32005
|
+
/** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
|
|
32006
|
+
* settings-popup edit (same intent, same undo step). */
|
|
32007
|
+
openMagnifierChipMenu(drawing) {
|
|
32008
|
+
const rect = drawing.chipRect;
|
|
32009
|
+
if (!rect) return;
|
|
32010
|
+
const id = drawing.id;
|
|
32011
|
+
this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
|
|
32012
|
+
const d = this.drawings.find((x) => x.id === id);
|
|
32013
|
+
if (!(d instanceof Magnifier)) return;
|
|
32014
|
+
d.applySettings({ "magnifier.timeframe": value });
|
|
32015
|
+
this.render();
|
|
32016
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32017
|
+
});
|
|
32018
|
+
}
|
|
31277
32019
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
31278
32020
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
31279
32021
|
deleteAt(x, y) {
|
|
@@ -31285,11 +32027,13 @@ var UserDrawingController = class {
|
|
|
31285
32027
|
}
|
|
31286
32028
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
31287
32029
|
* gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
|
|
31288
|
-
*
|
|
31289
|
-
|
|
32030
|
+
* `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
|
|
32031
|
+
* false when a mode/tool is already active (the normal press path owns it). */
|
|
32032
|
+
beginMeasureAt(x, y, snap = "off") {
|
|
31290
32033
|
if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
|
|
31291
32034
|
this.withModeIntent(() => this.toggleMeasure());
|
|
31292
|
-
this.
|
|
32035
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
32036
|
+
this.measure.down(x, y, g.x, g.y);
|
|
31293
32037
|
this.render();
|
|
31294
32038
|
return true;
|
|
31295
32039
|
}
|
|
@@ -31311,11 +32055,19 @@ var UserDrawingController = class {
|
|
|
31311
32055
|
return;
|
|
31312
32056
|
}
|
|
31313
32057
|
if (this.measureMode) {
|
|
31314
|
-
this.
|
|
32058
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
32059
|
+
this.measure.down(x, y, g.x, g.y);
|
|
31315
32060
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
31316
32061
|
this.render();
|
|
31317
32062
|
return;
|
|
31318
32063
|
}
|
|
32064
|
+
if (this.activeTool == null) {
|
|
32065
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
32066
|
+
if (chipOwner) {
|
|
32067
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
32068
|
+
return;
|
|
32069
|
+
}
|
|
32070
|
+
}
|
|
31319
32071
|
this.interaction.down(x, y, snap, shift);
|
|
31320
32072
|
}
|
|
31321
32073
|
pointerMove(x, y, snap = "off", shift = false) {
|
|
@@ -31324,7 +32076,8 @@ var UserDrawingController = class {
|
|
|
31324
32076
|
return;
|
|
31325
32077
|
}
|
|
31326
32078
|
if (this.measureMode) {
|
|
31327
|
-
this.
|
|
32079
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
32080
|
+
this.measure.move(g.x, g.y);
|
|
31328
32081
|
this.render();
|
|
31329
32082
|
return;
|
|
31330
32083
|
}
|
|
@@ -31342,13 +32095,14 @@ var UserDrawingController = class {
|
|
|
31342
32095
|
this.render();
|
|
31343
32096
|
}
|
|
31344
32097
|
}
|
|
31345
|
-
pointerUp(x, y) {
|
|
32098
|
+
pointerUp(x, y, snap = "off") {
|
|
31346
32099
|
if (this.eraserMode) {
|
|
31347
32100
|
this.erasing = false;
|
|
31348
32101
|
return;
|
|
31349
32102
|
}
|
|
31350
32103
|
if (this.measureMode) {
|
|
31351
|
-
this.
|
|
32104
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
32105
|
+
this.measure.up(x, y, g.x, g.y);
|
|
31352
32106
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
31353
32107
|
this.render();
|
|
31354
32108
|
return;
|
|
@@ -31393,6 +32147,7 @@ var UserDrawingController = class {
|
|
|
31393
32147
|
/** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
|
|
31394
32148
|
exitMeasure(clearGraphic = true) {
|
|
31395
32149
|
this.measureMode = false;
|
|
32150
|
+
this.interaction.clearSnapMarker();
|
|
31396
32151
|
if (clearGraphic) this.measure.clear();
|
|
31397
32152
|
this.toolbar.setMeasureActive(false);
|
|
31398
32153
|
this.render();
|
|
@@ -31531,17 +32286,31 @@ var UserDrawingController = class {
|
|
|
31531
32286
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
31532
32287
|
cursorAt(x, y) {
|
|
31533
32288
|
if (this.eraserMode) return "pointer";
|
|
32289
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
31534
32290
|
return this.interaction.cursorAt(x, y);
|
|
31535
32291
|
}
|
|
31536
|
-
/** Right-click
|
|
31537
|
-
*
|
|
31538
|
-
*
|
|
31539
|
-
*
|
|
32292
|
+
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
32293
|
+
* placement or measurement, and also plain-disarms an armed-but-idle drawing
|
|
32294
|
+
* tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
|
|
32295
|
+
* stay-in-drawing-mode (where Escape would leave a drawing tool armed).
|
|
32296
|
+
* Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
|
|
32297
|
+
* whether the press was consumed; false lets the host's context menu open
|
|
32298
|
+
* normally. */
|
|
31540
32299
|
cancelPlacement() {
|
|
31541
|
-
if (
|
|
31542
|
-
|
|
31543
|
-
|
|
31544
|
-
|
|
32300
|
+
if (this.measureMode || this.eraserMode) {
|
|
32301
|
+
this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
|
|
32302
|
+
return true;
|
|
32303
|
+
}
|
|
32304
|
+
if (this.interaction.isPlacing()) {
|
|
32305
|
+
this.interaction.cancel();
|
|
32306
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
32307
|
+
return true;
|
|
32308
|
+
}
|
|
32309
|
+
if (this.activeTool != null) {
|
|
32310
|
+
this.emit({ kind: "arm", type: null });
|
|
32311
|
+
return true;
|
|
32312
|
+
}
|
|
32313
|
+
return false;
|
|
31545
32314
|
}
|
|
31546
32315
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
31547
32316
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
@@ -31567,6 +32336,10 @@ var UserDrawingController = class {
|
|
|
31567
32336
|
return true;
|
|
31568
32337
|
}
|
|
31569
32338
|
if (this.interaction.cancel()) return true;
|
|
32339
|
+
if (this.measureMode) {
|
|
32340
|
+
this.withModeIntent(() => this.exitMeasure());
|
|
32341
|
+
return true;
|
|
32342
|
+
}
|
|
31570
32343
|
if (this.selectedIds.size) {
|
|
31571
32344
|
this.clearSelection();
|
|
31572
32345
|
return true;
|
|
@@ -31692,6 +32465,7 @@ var UserDrawingController = class {
|
|
|
31692
32465
|
if (!sctx) continue;
|
|
31693
32466
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
31694
32467
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32468
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
31695
32469
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
31696
32470
|
const slices = out.get(paneId) ?? [];
|
|
31697
32471
|
slices.push({ beforeZ, canvas });
|
|
@@ -31719,6 +32493,7 @@ var UserDrawingController = class {
|
|
|
31719
32493
|
dragged: this.interaction.activeDragId(),
|
|
31720
32494
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
31721
32495
|
};
|
|
32496
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
31722
32497
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
31723
32498
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
31724
32499
|
this.layoutTextEditor();
|
|
@@ -31726,6 +32501,10 @@ var UserDrawingController = class {
|
|
|
31726
32501
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
31727
32502
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
31728
32503
|
this.emitDraft(ghost);
|
|
32504
|
+
if (this.activeTool && !ghost) {
|
|
32505
|
+
const hint = getDrawingType(this.activeTool)?.placementHint;
|
|
32506
|
+
if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
|
|
32507
|
+
}
|
|
31729
32508
|
const markers = this.interaction.placingMarkers(proj);
|
|
31730
32509
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
31731
32510
|
const m = this.interaction.snapMarker();
|
|
@@ -31737,6 +32516,9 @@ var UserDrawingController = class {
|
|
|
31737
32516
|
this.closeTextEditor();
|
|
31738
32517
|
this.popup.destroy();
|
|
31739
32518
|
this.toolbar.destroy();
|
|
32519
|
+
this.seriesGwUnsub?.();
|
|
32520
|
+
this.seriesGwUnsub = null;
|
|
32521
|
+
this.seriesGw = null;
|
|
31740
32522
|
this.intentCb = null;
|
|
31741
32523
|
this.drawings = [];
|
|
31742
32524
|
}
|
|
@@ -32134,7 +32916,7 @@ function mergeSlices(indicator, user) {
|
|
|
32134
32916
|
}
|
|
32135
32917
|
|
|
32136
32918
|
// src/renderers/native/drawings/Projector.ts
|
|
32137
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
32919
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
32138
32920
|
return {
|
|
32139
32921
|
xOf: (time) => coords.timeToX(time),
|
|
32140
32922
|
yOf: (price, paneId) => {
|
|
@@ -32155,6 +32937,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
|
32155
32937
|
},
|
|
32156
32938
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
32157
32939
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
32940
|
+
seriesInRange,
|
|
32158
32941
|
width: coords.width,
|
|
32159
32942
|
height: coords.height
|
|
32160
32943
|
};
|
|
@@ -33991,13 +34774,13 @@ var NativeRenderer = class {
|
|
|
33991
34774
|
resetView: () => this.resetView(),
|
|
33992
34775
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
33993
34776
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
33994
|
-
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
34777
|
+
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
33995
34778
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
33996
34779
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
33997
34780
|
drawingsSnapMode: () => this.snapMode,
|
|
33998
34781
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
33999
34782
|
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
34000
|
-
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
34783
|
+
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
34001
34784
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
34002
34785
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
34003
34786
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
@@ -34024,6 +34807,23 @@ var NativeRenderer = class {
|
|
|
34024
34807
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
34025
34808
|
priceZ: (paneId) => paneId === PRICE_PANE_ID2 ? this.scene.candleZ : null,
|
|
34026
34809
|
requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
|
|
34810
|
+
// The look the price series ACTUALLY paints with: candle colors resolved through
|
|
34811
|
+
// the per-style override, line/area colors through their configured styles — so
|
|
34812
|
+
// series-mirroring content (the magnifier inset) matches the chart exactly.
|
|
34813
|
+
seriesLook: () => {
|
|
34814
|
+
const st = this.scene.style;
|
|
34815
|
+
const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
|
|
34816
|
+
const barsUp = st.bars.upColor ?? this.theme.upColor;
|
|
34817
|
+
const barsDown = st.bars.downColor ?? this.theme.downColor;
|
|
34818
|
+
const style = this.scene.priceStyle;
|
|
34819
|
+
return {
|
|
34820
|
+
style,
|
|
34821
|
+
upColor: style === "bars" ? barsUp : paint.up,
|
|
34822
|
+
downColor: style === "bars" ? barsDown : paint.down,
|
|
34823
|
+
lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
|
|
34824
|
+
};
|
|
34825
|
+
},
|
|
34826
|
+
chartBarMs: () => this.coords.barInterval,
|
|
34027
34827
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
34028
34828
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
34029
34829
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -34312,7 +35112,8 @@ var NativeRenderer = class {
|
|
|
34312
35112
|
}
|
|
34313
35113
|
const skipFit = opts?.preserveView === true && this.didInitialFit;
|
|
34314
35114
|
if (this.coords.width > 0 && !skipFit) {
|
|
34315
|
-
this.
|
|
35115
|
+
if (this.didInitialFit) this.reframeKeepZoom();
|
|
35116
|
+
else this.fitContent();
|
|
34316
35117
|
this.didInitialFit = true;
|
|
34317
35118
|
}
|
|
34318
35119
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
@@ -34960,6 +35761,7 @@ var NativeRenderer = class {
|
|
|
34960
35761
|
this.scaleDragHeight = res.height;
|
|
34961
35762
|
this.scaleDragStart = { ...res.holder.scale };
|
|
34962
35763
|
res.holder.manualScale = { ...res.holder.scale };
|
|
35764
|
+
this.axisScaleButtons?.reposition();
|
|
34963
35765
|
this.scheduler.invalidate(4 /* Full */);
|
|
34964
35766
|
}
|
|
34965
35767
|
/** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
|
|
@@ -34991,6 +35793,7 @@ var NativeRenderer = class {
|
|
|
34991
35793
|
const res = this.resolveScaleHolder(x, y);
|
|
34992
35794
|
if (!res) return;
|
|
34993
35795
|
res.holder.manualScale = null;
|
|
35796
|
+
this.axisScaleButtons?.reposition();
|
|
34994
35797
|
this.scheduler.invalidate(4 /* Full */);
|
|
34995
35798
|
}
|
|
34996
35799
|
/**
|
|
@@ -35414,7 +36217,8 @@ var NativeRenderer = class {
|
|
|
35414
36217
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
35415
36218
|
},
|
|
35416
36219
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
35417
|
-
(from, to) => this.barsInTimeRange(from, to)
|
|
36220
|
+
(from, to) => this.barsInTimeRange(from, to),
|
|
36221
|
+
this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
|
|
35418
36222
|
);
|
|
35419
36223
|
}
|
|
35420
36224
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|
|
@@ -35628,6 +36432,20 @@ var NativeRenderer = class {
|
|
|
35628
36432
|
this.coords.setViewport(v);
|
|
35629
36433
|
this.targetBarSpacing = v.barSpacing;
|
|
35630
36434
|
}
|
|
36435
|
+
/** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
|
|
36436
|
+
* zoom (bar spacing), re-anchor the newest bars at the default right offset.
|
|
36437
|
+
* `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
|
|
36438
|
+
* head may still be backfilling toward the previous depth, and raising the spacing
|
|
36439
|
+
* to its temporary bar count would lose the zoom this exists to keep. */
|
|
36440
|
+
reframeKeepZoom() {
|
|
36441
|
+
this.animator?.stop();
|
|
36442
|
+
this.panVelocity = 0;
|
|
36443
|
+
for (const pane of this.scene.panes.values()) pane.manualScale = null;
|
|
36444
|
+
for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
|
|
36445
|
+
const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
|
|
36446
|
+
this.coords.setViewport(v);
|
|
36447
|
+
this.targetBarSpacing = v.barSpacing;
|
|
36448
|
+
}
|
|
35631
36449
|
paneBoundsFor(paneId) {
|
|
35632
36450
|
const p = this.scene.panes.get(paneId);
|
|
35633
36451
|
return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
|
|
@@ -36607,6 +37425,21 @@ function resolveElement(container) {
|
|
|
36607
37425
|
// src/core/options.ts
|
|
36608
37426
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
36609
37427
|
|
|
37428
|
+
// src/widget/statusline-model.ts
|
|
37429
|
+
function segmentVisibility(parts, chartHidden = false) {
|
|
37430
|
+
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
37431
|
+
}
|
|
37432
|
+
function statuslineMenuItems(parts, chartVisible) {
|
|
37433
|
+
return [
|
|
37434
|
+
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
37435
|
+
{ id: "part:name", label: "Symbol name", checked: parts.name },
|
|
37436
|
+
{ id: "part:market", label: "Market status", checked: parts.market },
|
|
37437
|
+
{ id: "part:ohlc", label: "OHLC values", checked: parts.ohlc },
|
|
37438
|
+
{ id: "part:change", label: "Bar change values", checked: parts.change },
|
|
37439
|
+
{ id: "chart", label: chartVisible ? "Hide chart" : "Show chart", separatorBefore: true }
|
|
37440
|
+
];
|
|
37441
|
+
}
|
|
37442
|
+
|
|
36610
37443
|
// src/widget/format.ts
|
|
36611
37444
|
function decimalsFor(ref) {
|
|
36612
37445
|
const a = Math.abs(ref);
|
|
@@ -36654,7 +37487,13 @@ var CSS21 = `
|
|
|
36654
37487
|
padding: 2px 7px;
|
|
36655
37488
|
margin-left: -7px;
|
|
36656
37489
|
}
|
|
36657
|
-
|
|
37490
|
+
/* Hovering opens the chip the same way a legend row opens: solid chart background
|
|
37491
|
+
* plus the same inset neutral outline the indicator rows wear (InputsUI's
|
|
37492
|
+
* setRowHighlighted) \u2014 the two columns read as one family. */
|
|
37493
|
+
.vela-statusline:hover { background: var(--vela-bg); box-shadow: inset 0 0 0 1px var(--vela-border); }
|
|
37494
|
+
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
37495
|
+
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
37496
|
+
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
36658
37497
|
.vela-statusline .vela-sl-avatar {
|
|
36659
37498
|
width: 18px;
|
|
36660
37499
|
height: 18px;
|
|
@@ -36679,6 +37518,24 @@ var CSS21 = `
|
|
|
36679
37518
|
* these are the pre-ink fallbacks only. */
|
|
36680
37519
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
36681
37520
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
37521
|
+
/* The show-chart eye \u2014 out only while the chart is hidden (syncParts drives display),
|
|
37522
|
+
* replacing the value readout it took away. Same footprint as a legend action button. */
|
|
37523
|
+
.vela-statusline .vela-sl-eye {
|
|
37524
|
+
align-self: center;
|
|
37525
|
+
align-items: center;
|
|
37526
|
+
justify-content: center;
|
|
37527
|
+
width: 18px;
|
|
37528
|
+
height: 18px;
|
|
37529
|
+
padding: 0;
|
|
37530
|
+
border: none;
|
|
37531
|
+
border-radius: 3px;
|
|
37532
|
+
background: none;
|
|
37533
|
+
color: var(--vela-fg-muted);
|
|
37534
|
+
cursor: pointer;
|
|
37535
|
+
line-height: 0;
|
|
37536
|
+
flex: none;
|
|
37537
|
+
}
|
|
37538
|
+
.vela-statusline .vela-sl-eye:hover { color: var(--vela-fg); background: color-mix(in srgb, var(--vela-fg) 12%, transparent); }
|
|
36682
37539
|
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
36683
37540
|
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
36684
37541
|
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
@@ -36788,7 +37645,12 @@ var Statusline = class {
|
|
|
36788
37645
|
constructor(host, symbol, iconFor) {
|
|
36789
37646
|
this.host = host;
|
|
36790
37647
|
this.iconFor = iconFor;
|
|
36791
|
-
this.parts = { name: true, market: true, ohlc: true, change: true };
|
|
37648
|
+
this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
|
|
37649
|
+
/** The right-click action menu — present once a host wires it via {@link attachMenu}. */
|
|
37650
|
+
this.menu = null;
|
|
37651
|
+
this.menuHooks = null;
|
|
37652
|
+
/** Mirror of the renderer's `candleVisible` — see {@link setChartHidden}. */
|
|
37653
|
+
this.chartHidden = false;
|
|
36792
37654
|
this.lastBar = null;
|
|
36793
37655
|
this.hoverBar = null;
|
|
36794
37656
|
this.unsubs = [];
|
|
@@ -36804,6 +37666,15 @@ var Statusline = class {
|
|
|
36804
37666
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
36805
37667
|
this.fitMode = false;
|
|
36806
37668
|
this.fitRO = null;
|
|
37669
|
+
this.onContextMenu = (e) => {
|
|
37670
|
+
if (!this.menu || !this.menuHooks) return;
|
|
37671
|
+
e.preventDefault();
|
|
37672
|
+
e.stopPropagation();
|
|
37673
|
+
const chartVisible = this.menuHooks.chartVisible();
|
|
37674
|
+
this.setChartHidden(!chartVisible);
|
|
37675
|
+
this.menu.setItems(statuslineMenuItems(this.parts, chartVisible));
|
|
37676
|
+
this.menu.openAt(e.clientX, e.clientY);
|
|
37677
|
+
};
|
|
36807
37678
|
const doc = host.ownerDocument;
|
|
36808
37679
|
injectStyles(STYLE_ID24, CSS21, doc);
|
|
36809
37680
|
host.classList.add("vela-has-statusline");
|
|
@@ -36830,9 +37701,21 @@ var Statusline = class {
|
|
|
36830
37701
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
36831
37702
|
this.changeEl = doc.createElement("span");
|
|
36832
37703
|
this.changeEl.className = "vela-sl-change";
|
|
36833
|
-
this.
|
|
37704
|
+
this.eyeEl = doc.createElement("button");
|
|
37705
|
+
this.eyeEl.type = "button";
|
|
37706
|
+
this.eyeEl.className = "vela-sl-eye";
|
|
37707
|
+
this.eyeEl.innerHTML = iconAt("eye-off", 14);
|
|
37708
|
+
this.eyeEl.setAttribute("aria-label", "Show chart");
|
|
37709
|
+
this.eyeEl.style.display = "none";
|
|
37710
|
+
this.eyeEl.addEventListener("click", (e) => {
|
|
37711
|
+
e.stopPropagation();
|
|
37712
|
+
this.menuHooks?.setChartVisible(true);
|
|
37713
|
+
this.setChartHidden(false);
|
|
37714
|
+
});
|
|
37715
|
+
this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl, this.eyeEl);
|
|
36834
37716
|
host.appendChild(this.el);
|
|
36835
37717
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
37718
|
+
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
36836
37719
|
this.setMarketStatus("open");
|
|
36837
37720
|
this.render();
|
|
36838
37721
|
}
|
|
@@ -36842,6 +37725,7 @@ var Statusline = class {
|
|
|
36842
37725
|
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
36843
37726
|
this.avatarEl.replaceWith(fresh);
|
|
36844
37727
|
this.avatarEl = fresh;
|
|
37728
|
+
this.syncParts();
|
|
36845
37729
|
this.fit();
|
|
36846
37730
|
}
|
|
36847
37731
|
/**
|
|
@@ -36869,20 +37753,25 @@ var Statusline = class {
|
|
|
36869
37753
|
}
|
|
36870
37754
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
36871
37755
|
syncParts() {
|
|
36872
|
-
|
|
36873
|
-
this.
|
|
36874
|
-
this.
|
|
36875
|
-
this.
|
|
37756
|
+
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
37757
|
+
this.avatarEl.style.display = seg.avatar ? "" : "none";
|
|
37758
|
+
this.symbolEl.style.display = seg.symbol ? "" : "none";
|
|
37759
|
+
this.metaEl.style.display = seg.meta ? "" : "none";
|
|
37760
|
+
this.marketEl.style.display = seg.market ? "" : "none";
|
|
37761
|
+
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
37762
|
+
this.changeEl.style.display = seg.change ? "" : "none";
|
|
37763
|
+
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
36876
37764
|
}
|
|
36877
37765
|
/** Hide overflowing segments until the row fits its max-width (fit mode only). */
|
|
36878
37766
|
fit() {
|
|
36879
37767
|
if (!this.fitMode) return;
|
|
36880
37768
|
this.syncParts();
|
|
37769
|
+
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
36881
37770
|
const order = [
|
|
36882
|
-
[this.ohlcEl,
|
|
36883
|
-
[this.changeEl,
|
|
36884
|
-
[this.metaEl,
|
|
36885
|
-
[this.marketEl,
|
|
37771
|
+
[this.ohlcEl, seg.ohlc],
|
|
37772
|
+
[this.changeEl, seg.change],
|
|
37773
|
+
[this.metaEl, seg.meta],
|
|
37774
|
+
[this.marketEl, seg.market]
|
|
36886
37775
|
];
|
|
36887
37776
|
for (const [el, shown] of order) {
|
|
36888
37777
|
if (this.el.scrollWidth <= this.el.clientWidth) break;
|
|
@@ -36903,7 +37792,6 @@ var Statusline = class {
|
|
|
36903
37792
|
this.readout = readout;
|
|
36904
37793
|
this.render();
|
|
36905
37794
|
}
|
|
36906
|
-
/** Show/hide one part (the settings dialog's Status line tab drives these). */
|
|
36907
37795
|
/** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
|
|
36908
37796
|
setMeta(timeframe, provider) {
|
|
36909
37797
|
this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
|
|
@@ -36922,6 +37810,9 @@ var Statusline = class {
|
|
|
36922
37810
|
});
|
|
36923
37811
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
36924
37812
|
}
|
|
37813
|
+
/** Show/hide one part — the settings dialog's Status line tab and the right-click
|
|
37814
|
+
* menu both drive these. 'name' owns the venue/timeframe meta too (see
|
|
37815
|
+
* {@link segmentVisibility}). */
|
|
36925
37816
|
setPartVisible(part, visible) {
|
|
36926
37817
|
this.parts[part] = visible;
|
|
36927
37818
|
this.syncParts();
|
|
@@ -36930,6 +37821,47 @@ var Statusline = class {
|
|
|
36930
37821
|
partVisible(part) {
|
|
36931
37822
|
return this.parts[part];
|
|
36932
37823
|
}
|
|
37824
|
+
/** Mirror the chart's (price series') visibility: dim the whole line like a hidden
|
|
37825
|
+
* indicator's legend row, drop the value readout (OHLC + bar change — values of a
|
|
37826
|
+
* series that isn't painted), and put the show-chart eye out in its place. The
|
|
37827
|
+
* parts config is untouched, so showing the chart restores the readout exactly as
|
|
37828
|
+
* configured. Idempotent; {@link render} re-syncs it from the live renderer, so
|
|
37829
|
+
* toggles made elsewhere (the object tree's eye) converge too. */
|
|
37830
|
+
setChartHidden(hidden) {
|
|
37831
|
+
if (hidden === this.chartHidden) return;
|
|
37832
|
+
this.chartHidden = hidden;
|
|
37833
|
+
this.el.classList.toggle("vela-sl-chart-hidden", hidden);
|
|
37834
|
+
this.syncParts();
|
|
37835
|
+
this.fit();
|
|
37836
|
+
}
|
|
37837
|
+
/** Wire the right-click action menu: one checkable toggle per part plus hide/show
|
|
37838
|
+
* for the chart itself. The menu is built once; later calls just swap the hooks. */
|
|
37839
|
+
attachMenu(hooks) {
|
|
37840
|
+
this.menuHooks = hooks;
|
|
37841
|
+
if (this.menu) return;
|
|
37842
|
+
this.menu = new Menu({
|
|
37843
|
+
host: this.host,
|
|
37844
|
+
items: [],
|
|
37845
|
+
placement: "bottom-start",
|
|
37846
|
+
// Pointer-anchored action menu — checked state reads as a leading ✓ (the
|
|
37847
|
+
// same shape as the chart's own context menu).
|
|
37848
|
+
checkmarks: true,
|
|
37849
|
+
onSelect: (id) => this.runMenuItem(id)
|
|
37850
|
+
});
|
|
37851
|
+
this.el.addEventListener("contextmenu", this.onContextMenu);
|
|
37852
|
+
}
|
|
37853
|
+
runMenuItem(id) {
|
|
37854
|
+
const hooks = this.menuHooks;
|
|
37855
|
+
if (!hooks) return;
|
|
37856
|
+
if (id.startsWith("part:")) {
|
|
37857
|
+
const part = id.slice("part:".length);
|
|
37858
|
+
hooks.setPart(part, !this.parts[part]);
|
|
37859
|
+
} else if (id === "chart") {
|
|
37860
|
+
const next = !hooks.chartVisible();
|
|
37861
|
+
hooks.setChartVisible(next);
|
|
37862
|
+
this.setChartHidden(!next);
|
|
37863
|
+
}
|
|
37864
|
+
}
|
|
36933
37865
|
/** (Re)bind to a chart instance — called after every widget rebuild. */
|
|
36934
37866
|
onChart(chart) {
|
|
36935
37867
|
this.detach();
|
|
@@ -36951,7 +37883,11 @@ var Statusline = class {
|
|
|
36951
37883
|
this.detach();
|
|
36952
37884
|
this.fitRO?.disconnect();
|
|
36953
37885
|
this.fitRO = null;
|
|
37886
|
+
this.el.removeEventListener("contextmenu", this.onContextMenu);
|
|
37887
|
+
this.menu?.destroy();
|
|
37888
|
+
this.menu = null;
|
|
36954
37889
|
this.marketTip.destroy();
|
|
37890
|
+
this.eyeTip.destroy();
|
|
36955
37891
|
this.marketBubble.destroy();
|
|
36956
37892
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
36957
37893
|
this.el.remove();
|
|
@@ -36961,6 +37897,7 @@ var Statusline = class {
|
|
|
36961
37897
|
this.unsubs = [];
|
|
36962
37898
|
}
|
|
36963
37899
|
render() {
|
|
37900
|
+
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
36964
37901
|
const bar = this.hoverBar ?? this.lastBar;
|
|
36965
37902
|
if (!bar) {
|
|
36966
37903
|
this.ohlcEl.replaceChildren();
|
|
@@ -37031,7 +37968,7 @@ var FETCH_BACK_MS = 4 * 864e5;
|
|
|
37031
37968
|
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
37032
37969
|
var MIN_TIMER_MS = 15e3;
|
|
37033
37970
|
var MAX_TIMER_MS = 36e5;
|
|
37034
|
-
var
|
|
37971
|
+
var RETRY_MS2 = 6e4;
|
|
37035
37972
|
var MarketStatusTracker = class {
|
|
37036
37973
|
constructor(onStatus) {
|
|
37037
37974
|
this.onStatus = onStatus;
|
|
@@ -37068,7 +38005,7 @@ var MarketStatusTracker = class {
|
|
|
37068
38005
|
]);
|
|
37069
38006
|
if (my !== this.epoch) return;
|
|
37070
38007
|
if (!regular || !extended) {
|
|
37071
|
-
this.arm(my, data, symbol,
|
|
38008
|
+
this.arm(my, data, symbol, RETRY_MS2);
|
|
37072
38009
|
return;
|
|
37073
38010
|
}
|
|
37074
38011
|
const w = { regular, extended };
|
|
@@ -37329,6 +38266,7 @@ var TIME_AXIS_H2 = 22;
|
|
|
37329
38266
|
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
37330
38267
|
var CLUSTER_H2 = 24;
|
|
37331
38268
|
var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
|
|
38269
|
+
var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
|
|
37332
38270
|
var STYLE_ID26 = "vela-cell-controls";
|
|
37333
38271
|
var CSS23 = `
|
|
37334
38272
|
.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;}
|
|
@@ -37338,8 +38276,11 @@ var CSS23 = `
|
|
|
37338
38276
|
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
37339
38277
|
.vela-cc-grip:active{cursor:grabbing;}
|
|
37340
38278
|
`;
|
|
37341
|
-
function
|
|
37342
|
-
|
|
38279
|
+
function plotCenterX(width, toolbarGutter = 0, scaleGutter = 0) {
|
|
38280
|
+
return (width + toolbarGutter - scaleGutter) / 2;
|
|
38281
|
+
}
|
|
38282
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX, gutters = {}) {
|
|
38283
|
+
const cx = plotCenterX(width, gutters.toolbar ?? 0, gutters.scale ?? 0);
|
|
37343
38284
|
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
|
|
37344
38285
|
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
37345
38286
|
}
|
|
@@ -37358,7 +38299,7 @@ var CellControls = class {
|
|
|
37358
38299
|
if (this.suspended) return;
|
|
37359
38300
|
if (this.dragging) return;
|
|
37360
38301
|
const rect = this.host.getBoundingClientRect();
|
|
37361
|
-
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
|
|
38302
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, CELL_CONTROLS_PROXIMITY_PX, this.hostGutters()));
|
|
37362
38303
|
};
|
|
37363
38304
|
this.onHostLeave = () => {
|
|
37364
38305
|
if (this.dragging) return;
|
|
@@ -37369,7 +38310,7 @@ var CellControls = class {
|
|
|
37369
38310
|
this.root = host.ownerDocument.createElement("div");
|
|
37370
38311
|
Object.assign(this.root.style, {
|
|
37371
38312
|
position: "absolute",
|
|
37372
|
-
left:
|
|
38313
|
+
left: CLUSTER_LEFT_CSS,
|
|
37373
38314
|
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
37374
38315
|
transform: "translateX(-50%)",
|
|
37375
38316
|
zIndex: "6",
|
|
@@ -37469,6 +38410,16 @@ var CellControls = class {
|
|
|
37469
38410
|
this.suspended = on;
|
|
37470
38411
|
if (on) this.setNear(false);
|
|
37471
38412
|
}
|
|
38413
|
+
/** Live renderer gutters on the cell host (0 when unpublished — a test stub). */
|
|
38414
|
+
hostGutters() {
|
|
38415
|
+
const view = this.host.ownerDocument.defaultView;
|
|
38416
|
+
if (!view) return { toolbar: 0, scale: 0 };
|
|
38417
|
+
const cs = view.getComputedStyle(this.host);
|
|
38418
|
+
return {
|
|
38419
|
+
toolbar: Number.parseFloat(cs.getPropertyValue("--vela-toolbar-gutter")) || 0,
|
|
38420
|
+
scale: Number.parseFloat(cs.getPropertyValue("--vela-scale-gutter")) || 0
|
|
38421
|
+
};
|
|
38422
|
+
}
|
|
37472
38423
|
setNear(near) {
|
|
37473
38424
|
if (near === this.near) return;
|
|
37474
38425
|
this.near = near;
|
|
@@ -37709,6 +38660,12 @@ function cellDrawings(opt) {
|
|
|
37709
38660
|
if (opt === true || opt == null) return { toolbar: false };
|
|
37710
38661
|
return { ...opt, toolbar: false };
|
|
37711
38662
|
}
|
|
38663
|
+
function instanceDeltas(handle) {
|
|
38664
|
+
if (!handle) return void 0;
|
|
38665
|
+
const inputs = inputDeltas(handle.inputs, handle.inputValues());
|
|
38666
|
+
const props = inputDeltas(handle.props, handle.propValues());
|
|
38667
|
+
return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
|
|
38668
|
+
}
|
|
37712
38669
|
var ChartCell = class {
|
|
37713
38670
|
constructor(id, gridHost, seed, deps) {
|
|
37714
38671
|
this.id = id;
|
|
@@ -37759,7 +38716,14 @@ var ChartCell = class {
|
|
|
37759
38716
|
this.destroyed = false;
|
|
37760
38717
|
this.appTheme = deps.theme;
|
|
37761
38718
|
const symbol = prefixedSymbol(seed);
|
|
37762
|
-
this.state = {
|
|
38719
|
+
this.state = {
|
|
38720
|
+
symbol,
|
|
38721
|
+
provider: parseSymbol(symbol ?? "").provider ?? void 0,
|
|
38722
|
+
timeframe: seed.timeframe,
|
|
38723
|
+
priceStyle: seed.priceStyle,
|
|
38724
|
+
bars: seed.bars,
|
|
38725
|
+
session: normalizeSession(seed.session)
|
|
38726
|
+
};
|
|
37763
38727
|
const doc = gridHost.ownerDocument;
|
|
37764
38728
|
this.host = doc.createElement("div");
|
|
37765
38729
|
this.host.className = "vela-cell";
|
|
@@ -37847,6 +38811,11 @@ var ChartCell = class {
|
|
|
37847
38811
|
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
37848
38812
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
37849
38813
|
this.statusline?.onChart(this.inner);
|
|
38814
|
+
this.statusline?.attachMenu({
|
|
38815
|
+
setPart: (part, visible) => this.setStatuslinePart(part, visible),
|
|
38816
|
+
chartVisible: () => this.inner?.renderer.get("candleVisible") !== false,
|
|
38817
|
+
setChartVisible: (visible) => this.inner?.renderer.set("candleVisible", visible)
|
|
38818
|
+
});
|
|
37850
38819
|
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
37851
38820
|
void this.inner.data.ready().then(() => {
|
|
37852
38821
|
if (this.inner && this.state.symbol) {
|
|
@@ -37884,6 +38853,7 @@ var ChartCell = class {
|
|
|
37884
38853
|
this.syncPresentNatives();
|
|
37885
38854
|
this.refreshNativeCatalog();
|
|
37886
38855
|
});
|
|
38856
|
+
this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
|
|
37887
38857
|
this.inner.on("indicator:removed", ({ id: id2 }) => {
|
|
37888
38858
|
if (this.destroyed) return;
|
|
37889
38859
|
const idx = this.instances.findIndex((it) => it.handle?.id === id2);
|
|
@@ -37892,7 +38862,7 @@ var ChartCell = class {
|
|
|
37892
38862
|
this.instances.splice(idx, 1);
|
|
37893
38863
|
this.history.push({
|
|
37894
38864
|
undo: () => {
|
|
37895
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
38865
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
37896
38866
|
this.instances.push(snapshot);
|
|
37897
38867
|
this.deps.onIndicatorsChanged(this.id);
|
|
37898
38868
|
},
|
|
@@ -38053,7 +39023,7 @@ var ChartCell = class {
|
|
|
38053
39023
|
kind: "select",
|
|
38054
39024
|
label: "Bars to fetch",
|
|
38055
39025
|
id: "bars",
|
|
38056
|
-
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
39026
|
+
options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
|
|
38057
39027
|
get: () => String(this.state.bars ?? 1e3),
|
|
38058
39028
|
set: (v) => {
|
|
38059
39029
|
this.state.bars = Number(v);
|
|
@@ -38085,10 +39055,34 @@ var ChartCell = class {
|
|
|
38085
39055
|
id: "status-line",
|
|
38086
39056
|
rows: [
|
|
38087
39057
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
38088
|
-
{
|
|
38089
|
-
|
|
38090
|
-
|
|
38091
|
-
|
|
39058
|
+
{
|
|
39059
|
+
kind: "toggle",
|
|
39060
|
+
label: "Symbol name",
|
|
39061
|
+
id: "name",
|
|
39062
|
+
get: () => sl.partVisible("name"),
|
|
39063
|
+
set: (v) => this.setStatuslinePart("name", v)
|
|
39064
|
+
},
|
|
39065
|
+
{
|
|
39066
|
+
kind: "toggle",
|
|
39067
|
+
label: "Market status",
|
|
39068
|
+
id: "market",
|
|
39069
|
+
get: () => sl.partVisible("market"),
|
|
39070
|
+
set: (v) => this.setStatuslinePart("market", v)
|
|
39071
|
+
},
|
|
39072
|
+
{
|
|
39073
|
+
kind: "toggle",
|
|
39074
|
+
label: "OHLC values",
|
|
39075
|
+
id: "ohlc",
|
|
39076
|
+
get: () => sl.partVisible("ohlc"),
|
|
39077
|
+
set: (v) => this.setStatuslinePart("ohlc", v)
|
|
39078
|
+
},
|
|
39079
|
+
{
|
|
39080
|
+
kind: "toggle",
|
|
39081
|
+
label: "Bar change values",
|
|
39082
|
+
id: "change",
|
|
39083
|
+
get: () => sl.partVisible("change"),
|
|
39084
|
+
set: (v) => this.setStatuslinePart("change", v)
|
|
39085
|
+
},
|
|
38092
39086
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
38093
39087
|
{
|
|
38094
39088
|
kind: "toggle",
|
|
@@ -38141,7 +39135,7 @@ var ChartCell = class {
|
|
|
38141
39135
|
statusPrefs() {
|
|
38142
39136
|
const sl = this.statusline;
|
|
38143
39137
|
return {
|
|
38144
|
-
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
39138
|
+
parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
38145
39139
|
indicatorTitles: this.indicatorTitlesOn,
|
|
38146
39140
|
indicatorValues: this.indicatorValuesOn
|
|
38147
39141
|
};
|
|
@@ -38281,9 +39275,9 @@ var ChartCell = class {
|
|
|
38281
39275
|
this.manifest = list;
|
|
38282
39276
|
if (this.pendingManifestNames) {
|
|
38283
39277
|
if (list.length === 0) return;
|
|
38284
|
-
for (const
|
|
38285
|
-
const entry = list.find((e) => e.name ===
|
|
38286
|
-
if (entry) this.addManifestInstance(entry, { record: false });
|
|
39278
|
+
for (const led of this.pendingManifestNames) {
|
|
39279
|
+
const entry = list.find((e) => e.name === ledgerEntryName(led));
|
|
39280
|
+
if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
|
|
38287
39281
|
}
|
|
38288
39282
|
this.pendingManifestNames = null;
|
|
38289
39283
|
return;
|
|
@@ -38313,9 +39307,10 @@ var ChartCell = class {
|
|
|
38313
39307
|
}
|
|
38314
39308
|
for (const it of [...this.instances]) this.dropInstance(it);
|
|
38315
39309
|
if (this.manifest.length > 0) {
|
|
38316
|
-
for (const
|
|
38317
|
-
const entry = this.manifest.find((e) => e.name ===
|
|
38318
|
-
if (entry)
|
|
39310
|
+
for (const item of led.manifest) {
|
|
39311
|
+
const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
|
|
39312
|
+
if (entry)
|
|
39313
|
+
this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
|
|
38319
39314
|
}
|
|
38320
39315
|
this.pendingManifestNames = null;
|
|
38321
39316
|
} else if (!this.deps.manifestSettled()) {
|
|
@@ -38364,12 +39359,16 @@ var ChartCell = class {
|
|
|
38364
39359
|
* a persistence handler's `restore` runs silently, a user-driven call records.
|
|
38365
39360
|
*/
|
|
38366
39361
|
addExternalIndicator(entry) {
|
|
38367
|
-
this.addManifestInstance(
|
|
39362
|
+
this.addManifestInstance(
|
|
39363
|
+
{ ...entry, enabled: true },
|
|
39364
|
+
{ external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} }
|
|
39365
|
+
);
|
|
38368
39366
|
}
|
|
38369
39367
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
38370
39368
|
addManifestInstance(entry, opts = {}) {
|
|
38371
39369
|
if (this.destroyed) return;
|
|
38372
|
-
const
|
|
39370
|
+
const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
|
|
39371
|
+
const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
|
|
38373
39372
|
this.instances.push(it);
|
|
38374
39373
|
this.deps.onIndicatorsChanged(this.id);
|
|
38375
39374
|
if (opts.record === false) return;
|
|
@@ -38377,7 +39376,7 @@ var ChartCell = class {
|
|
|
38377
39376
|
this.history.push({
|
|
38378
39377
|
undo: () => this.dropInstance(snapshot),
|
|
38379
39378
|
redo: () => {
|
|
38380
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
39379
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
38381
39380
|
this.instances.push(snapshot);
|
|
38382
39381
|
this.deps.onIndicatorsChanged(this.id);
|
|
38383
39382
|
}
|
|
@@ -38390,7 +39389,7 @@ var ChartCell = class {
|
|
|
38390
39389
|
const snapshot = it;
|
|
38391
39390
|
this.history.push({
|
|
38392
39391
|
undo: () => {
|
|
38393
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
39392
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
38394
39393
|
this.instances.push(snapshot);
|
|
38395
39394
|
this.deps.onIndicatorsChanged(this.id);
|
|
38396
39395
|
},
|
|
@@ -38400,6 +39399,9 @@ var ChartCell = class {
|
|
|
38400
39399
|
dropInstance(it) {
|
|
38401
39400
|
const idx = this.instances.indexOf(it);
|
|
38402
39401
|
if (idx >= 0) this.instances.splice(idx, 1);
|
|
39402
|
+
const captured = instanceDeltas(it.handle);
|
|
39403
|
+
if (captured) it.values = captured;
|
|
39404
|
+
else delete it.values;
|
|
38403
39405
|
try {
|
|
38404
39406
|
it.handle?.remove();
|
|
38405
39407
|
} catch {
|
|
@@ -38442,9 +39444,13 @@ var ChartCell = class {
|
|
|
38442
39444
|
this.deps.onIndicatorsChanged(this.id);
|
|
38443
39445
|
});
|
|
38444
39446
|
}
|
|
38445
|
-
addToChart(entry) {
|
|
39447
|
+
addToChart(entry, values) {
|
|
38446
39448
|
try {
|
|
38447
|
-
return this.inner?.addIndicator(entry.script,
|
|
39449
|
+
return this.inner?.addIndicator(entry.script, {
|
|
39450
|
+
...entry.language !== void 0 ? { language: entry.language } : {},
|
|
39451
|
+
...values?.inputs ? { inputs: values.inputs } : {},
|
|
39452
|
+
...values?.props ? { props: values.props } : {}
|
|
39453
|
+
}) ?? null;
|
|
38448
39454
|
} catch (err) {
|
|
38449
39455
|
console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
|
|
38450
39456
|
return null;
|
|
@@ -38558,7 +39564,10 @@ var ChartCell = class {
|
|
|
38558
39564
|
// manifest — their plugin persists them via the `ext` seam instead.
|
|
38559
39565
|
indicators: indicatorLedger({
|
|
38560
39566
|
present: this.inner ? this.inner.presentNativeIndicators() : [],
|
|
38561
|
-
|
|
39567
|
+
instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
|
|
39568
|
+
const d = it.handle ? instanceDeltas(it.handle) : it.values;
|
|
39569
|
+
return d ? { name: it.entry.name, ...d } : it.entry.name;
|
|
39570
|
+
}),
|
|
38562
39571
|
pendingManifest: this.pendingManifestNames,
|
|
38563
39572
|
manifestSettled: this.deps.manifestSettled(),
|
|
38564
39573
|
volumePending: this.volumeMayBePending && this.volumeIntent
|
|
@@ -38865,7 +39874,7 @@ var SplitterLayer = class {
|
|
|
38865
39874
|
};
|
|
38866
39875
|
|
|
38867
39876
|
// src/workspace/VelaWorkspace.ts
|
|
38868
|
-
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
39877
|
+
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
|
|
38869
39878
|
var GAP_PX = 2;
|
|
38870
39879
|
var POOL_CAP = 16;
|
|
38871
39880
|
var TIME_AXIS_H4 = 22;
|