Extending the Chart

Ways a code indicator can modify the chart. The script re-runs on every book update while enabled.

Every drawing below (mark, horizontalBar, line, horizontalLine, box, text) accepts an optional onPress object. When set, the drawing becomes clickable and pressing it shows a tooltip with your text - use it to reveal the numbers behind a drawing, e.g. the buys/sells that sized a bar. Use \n for multiple lines.

chart.mark()

Draws a circle on the chart at a price/time.

  • price - real price (required).
  • color - CSS color, defaults "red".
  • radius - circle radius in px, defaults 4.
  • time - Date or ms timestamp, defaults current replay time. Floored to the chart interval bucket at draw time.
  • onPress - { text }; clicking the marker shows a tooltip with your text.
  • Markers are per symbol.
  • Deduped by price|timestamp - re-emitting the same event does not stack.
  • Capped at 5000 markers per symbol (oldest dropped).
  • Cleared when the script stops (Stop / edit / disable), like panels and bars.
API
chart.mark({ price, color?, radius?, time?, onPress? })

chart.horizontalBar()

Draws one horizontal bar at a price - the building block for a volume profile. By default it anchors to a candle column; set anchor to "rightAxis" or "leftAxis" to pin it against the right or left edge of the chart instead. Emit one call per price level each tick; the chart redraws the whole set every run.

  • price - real price the bar is anchored to (required).
  • width - bar length in px (fixed on screen). Provide this or widthTime.
  • widthTime - bar length in ms of chart time, converted to pixels at draw time so it scales with zoom. Use fraction * regionDurationMs to size a bar as a percentage of a region (e.g. a box or session); widthTime takes precedence over width.
  • anchor - "time" (default) anchors to the candle column at time; "rightAxis" pins the bar to the right edge and grows it left, and "leftAxis" pins it to the left edge and grows it right. Both axis anchors ignore time and align, so the profile stays put as the chart scrolls.
  • time - Date or ms timestamp, defaults current replay time. Floored to the chart interval bucket to pick the candle column the bar sits in (time anchor only).
  • color - CSS color, defaults a translucent blue.
  • align - "left" (default) or "right", the direction the bar grows from its candle-column anchor (time anchor only).
  • height - bar thickness in px, defaults to the chart row height.
  • borderColor - CSS color; a border is only drawn when set (e.g. outline the POC bar).
  • borderWidth - border thickness in px, defaults 1.
  • onPress - { text }; when set, clicking the bar shows a tooltip with your text (e.g. the buys/sells that sized it).
  • Bars are per symbol and drawn on the main chart only.
  • Replace semantics per tick: every horizontalBar call in a single run is collected into a fresh frame and committed after the run, so redraw the whole profile each tick - do not accumulate bars in state.
  • Capped at 5000 bars per symbol (oldest dropped).
API
chart.horizontalBar({ price, width, time?, color?, align?, anchor?, offsetTime?, height?, borderColor?, borderWidth?, onPress? })
Example
// Volume profile: sum traded volume per price and draw buy/sell split bars.
// onPress makes each bar clickable, revealing the buys/sells behind its width.
const MAX_WIDTH = 80
const profile = footprint.profile(0, time.getTime())
const prices = Object.keys(profile)

let maxTotal = 0
for (const price of prices) {
  if (profile[price].total > maxTotal) maxTotal = profile[price].total
}

if (maxTotal > 0) {
  for (const price of prices) {
    const { buys, sells, total } = profile[price]
    const onPress = { text: `Buys: ${buys}
Sells: ${sells}` }
    // Sell bar (full width) then buy bar over it, so buy sits nearest the axis.
    chart.horizontalBar({ price: Number(price), width: (total / maxTotal) * MAX_WIDTH, color: "rgba(239, 68, 68, 0.7)", anchor: "rightAxis", onPress })
    chart.horizontalBar({ price: Number(price), width: (buys / maxTotal) * MAX_WIDTH, color: "rgba(59, 130, 246, 0.7)", anchor: "rightAxis", onPress })
  }
}

chart.horizontalLine()

Draws one full-width horizontal line at a price - for support and resistance levels, VWAP anchors, or any fixed price marker. Spans the whole plot regardless of time. Emit one call per level each tick; the chart redraws the whole set every run.

  • price - real price the line sits at (required).
  • color - CSS color, defaults "#8b949e".
  • width - line thickness in px, defaults 2.
  • style - "solid" (default) or "dashed".
  • onPress - { text }; clicking the line shows a tooltip with your text.
  • Lines are per symbol and drawn on the main chart only.
  • Shares the chart.line frame - replace semantics per tick: redraw the whole set each run, do not accumulate in state.
API
chart.horizontalLine({ price, color?, width?, style?, onPress? })
Example
// Mark the session high and low as dashed levels.
const s = state as { high?: number; low?: number }
const last = book.bid[0]?.price
if (last !== undefined) {
  if (s.high === undefined || last > s.high) s.high = last
  if (s.low === undefined || last < s.low) s.low = last
}

if (s.high !== undefined) chart.horizontalLine({ price: s.high, color: "#f87171", style: "dashed" })
if (s.low !== undefined) chart.horizontalLine({ price: s.low, color: "#4ade80", style: "dashed" })

chart.box()

Draws one filled rectangle spanning a time range and a price range - a selection highlight / zone. The two corners (time1, price1) and (time2, price2) can be given in any order; the box snaps to whole candle columns and lightly shades everything in between. Emit one call per zone each tick; the chart redraws the whole set every run.

  • time1 / time2 - Date or ms timestamps for the left and right edges (default current replay time). Floored to candle buckets.
  • price1 / price2 - real prices for the top and bottom edges (required).
  • color - fill color, defaults a translucent blue.
  • borderColor - CSS color; a border is only drawn when set.
  • borderWidth - border thickness in px, defaults 1.
  • onPress - { text }; clicking the box shows a tooltip with your text.
  • Boxes are per symbol and drawn on the main chart only.
  • Replace semantics per tick: every box call in a single run is collected into a fresh frame and committed after the run - do not accumulate boxes in state.
API
chart.box({ time1, price1, time2, price2, color?, borderColor?, borderWidth?, onPress? })
Example
// Highlight the range of the last 20 candles (high to low).
const s = state as { hi?: number; lo?: number; start?: number }
const t = time.getTime()
const windowMs = footprint.interval * 20
if (s.start === undefined || t - s.start > windowMs) {
  s.start = t
  s.hi = undefined
  s.lo = undefined
}

const mid = book.bid[0]?.price
if (mid !== undefined) {
  if (s.hi === undefined || mid > s.hi) s.hi = mid
  if (s.lo === undefined || mid < s.lo) s.lo = mid
}

if (s.hi !== undefined && s.lo !== undefined) {
  chart.box({ time1: s.start, price1: s.hi, time2: t, price2: s.lo })
}

chart.text()

Draws a text label at a price and time - the primitive for per-level footprint numbers and summaries. The x anchor is the candle column center; nudge it with offsetX and set align to place labels left/right of a row. Emit your labels each tick; the chart redraws the whole set every run.

  • price - real price of the row (required).
  • text - the string to draw (required).
  • time - Date or ms timestamp (default current replay time), floored to the candle bucket.
  • align - "center" (default) | "left" | "right".
  • offsetX - horizontal nudge in px from the column center (default 0).
  • color defaults "#f3f4f6"; size (px) defaults 11.
  • Labels are per symbol and drawn on the main chart only, on top.
  • Replace semantics per tick: do not accumulate labels in state.
API
chart.text({ price, text, time?, color?, align?, offsetX?, size?, onPress? })
Example
// Per-level delta on the current candle's footprint rows.
const t = time.getTime()
const bucketMs = Math.floor(t / footprint.interval) * footprint.interval
const rows = footprint.buckets[bucketMs] ?? {}
for (const priceStr in rows) {
  const { buys, sells } = rows[priceStr]
  const delta = buys - sells
  chart.text({
    price: Number(priceStr),
    text: String(delta),
    time: t,
    offsetX: 6,
    align: "left",
    color: delta >= 0 ? "#3b82f6" : "#ef4444",
  })
}

chart.panel()

Declares a sub-panel below the price chart and returns a handle for plotting time series into it. Calling it is an idempotent upsert keyed by id, so it is safe to call on every tick.

  • id - panel name shown on the panel (required).
  • height - panel height in px, defaults 120 (clamped 40-600).
  • seriesId - series name within the panel (required).
  • points - { time?, value }[]. time is a Date or ms timestamp (defaults current replay time); value is the plotted number.
  • color - CSS color for the series.
  • type - "line" (default) or "histogram".
  • Panels and series are per symbol.
  • plot replaces the whole series each call - accumulate points yourself in state.
  • Each series is capped at 5000 points (oldest dropped); the panel auto-scales to the visible values.
  • Panels are cleared on Run/Stop/edit and only appear while the script is running on a symbol a chart is showing.
API
const panel = chart.panel({ id, height? })
panel.plot(seriesId, points, { color?, type? })
Example
// Plot resting bid/ask depth over time
const s = state as { bid?: { time: number; value: number }[]; ask?: { time: number; value: number }[] }
if (!s.bid) { s.bid = []; s.ask = [] }

const t = time.getTime()
s.bid.push({ time: t, value: book.bid.reduce((sum, l) => sum + l.size, 0) })
s.ask.push({ time: t, value: book.ask.reduce((sum, l) => sum + l.size, 0) })

const panel = chart.panel({ id: "Book depth", height: 120 })
panel.plot("bids", s.bid, { color: "#4ade80" })
panel.plot("asks", s.ask, { color: "#f87171" })

panel.cells()

Draws per-candle stat rows into a panel - a table under the chart with one colored box per candle column, like delta / delta % / volume diff rows. Cell rows only render while the chart is zoomed into footprint mode; the pane collapses automatically when zoomed out to OHLCV.

  • rowId - row name within the panel (required). Rows stack top to bottom in call order, each 20px tall.
  • cells - { time?, value, text?, color? }[]. time is a Date or ms timestamp (defaults current replay time), floored to the chart interval so each cell aligns with a candle column.
  • value - the number shown in the cell and used for the sign-based fill: positive blue, negative red, zero gray.
  • text - overrides the displayed value (e.g. "7.4%").
  • color - overrides the sign-based fill.
  • label - row label drawn in the right axis margin.
  • cells replaces the whole row each call - rebuild the row every tick, do not accumulate in state.
  • Cell text auto-shrinks with the candle width and hides when candles are too narrow to read.
  • A panel should use plot or cells, not both; a cells panel sizes from its row count and ignores height.
  • Each row is capped at 5000 cells (oldest dropped). The seeded "Delta Cells" indicator is the reference user.
API
const panel = chart.panel({ id })
panel.cells(rowId, cells, { label? })
Example
// Per-candle delta row: sum buys - sells per interval candle.
const interval = footprint.interval
const deltas: Record<number, number> = {}
for (const bucketKey in footprint.buckets) {
  const candleTs = Math.floor(Number(bucketKey) / interval) * interval
  const levels = footprint.buckets[bucketKey]
  let delta = deltas[candleTs] ?? 0
  for (const priceStr in levels) {
    delta += levels[priceStr].buys - levels[priceStr].sells
  }
  deltas[candleTs] = delta
}

const cells = Object.keys(deltas).map((ts) => ({ time: Number(ts), value: deltas[Number(ts)] }))
chart.panel({ id: "stats" }).cells("delta", cells, { label: "Delta" })