Tutorial 09 - plotext Backend
The plotext backend renders a maxplotlib canvas directly in the terminal. That makes it a good fit for SSH sessions, CLI workflows, quick diagnostics, and situations where you want the same Canvas API without a GUI browser or desktop window.
What this tutorial covers
Area |
Status |
|---|---|
|
✅ |
|
✅ |
|
✅ |
|
✅ |
|
✅ |
|
✅ |
|
✅ |
|
✅ |
Titles, labels, captions, limits, ticks, grid, log/symlog scales |
✅ |
|
✅ |
Layers |
✅ |
Multi-subplot canvases |
✅ |
|
✅ |
|
✅ |
Generic matplotlib patch geometry |
✅ (best effort) |
Terminal animation redraw loop |
✅ |
The goal is not pixel-identical matplotlib output. The goal is a faithful terminal representation of the same plot intent.
[1]:
from pathlib import Path
import matplotlib.patches as mpatches
import numpy as np
from maxplotlib import Canvas
1 · Figure lifecycle: plot(), build(), show(), and savefig()
Use canvas.plot(backend="plotext") when you want a reusable terminal figure object. The returned object supports:
.build()to get the rendered terminal plot as a string.show()to print it immediately.savefig(path)to write the terminal output to a text file
Use canvas.show(backend="plotext") when you just want direct terminal output.
[2]:
x = np.linspace(0, 2 * np.pi, 100)
canvas, ax = Canvas.subplots()
ax.plot(x, np.sin(x), color="cyan", label="sin(x)")
ax.set_title("Demo")
ax.set_xlabel("x")
ax.set_ylabel("y")
terminal_fig = canvas.plot(backend="plotext")
preview = terminal_fig.build(keep_colors=False)
print(preview)
# terminal_fig.show()
# canvas.show(backend="plotext")
Demo
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤ ▞▞ sin(x) ▄▄▞▀▀▀▀▀▄▄ │
│ ▞▀ ▀▚▄ │
│ ▞▀ ▀▄▖ │
0.67┤ ▗▀ ▝▄ │
│ ▄▀▘ ▚▖ │
0.33┤ ▗▞ ▝▖ │
│ ▄▘ ▝▚ │
│▗▞ ▀▖ │
0.00┤▘ ▝▚ ▞│
│ ▀▖ ▄▘│
│ ▝▚ ▗▞ │
-0.33┤ ▀▖ ▄▘ │
│ ▝▄ ▄▞ │
-0.67┤ ▚▖ ▗▀ │
│ ▝▀▖ ▄▞▘ │
│ ▝▀▄▖ ▄▞ │
-1.00┤ ▝▀▀▄▄▄▄▄▄▀▀ │
└┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘
0.0 1.6 3.1 4.7 6.3
y x
2 · Line plots
Line plots are the natural fit for plotext. Labels, markers, grid lines, axis titles, and legends all carry over cleanly.
[3]:
x = np.linspace(0, 2 * np.pi, 120)
canvas, ax = Canvas.subplots()
ax.plot(x, np.sin(x), color="cyan", label="sin(x)")
ax.plot(x, np.cos(x), color="yellow", label="cos(x)", marker="dot")
ax.plot(x, np.sin(2 * x), color="green", label="sin(2x)")
ax.set_title("Multiple terminal lines")
ax.set_xlabel("x")
ax.set_ylabel("value")
ax.set_grid(True)
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
Multiple terminal lines
┌┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┐
1.00┼ ▞▞ sin(x) ─▗▄▄▀▀▀▀▀▄▄─────────────┼──────▗▞▀▀▄──────┼─────────────•••••┤
│ •• cos(x) ▚▖ │ ▀▀▄ │ ▞▘ ▚ sin(x) •••• ││
│ ▞▞ sin(2x) ▐ │ ▀▚ │ ▞ ▀▖ │ ••• ││
0.67┼┼──▗▘─▗▄▘─••──▚───┼─────────▀▖──────┼──▗▀─────────cos(x)─────••─────────┼┤
││ ▗▘ ▗▘ •••▝▖ │ ▝▚▖ │ ▌ sin(2x) ••• ││
0.33┼┼─▞▗▄▘───────••▝▌─┼────────────▝▄───┼─▐─────────────▚─┼───••────────────┼┤
││▞▗▘ •▝▖│ ▚▖ │▗▘ ▝▖│ •• ││
│▐▄▘ •▚│ ▝▄▗▘ ▐│•• ││
0.00┼▌─────────────────▚─────────────────▞─────────────────▚─────────────────▗┤
││ ▝▖• ▐│▚▖ ••│▌ ▗▗▘│
││ │▐ • ▗▘│ ▝▚ • │▝▖ ▗▘▞││
-0.33┼┼─────────────────┼─▌─••─────────▗▘─┼───▚▖───────••───┼─▚──────────▗▀▘▞─┼┤
││ │ ▝▄ •• ▞ │ ▝▚ •• │ ▜ ▗▘ ▐ ││
-0.67┼┼─────────────────┼──▝▖──•••────▗▘──┼──────▀▄•••──────┼───▚─────▗▀▘─▗▘──┼┤
││ │ ▝▖ •• ▞▘ │ ••▄ │ ▝▖ ▄▞▘ ▗▘ ││
││ │ ▝▚ •▞• │ ••• ▀▄▄ │ ▝▄▖ ▗▘ ││
-1.00┼┼─────────────────┼──────▀▄▄▞▀──•••••••••─────────▀▚▄▄▄▄▄▞▀▘─▝▄▄▄▀▘─────┼┤
└┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼┘
0.0 1.6 3.1 4.7 6.3
value x
3 · Scatter plots
Scatter traces use plotext.scatter(...) under the hood. They combine well with a line on the same axes.
[4]:
rng = np.random.default_rng(42)
x = np.linspace(0, 8, 60)
samples_x = np.linspace(0, 8, 15)
samples_y = np.sin(samples_x) + rng.normal(0, 0.15, len(samples_x))
canvas, ax = Canvas.subplots()
ax.plot(x, np.sin(x), color="white", label="sin(x)")
ax.scatter(samples_x, samples_y, color="red", marker="x", label="samples")
ax.set_title("Scatter + line")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
Scatter + line
┌─────────────────────────────────────────────────────────────────────────┐
1.13┤ ▞▞ sin(x) x x │
│ xx samples ▞▀▀▀▚▖ sin(x) ▗▄▄▀▀x│
│ ▗▄▀ ▝▀▄ ▄▞▘ │
0.77┤ ▞▘ ▀▚▖ samples ▞ │
│ ▄▀ ▝▄ x▀ │
0.41┤ ▗▞ x ▚ ▞ │
│ ▗▘ x ▚▖ ▗▀ │
│ ▄▘ ▝▖ x▗▘ │
0.05┤x x▝▄ ▗▘ │
│ ▚ ▞▘ │
│ ▚x ▞ │
-0.31┤ ▚▖ x ▗▀ │
│ ▝▖ ▗▘ │
-0.68┤ ▝▀▖ ▗▞▘ │
│ ▝x▖ ▄▘ │
│ ▝▄▄ ▗▄▀ │
-1.04┤ ▀x▀▀▀▀x │
└┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘
0 2 4 6 8
y x
4 · Bar charts
Bars are supported too, so summary views and simple dashboards work well in the terminal.
[5]:
bins = np.arange(5)
values = np.array([4.0, 6.5, 3.2, 7.4, 5.8])
canvas, ax = Canvas.subplots()
ax.bar(bins, values, color="green", label="count")
ax.scatter(bins, values, color="yellow", label="sample mean")
ax.set_title("Bar + scatter overlay")
ax.set_xlabel("bin")
ax.set_ylabel("value")
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
Bar + scatter overlay
┌───────────────────────────────────────────────────────────────────────────┐
7.4┤ ██ count ███████▘██████ │
│ ▞▞ sample mean ███count██████ │
│ ██████▝███████ ██████████████ │
6.2┤ ██████████████ ███sample mean ██████▗██████│
│ ██████████████ ██████████████ █████████████│
4.9┤ ██████████████ ██████████████ █████████████│
│ ██████████████ ██████████████ █████████████│
│██████▖██████ ██████████████ ██████████████ █████████████│
3.7┤█████████████ ██████████████ ██████████████ █████████████│
│█████████████ ██████████████ ██████▗██████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
2.5┤█████████████ ██████████████ █████████████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
1.2┤█████████████ ██████████████ █████████████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
0.0┤█████████████ ██████████████ █████████████ ██████████████ █████████████│
└──────┬───────────────┬──────────────┬──────────────┬───────────────┬──────┘
0.0 1.0 2.0 3.0 4.0
value bin
5 · Filled regions
The backend supports both common fill_between(...) shapes:
Filling down to a scalar baseline
Filling the region between two full curves
[6]:
x = np.linspace(0, 5, 100)
canvas, ax = Canvas.subplots()
ax.fill_between(
x,
np.exp(-0.5 * x) * np.sin(3 * x) + 1.0,
0.0,
color="cyan",
label="signal envelope",
)
ax.set_title("fill_between() to a scalar baseline")
ax.set_xlabel("x")
ax.set_ylabel("amplitude")
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
fill_between() to a scalar baseline
┌──────────────────────────────────────────────────────────────────────────┐
1.78┤ ▞▞ signal envelope │
│█████████▙ │
│██████████▙ signal envelope │
1.57┤███████████▖ │
│████████████▖ │
1.37┤████████████▙ │
│█████████████▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▖ │
│████████████████████████████████████████▙▄ │
1.16┤███████████████████████████████████████████▖ │
│███████████████████████████████████████████████████████████████████████▄▄▄│
│██████████████████████████████████████████████████████████████▛▀ │
0.95┤███████████████████████████████████████████████████████████▛▀ │
│███████████████████████████████████████████████████████▛▀▀ │
0.74┤████████████████████████████▘ │
│███████████████████████████▘ │
│██████████████████████████▘ │
0.54┤████████████████████████▀ │
└┬─────────────────┬──────────────────┬─────────────────┬─────────────────┬┘
0.0 1.2 2.5 3.8 5.0
amplitude x
[7]:
x = np.linspace(0, 4, 100)
upper = np.sin(x) + 1.8
lower = np.cos(x) + 0.8
canvas, ax = Canvas.subplots()
ax.fill_between(x, upper, lower, color="blue", label="between curves")
ax.plot(x, upper, color="white", label="upper")
ax.plot(x, lower, color="yellow", label="lower")
ax.set_title("fill_between() between two curves")
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
fill_between() between two curves
┌─────────────────────────────────────────────────────────────────────────┐
2.80┤ ▞▞ between curves ▄▄▄▞▀▀▀▀▀▀▀▀▀▀▄▄▄▄ │
│ ▞▞ upper ▀██████████████████▀▀▀▄▄ between curves │
│ ▞▞ lower ████████████████████████▀▀▄▖ │
2.30┤ ▗▄▀▘███████████████████████████████████▝▀▀▄upper │
│ ▗▄▀▀▘██████████████████████████████████████████lower │
│▗▄▀▘██████████████████████████████████████████████████▀▚▄ │
1.80┤▀▀▀▀▀▀▀▄▄▄▄██████████████████████████████████████████████▀▚▄ │
│ ▀▀▄▄█████████████████████████████████████████████▀▚▄▖ │
1.30┤ ▀▀▚▄████████████████████████████████████████████▝▀▄▖ │
│ ▀▀▄▖███████████████████████████████████████████▝▀▚▄ │
│ ▝▀▄▖████████████████████████████████████████████▀▀▀│
0.80┤ ▝▀▚▄████████████████████████████████████████████│
│ ▀▚▄█████████████████████████████████████████│
│ ▀▚▄██████████████████████████████████████│
0.30┤ ▀▀▄▖██████████████████████████████████│
│ ▝▀▀▄▄█████████████████████████████▄│
│ ▀▀▄▄▄██████████████████▗▄▄▞▀▀ │
-0.20┤ ▀▀▀▀▄▄▄▄▄▄▄▄▄▄▄▀▀▀▘ │
└┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘
0 1 2 3 4
6 · Error bars and reference lines
Reference-line helpers (axhline, axvline, hlines, vlines) are all available, and errorbar() works for scalar or array-shaped errors.
[8]:
x = np.linspace(1, 10, 9)
y = np.sqrt(x)
canvas, ax = Canvas.subplots()
ax.errorbar(x, y, yerr=0.15, color="cyan", label="sqrt(x)")
ax.axhline(2.0, color="white")
ax.axvline(4.0, color="yellow")
ax.hlines([1.2, 2.7], xmin=[1, 5], xmax=[3, 9], color="green")
ax.vlines([2.0, 8.0], ymin=[1.0, 2.0], ymax=[1.8, 3.0], color="red")
ax.set_title("Error bars + reference lines")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
Error bars + reference lines
┌────────────────────────┬─────────────────────────────────────────────────┐
3.24┤ │ ││
│ │ sqrt(x) │ ┼│
│ │ ▌ ┼ │
2.85┤ │ ┼ ▌ │
│ │ ▝▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▌▀▀▀▀▀▀▀▀ │
2.47┤ │ ┼ ▌ │
│ │ ┼ ▌ │
│ │ │ │ ▌ │
2.08┤ │ ┼ ▌ │
├──────────────────│─────┼────────────────────────────────▘────────────────┤
│ ▖ ┼ │ │
1.70┤ ▌ │ │
│ ▌┼ │ │
1.31┤ ▌│ │ │
│▄▄▄▄▄▄▄▄▌▄▄▄▄▄▄▄▄ │ │
│┼ ▌ │ │
0.93┤│ ▘ │ │
└┬─────────────────┬─────┴────────────┬─────────────────┬─────────────────┬┘
1.0 3.2 5.5 7.8 10.0
y x
7 · Text and annotations
text() is mapped directly. annotate() works too, and when you pass arrowprops, the backend draws a connector line toward the annotated point.
[9]:
x = np.linspace(0, 6, 120)
y = np.sin(x)
peak_x = x[np.argmax(y)]
peak_y = y.max()
canvas, ax = Canvas.subplots()
ax.plot(x, y, color="cyan")
ax.text(0.8, -0.8, "terminal note", color="yellow")
ax.annotate(
"peak",
xy=(peak_x, peak_y),
xytext=(4.4, 0.4),
color="white",
arrowprops={"color": "green"},
)
ax.set_title("Text and annotations")
print(canvas.plot(backend="plotext").build(keep_colors=False))
Text and annotations
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤ ▄▄▞▀▀▚▄▄▖▄▖ │
│ ▗▄▀ ▝▀▀▚▄▄▖ │
│ ▄▞▘ ▝▀▝▀▀▚▄▄▖ │
0.67┤ ▞ ▚▄ ▝▀▀▀▄▄▄ │
│ ▞▀ ▚ ▀▀▀▄▄▄ │
│ ▗▞ ▀▚ ▀▀▀▄peak │
0.33┤ ▞▘ ▚ │
│ ▞ ▀▄ │
0.00┤▗▀ ▚▖ │
│▘ ▝▖ │
│ ▝▖ │
-0.33┤ ▝▀▖ ▞│
│ ▝▄ ▗▞ │
│ ▀▖ ▞▘ │
-0.67┤ ▝▄▖ ▄▀ │
│ terminal note ▝▄ ▄▞ │
│ ▀▚▄▖ ▄▄▀ │
-1.00┤ ▝▀▄▄▄▄▄▄▞▀ │
└┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘
0.0 1.5 3.0 4.5 6.0
8 · Limits, ticks, grid, and log scales
Axis metadata is one of the nice parts of keeping the same LinePlot API across backends.
[10]:
x = np.linspace(1, 20, 120)
canvas, ax = Canvas.subplots()
ax.plot(x, x**0.5, color="cyan", label="sqrt(x)")
ax.plot(x, np.log(x + 1), color="yellow", label="log(x + 1)")
ax.set_title("Axis controls")
ax.set_xlabel("x")
ax.set_ylabel("value")
ax.set_xlim(1, 20)
ax.set_ylim(0, 5)
ax.set_xticks([1, 2, 5, 10, 20], ["1", "2", "5", "10", "20"])
ax.set_yticks([0, 1, 2, 3, 4, 5])
ax.set_xscale("log")
ax.set_grid(True)
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
Axis controls
┌─────────────────────────────────────────────────────────────────────────────┐
5┼ ▞▞ sqrt(x) ──────────────────────────────────────────────────────────────┤
│ ▞▞ log(x + 1) │
│ ▌ │
4┼▗▌───────────────────────────────────────────────────────────────────────────┤
│▐ │
│▟ │
3┼▌▖───────────────────────────────────────────────────────────────────────────┤
│▗▌ │
│▛ │
│▘ │
2┼─────────────────────────────────────────────────────────────────────────────┤
│ │
│ │
1┼─────────────────────────────────────────────────────────────────────────────┤
│ │
│ │
0┼─────────────────────────────────────────────────────────────────────────────┤
└─────────────────────────────────────────────────────────────────────────────┘
5
value x
9 · Layers
Layer filtering works with the terminal backend too. This is useful when you want progressive reveals or to inspect subsets of a figure.
[11]:
x = np.linspace(0, 2 * np.pi, 100)
canvas, ax = Canvas.subplots()
ax.plot(x, np.sin(x), color="cyan", label="layer 0", layer=0)
ax.plot(x, np.cos(x), color="yellow", label="layer 1", layer=1)
ax.fill_between(x, np.sin(x) + 1.5, 0.0, color="green", label="layer 2", layer=2)
ax.set_title("Layers 0 and 1 only")
ax.set_legend(True)
[12]:
print(canvas.plot(backend="plotext", layers=[0]).build(keep_colors=False))
Layers 0 and 1 only
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤ ▞▞ layer 0 ▄▞▀▀▀▀▀▄▄ │
│ ▄▞▀ ▀▚▖ layer 0 │
│ ▄▞ ▝▀▖ │
0.67┤ ▗▀ ▝▚ │
│ ▞▘ ▀▄▖ │
│ ▗▀ ▐ │
0.33┤ ▞▘ ▚▖ │
│ ▗▀ ▝▄ │
0.00┤▞▘ ▚▖ ▗│
│ ▝▄ ▌│
│ ▚▖ ▗▀ │
-0.33┤ ▝▄ ▞▘ │
│ ▝▖ ▗▀ │
│ ▝▀▖ ▞▘ │
-0.67┤ ▝▚ ▗▀ │
│ ▀▄▖ ▄▀▘ │
│ ▝▄▖ ▗▄▀ │
-1.00┤ ▝▀▀▄▄▄▄▄▄▀▘ │
└┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘
0.0 1.6 3.1 4.7 6.3
[13]:
print(canvas.plot(backend="plotext", layers=[1]).build(keep_colors=False))
Layers 0 and 1 only
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤ ▞▞ layer 1 ▄▄▞▀▀│
│ ▀▚▄ layer 1 ▄▞▀ │
│ ▚▖ ▗▞ │
0.67┤ ▝▀▖ ▗▀▘ │
│ ▝▄ ▄▘ │
│ ▚▖ ▗▞ │
0.33┤ ▝▄ ▄▘ │
│ ▚ ▞ │
0.00┤ ▚ ▞ │
│ ▀▖ ▗▀ │
│ ▝▚ ▞▘ │
-0.33┤ ▚▖ ▗▞ │
│ ▝▄ ▄▘ │
│ ▚▖ ▗▞ │
-0.67┤ ▝▄ ▄▘ │
│ ▚▄ ▄▞ │
│ ▀▄▖ ▗▄▀ │
-1.00┤ ▝▀▚▄▄▄▄▄▞▀▘ │
└┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘
0.0 1.6 3.1 4.7 6.3
[14]:
print(canvas.plot(backend="plotext", layers=[0, 1]).build(keep_colors=False))
Layers 0 and 1 only
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤ ▞▞ layer 0 ▄▞▀▀▀▀▀▄▄ ▄▄▞▀▀│
│ ▞▞ layer 1 ▞▀ ▀▚▖ layer 0 ▄▞▀ │
│ ▚▖▞ ▝▀▖ ▗▞ │
0.67┤ ▗▀▝▀▖ ▝▚ layer 1 ▗▀▘ │
│ ▞▘ ▝▄ ▀▄▖ ▄▘ │
│ ▗▀ ▚▖ ▐ ▗▞ │
0.33┤ ▞▘ ▝▄ ▚▖ ▄▘ │
│ ▗▀ ▚ ▝▄ ▞ │
0.00┤▞▘ ▚ ▚▖ ▞ │
│▘ ▀▖ ▝▄ ▗▀ ▞│
│ ▝▚ ▚▖ ▞▘ ▗▀ │
-0.33┤ ▚▖ ▝▄ ▗▞ ▞▘ │
│ ▝▄ ▝▖ ▄▘ ▗▀ │
│ ▚▖ ▝▀▖ ▗▞ ▞▘ │
-0.67┤ ▝▄ ▝▚ ▄▘ ▗▀ │
│ ▚▄ ▄▞▄▖ ▄▀▘ │
│ ▀▄▖ ▗▄▀ ▝▄▖ ▗▄▀ │
-1.00┤ ▝▀▚▄▄▄▄▄▞▀▘ ▝▀▀▄▄▄▄▄▄▀▘ │
└┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘
0.0 1.6 3.1 4.7 6.3
10 · Multi-subplot canvases
Subplots are fully supported, including figure-level titles via canvas.suptitle(...).
[15]:
x = np.linspace(0, 2 * np.pi, 80)
rng = np.random.default_rng(5)
canvas, (ax1, ax2) = Canvas.subplots(ncols=2)
ax1.plot(x, np.sin(x), color="cyan", label="sin(x)")
ax1.plot(x, np.cos(x), color="yellow", label="cos(x)")
ax1.set_title("Signals")
ax1.set_xlabel("x")
ax1.set_ylabel("value")
ax1.set_legend(True)
cats = np.arange(6)
vals = rng.integers(2, 9, size=6)
ax2.bar(cats, vals, color="green", label="count")
ax2.scatter(cats, vals, color="red", label="points")
ax2.set_title("Counts")
ax2.set_xlabel("bin")
ax2.set_ylabel("value")
ax2.set_legend(True)
canvas.suptitle("Terminal dashboard")
print(canvas.plot(backend="plotext").build(keep_colors=False))
Terminal dashboard
Signals Counts
┌─────────────────────────────────┐ ┌───────────────────────────────────┐
1.00┤ ▞▞ sin(x) ▗▞▀│7.0┤ ██ count █ ██▘██ │
│ ▞▞ cos(x) ▄ sin(x) ▞▘ │ │ ▞▞ points █ █████count │
│ ▝▖ ▚ ▗▘ │ │██▗█████████ █████ │
0.67┤ ▐▝▖ ▌ cos(x)▘ │5.8┤████████████ █████points │
│ ▗▘ ▝▖ ▐ ▗▘ │ │████████████ █████ ▖ ▖ │
0.33┤ ▗▘ ▌ ▚ ▞ │4.7┤████████████ █████████████████│
│ ▌ ▝▖ ▌ ▗▘ │ │████████████ █████████████████│
│▗▘ ▐ ▚ ▗▘ │ │████████████ █████████████████│
0.00┤▌ ▌ ▚ ▐ ▗│3.5┤████████████ █████████████████│
│ ▝▖ ▌ ▗▘ ▗▘│ │████████████ █████████████████│
│ ▚ ▐ ▌ ▞ │ │████████████ █████████████████│
-0.33┤ ▚ ▚ ▞ ▗▘ │2.3┤█████████████████ █████████████████│
│ ▝▖ ▝▖ ▗▘ ▗▘ │ │██████████████▝██ █████████████████│
-0.67┤ ▚ ▐ ▌ ▌ │1.2┤█████████████████ █████████████████│
│ ▚▖ ▗▞ ▞ │ │█████████████████ █████████████████│
│ ▚▖ ▗▞ ▀▖ ▗▞ │ │█████████████████ █████████████████│
-1.00┤ ▝▙▄▟▘ ▀▚▄▄▀ │0.0┤█████████████████ █████████████████│
└┬───────┬───────┬───────┬───────┬┘ └──┬─────┬─────┬─────┬─────┬─────┬──┘
0.0 1.6 3.1 4.7 6.3 0.0 1.0 2.0 3.0 4.0 5.0
value x value bin
11 · Matrix-style imshow() output
add_imshow() is rendered as a terminal matrix plot. This is the terminal approximation of image-like numeric data, not a full matplotlib colormap implementation.
[16]:
data = np.arange(1, 26).reshape(5, 5)
canvas, ax = Canvas.subplots()
ax.add_imshow(data)
ax.set_title("Matrix-style imshow")
ax.set_xlabel("column")
ax.set_ylabel("row")
print(canvas.plot(backend="plotext").build(keep_colors=False))
Matrix-style imshow
┌─────────────────────────────────────────────────────────────────────────────┐
1┤█ █ █ █ █│
│ │
│ │
│ │
2┤█ █ █ █ █│
│ │
│ │
│ │
3┤█ █ █ █ █│
│ │
│ │
│ │
4┤█ █ █ █ █│
│ │
│ │
│ │
5┤█ █ █ █ █│
└┬──────────────────┬──────────────────┬──────────────────┬──────────────────┬┘
1 2 3 4 5
row column
12 · Patches and arbitrary patch geometry
The backend supports common matplotlib patch types directly and also uses matplotlib patch geometry as a best-effort fallback for many other patch subclasses.
matplotlib.patches.Rectanglematplotlib.patches.Circlematplotlib.patches.Polygonmatplotlib.patches.Ellipsemany other patch types that expose a usable matplotlib path
That is enough for many annotations, regions of interest, and geometric callouts.
[17]:
canvas, ax = Canvas.subplots()
ax.add_patch(
mpatches.Rectangle(
(0.2, 0.2), 1.3, 0.7, fill=False, edgecolor="yellow", label="window"
)
)
ax.add_patch(
mpatches.Circle((2.2, 1.6), 0.45, fill=False, edgecolor="cyan", label="sensor")
)
ax.add_patch(
mpatches.Polygon(
[[3.0, 0.5], [3.8, 1.2], [3.4, 2.0]],
fill=True,
facecolor="green",
label="region",
)
)
ax.add_patch(
mpatches.Ellipse(
(2.8, 1.0), 0.8, 0.5, fill=False, edgecolor="white", label="ellipse"
)
)
ax.set_xlim(0, 4.5)
ax.set_ylim(0, 2.5)
ax.set_title("Supported patch types")
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
Supported patch types
┌──────────────────────────────────────────────────────────────────────────┐
2.50┤ ▞▞ window │
│ ▞▞ sensor window │
│ ▞▞ region │
2.08┤ ▞▞ ellipse ▄▄▀▀▀▀▚▄▖ sensor │
│ ▗▞▀ ▝▚▄ region │
│ ▗▘ ▚ ellipse │
1.67┤ ▐ ▐ ▐████▙▖ │
│ ▝▖ ▞ ███████▖ │
1.25┤ ▝▄ ▄▞ ▄▄▄▄ ▟████████▖ │
│ ▀▚▄▄▄▄▄▄▄▄▄▞▀ ▀▚▄▄██████████▘ │
│ ▛ █▜██████▛▀ │
0.83┤ ▐▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ ▚▄▖ ▗▄▞████▀▘ │
│ ▐ ▐ ▝▀▀▀▀▀▀▀▀▘███▛▘ │
│ ▐ ▐ ▟██▀ │
0.42┤ ▐ ▐ ▀▘ │
│ ▐ ▐ │
│ ▝▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │
0.00┤ │
└┬─────────────────┬──────────────────┬─────────────────┬─────────────────┬┘
0.0 1.1 2.2 3.4 4.5
Plotly backend note (patches)
Plotly can render common Matplotlib patches too. Note: Plotly “shapes” do not appear in legends, so maxplotlib adds a small dummy legend entry for labeled patches.
[18]:
# Interactive rendering of the same canvas
canvas.show(backend="plotly")
Data type cannot be displayed: application/vnd.plotly.v1+json
Data type cannot be displayed: application/vnd.plotly.v1+json
13 · Captions, symlog scales, aspect, and colorbar notes
The last backend-specific pieces of the LinePlot surface also work in the terminal backend:
add_caption(...)set_xscale('symlog')andset_yscale('symlog')set_aspect(...)with a best-effort terminal plotsize approximationadd_colorbar(...)as a note-style value summary for matrix/image output
[19]:
x = np.linspace(-20, 20, 161)
canvas, ax = Canvas.subplots()
ax.plot(x, x**3, color="cyan", label="x^3")
ax.set_title("Symlog example")
ax.add_caption("caption text")
ax.set_xscale("symlog")
ax.set_yscale("symlog")
ax.set_aspect("equal")
ax.set_legend(True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
Symlog example | caption text
┌──────────────────────────────────┐
3.9┤ ▞▞ x^3 ▗▄▛│
│ ▄▛▀ │
2.6┤ ▗▟▀▘ │
│ ▗▞▀ │
1.3┤ ▗▛▘ │
0.0┤ ▄▄▄▄▀▘ │
│ ▗▄▀▀▀▀ │
-1.3┤ ▗▟▘ │
│ ▄▞▘ │
-2.6┤ ▗▄▛▘ │
│ ▄▟▀ │
-3.9┤▟▀▘ │
└┬───────┬────────┬───────┬───────┬┘
-1.32 -0.66 0.00 0.66 1.32
[20]:
heat = np.arange(1, 26).reshape(5, 5)
canvas, ax = Canvas.subplots()
ax.add_imshow(heat)
ax.add_colorbar(label="intensity")
ax.set_title("Matrix + colorbar note")
print(canvas.plot(backend="plotext").build(keep_colors=False))
Matrix + colorbar note | intensity: 1..25
┌─────────────────────────────────────────────────────────────────────────────┐
1┤█ █ █ █ █│
│ │
│ │
│ │
2┤█ █ █ █ █│
│ │
│ │
│ │
3┤█ █ █ █ █│
│ │
│ │
│ │
4┤█ █ █ █ █│
│ │
│ │
│ │
5┤█ █ █ █ █│
└┬──────────────────┬──────────────────┬──────────────────┬──────────────────┬┘
1 2 3 4 5
row column
14 · Saving terminal output to a file
Saving with the plotext backend writes the rendered terminal figure to a text file. By default the saved text is plain and easy to inspect in editors, CI logs, or generated artifacts.
Plotly backend note (symlog)
Plotly has no native symlog axis type. For symlog, maxplotlib applies a symmetric log transform to the data and uses a linear Plotly axis. This keeps the plot working across backends, but tick formatting may differ from Matplotlib/plotext.
[21]:
canvas.show(backend="plotly")
Data type cannot be displayed: application/vnd.plotly.v1+json
Data type cannot be displayed: application/vnd.plotly.v1+json
[22]:
x = np.linspace(0, 2 * np.pi, 60)
canvas, ax = Canvas.subplots()
ax.plot(x, np.sin(x), color="cyan", label="sin(x)")
ax.set_title("Saved terminal figure")
terminal_fig = canvas.plot(backend="plotext")
output_path = Path("plotext_output.txt")
terminal_fig.savefig(output_path)
print(output_path.read_text().splitlines()[0])
Saved terminal figure
15 · A simple terminal animation
plotext does not provide browser-widget animation like Plotly or Matplotlib’s GUI animation stack, but terminal animation is still practical: render a frame, clear the output, sleep briefly, and repeat.
The example below works well in a notebook cell or a terminal Python session. In notebooks, clear_output(wait=True) keeps the cell output updating in place.
[23]:
import time
from IPython.display import clear_output
x = np.linspace(0, 2 * np.pi, 120)
for phase in np.linspace(0, 2 * np.pi, 24):
canvas, ax = Canvas.subplots()
ax.plot(x, np.sin(x + phase), color="cyan", label="sin(x + phase)")
ax.plot(x, np.cos(x + phase), color="yellow", label="cos(x + phase)")
ax.set_ylim(-1.2, 1.2)
ax.set_title(f"Animated phase = {phase:.2f}")
ax.set_legend(True)
clear_output(wait=True)
print(canvas.plot(backend="plotext").build(keep_colors=False))
time.sleep(0.08)
Animated phase = 6.28
┌─────────────────────────────────────────────────────────────────────────┐
1.20┤ ▞▞ sin(x + phase) │
│ ▞▞ cos(x + phase) ▄▄▄ sin(x + phase) ▗▄▄▄│
│ ▝▀▚▄ ▗▄▀▀ ▀▚▄▖ ▄▞▀▘ │
0.80┤ ▀▚▖▞▘ ▝▚▄ cos(x + phase)▞▀ │
│ ▗▄▀▝▀▖ ▚▖ ▗▀▘ │
│ ▗▘ ▝▚▖ ▝▀▖ ▗▞▘ │
0.40┤ ▗▞▘ ▝▚ ▝▄▖ ▞▘ │
│ ▞▘ ▚▄ ▝▖ ▄▞ │
0.00┤▄▀ ▚ ▝▀▖ ▞ ▗│
│ ▀▚ ▝▄▖ ▞▀ ▗▘│
│ ▚▄ ▝▖ ▄▞ ▗▞▘ │
-0.40┤ ▚ ▝▚▖ ▞ ▗▄▘ │
│ ▀▚ ▝▄ ▞▀ ▗▘ │
│ ▀▄▖ ▀▚▗▄▀ ▄▀▘ │
-0.80┤ ▝▄▖ ▗▄▘▄▖ ▗▄▀ │
│ ▝▀▄▄▖ ▗▄▄▀▘ ▝▀▄▄ ▄▄▞▘ │
│ ▝▀▀▀▀▀▘ ▀▀▀▀▀▀▀ │
-1.20┤ │
└┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘
0.0 1.6 3.1 4.7 6.3
16 · Current limitations
Feature |
Status |
Notes |
|---|---|---|
|
⚠️ |
rendered as a compact note-style min/max summary rather than a true continuous side bar |
Arbitrary matplotlib patch subclasses |
⚠️ |
best-effort path extraction works for many patch types, but not every custom patch will map perfectly |
Exact matplotlib styling parity |
❌ |
Terminal rendering is approximate by design |
Notebook-style high-frame-rate animation |
⚠️ |
possible via redraw loops, but not a substitute for GUI animation widgets |
For terminal work, this backend now covers a large and practical portion of the LinePlot API. When you need richer styling, publication export, or interactive browser behavior, use the matplotlib, tikzfigure, or plotly backends instead.