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.
- mplify/DEFAULT_PARAMS.py +186 -0
- mplify/__init__.py +56 -0
- mplify/_colorbar.py +98 -0
- mplify/_colors.py +163 -0
- mplify/_core.py +551 -0
- mplify/_fonts.py +27 -0
- mplify/_scalebar.py +99 -0
- mplify/_ticks.py +122 -0
- mplify/_utils.py +48 -0
- mplify-1.0.0.dist-info/METADATA +380 -0
- mplify-1.0.0.dist-info/RECORD +12 -0
- mplify-1.0.0.dist-info/WHEEL +4 -0
mplify/DEFAULT_PARAMS.py
ADDED
|
@@ -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
|