figrecipe 0.6.0__py3-none-any.whl → 0.9.0__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 +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -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/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -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 +61 -13
- 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 +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -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 +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -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/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -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 +42 -480
- 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 +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -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/METADATA +0 -394
- 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.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Main composition logic for combining multiple figures."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Tuple, Union
|
|
7
|
+
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
|
|
10
|
+
from .._recorder import FigureRecord
|
|
11
|
+
from .._serializer import load_recipe
|
|
12
|
+
from .._wrappers import RecordingAxes, RecordingFigure
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compose(
|
|
16
|
+
layout: Tuple[int, int],
|
|
17
|
+
sources: Dict[Tuple[int, int], Union[str, Path, FigureRecord, Tuple]],
|
|
18
|
+
**kwargs,
|
|
19
|
+
) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
|
|
20
|
+
"""Compose a new figure from multiple recipe sources.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
layout : tuple
|
|
25
|
+
(nrows, ncols) for the new composite figure.
|
|
26
|
+
sources : dict
|
|
27
|
+
Mapping of (row, col) -> source specification.
|
|
28
|
+
Source can be:
|
|
29
|
+
- str/Path: Recipe file path (uses first axes)
|
|
30
|
+
- FigureRecord: Direct record (uses first axes)
|
|
31
|
+
- Tuple[source, ax_key]: Specific axes from source
|
|
32
|
+
|
|
33
|
+
**kwargs
|
|
34
|
+
Additional arguments passed to subplots().
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
fig : RecordingFigure
|
|
39
|
+
Composed figure.
|
|
40
|
+
axes : RecordingAxes or ndarray of RecordingAxes
|
|
41
|
+
Axes of the composed figure.
|
|
42
|
+
|
|
43
|
+
Examples
|
|
44
|
+
--------
|
|
45
|
+
>>> import figrecipe as fr
|
|
46
|
+
>>> fig, axes = fr.compose(
|
|
47
|
+
... layout=(1, 2),
|
|
48
|
+
... sources={
|
|
49
|
+
... (0, 0): "experiment_a.yaml",
|
|
50
|
+
... (0, 1): "experiment_b.yaml",
|
|
51
|
+
... }
|
|
52
|
+
... )
|
|
53
|
+
"""
|
|
54
|
+
from .. import subplots
|
|
55
|
+
|
|
56
|
+
nrows, ncols = layout
|
|
57
|
+
fig, axes = subplots(nrows=nrows, ncols=ncols, **kwargs)
|
|
58
|
+
|
|
59
|
+
for (row, col), source_spec in sources.items():
|
|
60
|
+
source_record, ax_key = _parse_source_spec(source_spec)
|
|
61
|
+
ax_record = source_record.axes.get(ax_key)
|
|
62
|
+
|
|
63
|
+
if ax_record is None:
|
|
64
|
+
available = list(source_record.axes.keys())
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Axes '{ax_key}' not found in source. Available: {available}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
target_ax = _get_axes_at(axes, row, col, nrows, ncols)
|
|
70
|
+
_replay_axes_record(target_ax, ax_record, fig.record, row, col)
|
|
71
|
+
|
|
72
|
+
return fig, axes
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _parse_source_spec(
|
|
76
|
+
spec: Union[str, Path, FigureRecord, Tuple],
|
|
77
|
+
) -> Tuple[FigureRecord, str]:
|
|
78
|
+
"""Parse source specification into (FigureRecord, ax_key).
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
spec : various
|
|
83
|
+
Source specification.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
tuple
|
|
88
|
+
(FigureRecord, ax_key)
|
|
89
|
+
"""
|
|
90
|
+
if isinstance(spec, (str, Path)):
|
|
91
|
+
return load_recipe(spec), "ax_0_0"
|
|
92
|
+
elif isinstance(spec, FigureRecord):
|
|
93
|
+
return spec, "ax_0_0"
|
|
94
|
+
elif isinstance(spec, tuple) and len(spec) == 2:
|
|
95
|
+
source, ax_key = spec
|
|
96
|
+
if isinstance(source, (str, Path)):
|
|
97
|
+
return load_recipe(source), ax_key
|
|
98
|
+
elif isinstance(source, FigureRecord):
|
|
99
|
+
return source, ax_key
|
|
100
|
+
raise TypeError(f"Invalid source in tuple: {type(source)}")
|
|
101
|
+
raise TypeError(f"Invalid source spec type: {type(spec)}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_axes_at(
|
|
105
|
+
axes: Union[RecordingAxes, NDArray],
|
|
106
|
+
row: int,
|
|
107
|
+
col: int,
|
|
108
|
+
nrows: int,
|
|
109
|
+
ncols: int,
|
|
110
|
+
) -> RecordingAxes:
|
|
111
|
+
"""Get axes at position, handling different array shapes.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
axes : RecordingAxes or ndarray
|
|
116
|
+
Axes object(s) from subplots.
|
|
117
|
+
row, col : int
|
|
118
|
+
Target position.
|
|
119
|
+
nrows, ncols : int
|
|
120
|
+
Grid dimensions.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
RecordingAxes
|
|
125
|
+
Axes at the specified position.
|
|
126
|
+
"""
|
|
127
|
+
if nrows == 1 and ncols == 1:
|
|
128
|
+
return axes
|
|
129
|
+
elif nrows == 1:
|
|
130
|
+
return axes[col]
|
|
131
|
+
elif ncols == 1:
|
|
132
|
+
return axes[row]
|
|
133
|
+
else:
|
|
134
|
+
return axes[row, col]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _replay_axes_record(
|
|
138
|
+
target_ax: RecordingAxes,
|
|
139
|
+
ax_record,
|
|
140
|
+
fig_record: FigureRecord,
|
|
141
|
+
row: int,
|
|
142
|
+
col: int,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Replay all calls from ax_record onto target axes.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
target_ax : RecordingAxes
|
|
149
|
+
Target axes to replay onto.
|
|
150
|
+
ax_record : AxesRecord
|
|
151
|
+
Source axes record with calls.
|
|
152
|
+
fig_record : FigureRecord
|
|
153
|
+
Figure record to update.
|
|
154
|
+
row, col : int
|
|
155
|
+
Target position for recording.
|
|
156
|
+
"""
|
|
157
|
+
from .._reproducer._core import _replay_call
|
|
158
|
+
|
|
159
|
+
mpl_ax = target_ax._ax if hasattr(target_ax, "_ax") else target_ax
|
|
160
|
+
result_cache: Dict[str, Any] = {}
|
|
161
|
+
|
|
162
|
+
# Replay plotting calls
|
|
163
|
+
for call in ax_record.calls:
|
|
164
|
+
result = _replay_call(mpl_ax, call, result_cache)
|
|
165
|
+
if result is not None:
|
|
166
|
+
result_cache[call.id] = result
|
|
167
|
+
|
|
168
|
+
# Replay decoration calls
|
|
169
|
+
for call in ax_record.decorations:
|
|
170
|
+
result = _replay_call(mpl_ax, call, result_cache)
|
|
171
|
+
if result is not None:
|
|
172
|
+
result_cache[call.id] = result
|
|
173
|
+
|
|
174
|
+
# Update figure record with imported axes
|
|
175
|
+
ax_key = f"ax_{row}_{col}"
|
|
176
|
+
fig_record.axes[ax_key] = ax_record
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
__all__ = ["compose"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Import axes from external recipes into existing figures."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Tuple, Union
|
|
7
|
+
|
|
8
|
+
from .._recorder import FigureRecord
|
|
9
|
+
from .._serializer import load_recipe
|
|
10
|
+
from .._wrappers import RecordingAxes, RecordingFigure
|
|
11
|
+
from ._compose import _replay_axes_record
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def import_axes(
|
|
15
|
+
fig: RecordingFigure,
|
|
16
|
+
target_position: Tuple[int, int],
|
|
17
|
+
source: Union[str, Path, FigureRecord],
|
|
18
|
+
source_axes: str = "ax_0_0",
|
|
19
|
+
) -> RecordingAxes:
|
|
20
|
+
"""Import axes from another recipe into an existing figure.
|
|
21
|
+
|
|
22
|
+
This function copies all plotting calls and decorations from a source
|
|
23
|
+
axes (in a recipe file or FigureRecord) to a target position in an
|
|
24
|
+
existing figure. The target axes is cleared before import.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
fig : RecordingFigure
|
|
29
|
+
Target figure to import into.
|
|
30
|
+
target_position : tuple
|
|
31
|
+
(row, col) position in target figure.
|
|
32
|
+
source : str, Path, or FigureRecord
|
|
33
|
+
Source recipe file path or FigureRecord object.
|
|
34
|
+
source_axes : str, optional
|
|
35
|
+
Key of axes to import from source (default: "ax_0_0").
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
RecordingAxes
|
|
40
|
+
The target axes after import.
|
|
41
|
+
|
|
42
|
+
Raises
|
|
43
|
+
------
|
|
44
|
+
ValueError
|
|
45
|
+
If source_axes key not found in source.
|
|
46
|
+
TypeError
|
|
47
|
+
If source is not a valid type.
|
|
48
|
+
|
|
49
|
+
Examples
|
|
50
|
+
--------
|
|
51
|
+
>>> import figrecipe as fr
|
|
52
|
+
>>> fig, axes = fr.subplots(1, 2)
|
|
53
|
+
>>> axes[0].plot([1, 2, 3], [1, 4, 9])
|
|
54
|
+
>>> fr.import_axes(fig, (0, 1), "analysis.yaml")
|
|
55
|
+
"""
|
|
56
|
+
# Load source if path
|
|
57
|
+
if isinstance(source, (str, Path)):
|
|
58
|
+
source_record = load_recipe(source)
|
|
59
|
+
elif isinstance(source, FigureRecord):
|
|
60
|
+
source_record = source
|
|
61
|
+
else:
|
|
62
|
+
raise TypeError(f"Invalid source type: {type(source)}")
|
|
63
|
+
|
|
64
|
+
# Get source axes record
|
|
65
|
+
ax_record = source_record.axes.get(source_axes)
|
|
66
|
+
if ax_record is None:
|
|
67
|
+
available = list(source_record.axes.keys())
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Axes '{source_axes}' not found in source. Available: {available}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Get target axes
|
|
73
|
+
row, col = target_position
|
|
74
|
+
target_ax = _get_target_axes(fig, row, col)
|
|
75
|
+
|
|
76
|
+
# Clear existing content
|
|
77
|
+
mpl_ax = target_ax._ax if hasattr(target_ax, "_ax") else target_ax
|
|
78
|
+
mpl_ax.clear()
|
|
79
|
+
|
|
80
|
+
# Replay source calls onto target
|
|
81
|
+
_replay_axes_record(target_ax, ax_record, fig.record, row, col)
|
|
82
|
+
|
|
83
|
+
return target_ax
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _get_target_axes(
|
|
87
|
+
fig: RecordingFigure,
|
|
88
|
+
row: int,
|
|
89
|
+
col: int,
|
|
90
|
+
) -> RecordingAxes:
|
|
91
|
+
"""Get target axes from figure at position.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
fig : RecordingFigure
|
|
96
|
+
The figure.
|
|
97
|
+
row, col : int
|
|
98
|
+
Target position.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
RecordingAxes
|
|
103
|
+
Axes at position.
|
|
104
|
+
|
|
105
|
+
Raises
|
|
106
|
+
------
|
|
107
|
+
IndexError
|
|
108
|
+
If position is out of range.
|
|
109
|
+
"""
|
|
110
|
+
if not hasattr(fig, "_axes"):
|
|
111
|
+
raise ValueError("Figure must have _axes attribute")
|
|
112
|
+
|
|
113
|
+
axes = fig._axes
|
|
114
|
+
try:
|
|
115
|
+
# Handle different axes array structures
|
|
116
|
+
if isinstance(axes, list):
|
|
117
|
+
if isinstance(axes[0], list):
|
|
118
|
+
return axes[row][col]
|
|
119
|
+
else:
|
|
120
|
+
return axes[max(row, col)]
|
|
121
|
+
else:
|
|
122
|
+
return axes[row, col]
|
|
123
|
+
except (IndexError, KeyError) as e:
|
|
124
|
+
raise IndexError(f"Position ({row}, {col}) out of range for figure axes") from e
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = ["import_axes"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Panel visibility management for composition feature."""
|
|
4
|
+
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
from .._wrappers import RecordingFigure
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def hide_panel(fig: RecordingFigure, position: Tuple[int, int]) -> None:
|
|
11
|
+
"""Hide a panel (visually drop it without deleting data).
|
|
12
|
+
|
|
13
|
+
The panel data is preserved in the recipe but not rendered.
|
|
14
|
+
Use show_panel() to restore visibility.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
fig : RecordingFigure
|
|
19
|
+
The figure containing the panel.
|
|
20
|
+
position : tuple
|
|
21
|
+
(row, col) position of the panel to hide.
|
|
22
|
+
|
|
23
|
+
Examples
|
|
24
|
+
--------
|
|
25
|
+
>>> import figrecipe as fr
|
|
26
|
+
>>> fig, axes = fr.subplots(1, 2)
|
|
27
|
+
>>> axes[0].plot([1, 2], [1, 2])
|
|
28
|
+
>>> fr.hide_panel(fig, (0, 1)) # Hide empty second panel
|
|
29
|
+
"""
|
|
30
|
+
ax_key = f"ax_{position[0]}_{position[1]}"
|
|
31
|
+
if ax_key in fig.record.axes:
|
|
32
|
+
fig.record.axes[ax_key].visible = False
|
|
33
|
+
_set_axes_visible(fig, position, False)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def show_panel(fig: RecordingFigure, position: Tuple[int, int]) -> None:
|
|
37
|
+
"""Show a previously hidden panel.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
fig : RecordingFigure
|
|
42
|
+
The figure containing the panel.
|
|
43
|
+
position : tuple
|
|
44
|
+
(row, col) position of the panel to show.
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
>>> import figrecipe as fr
|
|
49
|
+
>>> fig, axes = fr.subplots(1, 2)
|
|
50
|
+
>>> fr.hide_panel(fig, (0, 1))
|
|
51
|
+
>>> fr.show_panel(fig, (0, 1)) # Restore visibility
|
|
52
|
+
"""
|
|
53
|
+
ax_key = f"ax_{position[0]}_{position[1]}"
|
|
54
|
+
if ax_key in fig.record.axes:
|
|
55
|
+
fig.record.axes[ax_key].visible = True
|
|
56
|
+
_set_axes_visible(fig, position, True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def toggle_panel(fig: RecordingFigure, position: Tuple[int, int]) -> bool:
|
|
60
|
+
"""Toggle panel visibility.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
fig : RecordingFigure
|
|
65
|
+
The figure containing the panel.
|
|
66
|
+
position : tuple
|
|
67
|
+
(row, col) position of the panel.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
bool
|
|
72
|
+
New visibility state (True = visible, False = hidden).
|
|
73
|
+
|
|
74
|
+
Examples
|
|
75
|
+
--------
|
|
76
|
+
>>> import figrecipe as fr
|
|
77
|
+
>>> fig, ax = fr.subplots()
|
|
78
|
+
>>> fr.toggle_panel(fig, (0, 0)) # Returns False (now hidden)
|
|
79
|
+
>>> fr.toggle_panel(fig, (0, 0)) # Returns True (now visible)
|
|
80
|
+
"""
|
|
81
|
+
ax_key = f"ax_{position[0]}_{position[1]}"
|
|
82
|
+
if ax_key in fig.record.axes:
|
|
83
|
+
current = fig.record.axes[ax_key].visible
|
|
84
|
+
if current:
|
|
85
|
+
hide_panel(fig, position)
|
|
86
|
+
else:
|
|
87
|
+
show_panel(fig, position)
|
|
88
|
+
return not current
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _set_axes_visible(
|
|
93
|
+
fig: RecordingFigure,
|
|
94
|
+
position: Tuple[int, int],
|
|
95
|
+
visible: bool,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Set matplotlib axes visibility.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
fig : RecordingFigure
|
|
102
|
+
The figure.
|
|
103
|
+
position : tuple
|
|
104
|
+
(row, col) position.
|
|
105
|
+
visible : bool
|
|
106
|
+
Whether to make visible or hidden.
|
|
107
|
+
"""
|
|
108
|
+
row, col = position
|
|
109
|
+
try:
|
|
110
|
+
axes = fig._axes
|
|
111
|
+
if isinstance(axes, list):
|
|
112
|
+
if isinstance(axes[0], list):
|
|
113
|
+
ax = axes[row][col]
|
|
114
|
+
else:
|
|
115
|
+
ax = axes[max(row, col)]
|
|
116
|
+
else:
|
|
117
|
+
ax = axes[row, col]
|
|
118
|
+
|
|
119
|
+
mpl_ax = ax._ax if hasattr(ax, "_ax") else ax
|
|
120
|
+
mpl_ax.set_visible(visible)
|
|
121
|
+
except (IndexError, AttributeError, KeyError):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["hide_panel", "show_panel", "toggle_panel"]
|
figrecipe/_dev/__init__.py
CHANGED
|
@@ -16,105 +16,16 @@ Usage:
|
|
|
16
16
|
results = run_all_demos(fr, output_dir="./outputs")
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
import
|
|
20
|
-
from
|
|
21
|
-
|
|
22
|
-
from .._params import PLOTTING_METHODS
|
|
23
|
-
|
|
24
|
-
# Auto-import plotters from demo_plotters directory
|
|
25
|
-
_demo_dir = Path(__file__).parent / "demo_plotters"
|
|
26
|
-
PLOTTERS = {}
|
|
27
|
-
|
|
28
|
-
for method_name in sorted(PLOTTING_METHODS):
|
|
29
|
-
module_name = f"plot_{method_name}"
|
|
30
|
-
func_name = f"plot_{method_name}"
|
|
31
|
-
module_path = _demo_dir / f"{module_name}.py"
|
|
32
|
-
|
|
33
|
-
if module_path.exists():
|
|
34
|
-
try:
|
|
35
|
-
module = importlib.import_module(
|
|
36
|
-
f".demo_plotters.{module_name}", package="figrecipe._dev"
|
|
37
|
-
)
|
|
38
|
-
if hasattr(module, func_name):
|
|
39
|
-
PLOTTERS[method_name] = getattr(module, func_name)
|
|
40
|
-
except ImportError:
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def list_plotters():
|
|
45
|
-
"""List all available plotter names."""
|
|
46
|
-
return list(PLOTTERS.keys())
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def get_plotter(name):
|
|
50
|
-
"""Get a plotter function by name.
|
|
51
|
-
|
|
52
|
-
Parameters
|
|
53
|
-
----------
|
|
54
|
-
name : str
|
|
55
|
-
Name of the plotting method (e.g., 'plot', 'scatter').
|
|
56
|
-
|
|
57
|
-
Returns
|
|
58
|
-
-------
|
|
59
|
-
callable
|
|
60
|
-
The plotter function with signature (plt, rng, ax=None) -> (fig, ax).
|
|
61
|
-
"""
|
|
62
|
-
if name in PLOTTERS:
|
|
63
|
-
return PLOTTERS[name]
|
|
64
|
-
raise KeyError(f"Unknown plotter: {name}. Available: {list(PLOTTERS.keys())}")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def run_all_demos(plt, output_dir=None, show=False):
|
|
68
|
-
"""Run all demo plotters and optionally save outputs.
|
|
69
|
-
|
|
70
|
-
Parameters
|
|
71
|
-
----------
|
|
72
|
-
plt : module
|
|
73
|
-
figrecipe module (e.g., `import figrecipe as fr`).
|
|
74
|
-
output_dir : Path or str, optional
|
|
75
|
-
Directory to save output images.
|
|
76
|
-
show : bool
|
|
77
|
-
Whether to show figures interactively.
|
|
78
|
-
|
|
79
|
-
Returns
|
|
80
|
-
-------
|
|
81
|
-
dict
|
|
82
|
-
Results for each demo: {name: {'success': bool, 'error': str or None}}
|
|
83
|
-
"""
|
|
84
|
-
import matplotlib.pyplot as _plt
|
|
85
|
-
import numpy as np
|
|
86
|
-
|
|
87
|
-
rng = np.random.default_rng(42)
|
|
88
|
-
results = {}
|
|
89
|
-
|
|
90
|
-
if output_dir:
|
|
91
|
-
output_dir = Path(output_dir)
|
|
92
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
-
|
|
94
|
-
for name, func in PLOTTERS.items():
|
|
95
|
-
try:
|
|
96
|
-
fig, ax = func(plt, rng)
|
|
97
|
-
if output_dir:
|
|
98
|
-
out_path = output_dir / f"plot_{name}.png"
|
|
99
|
-
mpl_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
100
|
-
mpl_fig.savefig(out_path, dpi=100, bbox_inches="tight")
|
|
101
|
-
if show:
|
|
102
|
-
_plt.show()
|
|
103
|
-
else:
|
|
104
|
-
mpl_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
105
|
-
_plt.close(mpl_fig)
|
|
106
|
-
results[name] = {"success": True, "error": None}
|
|
107
|
-
except Exception as e:
|
|
108
|
-
results[name] = {"success": False, "error": str(e)}
|
|
109
|
-
|
|
110
|
-
return results
|
|
111
|
-
|
|
19
|
+
from . import browser
|
|
20
|
+
from ._plotters import PLOTTERS, get_plotter, list_plotters
|
|
21
|
+
from ._run_demos import run_all_demos
|
|
112
22
|
|
|
113
23
|
__all__ = [
|
|
114
24
|
"PLOTTERS",
|
|
115
25
|
"list_plotters",
|
|
116
26
|
"get_plotter",
|
|
117
27
|
"run_all_demos",
|
|
28
|
+
"browser",
|
|
118
29
|
]
|
|
119
30
|
|
|
120
31
|
# EOF
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Plotter registry for demo plotters."""
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .._params import PLOTTING_METHODS
|
|
9
|
+
|
|
10
|
+
# Auto-import plotters from demo_plotters subdirectories
|
|
11
|
+
_demo_dir = Path(__file__).parent / "demo_plotters"
|
|
12
|
+
PLOTTERS = {}
|
|
13
|
+
|
|
14
|
+
# Category subdirectories
|
|
15
|
+
_category_dirs = [
|
|
16
|
+
"line_curve",
|
|
17
|
+
"scatter_points",
|
|
18
|
+
"bar_categorical",
|
|
19
|
+
"distribution",
|
|
20
|
+
"image_matrix",
|
|
21
|
+
"contour_surface",
|
|
22
|
+
"spectral_signal",
|
|
23
|
+
"vector_flow",
|
|
24
|
+
"special",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Build mapping from method_name to category
|
|
28
|
+
_method_to_category = {}
|
|
29
|
+
for cat_dir in _category_dirs:
|
|
30
|
+
cat_path = _demo_dir / cat_dir
|
|
31
|
+
if cat_path.is_dir():
|
|
32
|
+
for plot_file in cat_path.glob("plot_*.py"):
|
|
33
|
+
method_name = plot_file.stem.replace("plot_", "")
|
|
34
|
+
_method_to_category[method_name] = cat_dir
|
|
35
|
+
|
|
36
|
+
for method_name in sorted(PLOTTING_METHODS):
|
|
37
|
+
module_name = f"plot_{method_name}"
|
|
38
|
+
func_name = f"plot_{method_name}"
|
|
39
|
+
|
|
40
|
+
# Check if we have this plotter in a category subdirectory
|
|
41
|
+
if method_name in _method_to_category:
|
|
42
|
+
cat_dir = _method_to_category[method_name]
|
|
43
|
+
try:
|
|
44
|
+
module = importlib.import_module(
|
|
45
|
+
f".demo_plotters.{cat_dir}.{module_name}", package="figrecipe._dev"
|
|
46
|
+
)
|
|
47
|
+
if hasattr(module, func_name):
|
|
48
|
+
PLOTTERS[method_name] = getattr(module, func_name)
|
|
49
|
+
except ImportError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def list_plotters():
|
|
54
|
+
"""List all available plotter names."""
|
|
55
|
+
return list(PLOTTERS.keys())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_plotter(name):
|
|
59
|
+
"""Get a plotter function by name.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
name : str
|
|
64
|
+
Name of the plotting method (e.g., 'plot', 'scatter').
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
callable
|
|
69
|
+
The plotter function with signature (plt, rng, ax=None) -> (fig, ax).
|
|
70
|
+
"""
|
|
71
|
+
if name in PLOTTERS:
|
|
72
|
+
return PLOTTERS[name]
|
|
73
|
+
raise KeyError(f"Unknown plotter: {name}. Available: {list(PLOTTERS.keys())}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# EOF
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Demo runner for all plotters."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ._plotters import PLOTTERS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_all_demos(plt, output_dir=None, show=False):
|
|
11
|
+
"""Run all demo plotters and optionally save outputs.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
plt : module
|
|
16
|
+
figrecipe module (e.g., `import figrecipe as fr`).
|
|
17
|
+
output_dir : Path or str, optional
|
|
18
|
+
Directory to save output images.
|
|
19
|
+
show : bool
|
|
20
|
+
Whether to show figures interactively.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
dict
|
|
25
|
+
Results for each demo: {name: {'success': bool, 'error': str or None}}
|
|
26
|
+
"""
|
|
27
|
+
import matplotlib.pyplot as _plt
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
rng = np.random.default_rng(42)
|
|
31
|
+
results = {}
|
|
32
|
+
|
|
33
|
+
if output_dir:
|
|
34
|
+
output_dir = Path(output_dir)
|
|
35
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
for name, func in PLOTTERS.items():
|
|
38
|
+
try:
|
|
39
|
+
fig, ax = func(plt, rng)
|
|
40
|
+
if output_dir:
|
|
41
|
+
out_path = output_dir / f"plot_{name}.png"
|
|
42
|
+
mpl_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
43
|
+
mpl_fig.savefig(out_path, dpi=100, bbox_inches="tight")
|
|
44
|
+
if show:
|
|
45
|
+
_plt.show()
|
|
46
|
+
else:
|
|
47
|
+
mpl_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
48
|
+
_plt.close(mpl_fig)
|
|
49
|
+
results[name] = {"success": True, "error": None}
|
|
50
|
+
except Exception as e:
|
|
51
|
+
results[name] = {"success": False, "error": str(e)}
|
|
52
|
+
|
|
53
|
+
return results
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# EOF
|