rust core · kitty graphics protocol · textual widget
Real plots,
real pixels,
in your terminal.
plotui draws interactive, Plotly-style 2D & 3D charts inside the terminal — rasterized by a Rust engine, delivered over the Kitty graphics protocol, embedded as a Textual widget. Rotate, pan, zoom. No block characters, ever.
what your terminal actually receives: \x1b_Gq=2,f=32,i=771,s=1104,v=456,m=1;iVBORw0KGgoAAAANSUhEUgAAB…\x1b\
how it works
The Rust core owns pixels — not the terminal.
One rule shapes everything: the engine has no event loop and no input handling. Your TUI framework owns the loop, forwards input to the camera, and asks for a frame. Because the core and the protocol layer are pure and I/O-free, the same engine can back every frontend — and be unit-tested by hashing pixel buffers.
f64 data─▶camera + rasterizer─▶RGBA buffer─▶kitty escape bytes─▶your terminal's cell grid
plotui-core
the engine
Pure Rust: data model, 3D orbit camera, rasterizer straight to RGBA. Scatter, line, bar, secondary y-axes, 3D graphs with per-node styling.
plotui-protocol
the wire
RGBA in, terminal bytes out. Kitty graphics escapes — chunked, placement-aware, and wrapped for tmux passthrough. Pure functions, no I/O.
PlotWidget
the plumbing
Drop it into a Textual app. It picks the render path per terminal, routes mouse events to the camera, and repaints without flicker. Hover + click picking for 3D graphs built in.
python api
An API you already know how to use.
Add traces, name them, and axes, ticks, and a legend appear on their own. The first 3D trace switches the plot to the orbit camera.
- Secondary axes —
axis="y2"/"y3"binds a series to an independent right-hand axis, tick labels tinted to the series color. - Pickable 3D graphs — hover lights nodes and edges up white; a click posts an
ElementPickedmessage. - Scriptable camera — save and restore view state, project nodes to exact pixel coordinates for overlays and hit-testing.
- Text overlays — splice terminal-crisp labels over the image without re-rasterizing.
from plotui import Plot
plot = Plot()
plot.add_line(xs, ys, name="forecast")
plot.add_scatter(xs2, ys2, name="observed")
plot.add_line(xs, tokens, name="tokens", axis="y2")
# any 3D trace switches to the orbit camera
plot.add_scatter3d(xs, ys, zs, color=(230, 60, 120))
# forward your framework's events…
plot.rotate(d_yaw, d_pitch)
plot.zoom_by(factor)
# …and place the bytes
escape = plot.render_kitty(cols, rows, cell_w, cell_h)
terminal support
Runs where the pixels are real.
The widget picks its render path per terminal automatically — Unicode-placeholder Kitty graphics where placeholders are supported, direct placement elsewhere — and you can override with PLOTUI_RENDER. Terminals without Kitty graphics get a notice naming supported ones. plotui never degrades to a fake plot.
get started
Build from source today. Wheels are coming.
You need Rust and Python 3.9+. maturin compiles the native module into your virtualenv; then run the demos in any supported terminal.
# clone, then:
❯ python -m venv .venv && source .venv/bin/activate
❯ pip install maturin textual
❯ maturin develop --release
# in Kitty / Ghostty / iTerm2 / WezTerm:
❯ python examples/raw_demo.py
❯ python examples/textual_graph.py