---
title: "Drawing objects"
description: "kScript (legacy) drawings were objects you created and owned: line.new(), box.new(), label.new(), polyline.new(), linefill.new(), and table.new() returned…"
order: 41
section: "functions"
---

<!-- source: docs/indicators/functions/drawing-objects.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Drawing objects

kScript (legacy) drawings were objects you created and owned: `line.new()`,
`box.new()`, `label.new()`, `polyline.new()`, `linefill.new()`, and
`table.new()` returned handles, and the handle was the point: restyle it
later, move it, delete it. An Indicator has three forms. A `box` or a
`segment` is declared once over two outputs and evaluated on every bar; a
run-level `draw.*` declaration is placed from the newest bar's outputs;
and a **handle** is an object the module creates under an integer id and
moves, restyles, or deletes on a later bar, exactly the kScript object
model. This page is the vocabulary, the port of each constructor, and the
rules each form runs under.

## The constructors, ported

| kScript | Indicator | Lifecycle |
| --- | --- | --- |
| `line.new(x1, y1, x2, y2)` | `segment(name, { yFrom, yTo, from, to })` per bar; `draw.line(id).set(x1, y1, x2, y2)` as a handle | per bar, or a handle the module owns |
| `box.new(x1, y1, x2, y2)` | `box(name, { top, bottom, from, to, when })` per bar; `draw.box(id).set(left, top, right, bottom)` as a handle | per bar, or a handle the module owns |
| `label.new(x, y, text)` | `draw.label(id).set(x, y).text(str_<slot>_sb)` as a handle; `render.label(name, { x, y, text })` for one label the newest bar wins | a handle, or one label |
| `polyline.new(points)` | `draw.polyline(id).setPoints(points, count)` as a handle; `draw.polyline(name, { points })` declared over output pairs | a handle of up to 256 points, or 64 output pairs from the newest bar |
| `linefill.new(lineA, lineB)` | a `box` per bar between the two outputs | per bar |
| `table.new(position, rows, cols)` + `setCell` | `render.table(name, { rows, cols, cells, position })` over string slots | the newest complete row wins |
| handle `.set_*()`, `.delete()` | the handle's setters (`set`, `setXy2`, `setRightBottom`, `color`, `fill`, `width`, `style`, `extend`, `size`, ...) and `delete()` | a setter stamps the bar it ran on; `delete()` frees the id |

Every piece is a decoration over numbers the module already computes:
outputs are metrics, and boxes, segments, drawings, and handles never
change a metric value ([Execution model](../core-concepts/execution-model.md)).
Two namespaces are both called `draw`: `draw.line(name, {...})` from
`./sdk/declare` declares a run-level drawing, and `draw.line(id)` from
`./gen/draw` returns a handle. A file imports one of them; a handle does
everything a run-level declaration does (create it under `bar.isLast()`),
so a file that draws handles imports `./gen/draw` only.

## Boxes

A box is declared once and evaluated on every bar. On bar `i` it spans
bars `i + from` to `i + to` (inclusive) and prices `min(top, bottom)` to
`max(top, bottom)`. Nothing is drawn on a bar where any referenced output
is `NaN`, or where the optional `when` gate is `0` or `NaN`.

Coordinates are output HANDLES: `output(...)` returns one, so bind it with
a top-level `const` and pass the const. `from` and `to` are bar offsets
(negative = past, positive = ahead; integer literals in -500..500, or a
handle whose per-bar value is truncated to the offset; default `0`).
Options: `when` (a gate handle), `panel` (`"overlay"` or `"lower"`,
default: the panel of `top`'s output), `color`, `borderColor`, `opacity`
(0..1, default 0.2), `borderWidth` (0..10, default 1; `0` for no border).
The fill takes the opacity, so `color` must be a hex, `rgb()`, or `hsl()`
color; a named color is refused.

The last five bars' range, tinted only while the range is expanding:

```typescript
import { box, input, none, ohlcv, output, param } from "./sdk/declare";
import { in_high, in_low } from "./gen/inputs";
import { emitRow, out_expanding, out_range_hi, out_range_lo } from "./gen/outputs";
import { p_bars } from "./gen/params";

param("bars", 5, { min: 2, max: 50, description: "Bars in the trailing range" });
input("high", ohlcv.high);
input("low", ohlcv.low);
const rangeHi = output("range_hi", none);
const rangeLo = output("range_lo", none);
const expanding = output("expanding", none);
// Behind the last five bars, tinted only while the range is wider than it was one bar ago.
box("range_zone", { top: rangeHi, bottom: rangeLo, from: -4, to: 0, when: expanding, color: "#f59e0b", opacity: 0.15, borderColor: "#f59e0b", borderWidth: 1 });

const MAX_BARS = 50;
const highs = new StaticArray<f64>(MAX_BARS);
const lows = new StaticArray<f64>(MAX_BARS);
let n: i32 = 5;
let cursor: i32 = 0;
let count: i32 = 0;
let hi: f64 = NaN;
let lo: f64 = NaN;
let prevWidth: f64 = NaN;
let width: f64 = NaN;

export function init(): void {
  n = i32(p_bars());
}

export function state(): i32 {
  highs[cursor] = in_high();
  lows[cursor] = in_low();
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  if (count < n) return 0;
  hi = -Infinity;
  lo = Infinity;
  for (let i = 0; i < n; i++) {
    if (highs[i] > hi) hi = highs[i];
    if (lows[i] < lo) lo = lows[i];
  }
  prevWidth = width;
  width = hi - lo;
  return 1;
}

export function finalize(): void {
  out_range_hi(hi);
  out_range_lo(lo);
  out_expanding(!isNaN(prevWidth) && width > prevWidth ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  cursor = 0;
  count = 0;
  hi = NaN;
  lo = NaN;
  prevWidth = NaN;
  width = NaN;
}
```

Two shapes that fall out of the per-bar rule:

- A shaded channel between two lines (kScript's `linefill.new`) is a box
  on every bar with `from` and `to` left at `0`: each bar contributes a
  one-bar-wide slice, and the slices tile into a band. The
  [anchored VWAP recipe](../cookbook/anchored-vwap.md) shades its band
  this way.
- A zone that lives until price breaks it is the same one-bar box gated by
  a `when` output that stays `1` while the zone is alive: the band starts
  at the pivot and stops on the bar that mitigates it (the
  [zone tracker](../cookbook/zone-tracker.md)). That is one port of
  "create the box at the pivot, `.delete()` it when mitigated"; the other
  is a box handle, below, which is the same object from creation to
  deletion.

## Segments

A segment is the straight line from `(i + from, yFrom)` to `(i + to,
yTo)`, evaluated on every bar `i` with the same offset, gate, and `NaN`
rules as a box. Options: `when`, `panel`, `color`, `width` (0.5..20,
default 1), `lineStyle` (`"solid"`, `"dashed"`, `"dotted"`). Absent
`color` and `panel` follow `yFrom`'s output.

A dotted projection from each bar's average toward where the slope points,
its length decided per bar by an output-valued offset:

```typescript
import { input, line, none, ohlcv, output, overlay, param, segment } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_anchor, out_reach, out_target } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Ema } from "./sdk/ta";

param("period", 20, { min: 2, max: 200 });
input("close", ohlcv.close);
const anchor = output("anchor", line, overlay, { color: "#38bdf8" });
const target = output("target", none);
const reach = output("reach", none);
// From this bar's average to the projected level, `reach` bars ahead: the offset is an output,
// so each bar decides its own length.
segment("projection", { yFrom: anchor, yTo: target, from: 0, to: reach, color: "#38bdf8", width: 1, lineStyle: "dotted" });

let ema = new Ema(20);
let value: f64 = NaN;
let prev: f64 = NaN;

export function init(): void {
  ema = new Ema(i32(p_period()));
}

export function state(): i32 {
  prev = value;
  value = ema.update(in_close());
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  const slope = isNaN(prev) ? 0.0 : value - prev;
  const bars = slope == 0.0 ? 0.0 : 5.0;
  out_anchor(value);
  out_target(value + slope * bars);
  out_reach(bars);
  emitRow();
}

export function reset(): void {
  ema.reset();
  value = NaN;
  prev = NaN;
}
```

Horizontal levels are segments with `yFrom` and `yTo` on the same output
and `from: 0, to: 1`: each bar draws its level to the next bar, and the
pieces chain into one line that steps when the level changes (the
[key levels recipe](../cookbook/key-levels.md)). Offsets past the loaded
range clamp to its edge, so a segment cannot reach into empty space to
the right of the newest bar; a handle's absolute coordinates can.

## Run-level drawings

`draw.line`, `draw.box`, `draw.polyline`, and `draw.label` from
`./sdk/declare` are the declared kind of object: one per declaration, its
coordinates read from outputs on the NEWEST ready bar only. Every
coordinate finite there means the object exists; any `NaN` there means no
object, regardless of earlier bars. This is the port of the kScript idiom
`if (isLastBar) { line.new(...) }` as a declaration: the host evaluates
the newest bar on its own, and re-evaluates it on every tick.

- `draw.line(name, { x1, y1, x2, y2, color?, width?, line_style? })`
- `draw.box(name, { left, top, right, bottom, color? })`
- `draw.polyline(name, { points, color?, width?, line_style? })`, where
  `points` is a flat `["x0", "y0", "x1", "y1", ...]` list of output-name
  pairs, at most 64 pairs
- `draw.label(name, { x, y, text, color? })`, `text` a string slot

X coordinates are epoch SECONDS (kScript used milliseconds): the `time`
source feeds them (`input("bar_t", time.bar_open_sec)`), and a point in
the past is that time minus a bar count times the interval in seconds.
Drawings reference outputs by NAME (strings), unlike per-bar boxes and
segments, which take handles.

The full kScript example, ported: a high and a low line, a zone box
between them, a three-point path, and a label, all placed from the
newest bar, plus a per-bar horizontal level for contrast. The drawings
reach two bars ahead by adding two intervals to the bar time.

```typescript
import { draw, input, line, none, ohlcv, output, overlay, param, segment, string, time } from "./sdk/declare";
import { in_bar_t, in_close, in_high, in_low } from "./gen/inputs";
import {
  emitRow,
  out_close_line,
  out_level,
  out_t0,
  out_t1,
  out_t2,
  out_zone_high,
  out_zone_low,
  out_zone_mid,
} from "./gen/outputs";
import { p_lookback } from "./gen/params";
import { sb_clear, sb_f64, sb_text, str_note_sb } from "./gen/strings";

param("lookback", 20, { min: 2, max: 500, description: "Bars in the zone's range" });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("bar_t", time.bar_open_sec);
output("close_line", line, overlay, { color: "#2563eb", width: 2, description: "Close, the anchor line" });
const level = output("level", none, overlay, { description: "The lookback high, a stepping level" });
output("zone_high", none, overlay, { description: "Lookback high: the top line and the box top" });
output("zone_low", none, overlay, { description: "Lookback low: the bottom line and the box bottom" });
output("zone_mid", none, overlay, { description: "The midpoint, the path's middle point" });
output("t0", none, overlay, { description: "This bar's open time, epoch seconds" });
output("t1", none, overlay, { description: "One bar ahead" });
output("t2", none, overlay, { description: "Two bars ahead" });
string("note", { max_bytes: 32 });
// Per bar: the lookback high carried one bar to the right, chaining into a stepped level line.
segment("level_line", { yFrom: level, yTo: level, from: 0, to: 1, color: "#94a3b8", width: 1, lineStyle: "dashed" });
// Run level, placed from the newest bar: two lines, a box, a path, and a label.
draw.line("top_line", { x1: "t0", y1: "zone_high", x2: "t2", y2: "zone_high", color: "#2563eb", width: 2 });
draw.line("bottom_line", { x1: "t0", y1: "zone_low", x2: "t2", y2: "zone_low", color: "#dc2626", width: 2 });
draw.box("zone", { left: "t0", top: "zone_high", right: "t2", bottom: "zone_low", color: "#dbeafe" });
draw.polyline("path", { points: ["t0", "zone_low", "t1", "zone_mid", "t2", "zone_high"], color: "#f97316", width: 2 });
draw.label("last", { x: "t1", y: "close_line", text: "note", color: "#111827" });

const MAX_BARS = 500;
const highs = new StaticArray<f64>(MAX_BARS);
const lows = new StaticArray<f64>(MAX_BARS);
let n: i32 = 20;
let cursor: i32 = 0;
let count: i32 = 0;
let hi: f64 = NaN;
let lo: f64 = NaN;
let close: f64 = NaN;
let t: f64 = NaN;
let prevT: f64 = NaN;
let intervalSec: f64 = NaN;

export function init(): void {
  n = i32(p_lookback());
}

export function state(): i32 {
  close = in_close();
  prevT = t;
  t = in_bar_t();
  // The interval is the spacing between consecutive bar opens; NaN on the first bar.
  if (!isNaN(prevT)) intervalSec = t - prevT;
  highs[cursor] = in_high();
  lows[cursor] = in_low();
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  if (count < n || isNaN(intervalSec)) return 0;
  hi = -Infinity;
  lo = Infinity;
  for (let i = 0; i < n; i++) {
    if (highs[i] > hi) hi = highs[i];
    if (lows[i] < lo) lo = lows[i];
  }
  return 1;
}

export function finalize(): void {
  out_close_line(close);
  out_level(hi);
  out_zone_high(hi);
  out_zone_low(lo);
  out_zone_mid((hi + lo) / 2.0);
  out_t0(t);
  out_t1(t + intervalSec);
  out_t2(t + 2.0 * intervalSec);
  sb_clear();
  sb_text("last ");
  sb_f64(close, 2);
  str_note_sb();
  emitRow();
}

export function reset(): void {
  cursor = 0;
  count = 0;
  hi = NaN;
  lo = NaN;
  close = NaN;
  t = NaN;
  prevT = NaN;
  intervalSec = NaN;
}
```

After this runs you see one stepped level line (per bar), two lines, one
box, one path, and one label (all from the newest bar). A declared drawing
that should be REMOVED emits `NaN` through one of its coordinate outputs
on the newest bar; a handle, below, has a `delete()`.

## Handles

A handle is the kScript object, ported whole: the module creates it, keeps
its identity from bar to bar, moves or restyles it later, and deletes it.
Four kinds: `line`, `box`, `label`, `polyline`.

**Declare the kinds you draw** at the top of the file, once per kind,
with the defaults every new handle of that kind starts from:
`handles.line({ panel, color, width, lineStyle, extend })`,
`handles.box({ panel, color, borderColor, opacity, borderWidth })`,
`handles.label({ text, panel, color, size, align })`, and
`handles.polyline({ panel, color, width, lineStyle })`, all from
`./sdk/declare`. There is no name (handles are ids the module picks) and
every option is optional; `handles.box()` enables boxes with the
defaults. A label's `text` names the string slot its text is read from,
so `handles.label` needs a `string(...)` declaration. A label's `align`
(`left`, `center` or `right`) is the text edge that sits on its x;
omitted centres the text, and (x, y) stays the point either way. Declaring any kind,
or calling `bar.isLast()`, switches the derived sheet to the third
runtime contract (`abi_version: "wrun-3"`); the numeric outputs compute
exactly as before.

**Make the objects once**, at module level or in `init()`, from
`./gen/draw`: `draw.line(id)`, `draw.box(id)`, `draw.label(id)`,
`draw.polyline(id)`. An id is any integer from `0` up, in ONE space
across the four kinds: while a box holds id `3`, a label cannot. An
object per bar would allocate per bar, which the module never does.

**Draw in `finalize()`**, beside the outputs. The rules, each refused by
name when broken:

| Call | On an id nobody holds | On a live id of the same kind |
| --- | --- | --- |
| `set(...)` (`setPoints` on a polyline) | creates the handle: `createdBar` is this bar, the style is the kind's defaults | moves it: this bar becomes its `mutatedBar` |
| the partial setters `setXy1`, `setXy2`, `setLeftTop`, `setRightBottom` | refused (nothing to remember the other corner from) | moves one end; the object re-sends the remembered rest |
| `color`, `fill`, `width`, `style`, `extend`, `opacity`, `size`, `border`, `zorder` | refused (the handle does not exist) | restyles it; this bar becomes its `mutatedBar` |
| `delete()` | a no-op | removes it; the id is free for a later bar (a new handle, a new creation bar) |
| `set(...)` on an id a handle of ANOTHER kind holds | | refused: delete that handle first |

Coordinates are absolute: `x` in epoch seconds (the `time` source's
`bar_open_sec`), `y` in price. Nothing clamps: a box that should end one
bar past the newest bar sets its right edge to `t + interval`, and the
chart draws it there. Every coordinate must be finite. Colors on a
setter are `rgba(r, g, b, a)` values (`rgb(r, g, b)` for opaque) from
`./gen/draw`; `style` takes `LineStyle.Solid`, `Dashed`, `Dotted`;
`extend` takes `Extend.None`, `Left`, `Right`, `Both`. Which setter
exists on which kind follows the kScript table: `width` and `style` on
lines and polylines, `extend` on lines, `border` and `fill` on boxes,
`size` and `fill` on labels, `opacity` and `zorder` on lines, boxes, and
labels.

A label's text comes from a string slot: build the line with `sb_*`,
then `tag.set(x, y).text(str_<slot>_sb)` sends the slot and draws the
label with the bytes the slot holds on that bar. Call it again on a later
bar to move or re-word the label (kScript's `set_xy` and `set_text`).
`tag.align(ALIGN_RIGHT)` (or `style.align(tag, ALIGN_RIGHT)`, the
constants from `./gen/draw`) makes the text end at x instead of centring
on it; `ALIGN_LEFT` starts it there and `ALIGN_DEFAULT` restores centred
text, clearing a declared `align` default.

Session zones: a box that grows with every bar of its session, a label on
it, four zones kept on the chart with the oldest deleted to make room,
and the open zone stretched one bar past the newest bar:

```typescript
import { handles, input, line, ohlcv, output, overlay, param, string, time } from "./sdk/declare";
import { BoxHandle, LabelHandle, bar, draw, rgba } from "./gen/draw";
import { in_high, in_low, in_t } from "./gen/inputs";
import { emitRow, out_zone_hi, out_zone_lo } from "./gen/outputs";
import { p_bars } from "./gen/params";
import { sb_clear, sb_int, sb_text, str_tag_sb } from "./gen/strings";

param("bars", 12, { min: 2, max: 500, description: "Bars per zone" });
input("high", ohlcv.high);
input("low", ohlcv.low);
input("t", time.bar_open_sec);
output("zone_hi", line, overlay, { color: "#38bdf8", description: "The open zone's high so far" });
output("zone_lo", line, overlay, { color: "#38bdf8", description: "The open zone's low so far" });
string("tag", { max_bytes: 16 });
// Enable box and label handles; every new box and label starts from these defaults.
handles.box({ color: "#38bdf8", opacity: 0.2, borderWidth: 1 });
handles.label({ text: "tag", color: "#e5e7eb", size: 11 });

// Four zones stay on the chart. Handle objects are made once; ids are one space across kinds.
const KEEP = 4;
const zones: BoxHandle[] = [draw.box(0), draw.box(1), draw.box(2), draw.box(3)];
const tags: LabelHandle[] = [draw.label(4), draw.label(5), draw.label(6), draw.label(7)];
let bars: i32 = 12;
let count: i32 = 0;
let slot: i32 = 0;
let serial: i32 = 0;
let t: f64 = NaN;
let prevT: f64 = NaN;
let start: f64 = NaN;
let hi: f64 = NaN;
let lo: f64 = NaN;

export function init(): void {
  bars = i32(p_bars());
}

export function state(): i32 {
  prevT = t;
  t = in_t();
  if (count == 0) {
    start = t;
    hi = in_high();
    lo = in_low();
  } else {
    hi = Math.max(hi, in_high());
    lo = Math.min(lo, in_low());
  }
  count += 1;
  return 1;
}

export function finalize(): void {
  out_zone_hi(hi);
  out_zone_lo(lo);
  const zone = zones[slot];
  const tag = tags[slot];
  if (count == 1) {
    // The zone's first bar: set(...) on an id nobody holds creates the box; fill(...) tints it.
    serial += 1;
    zone.set(start, hi, t, lo).fill(rgba(56, 189, 248, 51));
  } else {
    // Every later bar grows the same box: the id is what makes it the same box.
    zone.setLeftTop(start, hi).setRightBottom(t, lo);
  }
  sb_clear();
  sb_text("zone ");
  sb_int(serial);
  tag.set(start, hi).text(str_tag_sb);
  // On the newest bar only, stretch the open zone one bar past the loaded range.
  if (bar.isLast() && !isNaN(prevT)) zone.setRightBottom(t + (t - prevT), lo);
  if (count >= bars) {
    // The zone is complete: move to the next slot and delete the zone that slot held.
    count = 0;
    slot = (slot + 1) % KEEP;
    zones[slot].delete();
    tags[slot].delete();
  }
  emitRow();
}

export function reset(): void {
  count = 0;
  slot = 0;
  serial = 0;
  t = NaN;
  prevT = NaN;
  start = NaN;
  hi = NaN;
  lo = NaN;
}
```

Run over thirty hourly bars with twelve bars per zone, this leaves three
boxes and three labels: the first created on bar 0 and last moved on bar
11, the second on bars 12 and 23, the open one created on bar 24, moved
on bar 29, and reaching one hour past bar 29. The `delete()` calls on the
first three zone changes hit ids nobody holds and do nothing; the fourth
removes the oldest zone, and its id is created again on the next bar
with a new creation bar.

### Restyle later

A setter on a later bar is a mutation of the same object, and the bar it
ran on is recorded as the handle's last-mutation bar, exactly as the
engine stamps `set_color`. The prior high as a line that extends with
every bar, turns red and thick on the bar the close breaks it, stays for
a while, and is deleted, freeing the id for the next level:

```typescript
import { handles, input, line, none, ohlcv, output, overlay, param, time } from "./sdk/declare";
import { draw, rgba } from "./gen/draw";
import { in_close, in_high, in_t } from "./gen/inputs";
import { emitRow, out_broke, out_level } from "./gen/outputs";
import { p_hold, p_lookback } from "./gen/params";

param("lookback", 20, { min: 2, max: 200, description: "Bars in the prior high" });
param("hold", 10, { min: 1, max: 100, description: "Bars a broken line stays before it is deleted" });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("t", time.bar_open_sec);
output("level", line, overlay, { color: "#94a3b8", description: "The prior high the line sits on" });
output("broke", none, overlay, { description: "1 on the bar the close breaks the line" });
handles.line({ color: "#94a3b8", width: 1, lineStyle: "dashed" });

const MAX_LOOKBACK = 200;
const highs = new StaticArray<f64>(MAX_LOOKBACK);
const stop = draw.line(0);
let n: i32 = 20;
let hold: i32 = 10;
let cursor: i32 = 0;
let count: i32 = 0;
let t: f64 = NaN;
let close: f64 = NaN;
let level: f64 = NaN;
let levelT: f64 = NaN;
let fresh: bool = false;
let heldBars: i32 = -1;

export function init(): void {
  n = i32(p_lookback());
  hold = i32(p_hold());
}

export function state(): i32 {
  t = in_t();
  close = in_close();
  // The highest high of the previous n bars, this bar excluded.
  let prior: f64 = NaN;
  if (count >= n) {
    prior = -Infinity;
    for (let i = 0; i < n; i++) if (highs[i] > prior) prior = highs[i];
  }
  highs[cursor] = in_high();
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  if (isNaN(prior)) return 0;
  // A new level while no broken line is being held: the line restarts here.
  if (heldBars < 0 && prior != level) {
    level = prior;
    levelT = t;
    fresh = true;
  }
  return 1;
}

export function finalize(): void {
  out_level(level);
  if (fresh) {
    // Create the line (or re-create it on the same id after a delete).
    stop.set(levelT, level, t, level);
    fresh = false;
  } else if (heldBars < 0) {
    // Extend the same line to this bar: a mutation, so its mutated bar moves with it.
    stop.setXy2(t, level);
  }
  if (heldBars < 0 && close > level) {
    // The break: recolor and thicken the existing line, then hold it for a while.
    heldBars = 0;
    stop.color(rgba(239, 68, 68, 255)).width(2);
  } else if (heldBars >= 0) {
    heldBars += 1;
    if (heldBars >= hold) {
      // Delete the held line; the next bar creates a fresh one on the same id.
      stop.delete();
      heldBars = -1;
      level = NaN;
    }
  }
  out_broke(heldBars == 0 ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  cursor = 0;
  count = 0;
  t = NaN;
  close = NaN;
  level = NaN;
  levelT = NaN;
  fresh = false;
  heldBars = -1;
}
```

### Paths built point by point

A polyline handle takes its points from a buffer the module owns: `x0,
y0, x1, y1, ...` in a `StaticArray<f64>`, and `setPoints(points, count)`
sends the first `count` pairs (1..256). Re-send as the path grows; the
host copies the points, so the buffer is yours to shift. A zigzag through
the last twelve confirmed swings:

```typescript
import { handles, input, none, ohlcv, output, overlay, param, time } from "./sdk/declare";
import { draw } from "./gen/draw";
import { in_high, in_low, in_t } from "./gen/inputs";
import { emitRow, out_swing } from "./gen/outputs";
import { p_strength } from "./gen/params";

param("strength", 3, { min: 1, max: 20, description: "Bars on each side that confirm a swing" });
input("high", ohlcv.high);
input("low", ohlcv.low);
input("t", time.bar_open_sec);
output("swing", none, overlay, { description: "The newest confirmed swing price" });
handles.polyline({ color: "#f97316", width: 2 });

const KEEP = 12;
const MAX_STRENGTH = 20;
const WINDOW = 2 * MAX_STRENGTH + 1;
const highs = new StaticArray<f64>(WINDOW);
const lows = new StaticArray<f64>(WINDOW);
const times = new StaticArray<f64>(WINDOW);
// The path's points, x0, y0, x1, y1, ... in one buffer the host reads count pairs from.
const points = new StaticArray<f64>(2 * KEEP);
const path = draw.polyline(0);
let k: i32 = 3;
let window: i32 = 7;
let cursor: i32 = 0;
let filled: i32 = 0;
let stored: i32 = 0;
let lastDir: i32 = 0;
let swing: f64 = NaN;
let changed: bool = false;

function append(x: f64, y: f64): void {
  if (stored == KEEP) {
    // Full: drop the oldest point, keep the newest KEEP - 1.
    for (let i = 0; i < 2 * (KEEP - 1); i++) points[i] = points[i + 2];
    stored -= 1;
  }
  points[2 * stored] = x;
  points[2 * stored + 1] = y;
  stored += 1;
  changed = true;
}

export function init(): void {
  k = i32(p_strength());
  window = 2 * k + 1;
}

export function state(): i32 {
  highs[cursor] = in_high();
  lows[cursor] = in_low();
  times[cursor] = in_t();
  cursor = (cursor + 1) % window;
  if (filled < window) filled += 1;
  changed = false;
  if (filled < window) return 0;
  // The candidate is the bar k bars back, the center of the window.
  const center = (cursor + k) % window;
  let isHigh = true;
  let isLow = true;
  for (let i = 0; i < window; i++) {
    if (i == center) continue;
    if (highs[i] >= highs[center]) isHigh = false;
    if (lows[i] <= lows[center]) isLow = false;
  }
  // Swings alternate: a high after a high is skipped.
  if (isHigh && lastDir != 1) {
    lastDir = 1;
    swing = highs[center];
    append(times[center], swing);
  } else if (isLow && lastDir != -1) {
    lastDir = -1;
    swing = lows[center];
    append(times[center], swing);
  }
  return 1;
}

export function finalize(): void {
  out_swing(swing);
  // A new swing: re-emit the whole path under the same id (kScript's set_points).
  if (changed && stored >= 2) path.setPoints(points, stored);
  emitRow();
}

export function reset(): void {
  cursor = 0;
  filled = 0;
  stored = 0;
  lastDir = 0;
  swing = NaN;
  changed = false;
}
```

### The sheet form

A hand-written sheet declares the same thing as a `handles` map: one
entry per kind you draw, its fields the defaults of every new handle of
that kind, every field optional (`"box": {}` enables boxes with the
defaults). The zones Indicator's sheet:

```json
{
  "id": "fn-handles-sheet",
  "name": "Session zones",
  "abi_version": "wrun-3",
  "params": [{ "name": "bars", "default": 12, "min": 2, "max": 500, "description": "Bars per zone" }],
  "inputSources": {
    "high": { "source": "ohlcv", "field": "high" },
    "low": { "source": "ohlcv", "field": "low" },
    "t": { "source": "time", "field": "bar_open_sec" }
  },
  "inputs": [
    { "index": 0, "name": "high" },
    { "index": 1, "name": "low" },
    { "index": 2, "name": "t" }
  ],
  "outputs": [
    { "index": 0, "name": "zone_hi", "plot": "line", "panel": "overlay", "color": "#38bdf8" },
    { "index": 1, "name": "zone_lo", "plot": "line", "panel": "overlay", "color": "#38bdf8" }
  ],
  "string_slots": [{ "index": 0, "name": "tag", "max_bytes": 16 }],
  "handles": {
    "line": { "panel": "overlay", "color": "#38bdf8", "width": 1, "line_style": "solid", "extend": "none" },
    "box": { "panel": "overlay", "color": "#38bdf8", "border_color": "#38bdf8", "opacity": 0.2, "border_width": 1 },
    "label": { "panel": "overlay", "color": "#e5e7eb", "size": 11 },
    "polyline": { "panel": "overlay", "color": "#38bdf8", "width": 1, "line_style": "solid" }
  }
}
```

The values shown are the defaults themselves. `handles` needs
`"abi_version": "wrun-3"`, even empty; a `label` entry needs at least one
string slot; a kind not in the map refuses its draw calls by name at run
time, so an Indicator that draws boxes and labels lists exactly those
two. Field ranges are on the [Limits](../reference/limits.md) page.

### What the chart receives

After a run the chart holds one record per LIVE handle, in creation
order: its kind, the bar it was created on, the bar it was last mutated
on, and its final geometry and style, the same record a kScript drawing
produces (`createdBar`, `mutatedBar`, `props`). A deleted handle is
simply absent. On the live chart the newest bar is re-evaluated on every
tick: the host puts the module AND its handles back to the state after
the last closed bar and re-runs the bar, so a tick never stacks a second
copy of what the forming bar drew; when the bar closes, the host re-runs
it once more as a closed bar (with `bar.isLast()` false) before the new
bar starts, so a chart that has been open all day shows exactly what a
fresh load shows ([Execution model](../core-concepts/execution-model.md)).

## Limits

| Family | Cap |
| --- | --- |
| `box` declarations | 16 per sheet, each drawn once per bar |
| `segment` declarations | 16 per sheet, each drawn once per bar |
| renderers | 32 per package |
| declared drawings | 64 per package; a declared polyline of at most 64 points |
| live handles | 500 per kind, 1500 in total; a polyline handle of at most 256 points; 4096 draw calls per bar |
| string slots | 64, each at most 4096 bytes; 64 KiB of strings per row, 8 MiB per run |
| bar offsets | integer literals in -500..500; a box or segment offset clamps to the loaded range; a handle's absolute coordinates never clamp |

kScript capped each drawing kind at 500 live objects and made you delete
stale ones to stay under it; handles run under the same ceilings, and a
tracker that evicts its oldest zone with `delete()` stays under them
exactly as it did there. The caps on declared shapes are on declarations,
not objects: a repeating pattern is one `box` gated per bar, or one
handle per live occurrence.

## What you cannot do

- Move a declared shape later: only the forming bar is re-evaluated,
  where the host replaces that one bar's shapes on every tick, never
  stacks them. Anything that must move, restyle, or vanish on a later bar
  is a handle.
- Draw an unbounded number of things. The caps above are the whole
  budget; a handle-drawing Indicator deletes what it no longer needs.
- Reach more than 500 bars away with a literal offset on a box or
  segment; pass an output handle for a data-driven reach, and it still
  clamps to the loaded range. A handle takes an absolute time instead and
  never clamps.
- Fill a box with a named color: the fill takes the opacity, so `color`
  must be hex, `rgb()`, or `hsl()`.
- Reuse a name across families: outputs, boxes, segments, renderers, and
  declared drawings share one namespace (handles are ids and have no
  name).
- Let an output color, widen, or gate itself.
- Put text in an output, or a string in a param.
- Draw from `state()`: handle calls belong in `finalize()`, beside the
  outputs; `bar.isLast()` answers in both.
- Pin a label to the pane's right edge (kScript's `stickyRight`): a line
  handle extends past its points with `extend(Extend.Right)`, and a label
  sits where its coordinates say.
