figrecipe 0.5.0__py3-none-any.whl → 0.7.4__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.
- figrecipe/__init__.py +220 -819
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +258 -0
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +193 -170
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
figrecipe/_wrappers/_figure.py
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
"""Wrapped Figure that manages recording."""
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Tuple, Union
|
|
7
7
|
|
|
8
8
|
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
9
10
|
from matplotlib.figure import Figure
|
|
11
|
+
from numpy.typing import NDArray
|
|
10
12
|
|
|
11
13
|
from ._axes import RecordingAxes
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING:
|
|
14
|
-
from .._recorder import
|
|
15
|
-
from .._utils._numpy_io import DataFormat
|
|
16
|
+
from .._recorder import FigureRecord, Recorder
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class RecordingFigure:
|
|
@@ -79,6 +80,229 @@ class RecordingFigure:
|
|
|
79
80
|
"""Get the figure record."""
|
|
80
81
|
return self._recorder.figure_record
|
|
81
82
|
|
|
83
|
+
def _get_style_fontsize(self, key: str, default: float) -> float:
|
|
84
|
+
"""Get fontsize from loaded style."""
|
|
85
|
+
try:
|
|
86
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
87
|
+
|
|
88
|
+
if _STYLE_CACHE is not None:
|
|
89
|
+
fonts = getattr(_STYLE_CACHE, "fonts", None)
|
|
90
|
+
if fonts is not None:
|
|
91
|
+
return getattr(fonts, key, default)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
return default
|
|
95
|
+
|
|
96
|
+
def _get_theme_text_color(self, default: str = "black") -> str:
|
|
97
|
+
"""Get text color from loaded style's theme settings."""
|
|
98
|
+
try:
|
|
99
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
100
|
+
|
|
101
|
+
if _STYLE_CACHE is not None:
|
|
102
|
+
theme = getattr(_STYLE_CACHE, "theme", None)
|
|
103
|
+
if theme is not None:
|
|
104
|
+
mode = getattr(theme, "mode", "light")
|
|
105
|
+
theme_colors = getattr(theme, mode, None)
|
|
106
|
+
if theme_colors is not None:
|
|
107
|
+
return getattr(theme_colors, "text", default)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
return default
|
|
111
|
+
|
|
112
|
+
def suptitle(self, t: str, **kwargs) -> Any:
|
|
113
|
+
"""Set super title for the figure and record it.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
t : str
|
|
118
|
+
The super title text.
|
|
119
|
+
**kwargs
|
|
120
|
+
Additional arguments passed to matplotlib's suptitle().
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
Text
|
|
125
|
+
The matplotlib Text object.
|
|
126
|
+
"""
|
|
127
|
+
# Auto-apply fontsize from style if not specified
|
|
128
|
+
if "fontsize" not in kwargs:
|
|
129
|
+
kwargs["fontsize"] = self._get_style_fontsize("suptitle_pt", 10)
|
|
130
|
+
# Record the suptitle call
|
|
131
|
+
self._recorder.figure_record.suptitle = {"text": t, "kwargs": kwargs}
|
|
132
|
+
# Call the underlying figure's suptitle
|
|
133
|
+
return self._fig.suptitle(t, **kwargs)
|
|
134
|
+
|
|
135
|
+
def supxlabel(self, t: str, **kwargs) -> Any:
|
|
136
|
+
"""Set super x-label for the figure and record it.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
t : str
|
|
141
|
+
The super x-label text.
|
|
142
|
+
**kwargs
|
|
143
|
+
Additional arguments passed to matplotlib's supxlabel().
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
Text
|
|
148
|
+
The matplotlib Text object.
|
|
149
|
+
"""
|
|
150
|
+
# Auto-apply fontsize from style if not specified
|
|
151
|
+
if "fontsize" not in kwargs:
|
|
152
|
+
kwargs["fontsize"] = self._get_style_fontsize("supxlabel_pt", 8)
|
|
153
|
+
# Record the supxlabel call
|
|
154
|
+
self._recorder.figure_record.supxlabel = {"text": t, "kwargs": kwargs}
|
|
155
|
+
# Call the underlying figure's supxlabel
|
|
156
|
+
return self._fig.supxlabel(t, **kwargs)
|
|
157
|
+
|
|
158
|
+
def supylabel(self, t: str, **kwargs) -> Any:
|
|
159
|
+
"""Set super y-label for the figure and record it.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
t : str
|
|
164
|
+
The super y-label text.
|
|
165
|
+
**kwargs
|
|
166
|
+
Additional arguments passed to matplotlib's supylabel().
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
Text
|
|
171
|
+
The matplotlib Text object.
|
|
172
|
+
"""
|
|
173
|
+
# Auto-apply fontsize from style if not specified
|
|
174
|
+
if "fontsize" not in kwargs:
|
|
175
|
+
kwargs["fontsize"] = self._get_style_fontsize("supylabel_pt", 8)
|
|
176
|
+
# Record the supylabel call
|
|
177
|
+
self._recorder.figure_record.supylabel = {"text": t, "kwargs": kwargs}
|
|
178
|
+
# Call the underlying figure's supylabel
|
|
179
|
+
return self._fig.supylabel(t, **kwargs)
|
|
180
|
+
|
|
181
|
+
def add_panel_labels(
|
|
182
|
+
self,
|
|
183
|
+
labels: Optional[List[str]] = None,
|
|
184
|
+
loc: str = "upper left",
|
|
185
|
+
offset: Tuple[float, float] = (-0.1, 1.05),
|
|
186
|
+
fontsize: Optional[float] = None,
|
|
187
|
+
fontweight: str = "bold",
|
|
188
|
+
**kwargs,
|
|
189
|
+
) -> List[Any]:
|
|
190
|
+
"""Add panel labels (A, B, C, D, etc.) to multi-panel figures.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
labels : list of str, optional
|
|
195
|
+
Custom labels. If None, uses uppercase letters (A, B, C, ...).
|
|
196
|
+
loc : str
|
|
197
|
+
Location hint: 'upper left' (default), 'upper right', 'lower left', 'lower right'.
|
|
198
|
+
offset : tuple of float
|
|
199
|
+
(x, y) offset in axes coordinates from the corner.
|
|
200
|
+
Default is (-0.1, 1.05) for upper left positioning.
|
|
201
|
+
fontsize : float, optional
|
|
202
|
+
Font size in points. If None, uses style's title_pt or 10.
|
|
203
|
+
fontweight : str
|
|
204
|
+
Font weight (default: 'bold').
|
|
205
|
+
**kwargs
|
|
206
|
+
Additional arguments passed to ax.text().
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
list of Text
|
|
211
|
+
The matplotlib Text objects created.
|
|
212
|
+
|
|
213
|
+
Examples
|
|
214
|
+
--------
|
|
215
|
+
>>> fig, axes = fr.subplots(2, 2)
|
|
216
|
+
>>> fig.add_panel_labels() # Adds A, B, C, D
|
|
217
|
+
>>> fig.add_panel_labels(['i', 'ii', 'iii', 'iv']) # Custom labels
|
|
218
|
+
>>> fig.add_panel_labels(loc='upper right', offset=(1.05, 1.05))
|
|
219
|
+
"""
|
|
220
|
+
from ._panel_labels import add_panel_labels as _add_panel_labels
|
|
221
|
+
|
|
222
|
+
# Get fontsize from style if not specified
|
|
223
|
+
if fontsize is None:
|
|
224
|
+
fontsize = self._get_style_fontsize("title_pt", 10)
|
|
225
|
+
|
|
226
|
+
# Get theme text color (unless user provided 'color' in kwargs)
|
|
227
|
+
if "color" not in kwargs:
|
|
228
|
+
text_color = self._get_theme_text_color()
|
|
229
|
+
else:
|
|
230
|
+
text_color = kwargs.pop("color")
|
|
231
|
+
|
|
232
|
+
def record_callback(info):
|
|
233
|
+
self._recorder.figure_record.panel_labels = info
|
|
234
|
+
|
|
235
|
+
return _add_panel_labels(
|
|
236
|
+
all_axes=self.flat,
|
|
237
|
+
labels=labels,
|
|
238
|
+
loc=loc,
|
|
239
|
+
offset=offset,
|
|
240
|
+
fontsize=fontsize,
|
|
241
|
+
fontweight=fontweight,
|
|
242
|
+
text_color=text_color,
|
|
243
|
+
record_callback=record_callback,
|
|
244
|
+
**kwargs,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def set_title_metadata(self, title: str) -> "RecordingFigure":
|
|
248
|
+
"""Set figure title metadata (not rendered, stored in recipe).
|
|
249
|
+
|
|
250
|
+
This is for storing a publication/reference title for the figure,
|
|
251
|
+
separate from suptitle which is rendered on the figure.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
title : str
|
|
256
|
+
The figure title for publication/reference.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
RecordingFigure
|
|
261
|
+
Self for method chaining.
|
|
262
|
+
|
|
263
|
+
Examples
|
|
264
|
+
--------
|
|
265
|
+
>>> fig, ax = fr.subplots()
|
|
266
|
+
>>> fig.set_title_metadata("Effect of temperature on reaction rate")
|
|
267
|
+
>>> fig.set_caption("Figure 1. Reaction rates measured at various temperatures.")
|
|
268
|
+
"""
|
|
269
|
+
self._recorder.figure_record.title_metadata = title
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
def set_caption(self, caption: str) -> "RecordingFigure":
|
|
273
|
+
"""Set figure caption metadata (not rendered, stored in recipe).
|
|
274
|
+
|
|
275
|
+
This is for storing a publication caption for the figure,
|
|
276
|
+
typically used in scientific papers (e.g., "Fig. 1. Description...").
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
caption : str
|
|
281
|
+
The figure caption text.
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
RecordingFigure
|
|
286
|
+
Self for method chaining.
|
|
287
|
+
|
|
288
|
+
Examples
|
|
289
|
+
--------
|
|
290
|
+
>>> fig, ax = fr.subplots()
|
|
291
|
+
>>> fig.set_caption("Figure 1. Temperature dependence of reaction rates.")
|
|
292
|
+
"""
|
|
293
|
+
self._recorder.figure_record.caption = caption
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def title_metadata(self) -> Optional[str]:
|
|
298
|
+
"""Get the figure title metadata."""
|
|
299
|
+
return self._recorder.figure_record.title_metadata
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def caption(self) -> Optional[str]:
|
|
303
|
+
"""Get the figure caption metadata."""
|
|
304
|
+
return self._recorder.figure_record.caption
|
|
305
|
+
|
|
82
306
|
def __getattr__(self, name: str) -> Any:
|
|
83
307
|
"""Delegate attribute access to underlying figure."""
|
|
84
308
|
return getattr(self._fig, name)
|
|
@@ -88,6 +312,7 @@ class RecordingFigure:
|
|
|
88
312
|
fname,
|
|
89
313
|
save_recipe: bool = True,
|
|
90
314
|
recipe_format: Literal["csv", "npz", "inline"] = "csv",
|
|
315
|
+
verbose: bool = True,
|
|
91
316
|
**kwargs,
|
|
92
317
|
):
|
|
93
318
|
"""Save the figure image and optionally the recipe.
|
|
@@ -101,6 +326,8 @@ class RecordingFigure:
|
|
|
101
326
|
Recipe will be saved with same name but .yaml extension.
|
|
102
327
|
recipe_format : str
|
|
103
328
|
Format for data in recipe: 'csv' (default), 'npz', or 'inline'.
|
|
329
|
+
verbose : bool
|
|
330
|
+
If True (default), print save status.
|
|
104
331
|
**kwargs
|
|
105
332
|
Passed to matplotlib's savefig().
|
|
106
333
|
|
|
@@ -117,8 +344,17 @@ class RecordingFigure:
|
|
|
117
344
|
>>> fig.savefig('figure.png') # Saves both figure.png and figure.yaml
|
|
118
345
|
>>> fig.savefig('figure.png', save_recipe=False) # Image only
|
|
119
346
|
"""
|
|
347
|
+
# Finalize ticks and special plots before saving
|
|
348
|
+
from ..styles._style_applier import finalize_special_plots, finalize_ticks
|
|
349
|
+
from ..styles._style_loader import get_current_style_dict
|
|
350
|
+
|
|
351
|
+
style_dict = get_current_style_dict()
|
|
352
|
+
for ax in self._fig.get_axes():
|
|
353
|
+
finalize_ticks(ax)
|
|
354
|
+
finalize_special_plots(ax, style_dict)
|
|
355
|
+
|
|
120
356
|
# Handle file-like objects (BytesIO, etc.) - just pass through
|
|
121
|
-
if hasattr(fname,
|
|
357
|
+
if hasattr(fname, "write"):
|
|
122
358
|
self._fig.savefig(fname, **kwargs)
|
|
123
359
|
return fname
|
|
124
360
|
|
|
@@ -128,8 +364,12 @@ class RecordingFigure:
|
|
|
128
364
|
if save_recipe:
|
|
129
365
|
recipe_path = fname.with_suffix(".yaml")
|
|
130
366
|
self.save_recipe(recipe_path, include_data=True, data_format=recipe_format)
|
|
367
|
+
if verbose:
|
|
368
|
+
print(f"Saved: {fname} + {recipe_path}")
|
|
131
369
|
return fname, recipe_path
|
|
132
370
|
|
|
371
|
+
if verbose:
|
|
372
|
+
print(f"Saved: {fname}")
|
|
133
373
|
return fname
|
|
134
374
|
|
|
135
375
|
def save_recipe(
|
|
@@ -155,15 +395,19 @@ class RecordingFigure:
|
|
|
155
395
|
Path to saved recipe file.
|
|
156
396
|
"""
|
|
157
397
|
from .._serializer import save_recipe
|
|
158
|
-
|
|
398
|
+
|
|
399
|
+
return save_recipe(
|
|
400
|
+
self._recorder.figure_record, path, include_data, data_format
|
|
401
|
+
)
|
|
159
402
|
|
|
160
403
|
|
|
161
404
|
def create_recording_subplots(
|
|
162
405
|
nrows: int = 1,
|
|
163
406
|
ncols: int = 1,
|
|
164
407
|
recorder: Optional["Recorder"] = None,
|
|
408
|
+
panel_labels: bool = False,
|
|
165
409
|
**kwargs,
|
|
166
|
-
) -> Tuple[RecordingFigure, Union[RecordingAxes,
|
|
410
|
+
) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
|
|
167
411
|
"""Create a figure with recording-enabled axes.
|
|
168
412
|
|
|
169
413
|
Parameters
|
|
@@ -174,6 +418,9 @@ def create_recording_subplots(
|
|
|
174
418
|
Number of columns.
|
|
175
419
|
recorder : Recorder, optional
|
|
176
420
|
Recorder instance. Created if not provided.
|
|
421
|
+
panel_labels : bool
|
|
422
|
+
If True and figure has multiple panels, automatically add
|
|
423
|
+
panel labels (A, B, C, D, ...). Default is False.
|
|
177
424
|
**kwargs
|
|
178
425
|
Passed to plt.subplots().
|
|
179
426
|
|
|
@@ -181,8 +428,12 @@ def create_recording_subplots(
|
|
|
181
428
|
-------
|
|
182
429
|
fig : RecordingFigure
|
|
183
430
|
Wrapped figure.
|
|
184
|
-
axes : RecordingAxes or
|
|
185
|
-
Wrapped axes (single if 1x1, otherwise
|
|
431
|
+
axes : RecordingAxes or ndarray
|
|
432
|
+
Wrapped axes (single if 1x1, otherwise numpy array matching matplotlib).
|
|
433
|
+
|
|
434
|
+
Examples
|
|
435
|
+
--------
|
|
436
|
+
>>> fig, axes = fr.subplots(2, 2, panel_labels=True) # Auto-adds A, B, C, D
|
|
186
437
|
"""
|
|
187
438
|
from .._recorder import Recorder
|
|
188
439
|
|
|
@@ -205,23 +456,31 @@ def create_recording_subplots(
|
|
|
205
456
|
wrapped_fig = RecordingFigure(fig, recorder, wrapped_ax)
|
|
206
457
|
return wrapped_fig, wrapped_ax
|
|
207
458
|
|
|
208
|
-
# Handle 1D or 2D arrays
|
|
209
|
-
|
|
210
|
-
|
|
459
|
+
# Handle 1D or 2D arrays - reshape to (nrows, ncols) for uniform processing
|
|
460
|
+
mpl_axes_arr = np.asarray(mpl_axes)
|
|
461
|
+
if mpl_axes_arr.ndim == 1:
|
|
462
|
+
mpl_axes_arr = mpl_axes_arr.reshape(nrows, ncols)
|
|
211
463
|
|
|
212
464
|
wrapped_axes = []
|
|
213
|
-
for i in range(
|
|
465
|
+
for i in range(nrows):
|
|
214
466
|
row = []
|
|
215
|
-
for j in range(
|
|
216
|
-
row.append(RecordingAxes(
|
|
467
|
+
for j in range(ncols):
|
|
468
|
+
row.append(RecordingAxes(mpl_axes_arr[i, j], recorder, position=(i, j)))
|
|
217
469
|
wrapped_axes.append(row)
|
|
218
470
|
|
|
219
471
|
wrapped_fig = RecordingFigure(fig, recorder, wrapped_axes)
|
|
220
472
|
|
|
221
|
-
#
|
|
473
|
+
# Add panel labels if requested (multi-panel figures only)
|
|
474
|
+
if panel_labels:
|
|
475
|
+
wrapped_fig.add_panel_labels()
|
|
476
|
+
|
|
477
|
+
# Return in same shape as matplotlib (numpy arrays for consistency)
|
|
222
478
|
if nrows == 1:
|
|
223
|
-
|
|
479
|
+
# 1xN -> 1D array of shape (N,)
|
|
480
|
+
return wrapped_fig, np.array(wrapped_axes[0], dtype=object)
|
|
224
481
|
elif ncols == 1:
|
|
225
|
-
|
|
482
|
+
# Nx1 -> 1D array of shape (N,)
|
|
483
|
+
return wrapped_fig, np.array([row[0] for row in wrapped_axes], dtype=object)
|
|
226
484
|
else:
|
|
227
|
-
|
|
485
|
+
# NxM -> 2D array
|
|
486
|
+
return wrapped_fig, np.array(wrapped_axes, dtype=object)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Panel label utilities for multi-panel figures."""
|
|
4
|
+
|
|
5
|
+
import string
|
|
6
|
+
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ._axes import RecordingAxes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_panel_labels(
|
|
13
|
+
all_axes: List["RecordingAxes"],
|
|
14
|
+
labels: Optional[List[str]],
|
|
15
|
+
loc: str,
|
|
16
|
+
offset: Tuple[float, float],
|
|
17
|
+
fontsize: float,
|
|
18
|
+
fontweight: str,
|
|
19
|
+
text_color: str,
|
|
20
|
+
record_callback: Any,
|
|
21
|
+
**kwargs,
|
|
22
|
+
) -> List[Any]:
|
|
23
|
+
"""Add panel labels (A, B, C, D, etc.) to axes.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
all_axes : list of RecordingAxes
|
|
28
|
+
Flattened list of all axes.
|
|
29
|
+
labels : list of str or None
|
|
30
|
+
Custom labels. If None, uses uppercase letters.
|
|
31
|
+
loc : str
|
|
32
|
+
Location hint: 'upper left', 'upper right', 'lower left', 'lower right'.
|
|
33
|
+
offset : tuple of float
|
|
34
|
+
(x, y) offset in axes coordinates.
|
|
35
|
+
fontsize : float
|
|
36
|
+
Font size in points.
|
|
37
|
+
fontweight : str
|
|
38
|
+
Font weight.
|
|
39
|
+
text_color : str
|
|
40
|
+
Text color.
|
|
41
|
+
record_callback : callable
|
|
42
|
+
Callback to record panel labels info.
|
|
43
|
+
**kwargs
|
|
44
|
+
Additional arguments passed to ax.text().
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
list of Text
|
|
49
|
+
The matplotlib Text objects created.
|
|
50
|
+
"""
|
|
51
|
+
n_axes = len(all_axes)
|
|
52
|
+
|
|
53
|
+
# Generate default labels if not provided
|
|
54
|
+
if labels is None:
|
|
55
|
+
labels = list(string.ascii_uppercase[:n_axes])
|
|
56
|
+
elif len(labels) < n_axes:
|
|
57
|
+
# Extend with letters if not enough labels provided
|
|
58
|
+
labels = list(labels) + list(string.ascii_uppercase[len(labels) : n_axes])
|
|
59
|
+
|
|
60
|
+
# Calculate position based on loc
|
|
61
|
+
x, y, ha, va = _calculate_position(loc, offset)
|
|
62
|
+
|
|
63
|
+
# Record panel labels
|
|
64
|
+
record_callback(
|
|
65
|
+
{
|
|
66
|
+
"labels": labels[:n_axes],
|
|
67
|
+
"loc": loc,
|
|
68
|
+
"offset": offset,
|
|
69
|
+
"fontsize": fontsize,
|
|
70
|
+
"fontweight": fontweight,
|
|
71
|
+
"color": text_color,
|
|
72
|
+
"kwargs": kwargs,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Add labels to each axes
|
|
77
|
+
text_objects = []
|
|
78
|
+
for ax, label in zip(all_axes, labels[:n_axes]):
|
|
79
|
+
text = ax.ax.text(
|
|
80
|
+
x,
|
|
81
|
+
y,
|
|
82
|
+
label,
|
|
83
|
+
transform=ax.ax.transAxes,
|
|
84
|
+
fontsize=fontsize,
|
|
85
|
+
fontweight=fontweight,
|
|
86
|
+
color=text_color,
|
|
87
|
+
ha=ha,
|
|
88
|
+
va=va,
|
|
89
|
+
**kwargs,
|
|
90
|
+
)
|
|
91
|
+
text_objects.append(text)
|
|
92
|
+
|
|
93
|
+
return text_objects
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _calculate_position(
|
|
97
|
+
loc: str, offset: Tuple[float, float]
|
|
98
|
+
) -> Tuple[float, float, str, str]:
|
|
99
|
+
"""Calculate text position and alignment based on location.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
tuple
|
|
104
|
+
(x, y, ha, va) where ha/va are horizontal/vertical alignment.
|
|
105
|
+
"""
|
|
106
|
+
if loc == "upper left":
|
|
107
|
+
x, y = offset
|
|
108
|
+
ha, va = "right", "bottom"
|
|
109
|
+
elif loc == "upper right":
|
|
110
|
+
x, y = offset
|
|
111
|
+
ha, va = "left", "bottom"
|
|
112
|
+
elif loc == "lower left":
|
|
113
|
+
x, y = offset[0], -offset[1] + 1.0
|
|
114
|
+
ha, va = "right", "top"
|
|
115
|
+
elif loc == "lower right":
|
|
116
|
+
x, y = offset
|
|
117
|
+
ha, va = "left", "top"
|
|
118
|
+
else:
|
|
119
|
+
x, y = offset
|
|
120
|
+
ha, va = "right", "bottom"
|
|
121
|
+
|
|
122
|
+
return x, y, ha, va
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["add_panel_labels"]
|
|
126
|
+
|
|
127
|
+
# EOF
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Helper functions for custom plot methods in RecordingAxes."""
|
|
4
|
+
|
|
5
|
+
from typing import List, Tuple
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_colors_from_style(n_colors: int, explicit_colors=None) -> List:
|
|
11
|
+
"""Get colors from style or matplotlib defaults.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
n_colors : int
|
|
16
|
+
Number of colors needed.
|
|
17
|
+
explicit_colors : list or color, optional
|
|
18
|
+
Explicitly provided colors.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
list
|
|
23
|
+
List of colors.
|
|
24
|
+
"""
|
|
25
|
+
if explicit_colors is not None:
|
|
26
|
+
if isinstance(explicit_colors, list):
|
|
27
|
+
return explicit_colors
|
|
28
|
+
return [explicit_colors] * n_colors
|
|
29
|
+
|
|
30
|
+
from ..styles import get_style
|
|
31
|
+
|
|
32
|
+
style = get_style()
|
|
33
|
+
if style and "colors" in style and "palette" in style.colors:
|
|
34
|
+
palette = list(style.colors.palette)
|
|
35
|
+
colors = []
|
|
36
|
+
for c in palette:
|
|
37
|
+
if isinstance(c, (list, tuple)) and len(c) >= 3:
|
|
38
|
+
if all(v <= 1.0 for v in c):
|
|
39
|
+
colors.append(tuple(c))
|
|
40
|
+
else:
|
|
41
|
+
colors.append(tuple(v / 255.0 for v in c))
|
|
42
|
+
else:
|
|
43
|
+
colors.append(c)
|
|
44
|
+
return colors
|
|
45
|
+
|
|
46
|
+
# Matplotlib default color cycle
|
|
47
|
+
import matplotlib.pyplot as plt
|
|
48
|
+
|
|
49
|
+
return [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def beeswarm_positions(
|
|
53
|
+
data: np.ndarray,
|
|
54
|
+
width: float,
|
|
55
|
+
rng: np.random.Generator,
|
|
56
|
+
) -> np.ndarray:
|
|
57
|
+
"""Calculate beeswarm-style x positions to minimize overlap.
|
|
58
|
+
|
|
59
|
+
This is a simplified beeswarm that uses binning and jittering.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
data : array
|
|
64
|
+
Y values of points.
|
|
65
|
+
width : float
|
|
66
|
+
Maximum jitter width.
|
|
67
|
+
rng : Generator
|
|
68
|
+
Random number generator.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
array
|
|
73
|
+
X offsets for each point.
|
|
74
|
+
"""
|
|
75
|
+
n = len(data)
|
|
76
|
+
if n == 0:
|
|
77
|
+
return np.array([])
|
|
78
|
+
|
|
79
|
+
# Sort data and get order
|
|
80
|
+
order = np.argsort(data)
|
|
81
|
+
sorted_data = data[order]
|
|
82
|
+
|
|
83
|
+
# Group nearby points and offset them
|
|
84
|
+
x_offsets = np.zeros(n)
|
|
85
|
+
|
|
86
|
+
# Simple approach: bin by quantiles and spread within each bin
|
|
87
|
+
n_bins = max(1, int(np.sqrt(n)))
|
|
88
|
+
bin_edges = np.percentile(sorted_data, np.linspace(0, 100, n_bins + 1))
|
|
89
|
+
|
|
90
|
+
for i in range(n_bins):
|
|
91
|
+
mask = (sorted_data >= bin_edges[i]) & (sorted_data <= bin_edges[i + 1])
|
|
92
|
+
n_in_bin = mask.sum()
|
|
93
|
+
if n_in_bin > 0:
|
|
94
|
+
# Spread points evenly within bin width
|
|
95
|
+
offsets = np.linspace(-width / 2, width / 2, n_in_bin)
|
|
96
|
+
# Add small random noise
|
|
97
|
+
offsets += rng.uniform(-width * 0.1, width * 0.1, n_in_bin)
|
|
98
|
+
x_offsets[mask] = offsets
|
|
99
|
+
|
|
100
|
+
# Restore original order
|
|
101
|
+
result = np.zeros(n)
|
|
102
|
+
result[order] = x_offsets
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def compute_joyplot_kdes(arrays: List[np.ndarray], x: np.ndarray) -> Tuple[List, float]:
|
|
107
|
+
"""Compute KDEs for joyplot ridges.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
arrays : list
|
|
112
|
+
List of data arrays.
|
|
113
|
+
x : array
|
|
114
|
+
X values for KDE evaluation.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
tuple
|
|
119
|
+
(kdes, max_density)
|
|
120
|
+
"""
|
|
121
|
+
from scipy import stats
|
|
122
|
+
|
|
123
|
+
kdes = []
|
|
124
|
+
max_density = 0
|
|
125
|
+
for arr in arrays:
|
|
126
|
+
arr = np.asarray(arr)
|
|
127
|
+
if len(arr) > 1:
|
|
128
|
+
kde = stats.gaussian_kde(arr)
|
|
129
|
+
density = kde(x)
|
|
130
|
+
kdes.append(density)
|
|
131
|
+
max_density = max(max_density, np.max(density))
|
|
132
|
+
else:
|
|
133
|
+
kdes.append(np.zeros_like(x))
|
|
134
|
+
return kdes, max_density
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
__all__ = [
|
|
138
|
+
"get_colors_from_style",
|
|
139
|
+
"beeswarm_positions",
|
|
140
|
+
"compute_joyplot_kdes",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
# EOF
|