mplify 1.0.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.
mplify-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,380 @@
1
+ Metadata-Version: 2.3
2
+ Name: mplify
3
+ Version: 1.0.0
4
+ Summary: MatPlotLib Prettifier - make plots pretty with a single function call
5
+ Author: m-beau
6
+ Author-email: m-beau <maximebeaujeanroch047@gmail.com>
7
+ Requires-Dist: ipykernel>=7.3.0
8
+ Requires-Dist: matplotlib
9
+ Requires-Dist: numpy
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+
13
+ # mplify
14
+
15
+ **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.
16
+
17
+ ```python
18
+ import matplotlib.pyplot as plt
19
+ from mplify import mplp
20
+
21
+ plt.plot(x, y)
22
+ mplp() # applies to the last figure and axis
23
+ ```
24
+
25
+ <p align="center">
26
+ <img src="doc/img/01_hero.png" width="100%" alt="matplotlib defaults vs mplp()">
27
+ </p>
28
+
29
+ ---
30
+
31
+ ## The problem
32
+
33
+ Matplotlib is very highly customizable. That is the problem.
34
+
35
+ Say you want to rotate your x tick labels 30°, right-align them so they don't
36
+ collide with the axis, bump the axis label font, and drop the top and right
37
+ spines. Four small, obvious, universally wanted things. Here is matplotlib:
38
+
39
+ ```python
40
+ ax.set_xticks(positions)
41
+ ax.set_xticklabels(labels, rotation=30, ha='right', fontsize=16, fontweight='regular')
42
+ ax.set_xlabel('Condition', size=18, labelpad=0)
43
+ ax.set_ylabel('Response', size=18, labelpad=0)
44
+ ax.tick_params(axis='both', width=1, length=4, direction='out',
45
+ bottom=True, left=True, top=False, right=False)
46
+ ax.spines['top'].set_visible(False)
47
+ ax.spines['right'].set_visible(False)
48
+ for sp in ('left', 'bottom'):
49
+ ax.spines[sp].set_lw(1)
50
+ ```
51
+
52
+ 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.
53
+
54
+ 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:
55
+
56
+ 1. copy-pasting the same 15 lines of boilerplate into every script;
57
+ 2. re-googling "matplotlib rotate xticklabels" for the 200th time;
58
+ 3. 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.
59
+
60
+ ## The solution
61
+
62
+ `mplp()` is one callable with a flat, self-explanatory argument list. Everything
63
+ above becomes:
64
+
65
+ ```python
66
+ mplp(xticks=positions, xtickslabels=labels, xtickrot=30, xtickha='right', xlabel='Condition', ylabel='Response')
67
+ ```
68
+
69
+ Three things make this work:
70
+
71
+ **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.
72
+
73
+ **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.
74
+
75
+ **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.
76
+
77
+ 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](#saving).)
78
+
79
+ ---
80
+
81
+ ## Install
82
+
83
+ ```bash
84
+ pip install mplify # or: uv add mplify
85
+ ```
86
+
87
+ From source:
88
+
89
+ ```bash
90
+ git clone https://github.com/m-beau/mplify.git
91
+ cd mplify && uv sync
92
+ ```
93
+
94
+ Requires Python ≥ 3.10, matplotlib, numpy.
95
+
96
+ ---
97
+
98
+ ## Tour
99
+
100
+ Left panel is matplotlib's default in every figure below. Right panel is one
101
+ `mplp()` call. Full runnable versions of all of these live in
102
+ [`quickstart.ipynb`](quickstart.ipynb).
103
+
104
+ ### Limits, ticks and labels, in the right order
105
+
106
+ `mplp` applies limits before ticks (and re-applies them after), so you never have
107
+ to remember which call comes first.
108
+
109
+ ```python
110
+ mplp(xlim=(0, 8), ylim=(-0.5, 1),
111
+ xticks=[0, 2, 4, 6, 8], yticks=[-0.5, 0, 0.5, 1],
112
+ xlabel='Time (s)', ylabel='Amplitude (a.u.)')
113
+ ```
114
+
115
+ ![limits and ticks](doc/img/02_axes.png)
116
+
117
+ ### Tick labels: text, rotation, alignment
118
+
119
+ ```python
120
+ mplp(xticks=range(4), xtickslabels=categories, xtickrot=30, xtickha='right')
121
+ ```
122
+
123
+ ![rotated tick labels](doc/img/03_ticklabels.png)
124
+
125
+ ### Reference lines
126
+
127
+ ```python
128
+ mplp(hlines=[0], vlines=[np.pi, 2*np.pi],
129
+ lines_kwargs={'lw': 2, 'ls': ':', 'color': 'grey'})
130
+ ```
131
+
132
+ ![reference lines](doc/img/04_lines.png)
133
+
134
+ ### Good-looking colorbars
135
+
136
+ `plt.colorbar()` steals space from the parent axes, so a row of subplots ends up
137
+ with panels of different widths (and one of them mysteriously narrower than its
138
+ neighbours). mplify's colorbar is an inset anchored to the axis: the data area
139
+ keeps the exact size you gave it.
140
+
141
+ ```python
142
+ mplp(colorbar=True, vmin=-3, vmax=3, cmap='RdBu_r',
143
+ clabel='Z-score', cticks=[-2, 0, 2])
144
+ ```
145
+
146
+ ![colorbar](doc/img/05_colorbar.png)
147
+
148
+ ### Exotic colormaps
149
+
150
+ If your data span −2 to 5, `cmap='RdBu_r'` puts white at **1.5**. Half your
151
+ "blue" values are positive numbers. `center=0` re-anchors the colormap so white
152
+ means zero, without clipping the range.
153
+
154
+ ```python
155
+ ax.imshow(data, vmin=-2, vmax=5, cmap=get_bounded_cmap('RdBu_r', -2, 0, 5))
156
+ mplp(colorbar=True, cmap='RdBu_r', vmin=-2, center=0, vmax=5)
157
+ ```
158
+
159
+ ![bounded colormap](doc/img/06_bounded_cmap.png)
160
+
161
+ ### Scalebars instead of axes
162
+
163
+ For traces where the absolute values are meaningless but the scale isn't —
164
+ ephys, imaging, anything with a time base.
165
+
166
+ ```python
167
+ mplp(hide_axis=True,
168
+ xscalebar=5, yscalebar=200,
169
+ xscalebar_unit=' ms', yscalebar_unit=' μV')
170
+ ```
171
+
172
+ ![scalebar](doc/img/07_scalebar.png)
173
+
174
+ ### `size=` — one figure, three media
175
+
176
+ 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..
177
+
178
+ ```python
179
+ mplp(size='paper') # or 'slide' (default), 'poster'
180
+ mplp(size='xs') # or 's', 'm', 'l', 'xl', 'xxl'
181
+ ```
182
+
183
+ ![size presets](doc/img/08_sizes.png)
184
+
185
+ ### Bonus: color families for nested designs
186
+
187
+ Genotype × dose, region × condition, subject × session. One hue per group, one
188
+ shade within it — so the structure of the design is visible without reading the
189
+ legend.
190
+
191
+ ```python
192
+ families = get_color_families(ncolors=3, nfamilies=3, cmapstr='viridis')
193
+ ```
194
+
195
+ ![color families](doc/img/12_color_families.png)
196
+
197
+ Plus the usual conveniences:
198
+
199
+ ```python
200
+ get_ncolors_cmap(8, 'viridis') # N evenly spaced colors from any colormap
201
+ to_hex((70, 130, 180)) # accepts 0-1 or 0-255, hex, names, 'r'
202
+ html_palette(colors) # preview swatches inline in a notebook
203
+ ```
204
+
205
+ ![palettes](doc/img/10_palettes.png)
206
+
207
+ ### Everything at once
208
+
209
+ ```python
210
+ mplp(xlabel='Feature 1', ylabel='Feature 2',
211
+ colorbar=True, vmin=c.min(), vmax=c.max(), cmap='magma', clabel='F1 + F2',
212
+ hlines=[y.mean()], vlines=[x.mean()],
213
+ lines_kwargs={'lw': 1, 'ls': '--', 'color': 'grey', 'zorder': -1})
214
+ ```
215
+
216
+ ![everything](doc/img/09_everything.png)
217
+
218
+ 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):
219
+
220
+ ```python
221
+ ### Raw matplotlib code - much more verbose..!
222
+ import numpy as np
223
+ from matplotlib.font_manager import FontProperties
224
+ from mpl_toolkits.axes_grid1.inset_locator import inset_axes
225
+
226
+ # labels, title, fonts
227
+ ax.set_xlabel('Feature 1', size=18, weight='regular', labelpad=0, fontname='Arial')
228
+ ax.set_ylabel('Feature 2', size=18, weight='regular', labelpad=0, fontname='Arial')
229
+ ax.set_title('by hand', size=20, weight='regular')
230
+
231
+ # tick labels — set_ticks() first, or matplotlib warns and may mislabel them
232
+ fig.canvas.draw()
233
+ xticks, yticks = ax.get_xticks(), ax.get_yticks()
234
+ ax.set_xticks(xticks)
235
+ ax.set_xticklabels([f'{t:g}' for t in xticks], fontsize=16, fontweight='regular',
236
+ color=(0, 0, 0), rotation=0, ha='center', va='top', fontname='Arial')
237
+ ax.set_yticks(yticks)
238
+ ax.set_yticklabels([f'{t:g}' for t in yticks], fontsize=16, fontweight='regular',
239
+ color=(0, 0, 0), rotation=0, ha='right', va='center', fontname='Arial')
240
+ ax.set_xlim(xlim); ax.set_ylim(ylim) # ticks just widened your limits. put them back
241
+
242
+ # spines and ticks
243
+ ax.tick_params(axis='both', bottom=1, left=1, top=0, right=0,
244
+ width=1, length=4, direction='out')
245
+ for sp in ('left', 'bottom'):
246
+ ax.spines[sp].set_lw(1)
247
+ ax.spines['top'].set_visible(False)
248
+ ax.spines['right'].set_visible(False)
249
+
250
+ # reference lines
251
+ ax.axhline(y=y.mean(), lw=1, ls='--', color='grey', zorder=-1)
252
+ ax.axvline(x=x.mean(), lw=1, ls='--', color='grey', zorder=-1)
253
+
254
+ # a colorbar that doesn't steal width from the axis
255
+ cax = inset_axes(ax, width='3%', height='40%', loc='lower right',
256
+ bbox_to_anchor=(0.04, 0, 1, 1), bbox_transform=ax.transAxes,
257
+ borderpad=0)
258
+ sm = plt.cm.ScalarMappable(cmap='magma', norm=plt.Normalize(c.min(), c.max()))
259
+ sm.set_array([])
260
+ fig.colorbar(sm, cax=cax, ax=ax, orientation='vertical', label='F1 + F2')
261
+ cticks = np.arange(20, 141, 20)
262
+ cax.yaxis.set_ticks(cticks)
263
+ cax.yaxis.set_ticklabels([f'{t:g}' for t in cticks], ha='left')
264
+ cax.yaxis.set_tick_params(pad=5, labelsize=16)
265
+ cax.yaxis.label.set_font_properties(FontProperties(weight='regular', size=18))
266
+ cax.yaxis.label.set_rotation(-90)
267
+ cax.yaxis.label.set_va('bottom')
268
+ cax.yaxis.label.set_ha('center')
269
+ cax.yaxis.labelpad = 5
270
+
271
+ # line up labels across subplots, white figure background
272
+ fig.align_xlabels(fig.axes)
273
+ fig.align_ylabels(fig.axes)
274
+ fig.patch.set_facecolor('white')
275
+ ```
276
+
277
+ **41 lines, three imports, six APIs, two footguns** (`set_ticklabels` before
278
+ `set_ticks` warns and can silently mislabel your axis; setting ticks quietly
279
+ widens your limits, so you have to restore them afterwards).
280
+
281
+ ### `prettify=False` — surgical mode
282
+
283
+ Sometimes you've already got a figure you like and you want to change exactly one
284
+ thing. `prettify=False` applies *only* what you pass and leaves everything else
285
+ alone.
286
+
287
+ ```python
288
+ mplp(prettify=False, hide_top_right=True)
289
+ ```
290
+
291
+ ![prettify false](doc/img/11_prettify.png)
292
+
293
+ ---
294
+
295
+ ## Cheat sheet
296
+
297
+ | | Argument |
298
+ |---|---|
299
+ | Figure / axis size (inches) | `figsize=(w, h)`, `axsize=(w, h)` |
300
+ | Scale text for medium | `size='paper' / 'slide' / 'poster'` (also `xs`–`xxl`) |
301
+ | Limits | `xlim`, `ylim` |
302
+ | Tick positions | `xticks`, `yticks`, `reset_xticks`, `reset_yticks` |
303
+ | Tick label text | `xtickslabels`, `ytickslabels` |
304
+ | Tick label rotation / alignment | `xtickrot`, `ytickrot`, `xtickha`, `xtickva`, `ytickha`, `ytickva` |
305
+ | Font sizes | `title_s`, `axlab_s`, `ticklab_s`, `clabel_s`, `cticks_s` |
306
+ | Font weights | `title_w`, `axlab_w`, `ticklab_w`, `clabel_w` |
307
+ | Font family | `font_family` |
308
+ | Labels / title | `xlabel`, `ylabel`, `title`, `xlabelpad`, `ylabelpad` |
309
+ | Spines | `lw`, `hide_top_right`, `hide_axis` |
310
+ | Tick direction | `ticks_direction='in' / 'out'` |
311
+ | Legend | `show_legend`, `hide_legend`, `legend_loc=(x, y)` |
312
+ | Colorbar | `colorbar=True`, `vmin`, `vmax`, `cmap`, `center`, `clabel`, `cticks`, `ctickslabels`, `cbar_w`, `cbar_h`, `cbar_pad`, `clim` |
313
+ | Reference lines | `hlines`, `vlines`, `lines_kwargs` |
314
+ | Scalebars | `xscalebar`, `yscalebar`, `xscalebar_unit`, `yscalebar_unit`, `scalebarkwargs` |
315
+ | Subplot spacing | `hspace`, `wspace`, `tight_layout` |
316
+ | Label alignment across subplots | `align_x_labels`, `align_y_labels` |
317
+ | Transparent background | `transparent_background=True` |
318
+ | Save | `saveFig=True`, `saveDir`, `figname`, `_format` |
319
+ | Change only what I pass | `prettify=False` |
320
+
321
+ ### Helpers exported alongside `mplp`
322
+
323
+ | Function | Does |
324
+ |---|---|
325
+ | `get_bestticks(start, end, step=None, light=False)` | Ticks on round numbers (1 / 5 / 10 steps) |
326
+ | `get_bestticks_from_array(arr, ...)` | Same, from data |
327
+ | `get_labels_from_ticks(ticks)` | Consistently formatted tick label strings |
328
+ | `sci_notation(1.23e6, 2)` | `1.23·10⁶` as mathtext |
329
+ | `get_cmap`, `get_bounded_cmap`, `get_ncolors_cmap`, `get_color_families` | Colormaps and palettes |
330
+ | `to_rgb`, `to_hex`, `to_hsv`, `html_palette` | Color conversion and preview |
331
+ | `add_colorbar(fig, ax, ...)` | The size-preserving colorbar, standalone |
332
+ | `plot_scalebar(ax, ...)` | Scalebars, standalone |
333
+ | `set_ax_size(ax, w, h)` | Exact axis dimensions in inches |
334
+ | `save_mpl_fig(fig, name, dir, fmt)` | Save with Type-42 (editable) text |
335
+
336
+ ---
337
+
338
+ ## Saving
339
+
340
+ ```python
341
+ mplp(saveFig=True, saveDir='./figures', figname='fig2b', _format='pdf')
342
+ ```
343
+
344
+ 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
345
+ re-running the analysis (You will. There is always a label to fix.)
346
+
347
+ ---
348
+
349
+ ## Changing the defaults
350
+
351
+ mplify's defaults live in one hand-editable file,
352
+ [`src/mplify/DEFAULT_PARAMS.py`](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.
353
+
354
+ 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.
355
+
356
+ ```python
357
+ from mplify import default_mplp_params, SIZE_PRESETS # snapshots, for inspection
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Not a style sheet, not a wrapper
363
+
364
+ - **Not a style sheet.** Style sheets set global `rcParams` across 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.
365
+ - **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).
366
+
367
+ ---
368
+
369
+ ## Development
370
+
371
+ ```bash
372
+ uv sync # editable install into .venv
373
+ uv run python doc/make_figures.py # regenerate the README figures into doc img/
374
+ ```
375
+
376
+ The full gallery is [`quickstart.ipynb`](quickstart.ipynb) — open it in your editor of choice and point the kernel at `.venv`.
377
+
378
+ ## Related
379
+
380
+ [NeuroPyxels](https://github.com/m-beau/NeuroPyxels) — Neuropixels data analysis, where this codebase slowly grew up since 2016.