Skip to content

Drawings and tables ​

How do you put a label, a line, a box or a table on the chart and change it later? Each *.new() call makes a drawing and returns its id. Keep the id in a var and call the setters on later bars to move or restyle that one drawing, or call *.new() again to add another. The chart keeps the newest 50 drawings of each kind unless your declaration asks for more.

pine
//@version=6
indicator("20-bar high", overlay = true)
float hi = ta.highest(high, 20)
// var: one label and one line for the whole run, made on the first bar
var label tag = label.new(bar_index, hi, "", color = color.teal,
     style = label.style_label_down, textcolor = color.white)
var line level = line.new(bar_index, hi, bar_index + 1, hi,
     color = color.teal, extend = extend.right)
if barstate.islast
    tag.set_xy(bar_index, hi)
    tag.set_text("High " + str.tostring(hi, format.mintick))
    level.set_xy1(bar_index - 19, hi)
    level.set_xy2(bar_index, hi)

A drawing lives on the chart, not in a series: it has no value on each bar, and there is one of it, so a later bar that moves it moves it for the whole chart. That makes drawings right for what describes the present, such as a level, a note or a table, and plots right for what each bar had. For plots, see Plots and colours.

Making and changing a drawing ​

label.new(), line.new(), box.new(), polyline.new(), linefill.new() and table.new() each return an id of their own type. The kind's setters change one property, such as label.set_text(id, text) or line.set_color(id, color), and its getters read one back. Every setter and getter is also a method, so tag.set_text("…") and label.set_text(tag, "…") are the same call.

Where you keep the id decides how many drawings you get:

You writeYou get
var label tag = label.new(...), then settersOne label, made on the first bar and moved after
label.new(...) on every barA new label each bar, until the limit below starts removing the oldest
label.new(...) inside if condOne label on each bar where cond holds

A drawing's position takes two numbers. By default x is a bar index, so bar_index - 10 is ten bars back. With xloc = xloc.bar_time, x is a time in milliseconds, which suits a moment you know by its time, such as a session's open. Labels, lines, boxes and polylines can also take chart.points, which carry both: chart.point.from_index(bar_index, high), chart.point.from_time(time, high), or chart.point.now(high) for this bar.

How many stay ​

Labels, lines, boxes and polylines each count separately. When a script makes more of one kind than its limit, the engine removes the oldest until that many are left, so the chart always shows the newest. The limit is 50 of each, unless indicator() or strategy() says otherwise:

pine
//@version=6
// up to 200 labels stay, the newest
indicator("A label on every cross", overlay = true, max_labels_count = 200)
float fast = ta.ema(close, 9)
float slow = ta.ema(close, 21)
if ta.crossover(fast, slow)
    label.new(bar_index, low, "Up", style = label.style_label_up)
  • max_labels_count, max_lines_count, max_boxes_count and max_polylines_count set each kind's limit. This engine takes any positive number. TradingView's manual allows at most 500.
  • The engine lets a kind run up to five past its limit before it removes the oldest, so label.all can hold a few more than the limit while the chart shows the newest.
  • A drawing that was removed this way is off the chart and out of label.all, but its getters still read its last values.
  • Tables don't count toward a limit. A table made at a position where another table stands replaces it, so one position holds one table.
  • A linefill made on the same two lines as another replaces it, and a linefill goes when either of its lines is deleted.

Deleting and copying ​

label.delete(id) takes a label off the chart and out of label.all at once, and from then on its getters read na. Each kind has the same call. label.copy(id) makes a new drawing with the same values, which counts as a new one.

label.all, line.all, box.all, polyline.all, linefill.all and table.all are arrays of the live drawings of each kind, oldest first. Use them to clear what a script drew:

pine
//@version=6
indicator("Keep the last three crosses", overlay = true)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
if ta.cross(fast, slow)
    label.new(bar_index, high, fast > slow ? "Up" : "Down")
    // label.all holds the oldest first: delete until three are left
    while array.size(label.all) > 3
        label.delete(array.first(label.all))

Polylines and linefills ​

polyline.new(points) draws one shape through an array of chart.points, straight or with curved = true, and closed = true joins the last point to the first. Build the array, then make the polyline once, since a polyline has no setters.

linefill.new(line1, line2, color) colours the space between two lines and follows them as they move.

pine
//@version=6
indicator("Last five swing highs", overlay = true)
float ph = ta.pivothigh(high, 3, 3)
var array<chart.point> swings = array.new<chart.point>()
var polyline path = na
if not na(ph)
    // the swing was 3 bars back, so the point goes there (see Traps)
    swings.push(chart.point.from_index(bar_index - 3, ph))
    if swings.size() > 5
        swings.shift()
if barstate.islast and swings.size() > 1
    path.delete()  // each live bar is the last: drop the one before
    path := polyline.new(swings, line_color = color.orange,
         line_width = 2)

Tables ​

A table is a grid fixed to one of nine places on the pane, such as position.top_right, that stays put while the chart scrolls. Make it once with table.new(position, columns, rows) in a var, then fill its cells with table.cell(id, column, row, text).

pine
//@version=6
indicator("Stats table", overlay = true)
var table stats = table.new(position.top_right, 2, 2,
     bgcolor = color.new(color.gray, 85))
// work values out on every bar, outside the branch
float rsi = ta.rsi(close, 14)
float atr = ta.atr(14)
if barstate.islast
    stats.cell(0, 0, "RSI")
    stats.cell(1, 0, str.tostring(rsi, "#.0"))
    stats.cell(0, 1, "ATR")
    // text_color: without it a cell's text is color.black
    stats.cell(1, 1, str.tostring(atr, format.mintick),
         text_color = color.orange)
  • table.cell sets the whole cell. Anything the call leaves out goes back to its default, so a second table.cell on a cell without bgcolor clears the colour the first one set. The one thing kept is a merge made with table.merge_cells.
  • table.cell_set_text, table.cell_set_bgcolor and the other table.cell_set_* calls change one property and keep the rest.
  • A cell's text is color.black (#363A45) until a call gives it a colour, so on a dark chart give text_color every time.
  • table.merge_cells(id, start_column, start_row, end_column, end_row) joins a block of cells into one, and table.clear empties a block.

Drawing on the last bar only ​

A drawing that shows the present only needs work on the last bar. Gate it with if barstate.islast, as the examples above do, and the script makes and updates it once instead of on every bar of history.

A script that does heavy work only to draw it, such as refitting a model and redrawing its lines, can wait for the last closed bar:

pine
//@version=6
indicator("Fit on closed bars only", overlay = true)
var line fit = na
float end = ta.linreg(close, 50, 0)
float slope = end - ta.linreg(close, 50, 1)
// last_bar_index - 1 is the last closed bar while the newest one forms
if barstate.isconfirmed and bar_index >= last_bar_index - 1
    fit.delete()
    // it starts 49 bars back, so the engine warns (see Traps)
    fit := line.new(bar_index - 49, end - 49 * slope, bar_index, end,
         color = color.blue)

That runs the work once on history, and again each time a live bar closes. It's plain Pine, so it works the same on TradingView. It suits only drawings that are redrawn whole: anything the script keeps from older bars, such as a label at each pivot, would be lost.

Traps ​

  • Drawings vanish from the left. A script that makes a drawing on every bar keeps only the newest 50 of that kind. Raise max_*_count, or keep one drawing in a var and move it.
  • Reading drawings into a plot. A plot of label.all.size(), or of a getter, for drawings made under barstate.islast shows a history that no live bar had, since those drawings didn't exist then. The engine warns of it as a leak.
  • A drawing placed on an earlier bar. A label at bar_index - 5, as a pivot's is, sits on the chart where the pivot was, though the script only knew of it five bars later. Read history with that in mind. The engine warns of it as a leak.
  • ta.* inside the drawing's if. A ta.* call under if barstate.islast runs on that one bar, so its window is nearly empty. Work values out before the branch, as the table example does. See state in branches.
  • A deleted drawing reads na. Its getters give na, and a setter on it changes nothing. Make a new one if you need it back.
  • Drawings inside a request. A drawing made in the expression of request.security() does nothing there. Draw on the chart's own bars with the values the request returns.

See also ​

AmenoScript is compatible with Pine Script v6. Pine Script is a trademark of TradingView, Inc., which has no part in AmenoScript.