Charts
Order flow
Footprint bars, volume profile, market profile, delta, cumulative delta and relative volume.
The Position This Library Takes
Most charting libraries estimate directional volume. They do not know which side was the aggressor, so they infer it from where the price moved inside the bar, which is the tick rule from the 1990s. The delta, cumulative delta and footprint numbers that come out of it disagree with the platforms that read exchange aggressor flags, and traders notice.
ORTEX Charts takes the other route. The host supplies the aggressor data and the library renders it exactly. Where the data is missing it estimates, marks the result as estimated in the tooltip, and offers a strict mode that shows a gap rather than a guess.
That is a real requirement, not a preference: footprint bars, exact volume profiles and true delta need per-price bid and ask volume from your feed. Without it you get the same estimates everyone else produces, honestly labeled.
The Data Model
Three optional shapes carry order-flow information into the library.
// On a bar: volume that lifted the offer and volume that hit the bid.
interface Bar {
time: number; open: number; high: number; low: number; close: number; volume: number;
buyVolume?: number;
sellVolume?: number;
}
// Per bar, per price level: a footprint.
interface FootprintBar {
time: number;
levels: Array<{ price: number; bid: number; ask: number }>; // ascending by price
}
// On a tick: the aggressor side when the feed reports it.
interface Tick { time: number; price: number; size: number; side?: "buy" | "sell" }
bid is volume that traded at the bid, meaning aggressive sellers. ask is volume that
lifted the offer, meaning aggressive buyers.
buyVolume and sellVolume travel through every path in the engine: row conversion, bar
aggregation, the realtime append buffers, update, and the aligned columns indicators read.
Two consequences follow from how the columns are allocated:
- A series created without aggressor volumes ignores them on later bars. History cannot
be retro-fitted, so set the first
setDatawith the columns you intend to use. - A series created with them writes
NaNfor a bar that lacks them, which is whatmethod: "aggressor"shows as a gap.
Footprint Bars
const fp = chart.setFootprint(flow, {
tickSize: 0.01,
layout: "bidAsk",
imbalanceRatio: 3,
});
fp.updateBar(liveFootprintBar); // realtime
For every bar wider than minBarWidth the primitive draws one cell per price level: the bid
half shaded in the down color and the ask half in the up color by share of the busiest level
in that bar, the numbers when the cell is at least nine pixels high, diagonal imbalances
outlined, the bar point of control boxed, and delta plus total volume under the bar.
When bars are narrower than minBarWidth the cells are unreadable, so the primitive draws a
thin per-bar delta strip along the bottom of the pane instead. The information stays visible
at every zoom level rather than disappearing.
Hovering a cell shows its bid, ask, delta and total.
| Option | Default | What it does |
|---|---|---|
minBarWidth |
28 |
Below this bar width in pixels, the delta strip replaces the cells. |
cluster |
1 |
Merge this many consecutive price levels into one cell. |
tickSize |
0 |
Level height in price units; zero derives it from the smallest gap between levels. |
layout |
"bidAsk" |
bidAsk prints bid and ask per cell, delta prints ask − bid, volume prints the total. |
imbalanceRatio |
3 |
Outline a level whose ask is at least this many times the bid one level below, and the mirror. Zero disables it. |
heat |
true |
Shade cells by volume relative to the busiest level of the bar. |
showTotals |
true |
Print delta and total under each bar. |
showPoc |
true |
Outline the bar point of control. |
buyColor |
null |
Overrides the theme up color. |
sellColor |
null |
Overrides the theme down color. |
stripHeight |
4 |
Height of the fallback delta strip in pixels. |
setFootprint does two extra things worth knowing. It gives the main bars their aggressor
volumes if they arrived without them, so Delta and CVD become exact; and it feeds every
volume profile added with addVolumeProfile, so those become exact too.
@ortex-charts/financialBuilding Footprints From Ticks
If your feed gives sided ticks rather than pre-aggregated levels:
import { footprintFromTicks, mergeFootprint } from "@ortex-charts/financial";
import { parseResolution } from "@ortex-charts/math";
const flow = footprintFromTicks(ticks, parseResolution("1"), 0.01, "America/New_York");
const barsWithFlow = mergeFootprint(bars, flow);
chart.setData(barsWithFlow);
chart.setFootprint(flow, { tickSize: 0.01 });
Ticks without a side fall back to the tick rule, which is the estimate every platform
without aggressor data uses. Pass sided ticks for exact results.
footprintTotals(bar) gives { buy, sell, total, delta, poc } for one footprint bar, which
is what a host-drawn summary row wants.
Volume Profile and Market Profile
const vp = chart.addVolumeProfile({ mode: "visible", tickSize: 0.01, splitDelta: true });
vp.setRange(fromTime, toTime); // switch to a fixed range
vp.setOptions({ mode: "session" }); // one profile per trading day
vp.setOptions({ kind: "tpo", tpoPeriodMs: 30 * 60_000 }); // market profile
vp.profiles(); // the computed rows, POC and value area
Modes
visiblerecomputes for the bars currently on screen, cached per range and data version.fixedusesrange, which is what you set from a date-range drawing or a preset button.sessiondraws one profile per zoned trading day, anchored at the first bar of the day.
Kinds
volumecounts volume at price: a volume profile.tpocounts one letter pertpoPeriodMsperiod per row: a market profile. Letters are drawn when rows are at least nine pixels high and a histogram replaces them below that.
Accuracy
With footprint levels attached, volume lands in the row that contains its price and the
buy-and-sell split is real. The computed profile reports exact: true.
Without them, each bar volume is spread evenly over the rows its range covers and split by
where the close sits in that range. The profile reports exact: false and the tooltip says
"estimated". This is the same approximation other libraries make; the difference is that it
is labeled.
Point of Control and Value Area
The point of control is the row with the most volume, or the most letters for a market
profile. The value area grows outward from it, taking the larger neighbor at each step,
until it holds valueArea of the total, which defaults to 70 percent.
Options
| Option | Default | What it does |
|---|---|---|
kind |
"volume" |
volume or tpo. |
mode |
"visible" |
visible, fixed or session. |
range |
null |
[fromTime, toTime] for fixed. |
rows |
24 |
Number of price rows when tickSize is zero. |
tickSize |
0 |
Row height in price units; capped at 400 rows. |
width |
0.3 |
Widest bar as a fraction of the plot width, or of the session width. |
position |
"right" |
Which edge the profile grows from. |
valueArea |
0.7 |
Share of volume inside the value area. |
showPoc |
true |
Draw the point-of-control line. |
showValueArea |
true |
Draw the value area more opaquely. |
splitDelta |
false |
Draw buy and sell volume as two stacked segments. |
showLabels |
true |
Print the row total when there is room. |
color, buyColor, sellColor |
null |
Override the theme. |
opacity |
— | Fill opacity of the profile. |
tpoPeriodMs |
— | Market profile period; one letter per period. |
Profiles draw below the series, so they never cover the candles. Hovering a row shows its price range, volume, buy and sell split, and delta.
@ortex-charts/financialComputing a Profile Without Drawing One
computeProfile is the pure function behind the primitive and is what the tests cover. Use
it when you want the numbers for a table, an export or a rule. It takes bar columns, an
inclusive bar-index range, options, and an optional map of footprint bars keyed by bar time.
import { computeProfile, type FootprintBar } from "@ortex-charts/financial";
const raw = chart.main.raw;
if (!raw || !("high" in raw)) throw new Error("no bars yet");
const byTime = new Map<number, FootprintBar>(flow.map((f) => [f.time, f]));
const profile = computeProfile(
{ time: raw.time, high: raw.high, low: raw.low, close: raw.close, volume: raw.volume },
0,
raw.length - 1,
{ rows: 40, valueArea: 0.7 },
byTime,
);
console.log(profile.rows[profile.poc], profile.vaLow, profile.vaHigh, profile.exact);
The primitive already keeps its own computed results, so vp.profiles() is the cheaper
route when a profile is on screen anyway.
The Three Indicators
| Id | Pane | What it shows |
|---|---|---|
delta |
Separate, histogram colored by sign, level line at zero | Per-bar buy minus sell volume. |
cvd |
Separate, line | Cumulative delta. |
rvol |
Separate, histogram, level line at one | Volume over the mean of the previous length bars, the current bar excluded. |
delta and cvd share a method input:
autouses the real aggressor columns when they exist and estimates otherwise.aggressoruses only real data and writesNaNwhere it is missing, so a gap in the histogram is a gap in the feed rather than a zero.estimatealways uses the tick-rule estimate, which is useful when you want the number other platforms would show.
chart.addIndicator("delta", { method: "aggressor" });
chart.addIndicator("cvd", { method: "auto" });
chart.addIndicator("rvol", { length: 50 });
indicators.barDelta(columns, method) is exported for hosts that want the numbers without a
pane.
@ortex-charts/financialPutting It Together
import { createFinancialChart, mergeFootprint } from "@ortex-charts/financial";
const chart = createFinancialChart(el, {
timeZone: "America/New_York",
resolution: "1",
session: "0930-1600",
data: mergeFootprint(bars, flow),
seriesOptions: { priceFormat: { type: "price", precision: 2, minMove: 0.01 } },
});
chart.setFootprint(flow, { tickSize: 0.01, layout: "bidAsk", imbalanceRatio: 3 });
chart.addVolumeProfile({ mode: "session", tickSize: 0.01, splitDelta: true });
chart.addIndicator("delta", { method: "aggressor" });
chart.addIndicator("cvd");
What Is Not Here
- Footprint is a primitive drawn over whichever main series is shown, not a series type of its own, so there is no delta-colored candle style yet.
- Composite profiles across several symbols are not supported.
- Profiles do not animate as developing profiles during bar replay.
- The toolbar has no built-in profile toggle. Adding one is a
ToolbarActionthat callsaddVolumeProfile.