How outputs draw. The rule that keeps packages portable: outputs are
numbers, some numbers are decisions, and the sheet maps decisions to
looks. Styling is declared in the sheet (or in the output(...) options
the sheet is derived from), never computed in the module, so charts
restyle without recompute and every runtime renders the same declaration.
kScript (legacy) styled per call, with size tiers, tooltips, z-order,
palettes, and per-element gradients; this page is the Indicator
vocabulary for each, and which of them have no form yet.
Placement, per output
plot:line,bar,area,histogram,candle,shape,scatter, or""(nonein code) for a data-only output: computed, never drawn, the building block for gates and palettes below.panel:overlay(the price chart) orlower(its own pane). Put small-magnitude series (probabilities, oscillators) inlower; an odds-scale line on the price axis hugs the axis floor.unit: axis formatting (price,%, else an abbreviated label). This is kScript'sdefine(format=...), per output instead of per script.
Static style, per output
color (or a colors palette), width, opacity (0..1), line_style
(solid, dashed, dotted), description (the legend text).
Decisions to looks
- Per-bar coloring: emit the decision as an ordinary data-only output
(
"plot": ""), then on the styled output setcolor_by: "<that output>"plus acolorspalette (at least 2 entries); each bar's floored value indexes the palette, and a missing or out-of-range index falls back to entry 0. That floor + entry-0 rule is the chart engine'scolor_byrule, and the host-implemented ladders below adopt it unchanged. An output cannot color itself. This is the port ofcolorIndex: the index expression becomes an output. - Per-bar width: the color ladder's sibling. On the styled output set
width_by: "<data-only output>"plus awidthsladder (1..10 entries, each 0.5..20); each bar's value indexes the ladder (floor; a finite out-of-range index clamps to entry 0; a non-finite value,NaNor infinite, keeps the static width). Both halves or neither, each refused by name without the other. An output cannot set its own width. - Gated markers: a
plot: "shape"output withshape_where: "<gate output>"renders only where the gate is nonzero. An output cannot gate itself. - Shaded bands: top-level
fills: [{ "between": ["upper", "lower"], "color": "#94a3b8", "opacity": 0.15 }]; both sides must be DIFFERENT rendered outputs. Sheet-only today: there is no declaration form, and the chart does not draw fills yet (Drawing primitives has the per-bar box that does). - Banded ranges: top-level
ranges: [{ "upper": "band_hi", "lower": "band_lo", "color": "#94a3b8", "edge_width": 2, "edge_line_style": "dotted", "smooth": true }], orrange(upper, lower, options)in code; both sides must be DIFFERENT rendered outputs. Instead of the staticcolor, acolorspalette pluscolor_by(a data-only output) tints the band per bar (floor; finite out-of-range clamps to entry 0; a non-finite value = no tint that bar);color_byrequirescolors.colorsWITHOUTcolor_byis the band's sign palette: entry 0 tints spans whereupperplots abovelower, entry 1 where it plots below; a single entry tints everywhere, and entries past index 1 are unread.edge_widthis an integer 1..10,edge_line_styleone ofsolid/dashed/dotted, andsmootha boolean smoothing hint. Declare as many ranges as needed; the same pair may repeat with different options. Ranges ride the sheet for hosts that honor them; the chart lane does not draw them today. - User style knobs: a param with
style: { "output": "<name>", "property": "color" | "width" | "opacity" | "lineStyle" }never reaches the module (no accessor, its slot stays zero-filled), shows in the chart settings dialog, and redraws without recompute.colorandlineStyleknobs take string defaults ("#22c55e");widthandopacitytake numeric defaults. The bound output must be a rendered one. This is the port of kScript's style-only inputs (type="color", a width slider), and it is sheet-only: no declaration form yet.
Every reference is cross-validated: color_by, width_by, shape_where,
style.output, and each fills and ranges side must name a declared
(and, where required, rendered) output, none of the three may name its own
output (the self-reference is refused by name), and a publish names the
broken field.
Styling dresses per-output SERIES. When the package should also put text,
tables, shaped marks, or free-standing objects (lines, boxes, polylines,
labels) on the chart, that is the renderer and drawing vocabulary:
top-level renderers and drawings arrays over named outputs and string
slots, in Script definition. Shapes that repeat on
EVERY bar (a zone behind the last few bars, a projection ray from each
bar) are the boxes and segments below.
Text size, tooltips, alignment, z-order, palettes
The kScript styling vocabulary, item by item:
| kScript | Indicator |
|---|---|
size tiers ("tiny" 8 to "huge" 24) on text | size, an integer pixel count 6..64, on render.text and render.label |
tooltip on plots, cells, drawings | not in Indicators yet; description is the legend's long text |
| horizontal and vertical alignment words | not in Indicators yet on text; the nine anchors (top_left ... bottom_right) are a table's position |
zOrder / set_zorder | not in Indicators yet; declaration order is draw order |
palette("viridis"), a fresh mutable array | a colors literal array on the output, box, range, or bgcolor; bucket the driving value into its index (Color functions) |
per-element glow, opacity, gradients | opacity per output or box; a color_by ladder per bar; no glow, no continuous gradient |
Value-driven styling, kScript's pattern of computing the style from the
data per element, survives as the ladder: normalize the driving value,
floor it into a bucket, write the bucket to a none output, and index a
palette with it. The palette is finite, so the gradient is stepped, and
the decision is a metric anyone can read.
Boxes and segments
Per-bar shapes over outputs the package already computes, declared as the
top-level boxes and segments arrays (at most 16 entries each). They are
sheet-only and ABI-neutral like fills and ranges: no new wire data (the
referenced outputs already stream), legal on both runtime contracts,
rendered by chart hosts and ignored by scalar surfaces. Every referenced
output must be declared and MAY be data-only (plot: ""), which is the
usual shape: compute the coordinates, never draw them as lines.
On every bar row i:
- nothing is drawn when any referenced output is non-finite on that row, or
when the optional
whengate output is0or non-finite; - a box spans bars
i + x_from..i + x_toinclusive and pricesmin(top, bottom)..max(top, bottom); - a segment is the straight line from
(i + x_from, y_from)to(i + x_to, y_to).
x_from and x_to are bar OFFSETS relative to the current bar (negative =
past, positive = ahead; default 0): an integer in -500..500, or the name
of an output whose per-bar value, truncated to an integer, is the offset.
Offsets past the loaded range clamp to its edge. The forming (last) bar
re-evaluates on every tick: the host replaces that bar's shapes, never
stacks them. When color is absent the shape takes the referenced output's
color (top for a box, y_from for a segment), and when panel is absent
it takes that output's panel. Names follow the output grammar and share one
namespace with outputs, renderers, and drawings.
Box fields: name, top, bottom, x_from, x_to, when, panel
(overlay or lower), color, border_color, opacity (0..1, default
0.2), border_width (0..10, default 1). Segment fields: name, y_from,
y_to, x_from, x_to, when, panel, color, width (0.5..20,
default 1), line_style (solid, dashed, or dotted).
A box on every bar: the last five bars' high-low range, tinted only while the range is expanding. Three data-only outputs carry the coordinates and the gate; the sheet turns them into a zone:
{
"id": "range-box",
"abi_version": "wrun-1",
"warmup_bars": 5,
"params": [],
"inputSources": {
"high": { "source": "ohlcv", "field": "high" },
"low": { "source": "ohlcv", "field": "low" }
},
"inputs": [
{ "index": 0, "name": "high" },
{ "index": 1, "name": "low" }
],
"outputs": [
{ "index": 0, "name": "range_hi", "plot": "" },
{ "index": 1, "name": "range_lo", "plot": "" },
{ "index": 2, "name": "expanding", "plot": "" }
],
"boxes": [
{ "name": "range_zone", "top": "range_hi", "bottom": "range_lo", "x_from": -4, "x_to": 0, "when": "expanding", "panel": "overlay", "color": "#f59e0b", "opacity": 0.15, "border_color": "#f59e0b", "border_width": 1 }
]
}A segment on every bar: a dotted projection from each bar's anchor line to
a target level, its length set per bar by a data-only output (reach, an
output-valued offset):
{
"id": "projection-segment",
"abi_version": "wrun-1",
"warmup_bars": 1,
"params": [],
"inputSources": { "close": { "source": "ohlcv", "field": "close" } },
"inputs": [{ "index": 0, "name": "close" }],
"outputs": [
{ "index": 0, "name": "anchor", "plot": "line", "panel": "overlay", "color": "#38bdf8" },
{ "index": 1, "name": "target", "plot": "" },
{ "index": 2, "name": "reach", "plot": "" }
],
"segments": [
{ "name": "projection", "y_from": "anchor", "y_to": "target", "x_from": 0, "x_to": "reach", "color": "#38bdf8", "width": 1, "line_style": "dotted" }
]
}Code first, both declare from output HANDLES: output(...) returns one,
so bind it to a const and pass it. from / to take integer literals or
handles, and the option names are camelCase (borderColor, borderWidth,
yFrom, yTo, lineStyle), landing in the sheet as the snake_case fields
above. A complete package: a moving average with a one-percent band drawn
as a zone behind the last five bars while the average rises, plus a dotted
ray from each bar's average to the band top three bars ahead:
import { box, input, line, none, ohlcv, output, overlay, param, segment } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_band_hi, out_band_lo, out_mid, out_rising } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Sma } from "./sdk/ta";
param("period", 20, { min: 1, max: 200 });
input("close", ohlcv.close);
const mid = output("mid", line, overlay, { color: "#38bdf8" });
const bandHi = output("band_hi", none);
const bandLo = output("band_lo", none);
const rising = output("rising", none);
box("band_zone", { top: bandHi, bottom: bandLo, from: -4, to: 0, when: rising, opacity: 0.15, borderWidth: 0 });
segment("mid_ray", { yFrom: mid, yTo: bandHi, from: 0, to: 3, lineStyle: "dotted" });
let sma = new Sma(20);
let value: f64 = NaN;
let prev: f64 = NaN;
export function init(): void {
sma = new Sma(i32(p_period()));
}
export function state(): i32 {
prev = value;
value = sma.update(in_close());
return isNaN(value) ? 0 : 1;
}
export function finalize(): void {
out_mid(value);
out_band_hi(value * 1.01);
out_band_lo(value * 0.99);
out_rising(!isNaN(prev) && value > prev ? 1.0 : 0.0);
emitRow();
}
export function reset(): void {
sma.reset();
value = NaN;
prev = NaN;
}A worked example
Bollinger-style bands with a regime-colored (and regime-widened) midline, a shaded band, a banded range, and a width knob:
{
"id": "trend-paint",
"abi_version": "wrun-1",
"warmup_bars": 1,
"params": [
{ "name": "period", "default": 20, "min": 2, "max": 400 },
{ "name": "band_width", "default": 2, "min": 0.5, "max": 4, "description": "Stdev multiples" },
{ "name": "line_width", "default": 3, "style": { "output": "mid", "property": "width" } }
],
"inputSources": { "close": { "source": "ohlcv", "field": "close" } },
"inputs": [{ "index": 0, "name": "close" }],
"outputs": [
{ "index": 0, "name": "mid", "plot": "line", "panel": "overlay", "color_by": "regime", "colors": ["#ef4444", "#22c55e"], "width_by": "regime", "widths": [1, 3] },
{ "index": 1, "name": "band_hi", "plot": "line", "panel": "overlay", "opacity": 0.6 },
{ "index": 2, "name": "band_lo", "plot": "line", "panel": "overlay", "opacity": 0.6 },
{ "index": 3, "name": "regime", "plot": "" }
],
"fills": [{ "between": ["band_hi", "band_lo"], "color": "#94a3b8", "opacity": 0.15 }],
"ranges": [{ "upper": "band_hi", "lower": "band_lo", "color": "#94a3b8", "edge_width": 1, "edge_line_style": "dotted" }]
}The module computes four numbers per bar and never mentions a color:
import { in_close } from "./gen/inputs";
import { emitRow, out_band_hi, out_band_lo, out_mid, out_regime } from "./gen/outputs";
import { p_band_width, p_period } from "./gen/params";
import { Sma, Stdev } from "./sdk/ta";
let sma = new Sma(20);
let stdev = new Stdev(20);
let mult: f64 = 2.0;
let mid: f64 = NaN;
let sd: f64 = NaN;
let regime: f64 = 0.0;
let close: f64 = NaN;
export function init(): void {
const period = i32(p_period());
sma = new Sma(period);
stdev = new Stdev(period);
mult = p_band_width();
}
export function state(): i32 {
close = in_close();
mid = sma.update(close);
sd = stdev.update(close);
regime = close > mid ? 1.0 : 0.0;
return isNaN(mid) || isNaN(sd) ? 0 : 1;
}
export function finalize(): void {
out_mid(mid);
out_band_hi(mid + mult * sd);
out_band_lo(mid - mult * sd);
out_regime(regime);
emitRow();
}
export function reset(): void {
sma.reset();
stdev.reset();
mid = NaN;
sd = NaN;
regime = 0.0;
close = NaN;
}Note the style knob (line_width) has no p_ accessor and the module never
reads it: the chart applies it to the mid line directly. The regime
output computes on every bar but never draws; it exists so the palette
(and the widths ladder) on mid has a decision to index.
In code-first workspaces the output(...) options carry color, colors,
width, opacity, line_style, color_by, shape_where, width_by, and
widths, ranges declare as range(upper, lower, options?), boxes and
segments as box(name, options) / segment(name, options) over output
handles, and declaration order sets the indexes
(Script definition). Top-level fills and
style-knob params have no declaration form yet, and a generated sheet
refuses hand edits, so a package that needs either stays metadata-first
for now.
Where the rules live
Every styling reference is validated by the same schema at author, build,
install, and publish, and every refusal names the field
(outputs.0.color_by, boxes.0.color, ranges.0.edge_width); the
messages are in Common errors. The chart
engine's rendering of a declaration is the same on every host that draws
it, because the declaration, not the module, is what it reads.