Marks, spans and rays
Three declarations draw many small things on each bar, in one batch:
plot.marks (bubbles, dots, rings or squares, each at a price with a
quantity), plot.spans (boxes, each a price range over a run of bars)
and plot.rays (segments, each from one price to another over a run of
bars).
A section holds up to 64 records a bar. Each record is written on its own bar and rides that bar's outputs, so the chart draws and hovers a whole section at once: there are no ids to keep, nothing to delete and no live-object cap to stay under. A span or a ray may also run between two times your script knows, starting before the bar that writes it (Timed spans and rays). Anything drawn on most bars belongs here. An object that must move, restyle or vanish on a later bar is a handle instead, and so are a few objects across a chart (a session's box, a broken zone, a swing-to-swing line): a section spends its slots on every bar, record or not, and for tens of objects a chart handles measured cheaper to paint.
// One mark a bar on the close, sized by the bar's volume: an up bar takes the second colour.
plot.marks({ name: "prints", slots: 1, size: { law: "sqrt", unit: 1000, scale: 5 }, colors: ["theme.down", "theme.up"], tooltip: "{{qty}} at {{price}}" });
function onBar(): void {
const side = bar.close() >= bar.open() ? 1 : 0;
out_prints_mark(0, bar.close(), bar.volume(), side, 1, 0);
}How a section works
plot.marks(options), plot.spans(options) and plot.rays(options)
are top-level declarations: an object literal the build reads and
removes before the file compiles, like every plot declaration. name
and slots are required; slots (1..64, a number written out) is how
many records the section holds on one bar. The build generates a record
writer for each section, and out_<name>_clear(), which empties every
slot:
| Kind | Record writer | A slot holds |
|---|---|---|
| marks | out_ | the price, the quantity and the integer fields |
| spans | out_ | the top, the bottom, the length and the integer fields |
| rays | out_ | the two prices, the length and the integer fields |
| timed spans | out_ | the top, the bottom, the start and end times and the integer fields |
| timed rays | out_ | the two prices, the start and end times and the integer fields |
A spans or rays section is timed when your file calls its _at writer
and runs a length of bars when it calls the other one; a section takes
one of the two, and calling both is refused by name.
- Slots.
slotruns from 0 toslots - 1; any other slot stops the run with the section's name ("marks 'prints': a slot outside 0..0 was written"). - Empty slots. A slot not written on a bar is empty and draws
nothing. Every bar starts empty, so a bar that writes no record draws
none, and a forming bar the chart evaluates again on a live update
shows what the last evaluation wrote: four records, then one, leaves
one. A file with the four functions (
init,state,finalize,reset) callsout_<name>_clear()infinalize()before its writes, since every output there is written on every row. - The integer fields.
side(0..3) picks a mark's colour,fills(0..65535) is a count the hover readout prints, andoffsetMs(0 to 1,073,741,824 ms, about 12.4 days) places a mark inside its bar underx: 1.style(0..7) picks a span's or a ray's colour,word(0..30) picks one of itswords(a negative word, or one past the list, draws none), and a nonzeroextendruns it to the chart's right edge.lengthBarscounts the chart's bars after the record's own (0 is that bar alone; below 0 reads 0). A value past its range is clamped to it. - Outputs. Each section adds
slotsx 3 (marks),slotsx 4 (spans and rays) orslotsx 5 (timed spans and rays) hidden outputs, named<name>_<slot>_<field>, after every output you declare. They count toward the 256 outputs a file may declare; you never write them one by one. - Per bar. A section carries nothing from one bar to the next, so a per-bar script stays per bar with one: on a pan-back the chart computes only the bars it has not drawn.
Marks
plot.marks(options) draws up to 64 marks a bar, each at a price with a
quantity. The size follows the quantity, the colour follows the side,
and a caption can print the quantity on the larger marks.
// Up to 8 rings a bar, each placed at its own time inside the bar, the largest captioned.
plot.marks({ name: "trades", slots: 8, size: { law: "sqrt", unit: 10, scale: 5 }, opacity_pct: 30, look: 2, text: { mode: 1, field: "qty", format: "compact", min_px: 22 }, x: 1, colors: ["#F05351", "#27A69A"], tooltip: "{{side}} {{qty}} at {{price}}, {{fills}} fills", panel: "overlay" });| Word | Value | What it does |
|---|---|---|
slots | 1..64, required | the marks one bar holds |
size | { law, unit, scale } | how big a mark draws (below) |
opacity_pct | 10..80 (default 30) | the fill's opacity, in percent |
look | 0..3 (default 0) | 0 a bubble (a fill with a solid edge), 1 a soft dot (a denser fill, no edge), 2 a ring (a solid edge over a faint fill), 3 a square (a denser filled square) |
text | { mode, field, format, min_px } | a caption: mode 0 off, 1 on the largest mark of each bar (default), 2 on every mark; field "qty"; format "compact" (default), "price" (the chart's price digits) or "int"; min_px (0..64, default 22) the smallest diameter that takes one |
x | 0 or 1 (default 0) | 0 the bar's centre, 1 the mark's own time: the bar's open plus offsetMs |
colors | 2..4 colours (default ["theme.down", "theme.up"]) | side picks one: 0 the first, 1 the second |
tooltip | a template of at most 200 characters, or null (default null, no readout) | the hover readout over a mark: {{side}}, {{qty}}, {{price}}, {{fills}} |
panel | "overlay" | marks draw on the price pane |
Size. unit (above 0, default 1) is the quantity a base-size mark
holds and scale (1..10, default 5) sets the base: with
k = 0.5 + 0.1 x scale, a mark's diameter is 11 x k x sqrt(qty / unit)
px, rounded and kept between 11 x k and the smaller of 64 and
50 x k. law: "linear" drops the square root, so the diameter follows
the quantity instead of the area.
Order. Larger marks draw first, so a small mark is never hidden under a large one, and each translucent mark composes over the marks under it; captions draw last, over every mark.
Spans
plot.spans(options) draws up to 64 boxes a bar. A box covers a price
range from its own bar across lengthBars more of the chart's bars, or
on to the right edge with extend.
// The range of a bar whose body fills most of it, held for 10 bars: an up bar takes the first colour and word.
plot.spans({ name: "zones", slots: 1, colors: ["theme.up", "theme.down"], opacity: 0.15, border: { width: 1, opacity: 0.8 }, words: ["Up bar", "Down bar"], tooltip: "{{word}} {{bottom}} to {{top}} over {{length}} bars", panel: "overlay" });
function onBar(): void {
if (Math.abs(bar.close() - bar.open()) < 0.7 * (bar.high() - bar.low())) return;
const pick = bar.close() >= bar.open() ? 0 : 1;
out_zones_span(0, bar.high(), bar.low(), 10, pick, pick, 0);
}| Word | Value | What it does |
|---|---|---|
slots | 1..64, required | the boxes one bar holds |
colors | 1..8 colours (default ["theme.) | style picks one |
opacity | 0..1 (default 0.2) | the fill's opacity |
border | { width, opacity } (default width 1, opacity 0.8), or null for none | the edge: width 0..4 px, opacity 0..1 |
words | up to 16, each 1..32 characters | word picks one, drawn with the box |
tooltip | a template of at most 200 characters, or null (default null, no readout) | the hover readout over a box: {{top}}, {{bottom}}, {{length}}, {{word}} |
panel | "overlay" | spans draw on the price pane |
Rays
plot.rays(options) draws up to 64 segments a bar, each from its own
bar at y1 to lengthBars bars later at y2, or on to the right edge
with extend. A ray with a length of 0 and extend set is a level from
its bar to the edge.
// A long upper wick's high, carried 20 bars; a long lower wick's low, run to the right edge.
plot.rays({ name: "wicks", slots: 2, colors: ["theme.muted", "#f59e0b"], width: 1.5, line_style: "dashed", opacity: 0.9, words: ["Wick high", "Wick low"], tooltip: "{{word}} at {{y1}}", panel: "overlay" });
function onBar(): void {
const range = bar.high() - bar.low();
if (range <= 0.0) return;
if (bar.high() - Math.max(bar.open(), bar.close()) > 0.5 * range) out_wicks_ray(0, bar.high(), bar.high(), 20, 0, 0, 0);
if (Math.min(bar.open(), bar.close()) - bar.low() > 0.5 * range) out_wicks_ray(1, bar.low(), bar.low(), 0, 1, 1, 1);
}| Word | Value | What it does |
|---|---|---|
slots | 1..64, required | the segments one bar holds |
colors | 1..8 colours (default ["theme.) | style picks one |
width | 0.5..8 px (default 1) | the line's width |
line_style | "solid" (default), "dashed" or "dotted" | the line's pattern |
opacity | 0..1 (default 1) | the line's opacity |
words | up to 16, each 1..32 characters | word picks one, drawn with the segment |
tooltip | a template of at most 200 characters, or null (default null, no readout) | the hover readout over a segment: {{y1}}, {{y2}}, {{length}}, {{word}} |
panel | "overlay" | rays draw on the price pane |
Timed spans and rays
out_<name>_span_at(slot, top, bottom, startTs, endTs, style, word, extend)
and out_<name>_ray_at(slot, y1, y2, startTs, endTs, style, word, extend)
write a record that runs between two times your script knows: a box from
the bar at startTs to the bar at endTs, or a segment from
(startTs, y1) to (endTs, y2). The start may lie before the bar
that writes the record, so a box over the bars a condition held, written
on the bar it ended, is one record on one bar. The rule above still
decides: timed records that land on most bars belong here; a session's
box, a broken zone or a line between two swings, a few a chart, stay
handles.
// Each run of 10 bars boxed from its first bar's open to its last bar's close, written on the run's last bar.
chart.interval_sec();
plot.spans({ name: "range", slots: 1, colors: ["theme.accent"], opacity: 0.1, tooltip: "{{bottom}} to {{top}}, {{start}} to {{end}}" });
let intervalSec = 0.0;
let firstTs = 0.0;
let hi = 0.0;
let lo = 0.0;
let count = 0;
function onStart(): void {
intervalSec = p_chart_interval_sec();
}
function onBar(): void {
if (count == 0) {
firstTs = bar.time();
hi = bar.high();
lo = bar.low();
}
hi = Math.max(hi, bar.high());
lo = Math.min(lo, bar.low());
count += 1;
if (count == 10) {
out_range_span_at(0, hi, lo, firstTs, bar.time() + intervalSec, 0, -1, 0);
count = 0;
}
}- Times, not bar counts.
startTsandendTsare epoch seconds, the unitbar.time()reads, and the chart places both ends at their exact times, the way it places a handle's corners. A count of bars your script keeps is not the chart's count: the chart leaves out the bars it has no data for, a session market's grid skips its closed hours, and volume or range candles have no fixed width, so a box drawn back by a count lands on the wrong bar where a time lands exact. Take both ends from times your script stored (a bar's open isbar.time(), its close that pluschart.interval_sec()), and never convert them to milliseconds. - The end. An end not after the start is written as the start: the
record covers its start alone. A nonzero
extendcontinues the record past its end to the chart's right edge, a box at its prices and a ray along its slope, so a ray needs a real second point even when it is extended. - The integer fields are those of the run-of-bars writers:
stylepicks a colour,wordone ofwords, and a value past its range is clamped to it. - The hover readout takes
{{start}}and{{end}}in place of{{length}}. - A section's layout follows the writer your file calls: a timed
section holds
slotsx 5 outputs,<name>_<slot>_top(or_y1),_bottom(or_y2),_start,_endand_meta, and the sheet says"stride": 5.
A chart built before timed spans and rays reads the run-of-bars layout only and refuses an indicator with a timed section by name ("stride must be 4"), so an indicator that uses one needs the current chart.
Settings
Every look key except slots, panel and text.field takes
"@<param>", so a setting drives it (Setting kinds):
a colour takes a param.color; a number key a param.number or
param.int whose min..max lies inside the key's range; size.law,
text.format and line_style a param.choice over their words; and
look, text.mode and x a param.choice with at most as many options
as the key has, the picked option's position being the value ("Ring"
below sets look to 2), or a param.int inside the key's range.
param.number("min_size", 10, { min: 0.0001, max: 1000000000 });
param.choice("look", ["Bubble", "Soft dot", "Ring", "Square"], "Ring");
param.int("bubble_size", 5, { min: 1, max: 10 });
param.int("opacity", 30, { min: 10, max: 80 });
param.choice("show_size", ["Off", "Big bubbles", "All"], "Big bubbles");
param.choice("position", ["Candle center", "Trade time"], "Trade time");
param.color("buy_color", "#27A69A");
param.color("sell_color", "#F05351");
plot.marks({ name: "trades", slots: 8, size: { unit: "@min_size", scale: "@bubble_size" }, look: "@look", opacity_pct: "@opacity", text: { mode: "@show_size" }, x: "@position", colors: ["@sell_color", "@buy_color"] });A setting writes one value to every key it drives, so one param.choice
cannot set both a position key (look) and a word key (line_style):
that is refused by name; declare two settings.
What the chart receives
Run writes each section into the sheet with the two numbers the chart
reads it by: first_output, the index of the section's first output,
and stride, the fields one slot holds (3 for marks, 4 for spans and
rays, 5 for timed spans and rays). Slot i's fields are the outputs
whose indexes start at first_output + stride x i. A sheet always
carries every look key: Run
writes the default for each key a declaration leaves out (a nested key
of size, text or border too), and the chart draws what the sheet
says. The first example on this page derives:
{
"outputs": [
{ "index": 0, "name": "prints_0_price", "plot": "" },
{ "index": 1, "name": "prints_0_qty", "plot": "" },
{ "index": 2, "name": "prints_0_meta", "plot": "" }
],
"marks": [
{ "name": "prints", "slots": 1, "first_output": 0, "stride": 3, "size": { "law": "sqrt", "unit": 1000, "scale": 5 }, "opacity_pct": 30, "look": 0, "text": { "mode": 1, "field": "qty", "format": "compact", "min_px": 22 }, "x": 0, "colors": ["theme.down", "theme.up"], "tooltip": "{{qty}} at {{price}}", "panel": "overlay" }
]
}A slot's last field packs its integer fields into one number: a mark's
side + 4 x fills + 262144 x offsetMs, a span's or a ray's (timed or
not) style + 8 x word + 256 x extend. A timed slot's start and end
fields hold the two times in epoch seconds. An empty slot is NaN in every
field.
What refuses
Every misuse is refused by name when Run derives the sheet:
- a fifth section of one kind ("marks must declare at most 4 entries"),
or
slotsoutside 1..64 ("plot.marks 'm' slots must be an integer from 1 to 64 (the records it holds a bar)"); - sections that take the file past 256 outputs ("plot.marks 'm2' derives 66 outputs (22 slots x 3 fields), which takes the sheet to 259 outputs; a row carries at most 256, so declare fewer slots or fewer outputs");
- an output you declare under a derived name ("plot.marks 'm' derives output 'm_0_price', which is already declared; rename one"), and a section named like an output, a canvas or any other named part: names share one namespace;
colorsoutside 2..4 on marks or 1..8 on spans and rays, a colour that is not"#rrggbb","#rrggbbaa"or a theme token, more than 16words, and atooltipover 200 characters or naming a field its kind does not carry ("tooltip names {{word}}, which a marks record does not carry");- a setting whose kind or range does not fit its key ("plot.marks 'm' look references "@look", which is not a param.choice of at most 4 options or a param.int within 0..3");
- both writers of one section in one file ("plot.spans 'z' is written by
both out_z_span() (line 5, a run of bars) and out_z_span_at() (line 6,
two timestamps); a section holds one record layout, so call one of the
two writers"), and a timed section's tooltip naming
{{length}}.
Sections draw when the indicator runs in your browser and when it runs on
OpenMarket's servers as a Protected indicator: the server keeps the
section and sends its rows to your chart, which draws them. A timed section
needs a chart that draws stride-5 records (it advertises wrun-batched-v2);
an older chart keeps the indicator off its hosted lane by name. The caps are
on Limits.