# Concepts

> Charts, panes, series, scales, primitives and the render loop, in one page.

Section: Start here. Source: https://ortexcharts.com/docs/concepts

## The Shape of the Library

Seven packages sit in four layers, and dependencies only ever point downwards.

```text
react                                    components and hooks
financial   lite   viz   ui              product packages
core                                     canvas engine: panes, scales, series, interaction
math                                     pure functions, no DOM
```

Nothing sits below `math`. There is no third-party runtime dependency anywhere in the
library and no third-party code inside it: what you install is our code, written from the
ground up, and your lockfile gains one entry per package you actually import.

`@ortex-charts/math` has no access to the DOM and no canvas; it runs in Node, in a worker or
on a server. `@ortex-charts/core` owns the canvas
and the DOM. The product packages register series types, indicators, tools and chrome with
the core. `@ortex-charts/react` wraps the product packages and has no logic of its own.

The practical consequence is that you can drop a layer at any point. `createChartShell`
gives you a whole application; `createFinancialChart` gives you a price chart without
chrome; `createChart` gives you an engine with nothing on it. Each returns a handle that
exposes the layer below it, so you never have to choose the top layer and then discover a
wall.

## Chart, Pane, Price Scale, Series

A **chart** is a vertical stack of **panes** that share one time axis. A pane has its own
canvases, its own legend and any number of **price scales**. A **series** belongs to one
pane and one price scale.

```ts
const chart = createChart(el, { theme: "dark", timeZone: "America/New_York" });

const price = chart.addSeries("candlestick", { title: "AAPL" });
price.setData(bars);

const rsiPane = chart.addPane({ heightRatio: 0.25 });
const rsi = chart.addSeries("line", { title: "RSI", pane: rsiPane.id, color: "#A78BFA" });
rsi.setData(rsiPoints);
```

Pane heights are relative weights, not pixels. The first pane has a weight of one, so a pane
created with `heightRatio: 0.25` takes a fifth of the height. Users can drag the separator
between panes, collapse a pane to a legend strip, or maximize one; the core methods for that
are `setPaneCollapsed`, `maximizePane` and `restorePanes`.

Price scales are addressed by id. `"right"` and `"left"` are drawn on the corresponding
edge; any other id is an overlay scale that is not drawn at all, which is how a series gets
its own autoscaling without taking up axis width.

```ts
chart.addSeries("line", { title: "Volume ratio", priceScaleId: "left" });
chart.addSeries("line", { title: "Score", priceScaleId: "score-only" });   // invisible scale
chart.priceScale("left").applyOptions({ mode: "percentage", scaleMargins: { top: 0.2, bottom: 0.05 } });
```

Each scale carries its own mode: `normal`, `logarithmic`, `percentage` or `indexedTo100`.
Autoscale considers only the visible index range and only the visible series on that scale,
which is why zooming into a quiet stretch expands the price axis instead of leaving a flat
line at the bottom.

## The Index-Based Time Axis

The time axis is indexed by **bar position**, not by clock time. Bar 0 is the first bar of
the main series, bar 1 the second, and the pixel position of a bar is a linear function of
its index. Times in between are interpolated onto fractional indices.

This is what makes nights, weekends and holidays disappear without special cases: they are
simply absent from the index. It also means a chart can be scrolled past the last bar, since
indices extrapolate beyond the data, and that panning cost does not depend on how much
history is loaded.

The consequences worth internalizing:

- Ranges are expressed in indices for precision (`setVisibleRange(from, to)`) and in times
  for convenience (`setVisibleTimeRange(fromTime, toTime)`); the second converts to the
  first.
- The **main series** owns the timeline. It is the first bar series added, or whatever
  `chart.setMainSeries(id)` names.
- Every other series is **aligned** onto that timeline before it is drawn, which is the next
  concept.

```ts
chart.timeScale.setVisibleRange(bars.length - 260, bars.length - 1);
chart.timeScale.fitContent();
chart.timeScale.scrollToRealtime();
chart.timeScale.subscribeVisibleRangeChange((r) => console.log(r.fromTime, r.toTime, r.barSpacing));
```

## Alignment: Series That Do Not Share a Timeline

Short interest arrives daily while the price chart shows five-minute bars. A benchmark
trades on a different calendar. A signal fires on 30 dates in two years. All of these go on
the same chart, and none of them share the main timeline.

Every series therefore declares an `align` mode, and the engine produces one value per bar
of the main timeline before drawing:

| Mode | Behavior | Typical use |
|---|---|---|
| `exact` | A value appears only where the timestamps match exactly. | Indicators computed from the bars themselves. |
| `forwardFill` | The last known value is carried forward until the next one. | Slower-moving fundamentals: short interest, cost to borrow, ratings. |
| `nearest` | The closest value within `maxGapMs` is used. | Feeds whose timestamps are close but not identical. |
| `bucket` | Values are collected into the bar that contains them. | Event counts and other things that are summed per bar. |

`forwardFill` is the default for a new series, because it is what a host adding its own data
almost always wants. Indicators set `exact` for themselves.

Alignment is incremental. When a live tick extends the main timeline in place, only the tail
is re-aligned, so an overlay costs work proportional to the new bars rather than to all of
the history.

_Live example: [short-interest](https://ortexcharts.com/examples/short-interest)_

## Data Representation

Series data is stored as a struct of arrays, not an array of objects. A bar series is
`{ length, time, open, high, low, close, volume }` where every column is a `Float64Array`;
a value series is `{ length, time, value }`. `NaN` marks a gap.

You do not have to build those arrays. `setData` accepts whichever form you have:

```ts
series.setData(barRows);                                   // Bar[]
series.setData([[time, value], [time, value]]);            // [time, value] pairs
series.setData([{ time, value }, { time, value }]);        // point objects
series.setData({ length, time, value });                   // columns you already own
```

Passing columns you already own is the cheapest path and the one to prefer for large data
sets, because nothing is copied. The trade-off is the invariant that goes with it: aligned
columns handed to renderers are **views** into growable buffers, so never hold a reference
to `series.columns` across a data change.

Timestamps are always milliseconds since the Unix epoch, in UTC.

## The Render Loop

Drawing is scheduled on an animation frame and split into layers, so the common interactions
do not repaint everything.

- Each pane has a **main canvas** (background, grid, series, primitives) and an **overlay
  canvas** (crosshair, active drawings, tooltips). Moving the crosshair repaints only the
  overlay canvases and the legend.
- The time-axis label has a canvas of its own for the same reason.
- Panning and zooming repaint the main canvases, but only for the visible index range, and
  dense lines are decimated to one minimum and one maximum per pixel column.
- Legend DOM is rebuilt only when its content actually changes, so a 60 Hz pan does not
  churn the DOM.
- Live updates append into a capacity-doubling backing store, so a tick does not copy the
  columns, and indicators recompute a window rather than the whole history.

`chart.requestDraw()` schedules a frame, `chart.requestDraw("overlay")` schedules an overlay
frame, and `chart.flush()` draws synchronously, which is what a test or a screenshot wants.

## Primitives: Anything That Is Not a Series

A **primitive** is an object attached to a pane that can draw in three passes, take part in
autoscale, be hit-tested, and consume pointer events before the chart does. Markers,
drawings, events, footprints, volume profiles, alerts and indicator band fills are all
primitives, and so is anything you write.

```ts
import type { PanePrimitive, PaneView } from "@ortex-charts/core";

const sessionShading: PanePrimitive = {
  id: "session-shading",
  zIndex: -1,
  draw(ctx, view: PaneView, layer) {
    if (layer !== "belowSeries") return;
    ctx.fillStyle = "rgba(120, 140, 180, 0.08)";
    for (let i = view.from; i <= view.to; i++) {
      if (!isPreMarket(view.indexToTime(i))) continue;
      ctx.fillRect(view.x(i) - view.timeScale.barSpacing / 2, view.plot.y, view.timeScale.barSpacing, view.plot.height);
    }
  },
};

chart.addPrimitive(sessionShading);
```

The `PaneView` a primitive receives carries everything needed to place pixels: `x(index)`,
`y(value)`, `valueAt(px)`, `indexAt(px)`, the visible range, the master timeline, the theme,
the device pixel ratio and the current hover position.

## Options, Themes and Events

Options are deeply partial everywhere. `chart.applyOptions({ crosshair: { mode: "free" } })`
changes one nested key and leaves the rest alone, and the same is true of
`series.applyOptions`.

A **theme** is a flat token object. Switching themes is a repaint, never a rebuild: series,
indicators, drawings and the visible range all survive `chart.setTheme("light")`. See
[Colors, fonts and formatting](https://ortexcharts.com/docs/customization) for the token list.

Events are subscriptions that return their own unsubscribe function, which is the shape the
whole library uses:

```ts
const off = chart.subscribeCrosshairMove((e) => { /* … */ });
off();
```

The subscriptions on the core chart are `subscribeCrosshairMove`, `subscribeClick`,
`subscribeDblClick`, `subscribeDataChange`, `subscribePaneChange`, `subscribeLegendMenu` and
`chart.timeScale.subscribeVisibleRangeChange`.

## State Belongs to the Host

The chart never writes to storage and never talks to a server. It does not fetch bars, it
does not save layouts, and it has no account system.

What it does instead is hand you plain JSON at the boundary. A [layout](https://ortexcharts.com/docs/shell) is a
serializable object you store per user. [Drawings](https://ortexcharts.com/docs/drawings) serialize to an array you
store per symbol. [Alerts](https://ortexcharts.com/docs/replay) are a list you persist and re-apply. Where the data
comes from is a [datafeed or an adapter](https://ortexcharts.com/docs/data) that you supply.

This is a deliberate constraint. It keeps ORTEX out of your customers' data, it makes the
library work identically behind a login and on a public page, and it means there is no ORTEX
service that has to be up for your chart to work.

## Vocabulary

| Term | Meaning |
|---|---|
| Bar | One OHLCV row: `{ time, open, high, low, close, volume }`, optionally with `buyVolume` and `sellVolume`. |
| Tick | One trade: `{ time, price, size }`, optionally with an aggressor `side`. |
| Resolution | A bar size in datafeed notation: `1`, `5`, `60`, `1D`, `1W`, `1M`. |
| Session | Trading hours as `0930-1600`, possibly several segments, or `24x7`. |
| Main series | The bar series that owns the timeline. |
| Alignment | Placing another series' values onto the main timeline. |
| Pane | A horizontal band with its own canvases and scales. |
| Primitive | A drawable, hit-testable object on a pane that is not a series. |
| Overlay | A host-defined data series the user can switch on from the toolbar. |
| Layout | The user's chart configuration as JSON, stored by the host. |
