Getting Started
Get one candlestick chart on screen in 60 seconds.
Installation
npm install @finchart/core @finchart/dompnpm add @finchart/core @finchart/domyarn add @finchart/core @finchart/dombun add @finchart/core @finchart/domQuick Start
Give the chart an element to mount into, with a height:
<div id="chart" style="height: 400px"></div>Then:
import { candleSeries } from "@finchart/core";
import { PlotBuilder, browserDeps } from "@finchart/dom";
const plot = PlotBuilder.create(browserDeps(), candleSeries())
.addDataPoints([
{ x: 0, open: 100, high: 108, low: 98, close: 106 },
{ x: 1, open: 106, high: 112, low: 104, close: 109 },
{ x: 2, open: 109, high: 111, low: 101, close: 103 },
])
.setSize(800, 400)
.build(document.getElementById("chart")!);
export { plot };That is the whole file. The canvas takes its size from setSize, but it is layered over the container rather than laid out in it, so the container's own CSS decides how much of the page the chart takes — without the height, an empty <div> is 0 px tall and whatever follows it is drawn over. (With browserDeps({ autoSize: true }) the chart follows that size instead of setSize.) build() schedules the first frame — there is no render() to call.
Here's that same chart, live (resized to fit this page — the code above uses a fixed 800×400):
Drag to pan, scroll to zoom.
Data must be in ascending x order, or you'll get a
DataError. To find out before it throws,validateSeriesData(data)answers as a value — see Checking data before it goes in.
Real-Time Updates & Indicators
npm install @finchart/indicatorspnpm add @finchart/indicatorsyarn add @finchart/indicatorsbun add @finchart/indicatorsRegister the series with addSeries instead of the builder to get a handle back — that handle is what updateLast and an indicator's source both need:
import {
OHLCAccessor,
candleSeries,
conflated,
histogramSeries,
priceFormat,
timeTicks,
validateSeriesPoint,
} from "@finchart/core";
import type { OHLC } from "@finchart/core";
import { PlotBuilder, browserDeps } from "@finchart/dom";
import { attachMovingAverage } from "@finchart/indicators";
declare const bars: OHLC[];
const plot = PlotBuilder.create<OHLC>(browserDeps({ autoSize: true }))
.setSize(900, 480)
.setAxis({
x: { ticks: timeTicks() },
y: { position: "right", format: priceFormat({ compact: true }) },
})
.build(document.getElementById("chart")!);
const price = plot.mainPane.addSeries({
series: candleSeries(),
data: bars,
name: "Price",
});
plot.mainPane.use(attachMovingAverage({ source: price, period: 20 }));
const volumePane = plot.addPane({ flex: 0.25, minHeight: 48 });
const volume = volumePane.addSeries({
series: histogramSeries(),
// A bar without volume is a gap in the volume pane — not a bar of 0.
data: bars.map((bar) => ({ x: bar.x, y: bar.volume ?? null })),
name: "Volume",
});
const accessor = new OHLCAccessor();
// A loud feed: every `updateLast` copies the array, so fifty ticks between
// two frames pay fifty copies for a picture that shows only the last. A
// conflated feed coalesces the repeated updates to the same bar until the
// next frame — a tick that opens a new bar delivers the previous one at
// once; the screen then follows the socket by two scheduling steps (the
// feed's frame, then the render's), and without requestAnimationFrame
// delivery is immediate.
const priceFeed = conflated(price);
const volumeFeed = conflated(volume);
// The x of the last tick accepted — the feed may still be holding it, and
// `price.read()` does not know about a bar that has not been delivered yet.
let accepted: number | undefined;
export function onTick(bar: OHLC): void {
// The tick door's pre-check — the same rules updateLast runs, as a value
// instead of a DataError inside the socket callback. The baseline is the
// later of the bar you hold and the bar the feed is holding: judged
// against the held tail alone, a tick behind a pending bar would pass here
// and fail inside the feed's delivery.
const held = price.read().at(-1)?.x;
const lastX =
held === undefined ? accepted : accepted === undefined ? held : Math.max(held, accepted);
const issues = validateSeriesPoint(bar, accessor, { lastX });
if (issues) {
console.warn(issues[0].message);
return;
}
accepted = bar.x;
priceFeed.push(bar);
volumeFeed.push({ x: bar.x, y: bar.volume ?? null });
}
/**
* A REST gap-fill after a reconnect often hands the boundary bar back
* (inclusive end bounds). Bars declare one point per x, so append would
* reject it — drop what you already hold first, the same filter
* `infiniteHistory` applies to its own pages.
*/
export function onGapFill(page: OHLC[]): void {
// Deliver what the feeds are holding before reading the tail — a pending
// tick delivered after the page would land behind it.
priceFeed.flush();
volumeFeed.flush();
const lastX = price.read().at(-1)?.x;
const fresh = lastX === undefined ? page : page.filter((bar) => bar.x > lastX);
price.append(fresh);
// Every pane that rides the same bars lands the same page.
volume.append(fresh.map((bar) => ({ x: bar.x, y: bar.volume ?? null })));
// The held tail is the baseline again — the flush delivered the pending bar.
accepted = price.read().at(-1)?.x;
}
/** On teardown: a dispose flushes what is pending, then stops. */
export function disconnect(): void {
priceFeed.dispose();
volumeFeed.dispose();
}
export { plot };The two conflated feeds are for a loud socket: every updateLast copies the array, and a conflated feed coalesces the repeated updates to the same bar until the next frame instead (a tick that opens a new bar delivers the previous one at once). The live feed guide has the full wiring — aggregation, snapshots, reconnects and history.
See @finchart/indicators for the full indicator list, and the @finchart/dom README for what browserDeps wires up under the hood.
Curious how the pieces fit together? See Architecture. In a Next.js app, start with Next.js and React apps; for the axis's zone and market sessions, Time zones and sessions.