Visual Output (Viz)

The plot CSV (--plot / plot_path) only stores the numeric value of every plot() call, one column per plot. It carries no style information (colors, line widths, shapes) and nothing about the drawing objects (lines, labels, boxes, tables, polylines, linefills) a script creates.

The viz output fills that gap. It is an opt-in NDJSON stream that captures, per bar:

  • the style metadata of every plot-family call (plot, plotshape, plotchar, plotarrow, plotcandle, plotbar, bgcolor, barcolor, hline, fill),
  • the per-bar values those calls produce,
  • the per-bar dynamic colors (only when they change), and
  • a full snapshot — or an optional per-bar journal — of the drawing objects.

The plots CSV is unchanged and keeps working exactly as before. Viz output is entirely opt-in; enabling it never alters the CSV.

Enabling It

CLI

FlagEffect
--viz, -vzWrite the viz NDJSON to <output_dir>/<script>_viz.ndjson
--viz-path PATHWrite the viz NDJSON to PATH (implies --viz)
--viz-journalAlso record per-bar drawing create/update/delete events (implies --viz)
pyne run my_script.py BINANCE_BTCUSD_60 --viz
pyne run my_script.py BINANCE_BTCUSD_60 --viz-path out/viz.ndjson --viz-journal

Programmatically

ScriptRunner takes two viz parameters:

ParameterTypeDescription
viz_pathPath | NoneWrite the viz NDJSON here. None disables file output.
viz_journalboolDiff the live drawings every bar and emit create/update/delete events.
from pathlib import Path
from pynecore.core.script_runner import ScriptRunner

runner = ScriptRunner(
    script_path=Path("my_script.py"),
    ohlcv_iter=candles,
    syminfo=syminfo,
    viz_path=Path("out/viz.ndjson"),
    viz_journal=True,
)

for candle, plot_data in runner.run_iter():
    ...  # the NDJSON is written as bars are processed

Journaling can also run without a file: set viz_journal=True, leave viz_path=None, and attach a viz_events callback (see Programmatic Accessors) to receive the events in memory.

NDJSON Format

The stream is NDJSON: one JSON object per line. Every record is tagged by a "t" field. The records appear in this order:

  1. hdr — always the first line.
  2. Interleaved meta and bar records (and ev records when journaling). A plot’s meta is emitted lazily, on the first bar the plot actually fires, immediately before that bar’s bar record.
  3. drawings — a full end-of-run snapshot, second to last.
  4. end — always the last line.

hdr — header

FieldTypeDescription
tstr"hdr"
vintFormat version (1)
scriptobjectScript context: type, title, shorttitle, overlay, format, precision, explicit_plot_zorder, max_lines_count, max_labels_count, max_boxes_count, max_polylines_count (null fields dropped)
syminfoobjectSymbol context: tickerid, prefix, ticker, timeframe, mintick, timezone, type (empty fields dropped)
journalboolWhether per-bar ev records are present

meta — plot-family definition

The registered-once style of one plot-family call. Common fields are shared; kind-specific fields follow. display defaults to "all"; other defaults are applied per kind and null fields are dropped, so a record only lists what deviates from the default.

A meta record is emitted before the first bar record of the plot. When a plot only turns out to be dynamic on a later bar (its first differing color arrives after the record went out), the record is re-emitted with dynamic: true before that bar’s color delta — a consumer must treat a repeated id as an update of the earlier record.

FieldTypeApplies toNotes
tstrall"meta"
idstrallThe plot id (see Ordinal Ids)
kindstrallplot/shape/char/arrow/candle/bar/bgcolor/barcolor/hline/fill
titlestrallPresent when the call had a title
displaystrallall/none/pane/data_window/price_scale/status_line
dynamicboolalltrue when the call has a per-bar color channel
stylestrplotline/stepline/histogram/columns/area/… (default line)
colorstrmost#RRGGBBAA hex; the static color
linewidthintplot, hlineLine width in pixels
trackprice, histbase, joinplotPresent only when non-default
style, location, sizestrshapeDefaults xcross / abovebar / auto
charstrcharThe character (default )
location, sizestrcharDefaults abovebar / auto
text, textcolorstrshape, charMarker text and its color
colorup, colordownstrarrowUp / down arrow colors
minheight, maxheightintarrowPresent when set
color, wickcolor, bordercolorstrcandleCandle body / wick / border colors
colorstrbarBar color
price, linestylehlineLevel and solid/dashed/dotted (default solid)
plot1, plot2strfillIds of the two filled plots
hline1, hline2strfillIds of the two filled hlines
fillgapsboolfillPresent when true

bar — per-bar values

FieldTypeDescription
tstr"bar"
iintBar index (0-based)
timeintBar open time in milliseconds
vobject{plot_id: value} for every plot that fired this bar. Omitted when empty.
cobject{plot_id: color} — only for color channels that changed this bar. Omitted when empty.

plotcandle / plotbar expand to four keys per call: "<title> (open)", "<title> (high)", "<title> (low)", "<title> (close)". plotshape stores 0/1 (or null when its series is na); plotchar and plotarrow store the raw series value. Non-finite floats and na are encoded as null.

ev — drawing event (journal mode only)

FieldTypeDescription
tstr"ev"
iintBar index the change was detected on
opstr"create" / "update" / "delete"
objstrline/label/box/table/polyline/linefill
idintThe drawing’s vid (see Drawing vids)
sobjectThe full serialized drawing state (omitted for delete)

drawings — end-of-run snapshot

FieldTypeDescription
tstr"drawings"
linesarraySerialized line objects still live
labelsarraySerialized label objects
boxesarraySerialized box objects
tablesarraySerialized table objects (with cells; a merged cell carries merge: [start_col, start_row, end_col, end_row] — the range’s top-left cell renders, the rest are hidden)
polylinesarraySerialized polyline objects
linefillsarraySerialized linefill objects (each embeds line1_state / line2_state)

end — terminator

FieldTypeDescription
tstr"end"
barsintTotal number of bars written

Full Example

A three-bar indicator with a dynamic-colored plot, a static plot, a fill, a plotshape and an hline:

{"t":"hdr","v":1,"script":{"type":"indicator","title":"Demo","shorttitle":"demo","overlay":true,"format":"inherit","explicit_plot_zorder":false,"max_lines_count":50,"max_labels_count":50,"max_boxes_count":50,"max_polylines_count":50},"syminfo":{"tickerid":"BINANCE:BTCUSD","prefix":"BINANCE","ticker":"BINANCE:BTCUSD","timeframe":"60","mintick":0.01,"timezone":"UTC","type":"crypto"},"journal":false}
{"t":"meta","id":"close","kind":"plot","title":"close","display":"all","style":"line","color":"#F23645FF","linewidth":2}
{"t":"meta","id":"open","kind":"plot","title":"open","display":"all","style":"line","color":"#2962FFFF","linewidth":1}
{"t":"meta","id":"fill#0","kind":"fill","title":"band","display":"all","color":"#787B8619","plot1":"close","plot2":"open"}
{"t":"meta","id":"up","kind":"shape","title":"up","display":"all","style":"triangleup","location":"abovebar","size":"auto","color":"#4CAF50FF"}
{"t":"meta","id":"hline#0","kind":"hline","title":"level","display":"all","price":100.0,"color":"#787B86FF","linewidth":1,"linestyle":"solid"}
{"t":"bar","i":0,"time":1704067200000,"v":{"close":100.5,"open":100.0,"up":1}}
{"t":"bar","i":1,"time":1704070800000,"v":{"close":100.2,"open":100.5,"up":0},"c":{"close":"#00E676FF"}}
{"t":"bar","i":2,"time":1704074400000,"v":{"close":101.5,"open":100.2,"up":1}}
{"t":"drawings","lines":[],"labels":[],"boxes":[],"tables":[],"polylines":[],"linefills":[]}
{"t":"end","bars":3}

Only-On-Change Color Encoding

A plot’s color is registered once, on the bar the plot first fires, as the meta.color static color. On every later bar the runtime compares the call’s color to that registered color by object identity — a color.* constant or the exact same object is treated as static and never repeated.

Only a color that differs (for example the branches of a bar_index % 2 == 0 ? color.red : color.lime expression, or a fresh color.new(...) each bar) is a dynamic channel. Once a plot has become dynamic it records its color on every later bar, and each color is written to the bar record’s c object only when the encoded value actually changes from the last one emitted. In the example above, close alternates red/lime: bar 0 registers red as static (no c), bar 1 switches to lime (emitted), bar 2 switches back to red (emitted again, because the last emitted value was lime), and so on. A reader reconstructs the color for any bar by carrying the last emitted value forward, falling back to meta.color before the first emitted change.

bgcolor and barcolor likewise record every bar; an na (unpainted) bar is encoded as null, so a paint → unpaint → paint sequence round-trips exactly.

Ordinal-Id Semantics

plot, plotshape, plotchar, plotarrow, plotcandle and plotbar are identified by their title (plot(close, "RSI") → id "RSI"; duplicate titles get a numeric suffix). plotcandle / plotbar additionally expose their four OHLC value keys as "<title> (open)" etc.

bgcolor, barcolor, fill and hline have no natural title key, so they are identified by a per-family ordinal assigned in call order: bgcolor#0, bgcolor#1, fill#0, fill#1, hline#0, … The counters reset every bar, so as long as the script issues these calls in a stable order (the normal case) the ids are stable across bars. A fill records the ids of the plots or hlines it spans (plot1/plot2 or hline1/hline2).

Drawing vids

Every drawing object (line, label, box, table, polyline, linefill) is assigned a monotonically increasing integer vid at creation. line.copy() / label.copy() / box.copy() produce a clone with a fresh vid. Both the drawings snapshot and the journal ev records key objects by vid, and a linefill embeds the current state of its two lines under line1_state / line2_state.

Journal Mode

With viz_journal=True, the runtime diffs the live drawing registries against the previous bar and emits an ev record for every object that was created, changed, or deleted. A drawing that is created once and mutated each bar (e.g. a trend line whose endpoint is moved with line.set_xy2) produces exactly one create, one update per changed bar, and one delete. Without journaling the stream carries only the final drawings snapshot.

Programmatic Accessors

After (or during) a run, the runner exposes the same state the writer serializes:

AccessorKindDescription
runner.plot_metaproperty{id: PlotMeta} — the registered plot-family metadata
runner.drawings()methodA full drawings snapshot (same shape as the drawings NDJSON record)
runner.viz_eventsattributeOptional Callable[[list[dict]], None]; receives each bar’s journal events

Plot-family and drawing state is reset only at run start, so plot_meta / drawings() remain valid after the iterator is exhausted:

runner = ScriptRunner(script_path=Path("my_script.py"), ohlcv_iter=candles, syminfo=syminfo)
list(runner.run_iter())  # exhaust the iterator

for pid, meta in runner.plot_meta.items():
    print(pid, meta.kind, meta.color)

snap = runner.drawings()
print(len(snap["lines"]), "lines survived")

To capture per-bar journal events in memory, enable journaling and attach a callback before iterating:

runner = ScriptRunner(script_path=Path("my_script.py"), ohlcv_iter=candles,
                      syminfo=syminfo, viz_journal=True)
batches: list[list[dict]] = []
runner.viz_events = batches.append
list(runner.run_iter())

Reading the Live Per-Bar State

For the freshest per-bar view, read the library-level state inside the run_iter() loop — run_iter() yields before it clears the per-bar viz state:

from pynecore import lib

for candle, plot_data in runner.run_iter():
    # lib._plot_meta: {id: PlotMeta} registered so far
    # lib._viz_dyn:   {id: color} dynamic color channels active on THIS bar
    dyn = dict(lib._viz_dyn)  # copy — it is cleared after the yield

lib._viz_dyn holds the current-bar color of every channel that has become dynamic (a purely static plot, whose color always equals its registered meta.color, is absent). The NDJSON writer applies its only-on-change filter on top of this; an in-process reader sees the full per-bar value directly.