plotui 0.4.2__tar.gz → 0.5.0__tar.gz

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.
Files changed (52) hide show
  1. {plotui-0.4.2 → plotui-0.5.0}/Cargo.lock +11 -9
  2. {plotui-0.4.2 → plotui-0.5.0}/Cargo.toml +6 -6
  3. {plotui-0.4.2 → plotui-0.5.0}/PKG-INFO +90 -43
  4. {plotui-0.4.2 → plotui-0.5.0}/README.md +89 -42
  5. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-bind/README.md +89 -42
  6. plotui-0.5.0/crates/plotui-bind/src/dot.rs +886 -0
  7. plotui-0.5.0/crates/plotui-bind/src/lib.rs +1024 -0
  8. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-core/README.md +89 -42
  9. plotui-0.5.0/crates/plotui-core/fonts/MartianMono-OFL.txt +93 -0
  10. plotui-0.5.0/crates/plotui-core/src/font.rs +635 -0
  11. plotui-0.5.0/crates/plotui-core/src/glyphs.rs +2303 -0
  12. plotui-0.5.0/crates/plotui-core/src/layout.rs +1082 -0
  13. plotui-0.5.0/crates/plotui-core/src/lib.rs +8679 -0
  14. plotui-0.5.0/crates/plotui-core/src/marching.rs +567 -0
  15. plotui-0.5.0/crates/plotui-core/src/ribbon.rs +450 -0
  16. plotui-0.5.0/crates/plotui-core/src/ticks.rs +465 -0
  17. plotui-0.5.0/crates/plotui-core/tests/input_map.rs +166 -0
  18. plotui-0.5.0/crates/plotui-core/tests/pick_surface.rs +105 -0
  19. plotui-0.5.0/crates/plotui-core/tests/render.rs +1831 -0
  20. plotui-0.5.0/crates/plotui-protocol/README.md +275 -0
  21. plotui-0.5.0/crates/plotui-py/src/lib.rs +1588 -0
  22. plotui-0.5.0/crates/plotui-term/README.md +275 -0
  23. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-term/src/policy.rs +11 -0
  24. {plotui-0.4.2 → plotui-0.5.0}/pyproject.toml +1 -1
  25. plotui-0.5.0/python/plotui/__init__.py +47 -0
  26. {plotui-0.4.2 → plotui-0.5.0}/python/plotui/textual.py +194 -12
  27. plotui-0.4.2/crates/plotui-bind/src/lib.rs +0 -304
  28. plotui-0.4.2/crates/plotui-core/src/font.rs +0 -279
  29. plotui-0.4.2/crates/plotui-core/src/hershey.rs +0 -267
  30. plotui-0.4.2/crates/plotui-core/src/lib.rs +0 -2798
  31. plotui-0.4.2/crates/plotui-core/src/ticks.rs +0 -97
  32. plotui-0.4.2/crates/plotui-core/tests/render.rs +0 -860
  33. plotui-0.4.2/crates/plotui-protocol/README.md +0 -228
  34. plotui-0.4.2/crates/plotui-py/src/lib.rs +0 -612
  35. plotui-0.4.2/crates/plotui-term/README.md +0 -228
  36. plotui-0.4.2/python/plotui/__init__.py +0 -24
  37. {plotui-0.4.2 → plotui-0.5.0}/LICENSE +0 -0
  38. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-bind/Cargo.toml +0 -0
  39. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-core/Cargo.toml +0 -0
  40. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-protocol/Cargo.toml +0 -0
  41. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-protocol/src/diacritics.rs +0 -0
  42. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-protocol/src/lib.rs +0 -0
  43. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-protocol/tests/protocol.rs +0 -0
  44. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-py/Cargo.toml +0 -0
  45. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-term/Cargo.toml +0 -0
  46. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-term/src/compose.rs +0 -0
  47. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-term/src/detect.rs +0 -0
  48. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-term/src/ids.rs +0 -0
  49. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-term/src/lib.rs +0 -0
  50. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-term/src/tmux.rs +0 -0
  51. {plotui-0.4.2 → plotui-0.5.0}/crates/plotui-term/tests/term.rs +0 -0
  52. {plotui-0.4.2 → plotui-0.5.0}/python/plotui/_cli.py +0 -0
@@ -305,6 +305,7 @@ dependencies = [
305
305
  "crossterm_winapi",
306
306
  "derive_more",
307
307
  "document-features",
308
+ "filedescriptor",
308
309
  "mio",
309
310
  "parking_lot",
310
311
  "rustix",
@@ -1117,10 +1118,11 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
1117
1118
 
1118
1119
  [[package]]
1119
1120
  name = "plotui"
1120
- version = "0.4.2"
1121
+ version = "0.5.0"
1121
1122
  dependencies = [
1122
1123
  "clap",
1123
1124
  "crossterm",
1125
+ "plotui-bind",
1124
1126
  "plotui-core",
1125
1127
  "plotui-ratatui",
1126
1128
  "plotui-term",
@@ -1129,18 +1131,18 @@ dependencies = [
1129
1131
 
1130
1132
  [[package]]
1131
1133
  name = "plotui-bind"
1132
- version = "0.4.2"
1134
+ version = "0.5.0"
1133
1135
  dependencies = [
1134
1136
  "plotui-core",
1135
1137
  ]
1136
1138
 
1137
1139
  [[package]]
1138
1140
  name = "plotui-core"
1139
- version = "0.4.2"
1141
+ version = "0.5.0"
1140
1142
 
1141
1143
  [[package]]
1142
1144
  name = "plotui-ffi"
1143
- version = "0.4.2"
1145
+ version = "0.5.0"
1144
1146
  dependencies = [
1145
1147
  "cbindgen",
1146
1148
  "plotui-bind",
@@ -1151,7 +1153,7 @@ dependencies = [
1151
1153
 
1152
1154
  [[package]]
1153
1155
  name = "plotui-protocol"
1154
- version = "0.4.2"
1156
+ version = "0.5.0"
1155
1157
  dependencies = [
1156
1158
  "base64",
1157
1159
  "flate2",
@@ -1160,7 +1162,7 @@ dependencies = [
1160
1162
 
1161
1163
  [[package]]
1162
1164
  name = "plotui-py"
1163
- version = "0.4.2"
1165
+ version = "0.5.0"
1164
1166
  dependencies = [
1165
1167
  "numpy",
1166
1168
  "plotui-bind",
@@ -1172,7 +1174,7 @@ dependencies = [
1172
1174
 
1173
1175
  [[package]]
1174
1176
  name = "plotui-ratatui"
1175
- version = "0.4.2"
1177
+ version = "0.5.0"
1176
1178
  dependencies = [
1177
1179
  "crossterm",
1178
1180
  "plotui-core",
@@ -1183,7 +1185,7 @@ dependencies = [
1183
1185
 
1184
1186
  [[package]]
1185
1187
  name = "plotui-term"
1186
- version = "0.4.2"
1188
+ version = "0.5.0"
1187
1189
  dependencies = [
1188
1190
  "plotui-core",
1189
1191
  "plotui-protocol",
@@ -1192,7 +1194,7 @@ dependencies = [
1192
1194
 
1193
1195
  [[package]]
1194
1196
  name = "plotui-wasm"
1195
- version = "0.4.2"
1197
+ version = "0.5.0"
1196
1198
  dependencies = [
1197
1199
  "plotui-bind",
1198
1200
  "plotui-core",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/plotui-core", "crates/plotui-protocol", "crates/plotui-term", "crates/plotui-bind", "crates/plotui-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.4.2"
6
+ version = "0.5.0"
7
7
  edition = "2021"
8
8
  license = "MIT"
9
9
  repository = "https://github.com/sebaheg/plotui"
@@ -14,11 +14,11 @@ keywords = ["plot", "terminal", "tui", "kitty", "visualization"]
14
14
  categories = ["visualization", "command-line-utilities"]
15
15
 
16
16
  [workspace.dependencies]
17
- plotui-core = { version = "0.4.2", path = "crates/plotui-core" }
18
- plotui-protocol = { version = "0.4.2", path = "crates/plotui-protocol" }
19
- plotui-term = { version = "0.4.2", path = "crates/plotui-term" }
20
- plotui-bind = { version = "0.4.2", path = "crates/plotui-bind" }
21
- plotui-ratatui = { version = "0.4.2", path = "crates/plotui-ratatui" }
17
+ plotui-core = { version = "0.5.0", path = "crates/plotui-core" }
18
+ plotui-protocol = { version = "0.5.0", path = "crates/plotui-protocol" }
19
+ plotui-term = { version = "0.5.0", path = "crates/plotui-term" }
20
+ plotui-bind = { version = "0.5.0", path = "crates/plotui-bind" }
21
+ plotui-ratatui = { version = "0.5.0", path = "crates/plotui-ratatui" }
22
22
  ratatui = { version = "0.30", default-features = false }
23
23
  crossterm = "0.29"
24
24
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotui
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Requires-Dist: textual>=0.60 ; extra == 'textual'
5
5
  Provides-Extra: textual
6
6
  License-File: LICENSE
@@ -21,9 +21,12 @@ drops into a [Textual](https://textual.textualize.io/),
21
21
  [Bubble Tea](https://github.com/charmbracelet/bubbletea) app as a first-class
22
22
  widget, with the rendering engine written in Rust so it stays fast in 2D and 3D.
23
23
 
24
- > Status: **early scaffold.** Working today: 2D scatter/line/bar charts with
25
- > axes, ticks, and a legend; a 3D scatter/graph engine; a Kitty-image raw demo;
26
- > and a Textual widget. See the roadmap below.
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.
27
30
 
28
31
  ## Architecture
29
32
 
@@ -32,6 +35,10 @@ terminal.** It has no event loop and no input handling — the TUI framework
32
35
  (Textual, Ratatui, or Bubble Tea) owns the loop, forwards input to the
33
36
  camera, and asks for a frame.
34
37
 
38
+ ```
39
+ f64 data ─▶ camera + rasterizer ─▶ RGBA buffer ─▶ Kitty escape bytes ─▶ your terminal's cell grid
40
+ ```
41
+
35
42
  ```
36
43
  crates/
37
44
  plotui-core/ pure engine: data model, 3D camera, rasterizer → RGBA
@@ -49,7 +56,9 @@ examples/ raw_demo.py (Kitty images), textual_demo.py
49
56
  ```
50
57
 
51
58
  `core` and `protocol` are pure and I/O-free, so the same engine can back every
52
- frontend and be unit-tested by hashing pixel buffers.
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.
53
62
 
54
63
  ## Integrations
55
64
 
@@ -85,11 +94,41 @@ cargo binstall plotui # prebuilt, via cargo-binstall
85
94
  ```
86
95
 
87
96
  ```bash
88
- seq 1 100 | awk '{print $1, sin($1/10)}' | plotui line
97
+ seq 1 100 | LC_ALL=C awk '{print $1, sin($1/10)}' | plotui line
89
98
  plotui scatter -H -d, data.csv # header row + comma-delimited
90
- plotui bar counts.tsv
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
91
114
  ```
92
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
+
93
132
  Like every plotui frontend, the CLI needs a terminal with Kitty graphics
94
133
  (supported terminals below); elsewhere it prints a notice and exits.
95
134
 
@@ -128,7 +167,8 @@ terminals, never a degraded plot. Override with
128
167
  from plotui import Plot
129
168
 
130
169
  # 2D: axes, ticks, and a legend appear automatically. Traces added without a
131
- # color take palette slots in fixed order; `name=` puts a series in the legend.
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".
132
172
  plot = Plot()
133
173
  plot.add_line(xs, ys, name="forecast")
134
174
  plot.add_scatter(xs2, ys2, name="observed")
@@ -140,9 +180,42 @@ plot.add_bar(xs3, heights)
140
180
  plot.add_line(xs, tokens, name="tokens", axis="y2")
141
181
  plot.add_line(xs, cpu_minutes, name="cpu min", axis="y3")
142
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
+
143
211
  # 3D: any 3D trace switches the plot to the orbit camera.
144
212
  plot = Plot()
145
- plot.add_scatter3d(xs, ys, zs, color=(230, 60, 120), size=2.0)
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)])
146
219
 
147
220
  # Streaming: every add_* returns a trace handle. Append through it instead
148
221
  # of rebuilding — O(new points), autoscale follows; numpy arrays are read
@@ -202,40 +275,14 @@ interaction in a subclass, override the `apply_rotate` / `apply_pan` /
202
275
  routes through — do **not** override the Textual `on_*` handlers (Textual
203
276
  dispatches those to every class in the MRO, so both would run).
204
277
 
205
- ## Roadmap
206
-
207
- - [x] Flicker-free Kitty placement via Unicode-placeholder virtual placement
208
- (fixed image id, atomic replace) — wire the pixel path into the Textual widget
209
- - [x] 2D traces: scatter, line, bar; axes, ticks, tick labels, legend
210
- - [x] Independent right-hand y-axes (`axis="y2"`/`"y3"`) with tinted tick labels
211
- - [ ] 2D step trace; axis titles; time-formatted x ticks
212
- - [ ] 3D surface / mesh; axis cube with labels
213
- - [x] Interactive hover / pick for 3D graph nodes *and* edges (opt-in via
214
- `PlotWidget(..., pickable=True)`: hover lights the element up white,
215
- click posts `ElementPicked`)
216
- - [ ] Hover / pick for 2D traces; spatial index for large graphs
217
- - [x] Streaming append: trace handles, `extend`, `set_visible`, incremental bounds
218
- - [x] numpy fast-path input (one bulk copy, no per-element conversion)
219
- - [ ] Rolling window (`max_points`) for endless streams
220
- - [x] Graceful render-path auto-detection (placeholder / direct Kitty, with a
221
- supported-terminals notice elsewhere and a `PLOTUI_RENDER` override)
222
- - [ ] Sixel + iTerm2 OSC 1337 encoders for terminals without Kitty graphics
223
- - [x] Prebuilt wheels (maturin + GitHub Actions): `pip install plotui` — macOS
224
- arm64/x86_64, Linux x86_64/aarch64, abi3 ≥ 3.9; the wheel bundles the
225
- CLI binary
226
- - [x] Ratatui frontend (native): `plotui-ratatui` — StatefulWidget + app-owned
227
- PlotState, full parity with the Textual widget
228
- (`cargo run -p plotui-ratatui --example demo`)
229
- - [x] Bubble Tea frontend (cgo): `go/` bindings over the `plotui-ffi` C ABI +
230
- the `teaplot` component for Bubble Tea v2 (see `go/README.md`)
231
- - [x] CLI: `plotui line|scatter|bar` from stdin or a file — interactive on a
232
- TTY, one static frame when piped; installed via curl, Homebrew, cargo,
233
- or pip (see Install above)
234
- - [ ] CLI v2: `--follow` streaming, `scatter3d`, histogram/density/count
235
- transforms, `--out png`
236
- - [ ] Prebuilt static libs for the Go bindings (today: local source build)
237
-
238
278
  ## License
239
279
 
240
- MIT
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.
241
288
 
@@ -9,9 +9,12 @@ drops into a [Textual](https://textual.textualize.io/),
9
9
  [Bubble Tea](https://github.com/charmbracelet/bubbletea) app as a first-class
10
10
  widget, with the rendering engine written in Rust so it stays fast in 2D and 3D.
11
11
 
12
- > Status: **early scaffold.** Working today: 2D scatter/line/bar charts with
13
- > axes, ticks, and a legend; a 3D scatter/graph engine; a Kitty-image raw demo;
14
- > and a Textual widget. See the roadmap below.
12
+ > Status: **early scaffold.** Working today: 2D scatter/line/step/bar,
13
+ > histogram, box, heatmap, and band charts with axes, ticks, titles,
14
+ > explicit ranges, log scales, categorical labels, a colorbar and a legend;
15
+ > DAG/pipeline graphs with a layered layout and a DOT subset reader; a 3D
16
+ > scatter/graph/surface/mesh engine; a Kitty-image raw demo; and a Textual
17
+ > widget.
15
18
 
16
19
  ## Architecture
17
20
 
@@ -20,6 +23,10 @@ terminal.** It has no event loop and no input handling — the TUI framework
20
23
  (Textual, Ratatui, or Bubble Tea) owns the loop, forwards input to the
21
24
  camera, and asks for a frame.
22
25
 
26
+ ```
27
+ f64 data ─▶ camera + rasterizer ─▶ RGBA buffer ─▶ Kitty escape bytes ─▶ your terminal's cell grid
28
+ ```
29
+
23
30
  ```
24
31
  crates/
25
32
  plotui-core/ pure engine: data model, 3D camera, rasterizer → RGBA
@@ -37,7 +44,9 @@ examples/ raw_demo.py (Kitty images), textual_demo.py
37
44
  ```
38
45
 
39
46
  `core` and `protocol` are pure and I/O-free, so the same engine can back every
40
- frontend and be unit-tested by hashing pixel buffers.
47
+ frontend and be unit-tested by hashing pixel buffers. The protocol layer emits
48
+ Kitty graphics escapes — chunked, placement-aware, and wrapped for tmux
49
+ passthrough — as pure functions of the RGBA frame.
41
50
 
42
51
  ## Integrations
43
52
 
@@ -73,11 +82,41 @@ cargo binstall plotui # prebuilt, via cargo-binstall
73
82
  ```
74
83
 
75
84
  ```bash
76
- seq 1 100 | awk '{print $1, sin($1/10)}' | plotui line
85
+ seq 1 100 | LC_ALL=C awk '{print $1, sin($1/10)}' | plotui line
77
86
  plotui scatter -H -d, data.csv # header row + comma-delimited
78
- plotui bar counts.tsv
87
+ plotui bar counts.tsv # --horizontal, --stack, --group
88
+ plotui step states.txt # holds its value between samples
89
+ plotui hist samples.txt # binned automatically
90
+ plotui box -H measurements.tsv # one box per column
91
+ plotui dag pipeline.dot # a DAG from a DOT file; hover a task
92
+ # to light everything it waits on
93
+ plotui line --log-y --title "queue depth" \
94
+ --x-title minute --y-title items # titles and log scales
95
+ plotui line --x-range 0:100 --y-range 0:1 # pin an extent, LO:HI
96
+ tail -f app.log | LC_ALL=C awk '{print $2}' \
97
+ | plotui line -f --window 200 # live, on the last 200 samples
98
+ plotui example scatter # built-in demo scenes, no data needed
99
+ plotui example deps # plotui's own dependency graph, laid
100
+ # out live by a force simulation
101
+ plotui example pipeline # a nightly forecast DAG, running
79
102
  ```
80
103
 
104
+ `--follow` (`-f`) keeps the reader open instead of plotting once at EOF: rows
105
+ are parsed as they arrive and appended in place, so the chart grows without a
106
+ redraw from scratch and pan/zoom survive. It needs piped input and a terminal
107
+ to draw into — a malformed line is skipped rather than fatal, and the count is
108
+ reported when you quit. The shape of the input (delimiter, column count,
109
+ whether x is a calendar) is settled by the first row and held for the rest of
110
+ the stream.
111
+
112
+ On a long feed the whole run compresses into a sliver, so `--window <N>` keeps
113
+ the view on the last N samples and `--last <span>` on the last `30s` / `5m` /
114
+ `2h` of x. Neither drops data: the window is a *view*, drawn on the range
115
+ slider (which a window switches on) against the entire run, so you can drag
116
+ back through everything that has arrived. Doing so hands the view over — a
117
+ reader who has scrolled back to an incident does not want the next row to
118
+ yank them forward — and **f** goes live again, jumping to the head.
119
+
81
120
  Like every plotui frontend, the CLI needs a terminal with Kitty graphics
82
121
  (supported terminals below); elsewhere it prints a notice and exits.
83
122
 
@@ -116,7 +155,8 @@ terminals, never a degraded plot. Override with
116
155
  from plotui import Plot
117
156
 
118
157
  # 2D: axes, ticks, and a legend appear automatically. Traces added without a
119
- # color take palette slots in fixed order; `name=` puts a series in the legend.
158
+ # color take colorway slots in fixed order; `name=` puts a series in the
159
+ # legend. Colors accept (r, g, b) tuples or shorthands: "#e63c78", "red".
120
160
  plot = Plot()
121
161
  plot.add_line(xs, ys, name="forecast")
122
162
  plot.add_scatter(xs2, ys2, name="observed")
@@ -128,9 +168,42 @@ plot.add_bar(xs3, heights)
128
168
  plot.add_line(xs, tokens, name="tokens", axis="y2")
129
169
  plot.add_line(xs, cpu_minutes, name="cpu min", axis="y3")
130
170
 
171
+ # Titles: each buys its own margin, so the plot area shrinks rather than
172
+ # drawing over the data. The y title is drawn rotated in the left margin.
173
+ plot.set_title("p99 latency")
174
+ plot.set_x_title("requests")
175
+ plot.set_y_title("ms")
176
+
177
+ # Ranges and scales: an explicit range pins the *extent* only — no autoscale
178
+ # padding, and zoom/pan still compose on top of it (unlike set_x_window,
179
+ # which is the live window and supersedes the camera). A log axis ticks in
180
+ # powers of ten; values at or below zero have no log coordinate and neither
181
+ # set the range nor draw.
182
+ plot.set_x_range((0, 100)) # None restores autoscale
183
+ plot.set_y_range((0.1, 1e4))
184
+ plot.set_y_log(True) # set_x_log for x
185
+
186
+ # DAGs and pipelines: labelled boxes wired by arrows, laid out by rank. Node
187
+ # centres are data coordinates; each box is sized in pixels from its label,
188
+ # so zooming spreads the graph apart while the text stays readable.
189
+ from plotui import LayeredLayout, from_dot, reachable
190
+
191
+ layout = LayeredLayout(len(tasks), edges) # rankdir="TB" or "LR"
192
+ plot = Plot()
193
+ h = plot.add_graph2d(*layout.positions(), edges,
194
+ labels=tasks, routes=layout.routes())
195
+ plot.set_graph_colors(h, states) # repaint as the run advances
196
+ lit = reachable(len(tasks), edges, hovered) # everything it waits on
197
+ plot = from_dot(open("pipeline.dot").read()) # or straight from DOT
198
+
131
199
  # 3D: any 3D trace switches the plot to the orbit camera.
132
200
  plot = Plot()
133
- plot.add_scatter3d(xs, ys, zs, color=(230, 60, 120), size=2.0)
201
+ plot.add_scatter3d(xs, ys, zs, name="Cluster A") # colors from the colorway
202
+
203
+ # Colorways: the default sequence is pink/cyan/orange-first; swap it with a
204
+ # built-in name or your own list before adding traces.
205
+ plot.set_colorway("vivid") # "plotui", "muted", "vivid"
206
+ plot.set_colorway(["#e63c78", "cyan", (240, 161, 60)])
134
207
 
135
208
  # Streaming: every add_* returns a trace handle. Append through it instead
136
209
  # of rebuilding — O(new points), autoscale follows; numpy arrays are read
@@ -190,39 +263,13 @@ interaction in a subclass, override the `apply_rotate` / `apply_pan` /
190
263
  routes through — do **not** override the Textual `on_*` handlers (Textual
191
264
  dispatches those to every class in the MRO, so both would run).
192
265
 
193
- ## Roadmap
194
-
195
- - [x] Flicker-free Kitty placement via Unicode-placeholder virtual placement
196
- (fixed image id, atomic replace) — wire the pixel path into the Textual widget
197
- - [x] 2D traces: scatter, line, bar; axes, ticks, tick labels, legend
198
- - [x] Independent right-hand y-axes (`axis="y2"`/`"y3"`) with tinted tick labels
199
- - [ ] 2D step trace; axis titles; time-formatted x ticks
200
- - [ ] 3D surface / mesh; axis cube with labels
201
- - [x] Interactive hover / pick for 3D graph nodes *and* edges (opt-in via
202
- `PlotWidget(..., pickable=True)`: hover lights the element up white,
203
- click posts `ElementPicked`)
204
- - [ ] Hover / pick for 2D traces; spatial index for large graphs
205
- - [x] Streaming append: trace handles, `extend`, `set_visible`, incremental bounds
206
- - [x] numpy fast-path input (one bulk copy, no per-element conversion)
207
- - [ ] Rolling window (`max_points`) for endless streams
208
- - [x] Graceful render-path auto-detection (placeholder / direct Kitty, with a
209
- supported-terminals notice elsewhere and a `PLOTUI_RENDER` override)
210
- - [ ] Sixel + iTerm2 OSC 1337 encoders for terminals without Kitty graphics
211
- - [x] Prebuilt wheels (maturin + GitHub Actions): `pip install plotui` — macOS
212
- arm64/x86_64, Linux x86_64/aarch64, abi3 ≥ 3.9; the wheel bundles the
213
- CLI binary
214
- - [x] Ratatui frontend (native): `plotui-ratatui` — StatefulWidget + app-owned
215
- PlotState, full parity with the Textual widget
216
- (`cargo run -p plotui-ratatui --example demo`)
217
- - [x] Bubble Tea frontend (cgo): `go/` bindings over the `plotui-ffi` C ABI +
218
- the `teaplot` component for Bubble Tea v2 (see `go/README.md`)
219
- - [x] CLI: `plotui line|scatter|bar` from stdin or a file — interactive on a
220
- TTY, one static frame when piped; installed via curl, Homebrew, cargo,
221
- or pip (see Install above)
222
- - [ ] CLI v2: `--follow` streaming, `scatter3d`, histogram/density/count
223
- transforms, `--out png`
224
- - [ ] Prebuilt static libs for the Go bindings (today: local source build)
225
-
226
266
  ## License
227
267
 
228
- MIT
268
+ MIT, except for one embedded asset: chart text is set in
269
+ [Martian Mono](https://github.com/evilmartians/mono) (Copyright 2020 The
270
+ Martian Mono Project Authors), used under the SIL Open Font License 1.1. The
271
+ glyph outlines are compiled into `plotui-core` as
272
+ `crates/plotui-core/src/glyphs.rs`; the license travels with them in
273
+ `crates/plotui-core/fonts/MartianMono-OFL.txt`. The font carries no Reserved
274
+ Font Name, and nothing about the OFL reaches your code — it covers the font
275
+ data, not the crate.
@@ -9,9 +9,12 @@ drops into a [Textual](https://textual.textualize.io/),
9
9
  [Bubble Tea](https://github.com/charmbracelet/bubbletea) app as a first-class
10
10
  widget, with the rendering engine written in Rust so it stays fast in 2D and 3D.
11
11
 
12
- > Status: **early scaffold.** Working today: 2D scatter/line/bar charts with
13
- > axes, ticks, and a legend; a 3D scatter/graph engine; a Kitty-image raw demo;
14
- > and a Textual widget. See the roadmap below.
12
+ > Status: **early scaffold.** Working today: 2D scatter/line/step/bar,
13
+ > histogram, box, heatmap, and band charts with axes, ticks, titles,
14
+ > explicit ranges, log scales, categorical labels, a colorbar and a legend;
15
+ > DAG/pipeline graphs with a layered layout and a DOT subset reader; a 3D
16
+ > scatter/graph/surface/mesh engine; a Kitty-image raw demo; and a Textual
17
+ > widget.
15
18
 
16
19
  ## Architecture
17
20
 
@@ -20,6 +23,10 @@ terminal.** It has no event loop and no input handling — the TUI framework
20
23
  (Textual, Ratatui, or Bubble Tea) owns the loop, forwards input to the
21
24
  camera, and asks for a frame.
22
25
 
26
+ ```
27
+ f64 data ─▶ camera + rasterizer ─▶ RGBA buffer ─▶ Kitty escape bytes ─▶ your terminal's cell grid
28
+ ```
29
+
23
30
  ```
24
31
  crates/
25
32
  plotui-core/ pure engine: data model, 3D camera, rasterizer → RGBA
@@ -37,7 +44,9 @@ examples/ raw_demo.py (Kitty images), textual_demo.py
37
44
  ```
38
45
 
39
46
  `core` and `protocol` are pure and I/O-free, so the same engine can back every
40
- frontend and be unit-tested by hashing pixel buffers.
47
+ frontend and be unit-tested by hashing pixel buffers. The protocol layer emits
48
+ Kitty graphics escapes — chunked, placement-aware, and wrapped for tmux
49
+ passthrough — as pure functions of the RGBA frame.
41
50
 
42
51
  ## Integrations
43
52
 
@@ -73,11 +82,41 @@ cargo binstall plotui # prebuilt, via cargo-binstall
73
82
  ```
74
83
 
75
84
  ```bash
76
- seq 1 100 | awk '{print $1, sin($1/10)}' | plotui line
85
+ seq 1 100 | LC_ALL=C awk '{print $1, sin($1/10)}' | plotui line
77
86
  plotui scatter -H -d, data.csv # header row + comma-delimited
78
- plotui bar counts.tsv
87
+ plotui bar counts.tsv # --horizontal, --stack, --group
88
+ plotui step states.txt # holds its value between samples
89
+ plotui hist samples.txt # binned automatically
90
+ plotui box -H measurements.tsv # one box per column
91
+ plotui dag pipeline.dot # a DAG from a DOT file; hover a task
92
+ # to light everything it waits on
93
+ plotui line --log-y --title "queue depth" \
94
+ --x-title minute --y-title items # titles and log scales
95
+ plotui line --x-range 0:100 --y-range 0:1 # pin an extent, LO:HI
96
+ tail -f app.log | LC_ALL=C awk '{print $2}' \
97
+ | plotui line -f --window 200 # live, on the last 200 samples
98
+ plotui example scatter # built-in demo scenes, no data needed
99
+ plotui example deps # plotui's own dependency graph, laid
100
+ # out live by a force simulation
101
+ plotui example pipeline # a nightly forecast DAG, running
79
102
  ```
80
103
 
104
+ `--follow` (`-f`) keeps the reader open instead of plotting once at EOF: rows
105
+ are parsed as they arrive and appended in place, so the chart grows without a
106
+ redraw from scratch and pan/zoom survive. It needs piped input and a terminal
107
+ to draw into — a malformed line is skipped rather than fatal, and the count is
108
+ reported when you quit. The shape of the input (delimiter, column count,
109
+ whether x is a calendar) is settled by the first row and held for the rest of
110
+ the stream.
111
+
112
+ On a long feed the whole run compresses into a sliver, so `--window <N>` keeps
113
+ the view on the last N samples and `--last <span>` on the last `30s` / `5m` /
114
+ `2h` of x. Neither drops data: the window is a *view*, drawn on the range
115
+ slider (which a window switches on) against the entire run, so you can drag
116
+ back through everything that has arrived. Doing so hands the view over — a
117
+ reader who has scrolled back to an incident does not want the next row to
118
+ yank them forward — and **f** goes live again, jumping to the head.
119
+
81
120
  Like every plotui frontend, the CLI needs a terminal with Kitty graphics
82
121
  (supported terminals below); elsewhere it prints a notice and exits.
83
122
 
@@ -116,7 +155,8 @@ terminals, never a degraded plot. Override with
116
155
  from plotui import Plot
117
156
 
118
157
  # 2D: axes, ticks, and a legend appear automatically. Traces added without a
119
- # color take palette slots in fixed order; `name=` puts a series in the legend.
158
+ # color take colorway slots in fixed order; `name=` puts a series in the
159
+ # legend. Colors accept (r, g, b) tuples or shorthands: "#e63c78", "red".
120
160
  plot = Plot()
121
161
  plot.add_line(xs, ys, name="forecast")
122
162
  plot.add_scatter(xs2, ys2, name="observed")
@@ -128,9 +168,42 @@ plot.add_bar(xs3, heights)
128
168
  plot.add_line(xs, tokens, name="tokens", axis="y2")
129
169
  plot.add_line(xs, cpu_minutes, name="cpu min", axis="y3")
130
170
 
171
+ # Titles: each buys its own margin, so the plot area shrinks rather than
172
+ # drawing over the data. The y title is drawn rotated in the left margin.
173
+ plot.set_title("p99 latency")
174
+ plot.set_x_title("requests")
175
+ plot.set_y_title("ms")
176
+
177
+ # Ranges and scales: an explicit range pins the *extent* only — no autoscale
178
+ # padding, and zoom/pan still compose on top of it (unlike set_x_window,
179
+ # which is the live window and supersedes the camera). A log axis ticks in
180
+ # powers of ten; values at or below zero have no log coordinate and neither
181
+ # set the range nor draw.
182
+ plot.set_x_range((0, 100)) # None restores autoscale
183
+ plot.set_y_range((0.1, 1e4))
184
+ plot.set_y_log(True) # set_x_log for x
185
+
186
+ # DAGs and pipelines: labelled boxes wired by arrows, laid out by rank. Node
187
+ # centres are data coordinates; each box is sized in pixels from its label,
188
+ # so zooming spreads the graph apart while the text stays readable.
189
+ from plotui import LayeredLayout, from_dot, reachable
190
+
191
+ layout = LayeredLayout(len(tasks), edges) # rankdir="TB" or "LR"
192
+ plot = Plot()
193
+ h = plot.add_graph2d(*layout.positions(), edges,
194
+ labels=tasks, routes=layout.routes())
195
+ plot.set_graph_colors(h, states) # repaint as the run advances
196
+ lit = reachable(len(tasks), edges, hovered) # everything it waits on
197
+ plot = from_dot(open("pipeline.dot").read()) # or straight from DOT
198
+
131
199
  # 3D: any 3D trace switches the plot to the orbit camera.
132
200
  plot = Plot()
133
- plot.add_scatter3d(xs, ys, zs, color=(230, 60, 120), size=2.0)
201
+ plot.add_scatter3d(xs, ys, zs, name="Cluster A") # colors from the colorway
202
+
203
+ # Colorways: the default sequence is pink/cyan/orange-first; swap it with a
204
+ # built-in name or your own list before adding traces.
205
+ plot.set_colorway("vivid") # "plotui", "muted", "vivid"
206
+ plot.set_colorway(["#e63c78", "cyan", (240, 161, 60)])
134
207
 
135
208
  # Streaming: every add_* returns a trace handle. Append through it instead
136
209
  # of rebuilding — O(new points), autoscale follows; numpy arrays are read
@@ -190,39 +263,13 @@ interaction in a subclass, override the `apply_rotate` / `apply_pan` /
190
263
  routes through — do **not** override the Textual `on_*` handlers (Textual
191
264
  dispatches those to every class in the MRO, so both would run).
192
265
 
193
- ## Roadmap
194
-
195
- - [x] Flicker-free Kitty placement via Unicode-placeholder virtual placement
196
- (fixed image id, atomic replace) — wire the pixel path into the Textual widget
197
- - [x] 2D traces: scatter, line, bar; axes, ticks, tick labels, legend
198
- - [x] Independent right-hand y-axes (`axis="y2"`/`"y3"`) with tinted tick labels
199
- - [ ] 2D step trace; axis titles; time-formatted x ticks
200
- - [ ] 3D surface / mesh; axis cube with labels
201
- - [x] Interactive hover / pick for 3D graph nodes *and* edges (opt-in via
202
- `PlotWidget(..., pickable=True)`: hover lights the element up white,
203
- click posts `ElementPicked`)
204
- - [ ] Hover / pick for 2D traces; spatial index for large graphs
205
- - [x] Streaming append: trace handles, `extend`, `set_visible`, incremental bounds
206
- - [x] numpy fast-path input (one bulk copy, no per-element conversion)
207
- - [ ] Rolling window (`max_points`) for endless streams
208
- - [x] Graceful render-path auto-detection (placeholder / direct Kitty, with a
209
- supported-terminals notice elsewhere and a `PLOTUI_RENDER` override)
210
- - [ ] Sixel + iTerm2 OSC 1337 encoders for terminals without Kitty graphics
211
- - [x] Prebuilt wheels (maturin + GitHub Actions): `pip install plotui` — macOS
212
- arm64/x86_64, Linux x86_64/aarch64, abi3 ≥ 3.9; the wheel bundles the
213
- CLI binary
214
- - [x] Ratatui frontend (native): `plotui-ratatui` — StatefulWidget + app-owned
215
- PlotState, full parity with the Textual widget
216
- (`cargo run -p plotui-ratatui --example demo`)
217
- - [x] Bubble Tea frontend (cgo): `go/` bindings over the `plotui-ffi` C ABI +
218
- the `teaplot` component for Bubble Tea v2 (see `go/README.md`)
219
- - [x] CLI: `plotui line|scatter|bar` from stdin or a file — interactive on a
220
- TTY, one static frame when piped; installed via curl, Homebrew, cargo,
221
- or pip (see Install above)
222
- - [ ] CLI v2: `--follow` streaming, `scatter3d`, histogram/density/count
223
- transforms, `--out png`
224
- - [ ] Prebuilt static libs for the Go bindings (today: local source build)
225
-
226
266
  ## License
227
267
 
228
- MIT
268
+ MIT, except for one embedded asset: chart text is set in
269
+ [Martian Mono](https://github.com/evilmartians/mono) (Copyright 2020 The
270
+ Martian Mono Project Authors), used under the SIL Open Font License 1.1. The
271
+ glyph outlines are compiled into `plotui-core` as
272
+ `crates/plotui-core/src/glyphs.rs`; the license travels with them in
273
+ `crates/plotui-core/fonts/MartianMono-OFL.txt`. The font carries no Reserved
274
+ Font Name, and nothing about the OFL reaches your code — it covers the font
275
+ data, not the crate.