react-native-livechart

repository·main·Indexed 41 days ago

https://github.com/brandtnewlabs/react-native-livechart

A high-performance library for rendering live line and candlestick charts in React Native. It leverages @shopify/react-native-skia, react-native-reanimated, and react-native-gesture-handler to ensure animations run on the UI thread via SharedValues, minimizing JS bridge overhead. Features include single-series LiveChart and multi-series LiveChartSeries components, cross-fade transitions with LiveChartTransition, and worklet-safe formatters.

Tokens
75.3K
Snippets
221
Records
294
Agent score
52%

What's inside react-native-livechart

  1. Overview of react-native-livechart

    main

    react-native-livechart

    react-native-livechart is a high-performance library for rendering real-time line and candlestick charts in React Native. It is built using @shopify/react-native-skia, react-native-reanimated, and react-native-gesture-handler.

    Key Performance Feature: Data and live values flow through Reanimated SharedValues. This allows the chart to animate at native frame rates on the UI thread, avoiding per-frame traffic across the JavaScript bridge.

    Core Capabilities:

    • Line & Candlestick: Supports single-series line/area or OHLC candlestick modes with smooth morphing.
    • Multi-series: Supports multiple live series with toggleable legends, per-series dots, and a shared crosshair.
    • Scrubbing & Interaction: Includes a pan-able crosshair with a worklet-friendly onScrub payload and a price reticle for firing order callbacks via scrubAction.
    • Visual Effects: Features auto-detected momentum coloring, optional particle bursts, and screen shake on swings.
    • Markers & References: Supports trade-fill markers, custom glyphs, and reference lines/bands.
    • Theming: Provides light/dark themes with accent-driven palettes and per-key overrides.
  2. Supported platforms and chart types

    main

    Supported Platforms

    • iOS and Android (via React Native or Expo).
    • React Native Web is NOT supported. The library relies on Skia and Reanimated worklets running on the native UI thread. For web support, implement a static fallback or a different charting library when Platform.OS === 'web'.

    Supported Chart Types

    The library supports:

    • Line / Area charts
    • Candlestick (OHLC) charts

    Note: The component supports smooth morphing between these two modes. Bar charts, pie charts, and other types are not included.

  3. How to use PathBuilder for forward-compatible path construction

    main

    When working with @shopify/react-native-skia, you should use Skia.PathBuilder instead of mutating an existing SkPath (via moveTo, lineTo, etc.), as Skia 3.0 is moving toward immutable paths.

    To use a builder within a Reanimated worklet, you cannot simply pass a builder instance from a useRef because Skia.PathBuilder is not a Reanimated-shareable host object. Attempting to do so will result in the error: "Cannot convert undefined value to object".

    There are two valid ways to handle this:

    1. Create the builder inside the worklet: Use Make() inside the worklet to create a new builder and path every frame.
    2. Reuse a builder via SharedValue (Recommended): Pass the builder into the worklet using a SharedValue. This allows you to reuse the builder across frames while using .detach() to return a fresh path reference each frame, avoiding the need for manual 'ping-pong' logic to bypass Reanimated's value-equality short-circuit.
    // The recommended pattern used in the library:
    // Use the `usePathBuilder` hook which manages the SharedValue and .detach() logic
    const pathBuilder = usePathBuilder();
    
    const path = useDerivedValue(() => {
      // The builder is accessed via the SharedValue
      // .detach() returns a fresh SkPath reference each frame
      return pathBuilder.value.detach();
    });
  4. Understand Time-scroll and Zoom interaction constraints

    main

    When implementing time-scroll and zoom, be aware of how they interact with other chart features:

    • Multi-series behavior: In multi-series charts, each series' dot or value label tracks its value at the visible right edge while scrolled. This means the dot rides the end of the line rather than the live price.
    • Gesture Disambiguation: Time-scroll and pinch-zoom coexist with plot-area scrubbing. Gestures are disambiguated by:
      • Region: Using axisDrag to distinguish between axis interaction and plot interaction.
      • Press-hold: Using holdToScrub to distinguish between scrolling and scrubbing.
    • Pinch-zoom: Uses two-finger gestures, ensuring it does not conflict with one-finger scrolling or scrubbing gestures.
    • Multi-series scrubbing: Note that axisDrag in multi-series mode does not yet exclude the bottom band from scrubbing. For LiveChartSeries, using holdToScrub (the default) is the recommended approach to avoid gesture conflicts.
  5. Configure semantic states for chart gaps

    main

    The kind property in a ChartGap object determines how the chart handles price and visual representation.

    KindDefault price treatmentDefault market-state treatment
    "no-trades"Flat bridge at the previous observed value/closeNo time band
    "unavailable"Flat previous-value/close bridgeShaded time band with an optional label
    "unknown"No forward-filled priceShaded time band with an optional label

    Note: A gap with no preceding real observation cannot be price-bridged.

  6. Stack and cluster co-located markers

    main

    By default, markers at the same position overlap. You can use the markerCluster prop to manage overlapping markers.

    Horizontal Stacking (Default)

    Set markerCluster="stacked" to fan markers apart horizontally (left-over-right) at their specified side ("above", "below", or "center"). When the number of markers exceeds maxBeforeGroup (default 5), they collapse into a single count-badge marker.

    Vertical Stacking

    Pass direction: "vertical" to pile markers in a column. Each glyph keeps its time (x) and stacks along the value axis, growing away from the line in its side direction.

    Configuration Options

    • overlap: (0–1, default 0.75) Controls the density of the fan.
    • maxBeforeGroup: The threshold at which a cluster collapses into a group badge.
    • maxVisible: (Vertical only) Limits the number of oldest glyphs shown in a column, hiding newer overflow. This is useful for capping high-frequency bursts.
    • groupBadge: Defines how the collapsed group looks (see Customizing collapsed-group badges).
    // Horizontal stacking with custom tuning
    <LiveChart markerCluster={{ overlap: 0.75, maxBeforeGroup: 8 }} />
    
    // Vertical stacking (column look)
    <LiveChart markerCluster={{ direction: "vertical", overlap: 0.6, maxBeforeGroup: 20 }} />
    
    // Vertical stacking with a cap on visible markers
    <LiveChart markerCluster={{ direction: "vertical", overlap: 0.6, maxBeforeGroup: 20, maxVisible: 8 }} />
  7. Difference between `static` and `paused` props

    main

    While both props reduce active processing, they serve different purposes:

    PropBehaviorBest Use Case
    staticRemoves loops entirely. No animation, no data buffering, no recovery.Rendering many mini-charts (sparklines) in a list or grid.
    pausedFreezes a live chart. The tick loop and value buffering continue in the background. Resuming catches the window up to real-time.Pausing a single, high-importance interactive chart.
  8. Understand JavaScript frame-allocation pooling

    main

    The library implements JavaScript frame-allocation pooling to minimize garbage collection (GC) pressure during high-frequency chart updates (e.g., 60 fps).

    How it works

    • Buffer Reuse: Instead of creating new arrays or objects every frame, the engine reuses existing buffers.
    • Ping-Ponging: To ensure compatibility with Reanimated and Skia, the library uses a "ping-pong" strategy for numeric and Atlas result arrays. This means it switches between two buffers so that Reanimated/Skia can observe a reference change (triggering a redraw) while the previous frame's data remains immutable and readable.
    • Immutability: Certain objects like SkPath instances returned by PathBuilder.detach() are designed to be per-frame and immutable. Marker/particle Skia.RSXform and Skia.Color values remain dynamic to avoid mutating data currently being consumed by the renderer.

    Performance Impact

    Pooling has significantly reduced allocations across various components:

    • Single-series engine: Removed 120 objects/s.
    • Multi-series engine: Removed 180 objects/s.
    • Marker Atlas lists: Removed 180 containers/s.
    • Degen Atlas: Removed 300 + 60N JS allocations/s.
  9. Map price and time to pixels using ChartOverlayContext

    main

    The renderOverlay function receives ChartOverlayContext, which provides a per-frame scale SharedValue and worklets to bridge the gap between data values (price/time) and canvas pixels.

    The Easiest Path: Hooks

    Use usePriceY or useTimeX to project a price or time to a SharedValue<number> that tracks the live axis. Use the resulting value in a useAnimatedStyle.

    function PriceLevel({ ctx, price }) {
      const y = usePriceY(ctx, price); // reactive — tracks the rescaling axis
      const style = useAnimatedStyle(() => ({ transform: [{ translateY: y.get() }] }));
      return <Animated.View style={style} />;
    }

    The Manual Path

    Inside a worklet, call scale.get() to subscribe to the live scale, then pass that snapshot to the mapping worklets (priceToY, yToPrice, timeToX, xToTime).

    const style = useAnimatedStyle(() => {
      const s = scale.get();          // subscribe to the live scale
      const y = priceToY(142.5, s);   // project a price level to its pixel
      return { transform: [{ translateY: y }] };
    });
  10. How LiveChart handles data loading on scroll

    main

    LiveChart does not manage network connections (HTTP/WebSockets) itself. Instead, it uses a callback-driven pattern to tell your application what is visible and when older history is needed.

    Your application is responsible for:

    1. Fetching data using your existing client (fetch, TanStack Query, Apollo, etc.).
    2. Merging and sorting the data.
    3. Writing the resulting rows into Reanimated SharedValues.

    This pattern allows LiveChart to remain transport-agnostic while supporting high-performance scrolling and zooming.

    flowchart LR
      Chart["LiveChart callbacks"] --> Demand["History controller"]
      Demand --> REST["REST / query cache"]
      REST --> Merge["sort + dedupe + retain"]
      WS["Websocket / realtime SDK"] --> Merge
      Merge --> SV["SharedValues"]
      SV --> Chart
  11. How react-native-livechart works internally

    main

    The library is built for high-performance rendering on React Native using three core technologies:

    • Skia: Handles GPU-accelerated drawing of the grid, lines, candles, badges, and overlays.
    • Reanimated: Manages the timeline layout, smoothing, and scrub state. It uses a frame-callback engine (useFrameCallback) running on the UI thread to update display ranges and smoothed values.
    • Gesture Handler: Drives all user interactions, such as scrubbing.

    Data Flow Architecture: To avoid expensive JS-to-Native bridge traffic, the library uses SharedValues for history and live values. The frame-callback engine updates these SharedValues directly on the UI thread. Path builders and overlays then read these values inside useDerivedValue, ensuring that React re-renders are kept to a minimum.

  12. Use Segments to label and highlight time ranges

    main

    Segments allow you to label specific time ranges on a single-series LiveChart (e.g., pre-market, regular sessions, or overnight windows) using a 'scrub-focus' interaction.

    How it works:

    • At rest: The entire line is a uniform base color.
    • During interaction: When a user scrubs the chart (or when a segment is marked as active), the segment under the scrub line maintains its base color, while all other segments are de-emphasized using a mutedColor or mutedColors gradient.
    • Visuals: The de-emphasis is applied via a horizontal gradient directly to the line stroke (using alpha-reduced colors to fade the line), rather than drawing a panel over it.

    Key Features:

    • mutedColor / mutedColors: Defines how the segment fades when it is not the focus. Use an alpha-reduced color (e.g., rgba(...)) to genuinely fade the line.
    • divider & label: A divider: true property draws a dashed line at the segment's edge, and the label text is displayed only when the divider is present.
    • active: Forces a segment to be focused without requiring user scrubbing.
    • Defaults: If colors are omitted, mutedColor, dividerColor, and the label default to the chart's existing palette.
    import { LiveChart, type ChartSegment } from "react-native-livechart";
    
    const segments: ChartSegment[] = [
      // pre-market: left edge → t1 (de-emphasizes to grey when not focused)
      { to: t1, mutedColor: "rgba(154,160,166,0.4)" },
      // regular session, with a dashed divider + label at its start
      { from: t1, to: t2, mutedColor: "rgba(154,160,166,0.4)", divider: true, label: "Regular" },
      // after-hours: t2 → live edge; `active` force-focuses it without a scrub
      { from: t2, mutedColor: "rgba(154,160,166,0.4)", divider: true, label: "After hours" },
    ];
    
    <LiveChart data={data} value={value} segments={segments} />;