figrecipe 0.6.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 +106 -973
- 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 +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- 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/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +57 -9
- 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 +68 -1039
- 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/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- 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 +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- 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 +6 -0
- figrecipe/_recorder.py +35 -106
- 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/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- 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 +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- 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 +32 -478
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
figrecipe/_wrappers/_figure.py
CHANGED
|
@@ -93,6 +93,22 @@ class RecordingFigure:
|
|
|
93
93
|
pass
|
|
94
94
|
return default
|
|
95
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
|
+
|
|
96
112
|
def suptitle(self, t: str, **kwargs) -> Any:
|
|
97
113
|
"""Set super title for the figure and record it.
|
|
98
114
|
|
|
@@ -162,6 +178,131 @@ class RecordingFigure:
|
|
|
162
178
|
# Call the underlying figure's supylabel
|
|
163
179
|
return self._fig.supylabel(t, **kwargs)
|
|
164
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
|
+
|
|
165
306
|
def __getattr__(self, name: str) -> Any:
|
|
166
307
|
"""Delegate attribute access to underlying figure."""
|
|
167
308
|
return getattr(self._fig, name)
|
|
@@ -203,6 +344,15 @@ class RecordingFigure:
|
|
|
203
344
|
>>> fig.savefig('figure.png') # Saves both figure.png and figure.yaml
|
|
204
345
|
>>> fig.savefig('figure.png', save_recipe=False) # Image only
|
|
205
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
|
+
|
|
206
356
|
# Handle file-like objects (BytesIO, etc.) - just pass through
|
|
207
357
|
if hasattr(fname, "write"):
|
|
208
358
|
self._fig.savefig(fname, **kwargs)
|
|
@@ -255,6 +405,7 @@ def create_recording_subplots(
|
|
|
255
405
|
nrows: int = 1,
|
|
256
406
|
ncols: int = 1,
|
|
257
407
|
recorder: Optional["Recorder"] = None,
|
|
408
|
+
panel_labels: bool = False,
|
|
258
409
|
**kwargs,
|
|
259
410
|
) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
|
|
260
411
|
"""Create a figure with recording-enabled axes.
|
|
@@ -267,6 +418,9 @@ def create_recording_subplots(
|
|
|
267
418
|
Number of columns.
|
|
268
419
|
recorder : Recorder, optional
|
|
269
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.
|
|
270
424
|
**kwargs
|
|
271
425
|
Passed to plt.subplots().
|
|
272
426
|
|
|
@@ -276,6 +430,10 @@ def create_recording_subplots(
|
|
|
276
430
|
Wrapped figure.
|
|
277
431
|
axes : RecordingAxes or ndarray
|
|
278
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
|
|
279
437
|
"""
|
|
280
438
|
from .._recorder import Recorder
|
|
281
439
|
|
|
@@ -312,6 +470,10 @@ def create_recording_subplots(
|
|
|
312
470
|
|
|
313
471
|
wrapped_fig = RecordingFigure(fig, recorder, wrapped_axes)
|
|
314
472
|
|
|
473
|
+
# Add panel labels if requested (multi-panel figures only)
|
|
474
|
+
if panel_labels:
|
|
475
|
+
wrapped_fig.add_panel_labels()
|
|
476
|
+
|
|
315
477
|
# Return in same shape as matplotlib (numpy arrays for consistency)
|
|
316
478
|
if nrows == 1:
|
|
317
479
|
# 1xN -> 1D array of shape (N,)
|
|
@@ -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
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Violin plot helper functions for RecordingAxes."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_violin_inner_box(
|
|
11
|
+
ax,
|
|
12
|
+
dataset: List,
|
|
13
|
+
positions: List,
|
|
14
|
+
style: Dict[str, Any],
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Add box plot inside violin.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
ax : matplotlib.axes.Axes
|
|
21
|
+
The axes to draw on.
|
|
22
|
+
dataset : array-like
|
|
23
|
+
Data arrays for each violin.
|
|
24
|
+
positions : array-like
|
|
25
|
+
X positions of violins.
|
|
26
|
+
style : dict
|
|
27
|
+
Violin style configuration.
|
|
28
|
+
"""
|
|
29
|
+
from ..styles._style_applier import mm_to_pt
|
|
30
|
+
|
|
31
|
+
whisker_lw = mm_to_pt(style.get("whisker_mm", 0.2))
|
|
32
|
+
median_size = mm_to_pt(style.get("median_mm", 0.8))
|
|
33
|
+
|
|
34
|
+
for data, pos in zip(dataset, positions):
|
|
35
|
+
data = np.asarray(data)
|
|
36
|
+
q1, median, q3 = np.percentile(data, [25, 50, 75])
|
|
37
|
+
iqr = q3 - q1
|
|
38
|
+
whisker_low = max(data.min(), q1 - 1.5 * iqr)
|
|
39
|
+
whisker_high = min(data.max(), q3 + 1.5 * iqr)
|
|
40
|
+
|
|
41
|
+
# Draw box (Q1 to Q3)
|
|
42
|
+
ax.vlines(pos, q1, q3, colors="black", linewidths=whisker_lw, zorder=3)
|
|
43
|
+
# Draw whiskers
|
|
44
|
+
ax.vlines(
|
|
45
|
+
pos,
|
|
46
|
+
whisker_low,
|
|
47
|
+
q1,
|
|
48
|
+
colors="black",
|
|
49
|
+
linewidths=whisker_lw * 0.5,
|
|
50
|
+
zorder=3,
|
|
51
|
+
)
|
|
52
|
+
ax.vlines(
|
|
53
|
+
pos,
|
|
54
|
+
q3,
|
|
55
|
+
whisker_high,
|
|
56
|
+
colors="black",
|
|
57
|
+
linewidths=whisker_lw * 0.5,
|
|
58
|
+
zorder=3,
|
|
59
|
+
)
|
|
60
|
+
# Draw median as a white dot with black edge
|
|
61
|
+
ax.scatter(
|
|
62
|
+
[pos],
|
|
63
|
+
[median],
|
|
64
|
+
s=median_size**2,
|
|
65
|
+
c="white",
|
|
66
|
+
edgecolors="black",
|
|
67
|
+
linewidths=whisker_lw,
|
|
68
|
+
zorder=4,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def add_violin_inner_swarm(
|
|
73
|
+
ax,
|
|
74
|
+
dataset: List,
|
|
75
|
+
positions: List,
|
|
76
|
+
style: Dict[str, Any],
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Add swarm points inside violin.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
ax : matplotlib.axes.Axes
|
|
83
|
+
The axes to draw on.
|
|
84
|
+
dataset : array-like
|
|
85
|
+
Data arrays for each violin.
|
|
86
|
+
positions : array-like
|
|
87
|
+
X positions of violins.
|
|
88
|
+
style : dict
|
|
89
|
+
Violin style configuration.
|
|
90
|
+
"""
|
|
91
|
+
from ..styles._style_applier import mm_to_pt
|
|
92
|
+
|
|
93
|
+
point_size = mm_to_pt(style.get("median_mm", 0.8))
|
|
94
|
+
|
|
95
|
+
for data, pos in zip(dataset, positions):
|
|
96
|
+
data = np.asarray(data)
|
|
97
|
+
n = len(data)
|
|
98
|
+
|
|
99
|
+
# Simple swarm: jitter x positions
|
|
100
|
+
jitter = np.random.default_rng(42).uniform(-0.15, 0.15, n)
|
|
101
|
+
x_positions = pos + jitter
|
|
102
|
+
|
|
103
|
+
ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.5, zorder=3)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def add_violin_inner_stick(
|
|
107
|
+
ax,
|
|
108
|
+
dataset: List,
|
|
109
|
+
positions: List,
|
|
110
|
+
style: Dict[str, Any],
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Add stick (line) markers inside violin for each data point.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
ax : matplotlib.axes.Axes
|
|
117
|
+
The axes to draw on.
|
|
118
|
+
dataset : array-like
|
|
119
|
+
Data arrays for each violin.
|
|
120
|
+
positions : array-like
|
|
121
|
+
X positions of violins.
|
|
122
|
+
style : dict
|
|
123
|
+
Violin style configuration.
|
|
124
|
+
"""
|
|
125
|
+
from ..styles._style_applier import mm_to_pt
|
|
126
|
+
|
|
127
|
+
lw = mm_to_pt(style.get("whisker_mm", 0.2))
|
|
128
|
+
|
|
129
|
+
for data, pos in zip(dataset, positions):
|
|
130
|
+
data = np.asarray(data)
|
|
131
|
+
# Draw short horizontal lines at each data point
|
|
132
|
+
for val in data:
|
|
133
|
+
ax.hlines(
|
|
134
|
+
val,
|
|
135
|
+
pos - 0.05,
|
|
136
|
+
pos + 0.05,
|
|
137
|
+
colors="black",
|
|
138
|
+
linewidths=lw * 0.5,
|
|
139
|
+
alpha=0.3,
|
|
140
|
+
zorder=3,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def add_violin_inner_point(
|
|
145
|
+
ax,
|
|
146
|
+
dataset: List,
|
|
147
|
+
positions: List,
|
|
148
|
+
style: Dict[str, Any],
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Add point markers inside violin for each data point.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
ax : matplotlib.axes.Axes
|
|
155
|
+
The axes to draw on.
|
|
156
|
+
dataset : array-like
|
|
157
|
+
Data arrays for each violin.
|
|
158
|
+
positions : array-like
|
|
159
|
+
X positions of violins.
|
|
160
|
+
style : dict
|
|
161
|
+
Violin style configuration.
|
|
162
|
+
"""
|
|
163
|
+
from ..styles._style_applier import mm_to_pt
|
|
164
|
+
|
|
165
|
+
point_size = mm_to_pt(style.get("median_mm", 0.8)) * 0.5
|
|
166
|
+
|
|
167
|
+
for data, pos in zip(dataset, positions):
|
|
168
|
+
data = np.asarray(data)
|
|
169
|
+
x_positions = np.full_like(data, pos)
|
|
170
|
+
ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.3, zorder=3)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = [
|
|
174
|
+
"add_violin_inner_box",
|
|
175
|
+
"add_violin_inner_swarm",
|
|
176
|
+
"add_violin_inner_stick",
|
|
177
|
+
"add_violin_inner_point",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
# EOF
|
figrecipe/styles/__init__.py
CHANGED
|
@@ -18,12 +18,10 @@ Usage:
|
|
|
18
18
|
fig, ax = ps.subplots(**style.to_subplots_kwargs())
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
from .
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
list_available_fonts,
|
|
26
|
-
)
|
|
21
|
+
from ._dotdict import DotDict
|
|
22
|
+
from ._finalize import finalize_special_plots, finalize_ticks
|
|
23
|
+
from ._fonts import check_font, list_available_fonts
|
|
24
|
+
from ._style_applier import apply_style_mm
|
|
27
25
|
from ._style_loader import (
|
|
28
26
|
STYLE,
|
|
29
27
|
get_style,
|
|
@@ -33,8 +31,10 @@ from ._style_loader import (
|
|
|
33
31
|
to_subplots_kwargs,
|
|
34
32
|
unload_style,
|
|
35
33
|
)
|
|
34
|
+
from ._themes import apply_theme_colors
|
|
36
35
|
|
|
37
36
|
__all__ = [
|
|
37
|
+
"DotDict",
|
|
38
38
|
"load_style",
|
|
39
39
|
"unload_style",
|
|
40
40
|
"get_style",
|
|
@@ -46,4 +46,6 @@ __all__ = [
|
|
|
46
46
|
"apply_theme_colors",
|
|
47
47
|
"check_font",
|
|
48
48
|
"list_available_fonts",
|
|
49
|
+
"finalize_ticks",
|
|
50
|
+
"finalize_special_plots",
|
|
49
51
|
]
|