---
title: "Price canvases"
description: "Five declarations draw a canvas on the price chart from cells the chart already serves or from a frame the module writes: plot.heatmap (a time by price heatmap…"
order: 37
section: "presentation"
---

<!-- source: docs/indicators/presentation/price-canvases.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Price canvases

Five declarations draw a canvas on the price chart from cells the chart
already serves or from a frame the module writes: `plot.heatmap` (a
time by price heatmap), `plot.footprint` (one footprint column per bar),
`plot.tpo` (letter blocks per period and price), `plot.profile` (a volume
profile anchored to a session, a range, the developing session or the
visible window) and `plot.matrix` (a strike by expiry table docked on the
price axis). A `place: "side"` panel is not a canvas: on a chart it mounts
below like every panel, and its right-side home is a window ([Side
placement and windows](cards-frames-panels.md#side-placement-and-windows)).
Like `plot.levels`, each is an object literal the build reads and removes
before the file compiles; the module declares the canvas and the chart
draws it from the rows it already holds, so a footprint costs no output
and no transport of its own.

## What the canvases share

- **Names.** Every canvas has a `name`, unique across outputs, string
  slots, frames, levels, panels, renderers, drawings, heatmaps, profiles
  and matrices. A canvas is a child of the indicator: it shows and hides
  with the indicator's eye, and its legend X removes the indicator.
- **Cells.** A heatmap, footprint, TPO or profile reads a declared celled
  input through `cells`: `book` or `volume_profile` for a heatmap,
  `volume_profile` for a footprint or a profile, `volume_profile` or
  `intrabar` for a TPO (absent on a TPO means the chart's own candles).
  A `cells` word that names an input of another class is refused by name
  ("heatmap 'h' reads cells from a book or volume_profile input; 'm1' is
  intrabar").
- **Colours.** Every colour word takes `"#rrggbb"`, `"#rrggbbaa"` or a
  theme token (`theme.up`, `theme.down`, `theme.text`, `theme.muted`,
  `theme.bg`, `theme.grid`, `theme.accent`); a token follows the chart's
  theme and re-resolves on a theme switch without a rerun.
- **Type.** `font_family` is `"ui"`, `"mono"`, `"serif"` or `"rounded"`
  (system fonts only, nothing downloads); `font_weight` is `"normal"`,
  `"medium"` or `"bold"`; `font_size` is 6..64 px.
- **Numbers.** `format` is one of `price`, `%`, `si`, `int`, `0`, `0.0`,
  `0.00`, `0.000`, `usd`, `auto`; `decimals` (0..8), `signed` and `unit`
  (0..8 characters) refine it and are refused without it ("decimals needs
  format"). `price` prints at the chart's price digits.
- **Settings.** Any style key except a name or a reference (`cells`,
  `grid`, `price_low`, `price_step`, `start`, `end`, `frame`) takes
  `"@<param>"`, so a `param.color` or `param.choice` on the Style page
  repaints the canvas without a rerun of your code
  ([The Style page](../settings/style-page.md)).
- **Caps.** At most 4 heatmaps and 4 matrices per indicator; footprints,
  TPOs and profiles share one cap of 4. The numbers are on
  [Limits](../reference/limits.md).
- **Opt-in.** Every word has a default that keeps the chart's own look for
  that canvas; declare only what you want changed.

## Heatmaps

`plot.heatmap(options)` paints one coloured cell per bar and price row.
The cells come from a celled input (`cells`) or from a grid of outputs
the module writes (`grid`); exactly one of the two is declared.

```typescript
input("close", ohlcv.close);
input("depth", book.cells, { max_cells: 1000, block_size: 10 });
// The book as a heatmap behind the candles: size per level, the 98th percentile as the top of the scale.
plot.heatmap({ name: "liquidity", cells: "depth", value: "size", palette: ["theme.bg", "#2563eb", "#22d3ee", "#facc15"], opacity: 0.85, tooltip: "{{value:si}} at {{price}}" });
```

| Word | Value | What it does |
| --- | --- | --- |
| `cells` | a `book` or `volume_profile` input | the rows to paint: book levels, or profile buckets |
| `value` | book `"size"` (default), `"bid"`, `"ask"`, `"signed"`; profile `"total"` (default), `"buy"`, `"sell"`, `"delta"` | the number a cell carries; `signed` and `delta` make a diverging scale |
| `grid` | an `out.grid` name | the module's own grid instead of cells (below) |
| `price_low`, `price_step` | output names (`price_step` may be a number > 0) | with `grid`: the bottom edge of row 0 on each bar, and every row's height in price; both required, both refused beside `cells` |
| `row_height` | number > 0 | with book cells: the row height in price (default the book's own grouping); refused on other cells |
| `palette` | 2..8 colours, low to high | default `["theme.bg", "#2563eb", "#22d3ee", "#facc15"]`, or `["theme.down", "theme.bg", "theme.up"]` when `center` applies |
| `min`, `max` | numbers, `min` below `max` | the ends of the scale (default from the data, below) |
| `center` | number | the midpoint of a diverging scale; default 0 under `value: "signed"` or `"delta"`, else absent; refused outside `min..max` when both are declared |
| `auto_quantile` | 0.5..1 (default 0.98) | the quantile that sets an automatic end of the scale; refused beside both `min` and `max`; the viewer's Sensitivity row moves it |
| `scale` | `"linear"` (default), `"sqrt"`, `"log"` | how a value maps onto the palette; `"log"` is refused on a diverging scale |
| `floor` | number >= 0 (default 0) | a cell whose distance from the centre is at or under it draws nothing (so does an empty cell) |
| `opacity` | 0..1 (default 0.85) | multiplies every cell colour's own alpha |
| `behind_candles` | boolean (default true) | under the candles; `false` paints over them |
| `cell_gap` | 0..4 px (default 0) | a gap between neighbouring cells |
| `labels` | boolean (default false) | print each cell's value where the cell is tall and wide enough |
| `font_size`, `font_weight`, `font_family`, `text_color` | 6..64 (10), the weight word, the family word (`"ui"`), a colour (`theme.text`) | the label type |
| `format`, `decimals`, `signed`, `unit` | the number words (default `"auto"`) | how a label and the readout print a value |
| `tooltip` | a template of at most 200 characters | a hover readout over the cell; absent means no readout |
| `label` | 1..40 characters (default the name in words) | the readout's title |

The automatic scale is read over every cell of the full run: a
one-sided scale runs from 0 (for `size`, `bid`, `ask`, `total`, `buy` and
`sell`) or from the low quantile to the high quantile; a diverging scale
runs the same distance either side of `center`. Live ticks keep the run's
scale, so colours do not shift between runs. The viewer moves the
automatic end with the heatmap's Sensitivity row on the Style page
(Subtle to Vivid, starting at your `auto_quantile`), with no rerun
([The Style page](../settings/style-page.md#the-sensitivity-row)).

The tooltip template reads `{{value}}` and `{{value:<format>}}` (the
cell's number), `{{price}}` (the row's middle), `{{price_low}}`,
`{{price_high}}`, `{{time}}` and `{{label}}`; an unknown placeholder stays
as written. A heatmap draws beside the chart's own book or options
heatmap without touching it.

A book heatmap on a slow chart reads better in coarser rows. This one
regroups the book into rows of 25 price units, maps size through a
square-root scale so a few large resting orders do not wash out the rest,
leaves a one-pixel gap between cells and blanks the thinnest levels:

```typescript
input("close", ohlcv.close);
input("depth", book.cells, { max_cells: 1000, block_size: 10 });
plot.heatmap({ name: "depth_rows", cells: "depth", value: "size", row_height: 25, scale: "sqrt", cell_gap: 1, floor: 2, opacity: 0.9 });
```

### A grid the module computes

`out.grid(name, { rows })` declares `rows` (2..128) data-only outputs
named `<name>_0` to `<name>_<rows - 1>` and generates one writer,
`out_<name>(row: i32, value: f64)`, for `onBar()`. A row left unwritten
is an empty cell (NaN); a row outside `0..rows - 1` aborts the run by
name. The heatmap then names the grid and two price outputs: the bottom
edge of row 0 on each bar and the row height.

```typescript
param("rows", 64, { min: 2, max: 128 });
input("close", ohlcv.close);
output("low_edge", none, overlay, { description: "The bottom of row 0: 32 rows under the close" });
output("step", none, overlay, { description: "The row height in price" });
out.grid("heat", { rows: 64 });
plot.heatmap({ name: "liq", grid: "heat", price_low: "low_edge", price_step: "step", center: 0, floor: 0.5 });

function onBar(): void {
  const step = bar.close() * 0.0025;
  out_step(step);
  out_low_edge(bar.close() - 32.0 * step);
  // Row 32 sits on the close; rows above it read positive, rows below negative, fading with distance.
  for (let row = 0; row < 64; row++) out_heat(row, f64(row - 32) * bar.volume() / 32.0);
}
```

The grid's outputs count toward the 256 outputs a file may declare, and
the rows must be contiguous in declaration order (the build lays them out
for you). The [Liquidation Heat](../cookbook/liquidation-heat.md) recipe
is a complete grid heatmap, and
[OI Liquidation Heat](../cookbook/liquidation-heat-oi.md) builds the same
grid from open interest; [Book Heat](../cookbook/book-heat.md) paints the
order book.

## Footprints

`plot.footprint(options)` draws one footprint column per chart bar from
the bar's `[low, high, buy, sell]` buckets: the module only declares it.

```typescript
input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 8192 });
plot.footprint({ name: "fp", cells: "profile", mode: "split_bar", imbalance: true, imbalance_ratio: 3, stacked_imbalances: 3 });
```

| Word | Value | What it does |
| --- | --- | --- |
| `cells` | a `volume_profile` input | required; a book input is refused by name |
| `mode` | `"cluster"`, `"split_bar"` (default), `"stacked_bar"`, `"ladder"`, `"imbalance_only"` | the column's drawing |
| `buy_color`, `sell_color` | colours | default the chart's profile pair |
| `row_height` | number > 0 or `"auto"` (default) | the row height in price; `"auto"` is the input's bucket width times the footprint's row table ([Row height](#row-height)) |
| `labels` | boolean (default true) | print the cell numbers |
| `hide_zero` | boolean (default false) | leave zero cells blank |
| `text_color` | colour | the cell ink (default the chart's footprint ink) |
| `imbalance`, `imbalance_ratio`, `imbalance_buy_color`, `imbalance_sell_color`, `stacked_imbalances` | boolean (false), 1.1..20 (3), colours (`theme.up`, `theme.down`), 0..10 (3) | mark buy and sell imbalances at the ratio, and stacks of them |
| `poc`, `poc_color`, `poc_width` | boolean (true), colour (`theme.text`), 1..10 px (1) | the point of control per column |
| `value_area`, `vah`, `val`, `vah_color`, `val_color` | 0.5..0.95 (0.7), booleans (true), colours (`theme.text`) | the value area and its edges |
| `cell_metric` | `"volume"`, `"delta"` (default), `"trades"` | what a cluster cell reads |
| `volume_color` | colour (default `"#4a90e2"`) | the volume cell colour |
| `grading` | `"candle"` (default), `"session"`, `"visible"` | the range the cell shading is scaled over |
| `font_family`, `font_weight` | the family and weight words (default the chart's indicator font) | the type |
| `cell_hover` | boolean (default true) | the chart's cell readout on hover |

A cluster footprint reads volume per row rather than delta. This one
shades every cell against the session's largest cell instead of its own
candle's, in one teal, leaves zero cells blank in rows of 10 price units,
and turns the chart's hover readout off so the numbers printed in the
cells are the only readout:

```typescript
input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 8192 });
plot.footprint({ name: "clusters", cells: "profile", mode: "cluster", cell_metric: "volume", volume_color: "#2dd4bf", grading: "session", hide_zero: true, row_height: 10, cell_hover: false });
```

[Volume Footprint](../cookbook/volume-footprint.md) is the complete
recipe.

## TPO letters

`plot.tpo(options)` prints a letter for every letter interval of the
period in which a price row traded (profile buckets) or lay inside the
bar's range (finer bars, or the chart's own candles when `cells` is
absent).

```typescript
input("close", ohlcv.close);
input("m5", intrabar.cells, { interval: "5m", max_cells: 288 });
plot.tpo({ name: "tpo", cells: "m5", period: "day", letter_minutes: 30, initial_balance: true, single_prints: true });
```

| Word | Value | What it does |
| --- | --- | --- |
| `cells` | a `volume_profile` or `intrabar` input, or absent | the bars or buckets the letters are built from |
| `period` | `"day"` (default), `"week"`, `"month"` | one profile per period |
| `letter_minutes` | 1..240 (default 30) | the minutes one letter stands for |
| `row_height` | number > 0 or `"auto"` (default) | the row height in price; `"auto"` comes from the market, and a period past 512 rows is coarsened ([Row height](#row-height)) |
| `display` | `"letters"`, `"blocks"`, `"both"` (default) | letters, blocks, or both |
| `palette` | 2..26 colours | the colours across the letters (default the chart's TPO scheme) |
| `color_mode` | `"period"` (default), `"count"`, `"volume"`, `"delta"` | what colours a block; `"volume"` and `"delta"` need `volume_profile` cells |
| `value_area`, `poc`, `vah`, `val`, `poc_color`, `vah_color`, `val_color` | 0.5..0.95 (0.7), booleans (true), colours (`theme.text`, `theme.muted`, `theme.muted`) | the value area and its edges |
| `outside_va_opacity` | 0..1 (default 0.3) | how far rows outside the value area fade |
| `initial_balance`, `ib_color` | boolean (true), colour (`theme.text`) | the initial balance bracket |
| `single_prints`, `single_prints_color`, `poor_extremes`, `poor_extremes_color` | booleans (false), colours (`"#cc0033"`, `"#ffd966"`) | single prints and poor highs and lows |
| `counts` | boolean (default false) | the TPO count per row |
| `volume_profile` | boolean (default false) | a volume profile beside the letters; needs `volume_profile` cells |
| `font_family`, `font_weight`, `cell_hover` | as a footprint | the type and the hover readout |

A day profile coloured by what each row traded, with the structure marks
a profile reader looks for: poor highs and lows in amber, single prints
in red, the TPO count printed on every row and a volume profile beside
the letters:

```typescript
input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 8192 });
plot.tpo({ name: "day_tpo", cells: "profile", period: "day", color_mode: "volume", poor_extremes: true, poor_extremes_color: "#f59e0b", single_prints: true, single_prints_color: "#ef4444", counts: true, volume_profile: true });
```

A letter must be a whole multiple of the feeding bars' interval: 30-minute
letters over 1h candles cannot be placed, and the legend chip and the
Console say so ("tpo 't': 30-minute letters need bars of 30 minutes or a
whole divisor; the bars here are 1h (declare an intrabar input with
interval "5m" and name it in cells)"). [TPO Letters](../cookbook/tpo-letters.md)
is the complete recipe.

## Profiles anchored in time

`plot.profile(options)` draws a volume profile over a span of time from
`volume_profile` cells: one per session, one over a range two outputs
mark, the developing session, or the visible window.

```typescript
input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 8192 });
plot.profile({ name: "session_vp", cells: "profile", span: "session", session: "1d", value_area_shade: true, background: true });
```

| Word | Value | What it does |
| --- | --- | --- |
| `cells` | a `volume_profile` input | required |
| `span` | `"session"` (default), `"range"`, `"developing"`, `"visible"` | what one profile covers |
| `session` | `"auto"` (default), `"1h"`, `"2h"`, `"4h"`, `"6h"`, `"12h"`, `"1d"`, `"1w"`, `"us"`, `"eu"`, `"asia"` | the session length under `"session"` and `"developing"`; refused on `"range"` and `"visible"`; `"auto"` follows the chart interval |
| `start`, `end` | output names, epoch seconds read on the last ready row | the range under `span: "range"` (`start` required there, `end` defaults to the newest bar; both refused elsewhere); a run whose start is not a number, or whose end is not after it, mounts no profile and the legend chip says why |
| `mode` | `"stacked_bar"` (default), `"split_bar"`, `"outline"`, `"delta"` | the bars' drawing |
| `buy_color`, `sell_color` | colours | default the chart's profile pair |
| `dock` | `"left"`, `"right"` | which edge of the span the bars grow from (default left, right on `"visible"`) |
| `width_frac` | 0.05..1 | the bars' share of the span (default 0.7), or of the pane under `"developing"` and `"visible"` (default 0.2) |
| `row_height` | number > 0 or `"auto"` (default) | the row height in price; `"auto"` is the input's bucket width times the profile's row table ([Row height](#row-height)) |
| `value_area`, `value_area_shade`, `outside_buy_color`, `outside_sell_color` | 0.5..0.95 (0.7), boolean (true), colours (the base colours at half strength) | the value area and how rows outside it paint |
| `poc`, `poc_color`, `poc_width`, `vah`, `val`, `vah_color`, `val_color` | booleans (true), colours (`theme.text`), 1..10 px (1) | the point of control and the value area edges |
| `level_labels` | boolean (default false) | price tags on the three levels |
| `naked` | boolean (default false) | keep a level drawn until price trades through it |
| `extend` | `"none"`, `"right"` | extend the levels to the right (default right on `"developing"`, none otherwise) |
| `behind_candles` | boolean (default false) | under the candles; needs `span: "session"` and a filled mode |
| `background`, `background_color`, `background_opacity` | boolean (false), colour (`theme.text`), 0..1 (0.08) | a box behind each session; needs `span: "session"` |
| `font_family`, `font_weight`, `cell_hover` | as a footprint | the type and the hover readout |

[Session Volume Profile](../cookbook/session-volume-profile.md) is the
complete recipe. A profile the module computes itself (open interest by
strike, a liquidation map) is a `plot.levels` whose frame carries spans:

```typescript
const oiFrame = frame("oi_spans", { max_bytes: 32768 });
plot.levels({ name: "oi", frame: oiFrame, dock: "left", span: "time", width_frac: 0.4 });
```

With `span: "time"` the frame is `{ "spans": [...] }`, one entry per
profile, each drawn inside its own time span (with `span: "pane"`, the
default, the frame keeps the `{ prices, values, colors? }` shape docked on
the pane edge):

```json
{
  "spans": [
    {
      "start": 1727740800,
      "end": 1727827200,
      "prices": [61000, 61500, 62000],
      "values": [120.5, 310.2, 88.0]
    },
    {
      "start": 1727827200,
      "end": 1727913600,
      "prices": [61500, 62000, 62500],
      "values": [90.0, null, 140.0],
      "colors": ["theme.up", "theme.muted", "theme.up"]
    }
  ]
}
```

A frame holds at most 64 spans, each `start` before its `end` in epoch
seconds, 1..512 strictly ordered prices with as many values (null for a
gap) and optional colours, and at most 4096 rows across every span. A
frame whose shape does not match the declared `span` refuses the run with
`wrun_frame_invalid`. Every other `plot.levels` word applies
([Docked profiles](cards-frames-panels.md#docked-profiles));
[Session OI Levels](../cookbook/session-oi-levels.md) is the recipe.

## Row height

A footprint, a TPO and a profile draw one row per band of price,
`row_height` tall. A number is used as written. `"auto"`, the default,
takes its unit from the first of these the chart knows for the market,
never from the decimals the closes print:

1. **The bucket width** of the `volume_profile` input named in `cells`. A
   footprint and a profile always read one, so their auto stops here.
2. **The block size the chart serves** for the market (its TPO catalog,
   else its volume profile catalog), or, with no catalog, the venue's
   instrument tick when the chart holds one (CME futures).
3. **The median candle range** divided by 4, snapped up to 1, 2 or 5 x
   10^k.
4. **Otherwise 1.**

Rungs 1 and 2 are multiplied by the chart's row table, the figures the
chart's own footprint, TPO and session profile use; the range unit (rung
3) already scales with the interval, so it is used as is.

| Chart interval | 1m | 5m | 15m | 30m | 1h | 4h | 1d | 1w |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Footprint | 2 | 5 | 8 | 10 | 15 | 25 | 50 | 250 |
| TPO | 5 | 10 | 15 | 20 | 25 | 30 | 50 | 100 |
| Profile | 5 | 10 | 15 | 20 | 25 | 50 | 100 | 200 |

A week TPO multiplies its figure by 1.5 and a month TPO by 2, rounded
down (22 and 30 at 15m).

### 512 rows per period

512 rows is the most a TPO period (a day, a week or a month) holds before
the chart steps in. Past that it doubles the row height and folds again
until every period fits, at most 6 times, whether the height came from
`"auto"` or from a number.
The TPO still draws, and every run says so with a warning row in the
editor's Console and the warning on the indicator's legend chip, naming
the declaration and both heights:

- `tpo 'tpo': row_height auto coarsened from 1.5 to 12 (512 rows per day is the chart's limit)`
- `tpo 'tpo': row_height 1 coarsened to 8 (512 rows per day is the chart's limit)`

The cap depends on how far the market's price moves, so only the running
chart applies it; the build never refuses a row height for it. A number so
fine that one bar alone spans more than 4,096 rows is still refused by
name, and rows thinner than 3 px on screen merge as you zoom out
([Limits](../reference/limits.md)).

### BTC at 15m

[TPO Letters](../cookbook/tpo-letters.md) declares no `row_height` and no
`cells`. On Binance's BTCUSDC perpetual at 15m the chart serves a 5-dollar
TPO block and the TPO figure at 15m is 15, so a row is 5 x 15 = 75
dollars, the row the chart's own TPO draws there. A day holds about 25 to
90 rows, each tall enough for its letters (a letter needs a row about 8 px
tall).

The price tick times the same figure would make 0.1 x 15 = 1.5-dollar
rows: every row under a pixel at the default zoom, no letter printed, and
48 times the rows to fold and paint (22,946 over 16 days against 476).
That is why `"auto"` reads what the venue serves and never the decimals
the closes print.

## Strike matrices

`plot.matrix(options)` docks a table of numbers on the price axis: one
row per price, one column per expiry (or whatever the columns are), each
cell read from a frame the module writes on the live bar. It needs
`abi_version: "wrun-4"`, which a frame declaration stamps for you.

```typescript
const board = frame("board", { max_bytes: 65536 });
plot.matrix({ name: "gex", frame: board, dock: "right", column_width: 56, price_column: true, palette: ["theme.down", "theme.bg", "theme.up"], center: 0, format: "si", tooltip: "{{column}} {{value:si}} at {{price}}" });
```

| Word | Value | What it does |
| --- | --- | --- |
| `frame` | a declared frame | the snapshot below; required |
| `dock` | `"right"` (default), `"left"`, `"side"` | against the price axis, or in the strip beside the chart |
| `columns` | 1..12 names of 1..16 characters | the column labels (default the frame's `cols`, else `"1"`, `"2"`, ...); a frame with another column count refuses the run |
| `column_width` | 24..160 px (default 56) | one column's width |
| `row_max_px` | 8..64 px (default 24) | the tallest a row may grow |
| `price_column`, `header` | booleans (false, true) | a price column on the axis side; the sticky header row |
| `palette`, `min`, `max`, `center`, `auto_quantile`, `scale` | as a heatmap (no default palette) | the fill behind a numeric cell; without a palette a cell has no fill unless it carries its own colour |
| `opacity` | 0..1 (default 1) | multiplies every cell fill |
| `format`, `decimals`, `signed`, `unit` | the number words (default `"auto"`) | how a numeric cell prints when it carries no text |
| `font_size`, `font_weight`, `font_family`, `align`, `text_color` | 6..64 (10), the weight word, the family word (`"ui"`), `"left"`, `"center"`, `"right"` (`"right"`), a colour (`theme.text`) | the cell type |
| `header_text_color`, `header_background_color` | colours (`theme.muted`, `theme.bg`) | the header row |
| `background_color`, `background_opacity` | colour (`theme.bg`), 0..1 (0.85) | the backdrop behind the table |
| `grid_color`, `grid_width`, `grid_style`, `grid_lines` | colour (`theme.grid`), 0..10 (1), `"solid"`, `"dashed"`, `"dotted"`, `"all"`, `"rows"`, `"cols"`, `"none"` | the lines between cells |
| `border_color`, `border_width`, `border_style` | colour (`theme.grid`), 0..10 (0), the line style | the frame around the table |
| `cell_padding` | 0..12 px (default 4) | the inset inside a cell |
| `highlight_color` | colour (default `theme.accent`) | the outline of the frame's highlighted row and column |
| `tooltip`, `label` | a template of at most 200 characters; 1..40 characters | the hover readout and its title; the template reads `{{value}}`, `{{value:<format>}}`, `{{text}}`, `{{price}}`, `{{column}}` and `{{label}}` |

A matrix can take chrome of its own. This board docks on the left,
prints its header row in the accent colour on a dark fill, frames the
table with a dashed border and draws lines between rows only:

```typescript
const board = frame("board", { max_bytes: 65536 });
plot.matrix({ name: "oi_board", frame: board, dock: "left", header_text_color: "theme.accent", header_background_color: "#0f172a", border_color: "theme.grid", border_width: 1, border_style: "dashed", grid_lines: "rows" });
```

The frame is a strict object:

```json
{
  "prices": [60000, 62000, 64000, 66000],
  "cells": [
    [1.2e6, -4.1e5, null],
    [2.5e6, 8.0e5, "n/a"],
    [
      { "value": -3.3e6, "text_color": "theme.down", "font_weight": "bold" },
      1.1e6,
      2.0e5
    ],
    [4.0e5, 1.5e5, 9.0e4]
  ],
  "cols": ["27JUN", "25JUL", "26SEP"],
  "title": "GEX by expiry",
  "highlight": { "price": 64000, "col": 0 },
  "range": { "min": -5e6, "max": 5e6 }
}
```

`prices` holds 1..128 numbers in strictly rising or falling order;
`cells` holds exactly one row per price, every row the same width (1..12
columns, at most 1536 cells in all); `cols` names the columns (1..16
characters each); `title` (1..40 characters) prints above the table;
`highlight` marks a price row and a column index (0-based); `range` moves
the palette bounds per run. A cell is `null` (empty), a number (printed
through `format`), a string of up to 16 characters, or an object with any
of `value`, `text`, `color` (the fill), `text_color` and `font_weight`. A
frame that breaks any of this refuses the run with
`wrun_frame_invalid: <frame>.<path>`, never a truncated table.
[Strike Matrix](../cookbook/strike-matrix.md) builds the board from the
live options chain ([Options kit](../functions/options-kit.md)).

## The strip beside the chart

A matrix joins a strip to the right of the price axis with `dock:
"side"`, its rows still on the price pane's prices. The strip takes at
most 40% of the chart's width and folds away on a chart narrower than
480 px, where its items are not drawn. Matrices sit nearest the axis, one
column each; a column the strip cannot fit at its smallest size (24 px) is
not drawn. The strip has no pointer interaction yet: no hover cards there,
and no drag to resize it.

Panels do not use the strip: a `place: "side"` panel mounts below the
chart like every panel, and the chart offers it a window of its own
([Side placement and windows](cards-frames-panels.md#side-placement-and-windows)).
Its `width_px` (160..480) and `height_px` (80..800) stay accepted with
`place: "side"` only, reserved for a strip that panels do not use today.

## What refuses

Every misuse is refused by name when Run derives the sheet: a fifth
heatmap ("heatmaps must declare at most 4 entries"), `cells` beside
`grid`, a `value` word of the other cell class, `out.grid` rows outside
2..128 ("out.grid 'heat' rows must be between 2 and 128"), a grid heatmap
without `price_step`, a footprint over book cells, a TPO `color_mode` of
`"volume"` without profile cells, `letter_minutes` of 0, `span: "range"`
without `start`, `behind_candles` on a visible-window profile, a matrix
under `wrun-3` ("matrices needs abi_version "wrun-4" (wrun-3 has no frame
channel)"), 13 columns, `width_px` on a panel placed below. A frame that
fails its shape refuses the run instead, as `wrun_frame_invalid`. The caps
and ranges are on [Limits](../reference/limits.md).
