Skip to content
GetProfitable
Search

Labels, lines and tables

Lesson 12 · about 12 min

Plots are cheap but rigid: one value per bar, drawn on every bar. Drawing objects are the flexible alternative. A label is text at a point, a line connects two points, a box fills a rectangle, and a table is a grid pinned to the pane corner. Each is created with new(), returns an ID, and can be modified or deleted later, which is how scripts show a stats panel that updates in place.

Labels

//@version=6
indicator("Pivot labels", overlay=true, max_labels_count=100)
ph = ta.pivothigh(high, 5, 5)
pl = ta.pivotlow(low, 5, 5)
if not na(ph)
    label.new(bar_index - 5, ph, str.tostring(ph, format.mintick),
         style=label.style_label_down, color=color.new(color.red, 20), textcolor=color.white, size=size.small)
if not na(pl)
    label.new(bar_index - 5, pl, str.tostring(pl, format.mintick),
         style=label.style_label_up, color=color.new(color.green, 20), textcolor=color.white, size=size.small)

Unlike plot(), label.new() is allowed inside if blocks; that is the point of it. bar_index - 5 places the label on the bar where the pivot actually formed. str.tostring(price, format.mintick) formats to the symbol's tick size; str.tostring(x, "#.##") gives two decimals.

Scripts keep only the most recent max_labels_count labels (default 50, maximum 500) and delete older ones automatically. To keep a single label that moves, declare it with var, delete the old one and create a new one on barstate.islast, as in module 3, or use label.set_xy() and label.set_text() to update it in place.

Lines

//@version=6
indicator("Range lines", overlay=true)
len = input.int(20, "Range length", minval=2)
hi = ta.highest(high, len)
lo = ta.lowest(low, len)
var line hiLine = na
var line loLine = na
if barstate.islast
    line.delete(hiLine)
    line.delete(loLine)
    hiLine := line.new(bar_index - len + 1, hi, bar_index, hi, color=color.red, width=2, extend=extend.right)
    loLine := line.new(bar_index - len + 1, lo, bar_index, lo, color=color.green, width=2, extend=extend.right)

line.new(x1, y1, x2, y2) takes bar indexes by default (xloc.bar_index) or timestamps with xloc=xloc.bar_time. extend=extend.right projects the line forward, which is what you want for a level. line.delete(na) is a harmless no-op, so the pattern above is safe on the first run. Line styles: line.style_solid, line.style_dashed, line.style_dotted. box.new(left, top, right, bottom) works the same way for shaded rectangles such as an opening range.

Tables

A table is a grid anchored to a corner of the pane. It is created once and its cells are rewritten, so the pattern is always var table t = table.new(...) and then table.cell(...) inside if barstate.islast.

//@version=6
indicator("Stats table", overlay=true)
atr = ta.atr(14)
rsi = ta.rsi(close, 14)
hi20 = ta.highest(high, 20)
distToHigh = (hi20 - close) / close * 100

var table stats = table.new(position.top_right, 2, 4, bgcolor=color.new(color.black, 70), border_width=1, border_color=color.gray)

cell(int col, int row, string txt, color bg) =>
    table.cell(stats, col, row, txt, text_color=color.white, bgcolor=bg, text_size=size.small)

if barstate.islast
    header = color.new(color.gray, 40)
    cell(0, 0, "Metric", header)
    cell(1, 0, "Value", header)
    cell(0, 1, "ATR 14", na)
    cell(1, 1, str.tostring(atr, format.mintick), na)
    cell(0, 2, "RSI 14", na)
    cell(1, 2, str.tostring(rsi, "#.#"), rsi > 70 ? color.new(color.red, 60) : rsi < 30 ? color.new(color.green, 60) : na)
    cell(0, 3, "To 20-bar high", na)
    cell(1, 3, str.tostring(distToHigh, "#.##") + "%", na)

table.new(position, columns, rows) sizes the grid up front. Positions are position.top_left, top_center, top_right, middle_left and so on through bottom_right. Cells are addressed by column then row, zero-based. Writing cells only on the last bar is not just an optimisation: doing it on every bar is slow and, because a table has no history, the result is identical anyway.

The little cell() helper is a user function wrapping table.cell, which is a normal thing to do when you would otherwise repeat the same six arguments a dozen times. Passing na as a colour means "no background".

Object budgets

Each script gets a limit on live objects: labels and lines default to 50 each (max_labels_count and max_lines_count in the declaration raise it to 500), boxes likewise, and a handful of tables. Older objects are dropped when the limit is hit, which usually looks like "my lines disappear on the left of the chart". Drawing only what the viewer needs, deleting what is stale, and using var for singletons keeps you far below the budget.

Key idea: Drawing objects are created inside conditions, return IDs, and can be updated or deleted; tables are created once with var and their cells rewritten on barstate.islast.

Try it: Draw the current day's opening range as a box (first 30 minutes of the regular session) and extend it to the right. Add a two-row table showing the range height in ticks and as a percent of price. Then add labels at each confirmed pivot high and low from the last 100 bars.

Recap

  • label.new, line.new, box.new and table.new create objects that can be modified or deleted through their IDs, and can be called inside if.
  • str.tostring(x, format.mintick) and str.tostring(x, "#.##") format numbers for text.
  • Use var plus delete-and-recreate (or set_* calls) for objects that should exist once.
  • Tables are written on barstate.islast only, with cells addressed by column then row.
  • Raise max_labels_count and max_lines_count when you need more than 50; delete stale objects to stay under budget.

See it drawn

Original diagrams for the ideas on this page. Illustrative, not real market data.

A range beside a trendOne chart swinging between a flat floor and ceiling, another stepping upwards inside a pair of sloping lines.Range-boundresistancesupportprice bounces between two levelsTrendingthe trend channelhigher highs and higher lowsA range has two flat edges; a trend has two sloping ones.
Range versus trend. On the left price keeps bouncing between the same floor and ceiling, which is a range. On the right each high and each low is higher than the last, inside a pair of sloping lines called a channel.

Finished this module? Take the module quiz.