plotstyle 0.1.0a1__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.
- plotstyle/__init__.py +121 -0
- plotstyle/_utils/__init__.py +0 -0
- plotstyle/_utils/io.py +113 -0
- plotstyle/_utils/warnings.py +86 -0
- plotstyle/_version.py +24 -0
- plotstyle/cli/__init__.py +0 -0
- plotstyle/cli/main.py +553 -0
- plotstyle/color/__init__.py +42 -0
- plotstyle/color/_rendering.py +86 -0
- plotstyle/color/accessibility.py +286 -0
- plotstyle/color/data/okabe_ito.json +5 -0
- plotstyle/color/data/safe_grayscale.json +7 -0
- plotstyle/color/data/tol_bright.json +5 -0
- plotstyle/color/data/tol_muted.json +5 -0
- plotstyle/color/data/tol_vibrant.json +5 -0
- plotstyle/color/grayscale.py +284 -0
- plotstyle/color/palettes.py +259 -0
- plotstyle/core/__init__.py +0 -0
- plotstyle/core/export.py +418 -0
- plotstyle/core/figure.py +394 -0
- plotstyle/core/migrate.py +579 -0
- plotstyle/core/style.py +394 -0
- plotstyle/engine/__init__.py +0 -0
- plotstyle/engine/fonts.py +309 -0
- plotstyle/engine/latex.py +287 -0
- plotstyle/engine/rcparams.py +352 -0
- plotstyle/integrations/__init__.py +0 -0
- plotstyle/integrations/seaborn.py +305 -0
- plotstyle/preview/__init__.py +50 -0
- plotstyle/preview/gallery.py +337 -0
- plotstyle/preview/print_size.py +304 -0
- plotstyle/py.typed +0 -0
- plotstyle/specs/__init__.py +304 -0
- plotstyle/specs/_templates.toml +48 -0
- plotstyle/specs/acs.toml +36 -0
- plotstyle/specs/cell.toml +35 -0
- plotstyle/specs/elsevier.toml +35 -0
- plotstyle/specs/ieee.toml +35 -0
- plotstyle/specs/nature.toml +35 -0
- plotstyle/specs/plos.toml +35 -0
- plotstyle/specs/prl.toml +35 -0
- plotstyle/specs/schema.py +1095 -0
- plotstyle/specs/science.toml +35 -0
- plotstyle/specs/springer.toml +35 -0
- plotstyle/specs/units.py +761 -0
- plotstyle/specs/wiley.toml +35 -0
- plotstyle/validation/__init__.py +94 -0
- plotstyle/validation/checks/__init__.py +95 -0
- plotstyle/validation/checks/_base.py +149 -0
- plotstyle/validation/checks/colors.py +394 -0
- plotstyle/validation/checks/dimensions.py +166 -0
- plotstyle/validation/checks/export.py +205 -0
- plotstyle/validation/checks/lines.py +147 -0
- plotstyle/validation/checks/typography.py +200 -0
- plotstyle/validation/report.py +293 -0
- plotstyle-0.1.0a1.dist-info/METADATA +271 -0
- plotstyle-0.1.0a1.dist-info/RECORD +60 -0
- plotstyle-0.1.0a1.dist-info/WHEEL +4 -0
- plotstyle-0.1.0a1.dist-info/entry_points.txt +2 -0
- plotstyle-0.1.0a1.dist-info/licenses/LICENSE +21 -0
plotstyle/core/figure.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""Dimension-aware figure and subplot creation.
|
|
2
|
+
|
|
3
|
+
This module provides drop-in replacements for :func:`matplotlib.pyplot.figure`
|
|
4
|
+
and :func:`matplotlib.pyplot.subplots` that automatically size figures to
|
|
5
|
+
match a journal's column-width constraints.
|
|
6
|
+
|
|
7
|
+
``figure``
|
|
8
|
+
Create a single-axis figure whose dimensions conform to a journal spec.
|
|
9
|
+
|
|
10
|
+
``subplots``
|
|
11
|
+
Create a multi-panel figure, optionally annotated with spec-accurate
|
|
12
|
+
panel labels (a, b, c, …).
|
|
13
|
+
|
|
14
|
+
Both functions resolve the journal spec from the built-in registry and convert
|
|
15
|
+
physical column widths from millimetres to inches before delegating to
|
|
16
|
+
Matplotlib.
|
|
17
|
+
|
|
18
|
+
Design notes
|
|
19
|
+
------------
|
|
20
|
+
The golden ratio (φ ≈ 1.618) is used as the default aspect ratio because it
|
|
21
|
+
produces visually balanced figures without requiring explicit height
|
|
22
|
+
specification. Pass *aspect* to override it for any figure where a different
|
|
23
|
+
proportion is preferred (e.g. square plots or wide panoramic layouts).
|
|
24
|
+
|
|
25
|
+
Panel label normalisation
|
|
26
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
27
|
+
:func:`subplots` always returns an ``ndarray`` for the axes argument —
|
|
28
|
+
including the ``nrows=1, ncols=1`` case — so that callers can use ``.flat``
|
|
29
|
+
iteration and ``[i, j]`` indexing uniformly. This diverges slightly from
|
|
30
|
+
vanilla Matplotlib, which returns a bare :class:`~matplotlib.axes.Axes` for
|
|
31
|
+
the single-panel case; the difference is intentional and documented on the
|
|
32
|
+
:func:`subplots` return value.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from typing import TYPE_CHECKING, Final
|
|
38
|
+
|
|
39
|
+
import matplotlib.pyplot as plt
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
from plotstyle.specs import registry
|
|
43
|
+
from plotstyle.specs.units import Dimension
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from matplotlib.axes import Axes
|
|
47
|
+
from matplotlib.figure import Figure
|
|
48
|
+
|
|
49
|
+
from plotstyle.specs.schema import JournalSpec
|
|
50
|
+
|
|
51
|
+
__all__: list[str] = [
|
|
52
|
+
"figure",
|
|
53
|
+
"subplots",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Module-level constants
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# The golden ratio φ = (1 + √5) / 2 ≈ 1.618. Used as the default
|
|
61
|
+
# width-to-height aspect ratio; it is widely considered the most visually
|
|
62
|
+
# harmonious rectangle proportion and is a common default in scientific figure
|
|
63
|
+
# guidelines.
|
|
64
|
+
_GOLDEN_RATIO: Final[float] = (1 + 5**0.5) / 2
|
|
65
|
+
|
|
66
|
+
# Valid column-span values accepted by the public API. Stored as a frozenset
|
|
67
|
+
# so membership tests are O(1) and the collection is clearly immutable.
|
|
68
|
+
_VALID_COLUMNS: Final[frozenset[int]] = frozenset({1, 2})
|
|
69
|
+
|
|
70
|
+
# Axes-normalised coordinates for panel label placement. Placing labels
|
|
71
|
+
# slightly outside the axes box (negative x, y > 1) is the dominant
|
|
72
|
+
# convention in multi-panel scientific figures and avoids overlap with axis
|
|
73
|
+
# ticks and tick labels.
|
|
74
|
+
_LABEL_X: Final[float] = -0.1
|
|
75
|
+
_LABEL_Y: Final[float] = 1.05
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Internal helpers
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _validate_columns(columns: int) -> None:
|
|
83
|
+
"""Raise :exc:`ValueError` if *columns* is not a supported span value.
|
|
84
|
+
|
|
85
|
+
Centralising this check avoids duplicating the error message across
|
|
86
|
+
:func:`_resolve_width` and any future callers that accept a *columns*
|
|
87
|
+
parameter.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
columns: Column-span value to validate.
|
|
91
|
+
|
|
92
|
+
Raises
|
|
93
|
+
------
|
|
94
|
+
ValueError: If *columns* is not in ``{1, 2}``.
|
|
95
|
+
"""
|
|
96
|
+
if columns not in _VALID_COLUMNS:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"'columns' must be 1 (single-column) or 2 (double-column), "
|
|
99
|
+
f"got {columns!r}. "
|
|
100
|
+
"Check the journal spec for supported column widths."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resolve_width(journal: str, columns: int) -> float:
|
|
105
|
+
"""Resolve the figure width in inches for a journal and column span.
|
|
106
|
+
|
|
107
|
+
Fetches the journal spec from the registry, selects the appropriate
|
|
108
|
+
physical column width in millimetres, and converts it to inches using
|
|
109
|
+
:class:`~plotstyle.specs.units.Dimension`.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
journal: Journal preset name (e.g. ``"nature"``).
|
|
113
|
+
columns: Column span: ``1`` for single-column width, ``2`` for
|
|
114
|
+
double-column width.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
Figure width in inches.
|
|
119
|
+
|
|
120
|
+
Raises
|
|
121
|
+
------
|
|
122
|
+
ValueError: If *columns* is not ``1`` or ``2``.
|
|
123
|
+
plotstyle.specs.SpecNotFoundError: If *journal* is not registered.
|
|
124
|
+
"""
|
|
125
|
+
_validate_columns(columns)
|
|
126
|
+
|
|
127
|
+
spec: JournalSpec = registry.get(journal)
|
|
128
|
+
|
|
129
|
+
# Select the physical width from the spec based on the column span.
|
|
130
|
+
# Double-column figures span the full text width; single-column figures
|
|
131
|
+
# span only the narrower inset width.
|
|
132
|
+
width_mm: float = (
|
|
133
|
+
spec.dimensions.double_column_mm if columns == 2 else spec.dimensions.single_column_mm
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return Dimension(width_mm, "mm").to_inches()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _format_panel_label(index: int, spec: JournalSpec) -> str:
|
|
140
|
+
"""Format a single panel label string from a zero-based index and spec.
|
|
141
|
+
|
|
142
|
+
The label style (case, parentheses, sentence capitalisation) is driven
|
|
143
|
+
by :attr:`~plotstyle.specs.schema.TypographySpec.panel_label_case` on
|
|
144
|
+
the journal spec.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
index: Zero-based panel index. Index ``0`` maps to the letter
|
|
148
|
+
``"a"`` (or its styled equivalent), index ``1`` to ``"b"``,
|
|
149
|
+
and so on.
|
|
150
|
+
spec: Journal specification containing panel label formatting rules.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
Formatted panel label string (e.g. ``"a"``, ``"(B)"``, ``"A"``).
|
|
155
|
+
|
|
156
|
+
Notes
|
|
157
|
+
-----
|
|
158
|
+
Supported ``panel_label_case`` values and their output:
|
|
159
|
+
|
|
160
|
+
================ =========================================
|
|
161
|
+
Value Output for indices 0, 1, 2
|
|
162
|
+
================ =========================================
|
|
163
|
+
``"lower"`` ``a``, ``b``, ``c``
|
|
164
|
+
``"upper"`` ``A``, ``B``, ``C``
|
|
165
|
+
``"title"`` ``A``, ``B``, ``C`` (alias for upper)
|
|
166
|
+
``"parens_lower"`` ``(a)``, ``(b)``, ``(c)``
|
|
167
|
+
``"parens_upper"`` ``(A)``, ``(B)``, ``(C)``
|
|
168
|
+
``"sentence"`` ``A``, ``b``, ``c`` (first only capitalised)
|
|
169
|
+
================ =========================================
|
|
170
|
+
|
|
171
|
+
Any unrecognised value falls back to ``"lower"``.
|
|
172
|
+
"""
|
|
173
|
+
# Derive the base lowercase letter(s) from the zero-based index.
|
|
174
|
+
# Indices 0-25 map to a-z; indices 26+ produce two-character labels
|
|
175
|
+
# (aa, ab, ..., az, ba, ...) to support figures with more than 26 panels.
|
|
176
|
+
if index < 26:
|
|
177
|
+
letter: str = chr(ord("a") + index)
|
|
178
|
+
else:
|
|
179
|
+
i = index - 26
|
|
180
|
+
letter = chr(ord("a") + i // 26) + chr(ord("a") + i % 26)
|
|
181
|
+
case: str = spec.typography.panel_label_case
|
|
182
|
+
|
|
183
|
+
match case:
|
|
184
|
+
case "upper" | "title":
|
|
185
|
+
return letter.upper()
|
|
186
|
+
case "parens_lower":
|
|
187
|
+
return f"({letter})"
|
|
188
|
+
case "parens_upper":
|
|
189
|
+
return f"({letter.upper()})"
|
|
190
|
+
case "sentence":
|
|
191
|
+
# Sentence case: capitalise only the first panel, mirroring
|
|
192
|
+
# the convention used in journals such as Cell and eLife.
|
|
193
|
+
return letter.upper() if index == 0 else letter
|
|
194
|
+
case _:
|
|
195
|
+
# "lower" is the explicit default; unknown values fall through
|
|
196
|
+
# here rather than raising so that newer spec fields added in
|
|
197
|
+
# future schema versions degrade gracefully.
|
|
198
|
+
return letter
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _add_panel_labels(axes: np.ndarray, spec: JournalSpec) -> None:
|
|
202
|
+
"""Annotate every axes in *axes* with a spec-accurate panel label.
|
|
203
|
+
|
|
204
|
+
Labels are placed at a fixed position just above and to the left of each
|
|
205
|
+
axes bounding box, using the font size and weight prescribed by the
|
|
206
|
+
journal spec. This position (``x=-0.1, y=1.05`` in axes-normalised
|
|
207
|
+
coordinates) is the most common convention across biology and physics
|
|
208
|
+
journals.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
axes: 2-D ``ndarray`` of :class:`~matplotlib.axes.Axes` objects.
|
|
212
|
+
Must support ``.flat`` iteration (i.e. be the output of
|
|
213
|
+
:func:`numpy.atleast_2d` or equivalent).
|
|
214
|
+
spec: Journal specification defining label style, font size, and
|
|
215
|
+
font weight.
|
|
216
|
+
|
|
217
|
+
Notes
|
|
218
|
+
-----
|
|
219
|
+
The function mutates each axes in-place by calling
|
|
220
|
+
:meth:`~matplotlib.axes.Axes.text`. It returns ``None``; callers
|
|
221
|
+
that need to manipulate the label :class:`~matplotlib.text.Text`
|
|
222
|
+
objects after the fact should call :func:`_format_panel_label`
|
|
223
|
+
directly and manage text placement themselves.
|
|
224
|
+
"""
|
|
225
|
+
for idx, ax in enumerate(axes.flat):
|
|
226
|
+
label: str = _format_panel_label(idx, spec)
|
|
227
|
+
ax.text(
|
|
228
|
+
_LABEL_X,
|
|
229
|
+
_LABEL_Y,
|
|
230
|
+
label,
|
|
231
|
+
transform=ax.transAxes,
|
|
232
|
+
fontsize=spec.typography.panel_label_pt,
|
|
233
|
+
fontweight=spec.typography.panel_label_weight,
|
|
234
|
+
va="bottom",
|
|
235
|
+
ha="right",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _compute_figsize(width_in: float, aspect: float | None) -> tuple[float, float]:
|
|
240
|
+
"""Compute ``(width, height)`` in inches given a width and optional aspect.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
width_in: Figure width in inches.
|
|
244
|
+
aspect: Width-to-height ratio. When ``None``, the golden ratio is
|
|
245
|
+
used as the default.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
A ``(width_in, height_in)`` tuple ready to be passed to
|
|
250
|
+
``plt.subplots(figsize=...)``.
|
|
251
|
+
"""
|
|
252
|
+
ratio: float = aspect if aspect is not None else _GOLDEN_RATIO
|
|
253
|
+
return width_in, width_in / ratio
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------------------------
|
|
257
|
+
# Public API
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def figure(
|
|
262
|
+
journal: str,
|
|
263
|
+
*,
|
|
264
|
+
columns: int = 1,
|
|
265
|
+
aspect: float | None = None,
|
|
266
|
+
) -> tuple[Figure, Axes]:
|
|
267
|
+
"""Create a single-axis figure sized to a journal's column width.
|
|
268
|
+
|
|
269
|
+
Resolves the journal spec from the registry, converts the physical column
|
|
270
|
+
width from millimetres to inches, and creates a Matplotlib figure with
|
|
271
|
+
``constrained_layout`` enabled so that labels and titles fit within the
|
|
272
|
+
exported dimensions.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
journal: Journal preset name (e.g. ``"nature"``).
|
|
276
|
+
columns: Column span: ``1`` (default) for single-column width,
|
|
277
|
+
``2`` for double-column (full-text) width.
|
|
278
|
+
aspect: Width-to-height ratio for the figure. Defaults to the
|
|
279
|
+
golden ratio (≈ 1.618) when ``None``.
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
A ``(fig, ax)`` tuple containing the new
|
|
284
|
+
:class:`~matplotlib.figure.Figure` and its single
|
|
285
|
+
:class:`~matplotlib.axes.Axes`.
|
|
286
|
+
|
|
287
|
+
Raises
|
|
288
|
+
------
|
|
289
|
+
ValueError: If *columns* is not ``1`` or ``2``.
|
|
290
|
+
plotstyle.specs.SpecNotFoundError: If *journal* is not registered.
|
|
291
|
+
|
|
292
|
+
Example::
|
|
293
|
+
|
|
294
|
+
import plotstyle
|
|
295
|
+
|
|
296
|
+
fig, ax = plotstyle.figure("nature")
|
|
297
|
+
ax.plot([1, 2, 3], [4, 5, 6])
|
|
298
|
+
plotstyle.savefig(fig, "figure.pdf", journal="nature")
|
|
299
|
+
"""
|
|
300
|
+
width_in: float = _resolve_width(journal, columns)
|
|
301
|
+
figsize: tuple[float, float] = _compute_figsize(width_in, aspect)
|
|
302
|
+
|
|
303
|
+
fig, ax = plt.subplots(figsize=figsize, constrained_layout=True)
|
|
304
|
+
return fig, ax
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def subplots(
|
|
308
|
+
journal: str,
|
|
309
|
+
nrows: int = 1,
|
|
310
|
+
ncols: int = 1,
|
|
311
|
+
*,
|
|
312
|
+
columns: int = 1,
|
|
313
|
+
panels: bool = True,
|
|
314
|
+
aspect: float | None = None,
|
|
315
|
+
) -> tuple[Figure, np.ndarray]:
|
|
316
|
+
"""Create a multi-panel figure sized to a journal's column width.
|
|
317
|
+
|
|
318
|
+
Resolves the journal spec, sizes the figure to the requested column span,
|
|
319
|
+
and optionally annotates each axes with a spec-accurate panel label
|
|
320
|
+
(``a``, ``b``, ``c``, …).
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
journal: Journal preset name (e.g. ``"nature"``).
|
|
324
|
+
nrows: Number of subplot rows. Defaults to ``1``.
|
|
325
|
+
ncols: Number of subplot columns. Defaults to ``1``.
|
|
326
|
+
columns: Column span: ``1`` (default) for single-column width,
|
|
327
|
+
``2`` for double-column (full-text) width.
|
|
328
|
+
panels: When ``True`` (default), annotates each axes with a
|
|
329
|
+
panel label styled according to the journal specification.
|
|
330
|
+
Pass ``False`` to suppress labels entirely.
|
|
331
|
+
aspect: Width-to-height ratio for the whole figure. Defaults to
|
|
332
|
+
the golden ratio (≈ 1.618) when ``None``.
|
|
333
|
+
|
|
334
|
+
Returns
|
|
335
|
+
-------
|
|
336
|
+
A ``(fig, axes)`` tuple where *axes* is always a 2-D
|
|
337
|
+
:class:`numpy.ndarray` of :class:`~matplotlib.axes.Axes` objects
|
|
338
|
+
with shape ``(nrows, ncols)``.
|
|
339
|
+
|
|
340
|
+
Raises
|
|
341
|
+
------
|
|
342
|
+
ValueError: If *columns* is not ``1`` or ``2``.
|
|
343
|
+
plotstyle.specs.SpecNotFoundError: If *journal* is not registered.
|
|
344
|
+
|
|
345
|
+
Notes
|
|
346
|
+
-----
|
|
347
|
+
**Return-shape divergence from Matplotlib** — unlike
|
|
348
|
+
:func:`matplotlib.pyplot.subplots`, this function *always* returns
|
|
349
|
+
a 2-D ``ndarray``, including the ``nrows=1, ncols=1`` case.
|
|
350
|
+
This guarantees that callers can use ``.flat`` iteration and
|
|
351
|
+
``axes[i, j]`` indexing without special-casing the single-panel
|
|
352
|
+
path. Access the bare :class:`~matplotlib.axes.Axes` via
|
|
353
|
+
``axes[0, 0]`` when needed.
|
|
354
|
+
|
|
355
|
+
Example::
|
|
356
|
+
|
|
357
|
+
import plotstyle
|
|
358
|
+
|
|
359
|
+
fig, axes = plotstyle.subplots("nature", nrows=2, ncols=2, columns=2)
|
|
360
|
+
for ax in axes.flat:
|
|
361
|
+
ax.plot([1, 2, 3])
|
|
362
|
+
"""
|
|
363
|
+
spec: JournalSpec = registry.get(journal)
|
|
364
|
+
width_in: float = _resolve_width(journal, columns)
|
|
365
|
+
figsize: tuple[float, float] = _compute_figsize(width_in, aspect)
|
|
366
|
+
|
|
367
|
+
fig, axes_raw = plt.subplots(
|
|
368
|
+
nrows,
|
|
369
|
+
ncols,
|
|
370
|
+
figsize=figsize,
|
|
371
|
+
constrained_layout=True,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Normalise the axes return value to a 2-D ndarray so that panel label
|
|
375
|
+
# placement and downstream callers can always rely on a consistent shape.
|
|
376
|
+
#
|
|
377
|
+
# Matplotlib returns:
|
|
378
|
+
# - a bare Axes when nrows=1 and ncols=1
|
|
379
|
+
# - a 1-D ndarray when nrows=1 xor ncols=1
|
|
380
|
+
# - a 2-D ndarray when nrows > 1 and ncols > 1
|
|
381
|
+
#
|
|
382
|
+
# reshape(nrows, ncols) handles the 1-D case correctly for both
|
|
383
|
+
# nrows>1,ncols=1 and nrows=1,ncols>1; the bare-Axes case requires
|
|
384
|
+
# wrapping in a nested list first so that atleast_2d produces (1, 1).
|
|
385
|
+
if isinstance(axes_raw, np.ndarray):
|
|
386
|
+
axes_2d: np.ndarray = axes_raw.reshape(nrows, ncols)
|
|
387
|
+
else:
|
|
388
|
+
# Single bare Axes — wrap in a 2-D array to yield shape (1, 1).
|
|
389
|
+
axes_2d = np.atleast_2d(np.array(axes_raw))
|
|
390
|
+
|
|
391
|
+
if panels:
|
|
392
|
+
_add_panel_labels(axes_2d, spec)
|
|
393
|
+
|
|
394
|
+
return fig, axes_2d
|