---
title: "Plotting"
description: "An Indicator draws by declaring, not by calling. In kScript (legacy) a plotLine(...) call ran on every bar and handed the chart a styled series; an Indicator…"
order: 40
section: "functions"
---

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

# Plotting

An Indicator draws by declaring, not by calling. In kScript (legacy) a
`plotLine(...)` call ran on every bar and handed the chart a styled
series; an Indicator declares `output(name, plot, panel, options)` once at
the top of the file and writes one number per bar, and the host draws
that number as a line, a column, a mark, or a tint from the declaration.
That is why a chart can restyle without recompiling, why every host draws
the same thing, and why every drawn series is also a metric an alert can
read. This page maps every kScript plot function to its Indicator form,
and compiles the forms that exist.

## The map

| kScript | Indicator | Notes |
| --- | --- | --- |
| `plot(value, plotType=...)` | `output(name, line \| bar \| scatter \| candle, panel)` | the plot kind is the declaration, not a runtime argument |
| `plotLine(value, ...)` | `output(name, line, panel, { color, width, line_style, opacity })` | `fill=true` is `area`; `smooth` and `glow` have no form |
| `plotBar(value, ...)` | `output(name, bar, panel)` | one value per bar |
| `plotHistogram(value, base, colorIndex)` | `output(name, histogram, panel, { color_by, colors })` | columns grow from zero; a sign ladder colors them |
| `plotCandle(value)` | four outputs, or a box and a segment per bar | no four-output candle binding; see below |
| `plotShape(value, shape)` | `output(name, shape, panel, { shape_where })` or `render.shape` | `render.shape` picks the mark from nine shapes |
| `plotBatches(count, fn)`, `plotPie(...)` | **not in Indicators yet** | one thing per bar per renderer |
| `plotText(text, price)` | `render.text(name, { y, text, color, size })` | one mark per bar whose slot was written |
| `plotPriceLabel(price, text)` | a `text` renderer with `"style": "price_label"` in the sheet | a price tag per bar whose slot was written |
| `plotLabel(text, position)` | a `label` renderer with `"position"` in the sheet (the nine corners) | fixed position; the newest bar that wrote the slot wins |
| `plotTable(data, position)` | `render.table(name, { rows, cols, cells, position })` | cells are string slots |
| `plotRange(t1, p1, t2, p2)` | `box(...)` per bar, `draw.box` for one object, or a box handle | [Drawing objects](drawing-objects.md) |
| `plotMiniChartGrid(...)` | **not in Indicators yet** | no viewport-pinned panels |
| `plotStatRow(value, title, format)` | `render.stats_row(name, { output, title, format, polarity })` | a strip row over an output |
| `plotMatrix`, `plotCurve`, `plotTiles` | **not in Indicators yet** | [Foreign-domain panes](foreign-panes.md) |
| `hline(value)` | an output written to the constant every bar | or a segment `from: 0, to: 1` |
| `plotBgColor(color)` | `render.bgcolor(name, { where, color, color_by, colors })` | a tint per bar where the gate is nonzero |
| `fillBetween(a, b, color)` | a `box` per bar between the two outputs | `fills` exists in hand-written sheets; the chart does not draw it yet |
| `label=[...]`, `desc=[...]` | 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"`), `unit` (`price`, `%`, or a short
  axis label), `description` (the legend text).
- Per-bar color: `color_by: "<data-only output>"` plus `colors` (at least
  two entries). Each bar's value indexes the palette (floored); a missing
  or out-of-range value falls back to entry 0.
- Per-bar width: `width_by` plus `widths` (1..10 entries, each 0.5..20),
  the same ladder for line width.
- 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.
- `displacement_bars` (an integer in -500..500) draws the series shifted
  left or right without changing its values.

An output cannot color, widen, or gate itself: the palette index and the
gate must be different outputs. The rest of the vocabulary, including
what a hand-written sheet can add, is on the [Styling](styling.md) page.

## Lines, areas, columns, dots, marks

`plot()` and `plotLine()` become a `line` output; `fill=true` becomes
`area`. `plotBar()` becomes `bar` and `plotHistogram()` becomes
`histogram`: columns grown from zero, colored per bar through a `color_by`
ladder over a sign output (the `colorIndex = value >= 0 ? 0 : 1` idiom).
kScript's `base` argument moved the baseline; an Indicator's histogram
grows from zero, so a series read around another level is emitted as its
distance from that level. `plot(plotType="point")` becomes `scatter`.
`plotShape()` with `na` on quiet bars becomes a `shape` output plus a
`shape_where` gate: write the price every bar and let the gate decide
which bars draw. `hline(70)` is an output written to `70` on every bar.

```typescript
import { area, bar, histogram, input, line, lower, none, ohlcv, output, overlay, param, scatter, shape } from "./sdk/declare";
import { in_close, in_low, in_volume } from "./gen/inputs";
import {
  emitRow,
  out_cross_mark,
  out_crossed,
  out_delta,
  out_delta_sign,
  out_fast,
  out_overbought,
  out_rsi,
  out_slow,
  out_spread,
  out_volume,
} from "./gen/outputs";
import { p_fast, p_slow } from "./gen/params";
import { Cross, Rsi, Sma } from "./sdk/ta";

param("fast", 9, { min: 1, max: 200 });
param("slow", 21, { min: 2, max: 400 });
input("close", ohlcv.close);
input("low", ohlcv.low);
input("volume", ohlcv.volume);
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();
let fastValue: f64 = NaN;
let slowValue: f64 = NaN;
let rsiValue: f64 = NaN;
let close: f64 = NaN;
let low: f64 = NaN;
let volume: f64 = NaN;
let crossed: i32 = 0;

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

export function state(): i32 {
  close = in_close();
  low = in_low();
  volume = in_volume();
  fastValue = fast.update(close);
  slowValue = slow.update(close);
  rsiValue = rsi.update(close);
  crossed = cross.update(fastValue, slowValue);
  return isNaN(slowValue) ? 0 : 1;
}

export function finalize(): void {
  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);
  emitRow();
}

export function reset(): void {
  fast.reset();
  slow.reset();
  rsi.reset();
  cross.reset();
  fastValue = NaN;
  slowValue = NaN;
  rsiValue = NaN;
  close = NaN;
  low = NaN;
  volume = NaN;
  crossed = 0;
}
```

`cross_mark` is written on every bar (the bar's low) and drawn only where
`crossed` is `1`; the value on the other bars is still a metric row, which
is what lets an alert watch the gate itself. kScript's `momentum_fill`
(a two-line fill colored by sign) is this `delta` histogram, or two `none`
outputs and a `box` per bar between them ([Drawing primitives](drawing-primitives.md)).

## Candles

`candle` is a plot kind the sheet accepts, but an output is still one
number per bar: there is no declaration binding four outputs into an OHLC
candle. A derived candle series (Heikin Ashi, a synthetic bar) ports as
four outputs, or as a body box and a wick segment per bar, which draws a
real candle from the four numbers you compute:

```typescript
import { box, input, none, ohlcv, output, overlay, segment } from "./sdk/declare";
import { in_close, in_high, in_low, in_open } from "./gen/inputs";
import { emitRow, out_bearish, out_bullish, out_ha_close, out_ha_high, out_ha_low, out_ha_open } from "./gen/outputs";

input("close", ohlcv.close);
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
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;
let open: f64 = NaN;
let high: f64 = NaN;
let low: f64 = NaN;
let close: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  const o = in_open();
  const h = in_high();
  const l = in_low();
  const c = in_close();
  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.
  open = isNaN(prevOpen) ? (o + c) / 2.0 : (prevOpen + prevClose) / 2.0;
  high = Math.max(h, Math.max(open, close));
  low = Math.min(l, Math.min(open, close));
  prevOpen = open;
  prevClose = close;
  return 1;
}

export function finalize(): void {
  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);
  emitRow();
}

export function reset(): void {
  prevOpen = NaN;
  prevClose = NaN;
  open = NaN;
  high = NaN;
  low = NaN;
  close = NaN;
}
```

The four `none` outputs are still metrics (`.../ha_close` is alertable);
the chart shows only the boxes and wicks.

## 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 ready bar
in `finalize()` through generated senders in `./gen/strings`: 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 | kScript counterpart |
| --- | --- | --- |
| `render.text(name, { y, text, color?, size? })` | one text mark per bar whose slot was written, at (bar, `y`); with `"style": "price_label"` in the sheet, drawn as a price tag | `plotText`, `plotPriceLabel` |
| `render.label(name, { x, y, text, color?, size? })` | ONE label at (`x`, `y`), `x` an output in epoch seconds, or at a corner when the sheet gives it a `"position"`; the newest bar that wrote a nonempty slot wins | `plotPriceLabel` on the live bar, `plotLabel` at a corner |
| `render.table(name, { rows, cols, cells, position? })` | a grid of string slots (`rows * cols` names, row-major); the newest bar where every cell was written wins | `plotTable`; a 1x1 table is `plotLabel` |
| `render.shape(name, { output, shape, where? })` | a shaped mark per bar at the output's value where the gate is nonzero | `plotShape` with a named shape |
| `render.stats_row(name, { output, title?, format?, polarity? })` | a row in the statistics strip under the price pane, one cell per bar | `plotStatRow` |
| `render.bgcolor(name, { where, color?, color_by?, colors? })` | a background tint per bar where the gate is nonzero, static or by ladder | `plotBgColor`, `barcolor` |

`size` is an integer pixel count, 6..64 (kScript's `"small"` / `"large"`
tiers are `10` and `16`). `position` on a table or a label 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`). Declaring a string slot or a renderer switches the
derived sheet to the second runtime contract; a label's `position` and a
text renderer's `style` are fields of a hand-written sheet under the third
(`"abi_version": "wrun-3"`), since the declaration options do not take
them yet. The numeric outputs compute exactly as before under either.

A per-bar readout, a live price tag, a dashboard, a strip row, and a
regime tint together:

```typescript
import { input, line, none, ohlcv, output, overlay, param, render, string, time } from "./sdk/declare";
import { in_bar_t, in_close, in_volume } from "./gen/inputs";
import { emitRow, out_average, out_bar_time, out_stretched, out_stretch_pct, out_volume } from "./gen/outputs";
import { p_period, p_stretch } from "./gen/params";
import {
  sb_clear,
  sb_f64,
  sb_text,
  str_readout_sb,
  str_stretch_label,
  str_stretch_text_sb,
  str_tag_sb,
} from "./gen/strings";
import { Sma } from "./sdk/ta";

param("period", 20, { min: 1, max: 200 });
param("stretch", 2, { min: 0.1, max: 20, description: "Percent from the average that counts as stretched" });
input("close", ohlcv.close);
input("volume", ohlcv.volume);
input("bar_t", time.bar_open_sec);
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 price tag riding the newest bar.
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" });

let sma = new Sma(20);
let threshold: f64 = 2.0;
let value: f64 = NaN;
let close: f64 = NaN;
let volume: f64 = NaN;
let barTime: f64 = NaN;

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

export function state(): i32 {
  close = in_close();
  volume = in_volume();
  barTime = in_bar_t();
  value = sma.update(close);
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  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();
  emitRow();
}

export function reset(): void {
  sma.reset();
  value = NaN;
  close = NaN;
  volume = NaN;
  barTime = NaN;
}
```

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. `plotPriceLabel` on every signal bar is the same renderer
with the tag look: a `text` renderer whose sheet entry carries `"style":
"price_label"` draws each written bar's text as a price tag at `y`
instead of a bare mark. A label the module moves, re-words, or deletes on
a later bar is a label handle ([Drawing objects](drawing-objects.md)).

## Fixed-position text

`plotLabel(text, position="top_right")` sat at a viewport anchor and did
not move with price. The Indicator form is a `label` renderer with a
`"position"` instead of `x` and `y`: it sits at that corner, and the
newest bar that wrote its slot decides the text, so a slot rewritten
every bar is a live readout. A one-cell `render.table` at the same anchor
is the older form and still works. Both fields are sheet fields today, so
this Indicator is written sheet first: a corner readout of the average,
plus a price tag on the bars where the close is stretched from it.

```json
{
  "id": "fn-corner-label",
  "name": "Corner label and price tags",
  "abi_version": "wrun-3",
  "params": [{ "name": "period", "default": 20, "min": 1, "max": 200 }],
  "inputSources": { "close": { "source": "ohlcv", "field": "close" } },
  "inputs": [{ "index": 0, "name": "close" }],
  "outputs": [{ "index": 0, "name": "average", "plot": "line", "panel": "overlay", "color": "#38bdf8" }],
  "string_slots": [
    { "index": 0, "name": "status", "max_bytes": 32 },
    { "index": 1, "name": "tag", "max_bytes": 16 }
  ],
  "renderers": [
    { "kind": "label", "name": "status_corner", "text": "status", "position": "top_right", "size": 12 },
    { "kind": "text", "name": "stretch_tag", "y": "average", "text": "tag", "style": "price_label", "color": "#f59e0b", "size": 10 }
  ]
}
```

```typescript
import { in_close } from "./gen/inputs";
import { emitRow, out_average } from "./gen/outputs";
import { p_period } from "./gen/params";
import { sb_clear, sb_f64, sb_text, str_status_sb, str_tag_sb } from "./gen/strings";
import { Sma } from "./sdk/ta";

let sma = new Sma(20);
let value: f64 = NaN;
let close: f64 = NaN;

export function init(): void {
  sma = new Sma(i32(p_period()));
}

export function state(): i32 {
  close = in_close();
  value = sma.update(close);
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  out_average(value);
  // The corner label: rewritten on every bar, so the newest bar's text is what shows.
  sb_clear();
  sb_text("SMA ");
  sb_f64(value, 2);
  str_status_sb();
  // A price tag on stretched bars only: the slot stays unwritten on quiet bars.
  const stretch = value == 0.0 ? 0.0 : ((close - value) / value) * 100.0;
  if (Math.abs(stretch) >= 2.0) {
    sb_clear();
    sb_f64(stretch, 1);
    sb_text("%");
    str_tag_sb();
  }
  emitRow();
}

export function reset(): void {
  sma.reset();
  value = NaN;
  close = NaN;
}
```

`render.label` with `x` and `y` is the other kind of label, anchored to a
bar and a price, for a tag that rides the live edge; a label the module
owns and moves is a handle.

## Ranges and mini charts

`plotRange(time1, price1, time2, price2)` drew one rectangle from stored
timestamps. Per bar that 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)).

**Not in Indicators yet.** `plotMiniChartGrid` (viewport-pinned panels of
raw bars), `plotBatches` and `plotPie` (a variable number of marks per
bar), and the foreign-domain panes (`plotMatrix`, `plotCurve`,
`plotTiles`) have no renderer: every output is one number per bar on the
chart's time axis. The nearest form for a dashboard is `render.table`
over string slots; for a per-level picture it is the chart's own
footprint view beside the Indicator.

## The name requirement

Every kScript plot needed a unique `label`; an Indicator's outputs are
unique by construction (a duplicate `output("sma", ...)` is a named build
error, `duplicate output name 'sma'`), the name is the legend entry and
the metric id (`wrun/@you/name/sma`), 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 | an output | a box or a label (decorations are never metrics) |
| 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 or a price tag | `render.label` from a string slot | `render.text` (one mark per bar) |
| a price tag on every signal bar | `render.text` with the sheet's `style: "price_label"` | one label per signal |
| a corner readout that does not move with price | `render.label` with the sheet's `position` | a table with one cell |
| a dashboard | `render.table` | many labels |
| a label the module moves or deletes later | a label handle (`draw.label(id)`) | a renderer, which cannot be moved |
| a per-bar tint | `render.bgcolor` | recoloring a line |
| a level | an output written to the constant | a drawing |
