---
title: "Drawing primitives"
description: "Declarative visuals over outputs and string slots: markers on some bars, a background tint, a band between two lines, a dashboard's cells and a status card.…"
order: 42
section: "functions"
---

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

# Drawing primitives

Declarative visuals over outputs and string slots: markers on some bars,
a background tint, a band between two lines, a dashboard's cells and a
status card. Coordinate drawings and handles are on the
[Drawing objects](drawing-objects.md) page. In
kScript (legacy) these were `plotShape` with its marker set and locations,
`barcolor()`, `fillBetween()`, and rich `plotTable` cells, emitted bar by
bar like any plot. In Indicators each is a declaration over outputs and
string slots the module already writes. Markers, tints and bands appear
on each matching bar; tables and cards select one snapshot per run.

## Shape markers

`render.shape(name, { output, shape, where? })` draws one shaped mark per
bar at the output's value, gated (when `where` is declared) to bars
where the gate output is finite and nonzero. Nine shapes: `circle`,
`cross`, `triangle_up`, `triangle_down`, `diamond`, `arrow_up`,
`arrow_down`, `flag`, `square`. A `shape` output with `shape_where` is
the same idea with the host's default mark ([Plotting](plotting.md)).

The kScript idiom `if (crossover(fast, slow)) { plotShape(low,
shape="arrowUp", location="belowBar") }` is: a data-only gate output that
is `1` on the cross bar, an output carrying the price to mark, and a
renderer over the two. `location` has no counterpart: put the mark where
you want it by choosing the output (`low` for "below the bar", `high`
for "above", a level for "absolute").

| kScript `shape` | Indicator `shape` |
| --- | --- |
| `arrowUp`, `arrowDown` | `arrow_up`, `arrow_down` |
| `triangle` | `triangle_up` or `triangle_down` |
| `circle`, `cross`, `diamond`, `flag`, `square` | the same names |
| `label`, `char` | a `render.text` mark per bar from a string slot; `"style": "price_label"` in the sheet gives it the tag look ([Plotting](plotting.md)) |

`color` and `width` on a shape renderer are sheet fields the chart
honors; the code-first declaration takes `output`, `shape`, and `where`,
so a colored mark today is a `shape` output with `color` and
`shape_where`, or a hand-written sheet.

## Bar color

`barcolor("#16a34a")` recolored the candle itself. **Not in Indicators
yet** for candle bodies: `render.bgcolor(name, { where, color?,
color_by?, colors? })` tints the BACKGROUND of every bar where the gate
is finite and nonzero, either a static `color` or a per-bar `colors`
ladder indexed by a `color_by` output (floor; a finite out-of-range index
clamps to entry 0; a non-finite index means no tint on that bar). Bars
you do not tint keep the chart default. The delta-colored bars of a
footprint port as a bgcolor ladder over the delta's sign; the line you
draw on top can carry the same ladder through `color_by` on the output.

## Fill between

`fillBetween(upper, lower, color, opacity)` shaded the area between two
plotted series. The chart-drawn form is a `box` on every bar between the
two outputs with `from` and `to` left at `0`: each bar contributes a
one-bar slice and the slices tile into a band; the box's `color` and
`opacity` are the fill's. A conditional fill (bars where you did not call
`fillBetween`) is a `when` gate on the box, and a sign-colored fill is
two boxes gated by opposite outputs, one color each.

**Not in Indicators yet.** A `fills` entry in a hand-written sheet
(`"fills": [{ "between": ["upper", "lower"], "color": "#0ea5e9",
"opacity": 0.15 }]`) and a `range()` declaration both record the band,
but the chart lane does not draw either today and neither has a
per-bar color; a declaration form for `fills` waits on chart support. On
the chart, shade with the box.

```typescript
import { box, input, line, none, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_above, out_below, out_delta, out_zero } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Sma } from "./sdk/ta";

param("period", 20, { min: 2, max: 400 });
input("close", ohlcv.close);
const delta = output("delta", line, overlay, { color: "#e2e8f0", width: 1, description: "Close minus its average" });
const zero = output("zero", line, overlay, { color: "#64748b", width: 1, description: "Zero baseline" });
const below = output("below", none, overlay, { description: "1 while the delta is negative" });
const above = output("above", none, overlay, { description: "1 while the delta is positive or zero" });
// Two fills, one per sign: each bar contributes one slice between the delta and zero.
box("fill_up", { top: delta, bottom: zero, when: above, color: "#22c55e", opacity: 0.15, borderWidth: 0 });
box("fill_down", { top: zero, bottom: delta, when: below, color: "#ef4444", opacity: 0.15, borderWidth: 0 });

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

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

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

export function finalize(): void {
  out_delta(value);
  out_zero(0.0);
  out_below(value < 0.0 ? 1.0 : 0.0);
  out_above(value >= 0.0 ? 1.0 : 0.0);
  emitRow();
}

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

Both boundary series are outputs the chart draws, exactly as kScript
required both fills' series to be plotted; the difference is that here
the module could also leave one of them `none` and the box would still
tile, because a box reads outputs, not lines.

## Rich table cells

`plotTable` took per-cell objects with `value`, `color`, `textColor`,
`tooltip`, `align`, `colspan`, `rowspan`. `render.table(name, { rows,
cols, cells, position? })` takes `rows * cols` string-slot names, row
major, and the newest bar where every cell was written wins. A cell is
text: numbers go through `sb_f64`, and a cell that should read as a
color-coded value carries the sign in its text (`+1.2%`) or in a second
cell. **Not in Indicators yet.** Per-cell colors, tooltips, alignment,
and spans have no form; the table is plain text at an anchor.

## Status cards

`draw.card(name, { title, anchor?, offset?, z?, state_by?, rows })`
declares a run-level status card in `drawings[]`, available from
`abi_version: "wrun-2"`. Names share the output, renderer, drawing, box
and segment namespace. A sheet accepts at most 8 cards within its
64-drawing limit; each card has 1 to 12 rows.

`title` takes 1 to 40 characters; each row's `label` takes 1 to 24.
`anchor` uses the same nine positions as label renderers: `top_left`,
`top_center`, `top_right`, `middle_left`, `middle_center`, `middle_right`,
`bottom_left`, `bottom_center`, `bottom_right`. Defaults are `top_right`,
`offset: [0, 0]` and `z: 0`. Each offset is an integer from -200 to 200;
`z` is an integer.

Each row has an optional `value`: a literal string of 0 to 24 characters,
`{ text: "slot_name" }`, or `{ output: "output_name", format: "auto" }`.
Output references always name declared numeric outputs; `text` always
names a declared string slot. Slot contents retain their declared byte
limit. Formats are `auto` (the default, up to six significant digits),
`int` (rounded), `pct` (multiply by 100, two decimals and `%`), and `usd`
(two decimals with a `$` prefix).

A row's `color` is `#rrggbb` or an object containing both `color_by` and
`colors`, with at least two `#rrggbb` entries. The output's floored value
picks a palette entry; a finite index outside the palette selects entry 0.
`countdown_to: { output: "deadline" }` carries the numeric output as raw
epoch milliseconds, without converting it to seconds. `clock: true`
asks the chart to show its clock. Both may appear beside a value.

The newest ready bar with every referenced numeric output finite and
every referenced slot present wins. Empty strings count as present. If
no bar is complete, the newest ready bar still supplies the card: missing
values become `""`, missing colours and countdowns are omitted, and
missing state reads `ok`. With no ready bar the selection is
`{ kind: "card", name, card: null }`. `state_by` maps exactly `0` to `ok`,
`1` to `armed`, `2` to `fired`, and `3` to `error`; other values read `ok`.

The declaration parser's `object_array` option kind keeps `rows` as a
literal object array, including the nested value, colour and countdown
objects. References are literal names, never handles; variables, spreads
and expressions are refused. The scaffold's normal build extracts these
options and removes the declaration before AssemblyScript typechecking;
no card accessor or host import is needed. This declaration assumes the
named outputs and string slot have already been declared:

```typescript
draw.card("market_status", {
  title: "Market status",
  anchor: "top_right",
  offset: [-12, 12],
  z: 1,
  state_by: "status",
  rows: [
    { label: "Price", value: { output: "price", format: "usd" } },
    { label: "Change", value: { output: "change", format: "pct" }, color: { color_by: "direction", colors: ["#ef4444", "#22c55e"] } },
    { label: "Status", value: { text: "status_text" } },
    { label: "Next close", countdown_to: { output: "close_at_ms" } },
    { label: "Time", clock: true },
  ],
});
```

Cards share the selection's 2 MiB expanded-result budget. Accounting is
16 bytes per card and per card row, 8 per carried number (the two offsets,
`z`, and each countdown), plus the UTF-8 bytes of every carried string:
kind, name, title, state, anchor, row labels, resolved values and colours.
Exceeding the budget refuses with `wrun_render_result_too_large`.

## Everything rolls back correctly

Markers, tints and bands are per-bar records over outputs, so they follow
the engine's forming-bar rule: the host snapshots the module after the
last closed bar and replays the revised forming bar through `reset()` on
every tick, and that bar's marks, tints, and slices are replaced, never
stacked. Cards replace their selected snapshot on each run. Handle
drawings roll back the same way: the host keeps the
handles beside the module state, puts both back before each replay, and
re-runs the closed bar once more when it closes, so a handle the forming
bar created or moved is never duplicated by a tick. A `reset()` that
forgets a field is the one way to get a stale primitive
([Execution model](../core-concepts/execution-model.md)).

## Every primitive in one module

A marker on each bullish and bearish cross, a background tint by the
sign of the delta, a shaded band, and a two-cell dashboard, over one
moving-average pair.

```typescript
import { box, input, line, none, ohlcv, output, overlay, param, render, string } from "./sdk/declare";
import { in_close, in_high, in_low } from "./gen/inputs";
import { emitRow, out_bearish, out_bullish, out_delta_bucket, out_fast, out_high, out_low, out_slow } from "./gen/outputs";
import { p_fast, p_slow } from "./gen/params";
import { sb_clear, sb_f64, sb_text, str_delta_text_sb, str_regime_text } from "./gen/strings";
import { Cross, Sma } from "./sdk/ta";

param("fast", 9, { min: 1, max: 200 });
param("slow", 21, { min: 2, max: 400 });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
const fast = output("fast", line, overlay, { color: "#2563eb", width: 2, description: "Fast average" });
const slow = output("slow", line, overlay, { color: "#94a3b8", width: 1, description: "Slow average" });
output("high", none, overlay, { description: "The bar high: where a bearish mark sits" });
output("low", none, overlay, { description: "The bar low: where a bullish mark sits" });
output("bullish", none, overlay, { description: "1 on a bullish-cross bar" });
output("bearish", none, overlay, { description: "1 on a bearish-cross bar" });
output("delta_bucket", none, overlay, { description: "0 fast below slow, 1 fast above: the tint ladder index" });
string("regime_text", { max_bytes: 8 });
string("delta_text", { max_bytes: 16 });
// Markers: an arrow below the bar on a bullish cross, one above it on a bearish cross.
render.shape("buy_mark", { output: "low", shape: "arrow_up", where: "bullish" });
render.shape("sell_mark", { output: "high", shape: "arrow_down", where: "bearish" });
// Bar color: a background tint by regime; index 0 is red, index 1 green, on every bar.
render.bgcolor("regime_tint", { where: "fast", color_by: "delta_bucket", colors: ["#ef444418", "#22c55e18"] });
// Fill between: a slice per bar between the two averages.
box("ribbon", { top: fast, bottom: slow, color: "#2563eb", opacity: 0.1, borderWidth: 0 });
// A 1x2 dashboard: regime word, delta value.
render.table("dashboard", { rows: 1, cols: 2, cells: ["regime_text", "delta_text"], position: "top_right" });

let fastSma = new Sma(9);
let slowSma = new Sma(21);
const cross = new Cross();
let fastValue: f64 = NaN;
let slowValue: f64 = NaN;
let high: f64 = NaN;
let low: f64 = NaN;
let crossed: i32 = 0;

export function init(): void {
  fastSma = new Sma(i32(p_fast()));
  slowSma = new Sma(i32(p_slow()));
}

export function state(): i32 {
  const close = in_close();
  high = in_high();
  low = in_low();
  fastValue = fastSma.update(close);
  slowValue = slowSma.update(close);
  crossed = cross.update(fastValue, slowValue);
  return isNaN(slowValue) ? 0 : 1;
}

export function finalize(): void {
  const above = fastValue >= slowValue;
  out_fast(fastValue);
  out_slow(slowValue);
  out_high(high);
  out_low(low);
  out_bullish(crossed == 1 ? 1.0 : 0.0);
  out_bearish(crossed == -1 ? 1.0 : 0.0);
  out_delta_bucket(above ? 1.0 : 0.0);
  str_regime_text(above ? "long" : "short");
  sb_clear();
  sb_f64(fastValue - slowValue, 2);
  sb_text(" delta");
  str_delta_text_sb();
  emitRow();
}

export function reset(): void {
  fastSma.reset();
  slowSma.reset();
  cross.reset();
  fastValue = NaN;
  slowValue = NaN;
  high = NaN;
  low = NaN;
  crossed = 0;
}
```

`regime_tint` gates on `fast`, which is finite on every ready bar, so
the tint appears everywhere and the ladder picks the color; gate on a
`0`/`1` output instead to tint only some bars. The 8-digit hex colors
carry the tint's alpha.

<!-- indicator-docs:wrun4:start -->
## Frames, panels and compact widgets

Use `wrun-4` frames for one JSON snapshot that survives the whole run. Declare
`frame("book")`, then call `writeFrame(FRAME_BOOK, json)` from `finalize()`
using `./gen/frames`. Each frame holds its last write, up to 96 KiB; up to
eight frames share a 2 MiB transport budget with strings.

| Declaration | Snapshot |
| --- | --- |
| `plot.levels({ name, frame, dock, width_frac?, poc?, labels?, color? })` | `{ prices, values, colors? }`; 1..512 monotonic prices and equally sized values, null for gaps |
| `panel.bars`, `panel.line`, `panel.scatter`, `panel.histogram`, `panel.pie`, `panel.heatmap`, `panel.table`, `panel.tiles` | `{ rows }`; kind-specific tuples, at most 2000 rows, empty allowed |
| `draw.ladder({ name, frame, side, divider? })` | `{ rows, divider? }`; 1..64 price/value/fraction/color rows |
| `draw.feed({ name, frame, anchor?, offset?, z? })` | `{ lines }`; 1..50 millisecond-time/text/color rows |

Levels default to a width fraction of 0.12.

Panels declare name, title, x (`time`, `index`, `category`), place (`below`,
`side`) and frame. Bars, line and table also declare series. Histograms and
heatmaps use category x. Panel tables support styled cells with text, color,
an inline bar and a spark array. An unwritten frame leaves its consumer
absent; a malformed written frame refuses with `wrun_frame_invalid`.

`draw.meter({ name, label, fraction: { output }, ramp, text?, anchor?,
offset?, z? })` reads the last ready numeric fraction. Ramp has 2..5 hex
colors; text is a short literal or `{ slot }`. A card row's
`spark: { output, window }` carries its last 2..64 ready values, oldest first.

`out.inset("vol", { dock: "bottom", height_px: 28, shape: "histogram" })`
declares an ordinary numeric output in a compact strip. Every ready row
appears in its history. Insets work on every ABI.

A non-primary sheet input can also read
`{ source: "series", ref: "watch/flow/imbalance", missing: "carry" }`, or
an installed `@scope/name` snapshot. Readings bucket to the primary grid,
last reading wins, and carry uses the latest earlier reading. Choose nan
for gaps. Series inputs require the daemon host.

The scaffold extracts these object declarations before AssemblyScript type
checking. `frame()` returns its slot index; generated `FRAME_<NAME>`
constants provide the same index. `src/gen/frames.ts` exists only when
frames are declared. Its UTF-8 scratch buffer is reserved during module
initialization and reused, so repeated writes do not grow guest memory.
The frame contract (`docs/WRUN4_ABI.md` in the openmarket repository)
lists the complete row grammars, limits and selection rules; the caps an
author meets are on [Limits](../reference/limits.md).

### Pane pixel placement

Line, box, label and polyline handle declarations accept `anchor` as the
default for newly created handles: `handles.line({ anchor: "top_left" })`.
Absent means chart coordinates. The nine pane spots are `top_left`,
`top_center`, `top_right`, `middle_left`, `middle_center`, `middle_right`,
`bottom_left`, `bottom_center`, `bottom_right`; both coordinates become CSS
pixel offsets. `top` and `bottom` change only y; x remains chart time.
`left` and `right` change only x; y remains chart price.

Offsets start at the pane's content rectangle with no extra inset. Left
and top measure inward to the right and down; right and bottom measure
inward to the left and up. Centre offsets are signed, and negative offsets
are allowed everywhere. The renderer scales pixels once by device pixel
ratio and clips every drawing to its own pane. Time culling applies only
when x remains chart time.

Import `ANCHOR_TOP_LEFT` or another `ANCHOR_*` constant from `./gen/draw`.
After a handle's `set`, call `.anchor(ANCHOR_TOP_LEFT)` or
`style.anchor(handle, ANCHOR_TOP_LEFT)`. `ANCHOR_CHART` restores chart
coordinates and clears a declared default until the handle is recreated.
These helpers use the existing style import with prop 7, integer values
0..13. Invalid values refuse as `wrun_draw_style_out_of_range`.

Label handles also take `align`: which edge of the text sits on x, with
(x, y) staying the anchor point. `handles.label({ align: "left" })` is the
default for new labels; `left` starts the text at x, `right` ends it there,
`center` or an omitted word centres it as before. After a label's `text`,
call `.align(ALIGN_RIGHT)` or `style.align(label, ALIGN_RIGHT)` with an
`ALIGN_*` constant from `./gen/draw`; `ALIGN_DEFAULT` restores centred text
and clears a declared default. These use prop 8, integer values 0..3, on
labels only; other kinds refuse as `wrun_draw_prop_unsupported`.

### Drawings widgets

`om chart widget push --kind drawings --id terminal.ui --title "Terminal"`
accepts `--items '<json array>'`, or `--items -` to read the array from stdin.
The widget is `{ kind: "drawings", id, title, items }`, with title length
1..40 and 1..256 items. Placement belongs to each item; widget-level
anchor, offset, z, state, rows and lines are refused, as are unknown keys.

| Item kind | Fields |
| --- | --- |
| `line` | `anchor?`, finite `x1, y1, x2, y2`, `color?`, integer `width?` 1..10, `style?` solid/dashed/dotted |
| `box` | `anchor?`, finite `x1, y1, x2, y2`, `color?` fill, `border?`, integer `border_width?` 0..10, `text?` 0..80, `text_color?`, integer `text_size?` 8..48 |
| `label` | `anchor?`, finite `x, y`, `text` 1..80, `color?`, integer `size?` 8..48, `align?` left/center/right (the text edge on x; omitted centres) |
| `polyline` | `anchor?`, `points` with 2..64 finite `[x, y]` pairs, `color?`, `width?` 1..10 |

Colors are `#rrggbb`. Widget chart x is already epoch milliseconds; script
chart x uses seconds before selection. Anchored coordinates follow the
same placement word above. Box text is centred and clipped to the box;
`border_width: 0` draws no stroke. Rendering maps box text fields to
`text`, `textColor` and `textSize` in the engine.

The 32 KiB widget ceiling still applies. Even 256 otherwise valid labels
with 80-character text can exceed it; preflight refuses as
`intent_too_large` and preserves the previous widget. Reusing the id
replaces the widget; `--clear` writes the existing identity-only tombstone.
<!-- indicator-docs:wrun4:end -->
