---
url: /guide/getting-started.md
description: >-
  Stand a candlestick chart up in one file, then add volume, a crosshair and
  real-time updates. The 60-second example, and what each line buys you.
---

# Getting Started

Get one candlestick chart on screen in 60 seconds.

## Installation

::: code-group

```bash [npm]
npm install @finchart/core @finchart/dom
```

```bash [pnpm]
pnpm add @finchart/core @finchart/dom
```

```bash [yarn]
yarn add @finchart/core @finchart/dom
```

```bash [bun]
bun add @finchart/core @finchart/dom
```

:::

## Quick Start

Give the chart an element to mount into, with a height:

```html
<div id="chart" style="height: 400px"></div>
```

Then:

```ts
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](/guide/plot-contract#checking-data-before-it-goes-in).

## Real-Time Updates & Indicators

::: code-group

```bash [npm]
npm install @finchart/indicators
```

```bash [pnpm]
pnpm add @finchart/indicators
```

```bash [yarn]
yarn add @finchart/indicators
```

```bash [bun]
bun add @finchart/indicators
```

:::

Register 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:

```ts
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](/guide/live-feed) has the full wiring —
aggregation, snapshots, reconnects and history.

See [`@finchart/indicators`](https://github.com/FutureSeller/finchart/tree/main/packages/indicators)
for the full indicator list, and the
[`@finchart/dom`](https://github.com/FutureSeller/finchart/tree/main/packages/dom)
README for what `browserDeps` wires up under the hood.

Curious how the pieces fit together? See [Architecture](/guide/architecture).
In a Next.js app, start with [Next.js and React apps](/guide/nextjs); for
the axis's zone and market sessions, [Time zones and sessions](/guide/time-zones).
