The Textual widget
PlotWidget — terminal detection, input routing, picking, and flicker-free repaints.
from plotui.textual import PlotWidget
PlotWidget(
plot,
auto_rotate=False, # slow idle spin for 3D plots
cell_px=None, # device pixels per cell; None = detect
pickable=False, # 3D hover/click picking (posts messages)
crosshair=True, # 2D hover crosshair with values
render_mode="auto", # "placeholder" | "direct" to force a path
interactive_scale=0.5, # resolution while interacting, for large plots
)Input
Drag rotates, shift-drag pans, scroll zooms, double-click resets.
Arrow keys rotate, +/- zoom, r resets. All of it routes through the
plot's camera — subclass and override to change the bindings.
Picking messages
With pickable=True, moving the mouse over a node or edge lights it up
white and posts ElementHovered; a click posts ElementPicked with
("node" | "edge", index). Off by default so plots without click semantics
pay no per-mouse-move cost.
The 2D crosshair
On by default: hovering a 2D chart draws a vertical guide snapped to the
nearest sample, marks each series, and shows a value readout. Terminal mouse
events arrive per character cell, which is far finer than typical point
spacing — snapping makes the granularity invisible. crosshair=False opts
out.
Resolution
cell_px=None detects the terminal's true cell size so plots render at
native resolution. Large 3D scenes (many thousands of vertices — surfaces
count) drop to interactive_scale resolution while dragging or
auto-rotating, and snap back to full resolution the moment interaction
stops.
Text overlays
render_kitty_placeholder_cells returns per-cell placeholder strings so a
frontend can splice terminal-crisp text into a row of the image without
re-rasterizing — the hook behind label overlays.
Streaming
widget.plot exposes the wrapped Plot; after mutating it, call
widget.invalidate() to repaint. For live data, the widget wraps the
streaming API directly:
handle = plot.add_line([], [], name="loss")
widget.extend(handle, [t], [v]) # append + repaint, O(new points)
widget.set_visible(handle, False) # repaints only when the state changedRepeated extends between frames coalesce into one repaint, and a plot that
streams past the large-scene threshold picks up reduced-resolution
interaction automatically. examples/textual_stream.py is a complete
20 Hz feed with visibility toggles.