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.…

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 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).

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 shapeIndicator shape
arrowUp, arrowDownarrow_up, arrow_down
triangletriangle_up or triangle_down
circle, cross, diamond, flag, squarethe same names
label, chara render.text mark per bar from a string slot; "style": "price_label" in the sheet gives it the tag look (Plotting)

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.

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:

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).

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.

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.

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.

DeclarationSnapshot
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.

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 kindFields
lineanchor?, finite x1, y1, x2, y2, color?, integer width? 1..10, style? solid/dashed/dotted
boxanchor?, finite x1, y1, x2, y2, color? fill, border?, integer border_width? 0..10, text? 0..80, text_color?, integer text_size? 8..48
labelanchor?, finite x, y, text 1..80, color?, integer size? 8..48, align? left/center/right (the text edge on x; omitted centres)
polylineanchor?, 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.