Beautiful and
interactive plots,
right inside
your terminal
plotui draws interactive, Plotly-style 2D & 3D charts inside the terminal — rasterized by a Rust engine, delivered over the Kitty graphics protocol, embeddable in TUI frameworks such as Textual, Ratatui, or Bubble Tea. Select, rotate, pan, zoom, hover.
API
A plotting API you already know.
The same calls in Python, Rust, and Go, so examples translate one-to-one.
- 2D and 3D in one object — scatter, line, and bar; 3D scatter, surfaces, and graphs. The first 3D trace turns on the orbit camera.
- Interactive and pickable — orbit, zoom, and pan; hover highlights, and a click tells you what was picked.
- Drops in as a widget — a first-class widget for Textual, Ratatui, and Bubble Tea, or drive the engine yourself from any event loop.
from plotui import Plot
from plotui.textual import PlotWidget
plot = Plot()
# the 3D scene in the demo up top — legend and colors, automatic
plot.add_scatter3d(x_a, y_a, z_a, name="Cluster A")
plot.add_scatter3d(x_b, y_b, z_b, name="Cluster B")
plot.add_scatter3d(x_c, y_c, z_c, name="Cluster C")
# in a Textual app: drop it in as a widget, done
yield PlotWidget(plot)
# or drive it yourself from any event loop
plot.rotate(d_yaw, d_pitch)
escape = plot.render_kitty(cols, rows, cell_w, cell_h)
use plotui_ratatui::{PlotOptions, PlotState, PlotWidget};
let mut plot = plotui_core::Plot::new();
// the 3D scene in the demo up top — legend and colors, automatic
let name = |s: &str| Some(s.to_owned());
plot.add_scatter3d(cluster_a, plot.next_color(), 2.0, name("Cluster A"));
plot.add_scatter3d(cluster_b, plot.next_color(), 2.0, name("Cluster B"));
plot.add_scatter3d(cluster_c, plot.next_color(), 2.0, name("Cluster C"));
// in a Ratatui app: a stateful widget like any other
let mut state = PlotState::new(plot, PlotOptions::default());
frame.render_stateful_widget(PlotWidget, frame.area(), &mut state);
state.handle_event(&event);
// or drive the raw engine from any event loop
plot.rotate(d_yaw, d_pitch);
let escape = plotui_protocol::kitty(&plot.render(w, h), cols, rows);
import (
plotui "github.com/sebaheg/plotui/go"
"github.com/sebaheg/plotui/go/teaplot"
)
plot := plotui.New()
// the 3D scene in the demo up top — legend and colors, automatic
name := plotui.WithName
plot.AddScatter3D(xA, yA, zA, name("Cluster A"))
plot.AddScatter3D(xB, yB, zB, name("Cluster B"))
plot.AddScatter3D(xC, yC, zC, name("Cluster C"))
// in a Bubble Tea app: an Elm-style component, done
model := teaplot.New(plot)
// or drive it yourself from any event loop
plot.Rotate(dYaw, dPitch)
escape, _ := plot.RenderKitty(cols, rows, cellW, cellH, opts)
compatibility
Your framework, your terminal — supported.
A first-class widget for your TUI framework, real pixels in your terminal. If your pair is below, plotui just works — and feels the same everywhere: drag rotates, shift-drag pans, scroll zooms, clicks pick.
TUI frameworks
Textual
python
Drop PlotWidget into any Textual app — rotate, zoom, hover and click picking, live streaming updates. One import and you're plotting.
from plotui.textual import PlotWidget
Ratatui
rust
A native StatefulWidget — draw it like any other widget, hand it your events, and get flicker-free interactive plots in your Rust TUI.
use plotui_ratatui::PlotWidget;
Bubble Tea
go
The teaplot component for Bubble Tea v2 — Elm-style
Update/View, with pick and
hover messages ready for your update loop.
github.com/sebaheg/plotui/go/teaplot
Terminals
- placeholder
- the richest mode — plots are woven into the character grid, so they scroll, split, and repaint exactly like text
- direct
- pixels drawn straight at the plot's position, just as sharp — plotui manages repaints itself
- ~ partial
- these terminals' pixel-graphics support is new and still maturing — plots work, with occasional rough edges
examples
Surfaces, and every 2D chart, from the same engine.
plotui does both 2D and 3D plotting — line, scatter, bar, and stacked charts with automatic axes, ticks, and legend, and shaded 3D surfaces you can rotate. These two are no mock-ups: the real engine, compiled to WebAssembly, rasterizes every frame exactly as it does in your terminal.
streaming
Supports streaming data
Every add_* call returns a
stable trace handle. Appends through it cost only the new points — not the
history — and axes autoscale on the next frame. numpy arrays cross into the Rust core
in one bulk copy.
# handles come back from add_*
h = plot.add_line([], [], name="forecast")
o = plot.add_scatter([], [], name="observed")
g = plot.add_line([], [], name="load", axis="y2")
def feed(): # 20 times a second
widget.extend(h, [t], [forecast(t)])
widget.extend(o, [t], [measured(t)])
widget.extend(g, [t], [load(t)])
widget.set_visible(o, False) # legend toggle
installation
Several install options
The plotui CLI ships prebuilt for macOS and Linux — plot straight
from your shell: seq 100 | plotui line.
Python users get the CLI bundled with the library wheel.
# prebuilt binary, macOS & Linux:
❯ curl -fsSL https://plotui.xyz/install.sh | sh
# then run a built-in demo:
❯ plotui example scatter
# prebuilt bottle, macOS & Linux:
❯ brew install sebaheg/tap/plotui
# then run a built-in demo:
❯ plotui example scatter
# the Python library — the CLI comes bundled:
❯ pip install plotui
# then run a built-in demo:
❯ plotui example scatter
# prebuilt, via cargo-binstall:
❯ cargo binstall plotui
# or compile from source:
❯ cargo install plotui
# then run a built-in demo:
❯ plotui example scatter
feedback
Found a bug? Missing a feature?
plotui is young and moving fast — bug reports and feature requests genuinely shape what gets built next. If a plot renders wrong in your terminal, your setup isn't covered, or you're missing a chart type, say so on GitHub.