---
title: "Plotting"
description: "A wrun indicator draws by declaring, not by calling: it declares output(name, plot, panel, options) once at the top of the file and writes one number per bar…"
order: 33
section: "presentation"
---

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

# Plotting

A wrun indicator draws by declaring, not by calling: it declares
`output(name, plot, panel, options)` once at the top of the file and
writes one number per bar, and the chart draws that number as a line, a
column, a mark, or a tint from the declaration. That is why the module
never touches a color, and why every drawn output is a value an alert
can follow once the indicator is published ([Alerts](../functions/alerts.md)). This
page maps everything you might want to draw to its wrun form, and
compiles the forms that exist.

## The map

| You want | wrun form | Notes |
| --- | --- | --- |
| a plotted series | `output(name, line \| bar \| scatter \| candle, panel)` | the plot kind is the declaration, not a runtime argument |
| a line | `output(name, line, panel, { color, width, line_style, opacity, glow, step, smooth, gradient, split })` | a filled line is `area`; `glow` is a soft halo in pixels ([Styling](styling.md)); `smooth` bends it into curves, `step` draws it as horizontal and vertical segments, `gradient` shades the stroke by height, `split` colours it above and below a level or by slope |
| columns | `output(name, bar, panel, { width, base, grading, stack })` | one value per bar; `width` is a share of the bar slot (1 = columns touch), `base` the level they grow from, `grading` fades them by magnitude, `stack` stacks them on their siblings |
| a histogram | `output(name, histogram, panel, { color_by, colors, corner })` | columns grow from zero (or from `base`); a sign ladder colors them; `corner` rounds them, in pixels |
| candles | four consecutive `candle` outputs (open, high, low, close), or a box and a segment per bar | the chart draws the four as one candle; `candle_style` on the first output draws them hollow, as OHLC bars or as high-low bars; see below |
| a mark on some bars | `output(name, shape, panel, { shape, shape_where, location, char, fill, fill_opacity, glow })` or `render.shape` | either picks the mark from ten shapes (`char` draws one character); `location` sits it above or below the bar or at the pane's edge |
| several marks or a pie per bar | none: one thing per bar per renderer | a one-summary pie is a `panel.pie` snapshot; a per-level picture is a `plot.levels` frame ([Cards, frames and panels](cards-frames-panels.md)) |
| text at a price | `render.text(name, { y, text, color, size, style?, align?, valign?, font_weight?, font_family? })` | one mark per bar whose slot was written; a `style` other than plain text draws each as a tag (`price_label`, `pill`, `callout`, `badge`, `box`, `knockout`, `emblem`) |
| a price tag | `render.label(name, { x, y, text, style: "price_label" })` for one tag at the newest bar, or `render.text` with the same `style` for a tag per bar | a label's `style` is also `plain`, `pill`, `callout`, `badge`, `box`, `knockout` or `emblem` |
| a corner readout | `render.label(name, { position, text, offset? })` pinned to one of the nine anchors, a one-cell `render.table(name, { rows: 1, cols: 1, cells, position })`, or a `render.hud` card with one tile | fixed position at one of nine anchors; the newest bar that wrote the slot wins |
| an output on its own scale over the candles | `pane(name, { place: "price" })` and `output(name, plot, overlay, { pane: name })` | open interest or a cumulative delta drawn over price on a hidden scale of its own ([Panes](#panes)) |
| several panes below the chart | `pane(name, { title, height_frac, scale, min, max, format })` and `output(name, plot, lower, { pane: name })` | up to four named panes, stacked in declaration order ([Panes](#panes)) |
| a table | `render.table(name, { rows, cols, cells, position })` for a grid of words, styled by the same literal (widths, fills, lines, per-cell colours that follow an output; [Styled tables](cards-frames-panels.md#styled-tables)); `render.hud(name, { position, tiles })` for a card of typed tiles | cells are string slots; tiles read outputs and slots |
| a rectangle | `box(...)` per bar, `draw.box` for one object, or a box handle | [Drawing objects](drawing-objects.md) |
| a grid of mini charts pinned to the viewport | none | a dashboard is a `render.table` or a `render.hud` card; a snapshot panel below the chart is a `panel.*` frame ([Cards, frames and panels](cards-frames-panels.md)) |
| a statistics-strip row | `render.stats_row(name, { output, title, format, polarity })` | a strip row over an output |
| a heatmap, a curve over a category or an index, tiles | `panel.heatmap`, `panel.line` over a category or index x, `panel.tiles` | a frame snapshot in its own pane below the chart ([Cards, frames and panels](cards-frames-panels.md)) |
| a horizontal level | `output(name, line, panel, { role: "guide", align?, pill_style?, font_size?, axis_label? })` written to the constant every bar | one full-width line at the output's last value, out of the legend; `align` puts its label on the line, `axis_label: true` on the axis; or a segment `from: 0, to: 1` |
| a background tint | `render.bgcolor(name, { where, color, color_by, colors, width?, line_style? })` | a tint per bar where the gate is nonzero; with `width` a vertical line per gated bar instead of a band |
| colored candles | `render.barcolor(name, { where, color, color_by, colors })` | the bar's own candle tinted where the gate is nonzero |
| a fill between two lines | `range("a", "b", { color })` between two drawn outputs for a band with edges, `fill("a", "b", { color, opacity })` for the interior only, or a `box` per bar | the chart draws the range as a band with its two edges and the fill as shading with none ([Styling](styling.md)) |
| a name and a description | the output `name` and `description` | names are unique per family by construction |

## Outputs

`output(name, plot, panel, options)` returns a handle a `box` or
`segment` can name; a bare statement discards it.

- `plot`: `line`, `bar`, `area`, `histogram`, `candle`, `shape`,
  `scatter`, or `none` for a data-only output (computed, never drawn: the
  building block for gates, palettes, and shape coordinates).
- `panel`: `overlay` (the price pane) or `lower` (its own pane). Put
  small-magnitude series (oscillators, percentages, counts) in `lower`; a
  z-score on the price axis hugs the axis floor.
- Static style: `color`, `width`, `opacity` (0..1), `line_style`
  (`"solid"`, `"dashed"`, `"dotted"`), `glow` (a halo in pixels on a
  line, an area, columns, candles or marks), `z` (the paint order inside
  the pane, an integer -10..10), `description` (documents the output). A
  colour is `"#rrggbb"`, `"#rrggbbaa"` or a theme token such as
  `"theme.up"` ([Styling](styling.md#theme-colours)).
- Numbers: `format` (`price`, `%`, `si`, `int`, `0`, `0.0`, `0.00`,
  `0.000`, `usd`, `auto`), `decimals` (0..8), `signed` and `unit` (printed
  after the value; unbounded on an output, as it always was) shape the
  legend, the hover card and the axis tag; `decimals` and `signed` need
  `format`, and a `unit` prints only beside one
  ([Styling](styling.md#placement-per-output)).
- Per-bar color: `color_by: "<data-only output>"` plus `colors` (at least
  two entries). Each bar's value indexes the palette (floored); a finite
  value outside the palette takes entry 0, and a non-finite one draws the
  static color. `color_packed_by: "<output>"` reads a packed colour the
  module computes per bar instead (never beside `color_by`). A `shape`
  output takes either ladder too: each mark in its bar's colour.
- Per-bar width: `width_by` plus `widths` (1..10 entries, each 0.5..20),
  the same ladder for line width. On `bar` and `histogram` outputs `width`
  and `widths` are a share of the bar slot (`widths` entries from 0.05): 1
  means the columns touch, 0.5 half the slot, 2 overlap.
- Gated marks: `plot: shape` with `shape_where: "<gate output>"` draws a
  mark at the output's value only on bars where the gate is nonzero.
- Axis tags: `axis_label: true` tags this output's last value on the
  price axis (absent, only the first mounted output is tagged);
  `axis_name: true` adds the indicator's name to that tag (plus the
  output's label when several outputs are tagged); `price_line: true`
  draws a dotted line across the pane at the last value. The three are
  refused on a data-only output.
- `pane: "<name>"` puts the output in a named pane, declared with
  `pane(...)` below; absent, a `lower` output lands in the default pane.
- `displacement_bars` (an integer in -500..500) makes the chart draw the
  series shifted left or right without changing its values; past the
  loaded edge it extrapolates at the bar spacing.
  `displacement_bars_by: { param: "<name>", scale?: 1, offset?: 0 }` reads
  the shift from a setting instead: `round(scale * value + offset)` bars,
  clamped to -500..500, which wins over `displacement_bars` while the
  setting is a finite number.

An output cannot color, widen, or gate itself: the palette index and the
gate must be different outputs. The rest of the vocabulary is on the
[Styling](styling.md) page. The legend shows each output's `label`, or
its name read as words when there is none (`bb_upper` reads "Bb upper"),
and its value in the output's `format`; `legend: false` keeps an output
out of the legend, and `legend({ title: "..." })` adds words after the
indicator's name in the legend ([Legend](legend.md)).

## Panes

`pane(name, options)` declares a pane of the indicator's own, at most
four per indicator, and `pane: "<name>"` on an output puts the output in
it. The name `"lower"` styles the default pane; any other name is a new
pane below the chart, stacked in declaration order, the default pane
first unless it is declared later. These time-series panes sit at the
bottom of the indicator's stack, next to the time axis; the indicator's
panels (`panel.line`, `panel.bars` and the rest, on
[Cards, frames and panels](cards-frames-panels.md#panel-words)) stack
above them. A declared pane with nothing drawn in it is refused, and so
is a pane beside a declared overlay, with one exception: beside
`display({ overlay: "offchart" })` the indicator lives in the default
pane, so `pane("lower", { format: "0.00" })` formats that pane's axis
(`format`, `decimals`, `signed` and `unit` only; any other pane or word
is refused). The options, every one optional:

```typescript
pane("rsi", { title: "RSI {{period}}", height_frac: 0.2, min: 0, max: 100 });
pane("oi", { place: "price", scale: "log", format: "si" });
output("rsi", line, lower, { pane: "rsi", color: "#7c3aed" });
output("oi", line, overlay, { pane: "oi", color: "#f59e0b" });
```

- `title` (1..40 characters, `{{param}}` reads a setting): the pane's
  legend title. An indicator that homes below the chart (no drawn output
  on the price pane) keeps its own name on the pane holding its first
  drawn output, so that pane takes no title.
- `place`: `"below"` (the default) or `"price"`. A pane placed on price
  draws its outputs over the candles on a hidden scale of their own
  (open interest or a cumulative delta on price); its outputs declare
  `overlay`, a pane placed below takes `lower` outputs, and the pair must
  match.
- `height_frac` (0.05..0.6): the pane's share of the chart height,
  refused on a pane placed on price. Absent, a pane below an on-price
  indicator takes 0.22 and an indicator that homes below keeps the
  chart's default pane size.
- `scale`: `"linear"` (the default), `"log"`, `"percent"` or
  `"indexed"`; `invert: true` flips the axis; `padding: [top, bottom]`
  (each 0..0.4) sets the autoscale margins; `min` and `max` hold either
  end of the autoscale (RSI: `min: 0, max: 100`, `min` below `max`; a log
  scale refuses `min` at or below 0). Dragging the axis by hand still
  works.
- `format`, `decimals`, `signed`, `unit`: the pane's axis ticks, the
  same four words an output takes (`decimals`, `signed` and `unit` need
  `format`). Absent, a lower pane prints K, M and B from 1,000 and its own
  precision below that.

An inverted pane turns a loss into something that hangs: write a drawdown
as a positive percent and flip the axis, so zero sits at the top of the
pane and the deepest loss reaches lowest, with the running peak drawn
over price above it. The pane keeps a title because the indicator homes
on the price pane.

```typescript
output("peak", line, overlay, { color: "theme.muted", line_style: "dotted" });
pane("dd", { title: "Drawdown", height_frac: 0.18, invert: true, min: 0, padding: [0.02, 0.1], format: "%", decimals: 1 });
output("drawdown", area, lower, { pane: "dd", color: "#ef4444", fill_color: "#ef4444", fill_opacity: 0.25 });
```

Ranges, fills, boxes, segments and renderers follow the pane of the
output they anchor to, and a card, feed or meter with `panel: "lower"`
sits in the indicator's lower pane too.

## Lines, areas, columns, dots, marks

A line is a `line` output; a filled line is `area`. Columns are `bar`,
and a `histogram` is columns grown from zero, colored per bar through a
`color_by` ladder over a sign output (`value >= 0 ? 1 : 0`); `base`
grows the columns from another level instead (`colors[0]` at or above
it, `colors[1]` below). Dots are `scatter`. A mark on some bars is a
`shape` output plus a `shape_where` gate: write the price every bar and
let the gate decide which bars draw. A level such as 70 is a guide: a
`line` output with `role: "guide"` written to `70` on every bar, drawn
as one full-width line at its last finite value, kept out of the legend
and inside the pane's autoscale (an alert can still follow it). `align`
(`"left"`, `"center"`, `"right"`) puts the output's label on the line,
`pill_style` (`"filled"`, the default, or `"outlined"`) and `font_size`
(6..64) dress that label, and `axis_label: true` puts the value and the
label on the axis instead (never both). A guide takes no `color_by`,
`color_packed_by`, `width_by` or `price_line`.

The looks a line, an area, a column and a mark take beyond colour and
width (the words are opt-in; absent, the output draws as it always has):

| Plot | Words | What they draw |
| --- | --- | --- |
| `line` | `step: true` | horizontal then vertical segments (trailing stops, funding steps); refused beside `smooth` |
| `line`, `area` | `smooth: true` | the line (and an area's edge) bent into curves |
| `line` | `gradient: [...]` | the stroke shaded by height, 2 to 8 colours listed from the top of the pane to the bottom; never beside `color_by`, `color_packed_by` or `split` |
| `line` | `split: "level" \| "slope"`, `up_color`, `down_color`, `split_level?`, `split_fill?`, `split_base?` | `"level"`: `up_color` above `split_level` (default 0), `down_color` below, and `split_fill: true` shades down to `split_base` (default `split_level`); `"slope"`: `up_color` while rising, `down_color` while falling; both colours are required with `split` |
| `area` | `fill_color`, `fill_opacity`, `fill_gradient: [...]`, `gradient_mode` | the fill's own colour (default the line colour) at `fill_opacity` (0.4 on a flat fill, 1 on gradient stops), or 2 to 8 stops top to bottom laid over `"pane"` (the default), `"fill"` or `"line"`; `fill_color` and `fill_gradient` exclude each other |
| `area` | `fill_color_by` + `fill_colors`, or `fill_color_packed_by` | the fill coloured per bar by its own ladder (name the line's `color_by` output to follow the line); `fill_opacity` and `opacity` multiply each colour |
| `bar`, `histogram` | `width`, `base`, `grading: "linear" \| "square"`, `stack: "<name>"` | `width` as a share of the bar slot; `base` the level the columns grow from; `grading` fades each column by its magnitude within the visible range; bars sharing a `stack` name stack in declaration order, positives above 0 and negatives below (never beside `base`, and every member on one pane) |
| `shape`, `scatter` | `shape`, `location`, `char`, `font_family`, `fill`, `fill_opacity` | `shape` picks the mark (`circle`, the default, `cross`, `triangle_up`, `triangle_down`, `diamond`, `arrow_up`, `arrow_down`, `flag`, `square`, `char`); `location` is `"absolute"` (the default), `"above_bar"`, `"below_bar"`, `"top"` or `"bottom"`; `shape: "char"` with `char` (one character) draws that character in `font_family` (`ui`, `mono`, `serif`, `rounded`); `fill: false` draws the outline only and `fill_opacity` fades the interior, neither on a `char` mark |
| every drawn plot | `glow`, `opacity`, `z` | a halo in pixels; the opacity, which now fades every colour the output draws (ladders, packed colours, a column's sign pair, a candle's colours, marks, gradient stops); the paint order inside the pane |

`triangle_down` points down (apex at the bottom), `triangle_up` up.

```typescript sample=fn-plot-kinds
param("fast", 9, { min: 1, max: 200 });
param("slow", 21, { min: 2, max: 400 });
output("fast", line, overlay, { color: "#2563eb", width: 2, description: "Fast average, a line" });
output("slow", area, overlay, { color: "#94a3b8", opacity: 0.3, description: "Slow average, drawn as an area" });
output("cross_mark", shape, overlay, { color: "#16a34a", shape_where: "crossed", description: "A mark on the low of each bullish-cross bar" });
output("crossed", none, overlay, { description: "1 on the bar the fast average crosses above the slow: the mark's gate" });
output("volume", bar, lower, { color: "#4ecdc4", description: "Volume as bars" });
output("delta", histogram, lower, { color_by: "delta_sign", colors: ["#ef5350", "#26a69a"], description: "Fast minus slow as columns, colored by sign" });
output("delta_sign", none, lower, { description: "0 negative, 1 positive: the histogram's palette index" });
output("spread", scatter, lower, { color: "#f97316", description: "Close minus fast, as dots" });
output("rsi", line, lower, { color: "#7c3aed", width: 2, description: "RSI" });
output("overbought", line, lower, { color: "#ff0000", width: 1, line_style: "dashed", description: "The 70 level, written on every bar" });

let fast = new Sma(9);
let slow = new Sma(21);
let rsi = new Rsi(14);
const cross = new Cross();

function onStart(): void {
  fast = new Sma(i32(p_fast()));
  slow = new Sma(i32(p_slow()));
  rsi = new Rsi(14);
}

function onBar(): void {
  const close = bar.close();
  const low = bar.low();
  const volume = bar.volume();
  const fastValue = fast.update(close);
  const slowValue = slow.update(close);
  const rsiValue = rsi.update(close);
  const crossed = cross.update(fastValue, slowValue);
  if (isNaN(slowValue)) return;
  const delta = fastValue - slowValue;
  out_fast(fastValue);
  out_slow(slowValue);
  out_cross_mark(low);
  out_crossed(crossed == 1 ? 1.0 : 0.0);
  out_volume(volume);
  out_delta(delta);
  out_delta_sign(delta >= 0.0 ? 1.0 : 0.0);
  out_spread(close - fastValue);
  out_rsi(rsiValue);
  out_overbought(70.0);
}
```

`cross_mark` is written on every bar (the bar's low) and drawn only where
`crossed` is `1`. The gate is itself an output, so a declared `alert` can
fire on it ([Alerts](../functions/alerts.md)). A two-line fill colored by sign (a
momentum fill) is this `delta` histogram, or a `range()` between the two
lines with a two-entry `colors` sign palette ([Styling](styling.md)).

The other looks in the table, one coherent look per declaration. An
area's fill can fade: list the stops top to bottom and lay them over the
fill's own extent, so the colour is strongest at the line and gone at the
floor; `axis_name` puts the indicator's name on the axis tag beside the
value.

```typescript
// Cumulative delta as an area: amber at the line, nothing at the pane floor, the indicator's name on the axis tag.
output("cvd", area, lower, { color: "#f59e0b", width: 2, smooth: true, fill_gradient: ["#f59e0b", "#f59e0b00"], gradient_mode: "fill", fill_opacity: 0.9, axis_label: true, axis_name: true, format: "si" });
```

When the line already follows a ladder, the fill can follow the same one:
name the ladder output again on `fill_color_by` and give `fill_colors` a
rung per colour, so the edge and the fill switch together.

```typescript
// RSI as an area: oversold, neutral and overbought are the three rungs of one ladder, read by the edge and by the fill.
output("rsi", area, lower, { color_by: "zone", colors: ["theme.up", "theme.muted", "theme.down"], fill_color_by: "zone", fill_colors: ["theme.up", "theme.muted", "theme.down"], fill_opacity: 0.3, smooth: true });
output("zone", none, lower, { description: "0 under 30, 1 between, 2 over 70: the index both ladders read" });
```

A split line colours itself around a level or by slope, and `split_fill`
shades between the line and `split_base`, so an oscillator reads as a
two-tone area that reaches the floor instead of stopping at the split
level.

```typescript
// A money flow oscillator on a 0..100 pane: green above 50, red below, the shading hanging from the line down to 0.
output("mfi", line, lower, { width: 2, smooth: true, split: "level", split_level: 50, up_color: "theme.up", down_color: "theme.down", split_fill: true, split_base: 0 });
```

Columns that share a `stack` name pile up in declaration order, so two
volumes become one column per bar, the second share on top of the first;
`grading` fades the small columns.

```typescript
// Buy and sell volume piled into one column per bar.
output("buy_volume", bar, lower, { color: "theme.up", width: 0.8, stack: "volume", grading: "linear" });
output("sell_volume", bar, lower, { color: "theme.down", width: 0.8, stack: "volume", grading: "linear" });
```

A mark can be softened and lit: `fill_opacity` thins its interior and
`glow` puts a halo around it (`render.shape` takes the same two words),
which keeps a signal visible without hiding the candle under it.

```typescript
// A faded diamond with a halo above each signal bar.
output("signal_mark", shape, overlay, { shape: "diamond", shape_where: "fired", location: "above_bar", color: "#f59e0b", fill_opacity: 0.4, glow: 6 });
output("fired", none, overlay, { description: "1 on the bars that fire the signal: the mark's gate" });
```

A mark can also be a character: `shape: "char"` with `char` draws that
one character in `font_family`, here under each bar that crosses a
displaced average whose shift follows a setting through
`displacement_bars_by` ([Outputs](#outputs)).

```typescript
param.int("shift", 5, { min: -50, max: 50, label: "Shift (bars)" });
// A displaced average, and a character mark under each bar that crosses it.
output("dma", line, overlay, { color: "#38bdf8", width: 2, displacement_bars_by: { param: "shift" } });
output("cross_mark", shape, overlay, { shape: "char", char: "✕", font_family: "mono", shape_where: "crossed", location: "below_bar", color: "theme.text" });
output("crossed", none, overlay, { description: "1 on the bar the close crosses the displaced average: the mark's gate" });
```

Columns can grow from a level other than zero, and a guide's on-line
label can be an outlined pill rather than a filled one: RSI as columns
from 50, the sign pair colouring them above and below it, with the 70
level labelled on the left.

```typescript
// RSI as columns grown from 50, and the 70 level as a guide with an outlined pill.
output("rsi_bars", histogram, lower, { base: 50, colors: ["theme.up", "theme.down"], width: 0.6, grading: "square" });
output("overbought", line, lower, { role: "guide", align: "left", pill_style: "outlined", font_size: 10, color: "theme.muted", line_style: "dashed" });
```

## Candles

An output is one number per bar, so a candle takes four: declare four
consecutive `candle` outputs in the order open, high, low, close, and the
chart draws them as one candle, colored by the first output's `colors`
(bullish, then bearish) or its `color_by` ladder. A `candle` output that
does not start four consecutive candle outputs is refused when the chart
draws it ("candle output '<name>' must start four consecutive candle
outputs (open, high, low, close)"). A derived candle series (Heikin Ashi,
a synthetic bar) works either way. As four candle outputs:

```typescript
output("ha_open", candle, overlay, { colors: ["#4caf50", "#f44336"] });
output("ha_high", candle, overlay);
output("ha_low", candle, overlay);
output("ha_close", candle, overlay);
```

The group's first output carries the candle look, and the other three
refuse these words by name: `candle_style` is `"candle"` (the default),
`"hollow"` (up candles outlined), `"ohlc"` (OHLC bars) or `"high_low"`
(high-low bars); `border_colors` and `wick_colors` take one colour for
both directions or `[up, down]` (absent, border and wick follow the body
colour); `border_width` is 0..10 (default 1); `opacity` fades every
candle colour, and `glow` puts a halo on the candles. The legend lists a
candle group as one entry reading its open, high, low and close
([Legend](../presentation/legend.md)).

Hollow candles over the chart's own: the first output carries the look,
the borders and the wicks take colours of their own, and `opacity` lets
the real bars show through the group.

```typescript
// Heikin Ashi drawn hollow: up candles outlined, down candles filled, one grey wick colour for both.
output("ha_open", candle, overlay, { colors: ["theme.up", "theme.down"], candle_style: "hollow", border_colors: ["theme.up", "theme.down"], wick_colors: ["theme.muted"], border_width: 1.5, opacity: 0.85 });
output("ha_high", candle, overlay);
output("ha_low", candle, overlay);
output("ha_close", candle, overlay);
```

Or as a body box and a wick segment per bar, which draws the candle from
the same four numbers and gives each part its own color:

```typescript sample=fn-heikin-ashi
const haOpen = output("ha_open", none, overlay, { description: "Heikin Ashi open" });
const haHigh = output("ha_high", none, overlay, { description: "Heikin Ashi high" });
const haLow = output("ha_low", none, overlay, { description: "Heikin Ashi low" });
const haClose = output("ha_close", none, overlay, { description: "Heikin Ashi close" });
const bullish = output("bullish", none, overlay, { description: "1 when the smoothed close is above the smoothed open" });
const bearish = output("bearish", none, overlay, { description: "1 otherwise" });
// The body: one box per bar between open and close, one declaration per color.
box("body_up", { top: haClose, bottom: haOpen, when: bullish, color: "#4caf50", opacity: 0.9, borderWidth: 0 });
box("body_down", { top: haOpen, bottom: haClose, when: bearish, color: "#f44336", opacity: 0.9, borderWidth: 0 });
// The wick: a vertical segment from the high to the low on the same bar.
segment("wick", { yFrom: haHigh, yTo: haLow, color: "#9ca3af", width: 1 });

let prevOpen: f64 = NaN;
let prevClose: f64 = NaN;

function onBar(): void {
  const o = bar.open();
  const h = bar.high();
  const l = bar.low();
  const c = bar.close();
  const close = (o + h + l + c) / 4.0;
  // The first bar seeds the open from the raw bar; after that it is the previous smoothed midpoint.
  const open = isNaN(prevOpen) ? (o + c) / 2.0 : (prevOpen + prevClose) / 2.0;
  const high = Math.max(h, Math.max(open, close));
  const low = Math.min(l, Math.min(open, close));
  prevOpen = open;
  prevClose = close;
  out_ha_open(open);
  out_ha_high(high);
  out_ha_low(low);
  out_ha_close(close);
  out_bullish(close >= open ? 1.0 : 0.0);
  out_bearish(close < open ? 1.0 : 0.0);
}
```

The four `none` outputs draw nothing on their own; the chart shows only
the boxes and wicks. A data-only output is not offered as an alert
condition, so write a drawn output (or declare an `alert` over the gate)
for anything you want to alert on ([Alerts](../functions/alerts.md)). The recursion
the sample writes by hand ships as `HeikinAshi` in `./sdk/ta-plus`:
`update(open, high, low, close)` fills the fields `open`, `high`, `low`,
`close` and works over another market's candles too
([Extra indicators](../functions/extra-indicators.md)).

## Text, labels, tables, strips, tints

Text does not travel as an output. A `string("name", { max_bytes })`
declaration adds a byte-capped slot the module writes once per bar in
`onBar()` through its generated senders (nothing to import): build a
line with `sb_clear()`, `sb_text("...")`, `sb_int(n)`, `sb_f64(x,
decimals)` and send it with `str_<name>_sb()`, or send a whole string with
`str_<name>("...")`. Renderers then place the text:

| Renderer | Draws |
| --- | --- |
| `render.text(name, { y, text, color?, size?, style?, align?, valign?, font_weight?, font_family?, size_by?, color_by? + colors? \| color_packed_by?, panel? })` | one text mark per bar whose slot was written, at (bar, `y`); plain text is placed by `align` and `valign`, sized per bar by `size_by`, coloured per bar by its ladder; a tag `style` (`price_label`, `pill`, `callout`, `badge`, `box`, `knockout`, `emblem`) takes the tag words instead: `label_position`, `background_color` (or `background_color_by` + `background_colors`), `border_color`, `corner_radius`, `emblem_shape`, `emblem_color` |
| `render.label(name, { x, y, text, color?, size?, style?, align?, valign?, ...look })`, or `render.label(name, { position, text, offset?, ...look })` | ONE label at (`x`, `y`), `x` an output in epoch seconds, or pinned to one of the nine anchors by `position` and nudged by `offset` (`[x, y]` px, each -200..200, pointing inward from the anchored edges: a positive `x` moves a right-anchored label left, a positive `y` moves a bottom-anchored label up); the newest bar that wrote a nonempty slot (with finite `x` and `y`, when placed by them) wins; the look words are `background_color`, `border_color`, `corner_radius`, `padding` (0..64, default 4), `font_weight`, `font_family`, `emblem_shape`, `emblem_color`, `size_by` |
| `render.table(name, { rows, cols, cells, position?, ...look, styles? })` | a grid of string slots (`rows * cols` names, row-major); the newest bar where every cell was written wins; the look keys (width, column widths, fills, lines, font, alignment, header rows) and `styles` (one entry per styled cell) are on [Styled tables](cards-frames-panels.md#styled-tables) |
| `render.shape(name, { output, shape, where?, color?, color_by?, colors?, width?, location?, glow?, char?, font_family?, fill?, fill_opacity?, tooltip? })` | a shaped mark per bar at the output's value where the gate is nonzero, in `color`, or in the `colors` entry a `color_by` output picks per bar; `width` (px, any positive number; the engine clamps what it paints) is the mark size, `location` sits it `"absolute"`, `"above_bar"`, `"below_bar"`, `"top"` or `"bottom"`, `glow` adds a halo, `shape: "char"` with `char` draws one character in `font_family`, `fill: false` draws the outline only, `fill_opacity` fades the interior, `tooltip` is a template shown on the mark |
| `render.stats_row(name, { output, title?, format?, polarity?, color?, colors?, color_by?, color_packed_by?, priority?, visible? })` | a row in the statistics strip under the price pane, one cell per bar; `colors` is `[bull, bear]` on a diverging row or the ladder `color_by` indexes; `priority` 1..3 (default 2) keeps a row's text longest as bars narrow; `visible: false` draws no row at all |
| `render.bgcolor(name, { where, color?, color_by?, colors?, width?, line_style? })` | a background tint per bar where the gate is nonzero, static or by ladder; with `width` (0.5..10 px) a vertical line per gated bar through the pane instead of a tint, dashed by `line_style` |
| `render.barcolor(name, { where, color?, color_by?, colors? })` | the bar's own candle (body and wick) tinted where the gate is nonzero, static or by ladder; untinted bars keep the chart's candle colors |

`size` is an integer pixel count, 6..64 (`10` reads as small text, `16`
as large). A shape's `color_by` + `colors` ladder follows the
`bgcolor` rules: the output's value is floored into `colors`, a finite
index outside the palette takes entry 0, a non-finite value draws no mark
(the static `color` never substitutes), and either half without the other
is refused. `position` on a table is one of nine
anchors (`top_left`, `top_center`, `top_right`, `middle_left`,
`middle_center`, `middle_right`, `bottom_left`, `bottom_center`,
`bottom_right`). `format` and `polarity` on a stats row ride to the chart
verbatim (`si`, `signedSi`, `percent`, `price`, `raw`; `magnitude`,
`diverging`, `none`). `visible` on a stats row (default `true`) takes
`true`, `false` or an `"@<param>"` reference to a `param.bool`: the
setting's default draws, the setting switches the row from the settings
dialog, and while it is off the row draws nothing at all, not even an
empty strip line. One setting may switch several rows. The strip's own
look is one sheet key beside its rows, `stats_strip`: `grading` (the
population each cell's heat is ranked against: `rolling`, the default,
`daily`, `weekly`, `visible` or `whole`), `label_side` (`left` or
`right`, the edge the label gutter sits on) and `theme` (`candle`, the
chart's candle colours, or `teal_rose`, `viridis`, `inferno`,
`blue_red`, `mono`). A `param.choice` paints a key through
`style_targets` (`{ "path": ["stats_strip", "grading"] }`) and
`style_values` over that key's words, with its default's word written in
the key; the key exists only beside a stats row. A row's `color` bound to
a `param.color` paints the pick, the default included; mark the target
`"unset_at_default": true` and the chart removes the colour while the
setting holds its default, so the strip's theme colours the row until the
user picks one. Declaring a string slot or a renderer switches the
derived sheet to the second runtime contract; the numeric outputs compute
exactly as before. `style` on a text or label renderer picks its look,
the same eight words on both: `plain` (text alone, placed by `align` and
`valign`), `price_label` (the price tag), `pill` (a rounded chip at the
value), `callout` (a leader line to its bar), `badge` (a dot in the
label's color), `box` (a rounded box, the handle label's default),
`knockout` (a box in the chart's background colour that hides what is
behind the text) and `emblem` (a small mark before the text, its shape
`emblem_shape`: `dot`, `square`, `diamond`, `triangle_up`,
`triangle_down`). A tag look takes `label_position`, `background_color`,
`border_color` and `corner_radius`; plain text takes `align` and
`valign`; the two sets never mix on one renderer. `font_weight`
(`normal`, `medium`, `bold`) and `font_family` (`ui`, `mono`, `serif`,
`rounded`) apply to both. The label styles, the `tooltip` template and
`badges` are on [Labels, tooltips, badges](labels-and-tooltips.md).
A corner readout is a `render.label` with `position` (below), a one-cell
table or a HUD card ([HUD and hover cards](hud-and-hover-cards.md#hud-cards)).

A per-bar readout, a live label, a dashboard, a strip row, a
background tint, and a candle tint together:

```typescript sample=fn-text-labels
param("period", 20, { min: 1, max: 200 });
param("stretch", 2, { min: 0.1, max: 20, description: "Percent from the average that counts as stretched" });
output("average", line, overlay, { color: "#38bdf8", width: 2, description: "Simple average" });
output("bar_time", none, overlay, { description: "Bar open in epoch seconds, the label's x" });
output("stretch_pct", none, overlay, { description: "Close distance from the average, percent" });
output("stretched", none, overlay, { description: "1 while the close is stretched from the average" });
output("volume", none, overlay, { description: "Volume, shown in the strip" });
string("readout", { max_bytes: 32 });
string("tag", { max_bytes: 32 });
string("stretch_label", { max_bytes: 16 });
string("stretch_text", { max_bytes: 16 });
// A text mark on every stretched bar (the slot is left unwritten on quiet bars).
render.text("stretch_mark", { y: "average", text: "readout", color: "#f59e0b", size: 10 });
// One label riding the newest bar, at the average.
render.label("average_tag", { x: "bar_time", y: "average", text: "tag", color: "#38bdf8", size: 11 });
// A fixed-position 1x2 dashboard: the newest bar where both cells were written wins.
render.table("stats", { rows: 1, cols: 2, cells: ["stretch_label", "stretch_text"], position: "top_right" });
// Volume as a statistics-strip row.
render.stats_row("volume_row", { output: "volume", title: "Volume", format: "si", polarity: "magnitude" });
// A background tint on stretched bars.
render.bgcolor("stretch_tint", { where: "stretched", color: "#f59e0b22" });
// The stretched bars' own candles, body and wick, in the same amber.
render.barcolor("stretch_candles", { where: "stretched", color: "#f59e0b" });

let sma = new Sma(20);
let threshold: f64 = 2.0;

function onStart(): void {
  sma = new Sma(i32(p_period()));
  threshold = p_stretch();
}

function onBar(): void {
  const close = bar.close();
  const volume = bar.volume();
  const barTime = bar.time();
  const value = sma.update(close);
  if (isNaN(value)) return;
  const stretch = value == 0.0 ? NaN : ((close - value) / value) * 100.0;
  const stretched = Math.abs(stretch) >= threshold;
  out_average(value);
  out_bar_time(barTime);
  out_stretch_pct(stretch);
  out_stretched(stretched ? 1.0 : 0.0);
  out_volume(volume);
  if (stretched) {
    sb_clear();
    sb_f64(stretch, 1);
    sb_text("%");
    str_readout_sb();
  }
  sb_clear();
  sb_text("SMA ");
  sb_f64(value, 2);
  str_tag_sb();
  str_stretch_label("stretch");
  sb_clear();
  sb_f64(stretch, 2);
  sb_text("%");
  str_stretch_text_sb();
}
```

Leaving `readout` unwritten on quiet bars is the whole gating story for
`render.text`: a slot not written that bar is absent, and an absent slot
draws nothing. A price tag on every signal bar is the same renderer with
`style: "price_label"`: each written bar's text sits at `y` as a price
tag. A label the module moves, re-words, or deletes on a later bar is a
label handle ([Drawing objects](drawing-objects.md)).

### Mark size

`width` on a `render.shape` sets the mark's size in pixels, any positive
number; left out, the chart draws its own default size. Two sizes of one
mark grade a signal at a glance:

```typescript sample=fn-shape-width
// Volume spikes marked under the bar: a large dot over three times the average volume, a small one over twice.
param.int("length", 20, { min: 2, max: 200, label: "Average length" });
output("low", none, overlay, { description: "The bar low, where the marks sit" });
output("big", none, overlay, { description: "1 when volume is over three times its average" });
output("small", none, overlay, { description: "1 when volume is over twice its average but not three times" });
render.shape("big_spike", { output: "low", shape: "circle", where: "big", color: "#f97316", width: 12 });
render.shape("spike", { output: "low", shape: "circle", where: "small", color: "#f97316", width: 6 });

let average = new Sma(20);

function onStart(): void {
  average = new Sma(i32(p_length()));
}

function onBar(): void {
  const volume = bar.volume();
  const mean = average.update(volume);
  if (isNaN(mean) || mean <= 0.0) return;
  const ratio = volume / mean;
  out_low(bar.low());
  out_big(ratio > 3.0 ? 1.0 : 0.0);
  out_small(ratio > 2.0 && ratio <= 3.0 ? 1.0 : 0.0);
}
```

- **One renderer per size.** `width` is fixed in the declaration, so a
  large and a small dot are two `render.shape` lines over the same output,
  each with its own gate.
- **Gates that never overlap.** `small` is 1 only between two and three
  times the average, so a bar never carries both marks.

## Fixed-position text

Text that sits at a viewport anchor and does not move with price is a
`render.label` pinned by `position` over a string slot the module
rewrites on every bar, so the newest bar decides the text:
`render.label("status", { position: "top_right", text: "status_text", offset: [12, 8], style: "box", background_color: "theme.bg" })`.
With `position` the label needs no `x` and `y` (declaring them beside it
is refused: a corner label is nudged by `offset`, not placed by
coordinates, and each number of the offset points inward from the
anchored edges), and its `style` is one of `plain`, `box`, `knockout`,
`pill`, `badge` or `emblem` (`price_label` and `callout` belong to a
label at a price). The other corner readouts are a one-cell
`render.table` at that anchor
(`render.table("status", { rows: 1, cols: 1, cells: ["status_text"], position: "top_right" })`)
and a `render.hud` card with a `tile.pill` or `tile.value` at the same
anchor ([HUD and hover cards](hud-and-hover-cards.md#hud-cards)). A label the module owns
and moves is a handle ([Drawing objects](drawing-objects.md)).

## Legend, HUD and hover

Three surfaces read the indicator's newest values and are drawn by the
chart from declarations alone: the legend entry, a HUD card at one of the
nine anchors, and the card that opens when the cursor rests on a legend
entry, a HUD tile or a line. They share one vocabulary of blocks, and the
rest of this section documents them: `legend({ title })`, `label`,
`format` and `render.legend` on [Legend](legend.md);
`hover(handle, [block.*])` and `badges` on
[Hover cards](hud-and-hover-cards.md#hover-cards);
`render.hud(name, { position, title?, columns?, tiles, look?, ... })` on
[HUD cards](hud-and-hover-cards.md#hud-cards), with its surface and type
words and the twelve looks a card can wear; every `block.*` and `tile.*`
constructor, the colour, drawing, headline and height words of a tile,
on [Blocks and tiles](hud-and-hover-cards.md#blocks-and-tiles); the
three together, with a complete module, on
[What the chart shows](overview.md).

## Ranges and panels

A rectangle between two times and two prices: per bar it is a `box`
between two outputs over a bar-offset span; for one object placed from
the newest bar it is `draw.box` with four coordinate outputs, two of
them epoch seconds from the `time` source; for a rectangle the module
keeps, grows, and deletes it is a box handle
([Drawing objects](drawing-objects.md)).

A heatmap, a curve over a category or an index, tiles, and a one-summary
pie are frame-backed panels: `panel.heatmap`, `panel.line` over a
category or index x, `panel.tiles` and `panel.pie`, each a JSON snapshot
the module writes and the chart draws in its own pane below the price
chart ([Cards, frames and panels](cards-frames-panels.md)).

Viewport-pinned panels of raw bars and a variable number of marks per
bar have no renderer: every output is one number per bar on the chart's
time axis. For a dashboard use `render.table` over string slots; for a
per-level picture use a `plot.levels` frame docked on the price axis,
or the chart's own footprint view beside the wrun indicator.

## The name requirement

A wrun indicator's outputs are
unique by construction (a duplicate `output("sma", ...)` is refused in
the editor's Console, `duplicate output name 'sma'`), the name is the
legend entry (read as words), and `description` is the optional long
text. Names follow one grammar across outputs, boxes, segments,
renderers, and drawings, and share one namespace: a renderer cannot reuse
an output's name.

## When to use which

| You want | Use | Not |
| --- | --- | --- |
| a value someone could alert on | a drawn output, or a declared `alert` over a gate | a box or a label (decorations are never alert targets) |
| a mark on some bars only | `plot: shape` with `shape_where`, or `render.shape` with `where` | a text renderer on every bar |
| a live readout at the newest bar | `render.label` from a string slot | `render.text` (one mark per bar) |
| text on every signal bar | `render.text` from a slot written on those bars | one label per signal |
| a corner readout that does not move with price | a `render.label` with a `position`, a one-cell `render.table` with a `position`, or a `render.hud` tile | a label placed by `x` and `y` |
| a dashboard | `render.hud` with typed tiles (and a `look`), or `render.table` for a grid of words | many labels |
| a word or a value in the legend | `label` and `format` on the output, `render.legend` for a slot's words | a text renderer in the corner |
| an answer when the cursor rests on a line or a tile | `tooltip` on the output, or a `hover` block list | a table the user has to read across |
| a label the module moves or deletes later | a label handle (`draw.label(id)`) | a renderer, which cannot be moved |
| a per-bar tint behind the bars | `render.bgcolor` | recoloring a line |
| trend or regime colored candles | `render.barcolor` | a background tint fighting the candle for contrast |
| a level | a `line` output with `role: "guide"` written to the constant | a drawing |
| an oscillator and a second study in panes of their own | two `pane(...)` declarations and `pane` on their outputs | one crowded lower pane |
