Skip to content

React

React components

The same chart as declarative components, with hooks for the crosshair and for live updates.

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

What this example is for

Declarative components over the same imperative object: it mounts once, prop changes apply in place, and unmounting removes it. Hooks hand you the crosshair and the live feed. Reach for this in a React application. The instance stays reachable through a ref, so nothing about the engine is hidden behind the wrapper, and the bindings work on React 17, 18 and 19.

react-chart.ts
import { useEffect, useMemo, useState } from "react";
import {
    FinancialChart,
    Pane,
    Series,
    useChart,
    useLiveSource,
    type IndicatorSpec,
    type LiveItem,
    type LiveSource,
} from "@ortex-charts/react";
import type { Bar } from "@ortex-charts/math";

/** Indicators are described, not called. Identity is the JSON of each entry. */
const INDICATORS: IndicatorSpec[] = [{ id: "ema", inputs: { length: 21 } }, { id: "rsi", inputs: { length: 14 } }];

/** A live source is anything with a `subscribe` method. This one replays real bars. */
function replaySource(bars: Bar[], everyMs: number): LiveSource<LiveItem> {
    return {
        subscribe(listener) {
            let index = 0;
            const timer = window.setInterval(() => listener(bars[index++ % bars.length]), everyMs);
            return () => window.clearInterval(timer);
        },
    };
}

/**
 * A child with no output of its own: `useChart()` hands it the chart its
 * parent created, and it tidies up after itself like any other effect.
 */
function PriorClose({ price }: { price: number }): null {
    const chart = useChart();
    useEffect(() => {
        const main = chart?.mainSeries();
        if (!chart || !main) return;
        const id = chart.addPriceLine(main.id, { price, color: "#3AAEA9", lineStyle: "dashed", title: "Prior close" });
        return () => chart.removePriceLine(main.id, id);
    }, [chart, price]);
    return null;
}

function StockChart({
    history,
    arriving,
    shortInterest,
    theme,
    onStatus,
}: {
    history: Bar[];
    arriving: Bar[];
    shortInterest: Point[];
    theme: ExampleTheme;
    onStatus: (text: string) => void;
}) {
    const [streamed, setStreamed] = useState(0);

    // Object props are diffed by identity, so give them a stable one.
    const options = useMemo(
        () => ({ theme, timeZone: TIME_ZONE, resolution: "1D", watermark: { text: SYMBOL, visible: true, fontSize: 36 } }),
        [theme],
    );
    const siOptions = useMemo(
        () => ({ title: "Short interest, % of free float", color: "#A78BFA", priceFormat: { type: "percent" as const, precision: 2 } }),
        [],
    );
    const liveOptions = useMemo(() => ({ onUpdate: () => setStreamed((n) => n + 1) }), []);

    // One source for the lifetime of its dependencies, closed on unmount. A real
    // page returns `websocketSource({ url, parse })` from this factory.
    const source = useLiveSource(() => replaySource(arriving, 900), [arriving]);

    return (
        <FinancialChart
            data={history}
            options={options}
            indicators={INDICATORS}
            live={source}
            liveOptions={liveOptions}
            style={{ height: "100%" }}
            onCrosshairMove={(event) =>
                onStatus(
                    event.time === null
                        ? `${streamed} bars streamed in through useLiveSource.`
                        : `${new Date(event.time).toISOString().slice(0, 10)} — ${streamed} bars streamed in through useLiveSource.`,
                )
            }
        >
            <Pane heightRatio={0.24}>
                <Series type="area" data={shortInterest} options={siOptions} />
            </Pane>
            <PriorClose price={history[history.length - 1].close} />
        </FinancialChart>
    );
}