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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/Liveline.tsx
2
- import { useRef as useRef2, useState, useLayoutEffect, useMemo } from "react";
2
+ import { useRef as useRef2, useState, useLayoutEffect, useMemo, useCallback as useCallback2 } from "react";
3
3
 
4
4
  // src/theme.ts
5
5
  function parseColorRgb(color) {
@@ -61,6 +61,33 @@ function resolveTheme(color, mode) {
61
61
  badgeFont: '500 11px "SF Mono", Menlo, monospace'
62
62
  };
63
63
  }
64
+ var SERIES_COLORS = [
65
+ "#3b82f6",
66
+ // blue
67
+ "#ef4444",
68
+ // red
69
+ "#22c55e",
70
+ // green
71
+ "#f59e0b",
72
+ // amber
73
+ "#8b5cf6",
74
+ // violet
75
+ "#ec4899",
76
+ // pink
77
+ "#06b6d4",
78
+ // cyan
79
+ "#f97316"
80
+ // orange
81
+ ];
82
+ function resolveSeriesPalettes(series, mode) {
83
+ const map = /* @__PURE__ */ new Map();
84
+ for (let i = 0; i < series.length; i++) {
85
+ const s = series[i];
86
+ const color = s.color || SERIES_COLORS[i % SERIES_COLORS.length];
87
+ map.set(s.id, resolveTheme(color, mode));
88
+ }
89
+ return map;
90
+ }
64
91
 
65
92
  // src/useLivelineEngine.ts
66
93
  import { useRef, useEffect, useCallback } from "react";
@@ -361,8 +388,9 @@ function renderCurve(ctx, layout, palette, pts, showFill, lineAlpha = 1, fillAlp
361
388
  ctx.stroke();
362
389
  ctx.globalAlpha = baseAlpha;
363
390
  }
364
- function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0) {
391
+ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0, colorBlend = 1, skipDashLine = false, fillScale = 1) {
365
392
  const { h, pad, toX, toY, chartW, chartH } = layout;
393
+ const incomingAlpha = ctx.globalAlpha;
366
394
  const yMin = pad.top;
367
395
  const yMax = h - pad.bottom;
368
396
  const clampY = (y) => Math.max(yMin, Math.min(yMax, y));
@@ -371,8 +399,10 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
371
399
  const scroll = now_ms * LOADING_SCROLL_SPEED;
372
400
  const morphY = chartReveal < 1 ? (rawY, x) => {
373
401
  const t = Math.max(0, Math.min(1, (x - pad.left) / chartW));
402
+ const centerDist = Math.abs(t - 0.5) * 2;
403
+ const localReveal = Math.max(0, Math.min(1, (chartReveal - centerDist * 0.4) / 0.6));
374
404
  const baseY = loadingY(t, centerY, amplitude, scroll);
375
- return baseY + (rawY - baseY) * chartReveal;
405
+ return baseY + (rawY - baseY) * localReveal;
376
406
  } : (rawY, _x) => rawY;
377
407
  const pts = visible.map((p, i) => {
378
408
  const x = toX(p.time);
@@ -385,13 +415,14 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
385
415
  pts.push([tipX, morphY(clampY(toY(smoothValue)), tipX)]);
386
416
  if (pts.length < 2) return;
387
417
  let lineAlpha = 1;
388
- let fillAlpha = 1;
418
+ let fillAlpha = fillScale;
389
419
  if (chartReveal < 1) {
390
420
  const breath = loadingBreath(now_ms);
391
421
  lineAlpha = breath + (1 - breath) * chartReveal;
392
- fillAlpha = chartReveal;
422
+ fillAlpha = chartReveal * fillScale;
393
423
  }
394
- const strokeColor = chartReveal < 1 ? blendColor(palette.gridLabel, palette.line, Math.min(1, chartReveal * 3)) : void 0;
424
+ const colorT = Math.min(1, chartReveal * 3) * colorBlend;
425
+ const strokeColor = chartReveal < 1 || colorBlend < 1 ? blendColor(palette.gridLabel, palette.line, colorT) : void 0;
395
426
  const isScrubbing = scrubX !== null;
396
427
  ctx.save();
397
428
  ctx.beginPath();
@@ -408,26 +439,28 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
408
439
  ctx.beginPath();
409
440
  ctx.rect(scrubX, 0, layout.w - scrubX, h);
410
441
  ctx.clip();
411
- ctx.globalAlpha = 1 - scrubAmount * 0.6;
442
+ ctx.globalAlpha = incomingAlpha * (1 - scrubAmount * 0.6);
412
443
  renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
413
444
  ctx.restore();
414
445
  } else {
415
446
  renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
416
447
  }
417
448
  ctx.restore();
418
- const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
419
- const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
420
- ctx.setLineDash([4, 4]);
421
- ctx.strokeStyle = palette.dashLine;
422
- ctx.lineWidth = 1;
423
- const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
424
- ctx.globalAlpha = chartReveal < 1 ? dashBase * chartReveal : dashBase;
425
- ctx.beginPath();
426
- ctx.moveTo(pad.left, currentY);
427
- ctx.lineTo(layout.w - pad.right, currentY);
428
- ctx.stroke();
429
- ctx.setLineDash([]);
430
- ctx.globalAlpha = 1;
449
+ if (!skipDashLine) {
450
+ const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
451
+ const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
452
+ ctx.setLineDash([4, 4]);
453
+ ctx.strokeStyle = palette.dashLine;
454
+ ctx.lineWidth = 1;
455
+ const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
456
+ ctx.globalAlpha = incomingAlpha * (chartReveal < 1 ? dashBase * chartReveal : dashBase);
457
+ ctx.beginPath();
458
+ ctx.moveTo(pad.left, currentY);
459
+ ctx.lineTo(layout.w - pad.right, currentY);
460
+ ctx.stroke();
461
+ ctx.setLineDash([]);
462
+ }
463
+ ctx.globalAlpha = incomingAlpha;
431
464
  const last = pts[pts.length - 1];
432
465
  last[1] = Math.max(10, Math.min(h - 10, last[1]));
433
466
  return pts;
@@ -480,6 +513,33 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = per
480
513
  }
481
514
  ctx.fill();
482
515
  }
516
+ function drawMultiDot(ctx, x, y, color, pulse = true, now_ms = performance.now(), radius = 3) {
517
+ const baseAlpha = ctx.globalAlpha;
518
+ if (pulse) {
519
+ const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
520
+ if (t < 1) {
521
+ const ringRadius = 9 + t * 10;
522
+ const pulseAlpha = 0.3 * (1 - t);
523
+ ctx.beginPath();
524
+ ctx.arc(x, y, ringRadius, 0, Math.PI * 2);
525
+ ctx.strokeStyle = color;
526
+ ctx.lineWidth = 1.5;
527
+ ctx.globalAlpha = baseAlpha * pulseAlpha;
528
+ ctx.stroke();
529
+ }
530
+ }
531
+ ctx.globalAlpha = baseAlpha;
532
+ ctx.beginPath();
533
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
534
+ ctx.fillStyle = color;
535
+ ctx.fill();
536
+ }
537
+ function drawSimpleDot(ctx, x, y, color, radius = 3) {
538
+ ctx.beginPath();
539
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
540
+ ctx.fillStyle = color;
541
+ ctx.fill();
542
+ }
483
543
  function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
484
544
  const baseAlpha = ctx.globalAlpha;
485
545
  const upTarget = momentum === "up" ? 1 : 0;
@@ -578,6 +638,90 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
578
638
  ctx.fillText(separator + timeText, tx + valueW, ty);
579
639
  ctx.restore();
580
640
  }
641
+ function drawMultiCrosshair(ctx, layout, palette, hoverX, hoverTime, entries, formatValue, formatTime, scrubOpacity, tooltipY, tooltipOutline, liveDotX) {
642
+ if (scrubOpacity < 0.01 || entries.length === 0) return;
643
+ const { h, pad, toY } = layout;
644
+ ctx.save();
645
+ ctx.globalAlpha = scrubOpacity * 0.5;
646
+ ctx.strokeStyle = palette.crosshairLine;
647
+ ctx.lineWidth = 1;
648
+ ctx.beginPath();
649
+ ctx.moveTo(hoverX, pad.top);
650
+ ctx.lineTo(hoverX, h - pad.bottom);
651
+ ctx.stroke();
652
+ ctx.restore();
653
+ const dotRadius = 4 * Math.min(scrubOpacity * 3, 1);
654
+ if (dotRadius > 0.5) {
655
+ ctx.globalAlpha = 1;
656
+ for (const entry of entries) {
657
+ const y = toY(entry.value);
658
+ ctx.beginPath();
659
+ ctx.arc(hoverX, y, dotRadius, 0, Math.PI * 2);
660
+ ctx.fillStyle = entry.color;
661
+ ctx.fill();
662
+ }
663
+ }
664
+ if (scrubOpacity < 0.1 || layout.w < 300) return;
665
+ ctx.save();
666
+ ctx.globalAlpha = scrubOpacity;
667
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
668
+ ctx.textAlign = "left";
669
+ const timeText = formatTime(hoverTime);
670
+ const sep = " \xB7 ";
671
+ const dotInline = " ";
672
+ const segments = [
673
+ { text: timeText, color: palette.gridLabel }
674
+ ];
675
+ for (const e of entries) {
676
+ segments.push({ text: sep, color: palette.gridLabel });
677
+ segments.push({ text: dotInline, color: e.color, isDot: true });
678
+ const label = e.label ? `${e.label} ` : "";
679
+ if (label) segments.push({ text: label, color: palette.gridLabel });
680
+ segments.push({ text: formatValue(e.value), color: palette.tooltipText });
681
+ }
682
+ let totalW = 0;
683
+ const segWidths = [];
684
+ for (const seg of segments) {
685
+ const w = seg.isDot ? 12 : ctx.measureText(seg.text).width;
686
+ segWidths.push(w);
687
+ totalW += w;
688
+ }
689
+ let tx = hoverX - totalW / 2;
690
+ const minX = pad.left + 4;
691
+ const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
692
+ const maxX = dotRightEdge - totalW;
693
+ if (tx < minX) tx = minX;
694
+ if (tx > maxX) tx = maxX;
695
+ const ty = pad.top + (tooltipY ?? 14) + 10;
696
+ if (tooltipOutline !== false) {
697
+ ctx.strokeStyle = palette.tooltipBg;
698
+ ctx.lineWidth = 3;
699
+ ctx.lineJoin = "round";
700
+ let ox2 = tx;
701
+ for (let i = 0; i < segments.length; i++) {
702
+ const seg = segments[i];
703
+ if (!seg.isDot) {
704
+ ctx.strokeText(seg.text, ox2, ty);
705
+ }
706
+ ox2 += segWidths[i];
707
+ }
708
+ }
709
+ let ox = tx;
710
+ for (let i = 0; i < segments.length; i++) {
711
+ const seg = segments[i];
712
+ if (seg.isDot) {
713
+ ctx.beginPath();
714
+ ctx.arc(ox + 4, ty - 4, 3.5, 0, Math.PI * 2);
715
+ ctx.fillStyle = seg.color;
716
+ ctx.fill();
717
+ } else {
718
+ ctx.fillStyle = seg.color;
719
+ ctx.fillText(seg.text, ox, ty);
720
+ }
721
+ ox += segWidths[i];
722
+ }
723
+ ctx.restore();
724
+ }
581
725
 
582
726
  // src/draw/referenceLine.ts
583
727
  function drawReferenceLine(ctx, layout, palette, ref) {
@@ -944,6 +1088,337 @@ function drawParticles(ctx, state, dt) {
944
1088
  ctx.restore();
945
1089
  }
946
1090
 
1091
+ // src/draw/candlestick.ts
1092
+ var BULL = "#22c55e";
1093
+ var BEAR = "#ef4444";
1094
+ var BULL_RGB = [34, 197, 94];
1095
+ var BEAR_RGB = [239, 68, 68];
1096
+ function blendColor2(t) {
1097
+ const r = Math.round(BEAR_RGB[0] + (BULL_RGB[0] - BEAR_RGB[0]) * t);
1098
+ const g = Math.round(BEAR_RGB[1] + (BULL_RGB[1] - BEAR_RGB[1]) * t);
1099
+ const b = Math.round(BEAR_RGB[2] + (BULL_RGB[2] - BEAR_RGB[2]) * t);
1100
+ return `rgb(${r},${g},${b})`;
1101
+ }
1102
+ function parseRgb(color) {
1103
+ const hex = color.match(/^#([0-9a-f]{6})$/i);
1104
+ if (hex) {
1105
+ const h = hex[1];
1106
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
1107
+ }
1108
+ const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
1109
+ if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
1110
+ return [128, 128, 128];
1111
+ }
1112
+ function blendToAccent(candleColor, accentColor, t) {
1113
+ if (t <= 0) return candleColor;
1114
+ if (t >= 1) return accentColor;
1115
+ const [r1, g1, b1] = parseRgb(candleColor);
1116
+ const [r2, g2, b2] = parseRgb(accentColor);
1117
+ const r = Math.round(r1 + (r2 - r1) * t);
1118
+ const g = Math.round(g1 + (g2 - g1) * t);
1119
+ const b = Math.round(b1 + (b2 - b1) * t);
1120
+ return `rgb(${r},${g},${b})`;
1121
+ }
1122
+ function candleDims(layout, candleWidthSecs) {
1123
+ const pxPerSec = layout.chartW / (layout.rightEdge - layout.leftEdge);
1124
+ const candlePxW = candleWidthSecs * pxPerSec;
1125
+ const bodyW = Math.max(1, candlePxW * 0.7);
1126
+ const wickW = Math.max(0.8, Math.min(2, bodyW * 0.15));
1127
+ const radius = bodyW > 6 ? 1.5 : 0;
1128
+ return { bodyW, wickW, radius };
1129
+ }
1130
+ function roundedRect(ctx, x, y, w, h, r) {
1131
+ if (r <= 0 || h < r * 2) {
1132
+ ctx.rect(x, y, w, h);
1133
+ return;
1134
+ }
1135
+ ctx.moveTo(x + r, y);
1136
+ ctx.lineTo(x + w - r, y);
1137
+ ctx.arcTo(x + w, y, x + w, y + r, r);
1138
+ ctx.lineTo(x + w, y + h - r);
1139
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
1140
+ ctx.lineTo(x + r, y + h);
1141
+ ctx.arcTo(x, y + h, x, y + h - r, r);
1142
+ ctx.lineTo(x, y + r);
1143
+ ctx.arcTo(x, y, x + r, y, r);
1144
+ ctx.closePath();
1145
+ }
1146
+ function drawCandlesticks(ctx, layout, candles, candleWidthSecs, liveTime, now_ms, scrubX, scrubDim, liveAlpha = 1, liveBullBlend = -1, accentColor, accentBlend = 0) {
1147
+ if (candles.length === 0) return;
1148
+ const { toX, toY } = layout;
1149
+ const { bodyW, wickW, radius } = candleDims(layout, candleWidthSecs);
1150
+ const halfBody = bodyW / 2;
1151
+ const padL = layout.pad.left;
1152
+ const padR = layout.pad.left + layout.chartW;
1153
+ const livePulse = 0.12 + Math.sin(now_ms * 4e-3) * 0.08;
1154
+ for (const c of candles) {
1155
+ const cx = toX(c.time + candleWidthSecs / 2);
1156
+ if (cx + halfBody < padL || cx - halfBody > padR) continue;
1157
+ const isBull = c.close >= c.open;
1158
+ const isLive = c.time === liveTime;
1159
+ let color = isLive && liveBullBlend >= 0 ? blendColor2(liveBullBlend) : isBull ? BULL : BEAR;
1160
+ if (accentColor && accentBlend > 0.01) {
1161
+ color = blendToAccent(color, accentColor, accentBlend);
1162
+ }
1163
+ let candleAlpha = isLive ? liveAlpha : 1;
1164
+ if (scrubDim > 0.01 && scrubX > 0) {
1165
+ const dist = cx - scrubX;
1166
+ if (dist > 0) {
1167
+ const fadeZone = bodyW * 1.5;
1168
+ const dimT = Math.min(dist / fadeZone, 1);
1169
+ candleAlpha *= 1 - scrubDim * 0.5 * dimT;
1170
+ }
1171
+ }
1172
+ const baseAlpha = ctx.globalAlpha;
1173
+ ctx.globalAlpha = baseAlpha * candleAlpha;
1174
+ const bodyTop = toY(Math.max(c.open, c.close));
1175
+ const bodyBottom = toY(Math.min(c.open, c.close));
1176
+ const bodyH = Math.max(1, bodyBottom - bodyTop);
1177
+ const wickTop = toY(c.high);
1178
+ const wickBottom = toY(c.low);
1179
+ ctx.lineCap = "round";
1180
+ ctx.strokeStyle = color;
1181
+ if (bodyTop - wickTop > 0.5) {
1182
+ ctx.beginPath();
1183
+ ctx.moveTo(cx, bodyTop);
1184
+ ctx.lineTo(cx, wickTop);
1185
+ ctx.lineWidth = wickW;
1186
+ ctx.stroke();
1187
+ }
1188
+ if (wickBottom - bodyBottom > 0.5) {
1189
+ ctx.beginPath();
1190
+ ctx.moveTo(cx, bodyBottom);
1191
+ ctx.lineTo(cx, wickBottom);
1192
+ ctx.lineWidth = wickW;
1193
+ ctx.stroke();
1194
+ }
1195
+ ctx.fillStyle = color;
1196
+ ctx.beginPath();
1197
+ roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
1198
+ ctx.fill();
1199
+ if (isLive) {
1200
+ ctx.save();
1201
+ ctx.globalAlpha = baseAlpha * candleAlpha * livePulse;
1202
+ ctx.shadowColor = color;
1203
+ ctx.shadowBlur = 8;
1204
+ ctx.fillStyle = color;
1205
+ ctx.beginPath();
1206
+ roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
1207
+ ctx.fill();
1208
+ ctx.restore();
1209
+ }
1210
+ ctx.globalAlpha = baseAlpha;
1211
+ }
1212
+ }
1213
+ function drawClosePrice(ctx, layout, palette, liveCandle, scrubDim, bullBlend = -1) {
1214
+ const y = layout.toY(liveCandle.close);
1215
+ if (y < layout.pad.top || y > layout.h - layout.pad.bottom) return;
1216
+ const isBull = liveCandle.close >= liveCandle.open;
1217
+ const color = bullBlend >= 0 ? blendColor2(bullBlend) : isBull ? BULL : BEAR;
1218
+ const baseAlpha = ctx.globalAlpha;
1219
+ ctx.save();
1220
+ ctx.setLineDash([4, 4]);
1221
+ ctx.strokeStyle = color;
1222
+ ctx.lineWidth = 1;
1223
+ ctx.globalAlpha = baseAlpha * (1 - scrubDim * 0.3) * 0.4;
1224
+ ctx.beginPath();
1225
+ ctx.moveTo(layout.pad.left, y);
1226
+ ctx.lineTo(layout.w - layout.pad.right, y);
1227
+ ctx.stroke();
1228
+ ctx.setLineDash([]);
1229
+ ctx.restore();
1230
+ }
1231
+ function drawCandleCrosshair(ctx, layout, palette, hoverX, candle, hoverTime, formatValue, formatTime, opacity) {
1232
+ if (opacity < 0.01) return;
1233
+ const { h, pad } = layout;
1234
+ ctx.save();
1235
+ ctx.globalAlpha = opacity * 0.5;
1236
+ ctx.strokeStyle = palette.crosshairLine;
1237
+ ctx.lineWidth = 1;
1238
+ ctx.beginPath();
1239
+ ctx.moveTo(hoverX, pad.top);
1240
+ ctx.lineTo(hoverX, h - pad.bottom);
1241
+ ctx.stroke();
1242
+ ctx.restore();
1243
+ if (opacity < 0.1 || layout.w < 200) return;
1244
+ const isBull = candle.close >= candle.open;
1245
+ const valueColor = isBull ? BULL : BEAR;
1246
+ const cl = formatValue(candle.close);
1247
+ const time = formatTime(hoverTime);
1248
+ ctx.save();
1249
+ ctx.globalAlpha = opacity;
1250
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
1251
+ ctx.textAlign = "left";
1252
+ let parts;
1253
+ if (layout.w >= 400) {
1254
+ const o = formatValue(candle.open);
1255
+ const hi = formatValue(candle.high);
1256
+ const lo = formatValue(candle.low);
1257
+ parts = [
1258
+ { text: "O ", color: palette.gridLabel },
1259
+ { text: o, color: valueColor },
1260
+ { text: " H ", color: palette.gridLabel },
1261
+ { text: hi, color: valueColor },
1262
+ { text: " L ", color: palette.gridLabel },
1263
+ { text: lo, color: valueColor },
1264
+ { text: " C ", color: palette.gridLabel },
1265
+ { text: cl, color: valueColor },
1266
+ { text: " \xB7 ", color: palette.gridLabel },
1267
+ { text: time, color: palette.gridLabel }
1268
+ ];
1269
+ } else {
1270
+ parts = [
1271
+ { text: "C ", color: palette.gridLabel },
1272
+ { text: cl, color: valueColor },
1273
+ { text: " \xB7 ", color: palette.gridLabel },
1274
+ { text: time, color: palette.gridLabel }
1275
+ ];
1276
+ }
1277
+ let totalW = 0;
1278
+ const widths = [];
1279
+ for (const p of parts) {
1280
+ const w = ctx.measureText(p.text).width;
1281
+ widths.push(w);
1282
+ totalW += w;
1283
+ }
1284
+ let tx = hoverX - totalW / 2;
1285
+ const minX = pad.left + 4;
1286
+ const maxX = layout.w - pad.right - totalW;
1287
+ if (tx < minX) tx = minX;
1288
+ if (tx > maxX) tx = maxX;
1289
+ const ty = pad.top + 24;
1290
+ ctx.strokeStyle = palette.tooltipBg;
1291
+ ctx.lineWidth = 3;
1292
+ ctx.lineJoin = "round";
1293
+ let cx = tx;
1294
+ for (let i = 0; i < parts.length; i++) {
1295
+ ctx.strokeText(parts[i].text, cx, ty);
1296
+ cx += widths[i];
1297
+ }
1298
+ cx = tx;
1299
+ for (let i = 0; i < parts.length; i++) {
1300
+ ctx.fillStyle = parts[i].color;
1301
+ ctx.fillText(parts[i].text, cx, ty);
1302
+ cx += widths[i];
1303
+ }
1304
+ ctx.restore();
1305
+ }
1306
+ function drawLineModeCrosshair(ctx, layout, palette, hoverX, value, hoverTime, formatValue, formatTime, opacity) {
1307
+ if (opacity < 0.01) return;
1308
+ const { h, pad } = layout;
1309
+ const y = layout.toY(value);
1310
+ ctx.save();
1311
+ ctx.globalAlpha = opacity * 0.5;
1312
+ ctx.strokeStyle = palette.crosshairLine;
1313
+ ctx.lineWidth = 1;
1314
+ ctx.beginPath();
1315
+ ctx.moveTo(hoverX, pad.top);
1316
+ ctx.lineTo(hoverX, h - pad.bottom);
1317
+ ctx.stroke();
1318
+ ctx.globalAlpha = opacity * 0.3;
1319
+ ctx.beginPath();
1320
+ ctx.moveTo(pad.left, y);
1321
+ ctx.lineTo(layout.w - pad.right, y);
1322
+ ctx.stroke();
1323
+ ctx.restore();
1324
+ if (opacity < 0.1 || layout.w < 200) return;
1325
+ const val = formatValue(value);
1326
+ const time = formatTime(hoverTime);
1327
+ ctx.save();
1328
+ ctx.globalAlpha = opacity;
1329
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
1330
+ ctx.textAlign = "left";
1331
+ const parts = [
1332
+ { text: val, color: palette.line },
1333
+ { text: " \xB7 ", color: palette.gridLabel },
1334
+ { text: time, color: palette.gridLabel }
1335
+ ];
1336
+ let totalW = 0;
1337
+ const widths = [];
1338
+ for (const p of parts) {
1339
+ const w = ctx.measureText(p.text).width;
1340
+ widths.push(w);
1341
+ totalW += w;
1342
+ }
1343
+ let tx = hoverX - totalW / 2;
1344
+ const minX = pad.left + 4;
1345
+ const maxX = layout.w - pad.right - totalW;
1346
+ if (tx < minX) tx = minX;
1347
+ if (tx > maxX) tx = maxX;
1348
+ const ty = pad.top + 24;
1349
+ ctx.strokeStyle = palette.tooltipBg;
1350
+ ctx.lineWidth = 3;
1351
+ ctx.lineJoin = "round";
1352
+ let lx = tx;
1353
+ for (let i = 0; i < parts.length; i++) {
1354
+ ctx.strokeText(parts[i].text, lx, ty);
1355
+ lx += widths[i];
1356
+ }
1357
+ lx = tx;
1358
+ for (let i = 0; i < parts.length; i++) {
1359
+ ctx.fillStyle = parts[i].color;
1360
+ ctx.fillText(parts[i].text, lx, ty);
1361
+ lx += widths[i];
1362
+ }
1363
+ ctx.restore();
1364
+ }
1365
+
1366
+ // src/draw/empty.ts
1367
+ function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
1368
+ const chartW = w - pad.left - pad.right;
1369
+ const chartH = h - pad.top - pad.bottom;
1370
+ const centerY = pad.top + chartH / 2;
1371
+ const cx = pad.left + chartW / 2;
1372
+ const text = emptyText ?? "No data to display";
1373
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1374
+ ctx.save();
1375
+ ctx.font = "400 12px system-ui, -apple-system, sans-serif";
1376
+ const textW = ctx.measureText(text).width;
1377
+ const gapHalf = textW / 2 + 20;
1378
+ const fadeW = 30;
1379
+ if (!skipLine) {
1380
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1381
+ const breath = loadingBreath(now_ms);
1382
+ const numPts = 32;
1383
+ const pts = [];
1384
+ for (let i = 0; i <= numPts; i++) {
1385
+ const t = i / numPts;
1386
+ const x = pad.left + t * chartW;
1387
+ const y = loadingY(t, centerY, amplitude, scroll);
1388
+ pts.push([x, y]);
1389
+ }
1390
+ ctx.beginPath();
1391
+ ctx.moveTo(pts[0][0], pts[0][1]);
1392
+ drawSpline(ctx, pts);
1393
+ ctx.strokeStyle = palette.gridLabel;
1394
+ ctx.lineWidth = palette.lineWidth;
1395
+ ctx.globalAlpha = breath * alpha;
1396
+ ctx.lineCap = "round";
1397
+ ctx.lineJoin = "round";
1398
+ ctx.stroke();
1399
+ }
1400
+ ctx.save();
1401
+ ctx.globalCompositeOperation = "destination-out";
1402
+ const gapLeft = cx - gapHalf - fadeW;
1403
+ const gapRight = cx + gapHalf + fadeW;
1404
+ const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
1405
+ eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
1406
+ eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1407
+ eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1408
+ eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
1409
+ ctx.fillStyle = eraseGrad;
1410
+ ctx.globalAlpha = alpha;
1411
+ const eraseH = amplitude * 2 + palette.lineWidth + 6;
1412
+ ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
1413
+ ctx.restore();
1414
+ ctx.textAlign = "center";
1415
+ ctx.textBaseline = "middle";
1416
+ ctx.globalAlpha = 0.35 * alpha;
1417
+ ctx.fillStyle = palette.gridLabel;
1418
+ ctx.fillText(text, cx, centerY);
1419
+ ctx.restore();
1420
+ }
1421
+
947
1422
  // src/draw/index.ts
948
1423
  var SHAKE_DECAY_RATE = 2e-3;
949
1424
  var SHAKE_MIN_AMPLITUDE = 0.2;
@@ -1093,91 +1568,352 @@ function drawFrame(ctx, layout, palette, opts) {
1093
1568
  ctx.restore();
1094
1569
  }
1095
1570
  }
1096
-
1097
- // src/draw/loading.ts
1098
- function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1) {
1099
- const chartW = w - pad.left - pad.right;
1100
- const chartH = h - pad.top - pad.bottom;
1101
- const centerY = pad.top + chartH / 2;
1102
- const leftX = pad.left;
1103
- const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1104
- const scroll = now_ms * LOADING_SCROLL_SPEED;
1105
- const breath = loadingBreath(now_ms);
1106
- const numPts = 32;
1107
- const pts = [];
1108
- for (let i = 0; i <= numPts; i++) {
1109
- const t = i / numPts;
1110
- const x = leftX + t * chartW;
1111
- const y = loadingY(t, centerY, amplitude, scroll);
1112
- pts.push([x, y]);
1571
+ function drawMultiFrame(ctx, layout, opts) {
1572
+ const palette = opts.primaryPalette;
1573
+ const reveal = opts.chartReveal;
1574
+ const revealRamp = (start, end) => {
1575
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1576
+ return t * t * (3 - 2 * t);
1577
+ };
1578
+ if (opts.referenceLine && reveal > 0.01) {
1579
+ ctx.save();
1580
+ if (reveal < 1) ctx.globalAlpha = reveal;
1581
+ drawReferenceLine(ctx, layout, palette, opts.referenceLine);
1582
+ ctx.restore();
1113
1583
  }
1114
- ctx.beginPath();
1115
- ctx.moveTo(pts[0][0], pts[0][1]);
1116
- drawSpline(ctx, pts);
1117
- ctx.strokeStyle = palette.line;
1118
- ctx.lineWidth = palette.lineWidth;
1119
- ctx.globalAlpha = breath * alpha;
1120
- ctx.lineCap = "round";
1121
- ctx.lineJoin = "round";
1122
- ctx.stroke();
1123
- ctx.globalAlpha = 1;
1124
- }
1125
-
1126
- // src/draw/empty.ts
1127
- function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
1584
+ if (opts.showGrid) {
1585
+ const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1586
+ if (gridAlpha > 0.01) {
1587
+ ctx.save();
1588
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1589
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1590
+ ctx.restore();
1591
+ }
1592
+ }
1593
+ const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
1594
+ const allPts = [];
1595
+ for (let si = 0; si < opts.series.length; si++) {
1596
+ const s = opts.series[si];
1597
+ const seriesAlpha = s.alpha ?? 1;
1598
+ const secondaryFade = si > 0 && reveal < 1 ? Math.min(1, reveal * 2) : 1;
1599
+ const combinedAlpha = secondaryFade * seriesAlpha;
1600
+ if (combinedAlpha < 0.01) continue;
1601
+ ctx.save();
1602
+ if (combinedAlpha < 1) ctx.globalAlpha = combinedAlpha;
1603
+ const pts = drawLine(
1604
+ ctx,
1605
+ layout,
1606
+ s.palette,
1607
+ s.visible,
1608
+ s.smoothValue,
1609
+ opts.now,
1610
+ false,
1611
+ // no fill
1612
+ scrubX,
1613
+ opts.scrubAmount,
1614
+ reveal,
1615
+ opts.now_ms
1616
+ );
1617
+ ctx.restore();
1618
+ if (pts && pts.length > 0) {
1619
+ allPts.push({ pts, palette: s.palette, label: s.label, alpha: seriesAlpha });
1620
+ }
1621
+ }
1622
+ {
1623
+ const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1624
+ if (timeAlpha > 0.01) {
1625
+ ctx.save();
1626
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1627
+ drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1628
+ ctx.restore();
1629
+ }
1630
+ }
1631
+ if (reveal > 0.3 && allPts.length > 0) {
1632
+ const dotAlpha = (reveal - 0.3) / 0.7;
1633
+ const showPulse = opts.showPulse && reveal > 0.6 && opts.pauseProgress < 0.5;
1634
+ for (const entry of allPts) {
1635
+ if (entry.alpha < 0.01) continue;
1636
+ const lastPt = entry.pts[entry.pts.length - 1];
1637
+ ctx.save();
1638
+ ctx.globalAlpha = dotAlpha * entry.alpha;
1639
+ if (showPulse && entry.alpha > 0.5) {
1640
+ drawMultiDot(ctx, lastPt[0], lastPt[1], entry.palette.line, true, opts.now_ms, 3);
1641
+ } else {
1642
+ drawSimpleDot(ctx, lastPt[0], lastPt[1], entry.palette.line, 3);
1643
+ }
1644
+ if (entry.label) {
1645
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
1646
+ ctx.textAlign = "left";
1647
+ ctx.fillStyle = entry.palette.line;
1648
+ ctx.fillText(entry.label, lastPt[0] + 6, lastPt[1] + 3.5);
1649
+ }
1650
+ ctx.restore();
1651
+ }
1652
+ }
1653
+ ctx.save();
1654
+ ctx.globalCompositeOperation = "destination-out";
1655
+ const fadeGrad = ctx.createLinearGradient(layout.pad.left, 0, layout.pad.left + FADE_EDGE_WIDTH, 0);
1656
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
1657
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
1658
+ ctx.fillStyle = fadeGrad;
1659
+ ctx.fillRect(0, 0, layout.pad.left + FADE_EDGE_WIDTH, layout.h);
1660
+ ctx.restore();
1661
+ if (opts.hoverX !== null && opts.hoverTime !== null && opts.hoverEntries.length > 0 && allPts.length > 0 && opts.scrubAmount > 0.01) {
1662
+ let maxLiveDotX = 0;
1663
+ for (const entry of allPts) {
1664
+ if (entry.alpha < 0.01) continue;
1665
+ const lastX = entry.pts[entry.pts.length - 1][0];
1666
+ if (lastX > maxLiveDotX) maxLiveDotX = lastX;
1667
+ }
1668
+ const distToLive = maxLiveDotX - opts.hoverX;
1669
+ const fadeStart = Math.min(80, layout.chartW * 0.3);
1670
+ const scrubOpacity = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
1671
+ if (scrubOpacity > 0.01) {
1672
+ drawMultiCrosshair(
1673
+ ctx,
1674
+ layout,
1675
+ palette,
1676
+ opts.hoverX,
1677
+ opts.hoverTime,
1678
+ opts.hoverEntries,
1679
+ opts.formatValue,
1680
+ opts.formatTime,
1681
+ scrubOpacity,
1682
+ opts.tooltipY,
1683
+ opts.tooltipOutline,
1684
+ maxLiveDotX
1685
+ );
1686
+ }
1687
+ }
1688
+ }
1689
+ function drawCandleFrame(ctx, layout, palette, opts) {
1690
+ const { w, h, pad, chartW, chartH } = layout;
1691
+ const reveal = opts.chartReveal;
1692
+ const fullLineMode = opts.lineModeProg >= 0.99;
1693
+ const revealLine = fullLineMode ? 1 - reveal : (1 - reveal) * (1 - reveal) * (1 - reveal);
1694
+ const lp = Math.max(opts.lineModeProg, revealLine);
1695
+ const colorBlend = lp > 1e-3 ? opts.lineModeProg / lp : 1;
1696
+ const revealRamp = (start, end) => {
1697
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1698
+ return t * t * (3 - 2 * t);
1699
+ };
1700
+ const gridAlpha = revealRamp(0.25, 0.6);
1701
+ if (opts.showGrid && gridAlpha > 0.01) {
1702
+ ctx.save();
1703
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1704
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1705
+ ctx.restore();
1706
+ }
1707
+ let linePts;
1708
+ if (lp > 0.01 && opts.lineVisible.length >= 2) {
1709
+ const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
1710
+ ctx.save();
1711
+ ctx.globalAlpha = lp;
1712
+ linePts = drawLine(
1713
+ ctx,
1714
+ layout,
1715
+ palette,
1716
+ opts.lineVisible,
1717
+ opts.lineSmoothValue,
1718
+ opts.now,
1719
+ opts.lineModeProg > 0.01,
1720
+ scrubX,
1721
+ opts.scrubAmount,
1722
+ opts.chartReveal,
1723
+ opts.now_ms,
1724
+ colorBlend,
1725
+ !fullLineMode,
1726
+ opts.lineModeProg
1727
+ // fillScale — fill fades smoothly with line mode transition
1728
+ );
1729
+ ctx.restore();
1730
+ }
1731
+ const closeAlpha = revealRamp(0.4, 0.8);
1732
+ const closeSource = opts.closePriceCandle ?? opts.liveCandle;
1733
+ if (closeSource && closeAlpha > 0.01) {
1734
+ if (lp < 0.99) {
1735
+ ctx.save();
1736
+ ctx.globalAlpha = closeAlpha * (1 - lp);
1737
+ drawClosePrice(ctx, layout, palette, closeSource, opts.scrubAmount, opts.liveBullBlend);
1738
+ ctx.restore();
1739
+ }
1740
+ if (lp > 0.01 && !fullLineMode) {
1741
+ const dashY = layout.toY(closeSource.close);
1742
+ if (dashY >= pad.top && dashY <= h - pad.bottom) {
1743
+ ctx.save();
1744
+ ctx.setLineDash([4, 4]);
1745
+ ctx.strokeStyle = palette.dashLine;
1746
+ ctx.lineWidth = 1;
1747
+ ctx.globalAlpha = closeAlpha * lp * (1 - opts.scrubAmount * 0.2);
1748
+ ctx.beginPath();
1749
+ ctx.moveTo(pad.left, dashY);
1750
+ ctx.lineTo(w - pad.right, dashY);
1751
+ ctx.stroke();
1752
+ ctx.setLineDash([]);
1753
+ ctx.restore();
1754
+ }
1755
+ }
1756
+ }
1757
+ const candleAlpha = opts.chartReveal * (1 - lp);
1758
+ if (candleAlpha > 0.01) {
1759
+ const ohlcScale = reveal * reveal * (3 - 2 * reveal);
1760
+ const collapseC = (c) => ohlcScale >= 0.99 ? c : {
1761
+ time: c.time,
1762
+ open: c.close + (c.open - c.close) * ohlcScale,
1763
+ high: c.close + (c.high - c.close) * ohlcScale,
1764
+ low: c.close + (c.low - c.close) * ohlcScale,
1765
+ close: c.close
1766
+ };
1767
+ const revealCandles = ohlcScale < 0.99 ? opts.candles.map(collapseC) : opts.candles;
1768
+ const revealOld = ohlcScale < 0.99 && opts.oldCandles.length > 0 ? opts.oldCandles.map(collapseC) : opts.oldCandles;
1769
+ ctx.save();
1770
+ ctx.beginPath();
1771
+ ctx.rect(pad.left - 1, pad.top, chartW + 2, chartH);
1772
+ ctx.clip();
1773
+ const accentCol = lp > 0.01 ? palette.line : void 0;
1774
+ if (opts.morphT >= 0 && revealOld.length > 0) {
1775
+ ctx.globalAlpha = (1 - opts.morphT) * candleAlpha;
1776
+ drawCandlesticks(
1777
+ ctx,
1778
+ layout,
1779
+ revealOld,
1780
+ opts.oldWidth,
1781
+ -1,
1782
+ opts.now_ms,
1783
+ opts.hoverX ?? 0,
1784
+ opts.scrubAmount,
1785
+ 1,
1786
+ -1,
1787
+ accentCol,
1788
+ lp
1789
+ );
1790
+ ctx.globalAlpha = opts.morphT * candleAlpha;
1791
+ drawCandlesticks(
1792
+ ctx,
1793
+ layout,
1794
+ revealCandles,
1795
+ opts.displayCandleWidth,
1796
+ opts.liveCandle?.time ?? -1,
1797
+ opts.now_ms,
1798
+ opts.hoverX ?? 0,
1799
+ opts.scrubAmount,
1800
+ opts.liveBirthAlpha,
1801
+ opts.liveBullBlend,
1802
+ accentCol,
1803
+ lp
1804
+ );
1805
+ ctx.globalAlpha = 1;
1806
+ } else {
1807
+ if (candleAlpha < 1) ctx.globalAlpha = candleAlpha;
1808
+ drawCandlesticks(
1809
+ ctx,
1810
+ layout,
1811
+ revealCandles,
1812
+ opts.displayCandleWidth,
1813
+ opts.liveCandle?.time ?? -1,
1814
+ opts.now_ms,
1815
+ opts.hoverX ?? 0,
1816
+ opts.scrubAmount,
1817
+ opts.liveBirthAlpha,
1818
+ opts.liveBullBlend,
1819
+ accentCol,
1820
+ lp
1821
+ );
1822
+ }
1823
+ ctx.restore();
1824
+ }
1825
+ if (lp > 0.5 && linePts && linePts.length > 0 && reveal > 0.3) {
1826
+ const lastPt = linePts[linePts.length - 1];
1827
+ const dotAlpha = (lp - 0.5) * 2 * ((reveal - 0.3) / 0.7);
1828
+ const showPulse = lp > 0.8 && reveal > 0.6;
1829
+ if (dotAlpha > 0.01) {
1830
+ ctx.save();
1831
+ ctx.globalAlpha = dotAlpha;
1832
+ drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, opts.scrubAmount, opts.now_ms);
1833
+ ctx.restore();
1834
+ }
1835
+ }
1836
+ const timeAlpha = revealRamp(0.25, 0.6);
1837
+ if (timeAlpha > 0.01) {
1838
+ ctx.save();
1839
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1840
+ drawTimeAxis(ctx, layout, palette, opts.targetWindowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1841
+ ctx.restore();
1842
+ }
1843
+ ctx.save();
1844
+ ctx.globalCompositeOperation = "destination-out";
1845
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
1846
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
1847
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
1848
+ ctx.fillStyle = fadeGrad;
1849
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
1850
+ ctx.restore();
1851
+ if (opts.showEmptyOverlay) {
1852
+ const bgAlpha = 1 - opts.chartReveal;
1853
+ if (bgAlpha > 0.01) {
1854
+ const bgEmptyAlpha = (1 - opts.loadingAlpha) * bgAlpha;
1855
+ if (bgEmptyAlpha > 0.01) {
1856
+ drawEmpty(ctx, w, h, pad, palette, bgEmptyAlpha, opts.now_ms, true, opts.emptyText);
1857
+ }
1858
+ }
1859
+ }
1860
+ if (opts.chartReveal > 0.7 && opts.hoveredCandle && opts.hoverX !== null && opts.scrubAmount > 0.01) {
1861
+ if (opts.lineModeProg > 0.5) {
1862
+ drawLineModeCrosshair(
1863
+ ctx,
1864
+ layout,
1865
+ palette,
1866
+ opts.hoverX,
1867
+ opts.hoveredCandle.close,
1868
+ opts.hoverTime ?? 0,
1869
+ opts.formatValue,
1870
+ opts.formatTime,
1871
+ opts.scrubAmount
1872
+ );
1873
+ } else {
1874
+ drawCandleCrosshair(
1875
+ ctx,
1876
+ layout,
1877
+ palette,
1878
+ opts.hoverX,
1879
+ opts.hoveredCandle,
1880
+ opts.hoverTime ?? 0,
1881
+ opts.formatValue,
1882
+ opts.formatTime,
1883
+ opts.scrubAmount
1884
+ );
1885
+ }
1886
+ }
1887
+ }
1888
+
1889
+ // src/draw/loading.ts
1890
+ function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1, strokeColor) {
1128
1891
  const chartW = w - pad.left - pad.right;
1129
1892
  const chartH = h - pad.top - pad.bottom;
1130
1893
  const centerY = pad.top + chartH / 2;
1131
- const cx = pad.left + chartW / 2;
1132
- const text = emptyText ?? "No data to display";
1133
- ctx.font = "400 12px system-ui, -apple-system, sans-serif";
1894
+ const leftX = pad.left;
1134
1895
  const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1135
- const textW = ctx.measureText(text).width;
1136
- const gapHalf = textW / 2 + 20;
1137
- const fadeW = 30;
1138
- if (!skipLine) {
1139
- const scroll = now_ms * LOADING_SCROLL_SPEED;
1140
- const breath = loadingBreath(now_ms);
1141
- const numPts = 32;
1142
- const pts = [];
1143
- for (let i = 0; i <= numPts; i++) {
1144
- const t = i / numPts;
1145
- const x = pad.left + t * chartW;
1146
- const y = loadingY(t, centerY, amplitude, scroll);
1147
- pts.push([x, y]);
1148
- }
1149
- ctx.beginPath();
1150
- ctx.moveTo(pts[0][0], pts[0][1]);
1151
- drawSpline(ctx, pts);
1152
- ctx.strokeStyle = palette.gridLabel;
1153
- ctx.lineWidth = palette.lineWidth;
1154
- ctx.globalAlpha = breath * alpha;
1155
- ctx.lineCap = "round";
1156
- ctx.lineJoin = "round";
1157
- ctx.stroke();
1896
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1897
+ const breath = loadingBreath(now_ms);
1898
+ const numPts = 32;
1899
+ const pts = [];
1900
+ for (let i = 0; i <= numPts; i++) {
1901
+ const t = i / numPts;
1902
+ const x = leftX + t * chartW;
1903
+ const y = loadingY(t, centerY, amplitude, scroll);
1904
+ pts.push([x, y]);
1158
1905
  }
1159
1906
  ctx.save();
1160
- ctx.globalCompositeOperation = "destination-out";
1161
- const gapLeft = cx - gapHalf - fadeW;
1162
- const gapRight = cx + gapHalf + fadeW;
1163
- const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
1164
- eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
1165
- eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1166
- eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1167
- eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
1168
- ctx.fillStyle = eraseGrad;
1169
- ctx.globalAlpha = alpha;
1170
- const eraseH = amplitude * 2 + palette.lineWidth + 6;
1171
- ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
1907
+ ctx.beginPath();
1908
+ ctx.moveTo(pts[0][0], pts[0][1]);
1909
+ drawSpline(ctx, pts);
1910
+ ctx.strokeStyle = strokeColor ?? palette.line;
1911
+ ctx.lineWidth = palette.lineWidth;
1912
+ ctx.globalAlpha = breath * alpha;
1913
+ ctx.lineCap = "round";
1914
+ ctx.lineJoin = "round";
1915
+ ctx.stroke();
1172
1916
  ctx.restore();
1173
- ctx.textAlign = "center";
1174
- ctx.textBaseline = "middle";
1175
- ctx.globalAlpha = 0.35 * alpha;
1176
- ctx.fillStyle = palette.gridLabel;
1177
- ctx.fillText(text, cx, centerY);
1178
- ctx.globalAlpha = 1;
1179
- ctx.textAlign = "start";
1180
- ctx.textBaseline = "alphabetic";
1181
1917
  }
1182
1918
 
1183
1919
  // src/draw/badge.ts
@@ -1227,10 +1963,23 @@ var ADAPTIVE_SPEED_BOOST = 0.2;
1227
1963
  var MOMENTUM_GREEN = [34, 197, 94];
1228
1964
  var MOMENTUM_RED = [239, 68, 68];
1229
1965
  var CHART_REVEAL_SPEED = 0.14;
1966
+ var CHART_REVEAL_SPEED_FWD = 0.09;
1230
1967
  var PAUSE_PROGRESS_SPEED = 0.12;
1231
1968
  var PAUSE_CATCHUP_SPEED = 0.08;
1232
1969
  var PAUSE_CATCHUP_SPEED_FAST = 0.22;
1233
1970
  var LOADING_ALPHA_SPEED = 0.14;
1971
+ var SERIES_TOGGLE_SPEED = 0.1;
1972
+ var CANDLE_LERP_SPEED = 0.25;
1973
+ var CANDLE_WIDTH_TRANS_MS = 300;
1974
+ var LINE_MORPH_MS = 500;
1975
+ var CLOSE_LINE_LERP_SPEED = 0.25;
1976
+ var LINE_DENSITY_MS = 350;
1977
+ var LINE_LERP_BASE = 0.08;
1978
+ var LINE_ADAPTIVE_BOOST = 0.2;
1979
+ var LINE_SNAP_THRESHOLD = 1e-3;
1980
+ var RANGE_LERP_SPEED = 0.15;
1981
+ var RANGE_ADAPTIVE_BOOST = 0.2;
1982
+ var CANDLE_BUFFER = 0.05;
1234
1983
  function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
1235
1984
  const valGap = Math.abs(value - displayValue);
1236
1985
  const prevRange = displayMax - displayMin || 1;
@@ -1430,10 +2179,121 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
1430
2179
  }
1431
2180
  return badgeY;
1432
2181
  }
2182
+ function computeCandleRange(candles) {
2183
+ let min = Infinity;
2184
+ let max = -Infinity;
2185
+ for (const c of candles) {
2186
+ if (c.low < min) min = c.low;
2187
+ if (c.high > max) max = c.high;
2188
+ }
2189
+ if (!isFinite(min) || !isFinite(max)) return { min: 99, max: 101 };
2190
+ const range = max - min;
2191
+ const margin = range * 0.12;
2192
+ const minRange = range * 0.1 || 0.4;
2193
+ if (range < minRange) {
2194
+ const mid = (min + max) / 2;
2195
+ return { min: mid - minRange / 2, max: mid + minRange / 2 };
2196
+ }
2197
+ return { min: min - margin, max: max + margin };
2198
+ }
2199
+ function candleAtX(candles, hoverX, candleWidth, layout) {
2200
+ const time = layout.leftEdge + (hoverX - layout.pad.left) / layout.chartW * (layout.rightEdge - layout.leftEdge);
2201
+ let lo = 0;
2202
+ let hi = candles.length - 1;
2203
+ while (lo <= hi) {
2204
+ const mid = lo + hi >> 1;
2205
+ const c = candles[mid];
2206
+ if (time < c.time) hi = mid - 1;
2207
+ else if (time >= c.time + candleWidth) lo = mid + 1;
2208
+ else return c;
2209
+ }
2210
+ return null;
2211
+ }
2212
+ function updateCandleRange(computedRange, rangeInited, displayMin, displayMax, isTransitioning, windowTransProgress, wt, chartH, dt) {
2213
+ if (!rangeInited) {
2214
+ return {
2215
+ minVal: computedRange.min,
2216
+ maxVal: computedRange.max,
2217
+ valRange: computedRange.max - computedRange.min || 1e-3,
2218
+ displayMin: computedRange.min,
2219
+ displayMax: computedRange.max,
2220
+ rangeInited: true
2221
+ };
2222
+ }
2223
+ if (isTransitioning) {
2224
+ displayMin = wt.rangeFromMin + (wt.rangeToMin - wt.rangeFromMin) * windowTransProgress;
2225
+ displayMax = wt.rangeFromMax + (wt.rangeToMax - wt.rangeFromMax) * windowTransProgress;
2226
+ } else {
2227
+ const curRange = displayMax - displayMin || 1;
2228
+ const gapMin = Math.abs(displayMin - computedRange.min);
2229
+ const gapMax = Math.abs(displayMax - computedRange.max);
2230
+ const gapRatio = Math.min((gapMin + gapMax) / curRange, 1);
2231
+ const speed = RANGE_LERP_SPEED + (1 - gapRatio) * RANGE_ADAPTIVE_BOOST;
2232
+ displayMin = lerp(displayMin, computedRange.min, speed, dt);
2233
+ displayMax = lerp(displayMax, computedRange.max, speed, dt);
2234
+ const pxThreshold = 0.5 * curRange / chartH || 1e-3;
2235
+ if (Math.abs(displayMin - computedRange.min) < pxThreshold) displayMin = computedRange.min;
2236
+ if (Math.abs(displayMax - computedRange.max) < pxThreshold) displayMax = computedRange.max;
2237
+ }
2238
+ return {
2239
+ minVal: displayMin,
2240
+ maxVal: displayMax,
2241
+ valRange: displayMax - displayMin || 1e-3,
2242
+ displayMin,
2243
+ displayMax,
2244
+ rangeInited: true
2245
+ };
2246
+ }
2247
+ function updateCandleWindowTransition(targetWindowSecs, wt, displayWindow, displayMin, displayMax, now_ms, now, candles, liveCandle, candleWidth, buffer) {
2248
+ if (wt.to !== targetWindowSecs) {
2249
+ wt.from = displayWindow;
2250
+ wt.to = targetWindowSecs;
2251
+ wt.startMs = now_ms;
2252
+ wt.rangeFromMin = displayMin;
2253
+ wt.rangeFromMax = displayMax;
2254
+ const targetRightEdge = now + targetWindowSecs * buffer;
2255
+ const targetLeftEdge = targetRightEdge - targetWindowSecs;
2256
+ const targetVisible = [];
2257
+ for (const c of candles) {
2258
+ if (c.time + candleWidth >= targetLeftEdge && c.time <= targetRightEdge) {
2259
+ targetVisible.push(c);
2260
+ }
2261
+ }
2262
+ if (liveCandle && liveCandle.time + candleWidth >= targetLeftEdge && liveCandle.time <= targetRightEdge) {
2263
+ targetVisible.push(liveCandle);
2264
+ }
2265
+ if (targetVisible.length > 0) {
2266
+ const tr = computeCandleRange(targetVisible);
2267
+ wt.rangeToMin = tr.min;
2268
+ wt.rangeToMax = tr.max;
2269
+ }
2270
+ }
2271
+ let windowTransProgress = 0;
2272
+ let resultWindow;
2273
+ if (wt.startMs === 0) {
2274
+ resultWindow = targetWindowSecs;
2275
+ } else {
2276
+ const elapsed = now_ms - wt.startMs;
2277
+ const t = Math.min(elapsed / WINDOW_TRANSITION_MS, 1);
2278
+ const eased = (1 - Math.cos(t * Math.PI)) / 2;
2279
+ windowTransProgress = eased;
2280
+ const logFrom = Math.log(wt.from);
2281
+ const logTo = Math.log(wt.to);
2282
+ resultWindow = Math.exp(logFrom + (logTo - logFrom) * eased);
2283
+ if (t >= 1) {
2284
+ resultWindow = targetWindowSecs;
2285
+ wt.startMs = 0;
2286
+ windowTransProgress = 0;
2287
+ }
2288
+ }
2289
+ return { windowSecs: resultWindow, windowTransProgress };
2290
+ }
1433
2291
  function useLivelineEngine(canvasRef, containerRef, config) {
1434
2292
  const configRef = useRef(config);
1435
2293
  configRef.current = config;
1436
2294
  const displayValueRef = useRef(config.value);
2295
+ const displayValuesRef = useRef(/* @__PURE__ */ new Map());
2296
+ const seriesAlphaRef = useRef(/* @__PURE__ */ new Map());
1437
2297
  const displayMinRef = useRef(0);
1438
2298
  const displayMaxRef = useRef(0);
1439
2299
  const targetMinRef = useRef(0);
@@ -1466,13 +2326,49 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1466
2326
  const hoverXRef = useRef(null);
1467
2327
  const scrubAmountRef = useRef(0);
1468
2328
  const lastHoverRef = useRef(null);
2329
+ const lastHoverEntriesRef = useRef([]);
1469
2330
  const chartRevealRef = useRef(0);
1470
2331
  const pauseProgressRef = useRef(0);
1471
2332
  const timeDebtRef = useRef(0);
1472
2333
  const lastDataRef = useRef([]);
2334
+ const lastMultiSeriesRef = useRef([]);
1473
2335
  const frozenNowRef = useRef(0);
1474
2336
  const pausedDataRef = useRef(null);
2337
+ const pausedMultiDataRef = useRef(null);
1475
2338
  const loadingAlphaRef = useRef(config.loading ? 1 : 0);
2339
+ const displayCandleRef = useRef(null);
2340
+ const liveBirthAlphaRef = useRef(1);
2341
+ const liveBullRef = useRef(0.5);
2342
+ const lineSmoothCloseRef = useRef(0);
2343
+ const lineSmoothInitedRef = useRef(false);
2344
+ const closeLineSmoothRef = useRef(0);
2345
+ const closeLineSmoothInitedRef = useRef(false);
2346
+ const lineModeProgRef = useRef(0);
2347
+ const lineModeTransRef = useRef({ startMs: 0, from: 0, to: 0 });
2348
+ const lineDensityProgRef = useRef(0);
2349
+ const lineDensityTransRef = useRef({ startMs: 0, from: 0, to: 0 });
2350
+ const lineTickSmoothRef = useRef(0);
2351
+ const lineTickSmoothInitedRef = useRef(false);
2352
+ const candleWidthTransRef = useRef({
2353
+ fromWidth: config.candleWidth ?? 1,
2354
+ toWidth: config.candleWidth ?? 1,
2355
+ startMs: 0,
2356
+ rangeFromMin: 0,
2357
+ rangeFromMax: 0,
2358
+ rangeToMin: 0,
2359
+ rangeToMax: 0,
2360
+ oldCandles: [],
2361
+ oldWidth: config.candleWidth ?? 1
2362
+ });
2363
+ const prevCandleDataRef = useRef({ candles: [], width: config.candleWidth ?? 1 });
2364
+ const pausedCandlesRef = useRef(null);
2365
+ const pausedLiveRef = useRef(null);
2366
+ const pausedLineDataRef = useRef(null);
2367
+ const pausedLineValueRef = useRef(null);
2368
+ const lastCandlesRef = useRef([]);
2369
+ const lastLiveRef = useRef(null);
2370
+ const lastLineDataStashRef = useRef([]);
2371
+ const lastLineValueStashRef = useRef(void 0);
1476
2372
  useEffect(() => {
1477
2373
  const container = containerRef.current;
1478
2374
  if (!container) return;
@@ -1604,14 +2500,43 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1604
2500
  }
1605
2501
  applyDpr(ctx, dpr, w, h);
1606
2502
  const noMotion = reducedMotionRef.current;
1607
- if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
1608
- pausedDataRef.current = cfg.data.slice();
1609
- }
1610
- if (!cfg.paused) {
1611
- pausedDataRef.current = null;
2503
+ const isCandle = cfg.mode === "candle";
2504
+ if (isCandle) {
2505
+ if (cfg.paused && pausedCandlesRef.current === null && (cfg.candles?.length ?? 0) > 0) {
2506
+ pausedCandlesRef.current = cfg.candles.slice();
2507
+ pausedLiveRef.current = cfg.liveCandle ?? null;
2508
+ pausedLineDataRef.current = cfg.lineData?.slice() ?? null;
2509
+ pausedLineValueRef.current = cfg.lineValue ?? null;
2510
+ }
2511
+ if (!cfg.paused) {
2512
+ pausedCandlesRef.current = null;
2513
+ pausedLiveRef.current = null;
2514
+ pausedLineDataRef.current = null;
2515
+ pausedLineValueRef.current = null;
2516
+ }
2517
+ } else if (cfg.isMultiSeries && cfg.multiSeries) {
2518
+ if (cfg.paused && pausedMultiDataRef.current === null) {
2519
+ const snap = /* @__PURE__ */ new Map();
2520
+ for (const s of cfg.multiSeries) {
2521
+ if (s.data.length >= 2) snap.set(s.id, { data: s.data.slice(), value: s.value });
2522
+ }
2523
+ if (snap.size > 0) pausedMultiDataRef.current = snap;
2524
+ }
2525
+ if (!cfg.paused) {
2526
+ pausedMultiDataRef.current = null;
2527
+ }
2528
+ } else {
2529
+ if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
2530
+ pausedDataRef.current = cfg.data.slice();
2531
+ }
2532
+ if (!cfg.paused) {
2533
+ pausedDataRef.current = null;
2534
+ }
1612
2535
  }
1613
- const points = pausedDataRef.current ?? cfg.data;
1614
- const hasData = points.length >= 2;
2536
+ const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
2537
+ const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
2538
+ const hasMultiData = cfg.isMultiSeries && cfg.multiSeries ? cfg.multiSeries.some((s) => s.data.length >= 2) : false;
2539
+ const hasData = isCandle ? effectiveCandles.length >= 2 : hasMultiData || points.length >= 2;
1615
2540
  const pad = cfg.padding;
1616
2541
  const chartH = h - pad.top - pad.bottom;
1617
2542
  const pauseTarget = cfg.paused ? 1 : 0;
@@ -1633,18 +2558,62 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1633
2558
  if (loadingAlphaRef.current > 0.99) loadingAlphaRef.current = 1;
1634
2559
  const loadingAlpha = loadingAlphaRef.current;
1635
2560
  const revealTarget = !cfg.loading && hasData ? 1 : 0;
1636
- chartRevealRef.current = noMotion ? revealTarget : lerp(chartRevealRef.current, revealTarget, CHART_REVEAL_SPEED, dt);
2561
+ chartRevealRef.current = noMotion ? revealTarget : lerp(
2562
+ chartRevealRef.current,
2563
+ revealTarget,
2564
+ revealTarget === 1 ? CHART_REVEAL_SPEED_FWD : CHART_REVEAL_SPEED,
2565
+ dt
2566
+ );
1637
2567
  if (Math.abs(chartRevealRef.current - revealTarget) < 5e-3) {
1638
2568
  chartRevealRef.current = revealTarget;
1639
2569
  }
1640
2570
  const chartReveal = chartRevealRef.current;
1641
- const useStash = !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
1642
- if (hasData) {
1643
- lastDataRef.current = points;
2571
+ if (chartReveal < 0.01) {
2572
+ rangeInitedRef.current = false;
2573
+ }
2574
+ let useStash;
2575
+ let useMultiStash = false;
2576
+ if (isCandle) {
2577
+ useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
2578
+ } else {
2579
+ useMultiStash = !hasData && chartReveal > 5e-3 && lastMultiSeriesRef.current.length > 0;
2580
+ if (hasMultiData && cfg.multiSeries) {
2581
+ lastMultiSeriesRef.current = cfg.multiSeries.map((s) => ({
2582
+ id: s.id,
2583
+ data: s.data.slice(),
2584
+ value: s.value,
2585
+ palette: s.palette,
2586
+ label: s.label
2587
+ }));
2588
+ }
2589
+ if (hasData && !cfg.isMultiSeries) lastMultiSeriesRef.current = [];
2590
+ useStash = !useMultiStash && !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
2591
+ if (hasData && !cfg.isMultiSeries) lastDataRef.current = points;
1644
2592
  }
1645
- if (!hasData && !useStash) {
2593
+ if (isCandle) {
2594
+ const lmt = lineModeTransRef.current;
2595
+ const lineModeTarget = cfg.lineMode ? 1 : 0;
2596
+ if (lmt.to !== lineModeTarget) {
2597
+ lmt.from = lineModeProgRef.current;
2598
+ lmt.to = lineModeTarget;
2599
+ lmt.startMs = now_ms;
2600
+ }
2601
+ if (lmt.startMs > 0) {
2602
+ const elapsed = now_ms - lmt.startMs;
2603
+ const t = Math.min(elapsed / LINE_MORPH_MS, 1);
2604
+ lineModeProgRef.current = lmt.from + (lmt.to - lmt.from) * ((1 - Math.cos(t * Math.PI)) / 2);
2605
+ if (t >= 1) {
2606
+ lineModeProgRef.current = lmt.to;
2607
+ lmt.startMs = 0;
2608
+ }
2609
+ } else {
2610
+ lineModeProgRef.current = lmt.to;
2611
+ }
2612
+ }
2613
+ if (!hasData && !useStash && !useMultiStash) {
2614
+ const loadingColor = isCandle || cfg.isMultiSeries || lastMultiSeriesRef.current.length > 0 ? cfg.palette.gridLabel : void 0;
1646
2615
  if (loadingAlpha > 0.01) {
1647
- drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha);
2616
+ drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
1648
2617
  }
1649
2618
  if (1 - loadingAlpha > 0.01) {
1650
2619
  drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
@@ -1661,190 +2630,840 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1661
2630
  rafRef.current = requestAnimationFrame(draw);
1662
2631
  return;
1663
2632
  }
1664
- const effectivePoints = useStash ? lastDataRef.current : points;
1665
- const adaptiveSpeed = computeAdaptiveSpeed(
1666
- cfg.value,
1667
- displayValueRef.current,
1668
- displayMinRef.current,
1669
- displayMaxRef.current,
1670
- cfg.lerpSpeed,
1671
- noMotion
1672
- );
1673
- if (!useStash) {
1674
- displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
1675
- if (pauseProgress < 0.5) {
1676
- const prevRange = displayMaxRef.current - displayMinRef.current || 1;
1677
- if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
1678
- displayValueRef.current = cfg.value;
2633
+ if (isCandle) {
2634
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
2635
+ const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
2636
+ const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
2637
+ let effectiveLineData = pausedLineDataRef.current ?? cfg.lineData;
2638
+ let effectiveLineValue = pausedLineValueRef.current ?? cfg.lineValue;
2639
+ if (hasData && effectiveLineData && effectiveLineData.length > 0) {
2640
+ lastLineDataStashRef.current = effectiveLineData;
2641
+ lastLineValueStashRef.current = effectiveLineValue;
2642
+ }
2643
+ if (useStash && lastLineDataStashRef.current.length > 0) {
2644
+ effectiveLineData = lastLineDataStashRef.current;
2645
+ effectiveLineValue = lastLineValueStashRef.current;
2646
+ }
2647
+ const candleWidthSecs = cfg.candleWidth ?? 1;
2648
+ const cwt = candleWidthTransRef.current;
2649
+ let morphT = -1;
2650
+ let displayCandleWidth;
2651
+ if (cwt.startMs > 0) {
2652
+ const elapsed = now_ms - cwt.startMs;
2653
+ const t = Math.min(elapsed / CANDLE_WIDTH_TRANS_MS, 1);
2654
+ morphT = (1 - Math.cos(t * Math.PI)) / 2;
2655
+ displayCandleWidth = Math.exp(
2656
+ Math.log(cwt.fromWidth) + (Math.log(cwt.toWidth) - Math.log(cwt.fromWidth)) * morphT
2657
+ );
2658
+ if (t >= 1) {
2659
+ displayCandleWidth = cwt.toWidth;
2660
+ cwt.startMs = 0;
2661
+ morphT = -1;
2662
+ }
2663
+ } else {
2664
+ displayCandleWidth = cwt.toWidth;
2665
+ }
2666
+ if (candleWidthSecs !== cwt.toWidth) {
2667
+ cwt.oldCandles = prevCandleDataRef.current.candles;
2668
+ cwt.oldWidth = prevCandleDataRef.current.width;
2669
+ cwt.fromWidth = displayCandleWidth;
2670
+ cwt.toWidth = candleWidthSecs;
2671
+ cwt.startMs = now_ms;
2672
+ morphT = 0;
2673
+ cwt.rangeFromMin = displayMinRef.current;
2674
+ cwt.rangeFromMax = displayMaxRef.current;
2675
+ const curWindow = displayWindowRef.current;
2676
+ const re = now + curWindow * CANDLE_BUFFER;
2677
+ const le = re - curWindow;
2678
+ const targetVis = [];
2679
+ for (const c of effectiveCandles) {
2680
+ if (c.time + candleWidthSecs >= le && c.time <= re) targetVis.push(c);
2681
+ }
2682
+ if (rawLive) targetVis.push(rawLive);
2683
+ if (targetVis.length > 0) {
2684
+ const tr = computeCandleRange(targetVis);
2685
+ cwt.rangeToMin = tr.min;
2686
+ cwt.rangeToMax = tr.max;
2687
+ } else {
2688
+ cwt.rangeToMin = displayMinRef.current;
2689
+ cwt.rangeToMax = displayMaxRef.current;
2690
+ }
2691
+ }
2692
+ prevCandleDataRef.current = { candles: cfg.candles ?? [], width: candleWidthSecs };
2693
+ const lineModeProg = lineModeProgRef.current;
2694
+ const ldt = lineDensityTransRef.current;
2695
+ const hasTickData = effectiveLineData && effectiveLineData.length > 0;
2696
+ const densityTarget = cfg.lineMode && lineModeProg >= 0.3 && hasTickData ? 1 : 0;
2697
+ if (ldt.to !== densityTarget) {
2698
+ ldt.from = lineDensityProgRef.current;
2699
+ ldt.to = densityTarget;
2700
+ ldt.startMs = now_ms;
2701
+ }
2702
+ let lineDensityProg;
2703
+ if (ldt.startMs > 0) {
2704
+ const elapsed = now_ms - ldt.startMs;
2705
+ const t = Math.min(elapsed / LINE_DENSITY_MS, 1);
2706
+ lineDensityProg = ldt.from + (ldt.to - ldt.from) * (1 - (1 - t) * (1 - t));
2707
+ if (t >= 1) {
2708
+ lineDensityProg = ldt.to;
2709
+ ldt.startMs = 0;
2710
+ }
2711
+ } else {
2712
+ lineDensityProg = ldt.to;
2713
+ }
2714
+ lineDensityProgRef.current = lineDensityProg;
2715
+ const transition = windowTransitionRef.current;
2716
+ const windowResult = updateCandleWindowTransition(
2717
+ cfg.windowSecs,
2718
+ transition,
2719
+ displayWindowRef.current,
2720
+ displayMinRef.current,
2721
+ displayMaxRef.current,
2722
+ now_ms,
2723
+ now,
2724
+ effectiveCandles,
2725
+ rawLive,
2726
+ candleWidthSecs,
2727
+ CANDLE_BUFFER
2728
+ );
2729
+ displayWindowRef.current = windowResult.windowSecs;
2730
+ const windowSecs = windowResult.windowSecs;
2731
+ const windowTransProgress = windowResult.windowTransProgress;
2732
+ const isWindowTransitioning = transition.startMs > 0;
2733
+ const rightEdge = now + windowSecs * CANDLE_BUFFER;
2734
+ const leftEdge = rightEdge - windowSecs;
2735
+ let smoothLive;
2736
+ if (rawLive) {
2737
+ const prev = displayCandleRef.current;
2738
+ if (!prev || prev.time !== rawLive.time) {
2739
+ displayCandleRef.current = {
2740
+ time: rawLive.time,
2741
+ open: rawLive.open,
2742
+ high: rawLive.open,
2743
+ low: rawLive.open,
2744
+ close: rawLive.open
2745
+ };
2746
+ liveBirthAlphaRef.current = 0;
2747
+ } else {
2748
+ const dc2 = displayCandleRef.current;
2749
+ dc2.open = lerp(dc2.open, rawLive.open, CANDLE_LERP_SPEED, pausedDt);
2750
+ dc2.high = lerp(dc2.high, rawLive.high, CANDLE_LERP_SPEED, pausedDt);
2751
+ dc2.low = lerp(dc2.low, rawLive.low, CANDLE_LERP_SPEED, pausedDt);
2752
+ dc2.close = lerp(dc2.close, rawLive.close, CANDLE_LERP_SPEED, pausedDt);
2753
+ }
2754
+ liveBirthAlphaRef.current = lerp(liveBirthAlphaRef.current, 1, 0.2, pausedDt);
2755
+ if (liveBirthAlphaRef.current > 0.99) liveBirthAlphaRef.current = 1;
2756
+ const dc = displayCandleRef.current;
2757
+ const bullTarget = dc.close >= dc.open ? 1 : 0;
2758
+ liveBullRef.current = lerp(liveBullRef.current, bullTarget, 0.12, pausedDt);
2759
+ if (liveBullRef.current > 0.99) liveBullRef.current = 1;
2760
+ if (liveBullRef.current < 0.01) liveBullRef.current = 0;
2761
+ smoothLive = dc;
2762
+ } else {
2763
+ displayCandleRef.current = null;
2764
+ liveBirthAlphaRef.current = 1;
2765
+ liveBullRef.current = 0.5;
2766
+ }
2767
+ if (rawLive) {
2768
+ if (!closeLineSmoothInitedRef.current) {
2769
+ closeLineSmoothRef.current = rawLive.close;
2770
+ closeLineSmoothInitedRef.current = true;
2771
+ } else {
2772
+ closeLineSmoothRef.current = lerp(closeLineSmoothRef.current, rawLive.close, CLOSE_LINE_LERP_SPEED, pausedDt);
2773
+ const gap = Math.abs(closeLineSmoothRef.current - rawLive.close);
2774
+ const range = displayMaxRef.current - displayMinRef.current || 1;
2775
+ if (gap < range * 5e-4) closeLineSmoothRef.current = rawLive.close;
2776
+ }
2777
+ } else if (!useStash) {
2778
+ closeLineSmoothInitedRef.current = false;
2779
+ }
2780
+ if (rawLive) {
2781
+ if (!lineSmoothInitedRef.current) {
2782
+ lineSmoothCloseRef.current = rawLive.close;
2783
+ lineSmoothInitedRef.current = true;
2784
+ } else {
2785
+ const valGap = Math.abs(rawLive.close - lineSmoothCloseRef.current);
2786
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2787
+ const gapRatio = Math.min(valGap / prevRange, 1);
2788
+ const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
2789
+ lineSmoothCloseRef.current = lerp(lineSmoothCloseRef.current, rawLive.close, adaptiveSpeed, pausedDt);
2790
+ if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineSmoothCloseRef.current = rawLive.close;
2791
+ }
2792
+ } else if (!useStash) {
2793
+ lineSmoothInitedRef.current = false;
2794
+ }
2795
+ if (effectiveLineValue !== void 0 && hasTickData) {
2796
+ if (!lineTickSmoothInitedRef.current) {
2797
+ lineTickSmoothRef.current = effectiveLineValue;
2798
+ lineTickSmoothInitedRef.current = true;
2799
+ } else {
2800
+ const valGap = Math.abs(effectiveLineValue - lineTickSmoothRef.current);
2801
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2802
+ const gapRatio = Math.min(valGap / prevRange, 1);
2803
+ const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
2804
+ lineTickSmoothRef.current = lerp(lineTickSmoothRef.current, effectiveLineValue, adaptiveSpeed, pausedDt);
2805
+ if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineTickSmoothRef.current = effectiveLineValue;
2806
+ }
2807
+ } else if (!useStash) {
2808
+ lineTickSmoothInitedRef.current = false;
2809
+ }
2810
+ const visible = [];
2811
+ for (const c of effectiveCandles) {
2812
+ if (c.time + candleWidthSecs >= leftEdge && c.time <= rightEdge) visible.push(c);
2813
+ }
2814
+ if (smoothLive && smoothLive.time + displayCandleWidth >= leftEdge && smoothLive.time <= rightEdge) {
2815
+ visible.push(smoothLive);
2816
+ }
2817
+ let oldVisible = [];
2818
+ if (morphT >= 0 && cwt.oldCandles.length > 0) {
2819
+ for (const c of cwt.oldCandles) {
2820
+ if (c.time + cwt.oldWidth >= leftEdge && c.time <= rightEdge) oldVisible.push(c);
2821
+ }
2822
+ }
2823
+ if (hasData) {
2824
+ lastCandlesRef.current = visible;
2825
+ lastLiveRef.current = smoothLive ?? null;
2826
+ }
2827
+ const effectiveVisible = useStash ? lastCandlesRef.current : visible;
2828
+ const effectiveLive = useStash ? lastLiveRef.current ?? void 0 : smoothLive;
2829
+ const chartW = w - pad.left - pad.right;
2830
+ const computed = effectiveVisible.length > 0 ? computeCandleRange(effectiveVisible) : { min: displayMinRef.current, max: displayMaxRef.current };
2831
+ const rangeResult = updateCandleRange(
2832
+ computed,
2833
+ rangeInitedRef.current,
2834
+ displayMinRef.current,
2835
+ displayMaxRef.current,
2836
+ isWindowTransitioning,
2837
+ windowTransProgress,
2838
+ transition,
2839
+ chartH,
2840
+ pausedDt
2841
+ );
2842
+ if (morphT >= 0) {
2843
+ rangeResult.displayMin = cwt.rangeFromMin + (cwt.rangeToMin - cwt.rangeFromMin) * morphT;
2844
+ rangeResult.displayMax = cwt.rangeFromMax + (cwt.rangeToMax - cwt.rangeFromMax) * morphT;
2845
+ rangeResult.minVal = rangeResult.displayMin;
2846
+ rangeResult.maxVal = rangeResult.displayMax;
2847
+ rangeResult.valRange = rangeResult.displayMax - rangeResult.displayMin || 1e-3;
2848
+ }
2849
+ rangeInitedRef.current = rangeResult.rangeInited;
2850
+ displayMinRef.current = rangeResult.displayMin;
2851
+ displayMaxRef.current = rangeResult.displayMax;
2852
+ const { minVal, maxVal, valRange } = rangeResult;
2853
+ const layout = {
2854
+ w,
2855
+ h,
2856
+ pad,
2857
+ chartW,
2858
+ chartH,
2859
+ leftEdge,
2860
+ rightEdge,
2861
+ minVal,
2862
+ maxVal,
2863
+ valRange,
2864
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
2865
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
2866
+ };
2867
+ const hoverPx = hoverXRef.current;
2868
+ let hoveredCandle = null;
2869
+ let isActiveHover = false;
2870
+ if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
2871
+ hoveredCandle = candleAtX(effectiveVisible, hoverPx, displayCandleWidth, layout);
2872
+ if (hoveredCandle) isActiveHover = true;
2873
+ }
2874
+ const scrubTarget = isActiveHover ? 1 : 0;
2875
+ scrubAmountRef.current = lerp(scrubAmountRef.current, scrubTarget, 0.12, dt);
2876
+ if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
2877
+ if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
2878
+ const scrubAmount = scrubAmountRef.current;
2879
+ let drawHoverX = hoverPx;
2880
+ let drawHoverTime = 0;
2881
+ let drawHoverCandle = hoveredCandle;
2882
+ if (!isActiveHover && scrubAmount > 0 && lastHoverRef.current) {
2883
+ drawHoverX = lastHoverRef.current.x;
2884
+ drawHoverTime = lastHoverRef.current.time;
2885
+ drawHoverCandle = candleAtX(effectiveVisible, lastHoverRef.current.x, displayCandleWidth, layout);
2886
+ } else if (isActiveHover && hoverPx !== null) {
2887
+ drawHoverTime = layout.leftEdge + (hoverPx - pad.left) / chartW * (layout.rightEdge - layout.leftEdge);
2888
+ lastHoverRef.current = { x: hoverPx, value: hoveredCandle?.close ?? 0, time: drawHoverTime };
2889
+ }
2890
+ let drawCandles = effectiveVisible;
2891
+ let drawOldCandles = oldVisible;
2892
+ let drawLive = effectiveLive;
2893
+ if (lineModeProg > 0.01 && drawLive && lineSmoothInitedRef.current) {
2894
+ const blended = drawLive.close + (lineSmoothCloseRef.current - drawLive.close) * lineModeProg;
2895
+ drawLive = { ...drawLive, close: blended };
2896
+ const li = drawCandles.length - 1;
2897
+ if (li >= 0 && drawCandles[li].time === drawLive.time) {
2898
+ drawCandles = drawCandles.slice();
2899
+ drawCandles[li] = { ...drawCandles[li], close: blended };
2900
+ }
2901
+ }
2902
+ if (lineModeProg > 0.01 && lineModeProg < 0.99) {
2903
+ const collapseOHLC = (c) => {
2904
+ const inv = 1 - lineModeProg;
2905
+ return {
2906
+ time: c.time,
2907
+ open: c.close + (c.open - c.close) * inv,
2908
+ high: c.close + (c.high - c.close) * inv,
2909
+ low: c.close + (c.low - c.close) * inv,
2910
+ close: c.close
2911
+ };
2912
+ };
2913
+ drawCandles = drawCandles.map(collapseOHLC);
2914
+ if (drawOldCandles.length > 0) drawOldCandles = drawOldCandles.map(collapseOHLC);
2915
+ if (drawLive) drawLive = collapseOHLC(drawLive);
2916
+ }
2917
+ let lineVisible;
2918
+ let lineSmoothValue;
2919
+ if (effectiveLineData && effectiveLineData.length > 0 && (lineDensityProg > 0.01 || lineModeProg > 0.05)) {
2920
+ const closeRefs = [];
2921
+ for (const c of drawCandles) {
2922
+ closeRefs.push({ t: c.time + displayCandleWidth / 2, v: c.close });
2923
+ }
2924
+ if (drawLive) closeRefs.push({ t: now, v: drawLive.close });
2925
+ lineVisible = [];
2926
+ let refIdx = 0;
2927
+ for (const pt of effectiveLineData) {
2928
+ if (pt.time < leftEdge || pt.time > rightEdge) continue;
2929
+ while (refIdx < closeRefs.length - 2 && closeRefs[refIdx + 1].t < pt.time) refIdx++;
2930
+ let interpClose;
2931
+ if (closeRefs.length === 0) {
2932
+ interpClose = pt.value;
2933
+ } else if (closeRefs.length === 1 || pt.time <= closeRefs[0].t) {
2934
+ interpClose = closeRefs[0].v;
2935
+ } else if (refIdx >= closeRefs.length - 1) {
2936
+ interpClose = closeRefs[closeRefs.length - 1].v;
2937
+ } else {
2938
+ const a = closeRefs[refIdx];
2939
+ const b = closeRefs[refIdx + 1];
2940
+ const span = b.t - a.t;
2941
+ const frac = span > 0 ? Math.max(0, Math.min(1, (pt.time - a.t) / span)) : 0;
2942
+ interpClose = a.v + (b.v - a.v) * frac;
2943
+ }
2944
+ const blended = interpClose + (pt.value - interpClose) * lineDensityProg;
2945
+ lineVisible.push({ time: pt.time, value: blended });
2946
+ }
2947
+ const smoothTick = lineTickSmoothInitedRef.current ? lineTickSmoothRef.current : effectiveLineValue ?? effectiveLineData[effectiveLineData.length - 1].value;
2948
+ lineSmoothValue = lineSmoothCloseRef.current + (smoothTick - lineSmoothCloseRef.current) * lineDensityProg;
2949
+ } else {
2950
+ lineVisible = drawCandles.map((c) => ({
2951
+ time: c.time + displayCandleWidth / 2,
2952
+ value: c.close
2953
+ }));
2954
+ lineSmoothValue = lineSmoothInitedRef.current ? lineSmoothCloseRef.current : drawLive?.close ?? drawCandles[drawCandles.length - 1]?.close ?? 0;
2955
+ }
2956
+ if (chartReveal < 1 && lineVisible.length >= 2) {
2957
+ const firstTime = lineVisible[0].time;
2958
+ const windowSpan = rightEdge - leftEdge;
2959
+ if (firstTime - leftEdge > windowSpan * 0.05) {
2960
+ const firstVal = lineVisible[0].value;
2961
+ const step = windowSpan / 32;
2962
+ const padded = [];
2963
+ for (let t = leftEdge; t < firstTime - step * 0.5; t += step) {
2964
+ padded.push({ time: t, value: firstVal });
2965
+ }
2966
+ lineVisible = [...padded, ...lineVisible];
2967
+ }
2968
+ }
2969
+ drawCandleFrame(ctx, layout, cfg.palette, {
2970
+ candles: drawCandles,
2971
+ displayCandleWidth,
2972
+ oldCandles: drawOldCandles,
2973
+ oldWidth: cwt.oldWidth,
2974
+ morphT,
2975
+ liveCandle: drawLive,
2976
+ closePriceCandle: closeLineSmoothInitedRef.current && rawLive ? { ...rawLive, close: closeLineSmoothRef.current } : rawLive,
2977
+ liveTime: effectiveLive?.time ?? -1,
2978
+ liveBirthAlpha: liveBirthAlphaRef.current,
2979
+ liveBullBlend: liveBullRef.current,
2980
+ lineModeProg,
2981
+ chartReveal,
2982
+ now_ms,
2983
+ now,
2984
+ pauseProgress,
2985
+ showGrid: cfg.showGrid,
2986
+ scrubAmount,
2987
+ hoverX: drawHoverX,
2988
+ hoverValue: drawHoverCandle?.close ?? null,
2989
+ hoverTime: drawHoverTime,
2990
+ hoveredCandle: drawHoverCandle,
2991
+ formatValue: cfg.formatValue,
2992
+ formatTime: cfg.formatTime,
2993
+ gridState: gridStateRef.current,
2994
+ timeAxisState: timeAxisStateRef.current,
2995
+ dt: pausedDt,
2996
+ targetWindowSecs: cfg.windowSecs,
2997
+ tooltipY: cfg.tooltipY,
2998
+ tooltipOutline: cfg.tooltipOutline,
2999
+ lineVisible,
3000
+ lineSmoothValue,
3001
+ emptyText: cfg.emptyText,
3002
+ loadingAlpha,
3003
+ // Show empty overlay when not loading AND loadingAlpha has fully
3004
+ // decayed. This prevents the gradient gap from flashing during
3005
+ // loading→live (where loadingAlpha starts at ~1), while still
3006
+ // allowing smooth fade-out during empty→live (loadingAlpha is 0).
3007
+ showEmptyOverlay: !(cfg.loading ?? false) && loadingAlpha < 0.01
3008
+ });
3009
+ if (badgeRef.current) {
3010
+ if (lineModeProg > 0.5 && cfg.showBadge) {
3011
+ const momentum = detectMomentum(lineVisible);
3012
+ badgeYRef.current = updateBadgeDOM(
3013
+ badgeRef.current,
3014
+ cfg,
3015
+ lineSmoothValue,
3016
+ layout,
3017
+ momentum,
3018
+ badgeYRef.current,
3019
+ badgeColorRef.current,
3020
+ isWindowTransitioning,
3021
+ noMotion,
3022
+ ctx,
3023
+ pausedDt,
3024
+ chartReveal
3025
+ );
3026
+ const badgeFade = (lineModeProg - 0.5) * 2;
3027
+ if (badgeRef.current.container.style.display !== "none") {
3028
+ const base = badgeRef.current.container.style.opacity ? parseFloat(badgeRef.current.container.style.opacity) : 1;
3029
+ badgeRef.current.container.style.opacity = String(
3030
+ base * badgeFade * (1 - pauseProgress)
3031
+ );
3032
+ }
3033
+ } else {
3034
+ badgeRef.current.container.style.display = "none";
3035
+ }
3036
+ }
3037
+ } else if (cfg.isMultiSeries && cfg.multiSeries && cfg.multiSeries.length > 0 || useMultiStash) {
3038
+ const effectiveMultiSeries = useMultiStash ? lastMultiSeriesRef.current : cfg.multiSeries;
3039
+ let labelReserve = 0;
3040
+ if (effectiveMultiSeries.some((s) => s.label)) {
3041
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
3042
+ let maxLabelW = 0;
3043
+ for (const s of effectiveMultiSeries) {
3044
+ if (s.label) {
3045
+ const lw = ctx.measureText(s.label).width;
3046
+ if (lw > maxLabelW) maxLabelW = lw;
3047
+ }
3048
+ }
3049
+ labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
3050
+ }
3051
+ const chartW = w - pad.left - pad.right - labelReserve;
3052
+ const buffer = WINDOW_BUFFER;
3053
+ if (!useMultiStash) {
3054
+ const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
3055
+ for (const key of displayValuesRef.current.keys()) {
3056
+ if (!currentIds.has(key)) displayValuesRef.current.delete(key);
3057
+ }
3058
+ }
3059
+ const firstSeries = effectiveMultiSeries[0];
3060
+ const transition = windowTransitionRef.current;
3061
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
3062
+ const now = useMultiStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
3063
+ const smoothValues = /* @__PURE__ */ new Map();
3064
+ for (const s of effectiveMultiSeries) {
3065
+ let dv = displayValuesRef.current.get(s.id);
3066
+ if (dv === void 0) dv = s.value;
3067
+ if (!useMultiStash) {
3068
+ const adaptiveSpeed2 = computeAdaptiveSpeed(
3069
+ s.value,
3070
+ dv,
3071
+ displayMinRef.current,
3072
+ displayMaxRef.current,
3073
+ cfg.lerpSpeed,
3074
+ noMotion
3075
+ );
3076
+ dv = lerp(dv, s.value, adaptiveSpeed2, pausedDt);
3077
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
3078
+ if (Math.abs(dv - s.value) < prevRange * VALUE_SNAP_THRESHOLD) dv = s.value;
3079
+ displayValuesRef.current.set(s.id, dv);
3080
+ }
3081
+ smoothValues.set(s.id, dv);
3082
+ }
3083
+ const hiddenIds = cfg.hiddenSeriesIds;
3084
+ const seriesAlphas = seriesAlphaRef.current;
3085
+ for (const s of effectiveMultiSeries) {
3086
+ let alpha = seriesAlphas.get(s.id) ?? 1;
3087
+ const target = hiddenIds?.has(s.id) ? 0 : 1;
3088
+ alpha = noMotion ? target : lerp(alpha, target, SERIES_TOGGLE_SPEED, pausedDt);
3089
+ if (alpha < 0.01) alpha = 0;
3090
+ if (alpha > 0.99) alpha = 1;
3091
+ seriesAlphas.set(s.id, alpha);
3092
+ }
3093
+ const firstData = pausedMultiDataRef.current?.get(firstSeries.id)?.data ?? firstSeries.data;
3094
+ const windowResult = updateWindowTransition(
3095
+ cfg,
3096
+ transition,
3097
+ displayWindowRef.current,
3098
+ displayMinRef.current,
3099
+ displayMaxRef.current,
3100
+ noMotion,
3101
+ now_ms,
3102
+ now,
3103
+ firstData,
3104
+ smoothValues.get(firstSeries.id) ?? firstSeries.value,
3105
+ buffer
3106
+ );
3107
+ if (transition.startMs > 0 && effectiveMultiSeries.length > 1) {
3108
+ const targetRightEdge = now + cfg.windowSecs * buffer;
3109
+ const targetLeftEdge = targetRightEdge - cfg.windowSecs;
3110
+ let unionMin = Infinity;
3111
+ let unionMax = -Infinity;
3112
+ for (const s of effectiveMultiSeries) {
3113
+ const sData = pausedMultiDataRef.current?.get(s.id)?.data ?? s.data;
3114
+ const sv = smoothValues.get(s.id) ?? s.value;
3115
+ const targetVisible = [];
3116
+ for (const p of sData) {
3117
+ if (p.time >= targetLeftEdge - 2 && p.time <= targetRightEdge) targetVisible.push(p);
3118
+ }
3119
+ if (targetVisible.length > 0) {
3120
+ const range = computeRange(targetVisible, sv, cfg.referenceLine?.value, cfg.exaggerate);
3121
+ if (range.min < unionMin) unionMin = range.min;
3122
+ if (range.max > unionMax) unionMax = range.max;
3123
+ }
3124
+ }
3125
+ if (isFinite(unionMin) && isFinite(unionMax)) {
3126
+ transition.rangeToMin = unionMin;
3127
+ transition.rangeToMax = unionMax;
3128
+ }
3129
+ }
3130
+ displayWindowRef.current = windowResult.windowSecs;
3131
+ const windowSecs = windowResult.windowSecs;
3132
+ const windowTransProgress = windowResult.windowTransProgress;
3133
+ const isWindowTransitioning = transition.startMs > 0;
3134
+ const rightEdge = now + windowSecs * buffer;
3135
+ const leftEdge = rightEdge - windowSecs;
3136
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
3137
+ const seriesEntries = [];
3138
+ let globalMin = Infinity;
3139
+ let globalMax = -Infinity;
3140
+ for (const s of effectiveMultiSeries) {
3141
+ const snap = pausedMultiDataRef.current?.get(s.id);
3142
+ const seriesData = snap?.data ?? s.data;
3143
+ const visible = [];
3144
+ for (const p of seriesData) {
3145
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) visible.push(p);
3146
+ }
3147
+ const sv = smoothValues.get(s.id) ?? s.value;
3148
+ const alpha = seriesAlphas.get(s.id) ?? 1;
3149
+ if (visible.length >= 2) {
3150
+ if (alpha > 0.01) {
3151
+ const range = computeRange(visible, sv, cfg.referenceLine?.value, cfg.exaggerate);
3152
+ if (range.min < globalMin) globalMin = range.min;
3153
+ if (range.max > globalMax) globalMax = range.max;
3154
+ }
3155
+ seriesEntries.push({ visible, smoothValue: sv, palette: s.palette, label: s.label, alpha });
3156
+ }
3157
+ }
3158
+ if (seriesEntries.length === 0) {
3159
+ if (loadingAlpha > 0.01) {
3160
+ drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, cfg.palette.gridLabel);
3161
+ }
3162
+ if (1 - loadingAlpha > 0.01) {
3163
+ drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
3164
+ }
3165
+ ctx.save();
3166
+ ctx.globalCompositeOperation = "destination-out";
3167
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
3168
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
3169
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
3170
+ ctx.fillStyle = fadeGrad;
3171
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
3172
+ ctx.restore();
3173
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
3174
+ rafRef.current = requestAnimationFrame(draw);
3175
+ return;
3176
+ }
3177
+ const computedRange = { min: isFinite(globalMin) ? globalMin : 0, max: isFinite(globalMax) ? globalMax : 1 };
3178
+ const adaptiveSpeed = cfg.lerpSpeed + ADAPTIVE_SPEED_BOOST * 0.5;
3179
+ const rangeResult = updateRange(
3180
+ computedRange,
3181
+ rangeInitedRef.current,
3182
+ targetMinRef.current,
3183
+ targetMaxRef.current,
3184
+ displayMinRef.current,
3185
+ displayMaxRef.current,
3186
+ isWindowTransitioning,
3187
+ windowTransProgress,
3188
+ transition,
3189
+ adaptiveSpeed,
3190
+ chartH,
3191
+ pausedDt
3192
+ );
3193
+ rangeInitedRef.current = rangeResult.rangeInited;
3194
+ targetMinRef.current = rangeResult.targetMin;
3195
+ targetMaxRef.current = rangeResult.targetMax;
3196
+ displayMinRef.current = rangeResult.displayMin;
3197
+ displayMaxRef.current = rangeResult.displayMax;
3198
+ const { minVal, maxVal, valRange } = rangeResult;
3199
+ const layout = {
3200
+ w,
3201
+ h,
3202
+ pad,
3203
+ chartW,
3204
+ chartH,
3205
+ leftEdge,
3206
+ rightEdge,
3207
+ minVal,
3208
+ maxVal,
3209
+ valRange,
3210
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
3211
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
3212
+ };
3213
+ const hoverPx = hoverXRef.current;
3214
+ let drawHoverX = null;
3215
+ let drawHoverTime = null;
3216
+ let isActiveHover = false;
3217
+ let hoverEntries = [];
3218
+ if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
3219
+ const maxHoverX = layout.toX(now);
3220
+ const clampedX = Math.min(hoverPx, maxHoverX);
3221
+ const t = leftEdge + (clampedX - pad.left) / chartW * (rightEdge - leftEdge);
3222
+ drawHoverX = clampedX;
3223
+ drawHoverTime = t;
3224
+ isActiveHover = true;
3225
+ for (const entry of seriesEntries) {
3226
+ if ((entry.alpha ?? 1) < 0.5) continue;
3227
+ const v = interpolateAtTime(entry.visible, t);
3228
+ if (v !== null) {
3229
+ hoverEntries.push({ color: entry.palette.line, label: entry.label ?? "", value: v });
3230
+ }
3231
+ }
3232
+ lastHoverRef.current = { x: clampedX, value: hoverEntries[0]?.value ?? 0, time: t };
3233
+ lastHoverEntriesRef.current = hoverEntries;
3234
+ cfg.onHover?.({ time: t, value: hoverEntries[0]?.value ?? 0, x: clampedX, y: layout.toY(hoverEntries[0]?.value ?? 0) });
3235
+ }
3236
+ const scrubTarget = isActiveHover ? 1 : 0;
3237
+ if (noMotion) {
3238
+ scrubAmountRef.current = scrubTarget;
3239
+ } else {
3240
+ scrubAmountRef.current += (scrubTarget - scrubAmountRef.current) * SCRUB_LERP_SPEED;
3241
+ if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
3242
+ if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
3243
+ }
3244
+ if (!isActiveHover && scrubAmountRef.current > 0 && lastHoverRef.current) {
3245
+ drawHoverX = lastHoverRef.current.x;
3246
+ drawHoverTime = lastHoverRef.current.time;
3247
+ hoverEntries = lastHoverEntriesRef.current;
3248
+ }
3249
+ drawMultiFrame(ctx, layout, {
3250
+ series: seriesEntries,
3251
+ now,
3252
+ showGrid: cfg.showGrid,
3253
+ showPulse: cfg.showPulse,
3254
+ referenceLine: cfg.referenceLine,
3255
+ hoverX: drawHoverX,
3256
+ hoverTime: drawHoverTime,
3257
+ hoverEntries,
3258
+ scrubAmount: scrubAmountRef.current,
3259
+ windowSecs,
3260
+ formatValue: cfg.formatValue,
3261
+ formatTime: cfg.formatTime,
3262
+ gridState: gridStateRef.current,
3263
+ timeAxisState: timeAxisStateRef.current,
3264
+ dt,
3265
+ targetWindowSecs: cfg.windowSecs,
3266
+ tooltipY: cfg.tooltipY,
3267
+ tooltipOutline: cfg.tooltipOutline,
3268
+ chartReveal,
3269
+ pauseProgress,
3270
+ now_ms,
3271
+ primaryPalette: cfg.palette
3272
+ });
3273
+ const bgAlpha = 1 - chartReveal;
3274
+ if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
3275
+ const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
3276
+ if (bgEmptyAlpha > 0.01) {
3277
+ drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
1679
3278
  }
1680
3279
  }
1681
- }
1682
- const smoothValue = displayValueRef.current;
1683
- const chartW = w - pad.left - pad.right;
1684
- const needsArrowRoom = cfg.showMomentum;
1685
- const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
1686
- const transition = windowTransitionRef.current;
1687
- if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
1688
- const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
1689
- const windowResult = updateWindowTransition(
1690
- cfg,
1691
- transition,
1692
- displayWindowRef.current,
1693
- displayMinRef.current,
1694
- displayMaxRef.current,
1695
- noMotion,
1696
- now_ms,
1697
- now,
1698
- effectivePoints,
1699
- smoothValue,
1700
- buffer
1701
- );
1702
- displayWindowRef.current = windowResult.windowSecs;
1703
- const windowSecs = windowResult.windowSecs;
1704
- const windowTransProgress = windowResult.windowTransProgress;
1705
- const rightEdge = now + windowSecs * buffer;
1706
- const leftEdge = rightEdge - windowSecs;
1707
- const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
1708
- const visible = [];
1709
- for (const p of effectivePoints) {
1710
- if (p.time >= leftEdge - 2 && p.time <= filterRight) {
1711
- visible.push(p);
1712
- }
1713
- }
1714
- if (visible.length < 2) {
1715
3280
  if (badgeRef.current) badgeRef.current.container.style.display = "none";
1716
- rafRef.current = requestAnimationFrame(draw);
1717
- return;
1718
- }
1719
- const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
1720
- const isWindowTransitioning = transition.startMs > 0;
1721
- const rangeResult = updateRange(
1722
- computedRange,
1723
- rangeInitedRef.current,
1724
- targetMinRef.current,
1725
- targetMaxRef.current,
1726
- displayMinRef.current,
1727
- displayMaxRef.current,
1728
- isWindowTransitioning,
1729
- windowTransProgress,
1730
- transition,
1731
- adaptiveSpeed,
1732
- chartH,
1733
- pausedDt
1734
- );
1735
- rangeInitedRef.current = rangeResult.rangeInited;
1736
- targetMinRef.current = rangeResult.targetMin;
1737
- targetMaxRef.current = rangeResult.targetMax;
1738
- displayMinRef.current = rangeResult.displayMin;
1739
- displayMaxRef.current = rangeResult.displayMax;
1740
- const { minVal, maxVal, valRange } = rangeResult;
1741
- const layout = {
1742
- w,
1743
- h,
1744
- pad,
1745
- chartW,
1746
- chartH,
1747
- leftEdge,
1748
- rightEdge,
1749
- minVal,
1750
- maxVal,
1751
- valRange,
1752
- toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
1753
- toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
1754
- };
1755
- const momentum = cfg.momentumOverride ?? detectMomentum(visible);
1756
- const hoverResult = updateHoverState(
1757
- hoverXRef.current,
1758
- pad,
1759
- w,
1760
- layout,
1761
- now,
1762
- visible,
1763
- scrubAmountRef.current,
1764
- lastHoverRef.current,
1765
- cfg,
1766
- noMotion,
1767
- leftEdge,
1768
- rightEdge,
1769
- chartW,
1770
- dt
1771
- );
1772
- scrubAmountRef.current = hoverResult.scrubAmount;
1773
- lastHoverRef.current = hoverResult.lastHover;
1774
- const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
1775
- const lookback = Math.min(5, visible.length - 1);
1776
- const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
1777
- const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
1778
- drawFrame(ctx, layout, cfg.palette, {
1779
- visible,
1780
- smoothValue,
1781
- now,
1782
- momentum,
1783
- arrowState: arrowStateRef.current,
1784
- showGrid: cfg.showGrid,
1785
- showMomentum: cfg.showMomentum,
1786
- showPulse: cfg.showPulse,
1787
- showFill: cfg.showFill,
1788
- referenceLine: cfg.referenceLine,
1789
- hoverX: drawHoverX,
1790
- hoverValue: drawHoverValue,
1791
- hoverTime: drawHoverTime,
1792
- scrubAmount: scrubAmountRef.current,
1793
- windowSecs,
1794
- formatValue: cfg.formatValue,
1795
- formatTime: cfg.formatTime,
1796
- gridState: gridStateRef.current,
1797
- timeAxisState: timeAxisStateRef.current,
1798
- dt,
1799
- targetWindowSecs: cfg.windowSecs,
1800
- tooltipY: cfg.tooltipY,
1801
- tooltipOutline: cfg.tooltipOutline,
1802
- orderbookData: cfg.orderbookData,
1803
- orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
1804
- particleState: cfg.degenOptions ? particleStateRef.current : void 0,
1805
- particleOptions: cfg.degenOptions,
1806
- swingMagnitude,
1807
- shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
1808
- chartReveal,
1809
- pauseProgress,
1810
- now_ms
1811
- });
1812
- const bgAlpha = 1 - chartReveal;
1813
- if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
1814
- const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
1815
- if (bgEmptyAlpha > 0.01) {
1816
- drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
3281
+ } else {
3282
+ const effectivePoints = useStash ? lastDataRef.current : points;
3283
+ const adaptiveSpeed = computeAdaptiveSpeed(
3284
+ cfg.value,
3285
+ displayValueRef.current,
3286
+ displayMinRef.current,
3287
+ displayMaxRef.current,
3288
+ cfg.lerpSpeed,
3289
+ noMotion
3290
+ );
3291
+ if (!useStash) {
3292
+ displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
3293
+ if (pauseProgress < 0.5) {
3294
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
3295
+ if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
3296
+ displayValueRef.current = cfg.value;
3297
+ }
3298
+ }
1817
3299
  }
1818
- }
1819
- const badge = badgeRef.current;
1820
- if (badge) {
1821
- badgeYRef.current = updateBadgeDOM(
1822
- badge,
3300
+ const smoothValue = displayValueRef.current;
3301
+ const chartW = w - pad.left - pad.right;
3302
+ const needsArrowRoom = cfg.showMomentum;
3303
+ const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
3304
+ const transition = windowTransitionRef.current;
3305
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
3306
+ const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
3307
+ const windowResult = updateWindowTransition(
1823
3308
  cfg,
3309
+ transition,
3310
+ displayWindowRef.current,
3311
+ displayMinRef.current,
3312
+ displayMaxRef.current,
3313
+ noMotion,
3314
+ now_ms,
3315
+ now,
3316
+ effectivePoints,
1824
3317
  smoothValue,
1825
- layout,
1826
- momentum,
1827
- badgeYRef.current,
1828
- badgeColorRef.current,
3318
+ buffer
3319
+ );
3320
+ displayWindowRef.current = windowResult.windowSecs;
3321
+ const windowSecs = windowResult.windowSecs;
3322
+ const windowTransProgress = windowResult.windowTransProgress;
3323
+ const rightEdge = now + windowSecs * buffer;
3324
+ const leftEdge = rightEdge - windowSecs;
3325
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
3326
+ const visible = [];
3327
+ for (const p of effectivePoints) {
3328
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) {
3329
+ visible.push(p);
3330
+ }
3331
+ }
3332
+ if (visible.length < 2) {
3333
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
3334
+ rafRef.current = requestAnimationFrame(draw);
3335
+ return;
3336
+ }
3337
+ const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
3338
+ const isWindowTransitioning = transition.startMs > 0;
3339
+ const rangeResult = updateRange(
3340
+ computedRange,
3341
+ rangeInitedRef.current,
3342
+ targetMinRef.current,
3343
+ targetMaxRef.current,
3344
+ displayMinRef.current,
3345
+ displayMaxRef.current,
1829
3346
  isWindowTransitioning,
3347
+ windowTransProgress,
3348
+ transition,
3349
+ adaptiveSpeed,
3350
+ chartH,
3351
+ pausedDt
3352
+ );
3353
+ rangeInitedRef.current = rangeResult.rangeInited;
3354
+ targetMinRef.current = rangeResult.targetMin;
3355
+ targetMaxRef.current = rangeResult.targetMax;
3356
+ displayMinRef.current = rangeResult.displayMin;
3357
+ displayMaxRef.current = rangeResult.displayMax;
3358
+ const { minVal, maxVal, valRange } = rangeResult;
3359
+ const layout = {
3360
+ w,
3361
+ h,
3362
+ pad,
3363
+ chartW,
3364
+ chartH,
3365
+ leftEdge,
3366
+ rightEdge,
3367
+ minVal,
3368
+ maxVal,
3369
+ valRange,
3370
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
3371
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
3372
+ };
3373
+ const momentum = cfg.momentumOverride ?? detectMomentum(visible);
3374
+ const hoverResult = updateHoverState(
3375
+ hoverXRef.current,
3376
+ pad,
3377
+ w,
3378
+ layout,
3379
+ now,
3380
+ visible,
3381
+ scrubAmountRef.current,
3382
+ lastHoverRef.current,
3383
+ cfg,
1830
3384
  noMotion,
1831
- ctx,
1832
- pausedDt,
1833
- chartReveal
3385
+ leftEdge,
3386
+ rightEdge,
3387
+ chartW,
3388
+ dt
1834
3389
  );
1835
- if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
1836
- const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
1837
- badge.container.style.opacity = String(base * (1 - pauseProgress));
3390
+ scrubAmountRef.current = hoverResult.scrubAmount;
3391
+ lastHoverRef.current = hoverResult.lastHover;
3392
+ const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
3393
+ const lookback = Math.min(5, visible.length - 1);
3394
+ const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
3395
+ const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
3396
+ drawFrame(ctx, layout, cfg.palette, {
3397
+ visible,
3398
+ smoothValue,
3399
+ now,
3400
+ momentum,
3401
+ arrowState: arrowStateRef.current,
3402
+ showGrid: cfg.showGrid,
3403
+ showMomentum: cfg.showMomentum,
3404
+ showPulse: cfg.showPulse,
3405
+ showFill: cfg.showFill,
3406
+ referenceLine: cfg.referenceLine,
3407
+ hoverX: drawHoverX,
3408
+ hoverValue: drawHoverValue,
3409
+ hoverTime: drawHoverTime,
3410
+ scrubAmount: scrubAmountRef.current,
3411
+ windowSecs,
3412
+ formatValue: cfg.formatValue,
3413
+ formatTime: cfg.formatTime,
3414
+ gridState: gridStateRef.current,
3415
+ timeAxisState: timeAxisStateRef.current,
3416
+ dt,
3417
+ targetWindowSecs: cfg.windowSecs,
3418
+ tooltipY: cfg.tooltipY,
3419
+ tooltipOutline: cfg.tooltipOutline,
3420
+ orderbookData: cfg.orderbookData,
3421
+ orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
3422
+ particleState: cfg.degenOptions ? particleStateRef.current : void 0,
3423
+ particleOptions: cfg.degenOptions,
3424
+ swingMagnitude,
3425
+ shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
3426
+ chartReveal,
3427
+ pauseProgress,
3428
+ now_ms
3429
+ });
3430
+ const bgAlpha = 1 - chartReveal;
3431
+ if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
3432
+ const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
3433
+ if (bgEmptyAlpha > 0.01) {
3434
+ drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
3435
+ }
1838
3436
  }
1839
- }
1840
- const valEl = cfg.valueDisplayRef?.current;
1841
- if (valEl) {
1842
- const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
1843
- valEl.textContent = cfg.formatValue(displayVal);
1844
- if (cfg.valueMomentumColor) {
1845
- const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
1846
- if (mc) valEl.style.color = mc;
1847
- else valEl.style.removeProperty("color");
3437
+ const badge = badgeRef.current;
3438
+ if (badge) {
3439
+ badgeYRef.current = updateBadgeDOM(
3440
+ badge,
3441
+ cfg,
3442
+ smoothValue,
3443
+ layout,
3444
+ momentum,
3445
+ badgeYRef.current,
3446
+ badgeColorRef.current,
3447
+ isWindowTransitioning,
3448
+ noMotion,
3449
+ ctx,
3450
+ pausedDt,
3451
+ chartReveal
3452
+ );
3453
+ if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
3454
+ const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
3455
+ badge.container.style.opacity = String(base * (1 - pauseProgress));
3456
+ }
3457
+ }
3458
+ const valEl = cfg.valueDisplayRef?.current;
3459
+ if (valEl) {
3460
+ const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
3461
+ valEl.textContent = cfg.formatValue(displayVal);
3462
+ if (cfg.valueMomentumColor) {
3463
+ const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
3464
+ if (mc) valEl.style.color = mc;
3465
+ else valEl.style.removeProperty("color");
3466
+ }
1848
3467
  }
1849
3468
  }
1850
3469
  rafRef.current = requestAnimationFrame(draw);
@@ -1868,6 +3487,7 @@ var defaultFormatTime = (t) => {
1868
3487
  function Liveline({
1869
3488
  data,
1870
3489
  value,
3490
+ series: seriesProp,
1871
3491
  theme = "dark",
1872
3492
  color = "#3b82f6",
1873
3493
  window: windowSecs = 30,
@@ -1899,6 +3519,16 @@ function Liveline({
1899
3519
  onHover,
1900
3520
  cursor = "crosshair",
1901
3521
  pulse = true,
3522
+ mode = "line",
3523
+ candles,
3524
+ candleWidth,
3525
+ liveCandle,
3526
+ lineMode,
3527
+ lineData,
3528
+ lineValue,
3529
+ onModeChange,
3530
+ onSeriesToggle,
3531
+ seriesToggleCompact = false,
1902
3532
  className,
1903
3533
  style
1904
3534
  }) {
@@ -1908,8 +3538,30 @@ function Liveline({
1908
3538
  const windowBarRef = useRef2(null);
1909
3539
  const windowBtnRefs = useRef2(/* @__PURE__ */ new Map());
1910
3540
  const [indicatorStyle, setIndicatorStyle] = useState(null);
3541
+ const modeBarRef = useRef2(null);
3542
+ const modeBtnRefs = useRef2(/* @__PURE__ */ new Map());
3543
+ const [modeIndicatorStyle, setModeIndicatorStyle] = useState(null);
3544
+ const [hiddenSeries, setHiddenSeries] = useState(/* @__PURE__ */ new Set());
3545
+ const lastSeriesPropRef = useRef2(seriesProp);
3546
+ if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
1911
3547
  const palette = useMemo(() => resolveTheme(color, theme), [color, theme]);
1912
3548
  const isDark = theme === "dark";
3549
+ const isMultiSeries = seriesProp != null && seriesProp.length > 0;
3550
+ const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
3551
+ const seriesPalettes = useMemo(() => {
3552
+ if (!seriesProp || seriesProp.length === 0) return null;
3553
+ return resolveSeriesPalettes(seriesProp, theme);
3554
+ }, [seriesProp, theme]);
3555
+ const multiSeries = useMemo(() => {
3556
+ if (!seriesProp || !seriesPalettes) return void 0;
3557
+ return seriesProp.map((s, i) => ({
3558
+ id: s.id,
3559
+ data: s.data,
3560
+ value: s.value,
3561
+ palette: seriesPalettes.get(s.id) ?? resolveTheme(s.color || SERIES_COLORS[i % SERIES_COLORS.length], theme),
3562
+ label: s.label
3563
+ }));
3564
+ }, [seriesProp, seriesPalettes, theme]);
1913
3565
  const showMomentum = momentum !== false;
1914
3566
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
1915
3567
  const pad = {
@@ -1937,6 +3589,36 @@ function Liveline({
1937
3589
  });
1938
3590
  }
1939
3591
  }, [activeWindowSecs, windows]);
3592
+ const activeMode = lineMode ? "line" : "candle";
3593
+ useLayoutEffect(() => {
3594
+ if (!onModeChange) return;
3595
+ const btn = modeBtnRefs.current.get(activeMode);
3596
+ const bar = modeBarRef.current;
3597
+ if (btn && bar) {
3598
+ const barRect = bar.getBoundingClientRect();
3599
+ const btnRect = btn.getBoundingClientRect();
3600
+ setModeIndicatorStyle({
3601
+ left: btnRect.left - barRect.left,
3602
+ width: btnRect.width
3603
+ });
3604
+ }
3605
+ }, [activeMode, onModeChange]);
3606
+ const handleSeriesToggle = useCallback2((id) => {
3607
+ setHiddenSeries((prev) => {
3608
+ const next = new Set(prev);
3609
+ if (next.has(id)) {
3610
+ next.delete(id);
3611
+ onSeriesToggle?.(id, true);
3612
+ } else {
3613
+ const totalSeries = seriesProp?.length ?? 0;
3614
+ const visibleCount = totalSeries - next.size;
3615
+ if (visibleCount <= 1) return prev;
3616
+ next.add(id);
3617
+ onSeriesToggle?.(id, false);
3618
+ }
3619
+ return next;
3620
+ });
3621
+ }, [seriesProp?.length, onSeriesToggle]);
1940
3622
  const ws = windowStyle ?? "default";
1941
3623
  useLivelineEngine(canvasRef, containerRef, {
1942
3624
  data,
@@ -1945,10 +3627,10 @@ function Liveline({
1945
3627
  windowSecs: effectiveWindowSecs,
1946
3628
  lerpSpeed,
1947
3629
  showGrid: grid,
1948
- showBadge: badge,
1949
- showMomentum,
3630
+ showBadge: isMultiSeries ? false : badge,
3631
+ showMomentum: isMultiSeries ? false : showMomentum,
1950
3632
  momentumOverride,
1951
- showFill: fill,
3633
+ showFill: isMultiSeries ? false : fill,
1952
3634
  referenceLine,
1953
3635
  formatValue,
1954
3636
  formatTime,
@@ -1957,7 +3639,7 @@ function Liveline({
1957
3639
  showPulse: pulse,
1958
3640
  scrub,
1959
3641
  exaggerate,
1960
- degenOptions,
3642
+ degenOptions: isMultiSeries ? void 0 : degenOptions,
1961
3643
  badgeTail,
1962
3644
  badgeVariant,
1963
3645
  tooltipY,
@@ -1967,9 +3649,21 @@ function Liveline({
1967
3649
  orderbookData: orderbook,
1968
3650
  loading,
1969
3651
  paused,
1970
- emptyText
3652
+ emptyText,
3653
+ mode,
3654
+ candles,
3655
+ candleWidth,
3656
+ liveCandle,
3657
+ lineMode,
3658
+ lineData,
3659
+ lineValue,
3660
+ multiSeries,
3661
+ isMultiSeries,
3662
+ hiddenSeriesIds: hiddenSeries
1971
3663
  });
1972
3664
  const cursorStyle = scrub ? cursor : "default";
3665
+ const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
3666
+ const inactiveColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)";
1973
3667
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1974
3668
  showValue && /* @__PURE__ */ jsx(
1975
3669
  "span",
@@ -1989,68 +3683,244 @@ function Liveline({
1989
3683
  }
1990
3684
  }
1991
3685
  ),
1992
- windows && windows.length > 0 && /* @__PURE__ */ jsxs(
1993
- "div",
1994
- {
1995
- ref: windowBarRef,
1996
- style: {
1997
- position: "relative",
1998
- display: "inline-flex",
1999
- gap: ws === "text" ? 4 : 2,
2000
- background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
2001
- borderRadius: ws === "rounded" ? 999 : 6,
2002
- padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
2003
- marginBottom: 6,
2004
- marginLeft: pad.left
2005
- },
2006
- children: [
2007
- ws !== "text" && indicatorStyle && /* @__PURE__ */ jsx("div", { style: {
2008
- position: "absolute",
2009
- top: ws === "rounded" ? 3 : 2,
2010
- left: indicatorStyle.left,
2011
- width: indicatorStyle.width,
2012
- height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
2013
- background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
2014
- borderRadius: ws === "rounded" ? 999 : 4,
2015
- transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
2016
- pointerEvents: "none"
2017
- } }),
2018
- windows.map((w) => {
2019
- const isActive = w.secs === activeWindowSecs;
2020
- return /* @__PURE__ */ jsx(
3686
+ (windows && windows.length > 0 || onModeChange || showSeriesToggle) && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
3687
+ windows && windows.length > 0 && /* @__PURE__ */ jsxs(
3688
+ "div",
3689
+ {
3690
+ ref: windowBarRef,
3691
+ style: {
3692
+ position: "relative",
3693
+ display: "inline-flex",
3694
+ gap: ws === "text" ? 4 : 2,
3695
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3696
+ borderRadius: ws === "rounded" ? 999 : 6,
3697
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
3698
+ },
3699
+ children: [
3700
+ ws !== "text" && indicatorStyle && /* @__PURE__ */ jsx("div", { style: {
3701
+ position: "absolute",
3702
+ top: ws === "rounded" ? 3 : 2,
3703
+ left: indicatorStyle.left,
3704
+ width: indicatorStyle.width,
3705
+ height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
3706
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3707
+ borderRadius: ws === "rounded" ? 999 : 4,
3708
+ transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3709
+ pointerEvents: "none"
3710
+ } }),
3711
+ windows.map((w) => {
3712
+ const isActive = w.secs === activeWindowSecs;
3713
+ return /* @__PURE__ */ jsx(
3714
+ "button",
3715
+ {
3716
+ ref: (el) => {
3717
+ if (el) windowBtnRefs.current.set(w.secs, el);
3718
+ else windowBtnRefs.current.delete(w.secs);
3719
+ },
3720
+ onClick: () => {
3721
+ setActiveWindowSecs(w.secs);
3722
+ onWindowChange?.(w.secs);
3723
+ },
3724
+ style: {
3725
+ position: "relative",
3726
+ zIndex: 1,
3727
+ fontSize: 11,
3728
+ padding: ws === "text" ? "2px 6px" : "3px 10px",
3729
+ borderRadius: ws === "rounded" ? 999 : 4,
3730
+ border: "none",
3731
+ cursor: "pointer",
3732
+ fontFamily: "system-ui, -apple-system, sans-serif",
3733
+ fontWeight: isActive ? 600 : 400,
3734
+ background: "transparent",
3735
+ color: isActive ? activeColor : inactiveColor,
3736
+ transition: "color 0.2s, background 0.15s",
3737
+ lineHeight: "16px"
3738
+ },
3739
+ children: w.label
3740
+ },
3741
+ w.secs
3742
+ );
3743
+ })
3744
+ ]
3745
+ }
3746
+ ),
3747
+ onModeChange && /* @__PURE__ */ jsxs(
3748
+ "div",
3749
+ {
3750
+ ref: modeBarRef,
3751
+ style: {
3752
+ position: "relative",
3753
+ display: "inline-flex",
3754
+ gap: ws === "text" ? 4 : 2,
3755
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3756
+ borderRadius: ws === "rounded" ? 999 : 6,
3757
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
3758
+ },
3759
+ children: [
3760
+ ws !== "text" && modeIndicatorStyle && /* @__PURE__ */ jsx("div", { style: {
3761
+ position: "absolute",
3762
+ top: ws === "rounded" ? 3 : 2,
3763
+ left: modeIndicatorStyle.left,
3764
+ width: modeIndicatorStyle.width,
3765
+ height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
3766
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3767
+ borderRadius: ws === "rounded" ? 999 : 4,
3768
+ transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3769
+ pointerEvents: "none"
3770
+ } }),
3771
+ /* @__PURE__ */ jsx(
2021
3772
  "button",
2022
3773
  {
2023
3774
  ref: (el) => {
2024
- if (el) windowBtnRefs.current.set(w.secs, el);
2025
- else windowBtnRefs.current.delete(w.secs);
3775
+ if (el) modeBtnRefs.current.set("line", el);
3776
+ else modeBtnRefs.current.delete("line");
2026
3777
  },
2027
- onClick: () => {
2028
- setActiveWindowSecs(w.secs);
2029
- onWindowChange?.(w.secs);
3778
+ onClick: () => onModeChange("line"),
3779
+ style: {
3780
+ position: "relative",
3781
+ zIndex: 1,
3782
+ padding: "5px 7px",
3783
+ borderRadius: ws === "rounded" ? 999 : 4,
3784
+ border: "none",
3785
+ cursor: "pointer",
3786
+ background: "transparent",
3787
+ display: "flex",
3788
+ alignItems: "center"
2030
3789
  },
3790
+ children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
3791
+ "path",
3792
+ {
3793
+ 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",
3794
+ stroke: activeMode === "line" ? activeColor : inactiveColor,
3795
+ strokeWidth: activeMode === "line" ? 1.5 : 1.2,
3796
+ strokeLinecap: "round",
3797
+ fill: "none"
3798
+ }
3799
+ ) })
3800
+ }
3801
+ ),
3802
+ /* @__PURE__ */ jsx(
3803
+ "button",
3804
+ {
3805
+ ref: (el) => {
3806
+ if (el) modeBtnRefs.current.set("candle", el);
3807
+ else modeBtnRefs.current.delete("candle");
3808
+ },
3809
+ onClick: () => onModeChange("candle"),
2031
3810
  style: {
2032
3811
  position: "relative",
2033
3812
  zIndex: 1,
2034
- fontSize: 11,
2035
- padding: ws === "text" ? "2px 6px" : "3px 10px",
3813
+ padding: "5px 7px",
2036
3814
  borderRadius: ws === "rounded" ? 999 : 4,
2037
3815
  border: "none",
2038
3816
  cursor: "pointer",
2039
- fontFamily: "system-ui, -apple-system, sans-serif",
2040
- fontWeight: isActive ? 600 : 400,
2041
3817
  background: "transparent",
2042
- 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)",
2043
- transition: "color 0.2s, background 0.15s",
2044
- lineHeight: "16px"
3818
+ display: "flex",
3819
+ alignItems: "center"
2045
3820
  },
2046
- children: w.label
2047
- },
2048
- w.secs
2049
- );
2050
- })
2051
- ]
2052
- }
2053
- ),
3821
+ children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: [
3822
+ /* @__PURE__ */ jsx(
3823
+ "line",
3824
+ {
3825
+ x1: "3.5",
3826
+ y1: "1",
3827
+ x2: "3.5",
3828
+ y2: "11",
3829
+ stroke: activeMode === "candle" ? activeColor : inactiveColor,
3830
+ strokeWidth: "1"
3831
+ }
3832
+ ),
3833
+ /* @__PURE__ */ jsx(
3834
+ "rect",
3835
+ {
3836
+ x: "2",
3837
+ y: "3",
3838
+ width: "3",
3839
+ height: "5",
3840
+ rx: "0.5",
3841
+ fill: activeMode === "candle" ? activeColor : inactiveColor
3842
+ }
3843
+ ),
3844
+ /* @__PURE__ */ jsx(
3845
+ "line",
3846
+ {
3847
+ x1: "8.5",
3848
+ y1: "2",
3849
+ x2: "8.5",
3850
+ y2: "10",
3851
+ stroke: activeMode === "candle" ? activeColor : inactiveColor,
3852
+ strokeWidth: "1"
3853
+ }
3854
+ ),
3855
+ /* @__PURE__ */ jsx(
3856
+ "rect",
3857
+ {
3858
+ x: "7",
3859
+ y: "4",
3860
+ width: "3",
3861
+ height: "4",
3862
+ rx: "0.5",
3863
+ fill: activeMode === "candle" ? activeColor : inactiveColor
3864
+ }
3865
+ )
3866
+ ] })
3867
+ }
3868
+ )
3869
+ ]
3870
+ }
3871
+ ),
3872
+ showSeriesToggle && /* @__PURE__ */ jsx("div", { style: {
3873
+ display: "inline-flex",
3874
+ gap: ws === "text" ? 4 : 2,
3875
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3876
+ borderRadius: ws === "rounded" ? 999 : 6,
3877
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
3878
+ opacity: isMultiSeries ? 1 : 0,
3879
+ transition: "opacity 0.4s",
3880
+ pointerEvents: isMultiSeries ? "auto" : "none"
3881
+ }, children: (lastSeriesPropRef.current ?? []).map((s, si) => {
3882
+ const isHidden = hiddenSeries.has(s.id);
3883
+ const seriesColor = s.color || SERIES_COLORS[si % SERIES_COLORS.length];
3884
+ return /* @__PURE__ */ jsxs(
3885
+ "button",
3886
+ {
3887
+ onClick: () => handleSeriesToggle(s.id),
3888
+ style: {
3889
+ position: "relative",
3890
+ zIndex: 1,
3891
+ fontSize: 11,
3892
+ padding: seriesToggleCompact ? ws === "text" ? "2px 4px" : "5px 7px" : ws === "text" ? "2px 6px" : "3px 8px",
3893
+ borderRadius: ws === "rounded" ? 999 : 4,
3894
+ border: "none",
3895
+ cursor: "pointer",
3896
+ fontFamily: "system-ui, -apple-system, sans-serif",
3897
+ fontWeight: 500,
3898
+ background: isHidden ? "transparent" : ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3899
+ color: isHidden ? inactiveColor : activeColor,
3900
+ opacity: isHidden ? 0.4 : 1,
3901
+ transition: "opacity 0.2s, background 0.15s, color 0.2s",
3902
+ lineHeight: "16px",
3903
+ display: "flex",
3904
+ alignItems: "center",
3905
+ gap: seriesToggleCompact ? 0 : 4
3906
+ },
3907
+ children: [
3908
+ /* @__PURE__ */ jsx("span", { style: {
3909
+ width: seriesToggleCompact ? 8 : 6,
3910
+ height: seriesToggleCompact ? 8 : 6,
3911
+ borderRadius: "50%",
3912
+ background: seriesColor,
3913
+ flexShrink: 0,
3914
+ opacity: isHidden ? 0.4 : 1,
3915
+ transition: "opacity 0.2s"
3916
+ } }),
3917
+ !seriesToggleCompact && (s.label ?? s.id)
3918
+ ]
3919
+ },
3920
+ s.id
3921
+ );
3922
+ }) })
3923
+ ] }),
2054
3924
  /* @__PURE__ */ jsx(
2055
3925
  "div",
2056
3926
  {
@@ -2073,6 +3943,63 @@ function Liveline({
2073
3943
  )
2074
3944
  ] });
2075
3945
  }
3946
+
3947
+ // src/LivelineTransition.tsx
3948
+ import { useState as useState2, useEffect as useEffect2, useRef as useRef3 } from "react";
3949
+ import { jsx as jsx2 } from "react/jsx-runtime";
3950
+ function LivelineTransition({
3951
+ active,
3952
+ children,
3953
+ duration = 300,
3954
+ className,
3955
+ style
3956
+ }) {
3957
+ const childArray = Array.isArray(children) ? children : [children];
3958
+ const [mounted, setMounted] = useState2(() => /* @__PURE__ */ new Set([active]));
3959
+ const [visible, setVisible] = useState2(active);
3960
+ const prevRef = useRef3(active);
3961
+ useEffect2(() => {
3962
+ if (active === prevRef.current) return () => {
3963
+ };
3964
+ const oldKey = prevRef.current;
3965
+ prevRef.current = active;
3966
+ setMounted((prev) => /* @__PURE__ */ new Set([...prev, active]));
3967
+ let raf1 = requestAnimationFrame(() => {
3968
+ raf1 = requestAnimationFrame(() => setVisible(active));
3969
+ });
3970
+ const timer = setTimeout(() => {
3971
+ setMounted((prev) => {
3972
+ const next = new Set(prev);
3973
+ next.delete(oldKey);
3974
+ return next;
3975
+ });
3976
+ }, duration + 50);
3977
+ return () => {
3978
+ cancelAnimationFrame(raf1);
3979
+ clearTimeout(timer);
3980
+ };
3981
+ }, [active, duration]);
3982
+ return /* @__PURE__ */ jsx2("div", { className, style: { position: "relative", width: "100%", height: "100%", ...style }, children: childArray.map((child) => {
3983
+ const key = String(child.key ?? "");
3984
+ if (!mounted.has(key)) return null;
3985
+ const isActive = key === visible;
3986
+ return /* @__PURE__ */ jsx2(
3987
+ "div",
3988
+ {
3989
+ style: {
3990
+ position: "absolute",
3991
+ inset: 0,
3992
+ opacity: isActive ? 1 : 0,
3993
+ transition: `opacity ${duration}ms ease`,
3994
+ pointerEvents: isActive ? "auto" : "none"
3995
+ },
3996
+ children: child
3997
+ },
3998
+ key
3999
+ );
4000
+ }) });
4001
+ }
2076
4002
  export {
2077
- Liveline
4003
+ Liveline,
4004
+ LivelineTransition
2078
4005
  };