◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / How to Draw Labels, Boxes, and Lines in Pine Script v6 (and Manage Object Limits)
Pine Script

How to Draw Labels, Boxes, and Lines in Pine Script v6 (and Manage Object Limits)

A practical guide to Pine Script v6 drawing objects: creating labels with label.new(), boxes with box.new(), and lines with line.new(), plus the part most tutorials skip — max_labels_count, max_boxes_count, and max_lines_count, and the garbage-collection behaviour that silently deletes your oldest drawings. Covers deterministic FIFO management with arrays and the cheaper update-in-place pattern using var and setter functions. Educational material only, not financial advice: drawings are a visualization and inspection tool, and no chart annotation makes a strategy profitable — trading always carries a real risk of loss.

How do I draw labels, boxes, and lines in Pine Script?

In Pine Script v6 you create drawings with three constructor functions: label.new() for text anchored to a point, box.new() for a rectangle spanning two corners, and line.new() for a segment between two points. Each call returns an ID (of type label, box, or line) that you can store in a variable, modify later with setter functions like label.set_text() or line.set_xy2(), and remove with label.delete(), box.delete(), or line.delete(). That ID-based model is the key difference from plot()-family functions: plots are declared once at global scope and render for every bar, while drawing objects are created imperatively, exist as individual objects, and can be moved or destroyed at any time.

A minimal example that draws one of each on the last chart bar:

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("Label, box, line demo", overlay = true)

if barstate.islast
    // Label anchored to the current bar's high.
    label.new(bar_index, high, "Last bar close: " + str.tostring(close, format.mintick),
         style = label.style_label_down, color = color.blue, textcolor = color.white)
    // Box spanning the range of the last 10 bars.
    box.new(bar_index - 10, ta.highest(high, 10), bar_index, ta.lowest(low, 10),
         border_color = color.gray, bgcolor = color.new(color.gray, 85))
    // Horizontal line at the close 10 bars ago, extended right.
    line.new(bar_index - 10, close[10], bar_index, close[10],
         extend = extend.right, color = color.orange, width = 2)

Note the details that trip people up: overlay = true puts drawings on the price panel; x-coordinates default to bar index (xloc.bar_index), and you switch to xloc.bar_time when anchoring to timestamps; and every built-in is namespaced — label.new, ta.highest, str.tostring — because bare calls do not compile in v6.

Why do my old drawings disappear? Object limits and garbage collection

This is the behaviour most tutorials skip, and it is the number-one source of "my script only draws on recent bars" confusion. TradingView caps how many drawing objects of each type a script can keep alive at once. By default a script retains only roughly the last 50 labels, 50 lines, and 50 boxes. You can raise each cap independently in the declaration statement — indicator("...", max_labels_count = 500), and likewise max_lines_count and max_boxes_count — but 500 per type is the hard ceiling. There is no setting that keeps unlimited drawings.

When your script creates one more object than the limit allows, Pine does not raise an error. It silently garbage-collects the oldest object of that type — first in, first out — and your earliest annotation vanishes from the left side of the chart. This is why a script that calls label.new() on every signal appears to work perfectly during development on recent data, then looks broken when you scroll back: the old labels were deleted, deterministically, by the runtime.

Two practical consequences follow. First, if you genuinely need a marker on every qualifying bar across all of history, a drawing object is the wrong tool — use plotshape() or plotchar(), which are plots and have no such per-object cap. Second, if you do use drawings, decide explicitly how many you want alive and manage them yourself rather than letting the garbage collector decide. Relying on implicit FIFO deletion means the visible set of drawings depends on how many bars your plan loads and how many signals fired — which makes visual review of a strategy's behaviour unreliable, exactly where you want it dependable. The next two sections show both explicit approaches.

Signal labels done right: confirmed bars and raised limits

The most common real use of label.new() is marking signal bars. Two correctness details matter here. First, raise the label cap deliberately so you know exactly how much history is annotated. Second, gate signal-driven drawing on barstate.isconfirmed, so a label is only created once the bar has closed. Without that gate, a condition that flickers true intrabar creates a label on the realtime bar that may not correspond to any confirmed signal — the drawing equivalent of a repainting alert, and a classic way to convince yourself a signal "was there" when the closed bar never qualified.

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("Cross labels", overlay = true, max_labels_count = 500)

fast = ta.sma(close, 10)
slow = ta.sma(close, 30)
crossUp = ta.crossover(fast, slow)

if crossUp and barstate.isconfirmed
    label.new(bar_index, low, "Cross " + str.tostring(close, format.mintick),
         style = label.style_label_up, color = color.teal, textcolor = color.white)

With max_labels_count = 500, the last 500 crossovers stay annotated; beyond that, FIFO deletion resumes. If the marker itself is all you need and text is optional, prefer the plot-based form, which covers the whole chart with no object budget at all:

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("Cross markers", overlay = true)
fast = ta.sma(close, 10)
slow = ta.sma(close, 30)
sig  = ta.crossover(fast, slow)
plotshape(sig and barstate.isconfirmed, "Cross", shape.triangleup,
     location.belowbar, color.teal)

A useful rule of thumb: plotshape/plotchar for unlimited, uniform markers; label.new when you need per-signal dynamic text, precise placement, or objects you will later move or delete.

Deterministic cleanup: managing drawings with an array

When you want more control than "the runtime deletes whatever is oldest," keep your own registry of drawing IDs in an array and enforce the retention policy yourself. The pattern is: declare a var array of the drawing type (created once, persisted across bars), push each new object's ID, and once the array exceeds your chosen size, array.shift() the oldest ID off the front and delete it explicitly.

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("Managed labels", overlay = true, max_labels_count = 500)

maxKeep = input.int(20, "Labels to keep", minval = 1, maxval = 500)

var array<label> labels = array.new<label>()

sig = ta.crossover(close, ta.sma(close, 20))
if sig and barstate.isconfirmed
    l = label.new(bar_index, low, str.tostring(close, format.mintick),
         style = label.style_label_up, color = color.navy, textcolor = color.white)
    array.push(labels, l)
    if array.size(labels) > maxKeep
        label.delete(array.shift(labels))

Now the number of live labels is exactly maxKeep, regardless of how much history loaded or how dense the signals are — the behaviour is reproducible, which is what you want when using drawings to visually audit a strategy's entries. The same pattern works verbatim for array<line> and array<box> with line.delete() and box.delete().

Two footnotes. The var keyword is load-bearing: without it the array would be re-created empty on every bar and your registry would never accumulate. And keep max_labels_count at or above your retention size — your own FIFO must be at least as strict as the runtime's, or the garbage collector will delete labels behind your back and leave dangling IDs in the array (deleting an already-collected label is harmless, but your count of visible labels will no longer match your bookkeeping).

Cheaper still: update one object in place instead of creating many

Often you do not need a trail of drawings at all — you need one line or label that reflects the current state: the rolling 20-bar high, the active session's range box, a live status readout. Creating a new object every bar and deleting the previous one works, but the idiomatic pattern is to create the object once, store its ID in a var variable, and move it with setters on the last bar.

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("Live level line", overlay = true)

len = input.int(20, "Lookback", minval = 1)
hi  = ta.highest(high, len)

var line  hiLine  = na
var label hiLabel = na

if barstate.islast
    if na(hiLine)
        hiLine  := line.new(bar_index - len, hi, bar_index, hi,
             extend = extend.right, color = color.red)
        hiLabel := label.new(bar_index, hi, "",
             style = label.style_label_left, color = color.red, textcolor = color.white)
    line.set_xy1(hiLine, bar_index - len, hi)
    line.set_xy2(hiLine, bar_index, hi)
    label.set_xy(hiLabel, bar_index, hi)
    label.set_text(hiLabel, str.tostring(hi, format.mintick))

Because exactly two objects ever exist, object limits become irrelevant and the script does minimal work on historical bars — the barstate.islast gate means drawing code runs only where it is visible. The na-check-then-create idiom ensures the constructors run once; every subsequent update is a setter call.

A closing note in the spirit of honest tooling: drawings are how you inspect a script, not evidence that it works. A beautifully annotated chart can still repaint, peek ahead via request.security, or mis-handle shorts. Use labels and boxes to make behaviour visible, then verify the logic itself — that is the audit that matters, and no amount of chart decoration substitutes for it.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro