wickchart 1.4.0 → 1.6.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/core.js CHANGED
@@ -462,6 +462,259 @@ export function calcMACD(closes, fast = 12, slow = 26, signal = 9) {
462
462
  return { macd, signal: sig, hist };
463
463
  }
464
464
 
465
+ /** True range: max(h−l, |h−prev close|, |l−prev close|); first bar is h−l.
466
+ * @param {Bar[]} bars
467
+ * @returns {Array<number|null>}
468
+ */
469
+ export function calcTrueRange(bars) {
470
+ const out = new Array(bars.length).fill(null);
471
+ for (let i = 0; i < bars.length; i++) {
472
+ const b = bars[i];
473
+ out[i] =
474
+ i === 0
475
+ ? b.high - b.low
476
+ : Math.max(b.high - b.low, Math.abs(b.high - bars[i - 1].close), Math.abs(b.low - bars[i - 1].close));
477
+ }
478
+ return out;
479
+ }
480
+
481
+ /**
482
+ * Average True Range (Wilder smoothing; seeded with the SMA of the first
483
+ * `period` true ranges).
484
+ * @param {Bar[]} bars
485
+ * @param {number} period
486
+ * @returns {Array<number|null>}
487
+ */
488
+ export function calcATR(bars, period = 14) {
489
+ const out = new Array(bars.length).fill(null);
490
+ if (period < 1 || bars.length < period) return out;
491
+ const tr = calcTrueRange(bars);
492
+ let sum = 0;
493
+ for (let i = 0; i < period; i++) sum += tr[i];
494
+ let prev = sum / period;
495
+ out[period - 1] = prev;
496
+ for (let i = period; i < bars.length; i++) {
497
+ prev = (prev * (period - 1) + tr[i]) / period;
498
+ out[i] = prev;
499
+ }
500
+ return out;
501
+ }
502
+
503
+ /**
504
+ * Volume-weighted average price over the hlc3 typical price, anchored to
505
+ * each UTC day (resets at the session boundary).
506
+ * @param {Bar[]} bars
507
+ * @returns {Array<number|null>}
508
+ */
509
+ export function calcVWAP(bars) {
510
+ const out = new Array(bars.length).fill(null);
511
+ let pv = 0;
512
+ let vv = 0;
513
+ let day = null;
514
+ for (let i = 0; i < bars.length; i++) {
515
+ const b = bars[i];
516
+ const ms = b.time < 1e12 ? b.time * 1000 : b.time;
517
+ const d = Math.floor(ms / DAY);
518
+ if (d !== day) {
519
+ day = d;
520
+ pv = 0;
521
+ vv = 0;
522
+ }
523
+ pv += ((b.high + b.low + b.close) / 3) * b.volume;
524
+ vv += b.volume;
525
+ out[i] = vv > 0 ? pv / vv : null;
526
+ }
527
+ return out;
528
+ }
529
+
530
+ /**
531
+ * On-balance volume: cumulative volume signed by close-to-close direction.
532
+ * @param {Bar[]} bars
533
+ * @returns {Array<number|null>}
534
+ */
535
+ export function calcOBV(bars) {
536
+ const out = new Array(bars.length).fill(null);
537
+ let obv = 0;
538
+ for (let i = 0; i < bars.length; i++) {
539
+ if (i > 0) {
540
+ const d = bars[i].close - bars[i - 1].close;
541
+ obv += d > 0 ? bars[i].volume : d < 0 ? -bars[i].volume : 0;
542
+ }
543
+ out[i] = obv;
544
+ }
545
+ return out;
546
+ }
547
+
548
+ /** Highest-high / lowest-low window ending at `i` (shared by stoch/wr/donchian). */
549
+ function winHL(bars, i, period) {
550
+ let hh = -Infinity;
551
+ let ll = Infinity;
552
+ for (let j = i - period + 1; j <= i; j++) {
553
+ if (bars[j].high > hh) hh = bars[j].high;
554
+ if (bars[j].low < ll) ll = bars[j].low;
555
+ }
556
+ return [hh, ll];
557
+ }
558
+
559
+ /** SMA that tolerates leading nulls (windows over sparse raw series). */
560
+ function smaSparse(values, period) {
561
+ const out = new Array(values.length).fill(null);
562
+ let sum = 0;
563
+ let count = 0;
564
+ for (let i = 0; i < values.length; i++) {
565
+ const v = values[i];
566
+ if (isNum(v)) {
567
+ sum += v;
568
+ count++;
569
+ }
570
+ if (i >= period && isNum(values[i - period])) {
571
+ sum -= values[i - period];
572
+ count--;
573
+ }
574
+ if (count === period) out[i] = sum / period;
575
+ }
576
+ return out;
577
+ }
578
+
579
+ /**
580
+ * Stochastic oscillator (slow): raw %K over `period`, smoothed by `smooth`;
581
+ * %D is the SMA of %K.
582
+ * @param {Bar[]} bars
583
+ * @param {number} period
584
+ * @param {number} smooth
585
+ * @returns {{k:Array<number|null>, d:Array<number|null>}}
586
+ */
587
+ export function calcStoch(bars, period = 14, smooth = 3) {
588
+ const n = bars.length;
589
+ const raw = new Array(n).fill(null);
590
+ for (let i = period - 1; i < n; i++) {
591
+ const [hh, ll] = winHL(bars, i, period);
592
+ const span = hh - ll;
593
+ raw[i] = span > 0 ? ((bars[i].close - ll) / span) * 100 : null;
594
+ }
595
+ const k = smooth > 1 ? smaSparse(raw, smooth) : raw;
596
+ const d = smooth > 1 ? smaSparse(k, smooth) : k;
597
+ return { k, d };
598
+ }
599
+
600
+ /**
601
+ * Commodity Channel Index: typical price vs its SMA, scaled by mean deviation.
602
+ * @param {Bar[]} bars
603
+ * @param {number} period
604
+ * @returns {Array<number|null>}
605
+ */
606
+ export function calcCCI(bars, period = 20) {
607
+ const n = bars.length;
608
+ const out = new Array(n).fill(null);
609
+ if (period < 1 || n < period) return out;
610
+ const tp = bars.map((b) => (b.high + b.low + b.close) / 3);
611
+ const ma = calcSMA(tp, period);
612
+ for (let i = period - 1; i < n; i++) {
613
+ let md = 0;
614
+ for (let j = i - period + 1; j <= i; j++) md += Math.abs(tp[j] - ma[i]);
615
+ md /= period;
616
+ out[i] = md > 0 ? (tp[i] - ma[i]) / (0.015 * md) : 0;
617
+ }
618
+ return out;
619
+ }
620
+
621
+ /**
622
+ * Williams %R: −100 at the period low, 0 at the period high.
623
+ * @param {Bar[]} bars
624
+ * @param {number} period
625
+ * @returns {Array<number|null>}
626
+ */
627
+ export function calcWilliamsR(bars, period = 14) {
628
+ const n = bars.length;
629
+ const out = new Array(n).fill(null);
630
+ for (let i = period - 1; i < n; i++) {
631
+ const [hh, ll] = winHL(bars, i, period);
632
+ const span = hh - ll;
633
+ if (span <= 0) continue;
634
+ const r = ((hh - bars[i].close) / span) * -100;
635
+ out[i] = r === 0 ? 0 : r; // avoid −0 on the axis
636
+ }
637
+ return out;
638
+ }
639
+
640
+ /**
641
+ * Donchian channels: highest high / lowest low over `period`, plus mid.
642
+ * @param {Bar[]} bars
643
+ * @param {number} period
644
+ * @returns {{upper:Array<number|null>, mid:Array<number|null>, lower:Array<number|null>}}
645
+ */
646
+ export function calcDonchian(bars, period = 20) {
647
+ const n = bars.length;
648
+ const upper = new Array(n).fill(null);
649
+ const mid = new Array(n).fill(null);
650
+ const lower = new Array(n).fill(null);
651
+ for (let i = period - 1; i < n; i++) {
652
+ const [hh, ll] = winHL(bars, i, period);
653
+ upper[i] = hh;
654
+ lower[i] = ll;
655
+ mid[i] = (hh + ll) / 2;
656
+ }
657
+ return { upper, mid, lower };
658
+ }
659
+
660
+ /**
661
+ * Keltner channels: EMA mid ± mult × ATR.
662
+ * @param {Bar[]} bars
663
+ * @param {number} period
664
+ * @param {number} [mult]
665
+ * @returns {{upper:Array<number|null>, mid:Array<number|null>, lower:Array<number|null>}}
666
+ */
667
+ export function calcKeltner(bars, period = 20, mult = 2) {
668
+ const mid = calcEMA(bars.map((b) => b.close), period);
669
+ const atr = calcATR(bars, period);
670
+ const band = (f) => mid.map((m, i) => (isNum(m) && isNum(atr[i]) ? f(m, atr[i]) : null));
671
+ return { upper: band((m, a) => m + mult * a), mid, lower: band((m, a) => m - mult * a) };
672
+ }
673
+
674
+ /**
675
+ * SuperTrend: ATR bands that flip with the trend. Returns the trend line
676
+ * (support in uptrends, resistance in downtrends) with a one-bar null gap
677
+ * at flips so the renderer breaks the line.
678
+ * @param {Bar[]} bars
679
+ * @param {number} period
680
+ * @param {number} [mult]
681
+ * @returns {Array<number|null>}
682
+ */
683
+ export function calcSuperTrend(bars, period = 10, mult = 3) {
684
+ const n = bars.length;
685
+ const out = new Array(n).fill(null);
686
+ if (period < 1 || n < period) return out;
687
+ const atr = calcATR(bars, period);
688
+ let dir = 1;
689
+ let fUp = Infinity;
690
+ let fLo = -Infinity;
691
+ let started = false;
692
+ for (let i = 0; i < n; i++) {
693
+ if (!isNum(atr[i])) continue;
694
+ const b = bars[i];
695
+ const hl2 = (b.high + b.low) / 2;
696
+ const bUp = hl2 + mult * atr[i];
697
+ const bLo = hl2 - mult * atr[i];
698
+ if (!started) {
699
+ started = true;
700
+ fUp = bUp;
701
+ fLo = bLo;
702
+ dir = b.close >= hl2 ? 1 : -1;
703
+ out[i] = dir > 0 ? fLo : fUp;
704
+ continue;
705
+ }
706
+ const pc = bars[i - 1].close;
707
+ // carry a band forward unless it tightened, or the previous close broke it
708
+ fUp = bUp < fUp || pc > fUp ? bUp : fUp;
709
+ fLo = bLo > fLo || pc < fLo ? bLo : fLo;
710
+ const prevDir = dir;
711
+ if (b.close > fUp) dir = 1;
712
+ else if (b.close < fLo) dir = -1;
713
+ out[i] = dir === prevDir ? (dir > 0 ? fLo : fUp) : null;
714
+ }
715
+ return out;
716
+ }
717
+
465
718
  /* ------------------------------------------------------------------ *
466
719
  * Data merging & gaps
467
720
  * ------------------------------------------------------------------ */
@@ -823,6 +1076,44 @@ export const BUILTIN_INDICATORS = new Map(
823
1076
  params: { period: 50 },
824
1077
  compute: (bars, p) => calcEMA(closesOf(bars), p.period),
825
1078
  },
1079
+ vwap: {
1080
+ kind: 'overlay',
1081
+ params: {},
1082
+ compute: (bars) => calcVWAP(bars),
1083
+ },
1084
+ supertrend: {
1085
+ kind: 'overlay',
1086
+ params: { period: 10, mult: 3 },
1087
+ compute: (bars, p) => calcSuperTrend(bars, p.period, p.mult),
1088
+ },
1089
+ donchian: {
1090
+ kind: 'overlay',
1091
+ params: { period: 20 },
1092
+ compute: (bars, p) => {
1093
+ const c = calcDonchian(bars, p.period);
1094
+ return {
1095
+ lines: [
1096
+ { name: 'upper', values: c.upper },
1097
+ { name: 'mid', values: c.mid },
1098
+ { name: 'lower', values: c.lower },
1099
+ ],
1100
+ };
1101
+ },
1102
+ },
1103
+ keltner: {
1104
+ kind: 'overlay',
1105
+ params: { period: 20, mult: 2 },
1106
+ compute: (bars, p) => {
1107
+ const c = calcKeltner(bars, p.period, p.mult);
1108
+ return {
1109
+ lines: [
1110
+ { name: 'upper', values: c.upper },
1111
+ { name: 'mid', values: c.mid },
1112
+ { name: 'lower', values: c.lower },
1113
+ ],
1114
+ };
1115
+ },
1116
+ },
826
1117
  bb: {
827
1118
  kind: 'overlay',
828
1119
  params: { period: 20, mult: 2 },
@@ -862,6 +1153,49 @@ export const BUILTIN_INDICATORS = new Map(
862
1153
  };
863
1154
  },
864
1155
  },
1156
+ atr: {
1157
+ kind: 'pane',
1158
+ params: { period: 14 },
1159
+ fmt: 'price',
1160
+ compute: (bars, p) => calcATR(bars, p.period),
1161
+ },
1162
+ stoch: {
1163
+ kind: 'pane',
1164
+ params: { period: 14, smooth: 3 },
1165
+ guides: [20, 80],
1166
+ range: [0, 100],
1167
+ fmt: 'fixed1',
1168
+ compute: (bars, p) => {
1169
+ const r = calcStoch(bars, p.period, p.smooth);
1170
+ return {
1171
+ lines: [
1172
+ { name: 'k', values: r.k },
1173
+ { name: 'd', values: r.d },
1174
+ ],
1175
+ };
1176
+ },
1177
+ },
1178
+ obv: {
1179
+ kind: 'pane',
1180
+ params: {},
1181
+ fmt: 'compact',
1182
+ compute: (bars) => calcOBV(bars),
1183
+ },
1184
+ cci: {
1185
+ kind: 'pane',
1186
+ params: { period: 20 },
1187
+ guides: [-100, 100],
1188
+ fmt: 'fixed1',
1189
+ compute: (bars, p) => calcCCI(bars, p.period),
1190
+ },
1191
+ wr: {
1192
+ kind: 'pane',
1193
+ params: { period: 14 },
1194
+ guides: [-80, -20],
1195
+ range: [-100, 0],
1196
+ fmt: 'fixed1',
1197
+ compute: (bars, p) => calcWilliamsR(bars, p.period),
1198
+ },
865
1199
  })
866
1200
  );
867
1201
 
@@ -977,6 +1311,10 @@ const SCRIPT_FUNCS = {
977
1311
  max: { min: 2, max: 2 },
978
1312
  crossup: { min: 2, max: 2 },
979
1313
  crossdown: { min: 2, max: 2 },
1314
+ // bar-level functions — no leading series argument, they read OHLCV directly
1315
+ vwap: { min: 0, max: 0 },
1316
+ obv: { min: 0, max: 0 },
1317
+ atr: { min: 1, max: 1, scalar: [0] },
980
1318
  };
981
1319
 
982
1320
  const scriptErr = (msg) => new Error('script: ' + msg);
@@ -1219,20 +1557,20 @@ function binOp(op, a, b) {
1219
1557
  return NaN;
1220
1558
  }
1221
1559
 
1222
- function evalScriptNode(node, vars, n) {
1560
+ function evalScriptNode(node, vars, n, bars) {
1223
1561
  switch (node.type) {
1224
1562
  case 'num':
1225
1563
  return node.v;
1226
1564
  case 'var':
1227
1565
  return vars[node.name];
1228
1566
  case 'neg': {
1229
- const e = evalScriptNode(node.e, vars, n);
1567
+ const e = evalScriptNode(node.e, vars, n, bars);
1230
1568
  if (!Array.isArray(e)) return -e;
1231
1569
  return e.map((x) => (x == null ? NaN : -x));
1232
1570
  }
1233
1571
  case 'bin': {
1234
- const l = evalScriptNode(node.l, vars, n);
1235
- const r = evalScriptNode(node.r, vars, n);
1572
+ const l = evalScriptNode(node.l, vars, n, bars);
1573
+ const r = evalScriptNode(node.r, vars, n, bars);
1236
1574
  if (!Array.isArray(l) && !Array.isArray(r)) return binOp(node.op, l, r);
1237
1575
  const a = Array.isArray(l) ? l : new Array(n).fill(l);
1238
1576
  const b = Array.isArray(r) ? r : new Array(n).fill(r);
@@ -1241,14 +1579,18 @@ function evalScriptNode(node, vars, n) {
1241
1579
  return out;
1242
1580
  }
1243
1581
  case 'call':
1244
- return evalScriptCall(node, vars, n);
1582
+ return evalScriptCall(node, vars, n, bars);
1245
1583
  }
1246
1584
  return NaN;
1247
1585
  }
1248
1586
 
1249
- function evalScriptCall(node, vars, n) {
1587
+ function evalScriptCall(node, vars, n, bars) {
1250
1588
  const { name, args } = node;
1251
- const s0 = evalScriptNode(args[0], vars, n);
1589
+ // bar-level functions read several series at once — no leading series argument
1590
+ if (name === 'vwap') return calcVWAP(bars);
1591
+ if (name === 'obv') return calcOBV(bars);
1592
+ if (name === 'atr') return calcATR(bars, args[0].type === 'num' ? args[0].v : 1);
1593
+ const s0 = evalScriptNode(args[0], vars, n, bars);
1252
1594
  const a = Array.isArray(s0) ? s0 : new Array(n).fill(s0);
1253
1595
  // window functions must not read leading nulls as 0 — NaN them so results stay honest
1254
1596
  const clean = a.map((x) => (x == null ? NaN : x));
@@ -1287,13 +1629,13 @@ function evalScriptCall(node, vars, n) {
1287
1629
  case 'log': return clean.map((x) => (x <= 0 ? NaN : Math.log(x)));
1288
1630
  case 'min':
1289
1631
  case 'max': {
1290
- const b0 = evalScriptNode(args[1], vars, n);
1632
+ const b0 = evalScriptNode(args[1], vars, n, bars);
1291
1633
  const b = Array.isArray(b0) ? b0 : new Array(n).fill(b0);
1292
1634
  return a.map((x, i) => (name === 'min' ? Math.min(scriptNum(x), scriptNum(b[i])) : Math.max(scriptNum(x), scriptNum(b[i]))));
1293
1635
  }
1294
1636
  case 'crossup':
1295
1637
  case 'crossdown': {
1296
- const b0 = evalScriptNode(args[1], vars, n);
1638
+ const b0 = evalScriptNode(args[1], vars, n, bars);
1297
1639
  const b = Array.isArray(b0) ? b0 : new Array(n).fill(b0);
1298
1640
  const out = new Array(n).fill(0);
1299
1641
  for (let i = 1; i < n; i++) {
@@ -1331,7 +1673,7 @@ export function evalScript(compiled, bars) {
1331
1673
  hlc3: bars.map((b) => (b.high + b.low + b.close) / 3),
1332
1674
  ohlc4: bars.map((b) => (b.open + b.high + b.low + b.close) / 4),
1333
1675
  };
1334
- const res = evalScriptNode(c.ast, vars, n);
1676
+ const res = evalScriptNode(c.ast, vars, n, bars);
1335
1677
  const arr = Array.isArray(res) ? res : new Array(n).fill(res);
1336
1678
  for (let i = 0; i < n; i++) {
1337
1679
  const v = arr[i];
@@ -2395,7 +2737,7 @@ export const AI_TOOLS = [
2395
2737
  {
2396
2738
  tool: 'set_indicators',
2397
2739
  description:
2398
- 'Replace the indicators. Tokens: sma:20 ema:50 bb:20 vwap rsi:14 macd:12/26/9 volume, @hexcolor suffixes, or WickScript expressions like expr:{close - sma(close,20)} / pexpr:{rsi(close,14)}. Empty string clears all.',
2740
+ 'Replace the indicators. Tokens: sma:20 ema:50 bb:20 vwap supertrend:10/3 donchian:20 keltner:20 rsi:14 macd:12/26/9 stoch:14/3 atr:14 obv cci:20 wr:14 volume, @hexcolor suffixes, or WickScript expressions like expr:{close - sma(close,20)} / pexpr:{rsi(close,14)}. Empty string clears all.',
2399
2741
  args: { indicators: 'string — space/comma-separated tokens' },
2400
2742
  },
2401
2743
  {
package/src/wick-chart.js CHANGED
@@ -1598,8 +1598,9 @@ class WickChart extends HTMLElementBase {
1598
1598
  );
1599
1599
  }
1600
1600
  const timeH = 26;
1601
+ const dock = this._dockInset();
1601
1602
  const plotRight = Math.max(30, W - priceW);
1602
- const plotBottom = H - timeH;
1603
+ const plotBottom = H - timeH - dock;
1603
1604
  const paneList = this._ind.panes;
1604
1605
  const paneArea = paneList.length
1605
1606
  ? Math.min(
@@ -1622,6 +1623,7 @@ class WickChart extends HTMLElementBase {
1622
1623
  plotBottom,
1623
1624
  main: { y0: 0, y1: mainH, h: mainH },
1624
1625
  panes,
1626
+ dock: dock > 0 ? { y0: H - dock, h: dock } : null,
1625
1627
  });
1626
1628
 
1627
1629
  /* background */
@@ -2331,6 +2333,8 @@ class WickChart extends HTMLElementBase {
2331
2333
  const fmtV = (v) =>
2332
2334
  entry.def.fmt === 'fixed1'
2333
2335
  ? numberFmt(1).format(v)
2336
+ : entry.def.fmt === 'compact'
2337
+ ? fmtCompact(v)
2334
2338
  : numberFmt(this._prec(scale.rawHi || 1)).format(v);
2335
2339
 
2336
2340
  // pane scale (fixed range or autoscaled from visible values)
@@ -2455,13 +2459,19 @@ class WickChart extends HTMLElementBase {
2455
2459
  ctx.lineWidth = 1;
2456
2460
  ctx.restore();
2457
2461
 
2458
- // right-axis labels for guide levels
2462
+ // right-axis labels: guide levels, or the pane's own min/max when
2463
+ // an autoscaled pane has no guides (atr / obv / pexpr)
2459
2464
  ctx.font = axisFont(400);
2460
2465
  ctx.fillStyle = pal.text;
2461
2466
  ctx.textAlign = 'right';
2462
2467
  ctx.textBaseline = 'middle';
2463
- for (const g of entry.def.guides || []) {
2464
- ctx.fillText(fmtV(g), W - 6, pyOf(g));
2468
+ if ((entry.def.guides || []).length) {
2469
+ for (const g of entry.def.guides) {
2470
+ ctx.fillText(fmtV(g), W - 6, pyOf(g));
2471
+ }
2472
+ } else {
2473
+ ctx.fillText(fmtV(pmax), W - 6, pyOf(pmax) + 6);
2474
+ if (pmax !== pmin) ctx.fillText(fmtV(pmin), W - 6, pyOf(pmin) - 6);
2465
2475
  }
2466
2476
 
2467
2477
  // pane label + live values (script panes show their expression label)
@@ -2580,6 +2590,8 @@ class WickChart extends HTMLElementBase {
2580
2590
  const fmtV =
2581
2591
  paneUnder.entry.def.fmt === 'fixed1'
2582
2592
  ? (v) => v.toFixed(1)
2593
+ : paneUnder.entry.def.fmt === 'compact'
2594
+ ? (v) => fmtCompact(v)
2583
2595
  : (v) => f.format(v);
2584
2596
  this._pill(
2585
2597
  plotRight + 2,
@@ -2932,7 +2944,13 @@ class WickChart extends HTMLElementBase {
2932
2944
  * layer then receives that pointer's move/up/cancel events (plus a
2933
2945
  * 'cancel' on Escape) and the chart suppresses its own pan/measure/brush
2934
2946
  * for the duration.
2935
- * @param {{id?: string, draw: Function, onPointer?: Function}} layer
2947
+ *
2948
+ * A layer may also declare `insetBottom` (px, 0..160): the largest
2949
+ * declared inset reserves a docked strip at the very bottom of the
2950
+ * canvas — all chart content (panes + time axis) shrinks above it and
2951
+ * the strip is handed to layers as `api.layout.dock = { y0, h }`
2952
+ * (used by the wickchart-navigator plugin).
2953
+ * @param {{id?: string, draw: Function, onPointer?: Function, insetBottom?: number}} layer
2936
2954
  * @returns {object|null} the normalized layer handle (with `id`), or null
2937
2955
  * if the layer was rejected (no draw fn, or 16 layers already added)
2938
2956
  */
@@ -2946,6 +2964,10 @@ class WickChart extends HTMLElementBase {
2946
2964
  const entry = {
2947
2965
  id,
2948
2966
  draw: layer.draw,
2967
+ insetBottom:
2968
+ typeof layer.insetBottom === 'number' && Number.isFinite(layer.insetBottom)
2969
+ ? Math.max(0, Math.min(160, Math.round(layer.insetBottom)))
2970
+ : 0,
2949
2971
  onPointer: typeof layer.onPointer === 'function' ? layer.onPointer : null,
2950
2972
  };
2951
2973
  const at = this._layers.findIndex((l) => l.id === id);
@@ -2997,6 +3019,18 @@ class WickChart extends HTMLElementBase {
2997
3019
  }
2998
3020
  }
2999
3021
 
3022
+ /**
3023
+ * Bottom space reserved by plugin layers: the largest declared
3024
+ * `insetBottom` (px, clamped 0..160 at addLayer time), or 0.
3025
+ */
3026
+ _dockInset() {
3027
+ let dock = 0;
3028
+ for (const l of this._layers) {
3029
+ if (l.insetBottom > dock) dock = l.insetBottom;
3030
+ }
3031
+ return dock;
3032
+ }
3033
+
3000
3034
  /** Ask layers, in order, whether one claims this pointerdown. */
3001
3035
  _layerHit(e, pt) {
3002
3036
  for (const layer of this._layers) {