Skip to main content

plotui

Interactive 2D/3D plots in the terminal — Plotly-style — for Textual, Ratatui, and Bubble Tea, powered by a Rust core and the Kitty graphics protocol.

plotui renders scatter plots (and, soon, lines / surfaces / bars) as real pixel graphics inside a terminal, and lets you rotate, pan, and zoom them. It drops into a Textual, Ratatui, or Bubble Tea app as a first-class widget, with the rendering engine written in Rust so it stays fast in 2D and 3D.

Status: early scaffold. Working today: 2D scatter/line/bar charts with axes, ticks, and a legend; a 3D scatter/graph engine; a Kitty-image raw demo; and a Textual widget. See the roadmap below.

Architecture

The one rule that shapes everything: the Rust core owns pixels, not the terminal. It has no event loop and no input handling — the TUI framework (Textual, Ratatui, or Bubble Tea) owns the loop, forwards input to the camera, and asks for a frame.

crates/
  plotui-core/      pure engine: data model, 3D camera, rasterizer → RGBA
  plotui-protocol/  RGBA → terminal bytes (Kitty graphics protocol)
  plotui-term/      shared frontend glue: render-path detection, cell-pixel
                    probing, tmux passthrough, the per-frame render policy
  plotui-bind/      shared binding semantics: parsing, validation, defaults,
                    and their exact error messages (Python and Go agree)
  plotui-py/        PyO3 bindings → the `plotui._plotui` native module
  plotui-ratatui/   Ratatui widget (native Rust frontend)
  plotui-ffi/       C ABI (cdylib + staticlib) behind the Go bindings
python/plotui/      the Python package + Textual `PlotWidget`
go/                 Go bindings + `teaplot`, the Bubble Tea v2 component
examples/           raw_demo.py (Kitty images), textual_demo.py

core and protocol are pure and I/O-free, so the same engine can back every frontend and be unit-tested by hashing pixel buffers.

Integrations

Each TUI framework gets a first-class widget, not a port. All frontends sit on the same policy crates (plotui-term for detection/tmux/render policy, plotui-bind for argument validation and its exact error strings), so a plot looks and behaves identically whichever framework hosts it — down to the error messages.

Frontend How it works Where in the codebase Try it
Textual (Python) PlotWidget wraps the plotui._plotui native module (PyO3). Mouse events route to the camera, hover/click picking arrives as Textual messages, extend streams points in-place, and text overlays splice into the image without re-rasterizing. python/plotui/textual.py; native module in crates/plotui-py python examples/textual_graph.py
Ratatui (Rust) A native StatefulWidget plus an app-owned PlotState: hand it crossterm events, draw it like any other widget — frames and Kitty placement ride ratatui's own buffer diff, flicker-free. crates/plotui-ratatui cargo run -p plotui-ratatui --example demo
Bubble Tea (Go) teaplot.New(plot) returns an Elm-style model: Update consumes tea mouse/key events, View lays out the cell grid, and image escapes leave as tea.Raw commands. Links to the Rust engine statically over the plotui-ffi C ABI (cgo). go/ (bindings) + go/teaplot (component); ABI in crates/plotui-ffi go run ./examples/demo from go/ — see go/README.md
Browser (WASM) The same engine compiled to WebAssembly drives the live demos on the website: pointer events feed the engine's own camera, and every frame is its RGBA bytes blitted onto a canvas. Not a plotting-in-the-browser product — it exists so the site can show the real renderer. crates/plotui-wasm; consumed by site/ plotui.xyz/examples.html

The three TUI widgets have feature parity: render-path detection, tmux passthrough, drag/zoom/pan/keys, picking + hover, the 2D crosshair, text overlays, half-resolution interaction frames, and streaming extend.

Install the CLI

plotui is also a command-line tool: pipe columns of numbers in, get a real-pixel chart out — interactive on a TTY (pan, zoom, crosshair), a single printed frame when piped or with --static.

curl -fsSL https://plotui.xyz/install.sh | sh   # prebuilt binary
brew install sebaheg/tap/plotui                 # Homebrew (macOS / Linux)
pip install plotui                              # prebuilt wheel: the library + the CLI
cargo install plotui                            # build from source
cargo binstall plotui                           # prebuilt, via cargo-binstall
seq 1 100 | awk '{print $1, sin($1/10)}' | plotui line
plotui scatter -H -d, data.csv                  # header row + comma-delimited
plotui bar counts.tsv

Like every plotui frontend, the CLI needs a terminal with Kitty graphics (supported terminals below); elsewhere it prints a notice and exits.

Develop

Requires Rust and Python 3.9+. Build the native module into a virtualenv with maturin:

python -m venv .venv && source .venv/bin/activate
pip install maturin textual
maturin develop --release

Then, in a terminal with Kitty graphics support — Kitty, Ghostty, iTerm2 ≥ 3.5, WezTerm, or Konsole — for the full-resolution pixel demos:

python examples/raw_demo.py        # 3D scatter via Kitty images
python examples/textual_demo.py    # embedded in Textual
python examples/textual_graph.py   # interactive graph: hover + click-to-inspect

The Textual widget picks its render path per terminal: Unicode-placeholder Kitty graphics in Kitty/Ghostty, direct Kitty placement in iTerm2/WezTerm/ Konsole — plus Warp, Rio, and VS Code, whose younger Kitty decoders are supported but still maturing (VS Code needs its terminal.integrated.enableImages setting). plotui only draws real pixels — terminals without Kitty graphics get a notice naming supported terminals, never a degraded plot. Override with PLOTUI_RENDER=placeholder|direct or PlotWidget(..., render_mode=...).

Python API

from plotui import Plot

# 2D: axes, ticks, and a legend appear automatically. Traces added without a
# color take palette slots in fixed order; `name=` puts a series in the legend.
plot = Plot()
plot.add_line(xs, ys, name="forecast")
plot.add_scatter(xs2, ys2, name="observed")
plot.add_bar(xs3, heights)

# Secondary axes: axis="y2"/"y3" bind a series to an independent right-hand
# axis — its own autoscale and tick column, labels tinted to the series color
# (y2 innermost, y3 outermost). The grid stays with the left axis.
plot.add_line(xs, tokens, name="tokens", axis="y2")
plot.add_line(xs, cpu_minutes, name="cpu min", axis="y3")

# 3D: any 3D trace switches the plot to the orbit camera.
plot = Plot()
plot.add_scatter3d(xs, ys, zs, color=(230, 60, 120), size=2.0)

# Streaming: every add_* returns a trace handle. Append through it instead
# of rebuilding — O(new points), autoscale follows; numpy arrays are read
# in one bulk copy. set_visible toggles a series without losing its handle,
# palette slot, or node indices.
h = plot.add_line([], [], name="loss")
plot.extend(h, xs, ys)                # 3D scatter/line: extend(h, xs, ys, zs)
plot.set_visible(h, False)

# Interaction (forward your framework's events to these):
plot.rotate(d_yaw, d_pitch)
plot.zoom_by(factor)
plot.pan(dx, dy)
plot.reset()

# Render (the frontend places the bytes):
escape = plot.render_kitty(cols, rows, cell_w, cell_h)   # Kitty pixel image
pixels = plot.render_rgba(px_w, px_h)                    # raw RGBA8 bytes

Graphs take per-element styling, and the camera/projection state is fully scriptable — the hooks a host needs for label overlays, camera targeting, and rebuilding a plot without losing the view:

plot.add_graph3d(xs, ys, zs, edges=[(0, 1), (1, 2)],
                 node_colors=[...],          # one (r, g, b) per node
                 node_sizes=[...],           # per-node radius (else `size`)
                 edge_colors=[...],          # per-edge (r, g, b) (else derived)
                 node_shapes=[...])          # per-node "disc" | "ring" | "square" |
                                             #   "triangle" | "diamond" | "diamond-open" | "dot"
plot.set_show_box(False)                     # hide the 3D orientation cube
plot.set_bounds((x0, y0, z0), (x1, y1, z1))  # pin the data frame (else the nodes'
                                             #   bounding box); None, None restores
plot.set_chrome(grid=(26, 32, 36),           # recolour the non-data chrome to sit on
                frame=(43, 50, 55),          #   your own background: bg (legend box),
                ink=(103, 111, 118))         #   frame, grid, ink, ink_bright

state = plot.camera_state()                  # (yaw, pitch, zoom, pan_x, pan_y)
plot.set_camera_state(*state)                # restore (e.g. onto a new Plot)
plot.project_nodes(px_w, px_h)               # [(x_px, y_px, depth)] per node —
                                             # exact render/pick geometry

In Textual, use plotui.textual.PlotWidget(plot) and it handles the event plumbing for you. Pass pickable=True to make 3D graph nodes and edges interactive: hovering lights the element under the cursor up white, and clicking posts an ElementPicked message with ("node", i) or ("edge", i) (see examples/textual_graph.py, which opens a slide-in inspector from it).

The widget also supports text overlayswidget.set_overlay([(row, col, text, style), ...]) splices terminal-crisp text (labels, badges) over the image in every render mode without re-rasterizing — and exposes a widget.dragging property for hosts that defer work mid-gesture. To customize interaction in a subclass, override the apply_rotate / apply_pan / apply_zoom / apply_reset / on_click_at primitives that every input path routes through — do not override the Textual on_* handlers (Textual dispatches those to every class in the MRO, so both would run).

Roadmap

  • Flicker-free Kitty placement via Unicode-placeholder virtual placement (fixed image id, atomic replace) — wire the pixel path into the Textual widget
  • 2D traces: scatter, line, bar; axes, ticks, tick labels, legend
  • Independent right-hand y-axes (axis="y2"/"y3") with tinted tick labels
  • 2D step trace; axis titles; time-formatted x ticks
  • 3D surface / mesh; axis cube with labels
  • Interactive hover / pick for 3D graph nodes and edges (opt-in via PlotWidget(..., pickable=True): hover lights the element up white, click posts ElementPicked)
  • Hover / pick for 2D traces; spatial index for large graphs
  • Streaming append: trace handles, extend, set_visible, incremental bounds
  • numpy fast-path input (one bulk copy, no per-element conversion)
  • Rolling window (max_points) for endless streams
  • Graceful render-path auto-detection (placeholder / direct Kitty, with a supported-terminals notice elsewhere and a PLOTUI_RENDER override)
  • Sixel + iTerm2 OSC 1337 encoders for terminals without Kitty graphics
  • Prebuilt wheels (maturin + GitHub Actions): pip install plotui — macOS arm64/x86_64, Linux x86_64/aarch64, abi3 ≥ 3.9; the wheel bundles the CLI binary
  • Ratatui frontend (native): plotui-ratatui — StatefulWidget + app-owned PlotState, full parity with the Textual widget (cargo run -p plotui-ratatui --example demo)
  • Bubble Tea frontend (cgo): go/ bindings over the plotui-ffi C ABI + the teaplot component for Bubble Tea v2 (see go/README.md)
  • CLI: plotui line|scatter|bar from stdin or a file — interactive on a TTY, one static frame when piped; installed via curl, Homebrew, cargo, or pip (see Install above)
  • CLI v2: --follow streaming, scatter3d, histogram/density/count transforms, --out png
  • Prebuilt static libs for the Go bindings (today: local source build)

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plotui-0.4.2.tar.gz (115.5 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

plotui-0.4.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.2 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

plotui-0.4.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.2 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

plotui-0.4.2-cp39-abi3-macosx_11_0_arm64.whl (1.1 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

plotui-0.4.2-cp39-abi3-macosx_10_12_x86_64.whl (1.1 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file plotui-0.4.2.tar.gz.

File metadata

  • Download URL: plotui-0.4.2.tar.gz
  • Upload date:
  • Size: 115.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for plotui-0.4.2.tar.gz
Algorithm Hash digest
SHA256 f668fcc2d45e6cdb71e06644472b5618875b89e0ab59182eb2069ae64356813f
MD5 c7fe014c1f454bcbdc5c29939d22da47
BLAKE2b-256 3fec48654c5f3547de89c208f68bdc4dc67299c0fa85864a3de4eb7226e0e94d

See more details on using hashes here.

File details

Details for the file plotui-0.4.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for plotui-0.4.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e2ae3ef1b0bcc6f93ea3eb1c492546723230b3e48fcea64039a7c48616bb53c4
MD5 f24330db78bcb9a570d0180b77710d14
BLAKE2b-256 64de35339d2ced4648be672116ecad6261323962caf79cf2da82422cf2bb5c07

See more details on using hashes here.

File details

Details for the file plotui-0.4.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for plotui-0.4.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 a7bf9e4cad10ab28ae1d1dbadf344cab25fb4595737818c992e7cca4b5f953fb
MD5 68694ac29f0b4ccd6db3244bab139699
BLAKE2b-256 942b2aa0194f9f27c4a09c0c7b63e575538691f12c179ed7b579cc87f9ea53b6

See more details on using hashes here.

File details

Details for the file plotui-0.4.2-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for plotui-0.4.2-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c028ae67753a4c86a877e6438b42b9f9101a922117190d3c1a2e68a14f4fe46d
MD5 02f46053475c900a7df913fb9dd24f4f
BLAKE2b-256 b33872af9eb9afd49d0d41cc939ae1829cee3c13127e5b6a4f8005343f6409d8

See more details on using hashes here.

File details

Details for the file plotui-0.4.2-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for plotui-0.4.2-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 9be88eb7f5c7e70bc4b3beb4227651c049d3a46545bada093217d9605d97b15d
MD5 eb3d1c70d7e75e8530e6091d281115ad
BLAKE2b-256 8c54dad56f656b45f2dfa0f4f657e5ddf15b3b86a92dc84c65f7cddfb5d33a29

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.0

5 files

This release

0.4.2 This release

5 files

0.4.1

5 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page