mplify 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,186 @@
1
+ """mplify's default styling parameters — EDIT THIS FILE to change mplify's
2
+ defaults across your project.
3
+
4
+ - default_mplp_params: the base style mplp() falls back to for any parameter
5
+ you don't pass explicitly — fonts, spine/tick widths, colorbar, scalebar,
6
+ legend, etc. It's also, in effect, the 'm' / 'slide' size preset below.
7
+
8
+ - SIZE_PRESETS: per-size overrides layered on top of default_mplp_params
9
+ when you pass size=... to mplp() (e.g. mplp(size='poster')). Aliases
10
+ 'paper', 'slide' and 'poster' point at 's', 'm' and 'l' respectively.
11
+
12
+ mplp() re-reads this file from disk (checking its mtime) on every call, so
13
+ edits here take effect on your very next mplp() call — no kernel restart
14
+ and no need to have %autoreload enabled.
15
+ """
16
+
17
+ from mplify._fonts import DEFAULT_FONT_FAMILY
18
+
19
+ default_mplp_params = dict(
20
+ # font
21
+ font_family=DEFAULT_FONT_FAMILY,
22
+
23
+ # title
24
+ title_w='regular',
25
+ title_s=20,
26
+
27
+ # axis labels
28
+ axlab_w='regular',
29
+ axlab_s=18,
30
+
31
+ # tick labels
32
+ ticklab_w='regular',
33
+ ticklab_s=16,
34
+ ticks_direction='out',
35
+ xlabelpad=0,
36
+ ylabelpad=0,
37
+
38
+ # tick rotation and alignment
39
+ xtickrot=0,
40
+ ytickrot=0,
41
+ xtickha='center',
42
+ xtickva='top',
43
+ ytickha='right',
44
+ ytickva='center',
45
+
46
+ # spines and layout
47
+ lw=1,
48
+ hide_top_right=True,
49
+ hide_axis=False,
50
+ tight_layout=False,
51
+ align_x_labels=True,
52
+ align_y_labels=True,
53
+
54
+ # legend
55
+ show_legend=False,
56
+ hide_legend=False,
57
+ legend_loc=(1, 1),
58
+
59
+ # figure saving
60
+ saveFig=False,
61
+ saveDir='~/Downloads',
62
+ figname='figure',
63
+ _format='pdf',
64
+
65
+ # colorbar
66
+ colorbar=False,
67
+ cbar_w=0.03,
68
+ cbar_h=0.4,
69
+ clabel=None,
70
+ clabel_w='regular',
71
+ clabel_s=18,
72
+ cticks_s=16,
73
+ cbar_pad=0.01,
74
+
75
+ # horizontal and vertical lines
76
+ hlines=None,
77
+ vlines=None,
78
+ lines_kwargs={'lw': 1.5, 'ls': '--', 'color': 'k', 'zorder': -1000},
79
+
80
+ # scalebar
81
+ xscalebar=None,
82
+ yscalebar=None,
83
+ xscalebar_unit='ms',
84
+ yscalebar_unit='μV',
85
+ scalebarkwargs={
86
+ 'scalepad': 0.025,
87
+ 'fontsize': 14,
88
+ 'lw': 3,
89
+ 'loc': 'right',
90
+ 'offset_x': 0,
91
+ 'offset_y': 0,
92
+ },
93
+ )
94
+
95
+ # Each preset scales only the "meta-plotting" elements that need to grow or
96
+ # shrink with the viewing distance/medium — font sizes and line widths — so
97
+ # a plot keeps the same proportions, just legible wherever it ends up.
98
+ # Data-plotting choices (colors, markers, line styles, positions, ...) stay
99
+ # untouched. Use via mplp(size='poster'), or import SIZE_PRESETS directly.
100
+ SIZE_PRESETS = {
101
+
102
+ # xs: extra small — dense multi-panel figures, small insets, thumbnails
103
+ 'xs': dict(
104
+ title_s=14,
105
+ axlab_s=13,
106
+ ticklab_s=11,
107
+ lw=0.7,
108
+ cbar_w=0.02, # colorbar thickness scales with lw, else it reads as
109
+ # a hairline next to xs text and a starved sliver next to xxl text
110
+ clabel_s=13,
111
+ cticks_s=9,
112
+ lines_kwargs={'lw': 1},
113
+ scalebarkwargs={'fontsize': 10, 'lw': 2},
114
+ ),
115
+
116
+ # s: small — paper figures, printed at a few inches wide and read up close
117
+ 's': dict(
118
+ title_s=17,
119
+ axlab_s=15,
120
+ ticklab_s=14,
121
+ lw=0.85,
122
+ cbar_w=0.025,
123
+ clabel_s=15,
124
+ cticks_s=12,
125
+ lines_kwargs={'lw': 1.3},
126
+ scalebarkwargs={'fontsize': 12, 'lw': 2.5},
127
+ ),
128
+
129
+ # m: medium — slides, general on-screen work (mplify's original defaults)
130
+ 'm': dict(
131
+ title_s=20,
132
+ axlab_s=18,
133
+ ticklab_s=16,
134
+ lw=1,
135
+ cbar_w=0.03,
136
+ clabel_s=18,
137
+ cticks_s=14,
138
+ lines_kwargs={'lw': 1.5},
139
+ scalebarkwargs={'fontsize': 14, 'lw': 3},
140
+ ),
141
+
142
+ # l: large — posters, read from about a meter away
143
+ 'l': dict(
144
+ title_s=26,
145
+ axlab_s=23,
146
+ ticklab_s=21,
147
+ lw=1.3,
148
+ cbar_w=0.04,
149
+ clabel_s=23,
150
+ cticks_s=17,
151
+ lines_kwargs={'lw': 2},
152
+ scalebarkwargs={'fontsize': 18, 'lw': 4},
153
+ ),
154
+
155
+ # xl: extra large — posters/banners read from several meters away
156
+ 'xl': dict(
157
+ title_s=32,
158
+ axlab_s=29,
159
+ ticklab_s=26,
160
+ lw=1.6,
161
+ cbar_w=0.05,
162
+ clabel_s=29,
163
+ cticks_s=24,
164
+ lines_kwargs={'lw': 2.4},
165
+ scalebarkwargs={'fontsize': 22, 'lw': 5},
166
+ ),
167
+
168
+ # xxl: banner-sized — huge posters/wall displays read from far across a room
169
+ 'xxl': dict(
170
+ title_s=38,
171
+ axlab_s=34,
172
+ ticklab_s=30,
173
+ lw=1.9,
174
+ cbar_w=0.06,
175
+ clabel_s=34,
176
+ cticks_s=30,
177
+ lines_kwargs={'lw': 2.9},
178
+ scalebarkwargs={'fontsize': 27, 'lw': 6},
179
+ ),
180
+ }
181
+
182
+ # Semantic aliases, so you can call mplp(size='paper') instead of remembering
183
+ # which letter size maps to which output medium.
184
+ SIZE_PRESETS['paper'] = SIZE_PRESETS['s'] # small text, printed close-up
185
+ SIZE_PRESETS['slide'] = SIZE_PRESETS['m'] # mplify's original defaults
186
+ SIZE_PRESETS['poster'] = SIZE_PRESETS['l'] # large text, read from afar
mplify/__init__.py ADDED
@@ -0,0 +1,56 @@
1
+ """mplify: MatPlotLib prettifier"""
2
+
3
+ # Snapshots for inspection at import time — mplp() itself always re-reads
4
+ # DEFAULT_PARAMS.py fresh on every call, so edits there apply without re-importing.
5
+ from mplify.DEFAULT_PARAMS import default_mplp_params, SIZE_PRESETS
6
+ from mplify._core import mplp, save_mpl_fig
7
+ from mplify._utils import set_ax_size
8
+ from mplify._ticks import (
9
+ get_bestticks,
10
+ get_bestticks_from_array,
11
+ get_labels_from_ticks,
12
+ sci_notation,
13
+ )
14
+ from mplify._colors import (
15
+ to_rgb,
16
+ to_hex,
17
+ to_hsv,
18
+ get_cmap,
19
+ get_bounded_cmap,
20
+ get_ncolors_cmap,
21
+ get_color_families,
22
+ html_palette,
23
+ )
24
+ from mplify._colorbar import add_colorbar
25
+ from mplify._scalebar import plot_scalebar
26
+
27
+ __all__ = [
28
+ # Core
29
+ "mplp",
30
+ "default_mplp_params",
31
+ "SIZE_PRESETS",
32
+
33
+ # Save / size
34
+ "save_mpl_fig",
35
+ "set_ax_size",
36
+
37
+ # Ticks
38
+ "get_bestticks",
39
+ "get_bestticks_from_array",
40
+ "get_labels_from_ticks",
41
+ "sci_notation",
42
+
43
+ # Colors
44
+ "to_rgb",
45
+ "to_hex",
46
+ "to_hsv",
47
+ "get_cmap",
48
+ "get_bounded_cmap",
49
+ "get_ncolors_cmap",
50
+ "get_color_families",
51
+ "html_palette",
52
+
53
+ # Colorbar / scalebar
54
+ "add_colorbar",
55
+ "plot_scalebar",
56
+ ]
mplify/_colorbar.py ADDED
@@ -0,0 +1,98 @@
1
+ """Colorbar utilities."""
2
+
3
+ import matplotlib as mpl
4
+ import matplotlib.pyplot as plt
5
+ from mpl_toolkits.axes_grid1.inset_locator import inset_axes
6
+
7
+ from mplify._colors import get_bounded_cmap
8
+ from mplify._ticks import get_bestticks, get_labels_from_ticks
9
+
10
+
11
+ def add_colorbar(fig, ax, mappable=None, vmin=None, vmax=None,
12
+ width=0.01, height=0.5, cticks=None,
13
+ clabel=None, clabel_w='regular', clabel_s=20, cticks_s=16,
14
+ ctickslabels=None, cmap=None, center=None, pad=0.01,
15
+ clim=None, cbar_ax=None):
16
+ """Add a colorbar to a figure, preserving the main axis size.
17
+
18
+ Either provide a mappable (e.g. the return value of ax.imshow()) or
19
+ provide vmin, vmax, and cmap to create one.
20
+
21
+ Arguments:
22
+ - fig, ax: matplotlib figure and axis
23
+ - mappable: a ScalarMappable (e.g. from imshow, scatter). If None,
24
+ one is created from vmin/vmax/cmap.
25
+ - vmin, vmax: data range for the colorbar
26
+ - width, height: colorbar dimensions as fraction of axis size
27
+ - cticks: tick positions on the colorbar
28
+ - clabel: colorbar label text
29
+ - clabel_w, clabel_s: label weight and size
30
+ - cticks_s: tick label size
31
+ - ctickslabels: custom tick labels
32
+ - cmap: colormap name (required if mappable is None)
33
+ - center: center value for diverging colormaps (uses get_bounded_cmap)
34
+ - pad: padding between axis and colorbar as fraction of axis width
35
+ - clim: explicit (min, max) limits for the colorbar axis
36
+ - cbar_ax: pre-existing axis for the colorbar
37
+ """
38
+ # Validate vmin/vmax
39
+ if vmin is not None or vmax is not None:
40
+ if vmin is None or vmax is None:
41
+ raise ValueError("You must provide both vmin and vmax.")
42
+ if vmin is not None and vmax is not None:
43
+ if vmin >= vmax:
44
+ raise ValueError(f"vmin ({vmin}) must be less than vmax ({vmax}).")
45
+ if cticks is None:
46
+ cticks = get_bestticks(vmin, vmax, light=True)
47
+
48
+ # Create mappable if not provided
49
+ if mappable is None:
50
+ if vmin is None or vmax is None:
51
+ raise ValueError(
52
+ "Without a mappable, you must provide vmin and vmax.")
53
+ if cmap is None:
54
+ raise ValueError(
55
+ "Without a mappable, you must provide a colormap (e.g. 'viridis').")
56
+ if center is not None:
57
+ cmap = get_bounded_cmap(cmap, vmin, center, vmax)
58
+ norm = plt.Normalize(vmin, vmax)
59
+ mappable = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
60
+ mappable.set_array([])
61
+
62
+ # Create colorbar axis anchored to the parent axes so it survives
63
+ # tight_layout / constrained_layout adjustments.
64
+ if cbar_ax is None:
65
+ cbar_ax = inset_axes(
66
+ ax,
67
+ width=f"{width * 100}%",
68
+ height=f"{height * 100}%",
69
+ loc='lower right',
70
+ bbox_to_anchor=(pad + width, 0, 1, 1),
71
+ bbox_transform=ax.transAxes,
72
+ borderpad=0,
73
+ )
74
+
75
+ fig.colorbar(mappable, cax=cbar_ax, ax=ax,
76
+ orientation='vertical', label=clabel, use_gridspec=True)
77
+
78
+ # Format ticks and labels
79
+ if ctickslabels is None:
80
+ ctickslabels, _ = get_labels_from_ticks(cticks)
81
+ elif len(ctickslabels) != len(cticks):
82
+ raise ValueError(
83
+ f"ctickslabels length ({len(ctickslabels)}) must match "
84
+ f"cticks length ({len(cticks)}).")
85
+
86
+ if clabel is not None:
87
+ cbar_ax.yaxis.label.set_font_properties(
88
+ mpl.font_manager.FontProperties(weight=clabel_w, size=clabel_s))
89
+ cbar_ax.yaxis.label.set_rotation(-90)
90
+ cbar_ax.yaxis.label.set_va('bottom')
91
+ cbar_ax.yaxis.label.set_ha('center')
92
+ cbar_ax.yaxis.labelpad = 5
93
+ cbar_ax.yaxis.set_ticks(cticks)
94
+ cbar_ax.yaxis.set_ticklabels(ctickslabels, ha='left')
95
+ cbar_ax.yaxis.set_tick_params(pad=5, labelsize=cticks_s)
96
+ cbar_ax.set_ylim(clim)
97
+
98
+ return fig
mplify/_colors.py ADDED
@@ -0,0 +1,163 @@
1
+ """Color utilities built on top of matplotlib.colors."""
2
+
3
+ import matplotlib as mpl
4
+ import matplotlib.colors
5
+ import numpy as np
6
+
7
+
8
+ def to_rgb(color):
9
+ """Convert any matplotlib color specification to (r, g, b) tuple (0-1 range).
10
+
11
+ Accepts: named colors ('red', 'tab:blue'), hex strings ('#ff0000'),
12
+ single-letter codes ('r'), and RGB tuples.
13
+ """
14
+ return mpl.colors.to_rgb(color)
15
+
16
+
17
+ def to_hex(color):
18
+ """Convert any matplotlib color specification to hex string.
19
+
20
+ Accepts: named colors, hex strings, single-letter codes,
21
+ and RGB tuples (0-1 or 0-255 range).
22
+ """
23
+ if not isinstance(color, str):
24
+ color = tuple(color)
25
+ if any(c > 1 for c in color):
26
+ color = tuple(c / 255 for c in color)
27
+ return mpl.colors.to_hex(color)
28
+
29
+
30
+ def to_hsv(color):
31
+ """Convert any matplotlib color specification to HSV tuple."""
32
+ if isinstance(color, str):
33
+ color = mpl.colors.to_rgb(color)
34
+ return mpl.colors.rgb_to_hsv(color)
35
+
36
+
37
+ def get_cmap(cmap_str):
38
+ """Get a matplotlib colormap by name."""
39
+ return mpl.colormaps[cmap_str]
40
+
41
+
42
+ def get_bounded_cmap(cmap_str, vmin, center, vmax, colorseq='linear'):
43
+ """Create a colormap bounded around a center value.
44
+
45
+ Useful for diverging data where the center isn't at the midpoint
46
+ of the value range.
47
+
48
+ Arguments:
49
+ - cmap_str: name of a matplotlib colormap
50
+ - vmin, vmax: data range bounds
51
+ - center: center value for the colormap
52
+ - colorseq: 'linear' or 'nonlinear' reindexing
53
+ """
54
+ if not vmin <= center <= vmax:
55
+ raise ValueError(f'Must have vmin <= center <= vmax, got {vmin}, {center}, {vmax}')
56
+ cmap = get_cmap(cmap_str)
57
+
58
+ vrange = max(vmax - center, center - vmin)
59
+ if vrange == 0:
60
+ vrange = 1
61
+ if colorseq == 'linear':
62
+ vrange_sym = [-vrange, vrange]
63
+ cmin = (vmin - vrange_sym[0]) / (vrange_sym[1] - vrange_sym[0])
64
+ cmax = (vmax - vrange_sym[0]) / (vrange_sym[1] - vrange_sym[0])
65
+ colors_reindex = np.linspace(cmin, cmax, 256)
66
+ elif colorseq == 'nonlinear':
67
+ topratio = (vmax - center) / vrange
68
+ bottomratio = abs(vmin - center) / vrange
69
+ colors_reindex = np.append(
70
+ np.linspace(0, 0.5, int(256 * bottomratio / 2)),
71
+ np.linspace(0.5, 1, int(256 * topratio / 2)),
72
+ )
73
+ else:
74
+ raise ValueError(f"colorseq must be 'linear' or 'nonlinear', got {colorseq!r}")
75
+ return mpl.colors.ListedColormap(cmap(colors_reindex))
76
+
77
+
78
+ def get_ncolors_cmap(n, cmap_str="tab10", plot=False):
79
+ """Return n colors homogeneously distributed from a colormap.
80
+
81
+ Arguments:
82
+ - n: number of colors
83
+ - cmap_str: name of a matplotlib colormap
84
+ - plot: if True, display the palette as HTML (Jupyter notebooks)
85
+ """
86
+ n = int(n)
87
+ cmap = get_cmap(cmap_str)
88
+ ids = np.linspace(0, 1, n)
89
+ colors = cmap(ids)[:, :-1].tolist()
90
+ if plot:
91
+ html_palette(colors)
92
+ return colors
93
+
94
+
95
+ def get_color_families(ncolors, nfamilies, cmapstr=None, gap_between_families=4):
96
+ """Return nfamilies groups of ncolors perceptually close colors.
97
+
98
+ Within each family, colors are neighbours on a perceptually sequential colormap.
99
+ Between families, gap_between_families colors are skipped.
100
+
101
+ Arguments:
102
+ - ncolors: colors per family
103
+ - nfamilies: number of families
104
+ - cmapstr: colormap name (if None, uses matplotlib CSS colors sorted by HSV)
105
+ - gap_between_families: spacing between families (higher = more distinct families)
106
+ """
107
+ if cmapstr is None:
108
+ colors_all = _get_mpl_css_colors(sort=True, aslist=True)[15:-10]
109
+ indices = np.linspace(0, len(colors_all) - 1,
110
+ (ncolors + gap_between_families) * nfamilies).astype(np.int64)
111
+ colors = np.array(colors_all)[indices].tolist()
112
+ else:
113
+ colors = get_ncolors_cmap((ncolors + gap_between_families // 2) * nfamilies,
114
+ cmapstr, plot=False)
115
+ highsat_colors = [c for c in colors if to_hsv(c)[1] > 0.4]
116
+ seed_ids = np.linspace(0, len(highsat_colors) - ncolors, nfamilies).astype(np.int64)
117
+ return [[highsat_colors[si + i] for i in range(ncolors)] for si in seed_ids]
118
+
119
+
120
+ def html_palette(colors, maxwidth=20, as_str=False, show=True):
121
+ """Display colors as an SVG palette (works in Jupyter notebooks).
122
+
123
+ Arguments:
124
+ - colors: list of colors (RGB tuples, hex strings, or named colors)
125
+ - maxwidth: maximum colors per row
126
+ - as_str: if True, return raw SVG string instead of IPython HTML object
127
+ - show: if True, display the palette immediately
128
+ """
129
+ s = 55
130
+ n = min(len(colors), maxwidth)
131
+ col_rows = [colors[i * maxwidth:i * maxwidth + maxwidth]
132
+ for i in range(len(colors) // maxwidth + 1)]
133
+ col_rows = [c for c in col_rows if any(c)]
134
+ h = len(col_rows)
135
+ palette = f'<svg width="{n * s}" height="{s * h}">'
136
+ for r, row_colors in enumerate(col_rows):
137
+ for i, c in enumerate(row_colors):
138
+ c_hex = c if isinstance(c, str) else to_hex(c)
139
+ palette += (
140
+ f'<rect x="{i * s}" y="{r * s}" width="{s}" height="{s}" '
141
+ f'style="fill:{c_hex};stroke-width:2;stroke:rgb(255,255,255)"/>'
142
+ )
143
+ palette += '</svg>'
144
+ if not as_str:
145
+ from IPython.display import display, HTML as IPyHTML
146
+ palette = IPyHTML(palette)
147
+ if show:
148
+ display(palette)
149
+ return palette
150
+
151
+
152
+ def _get_mpl_css_colors(sort=True, aslist=False):
153
+ """Get matplotlib CSS4 colors, optionally sorted by HSV."""
154
+ colors = mpl.colors.CSS4_COLORS
155
+ if sort:
156
+ by_hsv = sorted(
157
+ (tuple(mpl.colors.rgb_to_hsv(mpl.colors.to_rgb(color))), name)
158
+ for name, color in colors.items()
159
+ )
160
+ colors = {name: colors[name] for hsv, name in by_hsv}
161
+ if aslist:
162
+ colors = list(colors.values())
163
+ return colors