Docs·Charting Library·Standalone Modules

Orderbook · Depth Profile

Standalone Orderbook + Depth Profile module — same engine renders two complementary views — (1) a vertical orderbook panel with signal rows (OBI, spoofing, absorption, walls, gaps) — and (2) a multi-exchange depth profile with horizontal bars per price level and labeled walls. Its own canvas, its own engine.

The Orderbook · Depth Profile is the most flexible standalone module: one engine, one canvas, two complementary rendering modes.

  • Orderbook view — a vertical depth ladder, a rolling micro-heatmap column for each snapshot, plus optional order-flow signal overlays (OBI, spoofing, absorption, net flow, walls, gaps) rendered as highlight rows on the ladder.
  • Depth Profile view — horizontal depth bars per price level, multi-exchange aggregation (AGG ALL or per-venue), labeled walls (e.g. "1.5k", "1.2k"), and an optional time-heatmap strip for the history of resting depth.

Both views share the same data feed and the same engine — createOrderbookBridge — and switch behaviour through public options (exchangeCount, heatmapCols, showSignalOverlays, setSignalWalls, etc.). Customers typically pick one mode based on what their trader wants to see; advanced products switch between them based on the user's panel choice.

Use this when:

  • You want a full side-panel orderbook view comparable to professional desktop tools.
  • Your data layer can produce order-flow signals server-side (or via a separate JS pipeline) and you want them visualised on the same canvas as the depth.
  • You need a multi-exchange depth profile with aggregated or per-venue liquidity rendered on horizontal bars per price level.
  • You need a single component that takes raw book snapshots and gives the user a complete order-flow read in one pane.

If you only need a focused single-exchange price ladder without aggregation or signal overlays, the lighter DOM Ladder module covers that use case with a smaller surface and lower data overhead.

The two rendering modes

The engine ships in one binary; which view you get is determined by how you configure it. Both modes are documented in detail below.

Orderbook view (single-venue, order-flow signals)

Orderbook view — single-venue DOM with SELL / BID / PRICE / ASK / BUY / DELTA columns at 50-tick aggregation, aggressive flow on both sides, green absorption row and red absorption row highlighted with delta annotations (+2.44, -0.28), cyan depth bars on the ASK side, and OBI / SPOOF / VPIN signal scores in the header
Orderbook view — DOM ladder + per-row aggressive flow (left SELL, right BUY) + delta column + signal-highlighted absorption rows. Driven by pushSnapshot and the setSignal* setters.

Configuration profile (mount with these options):

OptionValueEffect
exchangeCount0 or 1Single-venue book, no multi-exchange columns.
heatmapCols5-10Few rolling micro-heatmap columns next to the ladder.
showSignalOverlaystrueEnables OBI / SPOOF / absorption / walls / gaps overlays.

Depth Profile view (multi-exchange, horizontal liquidity bars)

Depth Profile view — multi-exchange aggregated liquidity landscape across a wide price band (73800-85500) at 50-tick aggregation with AGG ALL setting, horizontal red bars showing ask-side resting depth above mid and green bars showing bid-side depth below mid, key walls labeled at 82000, 78650 and 75000, and a thin time-heatmap strip on the left edge tracking how depth evolves
Depth Profile — wide-range overview, AGG ALL across 11 markets, walls labeled at major liquidity clusters.
Depth Profile detail view — tighter price band (79200-81900) at finer aggregation with 11 markets aggregated, horizontal bars showing resting depth at each price level, prominent wall labels including 1.5k, 1.2k, 1.1k, 999.0, 998.3, 765.7, 525.3 marking large resting orders, plus a side time-heatmap strip on the left and the latest mid at 80327.5
Depth Profile — close-up around the mid, labeled wall sizes for every major level, ideal for read-out of where liquidity sits right now.

Configuration profile (mount with these options):

OptionValueEffect
exchangeCount2-NAggregate (or display per-venue) depth across multiple exchanges.
heatmapCols20-100Wide history strip shows how depth evolves through time.
showCumulativetrueHorizontal bars expand to total resting depth per price row.
showSignalOverlaysfalseHide order-flow signal rows — focus the user on liquidity.

Same engine, same data. Switching between the two views is just setExchangeCount(n) + setHeatmapCols(n) + setShowSignalOverlays(b). You can expose this as a "View mode" toggle in your UI without re-mounting the canvas.

Install & import

JavaScript
import { createOrderbookBridge } from '@mrd/chart-engine'

Attach to a canvas

The panel needs its own <canvas> — never share with the main chart or with another standalone module.

HTML
<div class="ob-wrap" style="width: 360px; height: 720px;">
  <canvas ref="obCanvas" style="width: 100%; height: 100%;"></canvas>
</div>

Sizing rule. The canvas relies on width: 100%; height: 100%; the parent provides the actual pixel size. A zero-height parent is the #1 cause of "mounted but doesn't render". The panel is taller than it is wide — a ratio of roughly 1 : 2 (e.g. 360 × 720) works well at desktop sizes.

Mount

JavaScript
const ob = await createOrderbookBridge(canvas, {
  symbol: 'BTCUSDT',
  tickSize: 0.5,
  visibleTicks: 40,
  heatmapCols: 60,
  exchangeCount: 1,
  pricePrecision: 2,
  theme: 'dark',
})

ob.start()

Options

OptionTypeDefaultDescription
symbolstringOptional label used in the header.
tickSizenumber0.25Price increment between ladder rows.
visibleTicksnumber40Number of price rows in the ladder.
heatmapColsnumber5Number of trailing depth-snapshot columns rendered alongside the ladder.
exchangeCountnumber0Number of separate exchange columns when aggregating multi-exchange depth. 0 means single-exchange.
pricePrecisionnumber2Decimals shown on the price column.
theme'dark' | 'light''dark'Initial theme.
ringColsnumber300Internal ring-buffer capacity for the heatmap columns (advanced).
ringRowsnumber400Internal ring-buffer capacity for ladder rows (advanced).

Push data

Three primary data paths feed the panel. Pick the one that matches your data source.

ob.pushSnapshot(bids, asks, midPrice)

Push one full orderbook snapshot that both renders into the ladder and appends as a new heatmap column. This is the most common path — call it once per depth update from your data source.

ArgumentShapeDescription
bidsArray<[price, qty]>Best bid first.
asksArray<[price, qty]>Best ask first.
midPricenumberCurrent mid (drives ladder centering).
JavaScript
ws.on('depth:snapshot', (s) => {
  ob.pushSnapshot(s.bids, s.asks, s.mid)
})

ob.pushSnapshotFlat(bidFlat, askFlat, midPrice)

Same intent as pushSnapshot, but takes pre-flattened typed arrays — skips an allocation step in the hot 5-10 Hz loop. Recommended when you control the buffer life-cycle.

JavaScript
const bidBuf = new Float64Array(40 * 2)
const askBuf = new Float64Array(40 * 2)
ws.on('depth:snapshot', (s) => {
  for (let i = 0; i < s.bids.length; i++) {
    bidBuf[i * 2]     = s.bids[i][0]
    bidBuf[i * 2 + 1] = s.bids[i][1]
  }
  // ... same for asks
  ob.pushSnapshotFlat(bidBuf, askBuf, s.mid)
})

ob.setDepthBook(bids, asks, midPrice) / ob.setDepthBookFlat(...)

Same shape as pushSnapshot but only updates the ladder — does not append a heatmap column. Use when your heatmap data has a different cadence from your depth feed (e.g. ladder updates at 10 Hz, heatmap rolls forward only once per second).

ob.pushHeatmapCol(bids, asks, midPrice) / ob.pushHeatmapColFlat(...)

The complement to setDepthBook — appends a heatmap column without touching the ladder. Useful when:

  • You aggregate depth differently between the ladder display and the historical heatmap.
  • You want to advance the heatmap on a wall-clock tick independent of book activity.

ob.resetRing()

Wipe the heatmap ring buffer. Call on symbol change so old columns don't bleed into the new symbol's view.

Multi-exchange Depth Profile

The Depth Profile view aggregates resting liquidity across multiple exchanges into one panel. There are two patterns — pick the one your data layer supports.

If your backend can merge depth across N venues and emit a single combined order book, treat it like a normal pushSnapshot — the engine doesn't need to know how many exchanges fed into it.

JavaScript
const ob = await createOrderbookBridge(canvas, {
  symbol: 'BTCUSDT',
  tickSize: 50,        // wider aggregation → reads as "AGG 50" in the header
  visibleTicks: 60,
  heatmapCols: 80,     // wide history strip
  exchangeCount: 0,    // engine sees one merged book — don't split into columns
  showSignalOverlays: false,
})

ws.on('depth:merged', (s) => {
  ob.pushSnapshot(s.bids, s.asks, s.mid)
})

Server-merging gives you exact volume conservation and avoids the JS-side allocation cost of summing maps every tick. This is how the production product renders the AGG ALL mode.

Pattern B — client-merged book

When the backend ships one stream per venue, merge in JS before pushing.

JavaScript
const books = new Map()   // exchange → { bids: Map<price, qty>, asks: Map<price, qty> }

function merge() {
  const bidMap = new Map()
  const askMap = new Map()
  let mid = 0, count = 0
  for (const b of books.values()) {
    for (const [p, q] of b.bids) bidMap.set(p, (bidMap.get(p) || 0) + q)
    for (const [p, q] of b.asks) askMap.set(p, (askMap.get(p) || 0) + q)
    if (b.mid) { mid += b.mid; count++ }
  }
  const bids = [...bidMap].sort((a, b) => b[0] - a[0])
  const asks = [...askMap].sort((a, b) => a[0] - b[0])
  return { bids, asks, mid: count ? mid / count : 0 }
}

ws.on('depth:per-venue', (msg) => {
  books.set(msg.exchange, msg.book)
  const merged = merge()
  ob.pushSnapshot(merged.bids, merged.asks, merged.mid)
})

For 3-5 exchanges this stays well within 5-10 Hz budget. Beyond that, push the merge to the backend (Pattern A) to avoid main-thread allocation pressure.

Per-venue columns

If you want each exchange rendered as a separate column (instead of aggregating into one merged view), set exchangeCount and label them:

JavaScript
ob.setExchangeCount(3)
ob.setExchangeLabels(['BINANCE', 'BYBIT', 'OKX'])
// then push each venue's book through pushSnapshot in turn

The engine slots each new pushSnapshot into the next column position internally — your code stays the same; only the canvas layout changes.

Walls labels

Labeled wall sizes (e.g. "1.5k", "1.2k") visible in the screenshot are produced by setSignalWalls — push the list of [price, size] pairs from your wall detector and the engine renders both the highlight and the size label.

JavaScript
ws.on('walls:update', (w) => {
  ob.setSignalWalls(w.bids, w.asks)  // [[price, size], ...]
})

Detection (which orders count as walls) happens server-side — the engine just renders the list you give it.

Signal overlays

These are optional. The panel renders them as small overlays on the ladder if you push the data; if you don't, the panel is just a clean ladder + heatmap.

All signal setters are independent — push only the ones your data layer provides.

MethodDescription
ob.setSignalObi(obi, bidPrice, askPrice)Orderbook Imbalance — a normalised -1..+1 score plus the best bid/ask prices it was computed on.
ob.setSignalSpoof(score)Spoofing detector score (0..1). Higher = more suspicious.
ob.setSignalAbsorption(bidAbs, askAbs)Absorption strength on each side.
ob.setSignalFlow(netFlow, cumDelta)Net order flow + cumulative delta over a recent window.
ob.setSignalWalls(bidWalls, askWalls)Arrays of [price, size] walls to highlight on the ladder.
ob.setSignalGaps(bidGaps, askGaps)Arrays of price ranges with no resting liquidity (potential fast-fill zones).
ob.setShowSignalOverlays(show)Master toggle to hide / show all signal overlays at once.
JavaScript
// Hook the panel up to a server-side analyzer
ws.on('orderflow', (a) => {
  ob.setSignalObi(a.obi, a.bestBid, a.bestAsk)
  ob.setSignalSpoof(a.spoofScore)
  ob.setSignalAbsorption(a.bidAbs, a.askAbs)
  ob.setSignalFlow(a.netFlow, a.cumDelta)
  ob.setSignalWalls(a.bidWalls, a.askWalls)
})

ob.setExchangeLabels(labels)

When exchangeCount > 0, pass an array of short exchange labels (['BINANCE', 'BYBIT', 'OKX']) and the panel renders them as column headers above the heatmap.

Display controls

MethodEffect
ob.setSymbol(s)Header label.
ob.setTickSize(t)Row spacing.
ob.setVisibleTicks(n)Ladder height in rows.
ob.setExchangeCount(n)Number of exchange columns.
ob.setHeatmapCols(n)Trailing heatmap columns shown.
ob.setHeatmapAlphaMul(mul)Heatmap cell opacity scale (0.1-1.32).
ob.setPrecision(d)Decimals on the price column.
ob.setAutoCenter(b)When true, ladder follows the mid; when false, locks to the user's manual scroll position.
ob.centerOnMid()One-shot — re-centers immediately, regardless of auto-center state.
ob.setShowCumulative(show)Toggle the cumulative-depth bars.
ob.setTheme('dark' | 'light')Theme switch.

Hover

JavaScript
const price = ob.getHoverPrice()  // current hovered price
const qty   = ob.getHoverQty()    // size at hovered row
const side  = ob.getHoverSide()   // 0 = none, 1 = bid, 2 = ask

Call these inside your own pointer-event handler when the cursor is over the panel — useful for populating an order ticket or a side tooltip.

Lifecycle

JavaScript
const ob = await createOrderbookBridge(canvas, options)

ob.start()
ob.resize()      // call on parent CSS-size changes (ResizeObserver)
ob.stop()
ob.clear()       // wipe rendered state without destroying the engine
ob.destroy()     // permanent — frees buffers + listeners

destroy() is irreversible — the panel is unusable after. Create a new one if you need to re-mount.

Common pitfalls

Heatmap columns don't roll forward. You are calling setDepthBook (ladder-only) instead of pushSnapshot (ladder + heatmap). The two paths are deliberately separate; if you want both, call pushSnapshot.

Walls highlight disappears after every snapshot. The signal layer is explicit — you must re-push the wall list with each new analyzer frame, OR call setSignalWalls only when the wall list actually changes. The panel does not autodetect walls from the depth feed.

Order-flow score "lags by N seconds". The signal setters render whatever you push. If your server-side analyzer batches at 1 Hz, the on-screen OBI is 1 s out of date by design. Tune your analyzer cadence, not the panel.

Switching symbol shows the previous symbol's heatmap. You forgot to resetRing() on the symbol change. The heatmap ring is keyed by column position, not by symbol — old columns persist until they age out.

Numbers look enormous on small-cap altcoins. The panel renders qty in base coins by default. If your data feed sends notional (qty × price), the numbers will look 5-7 orders of magnitude too big. Decide upstream whether you're shipping base or notional and stick to it.

Two-canvas attempt: panel and chart on the same <canvas>. Not supported — each engine takes ownership of the canvas's drawing surface, event listeners, and DPR transform. Allocate a dedicated <canvas> per engine.

Next

  • DOM Ladder — the focused price-ladder module without the signal / heatmap-column overhead.
  • Tick Stream — trade tape, VWAP, large-trade markers, book micro-heatmap.
  • Orderbook Heatmap (Depth) — the depth-as-color-matrix view embedded in the main chart.