liveline 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -88,6 +88,33 @@ function resolveTheme(color, mode) {
88
88
  badgeFont: '500 11px "SF Mono", Menlo, monospace'
89
89
  };
90
90
  }
91
+ var SERIES_COLORS = [
92
+ "#3b82f6",
93
+ // blue
94
+ "#ef4444",
95
+ // red
96
+ "#22c55e",
97
+ // green
98
+ "#f59e0b",
99
+ // amber
100
+ "#8b5cf6",
101
+ // violet
102
+ "#ec4899",
103
+ // pink
104
+ "#06b6d4",
105
+ // cyan
106
+ "#f97316"
107
+ // orange
108
+ ];
109
+ function resolveSeriesPalettes(series, mode) {
110
+ const map = /* @__PURE__ */ new Map();
111
+ for (let i = 0; i < series.length; i++) {
112
+ const s = series[i];
113
+ const color = s.color || SERIES_COLORS[i % SERIES_COLORS.length];
114
+ map.set(s.id, resolveTheme(color, mode));
115
+ }
116
+ return map;
117
+ }
91
118
 
92
119
  // src/useLivelineEngine.ts
93
120
  var import_react = require("react");
@@ -513,6 +540,33 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = per
513
540
  }
514
541
  ctx.fill();
515
542
  }
543
+ function drawMultiDot(ctx, x, y, color, pulse = true, now_ms = performance.now(), radius = 3) {
544
+ const baseAlpha = ctx.globalAlpha;
545
+ if (pulse) {
546
+ const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
547
+ if (t < 1) {
548
+ const ringRadius = 9 + t * 10;
549
+ const pulseAlpha = 0.3 * (1 - t);
550
+ ctx.beginPath();
551
+ ctx.arc(x, y, ringRadius, 0, Math.PI * 2);
552
+ ctx.strokeStyle = color;
553
+ ctx.lineWidth = 1.5;
554
+ ctx.globalAlpha = baseAlpha * pulseAlpha;
555
+ ctx.stroke();
556
+ }
557
+ }
558
+ ctx.globalAlpha = baseAlpha;
559
+ ctx.beginPath();
560
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
561
+ ctx.fillStyle = color;
562
+ ctx.fill();
563
+ }
564
+ function drawSimpleDot(ctx, x, y, color, radius = 3) {
565
+ ctx.beginPath();
566
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
567
+ ctx.fillStyle = color;
568
+ ctx.fill();
569
+ }
516
570
  function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
517
571
  const baseAlpha = ctx.globalAlpha;
518
572
  const upTarget = momentum === "up" ? 1 : 0;
@@ -611,6 +665,90 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
611
665
  ctx.fillText(separator + timeText, tx + valueW, ty);
612
666
  ctx.restore();
613
667
  }
668
+ function drawMultiCrosshair(ctx, layout, palette, hoverX, hoverTime, entries, formatValue, formatTime, scrubOpacity, tooltipY, tooltipOutline, liveDotX) {
669
+ if (scrubOpacity < 0.01 || entries.length === 0) return;
670
+ const { h, pad, toY } = layout;
671
+ ctx.save();
672
+ ctx.globalAlpha = scrubOpacity * 0.5;
673
+ ctx.strokeStyle = palette.crosshairLine;
674
+ ctx.lineWidth = 1;
675
+ ctx.beginPath();
676
+ ctx.moveTo(hoverX, pad.top);
677
+ ctx.lineTo(hoverX, h - pad.bottom);
678
+ ctx.stroke();
679
+ ctx.restore();
680
+ const dotRadius = 4 * Math.min(scrubOpacity * 3, 1);
681
+ if (dotRadius > 0.5) {
682
+ ctx.globalAlpha = 1;
683
+ for (const entry of entries) {
684
+ const y = toY(entry.value);
685
+ ctx.beginPath();
686
+ ctx.arc(hoverX, y, dotRadius, 0, Math.PI * 2);
687
+ ctx.fillStyle = entry.color;
688
+ ctx.fill();
689
+ }
690
+ }
691
+ if (scrubOpacity < 0.1 || layout.w < 300) return;
692
+ ctx.save();
693
+ ctx.globalAlpha = scrubOpacity;
694
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
695
+ ctx.textAlign = "left";
696
+ const timeText = formatTime(hoverTime);
697
+ const sep = " \xB7 ";
698
+ const dotInline = " ";
699
+ const segments = [
700
+ { text: timeText, color: palette.gridLabel }
701
+ ];
702
+ for (const e of entries) {
703
+ segments.push({ text: sep, color: palette.gridLabel });
704
+ segments.push({ text: dotInline, color: e.color, isDot: true });
705
+ const label = e.label ? `${e.label} ` : "";
706
+ if (label) segments.push({ text: label, color: palette.gridLabel });
707
+ segments.push({ text: formatValue(e.value), color: palette.tooltipText });
708
+ }
709
+ let totalW = 0;
710
+ const segWidths = [];
711
+ for (const seg of segments) {
712
+ const w = seg.isDot ? 12 : ctx.measureText(seg.text).width;
713
+ segWidths.push(w);
714
+ totalW += w;
715
+ }
716
+ let tx = hoverX - totalW / 2;
717
+ const minX = pad.left + 4;
718
+ const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
719
+ const maxX = dotRightEdge - totalW;
720
+ if (tx < minX) tx = minX;
721
+ if (tx > maxX) tx = maxX;
722
+ const ty = pad.top + (tooltipY ?? 14) + 10;
723
+ if (tooltipOutline !== false) {
724
+ ctx.strokeStyle = palette.tooltipBg;
725
+ ctx.lineWidth = 3;
726
+ ctx.lineJoin = "round";
727
+ let ox2 = tx;
728
+ for (let i = 0; i < segments.length; i++) {
729
+ const seg = segments[i];
730
+ if (!seg.isDot) {
731
+ ctx.strokeText(seg.text, ox2, ty);
732
+ }
733
+ ox2 += segWidths[i];
734
+ }
735
+ }
736
+ let ox = tx;
737
+ for (let i = 0; i < segments.length; i++) {
738
+ const seg = segments[i];
739
+ if (seg.isDot) {
740
+ ctx.beginPath();
741
+ ctx.arc(ox + 4, ty - 4, 3.5, 0, Math.PI * 2);
742
+ ctx.fillStyle = seg.color;
743
+ ctx.fill();
744
+ } else {
745
+ ctx.fillStyle = seg.color;
746
+ ctx.fillText(seg.text, ox, ty);
747
+ }
748
+ ox += segWidths[i];
749
+ }
750
+ ctx.restore();
751
+ }
614
752
 
615
753
  // src/draw/referenceLine.ts
616
754
  function drawReferenceLine(ctx, layout, palette, ref) {
@@ -1457,6 +1595,124 @@ function drawFrame(ctx, layout, palette, opts) {
1457
1595
  ctx.restore();
1458
1596
  }
1459
1597
  }
1598
+ function drawMultiFrame(ctx, layout, opts) {
1599
+ const palette = opts.primaryPalette;
1600
+ const reveal = opts.chartReveal;
1601
+ const revealRamp = (start, end) => {
1602
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1603
+ return t * t * (3 - 2 * t);
1604
+ };
1605
+ if (opts.referenceLine && reveal > 0.01) {
1606
+ ctx.save();
1607
+ if (reveal < 1) ctx.globalAlpha = reveal;
1608
+ drawReferenceLine(ctx, layout, palette, opts.referenceLine);
1609
+ ctx.restore();
1610
+ }
1611
+ if (opts.showGrid) {
1612
+ const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1613
+ if (gridAlpha > 0.01) {
1614
+ ctx.save();
1615
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1616
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1617
+ ctx.restore();
1618
+ }
1619
+ }
1620
+ const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
1621
+ const allPts = [];
1622
+ for (let si = 0; si < opts.series.length; si++) {
1623
+ const s = opts.series[si];
1624
+ const seriesAlpha = s.alpha ?? 1;
1625
+ const secondaryFade = si > 0 && reveal < 1 ? Math.min(1, reveal * 2) : 1;
1626
+ const combinedAlpha = secondaryFade * seriesAlpha;
1627
+ if (combinedAlpha < 0.01) continue;
1628
+ ctx.save();
1629
+ if (combinedAlpha < 1) ctx.globalAlpha = combinedAlpha;
1630
+ const pts = drawLine(
1631
+ ctx,
1632
+ layout,
1633
+ s.palette,
1634
+ s.visible,
1635
+ s.smoothValue,
1636
+ opts.now,
1637
+ false,
1638
+ // no fill
1639
+ scrubX,
1640
+ opts.scrubAmount,
1641
+ reveal,
1642
+ opts.now_ms
1643
+ );
1644
+ ctx.restore();
1645
+ if (pts && pts.length > 0) {
1646
+ allPts.push({ pts, palette: s.palette, label: s.label, alpha: seriesAlpha });
1647
+ }
1648
+ }
1649
+ {
1650
+ const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1651
+ if (timeAlpha > 0.01) {
1652
+ ctx.save();
1653
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1654
+ drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1655
+ ctx.restore();
1656
+ }
1657
+ }
1658
+ if (reveal > 0.3 && allPts.length > 0) {
1659
+ const dotAlpha = (reveal - 0.3) / 0.7;
1660
+ const showPulse = opts.showPulse && reveal > 0.6 && opts.pauseProgress < 0.5;
1661
+ for (const entry of allPts) {
1662
+ if (entry.alpha < 0.01) continue;
1663
+ const lastPt = entry.pts[entry.pts.length - 1];
1664
+ ctx.save();
1665
+ ctx.globalAlpha = dotAlpha * entry.alpha;
1666
+ if (showPulse && entry.alpha > 0.5) {
1667
+ drawMultiDot(ctx, lastPt[0], lastPt[1], entry.palette.line, true, opts.now_ms, 3);
1668
+ } else {
1669
+ drawSimpleDot(ctx, lastPt[0], lastPt[1], entry.palette.line, 3);
1670
+ }
1671
+ if (entry.label) {
1672
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
1673
+ ctx.textAlign = "left";
1674
+ ctx.fillStyle = entry.palette.line;
1675
+ ctx.fillText(entry.label, lastPt[0] + 6, lastPt[1] + 3.5);
1676
+ }
1677
+ ctx.restore();
1678
+ }
1679
+ }
1680
+ ctx.save();
1681
+ ctx.globalCompositeOperation = "destination-out";
1682
+ const fadeGrad = ctx.createLinearGradient(layout.pad.left, 0, layout.pad.left + FADE_EDGE_WIDTH, 0);
1683
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
1684
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
1685
+ ctx.fillStyle = fadeGrad;
1686
+ ctx.fillRect(0, 0, layout.pad.left + FADE_EDGE_WIDTH, layout.h);
1687
+ ctx.restore();
1688
+ if (opts.hoverX !== null && opts.hoverTime !== null && opts.hoverEntries.length > 0 && allPts.length > 0 && opts.scrubAmount > 0.01) {
1689
+ let maxLiveDotX = 0;
1690
+ for (const entry of allPts) {
1691
+ if (entry.alpha < 0.01) continue;
1692
+ const lastX = entry.pts[entry.pts.length - 1][0];
1693
+ if (lastX > maxLiveDotX) maxLiveDotX = lastX;
1694
+ }
1695
+ const distToLive = maxLiveDotX - opts.hoverX;
1696
+ const fadeStart = Math.min(80, layout.chartW * 0.3);
1697
+ const scrubOpacity = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
1698
+ if (scrubOpacity > 0.01) {
1699
+ drawMultiCrosshair(
1700
+ ctx,
1701
+ layout,
1702
+ palette,
1703
+ opts.hoverX,
1704
+ opts.hoverTime,
1705
+ opts.hoverEntries,
1706
+ opts.formatValue,
1707
+ opts.formatTime,
1708
+ scrubOpacity,
1709
+ opts.tooltipY,
1710
+ opts.tooltipOutline,
1711
+ maxLiveDotX
1712
+ );
1713
+ }
1714
+ }
1715
+ }
1460
1716
  function drawCandleFrame(ctx, layout, palette, opts) {
1461
1717
  const { w, h, pad, chartW, chartH } = layout;
1462
1718
  const reveal = opts.chartReveal;
@@ -1739,6 +1995,7 @@ var PAUSE_PROGRESS_SPEED = 0.12;
1739
1995
  var PAUSE_CATCHUP_SPEED = 0.08;
1740
1996
  var PAUSE_CATCHUP_SPEED_FAST = 0.22;
1741
1997
  var LOADING_ALPHA_SPEED = 0.14;
1998
+ var SERIES_TOGGLE_SPEED = 0.1;
1742
1999
  var CANDLE_LERP_SPEED = 0.25;
1743
2000
  var CANDLE_WIDTH_TRANS_MS = 300;
1744
2001
  var LINE_MORPH_MS = 500;
@@ -2062,6 +2319,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2062
2319
  const configRef = (0, import_react.useRef)(config);
2063
2320
  configRef.current = config;
2064
2321
  const displayValueRef = (0, import_react.useRef)(config.value);
2322
+ const displayValuesRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
2323
+ const seriesAlphaRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
2065
2324
  const displayMinRef = (0, import_react.useRef)(0);
2066
2325
  const displayMaxRef = (0, import_react.useRef)(0);
2067
2326
  const targetMinRef = (0, import_react.useRef)(0);
@@ -2094,12 +2353,15 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2094
2353
  const hoverXRef = (0, import_react.useRef)(null);
2095
2354
  const scrubAmountRef = (0, import_react.useRef)(0);
2096
2355
  const lastHoverRef = (0, import_react.useRef)(null);
2356
+ const lastHoverEntriesRef = (0, import_react.useRef)([]);
2097
2357
  const chartRevealRef = (0, import_react.useRef)(0);
2098
2358
  const pauseProgressRef = (0, import_react.useRef)(0);
2099
2359
  const timeDebtRef = (0, import_react.useRef)(0);
2100
2360
  const lastDataRef = (0, import_react.useRef)([]);
2361
+ const lastMultiSeriesRef = (0, import_react.useRef)([]);
2101
2362
  const frozenNowRef = (0, import_react.useRef)(0);
2102
2363
  const pausedDataRef = (0, import_react.useRef)(null);
2364
+ const pausedMultiDataRef = (0, import_react.useRef)(null);
2103
2365
  const loadingAlphaRef = (0, import_react.useRef)(config.loading ? 1 : 0);
2104
2366
  const displayCandleRef = (0, import_react.useRef)(null);
2105
2367
  const liveBirthAlphaRef = (0, import_react.useRef)(1);
@@ -2279,6 +2541,17 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2279
2541
  pausedLineDataRef.current = null;
2280
2542
  pausedLineValueRef.current = null;
2281
2543
  }
2544
+ } else if (cfg.isMultiSeries && cfg.multiSeries) {
2545
+ if (cfg.paused && pausedMultiDataRef.current === null) {
2546
+ const snap = /* @__PURE__ */ new Map();
2547
+ for (const s of cfg.multiSeries) {
2548
+ if (s.data.length >= 2) snap.set(s.id, { data: s.data.slice(), value: s.value });
2549
+ }
2550
+ if (snap.size > 0) pausedMultiDataRef.current = snap;
2551
+ }
2552
+ if (!cfg.paused) {
2553
+ pausedMultiDataRef.current = null;
2554
+ }
2282
2555
  } else {
2283
2556
  if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
2284
2557
  pausedDataRef.current = cfg.data.slice();
@@ -2289,7 +2562,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2289
2562
  }
2290
2563
  const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
2291
2564
  const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
2292
- const hasData = isCandle ? effectiveCandles.length >= 2 : points.length >= 2;
2565
+ const hasMultiData = cfg.isMultiSeries && cfg.multiSeries ? cfg.multiSeries.some((s) => s.data.length >= 2) : false;
2566
+ const hasData = isCandle ? effectiveCandles.length >= 2 : hasMultiData || points.length >= 2;
2293
2567
  const pad = cfg.padding;
2294
2568
  const chartH = h - pad.top - pad.bottom;
2295
2569
  const pauseTarget = cfg.paused ? 1 : 0;
@@ -2325,11 +2599,23 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2325
2599
  rangeInitedRef.current = false;
2326
2600
  }
2327
2601
  let useStash;
2602
+ let useMultiStash = false;
2328
2603
  if (isCandle) {
2329
2604
  useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
2330
2605
  } else {
2331
- useStash = !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
2332
- if (hasData) lastDataRef.current = points;
2606
+ useMultiStash = !hasData && chartReveal > 5e-3 && lastMultiSeriesRef.current.length > 0;
2607
+ if (hasMultiData && cfg.multiSeries) {
2608
+ lastMultiSeriesRef.current = cfg.multiSeries.map((s) => ({
2609
+ id: s.id,
2610
+ data: s.data.slice(),
2611
+ value: s.value,
2612
+ palette: s.palette,
2613
+ label: s.label
2614
+ }));
2615
+ }
2616
+ if (hasData && !cfg.isMultiSeries) lastMultiSeriesRef.current = [];
2617
+ useStash = !useMultiStash && !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
2618
+ if (hasData && !cfg.isMultiSeries) lastDataRef.current = points;
2333
2619
  }
2334
2620
  if (isCandle) {
2335
2621
  const lmt = lineModeTransRef.current;
@@ -2351,8 +2637,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2351
2637
  lineModeProgRef.current = lmt.to;
2352
2638
  }
2353
2639
  }
2354
- if (!hasData && !useStash) {
2355
- const loadingColor = isCandle ? cfg.palette.gridLabel : void 0;
2640
+ if (!hasData && !useStash && !useMultiStash) {
2641
+ const loadingColor = isCandle || cfg.isMultiSeries || lastMultiSeriesRef.current.length > 0 ? cfg.palette.gridLabel : void 0;
2356
2642
  if (loadingAlpha > 0.01) {
2357
2643
  drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
2358
2644
  }
@@ -2775,6 +3061,250 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2775
3061
  badgeRef.current.container.style.display = "none";
2776
3062
  }
2777
3063
  }
3064
+ } else if (cfg.isMultiSeries && cfg.multiSeries && cfg.multiSeries.length > 0 || useMultiStash) {
3065
+ const effectiveMultiSeries = useMultiStash ? lastMultiSeriesRef.current : cfg.multiSeries;
3066
+ let labelReserve = 0;
3067
+ if (effectiveMultiSeries.some((s) => s.label)) {
3068
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
3069
+ let maxLabelW = 0;
3070
+ for (const s of effectiveMultiSeries) {
3071
+ if (s.label) {
3072
+ const lw = ctx.measureText(s.label).width;
3073
+ if (lw > maxLabelW) maxLabelW = lw;
3074
+ }
3075
+ }
3076
+ labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
3077
+ }
3078
+ const chartW = w - pad.left - pad.right - labelReserve;
3079
+ const buffer = WINDOW_BUFFER;
3080
+ if (!useMultiStash) {
3081
+ const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
3082
+ for (const key of displayValuesRef.current.keys()) {
3083
+ if (!currentIds.has(key)) displayValuesRef.current.delete(key);
3084
+ }
3085
+ }
3086
+ const firstSeries = effectiveMultiSeries[0];
3087
+ const transition = windowTransitionRef.current;
3088
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
3089
+ const now = useMultiStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
3090
+ const smoothValues = /* @__PURE__ */ new Map();
3091
+ for (const s of effectiveMultiSeries) {
3092
+ let dv = displayValuesRef.current.get(s.id);
3093
+ if (dv === void 0) dv = s.value;
3094
+ if (!useMultiStash) {
3095
+ const adaptiveSpeed2 = computeAdaptiveSpeed(
3096
+ s.value,
3097
+ dv,
3098
+ displayMinRef.current,
3099
+ displayMaxRef.current,
3100
+ cfg.lerpSpeed,
3101
+ noMotion
3102
+ );
3103
+ dv = lerp(dv, s.value, adaptiveSpeed2, pausedDt);
3104
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
3105
+ if (Math.abs(dv - s.value) < prevRange * VALUE_SNAP_THRESHOLD) dv = s.value;
3106
+ displayValuesRef.current.set(s.id, dv);
3107
+ }
3108
+ smoothValues.set(s.id, dv);
3109
+ }
3110
+ const hiddenIds = cfg.hiddenSeriesIds;
3111
+ const seriesAlphas = seriesAlphaRef.current;
3112
+ for (const s of effectiveMultiSeries) {
3113
+ let alpha = seriesAlphas.get(s.id) ?? 1;
3114
+ const target = hiddenIds?.has(s.id) ? 0 : 1;
3115
+ alpha = noMotion ? target : lerp(alpha, target, SERIES_TOGGLE_SPEED, pausedDt);
3116
+ if (alpha < 0.01) alpha = 0;
3117
+ if (alpha > 0.99) alpha = 1;
3118
+ seriesAlphas.set(s.id, alpha);
3119
+ }
3120
+ const firstData = pausedMultiDataRef.current?.get(firstSeries.id)?.data ?? firstSeries.data;
3121
+ const windowResult = updateWindowTransition(
3122
+ cfg,
3123
+ transition,
3124
+ displayWindowRef.current,
3125
+ displayMinRef.current,
3126
+ displayMaxRef.current,
3127
+ noMotion,
3128
+ now_ms,
3129
+ now,
3130
+ firstData,
3131
+ smoothValues.get(firstSeries.id) ?? firstSeries.value,
3132
+ buffer
3133
+ );
3134
+ if (transition.startMs > 0 && effectiveMultiSeries.length > 1) {
3135
+ const targetRightEdge = now + cfg.windowSecs * buffer;
3136
+ const targetLeftEdge = targetRightEdge - cfg.windowSecs;
3137
+ let unionMin = Infinity;
3138
+ let unionMax = -Infinity;
3139
+ for (const s of effectiveMultiSeries) {
3140
+ const sData = pausedMultiDataRef.current?.get(s.id)?.data ?? s.data;
3141
+ const sv = smoothValues.get(s.id) ?? s.value;
3142
+ const targetVisible = [];
3143
+ for (const p of sData) {
3144
+ if (p.time >= targetLeftEdge - 2 && p.time <= targetRightEdge) targetVisible.push(p);
3145
+ }
3146
+ if (targetVisible.length > 0) {
3147
+ const range = computeRange(targetVisible, sv, cfg.referenceLine?.value, cfg.exaggerate);
3148
+ if (range.min < unionMin) unionMin = range.min;
3149
+ if (range.max > unionMax) unionMax = range.max;
3150
+ }
3151
+ }
3152
+ if (isFinite(unionMin) && isFinite(unionMax)) {
3153
+ transition.rangeToMin = unionMin;
3154
+ transition.rangeToMax = unionMax;
3155
+ }
3156
+ }
3157
+ displayWindowRef.current = windowResult.windowSecs;
3158
+ const windowSecs = windowResult.windowSecs;
3159
+ const windowTransProgress = windowResult.windowTransProgress;
3160
+ const isWindowTransitioning = transition.startMs > 0;
3161
+ const rightEdge = now + windowSecs * buffer;
3162
+ const leftEdge = rightEdge - windowSecs;
3163
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
3164
+ const seriesEntries = [];
3165
+ let globalMin = Infinity;
3166
+ let globalMax = -Infinity;
3167
+ for (const s of effectiveMultiSeries) {
3168
+ const snap = pausedMultiDataRef.current?.get(s.id);
3169
+ const seriesData = snap?.data ?? s.data;
3170
+ const visible = [];
3171
+ for (const p of seriesData) {
3172
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) visible.push(p);
3173
+ }
3174
+ const sv = smoothValues.get(s.id) ?? s.value;
3175
+ const alpha = seriesAlphas.get(s.id) ?? 1;
3176
+ if (visible.length >= 2) {
3177
+ if (alpha > 0.01) {
3178
+ const range = computeRange(visible, sv, cfg.referenceLine?.value, cfg.exaggerate);
3179
+ if (range.min < globalMin) globalMin = range.min;
3180
+ if (range.max > globalMax) globalMax = range.max;
3181
+ }
3182
+ seriesEntries.push({ visible, smoothValue: sv, palette: s.palette, label: s.label, alpha });
3183
+ }
3184
+ }
3185
+ if (seriesEntries.length === 0) {
3186
+ if (loadingAlpha > 0.01) {
3187
+ drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, cfg.palette.gridLabel);
3188
+ }
3189
+ if (1 - loadingAlpha > 0.01) {
3190
+ drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
3191
+ }
3192
+ ctx.save();
3193
+ ctx.globalCompositeOperation = "destination-out";
3194
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
3195
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
3196
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
3197
+ ctx.fillStyle = fadeGrad;
3198
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
3199
+ ctx.restore();
3200
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
3201
+ rafRef.current = requestAnimationFrame(draw);
3202
+ return;
3203
+ }
3204
+ const computedRange = { min: isFinite(globalMin) ? globalMin : 0, max: isFinite(globalMax) ? globalMax : 1 };
3205
+ const adaptiveSpeed = cfg.lerpSpeed + ADAPTIVE_SPEED_BOOST * 0.5;
3206
+ const rangeResult = updateRange(
3207
+ computedRange,
3208
+ rangeInitedRef.current,
3209
+ targetMinRef.current,
3210
+ targetMaxRef.current,
3211
+ displayMinRef.current,
3212
+ displayMaxRef.current,
3213
+ isWindowTransitioning,
3214
+ windowTransProgress,
3215
+ transition,
3216
+ adaptiveSpeed,
3217
+ chartH,
3218
+ pausedDt
3219
+ );
3220
+ rangeInitedRef.current = rangeResult.rangeInited;
3221
+ targetMinRef.current = rangeResult.targetMin;
3222
+ targetMaxRef.current = rangeResult.targetMax;
3223
+ displayMinRef.current = rangeResult.displayMin;
3224
+ displayMaxRef.current = rangeResult.displayMax;
3225
+ const { minVal, maxVal, valRange } = rangeResult;
3226
+ const layout = {
3227
+ w,
3228
+ h,
3229
+ pad,
3230
+ chartW,
3231
+ chartH,
3232
+ leftEdge,
3233
+ rightEdge,
3234
+ minVal,
3235
+ maxVal,
3236
+ valRange,
3237
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
3238
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
3239
+ };
3240
+ const hoverPx = hoverXRef.current;
3241
+ let drawHoverX = null;
3242
+ let drawHoverTime = null;
3243
+ let isActiveHover = false;
3244
+ let hoverEntries = [];
3245
+ if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
3246
+ const maxHoverX = layout.toX(now);
3247
+ const clampedX = Math.min(hoverPx, maxHoverX);
3248
+ const t = leftEdge + (clampedX - pad.left) / chartW * (rightEdge - leftEdge);
3249
+ drawHoverX = clampedX;
3250
+ drawHoverTime = t;
3251
+ isActiveHover = true;
3252
+ for (const entry of seriesEntries) {
3253
+ if ((entry.alpha ?? 1) < 0.5) continue;
3254
+ const v = interpolateAtTime(entry.visible, t);
3255
+ if (v !== null) {
3256
+ hoverEntries.push({ color: entry.palette.line, label: entry.label ?? "", value: v });
3257
+ }
3258
+ }
3259
+ lastHoverRef.current = { x: clampedX, value: hoverEntries[0]?.value ?? 0, time: t };
3260
+ lastHoverEntriesRef.current = hoverEntries;
3261
+ cfg.onHover?.({ time: t, value: hoverEntries[0]?.value ?? 0, x: clampedX, y: layout.toY(hoverEntries[0]?.value ?? 0) });
3262
+ }
3263
+ const scrubTarget = isActiveHover ? 1 : 0;
3264
+ if (noMotion) {
3265
+ scrubAmountRef.current = scrubTarget;
3266
+ } else {
3267
+ scrubAmountRef.current += (scrubTarget - scrubAmountRef.current) * SCRUB_LERP_SPEED;
3268
+ if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
3269
+ if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
3270
+ }
3271
+ if (!isActiveHover && scrubAmountRef.current > 0 && lastHoverRef.current) {
3272
+ drawHoverX = lastHoverRef.current.x;
3273
+ drawHoverTime = lastHoverRef.current.time;
3274
+ hoverEntries = lastHoverEntriesRef.current;
3275
+ }
3276
+ drawMultiFrame(ctx, layout, {
3277
+ series: seriesEntries,
3278
+ now,
3279
+ showGrid: cfg.showGrid,
3280
+ showPulse: cfg.showPulse,
3281
+ referenceLine: cfg.referenceLine,
3282
+ hoverX: drawHoverX,
3283
+ hoverTime: drawHoverTime,
3284
+ hoverEntries,
3285
+ scrubAmount: scrubAmountRef.current,
3286
+ windowSecs,
3287
+ formatValue: cfg.formatValue,
3288
+ formatTime: cfg.formatTime,
3289
+ gridState: gridStateRef.current,
3290
+ timeAxisState: timeAxisStateRef.current,
3291
+ dt,
3292
+ targetWindowSecs: cfg.windowSecs,
3293
+ tooltipY: cfg.tooltipY,
3294
+ tooltipOutline: cfg.tooltipOutline,
3295
+ chartReveal,
3296
+ pauseProgress,
3297
+ now_ms,
3298
+ primaryPalette: cfg.palette
3299
+ });
3300
+ const bgAlpha = 1 - chartReveal;
3301
+ if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
3302
+ const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
3303
+ if (bgEmptyAlpha > 0.01) {
3304
+ drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
3305
+ }
3306
+ }
3307
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
2778
3308
  } else {
2779
3309
  const effectivePoints = useStash ? lastDataRef.current : points;
2780
3310
  const adaptiveSpeed = computeAdaptiveSpeed(
@@ -2984,6 +3514,7 @@ var defaultFormatTime = (t) => {
2984
3514
  function Liveline({
2985
3515
  data,
2986
3516
  value,
3517
+ series: seriesProp,
2987
3518
  theme = "dark",
2988
3519
  color = "#3b82f6",
2989
3520
  window: windowSecs = 30,
@@ -3023,6 +3554,8 @@ function Liveline({
3023
3554
  lineData,
3024
3555
  lineValue,
3025
3556
  onModeChange,
3557
+ onSeriesToggle,
3558
+ seriesToggleCompact = false,
3026
3559
  className,
3027
3560
  style
3028
3561
  }) {
@@ -3035,8 +3568,27 @@ function Liveline({
3035
3568
  const modeBarRef = (0, import_react2.useRef)(null);
3036
3569
  const modeBtnRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3037
3570
  const [modeIndicatorStyle, setModeIndicatorStyle] = (0, import_react2.useState)(null);
3571
+ const [hiddenSeries, setHiddenSeries] = (0, import_react2.useState)(/* @__PURE__ */ new Set());
3572
+ const lastSeriesPropRef = (0, import_react2.useRef)(seriesProp);
3573
+ if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
3038
3574
  const palette = (0, import_react2.useMemo)(() => resolveTheme(color, theme), [color, theme]);
3039
3575
  const isDark = theme === "dark";
3576
+ const isMultiSeries = seriesProp != null && seriesProp.length > 0;
3577
+ const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
3578
+ const seriesPalettes = (0, import_react2.useMemo)(() => {
3579
+ if (!seriesProp || seriesProp.length === 0) return null;
3580
+ return resolveSeriesPalettes(seriesProp, theme);
3581
+ }, [seriesProp, theme]);
3582
+ const multiSeries = (0, import_react2.useMemo)(() => {
3583
+ if (!seriesProp || !seriesPalettes) return void 0;
3584
+ return seriesProp.map((s, i) => ({
3585
+ id: s.id,
3586
+ data: s.data,
3587
+ value: s.value,
3588
+ palette: seriesPalettes.get(s.id) ?? resolveTheme(s.color || SERIES_COLORS[i % SERIES_COLORS.length], theme),
3589
+ label: s.label
3590
+ }));
3591
+ }, [seriesProp, seriesPalettes, theme]);
3040
3592
  const showMomentum = momentum !== false;
3041
3593
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
3042
3594
  const pad = {
@@ -3078,6 +3630,22 @@ function Liveline({
3078
3630
  });
3079
3631
  }
3080
3632
  }, [activeMode, onModeChange]);
3633
+ const handleSeriesToggle = (0, import_react2.useCallback)((id) => {
3634
+ setHiddenSeries((prev) => {
3635
+ const next = new Set(prev);
3636
+ if (next.has(id)) {
3637
+ next.delete(id);
3638
+ onSeriesToggle?.(id, true);
3639
+ } else {
3640
+ const totalSeries = seriesProp?.length ?? 0;
3641
+ const visibleCount = totalSeries - next.size;
3642
+ if (visibleCount <= 1) return prev;
3643
+ next.add(id);
3644
+ onSeriesToggle?.(id, false);
3645
+ }
3646
+ return next;
3647
+ });
3648
+ }, [seriesProp?.length, onSeriesToggle]);
3081
3649
  const ws = windowStyle ?? "default";
3082
3650
  useLivelineEngine(canvasRef, containerRef, {
3083
3651
  data,
@@ -3086,10 +3654,10 @@ function Liveline({
3086
3654
  windowSecs: effectiveWindowSecs,
3087
3655
  lerpSpeed,
3088
3656
  showGrid: grid,
3089
- showBadge: badge,
3090
- showMomentum,
3657
+ showBadge: isMultiSeries ? false : badge,
3658
+ showMomentum: isMultiSeries ? false : showMomentum,
3091
3659
  momentumOverride,
3092
- showFill: fill,
3660
+ showFill: isMultiSeries ? false : fill,
3093
3661
  referenceLine,
3094
3662
  formatValue,
3095
3663
  formatTime,
@@ -3098,7 +3666,7 @@ function Liveline({
3098
3666
  showPulse: pulse,
3099
3667
  scrub,
3100
3668
  exaggerate,
3101
- degenOptions,
3669
+ degenOptions: isMultiSeries ? void 0 : degenOptions,
3102
3670
  badgeTail,
3103
3671
  badgeVariant,
3104
3672
  tooltipY,
@@ -3115,7 +3683,10 @@ function Liveline({
3115
3683
  liveCandle,
3116
3684
  lineMode,
3117
3685
  lineData,
3118
- lineValue
3686
+ lineValue,
3687
+ multiSeries,
3688
+ isMultiSeries,
3689
+ hiddenSeriesIds: hiddenSeries
3119
3690
  });
3120
3691
  const cursorStyle = scrub ? cursor : "default";
3121
3692
  const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
@@ -3139,7 +3710,7 @@ function Liveline({
3139
3710
  }
3140
3711
  }
3141
3712
  ),
3142
- (windows && windows.length > 0 || onModeChange) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
3713
+ (windows && windows.length > 0 || onModeChange || showSeriesToggle) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
3143
3714
  windows && windows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3144
3715
  "div",
3145
3716
  {
@@ -3207,20 +3778,20 @@ function Liveline({
3207
3778
  style: {
3208
3779
  position: "relative",
3209
3780
  display: "inline-flex",
3210
- gap: 2,
3211
- background: isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3212
- borderRadius: 6,
3213
- padding: 2
3781
+ gap: ws === "text" ? 4 : 2,
3782
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3783
+ borderRadius: ws === "rounded" ? 999 : 6,
3784
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
3214
3785
  },
3215
3786
  children: [
3216
- modeIndicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
3787
+ ws !== "text" && modeIndicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
3217
3788
  position: "absolute",
3218
- top: 2,
3789
+ top: ws === "rounded" ? 3 : 2,
3219
3790
  left: modeIndicatorStyle.left,
3220
3791
  width: modeIndicatorStyle.width,
3221
- height: "calc(100% - 4px)",
3792
+ height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
3222
3793
  background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3223
- borderRadius: 4,
3794
+ borderRadius: ws === "rounded" ? 999 : 4,
3224
3795
  transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3225
3796
  pointerEvents: "none"
3226
3797
  } }),
@@ -3236,7 +3807,7 @@ function Liveline({
3236
3807
  position: "relative",
3237
3808
  zIndex: 1,
3238
3809
  padding: "5px 7px",
3239
- borderRadius: 4,
3810
+ borderRadius: ws === "rounded" ? 999 : 4,
3240
3811
  border: "none",
3241
3812
  cursor: "pointer",
3242
3813
  background: "transparent",
@@ -3267,7 +3838,7 @@ function Liveline({
3267
3838
  position: "relative",
3268
3839
  zIndex: 1,
3269
3840
  padding: "5px 7px",
3270
- borderRadius: 4,
3841
+ borderRadius: ws === "rounded" ? 999 : 4,
3271
3842
  border: "none",
3272
3843
  cursor: "pointer",
3273
3844
  background: "transparent",
@@ -3324,7 +3895,58 @@ function Liveline({
3324
3895
  )
3325
3896
  ]
3326
3897
  }
3327
- )
3898
+ ),
3899
+ showSeriesToggle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
3900
+ display: "inline-flex",
3901
+ gap: ws === "text" ? 4 : 2,
3902
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3903
+ borderRadius: ws === "rounded" ? 999 : 6,
3904
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
3905
+ opacity: isMultiSeries ? 1 : 0,
3906
+ transition: "opacity 0.4s",
3907
+ pointerEvents: isMultiSeries ? "auto" : "none"
3908
+ }, children: (lastSeriesPropRef.current ?? []).map((s, si) => {
3909
+ const isHidden = hiddenSeries.has(s.id);
3910
+ const seriesColor = s.color || SERIES_COLORS[si % SERIES_COLORS.length];
3911
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3912
+ "button",
3913
+ {
3914
+ onClick: () => handleSeriesToggle(s.id),
3915
+ style: {
3916
+ position: "relative",
3917
+ zIndex: 1,
3918
+ fontSize: 11,
3919
+ padding: seriesToggleCompact ? ws === "text" ? "2px 4px" : "5px 7px" : ws === "text" ? "2px 6px" : "3px 8px",
3920
+ borderRadius: ws === "rounded" ? 999 : 4,
3921
+ border: "none",
3922
+ cursor: "pointer",
3923
+ fontFamily: "system-ui, -apple-system, sans-serif",
3924
+ fontWeight: 500,
3925
+ background: isHidden ? "transparent" : ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3926
+ color: isHidden ? inactiveColor : activeColor,
3927
+ opacity: isHidden ? 0.4 : 1,
3928
+ transition: "opacity 0.2s, background 0.15s, color 0.2s",
3929
+ lineHeight: "16px",
3930
+ display: "flex",
3931
+ alignItems: "center",
3932
+ gap: seriesToggleCompact ? 0 : 4
3933
+ },
3934
+ children: [
3935
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
3936
+ width: seriesToggleCompact ? 8 : 6,
3937
+ height: seriesToggleCompact ? 8 : 6,
3938
+ borderRadius: "50%",
3939
+ background: seriesColor,
3940
+ flexShrink: 0,
3941
+ opacity: isHidden ? 0.4 : 1,
3942
+ transition: "opacity 0.2s"
3943
+ } }),
3944
+ !seriesToggleCompact && (s.label ?? s.id)
3945
+ ]
3946
+ },
3947
+ s.id
3948
+ );
3949
+ }) })
3328
3950
  ] }),
3329
3951
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3330
3952
  "div",