wickchart 1.7.0 → 2.0.0
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 +38 -16
- package/package.json +5 -4
- package/src/core.js +2590 -3012
- package/src/wick-chart.js +87 -935
- package/src/wick-feed.js +4 -12
- package/types/core.d.ts +4 -234
- package/types/wick-chart.d.ts +14 -263
- package/types/wick-feed.d.ts +2 -2
package/src/wick-chart.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* Zero dependencies. Canvas-rendered. Framework-agnostic (works in React,
|
|
13
13
|
* Vue, plain HTML). Themeable with --wick-* CSS custom properties.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* 2.0: the guided-playback (narrate/walk/sonify/story), co-view, scenario/
|
|
16
|
+
* risk-plan and AI-agent families live in their plugin packages; the core
|
|
17
|
+
* methods are warn-once stubs until a package attaches.
|
|
17
18
|
*
|
|
18
19
|
* MIT License.
|
|
19
20
|
* ========================================================================== */
|
|
@@ -26,22 +27,19 @@ import {
|
|
|
26
27
|
parseIndicators, normalizeIndicatorResult, BUILTIN_INDICATORS,
|
|
27
28
|
positionPnl, positionPnlPct, checkAlertCross, computeStats, safeColor,
|
|
28
29
|
SERIES_TYPES, calcHeikinAshi, buildColumns, computeVolumeProfile,
|
|
29
|
-
calcRSI, detectAnnotations,
|
|
30
|
+
calcRSI, detectAnnotations,
|
|
30
31
|
calcRealizedVol, volRegimeBands, percentileOfSorted, parseVolShading,
|
|
31
32
|
windowSummary, normalizeOverlays, barIndexForTime, resolveOverlayColor,
|
|
32
33
|
compileScript, predicateTrueSeries, scriptAlertStep,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
narrateWindow, brushStats,
|
|
36
|
-
easeInOutCubic, sceneList,
|
|
34
|
+
brushStats,
|
|
35
|
+
warnDeprecatedAlias,
|
|
37
36
|
} from './core.js';
|
|
38
37
|
|
|
39
38
|
/* ------------------------------------------------------------------ *
|
|
40
39
|
* <wick-chart>
|
|
41
40
|
* ------------------------------------------------------------------ */
|
|
42
41
|
|
|
43
|
-
/** Indicator registry — module scope
|
|
44
|
-
* <wick-chart> and the deprecated <hab-chart> alias element. */
|
|
42
|
+
/** Indicator registry — module scope, shared by every chart instance. */
|
|
45
43
|
const REGISTRY = new Map(BUILTIN_INDICATORS);
|
|
46
44
|
|
|
47
45
|
/** Bars from which the worker compute path engages — below it, sync wins
|
|
@@ -70,7 +68,7 @@ const HTMLElementBase = typeof HTMLElement !== 'undefined' ? HTMLElement : class
|
|
|
70
68
|
|
|
71
69
|
class WickChart extends HTMLElementBase {
|
|
72
70
|
static get observedAttributes() {
|
|
73
|
-
return ['theme', 'type', 'log', 'auto', 'indicators', 'precision', 'label', 'stats', 'profile', 'annotations', 'volshading', 'overlays', '
|
|
71
|
+
return ['theme', 'type', 'log', 'auto', 'indicators', 'precision', 'label', 'stats', 'profile', 'annotations', 'volshading', 'overlays', 'brush', 'alert-evaluate', 'timezone', 'vwap-anchor', 'worker'];
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
/**
|
|
@@ -97,7 +95,7 @@ class WickChart extends HTMLElementBase {
|
|
|
97
95
|
container-type: inline-size;
|
|
98
96
|
}
|
|
99
97
|
:host(:focus-visible) {
|
|
100
|
-
outline: 2px solid var(--wick-accent,
|
|
98
|
+
outline: 2px solid var(--wick-accent, #4c8dff);
|
|
101
99
|
outline-offset: -2px;
|
|
102
100
|
}
|
|
103
101
|
.wrap { position: absolute; inset: 0; overflow: hidden; }
|
|
@@ -127,26 +125,26 @@ class WickChart extends HTMLElementBase {
|
|
|
127
125
|
}
|
|
128
126
|
.legend .row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
|
129
127
|
.legend .sym {
|
|
130
|
-
color: var(--wick-text-strong,
|
|
128
|
+
color: var(--wick-text-strong, #e6edf3);
|
|
131
129
|
font-weight: 700;
|
|
132
130
|
font-size: 13px;
|
|
133
131
|
letter-spacing: 0.02em;
|
|
134
132
|
}
|
|
135
133
|
.legend .kv { display: inline-flex; gap: 5px; align-items: baseline; white-space: nowrap; }
|
|
136
|
-
.legend .k { color: var(--wick-text,
|
|
137
|
-
.legend .v { color: var(--wick-text-strong,
|
|
134
|
+
.legend .k { color: var(--wick-text, #8b949e); font-size: 11px; }
|
|
135
|
+
.legend .v { color: var(--wick-text-strong, #e6edf3); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
138
136
|
.legend .pct { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
139
|
-
.legend .up { color: var(--wick-up,
|
|
140
|
-
.legend .dn { color: var(--wick-down,
|
|
137
|
+
.legend .up { color: var(--wick-up, #16c784); }
|
|
138
|
+
.legend .dn { color: var(--wick-down, #ea3943); }
|
|
141
139
|
.legend .ind {
|
|
142
140
|
display: inline-flex; align-items: center; gap: 6px;
|
|
143
|
-
color: var(--wick-text,
|
|
141
|
+
color: var(--wick-text, #8b949e); font-size: 11.5px; white-space: nowrap;
|
|
144
142
|
}
|
|
145
143
|
.legend .ind i { width: 8px; height: 2.5px; border-radius: 2px; display: inline-block; }
|
|
146
144
|
.legend .ind .v { font-size: 12px; }
|
|
147
145
|
.legend .insight {
|
|
148
|
-
color: var(--wick-accent,
|
|
149
|
-
background: var(--wick-chip,
|
|
146
|
+
color: var(--wick-accent, #4c8dff);
|
|
147
|
+
background: var(--wick-chip, rgba(127, 137, 153, 0.12));
|
|
150
148
|
border-radius: 6px;
|
|
151
149
|
padding: 1px 8px;
|
|
152
150
|
font-size: 11.5px;
|
|
@@ -155,7 +153,7 @@ class WickChart extends HTMLElementBase {
|
|
|
155
153
|
.nodata {
|
|
156
154
|
position: absolute; inset: 0;
|
|
157
155
|
display: flex; align-items: center; justify-content: center;
|
|
158
|
-
color: var(--wick-text,
|
|
156
|
+
color: var(--wick-text, #8b949e);
|
|
159
157
|
font: 500 13px ${FONT_STACK};
|
|
160
158
|
pointer-events: none;
|
|
161
159
|
}
|
|
@@ -168,25 +166,25 @@ class WickChart extends HTMLElementBase {
|
|
|
168
166
|
}
|
|
169
167
|
.hud .pos {
|
|
170
168
|
display: inline-flex; gap: 9px; align-items: baseline; white-space: nowrap;
|
|
171
|
-
background: var(--wick-chip,
|
|
172
|
-
border: 1px solid var(--wick-border,
|
|
169
|
+
background: var(--wick-chip, rgba(127, 137, 153, 0.12));
|
|
170
|
+
border: 1px solid var(--wick-border, rgba(148, 163, 184, 0.2));
|
|
173
171
|
border-radius: 7px;
|
|
174
172
|
padding: 3px 9px;
|
|
175
173
|
}
|
|
176
174
|
.hud .statsrow {
|
|
177
175
|
display: inline-flex; gap: 12px; white-space: nowrap;
|
|
178
|
-
background: var(--wick-chip,
|
|
179
|
-
border: 1px solid var(--wick-border,
|
|
176
|
+
background: var(--wick-chip, rgba(127, 137, 153, 0.12));
|
|
177
|
+
border: 1px solid var(--wick-border, rgba(148, 163, 184, 0.2));
|
|
180
178
|
border-radius: 7px;
|
|
181
179
|
padding: 3px 10px;
|
|
182
|
-
color: var(--wick-text,
|
|
180
|
+
color: var(--wick-text, #8b949e);
|
|
183
181
|
font-variant-numeric: tabular-nums;
|
|
184
182
|
}
|
|
185
|
-
.hud .statsrow b { color: var(--wick-text-strong,
|
|
186
|
-
.hud .k { color: var(--wick-text,
|
|
187
|
-
.hud .v { color: var(--wick-text-strong,
|
|
188
|
-
.hud .up { color: var(--wick-up,
|
|
189
|
-
.hud .dn { color: var(--wick-down,
|
|
183
|
+
.hud .statsrow b { color: var(--wick-text-strong, #e6edf3); font-weight: 600; }
|
|
184
|
+
.hud .k { color: var(--wick-text, #8b949e); font-weight: 500; }
|
|
185
|
+
.hud .v { color: var(--wick-text-strong, #e6edf3); font-variant-numeric: tabular-nums; }
|
|
186
|
+
.hud .up { color: var(--wick-up, #16c784); }
|
|
187
|
+
.hud .dn { color: var(--wick-down, #ea3943); }
|
|
190
188
|
|
|
191
189
|
/* Narrow charts: the legend (top-left) and the HUD (top-right) are
|
|
192
190
|
both pinned to the top, so on a phone they land on top of each
|
|
@@ -263,31 +261,21 @@ class WickChart extends HTMLElementBase {
|
|
|
263
261
|
this._annoList = null;
|
|
264
262
|
this._volshade = null;
|
|
265
263
|
|
|
266
|
-
//
|
|
267
|
-
|
|
264
|
+
// co-view seams (2.0: driven by the wickchart-coview plugin — the
|
|
265
|
+
// layer/state contract its presence bands and ghost crosshair use,
|
|
266
|
+
// and disconnectedCallback cleans up)
|
|
268
267
|
this._coviewCh = null;
|
|
269
|
-
this._coviewPeer = '';
|
|
270
|
-
this._coviewLast = 0;
|
|
271
268
|
this._ghost = null;
|
|
272
269
|
this._ghostTimer = 0;
|
|
273
|
-
|
|
274
|
-
this._coviewLabel = null; // display name from the co-view-name attribute
|
|
275
|
-
this._presence = new PresenceTracker();
|
|
270
|
+
this._presence = null;
|
|
276
271
|
this._coviewBeat = 0;
|
|
277
|
-
|
|
272
|
+
// scenario/risk seams (2.0: driven by the wickchart-scenario plugin;
|
|
273
|
+
// _rightMargin reserves future space from the active scenario)
|
|
274
|
+
this._scenario = null;
|
|
275
|
+
this._riskPlan = null;
|
|
278
276
|
|
|
279
|
-
// sonification state
|
|
280
|
-
this._sonify = false;
|
|
281
|
-
this._actx = null;
|
|
282
|
-
this._lastToneIdx = -1;
|
|
283
|
-
this._playToken = 0;
|
|
284
277
|
this._measure = null; // { iA, pA, iB, pB, done }
|
|
285
278
|
this._measuring = false;
|
|
286
|
-
// bar-walk narrator state
|
|
287
|
-
this._walkTimer = 0;
|
|
288
|
-
// story mode state: token cancels stale async runs
|
|
289
|
-
this._storyToken = 0;
|
|
290
|
-
this._story = null;
|
|
291
279
|
// delta brush state: mode flag + current/finished selection
|
|
292
280
|
this._brush = false;
|
|
293
281
|
this._brushSel = null; // { i0, i1, stats } — the committed selection
|
|
@@ -339,11 +327,6 @@ class WickChart extends HTMLElementBase {
|
|
|
339
327
|
// server-side overlays (zones & levels)
|
|
340
328
|
this._overlays = [];
|
|
341
329
|
|
|
342
|
-
// scenario projection (ghost path + vol cone)
|
|
343
|
-
this._scenario = null;
|
|
344
|
-
// risk plan (R-multiple grid)
|
|
345
|
-
this._riskPlan = null;
|
|
346
|
-
|
|
347
330
|
this._onResize = () => this._invalidate();
|
|
348
331
|
this._onPointerDown = (e) => this._pointerDown(e);
|
|
349
332
|
this._onPointerMove = (e) => this._pointerMove(e);
|
|
@@ -357,7 +340,6 @@ class WickChart extends HTMLElementBase {
|
|
|
357
340
|
};
|
|
358
341
|
this._onWheel = (e) => this._wheel(e);
|
|
359
342
|
this._onDbl = () => {
|
|
360
|
-
this._stopPlayback();
|
|
361
343
|
this.fit();
|
|
362
344
|
};
|
|
363
345
|
this._onKey = (e) => this._keydown(e);
|
|
@@ -414,18 +396,16 @@ class WickChart extends HTMLElementBase {
|
|
|
414
396
|
if (document.fonts && document.fonts.ready) {
|
|
415
397
|
document.fonts.ready.then(() => this._invalidate()).catch(() => {});
|
|
416
398
|
}
|
|
417
|
-
if (this._coviewName) this._setupCoView();
|
|
418
399
|
this._watchDpr();
|
|
419
400
|
this._invalidate();
|
|
420
401
|
}
|
|
421
402
|
|
|
422
403
|
disconnectedCallback() {
|
|
423
404
|
this._connected = false;
|
|
424
|
-
this.stopWalk(true);
|
|
425
|
-
this.stopStory(true);
|
|
426
405
|
this._layerClaim = null;
|
|
406
|
+
// co-view seams (driven by the wickchart-coview plugin): close the
|
|
407
|
+
// room cleanly and stop its timers so a removed chart goes quiet
|
|
427
408
|
if (this._coviewCh) {
|
|
428
|
-
this._coviewSend({ type: 'bye' });
|
|
429
409
|
try {
|
|
430
410
|
this._coviewCh.close();
|
|
431
411
|
} catch (_) {}
|
|
@@ -433,12 +413,9 @@ class WickChart extends HTMLElementBase {
|
|
|
433
413
|
}
|
|
434
414
|
clearInterval(this._coviewBeat);
|
|
435
415
|
this._coviewBeat = 0;
|
|
436
|
-
if (this._presence && this._presence.peers.size) {
|
|
437
|
-
const left = this._presence.list();
|
|
438
|
-
this._presence = new PresenceTracker();
|
|
439
|
-
this._fire('peers', { peers: [], joined: [], left });
|
|
440
|
-
}
|
|
441
416
|
clearTimeout(this._ghostTimer);
|
|
417
|
+
this._ghost = null;
|
|
418
|
+
this._presence = null;
|
|
442
419
|
if (this._ro) this._ro.disconnect();
|
|
443
420
|
this._unwatchDpr();
|
|
444
421
|
const cv = this._canvas;
|
|
@@ -526,24 +503,12 @@ class WickChart extends HTMLElementBase {
|
|
|
526
503
|
this._overlays = ovs;
|
|
527
504
|
break;
|
|
528
505
|
}
|
|
529
|
-
case 'co-view':
|
|
530
|
-
this._coviewName = val || null;
|
|
531
|
-
this._setupCoView();
|
|
532
|
-
break;
|
|
533
|
-
case 'co-view-name':
|
|
534
|
-
// display name travels with every presence message; no re-render
|
|
535
|
-
this._coviewLabel = val || null;
|
|
536
|
-
break;
|
|
537
506
|
case 'brush':
|
|
538
507
|
this._brush = val != null && val !== 'false';
|
|
539
508
|
this._brushSel = null;
|
|
540
509
|
this._brushDrag = null;
|
|
541
510
|
this._invalidate();
|
|
542
511
|
break;
|
|
543
|
-
case 'sonify':
|
|
544
|
-
this._sonify = val != null && val !== 'false';
|
|
545
|
-
this._lastToneIdx = -1;
|
|
546
|
-
break;
|
|
547
512
|
// Default evaluation mode for alerts added without one. Anything
|
|
548
513
|
// other than "close" means live, so a typo cannot silently mute
|
|
549
514
|
// signals — it degrades to today's behaviour.
|
|
@@ -607,8 +572,7 @@ class WickChart extends HTMLElementBase {
|
|
|
607
572
|
});
|
|
608
573
|
}
|
|
609
574
|
|
|
610
|
-
/** The tag this class registers as.
|
|
611
|
-
* registered from the HabChart subclass, not this name.) */
|
|
575
|
+
/** The tag this class registers as. */
|
|
612
576
|
static get elementName() {
|
|
613
577
|
return 'wick-chart';
|
|
614
578
|
}
|
|
@@ -1018,8 +982,7 @@ class WickChart extends HTMLElementBase {
|
|
|
1018
982
|
* WickScript predicate (`when`) on every streamed bar and fire on its
|
|
1019
983
|
* false→true edge — e.g. `when: 'crossup(rsi(close,14), 30)'` or
|
|
1020
984
|
* `when: 'volume > sma(volume,20) * 3'`. Scripted events carry the
|
|
1021
|
-
* triggering close as `price` plus the `when` source
|
|
1022
|
-
* `hab:alert` alias still dispatched).
|
|
985
|
+
* triggering close as `price` plus the `when` source.
|
|
1023
986
|
* @param {{id?: string, price?: number, direction?: 'above'|'below'|'cross',
|
|
1024
987
|
* when?: string, once?: boolean}} alert
|
|
1025
988
|
* @returns {string|null} the alert id (null when no valid price/when,
|
|
@@ -1123,157 +1086,21 @@ class WickChart extends HTMLElementBase {
|
|
|
1123
1086
|
this._invalidate();
|
|
1124
1087
|
}
|
|
1125
1088
|
|
|
1126
|
-
/**
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
* chart.setScenario({ path: [64000, 65500, 68000], label: 'bull case' });
|
|
1131
|
-
* chart.setScenario({ horizon: 48, cone: true }); // cone-only
|
|
1132
|
-
*
|
|
1133
|
-
* The path is an array of prices (or {price} objects) for future bars
|
|
1134
|
-
* 1..N; horizon defaults to the path length (1–500). `cone` (default
|
|
1135
|
-
* true) draws ±levels·σ bands widening with √h from the current realized
|
|
1136
|
-
* vol; `color` accepts up|down|accent or safe CSS colors. Setting a
|
|
1137
|
-
* scenario reserves future space on the right; analysis data — excluded
|
|
1138
|
-
* from getState/setState.
|
|
1139
|
-
* @param {object} spec
|
|
1140
|
-
* @returns {object|null} the normalized scenario, or null when invalid
|
|
1141
|
-
*/
|
|
1142
|
-
setScenario(spec) {
|
|
1143
|
-
this._scenario = normalizeScenario(spec);
|
|
1144
|
-
this._invalidate();
|
|
1145
|
-
return this._scenario;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
clearScenario() {
|
|
1149
|
-
this._scenario = null;
|
|
1150
|
-
this._invalidate();
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
/** @returns {object|null} a copy of the active scenario */
|
|
1089
|
+
/** @returns {object|null} a copy of the active scenario (set through the
|
|
1090
|
+
* `_scenario` seam by the wickchart-scenario plugin; reserves future
|
|
1091
|
+
* space via _rightMargin) */
|
|
1154
1092
|
get scenario() {
|
|
1155
1093
|
if (!this._scenario) return null;
|
|
1156
1094
|
return { ...this._scenario, path: this._scenario.path.map((p) => ({ ...p })) };
|
|
1157
1095
|
}
|
|
1158
1096
|
|
|
1159
|
-
/**
|
|
1160
|
-
*
|
|
1161
|
-
* stop| (the risk unit); reward lines are drawn at kR beyond the entry
|
|
1162
|
-
* with the risk/reward zones shaded, so sizing and take-profit choices
|
|
1163
|
-
* read directly off the chart.
|
|
1164
|
-
*
|
|
1165
|
-
* chart.setRiskPlan({ entry: 64500, stop: 63800, multiples: [1, 2, 3] });
|
|
1166
|
-
* chart.setRiskPlan({ entry: 64500, stop: 63800, targets: [65900, 67300] });
|
|
1167
|
-
*
|
|
1168
|
-
* Direction is derived (stop below entry ⇒ long). Targets convert to
|
|
1169
|
-
* their R multiple; `multiples` win when both are given. Invalid specs
|
|
1170
|
-
* clear the plan (replace semantics, like setScenario); excluded from
|
|
1171
|
-
* getState/setState — it is app state, not chart state.
|
|
1172
|
-
* @param {object} spec
|
|
1173
|
-
* @returns {object|null} the normalized plan, or null when invalid
|
|
1174
|
-
*/
|
|
1175
|
-
setRiskPlan(spec) {
|
|
1176
|
-
this._riskPlan = normalizeRiskPlan(spec);
|
|
1177
|
-
this._invalidate();
|
|
1178
|
-
return this._riskPlan;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
clearRiskPlan() {
|
|
1182
|
-
if (this._riskPlan) {
|
|
1183
|
-
this._riskPlan = null;
|
|
1184
|
-
this._invalidate();
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/** @returns {object|null} a copy of the active risk plan */
|
|
1097
|
+
/** @returns {object|null} a copy of the active risk plan (set through
|
|
1098
|
+
* the `_riskPlan` seam by the wickchart-scenario plugin) */
|
|
1189
1099
|
get riskPlan() {
|
|
1190
1100
|
if (!this._riskPlan) return null;
|
|
1191
1101
|
return { ...this._riskPlan, levels: this._riskPlan.levels.map((l) => ({ ...l })) };
|
|
1192
1102
|
}
|
|
1193
1103
|
|
|
1194
|
-
/** σ-cone for the active scenario, cached per data version. */
|
|
1195
|
-
_scenarioConeCache() {
|
|
1196
|
-
if (!this._scenario || !this._data.length) return null;
|
|
1197
|
-
if (this._cache.v !== this._version) {
|
|
1198
|
-
this._cache = { v: this._version, map: {} };
|
|
1199
|
-
}
|
|
1200
|
-
if (!this._cache.map.__scenario) {
|
|
1201
|
-
const d = this._data;
|
|
1202
|
-
const vol = calcRealizedVol(d.map((b) => b.close), 20);
|
|
1203
|
-
let v = NaN;
|
|
1204
|
-
for (let i = vol.length - 1; i >= 0; i--) {
|
|
1205
|
-
if (Number.isFinite(vol[i])) { v = vol[i]; break; }
|
|
1206
|
-
}
|
|
1207
|
-
this._cache.map.__scenario = calcVolCone(
|
|
1208
|
-
d[d.length - 1].close,
|
|
1209
|
-
v,
|
|
1210
|
-
this._scenario.horizon,
|
|
1211
|
-
this._scenario.levels
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
return this._cache.map.__scenario;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
/* ------------------------------------------------------------ *
|
|
1218
|
-
* AI agent interface — the chart as a tool surface
|
|
1219
|
-
* ------------------------------------------------------------ */
|
|
1220
|
-
|
|
1221
|
-
/** Tool manifest for LLM control — JSON-safe copy of AI_TOOLS. */
|
|
1222
|
-
aiTools() {
|
|
1223
|
-
return JSON.parse(JSON.stringify(AI_TOOLS));
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
/** System prompt for agent control — paste into any LLM alongside aiTools(). */
|
|
1227
|
-
aiPrompt() {
|
|
1228
|
-
return aiPromptText();
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
/** Grounding context for a model: current state + visible-window summary. */
|
|
1232
|
-
aiContext() {
|
|
1233
|
-
return { state: this.getState(), window: this.getDataWindow() };
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
/**
|
|
1237
|
-
* Apply a list of {tool, args} ops (typically LLM output) through the
|
|
1238
|
-
* validated dispatcher in core. Never throws — each op resolves
|
|
1239
|
-
* {ok, tool, result} or {ok: false, tool, error} so an agent can
|
|
1240
|
-
* self-correct.
|
|
1241
|
-
* @param {any} ops
|
|
1242
|
-
* @returns {Array<object>}
|
|
1243
|
-
*/
|
|
1244
|
-
applyAI(ops) {
|
|
1245
|
-
return applyChartOps(this, ops);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
/**
|
|
1249
|
-
* Ask an AI to operate the chart. Builds the payload {system,
|
|
1250
|
-
* instruction, chart, tools}; with a `run` async function (your model
|
|
1251
|
-
* call — the chart itself never touches the network), applies the
|
|
1252
|
-
* returned ops and resolves {payload, ops, results}. Without `run`,
|
|
1253
|
-
* returns the payload for manual wiring — send it anywhere, then call
|
|
1254
|
-
* chart.applyAI(ops) with the model's answer.
|
|
1255
|
-
*
|
|
1256
|
-
* const { results } = await chart.ask('add RSI and mark the demand zone', {
|
|
1257
|
-
* run: async (payload) => (await callMyLLM(payload)).ops,
|
|
1258
|
-
* });
|
|
1259
|
-
*
|
|
1260
|
-
* @param {string} instruction natural-language request
|
|
1261
|
-
* @param {{run?: (payload: object) => Promise<any>}} [opts]
|
|
1262
|
-
*/
|
|
1263
|
-
async ask(instruction, opts = {}) {
|
|
1264
|
-
const payload = {
|
|
1265
|
-
system: aiPromptText(),
|
|
1266
|
-
instruction: String(instruction == null ? '' : instruction),
|
|
1267
|
-
chart: this.aiContext(),
|
|
1268
|
-
tools: this.aiTools(),
|
|
1269
|
-
};
|
|
1270
|
-
if (typeof opts.run !== 'function') {
|
|
1271
|
-
return { payload, ops: null, results: null };
|
|
1272
|
-
}
|
|
1273
|
-
const ops = await opts.run(payload);
|
|
1274
|
-
const results = this.applyAI(ops);
|
|
1275
|
-
return { payload, ops, results };
|
|
1276
|
-
}
|
|
1277
1104
|
|
|
1278
1105
|
/** Check alerts against an incoming bar (prev close → new close).
|
|
1279
1106
|
* Scripted (`when`) alerts evaluate their predicate series, cached per
|
|
@@ -1491,11 +1318,7 @@ class WickChart extends HTMLElementBase {
|
|
|
1491
1318
|
if (this._pal && this._palKey === this._theme) return this._pal;
|
|
1492
1319
|
const base = THEMES[this._theme] || THEMES.dark;
|
|
1493
1320
|
const cs = getComputedStyle(this);
|
|
1494
|
-
|
|
1495
|
-
const get = (name, fallback) => {
|
|
1496
|
-
const v = cs.getPropertyValue('--wick-' + name).trim() || cs.getPropertyValue('--hab-' + name).trim();
|
|
1497
|
-
return v || fallback;
|
|
1498
|
-
};
|
|
1321
|
+
const get = (name, fallback) => cs.getPropertyValue('--wick-' + name).trim() || fallback;
|
|
1499
1322
|
const pal = {};
|
|
1500
1323
|
for (const k of Object.keys(base)) {
|
|
1501
1324
|
if (k === 'overlay') {
|
|
@@ -1504,8 +1327,7 @@ class WickChart extends HTMLElementBase {
|
|
|
1504
1327
|
o.push(get('overlay-' + i, base.overlay[i]));
|
|
1505
1328
|
}
|
|
1506
1329
|
// allow single overlay color
|
|
1507
|
-
const single =
|
|
1508
|
-
cs.getPropertyValue('--wick-overlay').trim() || cs.getPropertyValue('--hab-overlay').trim();
|
|
1330
|
+
const single = get('overlay', '');
|
|
1509
1331
|
pal.overlay = single ? base.overlay.map(() => single) : o;
|
|
1510
1332
|
} else {
|
|
1511
1333
|
pal[k] = get(k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), base[k]);
|
|
@@ -2225,82 +2047,7 @@ class WickChart extends HTMLElementBase {
|
|
|
2225
2047
|
}
|
|
2226
2048
|
}
|
|
2227
2049
|
|
|
2228
|
-
|
|
2229
|
-
* the main plot so out-of-range bands never bleed into the axis) */
|
|
2230
|
-
if (this._scenario && d.length) {
|
|
2231
|
-
const sc = this._scenario;
|
|
2232
|
-
const col = resolveOverlayColor(sc.color, pal);
|
|
2233
|
-
const baseIdx = d.length - 1;
|
|
2234
|
-
const xAt = (h) => this._xFor(baseIdx + h);
|
|
2235
|
-
ctx.save();
|
|
2236
|
-
ctx.beginPath();
|
|
2237
|
-
ctx.rect(0, main.y0, plotRight, main.h);
|
|
2238
|
-
ctx.clip();
|
|
2239
|
-
if (sc.cone) {
|
|
2240
|
-
const cone = this._scenarioConeCache();
|
|
2241
|
-
if (cone) {
|
|
2242
|
-
for (let li = cone.levels.length - 1; li >= 0; li--) {
|
|
2243
|
-
const b = cone.bands[cone.levels[li]];
|
|
2244
|
-
ctx.globalAlpha = li === 0 ? 0.1 : 0.05;
|
|
2245
|
-
ctx.fillStyle = col;
|
|
2246
|
-
ctx.beginPath();
|
|
2247
|
-
ctx.moveTo(xAt(0), yOf(b.up[0]));
|
|
2248
|
-
for (let h = 1; h <= cone.horizon; h++) ctx.lineTo(xAt(h), yOf(b.up[h]));
|
|
2249
|
-
for (let h = cone.horizon; h >= 0; h--) ctx.lineTo(xAt(h), yOf(b.down[h]));
|
|
2250
|
-
ctx.closePath();
|
|
2251
|
-
ctx.fill();
|
|
2252
|
-
}
|
|
2253
|
-
const inner = cone.bands[cone.levels[0]];
|
|
2254
|
-
ctx.globalAlpha = 0.4;
|
|
2255
|
-
ctx.strokeStyle = col;
|
|
2256
|
-
ctx.lineWidth = 1;
|
|
2257
|
-
ctx.setLineDash([4, 4]);
|
|
2258
|
-
for (const arr of [inner.up, inner.down]) {
|
|
2259
|
-
ctx.beginPath();
|
|
2260
|
-
ctx.moveTo(xAt(0), yOf(arr[0]));
|
|
2261
|
-
for (let h = 1; h <= cone.horizon; h++) ctx.lineTo(xAt(h), yOf(arr[h]));
|
|
2262
|
-
ctx.stroke();
|
|
2263
|
-
}
|
|
2264
|
-
ctx.setLineDash([]);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
if (sc.path.length) {
|
|
2268
|
-
ctx.globalAlpha = 0.9;
|
|
2269
|
-
ctx.strokeStyle = col;
|
|
2270
|
-
ctx.lineWidth = 1.5;
|
|
2271
|
-
ctx.setLineDash([6, 4]);
|
|
2272
|
-
ctx.beginPath();
|
|
2273
|
-
ctx.moveTo(xAt(0), yOf(d[baseIdx].close));
|
|
2274
|
-
for (const p of sc.path) ctx.lineTo(xAt(p.h), yOf(p.price));
|
|
2275
|
-
ctx.stroke();
|
|
2276
|
-
ctx.setLineDash([]);
|
|
2277
|
-
ctx.fillStyle = col;
|
|
2278
|
-
for (const p of sc.path) {
|
|
2279
|
-
const y = yOf(p.price);
|
|
2280
|
-
if (y >= main.y0 && y <= main.y1) {
|
|
2281
|
-
ctx.beginPath();
|
|
2282
|
-
ctx.arc(xAt(p.h), y, 2.5, 0, Math.PI * 2);
|
|
2283
|
-
ctx.fill();
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
if (sc.label) {
|
|
2287
|
-
const p = sc.path[sc.path.length - 1];
|
|
2288
|
-
ctx.font = pillFont();
|
|
2289
|
-
ctx.globalAlpha = 0.95;
|
|
2290
|
-
ctx.fillStyle = col;
|
|
2291
|
-
ctx.textAlign = 'left';
|
|
2292
|
-
ctx.textBaseline = 'middle';
|
|
2293
|
-
ctx.fillText(
|
|
2294
|
-
sc.label,
|
|
2295
|
-
Math.min(xAt(p.h) + 8, plotRight - 4),
|
|
2296
|
-
clamp(yOf(p.price), main.y0 + 8, main.y1 - 8)
|
|
2297
|
-
);
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
ctx.restore();
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
/* position zones (under series) */
|
|
2050
|
+
/* position zones (under series) */
|
|
2304
2051
|
for (const pos of this._positions) {
|
|
2305
2052
|
const yE = clamp(yOf(pos.entry), main.y0, main.y1);
|
|
2306
2053
|
if (isNum(pos.target)) {
|
|
@@ -2315,68 +2062,7 @@ class WickChart extends HTMLElementBase {
|
|
|
2315
2062
|
}
|
|
2316
2063
|
}
|
|
2317
2064
|
|
|
2318
|
-
|
|
2319
|
-
if (this._riskPlan && d.length) {
|
|
2320
|
-
const rp = this._riskPlan;
|
|
2321
|
-
const fP = numberFmt(this._prec(scale.rawHi || 1));
|
|
2322
|
-
const yE = yOf(rp.entry);
|
|
2323
|
-
const yS = yOf(rp.stop);
|
|
2324
|
-
ctx.fillStyle = hexToRgba(pal.down, 0.06);
|
|
2325
|
-
ctx.fillRect(0, Math.min(yE, yS), plotRight, Math.abs(yS - yE));
|
|
2326
|
-
const yTop = yOf(rp.levels[rp.levels.length - 1].price);
|
|
2327
|
-
ctx.fillStyle = hexToRgba(pal.up, 0.05);
|
|
2328
|
-
ctx.fillRect(0, Math.min(yE, yTop), plotRight, Math.abs(yTop - yE));
|
|
2329
|
-
const line = (p, col, dash) => {
|
|
2330
|
-
const y = yOf(p);
|
|
2331
|
-
if (y < main.y0 || y > main.y1) return null;
|
|
2332
|
-
ctx.strokeStyle = col;
|
|
2333
|
-
ctx.lineWidth = 1.5;
|
|
2334
|
-
if (dash) ctx.setLineDash([5, 4]);
|
|
2335
|
-
ctx.beginPath();
|
|
2336
|
-
ctx.moveTo(0, Math.round(y) + 0.5);
|
|
2337
|
-
ctx.lineTo(plotRight, Math.round(y) + 0.5);
|
|
2338
|
-
ctx.stroke();
|
|
2339
|
-
ctx.setLineDash([]);
|
|
2340
|
-
ctx.lineWidth = 1;
|
|
2341
|
-
return y;
|
|
2342
|
-
};
|
|
2343
|
-
const pills = [];
|
|
2344
|
-
for (let i = rp.levels.length - 1; i >= 0; i--) {
|
|
2345
|
-
const lv = rp.levels[i];
|
|
2346
|
-
const y = line(lv.price, pal.up, true);
|
|
2347
|
-
if (y != null) {
|
|
2348
|
-
const kk = lv.k % 1 === 0 ? lv.k : +lv.k.toFixed(2);
|
|
2349
|
-
pills.push({ y, text: `${kk}R ${fP.format(lv.price)}`, bg: pal.up });
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
const yStop = line(rp.stop, pal.down, false);
|
|
2353
|
-
if (yStop != null) pills.push({ y: yStop, text: `STOP ${fP.format(rp.stop)}`, bg: pal.down });
|
|
2354
|
-
const yEnt = line(rp.entry, pal.accent, false);
|
|
2355
|
-
if (yEnt != null) {
|
|
2356
|
-
pills.push({ y: yEnt, text: `${rp.direction === 'long' ? 'LONG' : 'SHORT'} ${fP.format(rp.entry)}`, bg: pal.accent });
|
|
2357
|
-
}
|
|
2358
|
-
// stack right-edge pills instead of letting close lines overlap
|
|
2359
|
-
pills.sort((a, b) => a.y - b.y);
|
|
2360
|
-
let lastY = -Infinity;
|
|
2361
|
-
for (const p of pills) {
|
|
2362
|
-
const y = Math.max(p.y, lastY + 20);
|
|
2363
|
-
lastY = y;
|
|
2364
|
-
ctx.font = pillFont();
|
|
2365
|
-
const tw = ctx.measureText(p.text).width + 12;
|
|
2366
|
-
this._pill(plotRight - tw - 8, y, p.text, p.bg, pal.pillText, 'left', tw);
|
|
2367
|
-
}
|
|
2368
|
-
if (rp.label) {
|
|
2369
|
-
ctx.font = pillFont();
|
|
2370
|
-
ctx.fillStyle = pal.accent;
|
|
2371
|
-
ctx.globalAlpha = 0.9;
|
|
2372
|
-
ctx.textAlign = 'left';
|
|
2373
|
-
ctx.textBaseline = 'bottom';
|
|
2374
|
-
ctx.fillText(rp.label, 8, clamp(yE, main.y0 + 14, main.y1) - 3);
|
|
2375
|
-
ctx.globalAlpha = 1;
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
/* volume profile (behind the series) */
|
|
2065
|
+
/* volume profile (behind the series) */
|
|
2380
2066
|
if (this._profile) {
|
|
2381
2067
|
const pkey = `${i0}:${i1}:${this._version}`;
|
|
2382
2068
|
if (this._profileKey !== pkey) {
|
|
@@ -3048,78 +2734,6 @@ class WickChart extends HTMLElementBase {
|
|
|
3048
2734
|
);
|
|
3049
2735
|
}
|
|
3050
2736
|
|
|
3051
|
-
/* co-view presence: peer viewport bands along the top of the plot */
|
|
3052
|
-
if (this._presence && this._presence.peers.size && d.length) {
|
|
3053
|
-
const peers = this._presence.list().slice(0, 4);
|
|
3054
|
-
ctx.save();
|
|
3055
|
-
ctx.font = pillFont();
|
|
3056
|
-
for (let row = 0; row < peers.length; row++) {
|
|
3057
|
-
const p = peers[row];
|
|
3058
|
-
if (!p.range) continue;
|
|
3059
|
-
const cols = pal.overlay || [];
|
|
3060
|
-
const col = cols[(row + 1) % Math.max(cols.length, 1)] || pal.accent;
|
|
3061
|
-
const i0 = WickChart._indexForTime(this._data, p.range.from);
|
|
3062
|
-
const i1 = WickChart._indexForTime(this._data, p.range.to);
|
|
3063
|
-
const x0 = clamp(this._xFor(i0), 0, plotRight);
|
|
3064
|
-
const x1 = clamp(this._xFor(i1), 0, plotRight);
|
|
3065
|
-
const y = main.y0 + 2 + row * 5;
|
|
3066
|
-
ctx.globalAlpha = 0.8;
|
|
3067
|
-
ctx.fillStyle = col;
|
|
3068
|
-
ctx.fillRect(x0, y, Math.max(x1 - x0, 3), 3);
|
|
3069
|
-
if (x1 - x0 > 44) {
|
|
3070
|
-
ctx.globalAlpha = 0.95;
|
|
3071
|
-
ctx.textAlign = 'left';
|
|
3072
|
-
ctx.textBaseline = 'top';
|
|
3073
|
-
ctx.fillText(p.name || p.id, x0 + 3, y + 4);
|
|
3074
|
-
}
|
|
3075
|
-
}
|
|
3076
|
-
ctx.restore();
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
|
-
/* co-view ghost crosshair (peer pointer from another tab/chart) */
|
|
3080
|
-
if (this._ghost) {
|
|
3081
|
-
const g = this._ghost;
|
|
3082
|
-
const gx = this._xFor(g.index);
|
|
3083
|
-
const gxVisible = gx >= 0 && gx <= plotRight;
|
|
3084
|
-
ctx.save();
|
|
3085
|
-
ctx.strokeStyle = pal.accent;
|
|
3086
|
-
ctx.globalAlpha = 0.7;
|
|
3087
|
-
ctx.setLineDash([2, 3]);
|
|
3088
|
-
ctx.beginPath();
|
|
3089
|
-
if (gxVisible) {
|
|
3090
|
-
const cx = Math.round(gx) + 0.5;
|
|
3091
|
-
ctx.moveTo(cx, 0);
|
|
3092
|
-
ctx.lineTo(cx, plotBottom);
|
|
3093
|
-
}
|
|
3094
|
-
if (g.yFrac != null) {
|
|
3095
|
-
const gy = Math.round(main.y0 + g.yFrac * main.h) + 0.5;
|
|
3096
|
-
ctx.moveTo(0, gy);
|
|
3097
|
-
ctx.lineTo(plotRight, gy);
|
|
3098
|
-
if (gxVisible) {
|
|
3099
|
-
ctx.fillStyle = pal.accent;
|
|
3100
|
-
ctx.beginPath();
|
|
3101
|
-
ctx.arc(gx, main.y0 + g.yFrac * main.h, 3, 0, Math.PI * 2);
|
|
3102
|
-
ctx.fill();
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
ctx.stroke();
|
|
3106
|
-
ctx.restore();
|
|
3107
|
-
if (gxVisible && this._data[g.index]) {
|
|
3108
|
-
const tLabel = fmtFull(this._zt(this._data[g.index].time));
|
|
3109
|
-
ctx.font = pillFont();
|
|
3110
|
-
const tw = ctx.measureText(tLabel).width + 12;
|
|
3111
|
-
this._pill(
|
|
3112
|
-
clamp(gx - tw / 2, 2, plotRight - tw - 2),
|
|
3113
|
-
plotBottom + 2,
|
|
3114
|
-
tLabel,
|
|
3115
|
-
pal.accent,
|
|
3116
|
-
pal.pillText,
|
|
3117
|
-
'left',
|
|
3118
|
-
tw
|
|
3119
|
-
);
|
|
3120
|
-
}
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
2737
|
/* measure tool overlay */
|
|
3124
2738
|
if (this._measure && this._measure.pA != null && this._measure.pB != null) {
|
|
3125
2739
|
const m = this._measure;
|
|
@@ -3603,7 +3217,6 @@ class WickChart extends HTMLElementBase {
|
|
|
3603
3217
|
if (!this._ly || !this._data.length) return;
|
|
3604
3218
|
const idx = clamp(Math.round(this._indexForX(pt.x)), 0, this._data.length - 1);
|
|
3605
3219
|
this._hover = { index: idx, x: this._xFor(idx), y: pt.y };
|
|
3606
|
-
this._maybeSonify(idx);
|
|
3607
3220
|
this._emitCrosshair(this._hover);
|
|
3608
3221
|
this._invalidate();
|
|
3609
3222
|
}
|
|
@@ -3650,7 +3263,6 @@ class WickChart extends HTMLElementBase {
|
|
|
3650
3263
|
|
|
3651
3264
|
_pointerDown(e) {
|
|
3652
3265
|
if (e.button !== 0) return;
|
|
3653
|
-
this._stopPlayback(); // any touch interrupts the story
|
|
3654
3266
|
this._canvas.setPointerCapture(e.pointerId);
|
|
3655
3267
|
const pt = this._localPoint(e);
|
|
3656
3268
|
this._pointers.set(e.pointerId, pt);
|
|
@@ -3847,10 +3459,14 @@ class WickChart extends HTMLElementBase {
|
|
|
3847
3459
|
return useLog ? Math.pow(10, t) : t;
|
|
3848
3460
|
}
|
|
3849
3461
|
|
|
3462
|
+
/** Dispatch a `wick:name` event on the element. */
|
|
3463
|
+
_fire(name, detail) {
|
|
3464
|
+
this.dispatchEvent(new CustomEvent('wick:' + name, { detail }));
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3850
3467
|
_wheel(e) {
|
|
3851
3468
|
const ly = this._ly;
|
|
3852
3469
|
if (!ly || !this._data.length) return;
|
|
3853
|
-
this._stopPlayback();
|
|
3854
3470
|
e.preventDefault();
|
|
3855
3471
|
const pt = this._localPoint(e);
|
|
3856
3472
|
const dx = e.deltaX;
|
|
@@ -3884,7 +3500,6 @@ class WickChart extends HTMLElementBase {
|
|
|
3884
3500
|
_keydown(e) {
|
|
3885
3501
|
const ly = this._ly;
|
|
3886
3502
|
if (!ly || !this._data.length) return;
|
|
3887
|
-
this._stopPlayback();
|
|
3888
3503
|
if (e.key === 'Escape' && this._layerClaim) {
|
|
3889
3504
|
const claim = this._layerClaim;
|
|
3890
3505
|
this._layerClaim = null;
|
|
@@ -3905,7 +3520,6 @@ class WickChart extends HTMLElementBase {
|
|
|
3905
3520
|
const cur = this._hover ? this._hover.index : d.length - 1;
|
|
3906
3521
|
const idx = clamp(cur + (key === 'ArrowRight' ? step : -step), 0, d.length - 1);
|
|
3907
3522
|
this._hover = { index: idx, x: this._xFor(idx), y: this._hover ? this._hover.y : ly.main.y1 * 0.5 };
|
|
3908
|
-
this._maybeSonify(idx);
|
|
3909
3523
|
this._emitCrosshair(this._hover);
|
|
3910
3524
|
this._invalidate();
|
|
3911
3525
|
} else if (key === 'Home') {
|
|
@@ -3943,103 +3557,9 @@ class WickChart extends HTMLElementBase {
|
|
|
3943
3557
|
}
|
|
3944
3558
|
|
|
3945
3559
|
/* ------------------------------------------------------------ *
|
|
3946
|
-
*
|
|
3560
|
+
* Crosshair events
|
|
3947
3561
|
* ------------------------------------------------------------ */
|
|
3948
3562
|
|
|
3949
|
-
/** Lazily-created shared AudioContext (enable within a user gesture). */
|
|
3950
|
-
_audio() {
|
|
3951
|
-
if (this._actx) return this._actx;
|
|
3952
|
-
const AC = window.AudioContext || window.webkitAudioContext;
|
|
3953
|
-
if (!AC) return null;
|
|
3954
|
-
try {
|
|
3955
|
-
this._actx = new AC();
|
|
3956
|
-
} catch (_) {
|
|
3957
|
-
this._actx = null;
|
|
3958
|
-
}
|
|
3959
|
-
return this._actx;
|
|
3960
|
-
}
|
|
3961
|
-
|
|
3962
|
-
/** Short sine blip; `when` schedules against AudioContext time. */
|
|
3963
|
-
_tone(freq, dur = 0.14, when = 0) {
|
|
3964
|
-
const ctx = this._audio();
|
|
3965
|
-
if (!ctx) return;
|
|
3966
|
-
if (ctx.state === 'suspended') ctx.resume().catch(() => {});
|
|
3967
|
-
const t0 = when || ctx.currentTime;
|
|
3968
|
-
const osc = ctx.createOscillator();
|
|
3969
|
-
const gain = ctx.createGain();
|
|
3970
|
-
osc.type = 'sine';
|
|
3971
|
-
osc.frequency.value = freq;
|
|
3972
|
-
gain.gain.setValueAtTime(0.0001, t0);
|
|
3973
|
-
gain.gain.exponentialRampToValueAtTime(0.18, t0 + 0.012);
|
|
3974
|
-
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
|
3975
|
-
osc.connect(gain).connect(ctx.destination);
|
|
3976
|
-
osc.start(t0);
|
|
3977
|
-
osc.stop(t0 + dur + 0.03);
|
|
3978
|
-
}
|
|
3979
|
-
|
|
3980
|
-
/** One tone for a bar's close, pitched by its position on the y-scale. */
|
|
3981
|
-
_sonifyBar(i) {
|
|
3982
|
-
if (!this._sonify || !this._data.length || !this._lastScale) return;
|
|
3983
|
-
const d = this._renderBars();
|
|
3984
|
-
const b = d[clamp(i, 0, d.length - 1)];
|
|
3985
|
-
if (!b) return;
|
|
3986
|
-
this._tone(priceToFreq(b.close, this._lastScale));
|
|
3987
|
-
}
|
|
3988
|
-
|
|
3989
|
-
/** One tone per crosshair bar change (dedupes y-only moves). */
|
|
3990
|
-
_maybeSonify(idx) {
|
|
3991
|
-
if (!this._sonify) return;
|
|
3992
|
-
if (this._lastToneIdx === idx) return;
|
|
3993
|
-
this._lastToneIdx = idx;
|
|
3994
|
-
this._sonifyBar(idx);
|
|
3995
|
-
}
|
|
3996
|
-
|
|
3997
|
-
/**
|
|
3998
|
-
* Play the visible range as a pitch sweep (~4s), riding the crosshair —
|
|
3999
|
-
* the audible equivalent of running your eye along the price line.
|
|
4000
|
-
*/
|
|
4001
|
-
playRange() {
|
|
4002
|
-
if (!this._data.length || !this._ly) return;
|
|
4003
|
-
const ctx = this._audio();
|
|
4004
|
-
if (!ctx) return;
|
|
4005
|
-
if (ctx.state === 'suspended') ctx.resume().catch(() => {});
|
|
4006
|
-
const d = this._renderBars();
|
|
4007
|
-
const count = Math.max(2, Math.round(this._ly.plotRight / this._view.spacing));
|
|
4008
|
-
const i0 = clamp(Math.floor(this._view.rightIndex - count) - 1, 0, d.length - 1);
|
|
4009
|
-
const i1 = clamp(Math.ceil(this._view.rightIndex), 0, d.length - 1);
|
|
4010
|
-
if (i1 - i0 < 2) return;
|
|
4011
|
-
const N = Math.min(120, i1 - i0 + 1);
|
|
4012
|
-
const stepMs = Math.min(70, Math.max(24, 4000 / N));
|
|
4013
|
-
const t0 = ctx.currentTime + 0.05;
|
|
4014
|
-
for (let k = 0; k < N; k++) {
|
|
4015
|
-
const i = Math.round(i0 + ((i1 - i0) * k) / (N - 1));
|
|
4016
|
-
const b = d[i];
|
|
4017
|
-
if (!b) continue;
|
|
4018
|
-
this._tone(priceToFreq(b.close, this._lastScale), stepMs / 1000 * 0.9, t0 + (k * stepMs) / 1000);
|
|
4019
|
-
}
|
|
4020
|
-
// ride the crosshair along the sweep for sighted users
|
|
4021
|
-
this._playToken++;
|
|
4022
|
-
const token = this._playToken;
|
|
4023
|
-
let k = 0;
|
|
4024
|
-
const timer = setInterval(() => {
|
|
4025
|
-
if (token !== this._playToken || !this._connected) {
|
|
4026
|
-
clearInterval(timer);
|
|
4027
|
-
return;
|
|
4028
|
-
}
|
|
4029
|
-
if (k >= N) {
|
|
4030
|
-
clearInterval(timer);
|
|
4031
|
-
this._hover = null;
|
|
4032
|
-
this._emitCrosshair(null);
|
|
4033
|
-
this._invalidate();
|
|
4034
|
-
return;
|
|
4035
|
-
}
|
|
4036
|
-
const i = Math.round(i0 + ((i1 - i0) * k) / (N - 1));
|
|
4037
|
-
this._hover = { index: i, x: this._xFor(i), y: this._ly ? this._ly.main.h * 0.5 : 0 };
|
|
4038
|
-
this._invalidate();
|
|
4039
|
-
k++;
|
|
4040
|
-
}, stepMs);
|
|
4041
|
-
}
|
|
4042
|
-
|
|
4043
3563
|
_emitCrosshair(hover) {
|
|
4044
3564
|
let detail = null;
|
|
4045
3565
|
if (hover && this._data[hover.index]) {
|
|
@@ -4052,250 +3572,6 @@ class WickChart extends HTMLElementBase {
|
|
|
4052
3572
|
};
|
|
4053
3573
|
}
|
|
4054
3574
|
this._fire('crosshair', detail);
|
|
4055
|
-
|
|
4056
|
-
// co-view: share the pointer with peer charts (leave events bypass throttle)
|
|
4057
|
-
if (this._coviewCh) {
|
|
4058
|
-
if (!detail) {
|
|
4059
|
-
this._coviewSend({ type: 'cross', time: null, yFrac: null });
|
|
4060
|
-
} else {
|
|
4061
|
-
const now = performance.now();
|
|
4062
|
-
if (now - this._coviewLast > 40) {
|
|
4063
|
-
this._coviewLast = now;
|
|
4064
|
-
const ly = this._ly;
|
|
4065
|
-
this._coviewSend({
|
|
4066
|
-
type: 'cross',
|
|
4067
|
-
time: detail.bar.time,
|
|
4068
|
-
yFrac: ly && isNum(detail.y) ? clamp(detail.y / ly.main.h, 0, 1) : null,
|
|
4069
|
-
});
|
|
4070
|
-
}
|
|
4071
|
-
}
|
|
4072
|
-
}
|
|
4073
|
-
}
|
|
4074
|
-
|
|
4075
|
-
/* ------------------------------------------------------------ *
|
|
4076
|
-
* Cross-tab co-view (BroadcastChannel)
|
|
4077
|
-
* ------------------------------------------------------------ */
|
|
4078
|
-
|
|
4079
|
-
/** Join/leave the co-view channel named by the `co-view` attribute. */
|
|
4080
|
-
_setupCoView() {
|
|
4081
|
-
if (this._coviewCh) {
|
|
4082
|
-
try {
|
|
4083
|
-
this._coviewCh.close();
|
|
4084
|
-
} catch (_) {}
|
|
4085
|
-
this._coviewCh = null;
|
|
4086
|
-
}
|
|
4087
|
-
clearInterval(this._coviewBeat);
|
|
4088
|
-
this._coviewBeat = 0;
|
|
4089
|
-
clearTimeout(this._ghostTimer);
|
|
4090
|
-
if (this._ghost) {
|
|
4091
|
-
this._ghost = null;
|
|
4092
|
-
this._invalidate();
|
|
4093
|
-
}
|
|
4094
|
-
if (this._presence && this._presence.peers.size) {
|
|
4095
|
-
const left = this._presence.list();
|
|
4096
|
-
this._presence = new PresenceTracker();
|
|
4097
|
-
this._fire('peers', { peers: [], joined: [], left });
|
|
4098
|
-
}
|
|
4099
|
-
const name = this._coviewName;
|
|
4100
|
-
if (!name || !this._connected || typeof BroadcastChannel === 'undefined') return;
|
|
4101
|
-
if (!this._coviewPeer) this._coviewPeer = 'p' + Math.random().toString(36).slice(2, 8);
|
|
4102
|
-
try {
|
|
4103
|
-
const ch = new BroadcastChannel('wick-co-view:' + name);
|
|
4104
|
-
ch.onmessage = (ev) => this._onCoMessage(ev.data);
|
|
4105
|
-
this._coviewCh = ch;
|
|
4106
|
-
} catch (_) {}
|
|
4107
|
-
// presence: announce immediately, then heartbeat so idle peers stay
|
|
4108
|
-
// warm (and stale ones sweep) without waiting for a pan/zoom
|
|
4109
|
-
this._coviewSendView(true);
|
|
4110
|
-
this._coviewBeat = setInterval(() => {
|
|
4111
|
-
this._coviewSendView(true);
|
|
4112
|
-
const left = this._presence.sweep();
|
|
4113
|
-
if (left.length) {
|
|
4114
|
-
this._fire('peers', { peers: this._presence.list(), joined: [], left });
|
|
4115
|
-
this._invalidate();
|
|
4116
|
-
}
|
|
4117
|
-
}, 4000);
|
|
4118
|
-
}
|
|
4119
|
-
|
|
4120
|
-
/** Broadcast our visible range for presence; throttled unless forced. */
|
|
4121
|
-
_coviewSendView(force) {
|
|
4122
|
-
if (!this._coviewCh) return;
|
|
4123
|
-
const r = this.getVisibleRange();
|
|
4124
|
-
if (!r) return;
|
|
4125
|
-
const now = performance.now();
|
|
4126
|
-
if (!force && now - this._coviewViewLast < 120) return;
|
|
4127
|
-
this._coviewViewLast = now;
|
|
4128
|
-
this._coviewSend({
|
|
4129
|
-
type: 'view',
|
|
4130
|
-
from: r.from,
|
|
4131
|
-
to: r.to,
|
|
4132
|
-
name: this._coviewLabel || null,
|
|
4133
|
-
});
|
|
4134
|
-
}
|
|
4135
|
-
|
|
4136
|
-
_coviewSend(msg) {
|
|
4137
|
-
if (!this._coviewCh) return;
|
|
4138
|
-
try {
|
|
4139
|
-
this._coviewCh.postMessage({ v: 1, peer: this._coviewPeer, ...msg });
|
|
4140
|
-
} catch (_) {}
|
|
4141
|
-
}
|
|
4142
|
-
|
|
4143
|
-
_onCoMessage(m) {
|
|
4144
|
-
if (!m || m.v !== 1 || m.peer === this._coviewPeer) return;
|
|
4145
|
-
if (m.type === 'view') {
|
|
4146
|
-
const joined = this._presence.track(m.peer, {
|
|
4147
|
-
range: { from: m.from, to: m.to },
|
|
4148
|
-
name: m.name,
|
|
4149
|
-
});
|
|
4150
|
-
this._invalidate();
|
|
4151
|
-
if (joined) {
|
|
4152
|
-
const p = this._presence.peers.get(m.peer);
|
|
4153
|
-
this._fire('peers', {
|
|
4154
|
-
peers: this._presence.list(),
|
|
4155
|
-
joined: [p ? { ...p, range: p.range && { ...p.range } } : { id: m.peer }],
|
|
4156
|
-
left: [],
|
|
4157
|
-
});
|
|
4158
|
-
}
|
|
4159
|
-
return;
|
|
4160
|
-
}
|
|
4161
|
-
if (m.type === 'bye') {
|
|
4162
|
-
const left = this._presence.drop(m.peer);
|
|
4163
|
-
if (left) this._fire('peers', { peers: this._presence.list(), joined: [], left: [left] });
|
|
4164
|
-
this._invalidate();
|
|
4165
|
-
return;
|
|
4166
|
-
}
|
|
4167
|
-
if (m.type !== 'cross') return;
|
|
4168
|
-
if (m.time == null) {
|
|
4169
|
-
if (this._ghost) {
|
|
4170
|
-
this._ghost = null;
|
|
4171
|
-
clearTimeout(this._ghostTimer);
|
|
4172
|
-
this._invalidate();
|
|
4173
|
-
}
|
|
4174
|
-
return;
|
|
4175
|
-
}
|
|
4176
|
-
if (!isNum(m.time) || !this._data.length) return;
|
|
4177
|
-
this._ghost = {
|
|
4178
|
-
index: WickChart._indexForTime(this._data, m.time),
|
|
4179
|
-
yFrac: isNum(m.yFrac) ? clamp(m.yFrac, 0, 1) : null,
|
|
4180
|
-
at: Date.now(),
|
|
4181
|
-
};
|
|
4182
|
-
clearTimeout(this._ghostTimer);
|
|
4183
|
-
this._ghostTimer = setTimeout(() => {
|
|
4184
|
-
this._ghost = null;
|
|
4185
|
-
this._invalidate();
|
|
4186
|
-
}, 2500);
|
|
4187
|
-
this._invalidate();
|
|
4188
|
-
}
|
|
4189
|
-
|
|
4190
|
-
/** Dispatch `wick:name` (canonical) plus the deprecated `hab:name` alias,
|
|
4191
|
-
* so 0.x listeners keep working until 2.0. */
|
|
4192
|
-
_fire(name, detail) {
|
|
4193
|
-
this.dispatchEvent(new CustomEvent('wick:' + name, { detail }));
|
|
4194
|
-
this.dispatchEvent(new CustomEvent('hab:' + name, { detail }));
|
|
4195
|
-
}
|
|
4196
|
-
|
|
4197
|
-
/**
|
|
4198
|
-
* Live co-view peers: who else is in the room and the time window each
|
|
4199
|
-
* one is looking at — [{ id, name, range: {from, to}, at }], oldest
|
|
4200
|
-
* sighting first. Peers fade out ~12 s after their last sighting.
|
|
4201
|
-
* @returns {object[]}
|
|
4202
|
-
*/
|
|
4203
|
-
getPeers() {
|
|
4204
|
-
return this._presence ? this._presence.list() : [];
|
|
4205
|
-
}
|
|
4206
|
-
|
|
4207
|
-
/**
|
|
4208
|
-
* Narrated timeline for a window (default: the visible range) — pivot
|
|
4209
|
-
* highs/lows, volume spikes, gaps, RSI divergences plus derived legs
|
|
4210
|
-
* ("+12.4% over 38 bars"), sorted by index. Pure data, perfect for
|
|
4211
|
-
* caption UIs or the walk player.
|
|
4212
|
-
* chart.narrate(); // visible range
|
|
4213
|
-
* chart.narrate({ from, to }); // times in ms (s accepted)
|
|
4214
|
-
* @param {{from?: number, to?: number}} [range]
|
|
4215
|
-
* @returns {{i: number, time: number, type: string, side: string, note: string,
|
|
4216
|
-
* legPct?: number, legBars?: number}[]}
|
|
4217
|
-
*/
|
|
4218
|
-
narrate(range) {
|
|
4219
|
-
const d = this._data;
|
|
4220
|
-
if (!d.length) return [];
|
|
4221
|
-
let i0 = 0;
|
|
4222
|
-
let i1 = d.length - 1;
|
|
4223
|
-
if (range && isNum(range.from) && isNum(range.to)) {
|
|
4224
|
-
i0 = WickChart._indexForTime(d, WickChart._timeToMs(range.from));
|
|
4225
|
-
i1 = WickChart._indexForTime(d, WickChart._timeToMs(range.to));
|
|
4226
|
-
if (i0 > i1) [i0, i1] = [i1, i0];
|
|
4227
|
-
}
|
|
4228
|
-
return narrateWindow(d, i0, i1);
|
|
4229
|
-
}
|
|
4230
|
-
|
|
4231
|
-
/**
|
|
4232
|
-
* Walk the chart through history like a story: the viewport slides
|
|
4233
|
-
* from `from` to `to` while `wick:walk` events announce every step and
|
|
4234
|
-
* the narrator's events (spikes, gaps, pivots, legs) as they're crossed.
|
|
4235
|
-
* Any user interaction — pointer, wheel, keys, double-click — stops it.
|
|
4236
|
-
* chart.walk({ from: 0, to: 500, speed: 120, step: 10 });
|
|
4237
|
-
* chart.addEventListener('wick:walk', (e) => showCaption(e.detail));
|
|
4238
|
-
* // detail: { phase: 'step'|'end'|'stop', index, events: [...], from, to }
|
|
4239
|
-
* @param {{from?: number, to?: number, speed?: number, step?: number}} [opts]
|
|
4240
|
-
* from/to are bar indices (default: last ~500 bars → the end)
|
|
4241
|
-
* @returns {boolean} true when the walk started
|
|
4242
|
-
*/
|
|
4243
|
-
walk(opts = {}) {
|
|
4244
|
-
this.stopWalk(true);
|
|
4245
|
-
const d = this._data;
|
|
4246
|
-
if (!d.length || !this._connected) return false;
|
|
4247
|
-
const to = clamp(Math.round(+opts.to || d.length - 1), 0, d.length - 1);
|
|
4248
|
-
const from = clamp(Math.round(opts.from != null ? +opts.from : Math.max(0, to - 500)), 0, to);
|
|
4249
|
-
const span = to - from + 1;
|
|
4250
|
-
// window width: the current viewport, but never more than ~⅓ of the
|
|
4251
|
-
// span (a fully zoomed-out chart would otherwise start at `to`)
|
|
4252
|
-
const widthBars = clamp(
|
|
4253
|
-
Math.min(
|
|
4254
|
-
this._ly ? Math.round(this._ly.plotRight / this._view.spacing) : 120,
|
|
4255
|
-
Math.max(10, Math.ceil(span / 3))
|
|
4256
|
-
),
|
|
4257
|
-
10,
|
|
4258
|
-
span
|
|
4259
|
-
);
|
|
4260
|
-
const events = narrateWindow(d, from, to, { pivot: 8 });
|
|
4261
|
-
const speed = clamp(Math.round(+opts.speed || 120), 16, 2000);
|
|
4262
|
-
const step = clamp(Math.round(+opts.step || Math.max(1, Math.round(widthBars / 12))), 1, 500);
|
|
4263
|
-
let cursor = Math.min(from + widthBars - 1, to);
|
|
4264
|
-
let ev = 0;
|
|
4265
|
-
let ended = false;
|
|
4266
|
-
const tick = () => {
|
|
4267
|
-
if (ended) return;
|
|
4268
|
-
this._auto = false;
|
|
4269
|
-
this._view.rightIndex = cursor;
|
|
4270
|
-
this._clampView();
|
|
4271
|
-
this._invalidate();
|
|
4272
|
-
this._emitRange();
|
|
4273
|
-
const hits = [];
|
|
4274
|
-
while (ev < events.length && events[ev].i <= cursor) hits.push(events[ev++]);
|
|
4275
|
-
this._fire('walk', { phase: 'step', index: cursor, events: hits, from, to });
|
|
4276
|
-
if (cursor >= to) {
|
|
4277
|
-
ended = true;
|
|
4278
|
-
clearInterval(this._walkTimer);
|
|
4279
|
-
this._walkTimer = 0;
|
|
4280
|
-
this._fire('walk', { phase: 'end', index: cursor, events: [], from, to });
|
|
4281
|
-
} else {
|
|
4282
|
-
cursor = Math.min(cursor + step, to);
|
|
4283
|
-
}
|
|
4284
|
-
};
|
|
4285
|
-
this._walkTimer = setInterval(tick, speed);
|
|
4286
|
-
tick(); // first step lands immediately
|
|
4287
|
-
return true;
|
|
4288
|
-
}
|
|
4289
|
-
|
|
4290
|
-
/**
|
|
4291
|
-
* Stop the running walk (if any). Fires a final `wick:walk`
|
|
4292
|
-
* { phase: 'stop' } unless called internally.
|
|
4293
|
-
*/
|
|
4294
|
-
stopWalk(silent) {
|
|
4295
|
-
if (!this._walkTimer) return;
|
|
4296
|
-
clearInterval(this._walkTimer);
|
|
4297
|
-
this._walkTimer = 0;
|
|
4298
|
-
if (!silent) this._fire('walk', { phase: 'stop' });
|
|
4299
3575
|
}
|
|
4300
3576
|
|
|
4301
3577
|
/**
|
|
@@ -4333,171 +3609,47 @@ class WickChart extends HTMLElementBase {
|
|
|
4333
3609
|
return { i0, i1, stats: { ...stats, from: { ...stats.from }, to: { ...stats.to } } };
|
|
4334
3610
|
}
|
|
4335
3611
|
|
|
4336
|
-
/**
|
|
4337
|
-
* Capture the current chart state as a story scene: view, series type,
|
|
4338
|
-
* indicators, overlays, scenario and risk plan, plus a title/note.
|
|
4339
|
-
* Build guided tours by capturing several and playing them back.
|
|
4340
|
-
* const story = [
|
|
4341
|
-
* chart.captureScene('Overview', 'The full picture'),
|
|
4342
|
-
* { title: 'The breakout', range: { from, to }, indicators: 'sma:20' },
|
|
4343
|
-
* ];
|
|
4344
|
-
* chart.playStory(story);
|
|
4345
|
-
* @param {string} [title]
|
|
4346
|
-
* @param {string} [note]
|
|
4347
|
-
* @returns {object} scene (plain data — snapshot of the moment)
|
|
4348
|
-
*/
|
|
4349
|
-
captureScene(title, note) {
|
|
4350
|
-
const scene = {
|
|
4351
|
-
title: title != null ? String(title).slice(0, 60) : '',
|
|
4352
|
-
note: note != null ? String(note).slice(0, 200) : '',
|
|
4353
|
-
range: this.getVisibleRange() || undefined,
|
|
4354
|
-
type: this.getAttribute('type') || 'candles',
|
|
4355
|
-
indicators: this.getAttribute('indicators') || null,
|
|
4356
|
-
};
|
|
4357
|
-
const ovs = this.overlays;
|
|
4358
|
-
if (ovs.length) scene.overlays = ovs;
|
|
4359
|
-
const sc = this.scenario;
|
|
4360
|
-
if (sc) scene.scenario = sc;
|
|
4361
|
-
const rp = this.riskPlan;
|
|
4362
|
-
if (rp) scene.riskPlan = rp;
|
|
4363
|
-
return scene;
|
|
4364
|
-
}
|
|
4365
|
-
|
|
4366
|
-
/** @returns {object[]|null} a copy of the last played story */
|
|
4367
|
-
getStory() {
|
|
4368
|
-
return this._story ? this._story.map((s) => ({ ...s })) : null;
|
|
4369
|
-
}
|
|
4370
|
-
|
|
4371
|
-
/**
|
|
4372
|
-
* Play a story: each scene applies its state (type / indicators /
|
|
4373
|
-
* overlays / scenario / risk plan — set or clear), the camera eases
|
|
4374
|
-
* to its range, then holds for its dwell. `wick:story` events narrate:
|
|
4375
|
-
* { phase: 'scene' | 'end' | 'stop', index, total, scene, title, note }
|
|
4376
|
-
* Any user interaction — pointer, wheel, keys, double-click — stops it.
|
|
4377
|
-
* @param {object[]} story scenes (invalid entries dropped, max 20)
|
|
4378
|
-
* @param {{dwell?: number, panMs?: number, loop?: boolean}} [opts]
|
|
4379
|
-
* panMs clamps 100–5000 (default 900); loop replays forever
|
|
4380
|
-
* @returns {boolean} true when playback started
|
|
4381
|
-
*/
|
|
4382
|
-
playStory(story, opts = {}) {
|
|
4383
|
-
this.stopStory(true);
|
|
4384
|
-
const scenes = sceneList(story);
|
|
4385
|
-
if (!scenes.length || !this._data.length || !this._connected) return false;
|
|
4386
|
-
const token = ++this._storyToken;
|
|
4387
|
-
this._story = scenes;
|
|
4388
|
-
const panMs = clamp(Math.round(+opts.panMs || 900), 100, 5000);
|
|
4389
|
-
const loop = opts.loop === true;
|
|
4390
|
-
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4391
|
-
const run = async () => {
|
|
4392
|
-
let idx = 0;
|
|
4393
|
-
while (token === this._storyToken) {
|
|
4394
|
-
const sc = scenes[idx];
|
|
4395
|
-
this._fire('story', {
|
|
4396
|
-
phase: 'scene', index: idx, total: scenes.length,
|
|
4397
|
-
scene: sc, title: sc.title, note: sc.note,
|
|
4398
|
-
});
|
|
4399
|
-
this._applyScene(sc);
|
|
4400
|
-
const target = this._sceneTarget(sc);
|
|
4401
|
-
if (target) await this._storyTween(target, panMs, token);
|
|
4402
|
-
if (token !== this._storyToken) return;
|
|
4403
|
-
await wait(sc.dwell);
|
|
4404
|
-
if (token !== this._storyToken) return;
|
|
4405
|
-
idx++;
|
|
4406
|
-
if (idx >= scenes.length) {
|
|
4407
|
-
if (loop) idx = 0;
|
|
4408
|
-
else {
|
|
4409
|
-
this._fire('story', { phase: 'end', index: idx - 1, total: scenes.length });
|
|
4410
|
-
return;
|
|
4411
|
-
}
|
|
4412
|
-
}
|
|
4413
|
-
}
|
|
4414
|
-
};
|
|
4415
|
-
run();
|
|
4416
|
-
return true;
|
|
4417
|
-
}
|
|
4418
|
-
|
|
4419
|
-
/**
|
|
4420
|
-
* Stop story playback (if running). Fires a final `wick:story`
|
|
4421
|
-
* { phase: 'stop' } unless called internally.
|
|
4422
|
-
*/
|
|
4423
|
-
stopStory(silent) {
|
|
4424
|
-
if (!this._storyToken) return;
|
|
4425
|
-
this._storyToken = 0;
|
|
4426
|
-
if (!silent) this._fire('story', { phase: 'stop' });
|
|
4427
|
-
}
|
|
4428
|
-
|
|
4429
|
-
/** Apply a scene's state (only the fields it carries). */
|
|
4430
|
-
_applyScene(sc) {
|
|
4431
|
-
if (sc.type) this.setAttribute('type', sc.type);
|
|
4432
|
-
if (sc.indicators != null) this.setAttribute('indicators', sc.indicators);
|
|
4433
|
-
if (sc.overlays) this.setOverlays(sc.overlays);
|
|
4434
|
-
if (sc.scenario === 'clear') this.clearScenario();
|
|
4435
|
-
else if (sc.scenario) this.setScenario(sc.scenario);
|
|
4436
|
-
if (sc.riskPlan === 'clear') this.clearRiskPlan();
|
|
4437
|
-
else if (sc.riskPlan) this.setRiskPlan(sc.riskPlan);
|
|
4438
|
-
}
|
|
4439
|
-
|
|
4440
|
-
/** Map a scene's time range to bar indices (null when not applicable). */
|
|
4441
|
-
_sceneTarget(sc) {
|
|
4442
|
-
if (!sc.range || !this._data.length) return null;
|
|
4443
|
-
let i0 = WickChart._indexForTime(this._data, WickChart._timeToMs(sc.range.from));
|
|
4444
|
-
let i1 = WickChart._indexForTime(this._data, WickChart._timeToMs(sc.range.to));
|
|
4445
|
-
if (i0 > i1) [i0, i1] = [i1, i0];
|
|
4446
|
-
return i1 - i0 >= 2 ? { i0, i1 } : null;
|
|
4447
|
-
}
|
|
4448
|
-
|
|
4449
|
-
/** Ease the viewport to { i0, i1 } over `ms`; resolves early if the
|
|
4450
|
-
* token changes (superseded or stopped). rAF when available. */
|
|
4451
|
-
_storyTween(target, ms, token) {
|
|
4452
|
-
const ly = this._ly;
|
|
4453
|
-
const d = this._data;
|
|
4454
|
-
if (!ly || !d.length) return Promise.resolve();
|
|
4455
|
-
const sp1 = clamp(ly.plotRight / (target.i1 - target.i0), this._minSpacing(), WickChart._MAX_SP);
|
|
4456
|
-
const from = { right: this._view.rightIndex, sp: this._view.spacing };
|
|
4457
|
-
const to = { right: target.i1, sp: sp1 };
|
|
4458
|
-
const t0 = performance.now();
|
|
4459
|
-
this._auto = false;
|
|
4460
|
-
return new Promise((resolve) => {
|
|
4461
|
-
const step = () => {
|
|
4462
|
-
if (token !== this._storyToken) return resolve();
|
|
4463
|
-
const e = easeInOutCubic(Math.min(1, (performance.now() - t0) / ms));
|
|
4464
|
-
this._view.rightIndex = from.right + (to.right - from.right) * e;
|
|
4465
|
-
this._view.spacing = from.sp + (to.sp - from.sp) * e;
|
|
4466
|
-
this._clampView();
|
|
4467
|
-
this._invalidate();
|
|
4468
|
-
this._emitRange();
|
|
4469
|
-
if (e >= 1) resolve();
|
|
4470
|
-
else if (typeof requestAnimationFrame === 'function') requestAnimationFrame(step);
|
|
4471
|
-
else setTimeout(step, 16);
|
|
4472
|
-
};
|
|
4473
|
-
step();
|
|
4474
|
-
});
|
|
4475
|
-
}
|
|
4476
|
-
|
|
4477
|
-
/** Interrupt narrated playback (walk / story) on user input. */
|
|
4478
|
-
_stopPlayback() {
|
|
4479
|
-
if (this._walkTimer) this.stopWalk();
|
|
4480
|
-
if (this._storyToken) this.stopStory();
|
|
4481
|
-
}
|
|
4482
|
-
|
|
4483
3612
|
_emitRange() {
|
|
4484
3613
|
const r = this.getVisibleRange();
|
|
4485
3614
|
if (!r) return;
|
|
4486
3615
|
this._fire('range', r);
|
|
4487
|
-
if (this._coviewCh) this._coviewSendView();
|
|
4488
3616
|
}
|
|
4489
3617
|
}
|
|
4490
3618
|
|
|
3619
|
+
/* 2.0: the guided-playback, planning, co-view and agent families moved to
|
|
3620
|
+
* their packages. Until one attaches, the familiar methods warn once and
|
|
3621
|
+
* no-op (deleted in 3.0); attaching installs the real thing as an own
|
|
3622
|
+
* property that shadows these. */
|
|
3623
|
+
for (const [name, pkg] of [
|
|
3624
|
+
['narrate', 'wickchart-narrator'],
|
|
3625
|
+
['walk', 'wickchart-narrator'],
|
|
3626
|
+
['stopWalk', 'wickchart-narrator'],
|
|
3627
|
+
['playRange', 'wickchart-narrator'],
|
|
3628
|
+
['captureScene', 'wickchart-narrator'],
|
|
3629
|
+
['getStory', 'wickchart-narrator'],
|
|
3630
|
+
['playStory', 'wickchart-narrator'],
|
|
3631
|
+
['stopStory', 'wickchart-narrator'],
|
|
3632
|
+
['getPeers', 'wickchart-coview'],
|
|
3633
|
+
['setScenario', 'wickchart-scenario'],
|
|
3634
|
+
['clearScenario', 'wickchart-scenario'],
|
|
3635
|
+
['setRiskPlan', 'wickchart-scenario'],
|
|
3636
|
+
['clearRiskPlan', 'wickchart-scenario'],
|
|
3637
|
+
['aiTools', 'wickchart-ai'],
|
|
3638
|
+
['aiPrompt', 'wickchart-ai'],
|
|
3639
|
+
['aiContext', 'wickchart-ai'],
|
|
3640
|
+
['applyAI', 'wickchart-ai'],
|
|
3641
|
+
['ask', 'wickchart-ai'],
|
|
3642
|
+
]) {
|
|
3643
|
+
WickChart.prototype[name] = function () {
|
|
3644
|
+
warnDeprecatedAlias(`${name}() moved to the ${pkg} package in 2.0`);
|
|
3645
|
+
return undefined;
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
|
|
4491
3649
|
if (typeof customElements !== 'undefined') {
|
|
4492
3650
|
if (!customElements.get('wick-chart')) {
|
|
4493
3651
|
customElements.define('wick-chart', WickChart);
|
|
4494
3652
|
}
|
|
4495
|
-
// 0.x alias: same element under its old tag name (deprecated, removed in 2.0)
|
|
4496
|
-
if (!customElements.get('hab-chart')) {
|
|
4497
|
-
/** @deprecated use <wick-chart> */
|
|
4498
|
-
class HabChart extends WickChart {}
|
|
4499
|
-
customElements.define('hab-chart', HabChart);
|
|
4500
|
-
}
|
|
4501
3653
|
}
|
|
4502
3654
|
|
|
4503
3655
|
export default WickChart;
|