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…

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

kScriptIndicatorNotes
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 barno four-output candle binding; see below
plotShape(value, shape)output(name, shape, panel, { shape_where }) or render.shaperender.shape picks the mark from nine shapes
plotBatches(count, fn), plotPie(...)not in Indicators yetone 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 sheeta 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 handleDrawing objects
plotMiniChartGrid(...)not in Indicators yetno viewport-pinned panels
plotStatRow(value, title, format)render.stats_row(name, { output, title, format, polarity })a strip row over an output
plotMatrix, plotCurve, plotTilesnot in Indicators yetForeign-domain panes
hline(value)an output written to the constant every baror 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 outputsfills exists in hand-written sheets; the chart does not draw it yet
label=[...], desc=[...]the output name and descriptionnames 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 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.

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

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:

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:

RendererDrawskScript 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 tagplotText, 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 winsplotPriceLabel 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 winsplotTable; 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 nonzeroplotShape 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 barplotStatRow
render.bgcolor(name, { where, color?, color_by?, colors? })a background tint per bar where the gate is nonzero, static or by ladderplotBgColor, 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:

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

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.

{
  "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 }
  ]
}
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).

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 wantUseNot
a value someone could alert onan outputa box or a label (decorations are never metrics)
a mark on some bars onlyplot: shape with shape_where, or render.shape with wherea text renderer on every bar
a live readout or a price tagrender.label from a string slotrender.text (one mark per bar)
a price tag on every signal barrender.text with the sheet's style: "price_label"one label per signal
a corner readout that does not move with pricerender.label with the sheet's positiona table with one cell
a dashboardrender.tablemany labels
a label the module moves or deletes latera label handle (draw.label(id))a renderer, which cannot be moved
a per-bar tintrender.bgcolorrecoloring a line
a levelan output written to the constanta drawing