Library · Pine Script v6 · Library

PineDraw

by 345-strategies

PineDraw is a Pine Script v6 library that provides object primitives for drawing and updating price levels on the chart.

View on GitHub

GitHub stars5on the repository
Pine versionv6current
Lines of code396
Settings0inputs
LicenceMPL-2.0
Last update2 days ago26 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new library, select all of the template text and paste. Press Save and give it a name.
  4. 4Publish or import itA library draws nothing. Publish it from the editor, then import its functions in your own scripts.

Source Code

396 lines of Pine Script v6 from pine-draw/PineDraw.pine, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

PineDraw.pine
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// https://mozilla.org/MPL/2.0/.
//
// PineDraw - drawing and labeling components for Pine v6
// Extracted from TheStrat Suite, where they render six timeframes of levels on one chart.

//@version=6

// @description Object primitives for indicators that draw price levels: update lines,
// boxes and labels in place instead of deleting and recreating them every tick, pool
// labels so a busy chart stays under the object limit, and merge labels that land on the
// same price so they read as one row instead of overlapping - keeping each entry's own
// color and time anchor when they matter.
library("PineDraw", overlay = true)

// ============================================================================
// WHY UPDATE IN PLACE
// ============================================================================
// The obvious way to redraw a level is delete-then-create on every tick. It works until
// the chart is busy, then you are churning hundreds of objects a second and racing
// TradingView's per-script object cap. Every function here mutates an existing object
// when it has one and only allocates when it does not.

// @function Update a line in place, or create it if the reference is na.
// @param ref  Existing line, or na on the first call.
// @param x1   Left anchor, in bar time (ms).
// @param y1   Left price.
// @param x2   Right anchor, in bar time (ms).
// @param y2   Right price.
// @param col  Line color.
// @param w    Line width.
// @param style One of line.style_solid, line.style_dashed, line.style_dotted.
// @returns The line reference. Assign it back: `myLine := updateLine(myLine, ...)`.
export updateLine(line ref, int x1, float y1, int x2, float y2, color col, int w, string style) =>
    if na(ref)
        line.new(x1, y1, x2, y2, color = col, width = w, style = style, xloc = xloc.bar_time)
    else
        line.set_xy1(ref, x1, y1)
        line.set_xy2(ref, x2, y2)
        line.set_color(ref, col)
        line.set_width(ref, w)
        line.set_style(ref, style)
        ref

// @function Update a box in place, or create it if the reference is na.
// @param ref Existing box, or na on the first call.
// @param x1 Left anchor in bar time (ms).
// @param y1 Top price.
// @param x2 Right anchor in bar time (ms).
// @param y2 Bottom price.
// @param bg Fill color.
// @param border Border color.
// @returns The box reference.
export updateBox(box ref, int x1, float y1, int x2, float y2, color bg, color border) =>
    if na(ref)
        box.new(x1, y1, x2, y2, bgcolor = bg, border_color = border, xloc = xloc.bar_time)
    else
        box.set_lefttop(ref, x1, y1)
        box.set_rightbottom(ref, x2, y2)
        box.set_bgcolor(ref, bg)
        box.set_border_color(ref, border)
        ref

// @function Delete a line if it exists. Assign the result back to clear your reference.
// @param ref Line to remove, or na.
// @returns na, typed as a line.
export clearLine(line ref) =>
    if not na(ref)
        line.delete(ref)
    line(na)

// @function Delete a box if it exists. Assign the result back to clear your reference.
// @param ref Box to remove, or na.
// @returns na, typed as a box.
export clearBox(box ref) =>
    if not na(ref)
        box.delete(ref)
    box(na)

// ============================================================================
// LABEL POOL
// ============================================================================
// Labels are the easiest way to blow the object budget, because most scripts recreate
// every label on every bar. A pool keeps them allocated and rewrites their contents.
// Call reset() at the top of each render pass, then acquire() once per label you want.
// Anything left unused from the previous pass is parked off-screen rather than deleted,
// so the next pass reuses it.

// @type A reusable set of labels. Hold one in a `var` at the top of your script.
// @field items Every label ever allocated by this pool.
// @field used  How many have been handed out in the current render pass.
export type LabelPool
    array<label> items
    int used

// @function Create an empty pool. Assign to a `var` so it survives across bars.
// @returns A new LabelPool.
export newPool() =>
    LabelPool.new(array.new<label>(), 0)

// @function Start a render pass. Hides any label left over from the previous pass.
// @param this The pool.
// @returns The pool, for chaining.
export method reset(LabelPool this) =>
    if this.used > 0
        for i = 0 to this.used - 1
            label.set_text(array.get(this.items, i), "")
    this.used := 0
    this

// @function End a render pass tightly: delete every pooled label beyond the ones acquired
//   this pass. reset() alone only blanks leftovers, which keeps them against the object
//   cap and leaves empty label stubs; trim removes them, so switching a feature off
//   actually removes its labels. reset, acquire, trim is the recommended pass.
// @param this The pool.
// @returns The pool, for chaining.
export method trim(LabelPool this) =>
    while array.size(this.items) > this.used
        label.delete(array.pop(this.items))
    this

// @function Take the next label from the pool, creating one only if the pool is exhausted.
// @param this The pool.
// @param x Anchor, bar time in ms when useBarIndex is false, otherwise a bar index.
// @param y Price.
// @param txt Label text.
// @param useBarIndex Anchor to bar_index instead of bar time. Use for labels that should
//   float at a fixed offset from the current bar rather than sit at a point in history.
// @param bg Background color.
// @param txtCol Text color.
// @param sz One of size.tiny, size.small, size.normal, size.large.
// @returns The label, already positioned and styled.
export method acquire(LabelPool this, int x, float y, string txt, bool useBarIndex, color bg, color txtCol, string sz) =>
    xl = useBarIndex ? xloc.bar_index : xloc.bar_time
    label lbl = na
    if this.used < array.size(this.items)
        lbl := array.get(this.items, this.used)
        label.set_xloc(lbl, x, xl)
        label.set_y(lbl, y)
        label.set_text(lbl, txt)
        label.set_color(lbl, bg)
        label.set_textcolor(lbl, txtCol)
        label.set_size(lbl, sz)
        label.set_style(lbl, label.style_label_left)
    else
        lbl := label.new(x, y, txt, xloc = xl, style = label.style_label_left, color = bg, textcolor = txtCol, size = sz)
        array.push(this.items, lbl)
    this.used += 1
    lbl

// ============================================================================
// PRICE CONSOLIDATION
// ============================================================================
// Two levels at the same price produce two labels stacked on the same pixel row, and the
// text becomes unreadable. Merging them into one row is the difference between a chart
// that scales to several timeframes and one that does not.

// @type A set of labels-to-be, grouped by price.
// @field prices One entry per distinct price.
// @field texts  The merged text for each price.
export type LabelSet
    array<float> prices
    array<string> texts

// @function Create an empty label set for one render pass.
// @returns A new LabelSet.
export newSet() =>
    LabelSet.new(array.new<float>(), array.new<string>())

// @function Add a label at a price, merging into an existing row if one is close enough.
// @param this The set.
// @param price The price to label.
// @param txt The text to add.
// @param joiner Placed between merged entries, for example " + ".
// @param tolerance How far apart two prices can be and still share a row, in price units.
//   Pass 0 to merge only exact ticks. syminfo.mintick is a sane floor; a fraction of
//   ta.atr() adapts across instruments.
// @returns The set, for chaining.
export method add(LabelSet this, float price, string txt, string joiner, float tolerance) =>
    int hit = -1
    if array.size(this.prices) > 0
        for i = 0 to array.size(this.prices) - 1
            if math.abs(array.get(this.prices, i) - price) <= tolerance
                hit := i
                break
    if hit >= 0
        array.set(this.texts, hit, array.get(this.texts, hit) + joiner + txt)
    else
        array.push(this.prices, price)
        array.push(this.texts, txt)
    this

// @function Draw a consolidated set through a pool, in one call.
// @param this The set to draw.
// @param pool The label pool to draw through.
// @param x Anchor for every label in the set.
// @param useBarIndex Anchor to bar_index instead of bar time.
// @param bg Background color.
// @param txtCol Text color.
// @param sz Text size.
// @returns The number of labels drawn.
export method flush(LabelSet this, LabelPool pool, int x, bool useBarIndex, color bg, color txtCol, string sz) =>
    int n = array.size(this.prices)
    if n > 0
        for i = 0 to n - 1
            pool.acquire(x, array.get(this.prices, i), array.get(this.texts, i), useBarIndex, bg, txtCol, sz)
    n

// @type A set of labels-to-be where every entry keeps its own color and, optionally, its
//   own time anchor. The first entry at a price wins the row's color and anchor; later
//   entries only append text. Draw with flushChips (row color as the label background)
//   or flushText (transparent background, row color as the text).
// @field prices One entry per distinct price.
// @field texts  The merged text for each price.
// @field colors The color of each row (first entry wins).
// @field times  The time anchor of each row (first entry wins; na means "use the flush anchor").
export type ColoredLabelSet
    array<float> prices
    array<string> texts
    array<color> colors
    array<int> times

// @function Create an empty colored label set for one render pass.
// @returns A new ColoredLabelSet.
export newColoredSet() =>
    ColoredLabelSet.new(array.new<float>(), array.new<string>(), array.new<color>(), array.new<int>())

// @function Add a label at a price, merging into an existing row if one is close enough.
//   On a merge the first entry keeps the row's color and time; the new text is appended,
//   unless the row already contains it, in which case nothing is added - two levels with
//   the same name at the same price read once, not twice.
// @param this The set.
// @param price The price to label.
// @param txt The text to add.
// @param txtColor The text color this entry wants. Only the row's first entry's color is used.
// @param t Time anchor for the row, in bar time (ms). Pass int(na) to anchor at the flush x.
// @param joiner Placed between merged entries, for example " + ".
// @param tolerance How far apart two prices can be and still share a row, in price units.
//   Pass 0 to merge only exact matches; round prices through syminfo.mintick first.
// @returns The set, for chaining.
export method add(ColoredLabelSet this, float price, string txt, color txtColor, int t, string joiner, float tolerance) =>
    int hit = -1
    if array.size(this.prices) > 0
        for i = 0 to array.size(this.prices) - 1
            if math.abs(array.get(this.prices, i) - price) <= tolerance
                hit := i
                break
    if hit >= 0
        if not str.contains(array.get(this.texts, hit), txt)
            array.set(this.texts, hit, array.get(this.texts, hit) + joiner + txt)
    else
        array.push(this.prices, price)
        array.push(this.texts, txt)
        array.push(this.colors, txtColor)
        array.push(this.times, t)
    this

// @function Draw a consolidated set as solid chip labels: each row's color becomes its
//   label background. This is the classic level-tag look. Rows with a stored time anchor
//   draw there; rows without one draw at x. When useBarIndex is true, stored times are
//   ignored and every row draws at x, since the two coordinate spaces cannot mix.
// @param this The set to draw.
// @param pool The label pool to draw through.
// @param x Fallback anchor: bar time in ms, or a bar index when useBarIndex is true.
// @param useBarIndex Anchor to bar_index instead of bar time.
// @param txtCol Text color for every chip, for example color.white.
// @param sz Text size for every row.
// @returns The number of labels drawn.
export method flushChips(ColoredLabelSet this, LabelPool pool, int x, bool useBarIndex, color txtCol, string sz) =>
    int n = array.size(this.prices)
    if n > 0
        for i = 0 to n - 1
            int t = array.get(this.times, i)
            int xa = useBarIndex or na(t) ? x : t
            pool.acquire(xa, array.get(this.prices, i), array.get(this.texts, i), useBarIndex, array.get(this.colors, i), txtCol, sz)
    n

// @function Draw a consolidated set as transparent tag text: each row's color becomes its
//   text color and the background is invisible, so the text reads as part of its line.
//   Anchoring behaves exactly like flushChips.
// @param this The set to draw.
// @param pool The label pool to draw through.
// @param x Fallback anchor: bar time in ms, or a bar index when useBarIndex is true.
// @param useBarIndex Anchor to bar_index instead of bar time.
// @param sz Text size for every row.
// @returns The number of labels drawn.
export method flushText(ColoredLabelSet this, LabelPool pool, int x, bool useBarIndex, string sz) =>
    int n = array.size(this.prices)
    if n > 0
        for i = 0 to n - 1
            int t = array.get(this.times, i)
            int xa = useBarIndex or na(t) ? x : t
            pool.acquire(xa, array.get(this.prices, i), array.get(this.texts, i), useBarIndex, color.new(color.black, 100), array.get(this.colors, i), sz)
    n

// ============================================================================
// SMALL HELPERS
// ============================================================================

// @function Map a position name from an input dropdown to a table position constant.
// @param pos One of "Bottom Center", "Bottom Left", "Bottom Right", "Middle Center",
//   "Middle Left", "Middle Right", "Top Center", "Top Left", "Top Right". Anything else
//   returns bottom right.
// @returns A position.* constant.
export tablePos(string pos) =>
    switch pos
        "Bottom Center" => position.bottom_center
        "Bottom Left" => position.bottom_left
        "Bottom Right" => position.bottom_right
        "Middle Center" => position.middle_center
        "Middle Left" => position.middle_left
        "Middle Right" => position.middle_right
        "Top Center" => position.top_center
        "Top Left" => position.top_left
        "Top Right" => position.top_right
        => position.bottom_right

// @function Map a style name to a Pine line style constant.
// @param style "Solid", "Dashed" or "Dotted". Anything else returns dotted.
// @returns A line.style_* constant.
export lineStyle(string style) =>
    style == "Solid" ? line.style_solid : style == "Dashed" ? line.style_dashed : line.style_dotted

// @function Map a size name to a Pine size constant.
// @param name "Tiny", "Small", "Normal" or "Large". Anything else returns small.
// @returns A size.* constant.
export textSize(string name) =>
    switch name
        "Tiny" => size.tiny
        "Small" => size.small
        "Normal" => size.normal
        "Large" => size.large
        => size.small

// ============================================================================
// DEMO - a compact tour of every component, so the library shows itself on a
// publication chart: three update-in-place lines and a box over the last
// twenty bars, price-merged labels where each source keeps its own color (the
// prior bar's high often IS the 20-bar high, and the two merge into one row),
// the pooled lifecycle (reset, flush, trim), and a positioned table. Bar
// spacing comes from the chart timeframe, never time - time[1], which is
// hours wide on the first bar after a session gap.
// ============================================================================
float demoHi20 = ta.highest(high, 20)
float demoLo20 = ta.lowest(low, 20)
int   demoHiOff = ta.highestbars(high, 20)
int   demoLoOff = ta.lowestbars(low, 20)

var LabelPool demoPool = newPool()
var table demoTag = table.new(tablePos("Bottom Right"), 1, 1, bgcolor = color.new(color.gray, 85))

if barstate.islast
    demoPool.reset()
    int barMs = timeframe.in_seconds(timeframe.period) * 1000
    // Two right edges: the 20-bar levels extend eight bars out, the prior-bar
    // stubs three. Their labels anchor at their own line's end through the
    // set's stored times, so the chips sit at different x positions.
    int endT  = time + barMs * 8
    int endT2 = time + barMs * 3
    // Each line starts at the bar where its level actually printed, so every
    // element has its own length - levels look like levels, not a grid.
    int   hiT  = time[-demoHiOff]
    int   loT  = time[-demoLoOff]
    float mid  = math.avg(demoHi20, demoLo20)
    int   midT = math.max(hiT, loT)

    var line hiLn  = na
    var line loLn  = na
    var line midLn = na
    var line phLn  = na
    var line plLn  = na
    var box  rngBx = na
    hiLn  := updateLine(hiLn,  hiT,     demoHi20, endT,  demoHi20, color.teal,   2, lineStyle("Solid"))
    loLn  := updateLine(loLn,  loT,     demoLo20, endT,  demoLo20, color.maroon, 2, lineStyle("Solid"))
    midLn := updateLine(midLn, midT,    mid,      endT,  mid,      color.gray,   1, lineStyle("Dotted"))
    phLn  := updateLine(phLn,  time[1], high[1],  endT2, high[1],  color.aqua,   1, lineStyle("Dashed"))
    plLn  := updateLine(plLn,  time[1], low[1],   endT2, low[1],   color.orange, 1, lineStyle("Dashed"))
    rngBx := updateBox(rngBx, math.min(hiT, loT), demoHi20, endT, demoLo20, color.new(color.teal, 85), color.new(color.teal, 100))

    // Chip labels, each at its own line's end. The box's edge rows sit at
    // exactly the 20-bar extreme prices, so those rows always merge: every
    // chart shows "20-bar high + range top" as one chip, which is the
    // consolidation doing its job rather than two chips stacked unreadably.
    cset = newColoredSet()
    cset.add(demoHi20, "20-bar high",  color.teal,   int(na), " + ", syminfo.mintick)
    cset.add(demoHi20, "range top",    color.teal,   int(na), " + ", syminfo.mintick)
    cset.add(demoLo20, "20-bar low",   color.maroon, int(na), " + ", syminfo.mintick)
    cset.add(demoLo20, "range bottom", color.maroon, int(na), " + ", syminfo.mintick)
    cset.add(mid,      "midpoint",     color.gray,   int(na), " + ", syminfo.mintick)
    cset.add(high[1],  "prior high",   color.aqua,   endT2,   " + ", syminfo.mintick)
    cset.add(low[1],   "prior low",    color.orange, endT2,   " + ", syminfo.mintick)
    cset.flushChips(demoPool, endT, false, color.white, textSize("Small"))
    demoPool.trim()

    table.cell(demoTag, 0, 0, "PineDraw demo", text_color = color.gray, text_size = size.small)

Overview

PineDraw enables indicators and strategies to maintain lines, boxes, and labels in place by updating existing objects instead of deleting and recreating them every tick. This approach prevents object limit issues on busy charts. The library also pools labels to reduce overlaps by merging those at the same price while preserving individual colors and anchors.

Details

Published by

345-strategies on GitHub

Type

Library, Pine Script v6

Draws

Library (no plots)

Category

Support & resistance

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

indicatorpine-scriptthestrattradingtradingviewprice-actionstratpine-script-librarythestrat-suite

Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

ZigZag

by rubystar0512
5 stars

This library calculates ZigZag pivots and draws connecting lines on the price chart. It changes direction when price moves beyond a percentage deviation after a set pivot depth, then stores each pivot with optional labels for reversal price, cumulative volume,

Library v5 Support & resistance
Updated 2 years ago · None stated GitHub repository of ZigZag

FVGDetectorLibrary

by alvinng214
2 stars

This Pine Script library detects and plots Smart Money Concept Fair Value Gaps (FVGs) for both demand and supply zones directly on the price chart. It exports a detection function that accepts configurable filter thresholds ranging from standard three-bar imba

Library v5 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · None stated GitHub repository of FVGDetectorLibrary

SMC Engine Private

by skipp-dev
1 stars

This Pine Script v6 library serves as an internal engine for Smart Money Concepts (SMC), providing types, tracking, and rendering logic for order blocks, fair value gaps, and market structure. It calculates order block parameters including quality scores, heig

Library v6 SMC / ICT / price actionSupport & resistance
Updated 2 days ago · None stated GitHub repository of SMC Engine Private

Daily Deviation

by Electrified-Trading
2 stars

This library calculates arrays of daily price deviations from the open to the high, low, and close over a user-specified number of days. It optionally filters outliers using a maximum deviation threshold. The fromOpen function returns a normalized value showin

Library v5 Volatility & bandsSupport & resistance
Updated 1 year ago · MPL-2.0 GitHub repository of Daily Deviation

SFA FlowLiquidity v1

by genesisgrubio39
1 stars

The SFA FlowLiquidity library computes structural flow and liquidity events from price data. It detects sweep ups and downs with associated scores, identifies liquidity levels, and applies session and regime filters. Persistent arrays track liquidity prices, b

Library v6 SMC / ICT / price actionSupport & resistance
Updated 10 months ago · None stated GitHub repository of SFA FlowLiquidity v1

FunctionLinearRegression

by ricardosantos79
261 stars

This Pine Script v5 library provides functions to calculate and plot linear regression channels using sample point arrays. It evaluates paired X and Y arrays to calculate the regression slope, intercept, fitted values, maximum deviation, minimum deviation, and

Library v5 Trend
Updated 4 years ago · None stated GitHub repository of FunctionLinearRegression

More libraries

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers