npm.io
2.0.0 • Published 2d ago

@stacklatte/chart-core

Licence
MIT
Version
2.0.0
Deps
0
Size
907 kB
Vulns
0
Weekly
0

@stacklatte/chart-core

High-performance React Native trading charts — GPU-accelerated via React Native Skia. Works on iOS and Android.

npm License: MIT


Why chart-core

  • One component, not a kit. Candlesticks, line series, SMA/EMA/Bollinger Bands overlays, RSI + MACD sub-panels, pinch-zoom, pan, crosshair inspection, and live-streaming updates all ship built in — you're not assembling them from a primitives library.
  • Real indicator math. RSI uses Wilder smoothing, MACD is seeded and aligned correctly (12/26/9), Bollinger Bands use an O(n) sliding window — not approximations. 183 unit tests cover the math directly, independent of rendering.
  • GPU-rendered, not SVG. Built on Skia via react-native-skia — smooth at 60fps with hundreds of visible candles and a live feed ticking underneath.
  • LLM-safe declarative config. buildChartPipeline() takes a versioned ChartConfig document — every color and interval is a closed enum, never a raw string an LLM can typo — and returns validated, ready-to-render props, or throws a precise, itemized error. A generated JSON Schema ships in dist/ChartConfig.schema.json for structured output / tool-calling. See llms.txt for the quick path.
  • Also available for the web. @stacklatte/chart-web is the same engine, same props, rendered with Canvas 2D — no React Native required.
chart-core / chart-web General-purpose chart libs (Recharts, Victory) Web-only trading chart libs
Candlesticks + OHLC built in build it yourself
RSI / MACD / Bollinger built in varies by plugin
Pinch-zoom, pan, crosshair built in manual
Native (iOS/Android) and web two packages, one shared core web only web only
Declarative config + JSON Schema
TypeScript, strict varies varies

Install

npm install @stacklatte/chart-core @shopify/react-native-skia react-native-gesture-handler

Follow the setup guides for react-native-skia and react-native-gesture-handler.


Peer dependencies

Package Version
react >=19.0.0
react-native >=0.79.0
@shopify/react-native-skia >=2.0.0
react-native-gesture-handler >=2.0.0

Candlestick chart

Drop in OHLC candle data and get a GPU-rendered, pannable, zoomable chart in one component.

import { SLChart } from '@stacklatte/chart-core';
import type { Candle } from '@stacklatte/chart-core';
import { useWindowDimensions } from 'react-native';

const candles: Candle[] = [
  { timestamp: 1700000000000, open: 42000, high: 43500, low: 41800, close: 43100 },
  { timestamp: 1700003600000, open: 43100, high: 44200, low: 42900, close: 44000 },
  // ...
];

export default function TradingScreen() {
  const { width } = useWindowDimensions();
  return (
    <SLChart
      data={candles}
      width={width}
      height={400}
      theme="dark"
    />
  );
}

Line chart

<SLChart data={candles} width={width} height={400} chartType="line" theme="dark" />

Moving average overlays

Calculate SMA and EMA outside the component and pass them in as IndicatorLine objects.

import { SLChart, calcSMA, calcEMA } from '@stacklatte/chart-core';

const sma20 = calcSMA(candles, 20, 'sma-20', '#F5A623');
const ema9  = calcEMA(candles,  9, 'ema-9',  '#4a90e2');
const ema21 = calcEMA(candles, 21, 'ema-21', '#50E3C2');

<SLChart
  data={candles}
  indicators={[sma20, ema9, ema21].filter(Boolean)}
  width={width}
  height={400}
/>

Bollinger Bands

Bollinger Bands return three indicator lines (upper, basis, lower). Pass a ShadedArea to fill the region between the bands.

import { SLChart, calcBollingerBands } from '@stacklatte/chart-core';

const bb = calcBollingerBands(candles, 20, 2, {
  upper: '#9B59B6',
  basis: '#7F8C8D',
  lower: '#9B59B6',
});

<SLChart
  data={candles}
  indicators={bb ? [bb.upper, bb.basis, bb.lower] : []}
  shadedAreas={bb ? [{ fromId: 'bb-upper', toId: 'bb-lower', color: '#9B59B6', opacity: 0.1 }] : []}
  width={width}
  height={400}
/>

RSI sub-panel

Displays a Relative Strength Index panel below the main chart. RSI is computed internally using Wilder smoothing (period 14) on the visible candles.

<SLChart
  data={candles}
  showRsiPanel
  width={width}
  height={480}   // extra height for the sub-panel
/>

The panel includes 30/70 overbought/oversold threshold lines and a synchronized crosshair.


MACD sub-panel

MACD (12, 26, 9) — line, signal, and histogram bars — in a panel synchronized with the main chart.

<SLChart
  data={candles}
  showMacdPanel
  width={width}
  height={480}
/>

Run both panels at once:

<SLChart
  data={candles}
  showRsiPanel
  showMacdPanel
  width={width}
  height={560}
/>

OHLC HUD

An overlay that shows Open, High, Low, Close values for the currently inspected candle — and any indicator values at that timestamp.

<SLChart
  data={candles}
  showOhlcHud
  indicators={[ema9, ema21]}
  width={width}
  height={400}
/>

When no touch is active, the HUD shows the latest candle. On tap or drag it locks to the touched candle.


Crosshair callback

<SLChart
  data={candles}
  showOhlcHud
  onCrosshairChange={(candle) => {
    if (candle) {
      console.log(`Price: ${candle.close} at ${new Date(candle.timestamp).toISOString()}`);
    }
  }}
  width={width}
  height={400}
/>

Zoom and pan

Pinch to zoom and pan are built-in — no setup required.

<SLChart
  data={candles}
  visibleDataPoints={60}   // candles visible at initial load
  maxZoom={400}            // maximum candles visible (widest zoom out)
  width={width}
  height={400}
/>
Gesture Action
1-finger pan Scroll the time axis
2-finger pinch Zoom in/out around the focal point
Tap or pan (with showOhlcHud) Lock the crosshair to a candle
Pan to the right edge Re-engage live-follow mode

Live / streaming data

The chart auto-follows new candles when the viewport is at the latest data. After the user scrolls away, increment scrollToLatestTrigger to snap back.

const [candles, setCandles] = useState<Candle[]>(initialCandles);
const [trigger, setTrigger] = useState(0);

function onNewCandle(updatedCandles: Candle[]) {
  setCandles(updatedCandles);
  setTrigger(t => t + 1);   // snaps back to latest
}

<SLChart
  data={candles}
  scrollToLatestTrigger={trigger}
  width={width}
  height={400}
/>

Themes

Built-in presets
<SLChart data={candles} theme="dark"  width={width} height={400} />
<SLChart data={candles} theme="light" width={width} height={400} />
Custom theme

Pass any ChartThemeColors object to override every color token.

import type { ChartThemeColors } from '@stacklatte/chart-core';

const myTheme: ChartThemeColors = {
  background:    '#0d0d0d',
  gridH:         '#1a1a1a',
  gridV:         '#141414',
  priceLabel:    '#999999',
  timeLabel:     '#555555',
  crosshair:     '#777777',
  candleUp:      '#26a69a',
  candleDown:    '#ef5350',
  lineChart:     '#00e5ff',
  rsiLine:       '#f1c40f',
  rsiThreshold:  '#2a2a2a',
  hudBackground: 'rgba(0,0,0,0.85)',
  hudText:       '#ffffff',
};

<SLChart data={candles} theme={myTheme} width={width} height={400} />

All together

import { SLChart, calcEMA, calcBollingerBands } from '@stacklatte/chart-core';
import type { Candle, IndicatorLine, ShadedArea } from '@stacklatte/chart-core';
import { useMemo, useState } from 'react';
import { useWindowDimensions } from 'react-native';

export default function TradingScreen({ candles }: { candles: Candle[] }) {
  const { width } = useWindowDimensions();
  const [trigger, setTrigger] = useState(0);

  const { indicators, shadedAreas } = useMemo(() => {
    const lines: IndicatorLine[]  = [];
    const areas: ShadedArea[]     = [];

    const ema9  = calcEMA(candles, 9,  'ema-9',  '#4a90e2');
    const ema21 = calcEMA(candles, 21, 'ema-21', '#50E3C2');
    if (ema9)  lines.push(ema9);
    if (ema21) lines.push(ema21);

    const bb = calcBollingerBands(candles, 20, 2, {
      upper: '#9B59B6', basis: '#7F8C8D', lower: '#9B59B6',
    });
    if (bb) {
      lines.push(bb.upper, bb.basis, bb.lower);
      areas.push({ fromId: 'bb-upper', toId: 'bb-lower', color: '#9B59B6', opacity: 0.1 });
    }

    return { indicators: lines, shadedAreas: areas };
  }, [candles]);

  return (
    <SLChart
      data={candles}
      indicators={indicators}
      shadedAreas={shadedAreas}
      width={width}
      height={560}
      theme="dark"
      chartType="candle"
      showOhlcHud
      showRsiPanel
      showMacdPanel
      visibleDataPoints={60}
      scrollToLatestTrigger={trigger}
      onCrosshairChange={(c) => c && console.log('candle', c.close)}
    />
  );
}

Declarative config (ChartConfig) — and generating it with an LLM

Every example above sets SLChart props directly. There's a second, declarative path: describe the chart as a versioned ChartConfig document and let buildChartPipeline() validate, normalize, and compile it into props for you.

import { buildChartPipeline, SLChart } from '@stacklatte/chart-core';
import type { ChartConfig } from '@stacklatte/chart-core';

const config: ChartConfig = {
  version: '1',
  theme: 'dark',
  priceDisplay: 'candle',
  interval: '1h',
  overlays: [
    { type: 'ema', id: 'ema-9', params: { period: 9 }, color: 'cyan' },
    {
      type: 'bollinger_bands',
      params: { period: 20, stdDev: 2 },
      bands: {
        upper: { id: 'bb-upper', color: 'purple' },
        lower: { id: 'bb-lower', color: 'purple' },
        basis: { id: 'bb-basis', color: 'gray' },
      },
    },
  ],
  shadedAreas: [{ fromOverlayId: 'bb-upper', toOverlayId: 'bb-lower', color: 'purple', opacity: 0.15 }],
  subPanels: [{ type: 'rsi', params: { period: 14 } }],
  viewport: { visibleCandles: 100, followLive: true },
  hud: { showOhlcHud: true },
};

const compiled = buildChartPipeline(config, candles);
<SLChart {...compiled} width={width} height={560} />

This exists specifically so an LLM (or any code-generation step) can produce chart setup safely:

  • Every string is a closed enumtheme, interval, overlay type, and every color come from a fixed, named union (PaletteColor, ChartInterval, …). There's no raw hex, no arbitrary millisecond literal, no field an LLM can plausibly hallucinate a wrong-but-valid-looking value for.
  • It fails loudly, not silently. buildChartPipeline runs validateChartConfig first and throws one error listing every problem ([code] path: message) if the document doesn't match the schema — an agent gets something it can parse and self-correct from, instead of a chart that renders with subtly wrong data.
  • A matching JSON Schema ships in the package at dist/ChartConfig.schema.json (draft-07, additionalProperties: false throughout) — drop it straight into an LLM's structured-output or tool-call schema so the model can only produce a valid ChartConfig in the first place.

See llms.txt for a condensed reference aimed at coding agents.


Standalone indicator calculators

All calculators are exported for use outside the chart component — useful for displaying values in a separate UI element.

import { calcSMA, calcEMA, calcBollingerBands, calcRSI, calcMACD } from '@stacklatte/chart-core';
import type { RsiPoint, MacdResult } from '@stacklatte/chart-core';

const sma   = calcSMA(candles, 20, 'sma-20', '#F5A623');   // IndicatorLine | null
const ema   = calcEMA(candles, 20, 'ema-20', '#4a90e2');   // IndicatorLine | null
const bb    = calcBollingerBands(candles, 20, 2, colors);  // { upper, basis, lower } | null
const rsi   = calcRSI(candles, 14);                        // RsiPoint[]
const macd  = calcMACD(candles, 12, 26, 9);                // MacdResult | null

Props

Prop Type Default Description
data Candle[] required OHLC candles sorted ascending by timestamp (ms)
width number required Canvas width in logical pixels
height number required Canvas height in logical pixels
theme "dark" | "light" | ChartThemeColors "dark" Color preset or full custom theme
chartType "candle" | "line" "candle" Main series rendering style
indicators IndicatorLine[] [] Overlay lines (SMA, EMA, Bollinger Bands)
shadedAreas ShadedArea[] [] Filled band between two indicator lines
visibleDataPoints number 60 Number of candles visible at initial load
intervalMs number auto Milliseconds per candle — inferred from data if omitted
showGrid boolean true Horizontal and vertical grid lines
showOhlcHud boolean false OHLC info overlay
showRsiPanel boolean false RSI sub-panel
showMacdPanel boolean false MACD sub-panel
maxZoom number data length Maximum candles visible at once
hour12 boolean false 12-hour (AM/PM) time axis
scrollToLatestTrigger number Increment to snap to latest candle
onCrosshairChange (candle: Candle | null) => void Fires on crosshair lock/release

Data format

interface Candle {
  timestamp: number; // Unix milliseconds, sorted ascending
  open:      number;
  high:      number;
  low:       number;
  close:     number;
}

Architecture

Each drawing concern is a separate Skia component. Swap or extend individual layers without touching the rest.

Layer Renders
AxisLayer Time/price grid lines and labels
CandlestickLayer OHLC wicks and bodies
LineLayer Close-price line
IndicatorLayer Single indicator polyline
ShadedAreaLayer Filled region between two series
RsiLayer RSI sub-panel with 30/70 threshold lines
MacdLayer MACD sub-panel (line, signal, histogram)
CrosshairLayer Dashed vertical + horizontal crosshair
OhlcHud OHLC info overlay

All layers are exported for custom compositions:

import { SLChart, AxisLayer, CandlestickLayer, IndicatorLayer } from '@stacklatte/chart-core';

Contributing

Issues and PRs welcome. See the monorepo README for setup instructions.


License

MIT — see LICENSE.md.

Keywords