Edgoro
Indicator · Markers, drawings and colors

Pine Script table: an on-chart dashboard example

Build a dashboard table in Pine Script v6 with table.new() and table.cell(): create it once with var, fill it on the last bar, and color cells by value.

Short answer

Create the table once with var table t = table.new(position.top_right, columns, rows), then write its cells with table.cell() inside if barstate.islast, so the work is done on one bar instead of thousands. Each cell takes a string, so convert numbers with str.tostring().

The script

A two-column dashboard with the close, the change, RSI, ATR as a percentage of price, the trend against an SMA and relative volume. Paste it into a new indicator in the Pine Editor, then save and add it to the chart.

Pine Script v6
//@version=6
indicator("Market dashboard", overlay = true)

rsiLen   = input.int(14, "RSI length", minval = 2)
atrLen   = input.int(14, "ATR length", minval = 1)
trendLen = input.int(50, "Trend SMA length", minval = 2)
corner   = input.string("Top right", "Position", options = ["Top right", "Bottom right", "Bottom left", "Top left"])

rsi    = ta.rsi(close, rsiLen)
atr    = ta.atr(atrLen)
trend  = ta.sma(close, trendLen)
volAvg = ta.sma(volume, 20)
chgPct = (close - close[1]) / close[1] * 100

tablePos = switch corner
    "Bottom right" => position.bottom_right
    "Bottom left"  => position.bottom_left
    "Top left"     => position.top_left
    => position.top_right

// Created once: 2 columns, 7 rows
var table dash = table.new(tablePos, 2, 7, bgcolor = color.new(color.black, 15), border_color = color.new(color.gray, 60), border_width = 1)

// Writes one row: the name on the left, the value on the right
row(table t, int r, string name, string value, color bg) =>
    t.cell(0, r, name, text_color = color.silver, text_halign = text.align_left, text_size = size.small)
    t.cell(1, r, value, text_color = color.white, bgcolor = bg, text_halign = text.align_right, text_size = size.small)

if barstate.islast
    color chgBg   = chgPct >= 0 ? color.new(color.teal, 60) : color.new(color.red, 60)
    color rsiBg   = rsi > 70 ? color.new(color.red, 50) : rsi < 30 ? color.new(color.green, 50) : na
    color trendBg = close > trend ? color.new(color.teal, 60) : color.new(color.red, 60)
    float relVol  = volAvg > 0 ? volume / volAvg : na
    row(dash, 0, syminfo.ticker, timeframe.period, na)
    row(dash, 1, "Close", str.tostring(close, format.mintick), na)
    row(dash, 2, "Change", str.tostring(chgPct, "#.##") + "%", chgBg)
    row(dash, 3, "RSI " + str.tostring(rsiLen), str.tostring(rsi, "#.0"), rsiBg)
    row(dash, 4, "ATR, % of price", str.tostring(atr / close * 100, "#.##") + "%", na)
    row(dash, 5, "Trend vs SMA " + str.tostring(trendLen), close > trend ? "Above" : "Below", trendBg)
    row(dash, 6, "Relative volume", na(relVol) ? "n/a" : str.tostring(relVol, "#.##") + "x", na)

How it works

  • var table dash = table.new(tablePos, 2, 7, ...). var creates the table on the first bar only. The number of columns and rows is fixed here: this one has columns 0–1 and rows 0–6.
  • The row() function writes the two cells of a row, so each line of the dashboard is one call. A function can change the table it receives: it can't reassign a global variable, but the table is an object, and calling its methods is allowed.
  • if barstate.islast. A table only shows its final state, so filling it on historical bars is wasted work. On the live bar the block runs on every update, so the values stay current.
  • Cell colors are ordinary color expressions: green or red for the change and the trend, and na (no background) when RSI is between 30 and 70.
  • Formats. format.mintick prints a price with the symbol's precision; patterns such as "#.##" set the decimals. Symbols without volume get "n/a" instead of a division by zero.

Pitfalls

  • table.new() without var. The table is created again on every bar. Only the last one is visible, but each creation costs time. TradingView's own advice is to assign table.new() to a var variable.
  • Filling cells on every bar. The writes do nothing visible on historical bars, and on long intraday charts they add to the script's time and memory use (Memory limits exceeded lists tables among the causes). Wrap them in if barstate.islast.
  • Numbers in cells. table.cell() takes a string: "RSI " + rsi gives operator + with a string and a number. Use str.tostring(rsi, "#.0").
  • A row that doesn't exist. Rows and columns count from 0 and are fixed by table.new(). Writing to row 7 of a 7-row table stops the script with a runtime error: add a row to table.new() first.
  • Several tables. A script can show up to nine tables, one for each position on the chart. Two dashboards at the same position cover each other: put them in different corners, or use one table with more rows.

Variations

One column per timeframe. request.security() returns RSI from other timeframes, and a loop writes the rows. On the live bar the higher-timeframe values move until those bars close; the multi-timeframe guide explains when that matters.

//@version=6
indicator("RSI on three timeframes", overlay = true)
len = input.int(14, "RSI length", minval = 2)
rsiOf(string tf) => request.security(syminfo.tickerid, tf, ta.rsi(close, len))
r1 = rsiOf("60")
r2 = rsiOf("240")
r3 = rsiOf("D")
var table t = table.new(position.top_right, 2, 4, bgcolor = color.new(color.black, 15))
if barstate.islast
    array<string> names = array.from("1H", "4H", "1D")
    array<float> values = array.from(r1, r2, r3)
    t.cell(0, 0, "TF", text_color = color.silver)
    t.cell(1, 0, "RSI", text_color = color.silver)
    for i = 0 to 2
        float v = values.get(i)
        t.cell(0, i + 1, names.get(i), text_color = color.white)
        t.cell(1, i + 1, str.tostring(v, "#.0"), text_color = color.white, bgcolor = v > 70 ? color.new(color.red, 50) : v < 30 ? color.new(color.green, 50) : na)

A gradient instead of thresholds. color.from_gradient(rsi, 30, 70, color.new(color.green, 40), color.new(color.red, 40)) shades the RSI cell from green at 30 to red at 70.

Questions

How do I change the text size of a Pine Script table?

Pass text_size to table.cell(), for example text_size = size.small (size.tiny to size.huge, or size.auto), or change it later with table.cell_set_text_size(). Each cell has its own size.

Why is my Pine Script table not showing?

The usual causes: no cell was ever written (an empty table draws nothing), the cells are filled under a condition that is never true, or another table sits at the same position. Also check that the script has no runtime error in the Pine Editor console.

Can a table show values from other symbols or timeframes?

Yes: get each value with request.security() at the top level, then write it into a cell. Each call counts toward the script's limit on data requests, so a large multi-symbol table needs care; see the screener guide.

Every script on this page was checked with Edgoro Pine Check (Pine Script v6 rules). Pine Check is a static checker, not TradingView's compiler: TradingView compiles the script when you save it in the Pine Editor. The scripts are examples to learn from, not trading systems. Trading involves substantial risk of loss and is not suitable for every investor. Edgoro is a software tool, not a broker or financial adviser: nothing it produces is investment advice, and past or backtested performance does not guarantee future results.

Need a different version of this script?

Describe the change in plain English and Edgoro writes the Pine Script v6 for you, checked with Pine Check. For example:

“Show a table in the top right corner with RSI, ATR and the trend on the 1-hour, 4-hour and daily timeframes, green when bullish and red when bearish.”