Skip to content
Projects

plotext backend

Download notebookView on GitHubRun when the site was built.

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.

AreaStatus
plot() / line graphs✅
scatter()✅
bar()✅
fill_between() to a baseline✅
fill_between() between two curves✅
errorbar()✅
axhline() / axvline() / hlines() / vlines()✅
text() / annotate()✅
Titles, labels, captions, limits, ticks, grid, log/symlog scales✅
set_aspect()✅
Layers✅
Multi-subplot canvases✅
add_imshow() matrix-style rendering✅
add_colorbar() note-style summary✅
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.

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()

Section titled “1 · Figure lifecycle: plot(), build(), show(), and savefig()”

Use canvas.render(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.

x = np.linspace(0, 2 * np.pi, 100)
canvas = Canvas()
canvas.plot(x, np.sin(x), color="cyan", label="sin(x)")
canvas.set_title("Demo")
canvas.set_xlabel("x")
canvas.set_ylabel("y")
terminal_fig = canvas.render(backend="plotext")
preview = terminal_fig.build(keep_colors=False)
print(preview)
# terminal_fig.show()
# canvas.show(backend="plotext")
Demo
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤┌──────────┐ ▄▗▄▗▖▄▗▖ │
││ │▞▝▘ ▀▗▖ │
││ ▚ sin(x) │ ▝▝▖ │
││ │ ▚▗ │
0.50┤└──────────┘ ▘▖ │
│ ▗▝ ▝▗ │
│ ▗▘▘ ▚ │
│ ▞ ▝▖ │
0.00┤▗ ▚ ▖│
│ ▝▖ ▞ │
│ ▚ ▗▗▘ │
│ ▘▖ ▖▘ │
-0.50┤ ▝▗ ▗▝ │
│ ▘▚ ▖▘ │
│ ▝▖▖ ▖▀ │
│ ▝▘▄ ▗▖▞▝ │
-1.00┤ ▝▘▀▝▘▀▘▀ │
└┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬┘
0.0 1.0 2.1 3.1 4.2 5.2 6.3
y x

Line plots are the natural fit for plotext. Labels, markers, grid lines, axis titles, and legends all carry over cleanly.

x = np.linspace(0, 2 * np.pi, 120)
canvas = Canvas()
canvas.plot(x, np.sin(x), color="cyan", label="sin(x)")
canvas.plot(x, np.cos(x), color="yellow", label="cos(x)", marker="dot")
canvas.plot(x, np.sin(2 * x), color="green", label="sin(2x)")
canvas.set_title("Multiple terminal lines")
canvas.set_xlabel("x")
canvas.set_ylabel("value")
canvas.set_grid(True)
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
Multiple terminal lines
┌┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬┐
1.00┼┌───────────┐──▄▄▖▄▄▖▄▖─┼───────────┼───────▄▄▖▖┼───────────┼───────•••••┤
││ │▀▘ ▝▘▚▖ │ ▞▘ ▝▚ sin(x) │ •••• ││
││ ▚ sin(x) │▚ │▝▝▄ │ ▞ │▝▖ │ ••• ││
││ │ ▘▖ │ ▚▗ │ ▘ │ cos(x) │•• ││
0.50┼│ • cos(x) │•─▗────────┼─────▚─────┼──▞────────┼─sin(2x)──•••──────────┼┤
││ │•• ▘ │ ▀▗ │ ▗ │ ▝▗ ••│ ││
││ ▚ sin(2x) │ •▝ │ ▚ │ ▘ │ ▖ •• │ ││
││ │ •▘▖ │ ▀▗│▘ │ ▝ •• │ ││
0.00┼└───────────┘─────▗─────┼───────────█───────────┼─────▘─────┼──────────▗▖┤
││ │ ▖• │ ▗│▘▄ │ ••▝▗ │ ▗▀││
││ │ ▝ • │ ▗ │ ▚ │ • ▖ │ ▗▖▚▘││
││ │ ▘▖••│ ▘ │ ▘▄ │•• ▗ │ ▗▘ ▖ ││
-0.50┼┼───────────┼────────▗──••───────▞──┼─────▚────••────────▘──┼────▗▖▘─▖──┼┤
││ │ ▘ │••• ▗ │ ▘▚•••│ ▝▗ │ ▄▘ ▞ ││
││ │ ▝▖│ •• ▞ │ ••▖▖│ ▚│ ▞▘ ▝ ││
││ │ ▚▖ ▗▞• │ ••• ▝▚▗▖ ▗▄▘▛ ▗▘▘ ││
-1.00┼┼───────────┼───────────┼▝▝▀▀───•••••••••───────┼─▝▀▝▀▀▝▀▀──┼▝▀▘▀▘──────┼┤
└┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼┘
0.0 1.0 2.1 3.1 4.2 5.2 6.3
value x

Scatter traces use plotext.scatter(...) under the hood. They combine well with a line on the same axes.

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 = Canvas()
canvas.plot(x, np.sin(x), color="white", label="sin(x)")
canvas.scatter(samples_x, samples_y, color="red", marker="x", label="samples")
canvas.set_title("Scatter + line")
canvas.set_xlabel("x")
canvas.set_ylabel("y")
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
Scatter + line
┌──────────────────────────────────────────────────────────────────────────┐
1.1┤┌───────────┐ x x │
││ │ ▘▘▗▗ sin(x) ▗ ▖▘x│
││ ▚ sin(x) │ ▘▖ ▘▝ │
││ │ ▝ samples ▗ ▘ │
0.6┤│ x samples │ ▝ ▖ x │
││ │ x ▖ ▘ │
│└───────────┘ ▖ ▘ │
│ ▗ ▗ x ▝ │
0.0┤x x ▗ ▝ │
│ ▖ ▘ │
│ x ▘ │
│ ▗ x ▝ │
-0.5┤ ▗ ▖▝ │
│ ▘ ▖ │
│ ▘x ▝ │
│ ▝ ▖ ▖▗▝ │
-1.0┤ ▘x▝ ▘ x │
└┬───────────┬───────────┬────────────┬───────────┬───────────┬───────────┬┘
0.0 1.3 2.7 4.0 5.3 6.7 8.0
y x

Bars are supported too, so summary views and simple dashboards work well in the terminal.

bins = np.arange(5)
values = np.array([4.0, 6.5, 3.2, 7.4, 5.8])
canvas = Canvas()
canvas.bar(bins, values, color="green", label="count")
canvas.scatter(bins, values, color="yellow", label="sample mean")
canvas.set_title("Bar + scatter overlay")
canvas.set_xlabel("bin")
canvas.set_ylabel("value")
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
Bar + scatter overlay
┌───────────────────────────────────────────────────────────────────────────┐
7.4┤┌───────────────┐ ██████▗███████ │
││ │ ███count██████ │
││ █ count │█████▘██████ ██████████████ │
││ │████████████ ███sample mean ██████▖██████│
5.6┤│ ▚ sample mean │████████████ ██████████████ █████████████│
││ │████████████ ██████████████ █████████████│
│└───────────────┘████████████ ██████████████ █████████████│
│██████▗██████ ██████████████ ██████████████ █████████████│
3.7┤█████████████ ██████████████ ██████████████ █████████████│
│█████████████ ██████████████ ██████▗██████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
1.9┤█████████████ ██████████████ █████████████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
│█████████████ ██████████████ █████████████ ██████████████ █████████████│
0.0┤█████████████ ██████████████ █████████████ ██████████████ █████████████│
└──────┬───────────────┬──────────────┬──────────────┬───────────────┬──────┘
0 1 2 3 4
value bin

The backend supports both common fill_between(...) shapes:

  1. Filling down to a scalar baseline
  2. Filling the region between two full curves
x = np.linspace(0, 5, 100)
canvas = Canvas()
canvas.fill_between(
x,
np.exp(-0.5 * x) * np.sin(3 * x) + 1.0,
0.0,
color="cyan",
label="signal envelope",
)
canvas.set_title("fill_between() to a scalar baseline")
canvas.set_xlabel("x")
canvas.set_ylabel("amplitude")
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
fill_between() to a scalar baseline
┌──────────────────────────────────────────────────────────────────────────┐
1.78┤┌───────────────────┐ │
││ │ │
││ ▚ signal envelope │ signal envelope │
││ │ │
1.47┤└───────────────────┘ │
│ ▝ │
│ ▗ ▘ │
│ ▝ ▖▞▝▘▀▝▘▖ │
1.16┤ ▖ ▝ ▗▝ ▝▝▖ │
│ ▖ ▖ ▚ ▗▗▖▀▝▘▀▝▘▄▖│
│▗ ▗ ▗▘ ▘▚ ▗▗▘▘ │
│ ▗ ▗ ▝▖▖ ▖▄▘▘ │
0.85┤ ▖ ▘ ▝▝▘▄▗▖▀▝ │
│ ▖ ▗▝ │
│ ▗ ▖▘ │
│ ▝▖ ▗▝ │
0.54┤ ▀▝▘▘ │
└┬───────────┬───────────┬────────────┬───────────┬───────────┬───────────┬┘
0.0 0.8 1.7 2.5 3.3 4.2 5.0
amplitude x
x = np.linspace(0, 4, 100)
upper = np.sin(x) + 1.8
lower = np.cos(x) + 0.8
canvas = Canvas()
canvas.fill_between(x, upper, lower, color="blue", label="between curves")
canvas.plot(x, upper, color="white", label="upper")
canvas.plot(x, lower, color="yellow", label="lower")
canvas.set_title("fill_between() between two curves")
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
fill_between() between two curves
┌──────────────────────────────────────────────────────────────────────────┐
2.8┤┌──────────────────┐ ▗▗▖▄▗▖▄▗▄▗▖▄▗▖▄▗ │
││ │▘▘ ▘▀▝▖▄▗ between curves │
││ ▚ between curves │ ▘▀▖▄ │
││ │ ▝▘▄▗upper │
2.1┤│ ▚ upper │ lower │
││ │ ▘▚▗▖ │
││ ▚ lower │ ▀▖▖ │
││ │ ▝▝▖▖ │
│└──────────────────┘ ▝▝▘▄ │
1.3┤ ▝▘▚▗ ▝▘▚▗▖ │
│ ▘▚▗▖ ▀▘│
│ ▀▗▖ │
│ ▝▝▖▄ │
0.6┤ ▝▘▄ │
│ ▝▘▚▗ │
│ ▘▀▗▖▖ ▄▖│
│ ▝▘▚▗▖▄ ▄▗▖▀▝▘ │
-0.2┤ ▝▘▀▝▘▀▝▘▀▘▀▝▘▀▝▘ │
└┬───────────┬───────────┬────────────┬───────────┬───────────┬───────────┬┘
0.0 0.7 1.3 2.0 2.7 3.3 4.0

Reference-line helpers (axhline, axvline, hlines, vlines) are all available, and errorbar() works for scalar or array-shaped errors.

x = np.linspace(1, 10, 9)
y = np.sqrt(x)
canvas = Canvas()
canvas.errorbar(x, y, yerr=0.15, color="cyan", label="sqrt(x)")
canvas.axhline(2.0, color="white")
canvas.axvline(4.0, color="yellow")
canvas.hlines([1.2, 2.7], xmin=[1, 5], xmax=[3, 9], color="green")
canvas.vlines([2.0, 8.0], ymin=[1.0, 2.0], ymax=[1.8, 3.0], color="red")
canvas.set_title("Error bars + reference lines")
canvas.set_xlabel("x")
canvas.set_ylabel("y")
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
Error bars + reference lines
┌──────────────────┬────────────────────────────────────────────────────────┐
3.3┤┌───────────┐ │ sqrt(x) │ │
││ │ │ ────────────────────┼────┼──────────────────────────│
││ ┼ sqrt(x) │ │ ─────────────────┼▗────┼────┼───────────────── │
││ │ ─┼──────────────────┼─────┼────────────── │
2.7┤└───────────┘ │ ▘ │ │ ▝ │
│ ────┼───────┼────┼───────────────── │
│ ───────┼───────┼────┼───────── │
│ │ │ │ │
2.1┤ ─────────┼─┼─────────── │
├──────────────┼───┼─┼─────────────────▝────────────────────────────────────┤
│ ──▘─────┼───┼──── │
│ │ │ │ │
1.5┤ ─────┼────── │ │
│ │ │ │
│ ▘ ▘ │ │
│───┼── ▖ │ │
0.8┤ │ │ │
└┬───────────┬─────┴──────┬───────────┬───────────┬────────────┬───────────┬┘
0.5 2.9 5.3 7.8 10.2 12.6 15.0
y x

text() is mapped directly. annotate() works too, and when you pass arrowprops, the backend draws a connector line toward the annotated point.

x = np.linspace(0, 6, 120)
y = np.sin(x)
peak_x = x[np.argmax(y)]
peak_y = y.max()
canvas = Canvas()
canvas.plot(x, y, color="cyan")
canvas.text(0.8, -0.8, "terminal note", color="yellow")
canvas.annotate(
"peak",
xy=(peak_x, peak_y),
xytext=(4.4, 0.4),
color="white",
arrowprops={"color": "green"},
)
canvas.set_title("Text and annotations")
print(canvas.render(backend="plotext").build(keep_colors=False))
Text and annotations
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤ ▗▄▖▄▄▖▄▄ │
│ ▗▞▘▘ ▘▀▄▗ │
│ ▄▀ ▀▖ │
│ ▞▝ ▝▝▖ │
0.50┤ ▗▞ ▝▚ │
│ ▞ ▝▄ peak │
│ ▗▀ ▚ │
│ ▞ ▝▖ │
│ ▞ ▝▘▖ │
0.00┤▝ ▝▖ │
│ ▝▖▖ │
│ ▝▖ ▗▘│
│ ▝▖▖ ▄▘ │
-0.50┤ ▝▖ ▗▘ │
│ ▝▘▖ ▞▘ │
│ terminal note ▝▚▗ ▗▞▘ │
│ ▀▄▗ ▗▄▘▘ │
-1.00┤ ▀▀▝▀▀▝▀▘▘ │
└┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬┘
0 1 2 3 4 5 6

Axis metadata is one of the nice parts of keeping the same LinePlot API across backends.

x = np.linspace(1, 20, 120)
canvas = Canvas()
canvas.plot(x, x**0.5, color="cyan", label="sqrt(x)")
canvas.plot(x, np.log(x + 1), color="yellow", label="log(x + 1)")
canvas.set_title("Axis controls")
canvas.set_xlabel("x")
canvas.set_ylabel("value")
canvas.set_xlim(1, 20)
canvas.set_ylim(0, 5)
canvas.set_xticks([1, 2, 5, 10, 20], ["1", "2", "5", "10", "20"])
canvas.set_yticks([0, 1, 2, 3, 4, 5])
canvas.set_xscale("log")
canvas.set_grid(True)
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
Axis controls
┌┬┬───────────────────────────────────────────────────────────────────────────┐
5┼┌──────────────┐─────────────────────────────────────────────────────────────┤
││ │ │
││ ▚ sqrt(x) │ │
4┼│ │─────────────────────────────────────────────────────────────┤
││ ▚ log(x + 1) │ │
││ │ │
3┼└──────────────┘─────────────────────────────────────────────────────────────┤
│▙▛ │
│▟│ │
│▌│ │
2┼┼┼───────────────────────────────────────────────────────────────────────────┤
│││ │
│││ │
1┼┼┼───────────────────────────────────────────────────────────────────────────┤
│││ │
│││ │
0┼┼┼───────────────────────────────────────────────────────────────────────────┤
└┼┴───────────────────────────────────────────────────────────────────────────┘
5
value x

Layer filtering works with the terminal backend too. This is useful when you want progressive reveals or to inspect subsets of a figure.

x = np.linspace(0, 2 * np.pi, 100)
canvas = Canvas()
canvas.plot(x, np.sin(x), color="cyan", label="layer 0", layer=0)
canvas.plot(x, np.cos(x), color="yellow", label="layer 1", layer=1)
canvas.fill_between(x, np.sin(x) + 1.5, 0.0, color="green", label="layer 2", layer=2)
canvas.set_title("Layers 0 and 1 only")
canvas.set_legend(True)
print(canvas.render(backend="plotext", layers=[0]).build(keep_colors=False))
Layers 0 and 1 only
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤┌───────────┐ ▄▗▄▗▖▄▗▖ │
││ │▝▘ ▀▗▖ layer 0 │
││ ▚ layer 0 │ ▝▗▖ │
││ │ ▚ │
0.50┤└───────────┘ ▝▖ │
│ ▗▝ ▚ │
│ ▖▘ ▝▖ │
│ ▗▝ ▝▗ │
│ ▘ ▘▖ │
0.00┤▝ ▝▗ ▗▘│
│ ▘▖ ▖▘ │
│ ▝▖ ▗▝ │
│ ▚ ▖▘ │
-0.50┤ ▝▖ ▗▝ │
│ ▚ ▗▘▘ │
│ ▝▘▖ ▗▖▘ │
│ ▝▘▄ ▗▖▀ │
-1.00┤ ▝▘▀▝▘▀▘▀ │
└┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬┘
0.0 1.0 2.1 3.1 4.2 5.2 6.3
print(canvas.render(backend="plotext", layers=[1]).build(keep_colors=False))
Layers 0 and 1 only
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤┌───────────┐ ▗▗▖▄▖│
││ │ layer 1 ▗▗▘▘ │
││ ▚ layer 1 │ ▗▘▘ │
││ │ ▖▘ │
0.50┤└───────────┘ ▄▝ │
│ ▝▖ ▗▘ │
│ ▚ ▞ │
│ ▗ ▖ │
│ ▚ ▞ │
0.00┤ ▝▖ ▗▘ │
│ ▚ ▞ │
│ ▝▖ ▗▘ │
│ ▚ ▞ │
-0.50┤ ▝▖ ▗▘ │
│ ▝▗▖ ▗▖▘ │
│ ▚ ▞ │
│ ▝▘▄▗ ▖▄▝▘ │
-1.00┤ ▀▝▘▀▝▘▀ │
└┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬┘
0.0 1.0 2.1 3.1 4.2 5.2 6.3
print(canvas.render(backend="plotext", layers=[0, 1]).build(keep_colors=False))
Layers 0 and 1 only
┌─────────────────────────────────────────────────────────────────────────┐
1.00┤┌───────────┐ ▄▗▄▗▖▄▗▖ ▗▗▖▄▖│
││ │▝▘ ▀▗▖ layer 0 ▗▗▘▘ │
││ ▚ layer 0 │ ▝▗▖ ▗▘▘ │
││ │ ▚ layer 1 ▖▘ │
0.50┤│ ▚ layer 1 │ ▝▖ ▄▝ │
││ │▝▖ ▚ ▗▘ │
│└───────────┘ ▚ ▝▖ ▞ │
│ ▗▝ ▗ ▝▗ ▖ │
│ ▘ ▚ ▘▖ ▞ │
0.00┤▝ ▝▖ ▝▗ ▗▘ ▗▘│
│ ▚ ▘▖ ▞ ▖▘ │
│ ▝▖ ▝▖ ▗▘ ▗▝ │
│ ▚ ▚ ▞ ▖▘ │
-0.50┤ ▝▖ ▝▖ ▗▘ ▗▝ │
│ ▝▗▖ ▚▗▖▘ ▗▘▘ │
│ ▚ ▞▝▘▖ ▗▖▘ │
│ ▝▘▄▗ ▖▄▝▘ ▝▘▄ ▗▖▀ │
-1.00┤ ▀▝▘▀▝▘▀ ▝▘▀▝▘▀▘▀ │
└┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬┘
0.0 1.0 2.1 3.1 4.2 5.2 6.3

Subplots are fully supported, including figure-level titles via canvas.suptitle(...).

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.render(backend="plotext").build(keep_colors=False))
Terminal dashboard
Signals Counts
┌─────────────────────────────────┐ ┌───────────────────────────────────┐
1.00┤┌──────────┐ ▗▄▖│7.0┤┌──────────┐ ██▖██ │
││ │ sin(x) ▗▘ │ ││ │ █████count │
││ ▚ sin(x) │▖ ▞▘ │ ││ █ count │ █████ │
││ │▝▘ cos(x)▘ │ ││ │ █████points │
0.50┤│ ▚ cos(x) │ ▚ ▗▘ │5.2┤│ ▚ points │ █████ │
││ │ ▚ ▖ │ ││ │ ████████▘█████▘██│
│└──────────┘ ▌ ▗▘ │ │└──────────┘ █████████████████│
│▗▘ ▝ ▗ ▘ │ │████████████ █████████████████│
0.00┤▗ ▚ ▚ ▞ ▖│3.5┤████████████ █████████████████│
│ ▌ ▘ ▐ ▗▘│ │████████████ █████████████████│
│ ▗ ▐ ▖ ▘ │ │████████████ █████████████████│
│ ▚ ▚ ▞ ▞ │ │██████████████▗██ █████████████████│
-0.50┤ ▌ ▚ ▐ ▗▘ │1.8┤█████████████████ █████████████████│
│ ▝▖ ▗▄▘ ▗▘ │ │█████████████████ █████████████████│
│ ▝▖ ▗▀▖ ▌ │ │█████████████████ █████████████████│
│ ▀▄ ▄▀ ▐▖ ▄▀ │ │█████████████████ █████████████████│
-1.00┤ ▀▀▀ ▝▀▀▀▘ │0.0┤█████████████████ █████████████████│
└┬────┬─────┬────┬────┬─────┬────┬┘ └──┬─────┬─────┬─────┬─────┬─────┬──┘
0.0 1.0 2.1 3.1 4.2 5.2 6.3 0 1 2 3 4 5
value x value bin

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.

data = np.arange(1, 26).reshape(5, 5)
canvas = Canvas()
canvas.imshow(data)
canvas.set_title("Matrix-style imshow")
canvas.set_xlabel("column")
canvas.set_ylabel("row")
print(canvas.render(backend="plotext").build(keep_colors=False))
Matrix-style imshow
┌─────────────────────────────────────────────────────────────────────────────┐
4┤█ █ █ █ █│
│ │
│ │
│ │
3┤█ █ █ █ █│
│ │
│ │
│ │
2┤█ █ █ █ █│
│ │
│ │
│ │
1┤█ █ █ █ █│
│ │
│ │
│ │
0┤█ █ █ █ █│
└┬────────────┬───────────┬────────────┬────────────┬───────────┬────────────┬┘
0.0 0.7 1.3 2.0 2.7 3.3 4.0
row column

12 · Patches and arbitrary patch geometry

Section titled “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.Rectangle
  • matplotlib.patches.Circle
  • matplotlib.patches.Polygon
  • matplotlib.patches.Ellipse
  • many other patch types that expose a usable matplotlib path

That is enough for many annotations, regions of interest, and geometric callouts.

canvas = Canvas()
canvas.add_patch(
mpatches.Rectangle(
(0.2, 0.2), 1.3, 0.7, fill=False, edgecolor="yellow", label="window"
)
)
canvas.add_patch(
mpatches.Circle((2.2, 1.6), 0.45, fill=False, edgecolor="cyan", label="sensor")
)
canvas.add_patch(
mpatches.Polygon(
[[3.0, 0.5], [3.8, 1.2], [3.4, 2.0]],
fill=True,
facecolor="green",
label="region",
)
)
canvas.add_patch(
mpatches.Ellipse(
(2.8, 1.0), 0.8, 0.5, fill=False, edgecolor="white", label="ellipse"
)
)
canvas.set_xlim(0, 4.5)
canvas.set_ylim(0, 2.5)
canvas.set_title("Supported patch types")
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
Supported patch types
┌───────────────────────────────────────────────────────────────────────────┐
2.5┤┌───────────┐ │
││ │ window │
││ ▚ window │ │
││ │ ▗ ▗ ▗ ▗ ▗ sensor │
1.9┤│ ▚ sensor │ ▘ ▝ region │
││ │ ▖▘ ▘▖ ellipse │
││ ▚ region │ ▖ ▖ │
││ │ ▘ ▘ │
││ ▚ ellipse │ ▘▖ ▗ ▘ │
1.2┤│ │ ▝ ▗ ▗ ▗ ▐▝ ▘▝ ▝ ▘▝ ▘▖ ▝ │
│└───────────┘ ▐ ▌ │
│ ▝ ▘ ▝▗▗ ▖▖▘ │
│ ▘▝ ▝ ▘▝ │
0.6┤ │
│ ▝ │
│ │
│ ▝ ▘ │
0.0┤ │
└┬───────────┬────────────┬───────────┬───────────┬────────────┬───────────┬┘
0.0 0.8 1.5 2.2 3.0 3.8 4.5

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.

# Interactive rendering of the same canvas
canvas.show(backend="plotly")

13 · Captions, symlog scales, aspect, and colorbar notes

Section titled “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') and set_yscale('symlog')
  • set_aspect(...) with a best-effort terminal plotsize approximation
  • add_colorbar(...) as a note-style value summary for matrix/image output
x = np.linspace(-20, 20, 161)
canvas = Canvas()
canvas.plot(x, x**3, color="cyan", label="x^3")
canvas.set_title("Symlog example")
canvas.add_caption("caption text")
canvas.set_xscale("symlog")
canvas.set_yscale("symlog")
canvas.set_aspect("equal")
canvas.set_legend(True)
print(canvas.render(backend="plotext").build(keep_colors=False))
Symlog example
┌──────────────────────────────────┐
3.9┤┌───────┐ ▄▖│
││ │ ▄▟▀▘ │
││ ▚ x^3 │ ▗▄▀▘ │
2.0┤│ │ ▗▞▀ │
│└───────┘ ▄▀▘ │
│ ▖▖▄▀ │
0.0┤ ▄▀▝▝ ▘ │
│ ▗▄▀ │
-2.0┤ ▄▞▘ │
│ ▗▄▀▘ │
│ ▗▄▛▀ │
-3.9┤▝▀ │
└┬─────┬──────────┬────┬────┬──────┘
-1.32 -0.88 0.00 0.44 0.88
heat = np.arange(1, 26).reshape(5, 5)
canvas = Canvas()
canvas.imshow(heat)
canvas.add_colorbar(label="intensity")
canvas.set_title("Matrix + colorbar note")
print(canvas.render(backend="plotext").build(keep_colors=False))
Matrix + colorbar note | intensity: 1..25
┌─────────────────────────────────────────────────────────────────────────────┐
4┤█ █ █ █ █│
│ │
│ │
│ │
3┤█ █ █ █ █│
│ │
│ │
│ │
│ │
2┤█ █ █ █ █│
│ │
│ │
│ │
1┤█ █ █ █ █│
│ │
│ │
│ │
0┤█ █ █ █ █│
└┬────────────┬───────────┬────────────┬────────────┬───────────┬────────────┬┘
0.0 0.7 1.3 2.0 2.7 3.3 4.0

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 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.

canvas.show(backend="plotly")
x = np.linspace(0, 2 * np.pi, 60)
canvas = Canvas()
canvas.plot(x, np.sin(x), color="cyan", label="sin(x)")
canvas.set_title("Saved terminal figure")
terminal_fig = canvas.render(backend="plotext")
output_path = Path("plotext_output.txt")
terminal_fig.savefig(output_path)
print(output_path.read_text().splitlines()[0])
Saved terminal figure

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.

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 = Canvas()
canvas.plot(x, np.sin(x + phase), color="cyan", label="sin(x + phase)")
canvas.plot(x, np.cos(x + phase), color="yellow", label="cos(x + phase)")
canvas.set_ylim(-1.2, 1.2)
canvas.set_title(f"Animated phase = {phase:.2f}")
canvas.set_legend(True)
clear_output(wait=True)
print(canvas.render(backend="plotext").build(keep_colors=False))
time.sleep(0.08)
Animated phase = 6.28
┌──────────────────────────────────────────────────────────────────────────┐
1.2┤┌──────────────────┐ │
││ │▖ sin(x + phase) ▄▖│
││ ▚ sin(x + phase) │▝▀▝▚▖▖ ▄▗▀▘▀ │
││ │ ▝▚▖ cos(x + phase)▗▀ │
0.6┤│ ▚ cos(x + phase) │ ▀▄ ▖▞▘ │
││ │ ▝▄ ▗▞ │
│└──────────────────┘ ▘▄ ▗▖▘ │
│ ▄▝ ▝▖▖ ▚ ▗▗▘ │
│ ▞ ▝▖ ▘▚ ▗▘ │
0.0┤▝ ▝▝▖ ▚▗ ▗▘▘ ▞▘│
│ ▝▄ ▚ ▄▘ ▖▀ │
│ ▝▄ ▀▗ ▄▘ ▗▞ │
│ ▘▄ ▀▖ ▄▝ ▗▞ │
-0.6┤ ▚▖ ▀▄▗▞ ▗▝▘ │
│ ▀▄ ▄▀▝▚▖ ▗▖▞▘ │
│ ▝▀▖▄▄ ▄▄▗▀▘ ▝▝▚▖▄▖ ▗▄▖▀▘ │
│ ▘▀▀▝ ▝▘▀▀ │
-1.2┤ │
└┬───────────┬───────────┬────────────┬───────────┬───────────┬───────────┬┘
0.0 1.0 2.1 3.1 4.2 5.2 6.3
FeatureStatusNotes
add_colorbar()⚠️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.