Cards, frames and panels
Tables, cards, panels and widgets show one snapshot of your indicator rather than one value per bar: a styled table in a corner, a status card, a small chart in its own pane, a ladder, a feed or a meter.
Each is a declaration over outputs, string slots or frames the module already writes, and each selects one snapshot per run: levels docked on the price axis, a heatmap, a table or tiles in their own pane below the chart, and the widgets on the price pane. The per-bar vocabulary lives on other pages: marks, tints and fills on Plotting, coordinate drawings and handles on Drawing objects, the canvases drawn from cells (heatmaps, footprints, letter profiles, strike matrices) on Price canvases.
Every colour word on this page takes "#rrggbb", "#rrggbbaa" or a
theme token (theme.up, theme.down, theme.text, theme.muted,
theme.bg, theme.grid, theme.accent); a token follows the chart's
theme and re-resolves on a theme switch without a rerun. A number prints
through one format word (price, %, si, int, 0, 0.0,
0.00, 0.000, usd, auto) with decimals (0..8), signed and
unit (0..8 characters) beside it (Styling).
Marks, tints and fills
The three per-bar visuals that sit beside a card, in one line each:
- A shaped mark on some bars is
render.shape(name, { output, shape, where?, color?, color_by?, colors?, width?, location?, glow?, fill?, fill_opacity?, char?, font_family?, tooltip? }), one of ten shapes (circle,cross,triangle_up,triangle_downwith its apex down,diamond,arrow_up,arrow_down,flag,square, andchar, one character infont_family) drawn at the output's value where the gate is nonzero, so the mark sits where the output points (lowfor below the bar,highfor above, a level for an absolute price), or wherelocationputs it ("above_bar","below_bar","top","bottom") without a second output;width(px, any positive number) sizes it,glowhalos it,fill: falsekeeps the outline only,fill_opacityfades the interior andtooltipis a template shown on the mark: Plotting. - A tinted candle is
render.barcolor(name, { where, color?, color_by?, colors? }), body and wick, and a tint behind the bar isrender.bgcolorwith the same options; the two stack (a regime background under trend-colored candles). Withwidth(0.5..10 px) andline_styleabgcolordraws one vertical line per gated bar instead of a band: Plotting. - A fill between two lines is a
range()over two drawn outputs (edge_width: 0keeps the band and drops the edge lines), afill("upper", "lower", { color?, opacity?, color_by?, colors?, color_packed_by?, z? })that tints the interior only on one pane, or a one-barboxon every bar, gated bywhenfor a conditional fill: Plotting, Styling and Drawing objects.
Styled tables
render.table(name, { rows, cols, cells, position?, ...look, styles? })
draws a grid of words in a corner of the chart, and you style it in the
same declaration: column widths, fills with opacity and gradients, a
frame apart from the lines between cells, bold type, merged cells and
rounded corners. Each cell is a string slot your file writes in onBar(),
and the table shows the newest bar where every cell was written.
// A styled board in the top right corner: a title across both columns, then a trend row and a close row.
output("close_line", line, overlay, { color: "#94a3b8", description: "The close, drawn so the table has a chart" });
output("trend_state", none);
string("title", { max_bytes: 24 });
string("under_title", { max_bytes: 1 });
string("trend_label", { max_bytes: 16 });
string("trend_word", { max_bytes: 16 });
string("close_label", { max_bytes: 16 });
string("close_text", { max_bytes: 32 });
// The title spans both columns of row one, over the cell under_title holds. The label column is held at 90px and
// the value column is measured. Dotted lines sit between the rows; a solid, rounded frame sits around the table.
render.table("board", {
rows: 3,
cols: 2,
cells: ["title", "under_title", "trend_label", "trend_word", "close_label", "close_text"],
position: "top_right",
width: 220,
column_widths: [90, 0],
cell_padding: 6,
font_size: 11,
align: "right",
text_color: "#e2e8f0",
background_gradient: ["#0f172a", "#1e293b"],
background_opacity: 0.9,
border_color: "#475569",
border_width: 1,
corner_radius: 8,
grid_color: "#334155",
grid_width: 1,
grid_style: "dotted",
grid_lines: "rows",
styles: [
{ cell: "title", colspan: 2, align: "center", font_weight: "bold", gradient: ["#1d4ed8", "#0f172a"], gradient_direction: "horizontal" },
{ cell: "trend_label", align: "left" },
{ cell: "close_label", align: "left" },
{ cell: "trend_word", font_weight: "bold", color_by: "trend_state", colors: ["#7f1d1d", "#14532d"], opacity: 0.5 },
],
});
let average = new Sma(20);
function onBar(): void {
const close = bar.close();
if (isNaN(close)) return;
const mean = average.update(close);
const up = !isNaN(mean) && close > mean;
out_close_line(close);
out_trend_state(up ? 1.0 : 0.0);
str_title("Trend board");
// A covered cell is never drawn, but the table waits for a bar that wrote every cell, so write it empty.
str_under_title("");
str_trend_label("Trend");
str_trend_word(up ? "up" : "down");
str_close_label("Close");
sb_clear();
sb_auto(close);
str_close_text_sb();
}- The title spans both columns.
colspan: 2on thetitlecell covers the cell to its right. That cell keeps a slot of its own,under_title, written empty every bar: a covered cell is never drawn, but the table still waits for a bar that wrote all six cells. - Two gradients. The table blends top to bottom at 90% opacity; the title cell has its own gradient, left to right.
- A frame and a grid.
border_*is the line around the table, here solid with rounded corners.grid_*is the lines between cells, here dotted and between rows only. - A cell that follows the data. The trend cell's fill is picked by the
trend_stateoutput every bar: 0 is the first colour, 1 the second.
Table options
Every key is optional except rows, cols and cells.
| Key | What it sets | Values |
|---|---|---|
rows, cols, cells | the grid, and one slot name per cell, row by row | at most 32 rows and 8 columns; rows * cols names |
position, offset | the corner, and a nudge from it | one of the nine anchors, top_left to bottom_right; [x, y] in px, each within 200 |
width, column_widths | the table's width, and each column's | px, up to 4096; one width per column, 0 fits the words, and spare width goes to the 0 columns |
header_rows, header_row, header_column | which rows and column are headers | header_rows up to rows; header_row is the one-row switch; header_column makes the first column a header |
font_size, font_family, font_weight | the type | 6 to 64 px; a font name; "normal" or "bold" |
cell_padding, align, valign | the space and the alignment in every cell | 0 to 64 px; "left", "center", "right"; "top", "middle", "bottom" |
text_color, header_ | the text colour | a colour |
background_, header_, background_ | the fills, and how solid they are | a colour; opacity 0 to 1 |
background_, gradient_ | a fill that blends from colour to colour | 2 to 8 colours; "vertical" (the default) or "horizontal" |
border_color, border_width, border_style, corner_radius | the frame around the table | width 0 to 10; "solid", "dashed" or "dotted"; corners 0 to 32 px |
grid_color, grid_width, grid_style, grid_lines | the lines between cells | width 0 to 10; the three styles; "all", "rows", "cols" or "none" |
position_by + positions, font_size_by | a corner or a type size an output picks per bar | a list of anchors; an output holding the size |
rows_by | how many rows paint: a table whose rows come and go | a data-only output; its value on the table's bar, rounded down and held to 0..rows, is the count of rows painted from the top, the rest left out |
styles | one entry per styled cell, named by its slot | { cell, ... } with the cell keys below |
The cell keys, inside a styles entry:
| Key | What it sets |
|---|---|
colspan, rowspan | how many columns to the right, and rows below, the cell covers |
color, opacity | the cell's fill, and how solid it is |
gradient, gradient_ | the cell's own gradient (2 to 8 colours), and its own direction |
text_color, font_size, font_weight | the cell's type |
align, valign | the cell's alignment |
tooltip | the cell's hover text: a string slot, its words on the table's bar |
A table sized for its longest form can paint fewer rows, and a cell can explain itself on hover:
output("rows_shown", none); // 3 while the extra rows are on, else 2
string("label", { max_bytes: 16 });
string("value", { max_bytes: 16 });
string("note", { max_bytes: 96 });
// ... one slot per cell of rows 2 and 3 ...
render.table("board", { rows: 3, cols: 2, cells: ["label", "value", "a2", "b2", "a3", "b3"], rows_by: "rows_shown", styles: [{ cell: "value", tooltip: "note" }] });Colours that follow the data
Every colour key takes a ladder, the way render.bgcolor does. Name an
output with <key>_by, and its value on the table's bar picks an entry of
<key>s: color_by: "trend_state", colors: ["#7f1d1d", "#14532d"]. The
value is rounded down, a value outside the list picks the first entry,
and NaN keeps the static colour. An output that holds a packed colour is
named with <key>_packed_by instead. A colour reads one ladder, never
both.
Colours in a table are literals. A param.color paints an output, a
renderer's static colour or a legend entry, never a table.
A second board, with a header row and a tinted trend cell, in one literal:
output("close_line", line, overlay, { color: "#94a3b8", description: "The close, drawn so the table has a chart" });
output("trend_state", none);
string("tf_label", { max_bytes: 16 });
string("trend_word", { max_bytes: 16 });
string("close_label", { max_bytes: 16 });
string("close_text", { max_bytes: 32 });
// A 2 x 2 board: a bold label column held at 80px, numbers right aligned, hairline row separators,
// and the trend cell tinted by the trend output every bar (index 0 = down, 1 = up).
render.table("board", { rows: 2, cols: 2, cells: ["tf_label", "trend_word", "close_label", "close_text"], position: "top_right", width: 220, column_widths: [80, 0], header_rows: 1, cell_padding: 5, font_size: 11, align: "right", background_color: "#0f172a", background_opacity: 0.92, text_color: "#e2e8f0", header_background_color: "#1e293b", border_color: "#334155", border_width: 1, grid_color: "#1e293b", grid_width: 1, grid_lines: "rows", styles: [{ cell: "tf_label", align: "left", font_weight: "bold" }, { cell: "close_label", align: "left" }, { cell: "trend_word", color_by: "trend_state", colors: ["#7f1d1d", "#14532d"], opacity: 0.35 }] });
let average = new Sma(20);
function onBar(): void {
const close = bar.close();
if (isNaN(close)) return;
const mean = average.update(close);
out_close_line(close);
const up = !isNaN(mean) && close > mean;
out_trend_state(up ? 1.0 : 0.0);
str_tf_label("Trend");
str_trend_word(up ? "up" : "down");
str_close_label("Close");
sb_clear();
sb_auto(close);
str_close_text_sb();
}Limits and refusals
| Rule | What the build says |
|---|---|
| one width per column | render.table 'board' column_widths lists 1 widths for 2 columns (one per column; 0 = measured) |
| a span stays inside the grid | table 'board' cell 'title' spans 3 columns from column 1 of 2 |
| a slot that spans must fit at every cell that lists it, so a covered cell needs its own slot | table 'board' cell 'title' spans 2 columns from column 2 of 2 |
| a style names one of the table's cells | render.table 'board' styles names cell 'ghost', which is not one of its cells (title, under_title, trend_label, trend_word, close_label, close_text) |
rows_by names a data-only output | render.table 'board' rows_by names 'close_line', which is a drawn output; rows_by takes a data-only output (plot none) holding how many rows paint |
a cell's tooltip names a string slot | render.table 'board' styles cell 'title' tooltip names 'ghost', which is not a declared string slot (the cell's hover text is the slot's text) |
| a ladder comes as a pair | table 'board' cell 'trend_word' declares color_by without colors (both or neither) |
| colours are literals | render.table 'board' text_color references a param, but a param paints an output, a renderer's static colour or a legend entry, never a table; write a colour literal here |
| a gradient has 2 to 8 colours | gradient must list at least 2 color stops |
| opacity is 0 to 1 | opacity must be <= 1 |
| corners are 0 to 32 px | corner_radius must be <= 32 |
| words come from the list | option 'grid_lines' takes "all", "rows", "cols", "none" (a string literal), not 'diagonal' |
The words in a cell are plain text: never a link, never markup. A table
declared without the look keys draws the chart's plain table. The chart draws
tables with its own table engine, so a styled table shows in screenshots
like every other plot. For inline bars and sparklines in a table under the
chart, declare a panel.table over a frame instead (Frames, panels and
compact widgets, below); it takes the same look words, and up to 128 rows
by 12 columns.
Status cards
draw.card(name, { title, anchor?, offset?, z?, state_by?, rows, headline?, ...look }) 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, or 0 to 12 when it has a
headline. A pane holds 32 cards, feeds and meters across every indicator.
title takes 1 to 40 characters, 80 when it holds a {{template}}; each
row's label takes 1 to 24, 64 with a template. A template reads a
declared output as {{name}} (its declared format, else six significant
digits) or {{name:format}}, a string slot as {{name}}, and the chart's
names as {{symbol}}, {{exchange}} and {{timeframe}}; an unknown name
stays as written, and a templated output joins the card's complete-row
rule below. 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 -4096 to 4096; z is an integer.
Each row has an optional value: a literal string of 0 to 64 characters,
{ text: "slot_name" }, or { output: "output_name", format?, decimals?, signed?, unit? }. Output references always name declared numeric
outputs; text always names a declared string slot. Slot contents retain
their declared byte limit. The format words are the shared ten plus pct
(the value times 100, two decimals and %); without one a value prints
at six significant digits. usd prints a $ with two decimals under
1,000 and $1.2K, $1.2M, $1.2B, $1.2T above; auto prints six
significant digits with , thousands; price prints at the chart's
price digits; decimals overrides the word's digits, signed puts a +
on positives, unit is appended as written. A row that declares usd,
auto, int or pct with no decimals, signed or unit keeps the
text it always printed ($1234567.00, six significant digits with no
thousands separators, the rounded integer, the value times 100 at two
decimals), so an older card reads exactly as before; the shared text above
applies to the other words and to any row that declares a companion.
A row's color is a colour word or an object containing both color_by
and colors, with at least two colour words. 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.
A headline is one big number above the rows: { output | text, format?, decimals?, signed?, unit?, color?, font_size?, font_weight?, align? },
exactly one of output or text, the format and colour words as a row's,
font_size 6..64 (default 24), font_weight "bold" unless declared,
align "left", "center" or "right". rule draws a hairline between
the headline and the rows (default on when both exist) in rule_color
(default the label ink at 0.4 alpha).
The look, every key optional, absent keeping today's card:
| Keys | What they set |
|---|---|
chrome | "state" (the default: the state word, the stripe, the border in the state colour) or "plain" (no state word, no stripe, no border, a muted title, controls on hover); any key below declared beside it wins |
show_state, state_colors, stripe, controls | the state word; four colours for ok, armed, fired and error (exclusive with accent_color); the 3 px stripe; the hide and collapse glyphs "always", "hover" or "none" (with "none" the glyphs stay hidden at rest, and hovering the card still shows them, so a viewer can always hide or collapse it) |
accent_color, title_, title_font_size, title_ | the accent (default the state ink), the title's ink (default the accent), size and weight |
background_, background_, background_, gradient_ | the surface (default the theme card surface), its alpha (0..1), 2..8 stops "vertical" or "horizontal" |
border_color, border_width, border_style, corner_radius, padding | the border (default the accent, 0..10 px, "solid", "dashed" or "dotted"), the corners (0..32 px, default 6), the inset (0..24 px, default 6) |
width, opacity | a fixed width (80..1200 px; absent measures the text under a 40% cap, a declared width drops the cap and clamps to the pane width minus 16), the whole card's alpha |
font_size, font_family, font_weight, text_color, label_ | the type (6..64 px, default 12; "ui", "mono", "serif" or "rounded"; "normal", "medium" or "bold"), the value ink (default the theme text) and the label ink (default the theme muted ink) |
above_drawings | paint over every handle and user drawing, still under the legend (default under them) |
safe_area | start the offset past the chart's own chrome: the legend stack at top_left, the pane action bar at top_center and top_right, the price-axis tags on the right anchors (default the pane's 8 px edge) |
panel | "overlay" (the price pane) or "lower" (the indicator's own lower pane; an indicator with none keeps the card on price) |
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.
rows stays a literal object array, including the nested value,
colour and countdown objects. References are literal names, never
handles; variables, spreads and expressions are refused. Run reads
these options and removes the declaration before AssemblyScript
typechecks the file; no card accessor is needed. This
declaration assumes the named outputs and string slot have already been
declared:
draw.card("session_pnl", {
title: "{{symbol}} session",
anchor: "top_left",
safe_area: true,
chrome: "plain",
headline: { output: "pnl", format: "usd", signed: true, color: { color_by: "pnl_side", colors: ["theme.down", "theme.up"] } },
rows: [
{ label: "Trades", value: { output: "trades", format: "int" } },
{ label: "Win rate", value: { output: "win_rate_pct", format: "%", decimals: 1 } },
{ label: "Status", value: { text: "status_text" } },
{ label: "Next close", countdown_to: { output: "close_at_ms" } },
],
background_color: "#0f141bf2",
corner_radius: 10,
width: 220,
});draw.feed and draw.meter take the same look, placement and panel
words, plus a title; a feed also takes time_format, a meter
bar_height and track_color, and a ladder its own list (Frames, panels
and compact widgets, below).
Cards share the selection's 8 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, headline, 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 chart snapshots the module after the last closed bar and replays the revised forming bar from that snapshot as live updates arrive, 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 chart 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 an update (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.
param("fast", 9, { min: 1, max: 200 });
param("slow", 21, { min: 2, max: 400 });
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();
function onStart(): void {
fastSma = new Sma(i32(p_fast()));
slowSma = new Sma(i32(p_slow()));
}
function onBar(): void {
const close = bar.close();
const fastValue = fastSma.update(close);
const slowValue = slowSma.update(close);
const crossed = cross.update(fastValue, slowValue);
if (isNaN(slowValue)) return;
const above = fastValue >= slowValue;
out_fast(fastValue);
out_slow(slowValue);
out_high(bar.high());
out_low(bar.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();
}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 write it from onBar(): a
frame rebuilt on every bar or live tick is built in the generated frame
buffer with fb_clear(), fb_text(s), fb_str(s), fb_int(n),
fb_f64(x, decimals) and fb_num(x) and sent with
writeFrameBuffer(FRAME_BOOK), allocation-free; a small frame the module
already holds as a string goes through writeFrame(FRAME_BOOK, json). 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?, span?, ...style }) | { prices, values, colors?, ...fields }; 1..512 monotonic prices and equally sized values, null for gaps; with span: "time", { spans: [{ start, end, prices, values, colors? }] }, 1..64 spans and at most 4096 rows in all |
panel.bars, panel.line, panel.scatter, panel.histogram, panel.pie, panel.heatmap, panel.table, panel.tiles | { rows, ...chrome }; kind-specific tuples, at most 2000 rows (a table 128), empty allowed; a chrome field beside rows overrides the declared word for that run |
plot.matrix({ name, frame, dock?, columns?, ...style }) | { prices, cells, cols?, title?, highlight?, range? }; 1..128 prices, one cell row per price of 1..12 columns, at most 1536 cells (Price canvases) |
draw.ladder({ name, frame, side, divider?, title?, ...style }) | { rows, divider?, title? }; 1..256 price/value/fraction/color rows |
draw.feed({ name, frame, anchor?, offset?, z?, title?, time_format?, ...style }) | { lines, title? }; 1..50 millisecond-time/text/color rows |
draw. | { title?, panels }; 1..12 labelled panels of 0..100 candles |
Levels default to a width fraction of 0.12.
Every colour in a declaration or a frame takes #rrggbb, #rrggbbaa or a
theme token (theme.up, theme.down, theme.text, theme.muted,
theme.bg, theme.grid, theme.accent), resolved by the chart at paint
time and again on a theme switch. The levels style words (placement, bars,
labels, the point of control, hover and the frame fields they read) are on
Docked profiles.
Panels declare name, title, x (time, index, number, category),
place (below, side) and frame. Bars, line and table also declare
series (1..8, a table 1..12), each { name, color? } plus its own look.
Histograms and heatmaps use category x; a number x takes decimal row
keys (a price grid) on a line, bars or scatter. An unwritten frame leaves
its consumer absent; a malformed written frame refuses with
wrun_frame_invalid.
Panel words
Every word is optional and scoped by kind: a word on a kind it does not
list refuses by name ("<word> is only valid for font_family, whose default "ui" is the
app font (the four words are "ui", "mono", "serif", "rounded").
| Kind | Words |
|---|---|
| every kind | height_frac (0.05..0.9, the pane's share of the chart), maximize (the legend's fullscreen button), font_family, format with decimals (0..8), signed and unit (1..8 characters) for every value the panel prints |
| line, bars, histogram, scatter, pie | chrome ("box", "grid" or "none"; pie "box" or "none"), hover_card (a readout under the pointer, default on; not on pie or table), legend_style ("none", "title", "pane", "chips" on a line; bars drop "chips"; scatter and pie take "none" or "title"), x_title, y_title (1..40), y_min, y_max (the value axis), y_zero (line and scatter), x_min, x_max (a line with x index or number, scatter) |
| line, bars, histogram, scatter | badge ({ text: 1..24, color?, text_color? }): a chip on the panel's title row, filled theme.accent unless color says otherwise, its text in whichever of light or dark reads on the fill unless text_color picks one |
| line | stats_row (with maximize), smooth, points, labels (each series' last value at its end), glow, animate, legend_latest (with legend_style: "chips"), fill_mode ("flat" or "signed"), fill_, fill_, fill_fade, stroke_fade (`{ pivot: number |
| a line series | fill, fill_color, style ("line", "bars", "step"), smooth, points, labels, width (0.5..20), line_style, legend |
| bars | color_mode ("series" or "sign"), positive_color, negative_color, x_format, x_decimals, x_unit (with x index or number); a series legend |
| histogram | color, labels (each bin's count), bins (2..200 slots reserved on the axis) |
| scatter | color, guides (0..8 of `{ axis: "x" |
| pie | hole_total, hole_caption (1..24; both need hole > 0), slice_gap (0..8 px), border_color, border_width (0..10) |
| heatmap | scale ("palette" or "signed"), palette (2..8 colours), min, max, highlight ({ row?, col? }), row_title, col_title (1..24), positive_color, negative_color |
| tiles | columns (1..8), accent ("auto" or "neutral"), positive_color, negative_color |
| table | the styled-table words (position, offset, width, column_widths, cell_padding, font_size, font_weight, align, valign, text_color, header_, background_, background_, background_, gradient_, header_, border_color, border_width, border_style, grid_color, grid_width, grid_style, grid_lines, header_column) with the same ranges; a series align, format, decimals, signed, unit (the column's) |
A colour word of a panel declaration may name a setting instead of a
colour: "@<name>" for a declared param.color binds every colour word
above: a series' color and fill_color, positive_color and
negative_color, the fill_*_color pair, a heatmap's palette entries, a
histogram's or a scatter's color, the badge, marker, guide and quadrant
colours, the stroke fade's inks and the pie and table colours. The build
writes the setting's default in its place, so the sheet reads as if you
had written that colour, and the chart paints the trader's pick there
before every run; the setting's row is the colour's one control
(The Style page). A reference to anything
but a param.color is refused by name (panel.bars 'flow' positive_color references "@side", which is not a param.color), and an
@ in a title or a series name stays text. A colour inside a frame's
rows (a pie slice, a scatter dot, a tile, a table cell) is run data: the
module reads its param.color and writes the colour into the row.
param.color("bid_ink", "#22c55e", { label: "Bids" });
param.color("ask_ink", "#ef4444", { label: "Asks" });
panel.line({ name: "depth_curve", title: "Cumulative depth", x: "number", place: "below", frame: depth, chrome: "grid", series: [{ name: "Bids", color: "@bid_ink", fill: true, fill_color: "@bid_ink" }, { name: "Asks", color: "@ask_ink" }] });The rich words of a line (smooth, points, glow, animate, the
fills, the stroke fade, markers, stats_row, legend_latest,
hover_card, legend_style: "chips", a series fill_color, style: "step", smooth, points or labels) need a chrome of "grid" or
"none": the declared word, else "grid" on an unstacked index or
number line, else "box". A time line with chrome: "grid" spaces its
points by time and labels the axis in the chart's display timezone.
A rich line panel (a chrome of "grid" or "none", or a number or
index x) keeps its own value axis on the left, so the pane's right-hand
value strip stays blank: the strip belongs to series that share the
chart's time axis, and a curve over strikes or distances does not. An
indicator's panels also stack above its time-series panes: the panes that
read the time axis sit at the bottom, next to the time axis, and the
panels sit above them, each group in declaration order.
Panel frames
Beside rows, a frame may carry per-run chrome: title (1..40) and
caption (1..64) on every kind; badge ({ text, color?, text_color? })
on line, bars, histogram and scatter; markers, x_min, x_max, y_min,
y_max on a line; y_min, y_max on bars and histogram; x_min,
x_max, y_min, y_max, guides, quadrants on scatter; min, max,
highlight and summary (0..5 of { label, values }, the values following
the columns) on a heatmap; hole_text and hole_caption (1..24) on a pie.
A frame field overrides the declared word of the same name for that run,
markers replaces the declared list and badge replaces the declared
chip (a frame without the key leaves the declared chip standing); any
other key beside rows refuses by name. Frame markers take the same
words as declared ones, valign: "point" with y or series included,
so a module can point at the peak it just found.
Rows take colour words wherever they took a hex colour: a scatter row's
colour, a pie slice's, a tiles row's, a table cell's. A bars value may be
{ value, color? } to colour one bar. A tiles row may end with its own
format word ([label, value, caption?, color?, spark?, format?], with null
to skip an element). A table cell may be a styled object with text,
value (printed through the column's or the table's format when text is
absent), color, bar, spark, background_color, opacity,
gradient, text_color, font_size, font_weight, align, valign,
colspan and rowspan; a table takes 128 rows and 12 columns, the other
kinds 2000 rows.
Side placement and windows
place: "side" marks a panel as side content. On a chart it mounts below
the candles like every other panel, in a pane of its own with the same
options. What it adds is a window: the chart offers an Indicator with
side content a cell of its own, where the candles are hidden and the
Indicator's panels, tables and lower pane fill the whole height. The
window keeps the market and timeframe of its cell; that market is still
the file's first input and its clock, so the Indicator runs exactly as it
does on a chart.
How a reader opens one:
- The window icon beside the Indicator's name in the legend. It shows
while the sheet places any panel
side; once the reader has seen or used it, it moves in with the row's hover buttons. - The legend menu: Open as window (a new cell beside the chart, which keeps its Indicator), Show as window here (this cell becomes the window) and, on a window, Show chart.
- Add as window in the Indicators picker, on a package page and in search.
In a window the header carries the Indicator's name, its settings and its menu; the market chip still switches the market and the Indicator runs again. Drawing tools, price alerts and replay are off there. An Indicator with nothing to show beside the candles (price-pane plots only) gets a "Nothing to show as a window" card with Show chart. Removing the Indicator from a window that was opened into its own cell closes that cell and the layout returns to what it was; a chart turned into a window in place comes back as a chart.
The window sizes the panes itself: tables take the height their rows need
(together at most half the window), the other panels and the lower pane
share the rest. width_px (160..480) and height_px (80..800) are
accepted only with place: "side" and are reserved for a strip beside
the price pane that panels do not use today; the strip beside the price
axis is for dock: "side" matrices (Price canvases).
A window also remembers and answers:
- The pointer on a window's index or category panes (a curve over strikes,
a bars panel): a guide snaps to the nearest key with a dot per series, a
readout shows the values there, and the legend values follow the key;
leaving the pane shows the last row again.
tooltipEnabled: falseremoves the card only. - The split a reader drags between a window's panes is remembered across reloads for that Indicator; it resets when the Indicator's set of panes changes.
- Closing a window cell can be undone: Ctrl+Z / Cmd+Z brings the cell back where it was, with the Indicator.
- A draft you are editing can open as a window: press Open as window beside Run in the editor; every later Run lands in that window, and stopping the draft or closing its tab closes the cell. A draft window is not saved: after a reload the cell is an empty chart.
draw.meter({ name, label, fraction: { output }, ramp, text?, anchor?, offset?, z? }) reads the last ready numeric fraction. Ramp has 2..5
colour words; text is a short literal or { slot }. A card row's
spark: { output, window } carries its last 2..64 ready values, oldest first.
A ladder, feed or meter takes a title (1..40 characters; a ladder or
feed frame may carry its own "title" per run, and a meter title takes
the card templates), the card surface and chrome words (the background,
border, corner_radius, padding, width, opacity, the type words,
chrome, stripe, controls, accent_color, above_drawings,
safe_area and panel, with the card's ranges) and an offset of
-4096..4096 px. A ladder also takes width_frac (0.02..0.5, default
0.14), offset: [x, 0] (x 0..4096 px inward from the axis), opacity,
color (the row default), labels, format, decimals, signed,
unit (the value text), text_color, font_size, font_family,
font_weight and divider_color; its frame carries 1..256 rows with
colour words. A feed takes time_format ("HH:mm:ss", "HH:mm",
"MM-dd HH:mm" or "none"); a meter takes bar_height (2..40 px) and
track_color, and its ramp takes colour words.
draw.minicharts({ name, frame, ... }) pins a grid of up to 12 small
candle charts to the pane, the grid a multi-timeframe matrix draws. Its
options: anchor (default top_right) and x, y pixel
offsets from it (-2000..2000, default -16 and 48), z, columns 1..4
(2), panel_width 72..320 (128), panel_height 48..220 (72), gap 0..48
(8), the colours background_color, border_color, text_color,
bull_color, bear_color and wick_color, candle_style (candles,
hollow, bars, line, area), two moving averages (ma_fast_length,
ma_slow_length, each drawn only when given, with ma_fast_color,
ma_slow_color, ma_width 0.5..5 and the show_fast_ma /
show_slow_ma switches) and show_change / show_volume for the change
badge and the volume strip, which the chart computes from the candles.
Every option takes a literal. The colours, the switches, the average
lengths, ma_width and candle_style also take an "@<setting>"
reference, so a setting changes that option: a param.color for a
colour, a param.bool for a switch, a param.int with min 1 or more
for a length, a param.number or param.int within 0.5 to 5 for
ma_width, a param.choice over the style words for candle_style.
The setting's default draws until it is changed, and one setting may
drive several options. The
frame carries the title and each panel's label and candles, [time_ms, open, high, low, close, volume] oldest first, volume null for none.
Build it with the grid's writers: mc_begin(title) clears the frame
buffer and opens the grid, mc_panel(label) opens a panel,
mc_bar(openSec, open, high, low, close, volume) appends a candle (its
open in epoch seconds), mc_candles(label, list, bars, formingSec, open, high, low, close, volume) writes a whole panel from a CandleList of
closed candles plus the forming candle (NaN for none; it is drawn last
when it opens after the newest closed one, bars candles at most), and
writeMiniCharts(FRAME_GRID) sends it. mc_leg(label, list, bars, in_x_view(), in_x_cells(), bar.time()) reads that forming candle off a
candles input declared with view: "forming", the timeframe's live
candle on the live bar. A title of {{symbol}} reads as the chart's
market. A grid with no reference gets settings of its own: the look's
colours, candle style, averages and switches ("Panel background", "Bull
candle", "Fast MA length", "Candle style", "Show volume", ...), each
defaulting to the value written here; write colours as "#rrggbb" or
"#rrggbbaa".
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. An inset also takes
color, colors ([up, down] by sign, or a ladder beside color_by)
and opacity.
The build extracts these object declarations before AssemblyScript type
checking. frame() returns its slot index; generated FRAME_<NAME>
constants provide the same index. The frame builder exists only when
frames are declared. Its UTF-8 scratch is one StaticArray sized to the
largest declared max_bytes, reserved during module initialization and
reused by every fb_* append and by writeFrame, so repeated frame writes
never grow guest memory; the string handed to writeFrame is the caller's
own allocation, which is why a frame rebuilt per bar builds through fb_*
instead. fb_text appends raw JSON text (punctuation, keys, words),
fb_str a quoted and escaped JSON string, fb_int an integer, fb_f64 a
number with a fixed decimal count, fb_num a double's shortest spelling;
fb_f64 and fb_num write null for a non-finite value, JSON's gap. Both
senders pass the bytes the frame requires, so an oversized frame refuses
host-side by the slot's max_bytes, never truncated.
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. safe_area: true on a handle declaration
(handles.box({ anchor: "top_right", safe_area: true })) starts the
offsets past the chart's own chrome instead: the legend stack at the top
left, the pane action bar at the top centre and right, the price-axis tags
on the right.
After a handle's set, call .anchor(ANCHOR_TOP_LEFT) or
style.anchor(handle, ANCHOR_TOP_LEFT) with an ANCHOR_* constant. ANCHOR_CHART restores chart
coordinates and clears a declared default until the handle is recreated.
These helpers go through style 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; ALIGN_DEFAULT restores centred text
and clears a declared default. These use prop 8, integer values 0..3, on
labels and box text (a box takes align, valign and padding too);
other kinds refuse as wrun_draw_prop_unsupported.
Docked profiles
plot.levels({ name, frame, dock, ... }) draws a profile docked on the
price axis from a frame the module writes each run: one bar per price,
its length the row's value. The required words and today's optional ones
(width_frac, poc, labels, color) keep their meaning; everything
below is opt-in, erased before the file compiles, and takes "@<param>"
on any style key so the Style page can change it without a rerun of your
code (The Style page).
Where the profile sits:
| Word | Value | What it does |
|---|---|---|
panel | "overlay" (default), "lower" | on the price pane, or on the indicator's own lower pane, where the frame's prices read on that pane's value scale (an RSI or CVD profile beside the oscillator); "lower" needs a lower pane |
behind_candles | boolean (default false) | the bar fills draw under the candles; labels, the POC, the value-area lines, borders and the outline stay above; refused on "lower", beside gradient, or with shape: "outline" |
span | "pane" (default), "time" | docked on the pane edge, or one profile per time span the frame carries (Price canvases) |
offset | [x, 0], x 0..4096 px | inward from the pane edge, or from the neighbour named by beside |
beside | another declared level's name | dock right inside that level (open interest beside gamma); refused when it names itself, an undeclared level, a level on another panel or dock side, or closes a cycle |
width_px | 16..600 px | a fixed dock width instead of width_frac; refused beside it |
scale, scale_max | "own" (default), "shared", "fixed"; a number > 0 | "shared" scales every "shared" level of the indicator by the largest row among them; "fixed" fills the dock at scale_max (required there, refused otherwise) and clamps longer rows; the frame's scale_max overrides it per run |
thickness_px | 2..40 px | the tallest a bar may be, centred on its price |
step | number > 0 (price units) | every row spans its price plus and minus half a step (a strike ladder) |
How the bars paint:
| Word | Value | What it does |
|---|---|---|
color, opacity | a colour word; 0..1 (default 0.65) | the bar colour and the alpha every bar colour's own alpha is multiplied by; opacity: 1 paints the exact colour |
baseline | "edge" (default), "center" | "center" puts zero mid-dock: positive values grow one way, negative the other (a net gamma profile); under "edge" a negative row draws nothing |
series | 1..8 of { name, color }, unique names | one row split into stacked segments (calls and puts), fed by the frame's series arrays |
gradient, gradient_ | 2..8 colour words; "horizontal" (default) or "vertical" | fading bars: horizontal runs from the bar's root to its tip; a row or segment with its own colour keeps it and takes the stops' alpha profile; refused beside behind_candles |
border_color, border_width, border_style | a colour word; 0..10 px (1 when a colour is set, else 0); "solid", "dashed", "dotted" | an outline on every bar and segment |
shape | "bars" (default), "outline" | "outline" draws no fills, one stepped line along the bar tips per side, broken at null rows |
outside_opacity | 0..1 (default 0.5) | how far rows outside the frame's value area fade |
value_, value_, value_, value_ | a colour word (default the level colour); 0..10 px (default 1, 0 draws no lines); "dashed" unless declared; boolean | the two lines across the dock at the value area's low and high prices, and the VAH and VAL tags beside them |
Labels, the point of control and hover:
| Word | Value | What it does |
|---|---|---|
labels | boolean | the row labels; a frame with labels_text turns them on unless the declaration says false |
format, decimals, signed, unit | the shared number words | how a row without its own text (and its hover value) prints; without a format word the value prints as written |
text_color, font_size, font_weight, font_family | a colour word (default the row colour at full alpha); 6..64 px (default 10); the weight word; "ui", "mono", "serif", "rounded" (default the chart axis font) | the label type |
label_place | "outside" (default), "inside", "axis" | beyond the bar tip, inside the bar at its tip, or inside at its root |
hover | boolean (default false) | hovering a row opens the chart's hover card: the price, the value, one line per series, the row's tooltip |
poc, poc_color, poc_width, poc_line_style, poc_extend, poc_label | boolean; a colour word (default the POC row colour); 1..10 px; the line style; "dock" (default) or "pane" (across the whole pane); boolean | the point of control line (the largest row unless the frame names poc_price) and its tag (POC and the price, or the frame's poc_label_text) |
The frame carries the data and may carry per-run chrome beside prices,
values and colors:
| Field | Value | What it does |
|---|---|---|
prices, values, colors | 1..512 strictly ordered numbers; as many numbers or null (a gap that keeps its band); colour strings (#rrggbb, #rrggbbaa, rgb(), rgba(), a theme token; anything else falls back to the level colour) | the rows |
labels_text | one string of 0..24 characters or null per row | that text on the row; "" leaves the row bare; null prints the formatted value |
series | one array of numbers or null per declared series, each the length of prices | the segments, stacking outward from the baseline in series order; values still names the row for its label and hover; never beside colors |
lows, highs | one number or null per row, both arrays together, low below high | a row's own price band; a null pair falls back to step, else to the neighbours' midpoints |
tooltips | one string of 0..64 characters or null per row | the hover card's hint |
value_area | [low, high] prices | the value area: rows outside dim, two lines across the dock |
poc_price, poc_label_text | a number; 1..24 characters | the price the POC line marks; the tag's text (turns the tag on) |
scale_max | a number > 0 | the fixed scale for this run; scale: "fixed" only |
spans | the time-anchored shape | with span: "time", in place of the fields above |
A frame whose shape disagrees with the declaration (a series count that
differs, colors beside series, a scale_max on a level whose scale
is not "fixed", a spans frame on span: "pane") refuses the run as
wrun_frame_invalid. A two-sided profile of calls and puts, net at the
baseline, with a value area and a tagged point of control:
param.choice("side", ["right", "left"], "right");
param.color("call_ink", "#38bdf8");
param.color("put_ink", "#a78bfa");
const gexFrame = frame("gex_levels", { max_bytes: 32768 });
plot.levels({ name: "gex", frame: gexFrame, dock: "@side", baseline: "center", width_frac: 0.18, series: [{ name: "Calls", color: "@call_ink" }, { name: "Puts", color: "@put_ink" }], labels: true, format: "usd", label_place: "inside", poc: true, poc_extend: "pane", poc_label: true, hover: true, outside_opacity: 0.4 });{
"prices": [62000, 62500, 63000, 63500],
"values": [41000, -18500, 25000, 9000],
"series": [
[52000, 11000, 30000, 12000],
[-11000, -29500, -5000, -3000]
],
"labels_text": ["C 52.0K / P 11.0K", null, "", null],
"tooltips": ["Calls 1,240 OI / Puts 860 OI", null, null, null],
"value_area": [62000, 63000],
"poc_price": 62000,
"poc_label_text": "Max pain"
}A session volume profile the module bins itself, docked right: rows six
pixels thick, the value area marked by dotted amber lines with their VAH
and VAL tags, rows outside it faded, and the point of control drawn
across the dock.
const sessionFrame = frame("session_rows", { max_bytes: 16384 });
plot.levels({
name: "session_profile",
frame: sessionFrame,
dock: "right",
width_frac: 0.2,
color: "#38bdf8",
thickness_px: 6,
outside_opacity: 0.35,
value_area_color: "#f59e0b",
value_area_width: 1.5,
value_area_line_style: "dotted",
value_area_labels: true,
poc: true,
});Open interest by price with every bar outlined in a dotted hairline, the fills at half strength, and a strong dashed point of control across the whole pane with its tag on: the look for a profile that sits under other overlays and still has to read.
const oiRows = frame("oi_rows", { max_bytes: 16384 });
plot.levels({
name: "oi_profile",
frame: oiRows,
dock: "left",
width_frac: 0.15,
color: "#a78bfa",
opacity: 0.5,
border_color: "#c4b5fd",
border_width: 1,
border_style: "dotted",
poc: true,
poc_color: "#f472b6",
poc_width: 3,
poc_line_style: "dashed",
poc_extend: "pane",
poc_label: true,
});A liquidation map docked right inside that open interest profile, on a fixed scale so a new cluster never rescales the dock, each row spanning the price band its frame gives it instead of the midpoints between neighbours.
const oiRows = frame("oi_rows", { max_bytes: 16384 });
const liqRows = frame("liq_rows", { max_bytes: 16384 });
plot.levels({ name: "oi_profile", frame: oiRows, dock: "left", width_frac: 0.15, color: "#a78bfa", format: "si" });
plot.levels({
name: "liq_map",
frame: liqRows,
dock: "left",
beside: "oi_profile",
width_frac: 0.08,
color: "#f97316",
scale: "fixed",
scale_max: 50000000,
format: "usd",
hover: true,
});{
"prices": [61250, 61750, 62250, 62750],
"values": [12500000, 48000000, 9000000, 31000000],
"lows": [61000, 61500, 62000, 62500],
"highs": [61500, 62000, 62500, 63000],
"scale_max": 60000000
}Session profiles from cells
The chart can also draw a profile itself from volume_profile cells, one
per session, through plot.profile
(Price canvases); it styles
the same parts in its own words. Here each row splits buy and sell volume,
rows outside the value area take a faded pair of colours, the point of
control is thick, the three levels stay drawn until price trades through
them, and hovering a cell reads it out.
input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 8192 });
plot.profile({
name: "session_vp",
cells: "profile",
span: "session",
session: "1d",
mode: "split_bar",
buy_color: "#34d399",
sell_color: "#f87171",
row_height: 25,
value_area: 0.7,
outside_buy_color: "#34d39955",
outside_sell_color: "#f8717155",
poc: true,
poc_width: 3,
naked: true,
level_labels: true,
cell_hover: true,
});The caps a profile meets (four levels, 512 rows, 64 spans and 4096 rows across them, 96 KiB per frame) are on Limits.
Panel rows, kind by kind
A panel draws a small chart of its own in a pane below the chart
(place: "below"), or as side content ("side") that mounts below on a
chart and fills a window (Side placement and
windows). The file writes the panel's rows
into a frame, usually once, on the newest bar, and each kind reads its
rows in its own shape:
| Panel | One row | Options of its own |
|---|---|---|
panel.bars, panel.line | [key, v1, ..., vN]: one value per series, null for a gap | series (1 to 8, required), stacked; bars also orientation ("vertical" or "horizontal") |
panel.scatter | [key, x, y, size?, color?, label?]: a dot at (x, y), the label at most 24 characters | none |
panel.histogram | [label, count]: a bin's label (1 to 24 characters) and its count | bins (2 to 200), orientation; x: "category" only |
panel.pie | [name, value, color?]: one slice, at most 24 | hole (0 to 0.8) |
panel.heatmap | [x key, y key, value]: one cell, null for an empty one | x: "category" only |
panel.table | one cell per series and no key: words (at most 64 characters), a number, or a styled cell { text?, value?, color?, bar?, spark?, background_; at most 128 rows | series (1 to 12, required) |
panel.tiles | [label, value, caption?, color?, spark?]: one tile, at most 24 | none |
Every panel also takes name, title, x, place and frame, and the
look words of its kind (Frames, panels and compact widgets, above). x
says what a key is: "time" an epoch second, "index" a whole number,
"number" a decimal (a price grid), "category" a word of 1 to 64
characters. A panel holds at most 2,000 rows unless the table says fewer
(128), and an empty rows list draws an empty panel. A color is a colour
word: a hex literal such as "#16a34a", "#rrggbbaa" or a theme token.
Volume by weekday as stacked bars, one bar per day of the week:
// Volume by weekday in a pane below the chart: one bar per day of the week, up-bar and down-bar volume stacked.
output("weekday", none, overlay, { description: "The bar's weekday, 0 Sunday to 6 Saturday, UTC" });
const days = frame("days", { max_bytes: 1024 });
panel.bars({ name: "by_weekday", title: "Volume by weekday (UTC)", x: "category", place: "below", frame: days, stacked: true, series: [{ name: "Up bars", color: "#16a34a" }, { name: "Down bars", color: "#dc2626" }] });
const NAMES: StaticArray<string> = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const upVolume = new StaticArray<f64>(7);
const downVolume = new StaticArray<f64>(7);
let clock = new Clock();
function onBar(): void {
const close = bar.close();
if (isNaN(close)) return;
clock.update(bar.time());
const day = clock.weekday();
if (close >= bar.open()) upVolume[day] += bar.volume();
else downVolume[day] += bar.volume();
out_weekday(f64(day));
if (!bar.isLast()) return;
// One row per weekday: [category, up volume, down volume], one value per series.
fb_clear();
fb_text('{"rows":[');
for (let i = 0; i < 7; i++) {
if (i > 0) fb_text(",");
fb_text("[");
fb_str(NAMES[i]);
fb_text(",");
fb_f64(upVolume[i], 0);
fb_text(",");
fb_f64(downVolume[i], 0);
fb_text("]");
}
fb_text("]}");
writeFrameBuffer(FRAME_DAYS);
}- One value per series.
["Mon", up, down]carries the two declared series in their order;stacked: trueputs them in one bar. - Written once, on the newest bar. The sums grow on every bar, and the
frame is built on
bar.isLast()with thefb_*writers, which never allocate. - The last write wins. The panel shows what the newest bar wrote; a
frame never written leaves the panel off the chart, and a row in the
wrong shape stops the run with
wrun_frame_invalid.
The looks below take the words of the Panel words table, kind by kind;
every declaration assumes onBar() writes the named frame.
Line panels
A gamma exposure curve: net dealer gamma by strike, filled green where it
is positive and red where it is negative, the fill fading away from the
line, the spot strike marked with a badge that prints the value there, a
chip on the title row naming the regime, a callout pinned to the curve at
the peak strike, and a stats row for the maximised view. Reach for the
signed fill whenever a curve's sign is the story. The badge word is the
chip: its text, an optional fill (theme.accent when absent) and an
optional text colour (light or dark, whichever reads on the fill, when
absent). A marker with valign: "point" is the callout: instead of a line
down the pane it draws a dot on the curve with a short leader to its
label, anchored where the named series crosses x (the first series
when series is absent), or at an exact value when y is given.
const gexRows = frame("gex_rows", { max_bytes: 16384 });
panel.line({
name: "gex_curve",
title: "Gamma exposure by strike",
x: "number",
place: "below",
frame: gexRows,
height_frac: 0.3,
chrome: "grid",
maximize: true,
stats_row: true,
y_zero: true,
fill_mode: "signed",
fill_positive_color: "#22c55e",
fill_negative_color: "#ef4444",
fill_fade: true,
badge: { text: "LONG GAMMA", color: "#22c55e", text_color: "#0b0d12" },
markers: [
{ x: "spot", label: "Spot", badge: true, show_value: true },
{ x: 64000, label: "Peak", valign: "point", series: "Net gamma", color: "#22c55e" },
],
series: [{ name: "Net gamma", color: "#e2e8f0", width: 2, smooth: true }],
});The frame carries the rows and, per run, the chrome that moves: a caption
naming the expiry window, a fixed value axis so the curve does not rescale
on every tick, the badge (a frame badge replaces the declared chip for
that run, so the regime flips to "SHORT GAMMA" in red when the module
finds dealers short), and the markers. The frame's list replaces the
declared one, so the spot marker is written again beside the flip strike,
which the module finds where the curve crosses zero and washes down the
pane in its colour, and two callouts pin the peak and the trough: the peak
reads its height off the Net gamma series, the trough is pinned at an
exact y.
{
"title": "Gamma exposure by strike",
"caption": "Dealer gamma per 1% move, 0 to 7 days to expiry",
"y_min": -4000000,
"y_max": 6000000,
"badge": {
"text": "SHORT GAMMA",
"color": "#ef4444",
"text_color": "#f4f4f4"
},
"markers": [
{ "x": "spot", "label": "Spot", "badge": true, "show_value": true },
{
"x": 63500,
"label": "Flip",
"color": "#f59e0b",
"line_style": "dashed",
"valign": "middle",
"wash": true
},
{
"x": 64000,
"label": "Peak",
"valign": "point",
"series": "Net gamma",
"color": "#22c55e",
"show_value": true
},
{
"x": 61000,
"label": "Trough",
"valign": "point",
"y": -2500000,
"color": "#ef4444"
}
],
"rows": [
[60000, -1200000],
[61000, -2500000],
[62000, -800000],
[63000, 1500000],
[64000, 3900000],
[65000, 2200000]
]
}A cumulative depth curve, bids and asks by distance from the mid price: both axes titled, the x axis printed as a percentage with two decimals, the value axis pinned from 0 to 500 so the two sides share one scale across runs, and chips in the legend showing each side's latest value.
const depth = frame("depth", { max_bytes: 16384 });
panel.line({
name: "depth_curve",
title: "Cumulative depth",
x: "number",
place: "below",
frame: depth,
x_title: "Distance from mid",
y_title: "Size (BTC)",
x_format: "auto",
x_decimals: 2,
x_unit: "%",
y_min: 0,
y_max: 500,
legend_style: "chips",
legend_latest: true,
format: "0.0",
series: [
{ name: "Bids", color: "#22c55e" },
{ name: "Asks", color: "#ef4444" },
],
});A liquidation density curve around spot, glowing and animated as it updates, its stroke fading toward the edges, red for the longs below spot and green for the shorts above, with a tint under the line in the same ink. Reach for the fade when the pivot is the point and the tails are context.
const liqs = frame("liqs", { max_bytes: 16384 });
panel.line({
name: "liq_density",
title: "Liquidation density",
x: "number",
place: "below",
frame: liqs,
glow: true,
animate: true,
stroke_fade: { pivot: "spot", edge_opacity: 0.15, left_color: "#ef4444", right_color: "#22c55e" },
series: [{ name: "Liquidations", color: "#f59e0b", width: 1.5, fill: true, fill_color: "#f59e0b33" }],
});Bars
Funding by venue as bars coloured by sign, the axis pinned from -0.05% to 0.05% so venues compare across runs, and a readout under the pointer.
const funding = frame("funding", { max_bytes: 4096 });
panel.bars({
name: "funding_by_venue",
title: "8h funding by venue",
x: "category",
place: "below",
frame: funding,
color_mode: "sign",
positive_color: "#22c55e",
negative_color: "#ef4444",
y_title: "Funding",
y_min: -0.05,
y_max: 0.05,
hover_card: true,
format: "0.000",
unit: "%",
series: [{ name: "Funding", color: "#94a3b8" }],
});Scatter
Each venue as a dot of funding against open interest change: the plane cut into four named quadrants at zero, a guide where funding turns rich, the funding axis pinned so the quadrants keep their place, venue names on the dots with leader lines where they would collide, and every dot trailing its last positions, the trail fading as it ages.
const venues = frame("venues", { max_bytes: 8192 });
panel.scatter({
name: "funding_vs_oi",
title: "Funding against OI change",
x: "number",
place: "below",
frame: venues,
x_title: "8h funding",
y_title: "OI change, 24h",
x_min: -0.05,
x_max: 0.1,
labels: true,
label_overlap: "leader",
quadrants: {
x: 0,
y: 0,
colors: ["#22c55e22", "#3b82f622", "#ef444422", "#f59e0b22"],
labels: ["Longs crowding", "Shorts crowding", "Shorts unwinding", "Longs unwinding"],
},
guides: [{ axis: "x", value: 0.03, label: "Rich", color: "#f59e0b", line_style: "dashed" }],
trails: true,
trail_width: 1.5,
trail_fade: true,
});Pie
Open interest by expiry as a ring: the hole prints the total with a caption under it, a small gap between slices and a thin border on each in the pane's own ink.
const expiries = frame("expiries", { max_bytes: 4096 });
panel.pie({
name: "oi_by_expiry",
title: "Open interest by expiry",
x: "category",
place: "below",
frame: expiries,
hole: 0.55,
hole_total: true,
hole_caption: "Total OI",
slice_gap: 2,
border_color: "#0f172a",
border_width: 1,
format: "usd",
legend_style: "title",
});The frame's rows are [name, value, color?] slices; for a run where the
hole should say something other than the total, the frame carries its own
words, hole_text with a hole_caption that replaces the declared one.
{
"hole_text": "$1.92B",
"hole_caption": "Expiring this week",
"rows": [
["04 Oct", 820000000, "#38bdf8"],
["11 Oct", 460000000, "#818cf8"],
["25 Oct", 390000000, "#a78bfa"],
["27 Dec", 250000000, "#c4b5fd"]
]
}Heatmap
Funding by venue and hour as a signed heatmap, positive cells warm and negative cells cool around a neutral zero, the axes titled and a readout under the pointer. Reach for the signed scale when the data has a meaningful zero; a palette scale suits ranks and counts.
const hours = frame("hours", { max_bytes: 16384 });
panel.heatmap({
name: "funding_heat",
title: "Funding by venue and hour",
x: "category",
place: "below",
frame: hours,
scale: "signed",
positive_color: "#f97316",
negative_color: "#38bdf8",
row_title: "Venue",
col_title: "Hour (UTC)",
hover_card: true,
format: "0.000",
unit: "%",
});The frame's rows are [hour, venue, value] cells; beside them it can light
the live hour's column and add summary rows under the grid, their values
following the columns in the order the rows first named them.
{
"caption": "8h funding, the last 7 days",
"highlight": { "col": "16:00" },
"summary": [{ "label": "Mean", "values": [0.0047, 0.0037] }],
"rows": [
["08:00", "Binance", 0.01],
["08:00", "Bybit", 0.0062],
["08:00", "OKX", -0.002],
["16:00", "Binance", 0.0071],
["16:00", "Bybit", 0.0045],
["16:00", "OKX", -0.0004]
]
}Tiles
Tiles for the headline numbers, three across, each tile's accent following the sign of its value: green ink on a positive flow, red on a negative one, the numbers printed compact and signed.
const summary = frame("summary", { max_bytes: 4096 });
panel.tiles({
name: "flow_tiles",
title: "24h flow",
x: "category",
place: "below",
frame: summary,
columns: 3,
accent: "auto",
positive_color: "#22c55e",
negative_color: "#ef4444",
hover_card: true,
format: "si",
signed: true,
unit: "BTC",
});Table
A table beside the chart with its first column as the row headers: a dashed frame, muted header ink, hairlines between rows, every cell centred vertically, the two value columns right-aligned and each formatted its own way.
const board = frame("board", { max_bytes: 8192 });
panel.table({
name: "venue_board",
title: "Venue board",
x: "category",
place: "side",
frame: board,
width_px: 320,
header_column: true,
header_text_color: "#94a3b8",
header_background_color: "#1e293b",
text_color: "#e2e8f0",
border_color: "#475569",
border_width: 1,
border_style: "dashed",
grid_color: "#334155",
grid_width: 1,
grid_lines: "rows",
valign: "middle",
cell_padding: 6,
column_widths: [96, 0, 0, 0],
series: [
{ name: "Venue" },
{ name: "Market" },
{ name: "OI", align: "right", format: "usd" },
{ name: "Funding", align: "right", format: "0.000", unit: "%" },
],
});The frame's cells are words, numbers or styled cells: a styled cell may
span rows, so a venue's name sits once beside its two markets, and a
value cell with bar draws an inline bar behind the number, its length
0 to 1 of the cell; caption prints a line under the table.
{
"caption": "USDT perpetuals, 15:04 UTC",
"rows": [
[
{
"text": "Binance",
"rowspan": 2,
"valign": "middle",
"font_weight": "bold"
},
"BTCUSDT",
{ "value": 4210000000, "bar": 1 },
0.01
],
["", "ETHUSDT", { "value": 1830000000, "bar": 0.43 }, 0.0082],
[
{ "text": "Bybit", "font_weight": "bold" },
"BTCUSDT",
{ "value": 2640000000, "bar": 0.63 },
-0.0041
]
]
}Ladder, feed and meter
Three small widgets sit on the price pane. A ladder is a column of bars at their prices along one side, a feed is a few lines of text with their times, and a meter is one bar filled from 0 to 1.
| Widget | Declare | Write |
|---|---|---|
| Ladder | draw.ladder({ name, frame, side, divider?, title?, ...style }), side "left" or "right" | a frame { rows, divider?, title? }: 1 to 256 rows [price, value, fraction, color?], the fraction (0 to 1) the bar's length; divider { label, price } marks one price, and a divider in the frame replaces the declared one |
| Feed | draw.feed({ name, frame, anchor?, offset?, z?, title?, time_format?, ...style }) | a frame { lines, title? }: 1 to 50 lines [time, text, color?], the time in epoch milliseconds and the text at most 80 characters |
| Meter | draw.meter({ name, label, fraction: { output }, ramp, text?, anchor?, offset?, z?, title?, bar_height?, track_color?, ...style }) | no frame: the meter reads the output on the newest ready bar, a fraction from 0 to 1, colored along ramp (2 to 5 colors); text is a literal or { slot } |
The title, time_format, bar_height and track_color words, and the
card surface and chrome a widget shares, are listed under Frames, panels
and compact widgets above; a ladder's frame rows and a meter's ramp take
colour words.
The last bars' volume by price as a ladder on the right of the pane:
// The last bars' volume by price as a ladder on the right of the pane: eight rows from the window's low to its high, the busiest in amber.
param.int("window", 100, { min: 10, max: 500, label: "Bars in the window" });
output("close_line", line, overlay, { color: "#94a3b8", description: "The close" });
const rows = frame("rows", { max_bytes: 2048 });
draw.ladder({ name: "volume_by_price", frame: rows, side: "right" });
const MAX = 500;
const ROWS = 8;
const closes = new StaticArray<f64>(MAX);
const volumes = new StaticArray<f64>(MAX);
const binned = new StaticArray<f64>(ROWS);
let window: i32 = 100;
let head: i32 = 0;
let count: i32 = 0;
function onStart(): void {
window = i32(p_window());
}
function onBar(): void {
const close = bar.close();
if (isNaN(close)) return;
out_close_line(close);
closes[head] = close;
volumes[head] = bar.volume();
head = (head + 1) % window;
if (count < window) count += 1;
if (!bar.isLast()) return;
let lo = Infinity;
let hi = -Infinity;
for (let i = 0; i < count; i++) {
lo = Math.min(lo, closes[i]);
hi = Math.max(hi, closes[i]);
}
if (!(hi > lo)) return;
const step = (hi - lo) / f64(ROWS);
for (let r = 0; r < ROWS; r++) binned[r] = 0.0;
for (let i = 0; i < count; i++) {
const r = i32(Math.min(f64(ROWS - 1), Math.floor((closes[i] - lo) / step)));
binned[r] += volumes[i];
}
let busiest = 0.0;
for (let r = 0; r < ROWS; r++) busiest = Math.max(busiest, binned[r]);
if (busiest <= 0.0) return;
// One row per price band: [price, volume, the bar's length 0 to 1, color]. The divider marks the last close.
fb_clear();
fb_text('{"rows":[');
for (let r = 0; r < ROWS; r++) {
if (r > 0) fb_text(",");
fb_text("[");
fb_num(lo + step * (f64(r) + 0.5));
fb_text(",");
fb_f64(binned[r], 0);
fb_text(",");
fb_f64(binned[r] / busiest, 3);
fb_text(",");
fb_str(binned[r] == busiest ? "#f59e0b" : "#64748b");
fb_text("]");
}
fb_text('],"divider":{"label":"Last","price":');
fb_num(close);
fb_text("}}");
writeFrameBuffer(FRAME_ROWS);
}- Rows at prices. Each row is a price band's middle price, its volume, its bar's length against the busiest band, and its color.
- The divider.
"divider": { "label": "Last", "price": close }in the frame marks the last close. - Sized once. The window lives in two
StaticArrays made at module start, so nothing allocates per bar.
An RSI meter in the corner, and a feed of the RSI's last five crossings of 70 and 30:
// An RSI meter in the top-left corner, and a feed of the last five times the RSI crossed 70 or 30.
param.int("length", 14, { min: 2, max: 100, label: "RSI length" });
output("rsi", line, lower, { color: "#a855f7", description: "RSI" });
output("rsi_fraction", none, lower, { description: "The RSI as a fraction, 0 to 1: the meter's fill" });
string("rsi_text", { max_bytes: 8 });
draw.meter({ name: "rsi_meter", label: "RSI", fraction: { output: "rsi_fraction" }, ramp: ["#3b82f6", "#64748b", "#ef4444"], text: { slot: "rsi_text" }, anchor: "top_left" });
const events = frame("events", { max_bytes: 2048 });
draw.feed({ name: "rsi_events", frame: events, anchor: "bottom_left" });
const KEEP = 5;
const times = new StaticArray<f64>(KEEP);
const ups = new StaticArray<bool>(KEEP);
let head: i32 = 0;
let count: i32 = 0;
let rsi = new Rsi(14);
let prev: f64 = NaN;
function onStart(): void {
rsi = new Rsi(i32(p_length()));
}
function onBar(): void {
const value = rsi.update(bar.close());
if (isNaN(value)) return;
out_rsi(value);
out_rsi_fraction(value / 100.0);
sb_clear();
sb_f64(value, 1);
str_rsi_text_sb();
// A crossing joins the feed; the oldest of the five leaves.
const above = prev <= 70.0 && value > 70.0;
const below = prev >= 30.0 && value < 30.0;
if (above || below) {
times[head] = bar.time() * 1000.0;
ups[head] = above;
head = (head + 1) % KEEP;
if (count < KEEP) count += 1;
}
prev = value;
if (!bar.isLast() || count == 0) return;
// One line per crossing, newest first: [time in milliseconds, text, color].
fb_clear();
fb_text('{"lines":[');
for (let k = 0; k < count; k++) {
const i = (head - 1 - k + KEEP) % KEEP;
if (k > 0) fb_text(",");
fb_text("[");
fb_f64(times[i], 0);
fb_text(",");
fb_str(ups[i] ? "RSI crossed above 70" : "RSI crossed below 30");
fb_text(",");
fb_str(ups[i] ? "#ef4444" : "#3b82f6");
fb_text("]");
}
fb_text("]}");
writeFrameBuffer(FRAME_EVENTS);
}- The meter reads an output.
fraction: { output: "rsi_fraction" }takes the RSI over 100 on the newest bar, andtext: { slot: "rsi_text" }prints the reading on it. - The feed is a frame. Each line is
[time, text, color], the timebar.time() * 1000because a feed counts milliseconds, newest first. - Nothing to show, nothing written. The feed is written only once a crossing exists; until then the frame stays unwritten and the feed is off the chart.