Skip to content

Packages

React

Declarative components and hooks over the same imperative API.

What the Package Is

@ortex-charts/react is a thin binding layer with no logic of its own. Every component creates the corresponding imperative object in a layout effect, applies prop changes with applyOptions or setData, and disposes it on unmount. The underlying handle is always available through ref, so nothing is lost by using the components.

It is tested against React 17, 18 and 19, and declares them as peer dependencies with >=17.

npm install @ortex-charts/react
React components@ortex-charts/react
Loading react components
The same chart as declarative components, with hooks for the crosshair and for live updates.

Open this example with its source

The Components

Component Wraps Handle through ref
<Chart> createChart Chart
<Pane> chart.addPane — (provides its id through context)
<Series> chart.addSeries SeriesModel
<FinancialChart> createFinancialChart FinancialChart
<ChartShell> createChartShell ChartShell
<Sparkline> createSparkline Sparkline

The Financial Chart

The common case is one component with data, indicators and markers as props.

import { FinancialChart, type IndicatorSpec } from "@ortex-charts/react";
import type { Bar, SeriesMarker } from "@ortex-charts/react";
import { useMemo, useState } from "react";

const OPTIONS = { theme: "dark", timeZone: "America/New_York", resolution: "1D" } as const;

export function PriceChart({ bars, earnings }: { bars: Bar[]; earnings: SeriesMarker[] }) {
  const [hover, setHover] = useState("");

  const indicators = useMemo&#x3C;IndicatorSpec[]>(
    () => [
      { id: "ema", inputs: { length: 21 } },
      { id: "sma", inputs: { length: 200 } },
      { id: "rsi" },
    ],
    [],
  );

  return (
    &#x3C;>
      &#x3C;FinancialChart
        options={OPTIONS}
        data={bars}
        indicators={indicators}
        markers={earnings}
        style={{ height: 460 }}
        onCrosshairMove={(e) => setHover(e.time ? new Date(e.time).toDateString() : "")}
      />
      &#x3C;p>{hover}&#x3C;/p>
    &#x3C;/>
  );
}

indicators is diffed by identity: new entries are added, missing ones removed, and the rest are left alone rather than being torn down and recreated. The identity of an entry is the JSON of { id, inputs, options }, or an explicit key when you supply one, so two moving averages with different lengths are two distinct entries and an unchanged array causes no work at all.

data and markers are applied whenever the array identity changes, so a new array on every render means setData on every render. Memoize them or hold them in state.

options is compared shallowly at the top level and the changed keys go to applyOptions. Two keys — seriesType and volume — take effect only at creation, because they decide what the chart is made of.

The Core Chart With Children

For anything that is not a standard price chart, compose it.

import { Chart, Pane, Series } from "@ortex-charts/react";

&#x3C;Chart options={{ theme: "light", timeZone: "UTC" }} style={{ height: 520 }}>
  &#x3C;Series type="candlestick" data={bars} options={{ title: "AAPL" }} />

  &#x3C;Pane heightRatio={0.2}>
    &#x3C;Series type="histogram" data={volume} options={{ title: "Volume", colorMode: "mainDirection" }} />
  &#x3C;/Pane>

  &#x3C;Pane heightRatio={0.25}>
    &#x3C;Series type="line" data={shortInterest} options={{ title: "Short interest", align: "forwardFill" }} />
  &#x3C;/Pane>
&#x3C;/Chart>

<Series> renders nothing into the DOM; it manages a series on the chart from context. <Pane> provides its id to nested series, and options.pane on a series overrides the enclosing pane if you need to place one explicitly.

Changing type on a <Series> recreates it. Changing heightRatio or minHeight on a <Pane> applies live.

The Full Shell

import { ChartShell } from "@ortex-charts/react";

&#x3C;ChartShell
  symbol={symbol}
  resolution={resolution}
  layout={savedLayout}
  theme={theme}
  options={{
    datafeed,
    overlays,
    toolbar: { download: true },
    onLayoutChange: saveLayout,
    onSymbolChange: (s) => router.push(`/chart/${s}`),
  }}
  style={{ height: 640 }}
/>

symbol, resolution and theme are controlled props: the shell follows them when they change, so the chart and your application state stay in step in either direction.

layout is re-applied whenever its identity changes, which is what you want after loading a user's saved layout asynchronously.

options is read at creation. Callbacks inside it are always invoked through their latest value, so inline arrow functions are fine and do not need memoizing. Structural options — datafeed, overlays, toolbar — need a remount to change, and the idiomatic way to force one is a key:

&#x3C;ChartShell key={pageKind} options={{ datafeed, overlays: overlaysFor(pageKind) }} />

Hooks

useChart and usePaneId

Inside a <Chart> or <FinancialChart>, useChart() returns the core Chart or null until it exists after the first commit. usePaneId() returns the id of the enclosing <Pane>. Both are how you write your own child components.

import { useChart, usePaneId } from "@ortex-charts/react";
import { useEffect } from "react";

function SessionShading() {
  const chart = useChart();
  const paneId = usePaneId();

  useEffect(() => {
    if (!chart) return;
    const primitive = makeSessionShading();
    chart.addPrimitive(primitive, paneId);
    return () => chart.removePrimitive(primitive.id);
  }, [chart, paneId]);

  return null;
}

useChartEvents

The four event props — onCrosshairMove, onClick, onDblClick, onVisibleRangeChange — are accepted directly by <Chart> and <FinancialChart>. The hook behind them is exported for a chart you hold yourself:

useChartEvents(chart, { onCrosshairMove: setHover, onVisibleRangeChange: setRange });

Subscriptions are made once per chart instance and always call the latest handler, so handlers never need memoizing.

useLiveSource

A LiveSource must survive re-renders and be closed exactly once. useMemo does not guarantee either. useLiveSource does: it keeps the value for the lifetime of its dependencies, closes the previous one when they change, and closes the last one on unmount.

import { FinancialChart, useLiveSource } from "@ortex-charts/react";
import { websocketSource, type LiveItem } from "@ortex-charts/financial";

function LiveChart({ symbol, bars }: { symbol: string; bars: Bar[] }) {
  const source = useLiveSource(
    () =>
      websocketSource&#x3C;LiveItem>({
        url: `wss://feed.example.com/${symbol}`,
        parse: (raw) => JSON.parse(String(raw)) as LiveItem,
        onOpen: (send) => send(JSON.stringify({ subscribe: symbol })),
      }),
    [symbol],
  );

  return &#x3C;FinancialChart data={bars} live={source} options={{ resolution: "1" }} style={{ height: 420 }} />;
}

Anything with a close() method is closed, so the hook works for your own sources too.

Sparklines

import { Sparkline } from "@ortex-charts/react";

&#x3C;Sparkline data={closes} width={120} height={28} options={{ kind: "area", colorByTrend: true }} />

The container is an inline-block span with zero line height, so it sits on the text baseline. Hold a ref to call push for live values. See Sparklines.

Referential Stability

This is the one thing to get right, and it applies to every component in the package.

options and data props are compared by identity, not by value. A new object literal on every render is a new identity, so the effect runs every render.

// Wrong: a new object every render, so applyOptions runs every render.
&#x3C;FinancialChart options={{ theme: "dark", resolution: "1D" }} data={bars.map(toBar)} />

// Right: stable identities.
const OPTIONS = { theme: "dark", resolution: "1D" } as const;   // module constant
const data = useMemo(() => rows.map(toBar), [rows]);            // memoized

&#x3C;FinancialChart options={OPTIONS} data={data} />

Callbacks are the exception. Every event prop and every shell callback is routed through a ref, so inline functions are correct and cost nothing.

Getting the Imperative Handle

import { useRef } from "react";
import { FinancialChart, type FinancialChartHandle } from "@ortex-charts/react";

const ref = useRef&#x3C;FinancialChartHandle | null>(null);

&#x3C;FinancialChart ref={ref} data={bars} onReady={(c) => c.chart.timeScale.fitContent()} />;

// Later:
ref.current?.chart.timeScale.setVisibleTimeRange(from, to);
ref.current?.alerts().add({ price: 182.5, label: "Breakout" });

onReady fires once with the handle after creation, which is the right place for anything that has to happen before the user sees the first frame. The ref is the right place for anything that happens later.

For realtime updates, prefer the handle over the data prop: ref.current?.update(bar) is one row, while a new data array is a full setData.

Server-Side Rendering

The components check for window before creating anything, so they render an empty container on the server and fill it on hydration. In Next.js the file still needs "use client", because the effect only runs in a client component. There is no server-rendered chart image; see Installation and delivery.

Re-Exported Types

To avoid importing from four packages, the React package re-exports the types callers usually need alongside the components:

import type {
  ChartHandle, ChartOptions, DeepPartial, SeriesModel, SeriesDataInput, SeriesOptionsBase,
  SeriesTypeMap, CrosshairEvent, VisibleRangeEvent,
  FinancialChartHandle, FinancialChartOptions, SeriesMarker, LiveSource, LiveItem, StreamOptions,
  AddIndicatorOptions, IndicatorInstance,
  SparklineHandle, SparklineOptions, SparklineData,
  ChartShellHandle, ChartShellOptions, ChartLayout, OverlayDef,
  Bar,
} from "@ortex-charts/react";

What the Package Does Not Do

  • There is no React renderer for the canvas. Children of <Chart> are effects that call the imperative API; they do not reconcile drawing operations.
  • There are no components for drawings, events, order flow, replay or alerts. Those are primitives on the chart, reached through the handle. Wrapping one in a component of your own is about ten lines, as in the SessionShading example above.
  • <Series> does not manage realtime updates. Use the SeriesModel from ref and call update, or hand the chart a live source.