hedgehogs 0.0.1__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.
- hedgehogs/__init__.py +66 -0
- hedgehogs/core/__init__.py +1 -0
- hedgehogs/core/constants.py +6 -0
- hedgehogs/core/environment.py +8 -0
- hedgehogs/core/info.py +15 -0
- hedgehogs/core/palette.py +263 -0
- hedgehogs/core/version.py +4 -0
- hedgehogs/plots/__init__.py +13 -0
- hedgehogs/plots/helpers.py +129 -0
- hedgehogs/plots/output.py +75 -0
- hedgehogs/plots/pairplot.py +275 -0
- hedgehogs/plots/presentation.py +294 -0
- hedgehogs/plots/rendering.py +691 -0
- hedgehogs/plots/style.py +311 -0
- hedgehogs/plots/tree.py +269 -0
- hedgehogs/plots/typography.py +224 -0
- hedgehogs/py.typed +1 -0
- hedgehogs/tables/__init__.py +5 -0
- hedgehogs/tables/table.py +310 -0
- hedgehogs/terminal/__init__.py +6 -0
- hedgehogs/terminal/output.py +61 -0
- hedgehogs/terminal/progress.py +304 -0
- hedgehogs-0.0.1.dist-info/METADATA +133 -0
- hedgehogs-0.0.1.dist-info/RECORD +27 -0
- hedgehogs-0.0.1.dist-info/WHEEL +5 -0
- hedgehogs-0.0.1.dist-info/licenses/LICENSE +22 -0
- hedgehogs-0.0.1.dist-info/top_level.txt +1 -0
hedgehogs/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Hedgehogs — publication-quality scientific visualisation and reporting utilities."""
|
|
2
|
+
|
|
3
|
+
from .core.version import __version__, __date__
|
|
4
|
+
from .core.constants import EPS
|
|
5
|
+
from .core.palette import (
|
|
6
|
+
Palette,
|
|
7
|
+
get_palette,
|
|
8
|
+
build_color_map,
|
|
9
|
+
build_style_map,
|
|
10
|
+
register_palette,
|
|
11
|
+
save_palette,
|
|
12
|
+
load_palette,
|
|
13
|
+
)
|
|
14
|
+
from .plots.style import (
|
|
15
|
+
set_style,
|
|
16
|
+
reset_style,
|
|
17
|
+
journal_preset,
|
|
18
|
+
set_journal_style,
|
|
19
|
+
figsize,
|
|
20
|
+
subplots,
|
|
21
|
+
)
|
|
22
|
+
from .plots.helpers import (
|
|
23
|
+
finalize,
|
|
24
|
+
style_colorbar,
|
|
25
|
+
annotate_panels,
|
|
26
|
+
enable_minor_ticks,
|
|
27
|
+
apply_grid,
|
|
28
|
+
)
|
|
29
|
+
from . import plots
|
|
30
|
+
from .tables import Table
|
|
31
|
+
from .terminal import Progress
|
|
32
|
+
from .terminal import echo, rule
|
|
33
|
+
from .core.info import info
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"EPS",
|
|
37
|
+
"Palette",
|
|
38
|
+
"get_palette",
|
|
39
|
+
"build_color_map",
|
|
40
|
+
"build_style_map",
|
|
41
|
+
"register_palette",
|
|
42
|
+
"save_palette",
|
|
43
|
+
"load_palette",
|
|
44
|
+
"set_style",
|
|
45
|
+
"reset_style",
|
|
46
|
+
"journal_preset",
|
|
47
|
+
"set_journal_style",
|
|
48
|
+
"figsize",
|
|
49
|
+
"subplots",
|
|
50
|
+
"finalize",
|
|
51
|
+
"style_colorbar",
|
|
52
|
+
"annotate_panels",
|
|
53
|
+
"enable_minor_ticks",
|
|
54
|
+
"apply_grid",
|
|
55
|
+
"plots",
|
|
56
|
+
"Table",
|
|
57
|
+
"Progress",
|
|
58
|
+
"echo",
|
|
59
|
+
"rule",
|
|
60
|
+
"info",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def __dir__() -> list[str]:
|
|
65
|
+
"""List the supported public interface and package metadata."""
|
|
66
|
+
return sorted([*__all__, "__version__", "__date__"])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared metadata, palette and environment support."""
|
hedgehogs/core/info.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Package information utilities."""
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
|
|
5
|
+
from .version import __version__, __date__
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def info():
|
|
9
|
+
"""Print version and dependency info."""
|
|
10
|
+
text = (
|
|
11
|
+
f"Hedgehogs v{__version__} ({__date__})\n"
|
|
12
|
+
f" matplotlib {plt.matplotlib.__version__}"
|
|
13
|
+
)
|
|
14
|
+
print(text)
|
|
15
|
+
return text
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Colour palettes and colour mapping utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from numbers import Integral
|
|
8
|
+
from typing import Any
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
from matplotlib.colors import is_color_like
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_PALETTES = {
|
|
15
|
+
"tableau10": {
|
|
16
|
+
"colors": [
|
|
17
|
+
"#4e79a7", "#f28e2b", "#e15759", "#76b7b2", "#59a14f",
|
|
18
|
+
"#edc948", "#b07aa1", "#ff9da7", "#9c755f", "#bab0ac",
|
|
19
|
+
],
|
|
20
|
+
"names": [
|
|
21
|
+
"blue", "orange", "red", "teal", "green",
|
|
22
|
+
"yellow", "purple", "pink", "brown", "grey",
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
"okabe-ito": {
|
|
26
|
+
"colors": [
|
|
27
|
+
"#0072B2", "#E69F00", "#D55E00", "#009E73",
|
|
28
|
+
"#56B4E9", "#F0E442", "#CC79A7", "#000000",
|
|
29
|
+
],
|
|
30
|
+
"names": [
|
|
31
|
+
"blue", "orange", "red", "green",
|
|
32
|
+
"cyan", "yellow", "purple", "black",
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
"paul-tol-vibrant": {
|
|
36
|
+
"colors": [
|
|
37
|
+
"#0077BB", "#33BBEE", "#009988", "#EE7733",
|
|
38
|
+
"#CC3311", "#EE3377", "#BBBBBB",
|
|
39
|
+
],
|
|
40
|
+
"names": [
|
|
41
|
+
"blue", "cyan", "teal", "orange",
|
|
42
|
+
"red", "magenta", "grey",
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
"paul-tol-bright": {
|
|
46
|
+
"colors": [
|
|
47
|
+
"#4477AA", "#EE6677", "#228833", "#CCBB44",
|
|
48
|
+
"#66CCEE", "#AA3377", "#BBBBBB",
|
|
49
|
+
],
|
|
50
|
+
"names": [
|
|
51
|
+
"blue", "red", "green", "yellow",
|
|
52
|
+
"cyan", "purple", "grey",
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
"paul-tol-muted": {
|
|
56
|
+
"colors": [
|
|
57
|
+
"#332288", "#88CCEE", "#44AA99", "#117733",
|
|
58
|
+
"#999933", "#DDCC77", "#CC6677", "#882255", "#AA4499",
|
|
59
|
+
],
|
|
60
|
+
"names": [
|
|
61
|
+
"indigo", "cyan", "teal", "green",
|
|
62
|
+
"olive", "sand", "rose", "wine", "purple",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
"ibm": {
|
|
66
|
+
"colors": ["#648FFF", "#785EF0", "#DC267F", "#FE6100", "#FFB000"],
|
|
67
|
+
"names": ["blue", "indigo", "magenta", "orange", "gold"],
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Palette:
|
|
73
|
+
"""Colour palette with both name-based and index-based access."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, names: Iterable[str], colors: Iterable[str]) -> None:
|
|
76
|
+
self._names = list(names)
|
|
77
|
+
self._colors = list(colors)
|
|
78
|
+
self._map = dict(zip(self._names, self._colors))
|
|
79
|
+
|
|
80
|
+
def __repr__(self) -> str:
|
|
81
|
+
pairs = ", ".join(f"{n}={c}" for n, c in zip(self._names, self._colors))
|
|
82
|
+
return f"Palette({pairs})"
|
|
83
|
+
|
|
84
|
+
def __getitem__(self, key: int | slice | str):
|
|
85
|
+
if isinstance(key, Integral) and not isinstance(key, bool):
|
|
86
|
+
index = int(key)
|
|
87
|
+
return self._colors[index % len(self._colors)]
|
|
88
|
+
if isinstance(key, slice):
|
|
89
|
+
return self._colors[key]
|
|
90
|
+
if isinstance(key, str):
|
|
91
|
+
return self._map[key.lower()]
|
|
92
|
+
raise KeyError(key)
|
|
93
|
+
|
|
94
|
+
def __contains__(self, key: object) -> bool:
|
|
95
|
+
if isinstance(key, str):
|
|
96
|
+
return key.lower() in self._map
|
|
97
|
+
if isinstance(key, Integral) and not isinstance(key, bool):
|
|
98
|
+
index = int(key)
|
|
99
|
+
return -len(self._colors) <= index < len(self._colors)
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def __iter__(self):
|
|
103
|
+
return iter(self._colors)
|
|
104
|
+
|
|
105
|
+
def __len__(self) -> int:
|
|
106
|
+
return len(self._colors)
|
|
107
|
+
|
|
108
|
+
def keys(self):
|
|
109
|
+
return self._map.keys()
|
|
110
|
+
|
|
111
|
+
def values(self):
|
|
112
|
+
return self._map.values()
|
|
113
|
+
|
|
114
|
+
def items(self):
|
|
115
|
+
return self._map.items()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _resolve_palette(name: object) -> str:
|
|
119
|
+
p = str(name).lower()
|
|
120
|
+
if p in _PALETTES: return p
|
|
121
|
+
if "vibrant" in p: return "paul-tol-vibrant"
|
|
122
|
+
if "bright" in p: return "paul-tol-bright"
|
|
123
|
+
if "muted" in p: return "paul-tol-muted"
|
|
124
|
+
if "tol" in p or "paul" in p: return "paul-tol-vibrant"
|
|
125
|
+
if "tab" in p or "tableau" in p: return "tableau10"
|
|
126
|
+
if "ibm" in p: return "ibm"
|
|
127
|
+
if "okabe" in p or "ito" in p: return "okabe-ito"
|
|
128
|
+
warnings.warn(
|
|
129
|
+
f"Unrecognised palette name {name!r}; falling back to 'okabe-ito'. "
|
|
130
|
+
f"Known palettes: {sorted(_PALETTES)}",
|
|
131
|
+
UserWarning,
|
|
132
|
+
stacklevel=3,
|
|
133
|
+
)
|
|
134
|
+
return "okabe-ito"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_palette(palette: str | Palette = "okabe-ito", n: int | None = None) -> Palette:
|
|
138
|
+
"""Return a Palette object. Supports fuzzy name matching."""
|
|
139
|
+
if isinstance(palette, Palette):
|
|
140
|
+
if n is None:
|
|
141
|
+
return palette
|
|
142
|
+
source = palette
|
|
143
|
+
if not isinstance(n, int) or isinstance(n, bool) or n < 1:
|
|
144
|
+
raise ValueError("n must be a positive integer or None.")
|
|
145
|
+
return Palette(list(source.keys())[:n], list(source.values())[:n])
|
|
146
|
+
|
|
147
|
+
if n is not None and (
|
|
148
|
+
not isinstance(n, int)
|
|
149
|
+
or isinstance(n, bool)
|
|
150
|
+
or n < 1
|
|
151
|
+
):
|
|
152
|
+
raise ValueError("n must be a positive integer or None.")
|
|
153
|
+
|
|
154
|
+
key = _resolve_palette(palette)
|
|
155
|
+
entry = _PALETTES[key]
|
|
156
|
+
limit = len(entry["colors"]) if n is None else min(n, len(entry["colors"]))
|
|
157
|
+
return Palette(entry["names"][:limit], entry["colors"][:limit])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def build_color_map(
|
|
161
|
+
labels: Iterable[Any],
|
|
162
|
+
palette: str | Palette = "okabe-ito",
|
|
163
|
+
) -> dict[Any, str]:
|
|
164
|
+
"""Map unique labels to palette colours."""
|
|
165
|
+
pal = get_palette(palette=palette)
|
|
166
|
+
unique_labels = dict.fromkeys(labels)
|
|
167
|
+
return {
|
|
168
|
+
label: pal[index]
|
|
169
|
+
for index, label in enumerate(unique_labels)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def build_style_map(
|
|
174
|
+
labels: Iterable[Any],
|
|
175
|
+
palette: str | Palette = "okabe-ito",
|
|
176
|
+
) -> dict[Any, dict[str, str]]:
|
|
177
|
+
"""Map labels to distinct colour, line-style, and marker combinations."""
|
|
178
|
+
colours = get_palette(palette=palette)
|
|
179
|
+
line_styles = ("-", "--", "-.", ":")
|
|
180
|
+
markers = ("o", "s", "^", "D", "v", "P", "X")
|
|
181
|
+
return {
|
|
182
|
+
label: {
|
|
183
|
+
"color": colours[index],
|
|
184
|
+
"linestyle": line_styles[index % len(line_styles)],
|
|
185
|
+
"marker": markers[index % len(markers)],
|
|
186
|
+
}
|
|
187
|
+
for index, label in enumerate(dict.fromkeys(labels))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def register_palette(
|
|
192
|
+
name: str,
|
|
193
|
+
colors: Mapping[str, str] | Iterable[str],
|
|
194
|
+
names: Iterable[str] | None = None,
|
|
195
|
+
*,
|
|
196
|
+
overwrite: bool = False,
|
|
197
|
+
) -> Palette:
|
|
198
|
+
"""Register a custom palette for use by :func:`get_palette`."""
|
|
199
|
+
key = str(name).strip().lower()
|
|
200
|
+
if not key:
|
|
201
|
+
raise ValueError("Palette name must not be empty.")
|
|
202
|
+
if key in _PALETTES and not overwrite:
|
|
203
|
+
raise ValueError(f"Palette {key!r} is already registered.")
|
|
204
|
+
|
|
205
|
+
if isinstance(colors, Mapping):
|
|
206
|
+
if names is not None:
|
|
207
|
+
raise ValueError("names must be omitted when colors is a mapping.")
|
|
208
|
+
palette_names = [str(value) for value in colors]
|
|
209
|
+
palette_colors = list(colors.values())
|
|
210
|
+
else:
|
|
211
|
+
palette_colors = list(colors)
|
|
212
|
+
palette_names = (
|
|
213
|
+
[f"color-{index + 1}" for index in range(len(palette_colors))]
|
|
214
|
+
if names is None
|
|
215
|
+
else [str(value) for value in names]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if not palette_colors:
|
|
219
|
+
raise ValueError("A palette must contain at least one colour.")
|
|
220
|
+
if len(palette_names) != len(palette_colors):
|
|
221
|
+
raise ValueError("Palette names and colors must have the same length.")
|
|
222
|
+
if len(set(palette_names)) != len(palette_names):
|
|
223
|
+
raise ValueError("Palette colour names must be unique.")
|
|
224
|
+
|
|
225
|
+
invalid = [value for value in palette_colors if not is_color_like(value)]
|
|
226
|
+
if invalid:
|
|
227
|
+
raise ValueError(f"Invalid Matplotlib colour values: {invalid!r}.")
|
|
228
|
+
|
|
229
|
+
_PALETTES[key] = {
|
|
230
|
+
"names": palette_names,
|
|
231
|
+
"colors": [str(value) for value in palette_colors],
|
|
232
|
+
}
|
|
233
|
+
return get_palette(key)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def save_palette(name: str, path: str | PathLike[str]) -> Path:
|
|
237
|
+
"""Save a registered palette as portable JSON."""
|
|
238
|
+
key = _resolve_palette(name)
|
|
239
|
+
destination = Path(path).expanduser()
|
|
240
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
destination.write_text(
|
|
242
|
+
json.dumps({"name": key, **_PALETTES[key]}, indent=2) + "\n",
|
|
243
|
+
encoding="utf-8",
|
|
244
|
+
)
|
|
245
|
+
return destination
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_palette(
|
|
249
|
+
path: str | PathLike[str],
|
|
250
|
+
*,
|
|
251
|
+
name: str | None = None,
|
|
252
|
+
overwrite: bool = False,
|
|
253
|
+
) -> Palette:
|
|
254
|
+
"""Load and register a palette saved by :func:`save_palette`."""
|
|
255
|
+
source = Path(path).expanduser()
|
|
256
|
+
payload = json.loads(source.read_text(encoding="utf-8"))
|
|
257
|
+
palette_name = name or payload.get("name") or source.stem
|
|
258
|
+
return register_palette(
|
|
259
|
+
palette_name,
|
|
260
|
+
payload["colors"],
|
|
261
|
+
names=payload.get("names"),
|
|
262
|
+
overwrite=overwrite,
|
|
263
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Specialised Matplotlib plots with Hedgehogs presentation defaults."""
|
|
2
|
+
|
|
3
|
+
from .tree import tree
|
|
4
|
+
from .pairplot import pairplot
|
|
5
|
+
from .presentation import show
|
|
6
|
+
from .output import save
|
|
7
|
+
|
|
8
|
+
__all__ = ["tree", "pairplot", "show", "save"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __dir__() -> list[str]:
|
|
12
|
+
"""List the public plotting functions rather than implementation modules."""
|
|
13
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Matplotlib plotting helpers."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import matplotlib.ticker as mticker
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _SkipZeroFormatter(mticker.ScalarFormatter):
|
|
9
|
+
def __call__(self, x, pos=None):
|
|
10
|
+
return "" if math.isclose(x, 0.0, rel_tol=1e-5, abs_tol=1e-8) else super().__call__(x, pos)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _to_axes_list(ax):
|
|
14
|
+
if ax is None:
|
|
15
|
+
return [plt.gca()]
|
|
16
|
+
if hasattr(ax, "ravel") and callable(ax.ravel):
|
|
17
|
+
return ax.ravel().tolist()
|
|
18
|
+
if hasattr(ax, "__iter__") and not hasattr(ax, "plot"):
|
|
19
|
+
return list(ax)
|
|
20
|
+
return [ax]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fix_origin_overlap(ax=None, keep="x"):
|
|
24
|
+
if ax is None:
|
|
25
|
+
ax = plt.gca()
|
|
26
|
+
xlim, ylim = ax.get_xlim(), ax.get_ylim()
|
|
27
|
+
if not (xlim[0] <= 0 <= xlim[1] and ylim[0] <= 0 <= ylim[1]):
|
|
28
|
+
return
|
|
29
|
+
target = ax.yaxis if keep == "x" else ax.xaxis
|
|
30
|
+
current = target.get_major_formatter()
|
|
31
|
+
if isinstance(current, (mticker.ScalarFormatter, _SkipZeroFormatter)):
|
|
32
|
+
fmt = _SkipZeroFormatter()
|
|
33
|
+
try:
|
|
34
|
+
fmt.set_useOffset(current.get_useOffset())
|
|
35
|
+
fmt.set_useMathText(current.get_useMathText())
|
|
36
|
+
except AttributeError:
|
|
37
|
+
pass
|
|
38
|
+
target.set_major_formatter(fmt)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _apply_grid(ax=None, axis="both"):
|
|
42
|
+
if ax is None:
|
|
43
|
+
ax = plt.gca()
|
|
44
|
+
ax.grid(True, which="major", axis=axis,
|
|
45
|
+
linestyle=":", color="black", alpha=0.7)
|
|
46
|
+
ax.set_axisbelow(True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fix_legend_frame(ax=None):
|
|
50
|
+
if ax is None:
|
|
51
|
+
ax = plt.gca()
|
|
52
|
+
leg = ax.get_legend()
|
|
53
|
+
if leg is not None:
|
|
54
|
+
frame = leg.get_frame()
|
|
55
|
+
frame.set_linewidth(ax.spines["left"].get_linewidth())
|
|
56
|
+
frame.set_edgecolor(plt.rcParams.get("legend.edgecolor", "black"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _enable_minor_ticks(ax=None, x=True, y=True):
|
|
60
|
+
if ax is None:
|
|
61
|
+
ax = plt.gca()
|
|
62
|
+
if x:
|
|
63
|
+
ax.xaxis.set_minor_locator(mticker.AutoMinorLocator())
|
|
64
|
+
if y:
|
|
65
|
+
ax.yaxis.set_minor_locator(mticker.AutoMinorLocator())
|
|
66
|
+
major_tick = ax.xaxis.get_major_ticks()[0].tick1line
|
|
67
|
+
lw = major_tick.get_markeredgewidth() * 0.6
|
|
68
|
+
sz = major_tick.get_markersize() * 0.5
|
|
69
|
+
ax.tick_params(which="minor", direction="out",
|
|
70
|
+
length=sz, width=lw, top=True, right=True)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _style_colorbar(cb, label=None):
|
|
74
|
+
lw = cb.ax.spines["left"].get_linewidth()
|
|
75
|
+
cb.outline.set_linewidth(lw)
|
|
76
|
+
cb.outline.set_edgecolor("black")
|
|
77
|
+
cb.ax.tick_params(
|
|
78
|
+
direction="out",
|
|
79
|
+
width=cb.ax.yaxis.get_major_ticks()[0].tick1line.get_markeredgewidth(),
|
|
80
|
+
length=cb.ax.yaxis.get_major_ticks()[0].tick1line.get_markersize() * 0.7,
|
|
81
|
+
)
|
|
82
|
+
if label is not None:
|
|
83
|
+
cb.set_label(label)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _annotate_panels(axes, labels=None, loc="upper left",
|
|
87
|
+
offset=None, fontsize=None, fontweight="bold"):
|
|
88
|
+
axes = _to_axes_list(axes)
|
|
89
|
+
if labels is None:
|
|
90
|
+
labels = [f"({chr(97 + i)})" for i in range(len(axes))]
|
|
91
|
+
if fontsize is None:
|
|
92
|
+
fontsize = plt.rcParams["font.size"]
|
|
93
|
+
if offset is not None:
|
|
94
|
+
ox, oy = offset
|
|
95
|
+
elif "right" in loc:
|
|
96
|
+
ox, oy = 0.94, 0.93
|
|
97
|
+
else:
|
|
98
|
+
ox, oy = 0.06, 0.93
|
|
99
|
+
ha = "right" if "right" in loc else "left"
|
|
100
|
+
for a, lbl in zip(axes, labels):
|
|
101
|
+
a.text(ox, oy, lbl, transform=a.transAxes,
|
|
102
|
+
fontsize=fontsize, fontweight=fontweight, va="top", ha=ha)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _finalize(ax=None, fix_origin=True, keep="x",
|
|
106
|
+
grid=False, minor_ticks=False):
|
|
107
|
+
for a in _to_axes_list(ax):
|
|
108
|
+
if fix_origin:
|
|
109
|
+
_fix_origin_overlap(a, keep=keep)
|
|
110
|
+
_fix_legend_frame(a)
|
|
111
|
+
if grid:
|
|
112
|
+
_apply_grid(a)
|
|
113
|
+
if minor_ticks:
|
|
114
|
+
_enable_minor_ticks(a)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
finalize = _finalize
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
style_colorbar = _style_colorbar
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
annotate_panels = _annotate_panels
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
enable_minor_ticks = _enable_minor_ticks
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
apply_grid = _apply_grid
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Finish and save Matplotlib figures."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
from matplotlib.figure import Figure
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def save(
|
|
13
|
+
filename: str | PathLike[str],
|
|
14
|
+
*,
|
|
15
|
+
fig: Figure | None = None,
|
|
16
|
+
formats: Iterable[str] | None = None,
|
|
17
|
+
dpi: float = 300,
|
|
18
|
+
transparent: bool = False,
|
|
19
|
+
bbox_inches: str | None = "tight",
|
|
20
|
+
metadata: Mapping[str, Any] | None = None,
|
|
21
|
+
close: bool = False,
|
|
22
|
+
**savefig_kw: Any,
|
|
23
|
+
) -> tuple[Path, ...]:
|
|
24
|
+
"""Finish and save the current pyplot figure or an explicit ``fig``.
|
|
25
|
+
|
|
26
|
+
A filename extension selects one format. Without an extension, ``formats``
|
|
27
|
+
selects outputs and defaults to PDF and PNG. Return a tuple of written Paths.
|
|
28
|
+
An extension combined with formats raises ValueError; an absent current
|
|
29
|
+
figure also raises ValueError without creating an empty figure.
|
|
30
|
+
|
|
31
|
+
Examples: ``save("figure.pdf")`` or ``save("figure", fig=fig,
|
|
32
|
+
formats=("pdf", "png"))``. Explicit Matplotlib save options remain available.
|
|
33
|
+
"""
|
|
34
|
+
base = Path(filename).expanduser()
|
|
35
|
+
requested: tuple[str, ...]
|
|
36
|
+
destinations: tuple[Path, ...]
|
|
37
|
+
if "format" in savefig_kw:
|
|
38
|
+
raise ValueError("Select the format with the filename extension or formats, not format.")
|
|
39
|
+
if base.suffix:
|
|
40
|
+
if formats is not None:
|
|
41
|
+
raise ValueError("A filename extension and formats cannot be supplied together.")
|
|
42
|
+
requested = (base.suffix[1:].lower(),)
|
|
43
|
+
destinations = (base,)
|
|
44
|
+
else:
|
|
45
|
+
selected = ("pdf", "png") if formats is None else formats
|
|
46
|
+
if isinstance(selected, str):
|
|
47
|
+
selected = (selected,)
|
|
48
|
+
requested = tuple(str(value).lower().lstrip(".") for value in selected)
|
|
49
|
+
if not requested or any(not value for value in requested):
|
|
50
|
+
raise ValueError("formats must contain at least one file format.")
|
|
51
|
+
destinations = tuple(base.with_suffix(f".{suffix}") for suffix in requested)
|
|
52
|
+
if fig is None:
|
|
53
|
+
if not plt.get_fignums():
|
|
54
|
+
raise ValueError("No current figure to save; create one or supply fig.")
|
|
55
|
+
fig = plt.gcf()
|
|
56
|
+
from .presentation import prepare
|
|
57
|
+
prepare(fig)
|
|
58
|
+
base.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
options = {
|
|
61
|
+
"dpi": dpi,
|
|
62
|
+
"transparent": transparent,
|
|
63
|
+
"bbox_inches": bbox_inches,
|
|
64
|
+
"metadata": metadata,
|
|
65
|
+
**savefig_kw,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
with plt.rc_context({"pdf.fonttype": 42, "ps.fonttype": 42, "savefig.bbox": None}):
|
|
69
|
+
for destination, file_format in zip(destinations, requested):
|
|
70
|
+
fig.savefig(destination, format=file_format, **options)
|
|
71
|
+
|
|
72
|
+
if close:
|
|
73
|
+
plt.close(fig)
|
|
74
|
+
|
|
75
|
+
return destinations
|