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, defaults4.time-Dateor 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
5000markers per symbol (oldest dropped). - Cleared when the script stops (Stop / edit / disable), like panels and bars.
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 orwidthTime.widthTime- bar length in ms of chart time, converted to pixels at draw time so it scales with zoom. Usefraction * regionDurationMsto size a bar as a percentage of a region (e.g. a box or session);widthTimetakes precedence overwidth.anchor-"time"(default) anchors to the candle column attime;"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 ignoretimeandalign, so the profile stays put as the chart scrolls.time-Dateor 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, defaults1.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
horizontalBarcall 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 instate. - Capped at
5000bars per symbol (oldest dropped).
chart.horizontalBar({ price, width, time?, color?, align?, anchor?, offsetTime?, height?, borderColor?, borderWidth?, onPress? })// 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, defaults2.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.lineframe - replace semantics per tick: redraw the whole set each run, do not accumulate instate.
chart.horizontalLine({ price, color?, width?, style?, onPress? })// 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-Dateor 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, defaults1.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
boxcall in a single run is collected into a fresh frame and committed after the run - do not accumulate boxes instate.
chart.box({ time1, price1, time2, price2, color?, borderColor?, borderWidth?, onPress? })// 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-Dateor 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).colordefaults"#f3f4f6";size(px) defaults11.
- Labels are per symbol and drawn on the main chart only, on top.
- Replace semantics per tick: do not accumulate labels in
state.
chart.text({ price, text, time?, color?, align?, offsetX?, size?, onPress? })// 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, defaults120(clamped 40-600).seriesId- series name within the panel (required).points-{ time?, value }[].timeis aDateor ms timestamp (defaults current replay time);valueis the plotted number.color- CSS color for the series.type-"line"(default) or"histogram".
- Panels and series are per symbol.
plotreplaces the whole series each call - accumulate points yourself instate.- Each series is capped at
5000points (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.
const panel = chart.panel({ id, height? })
panel.plot(seriesId, points, { color?, type? })// 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? }[].timeis aDateor 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.
cellsreplaces the whole row each call - rebuild the row every tick, do not accumulate instate.- Cell text auto-shrinks with the candle width and hides when candles are too narrow to read.
- A panel should use
plotorcells, not both; a cells panel sizes from its row count and ignoresheight. - Each row is capped at
5000cells (oldest dropped). The seeded "Delta Cells" indicator is the reference user.
const panel = chart.panel({ id })
panel.cells(rowId, cells, { label? })// 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" })