Tables are objects that can be used to position information in specific and fixed locations in a script’s visual space. Contrary to all other plots or objects drawn in Pine, tables are not anchored to specific bars; they float in a script’s space, whether in overlay or pane mode, in studies or strategies, independently of the chart bars being viewed or the zoom factor used.

Tables contain cells arranged in columns and rows, much like a spreadsheet. They are created and populated in two distincts steps:

  1. A table’s structure and key attributes are defined using, which returns a table id that acts like a pointer to the table, just like label, line, or array ids do. The call will create the table object but does not display it.
  2. Once created, and for it to display, the table must be populated using one table.cell() call for each cell. Table cells can contain text, or not. This second step is when the width and height of cells are defined.

Most attributes of a previously created table can be changed using table.set_*() setter functions. Attributes of previously populated cells can be modified using table.cell_set_*() functions.

A table is positioned in an indicator’s space by anchoring it to one of nine references: the four corners or midpoints, including the center. Tables are positioned by expanding the table from its anchor, so a table anchored to the position.middle_right reference will be drawn by expanding up, down and left from that anchor.

Two modes are available to determine the width/height of table cells:

  • A default automatic mode calculates the width/height of cells in a column/row using the widest/highest text in them.
  • An explicit mode allows programmers to define the width/height of cells using a percentage of the indicator’s available x/y space.

Displayed table contents always represent the last state of the table, as it was drawn on the script’s last execution, on the dataset’s last bar. Contrary to values displayed in the Data Window or in indicator values, variable contents displayed in tables will thus not change as a script user moves his cursor over specific chart bars. For this reason, it is strongly recommended to always restrict execution of all table.*() calls to either the first or last bars of the dataset. Accordingly:

  • Use the var keyword to declare tables.
  • Enclose all other calls inside an if barstate.islast block.

Multiple tables can be used in one script, as long as they are each anchored to a different position. Each table object is identified by its own id. Limits on the quantity of cells in all tables are determined by the total number of cells used in one script.

Creating tables

When creating a table using, three parameters are mandatory: the table’s position and its number of columns and rows. Five other parameters are optional: the table’s background color, the color and width of the table’s outer frame, and the color and width of the borders around all cells, excluding the outer frame. All table attributes except its number of columns and rows can be modified using setter functions: table.set_position(), table.set_bgcolor(), table.set_frame_color(), table.set_frame_width(), table.set_border_color() and table.set_border_width().

Tables can be deleted using table.delete(), and their content can be selectively removed using table.clear().

When populating cells using table.cell(), you must supply an argument for four mandatory parameters: the table id the cell belongs to, its column and row index using indices that start at zero, and the text string the cell contains, which can be null. Seven other parameters are optional: the width and height of the cell, the text’s attributes (color, horizontal and vertical alignment, size), and the cell’s background color. All cell attributes can be modified using setter functions: table.cell_set_text(), table.cell_set_width(), table.cell_set_height(), table.cell_set_text_color(), table.cell_set_text_halign(), table.cell_set_text_valign(), table.cell_set_text_size() and table.cell_set_bgcolor().

Keep in mind that each successive call to table.cell() redefines all the cell’s properties, deleting any properties set by previous table.cell() calls on the same cell.

Placing a single value in a fixed position

Let’s create our first table, which will place the value of ATR in the upper-right corner of the chart. We first create a one-cell table, then populate that cell:

study("ATR", "", true)
// We use `var` to only initialize the table on the first bar.
var table atrDisplay =, 1, 1)
// We call `atr()` outside the `if` block so it executes on each bar.
myAtr = atr(14)
if barstate.islast
    // We only populate the table on the last bar.
    table.cell(atrDisplay, 0, 0, tostring(myAtr))

Note that:

  • We use the var keyword when creating the table with
  • We populate the cell inside an if barstate.islast block using table.cell().
  • When populating the cell, we do not specify the width or height. The width and height of our cell will thus adjust automatically to the text it contains.
  • We call atr(14) prior to entry in our if block so that it evaluates on each bar. Had we used tostring(atr(14)) inside the if block, the function would not have evaluated correctly because it would be called on the dataset’s last bar without having calculated the necessary values from the previous bars.

Let’s improve the usability and aesthethics of our script:

study("ATR", "", true)
i_atrP = input(14,  "ATR period", minval = 1, tooltip = "Using a period of 1 yields True Range.")

// ————— Produces a string format usable with `tostring()` to restrict precision to ticks.
f_tickFormat() =>
    _s = tostring(syminfo.mintick)
    _s := str.replace_all(_s, "25", "00")
    _s := str.replace_all(_s, "5",  "0")
    _s := str.replace_all(_s, "1",  "0")

var table atrDisplay =, 1, 1, bgcolor = color.gray, frame_width = 2, frame_color =
myAtr = atr(i_atrP)
if barstate.islast
    table.cell(atrDisplay, 0, 0, tostring(myAtr, f_tickFormat()), text_color = color.white)

Note that:

  • We used to define a background color, a frame color and its width.
  • When populating the cell with table.cell(), we set the text to display in white.
  • We used the f_tickFormat() function to restrict the precision of ATR to the chart’s tick precision.
  • We now use an input to allow the script user to specify the period of ATR. The input also includes a tooltip, which the user can see when he hovers over the “i” icon in the script’s “Settings/Inputs” tab.

Coloring the chart’s background

This example uses a one-cell table to color the chart’s background on the bull/bear state of RSI:

study("Chart background", "", true)
i_c_bull = input(, 95), "Bull", inline = "1")
i_c_bear = input(, 95), "Bear", inline = "1")
// ————— Function colors chart bg on RSI bull/bear state.
f_colorChartBg(_c_bull, _c_bear) =>
    var table _chartBg =, 1, 1)
    float _r = rsi(close, 20)
    color _c_bg = _r > 50 ? _c_bull : _r < 50 ? _c_bear : na
    if barstate.islast
        table.cell(_chartBg, 0, 0, width = 100, height = 100, bgcolor = _c_bg)

f_colorChartBg(i_c_bull, i_c_bear)

Note that:

  • We provide users with inputs allowing them to specify the bull/bear colors to use for the background, and send those input colors as arguments to our f_colorChartBg() function.
  • We create a new table only once, using the var keyword to declare the table.
  • We use table.cell() on the last bar only, to specify the cell’s properties. We make the cell the width and height of the indicator’s space, so it covers the whole chart.

Creating a display panel

Tables are ideal to create sophisticated display panels. Not only do they make it possible for display panels to always be visible in a constant position, they provide more flexible formatting because each cell’s properties are controlled separately: background, text color, size and alignment, etc.

Here, we create a basic display panel showing a user-selected quantity of MAs values. We display their period in the first column, then their value with a green/red/gray background that varies with price’s position with regards to each MA. When price is above/below the MA, the cell’s background is colored with the bull/bear color. When the MA falls between the current bar’s open and close, the cell’s background is of the neutral color.

study("Price vs MA", "", true)

var string GP1 = "Moving averages"
int     i_masQty    = input(20, "Quantity", minval = 1, maxval = 40, group = GP1, tooltip = "1-40")
int     i_masStart  = input(20, "Periods begin at", minval = 2, maxval = 200, group = GP1, tooltip = "2-200")
int     i_masStep   = input(20, "Periods increase by", minval = 1, maxval = 100, group = GP1, tooltip = "1-100")

var string GP2 = "Display"
string  i_tableYpos = input("top", "Panel position", inline = "11", options = ["top", "middle", "bottom"], group = GP2)
string  i_tableXpos = input("right", "", inline = "11", options = ["left", "center", "right"], group = GP2)
color   i_c_bull    = input(, 30), "Bull", inline = "12", group = GP2)
color   i_c_bear    = input(, 30), "Bear", inline = "12", group = GP2)
color   i_c_neutral = input(, 30), "Neutral", inline = "12", group = GP2)

// ————— Produces a string format usable with `tostring()` to restrict precision to ticks.
f_tickFormat() =>
    string _s = tostring(syminfo.mintick)
    _s := str.replace_all(_s, "25", "00")
    _s := str.replace_all(_s, "5",  "0")
    _s := str.replace_all(_s, "1",  "0")

var table panel = + "_" + i_tableXpos, 2, i_masQty + 1)
if barstate.islast
    // Table header.
    table.cell(panel, 0, 0, "MA", bgcolor = i_c_neutral)
    table.cell(panel, 1, 0, "Value", bgcolor = i_c_neutral)

int period = i_masStart
for _i = 1 to i_masQty
    // ————— Call MAs on each bar.
    float _ma = sma(close, period)
    // ————— Only execute table code on last bar.
    if barstate.islast
        // Period in left column.
        table.cell(panel, 0, _i, tostring(period), bgcolor = i_c_neutral)
        // If MA is between the open and close, use neutral color. If close is lower/higher than MA, use bull/bear color.
        _c_bg = close > _ma ? open < _ma ? i_c_neutral : i_c_bull : open > _ma ? i_c_neutral : i_c_bear
        // MA value in right column.
        table.cell(panel, 1, _i, tostring(_ma, f_tickFormat()), text_color =, bgcolor = _c_bg)
    period += i_masStep

Note that:

  • Users can select the table’s position from the inputs, as well as the bull/bear/neutral colors to be used for the background of the right column’s cells.
  • The table’s quantity of rows is determined using the number of MAs the user chooses to display. We add one row for the column headers.
  • Even though we populate the table cells on the last bar only, we need to execute the calls to sma() on every bar so they produce the correct results. The compiler warning that appears when you compile the code can be safely ignored.
  • We separate our inputs in two sections using group, and join the relevant ones on the same line using inline. We supply tooltips to document the limits of certain fields using tooltip.

Displaying a heatmap

Our next project is a heatmap, which will indicate the bull/bear relationship of the current price relative to its past values. To do so, we will use a table positioned at the bottom of the chart. We will display colors only, so our table will contain no text; we will simply color the background of its cells to produce our heatmap. The heatmap uses a user-selectable lookback period. It loops across that period to determine if price is above/below each bar in that past, and displays a progressively lighter intensity of the bull/bear color as we go further in the past:

study("Price vs Past", "", true)

var int MAX_LOOKBACK = 300

int     i_lookBack  = input(150, minval = 1, maxval = MAX_LOOKBACK, step = 10)
color   i_c_Bull    = input(#00FF00ff, "Bull", inline = "11")
color   i_c_Bear    = input(#FF0080ff, "Bear", inline = "11")

// ————— Function returning `_color` with `_transp` transparency.
f_colorNew(_color, _transp) =>
    // color _color : base color to derive a new transparency from. Its current transparency is ignored.
    // float _transp: 0-100 transparency of `_color` to return.
    _r = color.r(_color)
    _g = color.g(_color)
    _b = color.b(_color)
    color _return = color.rgb(_r, _g, _b, _transp)

// ————— Function draws a heatmap showing the position of the current `_src` relative to its past `_lookBack` values.
f_drawHeatmap(_src, _lookBack) =>
    // float _src     : evaluated price series.
    // int   _lookBack: number of past bars evaluated.
    // Dependency: MAX_LOOKBACK

    // Force historical buffer to a sufficient size.
    max_bars_back(_src, MAX_LOOKBACK)
    // Only run table code on last bar.
    if barstate.islast
        var _heatmap =, _lookBack, 1)
        for _i = 1 to i_lookBack
            float _transp = 100. * _i / _lookBack
            if _src > _src[_i]
                table.cell(_heatmap, _lookBack - _i, 0, bgcolor = f_colorNew(i_c_Bull, _transp))
                table.cell(_heatmap, _lookBack - _i, 0, bgcolor = f_colorNew(i_c_Bear, _transp))

f_drawHeatmap(high, i_lookBack)

Note that:

  • We define a maximum lookback period as a MAX_LOOKBACK constant. This is an important value and we use it for two purposes: to specify the number of columns we will create in our one-row table, and to specify the lookback period required for the _src argument in our function, so that we force Pine to create a historical buffer size that will allow us to refer to the required quantity of past values of _src in our for loop.
  • We offer users the possibility of configuring the bull/bear colors in the inputs and we use inline to place the color selections on the same line.
  • Inside our function, we enclose our table-creation code in an if barstate.islast construct so that it only runs on the last bar of the chart.
  • The initialization of the table is done inside the if statement. Because of that, and the fact that it uses the var keyword, initialization only occurs the first time the script executes on a last bar. Note that this behavior is different from the usual var declarations in the script’s global scope, where initialization occurs on the first bar of the dataset, at bar_index zero.
  • We do not specify an argument to the text parameter in our table.cell() calls, so an empty string is used.
  • We calculate our transparency in such a way that the intensity of the colors decreases as we go further in history.
  • We use dynamic color generation to create different transparencies of our base colors as needed.
  • Contrary to other objects displayed in Pine scripts, this heatmap’s cells are not linked to chart bars. The configured lookback period determines how many table cells the heatmap contains, and the heatmap will not change as the chart is panned horizontally, or scaled.
  • The maximum number of cells that can be displayed in the scritp’s visual space will depend on your viewing device’s resolution and the portion of the display used by your chart. Higher resolution screens and wider windows will allow more table cells to be displayed.


  • When creating tables in strategy scripts, keep in mind that unless the strategy uses calc_on_every_tick = true, table code enclosed in if barstate.islast blocks will not execute on each realtime update, so the table will not display as you expect.
  • Keep in mind that successive calls to table.cell() overwrite the cell’s properties specified by previous table.cell() calls. Use the setter functions to modify a cell’s properties.
  • Remember to control the execution of your table code wisely by restricting it to the necessary bars only. This saves server resources and your charts will display faster, so everybody wins.
Options v: v4