{"_id":"@ajfuller/react-native-liveline","_rev":"3-cbbc1be587e94080bb1c525a2a277329","name":"@ajfuller/react-native-liveline","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@ajfuller/react-native-liveline","version":"0.1.0","keywords":["react-native","ios","android"],"author":{"url":"https://github.com/fuller","name":"Andrew Fuller","email":"andrew_fuller@outlook.com"},"license":"MIT","_id":"@ajfuller/react-native-liveline@0.1.0","maintainers":[{"name":"ajfuller","email":"andrew_fuller@outlook.com"}],"contributors":[{"url":"https://github.com/benjitaylor","name":"Benji Taylor"}],"homepage":"https://github.com/fuller/react-native-liveline#readme","bugs":{"url":"https://github.com/fuller/react-native-liveline/issues"},"dist":{"shasum":"181b3030a31c27c33f636141f1b7d6ebab07faaa","tarball":"https://registry.npmjs.org/@ajfuller/react-native-liveline/-/react-native-liveline-0.1.0.tgz","fileCount":180,"integrity":"sha512-znVlRQzaHYZKSjVIEEsFbL6SseaD31JzWKLitD+8EUDiKy6XyQkjUT3hmG/e5ttSe3itZZtL6h224XPp19nJzA==","signatures":[{"sig":"MEUCIQDYe9ZtYLZU4gHPegj99iqshqmJpPONF8gcng/Zkuh4OQIgJYrGzUKeBlr3F5fgRNieDD7mEaWfVKIMPAwjBxzDjRU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":727638},"jest":{"preset":"react-native","modulePathIgnorePatterns":["<rootDir>/example/node_modules","<rootDir>/lib/"]},"main":"./lib/module/index.js","types":"./lib/typescript/src/index.d.ts","exports":{".":{"types":"./lib/typescript/src/index.d.ts","default":"./lib/module/index.js","react-native-liveline-source":"./src/index.tsx"},"./package.json":"./package.json"},"gitHead":"f0efb67626cf3228ceecc9ef8a8b7265456b95c3","scripts":{"lint":"eslint \"**/*.{js,ts,tsx}\"","test":"jest","clean":"del-cli lib","example":"yarn workspace react-native-liveline-example","prepare":"bob build","typecheck":"tsc"},"_npmUser":{"name":"ajfuller","email":"andrew_fuller@outlook.com"},"prettier":{"useTabs":false,"tabWidth":2,"quoteProps":"consistent","singleQuote":true,"trailingComma":"es5"},"repository":{"url":"git+https://github.com/fuller/react-native-liveline.git","type":"git"},"workspaces":["example"],"_npmVersion":"11.4.1","description":"Real-time animated charts for React Native. Line, multi-series, and candlestick modes, Skia-rendered, 60fps.","directories":{},"_nodeVersion":"22.16.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"packageManager":"yarn@4.11.0","devDependencies":{"jest":"^29.7.0","react":"19.2.0","turbo":"^2.9.16","eslint":"^9.39.4","del-cli":"^7.0.0","prettier":"^3.8.3","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/jest":"^30.0.0","@types/react":"^19.2.0","react-native":"0.83.10","@eslint/compat":"^2.1.0","@eslint/eslintrc":"^3.3.5","eslint-plugin-ft-flow":"^3.0.11","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.6","react-native-reanimated":"4.2.1","react-native-builder-bob":"^0.43.0","@react-native/babel-preset":"0.85.0","@shopify/react-native-skia":"2.4.18","@react-native/eslint-config":"0.85.0","react-native-gesture-handler":"~2.30.0"},"peerDependencies":{"react":"*","react-native":"*","react-native-reanimated":">=3.16.0","@shopify/react-native-skia":">=2.0.0","react-native-gesture-handler":">=2.16.0"},"_npmOperationalInternal":{"tmp":"tmp/react-native-liveline_0.1.0_1784318960346_0.7944561086653332","host":"s3://npm-registry-packages-npm-production"},"react-native-builder-bob":{"output":"lib","source":"src","targets":[["module",{"esm":true}],["typescript",{"project":"tsconfig.build.json"}]]},"create-react-native-library":{"type":"library","tools":["eslint"],"version":"0.63.0","languages":"js"}},"0.2.1":{"name":"@ajfuller/react-native-liveline","version":"0.2.1","keywords":["react-native","ios","android"],"author":{"url":"https://github.com/fuller","name":"Andrew Fuller","email":"andrew_fuller@outlook.com"},"license":"MIT","_id":"@ajfuller/react-native-liveline@0.2.1","maintainers":[{"name":"ajfuller","email":"andrew_fuller@outlook.com"}],"contributors":[{"url":"https://github.com/benjitaylor","name":"Benji Taylor"}],"homepage":"https://github.com/fuller/react-native-liveline#readme","bugs":{"url":"https://github.com/fuller/react-native-liveline/issues"},"dist":{"shasum":"7e80c0132757e57e8e76b5907e8b91a0fb22bee5","tarball":"https://registry.npmjs.org/@ajfuller/react-native-liveline/-/react-native-liveline-0.2.1.tgz","fileCount":245,"integrity":"sha512-PDBVOzboEsxSaSN/CwUGWfMMxzxoaJGTesXq2eg2/W41XcmKVqnKwDk8UCxzUSwLhZJmS0lcvg0DXvZKg+uU5w==","signatures":[{"sig":"MEYCIQCPc4UscWChchTkHm5PROYvPpnIyl2l2FgqaDME/squfAIhAN5tmgvajzo4SLGmoiv/W3NihTo+uH/N4rUJAN7xKCWS","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1151447},"jest":{"preset":"react-native","modulePathIgnorePatterns":["<rootDir>/example/node_modules","<rootDir>/lib/"]},"main":"./lib/module/index.js","types":"./lib/typescript/src/index.d.ts","exports":{".":{"types":"./lib/typescript/src/index.d.ts","default":"./lib/module/index.js","react-native-liveline-source":"./src/index.tsx"},"./package.json":"./package.json"},"gitHead":"50f6f5389276b5b0484e302b3e344f8b0eebb737","scripts":{"lint":"eslint \"**/*.{js,ts,tsx}\"","test":"jest","clean":"del-cli lib","example":"yarn workspace react-native-liveline-example","prepare":"bob build","typecheck":"tsc"},"_npmUser":{"name":"ajfuller","email":"andrew_fuller@outlook.com"},"prettier":{"useTabs":false,"tabWidth":2,"quoteProps":"consistent","singleQuote":true,"trailingComma":"es5"},"repository":{"url":"git+https://github.com/fuller/react-native-liveline.git","type":"git"},"workspaces":["example"],"_npmVersion":"11.4.1","description":"Real-time animated charts for React Native. Line, multi-series, and candlestick modes, Skia-rendered, 60fps.","directories":{},"sideEffects":false,"_nodeVersion":"22.16.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"packageManager":"yarn@4.11.0","devDependencies":{"jest":"^29.7.0","react":"19.2.0","turbo":"^2.9.16","eslint":"^9.39.4","del-cli":"^7.0.0","prettier":"^3.8.3","@eslint/js":"^9.39.5","typescript":"^6.0.3","@types/jest":"^30.0.0","@types/react":"^19.2.0","react-native":"0.83.10","@eslint/compat":"^2.1.0","@eslint/eslintrc":"^3.3.5","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.6","react-native-reanimated":"4.2.1","react-native-builder-bob":"^0.43.0","@react-native/babel-preset":"0.85.0","@shopify/react-native-skia":"2.4.18","@react-native/eslint-config":"0.85.0","react-native-gesture-handler":"3.1.0"},"peerDependencies":{"react":"*","react-native":"*","react-native-worklets":">=0.3.0","react-native-reanimated":">=4.0.0","@shopify/react-native-skia":">=2.0.0","react-native-gesture-handler":">=3.0.0"},"_npmOperationalInternal":{"tmp":"tmp/react-native-liveline_0.2.1_1785283639635_0.6374632174582358","host":"s3://npm-registry-packages-npm-production"},"react-native-builder-bob":{"output":"lib","source":"src","targets":[["module",{"esm":true}],["typescript",{"project":"tsconfig.build.json"}]]},"create-react-native-library":{"type":"library","tools":["eslint"],"version":"0.63.0","languages":"js"}},"0.3.0":{"name":"@ajfuller/react-native-liveline","version":"0.3.0","description":"Real-time animated charts for React Native. Line, multi-series, and candlestick modes, Skia-rendered, 60fps.","sideEffects":false,"main":"./lib/module/index.js","types":"./lib/typescript/src/index.d.ts","exports":{".":{"react-native-liveline-source":"./src/index.tsx","types":"./lib/typescript/src/index.d.ts","default":"./lib/module/index.js"},"./package.json":"./package.json"},"scripts":{"example":"yarn workspace react-native-liveline-example","clean":"del-cli lib","prepare":"bob build","typecheck":"tsc","test":"jest","lint":"eslint \"**/*.{js,ts,tsx}\""},"jest":{"preset":"react-native","modulePathIgnorePatterns":["<rootDir>/example/node_modules","<rootDir>/lib/","<rootDir>/.claude/"]},"keywords":["react-native","ios","android"],"repository":{"type":"git","url":"git+https://github.com/fuller/react-native-liveline.git"},"author":{"name":"Andrew Fuller","email":"andrew_fuller@outlook.com","url":"https://github.com/fuller"},"contributors":[{"name":"Benji Taylor","url":"https://github.com/benjitaylor"}],"license":"MIT","bugs":{"url":"https://github.com/fuller/react-native-liveline/issues"},"homepage":"https://github.com/fuller/react-native-liveline#readme","publishConfig":{"registry":"https://registry.npmjs.org/","access":"public"},"devDependencies":{"@eslint/compat":"^2.1.0","@eslint/eslintrc":"^3.3.5","@eslint/js":"^9.39.5","@react-native/babel-preset":"0.85.0","@react-native/eslint-config":"0.85.0","@shopify/react-native-skia":"2.4.18","@types/jest":"^30.0.0","@types/react":"^19.2.0","del-cli":"^7.0.0","eslint":"^9.39.4","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.6","jest":"^29.7.0","prettier":"^3.8.3","react":"19.2.0","react-native":"0.83.10","react-native-builder-bob":"^0.43.0","react-native-gesture-handler":"3.1.0","react-native-reanimated":"4.2.1","turbo":"^2.9.16","typescript":"^6.0.3"},"peerDependencies":{"@shopify/react-native-skia":">=2.0.0","react":"*","react-native":"*","react-native-gesture-handler":">=2.30.0","react-native-reanimated":">=4.0.0","react-native-worklets":">=0.3.0"},"workspaces":["example"],"packageManager":"yarn@4.11.0","react-native-builder-bob":{"source":"src","output":"lib","targets":[["module",{"esm":true}],["typescript",{"project":"tsconfig.build.json"}]]},"prettier":{"quoteProps":"consistent","singleQuote":true,"tabWidth":2,"trailingComma":"es5","useTabs":false},"create-react-native-library":{"type":"library","languages":"js","tools":["eslint"],"version":"0.63.0"},"_id":"@ajfuller/react-native-liveline@0.3.0","gitHead":"1383294d767f0b0f3434e46677141b4e7cb8f834","_nodeVersion":"22.16.0","_npmVersion":"11.4.1","dist":{"integrity":"sha512-bwWJaAvpg8ovlEYPP/f1jrsyADOuoCIIcMSQ4TYAnqk8A9ZDAENMgJ7EJA9Bblo+SiQ1Oy1PFMnXItAgvno/VQ==","shasum":"8bd08bc848ac694d4b84ebcd86956062bb8e4648","tarball":"https://registry.npmjs.org/@ajfuller/react-native-liveline/-/react-native-liveline-0.3.0.tgz","fileCount":280,"unpackedSize":1539169,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDzW2iW7pBudYU94girXDEcY/cgc3umbTzu61s4SsTnkAiBuqGtqEYrAmHCUK7kawfHwld4DXnzAxehCGK69j1TBxQ=="}]},"_npmUser":{"name":"ajfuller","email":"andrew_fuller@outlook.com"},"directories":{},"maintainers":[{"name":"ajfuller","email":"andrew_fuller@outlook.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/react-native-liveline_0.3.0_1785811710224_0.9132034237626154"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T20:09:20.224Z","modified":"2026-08-04T02:48:30.560Z","0.1.0":"2026-07-17T20:09:20.529Z","0.2.1":"2026-07-29T00:07:19.805Z","0.3.0":"2026-08-04T02:48:30.411Z"},"bugs":{"url":"https://github.com/fuller/react-native-liveline/issues"},"author":{"name":"Andrew Fuller","email":"andrew_fuller@outlook.com","url":"https://github.com/fuller"},"license":"MIT","homepage":"https://github.com/fuller/react-native-liveline#readme","keywords":["react-native","ios","android"],"repository":{"type":"git","url":"git+https://github.com/fuller/react-native-liveline.git"},"description":"Real-time animated charts for React Native. Line, multi-series, and candlestick modes, Skia-rendered, 60fps.","contributors":[{"name":"Benji Taylor","url":"https://github.com/benjitaylor"}],"maintainers":[{"name":"ajfuller","email":"andrew_fuller@outlook.com"}],"readme":"# react-native-liveline\n\n[![npm version](https://img.shields.io/npm/v/@ajfuller/react-native-liveline.svg)](https://www.npmjs.com/package/@ajfuller/react-native-liveline)\n[![license](https://img.shields.io/npm/l/@ajfuller/react-native-liveline.svg)](./LICENSE)\n\nA React Native port of [liveline](https://github.com/benjitaylor/liveline) by\n[Benji Taylor](https://github.com/benjitaylor) — real-time animated charts\n(line, multi-series, candlestick) with the same SDK shape as the web version.\nThe chart keeps animating even when your JS thread is blocked: the render\nengine runs entirely on the UI thread as a Reanimated worklet, painting into a\nSkia picture every frame, independent of React renders and JS-thread work.\n\nAll the hard design and animation work here — the curve fitting, the\nmomentum/degen feel, the badge and crosshair interactions, the candlestick\nmorph — is Benji's. This package adapts it to run natively via Reanimated\nworklets and Skia; see [Credits](#credits) below.\n\n## Installation\n\nPublished on npm: [`@ajfuller/react-native-liveline`](https://www.npmjs.com/package/@ajfuller/react-native-liveline).\n\n```sh\nnpm install @ajfuller/react-native-liveline\n```\n\nPeer dependencies (install alongside):\n\n```sh\nnpm install @shopify/react-native-skia react-native-reanimated react-native-worklets react-native-gesture-handler\n```\n\n| Peer | Required range |\n|---|---|\n| `@shopify/react-native-skia` | `>=2.0.0` |\n| `react-native-reanimated` | `>=4.0.0` |\n| `react-native-worklets` | `>=0.3.0` |\n| `react-native-gesture-handler` | `>=2.30.0` |\n\nReanimated 4 split its worklets runtime out into the separate\n`react-native-worklets` package (Reanimated itself declares this as its own\npeer dependency as of 4.0.0), so it's required alongside Reanimated here too.\nThis library hasn't been tested against Reanimated 3.x.\n\n**gesture-handler 2.30 and 3.x are both supported.** The scrub gesture uses\nthe classic `Gesture.Pan()` builder API, which exists unchanged in both majors\n— 3.x keeps it working but marks it deprecated in favour of a new hook-based\nAPI, and migrating is deferred to a future release. The `>=2.30.0` floor\nmatters for Expo: SDK 55 pins `~2.30.0`, so a narrower range would put every\nSDK 55 app into a peer conflict and an `expo-doctor` failure. Compatibility is\nverified by installing 2.30.0 and running the typecheck against it, not by\ninspection.\n\n### Reanimated Babel plugin\n\nAdd the Reanimated plugin to your `babel.config.js` (must be listed **last**):\n\n```js\nmodule.exports = {\n  presets: ['module:@react-native/babel-preset'],\n  plugins: [\n    // ...your other plugins\n    'react-native-reanimated/plugin',\n  ],\n};\n```\n\n### GestureHandlerRootView\n\nScrubbing is implemented as a gesture-handler `Pan` gesture. Wrap your app\nroot (once, near the top) in `GestureHandlerRootView`:\n\n```tsx\nimport { GestureHandlerRootView } from 'react-native-gesture-handler';\n\nexport default function App() {\n  return (\n    <GestureHandlerRootView style={{ flex: 1 }}>\n      {/* rest of your app */}\n    </GestureHandlerRootView>\n  );\n}\n```\n\n### Expo\n\nBecause this library depends on native code (Skia, Reanimated,\ngesture-handler), it does **not** work in Expo Go. Use a\n[development build](https://docs.expo.dev/develop/development-builds/introduction/)\n(`npx expo prebuild` + a custom dev client) or a bare workflow app.\n\n## Quick start\n\n```tsx\nimport { useEffect, useState } from 'react';\nimport { View } from 'react-native';\nimport { Liveline } from '@ajfuller/react-native-liveline';\nimport type { LivelinePoint } from '@ajfuller/react-native-liveline';\n\nfunction Chart() {\n  const [data, setData] = useState<LivelinePoint[]>([]);\n  const [value, setValue] = useState(0);\n\n  useEffect(() => {\n    // Feed data from a WebSocket, polling, etc.\n    // Each point: { time: unixSeconds, value: number }\n    let v = 100;\n    const id = setInterval(() => {\n      v += (Math.random() - 0.5) * 2;\n      setValue(v);\n      setData((prev) => [...prev, { time: Date.now() / 1000, value: v }]);\n    }, 500);\n    return () => clearInterval(id);\n  }, []);\n\n  return (\n    <View style={{ height: 300 }}>\n      <Liveline data={data} value={value} color=\"#3b82f6\" theme=\"dark\" />\n    </View>\n  );\n}\n```\n\nThe component fills its parent container — set a height on the parent. Pass\n`data` as a growing array of points and `value` as the latest number;\n`Liveline` handles smooth interpolation between updates.\n\n## Props\n\n**Data**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `LivelinePoint[]` | required | Array of `{ time, value }` points |\n| `value` | `number` | required | Latest value (smoothly interpolated) |\n\n**Appearance**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `theme` | `'light' \\| 'dark'` | `'dark'` | Color scheme |\n| `color` | `string` | `'#3b82f6'` | Accent color — all palette colors derived from this. Accepts `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`, or `rgba()`. Named CSS colors (e.g. `\"red\"`) are not supported and fall back to grey. |\n| `grid` | `boolean` | `true` | Y-axis grid lines + labels |\n| `badge` | `boolean` | `true` | Value pill tracking chart tip |\n| `badgeVariant` | `'default' \\| 'minimal'` | `'default'` | Badge style: accent-colored or white with grey text |\n| `badgeTail` | `boolean` | `true` | Pointed tail on badge pill |\n| `fill` | `boolean` | `true` | Gradient under the curve |\n| `pulse` | `boolean` | `true` | Pulsing ring on live dot |\n| `lineWidth` | `number` | `2` | Stroke width of the main line in pixels |\n\n**Features**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `momentum` | `boolean \\| Momentum` | `true` | Dot glow + arrows. `true` = auto-detect, or `'up' \\| 'down' \\| 'flat'` |\n| `scrub` | `boolean` | `true` | Crosshair scrubbing on touch-drag |\n| `scrubActivationDelay` | `number` | `0` | Ms of long-press before the scrub pan activates. `0` = immediate. Set e.g. `300` when the chart is embedded in a `ScrollView`/`FlatList` so flick-scrolls aren't stolen by the crosshair on first touch |\n| `active` | `boolean` | `true` | Suspends the per-frame UI-thread callback entirely when `false` — wire to list viewability so off-screen charts cost nothing. See [Charts in lists](#charts-in-lists) below |\n| `exaggerate` | `boolean` | `false` | Tight Y-axis — small moves fill chart height |\n| `showValue` | `boolean` | `false` | Large live value overlay (60fps, no re-renders) |\n| `valueMomentumColor` | `boolean` | `false` | Color the value text green/red by momentum |\n| `degen` | `boolean \\| DegenOptions` | `false` | Burst particles + chart shake on momentum swings |\n\n**Candlestick**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `mode` | `'line' \\| 'candle'` | `'line'` | Chart type |\n| `candles` | `CandlePoint[]` | — | OHLC candle data `{ time, open, high, low, close }` |\n| `candleWidth` | `number` | — | Seconds per candle |\n| `liveCandle` | `CandlePoint` | — | Current in-progress candle with real-time OHLC |\n| `lineMode` | `boolean` | — | Morph candles into a line display |\n| `lineData` | `LivelinePoint[]` | — | Tick-level data for line mode density |\n| `lineValue` | `number` | — | Current tick value for line mode |\n| `onModeChange` | `(mode: 'line' \\| 'candle') => void` | — | Callback for built-in line/candle toggle |\n\nWhen `mode=\"candle\"`, pass `candles` (committed OHLC bars) and `liveCandle`\n(the current bar, updated every tick). `candleWidth` sets the time bucket in\nseconds. The `lineMode` prop smoothly morphs between candle and line views —\ncandle bodies collapse to close price, then the line extends outward. Provide\n`lineData` and `lineValue` (tick-level resolution) for a smooth density\ntransition during the morph. The `onModeChange` prop renders a built-in\nline/candle toggle next to the time window buttons.\n\n**Multi-series**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `series` | `LivelineSeries[]` | — | Multiple overlapping lines `{ id, data, value, color, label? }` |\n| `onSeriesToggle` | `(id: string, visible: boolean) => void` | — | Callback when a series is toggled via built-in chips |\n| `seriesToggleCompact` | `boolean` | `false` | Show only colored dots in toggle (no text labels) |\n\nPass `series` instead of `data`/`value` to draw multiple lines sharing the\nsame axes. Each series gets its own color, label, and endpoint dot. Toggle\nchips appear automatically when there are 2+ series — tapping one hides/shows\nthat line with a smooth fade. The Y-axis range adjusts when series are\nhidden. Badge, momentum arrows, and fill are disabled in multi-series mode.\n\n**State**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `loading` | `boolean` | `false` | Breathing line animation — use while waiting for data |\n| `paused` | `boolean` | `false` | Smoothly freeze chart scrolling; resume catches up to real time |\n| `emptyText` | `string` | `'No data to display'` | Text shown in the empty state |\n\nWhen `loading` flips to `false` with data present, the loading line morphs\ninto the actual chart shape. In line mode, the fill, grid, and badge animate\nin. In candle mode, flat lines expand into full OHLC bodies while the morph\nline fades out. When `data` is empty and `loading` is `false`, a minimal \"No\ndata\" empty state is shown.\n\n**Time**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `window` | `number` | `30` | Visible time window in seconds |\n| `windows` | `WindowOption[]` | — | Time horizon buttons `[{ label, secs }]` |\n| `onWindowChange` | `(secs: number) => void` | — | Called when a window button is pressed |\n| `windowStyle` | `'default' \\| 'rounded' \\| 'text'` | `'default'` | Window button visual style |\n\n**Crosshair**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `tooltipY` | `number` | `14` | Vertical offset for crosshair tooltip text |\n| `tooltipOutline` | `boolean` | `true` | Stroke outline on tooltip text for readability |\n\n**Orderbook**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `orderbook` | `OrderbookData` | — | Bid/ask depth stream `{ bids, asks }` |\n\n**Fonts**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `fonts` | `Partial<LivelineFonts>` | platform monospace | Override the Skia fonts used for chart text (see [Fonts](#fonts-1) below) |\n\n**Advanced**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `referenceLine` | `ReferenceLine` | — | Horizontal reference line `{ value, label? }` |\n| `formatValue` | `(v: number) => string` (worklet) | `v.toFixed(2)` | Value label formatter |\n| `formatTime` | `(t: number) => string` (worklet) | `HH:MM:SS` | Time axis formatter |\n| `lerpSpeed` | `number` | `0.08` | Interpolation speed (0–1) |\n| `padding` | `Padding` | `{ top: 12, right: auto, bottom: 28, left: 12 }` | Chart padding override (`right` is 80/54/12 based on badge/grid) |\n| `onHover` | `(point: HoverPoint \\| null) => void` | — | Hover callback with `{ time, value, x, y }` |\n| `style` | `StyleProp<ViewStyle>` | — | Container style |\n\n**Accessibility**\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `accessibilityLabel` | `string` | `'Live chart'` | Label read by VoiceOver/TalkBack for the chart |\n| `testID` | `string` | — | Test id on the chart container; controls derive ids from it (see below) |\n\n### Accessibility\n\nThe chart is drawn with Skia, so without help a screen reader finds nothing but\nan unlabelled blank region. `Liveline` announces itself as an image (React\nNative has no chart role) with `accessibilityLabel`, and exposes the live\nnumber through `accessibilityValue` — formatted with your `formatValue`, with\nthe momentum direction appended:\n\n```\n\"BTC/USD, image. 64,201.55, rising\"\n```\n\nThis costs nothing when no screen reader is running. `Liveline` checks\n`AccessibilityInfo.isScreenReaderEnabled()` and subscribes to\n`screenReaderChanged`; while no reader is active there is no sampling timer, no\nstate, and no accessibility value at all. When a reader *is* active the value\nis sampled once a second — a screen reader cannot follow 60 updates a second,\nand restarts its utterance on every change, so a per-frame feed would read as\nan endless stutter. Readings that format identically are skipped, so a still\nchart stays quiet.\n\nNote that `formatValue` is called on the JS thread (about once a second) while\na screen reader is running, in addition to its usual per-frame call on the UI\nthread. Keep it free of UI-thread-only dependencies.\n\nThe window pills, line/candle toggle and series chips carry their own labels\nand selected/checked state. The icon-only mode toggle reads as \"Line chart\" and\n\"Candlestick chart\".\n\n**Test ids.** Give the chart a `testID` and the built-in controls derive theirs\nfrom it, so Detox and Maestro can drive them:\n\n| Element | Test id |\n|---------|---------|\n| Chart container | `${testID}` |\n| Window pill | `${testID}-window-${secs}` |\n| Mode toggle | `${testID}-mode-line`, `${testID}-mode-candle` |\n| Series chip | `${testID}-series-${id}` |\n\n### Charts in lists\n\nEach `Liveline` runs its own 60fps UI-thread frame loop, so a long list of\nticker rows keeps one loop running per row — including rows scrolled\noff-screen. Set `active={false}` on rows outside the viewport (via your\nlist's viewability callback) to suspend those loops entirely at no cost:\n\n```tsx\nconst viewabilityConfig = { itemVisiblePercentThreshold: 0 };\n\nfunction TickerList({ rows }: { rows: Ticker[] }) {\n  const [visibleIds, setVisibleIds] = useState<Set<string>>(new Set());\n\n  const onViewableItemsChanged = useRef(\n    ({ viewableItems }: { viewableItems: { key: string }[] }) => {\n      setVisibleIds(new Set(viewableItems.map((v) => v.key)));\n    }\n  ).current;\n\n  return (\n    <FlatList\n      data={rows}\n      keyExtractor={(row) => row.id}\n      viewabilityConfig={viewabilityConfig}\n      onViewableItemsChanged={onViewableItemsChanged}\n      renderItem={({ item }) => (\n        <Liveline\n          data={item.data}\n          value={item.value}\n          active={visibleIds.has(item.id)}\n        />\n      )}\n    />\n  );\n}\n```\n\n#### Hoist object and function props\n\n`Liveline` is wrapped in `React.memo`, and its props are compared shallowly. A\nnew object or function identity on every parent render defeats that — and it\ncosts more than a wasted render. The engine mirrors its config into a shared\nvalue on every commit, and the frame loop's idle detection keys off that\nconfig's object identity, so a chart that receives fresh props each render can\nnever go idle even when nothing about it has changed.\n\nThis matters most in exactly the list above, where one row's tick re-renders the\nwhole list.\n\n```tsx\n// ✗ new identity every render — memo always misses, chart never idles\n<Liveline\n  data={item.data}\n  padding={{ top: 8, right: 60, bottom: 24, left: 8 }}\n  windows={[{ secs: 30, label: '30s' }, { secs: 60, label: '1m' }]}\n  formatValue={(v) => `$${v.toFixed(2)}`}\n/>\n\n// ✓ hoisted to module scope (or useMemo / useCallback if they depend on props)\nconst PADDING = { top: 8, right: 60, bottom: 24, left: 8 };\nconst WINDOWS = [{ secs: 30, label: '30s' }, { secs: 60, label: '1m' }];\nconst formatUsd = (v: number) => {\n  'worklet';\n  return `$${v.toFixed(2)}`;\n};\n\n<Liveline data={item.data} padding={PADDING} windows={WINDOWS} formatValue={formatUsd} />\n```\n\n`data` and `value` are expected to change — that is the live feed, and the\nengine diffs `data` rather than re-sending it. It is the *configuration* props\nthat should be stable.\n\nNote that `formatValue` / `formatTime` run on the UI thread and need the\n`'worklet'` directive, which also makes hoisting them the natural choice.\n\n### `LivelineTransition`\n\nCross-fades between chart components (e.g. line ↔ candlestick). Children must\nhave unique `key` props matching possible `active` values. If `active` doesn't\nmatch any child's key, nothing renders (there is no visible chart until\n`active` changes to a valid key) — in development this logs a warning naming\nthe bad value and the keys that are available.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `active` | `string` | required | Key of the active child to display |\n| `children` | `ReactElement \\| ReactElement[]` | required | Chart elements with unique `key` props |\n| `duration` | `number` | `300` | Cross-fade duration in ms |\n| `style` | `StyleProp<ViewStyle>` | — | Container style |\n\n```tsx\n<LivelineTransition active={chartType}>\n  <Liveline key=\"line\" data={data} value={value} />\n  <Liveline\n    key=\"candle\"\n    mode=\"candle\"\n    candles={candles}\n    candleWidth={5}\n    data={data}\n    value={value}\n  />\n</LivelineTransition>\n```\n\n## Examples\n\n### Basic (line + badge)\n\n```tsx\n<Liveline data={data} value={value} color=\"#3b82f6\" theme=\"dark\" />\n```\n\n### Candlestick (minimal)\n\n```tsx\n<Liveline\n  mode=\"candle\"\n  data={ticks}\n  value={latestTick}\n  candles={candles}\n  candleWidth={60}\n  liveCandle={liveCandle}\n  color=\"#f7931a\"\n  formatValue={(v) => {\n    'worklet';\n    return `$${v.toFixed(2)}`;\n  }}\n/>\n```\n\n### Crypto-style (momentum + degen + exaggerate)\n\n```tsx\n<Liveline\n  data={data}\n  value={value}\n  color=\"#f7931a\"\n  exaggerate\n  degen\n  showValue\n  valueMomentumColor\n  formatValue={(v) => {\n    'worklet';\n    return `$${v.toFixed(2)}`;\n  }}\n/>\n```\n\n### Multi-series (prediction market)\n\n```tsx\n<Liveline\n  data={[]}\n  value={0}\n  series={[\n    { id: 'yes', data: yesData, value: yesValue, color: '#3b82f6', label: 'Yes' },\n    { id: 'no', data: noData, value: noValue, color: '#ef4444', label: 'No' },\n  ]}\n  grid\n  scrub\n  pulse\n  windowStyle=\"rounded\"\n  formatValue={(v) => {\n    'worklet';\n    return v.toFixed(1) + '%';\n  }}\n  onSeriesToggle={(id, visible) => console.log(id, visible)}\n  windows={[\n    { label: '10s', secs: 10 },\n    { label: '30s', secs: 30 },\n    { label: '1m', secs: 60 },\n  ]}\n/>\n```\n\n### Orderbook (orderbook data + particles)\n\n```tsx\n<Liveline\n  data={data}\n  value={value}\n  color=\"#f7931a\"\n  orderbook={{ bids: [[100, 2], [99, 5]], asks: [[101, 3], [102, 4]] }}\n  degen\n  showValue\n/>\n```\n\n## React Native differences from the web version\n\nThis port keeps the same SDK shape as [liveline](https://github.com/benjitaylor/liveline)\nbut a few props differ because of the native/worklet environment:\n\n- **`formatValue` / `formatTime` must be worklets.** They run on the UI\n  thread, not the JS thread. Add the `'worklet'` directive as the first line\n  of the function:\n\n  ```tsx\n  <Liveline\n    data={data}\n    value={value}\n    formatValue={(v) => {\n      'worklet';\n      return `$${v.toFixed(2)}`;\n    }}\n  />\n  ```\n\n  The defaults (`v.toFixed(2)` and `HH:MM:SS`) are already worklets.\n\n  **They must also be pure functions of their input.** Label text is cached\n  per axis tick / grid line and re-computed only when the formatter's\n  *identity* changes, not every frame — so a formatter that reads anything\n  besides its argument (relative time like `\"5s ago\"`, a mutable\n  locale/timezone captured by closure) will render stale text. If the\n  formatting rule changes, pass a new function instance to invalidate the\n  cache.\n\n- **`style` is a React Native `ViewStyle`**, not `CSSProperties`. There is no\n  `className` or `cursor` prop — those were web-only (CSS class + cursor\n  affordance), and don't apply on native.\n\n- **New optional `fonts` prop** (`Partial<LivelineFonts>`) lets you override\n  the Skia fonts used for chart text (`label`, `value`, `badge`, `crosshair`,\n  `orderbook`, `empty`, `refLabel`, `seriesLabel`). Defaults come from\n  `matchFont` (Menlo on iOS, `monospace` on Android). Build custom fonts with\n  `matchFont` from `@shopify/react-native-skia`:\n\n  ```tsx\n  import { matchFont } from '@shopify/react-native-skia';\n  import { Liveline } from '@ajfuller/react-native-liveline';\n\n  const valueFont = matchFont({\n    fontFamily: 'Menlo',\n    fontSize: 13,\n    fontWeight: '700',\n  });\n\n  <Liveline data={data} value={value} fonts={{ value: valueFont }} />;\n  ```\n\n- **`scrub` is touch-drag**, implemented as a `react-native-gesture-handler`\n  `Pan` gesture (the web version uses mouse hover). Requires your app root to\n  be wrapped in `GestureHandlerRootView` (see Installation above). When\n  `scrubActivationDelay` is set, the gesture requires a long-press hold\n  before activating (via `.activateAfterLongPress`), so an outer\n  `ScrollView`/`FlatList` keeps its own pan gesture for quick flicks.\n\n- **`showValue` renders via an animated `TextInput`** (the \"ReText\" pattern —\n  an `Animated.createAnimatedComponent(TextInput)` driven by\n  `useAnimatedProps` from the UI thread), instead of a DOM node updated\n  imperatively.\n\n## How it works\n\n- **UI-thread engine** — the per-frame render step runs inside a Reanimated\n  `useFrameCallback` worklet, not React state/render.\n- **`SkPicture` per frame** — each frame records draw commands into a Skia\n  `PictureRecorder` and hands the resulting picture to `<Canvas><Picture /></Canvas>`,\n  so nothing round-trips through the JS thread or bridge.\n- **Survives a blocked JS thread** — because layout, config, and drawing all\n  live in shared values and worklets, the chart keeps animating smoothly even\n  while your JS thread is busy (e.g. a long synchronous computation, a slow\n  screen transition, or a stress test `while` loop).\n- **Canvas2D-shaped draw layer** — the draw code is ported from the web\n  version against a worklet-safe `CanvasRenderingContext2D`-shaped shim over\n  `SkCanvas`, preserving the original curve-fitting, layout, and animation\n  logic almost verbatim.\n\n## Credits\n\nThis library is a React Native port of\n[**liveline**](https://github.com/benjitaylor/liveline), created by\n[**Benji Taylor**](https://github.com/benjitaylor). The original web library\ndid the hard work: the chart design, the curve fitting and layout math, the\nmomentum/degen animation feel, the badge and crosshair interactions, and the\ncandlestick line-morph. This port keeps that design and behavior intact —\nmost of the draw and engine code here is adapted near-verbatim from Benji's\nimplementation, retargeted from Canvas2D/DOM to Reanimated worklets and Skia\nso it can run on the UI thread natively.\n\nIf you find this useful, go star the\n[original project](https://github.com/benjitaylor/liveline).\n\n## License\n\nMIT. Original work © 2025-2026 Benji Taylor; React Native port © 2026 Andrew\nFuller. See [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}