cplots 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.
cplots/__init__.py ADDED
@@ -0,0 +1,85 @@
1
+ """cplots: a backend-agnostic cosmological plotting library.
2
+
3
+ Three-layer architecture:
4
+ PlotData (immutable input) → PlotSpec IR → Backend (renders native figure)
5
+
6
+ Backends (matplotlib, plotly, altair) are selected per call, via context
7
+ manager, or globally. Third-party backends register via entry points.
8
+
9
+ Example:
10
+ >>> import numpy as np
11
+ >>> import cplots
12
+ >>> k = np.logspace(-3, 0, 200)
13
+ >>> pk = k ** -1.5
14
+ >>> cplots.PKSpectrum(k, pk, label="P(k)").show()
15
+ """
16
+
17
+ # Trigger backend registration by importing the backends package.
18
+ import cplots.backends # noqa: E402, F401
19
+ from cplots.config import backend, resolve_backend, set_backend
20
+ from cplots.core.base import Plot
21
+ from cplots.core.data import (
22
+ BackgroundData,
23
+ ClData,
24
+ PKData,
25
+ PlotData,
26
+ TriangleData,
27
+ XYData,
28
+ XYZData,
29
+ )
30
+ from cplots.core.incremental import (
31
+ build_incrementally,
32
+ incremental,
33
+ labeled_prefixes,
34
+ sequence_prefixes,
35
+ )
36
+ from cplots.core.protocols import BackendProtocol
37
+ from cplots.core.registry import get_backend, register_backend
38
+ from cplots.core.spec import FigureSpec, PanelSpec, PlotSpec, Trace
39
+ from cplots.core.theme import DARK, PUBLICATION, ThemeSpec
40
+ from cplots.data.adapters import to_plot_data
41
+ from cplots.figure import Figure
42
+ from cplots.plots.background import BackgroundEvolution
43
+ from cplots.plots.power_spectrum import ClSpectrum, PKSpectrum, PowerSpectrumBase
44
+ from cplots.plots.triangle import TrianglePlot
45
+ from cplots.plots.xy import XYPlot
46
+ from cplots.plots.xyz_colored import XYZColored
47
+ from cplots.plots.xyz_colored_grid import XYZColoredGrid
48
+
49
+ __all__ = [
50
+ "DARK",
51
+ "Figure",
52
+ "FigureSpec",
53
+ "PanelSpec",
54
+ "PUBLICATION",
55
+ "BackendProtocol",
56
+ "BackgroundData",
57
+ "BackgroundEvolution",
58
+ "ClData",
59
+ "ClSpectrum",
60
+ "PKData",
61
+ "PKSpectrum",
62
+ "Plot",
63
+ "PlotData",
64
+ "PlotSpec",
65
+ "PowerSpectrumBase",
66
+ "ThemeSpec",
67
+ "Trace",
68
+ "TriangleData",
69
+ "TrianglePlot",
70
+ "XYData",
71
+ "XYPlot",
72
+ "XYZColored",
73
+ "XYZColoredGrid",
74
+ "XYZData",
75
+ "backend",
76
+ "build_incrementally",
77
+ "get_backend",
78
+ "incremental",
79
+ "labeled_prefixes",
80
+ "register_backend",
81
+ "resolve_backend",
82
+ "sequence_prefixes",
83
+ "set_backend",
84
+ "to_plot_data",
85
+ ]
@@ -0,0 +1,4 @@
1
+ from cplots.backends._base import BaseBackend
2
+ from cplots.backends.matplotlib import MatplotlibBackend
3
+
4
+ __all__ = ["BaseBackend", "MatplotlibBackend"]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class BaseBackend:
7
+ """Optional base class for backends that want automatic registration.
8
+
9
+ Subclass with the ``name`` keyword to self-register on class creation::
10
+
11
+ class MyBackend(BaseBackend, name="mybackend"):
12
+ def render(self, spec): ...
13
+ ...
14
+
15
+ The ``name`` keyword triggers :func:`~cplots.core.registry.register_backend`
16
+ at class-definition time so no manual registration call is required.
17
+ """
18
+
19
+ def __init_subclass__(cls, name: str | None = None, **kw: Any) -> None:
20
+ super().__init_subclass__(**kw)
21
+ if name is not None:
22
+ from cplots.core.registry import register_backend
23
+
24
+ register_backend(name, cls())
25
+
26
+ def render_figure(self, spec: Any) -> Any:
27
+ """Render a multi-panel FigureSpec.
28
+
29
+ Raises:
30
+ NotImplementedError: Subclasses that support multi-panel layouts
31
+ must override this method.
32
+ """
33
+ raise NotImplementedError(
34
+ f"{type(self).__name__} does not implement render_figure(). "
35
+ "Multi-panel layouts require a backend that supports FigureSpec."
36
+ )
@@ -0,0 +1,296 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from cplots.backends._base import BaseBackend
7
+ from cplots.core.labels import strip_latex
8
+ from cplots.core.spec import FigureSpec, PlotSpec
9
+ from cplots.core.theme import ThemeSpec
10
+
11
+ if TYPE_CHECKING:
12
+ import altair as alt
13
+
14
+
15
+ def _css_color(color: str) -> str:
16
+ """Normalise matplotlib's 'none' to a CSS-compatible transparent value."""
17
+ return "transparent" if color == "none" else color
18
+
19
+
20
+ class AltairBackend(BaseBackend, name="altair"):
21
+ """Altair (Vega-Lite) backend for cplots.
22
+
23
+ Produces ``altair.Chart`` objects. Registered automatically under the name
24
+ ``"altair"``.
25
+
26
+ Note:
27
+ Rowspan > 1 in multi-panel layouts is not supported; use the matplotlib
28
+ or plotly backend for spanning layouts.
29
+
30
+ Requires: ``pip install cplots[altair]``
31
+ """
32
+
33
+ @staticmethod
34
+ def _fmt(text: str, spec: PlotSpec) -> str:
35
+ return text if spec.latex_labels else strip_latex(text)
36
+
37
+ def __init__(self) -> None:
38
+ try:
39
+ import altair # noqa: F401
40
+ except ImportError as exc:
41
+ raise ImportError(
42
+ "The altair backend requires altair. "
43
+ "Install it with: pip install cplots[altair]"
44
+ ) from exc
45
+
46
+ def render(self, spec: PlotSpec, *, interactive: bool = True) -> alt.Chart:
47
+ """Render a single PlotSpec to an Altair Chart.
48
+
49
+ Args:
50
+ spec: Backend-agnostic plot specification.
51
+ interactive: Whether to call ``.interactive()`` on the chart,
52
+ enabling pan and zoom in Jupyter notebooks.
53
+
54
+ Returns:
55
+ An ``altair.Chart`` (or layered chart for multi-trace plots).
56
+ """
57
+ import altair as alt
58
+ import pandas as pd
59
+
60
+ x_label = self._fmt(spec.x_label, spec)
61
+ y_label = self._fmt(spec.y_label, spec)
62
+ title = self._fmt(spec.title, spec)
63
+
64
+ if spec.colormap is not None:
65
+ chart = self._render_colormap(spec, x_label, y_label)
66
+ else:
67
+ layers = []
68
+ colors = spec.theme.palette if spec.theme else None
69
+ for i, trace in enumerate(spec.traces):
70
+ df = pd.DataFrame({"x": trace.x, "y": trace.y})
71
+ color = colors[i % len(colors)] if colors else alt.Undefined
72
+ label = self._fmt(trace.label, spec) if trace.label else None
73
+ layer = (
74
+ alt.Chart(df)
75
+ .mark_line()
76
+ .encode(
77
+ x=alt.X("x", scale=alt.Scale(type=spec.x_scale), title=x_label),
78
+ y=alt.Y("y", scale=alt.Scale(type=spec.y_scale), title=y_label),
79
+ color=alt.value(color) if isinstance(color, str) else alt.Undefined,
80
+ tooltip=["x", "y"],
81
+ )
82
+ )
83
+ if label:
84
+ layer = layer.properties(title=label)
85
+ layers.append(layer)
86
+ chart = alt.layer(*layers)
87
+
88
+ props: dict[str, Any] = {}
89
+ if title:
90
+ props["title"] = title
91
+ if spec.figsize:
92
+ props["width"] = int(spec.figsize[0] * 96)
93
+ props["height"] = int(spec.figsize[1] * 96)
94
+ if props:
95
+ chart = chart.properties(**props)
96
+ if interactive:
97
+ chart = chart.interactive()
98
+ return chart
99
+
100
+ def _render_colormap(
101
+ self,
102
+ spec: PlotSpec,
103
+ x_label: str,
104
+ y_label: str,
105
+ *,
106
+ color_domain: list[float] | None = None,
107
+ show_legend: bool = True,
108
+ gradient_length: int | None = None,
109
+ ) -> Any:
110
+ import altair as alt
111
+ import pandas as pd
112
+
113
+ frames = []
114
+ for trace in spec.traces:
115
+ df = pd.DataFrame({
116
+ "x": trace.x,
117
+ "y": trace.y,
118
+ "z": trace.color_value,
119
+ "label": self._fmt(trace.label, spec) if trace.label else "",
120
+ })
121
+ frames.append(df)
122
+ data = pd.concat(frames, ignore_index=True)
123
+
124
+ chart_height = gradient_length or (int(spec.figsize[1] * 96) if spec.figsize else 300)
125
+ legend = alt.Legend(
126
+ title=self._fmt(spec.z_label, spec) if spec.z_label else None,
127
+ gradientLength=chart_height,
128
+ titleOrient="bottom",
129
+ )
130
+ scale_kwargs: dict[str, Any] = {"scheme": spec.colormap}
131
+ if color_domain is not None:
132
+ scale_kwargs["domain"] = color_domain
133
+ color_enc = alt.Color(
134
+ "z:Q",
135
+ scale=alt.Scale(**scale_kwargs),
136
+ legend=legend if (spec.colorbar and show_legend) else None,
137
+ )
138
+ return (
139
+ alt.Chart(data)
140
+ .mark_line()
141
+ .encode(
142
+ x=alt.X("x:Q", scale=alt.Scale(type=spec.x_scale), title=x_label),
143
+ y=alt.Y("y:Q", scale=alt.Scale(type=spec.y_scale), title=y_label),
144
+ color=color_enc,
145
+ detail="label:N",
146
+ tooltip=["x:Q", "y:Q", "z:Q"],
147
+ )
148
+ )
149
+
150
+ def _render_panel(
151
+ self,
152
+ spec: PlotSpec,
153
+ *,
154
+ interactive: bool = True,
155
+ panel_width: int | None = None,
156
+ panel_height: int | None = None,
157
+ ) -> Any:
158
+ """Render a single PlotSpec as an Altair chart.
159
+
160
+ ``panel_width`` / ``panel_height`` override the chart size when the
161
+ caller (e.g. ``render_figure``) wants to distribute a shared figsize
162
+ across panels. Size must be set on each individual chart rather than
163
+ on the combined ``hconcat``/``vconcat`` container, which does not
164
+ accept ``width``/``height`` properties.
165
+ """
166
+ chart = self.render(spec, interactive=interactive)
167
+ props: dict[str, Any] = {}
168
+ if panel_width is not None:
169
+ props["width"] = panel_width
170
+ if panel_height is not None:
171
+ props["height"] = panel_height
172
+ if props:
173
+ chart = chart.properties(**props)
174
+ return chart
175
+
176
+ def render_figure(self, spec: FigureSpec) -> Any:
177
+ """Render a multi-panel FigureSpec as an Altair hconcat/vconcat chart.
178
+
179
+ Args:
180
+ spec: Backend-agnostic multi-panel figure specification.
181
+
182
+ Returns:
183
+ An ``altair.Chart``, ``altair.HConcatChart``, or
184
+ ``altair.VConcatChart`` depending on layout.
185
+
186
+ Raises:
187
+ NotImplementedError: If any panel has ``rowspan > 1``.
188
+ """
189
+ import altair as alt
190
+
191
+ if spec.theme is not None:
192
+ self.apply_theme(spec.theme)
193
+
194
+ for panel in spec.panels:
195
+ if panel.rowspan > 1:
196
+ raise NotImplementedError(
197
+ "The Altair backend does not support rowspan > 1. "
198
+ "Use the matplotlib or plotly backend for spanning layouts."
199
+ )
200
+
201
+ # Distribute figsize across panels; size is set per-chart because
202
+ # HConcatChart / VConcatChart do not accept width/height properties.
203
+ panel_w = int(spec.figsize[0] * 96 / spec.ncols) if spec.figsize else None
204
+ panel_h = int(spec.figsize[1] * 96 / spec.nrows) if spec.figsize else None
205
+
206
+ if spec.colormap is not None:
207
+ z_vals = [
208
+ t.color_value
209
+ for p in spec.panels
210
+ for t in p.plot.traces
211
+ if t.color_value is not None
212
+ ]
213
+ vmin, vmax = min(z_vals), max(z_vals)
214
+ domain = [vmin, vmax]
215
+ n_panels = len(spec.panels)
216
+ panel_counter = 0
217
+
218
+ rows: list[Any] = []
219
+ for r in range(spec.nrows):
220
+ row_panels = sorted(
221
+ [p for p in spec.panels if p.row == r], key=lambda p: p.col
222
+ )
223
+ charts: list[Any] = []
224
+ for panel in row_panels:
225
+ p_spec = panel.plot
226
+ x_label = self._fmt(p_spec.x_label, p_spec)
227
+ y_label = self._fmt(p_spec.y_label, p_spec)
228
+ is_last = panel_counter == n_panels - 1
229
+ chart = self._render_colormap(
230
+ p_spec,
231
+ x_label,
232
+ y_label,
233
+ color_domain=domain,
234
+ show_legend=is_last and spec.colorbar,
235
+ gradient_length=panel_h,
236
+ )
237
+ props: dict[str, Any] = {}
238
+ if panel_w is not None:
239
+ props["width"] = panel_w
240
+ if panel_h is not None:
241
+ props["height"] = panel_h
242
+ if props:
243
+ chart = chart.properties(**props)
244
+ charts.append(chart)
245
+ panel_counter += 1
246
+ rows.append(charts[0] if len(charts) == 1 else alt.hconcat(*charts))
247
+
248
+ return rows[0] if len(rows) == 1 else alt.vconcat(*rows)
249
+
250
+ rows_plain: list[Any] = []
251
+ for r in range(spec.nrows):
252
+ row_panels = sorted(
253
+ [p for p in spec.panels if p.row == r], key=lambda p: p.col
254
+ )
255
+ charts_plain = [
256
+ self._render_panel(p.plot, panel_width=panel_w, panel_height=panel_h)
257
+ for p in row_panels
258
+ ]
259
+ rows_plain.append(
260
+ charts_plain[0] if len(charts_plain) == 1 else alt.hconcat(*charts_plain)
261
+ )
262
+
263
+ return rows_plain[0] if len(rows_plain) == 1 else alt.vconcat(*rows_plain)
264
+
265
+ def show(self, fig: alt.Chart, **kwargs: Any) -> None:
266
+ fig.show(**kwargs)
267
+
268
+ def save(
269
+ self,
270
+ fig: alt.Chart,
271
+ path: str | os.PathLike[str],
272
+ **kwargs: Any,
273
+ ) -> None:
274
+ fig.save(str(path), **kwargs)
275
+
276
+ def apply_theme(self, theme: ThemeSpec) -> None:
277
+ """Register and enable a custom Vega-Lite theme from a ThemeSpec.
278
+
279
+ Args:
280
+ theme: Theme to apply. Sets background color, font family, and
281
+ axis label font sizes via the Vega-Lite theme config.
282
+ """
283
+ import altair as alt
284
+
285
+ alt.themes.register(
286
+ "cplots_custom",
287
+ lambda: {
288
+ "config": {
289
+ "background": _css_color(theme.background_color),
290
+ "font": theme.font_family,
291
+ "axisX": {"labelFontSize": theme.font_size},
292
+ "axisY": {"labelFontSize": theme.font_size},
293
+ }
294
+ },
295
+ )
296
+ alt.themes.enable("cplots_custom")
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from cplots.backends._base import BaseBackend
7
+ from cplots.core.spec import FigureSpec, PlotSpec
8
+ from cplots.core.theme import ThemeSpec
9
+
10
+ if TYPE_CHECKING:
11
+ import matplotlib.axes
12
+ import matplotlib.figure
13
+
14
+
15
+ class MatplotlibBackend(BaseBackend, name="matplotlib"):
16
+ """Matplotlib backend for cplots.
17
+
18
+ Produces ``matplotlib.figure.Figure`` objects. Registered automatically
19
+ under the name ``"matplotlib"``.
20
+
21
+ Requires: ``pip install cplots[matplotlib]``
22
+ """
23
+
24
+ def __init__(self) -> None:
25
+ try:
26
+ import matplotlib as mpl # noqa: F401
27
+ except ImportError as exc:
28
+ raise ImportError(
29
+ "The matplotlib backend requires matplotlib. "
30
+ "Install it with: pip install cplots[matplotlib]"
31
+ ) from exc
32
+
33
+ def _draw_panel(
34
+ self,
35
+ fig: matplotlib.figure.Figure,
36
+ ax: matplotlib.axes.Axes,
37
+ spec: PlotSpec,
38
+ theme: ThemeSpec | None,
39
+ ) -> None:
40
+ """Draw a single PlotSpec onto an existing Axes (palette and colormap paths)."""
41
+ bg = theme.background_color if theme else "white"
42
+ ax.set_facecolor(bg)
43
+ if spec.colormap is not None:
44
+ self._render_colormap(fig, ax, spec)
45
+ else:
46
+ colors = theme.palette if theme else None
47
+ for i, trace in enumerate(spec.traces):
48
+ color = colors[i % len(colors)] if colors else None
49
+ ax.plot(trace.x, trace.y, label=trace.label or None, color=color, **trace.style)
50
+ if any(t.label for t in spec.traces):
51
+ ax.legend()
52
+ ax.set_xscale(spec.x_scale)
53
+ ax.set_yscale(spec.y_scale)
54
+ if spec.x_label:
55
+ ax.set_xlabel(spec.x_label)
56
+ if spec.y_label:
57
+ ax.set_ylabel(spec.y_label)
58
+ if spec.title:
59
+ ax.set_title(spec.title)
60
+
61
+ def _draw_colormap_lines(
62
+ self,
63
+ ax: matplotlib.axes.Axes,
64
+ spec: PlotSpec,
65
+ norm: Any,
66
+ cmap: Any,
67
+ ) -> None:
68
+ """Draw colormap traces using pre-computed global norm/cmap (no colorbar)."""
69
+ for trace in spec.traces:
70
+ color = cmap(norm(trace.color_value)) if trace.color_value is not None else None
71
+ ax.plot(trace.x, trace.y, label=trace.label or None, color=color, **trace.style)
72
+ ax.set_xscale(spec.x_scale)
73
+ ax.set_yscale(spec.y_scale)
74
+ if spec.x_label:
75
+ ax.set_xlabel(spec.x_label)
76
+ if spec.y_label:
77
+ ax.set_ylabel(spec.y_label)
78
+ if spec.title:
79
+ ax.set_title(spec.title)
80
+
81
+ def render(self, spec: PlotSpec) -> matplotlib.figure.Figure:
82
+ """Render a single PlotSpec to a matplotlib Figure.
83
+
84
+ Args:
85
+ spec: Backend-agnostic plot specification.
86
+
87
+ Returns:
88
+ A closed ``matplotlib.figure.Figure`` (call ``show()`` to display).
89
+ """
90
+ import matplotlib.pyplot as plt
91
+
92
+ # Apply theme to rcParams before creating the figure so that newly
93
+ # created Text/Tick/Axis objects inherit the right defaults.
94
+ if spec.theme is not None:
95
+ self.apply_theme(spec.theme)
96
+
97
+ fig, ax = plt.subplots(figsize=spec.figsize)
98
+
99
+ # Set backgrounds directly on the objects — rcParams alone won't
100
+ # retroactively update a figure that has already been created.
101
+ bg = spec.theme.background_color if spec.theme else "white"
102
+ fig.patch.set_facecolor(bg)
103
+ self._draw_panel(fig, ax, spec, spec.theme)
104
+
105
+ plt.close(fig)
106
+ return fig
107
+
108
+ def _render_colormap(self, fig: Any, ax: Any, spec: PlotSpec) -> None:
109
+ import matplotlib.cm as cm
110
+ import matplotlib.colors as mcolors
111
+ import matplotlib.pyplot as plt
112
+
113
+ z_values = [t.color_value for t in spec.traces if t.color_value is not None]
114
+ vmin, vmax = min(z_values), max(z_values)
115
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
116
+ cmap = plt.get_cmap(spec.colormap)
117
+
118
+ for trace in spec.traces:
119
+ color = cmap(norm(trace.color_value)) if trace.color_value is not None else None
120
+ ax.plot(trace.x, trace.y, label=trace.label or None, color=color, **trace.style)
121
+
122
+ if spec.colorbar:
123
+ sm = cm.ScalarMappable(norm=norm, cmap=cmap)
124
+ sm.set_array([])
125
+ pad = spec.colorbar_pad if spec.colorbar_pad is not None else 0.02
126
+ fig.colorbar(sm, ax=ax, label=spec.z_label or None, pad=pad)
127
+
128
+ def render_figure(self, spec: FigureSpec) -> matplotlib.figure.Figure:
129
+ """Render a multi-panel FigureSpec to a matplotlib Figure.
130
+
131
+ Args:
132
+ spec: Backend-agnostic multi-panel figure specification.
133
+
134
+ Returns:
135
+ A closed ``matplotlib.figure.Figure``.
136
+ """
137
+ import matplotlib.gridspec as mgridspec
138
+ import matplotlib.pyplot as plt
139
+
140
+ if spec.theme is not None:
141
+ self.apply_theme(spec.theme)
142
+
143
+ bg = spec.theme.background_color if spec.theme else "white"
144
+ fig = plt.figure(figsize=spec.figsize)
145
+ fig.patch.set_facecolor(bg)
146
+
147
+ gs = mgridspec.GridSpec(
148
+ spec.nrows,
149
+ spec.ncols,
150
+ width_ratios=spec.width_ratios,
151
+ height_ratios=spec.height_ratios,
152
+ figure=fig,
153
+ )
154
+
155
+ # Pre-compute global colormap normalization for the shared colorbar path.
156
+ shared_norm: Any = None
157
+ shared_cmap: Any = None
158
+ if spec.colormap is not None:
159
+ import matplotlib.colors as mcolors
160
+
161
+ z_vals = [
162
+ t.color_value
163
+ for p in spec.panels
164
+ for t in p.plot.traces
165
+ if t.color_value is not None
166
+ ]
167
+ vmin, vmax = min(z_vals), max(z_vals)
168
+ shared_norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
169
+ shared_cmap = plt.get_cmap(spec.colormap)
170
+
171
+ col_x_master: dict[int, matplotlib.axes.Axes] = {}
172
+ row_y_master: dict[int, matplotlib.axes.Axes] = {}
173
+ all_axes: list[matplotlib.axes.Axes] = []
174
+
175
+ for panel in spec.panels:
176
+ r, c, rs, cs = panel.row, panel.col, panel.rowspan, panel.colspan
177
+ share_x = col_x_master.get(c) if spec.sharex else None
178
+ share_y = row_y_master.get(r) if spec.sharey else None
179
+ ax = fig.add_subplot(gs[r : r + rs, c : c + cs], sharex=share_x, sharey=share_y)
180
+ col_x_master.setdefault(c, ax)
181
+ row_y_master.setdefault(r, ax)
182
+ ax.set_facecolor(bg)
183
+ all_axes.append(ax)
184
+
185
+ if shared_norm is not None:
186
+ self._draw_colormap_lines(ax, panel.plot, shared_norm, shared_cmap)
187
+ else:
188
+ self._draw_panel(fig, ax, panel.plot, spec.theme)
189
+
190
+ if shared_norm is not None and spec.colorbar:
191
+ import matplotlib.cm as cm
192
+
193
+ sm = cm.ScalarMappable(norm=shared_norm, cmap=shared_cmap)
194
+ sm.set_array([])
195
+ pad = spec.colorbar_pad if spec.colorbar_pad is not None else 0.02
196
+ fig.colorbar(sm, ax=all_axes, label=spec.z_label or None, pad=pad)
197
+
198
+ plt.close(fig)
199
+ return fig
200
+
201
+ def show(self, fig: matplotlib.figure.Figure, **kwargs: Any) -> None:
202
+ import matplotlib.pyplot as plt
203
+
204
+ plt.figure(fig.number)
205
+ plt.show(**kwargs)
206
+
207
+ def save(
208
+ self,
209
+ fig: matplotlib.figure.Figure,
210
+ path: str | os.PathLike[str],
211
+ **kwargs: Any,
212
+ ) -> None:
213
+ fig.savefig(path, **kwargs)
214
+
215
+ def apply_theme(self, theme: ThemeSpec) -> None:
216
+ """Apply a ThemeSpec to matplotlib's global rcParams.
217
+
218
+ Args:
219
+ theme: Theme to apply. ``theme.extra`` keys are applied directly
220
+ to ``mpl.rcParams``. rcParams are reset to defaults first so
221
+ previous theme settings do not bleed into the current render.
222
+ """
223
+ import matplotlib as mpl
224
+
225
+ # Reset to defaults first so that keys set by a previous theme
226
+ # (e.g. DARK's white tick/label colors) don't bleed into this render.
227
+ mpl.rcdefaults()
228
+ mpl.rcParams["font.family"] = theme.font_family
229
+ mpl.rcParams["font.size"] = theme.font_size
230
+ mpl.rcParams["figure.facecolor"] = theme.background_color
231
+ mpl.rcParams["axes.facecolor"] = theme.background_color
232
+ for key, value in theme.extra.items():
233
+ mpl.rcParams[key] = value