liveline 0.0.6 → 0.0.7

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Liveline
2
2
 
3
- Real-time animated charts for React. Line and candlestick modes, canvas-rendered, 60fps, zero CSS imports.
3
+ Real-time animated charts for React. Line, multi-series, and candlestick modes, canvas-rendered, 60fps, zero CSS imports.
4
4
 
5
5
  ## Install
6
6
 
@@ -54,6 +54,7 @@ The component fills its parent container. Set a height on the parent. Pass `data
54
54
  | `badgeTail` | `boolean` | `true` | Pointed tail on badge pill |
55
55
  | `fill` | `boolean` | `true` | Gradient under the curve |
56
56
  | `pulse` | `boolean` | `true` | Pulsing ring on live dot |
57
+ | `lineWidth` | `number` | `2` | Stroke width of the main line in pixels |
57
58
 
58
59
  **Features**
59
60
 
@@ -83,6 +84,16 @@ When `mode="candle"`, pass `candles` (committed OHLC bars) and `liveCandle` (the
83
84
 
84
85
  The `onModeChange` prop renders a built-in line/candle toggle next to the time window buttons.
85
86
 
87
+ **Multi-series**
88
+
89
+ | Prop | Type | Default | Description |
90
+ |------|------|---------|-------------|
91
+ | `series` | `LivelineSeries[]` | — | Multiple overlapping lines `{ id, data, value, color, label? }` |
92
+ | `onSeriesToggle` | `(id, visible) => void` | — | Callback when a series is toggled via built-in chips |
93
+ | `seriesToggleCompact` | `boolean` | `false` | Show only colored dots in toggle (no text labels) |
94
+
95
+ Pass `series` instead of `data`/`value` to draw multiple lines sharing the same axes. Each series gets its own color, label, and endpoint dot. Toggle chips appear automatically when there are 2+ series — clicking one hides/shows that line with a smooth fade. The Y-axis range adjusts when series are hidden. Badge, momentum arrows, and fill are disabled in multi-series mode.
96
+
86
97
  **State**
87
98
 
88
99
  | Prop | Type | Default | Description |
@@ -123,7 +134,7 @@ When `loading` flips to `false` with data present, the loading line morphs into
123
134
  | `formatValue` | `(v: number) => string` | `v.toFixed(2)` | Value label formatter |
124
135
  | `formatTime` | `(t: number) => string` | `HH:MM:SS` | Time axis formatter |
125
136
  | `lerpSpeed` | `number` | `0.08` | Interpolation speed (0–1) |
126
- | `padding` | `Padding` | `{ top: 12, right: 80, bottom: 28, left: 12 }` | Chart padding override |
137
+ | `padding` | `Padding` | `{ top: 12, right: auto, bottom: 28, left: 12 }` | Chart padding override (`right` is 80/54/12 based on badge/grid) |
127
138
  | `onHover` | `(point \| null) => void` | — | Hover callback with `{ time, value, x, y }` |
128
139
  | `cursor` | `string` | `'crosshair'` | CSS cursor on canvas hover |
129
140
  | `className` | `string` | — | Container class |
@@ -209,6 +220,30 @@ When `loading` flips to `false` with data present, the loading line morphs into
209
220
  />
210
221
  ```
211
222
 
223
+ ### Multi-series (prediction market)
224
+
225
+ ```tsx
226
+ <Liveline
227
+ data={[]}
228
+ value={0}
229
+ series={[
230
+ { id: 'yes', data: yesData, value: yesValue, color: '#3b82f6', label: 'Yes' },
231
+ { id: 'no', data: noData, value: noValue, color: '#ef4444', label: 'No' },
232
+ ]}
233
+ grid
234
+ scrub
235
+ pulse
236
+ windowStyle="rounded"
237
+ formatValue={(v) => v.toFixed(1) + '%'}
238
+ onSeriesToggle={(id, visible) => console.log(id, visible)}
239
+ windows={[
240
+ { label: '10s', secs: 10 },
241
+ { label: '30s', secs: 30 },
242
+ { label: '1m', secs: 60 },
243
+ ]}
244
+ />
245
+ ```
246
+
212
247
  ### Loading + pause
213
248
 
214
249
  ```tsx
@@ -241,6 +276,7 @@ When `loading` flips to `false` with data present, the loading line morphs into
241
276
  - **Frame-rate-independent lerp** on value, Y-axis range, badge color, and scrub opacity
242
277
  - **Candlestick rendering** — OHLC bodies + wicks with bull/bear coloring, smooth live candle updates
243
278
  - **Line/candle morph** — candle bodies collapse to close price, morph line extends center-out, coordinated alpha crossfade
279
+ - **Multi-series** — overlapping lines with per-series toggle, smooth alpha fade, and dynamic Y-axis range
244
280
  - **ResizeObserver** tracks container size — no per-frame layout reads
245
281
  - **Theme derivation** — full palette from one accent color + light/dark mode
246
282
  - **Binary search interpolation** for hover value lookup
package/dist/index.cjs CHANGED
@@ -1985,6 +1985,7 @@ var BADGE_Y_LERP_TRANSITIONING = 0.5;
1985
1985
  var MOMENTUM_COLOR_LERP = 0.12;
1986
1986
  var WINDOW_TRANSITION_MS = 750;
1987
1987
  var WINDOW_BUFFER = 0.05;
1988
+ var WINDOW_BUFFER_NO_BADGE = 0.015;
1988
1989
  var VALUE_SNAP_THRESHOLD = 1e-3;
1989
1990
  var ADAPTIVE_SPEED_BOOST = 0.2;
1990
1991
  var MOMENTUM_GREEN = [34, 197, 94];
@@ -2006,7 +2007,7 @@ var LINE_ADAPTIVE_BOOST = 0.2;
2006
2007
  var LINE_SNAP_THRESHOLD = 1e-3;
2007
2008
  var RANGE_LERP_SPEED = 0.15;
2008
2009
  var RANGE_ADAPTIVE_BOOST = 0.2;
2009
- var CANDLE_BUFFER = 0.05;
2010
+ var CANDLE_BUFFER_NO_BADGE = 0.015;
2010
2011
  function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
2011
2012
  const valGap = Math.abs(value - displayValue);
2012
2013
  const prevRange = displayMax - displayMin || 1;
@@ -2658,6 +2659,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2658
2659
  return;
2659
2660
  }
2660
2661
  if (isCandle) {
2662
+ const candleBuffer = CANDLE_BUFFER_NO_BADGE;
2661
2663
  if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
2662
2664
  const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
2663
2665
  const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
@@ -2700,7 +2702,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2700
2702
  cwt.rangeFromMin = displayMinRef.current;
2701
2703
  cwt.rangeFromMax = displayMaxRef.current;
2702
2704
  const curWindow = displayWindowRef.current;
2703
- const re = now + curWindow * CANDLE_BUFFER;
2705
+ const re = now + curWindow * candleBuffer;
2704
2706
  const le = re - curWindow;
2705
2707
  const targetVis = [];
2706
2708
  for (const c of effectiveCandles) {
@@ -2751,13 +2753,13 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2751
2753
  effectiveCandles,
2752
2754
  rawLive,
2753
2755
  candleWidthSecs,
2754
- CANDLE_BUFFER
2756
+ candleBuffer
2755
2757
  );
2756
2758
  displayWindowRef.current = windowResult.windowSecs;
2757
2759
  const windowSecs = windowResult.windowSecs;
2758
2760
  const windowTransProgress = windowResult.windowTransProgress;
2759
2761
  const isWindowTransitioning = transition.startMs > 0;
2760
- const rightEdge = now + windowSecs * CANDLE_BUFFER;
2762
+ const rightEdge = now + windowSecs * candleBuffer;
2761
2763
  const leftEdge = rightEdge - windowSecs;
2762
2764
  let smoothLive;
2763
2765
  if (rawLive) {
@@ -3076,7 +3078,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
3076
3078
  labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
3077
3079
  }
3078
3080
  const chartW = w - pad.left - pad.right - labelReserve;
3079
- const buffer = WINDOW_BUFFER;
3081
+ const buffer = cfg.showBadge ? WINDOW_BUFFER : WINDOW_BUFFER_NO_BADGE;
3080
3082
  if (!useMultiStash) {
3081
3083
  const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
3082
3084
  for (const key of displayValuesRef.current.keys()) {
@@ -3326,8 +3328,9 @@ function useLivelineEngine(canvasRef, containerRef, config) {
3326
3328
  }
3327
3329
  const smoothValue = displayValueRef.current;
3328
3330
  const chartW = w - pad.left - pad.right;
3329
- const needsArrowRoom = cfg.showMomentum;
3330
- const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
3331
+ const baseBuffer = cfg.showBadge ? WINDOW_BUFFER : WINDOW_BUFFER_NO_BADGE;
3332
+ const needsArrowRoom = cfg.showMomentum && cfg.showBadge;
3333
+ const buffer = needsArrowRoom ? Math.max(baseBuffer, 37 / Math.max(chartW, 1)) : baseBuffer;
3331
3334
  const transition = windowTransitionRef.current;
3332
3335
  if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
3333
3336
  const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
@@ -3556,6 +3559,7 @@ function Liveline({
3556
3559
  onModeChange,
3557
3560
  onSeriesToggle,
3558
3561
  seriesToggleCompact = false,
3562
+ lineWidth,
3559
3563
  className,
3560
3564
  style
3561
3565
  }) {
@@ -3571,7 +3575,11 @@ function Liveline({
3571
3575
  const [hiddenSeries, setHiddenSeries] = (0, import_react2.useState)(/* @__PURE__ */ new Set());
3572
3576
  const lastSeriesPropRef = (0, import_react2.useRef)(seriesProp);
3573
3577
  if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
3574
- const palette = (0, import_react2.useMemo)(() => resolveTheme(color, theme), [color, theme]);
3578
+ const palette = (0, import_react2.useMemo)(() => {
3579
+ const p = resolveTheme(color, theme);
3580
+ if (lineWidth != null) p.lineWidth = lineWidth;
3581
+ return p;
3582
+ }, [color, theme, lineWidth]);
3575
3583
  const isDark = theme === "dark";
3576
3584
  const isMultiSeries = seriesProp != null && seriesProp.length > 0;
3577
3585
  const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
@@ -3591,9 +3599,10 @@ function Liveline({
3591
3599
  }, [seriesProp, seriesPalettes, theme]);
3592
3600
  const showMomentum = momentum !== false;
3593
3601
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
3602
+ const defaultRight = badge ? 80 : grid ? 54 : 12;
3594
3603
  const pad = {
3595
3604
  top: paddingOverride?.top ?? 12,
3596
- right: paddingOverride?.right ?? 80,
3605
+ right: paddingOverride?.right ?? defaultRight,
3597
3606
  bottom: paddingOverride?.bottom ?? 28,
3598
3607
  left: paddingOverride?.left ?? 12
3599
3608
  };
package/dist/index.d.cts CHANGED
@@ -81,6 +81,7 @@ interface LivelineProps {
81
81
  onHover?: (point: HoverPoint | null) => void;
82
82
  cursor?: string;
83
83
  pulse?: boolean;
84
+ lineWidth?: number;
84
85
  mode?: 'line' | 'candle';
85
86
  candles?: CandlePoint[];
86
87
  candleWidth?: number;
@@ -102,7 +103,7 @@ interface CandlePoint {
102
103
  close: number;
103
104
  }
104
105
 
105
- declare function Liveline({ data, value, series: seriesProp, theme, color, window: windowSecs, grid, badge, momentum, fill, scrub, loading, paused, emptyText, exaggerate, degen: degenProp, badgeTail, badgeVariant, showValue, valueMomentumColor, windows, onWindowChange, windowStyle, tooltipY, tooltipOutline, orderbook, referenceLine, formatValue, formatTime, lerpSpeed, padding: paddingOverride, onHover, cursor, pulse, mode, candles, candleWidth, liveCandle, lineMode, lineData, lineValue, onModeChange, onSeriesToggle, seriesToggleCompact, className, style, }: LivelineProps): react_jsx_runtime.JSX.Element;
106
+ declare function Liveline({ data, value, series: seriesProp, theme, color, window: windowSecs, grid, badge, momentum, fill, scrub, loading, paused, emptyText, exaggerate, degen: degenProp, badgeTail, badgeVariant, showValue, valueMomentumColor, windows, onWindowChange, windowStyle, tooltipY, tooltipOutline, orderbook, referenceLine, formatValue, formatTime, lerpSpeed, padding: paddingOverride, onHover, cursor, pulse, mode, candles, candleWidth, liveCandle, lineMode, lineData, lineValue, onModeChange, onSeriesToggle, seriesToggleCompact, lineWidth, className, style, }: LivelineProps): react_jsx_runtime.JSX.Element;
106
107
 
107
108
  interface LivelineTransitionProps {
108
109
  /** Key of the active child to display. Must match a child's `key` prop. */
package/dist/index.d.ts CHANGED
@@ -81,6 +81,7 @@ interface LivelineProps {
81
81
  onHover?: (point: HoverPoint | null) => void;
82
82
  cursor?: string;
83
83
  pulse?: boolean;
84
+ lineWidth?: number;
84
85
  mode?: 'line' | 'candle';
85
86
  candles?: CandlePoint[];
86
87
  candleWidth?: number;
@@ -102,7 +103,7 @@ interface CandlePoint {
102
103
  close: number;
103
104
  }
104
105
 
105
- declare function Liveline({ data, value, series: seriesProp, theme, color, window: windowSecs, grid, badge, momentum, fill, scrub, loading, paused, emptyText, exaggerate, degen: degenProp, badgeTail, badgeVariant, showValue, valueMomentumColor, windows, onWindowChange, windowStyle, tooltipY, tooltipOutline, orderbook, referenceLine, formatValue, formatTime, lerpSpeed, padding: paddingOverride, onHover, cursor, pulse, mode, candles, candleWidth, liveCandle, lineMode, lineData, lineValue, onModeChange, onSeriesToggle, seriesToggleCompact, className, style, }: LivelineProps): react_jsx_runtime.JSX.Element;
106
+ declare function Liveline({ data, value, series: seriesProp, theme, color, window: windowSecs, grid, badge, momentum, fill, scrub, loading, paused, emptyText, exaggerate, degen: degenProp, badgeTail, badgeVariant, showValue, valueMomentumColor, windows, onWindowChange, windowStyle, tooltipY, tooltipOutline, orderbook, referenceLine, formatValue, formatTime, lerpSpeed, padding: paddingOverride, onHover, cursor, pulse, mode, candles, candleWidth, liveCandle, lineMode, lineData, lineValue, onModeChange, onSeriesToggle, seriesToggleCompact, lineWidth, className, style, }: LivelineProps): react_jsx_runtime.JSX.Element;
106
107
 
107
108
  interface LivelineTransitionProps {
108
109
  /** Key of the active child to display. Must match a child's `key` prop. */
package/dist/index.js CHANGED
@@ -1958,6 +1958,7 @@ var BADGE_Y_LERP_TRANSITIONING = 0.5;
1958
1958
  var MOMENTUM_COLOR_LERP = 0.12;
1959
1959
  var WINDOW_TRANSITION_MS = 750;
1960
1960
  var WINDOW_BUFFER = 0.05;
1961
+ var WINDOW_BUFFER_NO_BADGE = 0.015;
1961
1962
  var VALUE_SNAP_THRESHOLD = 1e-3;
1962
1963
  var ADAPTIVE_SPEED_BOOST = 0.2;
1963
1964
  var MOMENTUM_GREEN = [34, 197, 94];
@@ -1979,7 +1980,7 @@ var LINE_ADAPTIVE_BOOST = 0.2;
1979
1980
  var LINE_SNAP_THRESHOLD = 1e-3;
1980
1981
  var RANGE_LERP_SPEED = 0.15;
1981
1982
  var RANGE_ADAPTIVE_BOOST = 0.2;
1982
- var CANDLE_BUFFER = 0.05;
1983
+ var CANDLE_BUFFER_NO_BADGE = 0.015;
1983
1984
  function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
1984
1985
  const valGap = Math.abs(value - displayValue);
1985
1986
  const prevRange = displayMax - displayMin || 1;
@@ -2631,6 +2632,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2631
2632
  return;
2632
2633
  }
2633
2634
  if (isCandle) {
2635
+ const candleBuffer = CANDLE_BUFFER_NO_BADGE;
2634
2636
  if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
2635
2637
  const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
2636
2638
  const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
@@ -2673,7 +2675,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2673
2675
  cwt.rangeFromMin = displayMinRef.current;
2674
2676
  cwt.rangeFromMax = displayMaxRef.current;
2675
2677
  const curWindow = displayWindowRef.current;
2676
- const re = now + curWindow * CANDLE_BUFFER;
2678
+ const re = now + curWindow * candleBuffer;
2677
2679
  const le = re - curWindow;
2678
2680
  const targetVis = [];
2679
2681
  for (const c of effectiveCandles) {
@@ -2724,13 +2726,13 @@ function useLivelineEngine(canvasRef, containerRef, config) {
2724
2726
  effectiveCandles,
2725
2727
  rawLive,
2726
2728
  candleWidthSecs,
2727
- CANDLE_BUFFER
2729
+ candleBuffer
2728
2730
  );
2729
2731
  displayWindowRef.current = windowResult.windowSecs;
2730
2732
  const windowSecs = windowResult.windowSecs;
2731
2733
  const windowTransProgress = windowResult.windowTransProgress;
2732
2734
  const isWindowTransitioning = transition.startMs > 0;
2733
- const rightEdge = now + windowSecs * CANDLE_BUFFER;
2735
+ const rightEdge = now + windowSecs * candleBuffer;
2734
2736
  const leftEdge = rightEdge - windowSecs;
2735
2737
  let smoothLive;
2736
2738
  if (rawLive) {
@@ -3049,7 +3051,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
3049
3051
  labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
3050
3052
  }
3051
3053
  const chartW = w - pad.left - pad.right - labelReserve;
3052
- const buffer = WINDOW_BUFFER;
3054
+ const buffer = cfg.showBadge ? WINDOW_BUFFER : WINDOW_BUFFER_NO_BADGE;
3053
3055
  if (!useMultiStash) {
3054
3056
  const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
3055
3057
  for (const key of displayValuesRef.current.keys()) {
@@ -3299,8 +3301,9 @@ function useLivelineEngine(canvasRef, containerRef, config) {
3299
3301
  }
3300
3302
  const smoothValue = displayValueRef.current;
3301
3303
  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 baseBuffer = cfg.showBadge ? WINDOW_BUFFER : WINDOW_BUFFER_NO_BADGE;
3305
+ const needsArrowRoom = cfg.showMomentum && cfg.showBadge;
3306
+ const buffer = needsArrowRoom ? Math.max(baseBuffer, 37 / Math.max(chartW, 1)) : baseBuffer;
3304
3307
  const transition = windowTransitionRef.current;
3305
3308
  if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
3306
3309
  const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
@@ -3529,6 +3532,7 @@ function Liveline({
3529
3532
  onModeChange,
3530
3533
  onSeriesToggle,
3531
3534
  seriesToggleCompact = false,
3535
+ lineWidth,
3532
3536
  className,
3533
3537
  style
3534
3538
  }) {
@@ -3544,7 +3548,11 @@ function Liveline({
3544
3548
  const [hiddenSeries, setHiddenSeries] = useState(/* @__PURE__ */ new Set());
3545
3549
  const lastSeriesPropRef = useRef2(seriesProp);
3546
3550
  if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
3547
- const palette = useMemo(() => resolveTheme(color, theme), [color, theme]);
3551
+ const palette = useMemo(() => {
3552
+ const p = resolveTheme(color, theme);
3553
+ if (lineWidth != null) p.lineWidth = lineWidth;
3554
+ return p;
3555
+ }, [color, theme, lineWidth]);
3548
3556
  const isDark = theme === "dark";
3549
3557
  const isMultiSeries = seriesProp != null && seriesProp.length > 0;
3550
3558
  const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
@@ -3564,9 +3572,10 @@ function Liveline({
3564
3572
  }, [seriesProp, seriesPalettes, theme]);
3565
3573
  const showMomentum = momentum !== false;
3566
3574
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
3575
+ const defaultRight = badge ? 80 : grid ? 54 : 12;
3567
3576
  const pad = {
3568
3577
  top: paddingOverride?.top ?? 12,
3569
- right: paddingOverride?.right ?? 80,
3578
+ right: paddingOverride?.right ?? defaultRight,
3570
3579
  bottom: paddingOverride?.bottom ?? 28,
3571
3580
  left: paddingOverride?.left ?? 12
3572
3581
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liveline",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "description": "Real-time animated charts for React — line, candlestick, and multi-series modes",
6
6
  "keywords": [