plotui 0.5.1__cp39-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ Metadata-Version: 2.4
2
+ Name: plotui
3
+ Version: 0.5.1
4
+ Requires-Dist: textual>=0.60 ; extra == 'textual'
5
+ Provides-Extra: textual
6
+ License-File: LICENSE
7
+ Summary: Interactive 2D/3D plots in the terminal — Plotly-style, for Textual, powered by a Rust core and the Kitty graphics protocol.
8
+ Keywords: terminal,plot,3d,textual,kitty,tui,visualization
9
+ License: MIT
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
12
+
13
+ # plotui
14
+
15
+ **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.**
16
+
17
+ `plotui` renders scatter plots (and, soon, lines / surfaces / bars) as real
18
+ pixel graphics inside a terminal, and lets you rotate, pan, and zoom them. It
19
+ drops into a [Textual](https://textual.textualize.io/),
20
+ [Ratatui](https://ratatui.rs/), or
21
+ [Bubble Tea](https://github.com/charmbracelet/bubbletea) app as a first-class
22
+ widget, with the rendering engine written in Rust so it stays fast in 2D and 3D.
23
+
24
+ > Status: **early scaffold.** Working today: 2D scatter/line/step/bar,
25
+ > histogram, box, heatmap, and band charts with axes, ticks, titles,
26
+ > explicit ranges, log scales, categorical labels, a colorbar and a legend;
27
+ > DAG/pipeline graphs with a layered layout and a DOT subset reader; a 3D
28
+ > scatter/graph/surface/mesh engine; a Kitty-image raw demo; and a Textual
29
+ > widget.
30
+
31
+ ## Architecture
32
+
33
+ The one rule that shapes everything: **the Rust core owns pixels, not the
34
+ terminal.** It has no event loop and no input handling — the TUI framework
35
+ (Textual, Ratatui, or Bubble Tea) owns the loop, forwards input to the
36
+ camera, and asks for a frame.
37
+
38
+ ```
39
+ f64 data ─▶ camera + rasterizer ─▶ RGBA buffer ─▶ Kitty escape bytes ─▶ your terminal's cell grid
40
+ ```
41
+
42
+ ```
43
+ crates/
44
+ plotui-core/ pure engine: data model, 3D camera, rasterizer → RGBA
45
+ plotui-protocol/ RGBA → terminal bytes (Kitty graphics protocol)
46
+ plotui-term/ shared frontend glue: render-path detection, cell-pixel
47
+ probing, tmux passthrough, the per-frame render policy
48
+ plotui-bind/ shared binding semantics: parsing, validation, defaults,
49
+ and their exact error messages (Python and Go agree)
50
+ plotui-py/ PyO3 bindings → the `plotui._plotui` native module
51
+ plotui-ratatui/ Ratatui widget (native Rust frontend)
52
+ plotui-ffi/ C ABI (cdylib + staticlib) behind the Go bindings
53
+ python/plotui/ the Python package + Textual `PlotWidget`
54
+ go/ Go bindings + `teaplot`, the Bubble Tea v2 component
55
+ examples/ raw_demo.py (Kitty images), textual_demo.py
56
+ ```
57
+
58
+ `core` and `protocol` are pure and I/O-free, so the same engine can back every
59
+ frontend and be unit-tested by hashing pixel buffers. The protocol layer emits
60
+ Kitty graphics escapes — chunked, placement-aware, and wrapped for tmux
61
+ passthrough — as pure functions of the RGBA frame.
62
+
63
+ ## Integrations
64
+
65
+ Each TUI framework gets a first-class widget, not a port. All frontends sit on
66
+ the same policy crates (`plotui-term` for detection/tmux/render policy,
67
+ `plotui-bind` for argument validation and its exact error strings), so a plot
68
+ looks and behaves identically whichever framework hosts it — down to the error
69
+ messages.
70
+
71
+ | Frontend | How it works | Where in the codebase | Try it |
72
+ | --- | --- | --- | --- |
73
+ | **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` |
74
+ | **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` |
75
+ | **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](go/README.md) |
76
+ | **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](https://plotui.xyz/examples.html) |
77
+
78
+ The three TUI widgets have feature parity: render-path detection, tmux
79
+ passthrough, drag/zoom/pan/keys, picking + hover, the 2D crosshair, text
80
+ overlays, half-resolution interaction frames, and streaming extend.
81
+
82
+ ## Install the CLI
83
+
84
+ `plotui` is also a command-line tool: pipe columns of numbers in, get a
85
+ real-pixel chart out — interactive on a TTY (pan, zoom, crosshair), a single
86
+ printed frame when piped or with `--static`.
87
+
88
+ ```bash
89
+ curl -fsSL https://plotui.xyz/install.sh | sh # prebuilt binary
90
+ brew install sebaheg/tap/plotui # Homebrew (macOS / Linux)
91
+ pip install plotui # prebuilt wheel: the library + the CLI
92
+ cargo install plotui # build from source
93
+ cargo binstall plotui # prebuilt, via cargo-binstall
94
+ ```
95
+
96
+ ```bash
97
+ seq 1 100 | LC_ALL=C awk '{print $1, sin($1/10)}' | plotui line
98
+ plotui scatter -H -d, data.csv # header row + comma-delimited
99
+ plotui bar counts.tsv # --horizontal, --stack, --group
100
+ plotui step states.txt # holds its value between samples
101
+ plotui hist samples.txt # binned automatically
102
+ plotui box -H measurements.tsv # one box per column
103
+ plotui dag pipeline.dot # a DAG from a DOT file; hover a task
104
+ # to light everything it waits on
105
+ plotui line --log-y --title "queue depth" \
106
+ --x-title minute --y-title items # titles and log scales
107
+ plotui line --x-range 0:100 --y-range 0:1 # pin an extent, LO:HI
108
+ tail -f app.log | LC_ALL=C awk '{print $2}' \
109
+ | plotui line -f --window 200 # live, on the last 200 samples
110
+ plotui example scatter # built-in demo scenes, no data needed
111
+ plotui example deps # plotui's own dependency graph, laid
112
+ # out live by a force simulation
113
+ plotui example pipeline # a nightly forecast DAG, running
114
+ ```
115
+
116
+ `--follow` (`-f`) keeps the reader open instead of plotting once at EOF: rows
117
+ are parsed as they arrive and appended in place, so the chart grows without a
118
+ redraw from scratch and pan/zoom survive. It needs piped input and a terminal
119
+ to draw into — a malformed line is skipped rather than fatal, and the count is
120
+ reported when you quit. The shape of the input (delimiter, column count,
121
+ whether x is a calendar) is settled by the first row and held for the rest of
122
+ the stream.
123
+
124
+ On a long feed the whole run compresses into a sliver, so `--window <N>` keeps
125
+ the view on the last N samples and `--last <span>` on the last `30s` / `5m` /
126
+ `2h` of x. Neither drops data: the window is a *view*, drawn on the range
127
+ slider (which a window switches on) against the entire run, so you can drag
128
+ back through everything that has arrived. Doing so hands the view over — a
129
+ reader who has scrolled back to an incident does not want the next row to
130
+ yank them forward — and **f** goes live again, jumping to the head.
131
+
132
+ Like every plotui frontend, the CLI needs a terminal with Kitty graphics
133
+ (supported terminals below); elsewhere it prints a notice and exits.
134
+
135
+ ## Develop
136
+
137
+ Requires Rust and Python 3.9+. Build the native module into a virtualenv with
138
+ [maturin](https://www.maturin.rs/):
139
+
140
+ ```bash
141
+ python -m venv .venv && source .venv/bin/activate
142
+ pip install maturin textual
143
+ maturin develop --release
144
+ ```
145
+
146
+ Then, in a terminal with Kitty graphics support — **Kitty**, **Ghostty**,
147
+ **iTerm2 ≥ 3.5**, **WezTerm**, or **Konsole** — for the full-resolution pixel demos:
148
+
149
+ ```bash
150
+ python examples/raw_demo.py # 3D scatter via Kitty images
151
+ python examples/textual_demo.py # embedded in Textual
152
+ python examples/textual_graph.py # interactive graph: hover + click-to-inspect
153
+ ```
154
+
155
+ The Textual widget picks its render path per terminal: Unicode-placeholder
156
+ Kitty graphics in Kitty/Ghostty, direct Kitty placement in iTerm2/WezTerm/
157
+ Konsole — plus Warp, Rio, and VS Code, whose younger Kitty decoders are
158
+ supported but still maturing (VS Code needs its
159
+ `terminal.integrated.enableImages` setting). plotui only draws
160
+ real pixels — terminals without Kitty graphics get a notice naming supported
161
+ terminals, never a degraded plot. Override with
162
+ `PLOTUI_RENDER=placeholder|direct` or `PlotWidget(..., render_mode=...)`.
163
+
164
+ ## Python API
165
+
166
+ ```python
167
+ from plotui import Plot
168
+
169
+ # 2D: axes, ticks, and a legend appear automatically. Traces added without a
170
+ # color take colorway slots in fixed order; `name=` puts a series in the
171
+ # legend. Colors accept (r, g, b) tuples or shorthands: "#e63c78", "red".
172
+ plot = Plot()
173
+ plot.add_line(xs, ys, name="forecast")
174
+ plot.add_scatter(xs2, ys2, name="observed")
175
+ plot.add_bar(xs3, heights)
176
+
177
+ # Secondary axes: axis="y2"/"y3" bind a series to an independent right-hand
178
+ # axis — its own autoscale and tick column, labels tinted to the series color
179
+ # (y2 innermost, y3 outermost). The grid stays with the left axis.
180
+ plot.add_line(xs, tokens, name="tokens", axis="y2")
181
+ plot.add_line(xs, cpu_minutes, name="cpu min", axis="y3")
182
+
183
+ # Titles: each buys its own margin, so the plot area shrinks rather than
184
+ # drawing over the data. The y title is drawn rotated in the left margin.
185
+ plot.set_title("p99 latency")
186
+ plot.set_x_title("requests")
187
+ plot.set_y_title("ms")
188
+
189
+ # Ranges and scales: an explicit range pins the *extent* only — no autoscale
190
+ # padding, and zoom/pan still compose on top of it (unlike set_x_window,
191
+ # which is the live window and supersedes the camera). A log axis ticks in
192
+ # powers of ten; values at or below zero have no log coordinate and neither
193
+ # set the range nor draw.
194
+ plot.set_x_range((0, 100)) # None restores autoscale
195
+ plot.set_y_range((0.1, 1e4))
196
+ plot.set_y_log(True) # set_x_log for x
197
+
198
+ # DAGs and pipelines: labelled boxes wired by arrows, laid out by rank. Node
199
+ # centres are data coordinates; each box is sized in pixels from its label,
200
+ # so zooming spreads the graph apart while the text stays readable.
201
+ from plotui import LayeredLayout, from_dot, reachable
202
+
203
+ layout = LayeredLayout(len(tasks), edges) # rankdir="TB" or "LR"
204
+ plot = Plot()
205
+ h = plot.add_graph2d(*layout.positions(), edges,
206
+ labels=tasks, routes=layout.routes())
207
+ plot.set_graph_colors(h, states) # repaint as the run advances
208
+ lit = reachable(len(tasks), edges, hovered) # everything it waits on
209
+ plot = from_dot(open("pipeline.dot").read()) # or straight from DOT
210
+
211
+ # 3D: any 3D trace switches the plot to the orbit camera.
212
+ plot = Plot()
213
+ plot.add_scatter3d(xs, ys, zs, name="Cluster A") # colors from the colorway
214
+
215
+ # Colorways: the default sequence is pink/cyan/orange-first; swap it with a
216
+ # built-in name or your own list before adding traces.
217
+ plot.set_colorway("vivid") # "plotui", "muted", "vivid"
218
+ plot.set_colorway(["#e63c78", "cyan", (240, 161, 60)])
219
+
220
+ # Streaming: every add_* returns a trace handle. Append through it instead
221
+ # of rebuilding — O(new points), autoscale follows; numpy arrays are read
222
+ # in one bulk copy. set_visible toggles a series without losing its handle,
223
+ # palette slot, or node indices.
224
+ h = plot.add_line([], [], name="loss")
225
+ plot.extend(h, xs, ys) # 3D scatter/line: extend(h, xs, ys, zs)
226
+ plot.set_visible(h, False)
227
+
228
+ # Interaction (forward your framework's events to these):
229
+ plot.rotate(d_yaw, d_pitch)
230
+ plot.zoom_by(factor)
231
+ plot.pan(dx, dy)
232
+ plot.reset()
233
+
234
+ # Render (the frontend places the bytes):
235
+ escape = plot.render_kitty(cols, rows, cell_w, cell_h) # Kitty pixel image
236
+ pixels = plot.render_rgba(px_w, px_h) # raw RGBA8 bytes
237
+ ```
238
+
239
+ Graphs take per-element styling, and the camera/projection state is fully
240
+ scriptable — the hooks a host needs for label overlays, camera targeting, and
241
+ rebuilding a plot without losing the view:
242
+
243
+ ```python
244
+ plot.add_graph3d(xs, ys, zs, edges=[(0, 1), (1, 2)],
245
+ node_colors=[...], # one (r, g, b) per node
246
+ node_sizes=[...], # per-node radius (else `size`)
247
+ edge_colors=[...], # per-edge (r, g, b) (else derived)
248
+ node_shapes=[...]) # per-node "disc" | "ring" | "square" |
249
+ # "triangle" | "diamond" | "diamond-open" | "dot"
250
+ plot.set_show_box(False) # hide the 3D orientation cube
251
+ plot.set_bounds((x0, y0, z0), (x1, y1, z1)) # pin the data frame (else the nodes'
252
+ # bounding box); None, None restores
253
+ plot.set_chrome(grid=(26, 32, 36), # recolour the non-data chrome to sit on
254
+ frame=(43, 50, 55), # your own background: bg (legend box),
255
+ ink=(103, 111, 118)) # frame, grid, ink, ink_bright
256
+
257
+ state = plot.camera_state() # (yaw, pitch, zoom, pan_x, pan_y)
258
+ plot.set_camera_state(*state) # restore (e.g. onto a new Plot)
259
+ plot.project_nodes(px_w, px_h) # [(x_px, y_px, depth)] per node —
260
+ # exact render/pick geometry
261
+ ```
262
+
263
+ In Textual, use `plotui.textual.PlotWidget(plot)` and it handles the event
264
+ plumbing for you. Pass `pickable=True` to make 3D graph nodes and edges
265
+ interactive: hovering lights the element under the cursor up white, and
266
+ clicking posts an `ElementPicked` message with `("node", i)` or `("edge", i)`
267
+ (see `examples/textual_graph.py`, which opens a slide-in inspector from it).
268
+
269
+ The widget also supports **text overlays** — `widget.set_overlay([(row, col,
270
+ text, style), ...])` splices terminal-crisp text (labels, badges) over the
271
+ image in every render mode without re-rasterizing — and exposes a
272
+ `widget.dragging` property for hosts that defer work mid-gesture. To customize
273
+ interaction in a subclass, override the `apply_rotate` / `apply_pan` /
274
+ `apply_zoom` / `apply_reset` / `on_click_at` primitives that every input path
275
+ routes through — do **not** override the Textual `on_*` handlers (Textual
276
+ dispatches those to every class in the MRO, so both would run).
277
+
278
+ ## License
279
+
280
+ MIT, except for one embedded asset: chart text is set in
281
+ [Martian Mono](https://github.com/evilmartians/mono) (Copyright 2020 The
282
+ Martian Mono Project Authors), used under the SIL Open Font License 1.1. The
283
+ glyph outlines are compiled into `plotui-core` as
284
+ `crates/plotui-core/src/glyphs.rs`; the license travels with them in
285
+ `crates/plotui-core/fonts/MartianMono-OFL.txt`. The font carries no Reserved
286
+ Font Name, and nothing about the OFL reaches your code — it covers the font
287
+ data, not the crate.
288
+
@@ -0,0 +1,11 @@
1
+ plotui/__init__.py,sha256=Tp-dUbLiuRI39fm3BbENqjE1fokSouH53js2IvLfX3g,2088
2
+ plotui/_bin/plotui.exe,sha256=aR6FZbp0uQv7bhp_gWABSELbUa-7ZyuTbt2YK0arBvo,1940480
3
+ plotui/_cli.py,sha256=IQyM6Pm-xOa_irMWxD9Xx8mAzl0RBuGsOtzQsdd3cRU,1180
4
+ plotui/_plotui.pyd,sha256=gZmNABWJ8Jb6SwQUmoNNnuIjavpeoawhVGs1_t85R5Q,1317376
5
+ plotui/textual.py,sha256=ePhPYt6u17Q7YiYS0nK11UW4yuAFUcjA4GEGjOKqO20,34704
6
+ plotui-0.5.1.dist-info/METADATA,sha256=p6S6-buhB-4bTeSR64FxuEES83fZ__I7nr5yCW293Fs,15826
7
+ plotui-0.5.1.dist-info/WHEEL,sha256=xe4_tbg8wYdeh4O7nLbnWckzIQtCR4nODDgwke_TKy8,95
8
+ plotui-0.5.1.dist-info/entry_points.txt,sha256=omVtgXTZI-S8tlNy-u_BGA5z2fIKSy0HZINbkqxFgH4,42
9
+ plotui-0.5.1.dist-info/licenses/LICENSE,sha256=KAEbjdC1FAlj4nWDuDaxLrwtMCFHcxUlTkHalxb6nMI,1095
10
+ plotui-0.5.1.dist-info/sboms/plotui-py.cyclonedx.json,sha256=-y2s6Z079o-CNrxPVyWnc_CoOA3tOFKrGxVPjdgJKpA,43598
11
+ plotui-0.5.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.15.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-abi3-win_amd64
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ plotui=plotui._cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sebastian Haglund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.