Skip to main content

Inline matplotlib plots in your terminal via sixel, in a tmux pane, over SSH

Project description

plotty

CI security: pip-audit Python License: MIT

Inline matplotlib plots in your terminal — rendered as sixel in a dedicated tmux pane, including over SSH. No browser, no X11, no Jupyter server.

plotty demo

plotty is a matplotlib backend that draws figures directly in your terminal, so a tmux + ipython (+ nvim) workflow shows plots the way a Jupyter or VS Code notebook does. Activate it once and your figures appear in a tmux pane next to your REPL — locally or on a remote machine over SSH. It's inspired by and the Python analogue of MuxDisplay.jl.

import plotty
plotty.enable()

import matplotlib.pyplot as plt
plt.plot([1, 4, 9, 16])     # shows up in the plot pane

Why / when to use it

If you do interactive analysis in a terminal — ipython inside tmux, editing in nvim, frequently SSH'd into a remote box — you normally lose inline plots: plt.show() wants a GUI and Jupyter wants a browser. plotty fills that gap and covers three setups:

  • Local tmux. Run your REPL in one pane; plots render in another.
  • Remote over SSH. Run everything on the remote inside tmux. Only the rendered sixel bytes cross the wire (drawn by your local terminal); the control plane — signals, pidfile, image hand-off — stays host-local, so it behaves exactly like a local session.
  • Nested tmux (local tmux → ssh → remote tmux). Supported with a small, one-time tmux config change — see Nested tmux.

Requirements

Python ≥ 3.7
tmux 3.4, built with sixel support (--enable-sixel)
Terminal a sixel-capable terminal for display — e.g. WezTerm, foot, Konsole, xterm -ti vt340
Python deps matplotlib (and numpy, which ships with matplotlib) — that's all

Check tmux:

tmux -V                                                   # need >= 3.4
strings "$(command -v tmux)" | grep -qi sixel && echo "sixel: yes" || echo "sixel: MISSING"

Not in tmux? plotty falls back to writing sixel straight to your terminal's stdout, so it still works in any sixel-capable terminal without tmux.

Install

plotty installs with uv (which indexes PyPI) or pip:

uv add plotty            # add to your project (resolved + locked)
# or
uv pip install plotty    # into the active environment
# or
pip install plotty

From source:

git clone https://github.com/xuesoso/plotty && cd plotty
uv pip install .

Quick start

import plotty
plotty.enable()                 # auto-detect a renderer, target the last tmux pane,
                                # and spawn a tiny viewer there

import matplotlib.pyplot as plt
plt.plot([1, 4, 9, 16])
# IPython: the figure appears automatically after each cell.
# Plain REPL: call plt.show().

plotty.disable()                # stop the viewer and restore matplotlib

Inside tmux, plotty draws into the last pane of the current window by default, so split a pane first (Ctrl-b "), then call enable(). Target another pane with enable(target_pane=...).

Public API: enable(), disable(), redraw(), view().

Demo

Run the bundled example to see it in action (split off a plot pane first, then python examples/demo.py). The GIF below is the expected output:

python examples/demo.py

plotty rendering the examples/demo.py plots in a tmux pane

How it works

Two cooperating pieces share state via the filesystem + OS signals:

  • Backend (module://plotty, runs in your REPL): on each figure it saves a PNG, atomically publishes it to ~/.cache/plotty/last.png, and signals the viewer.
  • Viewer (runs in the plot pane): redraws on a new figure (SIGUSR1) and on pane resize/zoom (SIGWINCH). It's event-driven (signal.pause()), idle at zero CPU, and self-cleaning.

Because only sixel bytes cross SSH and everything else is host-local, remote use is identical to local.

Display modes

  • Viewer mode (default in tmux) — a small viewer process lives in the target pane and redraws on new figures and on pane resize/zoom. Recommended; it's the mode that survives resizing.
  • Inline mode (default outside tmux, or enable(inline=True)) — the backend renders sixel itself, with no helper process, and writes it to the target pane's tty (in tmux) or to your stdout (no tmux). It does not auto-redraw on resize.
plotty.enable(inline=True)      # force inline even inside tmux

Sixel encoders

plotty ships with a built-in, dependency-free sixel encoder (pure stdlib + numpy), so it works out of the box with no external tools.

If one is on your PATH, plotty auto-detects an external encoder for higher-quality (dithered) output, in priority order:

  1. chafa — recommended
  2. img2sixel (libsixel)
  3. ImageMagick (magick / convert)

Force the built-in encoder regardless of what's installed:

plotty.enable(imgcat="builtin")     # or:  PLOTTY_IMGCAT=builtin

plotty is sixel-only by design — sixel is the only path that survives tmux and SSH. Non-sixel terminal-image protocols (kitty / iTerm) are not used. A custom non-sixel imgcat= may be passed but will warn that it may not display over SSH.

tmux configuration

plotty works with no config on a single tmux as long as tmux is ≥ 3.4 with sixel and your terminal supports sixel (i.e. Wezterm, iTerm2, xterm, xfce term, VSCode). Reference Are We Sixel Yet? for a complete list. If plots don't appear (or you see raw escape-sequence junk instead of an image), tmux hasn't recognized that your terminal can render sixel — its auto-detection isn't always reliable, especially over SSH. Tell it explicitly in ~/.tmux.conf:

set -as terminal-features ',*:sixel'

Nested tmux (local + remote)

A common remote setup is a tmux inside a tmux:

local terminal → local tmux → ssh → remote tmux → REPL + plot pane

For the image to flow all the way out, every tmux layer must render and forward the sixel — which means setting the feature on both the local and the remote tmux:

# add to ~/.tmux.conf on BOTH the local laptop and the remote machine
set -as terminal-features ',*:sixel'

Without this, the inner (remote) tmux doesn't know to forward sixel and the raw escape sequence leaks through as garbage characters. Verify a layer sees the feature with:

tmux display-message -p '#{client_termfeatures}'   # should contain "sixel"

Both tmux layers must be ≥ 3.4 and built with sixel.

Configuration reference

enable() arguments (each has an environment-variable default):

argument env var default meaning
target_pane PLOTTY_PANE -1 tmux pane for the plot; negative indexes from the end (-1 = last)
size PLOTTY_SIZE 60 display width in terminal cells
dpi PLOTTY_DPI matplotlib default savefig DPI of the source image (raise it for sharper plots at large size)
imgcat PLOTTY_IMGCAT auto renderer command; "builtin" forces the built-in encoder
inline PLOTTY_INLINE auto True/False to force inline vs viewer-pane mode
clear PLOTTY_CLEAR True clear the pane before each draw
close PLOTTY_CLOSE True close figures after display
tmux PLOTTY_TMUX tmux tmux binary to use
viewer True spawn the viewer process (tmux mode)
verbose 1 print startup health-check warnings
PLOTTY_CACHE ~/.cache/plotty state directory (last.png, pidfile)

size and dpi are independent: size is how wide the image is displayed, dpi is how many pixels the source has. For a crisp image at a large size, raise dpi so the source has enough pixels.

Troubleshooting

  • Garbage / +++ instead of an image: a tmux layer isn't forwarding sixel. Add set -as terminal-features ',*:sixel' to that layer (both layers if nested) and confirm tmux ≥ 3.4 with sixel.
  • Nothing appears: check tmux -V ≥ 3.4 and sixel support (strings $(command -v tmux) | grep -i sixel); confirm your terminal supports sixel; run plotty.enable(verbose=1) to print diagnostics.
  • Image too large / small: tune size. Blurry when enlarged? raise dpi.
  • Plot doesn't refresh when you resize the pane: use viewer mode (the default in tmux); inline mode doesn't auto-redraw on resize.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plotty-1.0.0.tar.gz (20.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

plotty-1.0.0-py3-none-any.whl (15.0 kB view details)

Uploaded Python 3

File details

Details for the file plotty-1.0.0.tar.gz.

File metadata

  • Download URL: plotty-1.0.0.tar.gz
  • Upload date:
  • Size: 20.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for plotty-1.0.0.tar.gz
Algorithm Hash digest
SHA256 bedd1aeddd89218d802812c088ec328987eb83594f9ef2cef7d04f04f9958e18
MD5 20ef1aff16174e2afffbb3bbe2af6b5b
BLAKE2b-256 b8ed7e9848f040755799fa5476f4b8c92a509b5431fd629796ed29539f3db171

See more details on using hashes here.

File details

Details for the file plotty-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: plotty-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 15.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for plotty-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 66ee8763b08092e4e21d0bbe7ff168d733870e2fd45bd61df7260b9f2f4a576a
MD5 70f2b1b3f6c5e7e828d896785588f323
BLAKE2b-256 e730e890f5b032342127172cb5ce8a2d75f4f6a7480f9d078c7dabcd80f54f6e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page