Collections
Arrays, maps and matrices in ChartScript: creating them, keeping them across bars, the most useful functions, errors and size limits.
A series gives you one value per bar. Collections hold many values at once, independent of bars: the last ten swing highs, a count per weekday, a grid of correlations. ChartScript has three kinds:
| Kind | Holds | Typical use | Limit |
|---|---|---|---|
| array | an ordered list | levels, recent events, values to average | 100,000 elements |
| map | key → value pairs | counts or totals per name, per hour, per enum value | 50,000 entries |
| matrix | rows × columns of one type | grids, small linear algebra | 100,000 cells |
Every collection function can be called in two ways: array.push(arr, x) or, as a method, arr.push(x). The examples use the method form, which reads more naturally.
#Arrays
#Creating
const a = array.new<float>() // empty
const b = array.new<float>(5, 0.0) // five elements, all 0.0
const c = array.new_int(3, 7) // shortcut forms: new_float, new_int, new_bool, new_string, new_color, new_label, new_line, new_box
const d = array.from(high, low, close) // from values; the type comes from them
const e: float[] = array.new<float>() // T[] is another way to write array<T>
plot(b.size() + c.size() + d.size())An array can hold any one type: numbers, strings, colors, bools, drawings, your own user types, even other arrays.
#Keeping an array across bars
An array declared with const or let is a new, empty array on every bar. To collect values over time, declare it with state:
//@chartscript 1
indicator("Average of the last swing highs", { overlay: true })
const keep = input.int(5, "Swings to average", { minval: 1, maxval: 50 })
state swings = array.new<float>()
const ph = ta.pivothigh(high, 5, 5)
if (!na(ph)) {
swings.push(ph)
if (swings.size() > keep) {
swings.shift() // drop the oldest
}
}
const avgSwing = swings.size() > 0 ? swings.avg() : na
plot(avgSwing, "Average swing high", color.orange, 2, plot.style_stepline)Changes to a state array during a live update are rolled back before the next update, like any state value. See Execution model.
#Reading and changing
| Task | Call |
|---|---|
| number of elements | a.size() |
| element at an index | a.get(i); a.first(), a.last() |
| replace an element | a.set(i, x) |
| add at the end / at the front | a.push(x) / a.unshift(x) |
| insert at an index | a.insert(i, x) |
| remove from the end / the front / an index | a.pop() / a.shift() / a.remove(i) (each returns the removed value) |
| empty it | a.clear() |
| search | a.includes(x), a.indexof(x), a.lastindexof(x), a.binary_search(x) on a sorted array |
| sort | a.sort(), a.sort(order.descending); a.sort_indices() gives the order without moving anything |
| copy, part, join | a.copy(), a.slice(from, to), a.concat(b) |
| statistics | a.sum(), a.avg(), a.min(), a.max(), a.median(), a.mode(), a.stdev(), a.variance(), a.range(), a.percentrank(i) |
| text | a.join(", ") (string arrays) |
Negative indexes count from the end: a.get(-1) is the last element, a.get(-2) the one before.
An index past the end stops the script with CS5101, which names the index and the size. It is a runtime error, not na, because a silent na would hide the bug. Check a.size() first when the array can be short.
The full list, with every parameter, is on the array reference page.
#Square brackets are history, not indexing
a[1] is not the element at index 1. It is the array as it was on the previous bar, which for a state array is the same array (the compiler warns, CS2301). Use a.get(1).
//@chartscript 1
indicator("Brackets on an array")
state closes = array.new<float>()
closes.push(close)
const prev = closes[1] // the same array, not an element
plot(na(prev) ? 0 : prev.size()) // grows every bar, like closes itself#Maps
A map stores values under keys. Keys can be int, float, bool, string, color or an enum; values can be any type.
//@chartscript 1
indicator("Up-bar rate by hour (UTC)")
state upByHour = map.new<int, int>()
state allByHour = map.new<int, int>()
const h = hour(time)
allByHour.put(h, nz(allByHour.get(h)) + 1)
if (close > open) {
upByHour.put(h, nz(upByHour.get(h)) + 1)
}
const rate = allByHour.contains(h) ? nz(upByHour.get(h)) / allByHour.get(h) * 100 : na
plot(rate, "Up bars this hour, %", color.accent, { style: plot.style_columns })
hline(50, "Half", color.muted, hline.style_dotted)| Task | Call |
|---|---|
| store (replaces an existing value) | m.put(key, value), returns the old value or na |
| read | m.get(key), na when the key is missing |
| test | m.contains(key) |
| remove | m.remove(key), returns the removed value or na |
| size, keys, values | m.size(), m.keys(), m.values() (arrays, in insertion order) |
| copy, merge, empty | m.copy(), m.put_all(other), m.clear() |
- A missing key gives
naof the value type.nzturns that into 0, as above. - Maps remember insertion order:
m.keys()andfor (const [k, v] of m)(see Loops) go through the keys in the order they were first added. - A key that is
nastops the script (CS5105).
The full list is on the map reference page.
#Matrices
A matrix is a grid with a fixed type. Create it with a size and a starting value, then read and write cells by row and column (both from 0):
const m = matrix.new<float>(2, 3, 0.0) // 2 rows, 3 columns of 0.0
m.set(0, 2, close)
m.set(1, 0, open)
const topRight = m.get(0, 2)
const firstRow = m.row(0) // an array
plot(topRight + firstRow.size() + m.rows() + m.columns())Beyond the basics (rows, columns, row, col, add_row, add_col, remove_row, remove_col, fill, copy, submatrix, transpose, sort, reshape, concat) there is a set of linear-algebra functions for numeric matrices: mult, sum, diff, pow, det, inv, pinv, rank, trace, kron, eigenvalues, eigenvectors, plus checks such as is_square and is_symmetric. Note that m.sum(other) adds two matrices cell by cell; it is not the total of the cells.
- Operations whose sizes do not fit (multiplying a 2×3 matrix by a 2×3 matrix) stop the script with CS5106.
eigenvaluesandeigenvectorstake square symmetric matrices in this version (CS5107 otherwise).- A matrix cannot be used directly in
for … of; loop withfor (let r = 0; r < m.rows(); r++)and takem.row(r).
The full list is on the matrix reference page.
#References and copies
Collections are passed by reference. const b = a does not copy the array; both names point to the same one, and a function you pass it to can change it. When you need an independent copy, use a.copy() (m.copy() for maps and matrices).
A collection variable can also be na (for example let xs: float[] = na). Calling a method on it stops the script with CS5102.
#Limits
| Limit | Value | Error |
|---|---|---|
| elements in one array | 100,000 | CS5103 |
| entries in one map | 50,000 | CS5103 |
| cells in one matrix | 100,000 | CS5103 |
Collections also count toward the script's overall memory budget (CS5403). A state array that grows by one element per bar on a long chart is fine; one that grows by thousands per bar is not. Trim old elements with shift() once you have enough.
#Common mistakes
- Missing
state. Declared withconstorlet, the array is rebuilt empty on every bar, sosize()never grows past what one bar adds. a[i]instead ofa.get(i). Brackets are history.- A range loop
for (let i = 0 to a.size() - 1)over an empty array. It runs twice, counting down; see the empty-array trap. Writefor (let i = 0; i < a.size(); i++)orfor (const x of a). - Treating a missing map key as 0. It is
na; wrap it innz()before adding to it.