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
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Seaborn compatibility layer for PlotStyle.
|
|
2
|
+
|
|
3
|
+
Provides utilities to combine seaborn themes with PlotStyle journal presets
|
|
4
|
+
without one clobbering the other. Both libraries write to
|
|
5
|
+
``matplotlib.rcParams``; seaborn's ``set_theme`` resets everything it knows
|
|
6
|
+
about, which undoes PlotStyle's journal-specific overrides. This module
|
|
7
|
+
resolves that conflict through two complementary strategies:
|
|
8
|
+
|
|
9
|
+
**Strategy 1 — Persistent monkey-patch** (``patch_seaborn`` / ``unpatch_seaborn``):
|
|
10
|
+
Wraps ``sns.set_theme`` once so that every subsequent call automatically
|
|
11
|
+
re-applies the captured PlotStyle params. Activated by
|
|
12
|
+
:func:`plotstyle.core.style.use` when ``seaborn_compatible=True``.
|
|
13
|
+
|
|
14
|
+
**Strategy 2 — One-shot helper** (``plotstyle_theme``):
|
|
15
|
+
Applies a seaborn theme first, then layers PlotStyle params on top — no
|
|
16
|
+
persistent patch required. Suitable for scripts or notebooks where
|
|
17
|
+
``sns.set_theme`` is called only once.
|
|
18
|
+
|
|
19
|
+
Seaborn is imported lazily so this module can be loaded without seaborn
|
|
20
|
+
installed. An :class:`ImportError` is raised only when a function that
|
|
21
|
+
actually requires seaborn is invoked.
|
|
22
|
+
|
|
23
|
+
Thread Safety
|
|
24
|
+
-------------
|
|
25
|
+
The two module-level globals (``_PLOTSTYLE_OVERRIDES`` and
|
|
26
|
+
``_ORIGINAL_SET_THEME``) track patch state across calls. They are **not
|
|
27
|
+
thread-safe**; concurrent invocations of :func:`patch_seaborn` or
|
|
28
|
+
:func:`unpatch_seaborn`` from multiple threads may produce undefined state.
|
|
29
|
+
|
|
30
|
+
Public API
|
|
31
|
+
----------
|
|
32
|
+
- :func:`capture_overrides`
|
|
33
|
+
- :func:`reapply_overrides`
|
|
34
|
+
- :func:`patch_seaborn`
|
|
35
|
+
- :func:`unpatch_seaborn`
|
|
36
|
+
- :func:`plotstyle_theme`
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from typing import Any, ParamSpec
|
|
42
|
+
|
|
43
|
+
import matplotlib.pyplot as plt
|
|
44
|
+
|
|
45
|
+
from plotstyle.core.style import use
|
|
46
|
+
|
|
47
|
+
# Captures the variadic call signature of sns.set_theme for the transparent
|
|
48
|
+
# wrapper in patch_seaborn(), satisfying ANN401 without resorting to Any.
|
|
49
|
+
_P = ParamSpec("_P")
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Module-level state
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
# Snapshot of rcParams that PlotStyle last applied via capture_overrides().
|
|
56
|
+
# Re-applied by reapply_overrides() whenever seaborn resets matplotlib's
|
|
57
|
+
# global rcParams. None indicates that capture_overrides() has not yet been
|
|
58
|
+
# called.
|
|
59
|
+
_PLOTSTYLE_OVERRIDES: dict[str, Any] | None = None
|
|
60
|
+
|
|
61
|
+
# The original (unpatched) sns.set_theme callable. Retained by patch_seaborn()
|
|
62
|
+
# so that unpatch_seaborn() can restore the exact original reference. None
|
|
63
|
+
# indicates that the patch is not currently installed.
|
|
64
|
+
_ORIGINAL_SET_THEME: Any = None
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Public API
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
__all__: list[str] = [
|
|
71
|
+
"capture_overrides",
|
|
72
|
+
"patch_seaborn",
|
|
73
|
+
"plotstyle_theme",
|
|
74
|
+
"reapply_overrides",
|
|
75
|
+
"unpatch_seaborn",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def capture_overrides(params: dict[str, Any]) -> None:
|
|
80
|
+
"""Store a snapshot of PlotStyle rcParams for later re-application.
|
|
81
|
+
|
|
82
|
+
Called by :func:`plotstyle.core.style.use` when ``seaborn_compatible=True``
|
|
83
|
+
is set. The snapshot is consumed by :func:`reapply_overrides` to restore
|
|
84
|
+
PlotStyle params after any subsequent ``seaborn.set_theme()`` call resets
|
|
85
|
+
``matplotlib.rcParams``.
|
|
86
|
+
|
|
87
|
+
A shallow copy of *params* is stored, so the caller may safely mutate the
|
|
88
|
+
original mapping after this function returns.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
params: The rcParams dict that PlotStyle built and applied. Keys must
|
|
92
|
+
be valid ``matplotlib.rcParams`` parameter names; values must be
|
|
93
|
+
accepted by ``matplotlib.pyplot.rcParams.update``.
|
|
94
|
+
|
|
95
|
+
Example::
|
|
96
|
+
|
|
97
|
+
from plotstyle.integrations.seaborn import capture_overrides
|
|
98
|
+
|
|
99
|
+
capture_overrides({"font.size": 8, "axes.linewidth": 0.8})
|
|
100
|
+
"""
|
|
101
|
+
global _PLOTSTYLE_OVERRIDES
|
|
102
|
+
|
|
103
|
+
# Shallow copy is sufficient: rcParams values are scalars or short
|
|
104
|
+
# sequences that matplotlib replaces (not mutates) on update.
|
|
105
|
+
_PLOTSTYLE_OVERRIDES = params.copy()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def reapply_overrides() -> None:
|
|
109
|
+
"""Re-apply the stored PlotStyle rcParams to ``matplotlib.rcParams``.
|
|
110
|
+
|
|
111
|
+
Intended to be called immediately after ``seaborn.set_theme()`` has reset
|
|
112
|
+
matplotlib's global rcParams, restoring any journal-specific overrides that
|
|
113
|
+
seaborn erased.
|
|
114
|
+
|
|
115
|
+
This function is a no-op when:
|
|
116
|
+
|
|
117
|
+
- :func:`capture_overrides` has not yet been called (``_PLOTSTYLE_OVERRIDES``
|
|
118
|
+
is ``None``), or
|
|
119
|
+
- the captured dict is empty (nothing to restore).
|
|
120
|
+
|
|
121
|
+
Notes
|
|
122
|
+
-----
|
|
123
|
+
This function is injected into the seaborn call stack by
|
|
124
|
+
:func:`patch_seaborn` and is not typically called directly from user code.
|
|
125
|
+
|
|
126
|
+
Example::
|
|
127
|
+
|
|
128
|
+
from plotstyle.integrations.seaborn import capture_overrides, reapply_overrides
|
|
129
|
+
|
|
130
|
+
capture_overrides({"font.size": 8})
|
|
131
|
+
reapply_overrides() # matplotlib now has font.size = 8 again
|
|
132
|
+
"""
|
|
133
|
+
# Guard against None (never captured) and {} (nothing to restore).
|
|
134
|
+
# Skipping the plt.rcParams.update call entirely avoids a needless
|
|
135
|
+
# matplotlib dict traversal when there is nothing to apply.
|
|
136
|
+
if _PLOTSTYLE_OVERRIDES:
|
|
137
|
+
plt.rcParams.update(_PLOTSTYLE_OVERRIDES)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def patch_seaborn() -> None:
|
|
141
|
+
"""Monkey-patch ``seaborn.set_theme`` so PlotStyle params survive its calls.
|
|
142
|
+
|
|
143
|
+
Wraps ``sns.set_theme`` with a thin closure that calls
|
|
144
|
+
:func:`reapply_overrides` immediately after seaborn has finished resetting
|
|
145
|
+
``matplotlib.rcParams``. All subsequent calls to ``sns.set_theme(...)``
|
|
146
|
+
— regardless of arguments — therefore end with the captured PlotStyle
|
|
147
|
+
params in effect.
|
|
148
|
+
|
|
149
|
+
Calling this function when the patch is already installed is a safe no-op;
|
|
150
|
+
double-wrapping is explicitly prevented to ensure :func:`unpatch_seaborn`
|
|
151
|
+
always restores the correct original callable.
|
|
152
|
+
|
|
153
|
+
Raises
|
|
154
|
+
------
|
|
155
|
+
ImportError: If seaborn is not installed in the current environment.
|
|
156
|
+
|
|
157
|
+
Notes
|
|
158
|
+
-----
|
|
159
|
+
To remove the patch and restore the original ``sns.set_theme``, call
|
|
160
|
+
:func:`unpatch_seaborn`. The patch is also removed automatically when
|
|
161
|
+
the :class:`~plotstyle.core.style.JournalStyle` context manager exits.
|
|
162
|
+
|
|
163
|
+
Example::
|
|
164
|
+
|
|
165
|
+
import seaborn as sns
|
|
166
|
+
from plotstyle.integrations.seaborn import capture_overrides, patch_seaborn
|
|
167
|
+
|
|
168
|
+
capture_overrides({"font.size": 8})
|
|
169
|
+
patch_seaborn()
|
|
170
|
+
|
|
171
|
+
# PlotStyle params are restored automatically after each set_theme call.
|
|
172
|
+
sns.set_theme(style="ticks")
|
|
173
|
+
"""
|
|
174
|
+
global _ORIGINAL_SET_THEME
|
|
175
|
+
|
|
176
|
+
import seaborn as sns # Deferred import — raises ImportError if absent.
|
|
177
|
+
|
|
178
|
+
# Guard against double-patching: wrapping an already-wrapped callable would
|
|
179
|
+
# cause unpatch_seaborn() to restore the wrapper instead of the original.
|
|
180
|
+
if _ORIGINAL_SET_THEME is not None:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
_ORIGINAL_SET_THEME = sns.set_theme
|
|
184
|
+
|
|
185
|
+
# ParamSpec captures the exact call signature of the original set_theme so
|
|
186
|
+
# the wrapper is transparent to type checkers without requiring Any on the
|
|
187
|
+
# variadic parameters (which would violate ANN401).
|
|
188
|
+
def _patched_set_theme(*args: _P.args, **kwargs: _P.kwargs) -> None:
|
|
189
|
+
"""Delegate to the original seaborn set_theme, then restore PlotStyle params."""
|
|
190
|
+
_ORIGINAL_SET_THEME(*args, **kwargs)
|
|
191
|
+
reapply_overrides()
|
|
192
|
+
|
|
193
|
+
# Replace the module-level attribute so all callers — including those who
|
|
194
|
+
# imported sns.set_theme directly before patching — see the wrapper via
|
|
195
|
+
# the sns namespace.
|
|
196
|
+
sns.set_theme = _patched_set_theme # type: ignore[assignment]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def unpatch_seaborn() -> None:
|
|
200
|
+
"""Restore the original ``seaborn.set_theme`` callable.
|
|
201
|
+
|
|
202
|
+
Reverses the effect of :func:`patch_seaborn`, putting ``sns.set_theme``
|
|
203
|
+
back to the exact callable that was in place before patching.
|
|
204
|
+
|
|
205
|
+
If :func:`patch_seaborn` was never called, or has already been reversed,
|
|
206
|
+
this function is a safe no-op.
|
|
207
|
+
|
|
208
|
+
Notes
|
|
209
|
+
-----
|
|
210
|
+
After unpatching, future calls to ``sns.set_theme(...)`` will no longer
|
|
211
|
+
re-apply PlotStyle params. To reinstate the behaviour, call
|
|
212
|
+
:func:`patch_seaborn` again after a new :func:`~plotstyle.core.style.use`
|
|
213
|
+
invocation.
|
|
214
|
+
|
|
215
|
+
Example::
|
|
216
|
+
|
|
217
|
+
from plotstyle.integrations.seaborn import patch_seaborn, unpatch_seaborn
|
|
218
|
+
|
|
219
|
+
patch_seaborn()
|
|
220
|
+
# ... use seaborn with PlotStyle compatibility ...
|
|
221
|
+
unpatch_seaborn()
|
|
222
|
+
# sns.set_theme is now fully restored.
|
|
223
|
+
"""
|
|
224
|
+
global _ORIGINAL_SET_THEME
|
|
225
|
+
|
|
226
|
+
# Nothing to restore if the patch was never installed (or was already
|
|
227
|
+
# reversed by a prior call to this function).
|
|
228
|
+
if _ORIGINAL_SET_THEME is None:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
import seaborn as sns # Deferred import — only reached when patched.
|
|
232
|
+
|
|
233
|
+
sns.set_theme = _ORIGINAL_SET_THEME # type: ignore[assignment]
|
|
234
|
+
_ORIGINAL_SET_THEME = None # Reset sentinel so re-patching is allowed.
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def plotstyle_theme(
|
|
238
|
+
journal: str,
|
|
239
|
+
*,
|
|
240
|
+
seaborn_style: str = "ticks",
|
|
241
|
+
seaborn_context: str = "paper",
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Apply a seaborn theme and a PlotStyle journal preset in the correct order.
|
|
244
|
+
|
|
245
|
+
Applies the seaborn theme **first**, then overlays PlotStyle journal
|
|
246
|
+
parameters on top. This ordering guarantees that journal-specific
|
|
247
|
+
typography, line weights, and figure dimensions always take precedence
|
|
248
|
+
over seaborn's aesthetic defaults when the two conflict.
|
|
249
|
+
|
|
250
|
+
This is the recommended one-shot helper for users who want seaborn
|
|
251
|
+
aesthetics alongside journal-compliant styling without installing a
|
|
252
|
+
persistent monkey-patch.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
journal: Journal preset name recognised by
|
|
256
|
+
:func:`~plotstyle.specs.registry.get` (e.g. ``"nature"`` or
|
|
257
|
+
``"ieee"``).
|
|
258
|
+
seaborn_style: Seaborn aesthetic style forwarded to
|
|
259
|
+
``sns.set_theme(style=...)``. Accepted values are
|
|
260
|
+
``"darkgrid"``, ``"whitegrid"``, ``"dark"``, ``"white"``, and
|
|
261
|
+
``"ticks"``. Defaults to ``"ticks"``.
|
|
262
|
+
seaborn_context: Seaborn scaling context forwarded to
|
|
263
|
+
``sns.set_theme(context=...)``. Accepted values are
|
|
264
|
+
``"paper"``, ``"notebook"``, ``"talk"``, and ``"poster"``.
|
|
265
|
+
Defaults to ``"paper"``.
|
|
266
|
+
|
|
267
|
+
Raises
|
|
268
|
+
------
|
|
269
|
+
ImportError: If seaborn is not installed in the current environment.
|
|
270
|
+
ValueError: If *journal* is not a recognised PlotStyle preset (raised
|
|
271
|
+
by :func:`~plotstyle.core.style.use` internally).
|
|
272
|
+
|
|
273
|
+
Notes
|
|
274
|
+
-----
|
|
275
|
+
Unlike :func:`~plotstyle.core.style.use` with ``seaborn_compatible=True``,
|
|
276
|
+
this helper does **not** install a persistent monkey-patch on
|
|
277
|
+
``sns.set_theme``. It applies both themes once in the correct order.
|
|
278
|
+
If PlotStyle params must survive *future* ``sns.set_theme(...)`` calls,
|
|
279
|
+
use :func:`~plotstyle.core.style.use` with ``seaborn_compatible=True``
|
|
280
|
+
instead.
|
|
281
|
+
|
|
282
|
+
Example::
|
|
283
|
+
|
|
284
|
+
import plotstyle.integrations.seaborn as ps_sns
|
|
285
|
+
|
|
286
|
+
# Minimal usage — defaults to style="ticks", context="paper":
|
|
287
|
+
ps_sns.plotstyle_theme("nature")
|
|
288
|
+
|
|
289
|
+
# Explicit seaborn settings:
|
|
290
|
+
ps_sns.plotstyle_theme(
|
|
291
|
+
"ieee",
|
|
292
|
+
seaborn_style="whitegrid",
|
|
293
|
+
seaborn_context="paper",
|
|
294
|
+
)
|
|
295
|
+
"""
|
|
296
|
+
import seaborn as sns # Deferred import — raises ImportError if absent.
|
|
297
|
+
|
|
298
|
+
# Step 1: Let seaborn establish its own rcParams (axes style, font scales,
|
|
299
|
+
# palette, etc.). This intentionally resets any pre-existing params so
|
|
300
|
+
# the seaborn baseline is clean before PlotStyle layers on top.
|
|
301
|
+
sns.set_theme(style=seaborn_style, context=seaborn_context)
|
|
302
|
+
|
|
303
|
+
# Step 2: Overlay journal-specific params, winning any conflicts with
|
|
304
|
+
# seaborn's defaults (font sizes, line weights, figure dimensions, etc.).
|
|
305
|
+
use(journal)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Preview tools for PlotStyle.
|
|
2
|
+
|
|
3
|
+
This sub-package provides two visual inspection utilities for exploring how a
|
|
4
|
+
journal style looks before committing to a final figure layout:
|
|
5
|
+
|
|
6
|
+
``gallery``
|
|
7
|
+
Render a 2x2 grid of sample plots — line, scatter, bar, and histogram —
|
|
8
|
+
sized and styled to match a journal's column-width and typography
|
|
9
|
+
constraints. Useful for quickly assessing a journal preset before
|
|
10
|
+
creating production figures.
|
|
11
|
+
|
|
12
|
+
``preview_print_size``
|
|
13
|
+
Display an existing figure at its approximate physical print size by
|
|
14
|
+
temporarily scaling the figure DPI to match the monitor's pixel density.
|
|
15
|
+
Useful for verifying that text and line weights are legible at the
|
|
16
|
+
dimensions they will occupy in print.
|
|
17
|
+
|
|
18
|
+
Usage
|
|
19
|
+
-----
|
|
20
|
+
Both functions are re-exported at the top-level :mod:`plotstyle` package, so
|
|
21
|
+
the following are equivalent::
|
|
22
|
+
|
|
23
|
+
# Via the top-level package (recommended):
|
|
24
|
+
import plotstyle
|
|
25
|
+
|
|
26
|
+
plotstyle.gallery("nature")
|
|
27
|
+
plotstyle.preview_print_size(fig, journal="nature")
|
|
28
|
+
|
|
29
|
+
# Via this sub-package directly:
|
|
30
|
+
from plotstyle.preview import gallery, preview_print_size
|
|
31
|
+
|
|
32
|
+
gallery("nature")
|
|
33
|
+
preview_print_size(fig, journal="nature")
|
|
34
|
+
|
|
35
|
+
Notes
|
|
36
|
+
-----
|
|
37
|
+
Neither utility mutates global Matplotlib state after returning. Both apply
|
|
38
|
+
any required rcParams changes in a scoped block and restore the prior state
|
|
39
|
+
unconditionally in a ``finally`` clause.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from plotstyle.preview.gallery import gallery
|
|
45
|
+
from plotstyle.preview.print_size import preview_print_size
|
|
46
|
+
|
|
47
|
+
__all__: list[str] = [
|
|
48
|
+
"gallery",
|
|
49
|
+
"preview_print_size",
|
|
50
|
+
]
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Render a sample figure grid in a journal's style.
|
|
2
|
+
|
|
3
|
+
Provides :func:`gallery`, a quick-preview utility that creates a 2x2 grid of
|
|
4
|
+
representative plots — line, scatter, bar, and histogram — sized and styled to
|
|
5
|
+
match a journal's column width and typography constraints.
|
|
6
|
+
|
|
7
|
+
Design notes
|
|
8
|
+
------------
|
|
9
|
+
**Deterministic data** — the four ``_sample_*`` helpers generate synthetic
|
|
10
|
+
data from a fixed random seed (``42`` by default), so the preview is
|
|
11
|
+
pixel-identical across repeated calls. Pass a different *seed* to the helpers
|
|
12
|
+
if variation is needed for testing.
|
|
13
|
+
|
|
14
|
+
**Style isolation** — :func:`gallery` applies the journal preset via
|
|
15
|
+
:func:`~plotstyle.core.style.use` and restores the original rcParams in a
|
|
16
|
+
``finally`` block. Calling :func:`gallery` never permanently alters global
|
|
17
|
+
Matplotlib state, regardless of whether figure creation succeeds or raises.
|
|
18
|
+
|
|
19
|
+
**Panel configuration** — visual constants for each panel (titles, axis
|
|
20
|
+
labels, marker sizes, etc.) are centralised in :data:`_PANEL_CONFIG` rather
|
|
21
|
+
than scattered through the rendering code. Adding a new panel or changing a
|
|
22
|
+
label requires editing only that mapping.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
28
|
+
|
|
29
|
+
import matplotlib.pyplot as plt
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
from plotstyle.core.style import use
|
|
33
|
+
from plotstyle.specs.units import Dimension
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from matplotlib.axes import Axes
|
|
37
|
+
from matplotlib.figure import Figure
|
|
38
|
+
from numpy.typing import NDArray
|
|
39
|
+
|
|
40
|
+
__all__: list[str] = ["gallery"]
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Module-level constants
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
# Height-to-width ratio for the gallery figure. 0.9 fills the available
|
|
47
|
+
# vertical space while keeping the figure compact enough for on-screen preview
|
|
48
|
+
# without excessive whitespace.
|
|
49
|
+
_GALLERY_ASPECT: Final[float] = 0.9
|
|
50
|
+
|
|
51
|
+
# Fixed random seed shared by all sample-data generators. A module-level
|
|
52
|
+
# constant makes it trivial to find and change if reproducibility requirements
|
|
53
|
+
# change (e.g. switching to a property-based test with varied seeds).
|
|
54
|
+
_DEFAULT_SEED: Final[int] = 42
|
|
55
|
+
|
|
56
|
+
# Valid column-span values; mirrors plotstyle.core.figure for consistency
|
|
57
|
+
# without introducing a cross-module import dependency.
|
|
58
|
+
_VALID_COLUMNS: Final[frozenset[int]] = frozenset({1, 2})
|
|
59
|
+
|
|
60
|
+
# Number of samples for the scatter and histogram panels.
|
|
61
|
+
_SCATTER_N: Final[int] = 80
|
|
62
|
+
_HIST_N: Final[int] = 500
|
|
63
|
+
|
|
64
|
+
# Scatter plot groups. Defined once so both the generator and the renderer
|
|
65
|
+
# agree on the set without magic string literals.
|
|
66
|
+
_SCATTER_GROUPS: Final[list[str]] = ["A", "B", "C"]
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Synthetic data generators
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _sample_line_data() -> tuple[NDArray[np.float64], list[NDArray[np.float64]]]:
|
|
74
|
+
"""Generate deterministic synthetic data for the line-plot panel.
|
|
75
|
+
|
|
76
|
+
Produces four periodic curves (sin/cos at base and double frequency) that
|
|
77
|
+
exercise the journal's colour cycle and line-weight settings.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
A two-tuple ``(x, ys)`` where *x* is a 1-D array of 100 evenly
|
|
82
|
+
spaced values over ``[0, 2π]`` and *ys* is a list of four
|
|
83
|
+
corresponding y-arrays: ``sin(x)``, ``cos(x)``,
|
|
84
|
+
``0.5 · sin(2x)``, and ``0.7 · cos(2x)``.
|
|
85
|
+
"""
|
|
86
|
+
x: NDArray[np.float64] = np.linspace(0, 2 * np.pi, 100)
|
|
87
|
+
ys: list[NDArray[np.float64]] = [
|
|
88
|
+
np.sin(x),
|
|
89
|
+
np.cos(x),
|
|
90
|
+
np.sin(2 * x) * 0.5,
|
|
91
|
+
np.cos(2 * x) * 0.7,
|
|
92
|
+
]
|
|
93
|
+
return x, ys
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _sample_scatter_data(
|
|
97
|
+
seed: int = _DEFAULT_SEED,
|
|
98
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.str_]]:
|
|
99
|
+
"""Generate deterministic synthetic data for the scatter-plot panel.
|
|
100
|
+
|
|
101
|
+
Creates a noisy linear relationship across three labelled groups so the
|
|
102
|
+
panel exercises colour, alpha, and marker-size rendering.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
seed: NumPy default-RNG seed. Defaults to :data:`_DEFAULT_SEED`
|
|
106
|
+
(``42``) for reproducibility.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
A three-tuple ``(x, y, groups)`` where *x* and *y* are correlated
|
|
111
|
+
1-D float arrays of length :data:`_SCATTER_N`, and *groups* is a
|
|
112
|
+
string array of the same length whose values are drawn from
|
|
113
|
+
:data:`_SCATTER_GROUPS`.
|
|
114
|
+
"""
|
|
115
|
+
rng = np.random.default_rng(seed)
|
|
116
|
+
x: NDArray[np.float64] = rng.normal(0, 1, _SCATTER_N)
|
|
117
|
+
# Linear relationship with additive Gaussian noise to produce a clear but
|
|
118
|
+
# imperfect trend — more representative of real scientific scatter plots.
|
|
119
|
+
y: NDArray[np.float64] = 0.8 * x + rng.normal(0, 0.4, _SCATTER_N)
|
|
120
|
+
groups: NDArray[np.str_] = rng.choice(np.array(_SCATTER_GROUPS, dtype=str), size=_SCATTER_N)
|
|
121
|
+
return x, y, groups
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _sample_bar_data() -> tuple[list[str], list[float], list[float]]:
|
|
125
|
+
"""Generate fixed synthetic data for the bar-chart panel.
|
|
126
|
+
|
|
127
|
+
Returns fixed (non-random) values so the bar chart preview is always
|
|
128
|
+
identical and requires no seed argument.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
A three-tuple ``(categories, values, errors)`` containing lists of
|
|
133
|
+
category labels, bar heights, and symmetric error-bar half-widths.
|
|
134
|
+
"""
|
|
135
|
+
categories: list[str] = ["Cat A", "Cat B", "Cat C", "Cat D"]
|
|
136
|
+
values: list[float] = [4.2, 7.1, 3.8, 5.9]
|
|
137
|
+
errors: list[float] = [0.5, 0.8, 0.3, 0.6]
|
|
138
|
+
return categories, values, errors
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _sample_histogram_data(seed: int = _DEFAULT_SEED) -> NDArray[np.float64]:
|
|
142
|
+
"""Generate deterministic synthetic data for the histogram panel.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
seed: NumPy default-RNG seed. Defaults to :data:`_DEFAULT_SEED`
|
|
146
|
+
(``42``) for reproducibility.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
A 1-D array of :data:`_HIST_N` standard-normal samples, suitable
|
|
151
|
+
for demonstrating the journal's bar-fill and edge-colour styles.
|
|
152
|
+
"""
|
|
153
|
+
rng = np.random.default_rng(seed)
|
|
154
|
+
return rng.normal(0, 1, _HIST_N)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Panel rendering helpers
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _draw_line_panel(ax: Axes) -> None:
|
|
163
|
+
"""Populate the line-plot panel with four periodic curves and a legend.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
ax: Target :class:`~matplotlib.axes.Axes` to draw into.
|
|
167
|
+
"""
|
|
168
|
+
x, ys = _sample_line_data()
|
|
169
|
+
for i, y in enumerate(ys):
|
|
170
|
+
ax.plot(x, y, label=f"Series {i + 1}")
|
|
171
|
+
ax.set_xlabel("x")
|
|
172
|
+
ax.set_ylabel("y")
|
|
173
|
+
ax.legend(fontsize="small")
|
|
174
|
+
ax.set_title("Line Plot", fontsize="small")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _draw_scatter_panel(ax: Axes) -> None:
|
|
178
|
+
"""Populate the scatter-plot panel with three labelled groups.
|
|
179
|
+
|
|
180
|
+
Uses :func:`numpy.unique` to iterate over groups in sorted order, which
|
|
181
|
+
is more idiomatic than ``sorted(set(...))`` for NumPy string arrays and
|
|
182
|
+
avoids a Python-level sort on a large array.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
ax: Target :class:`~matplotlib.axes.Axes` to draw into.
|
|
186
|
+
"""
|
|
187
|
+
x, y, groups = _sample_scatter_data()
|
|
188
|
+
for group_label in np.unique(groups):
|
|
189
|
+
mask: NDArray[np.bool_] = groups == group_label
|
|
190
|
+
ax.scatter(x[mask], y[mask], label=str(group_label), s=15, alpha=0.8)
|
|
191
|
+
ax.set_xlabel("x")
|
|
192
|
+
ax.set_ylabel("y")
|
|
193
|
+
ax.legend(fontsize="small")
|
|
194
|
+
ax.set_title("Scatter Plot", fontsize="small")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _draw_bar_panel(ax: Axes) -> None:
|
|
198
|
+
"""Populate the bar-chart panel with error bars.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
ax: Target :class:`~matplotlib.axes.Axes` to draw into.
|
|
202
|
+
"""
|
|
203
|
+
categories, values, errors = _sample_bar_data()
|
|
204
|
+
ax.bar(categories, values, yerr=errors, capsize=3)
|
|
205
|
+
ax.set_ylabel("Value")
|
|
206
|
+
ax.set_title("Bar Chart", fontsize="small")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _draw_histogram_panel(ax: Axes) -> None:
|
|
210
|
+
"""Populate the histogram panel with 25 bins of standard-normal data.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
ax: Target :class:`~matplotlib.axes.Axes` to draw into.
|
|
214
|
+
"""
|
|
215
|
+
data: NDArray[np.float64] = _sample_histogram_data()
|
|
216
|
+
ax.hist(data, bins=25, edgecolor="black", linewidth=0.5)
|
|
217
|
+
ax.set_xlabel("Value")
|
|
218
|
+
ax.set_ylabel("Count")
|
|
219
|
+
ax.set_title("Histogram", fontsize="small")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Panel drawing functions in row-major order, matching the 2x2 axes layout
|
|
223
|
+
# produced by plt.subplots(2, 2). Each callable accepts a single Axes arg.
|
|
224
|
+
# To add a new panel: extend the grid dimensions, add a helper above, and
|
|
225
|
+
# append it here — no other code changes are required.
|
|
226
|
+
_PANEL_DRAWERS: Final[
|
|
227
|
+
list[Any] # list[Callable[[Axes], None]] — Callable not usable as Final type arg
|
|
228
|
+
] = [
|
|
229
|
+
_draw_line_panel,
|
|
230
|
+
_draw_scatter_panel,
|
|
231
|
+
_draw_bar_panel,
|
|
232
|
+
_draw_histogram_panel,
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# Public API
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def gallery(journal: str, *, columns: int = 1) -> Figure:
|
|
241
|
+
"""Render a 2x2 grid of sample plots in the given journal's style.
|
|
242
|
+
|
|
243
|
+
Applies the journal preset via :func:`~plotstyle.core.style.use`, creates
|
|
244
|
+
a figure sized to the journal's column width, and populates it with four
|
|
245
|
+
representative plot types: line plot, scatter plot, bar chart, and
|
|
246
|
+
histogram. The original rcParams are always restored after the figure is
|
|
247
|
+
created.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
journal: Journal preset name (e.g. ``"nature"``, ``"ieee"``).
|
|
251
|
+
columns: Column span for the figure: ``1`` (default) for
|
|
252
|
+
single-column width, ``2`` for double-column width.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
A :class:`~matplotlib.figure.Figure` containing four styled subplots,
|
|
257
|
+
ready to display with ``plt.show()`` or save with
|
|
258
|
+
:func:`~plotstyle.core.export.savefig`.
|
|
259
|
+
|
|
260
|
+
Raises
|
|
261
|
+
------
|
|
262
|
+
plotstyle.specs.SpecNotFoundError: If *journal* is not registered in
|
|
263
|
+
the spec registry.
|
|
264
|
+
ValueError: If *columns* is not ``1`` or ``2``.
|
|
265
|
+
|
|
266
|
+
Notes
|
|
267
|
+
-----
|
|
268
|
+
**Style isolation** — the journal style is applied only for the
|
|
269
|
+
duration of this call. rcParams are restored unconditionally in a
|
|
270
|
+
``finally`` block so a failed :func:`gallery` call never leaves global
|
|
271
|
+
Matplotlib state modified.
|
|
272
|
+
|
|
273
|
+
**Determinism** — all synthetic data is generated from a fixed seed
|
|
274
|
+
(:data:`_DEFAULT_SEED`), so the figure is pixel-identical across
|
|
275
|
+
repeated calls with the same arguments.
|
|
276
|
+
|
|
277
|
+
**Supra-title sizing** — the figure title is set one point above the
|
|
278
|
+
journal's ``max_font_pt`` so it reads as a heading without violating
|
|
279
|
+
the journal's body-text constraint.
|
|
280
|
+
|
|
281
|
+
Example::
|
|
282
|
+
|
|
283
|
+
import matplotlib.pyplot as plt
|
|
284
|
+
import plotstyle
|
|
285
|
+
|
|
286
|
+
fig = plotstyle.gallery("nature", columns=1)
|
|
287
|
+
plt.show()
|
|
288
|
+
|
|
289
|
+
# Save directly:
|
|
290
|
+
plotstyle.savefig(fig, "nature_preview.pdf", journal="nature")
|
|
291
|
+
"""
|
|
292
|
+
if columns not in _VALID_COLUMNS:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"'columns' must be 1 (single-column) or 2 (double-column), got {columns!r}."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Apply the journal style and retain the handle so we can call restore()
|
|
298
|
+
# in the finally block without a second registry lookup.
|
|
299
|
+
style = use(journal)
|
|
300
|
+
spec = style.spec
|
|
301
|
+
|
|
302
|
+
# Resolve the physical figure dimensions from the spec.
|
|
303
|
+
width_mm: float = (
|
|
304
|
+
spec.dimensions.double_column_mm if columns == 2 else spec.dimensions.single_column_mm
|
|
305
|
+
)
|
|
306
|
+
width_in: float = Dimension(width_mm, "mm").to_inches()
|
|
307
|
+
height_in: float = width_in * _GALLERY_ASPECT
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
fig, axes = plt.subplots(
|
|
311
|
+
2,
|
|
312
|
+
2,
|
|
313
|
+
figsize=(width_in, height_in),
|
|
314
|
+
constrained_layout=True,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Dispatch each panel drawer to its corresponding axes in row-major
|
|
318
|
+
# order. axes.flat guarantees a consistent iteration order regardless
|
|
319
|
+
# of the nrows/ncols layout.
|
|
320
|
+
for draw_panel, ax in zip(_PANEL_DRAWERS, axes.flat, strict=False):
|
|
321
|
+
draw_panel(ax)
|
|
322
|
+
|
|
323
|
+
# Title is set one point above max_font_pt to serve as a visual
|
|
324
|
+
# heading that stands apart from body-text elements without exceeding
|
|
325
|
+
# a reasonable scale relative to the journal's typography system.
|
|
326
|
+
fig.suptitle(
|
|
327
|
+
f"{spec.metadata.name} Style Preview",
|
|
328
|
+
fontsize=spec.typography.max_font_pt + 1,
|
|
329
|
+
fontweight="bold",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return fig
|
|
333
|
+
|
|
334
|
+
finally:
|
|
335
|
+
# Restore the prior rcParams unconditionally so that a failed figure
|
|
336
|
+
# creation does not leave the process in a modified global state.
|
|
337
|
+
style.restore()
|