Drawing reference
Every drawing primitive in Delta DSL — per-bar plots, world-coord shapes, persistent slots, drawing-tool integration, color helpers, and the rules that govern them.
Drawing in Delta DSL is declarative. You don't grab a canvas and stroke; you call a plot* / pane* / draw* / *New function with the data you want drawn, and the engine records it. Records are flushed in batches each frame so a script that emits 50 000 markers is no slower than one that emits 50.
This page covers every drawing primitive. There are five families:
| Family | Coordinate system | Persists across frames | Use for |
|---|---|---|---|
| Per-bar plots | Bar index | No | Lines / bands / markers tied to OHLCV |
| World-coord shapes | (timestamp, price) | No (re-emit each frame) | One-shot annotations |
| Pane plots | (timestamp, value) inside a sub-pane | No | Oscillator lines / bands |
| Persistent slots | (timestamp, price) | Yes (same name) | HUDs, evolving labels |
| Drawing-tool shapes | (timestamp, price) | Yes; user-editable | Trendlines / hlines / fibs that the trader can grip-drag |
The Color helpers section near the bottom covers rgb, rgba, withAlpha, gradient.
A note on color
plot* and pane* arguments named color / bg / border are single static strings — they are read once at draw-call time. Passing a series of color strings (e.g. iff(cond, "#0ECB81", "#F6465D")) silently falls back to a default colour because the bridge's color parser only understands strings.
To paint a multi-tone trace, split the source via mask(series, cond) and emit one plotLine (or plotShape, etc.) per colour:
ma = sma(close, 20)
bullMa = mask(ma, close >= ma)
bearMa = mask(ma, close < ma)
plotLine(bullMa, color="#0ECB81", width=2)
plotLine(bearMa, color="#F6465D", width=2)
Per-bar plots
These render one shape (or one line segment) per bar of the visible window, using the bar's index for the X coordinate and the supplied series value for Y. They live on the candle pane (in price space) and respect @pane "overlay" (the default). For sub-pane equivalents see Pane plots.
plotLine(series, color="#…", width=1.5)
Smooth polyline through series. NaN entries become gaps.
ma = sma(close, 20)
plotLine(ma, color="#F0B90B", width=2)
| Param | Type | Default | Description |
|---|---|---|---|
series | series<number> | — | Y values per bar |
color | string | "#ffffff99" | Line colour |
width | number | 1.5 | Stroke width in pixels |
plotDashedLine(series, color, width=1, dash=4, gap=4)
Same as plotLine but with a dashed stroke. dash and gap are measured in pixels.
plotDashedLine(sma(close, 200), color="#ffffff77", width=1, dash=6, gap=6)
plotBand(upper, lower, color="rgba(...)")
Fill the area between two series. NaN gaps in either series leave the corresponding region empty.
upper = bb_upper(close, 20, 2)
lower = bb_lower(close, 20, 2)
plotBand(upper, lower, color="rgba(240,185,11,0.06)")
plotLine(upper, color="rgba(240,185,11,0.5)")
plotLine(lower, color="rgba(240,185,11,0.5)")
plotCandle(open, high, low, close, bullColor=, bearColor=, wickColor=, width=)
Per-bar OHLC primitive. Each bar draws a wick from high[i] to low[i] plus a body from open[i] to close[i]. Bars where any of open / high / low / close is NaN are skipped — pair with mask(...) to draw on a sparse subset.
| Param | Default | Notes |
|---|---|---|
bullColor | "#089981" | Used when close ≥ open |
bearColor | "#F23645" | Used when close < open |
wickColor | body colour | Set to a separate string for two-tone candles |
width | engine adaptive | Override only for unusual layouts |
// Heikin-Ashi candles overlaid on the chart:
ha_close = (open + high + low + close) / 4
ha_open = sma(ha_close, 1) // simplified — full HA carries forward
ha_high = max(high, max(ha_open, ha_close))
ha_low = min(low, min(ha_open, ha_close))
plotCandle(ha_open, ha_high, ha_low, ha_close,
bullColor="rgba(14,203,129,0.7)",
bearColor="rgba(246,70,93,0.7)")
plotHline(price, color, width=1) / plotDashedHline(price, color, width=1, dash=6, gap=4)
Horizontal line at a fixed price. The line spans the full visible window.
plotHline(50000, color="rgba(240,185,11,0.4)")
plotDashedHline(45000, color="rgba(240,185,11,0.25)")
plotMarkerUp(boolSeries, priceSeries, color="#…", size=6), plotMarkerDown(...)
Triangle markers. boolSeries[i] truthy → draw a triangle at (i, priceSeries[i]).
isCrossUp = crossover(close, sma(close, 20))
plotMarkerUp(isCrossUp, low - atr(high,low,close,14)*0.5,
color="#0ECB81", size=8)
plotShape(boolSeries, priceSeries, shape="circle", color, size=4)
General-purpose shape marker. shape accepts:
| Value | Glyph |
|---|---|
"circle" | filled circle |
"triangleUp" / "triangleDown" | filled triangle |
"square" | filled square |
"diamond" | filled diamond |
"cross" | + shape |
"xcross" | × shape |
"arrowUp" / "arrowDown" | block arrow |
plotShape(volume > sma(volume, 20) * 2, low, shape="diamond", color="#F0B90B", size=6)
plotText(boolSeries, priceSeries, text, color, size=10)
Text label on every truthy bar. text is a single string applied to all firing bars.
plotText(rsi(close, 14) > 70, high, "OB", color="#F6465D", size=11)
For per-bar dynamic text, use the world-coord plotLabel inside a for loop.
plotChar(boolSeries, priceSeries, char, color, size=10)
Unicode glyph at each truthy bar. Use raw glyphs (▲, ▼, ·, →) — emoji render inconsistently across platforms.
plotChar(crossover(close, sma(close, 50)), low, "▲", color="#0ECB81", size=14)
plotChar(crossunder(close, sma(close, 50)), high, "▼", color="#F6465D", size=14)
plotBgColor(boolSeries, color="rgba(...)")
Translucent vertical strip behind every truthy bar. Adjacent runs are merged into one rect for performance.
isWeekend = dayofweek(time) == 0 or dayofweek(time) == 6
plotBgColor(isWeekend, color="rgba(255,255,255,0.04)")
World-coordinate shapes
These take timestamps (epoch ms, the time series or scalars from it) and prices as anchors. Shapes pan and zoom with the chart — anchored to the data, not the screen.
plotLineXY(t1, p1, t2, p2, color=, width=, dash=, gap=, extend=)
Trendline between two anchors.
extend accepts:
| Value | Effect |
|---|---|
"left" | Extend only to the left edge |
"right" | Extend only to the right edge |
"both" | Extend in both directions |
"up" / "down" | Vertical extension (mostly for diagnostics) |
| numeric bitmask | Combine flags directly |
phPrice = highest(high, 50)[0]
plPrice = lowest(low, 50)[0]
hiTime = time[highestbars(high, 50)[0]]
loTime = time[lowestbars(low, 50)[0]]
plotLineXY(loTime, plPrice, hiTime, phPrice,
color="#F0B90B", width=1.5, extend="right")
plotVLine(t, color=, width=, dash=, gap=)
Vertical line at a timestamp, spanning the full chart height.
isLondonOpen = hour(time) == 8 and minute(time) == 0
// emit one VLine per London open:
for i = 0 to bars - 1
if at(isLondonOpen, i)
plotVLine(at(time, i), color="rgba(240,185,11,0.3)", width=1, dash=4, gap=4)
end
end
plotBox(t1, p1, t2, p2, bgColor=, borderColor=, width=, dashed=)
Filled + stroked rectangle in (time, price) space. Pass alpha=0 (e.g. "rgba(0,0,0,0)") in either color to skip that side.
hi = highest(high, 100)
lo = lowest(low, 100)
plotBox(time[99], lo[0], time[0], hi[0],
bgColor="rgba(240,185,11,0.04)",
borderColor="rgba(240,185,11,0.5)",
width=1)
plotLabel(t, p, text, color=, bg=, border=, size=, align=, anchor=)
Single-shot text chip at (t, p).
anchor controls chip placement around the point with an arrow tail:
| Value | Behaviour |
|---|---|
"none" (default) | Chip sits ON the point, aligned via align |
"above" | Chip BELOW the point, arrow points UP |
"below" | Chip ABOVE the point, arrow points DOWN |
"left" | Chip RIGHT of the point, arrow points LEFT |
"right" | Chip LEFT of the point, arrow points RIGHT |
align (when anchor="none") accepts "left", "center", "right".
plotLabel(time[0], close[0], "Now: " + tostring(close[0], "0.00"),
color="#F0B90B", anchor="left")
plotPolyline(times, prices, color=, width=, dashed=, dash=, gap=)
Connect arbitrary world-anchored points (parallel arrays). Straight segments — no spline smoothing.
// Draw a zigzag through the last 5 swing pivots (build the arrays in a for-loop):
ts = nz(0, 0) // empty number-series placeholder via shift trick
ps = nz(0, 0) // (in practice you'd build via a fn or
// pre-compute via `valuewhen` + `shift`)
plotPolyline(ts, ps, color="#F0B90B", width=1.5, dashed=true)
plotPolygon(times, prices, fill=, border=, width=)
Filled polygon in world coords. Implicitly closed.
plotPolygon(timesArr, pricesArr,
fill="rgba(240,185,11,0.06)",
border="rgba(240,185,11,0.5)",
width=1)
plotArrow(t1, p1, t2, p2, color=, width=, head=)
Arrow with a triangular head from (t1, p1) to (t2, p2).
plotArrow(time[10], low[10], time[0], close[0],
color="#0ECB81", width=2, head=10)
Pane plots
Available only inside @pane "below" or a custom aux pane. They map to the pane's own value space (e.g. RSI 0-100), not the candle price scale.
@pane "below"
paneHeight(0.25)
paneRange(0, 100)
r = rsi(close, 14)
paneLine(r, color="#F0B90B", width=1.5)
paneHline(70, color="rgba(246,70,93,0.6)")
paneHline(30, color="rgba(14,203,129,0.6)")
paneBand(70, 30, color="rgba(240,185,11,0.06)")
| Function | Effect |
|---|---|
paneLine(series, color, width=1.5) | Polyline inside the band |
paneDashedLine(series, color, width=1, dash=4, gap=4) | Dashed polyline |
paneHline(value, color, width=1) | Horizontal at a fixed pane value |
paneBand(upper, lower, color) | Filled OB/OS band |
paneFill(series, base=0, color) | Histogram fill from base up/down to series[i] |
paneCandle(open, high, low, close, …) | Per-bar OHLC inside the pane |
paneLabel(value, text, color, size=10, align) | Annotate a level |
paneRange(min, max) | Lock the pane Y axis (else auto-fit) |
paneHeight(ratio) | Pane height, 0.10..0.50 of chart area |
paneRange and paneHeight are one-shot — call them once near the top of the script. Multiple calls override.
The world-coordinate primitives have pane equivalents too: panePlotLine, panePlotVLine, panePlotBox, panePlotLabel. Y is in the pane's value space, not price.
See Panes for the full pane workflow.
Persistent slots
The first argument to every *New function is a string slot name. The same name across frames mutates the same primitive in place; omit a previously-emitted name and the primitive is freed at end-of-frame.
This is how you build:
- Live HUDs that update text/anchors every redraw.
- Persistent boxes around evolving ranges.
- Snake-line drawings from a fixed point to "now".
| Function | Args |
|---|---|
labelNew(name, t, p, text, color=, bg=, border=, size=, align=, anchor=, pane=) | Slotted version of plotLabel |
boxNew(name, t1, p1, t2, p2, bgColor=, borderColor=, width=, dashed=, extend=, pane=) | Slotted box |
lineNew(name, t1, p1, t2, p2, color=, width=, dash=, gap=, extend=, pane=) | Slotted line |
polylineNew(name, times, prices, color=, width=, dashed=, fill=, closed=, pane=) | Slotted polyline |
@name "Live HUD"
r = rsi(close, 14)
atr14 = atr(high, low, close, 14)
text = "RSI " + tostring(r[0], "0.0")
+ "\nATR " + tostring(atr14[0], "0.00")
labelNew("hud", time[0], high[0], text,
anchor="above", align="left",
color="#F0B90B", bg="rgba(0,0,0,0.6)")
The "hud" slot is mutated in place every redraw — no flicker, no re-create cost.
Slot conventions
- Use a stable name across frames as long as the primitive should stay visible.
- Patterns:
"hud"/"trend_label"— fixed-purpose primitives."buy"/"sell"— one slot per side."alert_" + tostring(i)— short-lived primitives keyed by event bar."trend_" + tostring(j)— N-th lookback level.
Slot caps
- 500 labels per script
- 50 boxes per script
- 100 lines per script
- 50 polylines per script
Allocations beyond the cap silently no-op so a misbehaving script can't tank the page. The profiler badge in the script editor shows the live count.
pane= argument
Persistent primitives accept a pane= keyword:
| Value | Effect |
|---|---|
"overlay" (default) | Render on the candle pane |
"below" | Render in the legacy below pane |
<aux-pane-id> | Render in the named aux pane (e.g. "rsi", "macd") |
A label living in the candle pane and a label living in @pane "rsi" use independent slot tables — same name doesn't conflict.
Drawing-tool shapes
Persistent shapes routed through the engine's regular drawings Vec — the same pool that user-drawn shapes live in. Right-click delete, grip-drag, and snap-to-bar all work on them. Same string-slot semantics as labelNew.
| Function | Anchors |
|---|---|
drawingTrendline(name, t1, p1, t2, p2, color=, width=, dashed=, pane=) | Two (t, p) |
drawingHline(name, price, color=, width=, dashed=, pane=) | One price |
drawingFibRet(name, t1, p1, t2, p2, color=, width=, pane=) | Two (t, p) define 0% / 100% |
drawingChannel(name, t1, p1, t2, p2, t3, p3, color=, width=, pane=) | Anchors 1+2 set the trend, 3 sets the parallel offset |
// Auto-fib retracement off the last 50-bar range:
hiBars = highestbars(high, 50)[0]
loBars = lowestbars( low, 50)[0]
drawingFibRet("auto_fib",
time[loBars], at(low, bars - 1 - loBars),
time[hiBars], at(high, bars - 1 - hiBars),
color="#F0B90B", width=1)
When the user drags the script-emitted trendline by hand, the new anchors are visible to the next compute via getUserDrawings — a great way to build "snap-to-trader-line" interactive tools.
getUserDrawings()
Returns a frozen array of { id, owner, kind, pane, anchors, anchorsRaw, style } — every drawing currently on the chart that the user (NOT the script) created. Kinds include "trendline", "hline", "fib_ret", "channel", "arrow", "range", "circle", "fib_ext", "brush", "path", "elliott", "anchored_vwap", "position", "price_label", "arrow_marker_up", "arrow_marker_down", "text_note".
draws = getUserDrawings()
n = 0 // count via an `at`-style helper if needed
Important. Delta DSL grammar does NOT support
obj.fieldmember access on arbitrary values. To read fields of agetUserDrawings()entry, you need a stdlib helper that returns a primitive (e.g. a futurefirstUserDrawingPrice("hline")helper). Right nowgetUserDrawings()is a forward-looking primitive whose raw shape is exposed but only consumable through future helper APIs. UsedrawingHline/drawingTrendlineetc. for the create / mutate side.
Color helpers
Compose colours dynamically inside the script. All four are scalar (return strings, not series).
rgb(r, g, b)
Compose a #rrggbb hex from 0-255 channels. Channels are clamped to [0, 255].
plotLine(ma, color=rgb(240, 185, 11), width=2) // #f0b90b
rgba(r, g, b, a)
Compose an rgba(...) string. a ∈ [0, 1].
plotBand(upper, lower, color=rgba(240, 185, 11, 0.06))
withAlpha(colorStr, alpha)
Apply an alpha override to any colour string. Recognises #rgb, #rrggbb, #rrggbbaa, rgb(...), rgba(...). Other inputs return unchanged.
@input lineColor = input.color("#F0B90B", "Line color")
plotLine(ma, color=lineColor)
plotBand(upper, lower, color=withAlpha(lineColor, 0.08)) // same hue, dimmer band
gradient(t, colorA, colorB)
Linearly interpolate between two colours by t ∈ [0, 1]. Returns colorA at 0, colorB at 1; out-of-range clamps. Useful for "value heat" colouring of static thresholds:
@input ratio = input.float(0.5, "Mix", minval=0, maxval=1, step=0.05)
mixedColor = gradient(ratio, "#0ECB81", "#F6465D")
plotLine(ma, color=mixedColor, width=2)
Note. Per-bar dynamic colours are not supported by
plotLine(see the top of this page).gradient/withAlpha/rgb/rgbaare useful for ONE-SHOT colour derivation at compute time — their output should be a single string passed once to a draw call.
Performance notes
- Every per-bar plot is O(n) where
n = bars— the engine iterates the visible window once per draw call. - World-coord primitives (
plotLineXY,plotBox,plotLabel, …) are O(1) per call. Don't worry about emitting a few hundred per script. - Persistent slots are O(1) per call too, but count toward the slot caps (500 labels / 50 boxes / 100 lines / 50 polylines per script). A
for i = 0 to bars-1 ... labelNew(...) endover a 5 000-bar window WILL cap out — for that, useplotShape(which compresses across bars) instead. - Total draw records per script are capped at 50 000. A script that produces more silently truncates and shows a warning chip in the legend.