Skip to content

Start here

Concepts

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

The Shape of the Library

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

react                                    components and hooks
financial   lite   viz   ui              product packages
core                                     canvas engine: panes, scales, series, interaction
math                                     pure functions, no DOM
d3-array  d3-scale  d3-shape  d3-time  d3-time-format  d3-format

@ortex-charts/math has no access to the DOM and no canvas; it runs in Node, in a worker or on a server, and it is the only place D3 is imported. @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.

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.

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.
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.

Short interest on the chart@ortex-charts/financial
Loading short interest on the chart
ORTEX Estimated Short Interest and Days to Cover for IBM, plotted from a JSON feed on their own panes.

Open this example with its source

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:

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.

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 for the token list.

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

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 is a serializable object you store per user. Drawings serialize to an array you store per symbol. Alerts are a list you persist and re-apply. Where the data comes from is a datafeed or an adapter 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.