Runnable sheets provide a built-in plot() function injected into every cell's namespace. No import is needed. Each call produces a line-plot panel rendered directly below the cell output.
plot(y) # single series, x = 0, 1, 2, ...
plot(x, y) # single series, explicit x
plot(x, y1, y2, ...) # multiple series, shared x
# Keyword arguments (all optional):
plot(y, title='My plot', x_label='time', y_label='value')
title — string displayed above the plot (default: 'Plot').
x_label — x-axis label (default: 'x').
y_label — y-axis label (default: 'y').
import math
y = [math.sin(i * 0.3) for i in range(30)]
plot(y, title='Sine wave', y_label='sin(x)')
x = [i * 0.2 for i in range(40)]
y = [math.exp(-0.1 * v) * math.cos(v) for v in x]
plot(x, y, title='Damped cosine', x_label='t', y_label='amplitude')
With 3+ arguments where the first two have equal length, the first argument is treated as x and the rest as y-series.
x = [i * 0.25 for i in range(32)]
y1 = [math.sin(v) for v in x]
y2 = [math.cos(v) for v in x]
plot(x, y1, y2, title='sin and cos', x_label='radians')
Each plot() call in a cell appends a separate panel below the cell, in call order.
plot([1, 4, 9, 16, 25], title='Squares')
plot([1, 2, 6, 24, 120], title='Factorials')
The function accepts any numeric iterable: list, tuple, range, generator expressions, or numpy arrays. Scalars are wrapped in a one-element list automatically.
All values must be finite real numbers. Non-numeric values and infinities raise TypeError or ValueError immediately.
With exactly two arguments, plot(a, b) always means x=a, y=b — it never means two y-series. If len(a) != len(b) a ValueError is raised. To plot two y-series against a shared x, use three arguments: plot(x, y1, y2).
Plots are rendered as native wx panels using custom drawing (no matplotlib dependency). Features:
• Title centred above the plot area
• x and y axis labels
• Auto-scaled axes with min/max tick values shown
• Horizontal gridlines
• Fixed panel height (250px); width tracks window width
• Multiple series are drawn in distinct colours cycling through blue, red, green, purple (matplotlib C0–C3 palette)
plot() returns the plot spec dict ({"kind": "plot", "title": ..., "series": [...], ...}). This can be ignored or inspected/serialised if needed.
A runnable test sheet covering all signatures lives at runnable-scripts/plot-test.
Runnable sheets now support real matplotlib/seaborn output via show(fig). If fig is a matplotlib Figure object, the kernel serialises it as PNG and the browser renders it inline beneath the cell output.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import seaborn as sns
fig, ax = plt.subplots(figsize=(8, 3))
sns.lineplot(x=[0, 1, 2], y=[0, 1, 0], ax=ax)
ax.set_title('Inline seaborn plot')
show(fig)
plt.close(fig)
This section was documented but not actually implemented until 2026-07-10 (commit ebd6043, "Support inline figure output in notes browser") — before that, show(fig) fell through to the str() fallback (e.g. Figure(1400x650)) instead of rendering.
What actually ships: ImageOutput in sheet_kernel.py wraps PNG bytes and returns {"kind": "image", "format": "png", "data": "<base64>"} via the __show__ protocol (same pattern as HtmlOutput). show()'s matplotlib-Figure branch calls obj.savefig(buf, format='png', bbox_inches='tight', dpi=120) then wraps the bytes in ImageOutput. Displayed in sheet_ui.py's _append_show_item via a plain wx.StaticBitmap added directly to the cell's output sizer — no wx.MemoryFSHandler or HTML embedding needed, because cell output was already a sizer of mixed native widgets (same pattern as the built-in plot()'s _PlotPanel), unlike a whole note page (see notes-browser/svg-diagrams for why that case needed the heavier mechanism).
History, for anyone who finds this confusing later: on 2026-07-13, working from a stale local clone that predated ebd6043, this exact bug was independently re-diagnosed and re-fixed via a different, more complex mechanism (a JSONML ["img", {"src": "data:image/png;base64,..."}] node rehomed through wx.MemoryFSHandler). Both pushed to the same branch from different clones; reconciled 2026-07-17 by keeping ebd6043's simpler original and dropping the redundant reinvention. The lesson: re-fetch/pull before starting new work in a long session, not just once at the start — this repo has multiple active clones/sessions.
Tests: test_sheet_image_output.py (repo root) — the kernel-level ImageOutput shape and (display-gated) the wx.StaticBitmap rendering. test_sheet_kernel_show.py covers the other show() branches (__show__ protocol, ndarray, DataFrame, string fallback) and last_run_at tracking — it used to also test the abandoned JSONML-node approach; that coverage was removed 2026-07-17 rather than duplicate test_sheet_image_output.py's correct assertions about the shape that actually ships.
Related: for static (non-runnable) notes — both genuine diagrams (svg blocks) and "fixed" runnable-note output (image blocks, see notes-browser/fixed-notes) — a whole page is one HTML blob (NotesHTMLRenderer.render()), so those two block types do need wx.MemoryFSHandler (via svg_render.py) the way live cell output doesn't. _show_item_to_block in sheet_ui.py (the "Fix as New Note" conversion) bridges the two: it reads the live {"kind": "image", ...} show-item and writes the persistent {"image": {"format": ..., "data": ...}} block — same field names, different container, deliberately.