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.

drag to orbit · double-click to reset — like the real widget

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 axesaxis="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 ElementPicked message.
  • 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.

Kitty placeholder Ghostty placeholder iTerm2 ≥ 3.5 direct WezTerm direct Konsole direct tmux passthrough

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

status: early scaffold

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.

2D scatter · line · bar
3D surface / mesh
secondary y-axes
2D hover & pick
3D graph picking
numpy zero-copy
render auto-detect
prebuilt wheels
tmux passthrough
Ratatui / Bubble Tea
# 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