liveline 0.0.4 → 0.0.6

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/dist/index.cjs CHANGED
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- Liveline: () => Liveline
23
+ Liveline: () => Liveline,
24
+ LivelineTransition: () => LivelineTransition
24
25
  });
25
26
  module.exports = __toCommonJS(index_exports);
26
27
 
@@ -87,6 +88,33 @@ function resolveTheme(color, mode) {
87
88
  badgeFont: '500 11px "SF Mono", Menlo, monospace'
88
89
  };
89
90
  }
91
+ var SERIES_COLORS = [
92
+ "#3b82f6",
93
+ // blue
94
+ "#ef4444",
95
+ // red
96
+ "#22c55e",
97
+ // green
98
+ "#f59e0b",
99
+ // amber
100
+ "#8b5cf6",
101
+ // violet
102
+ "#ec4899",
103
+ // pink
104
+ "#06b6d4",
105
+ // cyan
106
+ "#f97316"
107
+ // orange
108
+ ];
109
+ function resolveSeriesPalettes(series, mode) {
110
+ const map = /* @__PURE__ */ new Map();
111
+ for (let i = 0; i < series.length; i++) {
112
+ const s = series[i];
113
+ const color = s.color || SERIES_COLORS[i % SERIES_COLORS.length];
114
+ map.set(s.id, resolveTheme(color, mode));
115
+ }
116
+ return map;
117
+ }
90
118
 
91
119
  // src/useLivelineEngine.ts
92
120
  var import_react = require("react");
@@ -387,8 +415,9 @@ function renderCurve(ctx, layout, palette, pts, showFill, lineAlpha = 1, fillAlp
387
415
  ctx.stroke();
388
416
  ctx.globalAlpha = baseAlpha;
389
417
  }
390
- function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0) {
418
+ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0, colorBlend = 1, skipDashLine = false, fillScale = 1) {
391
419
  const { h, pad, toX, toY, chartW, chartH } = layout;
420
+ const incomingAlpha = ctx.globalAlpha;
392
421
  const yMin = pad.top;
393
422
  const yMax = h - pad.bottom;
394
423
  const clampY = (y) => Math.max(yMin, Math.min(yMax, y));
@@ -397,8 +426,10 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
397
426
  const scroll = now_ms * LOADING_SCROLL_SPEED;
398
427
  const morphY = chartReveal < 1 ? (rawY, x) => {
399
428
  const t = Math.max(0, Math.min(1, (x - pad.left) / chartW));
429
+ const centerDist = Math.abs(t - 0.5) * 2;
430
+ const localReveal = Math.max(0, Math.min(1, (chartReveal - centerDist * 0.4) / 0.6));
400
431
  const baseY = loadingY(t, centerY, amplitude, scroll);
401
- return baseY + (rawY - baseY) * chartReveal;
432
+ return baseY + (rawY - baseY) * localReveal;
402
433
  } : (rawY, _x) => rawY;
403
434
  const pts = visible.map((p, i) => {
404
435
  const x = toX(p.time);
@@ -411,13 +442,14 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
411
442
  pts.push([tipX, morphY(clampY(toY(smoothValue)), tipX)]);
412
443
  if (pts.length < 2) return;
413
444
  let lineAlpha = 1;
414
- let fillAlpha = 1;
445
+ let fillAlpha = fillScale;
415
446
  if (chartReveal < 1) {
416
447
  const breath = loadingBreath(now_ms);
417
448
  lineAlpha = breath + (1 - breath) * chartReveal;
418
- fillAlpha = chartReveal;
449
+ fillAlpha = chartReveal * fillScale;
419
450
  }
420
- const strokeColor = chartReveal < 1 ? blendColor(palette.gridLabel, palette.line, Math.min(1, chartReveal * 3)) : void 0;
451
+ const colorT = Math.min(1, chartReveal * 3) * colorBlend;
452
+ const strokeColor = chartReveal < 1 || colorBlend < 1 ? blendColor(palette.gridLabel, palette.line, colorT) : void 0;
421
453
  const isScrubbing = scrubX !== null;
422
454
  ctx.save();
423
455
  ctx.beginPath();
@@ -434,26 +466,28 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
434
466
  ctx.beginPath();
435
467
  ctx.rect(scrubX, 0, layout.w - scrubX, h);
436
468
  ctx.clip();
437
- ctx.globalAlpha = 1 - scrubAmount * 0.6;
469
+ ctx.globalAlpha = incomingAlpha * (1 - scrubAmount * 0.6);
438
470
  renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
439
471
  ctx.restore();
440
472
  } else {
441
473
  renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
442
474
  }
443
475
  ctx.restore();
444
- const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
445
- const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
446
- ctx.setLineDash([4, 4]);
447
- ctx.strokeStyle = palette.dashLine;
448
- ctx.lineWidth = 1;
449
- const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
450
- ctx.globalAlpha = chartReveal < 1 ? dashBase * chartReveal : dashBase;
451
- ctx.beginPath();
452
- ctx.moveTo(pad.left, currentY);
453
- ctx.lineTo(layout.w - pad.right, currentY);
454
- ctx.stroke();
455
- ctx.setLineDash([]);
456
- ctx.globalAlpha = 1;
476
+ if (!skipDashLine) {
477
+ const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
478
+ const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
479
+ ctx.setLineDash([4, 4]);
480
+ ctx.strokeStyle = palette.dashLine;
481
+ ctx.lineWidth = 1;
482
+ const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
483
+ ctx.globalAlpha = incomingAlpha * (chartReveal < 1 ? dashBase * chartReveal : dashBase);
484
+ ctx.beginPath();
485
+ ctx.moveTo(pad.left, currentY);
486
+ ctx.lineTo(layout.w - pad.right, currentY);
487
+ ctx.stroke();
488
+ ctx.setLineDash([]);
489
+ }
490
+ ctx.globalAlpha = incomingAlpha;
457
491
  const last = pts[pts.length - 1];
458
492
  last[1] = Math.max(10, Math.min(h - 10, last[1]));
459
493
  return pts;
@@ -506,6 +540,33 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = per
506
540
  }
507
541
  ctx.fill();
508
542
  }
543
+ function drawMultiDot(ctx, x, y, color, pulse = true, now_ms = performance.now(), radius = 3) {
544
+ const baseAlpha = ctx.globalAlpha;
545
+ if (pulse) {
546
+ const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
547
+ if (t < 1) {
548
+ const ringRadius = 9 + t * 10;
549
+ const pulseAlpha = 0.3 * (1 - t);
550
+ ctx.beginPath();
551
+ ctx.arc(x, y, ringRadius, 0, Math.PI * 2);
552
+ ctx.strokeStyle = color;
553
+ ctx.lineWidth = 1.5;
554
+ ctx.globalAlpha = baseAlpha * pulseAlpha;
555
+ ctx.stroke();
556
+ }
557
+ }
558
+ ctx.globalAlpha = baseAlpha;
559
+ ctx.beginPath();
560
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
561
+ ctx.fillStyle = color;
562
+ ctx.fill();
563
+ }
564
+ function drawSimpleDot(ctx, x, y, color, radius = 3) {
565
+ ctx.beginPath();
566
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
567
+ ctx.fillStyle = color;
568
+ ctx.fill();
569
+ }
509
570
  function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
510
571
  const baseAlpha = ctx.globalAlpha;
511
572
  const upTarget = momentum === "up" ? 1 : 0;
@@ -604,6 +665,90 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
604
665
  ctx.fillText(separator + timeText, tx + valueW, ty);
605
666
  ctx.restore();
606
667
  }
668
+ function drawMultiCrosshair(ctx, layout, palette, hoverX, hoverTime, entries, formatValue, formatTime, scrubOpacity, tooltipY, tooltipOutline, liveDotX) {
669
+ if (scrubOpacity < 0.01 || entries.length === 0) return;
670
+ const { h, pad, toY } = layout;
671
+ ctx.save();
672
+ ctx.globalAlpha = scrubOpacity * 0.5;
673
+ ctx.strokeStyle = palette.crosshairLine;
674
+ ctx.lineWidth = 1;
675
+ ctx.beginPath();
676
+ ctx.moveTo(hoverX, pad.top);
677
+ ctx.lineTo(hoverX, h - pad.bottom);
678
+ ctx.stroke();
679
+ ctx.restore();
680
+ const dotRadius = 4 * Math.min(scrubOpacity * 3, 1);
681
+ if (dotRadius > 0.5) {
682
+ ctx.globalAlpha = 1;
683
+ for (const entry of entries) {
684
+ const y = toY(entry.value);
685
+ ctx.beginPath();
686
+ ctx.arc(hoverX, y, dotRadius, 0, Math.PI * 2);
687
+ ctx.fillStyle = entry.color;
688
+ ctx.fill();
689
+ }
690
+ }
691
+ if (scrubOpacity < 0.1 || layout.w < 300) return;
692
+ ctx.save();
693
+ ctx.globalAlpha = scrubOpacity;
694
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
695
+ ctx.textAlign = "left";
696
+ const timeText = formatTime(hoverTime);
697
+ const sep = " \xB7 ";
698
+ const dotInline = " ";
699
+ const segments = [
700
+ { text: timeText, color: palette.gridLabel }
701
+ ];
702
+ for (const e of entries) {
703
+ segments.push({ text: sep, color: palette.gridLabel });
704
+ segments.push({ text: dotInline, color: e.color, isDot: true });
705
+ const label = e.label ? `${e.label} ` : "";
706
+ if (label) segments.push({ text: label, color: palette.gridLabel });
707
+ segments.push({ text: formatValue(e.value), color: palette.tooltipText });
708
+ }
709
+ let totalW = 0;
710
+ const segWidths = [];
711
+ for (const seg of segments) {
712
+ const w = seg.isDot ? 12 : ctx.measureText(seg.text).width;
713
+ segWidths.push(w);
714
+ totalW += w;
715
+ }
716
+ let tx = hoverX - totalW / 2;
717
+ const minX = pad.left + 4;
718
+ const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
719
+ const maxX = dotRightEdge - totalW;
720
+ if (tx < minX) tx = minX;
721
+ if (tx > maxX) tx = maxX;
722
+ const ty = pad.top + (tooltipY ?? 14) + 10;
723
+ if (tooltipOutline !== false) {
724
+ ctx.strokeStyle = palette.tooltipBg;
725
+ ctx.lineWidth = 3;
726
+ ctx.lineJoin = "round";
727
+ let ox2 = tx;
728
+ for (let i = 0; i < segments.length; i++) {
729
+ const seg = segments[i];
730
+ if (!seg.isDot) {
731
+ ctx.strokeText(seg.text, ox2, ty);
732
+ }
733
+ ox2 += segWidths[i];
734
+ }
735
+ }
736
+ let ox = tx;
737
+ for (let i = 0; i < segments.length; i++) {
738
+ const seg = segments[i];
739
+ if (seg.isDot) {
740
+ ctx.beginPath();
741
+ ctx.arc(ox + 4, ty - 4, 3.5, 0, Math.PI * 2);
742
+ ctx.fillStyle = seg.color;
743
+ ctx.fill();
744
+ } else {
745
+ ctx.fillStyle = seg.color;
746
+ ctx.fillText(seg.text, ox, ty);
747
+ }
748
+ ox += segWidths[i];
749
+ }
750
+ ctx.restore();
751
+ }
607
752
 
608
753
  // src/draw/referenceLine.ts
609
754
  function drawReferenceLine(ctx, layout, palette, ref) {
@@ -970,6 +1115,337 @@ function drawParticles(ctx, state, dt) {
970
1115
  ctx.restore();
971
1116
  }
972
1117
 
1118
+ // src/draw/candlestick.ts
1119
+ var BULL = "#22c55e";
1120
+ var BEAR = "#ef4444";
1121
+ var BULL_RGB = [34, 197, 94];
1122
+ var BEAR_RGB = [239, 68, 68];
1123
+ function blendColor2(t) {
1124
+ const r = Math.round(BEAR_RGB[0] + (BULL_RGB[0] - BEAR_RGB[0]) * t);
1125
+ const g = Math.round(BEAR_RGB[1] + (BULL_RGB[1] - BEAR_RGB[1]) * t);
1126
+ const b = Math.round(BEAR_RGB[2] + (BULL_RGB[2] - BEAR_RGB[2]) * t);
1127
+ return `rgb(${r},${g},${b})`;
1128
+ }
1129
+ function parseRgb(color) {
1130
+ const hex = color.match(/^#([0-9a-f]{6})$/i);
1131
+ if (hex) {
1132
+ const h = hex[1];
1133
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
1134
+ }
1135
+ const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
1136
+ if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
1137
+ return [128, 128, 128];
1138
+ }
1139
+ function blendToAccent(candleColor, accentColor, t) {
1140
+ if (t <= 0) return candleColor;
1141
+ if (t >= 1) return accentColor;
1142
+ const [r1, g1, b1] = parseRgb(candleColor);
1143
+ const [r2, g2, b2] = parseRgb(accentColor);
1144
+ const r = Math.round(r1 + (r2 - r1) * t);
1145
+ const g = Math.round(g1 + (g2 - g1) * t);
1146
+ const b = Math.round(b1 + (b2 - b1) * t);
1147
+ return `rgb(${r},${g},${b})`;
1148
+ }
1149
+ function candleDims(layout, candleWidthSecs) {
1150
+ const pxPerSec = layout.chartW / (layout.rightEdge - layout.leftEdge);
1151
+ const candlePxW = candleWidthSecs * pxPerSec;
1152
+ const bodyW = Math.max(1, candlePxW * 0.7);
1153
+ const wickW = Math.max(0.8, Math.min(2, bodyW * 0.15));
1154
+ const radius = bodyW > 6 ? 1.5 : 0;
1155
+ return { bodyW, wickW, radius };
1156
+ }
1157
+ function roundedRect(ctx, x, y, w, h, r) {
1158
+ if (r <= 0 || h < r * 2) {
1159
+ ctx.rect(x, y, w, h);
1160
+ return;
1161
+ }
1162
+ ctx.moveTo(x + r, y);
1163
+ ctx.lineTo(x + w - r, y);
1164
+ ctx.arcTo(x + w, y, x + w, y + r, r);
1165
+ ctx.lineTo(x + w, y + h - r);
1166
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
1167
+ ctx.lineTo(x + r, y + h);
1168
+ ctx.arcTo(x, y + h, x, y + h - r, r);
1169
+ ctx.lineTo(x, y + r);
1170
+ ctx.arcTo(x, y, x + r, y, r);
1171
+ ctx.closePath();
1172
+ }
1173
+ function drawCandlesticks(ctx, layout, candles, candleWidthSecs, liveTime, now_ms, scrubX, scrubDim, liveAlpha = 1, liveBullBlend = -1, accentColor, accentBlend = 0) {
1174
+ if (candles.length === 0) return;
1175
+ const { toX, toY } = layout;
1176
+ const { bodyW, wickW, radius } = candleDims(layout, candleWidthSecs);
1177
+ const halfBody = bodyW / 2;
1178
+ const padL = layout.pad.left;
1179
+ const padR = layout.pad.left + layout.chartW;
1180
+ const livePulse = 0.12 + Math.sin(now_ms * 4e-3) * 0.08;
1181
+ for (const c of candles) {
1182
+ const cx = toX(c.time + candleWidthSecs / 2);
1183
+ if (cx + halfBody < padL || cx - halfBody > padR) continue;
1184
+ const isBull = c.close >= c.open;
1185
+ const isLive = c.time === liveTime;
1186
+ let color = isLive && liveBullBlend >= 0 ? blendColor2(liveBullBlend) : isBull ? BULL : BEAR;
1187
+ if (accentColor && accentBlend > 0.01) {
1188
+ color = blendToAccent(color, accentColor, accentBlend);
1189
+ }
1190
+ let candleAlpha = isLive ? liveAlpha : 1;
1191
+ if (scrubDim > 0.01 && scrubX > 0) {
1192
+ const dist = cx - scrubX;
1193
+ if (dist > 0) {
1194
+ const fadeZone = bodyW * 1.5;
1195
+ const dimT = Math.min(dist / fadeZone, 1);
1196
+ candleAlpha *= 1 - scrubDim * 0.5 * dimT;
1197
+ }
1198
+ }
1199
+ const baseAlpha = ctx.globalAlpha;
1200
+ ctx.globalAlpha = baseAlpha * candleAlpha;
1201
+ const bodyTop = toY(Math.max(c.open, c.close));
1202
+ const bodyBottom = toY(Math.min(c.open, c.close));
1203
+ const bodyH = Math.max(1, bodyBottom - bodyTop);
1204
+ const wickTop = toY(c.high);
1205
+ const wickBottom = toY(c.low);
1206
+ ctx.lineCap = "round";
1207
+ ctx.strokeStyle = color;
1208
+ if (bodyTop - wickTop > 0.5) {
1209
+ ctx.beginPath();
1210
+ ctx.moveTo(cx, bodyTop);
1211
+ ctx.lineTo(cx, wickTop);
1212
+ ctx.lineWidth = wickW;
1213
+ ctx.stroke();
1214
+ }
1215
+ if (wickBottom - bodyBottom > 0.5) {
1216
+ ctx.beginPath();
1217
+ ctx.moveTo(cx, bodyBottom);
1218
+ ctx.lineTo(cx, wickBottom);
1219
+ ctx.lineWidth = wickW;
1220
+ ctx.stroke();
1221
+ }
1222
+ ctx.fillStyle = color;
1223
+ ctx.beginPath();
1224
+ roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
1225
+ ctx.fill();
1226
+ if (isLive) {
1227
+ ctx.save();
1228
+ ctx.globalAlpha = baseAlpha * candleAlpha * livePulse;
1229
+ ctx.shadowColor = color;
1230
+ ctx.shadowBlur = 8;
1231
+ ctx.fillStyle = color;
1232
+ ctx.beginPath();
1233
+ roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
1234
+ ctx.fill();
1235
+ ctx.restore();
1236
+ }
1237
+ ctx.globalAlpha = baseAlpha;
1238
+ }
1239
+ }
1240
+ function drawClosePrice(ctx, layout, palette, liveCandle, scrubDim, bullBlend = -1) {
1241
+ const y = layout.toY(liveCandle.close);
1242
+ if (y < layout.pad.top || y > layout.h - layout.pad.bottom) return;
1243
+ const isBull = liveCandle.close >= liveCandle.open;
1244
+ const color = bullBlend >= 0 ? blendColor2(bullBlend) : isBull ? BULL : BEAR;
1245
+ const baseAlpha = ctx.globalAlpha;
1246
+ ctx.save();
1247
+ ctx.setLineDash([4, 4]);
1248
+ ctx.strokeStyle = color;
1249
+ ctx.lineWidth = 1;
1250
+ ctx.globalAlpha = baseAlpha * (1 - scrubDim * 0.3) * 0.4;
1251
+ ctx.beginPath();
1252
+ ctx.moveTo(layout.pad.left, y);
1253
+ ctx.lineTo(layout.w - layout.pad.right, y);
1254
+ ctx.stroke();
1255
+ ctx.setLineDash([]);
1256
+ ctx.restore();
1257
+ }
1258
+ function drawCandleCrosshair(ctx, layout, palette, hoverX, candle, hoverTime, formatValue, formatTime, opacity) {
1259
+ if (opacity < 0.01) return;
1260
+ const { h, pad } = layout;
1261
+ ctx.save();
1262
+ ctx.globalAlpha = opacity * 0.5;
1263
+ ctx.strokeStyle = palette.crosshairLine;
1264
+ ctx.lineWidth = 1;
1265
+ ctx.beginPath();
1266
+ ctx.moveTo(hoverX, pad.top);
1267
+ ctx.lineTo(hoverX, h - pad.bottom);
1268
+ ctx.stroke();
1269
+ ctx.restore();
1270
+ if (opacity < 0.1 || layout.w < 200) return;
1271
+ const isBull = candle.close >= candle.open;
1272
+ const valueColor = isBull ? BULL : BEAR;
1273
+ const cl = formatValue(candle.close);
1274
+ const time = formatTime(hoverTime);
1275
+ ctx.save();
1276
+ ctx.globalAlpha = opacity;
1277
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
1278
+ ctx.textAlign = "left";
1279
+ let parts;
1280
+ if (layout.w >= 400) {
1281
+ const o = formatValue(candle.open);
1282
+ const hi = formatValue(candle.high);
1283
+ const lo = formatValue(candle.low);
1284
+ parts = [
1285
+ { text: "O ", color: palette.gridLabel },
1286
+ { text: o, color: valueColor },
1287
+ { text: " H ", color: palette.gridLabel },
1288
+ { text: hi, color: valueColor },
1289
+ { text: " L ", color: palette.gridLabel },
1290
+ { text: lo, color: valueColor },
1291
+ { text: " C ", color: palette.gridLabel },
1292
+ { text: cl, color: valueColor },
1293
+ { text: " \xB7 ", color: palette.gridLabel },
1294
+ { text: time, color: palette.gridLabel }
1295
+ ];
1296
+ } else {
1297
+ parts = [
1298
+ { text: "C ", color: palette.gridLabel },
1299
+ { text: cl, color: valueColor },
1300
+ { text: " \xB7 ", color: palette.gridLabel },
1301
+ { text: time, color: palette.gridLabel }
1302
+ ];
1303
+ }
1304
+ let totalW = 0;
1305
+ const widths = [];
1306
+ for (const p of parts) {
1307
+ const w = ctx.measureText(p.text).width;
1308
+ widths.push(w);
1309
+ totalW += w;
1310
+ }
1311
+ let tx = hoverX - totalW / 2;
1312
+ const minX = pad.left + 4;
1313
+ const maxX = layout.w - pad.right - totalW;
1314
+ if (tx < minX) tx = minX;
1315
+ if (tx > maxX) tx = maxX;
1316
+ const ty = pad.top + 24;
1317
+ ctx.strokeStyle = palette.tooltipBg;
1318
+ ctx.lineWidth = 3;
1319
+ ctx.lineJoin = "round";
1320
+ let cx = tx;
1321
+ for (let i = 0; i < parts.length; i++) {
1322
+ ctx.strokeText(parts[i].text, cx, ty);
1323
+ cx += widths[i];
1324
+ }
1325
+ cx = tx;
1326
+ for (let i = 0; i < parts.length; i++) {
1327
+ ctx.fillStyle = parts[i].color;
1328
+ ctx.fillText(parts[i].text, cx, ty);
1329
+ cx += widths[i];
1330
+ }
1331
+ ctx.restore();
1332
+ }
1333
+ function drawLineModeCrosshair(ctx, layout, palette, hoverX, value, hoverTime, formatValue, formatTime, opacity) {
1334
+ if (opacity < 0.01) return;
1335
+ const { h, pad } = layout;
1336
+ const y = layout.toY(value);
1337
+ ctx.save();
1338
+ ctx.globalAlpha = opacity * 0.5;
1339
+ ctx.strokeStyle = palette.crosshairLine;
1340
+ ctx.lineWidth = 1;
1341
+ ctx.beginPath();
1342
+ ctx.moveTo(hoverX, pad.top);
1343
+ ctx.lineTo(hoverX, h - pad.bottom);
1344
+ ctx.stroke();
1345
+ ctx.globalAlpha = opacity * 0.3;
1346
+ ctx.beginPath();
1347
+ ctx.moveTo(pad.left, y);
1348
+ ctx.lineTo(layout.w - pad.right, y);
1349
+ ctx.stroke();
1350
+ ctx.restore();
1351
+ if (opacity < 0.1 || layout.w < 200) return;
1352
+ const val = formatValue(value);
1353
+ const time = formatTime(hoverTime);
1354
+ ctx.save();
1355
+ ctx.globalAlpha = opacity;
1356
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
1357
+ ctx.textAlign = "left";
1358
+ const parts = [
1359
+ { text: val, color: palette.line },
1360
+ { text: " \xB7 ", color: palette.gridLabel },
1361
+ { text: time, color: palette.gridLabel }
1362
+ ];
1363
+ let totalW = 0;
1364
+ const widths = [];
1365
+ for (const p of parts) {
1366
+ const w = ctx.measureText(p.text).width;
1367
+ widths.push(w);
1368
+ totalW += w;
1369
+ }
1370
+ let tx = hoverX - totalW / 2;
1371
+ const minX = pad.left + 4;
1372
+ const maxX = layout.w - pad.right - totalW;
1373
+ if (tx < minX) tx = minX;
1374
+ if (tx > maxX) tx = maxX;
1375
+ const ty = pad.top + 24;
1376
+ ctx.strokeStyle = palette.tooltipBg;
1377
+ ctx.lineWidth = 3;
1378
+ ctx.lineJoin = "round";
1379
+ let lx = tx;
1380
+ for (let i = 0; i < parts.length; i++) {
1381
+ ctx.strokeText(parts[i].text, lx, ty);
1382
+ lx += widths[i];
1383
+ }
1384
+ lx = tx;
1385
+ for (let i = 0; i < parts.length; i++) {
1386
+ ctx.fillStyle = parts[i].color;
1387
+ ctx.fillText(parts[i].text, lx, ty);
1388
+ lx += widths[i];
1389
+ }
1390
+ ctx.restore();
1391
+ }
1392
+
1393
+ // src/draw/empty.ts
1394
+ function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
1395
+ const chartW = w - pad.left - pad.right;
1396
+ const chartH = h - pad.top - pad.bottom;
1397
+ const centerY = pad.top + chartH / 2;
1398
+ const cx = pad.left + chartW / 2;
1399
+ const text = emptyText ?? "No data to display";
1400
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1401
+ ctx.save();
1402
+ ctx.font = "400 12px system-ui, -apple-system, sans-serif";
1403
+ const textW = ctx.measureText(text).width;
1404
+ const gapHalf = textW / 2 + 20;
1405
+ const fadeW = 30;
1406
+ if (!skipLine) {
1407
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1408
+ const breath = loadingBreath(now_ms);
1409
+ const numPts = 32;
1410
+ const pts = [];
1411
+ for (let i = 0; i <= numPts; i++) {
1412
+ const t = i / numPts;
1413
+ const x = pad.left + t * chartW;
1414
+ const y = loadingY(t, centerY, amplitude, scroll);
1415
+ pts.push([x, y]);
1416
+ }
1417
+ ctx.beginPath();
1418
+ ctx.moveTo(pts[0][0], pts[0][1]);
1419
+ drawSpline(ctx, pts);
1420
+ ctx.strokeStyle = palette.gridLabel;
1421
+ ctx.lineWidth = palette.lineWidth;
1422
+ ctx.globalAlpha = breath * alpha;
1423
+ ctx.lineCap = "round";
1424
+ ctx.lineJoin = "round";
1425
+ ctx.stroke();
1426
+ }
1427
+ ctx.save();
1428
+ ctx.globalCompositeOperation = "destination-out";
1429
+ const gapLeft = cx - gapHalf - fadeW;
1430
+ const gapRight = cx + gapHalf + fadeW;
1431
+ const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
1432
+ eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
1433
+ eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1434
+ eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1435
+ eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
1436
+ ctx.fillStyle = eraseGrad;
1437
+ ctx.globalAlpha = alpha;
1438
+ const eraseH = amplitude * 2 + palette.lineWidth + 6;
1439
+ ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
1440
+ ctx.restore();
1441
+ ctx.textAlign = "center";
1442
+ ctx.textBaseline = "middle";
1443
+ ctx.globalAlpha = 0.35 * alpha;
1444
+ ctx.fillStyle = palette.gridLabel;
1445
+ ctx.fillText(text, cx, centerY);
1446
+ ctx.restore();
1447
+ }
1448
+
973
1449
  // src/draw/index.ts
974
1450
  var SHAKE_DECAY_RATE = 2e-3;
975
1451
  var SHAKE_MIN_AMPLITUDE = 0.2;
@@ -1119,91 +1595,352 @@ function drawFrame(ctx, layout, palette, opts) {
1119
1595
  ctx.restore();
1120
1596
  }
1121
1597
  }
1122
-
1123
- // src/draw/loading.ts
1124
- function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1) {
1125
- const chartW = w - pad.left - pad.right;
1126
- const chartH = h - pad.top - pad.bottom;
1127
- const centerY = pad.top + chartH / 2;
1128
- const leftX = pad.left;
1129
- const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1130
- const scroll = now_ms * LOADING_SCROLL_SPEED;
1131
- const breath = loadingBreath(now_ms);
1132
- const numPts = 32;
1133
- const pts = [];
1134
- for (let i = 0; i <= numPts; i++) {
1135
- const t = i / numPts;
1136
- const x = leftX + t * chartW;
1137
- const y = loadingY(t, centerY, amplitude, scroll);
1138
- pts.push([x, y]);
1598
+ function drawMultiFrame(ctx, layout, opts) {
1599
+ const palette = opts.primaryPalette;
1600
+ const reveal = opts.chartReveal;
1601
+ const revealRamp = (start, end) => {
1602
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1603
+ return t * t * (3 - 2 * t);
1604
+ };
1605
+ if (opts.referenceLine && reveal > 0.01) {
1606
+ ctx.save();
1607
+ if (reveal < 1) ctx.globalAlpha = reveal;
1608
+ drawReferenceLine(ctx, layout, palette, opts.referenceLine);
1609
+ ctx.restore();
1139
1610
  }
1140
- ctx.beginPath();
1141
- ctx.moveTo(pts[0][0], pts[0][1]);
1142
- drawSpline(ctx, pts);
1143
- ctx.strokeStyle = palette.line;
1144
- ctx.lineWidth = palette.lineWidth;
1145
- ctx.globalAlpha = breath * alpha;
1146
- ctx.lineCap = "round";
1147
- ctx.lineJoin = "round";
1148
- ctx.stroke();
1149
- ctx.globalAlpha = 1;
1150
- }
1151
-
1152
- // src/draw/empty.ts
1153
- function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
1611
+ if (opts.showGrid) {
1612
+ const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1613
+ if (gridAlpha > 0.01) {
1614
+ ctx.save();
1615
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1616
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1617
+ ctx.restore();
1618
+ }
1619
+ }
1620
+ const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
1621
+ const allPts = [];
1622
+ for (let si = 0; si < opts.series.length; si++) {
1623
+ const s = opts.series[si];
1624
+ const seriesAlpha = s.alpha ?? 1;
1625
+ const secondaryFade = si > 0 && reveal < 1 ? Math.min(1, reveal * 2) : 1;
1626
+ const combinedAlpha = secondaryFade * seriesAlpha;
1627
+ if (combinedAlpha < 0.01) continue;
1628
+ ctx.save();
1629
+ if (combinedAlpha < 1) ctx.globalAlpha = combinedAlpha;
1630
+ const pts = drawLine(
1631
+ ctx,
1632
+ layout,
1633
+ s.palette,
1634
+ s.visible,
1635
+ s.smoothValue,
1636
+ opts.now,
1637
+ false,
1638
+ // no fill
1639
+ scrubX,
1640
+ opts.scrubAmount,
1641
+ reveal,
1642
+ opts.now_ms
1643
+ );
1644
+ ctx.restore();
1645
+ if (pts && pts.length > 0) {
1646
+ allPts.push({ pts, palette: s.palette, label: s.label, alpha: seriesAlpha });
1647
+ }
1648
+ }
1649
+ {
1650
+ const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1651
+ if (timeAlpha > 0.01) {
1652
+ ctx.save();
1653
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1654
+ drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1655
+ ctx.restore();
1656
+ }
1657
+ }
1658
+ if (reveal > 0.3 && allPts.length > 0) {
1659
+ const dotAlpha = (reveal - 0.3) / 0.7;
1660
+ const showPulse = opts.showPulse && reveal > 0.6 && opts.pauseProgress < 0.5;
1661
+ for (const entry of allPts) {
1662
+ if (entry.alpha < 0.01) continue;
1663
+ const lastPt = entry.pts[entry.pts.length - 1];
1664
+ ctx.save();
1665
+ ctx.globalAlpha = dotAlpha * entry.alpha;
1666
+ if (showPulse && entry.alpha > 0.5) {
1667
+ drawMultiDot(ctx, lastPt[0], lastPt[1], entry.palette.line, true, opts.now_ms, 3);
1668
+ } else {
1669
+ drawSimpleDot(ctx, lastPt[0], lastPt[1], entry.palette.line, 3);
1670
+ }
1671
+ if (entry.label) {
1672
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
1673
+ ctx.textAlign = "left";
1674
+ ctx.fillStyle = entry.palette.line;
1675
+ ctx.fillText(entry.label, lastPt[0] + 6, lastPt[1] + 3.5);
1676
+ }
1677
+ ctx.restore();
1678
+ }
1679
+ }
1680
+ ctx.save();
1681
+ ctx.globalCompositeOperation = "destination-out";
1682
+ const fadeGrad = ctx.createLinearGradient(layout.pad.left, 0, layout.pad.left + FADE_EDGE_WIDTH, 0);
1683
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
1684
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
1685
+ ctx.fillStyle = fadeGrad;
1686
+ ctx.fillRect(0, 0, layout.pad.left + FADE_EDGE_WIDTH, layout.h);
1687
+ ctx.restore();
1688
+ if (opts.hoverX !== null && opts.hoverTime !== null && opts.hoverEntries.length > 0 && allPts.length > 0 && opts.scrubAmount > 0.01) {
1689
+ let maxLiveDotX = 0;
1690
+ for (const entry of allPts) {
1691
+ if (entry.alpha < 0.01) continue;
1692
+ const lastX = entry.pts[entry.pts.length - 1][0];
1693
+ if (lastX > maxLiveDotX) maxLiveDotX = lastX;
1694
+ }
1695
+ const distToLive = maxLiveDotX - opts.hoverX;
1696
+ const fadeStart = Math.min(80, layout.chartW * 0.3);
1697
+ const scrubOpacity = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
1698
+ if (scrubOpacity > 0.01) {
1699
+ drawMultiCrosshair(
1700
+ ctx,
1701
+ layout,
1702
+ palette,
1703
+ opts.hoverX,
1704
+ opts.hoverTime,
1705
+ opts.hoverEntries,
1706
+ opts.formatValue,
1707
+ opts.formatTime,
1708
+ scrubOpacity,
1709
+ opts.tooltipY,
1710
+ opts.tooltipOutline,
1711
+ maxLiveDotX
1712
+ );
1713
+ }
1714
+ }
1715
+ }
1716
+ function drawCandleFrame(ctx, layout, palette, opts) {
1717
+ const { w, h, pad, chartW, chartH } = layout;
1718
+ const reveal = opts.chartReveal;
1719
+ const fullLineMode = opts.lineModeProg >= 0.99;
1720
+ const revealLine = fullLineMode ? 1 - reveal : (1 - reveal) * (1 - reveal) * (1 - reveal);
1721
+ const lp = Math.max(opts.lineModeProg, revealLine);
1722
+ const colorBlend = lp > 1e-3 ? opts.lineModeProg / lp : 1;
1723
+ const revealRamp = (start, end) => {
1724
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1725
+ return t * t * (3 - 2 * t);
1726
+ };
1727
+ const gridAlpha = revealRamp(0.25, 0.6);
1728
+ if (opts.showGrid && gridAlpha > 0.01) {
1729
+ ctx.save();
1730
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1731
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1732
+ ctx.restore();
1733
+ }
1734
+ let linePts;
1735
+ if (lp > 0.01 && opts.lineVisible.length >= 2) {
1736
+ const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
1737
+ ctx.save();
1738
+ ctx.globalAlpha = lp;
1739
+ linePts = drawLine(
1740
+ ctx,
1741
+ layout,
1742
+ palette,
1743
+ opts.lineVisible,
1744
+ opts.lineSmoothValue,
1745
+ opts.now,
1746
+ opts.lineModeProg > 0.01,
1747
+ scrubX,
1748
+ opts.scrubAmount,
1749
+ opts.chartReveal,
1750
+ opts.now_ms,
1751
+ colorBlend,
1752
+ !fullLineMode,
1753
+ opts.lineModeProg
1754
+ // fillScale — fill fades smoothly with line mode transition
1755
+ );
1756
+ ctx.restore();
1757
+ }
1758
+ const closeAlpha = revealRamp(0.4, 0.8);
1759
+ const closeSource = opts.closePriceCandle ?? opts.liveCandle;
1760
+ if (closeSource && closeAlpha > 0.01) {
1761
+ if (lp < 0.99) {
1762
+ ctx.save();
1763
+ ctx.globalAlpha = closeAlpha * (1 - lp);
1764
+ drawClosePrice(ctx, layout, palette, closeSource, opts.scrubAmount, opts.liveBullBlend);
1765
+ ctx.restore();
1766
+ }
1767
+ if (lp > 0.01 && !fullLineMode) {
1768
+ const dashY = layout.toY(closeSource.close);
1769
+ if (dashY >= pad.top && dashY <= h - pad.bottom) {
1770
+ ctx.save();
1771
+ ctx.setLineDash([4, 4]);
1772
+ ctx.strokeStyle = palette.dashLine;
1773
+ ctx.lineWidth = 1;
1774
+ ctx.globalAlpha = closeAlpha * lp * (1 - opts.scrubAmount * 0.2);
1775
+ ctx.beginPath();
1776
+ ctx.moveTo(pad.left, dashY);
1777
+ ctx.lineTo(w - pad.right, dashY);
1778
+ ctx.stroke();
1779
+ ctx.setLineDash([]);
1780
+ ctx.restore();
1781
+ }
1782
+ }
1783
+ }
1784
+ const candleAlpha = opts.chartReveal * (1 - lp);
1785
+ if (candleAlpha > 0.01) {
1786
+ const ohlcScale = reveal * reveal * (3 - 2 * reveal);
1787
+ const collapseC = (c) => ohlcScale >= 0.99 ? c : {
1788
+ time: c.time,
1789
+ open: c.close + (c.open - c.close) * ohlcScale,
1790
+ high: c.close + (c.high - c.close) * ohlcScale,
1791
+ low: c.close + (c.low - c.close) * ohlcScale,
1792
+ close: c.close
1793
+ };
1794
+ const revealCandles = ohlcScale < 0.99 ? opts.candles.map(collapseC) : opts.candles;
1795
+ const revealOld = ohlcScale < 0.99 && opts.oldCandles.length > 0 ? opts.oldCandles.map(collapseC) : opts.oldCandles;
1796
+ ctx.save();
1797
+ ctx.beginPath();
1798
+ ctx.rect(pad.left - 1, pad.top, chartW + 2, chartH);
1799
+ ctx.clip();
1800
+ const accentCol = lp > 0.01 ? palette.line : void 0;
1801
+ if (opts.morphT >= 0 && revealOld.length > 0) {
1802
+ ctx.globalAlpha = (1 - opts.morphT) * candleAlpha;
1803
+ drawCandlesticks(
1804
+ ctx,
1805
+ layout,
1806
+ revealOld,
1807
+ opts.oldWidth,
1808
+ -1,
1809
+ opts.now_ms,
1810
+ opts.hoverX ?? 0,
1811
+ opts.scrubAmount,
1812
+ 1,
1813
+ -1,
1814
+ accentCol,
1815
+ lp
1816
+ );
1817
+ ctx.globalAlpha = opts.morphT * candleAlpha;
1818
+ drawCandlesticks(
1819
+ ctx,
1820
+ layout,
1821
+ revealCandles,
1822
+ opts.displayCandleWidth,
1823
+ opts.liveCandle?.time ?? -1,
1824
+ opts.now_ms,
1825
+ opts.hoverX ?? 0,
1826
+ opts.scrubAmount,
1827
+ opts.liveBirthAlpha,
1828
+ opts.liveBullBlend,
1829
+ accentCol,
1830
+ lp
1831
+ );
1832
+ ctx.globalAlpha = 1;
1833
+ } else {
1834
+ if (candleAlpha < 1) ctx.globalAlpha = candleAlpha;
1835
+ drawCandlesticks(
1836
+ ctx,
1837
+ layout,
1838
+ revealCandles,
1839
+ opts.displayCandleWidth,
1840
+ opts.liveCandle?.time ?? -1,
1841
+ opts.now_ms,
1842
+ opts.hoverX ?? 0,
1843
+ opts.scrubAmount,
1844
+ opts.liveBirthAlpha,
1845
+ opts.liveBullBlend,
1846
+ accentCol,
1847
+ lp
1848
+ );
1849
+ }
1850
+ ctx.restore();
1851
+ }
1852
+ if (lp > 0.5 && linePts && linePts.length > 0 && reveal > 0.3) {
1853
+ const lastPt = linePts[linePts.length - 1];
1854
+ const dotAlpha = (lp - 0.5) * 2 * ((reveal - 0.3) / 0.7);
1855
+ const showPulse = lp > 0.8 && reveal > 0.6;
1856
+ if (dotAlpha > 0.01) {
1857
+ ctx.save();
1858
+ ctx.globalAlpha = dotAlpha;
1859
+ drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, opts.scrubAmount, opts.now_ms);
1860
+ ctx.restore();
1861
+ }
1862
+ }
1863
+ const timeAlpha = revealRamp(0.25, 0.6);
1864
+ if (timeAlpha > 0.01) {
1865
+ ctx.save();
1866
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1867
+ drawTimeAxis(ctx, layout, palette, opts.targetWindowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1868
+ ctx.restore();
1869
+ }
1870
+ ctx.save();
1871
+ ctx.globalCompositeOperation = "destination-out";
1872
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
1873
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
1874
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
1875
+ ctx.fillStyle = fadeGrad;
1876
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
1877
+ ctx.restore();
1878
+ if (opts.showEmptyOverlay) {
1879
+ const bgAlpha = 1 - opts.chartReveal;
1880
+ if (bgAlpha > 0.01) {
1881
+ const bgEmptyAlpha = (1 - opts.loadingAlpha) * bgAlpha;
1882
+ if (bgEmptyAlpha > 0.01) {
1883
+ drawEmpty(ctx, w, h, pad, palette, bgEmptyAlpha, opts.now_ms, true, opts.emptyText);
1884
+ }
1885
+ }
1886
+ }
1887
+ if (opts.chartReveal > 0.7 && opts.hoveredCandle && opts.hoverX !== null && opts.scrubAmount > 0.01) {
1888
+ if (opts.lineModeProg > 0.5) {
1889
+ drawLineModeCrosshair(
1890
+ ctx,
1891
+ layout,
1892
+ palette,
1893
+ opts.hoverX,
1894
+ opts.hoveredCandle.close,
1895
+ opts.hoverTime ?? 0,
1896
+ opts.formatValue,
1897
+ opts.formatTime,
1898
+ opts.scrubAmount
1899
+ );
1900
+ } else {
1901
+ drawCandleCrosshair(
1902
+ ctx,
1903
+ layout,
1904
+ palette,
1905
+ opts.hoverX,
1906
+ opts.hoveredCandle,
1907
+ opts.hoverTime ?? 0,
1908
+ opts.formatValue,
1909
+ opts.formatTime,
1910
+ opts.scrubAmount
1911
+ );
1912
+ }
1913
+ }
1914
+ }
1915
+
1916
+ // src/draw/loading.ts
1917
+ function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1, strokeColor) {
1154
1918
  const chartW = w - pad.left - pad.right;
1155
1919
  const chartH = h - pad.top - pad.bottom;
1156
1920
  const centerY = pad.top + chartH / 2;
1157
- const cx = pad.left + chartW / 2;
1158
- const text = emptyText ?? "No data to display";
1159
- ctx.font = "400 12px system-ui, -apple-system, sans-serif";
1921
+ const leftX = pad.left;
1160
1922
  const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1161
- const textW = ctx.measureText(text).width;
1162
- const gapHalf = textW / 2 + 20;
1163
- const fadeW = 30;
1164
- if (!skipLine) {
1165
- const scroll = now_ms * LOADING_SCROLL_SPEED;
1166
- const breath = loadingBreath(now_ms);
1167
- const numPts = 32;
1168
- const pts = [];
1169
- for (let i = 0; i <= numPts; i++) {
1170
- const t = i / numPts;
1171
- const x = pad.left + t * chartW;
1172
- const y = loadingY(t, centerY, amplitude, scroll);
1173
- pts.push([x, y]);
1174
- }
1175
- ctx.beginPath();
1176
- ctx.moveTo(pts[0][0], pts[0][1]);
1177
- drawSpline(ctx, pts);
1178
- ctx.strokeStyle = palette.gridLabel;
1179
- ctx.lineWidth = palette.lineWidth;
1180
- ctx.globalAlpha = breath * alpha;
1181
- ctx.lineCap = "round";
1182
- ctx.lineJoin = "round";
1183
- ctx.stroke();
1923
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1924
+ const breath = loadingBreath(now_ms);
1925
+ const numPts = 32;
1926
+ const pts = [];
1927
+ for (let i = 0; i <= numPts; i++) {
1928
+ const t = i / numPts;
1929
+ const x = leftX + t * chartW;
1930
+ const y = loadingY(t, centerY, amplitude, scroll);
1931
+ pts.push([x, y]);
1184
1932
  }
1185
1933
  ctx.save();
1186
- ctx.globalCompositeOperation = "destination-out";
1187
- const gapLeft = cx - gapHalf - fadeW;
1188
- const gapRight = cx + gapHalf + fadeW;
1189
- const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
1190
- eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
1191
- eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1192
- eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1193
- eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
1194
- ctx.fillStyle = eraseGrad;
1195
- ctx.globalAlpha = alpha;
1196
- const eraseH = amplitude * 2 + palette.lineWidth + 6;
1197
- ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
1934
+ ctx.beginPath();
1935
+ ctx.moveTo(pts[0][0], pts[0][1]);
1936
+ drawSpline(ctx, pts);
1937
+ ctx.strokeStyle = strokeColor ?? palette.line;
1938
+ ctx.lineWidth = palette.lineWidth;
1939
+ ctx.globalAlpha = breath * alpha;
1940
+ ctx.lineCap = "round";
1941
+ ctx.lineJoin = "round";
1942
+ ctx.stroke();
1198
1943
  ctx.restore();
1199
- ctx.textAlign = "center";
1200
- ctx.textBaseline = "middle";
1201
- ctx.globalAlpha = 0.35 * alpha;
1202
- ctx.fillStyle = palette.gridLabel;
1203
- ctx.fillText(text, cx, centerY);
1204
- ctx.globalAlpha = 1;
1205
- ctx.textAlign = "start";
1206
- ctx.textBaseline = "alphabetic";
1207
1944
  }
1208
1945
 
1209
1946
  // src/draw/badge.ts
@@ -1253,10 +1990,23 @@ var ADAPTIVE_SPEED_BOOST = 0.2;
1253
1990
  var MOMENTUM_GREEN = [34, 197, 94];
1254
1991
  var MOMENTUM_RED = [239, 68, 68];
1255
1992
  var CHART_REVEAL_SPEED = 0.14;
1993
+ var CHART_REVEAL_SPEED_FWD = 0.09;
1256
1994
  var PAUSE_PROGRESS_SPEED = 0.12;
1257
1995
  var PAUSE_CATCHUP_SPEED = 0.08;
1258
1996
  var PAUSE_CATCHUP_SPEED_FAST = 0.22;
1259
1997
  var LOADING_ALPHA_SPEED = 0.14;
1998
+ var SERIES_TOGGLE_SPEED = 0.1;
1999
+ var CANDLE_LERP_SPEED = 0.25;
2000
+ var CANDLE_WIDTH_TRANS_MS = 300;
2001
+ var LINE_MORPH_MS = 500;
2002
+ var CLOSE_LINE_LERP_SPEED = 0.25;
2003
+ var LINE_DENSITY_MS = 350;
2004
+ var LINE_LERP_BASE = 0.08;
2005
+ var LINE_ADAPTIVE_BOOST = 0.2;
2006
+ var LINE_SNAP_THRESHOLD = 1e-3;
2007
+ var RANGE_LERP_SPEED = 0.15;
2008
+ var RANGE_ADAPTIVE_BOOST = 0.2;
2009
+ var CANDLE_BUFFER = 0.05;
1260
2010
  function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
1261
2011
  const valGap = Math.abs(value - displayValue);
1262
2012
  const prevRange = displayMax - displayMin || 1;
@@ -1456,10 +2206,121 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
1456
2206
  }
1457
2207
  return badgeY;
1458
2208
  }
2209
+ function computeCandleRange(candles) {
2210
+ let min = Infinity;
2211
+ let max = -Infinity;
2212
+ for (const c of candles) {
2213
+ if (c.low < min) min = c.low;
2214
+ if (c.high > max) max = c.high;
2215
+ }
2216
+ if (!isFinite(min) || !isFinite(max)) return { min: 99, max: 101 };
2217
+ const range = max - min;
2218
+ const margin = range * 0.12;
2219
+ const minRange = range * 0.1 || 0.4;
2220
+ if (range < minRange) {
2221
+ const mid = (min + max) / 2;
2222
+ return { min: mid - minRange / 2, max: mid + minRange / 2 };
2223
+ }
2224
+ return { min: min - margin, max: max + margin };
2225
+ }
2226
+ function candleAtX(candles, hoverX, candleWidth, layout) {
2227
+ const time = layout.leftEdge + (hoverX - layout.pad.left) / layout.chartW * (layout.rightEdge - layout.leftEdge);
2228
+ let lo = 0;
2229
+ let hi = candles.length - 1;
2230
+ while (lo <= hi) {
2231
+ const mid = lo + hi >> 1;
2232
+ const c = candles[mid];
2233
+ if (time < c.time) hi = mid - 1;
2234
+ else if (time >= c.time + candleWidth) lo = mid + 1;
2235
+ else return c;
2236
+ }
2237
+ return null;
2238
+ }
2239
+ function updateCandleRange(computedRange, rangeInited, displayMin, displayMax, isTransitioning, windowTransProgress, wt, chartH, dt) {
2240
+ if (!rangeInited) {
2241
+ return {
2242
+ minVal: computedRange.min,
2243
+ maxVal: computedRange.max,
2244
+ valRange: computedRange.max - computedRange.min || 1e-3,
2245
+ displayMin: computedRange.min,
2246
+ displayMax: computedRange.max,
2247
+ rangeInited: true
2248
+ };
2249
+ }
2250
+ if (isTransitioning) {
2251
+ displayMin = wt.rangeFromMin + (wt.rangeToMin - wt.rangeFromMin) * windowTransProgress;
2252
+ displayMax = wt.rangeFromMax + (wt.rangeToMax - wt.rangeFromMax) * windowTransProgress;
2253
+ } else {
2254
+ const curRange = displayMax - displayMin || 1;
2255
+ const gapMin = Math.abs(displayMin - computedRange.min);
2256
+ const gapMax = Math.abs(displayMax - computedRange.max);
2257
+ const gapRatio = Math.min((gapMin + gapMax) / curRange, 1);
2258
+ const speed = RANGE_LERP_SPEED + (1 - gapRatio) * RANGE_ADAPTIVE_BOOST;
2259
+ displayMin = lerp(displayMin, computedRange.min, speed, dt);
2260
+ displayMax = lerp(displayMax, computedRange.max, speed, dt);
2261
+ const pxThreshold = 0.5 * curRange / chartH || 1e-3;
2262
+ if (Math.abs(displayMin - computedRange.min) < pxThreshold) displayMin = computedRange.min;
2263
+ if (Math.abs(displayMax - computedRange.max) < pxThreshold) displayMax = computedRange.max;
2264
+ }
2265
+ return {
2266
+ minVal: displayMin,
2267
+ maxVal: displayMax,
2268
+ valRange: displayMax - displayMin || 1e-3,
2269
+ displayMin,
2270
+ displayMax,
2271
+ rangeInited: true
2272
+ };
2273
+ }
2274
+ function updateCandleWindowTransition(targetWindowSecs, wt, displayWindow, displayMin, displayMax, now_ms, now, candles, liveCandle, candleWidth, buffer) {
2275
+ if (wt.to !== targetWindowSecs) {
2276
+ wt.from = displayWindow;
2277
+ wt.to = targetWindowSecs;
2278
+ wt.startMs = now_ms;
2279
+ wt.rangeFromMin = displayMin;
2280
+ wt.rangeFromMax = displayMax;
2281
+ const targetRightEdge = now + targetWindowSecs * buffer;
2282
+ const targetLeftEdge = targetRightEdge - targetWindowSecs;
2283
+ const targetVisible = [];
2284
+ for (const c of candles) {
2285
+ if (c.time + candleWidth >= targetLeftEdge && c.time <= targetRightEdge) {
2286
+ targetVisible.push(c);
2287
+ }
2288
+ }
2289
+ if (liveCandle && liveCandle.time + candleWidth >= targetLeftEdge && liveCandle.time <= targetRightEdge) {
2290
+ targetVisible.push(liveCandle);
2291
+ }
2292
+ if (targetVisible.length > 0) {
2293
+ const tr = computeCandleRange(targetVisible);
2294
+ wt.rangeToMin = tr.min;
2295
+ wt.rangeToMax = tr.max;
2296
+ }
2297
+ }
2298
+ let windowTransProgress = 0;
2299
+ let resultWindow;
2300
+ if (wt.startMs === 0) {
2301
+ resultWindow = targetWindowSecs;
2302
+ } else {
2303
+ const elapsed = now_ms - wt.startMs;
2304
+ const t = Math.min(elapsed / WINDOW_TRANSITION_MS, 1);
2305
+ const eased = (1 - Math.cos(t * Math.PI)) / 2;
2306
+ windowTransProgress = eased;
2307
+ const logFrom = Math.log(wt.from);
2308
+ const logTo = Math.log(wt.to);
2309
+ resultWindow = Math.exp(logFrom + (logTo - logFrom) * eased);
2310
+ if (t >= 1) {
2311
+ resultWindow = targetWindowSecs;
2312
+ wt.startMs = 0;
2313
+ windowTransProgress = 0;
2314
+ }
2315
+ }
2316
+ return { windowSecs: resultWindow, windowTransProgress };
2317
+ }
1459
2318
  function useLivelineEngine(canvasRef, containerRef, config) {
1460
2319
  const configRef = (0, import_react.useRef)(config);
1461
2320
  configRef.current = config;
1462
2321
  const displayValueRef = (0, import_react.useRef)(config.value);
2322
+ const displayValuesRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
2323
+ const seriesAlphaRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
1463
2324
  const displayMinRef = (0, import_react.useRef)(0);
1464
2325
  const displayMaxRef = (0, import_react.useRef)(0);
1465
2326
  const targetMinRef = (0, import_react.useRef)(0);
@@ -1492,13 +2353,49 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1492
2353
  const hoverXRef = (0, import_react.useRef)(null);
1493
2354
  const scrubAmountRef = (0, import_react.useRef)(0);
1494
2355
  const lastHoverRef = (0, import_react.useRef)(null);
2356
+ const lastHoverEntriesRef = (0, import_react.useRef)([]);
1495
2357
  const chartRevealRef = (0, import_react.useRef)(0);
1496
2358
  const pauseProgressRef = (0, import_react.useRef)(0);
1497
2359
  const timeDebtRef = (0, import_react.useRef)(0);
1498
2360
  const lastDataRef = (0, import_react.useRef)([]);
2361
+ const lastMultiSeriesRef = (0, import_react.useRef)([]);
1499
2362
  const frozenNowRef = (0, import_react.useRef)(0);
1500
2363
  const pausedDataRef = (0, import_react.useRef)(null);
2364
+ const pausedMultiDataRef = (0, import_react.useRef)(null);
1501
2365
  const loadingAlphaRef = (0, import_react.useRef)(config.loading ? 1 : 0);
2366
+ const displayCandleRef = (0, import_react.useRef)(null);
2367
+ const liveBirthAlphaRef = (0, import_react.useRef)(1);
2368
+ const liveBullRef = (0, import_react.useRef)(0.5);
2369
+ const lineSmoothCloseRef = (0, import_react.useRef)(0);
2370
+ const lineSmoothInitedRef = (0, import_react.useRef)(false);
2371
+ const closeLineSmoothRef = (0, import_react.useRef)(0);
2372
+ const closeLineSmoothInitedRef = (0, import_react.useRef)(false);
2373
+ const lineModeProgRef = (0, import_react.useRef)(0);
2374
+ const lineModeTransRef = (0, import_react.useRef)({ startMs: 0, from: 0, to: 0 });
2375
+ const lineDensityProgRef = (0, import_react.useRef)(0);
2376
+ const lineDensityTransRef = (0, import_react.useRef)({ startMs: 0, from: 0, to: 0 });
2377
+ const lineTickSmoothRef = (0, import_react.useRef)(0);
2378
+ const lineTickSmoothInitedRef = (0, import_react.useRef)(false);
2379
+ const candleWidthTransRef = (0, import_react.useRef)({
2380
+ fromWidth: config.candleWidth ?? 1,
2381
+ toWidth: config.candleWidth ?? 1,
2382
+ startMs: 0,
2383
+ rangeFromMin: 0,
2384
+ rangeFromMax: 0,
2385
+ rangeToMin: 0,
2386
+ rangeToMax: 0,
2387
+ oldCandles: [],
2388
+ oldWidth: config.candleWidth ?? 1
2389
+ });
2390
+ const prevCandleDataRef = (0, import_react.useRef)({ candles: [], width: config.candleWidth ?? 1 });
2391
+ const pausedCandlesRef = (0, import_react.useRef)(null);
2392
+ const pausedLiveRef = (0, import_react.useRef)(null);
2393
+ const pausedLineDataRef = (0, import_react.useRef)(null);
2394
+ const pausedLineValueRef = (0, import_react.useRef)(null);
2395
+ const lastCandlesRef = (0, import_react.useRef)([]);
2396
+ const lastLiveRef = (0, import_react.useRef)(null);
2397
+ const lastLineDataStashRef = (0, import_react.useRef)([]);
2398
+ const lastLineValueStashRef = (0, import_react.useRef)(void 0);
1502
2399
  (0, import_react.useEffect)(() => {
1503
2400
  const container = containerRef.current;
1504
2401
  if (!container) return;
@@ -1630,14 +2527,43 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1630
2527
  }
1631
2528
  applyDpr(ctx, dpr, w, h);
1632
2529
  const noMotion = reducedMotionRef.current;
1633
- if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
1634
- pausedDataRef.current = cfg.data.slice();
1635
- }
1636
- if (!cfg.paused) {
1637
- pausedDataRef.current = null;
2530
+ const isCandle = cfg.mode === "candle";
2531
+ if (isCandle) {
2532
+ if (cfg.paused && pausedCandlesRef.current === null && (cfg.candles?.length ?? 0) > 0) {
2533
+ pausedCandlesRef.current = cfg.candles.slice();
2534
+ pausedLiveRef.current = cfg.liveCandle ?? null;
2535
+ pausedLineDataRef.current = cfg.lineData?.slice() ?? null;
2536
+ pausedLineValueRef.current = cfg.lineValue ?? null;
2537
+ }
2538
+ if (!cfg.paused) {
2539
+ pausedCandlesRef.current = null;
2540
+ pausedLiveRef.current = null;
2541
+ pausedLineDataRef.current = null;
2542
+ pausedLineValueRef.current = null;
2543
+ }
2544
+ } else if (cfg.isMultiSeries && cfg.multiSeries) {
2545
+ if (cfg.paused && pausedMultiDataRef.current === null) {
2546
+ const snap = /* @__PURE__ */ new Map();
2547
+ for (const s of cfg.multiSeries) {
2548
+ if (s.data.length >= 2) snap.set(s.id, { data: s.data.slice(), value: s.value });
2549
+ }
2550
+ if (snap.size > 0) pausedMultiDataRef.current = snap;
2551
+ }
2552
+ if (!cfg.paused) {
2553
+ pausedMultiDataRef.current = null;
2554
+ }
2555
+ } else {
2556
+ if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
2557
+ pausedDataRef.current = cfg.data.slice();
2558
+ }
2559
+ if (!cfg.paused) {
2560
+ pausedDataRef.current = null;
2561
+ }
1638
2562
  }
1639
- const points = pausedDataRef.current ?? cfg.data;
1640
- const hasData = points.length >= 2;
2563
+ const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
2564
+ const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
2565
+ const hasMultiData = cfg.isMultiSeries && cfg.multiSeries ? cfg.multiSeries.some((s) => s.data.length >= 2) : false;
2566
+ const hasData = isCandle ? effectiveCandles.length >= 2 : hasMultiData || points.length >= 2;
1641
2567
  const pad = cfg.padding;
1642
2568
  const chartH = h - pad.top - pad.bottom;
1643
2569
  const pauseTarget = cfg.paused ? 1 : 0;
@@ -1659,18 +2585,62 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1659
2585
  if (loadingAlphaRef.current > 0.99) loadingAlphaRef.current = 1;
1660
2586
  const loadingAlpha = loadingAlphaRef.current;
1661
2587
  const revealTarget = !cfg.loading && hasData ? 1 : 0;
1662
- chartRevealRef.current = noMotion ? revealTarget : lerp(chartRevealRef.current, revealTarget, CHART_REVEAL_SPEED, dt);
2588
+ chartRevealRef.current = noMotion ? revealTarget : lerp(
2589
+ chartRevealRef.current,
2590
+ revealTarget,
2591
+ revealTarget === 1 ? CHART_REVEAL_SPEED_FWD : CHART_REVEAL_SPEED,
2592
+ dt
2593
+ );
1663
2594
  if (Math.abs(chartRevealRef.current - revealTarget) < 5e-3) {
1664
2595
  chartRevealRef.current = revealTarget;
1665
2596
  }
1666
2597
  const chartReveal = chartRevealRef.current;
1667
- const useStash = !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
1668
- if (hasData) {
1669
- lastDataRef.current = points;
2598
+ if (chartReveal < 0.01) {
2599
+ rangeInitedRef.current = false;
2600
+ }
2601
+ let useStash;
2602
+ let useMultiStash = false;
2603
+ if (isCandle) {
2604
+ useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
2605
+ } else {
2606
+ useMultiStash = !hasData && chartReveal > 5e-3 && lastMultiSeriesRef.current.length > 0;
2607
+ if (hasMultiData && cfg.multiSeries) {
2608
+ lastMultiSeriesRef.current = cfg.multiSeries.map((s) => ({
2609
+ id: s.id,
2610
+ data: s.data.slice(),
2611
+ value: s.value,
2612
+ palette: s.palette,
2613
+ label: s.label
2614
+ }));
2615
+ }
2616
+ if (hasData && !cfg.isMultiSeries) lastMultiSeriesRef.current = [];
2617
+ useStash = !useMultiStash && !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
2618
+ if (hasData && !cfg.isMultiSeries) lastDataRef.current = points;
1670
2619
  }
1671
- if (!hasData && !useStash) {
2620
+ if (isCandle) {
2621
+ const lmt = lineModeTransRef.current;
2622
+ const lineModeTarget = cfg.lineMode ? 1 : 0;
2623
+ if (lmt.to !== lineModeTarget) {
2624
+ lmt.from = lineModeProgRef.current;
2625
+ lmt.to = lineModeTarget;
2626
+ lmt.startMs = now_ms;
2627
+ }
2628
+ if (lmt.startMs > 0) {
2629
+ const elapsed = now_ms - lmt.startMs;
2630
+ const t = Math.min(elapsed / LINE_MORPH_MS, 1);
2631
+ lineModeProgRef.current = lmt.from + (lmt.to - lmt.from) * ((1 - Math.cos(t * Math.PI)) / 2);
2632
+ if (t >= 1) {
2633
+ lineModeProgRef.current = lmt.to;
2634
+ lmt.startMs = 0;
2635
+ }
2636
+ } else {
2637
+ lineModeProgRef.current = lmt.to;
2638
+ }
2639
+ }
2640
+ if (!hasData && !useStash && !useMultiStash) {
2641
+ const loadingColor = isCandle || cfg.isMultiSeries || lastMultiSeriesRef.current.length > 0 ? cfg.palette.gridLabel : void 0;
1672
2642
  if (loadingAlpha > 0.01) {
1673
- drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha);
2643
+ drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
1674
2644
  }
1675
2645
  if (1 - loadingAlpha > 0.01) {
1676
2646
  drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
@@ -1687,190 +2657,840 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1687
2657
  rafRef.current = requestAnimationFrame(draw);
1688
2658
  return;
1689
2659
  }
1690
- const effectivePoints = useStash ? lastDataRef.current : points;
1691
- const adaptiveSpeed = computeAdaptiveSpeed(
1692
- cfg.value,
1693
- displayValueRef.current,
1694
- displayMinRef.current,
1695
- displayMaxRef.current,
1696
- cfg.lerpSpeed,
1697
- noMotion
1698
- );
1699
- if (!useStash) {
1700
- displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
1701
- if (pauseProgress < 0.5) {
1702
- const prevRange = displayMaxRef.current - displayMinRef.current || 1;
1703
- if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
1704
- displayValueRef.current = cfg.value;
2660
+ if (isCandle) {
2661
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
2662
+ const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
2663
+ const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
2664
+ let effectiveLineData = pausedLineDataRef.current ?? cfg.lineData;
2665
+ let effectiveLineValue = pausedLineValueRef.current ?? cfg.lineValue;
2666
+ if (hasData && effectiveLineData && effectiveLineData.length > 0) {
2667
+ lastLineDataStashRef.current = effectiveLineData;
2668
+ lastLineValueStashRef.current = effectiveLineValue;
2669
+ }
2670
+ if (useStash && lastLineDataStashRef.current.length > 0) {
2671
+ effectiveLineData = lastLineDataStashRef.current;
2672
+ effectiveLineValue = lastLineValueStashRef.current;
2673
+ }
2674
+ const candleWidthSecs = cfg.candleWidth ?? 1;
2675
+ const cwt = candleWidthTransRef.current;
2676
+ let morphT = -1;
2677
+ let displayCandleWidth;
2678
+ if (cwt.startMs > 0) {
2679
+ const elapsed = now_ms - cwt.startMs;
2680
+ const t = Math.min(elapsed / CANDLE_WIDTH_TRANS_MS, 1);
2681
+ morphT = (1 - Math.cos(t * Math.PI)) / 2;
2682
+ displayCandleWidth = Math.exp(
2683
+ Math.log(cwt.fromWidth) + (Math.log(cwt.toWidth) - Math.log(cwt.fromWidth)) * morphT
2684
+ );
2685
+ if (t >= 1) {
2686
+ displayCandleWidth = cwt.toWidth;
2687
+ cwt.startMs = 0;
2688
+ morphT = -1;
2689
+ }
2690
+ } else {
2691
+ displayCandleWidth = cwt.toWidth;
2692
+ }
2693
+ if (candleWidthSecs !== cwt.toWidth) {
2694
+ cwt.oldCandles = prevCandleDataRef.current.candles;
2695
+ cwt.oldWidth = prevCandleDataRef.current.width;
2696
+ cwt.fromWidth = displayCandleWidth;
2697
+ cwt.toWidth = candleWidthSecs;
2698
+ cwt.startMs = now_ms;
2699
+ morphT = 0;
2700
+ cwt.rangeFromMin = displayMinRef.current;
2701
+ cwt.rangeFromMax = displayMaxRef.current;
2702
+ const curWindow = displayWindowRef.current;
2703
+ const re = now + curWindow * CANDLE_BUFFER;
2704
+ const le = re - curWindow;
2705
+ const targetVis = [];
2706
+ for (const c of effectiveCandles) {
2707
+ if (c.time + candleWidthSecs >= le && c.time <= re) targetVis.push(c);
2708
+ }
2709
+ if (rawLive) targetVis.push(rawLive);
2710
+ if (targetVis.length > 0) {
2711
+ const tr = computeCandleRange(targetVis);
2712
+ cwt.rangeToMin = tr.min;
2713
+ cwt.rangeToMax = tr.max;
2714
+ } else {
2715
+ cwt.rangeToMin = displayMinRef.current;
2716
+ cwt.rangeToMax = displayMaxRef.current;
2717
+ }
2718
+ }
2719
+ prevCandleDataRef.current = { candles: cfg.candles ?? [], width: candleWidthSecs };
2720
+ const lineModeProg = lineModeProgRef.current;
2721
+ const ldt = lineDensityTransRef.current;
2722
+ const hasTickData = effectiveLineData && effectiveLineData.length > 0;
2723
+ const densityTarget = cfg.lineMode && lineModeProg >= 0.3 && hasTickData ? 1 : 0;
2724
+ if (ldt.to !== densityTarget) {
2725
+ ldt.from = lineDensityProgRef.current;
2726
+ ldt.to = densityTarget;
2727
+ ldt.startMs = now_ms;
2728
+ }
2729
+ let lineDensityProg;
2730
+ if (ldt.startMs > 0) {
2731
+ const elapsed = now_ms - ldt.startMs;
2732
+ const t = Math.min(elapsed / LINE_DENSITY_MS, 1);
2733
+ lineDensityProg = ldt.from + (ldt.to - ldt.from) * (1 - (1 - t) * (1 - t));
2734
+ if (t >= 1) {
2735
+ lineDensityProg = ldt.to;
2736
+ ldt.startMs = 0;
2737
+ }
2738
+ } else {
2739
+ lineDensityProg = ldt.to;
2740
+ }
2741
+ lineDensityProgRef.current = lineDensityProg;
2742
+ const transition = windowTransitionRef.current;
2743
+ const windowResult = updateCandleWindowTransition(
2744
+ cfg.windowSecs,
2745
+ transition,
2746
+ displayWindowRef.current,
2747
+ displayMinRef.current,
2748
+ displayMaxRef.current,
2749
+ now_ms,
2750
+ now,
2751
+ effectiveCandles,
2752
+ rawLive,
2753
+ candleWidthSecs,
2754
+ CANDLE_BUFFER
2755
+ );
2756
+ displayWindowRef.current = windowResult.windowSecs;
2757
+ const windowSecs = windowResult.windowSecs;
2758
+ const windowTransProgress = windowResult.windowTransProgress;
2759
+ const isWindowTransitioning = transition.startMs > 0;
2760
+ const rightEdge = now + windowSecs * CANDLE_BUFFER;
2761
+ const leftEdge = rightEdge - windowSecs;
2762
+ let smoothLive;
2763
+ if (rawLive) {
2764
+ const prev = displayCandleRef.current;
2765
+ if (!prev || prev.time !== rawLive.time) {
2766
+ displayCandleRef.current = {
2767
+ time: rawLive.time,
2768
+ open: rawLive.open,
2769
+ high: rawLive.open,
2770
+ low: rawLive.open,
2771
+ close: rawLive.open
2772
+ };
2773
+ liveBirthAlphaRef.current = 0;
2774
+ } else {
2775
+ const dc2 = displayCandleRef.current;
2776
+ dc2.open = lerp(dc2.open, rawLive.open, CANDLE_LERP_SPEED, pausedDt);
2777
+ dc2.high = lerp(dc2.high, rawLive.high, CANDLE_LERP_SPEED, pausedDt);
2778
+ dc2.low = lerp(dc2.low, rawLive.low, CANDLE_LERP_SPEED, pausedDt);
2779
+ dc2.close = lerp(dc2.close, rawLive.close, CANDLE_LERP_SPEED, pausedDt);
2780
+ }
2781
+ liveBirthAlphaRef.current = lerp(liveBirthAlphaRef.current, 1, 0.2, pausedDt);
2782
+ if (liveBirthAlphaRef.current > 0.99) liveBirthAlphaRef.current = 1;
2783
+ const dc = displayCandleRef.current;
2784
+ const bullTarget = dc.close >= dc.open ? 1 : 0;
2785
+ liveBullRef.current = lerp(liveBullRef.current, bullTarget, 0.12, pausedDt);
2786
+ if (liveBullRef.current > 0.99) liveBullRef.current = 1;
2787
+ if (liveBullRef.current < 0.01) liveBullRef.current = 0;
2788
+ smoothLive = dc;
2789
+ } else {
2790
+ displayCandleRef.current = null;
2791
+ liveBirthAlphaRef.current = 1;
2792
+ liveBullRef.current = 0.5;
2793
+ }
2794
+ if (rawLive) {
2795
+ if (!closeLineSmoothInitedRef.current) {
2796
+ closeLineSmoothRef.current = rawLive.close;
2797
+ closeLineSmoothInitedRef.current = true;
2798
+ } else {
2799
+ closeLineSmoothRef.current = lerp(closeLineSmoothRef.current, rawLive.close, CLOSE_LINE_LERP_SPEED, pausedDt);
2800
+ const gap = Math.abs(closeLineSmoothRef.current - rawLive.close);
2801
+ const range = displayMaxRef.current - displayMinRef.current || 1;
2802
+ if (gap < range * 5e-4) closeLineSmoothRef.current = rawLive.close;
2803
+ }
2804
+ } else if (!useStash) {
2805
+ closeLineSmoothInitedRef.current = false;
2806
+ }
2807
+ if (rawLive) {
2808
+ if (!lineSmoothInitedRef.current) {
2809
+ lineSmoothCloseRef.current = rawLive.close;
2810
+ lineSmoothInitedRef.current = true;
2811
+ } else {
2812
+ const valGap = Math.abs(rawLive.close - lineSmoothCloseRef.current);
2813
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2814
+ const gapRatio = Math.min(valGap / prevRange, 1);
2815
+ const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
2816
+ lineSmoothCloseRef.current = lerp(lineSmoothCloseRef.current, rawLive.close, adaptiveSpeed, pausedDt);
2817
+ if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineSmoothCloseRef.current = rawLive.close;
2818
+ }
2819
+ } else if (!useStash) {
2820
+ lineSmoothInitedRef.current = false;
2821
+ }
2822
+ if (effectiveLineValue !== void 0 && hasTickData) {
2823
+ if (!lineTickSmoothInitedRef.current) {
2824
+ lineTickSmoothRef.current = effectiveLineValue;
2825
+ lineTickSmoothInitedRef.current = true;
2826
+ } else {
2827
+ const valGap = Math.abs(effectiveLineValue - lineTickSmoothRef.current);
2828
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2829
+ const gapRatio = Math.min(valGap / prevRange, 1);
2830
+ const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
2831
+ lineTickSmoothRef.current = lerp(lineTickSmoothRef.current, effectiveLineValue, adaptiveSpeed, pausedDt);
2832
+ if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineTickSmoothRef.current = effectiveLineValue;
2833
+ }
2834
+ } else if (!useStash) {
2835
+ lineTickSmoothInitedRef.current = false;
2836
+ }
2837
+ const visible = [];
2838
+ for (const c of effectiveCandles) {
2839
+ if (c.time + candleWidthSecs >= leftEdge && c.time <= rightEdge) visible.push(c);
2840
+ }
2841
+ if (smoothLive && smoothLive.time + displayCandleWidth >= leftEdge && smoothLive.time <= rightEdge) {
2842
+ visible.push(smoothLive);
2843
+ }
2844
+ let oldVisible = [];
2845
+ if (morphT >= 0 && cwt.oldCandles.length > 0) {
2846
+ for (const c of cwt.oldCandles) {
2847
+ if (c.time + cwt.oldWidth >= leftEdge && c.time <= rightEdge) oldVisible.push(c);
2848
+ }
2849
+ }
2850
+ if (hasData) {
2851
+ lastCandlesRef.current = visible;
2852
+ lastLiveRef.current = smoothLive ?? null;
2853
+ }
2854
+ const effectiveVisible = useStash ? lastCandlesRef.current : visible;
2855
+ const effectiveLive = useStash ? lastLiveRef.current ?? void 0 : smoothLive;
2856
+ const chartW = w - pad.left - pad.right;
2857
+ const computed = effectiveVisible.length > 0 ? computeCandleRange(effectiveVisible) : { min: displayMinRef.current, max: displayMaxRef.current };
2858
+ const rangeResult = updateCandleRange(
2859
+ computed,
2860
+ rangeInitedRef.current,
2861
+ displayMinRef.current,
2862
+ displayMaxRef.current,
2863
+ isWindowTransitioning,
2864
+ windowTransProgress,
2865
+ transition,
2866
+ chartH,
2867
+ pausedDt
2868
+ );
2869
+ if (morphT >= 0) {
2870
+ rangeResult.displayMin = cwt.rangeFromMin + (cwt.rangeToMin - cwt.rangeFromMin) * morphT;
2871
+ rangeResult.displayMax = cwt.rangeFromMax + (cwt.rangeToMax - cwt.rangeFromMax) * morphT;
2872
+ rangeResult.minVal = rangeResult.displayMin;
2873
+ rangeResult.maxVal = rangeResult.displayMax;
2874
+ rangeResult.valRange = rangeResult.displayMax - rangeResult.displayMin || 1e-3;
2875
+ }
2876
+ rangeInitedRef.current = rangeResult.rangeInited;
2877
+ displayMinRef.current = rangeResult.displayMin;
2878
+ displayMaxRef.current = rangeResult.displayMax;
2879
+ const { minVal, maxVal, valRange } = rangeResult;
2880
+ const layout = {
2881
+ w,
2882
+ h,
2883
+ pad,
2884
+ chartW,
2885
+ chartH,
2886
+ leftEdge,
2887
+ rightEdge,
2888
+ minVal,
2889
+ maxVal,
2890
+ valRange,
2891
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
2892
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
2893
+ };
2894
+ const hoverPx = hoverXRef.current;
2895
+ let hoveredCandle = null;
2896
+ let isActiveHover = false;
2897
+ if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
2898
+ hoveredCandle = candleAtX(effectiveVisible, hoverPx, displayCandleWidth, layout);
2899
+ if (hoveredCandle) isActiveHover = true;
2900
+ }
2901
+ const scrubTarget = isActiveHover ? 1 : 0;
2902
+ scrubAmountRef.current = lerp(scrubAmountRef.current, scrubTarget, 0.12, dt);
2903
+ if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
2904
+ if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
2905
+ const scrubAmount = scrubAmountRef.current;
2906
+ let drawHoverX = hoverPx;
2907
+ let drawHoverTime = 0;
2908
+ let drawHoverCandle = hoveredCandle;
2909
+ if (!isActiveHover && scrubAmount > 0 && lastHoverRef.current) {
2910
+ drawHoverX = lastHoverRef.current.x;
2911
+ drawHoverTime = lastHoverRef.current.time;
2912
+ drawHoverCandle = candleAtX(effectiveVisible, lastHoverRef.current.x, displayCandleWidth, layout);
2913
+ } else if (isActiveHover && hoverPx !== null) {
2914
+ drawHoverTime = layout.leftEdge + (hoverPx - pad.left) / chartW * (layout.rightEdge - layout.leftEdge);
2915
+ lastHoverRef.current = { x: hoverPx, value: hoveredCandle?.close ?? 0, time: drawHoverTime };
2916
+ }
2917
+ let drawCandles = effectiveVisible;
2918
+ let drawOldCandles = oldVisible;
2919
+ let drawLive = effectiveLive;
2920
+ if (lineModeProg > 0.01 && drawLive && lineSmoothInitedRef.current) {
2921
+ const blended = drawLive.close + (lineSmoothCloseRef.current - drawLive.close) * lineModeProg;
2922
+ drawLive = { ...drawLive, close: blended };
2923
+ const li = drawCandles.length - 1;
2924
+ if (li >= 0 && drawCandles[li].time === drawLive.time) {
2925
+ drawCandles = drawCandles.slice();
2926
+ drawCandles[li] = { ...drawCandles[li], close: blended };
2927
+ }
2928
+ }
2929
+ if (lineModeProg > 0.01 && lineModeProg < 0.99) {
2930
+ const collapseOHLC = (c) => {
2931
+ const inv = 1 - lineModeProg;
2932
+ return {
2933
+ time: c.time,
2934
+ open: c.close + (c.open - c.close) * inv,
2935
+ high: c.close + (c.high - c.close) * inv,
2936
+ low: c.close + (c.low - c.close) * inv,
2937
+ close: c.close
2938
+ };
2939
+ };
2940
+ drawCandles = drawCandles.map(collapseOHLC);
2941
+ if (drawOldCandles.length > 0) drawOldCandles = drawOldCandles.map(collapseOHLC);
2942
+ if (drawLive) drawLive = collapseOHLC(drawLive);
2943
+ }
2944
+ let lineVisible;
2945
+ let lineSmoothValue;
2946
+ if (effectiveLineData && effectiveLineData.length > 0 && (lineDensityProg > 0.01 || lineModeProg > 0.05)) {
2947
+ const closeRefs = [];
2948
+ for (const c of drawCandles) {
2949
+ closeRefs.push({ t: c.time + displayCandleWidth / 2, v: c.close });
2950
+ }
2951
+ if (drawLive) closeRefs.push({ t: now, v: drawLive.close });
2952
+ lineVisible = [];
2953
+ let refIdx = 0;
2954
+ for (const pt of effectiveLineData) {
2955
+ if (pt.time < leftEdge || pt.time > rightEdge) continue;
2956
+ while (refIdx < closeRefs.length - 2 && closeRefs[refIdx + 1].t < pt.time) refIdx++;
2957
+ let interpClose;
2958
+ if (closeRefs.length === 0) {
2959
+ interpClose = pt.value;
2960
+ } else if (closeRefs.length === 1 || pt.time <= closeRefs[0].t) {
2961
+ interpClose = closeRefs[0].v;
2962
+ } else if (refIdx >= closeRefs.length - 1) {
2963
+ interpClose = closeRefs[closeRefs.length - 1].v;
2964
+ } else {
2965
+ const a = closeRefs[refIdx];
2966
+ const b = closeRefs[refIdx + 1];
2967
+ const span = b.t - a.t;
2968
+ const frac = span > 0 ? Math.max(0, Math.min(1, (pt.time - a.t) / span)) : 0;
2969
+ interpClose = a.v + (b.v - a.v) * frac;
2970
+ }
2971
+ const blended = interpClose + (pt.value - interpClose) * lineDensityProg;
2972
+ lineVisible.push({ time: pt.time, value: blended });
2973
+ }
2974
+ const smoothTick = lineTickSmoothInitedRef.current ? lineTickSmoothRef.current : effectiveLineValue ?? effectiveLineData[effectiveLineData.length - 1].value;
2975
+ lineSmoothValue = lineSmoothCloseRef.current + (smoothTick - lineSmoothCloseRef.current) * lineDensityProg;
2976
+ } else {
2977
+ lineVisible = drawCandles.map((c) => ({
2978
+ time: c.time + displayCandleWidth / 2,
2979
+ value: c.close
2980
+ }));
2981
+ lineSmoothValue = lineSmoothInitedRef.current ? lineSmoothCloseRef.current : drawLive?.close ?? drawCandles[drawCandles.length - 1]?.close ?? 0;
2982
+ }
2983
+ if (chartReveal < 1 && lineVisible.length >= 2) {
2984
+ const firstTime = lineVisible[0].time;
2985
+ const windowSpan = rightEdge - leftEdge;
2986
+ if (firstTime - leftEdge > windowSpan * 0.05) {
2987
+ const firstVal = lineVisible[0].value;
2988
+ const step = windowSpan / 32;
2989
+ const padded = [];
2990
+ for (let t = leftEdge; t < firstTime - step * 0.5; t += step) {
2991
+ padded.push({ time: t, value: firstVal });
2992
+ }
2993
+ lineVisible = [...padded, ...lineVisible];
2994
+ }
2995
+ }
2996
+ drawCandleFrame(ctx, layout, cfg.palette, {
2997
+ candles: drawCandles,
2998
+ displayCandleWidth,
2999
+ oldCandles: drawOldCandles,
3000
+ oldWidth: cwt.oldWidth,
3001
+ morphT,
3002
+ liveCandle: drawLive,
3003
+ closePriceCandle: closeLineSmoothInitedRef.current && rawLive ? { ...rawLive, close: closeLineSmoothRef.current } : rawLive,
3004
+ liveTime: effectiveLive?.time ?? -1,
3005
+ liveBirthAlpha: liveBirthAlphaRef.current,
3006
+ liveBullBlend: liveBullRef.current,
3007
+ lineModeProg,
3008
+ chartReveal,
3009
+ now_ms,
3010
+ now,
3011
+ pauseProgress,
3012
+ showGrid: cfg.showGrid,
3013
+ scrubAmount,
3014
+ hoverX: drawHoverX,
3015
+ hoverValue: drawHoverCandle?.close ?? null,
3016
+ hoverTime: drawHoverTime,
3017
+ hoveredCandle: drawHoverCandle,
3018
+ formatValue: cfg.formatValue,
3019
+ formatTime: cfg.formatTime,
3020
+ gridState: gridStateRef.current,
3021
+ timeAxisState: timeAxisStateRef.current,
3022
+ dt: pausedDt,
3023
+ targetWindowSecs: cfg.windowSecs,
3024
+ tooltipY: cfg.tooltipY,
3025
+ tooltipOutline: cfg.tooltipOutline,
3026
+ lineVisible,
3027
+ lineSmoothValue,
3028
+ emptyText: cfg.emptyText,
3029
+ loadingAlpha,
3030
+ // Show empty overlay when not loading AND loadingAlpha has fully
3031
+ // decayed. This prevents the gradient gap from flashing during
3032
+ // loading→live (where loadingAlpha starts at ~1), while still
3033
+ // allowing smooth fade-out during empty→live (loadingAlpha is 0).
3034
+ showEmptyOverlay: !(cfg.loading ?? false) && loadingAlpha < 0.01
3035
+ });
3036
+ if (badgeRef.current) {
3037
+ if (lineModeProg > 0.5 && cfg.showBadge) {
3038
+ const momentum = detectMomentum(lineVisible);
3039
+ badgeYRef.current = updateBadgeDOM(
3040
+ badgeRef.current,
3041
+ cfg,
3042
+ lineSmoothValue,
3043
+ layout,
3044
+ momentum,
3045
+ badgeYRef.current,
3046
+ badgeColorRef.current,
3047
+ isWindowTransitioning,
3048
+ noMotion,
3049
+ ctx,
3050
+ pausedDt,
3051
+ chartReveal
3052
+ );
3053
+ const badgeFade = (lineModeProg - 0.5) * 2;
3054
+ if (badgeRef.current.container.style.display !== "none") {
3055
+ const base = badgeRef.current.container.style.opacity ? parseFloat(badgeRef.current.container.style.opacity) : 1;
3056
+ badgeRef.current.container.style.opacity = String(
3057
+ base * badgeFade * (1 - pauseProgress)
3058
+ );
3059
+ }
3060
+ } else {
3061
+ badgeRef.current.container.style.display = "none";
3062
+ }
3063
+ }
3064
+ } else if (cfg.isMultiSeries && cfg.multiSeries && cfg.multiSeries.length > 0 || useMultiStash) {
3065
+ const effectiveMultiSeries = useMultiStash ? lastMultiSeriesRef.current : cfg.multiSeries;
3066
+ let labelReserve = 0;
3067
+ if (effectiveMultiSeries.some((s) => s.label)) {
3068
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
3069
+ let maxLabelW = 0;
3070
+ for (const s of effectiveMultiSeries) {
3071
+ if (s.label) {
3072
+ const lw = ctx.measureText(s.label).width;
3073
+ if (lw > maxLabelW) maxLabelW = lw;
3074
+ }
3075
+ }
3076
+ labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
3077
+ }
3078
+ const chartW = w - pad.left - pad.right - labelReserve;
3079
+ const buffer = WINDOW_BUFFER;
3080
+ if (!useMultiStash) {
3081
+ const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
3082
+ for (const key of displayValuesRef.current.keys()) {
3083
+ if (!currentIds.has(key)) displayValuesRef.current.delete(key);
3084
+ }
3085
+ }
3086
+ const firstSeries = effectiveMultiSeries[0];
3087
+ const transition = windowTransitionRef.current;
3088
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
3089
+ const now = useMultiStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
3090
+ const smoothValues = /* @__PURE__ */ new Map();
3091
+ for (const s of effectiveMultiSeries) {
3092
+ let dv = displayValuesRef.current.get(s.id);
3093
+ if (dv === void 0) dv = s.value;
3094
+ if (!useMultiStash) {
3095
+ const adaptiveSpeed2 = computeAdaptiveSpeed(
3096
+ s.value,
3097
+ dv,
3098
+ displayMinRef.current,
3099
+ displayMaxRef.current,
3100
+ cfg.lerpSpeed,
3101
+ noMotion
3102
+ );
3103
+ dv = lerp(dv, s.value, adaptiveSpeed2, pausedDt);
3104
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
3105
+ if (Math.abs(dv - s.value) < prevRange * VALUE_SNAP_THRESHOLD) dv = s.value;
3106
+ displayValuesRef.current.set(s.id, dv);
3107
+ }
3108
+ smoothValues.set(s.id, dv);
3109
+ }
3110
+ const hiddenIds = cfg.hiddenSeriesIds;
3111
+ const seriesAlphas = seriesAlphaRef.current;
3112
+ for (const s of effectiveMultiSeries) {
3113
+ let alpha = seriesAlphas.get(s.id) ?? 1;
3114
+ const target = hiddenIds?.has(s.id) ? 0 : 1;
3115
+ alpha = noMotion ? target : lerp(alpha, target, SERIES_TOGGLE_SPEED, pausedDt);
3116
+ if (alpha < 0.01) alpha = 0;
3117
+ if (alpha > 0.99) alpha = 1;
3118
+ seriesAlphas.set(s.id, alpha);
3119
+ }
3120
+ const firstData = pausedMultiDataRef.current?.get(firstSeries.id)?.data ?? firstSeries.data;
3121
+ const windowResult = updateWindowTransition(
3122
+ cfg,
3123
+ transition,
3124
+ displayWindowRef.current,
3125
+ displayMinRef.current,
3126
+ displayMaxRef.current,
3127
+ noMotion,
3128
+ now_ms,
3129
+ now,
3130
+ firstData,
3131
+ smoothValues.get(firstSeries.id) ?? firstSeries.value,
3132
+ buffer
3133
+ );
3134
+ if (transition.startMs > 0 && effectiveMultiSeries.length > 1) {
3135
+ const targetRightEdge = now + cfg.windowSecs * buffer;
3136
+ const targetLeftEdge = targetRightEdge - cfg.windowSecs;
3137
+ let unionMin = Infinity;
3138
+ let unionMax = -Infinity;
3139
+ for (const s of effectiveMultiSeries) {
3140
+ const sData = pausedMultiDataRef.current?.get(s.id)?.data ?? s.data;
3141
+ const sv = smoothValues.get(s.id) ?? s.value;
3142
+ const targetVisible = [];
3143
+ for (const p of sData) {
3144
+ if (p.time >= targetLeftEdge - 2 && p.time <= targetRightEdge) targetVisible.push(p);
3145
+ }
3146
+ if (targetVisible.length > 0) {
3147
+ const range = computeRange(targetVisible, sv, cfg.referenceLine?.value, cfg.exaggerate);
3148
+ if (range.min < unionMin) unionMin = range.min;
3149
+ if (range.max > unionMax) unionMax = range.max;
3150
+ }
3151
+ }
3152
+ if (isFinite(unionMin) && isFinite(unionMax)) {
3153
+ transition.rangeToMin = unionMin;
3154
+ transition.rangeToMax = unionMax;
3155
+ }
3156
+ }
3157
+ displayWindowRef.current = windowResult.windowSecs;
3158
+ const windowSecs = windowResult.windowSecs;
3159
+ const windowTransProgress = windowResult.windowTransProgress;
3160
+ const isWindowTransitioning = transition.startMs > 0;
3161
+ const rightEdge = now + windowSecs * buffer;
3162
+ const leftEdge = rightEdge - windowSecs;
3163
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
3164
+ const seriesEntries = [];
3165
+ let globalMin = Infinity;
3166
+ let globalMax = -Infinity;
3167
+ for (const s of effectiveMultiSeries) {
3168
+ const snap = pausedMultiDataRef.current?.get(s.id);
3169
+ const seriesData = snap?.data ?? s.data;
3170
+ const visible = [];
3171
+ for (const p of seriesData) {
3172
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) visible.push(p);
3173
+ }
3174
+ const sv = smoothValues.get(s.id) ?? s.value;
3175
+ const alpha = seriesAlphas.get(s.id) ?? 1;
3176
+ if (visible.length >= 2) {
3177
+ if (alpha > 0.01) {
3178
+ const range = computeRange(visible, sv, cfg.referenceLine?.value, cfg.exaggerate);
3179
+ if (range.min < globalMin) globalMin = range.min;
3180
+ if (range.max > globalMax) globalMax = range.max;
3181
+ }
3182
+ seriesEntries.push({ visible, smoothValue: sv, palette: s.palette, label: s.label, alpha });
3183
+ }
3184
+ }
3185
+ if (seriesEntries.length === 0) {
3186
+ if (loadingAlpha > 0.01) {
3187
+ drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, cfg.palette.gridLabel);
3188
+ }
3189
+ if (1 - loadingAlpha > 0.01) {
3190
+ drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
3191
+ }
3192
+ ctx.save();
3193
+ ctx.globalCompositeOperation = "destination-out";
3194
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
3195
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
3196
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
3197
+ ctx.fillStyle = fadeGrad;
3198
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
3199
+ ctx.restore();
3200
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
3201
+ rafRef.current = requestAnimationFrame(draw);
3202
+ return;
3203
+ }
3204
+ const computedRange = { min: isFinite(globalMin) ? globalMin : 0, max: isFinite(globalMax) ? globalMax : 1 };
3205
+ const adaptiveSpeed = cfg.lerpSpeed + ADAPTIVE_SPEED_BOOST * 0.5;
3206
+ const rangeResult = updateRange(
3207
+ computedRange,
3208
+ rangeInitedRef.current,
3209
+ targetMinRef.current,
3210
+ targetMaxRef.current,
3211
+ displayMinRef.current,
3212
+ displayMaxRef.current,
3213
+ isWindowTransitioning,
3214
+ windowTransProgress,
3215
+ transition,
3216
+ adaptiveSpeed,
3217
+ chartH,
3218
+ pausedDt
3219
+ );
3220
+ rangeInitedRef.current = rangeResult.rangeInited;
3221
+ targetMinRef.current = rangeResult.targetMin;
3222
+ targetMaxRef.current = rangeResult.targetMax;
3223
+ displayMinRef.current = rangeResult.displayMin;
3224
+ displayMaxRef.current = rangeResult.displayMax;
3225
+ const { minVal, maxVal, valRange } = rangeResult;
3226
+ const layout = {
3227
+ w,
3228
+ h,
3229
+ pad,
3230
+ chartW,
3231
+ chartH,
3232
+ leftEdge,
3233
+ rightEdge,
3234
+ minVal,
3235
+ maxVal,
3236
+ valRange,
3237
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
3238
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
3239
+ };
3240
+ const hoverPx = hoverXRef.current;
3241
+ let drawHoverX = null;
3242
+ let drawHoverTime = null;
3243
+ let isActiveHover = false;
3244
+ let hoverEntries = [];
3245
+ if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
3246
+ const maxHoverX = layout.toX(now);
3247
+ const clampedX = Math.min(hoverPx, maxHoverX);
3248
+ const t = leftEdge + (clampedX - pad.left) / chartW * (rightEdge - leftEdge);
3249
+ drawHoverX = clampedX;
3250
+ drawHoverTime = t;
3251
+ isActiveHover = true;
3252
+ for (const entry of seriesEntries) {
3253
+ if ((entry.alpha ?? 1) < 0.5) continue;
3254
+ const v = interpolateAtTime(entry.visible, t);
3255
+ if (v !== null) {
3256
+ hoverEntries.push({ color: entry.palette.line, label: entry.label ?? "", value: v });
3257
+ }
3258
+ }
3259
+ lastHoverRef.current = { x: clampedX, value: hoverEntries[0]?.value ?? 0, time: t };
3260
+ lastHoverEntriesRef.current = hoverEntries;
3261
+ cfg.onHover?.({ time: t, value: hoverEntries[0]?.value ?? 0, x: clampedX, y: layout.toY(hoverEntries[0]?.value ?? 0) });
3262
+ }
3263
+ const scrubTarget = isActiveHover ? 1 : 0;
3264
+ if (noMotion) {
3265
+ scrubAmountRef.current = scrubTarget;
3266
+ } else {
3267
+ scrubAmountRef.current += (scrubTarget - scrubAmountRef.current) * SCRUB_LERP_SPEED;
3268
+ if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
3269
+ if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
3270
+ }
3271
+ if (!isActiveHover && scrubAmountRef.current > 0 && lastHoverRef.current) {
3272
+ drawHoverX = lastHoverRef.current.x;
3273
+ drawHoverTime = lastHoverRef.current.time;
3274
+ hoverEntries = lastHoverEntriesRef.current;
3275
+ }
3276
+ drawMultiFrame(ctx, layout, {
3277
+ series: seriesEntries,
3278
+ now,
3279
+ showGrid: cfg.showGrid,
3280
+ showPulse: cfg.showPulse,
3281
+ referenceLine: cfg.referenceLine,
3282
+ hoverX: drawHoverX,
3283
+ hoverTime: drawHoverTime,
3284
+ hoverEntries,
3285
+ scrubAmount: scrubAmountRef.current,
3286
+ windowSecs,
3287
+ formatValue: cfg.formatValue,
3288
+ formatTime: cfg.formatTime,
3289
+ gridState: gridStateRef.current,
3290
+ timeAxisState: timeAxisStateRef.current,
3291
+ dt,
3292
+ targetWindowSecs: cfg.windowSecs,
3293
+ tooltipY: cfg.tooltipY,
3294
+ tooltipOutline: cfg.tooltipOutline,
3295
+ chartReveal,
3296
+ pauseProgress,
3297
+ now_ms,
3298
+ primaryPalette: cfg.palette
3299
+ });
3300
+ const bgAlpha = 1 - chartReveal;
3301
+ if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
3302
+ const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
3303
+ if (bgEmptyAlpha > 0.01) {
3304
+ drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
1705
3305
  }
1706
3306
  }
1707
- }
1708
- const smoothValue = displayValueRef.current;
1709
- const chartW = w - pad.left - pad.right;
1710
- const needsArrowRoom = cfg.showMomentum;
1711
- const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
1712
- const transition = windowTransitionRef.current;
1713
- if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
1714
- const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
1715
- const windowResult = updateWindowTransition(
1716
- cfg,
1717
- transition,
1718
- displayWindowRef.current,
1719
- displayMinRef.current,
1720
- displayMaxRef.current,
1721
- noMotion,
1722
- now_ms,
1723
- now,
1724
- effectivePoints,
1725
- smoothValue,
1726
- buffer
1727
- );
1728
- displayWindowRef.current = windowResult.windowSecs;
1729
- const windowSecs = windowResult.windowSecs;
1730
- const windowTransProgress = windowResult.windowTransProgress;
1731
- const rightEdge = now + windowSecs * buffer;
1732
- const leftEdge = rightEdge - windowSecs;
1733
- const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
1734
- const visible = [];
1735
- for (const p of effectivePoints) {
1736
- if (p.time >= leftEdge - 2 && p.time <= filterRight) {
1737
- visible.push(p);
1738
- }
1739
- }
1740
- if (visible.length < 2) {
1741
3307
  if (badgeRef.current) badgeRef.current.container.style.display = "none";
1742
- rafRef.current = requestAnimationFrame(draw);
1743
- return;
1744
- }
1745
- const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
1746
- const isWindowTransitioning = transition.startMs > 0;
1747
- const rangeResult = updateRange(
1748
- computedRange,
1749
- rangeInitedRef.current,
1750
- targetMinRef.current,
1751
- targetMaxRef.current,
1752
- displayMinRef.current,
1753
- displayMaxRef.current,
1754
- isWindowTransitioning,
1755
- windowTransProgress,
1756
- transition,
1757
- adaptiveSpeed,
1758
- chartH,
1759
- pausedDt
1760
- );
1761
- rangeInitedRef.current = rangeResult.rangeInited;
1762
- targetMinRef.current = rangeResult.targetMin;
1763
- targetMaxRef.current = rangeResult.targetMax;
1764
- displayMinRef.current = rangeResult.displayMin;
1765
- displayMaxRef.current = rangeResult.displayMax;
1766
- const { minVal, maxVal, valRange } = rangeResult;
1767
- const layout = {
1768
- w,
1769
- h,
1770
- pad,
1771
- chartW,
1772
- chartH,
1773
- leftEdge,
1774
- rightEdge,
1775
- minVal,
1776
- maxVal,
1777
- valRange,
1778
- toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
1779
- toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
1780
- };
1781
- const momentum = cfg.momentumOverride ?? detectMomentum(visible);
1782
- const hoverResult = updateHoverState(
1783
- hoverXRef.current,
1784
- pad,
1785
- w,
1786
- layout,
1787
- now,
1788
- visible,
1789
- scrubAmountRef.current,
1790
- lastHoverRef.current,
1791
- cfg,
1792
- noMotion,
1793
- leftEdge,
1794
- rightEdge,
1795
- chartW,
1796
- dt
1797
- );
1798
- scrubAmountRef.current = hoverResult.scrubAmount;
1799
- lastHoverRef.current = hoverResult.lastHover;
1800
- const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
1801
- const lookback = Math.min(5, visible.length - 1);
1802
- const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
1803
- const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
1804
- drawFrame(ctx, layout, cfg.palette, {
1805
- visible,
1806
- smoothValue,
1807
- now,
1808
- momentum,
1809
- arrowState: arrowStateRef.current,
1810
- showGrid: cfg.showGrid,
1811
- showMomentum: cfg.showMomentum,
1812
- showPulse: cfg.showPulse,
1813
- showFill: cfg.showFill,
1814
- referenceLine: cfg.referenceLine,
1815
- hoverX: drawHoverX,
1816
- hoverValue: drawHoverValue,
1817
- hoverTime: drawHoverTime,
1818
- scrubAmount: scrubAmountRef.current,
1819
- windowSecs,
1820
- formatValue: cfg.formatValue,
1821
- formatTime: cfg.formatTime,
1822
- gridState: gridStateRef.current,
1823
- timeAxisState: timeAxisStateRef.current,
1824
- dt,
1825
- targetWindowSecs: cfg.windowSecs,
1826
- tooltipY: cfg.tooltipY,
1827
- tooltipOutline: cfg.tooltipOutline,
1828
- orderbookData: cfg.orderbookData,
1829
- orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
1830
- particleState: cfg.degenOptions ? particleStateRef.current : void 0,
1831
- particleOptions: cfg.degenOptions,
1832
- swingMagnitude,
1833
- shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
1834
- chartReveal,
1835
- pauseProgress,
1836
- now_ms
1837
- });
1838
- const bgAlpha = 1 - chartReveal;
1839
- if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
1840
- const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
1841
- if (bgEmptyAlpha > 0.01) {
1842
- drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
3308
+ } else {
3309
+ const effectivePoints = useStash ? lastDataRef.current : points;
3310
+ const adaptiveSpeed = computeAdaptiveSpeed(
3311
+ cfg.value,
3312
+ displayValueRef.current,
3313
+ displayMinRef.current,
3314
+ displayMaxRef.current,
3315
+ cfg.lerpSpeed,
3316
+ noMotion
3317
+ );
3318
+ if (!useStash) {
3319
+ displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
3320
+ if (pauseProgress < 0.5) {
3321
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
3322
+ if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
3323
+ displayValueRef.current = cfg.value;
3324
+ }
3325
+ }
1843
3326
  }
1844
- }
1845
- const badge = badgeRef.current;
1846
- if (badge) {
1847
- badgeYRef.current = updateBadgeDOM(
1848
- badge,
3327
+ const smoothValue = displayValueRef.current;
3328
+ const chartW = w - pad.left - pad.right;
3329
+ const needsArrowRoom = cfg.showMomentum;
3330
+ const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
3331
+ const transition = windowTransitionRef.current;
3332
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
3333
+ const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
3334
+ const windowResult = updateWindowTransition(
1849
3335
  cfg,
3336
+ transition,
3337
+ displayWindowRef.current,
3338
+ displayMinRef.current,
3339
+ displayMaxRef.current,
3340
+ noMotion,
3341
+ now_ms,
3342
+ now,
3343
+ effectivePoints,
1850
3344
  smoothValue,
1851
- layout,
1852
- momentum,
1853
- badgeYRef.current,
1854
- badgeColorRef.current,
3345
+ buffer
3346
+ );
3347
+ displayWindowRef.current = windowResult.windowSecs;
3348
+ const windowSecs = windowResult.windowSecs;
3349
+ const windowTransProgress = windowResult.windowTransProgress;
3350
+ const rightEdge = now + windowSecs * buffer;
3351
+ const leftEdge = rightEdge - windowSecs;
3352
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
3353
+ const visible = [];
3354
+ for (const p of effectivePoints) {
3355
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) {
3356
+ visible.push(p);
3357
+ }
3358
+ }
3359
+ if (visible.length < 2) {
3360
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
3361
+ rafRef.current = requestAnimationFrame(draw);
3362
+ return;
3363
+ }
3364
+ const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
3365
+ const isWindowTransitioning = transition.startMs > 0;
3366
+ const rangeResult = updateRange(
3367
+ computedRange,
3368
+ rangeInitedRef.current,
3369
+ targetMinRef.current,
3370
+ targetMaxRef.current,
3371
+ displayMinRef.current,
3372
+ displayMaxRef.current,
1855
3373
  isWindowTransitioning,
3374
+ windowTransProgress,
3375
+ transition,
3376
+ adaptiveSpeed,
3377
+ chartH,
3378
+ pausedDt
3379
+ );
3380
+ rangeInitedRef.current = rangeResult.rangeInited;
3381
+ targetMinRef.current = rangeResult.targetMin;
3382
+ targetMaxRef.current = rangeResult.targetMax;
3383
+ displayMinRef.current = rangeResult.displayMin;
3384
+ displayMaxRef.current = rangeResult.displayMax;
3385
+ const { minVal, maxVal, valRange } = rangeResult;
3386
+ const layout = {
3387
+ w,
3388
+ h,
3389
+ pad,
3390
+ chartW,
3391
+ chartH,
3392
+ leftEdge,
3393
+ rightEdge,
3394
+ minVal,
3395
+ maxVal,
3396
+ valRange,
3397
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
3398
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
3399
+ };
3400
+ const momentum = cfg.momentumOverride ?? detectMomentum(visible);
3401
+ const hoverResult = updateHoverState(
3402
+ hoverXRef.current,
3403
+ pad,
3404
+ w,
3405
+ layout,
3406
+ now,
3407
+ visible,
3408
+ scrubAmountRef.current,
3409
+ lastHoverRef.current,
3410
+ cfg,
1856
3411
  noMotion,
1857
- ctx,
1858
- pausedDt,
1859
- chartReveal
3412
+ leftEdge,
3413
+ rightEdge,
3414
+ chartW,
3415
+ dt
1860
3416
  );
1861
- if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
1862
- const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
1863
- badge.container.style.opacity = String(base * (1 - pauseProgress));
3417
+ scrubAmountRef.current = hoverResult.scrubAmount;
3418
+ lastHoverRef.current = hoverResult.lastHover;
3419
+ const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
3420
+ const lookback = Math.min(5, visible.length - 1);
3421
+ const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
3422
+ const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
3423
+ drawFrame(ctx, layout, cfg.palette, {
3424
+ visible,
3425
+ smoothValue,
3426
+ now,
3427
+ momentum,
3428
+ arrowState: arrowStateRef.current,
3429
+ showGrid: cfg.showGrid,
3430
+ showMomentum: cfg.showMomentum,
3431
+ showPulse: cfg.showPulse,
3432
+ showFill: cfg.showFill,
3433
+ referenceLine: cfg.referenceLine,
3434
+ hoverX: drawHoverX,
3435
+ hoverValue: drawHoverValue,
3436
+ hoverTime: drawHoverTime,
3437
+ scrubAmount: scrubAmountRef.current,
3438
+ windowSecs,
3439
+ formatValue: cfg.formatValue,
3440
+ formatTime: cfg.formatTime,
3441
+ gridState: gridStateRef.current,
3442
+ timeAxisState: timeAxisStateRef.current,
3443
+ dt,
3444
+ targetWindowSecs: cfg.windowSecs,
3445
+ tooltipY: cfg.tooltipY,
3446
+ tooltipOutline: cfg.tooltipOutline,
3447
+ orderbookData: cfg.orderbookData,
3448
+ orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
3449
+ particleState: cfg.degenOptions ? particleStateRef.current : void 0,
3450
+ particleOptions: cfg.degenOptions,
3451
+ swingMagnitude,
3452
+ shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
3453
+ chartReveal,
3454
+ pauseProgress,
3455
+ now_ms
3456
+ });
3457
+ const bgAlpha = 1 - chartReveal;
3458
+ if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
3459
+ const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
3460
+ if (bgEmptyAlpha > 0.01) {
3461
+ drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
3462
+ }
1864
3463
  }
1865
- }
1866
- const valEl = cfg.valueDisplayRef?.current;
1867
- if (valEl) {
1868
- const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
1869
- valEl.textContent = cfg.formatValue(displayVal);
1870
- if (cfg.valueMomentumColor) {
1871
- const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
1872
- if (mc) valEl.style.color = mc;
1873
- else valEl.style.removeProperty("color");
3464
+ const badge = badgeRef.current;
3465
+ if (badge) {
3466
+ badgeYRef.current = updateBadgeDOM(
3467
+ badge,
3468
+ cfg,
3469
+ smoothValue,
3470
+ layout,
3471
+ momentum,
3472
+ badgeYRef.current,
3473
+ badgeColorRef.current,
3474
+ isWindowTransitioning,
3475
+ noMotion,
3476
+ ctx,
3477
+ pausedDt,
3478
+ chartReveal
3479
+ );
3480
+ if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
3481
+ const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
3482
+ badge.container.style.opacity = String(base * (1 - pauseProgress));
3483
+ }
3484
+ }
3485
+ const valEl = cfg.valueDisplayRef?.current;
3486
+ if (valEl) {
3487
+ const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
3488
+ valEl.textContent = cfg.formatValue(displayVal);
3489
+ if (cfg.valueMomentumColor) {
3490
+ const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
3491
+ if (mc) valEl.style.color = mc;
3492
+ else valEl.style.removeProperty("color");
3493
+ }
1874
3494
  }
1875
3495
  }
1876
3496
  rafRef.current = requestAnimationFrame(draw);
@@ -1894,6 +3514,7 @@ var defaultFormatTime = (t) => {
1894
3514
  function Liveline({
1895
3515
  data,
1896
3516
  value,
3517
+ series: seriesProp,
1897
3518
  theme = "dark",
1898
3519
  color = "#3b82f6",
1899
3520
  window: windowSecs = 30,
@@ -1925,6 +3546,16 @@ function Liveline({
1925
3546
  onHover,
1926
3547
  cursor = "crosshair",
1927
3548
  pulse = true,
3549
+ mode = "line",
3550
+ candles,
3551
+ candleWidth,
3552
+ liveCandle,
3553
+ lineMode,
3554
+ lineData,
3555
+ lineValue,
3556
+ onModeChange,
3557
+ onSeriesToggle,
3558
+ seriesToggleCompact = false,
1928
3559
  className,
1929
3560
  style
1930
3561
  }) {
@@ -1934,8 +3565,30 @@ function Liveline({
1934
3565
  const windowBarRef = (0, import_react2.useRef)(null);
1935
3566
  const windowBtnRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1936
3567
  const [indicatorStyle, setIndicatorStyle] = (0, import_react2.useState)(null);
3568
+ const modeBarRef = (0, import_react2.useRef)(null);
3569
+ const modeBtnRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3570
+ const [modeIndicatorStyle, setModeIndicatorStyle] = (0, import_react2.useState)(null);
3571
+ const [hiddenSeries, setHiddenSeries] = (0, import_react2.useState)(/* @__PURE__ */ new Set());
3572
+ const lastSeriesPropRef = (0, import_react2.useRef)(seriesProp);
3573
+ if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
1937
3574
  const palette = (0, import_react2.useMemo)(() => resolveTheme(color, theme), [color, theme]);
1938
3575
  const isDark = theme === "dark";
3576
+ const isMultiSeries = seriesProp != null && seriesProp.length > 0;
3577
+ const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
3578
+ const seriesPalettes = (0, import_react2.useMemo)(() => {
3579
+ if (!seriesProp || seriesProp.length === 0) return null;
3580
+ return resolveSeriesPalettes(seriesProp, theme);
3581
+ }, [seriesProp, theme]);
3582
+ const multiSeries = (0, import_react2.useMemo)(() => {
3583
+ if (!seriesProp || !seriesPalettes) return void 0;
3584
+ return seriesProp.map((s, i) => ({
3585
+ id: s.id,
3586
+ data: s.data,
3587
+ value: s.value,
3588
+ palette: seriesPalettes.get(s.id) ?? resolveTheme(s.color || SERIES_COLORS[i % SERIES_COLORS.length], theme),
3589
+ label: s.label
3590
+ }));
3591
+ }, [seriesProp, seriesPalettes, theme]);
1939
3592
  const showMomentum = momentum !== false;
1940
3593
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
1941
3594
  const pad = {
@@ -1963,6 +3616,36 @@ function Liveline({
1963
3616
  });
1964
3617
  }
1965
3618
  }, [activeWindowSecs, windows]);
3619
+ const activeMode = lineMode ? "line" : "candle";
3620
+ (0, import_react2.useLayoutEffect)(() => {
3621
+ if (!onModeChange) return;
3622
+ const btn = modeBtnRefs.current.get(activeMode);
3623
+ const bar = modeBarRef.current;
3624
+ if (btn && bar) {
3625
+ const barRect = bar.getBoundingClientRect();
3626
+ const btnRect = btn.getBoundingClientRect();
3627
+ setModeIndicatorStyle({
3628
+ left: btnRect.left - barRect.left,
3629
+ width: btnRect.width
3630
+ });
3631
+ }
3632
+ }, [activeMode, onModeChange]);
3633
+ const handleSeriesToggle = (0, import_react2.useCallback)((id) => {
3634
+ setHiddenSeries((prev) => {
3635
+ const next = new Set(prev);
3636
+ if (next.has(id)) {
3637
+ next.delete(id);
3638
+ onSeriesToggle?.(id, true);
3639
+ } else {
3640
+ const totalSeries = seriesProp?.length ?? 0;
3641
+ const visibleCount = totalSeries - next.size;
3642
+ if (visibleCount <= 1) return prev;
3643
+ next.add(id);
3644
+ onSeriesToggle?.(id, false);
3645
+ }
3646
+ return next;
3647
+ });
3648
+ }, [seriesProp?.length, onSeriesToggle]);
1966
3649
  const ws = windowStyle ?? "default";
1967
3650
  useLivelineEngine(canvasRef, containerRef, {
1968
3651
  data,
@@ -1971,10 +3654,10 @@ function Liveline({
1971
3654
  windowSecs: effectiveWindowSecs,
1972
3655
  lerpSpeed,
1973
3656
  showGrid: grid,
1974
- showBadge: badge,
1975
- showMomentum,
3657
+ showBadge: isMultiSeries ? false : badge,
3658
+ showMomentum: isMultiSeries ? false : showMomentum,
1976
3659
  momentumOverride,
1977
- showFill: fill,
3660
+ showFill: isMultiSeries ? false : fill,
1978
3661
  referenceLine,
1979
3662
  formatValue,
1980
3663
  formatTime,
@@ -1983,7 +3666,7 @@ function Liveline({
1983
3666
  showPulse: pulse,
1984
3667
  scrub,
1985
3668
  exaggerate,
1986
- degenOptions,
3669
+ degenOptions: isMultiSeries ? void 0 : degenOptions,
1987
3670
  badgeTail,
1988
3671
  badgeVariant,
1989
3672
  tooltipY,
@@ -1993,9 +3676,21 @@ function Liveline({
1993
3676
  orderbookData: orderbook,
1994
3677
  loading,
1995
3678
  paused,
1996
- emptyText
3679
+ emptyText,
3680
+ mode,
3681
+ candles,
3682
+ candleWidth,
3683
+ liveCandle,
3684
+ lineMode,
3685
+ lineData,
3686
+ lineValue,
3687
+ multiSeries,
3688
+ isMultiSeries,
3689
+ hiddenSeriesIds: hiddenSeries
1997
3690
  });
1998
3691
  const cursorStyle = scrub ? cursor : "default";
3692
+ const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
3693
+ const inactiveColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)";
1999
3694
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2000
3695
  showValue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2001
3696
  "span",
@@ -2015,68 +3710,244 @@ function Liveline({
2015
3710
  }
2016
3711
  }
2017
3712
  ),
2018
- windows && windows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2019
- "div",
2020
- {
2021
- ref: windowBarRef,
2022
- style: {
2023
- position: "relative",
2024
- display: "inline-flex",
2025
- gap: ws === "text" ? 4 : 2,
2026
- background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
2027
- borderRadius: ws === "rounded" ? 999 : 6,
2028
- padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
2029
- marginBottom: 6,
2030
- marginLeft: pad.left
2031
- },
2032
- children: [
2033
- ws !== "text" && indicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
2034
- position: "absolute",
2035
- top: ws === "rounded" ? 3 : 2,
2036
- left: indicatorStyle.left,
2037
- width: indicatorStyle.width,
2038
- height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
2039
- background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
2040
- borderRadius: ws === "rounded" ? 999 : 4,
2041
- transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
2042
- pointerEvents: "none"
2043
- } }),
2044
- windows.map((w) => {
2045
- const isActive = w.secs === activeWindowSecs;
2046
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3713
+ (windows && windows.length > 0 || onModeChange || showSeriesToggle) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
3714
+ windows && windows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3715
+ "div",
3716
+ {
3717
+ ref: windowBarRef,
3718
+ style: {
3719
+ position: "relative",
3720
+ display: "inline-flex",
3721
+ gap: ws === "text" ? 4 : 2,
3722
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3723
+ borderRadius: ws === "rounded" ? 999 : 6,
3724
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
3725
+ },
3726
+ children: [
3727
+ ws !== "text" && indicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
3728
+ position: "absolute",
3729
+ top: ws === "rounded" ? 3 : 2,
3730
+ left: indicatorStyle.left,
3731
+ width: indicatorStyle.width,
3732
+ height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
3733
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3734
+ borderRadius: ws === "rounded" ? 999 : 4,
3735
+ transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3736
+ pointerEvents: "none"
3737
+ } }),
3738
+ windows.map((w) => {
3739
+ const isActive = w.secs === activeWindowSecs;
3740
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3741
+ "button",
3742
+ {
3743
+ ref: (el) => {
3744
+ if (el) windowBtnRefs.current.set(w.secs, el);
3745
+ else windowBtnRefs.current.delete(w.secs);
3746
+ },
3747
+ onClick: () => {
3748
+ setActiveWindowSecs(w.secs);
3749
+ onWindowChange?.(w.secs);
3750
+ },
3751
+ style: {
3752
+ position: "relative",
3753
+ zIndex: 1,
3754
+ fontSize: 11,
3755
+ padding: ws === "text" ? "2px 6px" : "3px 10px",
3756
+ borderRadius: ws === "rounded" ? 999 : 4,
3757
+ border: "none",
3758
+ cursor: "pointer",
3759
+ fontFamily: "system-ui, -apple-system, sans-serif",
3760
+ fontWeight: isActive ? 600 : 400,
3761
+ background: "transparent",
3762
+ color: isActive ? activeColor : inactiveColor,
3763
+ transition: "color 0.2s, background 0.15s",
3764
+ lineHeight: "16px"
3765
+ },
3766
+ children: w.label
3767
+ },
3768
+ w.secs
3769
+ );
3770
+ })
3771
+ ]
3772
+ }
3773
+ ),
3774
+ onModeChange && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3775
+ "div",
3776
+ {
3777
+ ref: modeBarRef,
3778
+ style: {
3779
+ position: "relative",
3780
+ display: "inline-flex",
3781
+ gap: ws === "text" ? 4 : 2,
3782
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3783
+ borderRadius: ws === "rounded" ? 999 : 6,
3784
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
3785
+ },
3786
+ children: [
3787
+ ws !== "text" && modeIndicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
3788
+ position: "absolute",
3789
+ top: ws === "rounded" ? 3 : 2,
3790
+ left: modeIndicatorStyle.left,
3791
+ width: modeIndicatorStyle.width,
3792
+ height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
3793
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3794
+ borderRadius: ws === "rounded" ? 999 : 4,
3795
+ transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3796
+ pointerEvents: "none"
3797
+ } }),
3798
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2047
3799
  "button",
2048
3800
  {
2049
3801
  ref: (el) => {
2050
- if (el) windowBtnRefs.current.set(w.secs, el);
2051
- else windowBtnRefs.current.delete(w.secs);
3802
+ if (el) modeBtnRefs.current.set("line", el);
3803
+ else modeBtnRefs.current.delete("line");
2052
3804
  },
2053
- onClick: () => {
2054
- setActiveWindowSecs(w.secs);
2055
- onWindowChange?.(w.secs);
3805
+ onClick: () => onModeChange("line"),
3806
+ style: {
3807
+ position: "relative",
3808
+ zIndex: 1,
3809
+ padding: "5px 7px",
3810
+ borderRadius: ws === "rounded" ? 999 : 4,
3811
+ border: "none",
3812
+ cursor: "pointer",
3813
+ background: "transparent",
3814
+ display: "flex",
3815
+ alignItems: "center"
2056
3816
  },
3817
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3818
+ "path",
3819
+ {
3820
+ d: "M1 8.5C2.5 8.5 3 4 5.5 4S7.5 7 8.5 7C9.5 7 10 3.5 11 3.5",
3821
+ stroke: activeMode === "line" ? activeColor : inactiveColor,
3822
+ strokeWidth: activeMode === "line" ? 1.5 : 1.2,
3823
+ strokeLinecap: "round",
3824
+ fill: "none"
3825
+ }
3826
+ ) })
3827
+ }
3828
+ ),
3829
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3830
+ "button",
3831
+ {
3832
+ ref: (el) => {
3833
+ if (el) modeBtnRefs.current.set("candle", el);
3834
+ else modeBtnRefs.current.delete("candle");
3835
+ },
3836
+ onClick: () => onModeChange("candle"),
2057
3837
  style: {
2058
3838
  position: "relative",
2059
3839
  zIndex: 1,
2060
- fontSize: 11,
2061
- padding: ws === "text" ? "2px 6px" : "3px 10px",
3840
+ padding: "5px 7px",
2062
3841
  borderRadius: ws === "rounded" ? 999 : 4,
2063
3842
  border: "none",
2064
3843
  cursor: "pointer",
2065
- fontFamily: "system-ui, -apple-system, sans-serif",
2066
- fontWeight: isActive ? 600 : 400,
2067
3844
  background: "transparent",
2068
- color: isActive ? isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)" : isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)",
2069
- transition: "color 0.2s, background 0.15s",
2070
- lineHeight: "16px"
3845
+ display: "flex",
3846
+ alignItems: "center"
2071
3847
  },
2072
- children: w.label
2073
- },
2074
- w.secs
2075
- );
2076
- })
2077
- ]
2078
- }
2079
- ),
3848
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: [
3849
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3850
+ "line",
3851
+ {
3852
+ x1: "3.5",
3853
+ y1: "1",
3854
+ x2: "3.5",
3855
+ y2: "11",
3856
+ stroke: activeMode === "candle" ? activeColor : inactiveColor,
3857
+ strokeWidth: "1"
3858
+ }
3859
+ ),
3860
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3861
+ "rect",
3862
+ {
3863
+ x: "2",
3864
+ y: "3",
3865
+ width: "3",
3866
+ height: "5",
3867
+ rx: "0.5",
3868
+ fill: activeMode === "candle" ? activeColor : inactiveColor
3869
+ }
3870
+ ),
3871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3872
+ "line",
3873
+ {
3874
+ x1: "8.5",
3875
+ y1: "2",
3876
+ x2: "8.5",
3877
+ y2: "10",
3878
+ stroke: activeMode === "candle" ? activeColor : inactiveColor,
3879
+ strokeWidth: "1"
3880
+ }
3881
+ ),
3882
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3883
+ "rect",
3884
+ {
3885
+ x: "7",
3886
+ y: "4",
3887
+ width: "3",
3888
+ height: "4",
3889
+ rx: "0.5",
3890
+ fill: activeMode === "candle" ? activeColor : inactiveColor
3891
+ }
3892
+ )
3893
+ ] })
3894
+ }
3895
+ )
3896
+ ]
3897
+ }
3898
+ ),
3899
+ showSeriesToggle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
3900
+ display: "inline-flex",
3901
+ gap: ws === "text" ? 4 : 2,
3902
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3903
+ borderRadius: ws === "rounded" ? 999 : 6,
3904
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
3905
+ opacity: isMultiSeries ? 1 : 0,
3906
+ transition: "opacity 0.4s",
3907
+ pointerEvents: isMultiSeries ? "auto" : "none"
3908
+ }, children: (lastSeriesPropRef.current ?? []).map((s, si) => {
3909
+ const isHidden = hiddenSeries.has(s.id);
3910
+ const seriesColor = s.color || SERIES_COLORS[si % SERIES_COLORS.length];
3911
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3912
+ "button",
3913
+ {
3914
+ onClick: () => handleSeriesToggle(s.id),
3915
+ style: {
3916
+ position: "relative",
3917
+ zIndex: 1,
3918
+ fontSize: 11,
3919
+ padding: seriesToggleCompact ? ws === "text" ? "2px 4px" : "5px 7px" : ws === "text" ? "2px 6px" : "3px 8px",
3920
+ borderRadius: ws === "rounded" ? 999 : 4,
3921
+ border: "none",
3922
+ cursor: "pointer",
3923
+ fontFamily: "system-ui, -apple-system, sans-serif",
3924
+ fontWeight: 500,
3925
+ background: isHidden ? "transparent" : ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3926
+ color: isHidden ? inactiveColor : activeColor,
3927
+ opacity: isHidden ? 0.4 : 1,
3928
+ transition: "opacity 0.2s, background 0.15s, color 0.2s",
3929
+ lineHeight: "16px",
3930
+ display: "flex",
3931
+ alignItems: "center",
3932
+ gap: seriesToggleCompact ? 0 : 4
3933
+ },
3934
+ children: [
3935
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
3936
+ width: seriesToggleCompact ? 8 : 6,
3937
+ height: seriesToggleCompact ? 8 : 6,
3938
+ borderRadius: "50%",
3939
+ background: seriesColor,
3940
+ flexShrink: 0,
3941
+ opacity: isHidden ? 0.4 : 1,
3942
+ transition: "opacity 0.2s"
3943
+ } }),
3944
+ !seriesToggleCompact && (s.label ?? s.id)
3945
+ ]
3946
+ },
3947
+ s.id
3948
+ );
3949
+ }) })
3950
+ ] }),
2080
3951
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2081
3952
  "div",
2082
3953
  {
@@ -2099,7 +3970,64 @@ function Liveline({
2099
3970
  )
2100
3971
  ] });
2101
3972
  }
3973
+
3974
+ // src/LivelineTransition.tsx
3975
+ var import_react3 = require("react");
3976
+ var import_jsx_runtime2 = require("react/jsx-runtime");
3977
+ function LivelineTransition({
3978
+ active,
3979
+ children,
3980
+ duration = 300,
3981
+ className,
3982
+ style
3983
+ }) {
3984
+ const childArray = Array.isArray(children) ? children : [children];
3985
+ const [mounted, setMounted] = (0, import_react3.useState)(() => /* @__PURE__ */ new Set([active]));
3986
+ const [visible, setVisible] = (0, import_react3.useState)(active);
3987
+ const prevRef = (0, import_react3.useRef)(active);
3988
+ (0, import_react3.useEffect)(() => {
3989
+ if (active === prevRef.current) return () => {
3990
+ };
3991
+ const oldKey = prevRef.current;
3992
+ prevRef.current = active;
3993
+ setMounted((prev) => /* @__PURE__ */ new Set([...prev, active]));
3994
+ let raf1 = requestAnimationFrame(() => {
3995
+ raf1 = requestAnimationFrame(() => setVisible(active));
3996
+ });
3997
+ const timer = setTimeout(() => {
3998
+ setMounted((prev) => {
3999
+ const next = new Set(prev);
4000
+ next.delete(oldKey);
4001
+ return next;
4002
+ });
4003
+ }, duration + 50);
4004
+ return () => {
4005
+ cancelAnimationFrame(raf1);
4006
+ clearTimeout(timer);
4007
+ };
4008
+ }, [active, duration]);
4009
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, style: { position: "relative", width: "100%", height: "100%", ...style }, children: childArray.map((child) => {
4010
+ const key = String(child.key ?? "");
4011
+ if (!mounted.has(key)) return null;
4012
+ const isActive = key === visible;
4013
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4014
+ "div",
4015
+ {
4016
+ style: {
4017
+ position: "absolute",
4018
+ inset: 0,
4019
+ opacity: isActive ? 1 : 0,
4020
+ transition: `opacity ${duration}ms ease`,
4021
+ pointerEvents: isActive ? "auto" : "none"
4022
+ },
4023
+ children: child
4024
+ },
4025
+ key
4026
+ );
4027
+ }) });
4028
+ }
2102
4029
  // Annotate the CommonJS export names for ESM import in node:
2103
4030
  0 && (module.exports = {
2104
- Liveline
4031
+ Liveline,
4032
+ LivelineTransition
2105
4033
  });