wickchart 0.3.0 → 1.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/src/feeds.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- * HabView feeds — data-source helpers shared by <hab-feed> and the demo app.
2
+ * WickChart feeds — data-source helpers shared by <wick-feed> and the demo app.
3
3
  * Browser module (uses fetch/WebSocket inside functions); importable in Node
4
4
  * for unit-testing the pure generators.
5
5
  * MIT License.
@@ -1,16 +1,19 @@
1
1
  /* ==========================================================================
2
- * <hab-chart> — a modern, dependency-free financial charting web component.
2
+ * <wick-chart> — a modern, dependency-free financial charting web component.
3
3
  *
4
- * <hab-chart label="BTC · 1h" type="candles" indicators="sma:20 volume">
5
- * </hab-chart>
4
+ * <wick-chart label="BTC · 1h" type="candles" indicators="sma:20 volume">
5
+ * </wick-chart>
6
6
  * <script type="module">
7
- * const chart = document.querySelector('hab-chart');
7
+ * const chart = document.querySelector('wick-chart');
8
8
  * chart.setData(bars); // [{ time, open, high, low, close, volume }]
9
9
  * chart.update(bar); // streaming update / append
10
10
  * </script>
11
11
  *
12
12
  * Zero dependencies. Canvas-rendered. Framework-agnostic (works in React,
13
- * Vue, Svelte, plain HTML). Themeable with --hab-* CSS custom properties.
13
+ * Vue, plain HTML). Themeable with --wick-* CSS custom properties.
14
+ *
15
+ * The 0.x names (<hab-chart>, --hab-*, hab:* events) still work as
16
+ * deprecated aliases — see the README migration notes.
14
17
  *
15
18
  * MIT License.
16
19
  * ========================================================================== */
@@ -24,19 +27,25 @@ import {
24
27
  positionPnl, checkAlertCross, computeStats, safeColor,
25
28
  SERIES_TYPES, calcHeikinAshi, buildColumns, computeVolumeProfile,
26
29
  calcRSI, detectAnnotations, priceToFreq,
30
+ calcRealizedVol, volRegimeBands, percentileOfSorted, parseVolShading,
31
+ windowSummary,
27
32
  } from './core.js';
28
33
 
29
34
  /* ------------------------------------------------------------------ *
30
- * <hab-chart>
35
+ * <wick-chart>
31
36
  * ------------------------------------------------------------------ */
32
37
 
38
+ /** Indicator registry — module scope so registrations are shared by
39
+ * <wick-chart> and the deprecated <hab-chart> alias element. */
40
+ const REGISTRY = new Map(BUILTIN_INDICATORS);
41
+
33
42
  /* SSR safety: importing this module under Node (Next.js/Nuxt server render)
34
43
  * must not throw — the element simply registers only in browsers. */
35
44
  const HTMLElementBase = typeof HTMLElement !== 'undefined' ? HTMLElement : class {};
36
45
 
37
- class HabChart extends HTMLElementBase {
46
+ class WickChart extends HTMLElementBase {
38
47
  static get observedAttributes() {
39
- return ['theme', 'type', 'log', 'auto', 'indicators', 'precision', 'label', 'stats', 'profile', 'annotations', 'co-view', 'sonify'];
48
+ return ['theme', 'type', 'log', 'auto', 'indicators', 'precision', 'label', 'stats', 'profile', 'annotations', 'volshading', 'co-view', 'sonify'];
40
49
  }
41
50
 
42
51
  constructor() {
@@ -53,7 +62,7 @@ class HabChart extends HTMLElementBase {
53
62
  contain: content;
54
63
  }
55
64
  :host(:focus-visible) {
56
- outline: 2px solid var(--hab-accent, #4c8dff);
65
+ outline: 2px solid var(--wick-accent, var(--hab-accent, #4c8dff));
57
66
  outline-offset: -2px;
58
67
  }
59
68
  .wrap { position: absolute; inset: 0; overflow: hidden; }
@@ -76,26 +85,26 @@ class HabChart extends HTMLElementBase {
76
85
  }
77
86
  .legend .row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
78
87
  .legend .sym {
79
- color: var(--hab-text-strong, #e6edf3);
88
+ color: var(--wick-text-strong, var(--hab-text-strong, #e6edf3));
80
89
  font-weight: 700;
81
90
  font-size: 13px;
82
91
  letter-spacing: 0.02em;
83
92
  }
84
93
  .legend .kv { display: inline-flex; gap: 5px; align-items: baseline; white-space: nowrap; }
85
- .legend .k { color: var(--hab-text, #8b949e); font-size: 11px; }
86
- .legend .v { color: var(--hab-text-strong, #e6edf3); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
94
+ .legend .k { color: var(--wick-text, var(--hab-text, #8b949e)); font-size: 11px; }
95
+ .legend .v { color: var(--wick-text-strong, var(--hab-text-strong, #e6edf3)); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
87
96
  .legend .pct { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
88
- .legend .up { color: var(--hab-up, #16c784); }
89
- .legend .dn { color: var(--hab-down, #ea3943); }
97
+ .legend .up { color: var(--wick-up, var(--hab-up, #16c784)); }
98
+ .legend .dn { color: var(--wick-down, var(--hab-down, #ea3943)); }
90
99
  .legend .ind {
91
100
  display: inline-flex; align-items: center; gap: 6px;
92
- color: var(--hab-text, #8b949e); font-size: 11.5px; white-space: nowrap;
101
+ color: var(--wick-text, var(--hab-text, #8b949e)); font-size: 11.5px; white-space: nowrap;
93
102
  }
94
103
  .legend .ind i { width: 8px; height: 2.5px; border-radius: 2px; display: inline-block; }
95
104
  .legend .ind .v { font-size: 12px; }
96
105
  .legend .insight {
97
- color: var(--hab-accent, #4c8dff);
98
- background: var(--hab-chip, rgba(127, 137, 153, 0.12));
106
+ color: var(--wick-accent, var(--hab-accent, #4c8dff));
107
+ background: var(--wick-chip, var(--hab-chip, rgba(127, 137, 153, 0.12)));
99
108
  border-radius: 6px;
100
109
  padding: 1px 8px;
101
110
  font-size: 11.5px;
@@ -104,7 +113,7 @@ class HabChart extends HTMLElementBase {
104
113
  .nodata {
105
114
  position: absolute; inset: 0;
106
115
  display: flex; align-items: center; justify-content: center;
107
- color: var(--hab-text, #8b949e);
116
+ color: var(--wick-text, var(--hab-text, #8b949e));
108
117
  font: 500 13px ${FONT_STACK};
109
118
  pointer-events: none;
110
119
  }
@@ -117,25 +126,25 @@ class HabChart extends HTMLElementBase {
117
126
  }
118
127
  .hud .pos {
119
128
  display: inline-flex; gap: 9px; align-items: baseline; white-space: nowrap;
120
- background: var(--hab-chip, rgba(127, 137, 153, 0.12));
121
- border: 1px solid var(--hab-border, rgba(148, 163, 184, 0.2));
129
+ background: var(--wick-chip, var(--hab-chip, rgba(127, 137, 153, 0.12)));
130
+ border: 1px solid var(--wick-border, var(--hab-border, rgba(148, 163, 184, 0.2)));
122
131
  border-radius: 7px;
123
132
  padding: 3px 9px;
124
133
  }
125
134
  .hud .statsrow {
126
135
  display: inline-flex; gap: 12px; white-space: nowrap;
127
- background: var(--hab-chip, rgba(127, 137, 153, 0.12));
128
- border: 1px solid var(--hab-border, rgba(148, 163, 184, 0.2));
136
+ background: var(--wick-chip, var(--hab-chip, rgba(127, 137, 153, 0.12)));
137
+ border: 1px solid var(--wick-border, var(--hab-border, rgba(148, 163, 184, 0.2)));
129
138
  border-radius: 7px;
130
139
  padding: 3px 10px;
131
- color: var(--hab-text, #8b949e);
140
+ color: var(--wick-text, var(--hab-text, #8b949e));
132
141
  font-variant-numeric: tabular-nums;
133
142
  }
134
- .hud .statsrow b { color: var(--hab-text-strong, #e6edf3); font-weight: 600; }
135
- .hud .k { color: var(--hab-text, #8b949e); font-weight: 500; }
136
- .hud .v { color: var(--hab-text-strong, #e6edf3); font-variant-numeric: tabular-nums; }
137
- .hud .up { color: var(--hab-up, #16c784); }
138
- .hud .dn { color: var(--hab-down, #ea3943); }
143
+ .hud .statsrow b { color: var(--wick-text-strong, var(--hab-text-strong, #e6edf3)); font-weight: 600; }
144
+ .hud .k { color: var(--wick-text, var(--hab-text, #8b949e)); font-weight: 500; }
145
+ .hud .v { color: var(--wick-text-strong, var(--hab-text-strong, #e6edf3)); font-variant-numeric: tabular-nums; }
146
+ .hud .up { color: var(--wick-up, var(--hab-up, #16c784)); }
147
+ .hud .dn { color: var(--wick-down, var(--hab-down, #ea3943)); }
139
148
  </style>
140
149
  <div class="wrap" part="wrap">
141
150
  <canvas part="canvas" role="img"></canvas>
@@ -184,6 +193,7 @@ class HabChart extends HTMLElementBase {
184
193
  this._annotations = false;
185
194
  this._annoKey = '';
186
195
  this._annoList = null;
196
+ this._volshade = null;
187
197
 
188
198
  // cross-tab co-view state
189
199
  this._coviewName = null;
@@ -302,7 +312,7 @@ class HabChart extends HTMLElementBase {
302
312
  this._label = val || '';
303
313
  break;
304
314
  case 'indicators':
305
- this._ind = parseIndicators(val, HabChart._registry());
315
+ this._ind = parseIndicators(val, WickChart._registry());
306
316
  break;
307
317
  case 'stats':
308
318
  this._stats = val != null && val !== 'false';
@@ -316,6 +326,10 @@ class HabChart extends HTMLElementBase {
316
326
  this._annotations = val != null && val !== 'false';
317
327
  this._annoKey = '';
318
328
  break;
329
+ case 'volshading':
330
+ this._volshade = val != null && val !== 'false' ? parseVolShading(val) : null;
331
+ this._legendKey = '';
332
+ break;
319
333
  case 'co-view':
320
334
  this._coviewName = val || null;
321
335
  this._setupCoView();
@@ -332,20 +346,15 @@ class HabChart extends HTMLElementBase {
332
346
  * Indicator registry
333
347
  * ------------------------------------------------------------ */
334
348
 
335
- static _registryMap = null;
336
-
337
- /** Lazily-built registry, seeded with the built-in indicators. */
349
+ /** Indicator registry (module scope — shared with the legacy alias tag). */
338
350
  static _registry() {
339
- if (!HabChart._registryMap) {
340
- HabChart._registryMap = new Map(BUILTIN_INDICATORS);
341
- }
342
- return HabChart._registryMap;
351
+ return REGISTRY;
343
352
  }
344
353
 
345
354
  /**
346
355
  * Register a custom indicator.
347
356
  *
348
- * HabChart.registerIndicator('vwap', {
357
+ * WickChart.registerIndicator('vwap', {
349
358
  * kind: 'overlay', // or 'pane'
350
359
  * params: { period: 20 }, // defaults; settable via name:period
351
360
  * compute(bars, params) { // bars: normalized {time,o,h,l,c,v}
@@ -366,7 +375,7 @@ class HabChart extends HTMLElementBase {
366
375
  if (!def || typeof def.compute !== 'function') {
367
376
  throw new Error('registerIndicator: def.compute must be a function');
368
377
  }
369
- HabChart._registry().set(name.toLowerCase(), {
378
+ REGISTRY.set(name.toLowerCase(), {
370
379
  kind: def.kind === 'pane' ? 'pane' : 'overlay',
371
380
  ...def,
372
381
  });
@@ -396,7 +405,7 @@ class HabChart extends HTMLElementBase {
396
405
  }
397
406
  const norm = [];
398
407
  for (const b of bars) {
399
- const nb = HabChart._normBar(b);
408
+ const nb = WickChart._normBar(b);
400
409
  if (nb) norm.push(nb);
401
410
  }
402
411
  // skip the O(n log n) sort when already ascending (typical for feeds)
@@ -425,7 +434,7 @@ class HabChart extends HTMLElementBase {
425
434
  * @param {import('./core.js').Bar} bar
426
435
  */
427
436
  update(bar) {
428
- const b = HabChart._normBar(bar);
437
+ const b = WickChart._normBar(bar);
429
438
  if (!b) return;
430
439
  const d = this._data;
431
440
  const last = d[d.length - 1];
@@ -485,7 +494,7 @@ class HabChart extends HTMLElementBase {
485
494
  }
486
495
  const older = [];
487
496
  for (const b of bars) {
488
- const nb = HabChart._normBar(b);
497
+ const nb = WickChart._normBar(b);
489
498
  if (nb) older.push(nb);
490
499
  }
491
500
  const { bars: merged, added } = mergeOlderData(this._data, older);
@@ -533,14 +542,14 @@ class HabChart extends HTMLElementBase {
533
542
  setVisibleRange(range) {
534
543
  const d = this._data;
535
544
  if (!d.length || !range || !this._ly) return;
536
- const from = HabChart._timeToMs(range.from);
537
- const to = HabChart._timeToMs(range.to);
538
- let i0 = HabChart._indexForTime(d, from);
539
- let i1 = HabChart._indexForTime(d, to);
545
+ const from = WickChart._timeToMs(range.from);
546
+ const to = WickChart._timeToMs(range.to);
547
+ let i0 = WickChart._indexForTime(d, from);
548
+ let i1 = WickChart._indexForTime(d, to);
540
549
  if (i0 > i1) [i0, i1] = [i1, i0];
541
550
  if (i1 - i0 < 2) return;
542
551
  const { plotRight } = this._ly;
543
- this._view.spacing = clamp(plotRight / (i1 - i0), this._minSpacing(), HabChart._MAX_SP);
552
+ this._view.spacing = clamp(plotRight / (i1 - i0), this._minSpacing(), WickChart._MAX_SP);
544
553
  this._view.rightIndex = i1;
545
554
  this._auto = false;
546
555
  this._clampView();
@@ -553,6 +562,41 @@ class HabChart extends HTMLElementBase {
553
562
  return this._canvas.toDataURL('image/png');
554
563
  }
555
564
 
565
+ /**
566
+ * AI-ready summary of the visible window: structured fields plus a
567
+ * ready-to-paste markdown rendering (`text`). Computed locally —
568
+ * nothing leaves the page until the user copies it somewhere.
569
+ * @returns {object|null}
570
+ */
571
+ getDataWindow() {
572
+ const d = this._data;
573
+ if (!d.length || !this._ly) return null;
574
+ const { plotRight } = this._ly;
575
+ const { rightIndex, spacing } = this._view;
576
+ const i0 = Math.max(0, Math.round(rightIndex - plotRight / spacing));
577
+ const i1 = clamp(Math.round(rightIndex), 0, d.length - 1);
578
+ const s = windowSummary(d, i0, i1, { dtMs: this._dt, label: this._label });
579
+ if (!s) return null;
580
+ // snapshot active indicator values at the right edge (scripts show their expression)
581
+ const f = numberFmt(this._prec(d[i1].close));
582
+ const snap = [];
583
+ for (const entry of this._ind.overlays.concat(this._ind.panes)) {
584
+ if (entry.name === 'volume') continue;
585
+ const res = this._indicatorSeries(entry);
586
+ for (const ln of res.lines) {
587
+ const v = ln.values[i1];
588
+ if (!isNum(v)) continue;
589
+ const isScript = entry.name === 'expr' || entry.name === 'pexpr';
590
+ const name = isScript
591
+ ? ln.name || entry.name
592
+ : entry.name + (Object.keys(entry.params).length ? ':' + Object.values(entry.params).join('/') : '');
593
+ snap.push(`${name} = ${f.format(v)}`);
594
+ }
595
+ }
596
+ if (snap.length) s.text += `\n- Indicators: ${snap.join('; ')}.`;
597
+ return s;
598
+ }
599
+
556
600
  /* ------------------------------------------------------------ *
557
601
  * State serialization
558
602
  * ------------------------------------------------------------ */
@@ -575,6 +619,9 @@ class HabChart extends HTMLElementBase {
575
619
  stats: this._stats,
576
620
  profile: this._profile,
577
621
  annotations: this._annotations,
622
+ volshading: this._volshade
623
+ ? `${this._volshade.p1}/${this._volshade.p2}`
624
+ : false,
578
625
  indicators: ind.join(' '),
579
626
  view: range ? { from: range.from, to: range.to } : null,
580
627
  positions: this._positions.map((p) => ({
@@ -599,6 +646,8 @@ class HabChart extends HTMLElementBase {
599
646
  if (typeof state.stats === 'boolean') this.setAttribute('stats', String(state.stats));
600
647
  if (typeof state.profile === 'boolean') this.setAttribute('profile', String(state.profile));
601
648
  if (typeof state.annotations === 'boolean') this.setAttribute('annotations', String(state.annotations));
649
+ if (state.volshading === true) this.setAttribute('volshading', 'true');
650
+ else if (typeof state.volshading === 'string' && state.volshading) this.setAttribute('volshading', state.volshading);
602
651
  if (typeof state.label === 'string') this.setAttribute('label', state.label);
603
652
  if (typeof state.indicators === 'string') {
604
653
  this.setAttribute('indicators', state.indicators);
@@ -678,7 +727,8 @@ class HabChart extends HTMLElementBase {
678
727
  }
679
728
 
680
729
  /**
681
- * Price alert. Fires `hab:alert` ({id, price, bar}) on an edge crossing
730
+ * Price alert. Fires `wick:alert` ({id, price, bar}) on an edge crossing
731
+ * (plus the deprecated `hab:alert` alias)
682
732
  * during streaming updates.
683
733
  * @param {{id?: string, price: number, direction?: 'above'|'below'|'cross',
684
734
  * once?: boolean}} alert
@@ -717,9 +767,7 @@ class HabChart extends HTMLElementBase {
717
767
  if (a.fired) continue;
718
768
  if (checkAlertCross(a, prevClose, bar.close)) {
719
769
  a.fired = true;
720
- this.dispatchEvent(
721
- new CustomEvent('hab:alert', { detail: { id: a.id, price: a.price, bar } })
722
- );
770
+ this._fire('alert', { id: a.id, price: a.price, bar });
723
771
  if (a.once) this._alerts = this._alerts.filter((x) => x !== a);
724
772
  }
725
773
  }
@@ -761,7 +809,7 @@ class HabChart extends HTMLElementBase {
761
809
  if (!b) return null;
762
810
  const t = b.time != null ? b.time : b.t;
763
811
  if (!isNum(t)) return null;
764
- const time = HabChart._timeToMs(t);
812
+ const time = WickChart._timeToMs(t);
765
813
  const close = isNum(b.close) ? b.close : isNum(b.value) ? b.value : NaN;
766
814
  if (!isNum(close)) return null;
767
815
  const open = isNum(b.open) ? b.open : close;
@@ -840,8 +888,9 @@ class HabChart extends HTMLElementBase {
840
888
  if (this._pal && this._palKey === this._theme) return this._pal;
841
889
  const base = THEMES[this._theme] || THEMES.dark;
842
890
  const cs = getComputedStyle(this);
891
+ // --wick-* is canonical; --hab-* still honored as the 0.x fallback
843
892
  const get = (name, fallback) => {
844
- const v = cs.getPropertyValue('--hab-' + name).trim();
893
+ const v = cs.getPropertyValue('--wick-' + name).trim() || cs.getPropertyValue('--hab-' + name).trim();
845
894
  return v || fallback;
846
895
  };
847
896
  const pal = {};
@@ -852,7 +901,8 @@ class HabChart extends HTMLElementBase {
852
901
  o.push(get('overlay-' + i, base.overlay[i]));
853
902
  }
854
903
  // allow single overlay color
855
- const single = cs.getPropertyValue('--hab-overlay').trim();
904
+ const single =
905
+ cs.getPropertyValue('--wick-overlay').trim() || cs.getPropertyValue('--hab-overlay').trim();
856
906
  pal.overlay = single ? base.overlay.map(() => single) : o;
857
907
  } else {
858
908
  pal[k] = get(k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), base[k]);
@@ -889,6 +939,23 @@ class HabChart extends HTMLElementBase {
889
939
  return this._cache.map.__rsi14;
890
940
  }
891
941
 
942
+ /** Volatility-regime data (realized vol + percentile bands), cached per data version. */
943
+ _volShadeCache() {
944
+ if (!this._volshade) return null;
945
+ if (this._cache.v !== this._version) {
946
+ this._cache = { v: this._version, map: {} };
947
+ }
948
+ if (!this._cache.map.__volshade) {
949
+ const closes = this._data.map((b) => b.close);
950
+ const vol = calcRealizedVol(closes, this._volshade.period);
951
+ this._cache.map.__volshade = {
952
+ vol,
953
+ ...volRegimeBands(vol, this._volshade.p1, this._volshade.p2),
954
+ };
955
+ }
956
+ return this._cache.map.__volshade;
957
+ }
958
+
892
959
  /** Compute (and cache per data version) an indicator entry's series. */
893
960
  _indicatorSeries(entry) {
894
961
  if (this._cache.v !== this._version) {
@@ -954,7 +1021,7 @@ class HabChart extends HTMLElementBase {
954
1021
  if (!d.length || !this._ly) return;
955
1022
  const { plotRight } = this._ly;
956
1023
  const target = Math.min(d.length, 150);
957
- this._view.spacing = clamp(plotRight / target, this._minSpacing(), HabChart._MAX_SP);
1024
+ this._view.spacing = clamp(plotRight / target, this._minSpacing(), WickChart._MAX_SP);
958
1025
  this._view.rightIndex = d.length - 1 + this._rightMargin();
959
1026
  }
960
1027
 
@@ -963,7 +1030,7 @@ class HabChart extends HTMLElementBase {
963
1030
  const ly = this._ly;
964
1031
  if (!d.length || !ly) return;
965
1032
  const v = this._view;
966
- v.spacing = clamp(v.spacing, this._minSpacing(), HabChart._MAX_SP);
1033
+ v.spacing = clamp(v.spacing, this._minSpacing(), WickChart._MAX_SP);
967
1034
  const visible = ly.plotRight / v.spacing;
968
1035
  const maxRight = d.length - 1 + Math.max(6, visible * 0.5);
969
1036
  const minRight = Math.min(2, d.length - 1);
@@ -1288,6 +1355,32 @@ class HabChart extends HTMLElementBase {
1288
1355
  }
1289
1356
  ctx.stroke();
1290
1357
 
1358
+ /* volatility-regime shading (behind everything but the grid) */
1359
+ if (this._volshade) {
1360
+ const vs = this._volShadeCache();
1361
+ if (vs) {
1362
+ const flushRun = (val, a, b) => {
1363
+ if (val !== 0 && val !== 2) return;
1364
+ const xa = clamp(this._xFor(a) - sp * 0.5, 0, plotRight);
1365
+ const xb = clamp(this._xFor(b) + sp * 0.5, 0, plotRight);
1366
+ if (xb <= xa) return;
1367
+ ctx.fillStyle = val === 0 ? hexToRgba(pal.accent, 0.05) : hexToRgba(pal.down, 0.07);
1368
+ ctx.fillRect(xa, main.y0, xb - xa, main.h);
1369
+ };
1370
+ let runVal = -2;
1371
+ let runA = i0;
1372
+ for (let i = i0; i <= i1; i++) {
1373
+ const rv = vs.regimes[i] == null ? -1 : vs.regimes[i];
1374
+ if (rv !== runVal) {
1375
+ flushRun(runVal, runA, i - 1);
1376
+ runVal = rv;
1377
+ runA = i;
1378
+ }
1379
+ }
1380
+ flushRun(runVal, runA, i1);
1381
+ }
1382
+ }
1383
+
1291
1384
  /* position zones (under series) */
1292
1385
  for (const pos of this._positions) {
1293
1386
  const yE = clamp(yOf(pos.entry), main.y0, main.y1);
@@ -1367,9 +1460,7 @@ class HabChart extends HTMLElementBase {
1367
1460
  this._annoList = detectAnnotations(this._data, i0, i1, this._cachedRSI14());
1368
1461
  this._annoKey = akey;
1369
1462
  this._legendKey = ''; // legend may now show insights at the hovered bar
1370
- this.dispatchEvent(
1371
- new CustomEvent('hab:annotations', { detail: { annotations: this._annoList } })
1372
- );
1463
+ this._fire('annotations', { annotations: this._annoList });
1373
1464
  }
1374
1465
  const A = this._annoList;
1375
1466
  if (A.length) {
@@ -1825,21 +1916,21 @@ class HabChart extends HTMLElementBase {
1825
1916
  ctx.fillText(fmtV(g), W - 6, pyOf(g));
1826
1917
  }
1827
1918
 
1828
- // pane label + live values
1919
+ // pane label + live values (script panes show their expression label)
1829
1920
  const hi = this._hover ? clamp(this._hover.index, 0, d.length - 1) : d.length - 1;
1830
1921
  const vals = res.lines
1831
1922
  .map((ln) => (isNum(ln.values[hi]) ? fmtV(ln.values[hi]) : '—'))
1832
1923
  .join(' ');
1924
+ const paneLabel =
1925
+ (entry.name === 'expr' || entry.name === 'pexpr') && res.lines[0] && res.lines[0].name
1926
+ ? res.lines[0].name
1927
+ : `${entry.name.toUpperCase()} ${Object.values(entry.params).join(' ')}`;
1833
1928
  ctx.font = axisFont(600);
1834
1929
  ctx.fillStyle = pal.text;
1835
1930
  ctx.textAlign = 'left';
1836
1931
  ctx.textBaseline = 'top';
1837
1932
  ctx.globalAlpha = 0.9;
1838
- ctx.fillText(
1839
- `${entry.name.toUpperCase()} ${Object.values(entry.params).join(' ')}${vals ? ' ' + vals : ''}`,
1840
- 8,
1841
- pr.y0 + 5
1842
- );
1933
+ ctx.fillText(`${paneLabel}${vals ? ' ' + vals : ''}`, 8, pr.y0 + 5);
1843
1934
  ctx.globalAlpha = 1;
1844
1935
  }
1845
1936
 
@@ -2103,7 +2194,8 @@ class HabChart extends HTMLElementBase {
2103
2194
  const idx = clamp(hoverIdx, 0, d.length - 1);
2104
2195
  const key = [
2105
2196
  idx, this._version, this._type, this._label, this._theme,
2106
- this.getAttribute('indicators'), this._positions.length, this._posVersion || 0,
2197
+ this.getAttribute('indicators'), this.getAttribute('volshading'),
2198
+ this._positions.length, this._posVersion || 0,
2107
2199
  ].join('|');
2108
2200
  if (key === this._legendKey) return;
2109
2201
  this._legendKey = key;
@@ -2143,12 +2235,29 @@ class HabChart extends HTMLElementBase {
2143
2235
  const vals = res.lines
2144
2236
  .map((ln) => (isNum(ln.values[idx]) ? f.format(ln.values[idx]) : '—'))
2145
2237
  .join(' ');
2146
- html +=
2147
- `<div class="row"><span class="ind">` +
2148
- `<i style="background:${dotColor}"></i>${entry.name.toUpperCase()} ${Object.values(entry.params).join(' ')}` +
2149
- `</span><span class="v">${vals}</span></div>`;
2238
+ // script indicators carry their expression in the line name; built-ins show name+params
2239
+ const label =
2240
+ (entry.name === 'expr' || entry.name === 'pexpr') && res.lines[0].name
2241
+ ? esc(res.lines[0].name)
2242
+ : `${entry.name.toUpperCase()} ${Object.values(entry.params).join(' ')}`;
2243
+ html += `<div class="row"><span class="ind"><i style="background:${dotColor}"></i>${label}</span><span class="v">${vals}</span></div>`;
2150
2244
  });
2151
2245
 
2246
+ if (this._volshade) {
2247
+ const vs = this._volShadeCache();
2248
+ const rv = vs ? vs.regimes[idx] : -1;
2249
+ if (vs && rv >= 0) {
2250
+ const pct = percentileOfSorted(vs.sorted, vs.vol[idx]);
2251
+ const name = rv === 0 ? 'calm' : rv === 2 ? 'hot' : 'normal';
2252
+ const dot = rv === 0 ? palNow.accent : rv === 2 ? palNow.down : palNow.text;
2253
+ html +=
2254
+ `<div class="row"><span class="ind">` +
2255
+ `<i style="background:${dot}"></i>VOL ${this._volshade.p1}/${this._volshade.p2} · ${name}` +
2256
+ `${isNum(pct) ? ` · ${pct.toFixed(0)}%ile` : ''}` +
2257
+ `</span></div>`;
2258
+ }
2259
+ }
2260
+
2152
2261
  if (this._annotations && this._annoList) {
2153
2262
  const notes = this._annoList.filter((a) => a.i === idx).map((a) => a.note);
2154
2263
  if (notes.length) {
@@ -2245,7 +2354,7 @@ class HabChart extends HTMLElementBase {
2245
2354
  const s = clamp(
2246
2355
  (this._pinch.spacing * dist) / this._pinch.dist,
2247
2356
  this._minSpacing(),
2248
- HabChart._MAX_SP
2357
+ WickChart._MAX_SP
2249
2358
  );
2250
2359
  this._view.spacing = s;
2251
2360
  this._view.rightIndex = this._pinch.idxAtMid + (ly.plotRight - mid.x) / s;
@@ -2298,15 +2407,11 @@ class HabChart extends HTMLElementBase {
2298
2407
  const d = this._data;
2299
2408
  const barA = d[clamp(m.iA, 0, d.length - 1)];
2300
2409
  const barB = d[clamp(m.iB, 0, d.length - 1)];
2301
- this.dispatchEvent(
2302
- new CustomEvent('hab:measure', {
2303
- detail: {
2304
- from: { index: m.iA, time: barA.time, price: m.pA },
2305
- to: { index: m.iB, time: barB.time, price: m.pB },
2306
- bars: Math.abs(m.iB - m.iA),
2307
- },
2308
- })
2309
- );
2410
+ this._fire('measure', {
2411
+ from: { index: m.iA, time: barA.time, price: m.pA },
2412
+ to: { index: m.iB, time: barB.time, price: m.pB },
2413
+ bars: Math.abs(m.iB - m.iA),
2414
+ });
2310
2415
  }
2311
2416
  } else if (this._pan && had && !this._pan.moved && this._data.length) {
2312
2417
  if (this._measure) {
@@ -2318,11 +2423,7 @@ class HabChart extends HTMLElementBase {
2318
2423
  const pt = this._localPoint(e);
2319
2424
  const idx = clamp(Math.round(this._indexForX(pt.x)), 0, this._data.length - 1);
2320
2425
  const price = this._yToPrice(pt.y);
2321
- this.dispatchEvent(
2322
- new CustomEvent('hab:select', {
2323
- detail: { index: idx, bar: this._data[idx], price },
2324
- })
2325
- );
2426
+ this._fire('select', { index: idx, bar: this._data[idx], price });
2326
2427
  }
2327
2428
  }
2328
2429
  this._pan = null;
@@ -2359,7 +2460,7 @@ class HabChart extends HTMLElementBase {
2359
2460
 
2360
2461
  const factor = Math.exp(-dy * (e.ctrlKey ? 0.008 : 0.0016));
2361
2462
  const oldSp = this._view.spacing;
2362
- const newSp = clamp(oldSp * factor, this._minSpacing(), HabChart._MAX_SP);
2463
+ const newSp = clamp(oldSp * factor, this._minSpacing(), WickChart._MAX_SP);
2363
2464
  if (newSp === oldSp) return;
2364
2465
  const idxAtCursor = this._indexForX(pt.x);
2365
2466
  this._view.spacing = newSp;
@@ -2396,12 +2497,12 @@ class HabChart extends HTMLElementBase {
2396
2497
  this._invalidate();
2397
2498
  this._emitRange();
2398
2499
  } else if (key === '+' || key === '=') {
2399
- this._view.spacing = clamp(this._view.spacing * 1.25, this._minSpacing(), HabChart._MAX_SP);
2500
+ this._view.spacing = clamp(this._view.spacing * 1.25, this._minSpacing(), WickChart._MAX_SP);
2400
2501
  this._clampView();
2401
2502
  this._invalidate();
2402
2503
  this._emitRange();
2403
2504
  } else if (key === '-' || key === '_') {
2404
- this._view.spacing = clamp(this._view.spacing / 1.25, this._minSpacing(), HabChart._MAX_SP);
2505
+ this._view.spacing = clamp(this._view.spacing / 1.25, this._minSpacing(), WickChart._MAX_SP);
2405
2506
  this._clampView();
2406
2507
  this._invalidate();
2407
2508
  this._emitRange();
@@ -2528,7 +2629,7 @@ class HabChart extends HTMLElementBase {
2528
2629
  price: this._yToPrice(hover.y),
2529
2630
  };
2530
2631
  }
2531
- this.dispatchEvent(new CustomEvent('hab:crosshair', { detail }));
2632
+ this._fire('crosshair', detail);
2532
2633
 
2533
2634
  // co-view: share the pointer with peer charts (leave events bypass throttle)
2534
2635
  if (this._coviewCh) {
@@ -2570,7 +2671,7 @@ class HabChart extends HTMLElementBase {
2570
2671
  if (!name || !this._connected || typeof BroadcastChannel === 'undefined') return;
2571
2672
  if (!this._coviewPeer) this._coviewPeer = 'p' + Math.random().toString(36).slice(2, 8);
2572
2673
  try {
2573
- const ch = new BroadcastChannel('hab-co-view:' + name);
2674
+ const ch = new BroadcastChannel('wick-co-view:' + name);
2574
2675
  ch.onmessage = (ev) => this._onCoMessage(ev.data);
2575
2676
  this._coviewCh = ch;
2576
2677
  } catch (_) {}
@@ -2595,7 +2696,7 @@ class HabChart extends HTMLElementBase {
2595
2696
  }
2596
2697
  if (!isNum(m.time) || !this._data.length) return;
2597
2698
  this._ghost = {
2598
- index: HabChart._indexForTime(this._data, m.time),
2699
+ index: WickChart._indexForTime(this._data, m.time),
2599
2700
  yFrac: isNum(m.yFrac) ? clamp(m.yFrac, 0, 1) : null,
2600
2701
  at: Date.now(),
2601
2702
  };
@@ -2607,15 +2708,31 @@ class HabChart extends HTMLElementBase {
2607
2708
  this._invalidate();
2608
2709
  }
2609
2710
 
2711
+ /** Dispatch `wick:name` (canonical) plus the deprecated `hab:name` alias,
2712
+ * so 0.x listeners keep working until 2.0. */
2713
+ _fire(name, detail) {
2714
+ this.dispatchEvent(new CustomEvent('wick:' + name, { detail }));
2715
+ this.dispatchEvent(new CustomEvent('hab:' + name, { detail }));
2716
+ }
2717
+
2610
2718
  _emitRange() {
2611
2719
  const r = this.getVisibleRange();
2612
2720
  if (!r) return;
2613
- this.dispatchEvent(new CustomEvent('hab:range', { detail: r }));
2721
+ this._fire('range', r);
2614
2722
  }
2615
2723
  }
2616
2724
 
2617
- if (typeof customElements !== 'undefined' && !customElements.get('hab-chart')) {
2618
- customElements.define('hab-chart', HabChart);
2725
+ if (typeof customElements !== 'undefined') {
2726
+ if (!customElements.get('wick-chart')) {
2727
+ customElements.define('wick-chart', WickChart);
2728
+ }
2729
+ // 0.x alias: same element under its old tag name (deprecated, removed in 2.0)
2730
+ if (!customElements.get('hab-chart')) {
2731
+ /** @deprecated use <wick-chart> */
2732
+ class HabChart extends WickChart {}
2733
+ customElements.define('hab-chart', HabChart);
2734
+ }
2619
2735
  }
2620
2736
 
2621
- export default HabChart;
2737
+ export default WickChart;
2738
+ export { WickChart };