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.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";
@@ -486,6 +513,33 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = per
486
513
  }
487
514
  ctx.fill();
488
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
+ }
489
543
  function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
490
544
  const baseAlpha = ctx.globalAlpha;
491
545
  const upTarget = momentum === "up" ? 1 : 0;
@@ -584,6 +638,90 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
584
638
  ctx.fillText(separator + timeText, tx + valueW, ty);
585
639
  ctx.restore();
586
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
+ }
587
725
 
588
726
  // src/draw/referenceLine.ts
589
727
  function drawReferenceLine(ctx, layout, palette, ref) {
@@ -1430,6 +1568,124 @@ function drawFrame(ctx, layout, palette, opts) {
1430
1568
  ctx.restore();
1431
1569
  }
1432
1570
  }
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();
1583
+ }
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
+ }
1433
1689
  function drawCandleFrame(ctx, layout, palette, opts) {
1434
1690
  const { w, h, pad, chartW, chartH } = layout;
1435
1691
  const reveal = opts.chartReveal;
@@ -1712,6 +1968,7 @@ var PAUSE_PROGRESS_SPEED = 0.12;
1712
1968
  var PAUSE_CATCHUP_SPEED = 0.08;
1713
1969
  var PAUSE_CATCHUP_SPEED_FAST = 0.22;
1714
1970
  var LOADING_ALPHA_SPEED = 0.14;
1971
+ var SERIES_TOGGLE_SPEED = 0.1;
1715
1972
  var CANDLE_LERP_SPEED = 0.25;
1716
1973
  var CANDLE_WIDTH_TRANS_MS = 300;
1717
1974
  var LINE_MORPH_MS = 500;
@@ -2035,6 +2292,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2035
2292
  const configRef = useRef(config);
2036
2293
  configRef.current = config;
2037
2294
  const displayValueRef = useRef(config.value);
2295
+ const displayValuesRef = useRef(/* @__PURE__ */ new Map());
2296
+ const seriesAlphaRef = useRef(/* @__PURE__ */ new Map());
2038
2297
  const displayMinRef = useRef(0);
2039
2298
  const displayMaxRef = useRef(0);
2040
2299
  const targetMinRef = useRef(0);
@@ -2067,12 +2326,15 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2067
2326
  const hoverXRef = useRef(null);
2068
2327
  const scrubAmountRef = useRef(0);
2069
2328
  const lastHoverRef = useRef(null);
2329
+ const lastHoverEntriesRef = useRef([]);
2070
2330
  const chartRevealRef = useRef(0);
2071
2331
  const pauseProgressRef = useRef(0);
2072
2332
  const timeDebtRef = useRef(0);
2073
2333
  const lastDataRef = useRef([]);
2334
+ const lastMultiSeriesRef = useRef([]);
2074
2335
  const frozenNowRef = useRef(0);
2075
2336
  const pausedDataRef = useRef(null);
2337
+ const pausedMultiDataRef = useRef(null);
2076
2338
  const loadingAlphaRef = useRef(config.loading ? 1 : 0);
2077
2339
  const displayCandleRef = useRef(null);
2078
2340
  const liveBirthAlphaRef = useRef(1);
@@ -2252,6 +2514,17 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2252
2514
  pausedLineDataRef.current = null;
2253
2515
  pausedLineValueRef.current = null;
2254
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
+ }
2255
2528
  } else {
2256
2529
  if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
2257
2530
  pausedDataRef.current = cfg.data.slice();
@@ -2262,7 +2535,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2262
2535
  }
2263
2536
  const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
2264
2537
  const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
2265
- const hasData = isCandle ? effectiveCandles.length >= 2 : points.length >= 2;
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;
2266
2540
  const pad = cfg.padding;
2267
2541
  const chartH = h - pad.top - pad.bottom;
2268
2542
  const pauseTarget = cfg.paused ? 1 : 0;
@@ -2298,11 +2572,23 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2298
2572
  rangeInitedRef.current = false;
2299
2573
  }
2300
2574
  let useStash;
2575
+ let useMultiStash = false;
2301
2576
  if (isCandle) {
2302
2577
  useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
2303
2578
  } else {
2304
- useStash = !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
2305
- if (hasData) lastDataRef.current = points;
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;
2306
2592
  }
2307
2593
  if (isCandle) {
2308
2594
  const lmt = lineModeTransRef.current;
@@ -2324,8 +2610,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2324
2610
  lineModeProgRef.current = lmt.to;
2325
2611
  }
2326
2612
  }
2327
- if (!hasData && !useStash) {
2328
- const loadingColor = isCandle ? cfg.palette.gridLabel : void 0;
2613
+ if (!hasData && !useStash && !useMultiStash) {
2614
+ const loadingColor = isCandle || cfg.isMultiSeries || lastMultiSeriesRef.current.length > 0 ? cfg.palette.gridLabel : void 0;
2329
2615
  if (loadingAlpha > 0.01) {
2330
2616
  drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
2331
2617
  }
@@ -2748,6 +3034,250 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2748
3034
  badgeRef.current.container.style.display = "none";
2749
3035
  }
2750
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);
3278
+ }
3279
+ }
3280
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
2751
3281
  } else {
2752
3282
  const effectivePoints = useStash ? lastDataRef.current : points;
2753
3283
  const adaptiveSpeed = computeAdaptiveSpeed(
@@ -2957,6 +3487,7 @@ var defaultFormatTime = (t) => {
2957
3487
  function Liveline({
2958
3488
  data,
2959
3489
  value,
3490
+ series: seriesProp,
2960
3491
  theme = "dark",
2961
3492
  color = "#3b82f6",
2962
3493
  window: windowSecs = 30,
@@ -2996,6 +3527,8 @@ function Liveline({
2996
3527
  lineData,
2997
3528
  lineValue,
2998
3529
  onModeChange,
3530
+ onSeriesToggle,
3531
+ seriesToggleCompact = false,
2999
3532
  className,
3000
3533
  style
3001
3534
  }) {
@@ -3008,8 +3541,27 @@ function Liveline({
3008
3541
  const modeBarRef = useRef2(null);
3009
3542
  const modeBtnRefs = useRef2(/* @__PURE__ */ new Map());
3010
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;
3011
3547
  const palette = useMemo(() => resolveTheme(color, theme), [color, theme]);
3012
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]);
3013
3565
  const showMomentum = momentum !== false;
3014
3566
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
3015
3567
  const pad = {
@@ -3051,6 +3603,22 @@ function Liveline({
3051
3603
  });
3052
3604
  }
3053
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]);
3054
3622
  const ws = windowStyle ?? "default";
3055
3623
  useLivelineEngine(canvasRef, containerRef, {
3056
3624
  data,
@@ -3059,10 +3627,10 @@ function Liveline({
3059
3627
  windowSecs: effectiveWindowSecs,
3060
3628
  lerpSpeed,
3061
3629
  showGrid: grid,
3062
- showBadge: badge,
3063
- showMomentum,
3630
+ showBadge: isMultiSeries ? false : badge,
3631
+ showMomentum: isMultiSeries ? false : showMomentum,
3064
3632
  momentumOverride,
3065
- showFill: fill,
3633
+ showFill: isMultiSeries ? false : fill,
3066
3634
  referenceLine,
3067
3635
  formatValue,
3068
3636
  formatTime,
@@ -3071,7 +3639,7 @@ function Liveline({
3071
3639
  showPulse: pulse,
3072
3640
  scrub,
3073
3641
  exaggerate,
3074
- degenOptions,
3642
+ degenOptions: isMultiSeries ? void 0 : degenOptions,
3075
3643
  badgeTail,
3076
3644
  badgeVariant,
3077
3645
  tooltipY,
@@ -3088,7 +3656,10 @@ function Liveline({
3088
3656
  liveCandle,
3089
3657
  lineMode,
3090
3658
  lineData,
3091
- lineValue
3659
+ lineValue,
3660
+ multiSeries,
3661
+ isMultiSeries,
3662
+ hiddenSeriesIds: hiddenSeries
3092
3663
  });
3093
3664
  const cursorStyle = scrub ? cursor : "default";
3094
3665
  const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
@@ -3112,7 +3683,7 @@ function Liveline({
3112
3683
  }
3113
3684
  }
3114
3685
  ),
3115
- (windows && windows.length > 0 || onModeChange) && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
3686
+ (windows && windows.length > 0 || onModeChange || showSeriesToggle) && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
3116
3687
  windows && windows.length > 0 && /* @__PURE__ */ jsxs(
3117
3688
  "div",
3118
3689
  {
@@ -3180,20 +3751,20 @@ function Liveline({
3180
3751
  style: {
3181
3752
  position: "relative",
3182
3753
  display: "inline-flex",
3183
- gap: 2,
3184
- background: isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3185
- borderRadius: 6,
3186
- padding: 2
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
3187
3758
  },
3188
3759
  children: [
3189
- modeIndicatorStyle && /* @__PURE__ */ jsx("div", { style: {
3760
+ ws !== "text" && modeIndicatorStyle && /* @__PURE__ */ jsx("div", { style: {
3190
3761
  position: "absolute",
3191
- top: 2,
3762
+ top: ws === "rounded" ? 3 : 2,
3192
3763
  left: modeIndicatorStyle.left,
3193
3764
  width: modeIndicatorStyle.width,
3194
- height: "calc(100% - 4px)",
3765
+ height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
3195
3766
  background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3196
- borderRadius: 4,
3767
+ borderRadius: ws === "rounded" ? 999 : 4,
3197
3768
  transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3198
3769
  pointerEvents: "none"
3199
3770
  } }),
@@ -3209,7 +3780,7 @@ function Liveline({
3209
3780
  position: "relative",
3210
3781
  zIndex: 1,
3211
3782
  padding: "5px 7px",
3212
- borderRadius: 4,
3783
+ borderRadius: ws === "rounded" ? 999 : 4,
3213
3784
  border: "none",
3214
3785
  cursor: "pointer",
3215
3786
  background: "transparent",
@@ -3240,7 +3811,7 @@ function Liveline({
3240
3811
  position: "relative",
3241
3812
  zIndex: 1,
3242
3813
  padding: "5px 7px",
3243
- borderRadius: 4,
3814
+ borderRadius: ws === "rounded" ? 999 : 4,
3244
3815
  border: "none",
3245
3816
  cursor: "pointer",
3246
3817
  background: "transparent",
@@ -3297,7 +3868,58 @@ function Liveline({
3297
3868
  )
3298
3869
  ]
3299
3870
  }
3300
- )
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
+ }) })
3301
3923
  ] }),
3302
3924
  /* @__PURE__ */ jsx(
3303
3925
  "div",