mplify
MatPlotLib prettifier. One function, mplp() (MPLP: MatPlotLib Prettify, or Make Plot Pretty), that turns a matplotlib plot into a great v1 figure ready for your slides, poster, or paper.
import matplotlib.pyplot as plt
from mplify import mplp
plt.plot(x, y)
mplp() # applies to the last figure and axis
The problem
Matplotlib is very highly customizable. That is the problem.
Say you want to rotate your x tick labels 30°, right-align them so they don't collide with the axis, bump the axis label font, and drop the top and right spines. Four small, obvious, universally wanted things. Here is matplotlib:
ax.set_xticks(positions)
ax.set_xticklabels(labels, rotation=30, ha='right', fontsize=16, fontweight='regular')
ax.set_xlabel('Condition', size=18, labelpad=0)
ax.set_ylabel('Response', size=18, labelpad=0)
ax.tick_params(axis='both', width=1, length=4, direction='out',
bottom=True, left=True, top=False, right=False)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
for sp in ('left', 'bottom'):
ax.spines[sp].set_lw(1)
Five different APIs (set_xticklabels, set_xlabel, tick_params, spines), three different spellings of the same concept (fontsize, size, fontweight/weight), and potential order-related bugs: ticks and limits interact, so calling them in the wrong order silently gives you a different figure.
To achieve the desired result, the knobs exist, but they're scattered across a documentation surface large enough that most of us end up doing one of three things:
- copy-pasting the same 15 lines of boilerplate into every script;
- re-googling "matplotlib rotate xticklabels" for the 200th time;
- asking an LLM, which returns 40 lines of code, half of it redundant, and all of it subtly different from the 40 lines it gave you last week.
The solution
mplp() is one callable with a flat, self-explanatory argument list. Everything
above becomes:
mplp(xticks=positions, xtickslabels=labels, xtickrot=30, xtickha='right', xlabel='Condition', ylabel='Response')
Three things make this work:
Sensible defaults. Call mplp() with no arguments and it applies a handcrafted default styling: larger font sizes, fatter spines, top and right spines gone, ticks pointing out, editable text in saved PDFs.
Implicitly callable. mplp() reads the current figure via plt.gcf()/plt.gca(), so you can simply call it at the end of your script, whether you're using matplotlib in explicit (object-oriented) or implicit (without declaring figures and axis, MATLAB-inherited) mode. But you can always feed fig and ax to mplp explicitly.
One flat layer of arguments. All the common figure tweaks are a self-explanatory keyword that can be remembered through checking the arguments of mplp(): xtickrot, ticklab_s, hide_top_right, hlines, clabel, legend_loc, saveFig. Anything you don't pass keeps mplify's default; anything you do pass takes precedence.
And it stays out of your way: mplp() edits the axis you hand it and nothing else — no style sheet to install, no rcParams rewritten mid-script, no surprises in the next figure. (The one exception is deliberate: importing mplify sets pdf/ps/svg font types to keep text editable in saved vector files. See Saving.)
Install
pip install mplify # or: uv add mplify
From source:
git clone https://github.com/m-beau/mplify.git
cd mplify && uv sync
Requires Python ≥ 3.10, matplotlib, numpy.
Tour
Left panel is matplotlib's default in every figure below. Right panel is one
mplp() call. Full runnable versions of all of these live in
quickstart.ipynb.
Limits, ticks and labels, in the right order
mplp applies limits before ticks (and re-applies them after), so you never have
to remember which call comes first.
mplp(xlim=(0, 8), ylim=(-0.5, 1),
xticks=[0, 2, 4, 6, 8], yticks=[-0.5, 0, 0.5, 1],
xlabel='Time (s)', ylabel='Amplitude (a.u.)')
Tick labels: text, rotation, alignment
mplp(xticks=range(4), xtickslabels=categories, xtickrot=30, xtickha='right')
Reference lines
mplp(hlines=[0], vlines=[np.pi, 2*np.pi],
lines_kwargs={'lw': 2, 'ls': ':', 'color': 'grey'})
Good-looking colorbars
plt.colorbar() steals space from the parent axes, so a row of subplots ends up
with panels of different widths (and one of them mysteriously narrower than its
neighbours). mplify's colorbar is an inset anchored to the axis: the data area
keeps the exact size you gave it.
mplp(colorbar=True, vmin=-3, vmax=3, cmap='RdBu_r',
clabel='Z-score', cticks=[-2, 0, 2])
Exotic colormaps
If your data span −2 to 5, cmap='RdBu_r' puts white at 1.5. Half your
"blue" values are positive numbers. center=0 re-anchors the colormap so white
means zero, without clipping the range.
ax.imshow(data, vmin=-2, vmax=5, cmap=get_bounded_cmap('RdBu_r', -2, 0, 5))
mplp(colorbar=True, cmap='RdBu_r', vmin=-2, center=0, vmax=5)
Scalebars instead of axes
For traces where the absolute values are meaningless but the scale isn't — ephys, imaging, anything with a time base.
mplp(hide_axis=True,
xscalebar=5, yscalebar=200,
xscalebar_unit=' ms', yscalebar_unit=' μV')
size= — one figure, three media
The most common figure reformatting need: scaling a figure's "metadata" with respect to its data for different media. size rescales fonts, spine widths, tick widths, colorbar thickness and scalebar text for the viewing distance..
mplp(size='paper') # or 'slide' (default), 'poster'
mplp(size='xs') # or 's', 'm', 'l', 'xl', 'xxl'
Bonus: color families for nested designs
Genotype × dose, region × condition, subject × session. One hue per group, one shade within it — so the structure of the design is visible without reading the legend.
families = get_color_families(ncolors=3, nfamilies=3, cmapstr='viridis')
Plus the usual conveniences:
get_ncolors_cmap(8, 'viridis') # N evenly spaced colors from any colormap
to_hex((70, 130, 180)) # accepts 0-1 or 0-255, hex, names, 'r'
html_palette(colors) # preview swatches inline in a notebook
Everything at once
mplp(xlabel='Feature 1', ylabel='Feature 2',
colorbar=True, vmin=c.min(), vmax=c.max(), cmap='magma', clabel='F1 + F2',
hlines=[y.mean()], vlines=[x.mean()],
lines_kwargs={'lw': 1, 'ls': '--', 'color': 'grey', 'zorder': -1})
Four lines. Just to bring the point home, here is the raw matplotlib code that would be needed to produces the exact same panel (i.e. that an LLM would provide):
### Raw matplotlib code - much more verbose..!
import numpy as np
from matplotlib.font_manager import FontProperties
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
# labels, title, fonts
ax.set_xlabel('Feature 1', size=18, weight='regular', labelpad=0, fontname='Arial')
ax.set_ylabel('Feature 2', size=18, weight='regular', labelpad=0, fontname='Arial')
ax.set_title('by hand', size=20, weight='regular')
# tick labels — set_ticks() first, or matplotlib warns and may mislabel them
fig.canvas.draw()
xticks, yticks = ax.get_xticks(), ax.get_yticks()
ax.set_xticks(xticks)
ax.set_xticklabels([f'{t:g}' for t in xticks], fontsize=16, fontweight='regular',
color=(0, 0, 0), rotation=0, ha='center', va='top', fontname='Arial')
ax.set_yticks(yticks)
ax.set_yticklabels([f'{t:g}' for t in yticks], fontsize=16, fontweight='regular',
color=(0, 0, 0), rotation=0, ha='right', va='center', fontname='Arial')
ax.set_xlim(xlim); ax.set_ylim(ylim) # ticks just widened your limits. put them back
# spines and ticks
ax.tick_params(axis='both', bottom=1, left=1, top=0, right=0,
width=1, length=4, direction='out')
for sp in ('left', 'bottom'):
ax.spines[sp].set_lw(1)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# reference lines
ax.axhline(y=y.mean(), lw=1, ls='--', color='grey', zorder=-1)
ax.axvline(x=x.mean(), lw=1, ls='--', color='grey', zorder=-1)
# a colorbar that doesn't steal width from the axis
cax = inset_axes(ax, width='3%', height='40%', loc='lower right',
bbox_to_anchor=(0.04, 0, 1, 1), bbox_transform=ax.transAxes,
borderpad=0)
sm = plt.cm.ScalarMappable(cmap='magma', norm=plt.Normalize(c.min(), c.max()))
sm.set_array([])
fig.colorbar(sm, cax=cax, ax=ax, orientation='vertical', label='F1 + F2')
cticks = np.arange(20, 141, 20)
cax.yaxis.set_ticks(cticks)
cax.yaxis.set_ticklabels([f'{t:g}' for t in cticks], ha='left')
cax.yaxis.set_tick_params(pad=5, labelsize=16)
cax.yaxis.label.set_font_properties(FontProperties(weight='regular', size=18))
cax.yaxis.label.set_rotation(-90)
cax.yaxis.label.set_va('bottom')
cax.yaxis.label.set_ha('center')
cax.yaxis.labelpad = 5
# line up labels across subplots, white figure background
fig.align_xlabels(fig.axes)
fig.align_ylabels(fig.axes)
fig.patch.set_facecolor('white')
41 lines, three imports, six APIs, two footguns (set_ticklabels before
set_ticks warns and can silently mislabel your axis; setting ticks quietly
widens your limits, so you have to restore them afterwards).
prettify=False — surgical mode
Sometimes you've already got a figure you like and you want to change exactly one
thing. prettify=False applies only what you pass and leaves everything else
alone.
mplp(prettify=False, hide_top_right=True)
Cheat sheet
| Argument | |
|---|---|
| Figure / axis size (inches) | figsize=(w, h), axsize=(w, h) |
| Scale text for medium | size='paper' / 'slide' / 'poster' (also xs–xxl) |
| Limits | xlim, ylim |
| Tick positions | xticks, yticks, reset_xticks, reset_yticks |
| Tick label text | xtickslabels, ytickslabels |
| Tick label rotation / alignment | xtickrot, ytickrot, xtickha, xtickva, ytickha, ytickva |
| Font sizes | title_s, axlab_s, ticklab_s, clabel_s, cticks_s |
| Font weights | title_w, axlab_w, ticklab_w, clabel_w |
| Font family | font_family |
| Labels / title | xlabel, ylabel, title, xlabelpad, ylabelpad |
| Spines | lw, hide_top_right, hide_axis |
| Tick direction | ticks_direction='in' / 'out' |
| Legend | show_legend, hide_legend, legend_loc=(x, y) |
| Colorbar | colorbar=True, vmin, vmax, cmap, center, clabel, cticks, ctickslabels, cbar_w, cbar_h, cbar_pad, clim |
| Reference lines | hlines, vlines, lines_kwargs |
| Scalebars | xscalebar, yscalebar, xscalebar_unit, yscalebar_unit, scalebarkwargs |
| Subplot spacing | hspace, wspace, tight_layout |
| Label alignment across subplots | align_x_labels, align_y_labels |
| Transparent background | transparent_background=True |
| Save | saveFig=True, saveDir, figname, _format |
| Change only what I pass | prettify=False |
Helpers exported alongside mplp
| Function | Does |
|---|---|
get_bestticks(start, end, step=None, light=False) |
Ticks on round numbers (1 / 5 / 10 steps) |
get_bestticks_from_array(arr, ...) |
Same, from data |
get_labels_from_ticks(ticks) |
Consistently formatted tick label strings |
sci_notation(1.23e6, 2) |
1.23·10⁶ as mathtext |
get_cmap, get_bounded_cmap, get_ncolors_cmap, get_color_families |
Colormaps and palettes |
to_rgb, to_hex, to_hsv, html_palette |
Color conversion and preview |
add_colorbar(fig, ax, ...) |
The size-preserving colorbar, standalone |
plot_scalebar(ax, ...) |
Scalebars, standalone |
set_ax_size(ax, w, h) |
Exact axis dimensions in inches |
save_mpl_fig(fig, name, dir, fmt) |
Save with Type-42 (editable) text |
Saving
mplp(saveFig=True, saveDir='./figures', figname='fig2b', _format='pdf')
Saves at 500 dpi with pdf.fonttype = 42, i.e. text stays text. You can open the PDF in Illustrator/Inkscape and fix your typo without
re-running the analysis (You will. There is always a label to fix.)
Changing the defaults
mplify's defaults live in one hand-editable file,
src/mplify/DEFAULT_PARAMS.py: default_mplp_params for the base style, SIZE_PRESETS for the paper/slide/poster xs/s/m/l/xl/xxl defaults.
Edit it and your next mplp() call picks the change up immediately — the file is re-read from disk whenever its mtime changes. No kernel restart or %autoreload needed.
from mplify import default_mplp_params, SIZE_PRESETS # snapshots, for inspection
Not a style sheet, not a wrapper
- Not a style sheet. Style sheets set global
rcParamsacross all figures; they can't rotate specific tick labels or put a colorbar on a specific axis. mplify operates per-axis, at call time, after your data is plotted. - Not a plotting wrapper. mplify never draws your data. You keep
ax.plot,ax.imshow, seaborn, whatever you already use (if it's built on top of matplotlib, of course).
Development
uv sync # editable install into .venv
uv run python doc/make_figures.py # regenerate the README figures into doc img/
The full gallery is quickstart.ipynb — open it in your editor of choice and point the kernel at .venv.
Related
NeuroPyxels — Neuropixels data analysis, where this codebase slowly grew up since 2016.
Metadata
Release files for mplify 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mplify-1.0.1.tar.gz | 20.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mplify-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.8 kB
Release files / mplify-1.0.1.tar.gz
| Download URL | mplify-1.0.1.tar.gz |
|---|---|
| Size | 20.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e52eed2703873a688dc5799d87ccfd7f89084768bc40926907af21cfacc4490a
|
|
BLAKE2b-256 checksum How to use checksums |
289256128574820444b5d44a414f4b5af99a4896ea6624add051211d5f879244
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / mplify-1.0.1-py3-none-any.whl
| Download URL | mplify-1.0.1-py3-none-any.whl |
|---|---|
| Size | 24.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3b06b0b660d22703b2a420768ebf47e2f6e414f16370410661e33bae5776492a
|
|
BLAKE2b-256 checksum How to use checksums |
1a88ddecdf3637110e03391da9a7a0f20dab27b30064aaed933ffc0832a2097b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|