npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nativeline

v0.0.4

Published

Real-time animated charts for React Native + Expo — line, candlestick, and multi-series modes, Skia-rendered, 60fps on the UI thread, with live and static viewports

Readme

Nativeline

npm version npm downloads license types

Real-time animated charts for React Native + Expo. Line, candlestick, multi‑series, orderbook, and degen modes — Skia‑rendered at 60fps, with live and static viewports.

Nativeline is a React Native port of liveline: the same buttery interpolation engine and visual language, rebuilt on @shopify/react-native-skia + Reanimated, plus a static viewport for historical / fixed‑range data that liveline can't do.

import { Nativeline } from 'nativeline'

<View style={{ height: 300 }}>
  <Nativeline data={data} value={price} viewport="live" color="#3b82f6" />
</View>

Contents


Features

  • Five modes, one component — line, candlestick (with a line↔candle morph), multi‑series, orderbook stream, and "degen" particles + shake. Selected by props, not different components.
  • Live and static viewports — trail the wall clock like a ticker, or pin a fixed historical domain. Both still animate (value/range lerps, entrance morphs, scrub).
  • 60fps on Skia — every frame is recorded as an SkPicture and composited off the JS thread. No per‑frame React re‑renders.
  • Frictionless formattersformatValue / formatTime are ordinary JS closures (no 'worklet'), so you can format currency, dates, anything.
  • Scrub / crosshair with a gesture‑driven tooltip that glides back to the live tip on release.
  • Momentum aware — auto up/down/flat detection drives the badge color, dot, and optional arrows.
  • Fully themeable from a single accent color, light/dark, with a background hook so labels never halo on a colored card.
  • TypeScript‑first, dual ESM + CJS, tree‑shakeable, zero runtime deps (Skia / Reanimated / Gesture Handler are peers).

Install

Nativeline renders with Skia, a native module — it works with Expo via a dev client / prebuild / EAS build, not Expo Go.

npm install nativeline
# peers (install the versions your app already uses):
npx expo install @shopify/react-native-skia react-native-reanimated react-native-gesture-handler

Then:

  1. Reanimated Babel plugin — add 'react-native-reanimated/plugin' last in babel.config.js plugins.
  2. Gesture handler root — wrap your app in <GestureHandlerRootView style={{ flex: 1 }}>.
  3. Build a dev clientnpx expo run:ios / run:android, or an EAS dev build. (Expo Go can't load Skia.)

Peer versions (minimums): react >=18, react-native >=0.73, @shopify/react-native-skia >=1.5.0, react-native-reanimated >=3.10.0, react-native-gesture-handler >=2.16.0.


Quick start

import { useEffect, useRef, useState } from 'react'
import { View } from 'react-native'
import { Nativeline } from 'nativeline'
import type { NativelinePoint } from 'nativeline'

export function LivePrice() {
  const [data, setData] = useState<NativelinePoint[]>([])
  const [value, setValue] = useState(100)
  const v = useRef(100)

  useEffect(() => {
    const id = setInterval(() => {
      v.current = Math.max(1, v.current + (Math.random() - 0.5) * 2)
      const now = Date.now() / 1000
      setValue(v.current)
      setData((prev) => [...prev, { time: now, value: v.current }].slice(-300))
    }, 500)
    return () => clearInterval(id)
  }, [])

  return (
    <View style={{ height: 300 }}>
      <Nativeline
        data={data}
        value={value}
        viewport="live"
        window={30}
        color="#3b82f6"
        theme="dark"
        formatValue={(v) => `$${v.toFixed(2)}`}
      />
    </View>
  )
}

Data shape. Every point is { time: unixSeconds, value: number }, ordered ascending by time (a caller contract — nativeline doesn't sort for you). value is the current value that drives the badge and the live dot.


Feeding data — live & static

The viewport is nativeline's one real addition over liveline. viewport only changes how the X axis behaves; you feed the points either way. Both viewports animate — the difference is whether the axis chases the wall clock.

| | viewport="live" | viewport="static" | |---|---|---| | X axis | trails Date.now() | fixed domain (or auto‑fit) | | Scrolls with time | ✅ | ❌ | | Animates value/range/scrub | ✅ | ✅ | | Left‑edge fade | ✅ | ❌ | | Use for | tickers, live feeds | history, ranges, snapshots |

Live data — stream ticks in

Keep an array of points in state and append each tick, trimming to a rolling window. Drive value from the same tick. The axis trails Date.now(), so the point's time must be current (Date.now() / 1000). Do the fast mutation on a ref and let a light setState push the array — the chart itself never re‑renders React (it reads the config via a ref and animates on its own loop).

function LiveChart({ socket }: { socket: PriceSocket }) {
  const [data, setData] = useState<NativelinePoint[]>([])
  const [value, setValue] = useState(0)

  useEffect(() => {
    const off = socket.onTick((price) => {
      const now = Date.now() / 1000
      setValue(price)
      setData((prev) => {
        const next = [...prev, { time: now, value: price }]
        // keep a rolling window a bit larger than what's shown (window=30s here)
        const cutoff = now - 120
        return next[0] && next[0].time < cutoff ? next.filter((p) => p.time >= cutoff) : next
      })
    })
    return off
  }, [socket])

  return (
    <View style={{ height: 300 }}>
      <Nativeline data={data} value={value} viewport="live" window={30} color="#4d8bff" />
    </View>
  )
}
  • window is the trailing span in seconds (default 30). Keep a little more data than window so the left edge has something to fade out.
  • Polling instead of a socket? Same shape — replace socket.onTick with a setInterval that fetches the latest price.
  • Gaps are fine: if ticks stop, the last point simply trails toward the left as the clock moves.

Static data — load a historical range

Fetch a fixed dataset once, hand it over with viewport="static", and set value to the last point. Give a domain (unix seconds) to pin the exact range, or omit it to auto‑fit to the data's own extent. Nothing chases the clock — but the entrance, value/range lerps, and scrub still animate.

function HistoryChart({ tokenId, range }: { tokenId: string; range: '1D' | '1W' | '1M' }) {
  const [data, setData] = useState<NativelinePoint[]>([])

  // Re-fetch whenever the range changes — each range is its own static dataset.
  useEffect(() => {
    let alive = true
    fetchCandles(tokenId, range).then((rows) => {
      if (!alive) return
      setData(rows.map((r) => ({ time: r.t, value: r.close })))
    })
    return () => { alive = false }
  }, [tokenId, range])

  const last = data.length ? data[data.length - 1].value : 0

  return (
    <View style={{ height: 300 }}>
      <Nativeline
        data={data}
        value={last}
        viewport="static"
        // domain={{ from: rangeStartSecs, to: rangeEndSecs }}  // or omit to auto-fit
        color="#4d8bff"
        formatValue={(v) => `$${v.toFixed(2)}`}
        formatTime={(t) => new Date(t * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
      />
    </View>
  )
}
  • Switching ranges = swapping the data array; nativeline morphs the range/value smoothly on the change.
  • Candles? Same idea — pass candles/candleWidth (+ mode="candle") instead of a value line. See Modes.
  • Use formatTime for the axis labels and formatHoverTime for a range‑aware scrub tooltip (date on wide ranges, time intraday).

Modes

<Nativeline> is a single component; mode / series / orderbook / degen select what it draws.

Line (default)

Line + gradient fill + live dot + pulse + momentum arrows.

<Nativeline data={data} value={value} color="#3b82f6" />

Candlestick

OHLC bars with a smooth morph to/from the line. Pass data/lineData too to enable the morph; hover reads O/H/L/C, and the value badge shows beside the latest candle (colored by the candle's bull/bear).

<Nativeline
  mode="candle"
  candles={candles}         // { time, open, high, low, close }[]
  candleWidth={3600}        // candle interval, seconds
  data={lineData} value={last} lineData={lineData} lineValue={last}
  onModeChange={(m) => setMode(m)}
/>

Multi‑series

Several lines on one shared axis, per‑series dots + a combined crosshair, and built‑in show/hide chips. Overrides data/value.

<Nativeline
  series={[
    { id: 'btc', label: 'BTC', color: '#f7931a', value: 100, data: btc },
    { id: 'eth', label: 'ETH', color: '#627eea', value: 100, data: eth },
  ]}
  seriesToggle          // built-in chips; set false to use your own legend
  formatValue={(v) => `${Math.round(v)}%`}
/>

Orderbook

A Kalshi‑style rising $size stream (green bids / red asks) behind a live line; its speed follows price momentum and how fast the book churns.

<Nativeline
  data={data}
  value={value}
  orderbook={{ bids: [[price, size], /* … */], asks: [[price, size], /* … */] }}
/>

Degen

Particle bursts fly off the tip on up‑swings, and the whole chart briefly shakes.

<Nativeline data={data} value={value} degen />
// or tune it:
<Nativeline data={data} value={value} degen={{ scale: 1.4, downMomentum: true }} />

Theming

Everything derives from a single accent color + theme. Momentum colors are always semantic green/red.

<Nativeline
  color="#3b82f6"        // accent: line, fill, badge, dot-flat
  theme="dark"           // 'light' | 'dark'
  background={cardBg}    // set to your container color so label outlines/fades don't halo
  lineWidth={2}
/>
  • background — nativeline fades grid labels and outlines the orderbook stream toward the background rather than with alpha. If your <Canvas> sits on a card that isn't the theme's default background, pass that card color here so nothing gets a dark halo.
  • badgeMomentumColortrue (default) colors the badge green/red by momentum; set false to keep it the accent color (useful when color already encodes direction).
  • momentumfalse hides the arrows entirely; 'up' / 'down' forces the direction; true auto‑detects.

Props

data and value are the only required props. Everything else is optional.

Data

| Prop | Type | Default | Notes | |---|---|---|---| | data | NativelinePoint[] | — | { time: unixSeconds, value }, ascending by time | | value | number | — | current value → badge + live dot | | series | NativelineSeries[] | — | multi‑series; overrides data/value |

Viewport

| Prop | Type | Default | Notes | |---|---|---|---| | viewport | 'live' \| 'static' | 'live' | live trails the clock; static is a fixed domain | | domain | { from, to } | auto‑fit | static X range (unix seconds) | | window | number | 30 | live trailing window, seconds |

Modes

| Prop | Type | Default | Notes | |---|---|---|---| | mode | 'line' \| 'candle' | 'line' | with candles + candleWidth | | candles | CandlePoint[] | — | { time, open, high, low, close }[] | | candleWidth | number | — | candle interval, seconds | | liveCandle | CandlePoint | — | in‑progress candle that updates each tick | | lineData / lineValue | NativelinePoint[] / number | — | enable the line↔candle morph | | orderbook | OrderbookData | — | { bids: [price,size][], asks: [price,size][] } (line mode) | | degen | boolean \| { scale?, downMomentum? } | false | particles + shake (line mode) |

Appearance

| Prop | Type | Default | Notes | |---|---|---|---| | color | string | #3b82f6 | accent | | theme | 'light' \| 'dark' | 'dark' | | | background | string | theme bg | match your container to kill label halos | | lineWidth | number | 2 | | | grid fill scrub pulse | boolean | true | feature flags | | xAxis / yAxis | boolean | true | hide the time / value axis labels for compact cards (grid lines stay — use grid={false} for no lines) | | badge | boolean | true | the value pill at the tip — shows in candle mode too | | badgeScale | number | 1 | badge size multiplier | | badgeMomentumColor | boolean | true | false = badge uses color | | badgeVariant | 'default' \| 'minimal' | 'default' | | | badgeTail | boolean | true | the little pointer on the pill | | momentum | boolean \| 'up' \| 'down' | true | false hides arrows; string forces direction | | referenceLine | { value, label? } | — | horizontal marker | | padding | { top, right, bottom, left } | auto | the right gutter auto‑measures the widest grid/badge number so labels never clip; pass right to override | | showValue | boolean | false | large value overlay above the chart | | valueMomentumColor | boolean | false | color the showValue overlay by momentum | | tooltipY / tooltipOutline | number / boolean | 14 / true | scrub tooltip position / outline |

Multi‑series

| Prop | Type | Default | Notes | |---|---|---|---| | seriesToggle | boolean | true | built‑in show/hide chips | | seriesToggleCompact | boolean | false | dots only, no labels | | onSeriesToggle | (id, visible) => void | — | toggle callback |

Window buttons

| Prop | Type | Notes | |---|---|---| | windows | { label, secs }[] | render live‑window buttons | | onWindowChange | (secs) => void | window callback | | windowStyle | 'default' \| 'rounded' \| 'text' | button style |

Formatters & callbacks

| Prop | Type | Notes | |---|---|---| | formatValue | (v: number) => string | axis + badge + tooltip (plain JS) | | formatTime | (t: number) => string | time axis labels | | formatHoverTime | (t: number) => string | scrub tooltip time (falls back to formatTime) | | onHover | (HoverPoint \| null) => void | scrub callback → { time, value, x, y } | | onModeChange | (mode) => void | |

React Native

| Prop | Type | Notes | |---|---|---| | style | StyleProp<ViewStyle> | on the wrapping view | | fonts | { label?, tooltip?, badge?, value? } | per‑part Skia SkFont overrides (grid, scrub tooltip, badge, showValue) | | labelFontFamily | string | monospace family (e.g. 'Menlo') — applies to every part unless overridden per‑font | | loading paused | boolean | loading shimmer / freeze | | emptyText | string | empty‑state label | | exaggerate | boolean | tighter range for near‑flat data | | lerpSpeed | number | smoothing speed (0–1) |

Full types: src/types.ts.


Recipes

Persist the user's line/candle choice (e.g. with zustand + AsyncStorage):

const mode = useChartStore((s) => s.mode)        // default 'line'
<Nativeline mode={mode} onModeChange={useChartStore.getState().setMode} /* … */ />

Drive a price display from the crosshair:

const [cursor, setCursor] = useState<number | null>(null)
<Nativeline data={data} value={value} onHover={(p) => setCursor(p ? p.value : null)} />
<Text>{formatMoney(cursor ?? value)}</Text>

Range‑aware hover time (date on wide ranges, time intraday):

<Nativeline
  formatTime={(t) => fmtAxis(t, range)}
  formatHoverTime={(t) => (range === '1D' ? fmtTime(t) : fmtDate(t))}
/>

Hide the momentum arrows but keep the momentum‑colored badge:

<Nativeline momentum={false} />

Live window buttons:

<Nativeline
  windows={[{ label: '15s', secs: 15 }, { label: '1m', secs: 60 }, { label: '5m', secs: 300 }]}
  onWindowChange={(secs) => console.log('window', secs)}
/>

Compact card — a spark‑style chart with no axis clutter (grid lines stay, labels go):

<Nativeline data={data} value={value} xAxis={false} yAxis={false} />
// or drop the grid lines too:
<Nativeline data={data} value={value} grid={false} />

Custom per‑part typography — override any font independently (build them with Skia's matchFont):

import { matchFont } from '@shopify/react-native-skia'

const mono = { fontFamily: 'Menlo', fontSize: 11 }
<Nativeline
  labelFontFamily="Menlo"                              // family for every part
  fonts={{
    label: matchFont(mono),                            // grid / axis labels
    tooltip: matchFont({ fontFamily: 'Inter', fontSize: 13 }), // scrub tooltip
    badge: matchFont({ ...mono, fontSize: 12 }),        // the value pill
  }}
/>

Performance

Nativeline runs its animation loop on the JS thread (a requestAnimationFrame loop mutating refs, exactly like liveline) and records an SkPicture per frame that Skia composites off‑thread. React never re‑renders on data updates — only on discrete UI events (window pill, hidden series, mode toggle).

This is a deliberate trade‑off: the loop being on the JS thread is what lets your formatValue / formatTime be ordinary JS. If your JS thread is busy (heavy lists, frequent setState), you may see occasional jank — keep the surrounding screen light, and feed data with a ref + throttled setState rather than on every event.

Hot paths (like the particle field) batch their Skia draws through a single reused Paint to keep per‑frame allocation — and GC — low.


Troubleshooting / FAQ

"Unable to resolve nativeline" after installing. Almost always a stale Metro cache — restart with npx expo start -c. Metro builds its resolver map at startup, so a plain reload won't pick up a new dependency.

"PlatformConstants could not be found" / a second React Native instance. A duplicate copy of react-native (or Skia/Reanimated) got bundled. Make sure nativeline's peers resolve to your app's single copies — a normal npm install does this; if you're linking nativeline locally, force single copies of the peers in metro.config.js.

Nothing renders / blank canvas. You're probably on Expo Go — Skia needs a dev client (expo run:ios / EAS dev build). Also confirm the chart has a fixed height (<View style={{ height: 300 }}>).

Labels have a dark halo on my card. Pass background={yourCardColor} so nativeline fades/outlines toward the actual background instead of the theme default.

The badge overlaps my y‑axis labels. Give the chart room on the right: padding={{ right: 64 }} (it auto‑reserves some, but tune it to your formatValue width).

Tiny prices show weird axis labels. nativeline keeps precision for tiny values — pass a formatValue that renders them the way you want (e.g. subscript notation).


Architecture

Four layers, one stateful node — identical shape to liveline:

Component   Nativeline.tsx        public API, prop normalization, chrome, discrete UI state
   │
Engine      engine/useEngine.ts   JS-thread rAF loop, refs, the live/static viewport provider;
   │                              builds ChartLayout {toX,toY} each frame, records an SkPicture
Draw        draw/* · skia/gfx.ts  Skia painting (PictureRecorder), fixed painter's order
   │
Math/Theme  math/* · theme.ts     numeric core (spline, lerp, range, momentum, interpolate)

Each frame the engine advances all animated state, records an SkPicture in strict back‑to‑front order, and assigns it to a Reanimated shared value. <Canvas><Picture picture={sv}/></Canvas> composites that picture off the JS thread.


Why a port instead of liveline?

liveline is Canvas 2D + DOM + requestAnimationFrame — none of which exist in React Native. Nativeline keeps liveline's brain (all numeric + animation logic is portable) and replaces its eyes:

| Concern | liveline (web) | nativeline (RN) | |---|---|---| | Rendering surface | <canvas> / CanvasRenderingContext2D | @shopify/react-native-skia | | Frame loop | requestAnimationFrame | requestAnimationFrame → records an SkPicture | | Curve | ctx.bezierCurveTo | path.cubicTo (same Fritsch‑Carlson spline) | | Left‑edge fade | globalCompositeOperation | BlendMode.DstOut | | Text measure | ctx.measureText | SkFont.measureText | | Hover / scrub | mouse + touch | react-native-gesture-handler Pan | | showValue overlay | DOM + CSS | Reanimated <Animated.Text> |

The math and theme derivation port near‑verbatim. The engine deliberately runs on the JS thread (not a worklet), so your formatter closures are ordinary JS.

Fixes baked in vs. liveline: lerp speed clamped to [0, 0.999] (a speed >= 1 produced NaN that poisoned every smoothed value); non‑finite data sanitized at ingestion; a single unified color parser (liveline had two that disagreed); a bounded grid‑label loop; reduced‑motion honored in the candle pipeline.


Contributing

git clone https://github.com/ministic-dev/nativeline
cd nativeline
npm install
npm test          # vitest — 49 tests over the pure core
npm run typecheck # tsc --noEmit
npm run build     # tsup → dist (ESM + CJS + d.ts)

The example/ folder is a standalone Expo app that exercises every mode:

cd example && npm install && npx expo run:ios

Roadmap

Feature‑complete — every liveline mode is ported plus the static viewport. Before 1.0:

  • [x] Line + candlestick + multi‑series + orderbook + degen
  • [x] Live + static viewports
  • [x] Unit tests for the pure core (49)
  • [x] Standalone example app (all six modes)
  • [ ] More prop‑level tests / visual regression
  • [ ] Web target (Skia web)

License

MIT. Portions ported from liveline (MIT, © 2026 Benji Taylor) — see LICENSE.