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
figrecipe/_cli/_style.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""style command - Style management subcommands."""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def style() -> None:
|
|
11
|
+
"""Manage figure styles and presets."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@style.command("list")
|
|
16
|
+
def list_styles() -> None:
|
|
17
|
+
"""List available style presets."""
|
|
18
|
+
from .. import list_presets
|
|
19
|
+
|
|
20
|
+
presets = list_presets()
|
|
21
|
+
|
|
22
|
+
click.echo("Available style presets:")
|
|
23
|
+
for preset in presets:
|
|
24
|
+
click.echo(f" - {preset}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@style.command("show")
|
|
28
|
+
@click.argument("name")
|
|
29
|
+
def show_style(name: str) -> None:
|
|
30
|
+
"""Show details of a style preset.
|
|
31
|
+
|
|
32
|
+
NAME is the preset name (e.g., SCITEX, MATPLOTLIB).
|
|
33
|
+
"""
|
|
34
|
+
from ruamel.yaml import YAML
|
|
35
|
+
|
|
36
|
+
from ..styles._style_loader import load_preset
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
style_dict = load_preset(name)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise click.ClickException(f"Failed to load preset '{name}': {e}") from e
|
|
42
|
+
|
|
43
|
+
yaml = YAML()
|
|
44
|
+
yaml.default_flow_style = False
|
|
45
|
+
|
|
46
|
+
click.echo(f"Style preset: {name}\n")
|
|
47
|
+
|
|
48
|
+
import io
|
|
49
|
+
|
|
50
|
+
stream = io.StringIO()
|
|
51
|
+
yaml.dump(style_dict, stream)
|
|
52
|
+
click.echo(stream.getvalue())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@style.command("apply")
|
|
56
|
+
@click.argument("name")
|
|
57
|
+
def apply_style_cmd(name: str) -> None:
|
|
58
|
+
"""Apply a style preset globally.
|
|
59
|
+
|
|
60
|
+
NAME is the preset name (e.g., SCITEX, MATPLOTLIB).
|
|
61
|
+
"""
|
|
62
|
+
from .. import load_style
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
load_style(name)
|
|
66
|
+
click.echo(f"Applied style: {name}")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise click.ClickException(f"Failed to apply style: {e}") from e
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@style.command("reset")
|
|
72
|
+
def reset_style() -> None:
|
|
73
|
+
"""Reset to default matplotlib style."""
|
|
74
|
+
from .. import unload_style
|
|
75
|
+
|
|
76
|
+
unload_style()
|
|
77
|
+
click.echo("Style reset to defaults.")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""validate command - Verify recipe reproducibility."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.argument("source", type=click.Path(exists=True))
|
|
12
|
+
@click.option(
|
|
13
|
+
"--threshold",
|
|
14
|
+
type=float,
|
|
15
|
+
default=100.0,
|
|
16
|
+
help="MSE threshold for validation (default: 100).",
|
|
17
|
+
)
|
|
18
|
+
@click.option(
|
|
19
|
+
"--strict",
|
|
20
|
+
is_flag=True,
|
|
21
|
+
help="Fail on any difference.",
|
|
22
|
+
)
|
|
23
|
+
@click.option(
|
|
24
|
+
"-q",
|
|
25
|
+
"--quiet",
|
|
26
|
+
is_flag=True,
|
|
27
|
+
help="Only output pass/fail status.",
|
|
28
|
+
)
|
|
29
|
+
def validate(
|
|
30
|
+
source: str,
|
|
31
|
+
threshold: float,
|
|
32
|
+
strict: bool,
|
|
33
|
+
quiet: bool,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Validate that a recipe reproduces its original figure.
|
|
36
|
+
|
|
37
|
+
SOURCE is the path to a .yaml recipe file.
|
|
38
|
+
"""
|
|
39
|
+
from .. import validate as fr_validate
|
|
40
|
+
|
|
41
|
+
source_path = Path(source)
|
|
42
|
+
|
|
43
|
+
if strict:
|
|
44
|
+
threshold = 0.0
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
result = fr_validate(source_path, mse_threshold=threshold)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
raise click.ClickException(f"Validation failed: {e}") from e
|
|
50
|
+
|
|
51
|
+
if quiet:
|
|
52
|
+
if result.valid:
|
|
53
|
+
click.echo("PASS")
|
|
54
|
+
else:
|
|
55
|
+
click.echo("FAIL")
|
|
56
|
+
raise SystemExit(1)
|
|
57
|
+
else:
|
|
58
|
+
click.echo(f"Validation: {'PASS' if result.valid else 'FAIL'}")
|
|
59
|
+
click.echo(f"MSE: {result.mse:.6f}")
|
|
60
|
+
click.echo(f"Threshold: {threshold}")
|
|
61
|
+
|
|
62
|
+
if hasattr(result, "message") and result.message:
|
|
63
|
+
click.echo(f"Message: {result.message}")
|
|
64
|
+
|
|
65
|
+
if not result.valid:
|
|
66
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""version command - Show version information."""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command()
|
|
9
|
+
@click.option(
|
|
10
|
+
"--full",
|
|
11
|
+
is_flag=True,
|
|
12
|
+
help="Show full version info with dependencies.",
|
|
13
|
+
)
|
|
14
|
+
def version(full: bool) -> None:
|
|
15
|
+
"""Show version information."""
|
|
16
|
+
from .. import __version__
|
|
17
|
+
|
|
18
|
+
click.echo(f"figrecipe {__version__}")
|
|
19
|
+
|
|
20
|
+
if full:
|
|
21
|
+
click.echo()
|
|
22
|
+
_show_dependency_versions()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _show_dependency_versions() -> None:
|
|
26
|
+
"""Show versions of key dependencies."""
|
|
27
|
+
deps = [
|
|
28
|
+
("matplotlib", "matplotlib"),
|
|
29
|
+
("numpy", "numpy"),
|
|
30
|
+
("ruamel.yaml", "ruamel.yaml"),
|
|
31
|
+
("scipy", "scipy"),
|
|
32
|
+
("Pillow", "PIL"),
|
|
33
|
+
("seaborn", "seaborn"),
|
|
34
|
+
("pandas", "pandas"),
|
|
35
|
+
("flask", "flask"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
click.echo("Dependencies:")
|
|
39
|
+
for name, module in deps:
|
|
40
|
+
try:
|
|
41
|
+
mod = __import__(module)
|
|
42
|
+
ver = getattr(mod, "__version__", "unknown")
|
|
43
|
+
click.echo(f" {name}: {ver}")
|
|
44
|
+
except ImportError:
|
|
45
|
+
click.echo(f" {name}: not installed")
|
|
46
|
+
|
|
47
|
+
# Python version
|
|
48
|
+
import sys
|
|
49
|
+
|
|
50
|
+
click.echo(f"\nPython: {sys.version}")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Composition module for combining multiple figures.
|
|
4
|
+
|
|
5
|
+
This module provides functionality to:
|
|
6
|
+
- Compose new figures from multiple recipe sources
|
|
7
|
+
- Import axes from external recipes into existing figures
|
|
8
|
+
- Hide/show panels for visual composition
|
|
9
|
+
- Align and distribute panels
|
|
10
|
+
|
|
11
|
+
Phase 1-3 of the composition feature.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ._alignment import AlignmentMode, align_panels, distribute_panels, smart_align
|
|
15
|
+
from ._compose import compose
|
|
16
|
+
from ._import_axes import import_axes
|
|
17
|
+
from ._visibility import hide_panel, show_panel, toggle_panel
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
# Phase 1: Composition
|
|
21
|
+
"compose",
|
|
22
|
+
"import_axes",
|
|
23
|
+
# Phase 2: Visibility
|
|
24
|
+
"hide_panel",
|
|
25
|
+
"show_panel",
|
|
26
|
+
"toggle_panel",
|
|
27
|
+
# Phase 3: Alignment
|
|
28
|
+
"AlignmentMode",
|
|
29
|
+
"align_panels",
|
|
30
|
+
"distribute_panels",
|
|
31
|
+
"smart_align",
|
|
32
|
+
]
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Panel alignment tools for composition feature.
|
|
4
|
+
|
|
5
|
+
Provides alignment and distribution functions for multi-panel figures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import List, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
from matplotlib.transforms import Bbox
|
|
12
|
+
|
|
13
|
+
from .._wrappers import RecordingFigure
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AlignmentMode(Enum):
|
|
17
|
+
"""Alignment modes for panel positioning."""
|
|
18
|
+
|
|
19
|
+
LEFT = "left"
|
|
20
|
+
RIGHT = "right"
|
|
21
|
+
TOP = "top"
|
|
22
|
+
BOTTOM = "bottom"
|
|
23
|
+
CENTER_H = "center_h" # Horizontal center
|
|
24
|
+
CENTER_V = "center_v" # Vertical center
|
|
25
|
+
AXIS_X = "axis_x" # Align x-axes
|
|
26
|
+
AXIS_Y = "axis_y" # Align y-axes
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def align_panels(
|
|
30
|
+
fig: RecordingFigure,
|
|
31
|
+
panels: List[Tuple[int, int]],
|
|
32
|
+
mode: Union[str, AlignmentMode],
|
|
33
|
+
reference: Optional[Tuple[int, int]] = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Align multiple panels to a reference panel.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
fig : RecordingFigure
|
|
40
|
+
The figure containing the panels.
|
|
41
|
+
panels : list of tuple
|
|
42
|
+
List of (row, col) positions to align.
|
|
43
|
+
mode : str or AlignmentMode
|
|
44
|
+
Alignment mode: 'left', 'right', 'top', 'bottom',
|
|
45
|
+
'center_h', 'center_v', 'axis_x', 'axis_y'.
|
|
46
|
+
reference : tuple, optional
|
|
47
|
+
Reference panel position. If None, uses first panel.
|
|
48
|
+
|
|
49
|
+
Examples
|
|
50
|
+
--------
|
|
51
|
+
>>> import figrecipe as fr
|
|
52
|
+
>>> fig, axes = fr.subplots(2, 2)
|
|
53
|
+
>>> # Align left column panels to left edge
|
|
54
|
+
>>> fr.align_panels(fig, [(0, 0), (1, 0)], mode="left")
|
|
55
|
+
"""
|
|
56
|
+
mode = AlignmentMode(mode) if isinstance(mode, str) else mode
|
|
57
|
+
|
|
58
|
+
if not panels:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
ref_pos = reference or panels[0]
|
|
62
|
+
ref_ax = _get_mpl_axes(fig, ref_pos)
|
|
63
|
+
if ref_ax is None:
|
|
64
|
+
return
|
|
65
|
+
ref_bbox = ref_ax.get_position()
|
|
66
|
+
|
|
67
|
+
for pos in panels:
|
|
68
|
+
if pos == ref_pos:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
ax = _get_mpl_axes(fig, pos)
|
|
72
|
+
if ax is None:
|
|
73
|
+
continue
|
|
74
|
+
bbox = ax.get_position()
|
|
75
|
+
new_bbox = _calculate_aligned_bbox(bbox, ref_bbox, mode)
|
|
76
|
+
ax.set_position(new_bbox)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def distribute_panels(
|
|
80
|
+
fig: RecordingFigure,
|
|
81
|
+
panels: List[Tuple[int, int]],
|
|
82
|
+
direction: str = "horizontal",
|
|
83
|
+
spacing_mm: Optional[float] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Distribute panels evenly with optional fixed spacing.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
fig : RecordingFigure
|
|
90
|
+
The figure containing the panels.
|
|
91
|
+
panels : list of tuple
|
|
92
|
+
List of (row, col) positions to distribute.
|
|
93
|
+
direction : str
|
|
94
|
+
'horizontal' or 'vertical'.
|
|
95
|
+
spacing_mm : float, optional
|
|
96
|
+
Fixed spacing in mm. If None, distribute evenly within
|
|
97
|
+
current bounds.
|
|
98
|
+
|
|
99
|
+
Examples
|
|
100
|
+
--------
|
|
101
|
+
>>> import figrecipe as fr
|
|
102
|
+
>>> fig, axes = fr.subplots(1, 3)
|
|
103
|
+
>>> # Distribute evenly
|
|
104
|
+
>>> fr.distribute_panels(fig, [(0, 0), (0, 1), (0, 2)])
|
|
105
|
+
>>> # With fixed 5mm spacing
|
|
106
|
+
>>> fr.distribute_panels(fig, [(0, 0), (0, 1), (0, 2)], spacing_mm=5)
|
|
107
|
+
"""
|
|
108
|
+
if len(panels) < 2:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# Sort panels by position
|
|
112
|
+
if direction == "horizontal":
|
|
113
|
+
sorted_panels = sorted(panels, key=lambda p: p[1])
|
|
114
|
+
else:
|
|
115
|
+
sorted_panels = sorted(panels, key=lambda p: p[0])
|
|
116
|
+
|
|
117
|
+
# Get bounding boxes
|
|
118
|
+
bboxes = []
|
|
119
|
+
valid_panels = []
|
|
120
|
+
for p in sorted_panels:
|
|
121
|
+
ax = _get_mpl_axes(fig, p)
|
|
122
|
+
if ax is not None:
|
|
123
|
+
bboxes.append(ax.get_position())
|
|
124
|
+
valid_panels.append(p)
|
|
125
|
+
|
|
126
|
+
if len(valid_panels) < 2:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Calculate even distribution
|
|
130
|
+
if direction == "horizontal":
|
|
131
|
+
_distribute_horizontal(fig, valid_panels, bboxes, spacing_mm)
|
|
132
|
+
else:
|
|
133
|
+
_distribute_vertical(fig, valid_panels, bboxes, spacing_mm)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def smart_align(
|
|
137
|
+
fig: RecordingFigure,
|
|
138
|
+
panels: Optional[List[Tuple[int, int]]] = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Automatically align panels in a compact grid layout.
|
|
141
|
+
|
|
142
|
+
Works like human behavior:
|
|
143
|
+
1. Detect grid structure (nrows, ncols)
|
|
144
|
+
2. Place panels from top-left to bottom-right
|
|
145
|
+
3. Calculate minimum rectangle to cover all content in each row/column
|
|
146
|
+
4. Unify row heights and column widths
|
|
147
|
+
5. Use space effectively with theme margins and spacing
|
|
148
|
+
|
|
149
|
+
Uses margin and spacing values from the loaded SCITEX theme:
|
|
150
|
+
- margins.left_mm, margins.right_mm, margins.top_mm, margins.bottom_mm
|
|
151
|
+
- spacing.horizontal_mm, spacing.vertical_mm
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
fig : RecordingFigure
|
|
156
|
+
The figure containing the panels.
|
|
157
|
+
panels : list of tuple, optional
|
|
158
|
+
Specific panels to align. If None, aligns all panels.
|
|
159
|
+
|
|
160
|
+
Examples
|
|
161
|
+
--------
|
|
162
|
+
>>> import figrecipe as fr
|
|
163
|
+
>>> fig, axes = fr.subplots(2, 2)
|
|
164
|
+
>>> # ... add plots ...
|
|
165
|
+
>>> fr.smart_align(fig) # Align all panels using theme settings
|
|
166
|
+
"""
|
|
167
|
+
from .._utils._units import mm_to_inch
|
|
168
|
+
|
|
169
|
+
if panels is None:
|
|
170
|
+
panels = [tuple(map(int, ax_key.split("_")[1:3])) for ax_key in fig.record.axes]
|
|
171
|
+
|
|
172
|
+
if not panels:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Get matplotlib figure
|
|
176
|
+
mpl_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
177
|
+
|
|
178
|
+
# Get style from loaded theme
|
|
179
|
+
try:
|
|
180
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
181
|
+
|
|
182
|
+
style = _STYLE_CACHE
|
|
183
|
+
except (ImportError, AttributeError):
|
|
184
|
+
style = None
|
|
185
|
+
|
|
186
|
+
# Extract margin values from theme (with defaults)
|
|
187
|
+
if style and hasattr(style, "margins"):
|
|
188
|
+
margin_left = style.margins.get("left_mm", 6)
|
|
189
|
+
margin_right = style.margins.get("right_mm", 1)
|
|
190
|
+
margin_top = style.margins.get("top_mm", 5)
|
|
191
|
+
margin_bottom = style.margins.get("bottom_mm", 5)
|
|
192
|
+
else:
|
|
193
|
+
margin_left = margin_right = margin_top = margin_bottom = 5
|
|
194
|
+
|
|
195
|
+
# Extract spacing values from theme (with defaults)
|
|
196
|
+
if style and hasattr(style, "spacing"):
|
|
197
|
+
spacing_h_mm = style.spacing.get("horizontal_mm", 10)
|
|
198
|
+
spacing_v_mm = style.spacing.get("vertical_mm", 15)
|
|
199
|
+
else:
|
|
200
|
+
spacing_h_mm = 10
|
|
201
|
+
spacing_v_mm = 15
|
|
202
|
+
|
|
203
|
+
# Determine grid dimensions
|
|
204
|
+
max_row = max(p[0] for p in panels)
|
|
205
|
+
max_col = max(p[1] for p in panels)
|
|
206
|
+
nrows = max_row + 1
|
|
207
|
+
ncols = max_col + 1
|
|
208
|
+
|
|
209
|
+
# Get figure size in inches
|
|
210
|
+
fig_width, fig_height = mpl_fig.get_size_inches()
|
|
211
|
+
|
|
212
|
+
# Convert margins/spacing to figure fraction
|
|
213
|
+
margin_left_frac = mm_to_inch(margin_left) / fig_width
|
|
214
|
+
margin_right_frac = mm_to_inch(margin_right) / fig_width
|
|
215
|
+
margin_top_frac = mm_to_inch(margin_top) / fig_height
|
|
216
|
+
margin_bottom_frac = mm_to_inch(margin_bottom) / fig_height
|
|
217
|
+
spacing_frac_w = mm_to_inch(spacing_h_mm) / fig_width
|
|
218
|
+
spacing_frac_h = mm_to_inch(spacing_v_mm) / fig_height
|
|
219
|
+
|
|
220
|
+
# Build grid of axes
|
|
221
|
+
grid = {}
|
|
222
|
+
for pos in panels:
|
|
223
|
+
ax = _get_mpl_axes(fig, pos)
|
|
224
|
+
if ax is not None:
|
|
225
|
+
grid[pos] = ax
|
|
226
|
+
|
|
227
|
+
# Calculate content-based widths for each column
|
|
228
|
+
col_widths = []
|
|
229
|
+
for c in range(ncols):
|
|
230
|
+
max_width = 0
|
|
231
|
+
for r in range(nrows):
|
|
232
|
+
if (r, c) in grid:
|
|
233
|
+
bbox = grid[(r, c)].get_position()
|
|
234
|
+
max_width = max(max_width, bbox.width)
|
|
235
|
+
col_widths.append(max_width if max_width > 0 else 0.2)
|
|
236
|
+
|
|
237
|
+
# Calculate content-based heights for each row
|
|
238
|
+
row_heights = []
|
|
239
|
+
for r in range(nrows):
|
|
240
|
+
max_height = 0
|
|
241
|
+
for c in range(ncols):
|
|
242
|
+
if (r, c) in grid:
|
|
243
|
+
bbox = grid[(r, c)].get_position()
|
|
244
|
+
max_height = max(max_height, bbox.height)
|
|
245
|
+
row_heights.append(max_height if max_height > 0 else 0.15)
|
|
246
|
+
|
|
247
|
+
# Calculate total content size
|
|
248
|
+
total_content_w = sum(col_widths) + spacing_frac_w * (ncols - 1)
|
|
249
|
+
total_content_h = sum(row_heights) + spacing_frac_h * (nrows - 1)
|
|
250
|
+
|
|
251
|
+
# Available space after asymmetric margins
|
|
252
|
+
avail_w = 1.0 - margin_left_frac - margin_right_frac
|
|
253
|
+
avail_h = 1.0 - margin_top_frac - margin_bottom_frac
|
|
254
|
+
|
|
255
|
+
# Scale factor to fit content in available space
|
|
256
|
+
scale_w = avail_w / total_content_w if total_content_w > 0 else 1.0
|
|
257
|
+
scale_h = avail_h / total_content_h if total_content_h > 0 else 1.0
|
|
258
|
+
scale = min(scale_w, scale_h, 1.0) # Don't enlarge, only shrink if needed
|
|
259
|
+
|
|
260
|
+
# Apply scaling
|
|
261
|
+
col_widths = [w * scale for w in col_widths]
|
|
262
|
+
row_heights = [h * scale for h in row_heights]
|
|
263
|
+
spacing_w = spacing_frac_w * scale
|
|
264
|
+
spacing_h = spacing_frac_h * scale
|
|
265
|
+
|
|
266
|
+
# Recalculate total after scaling
|
|
267
|
+
total_w = sum(col_widths) + spacing_w * (ncols - 1)
|
|
268
|
+
total_h = sum(row_heights) + spacing_h * (nrows - 1)
|
|
269
|
+
|
|
270
|
+
# Position grid: left-aligned with left margin, centered vertically
|
|
271
|
+
start_x = margin_left_frac + (avail_w - total_w) / 2
|
|
272
|
+
|
|
273
|
+
# Position panels from top-left to bottom-right
|
|
274
|
+
# Matplotlib y=0 is bottom, so we work from top down
|
|
275
|
+
y = 1.0 - margin_top_frac - (avail_h - total_h) / 2
|
|
276
|
+
for r in range(nrows):
|
|
277
|
+
y -= row_heights[r]
|
|
278
|
+
x = start_x
|
|
279
|
+
for c in range(ncols):
|
|
280
|
+
if (r, c) in grid:
|
|
281
|
+
ax = grid[(r, c)]
|
|
282
|
+
new_bbox = Bbox.from_bounds(x, y, col_widths[c], row_heights[r])
|
|
283
|
+
ax.set_position(new_bbox)
|
|
284
|
+
x += col_widths[c] + spacing_w
|
|
285
|
+
y -= spacing_h
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _get_mpl_axes(fig: RecordingFigure, position: Tuple[int, int]):
|
|
289
|
+
"""Get matplotlib axes at position.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
fig : RecordingFigure
|
|
294
|
+
The figure.
|
|
295
|
+
position : tuple
|
|
296
|
+
(row, col) position.
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
matplotlib.axes.Axes or None
|
|
301
|
+
The matplotlib axes, or None if not found.
|
|
302
|
+
"""
|
|
303
|
+
row, col = position
|
|
304
|
+
try:
|
|
305
|
+
axes = fig._axes
|
|
306
|
+
if isinstance(axes, list):
|
|
307
|
+
if isinstance(axes[0], list):
|
|
308
|
+
ax = axes[row][col]
|
|
309
|
+
else:
|
|
310
|
+
# 1D list for single row/column
|
|
311
|
+
ax = axes[max(row, col)]
|
|
312
|
+
else:
|
|
313
|
+
# Numpy array
|
|
314
|
+
ax = axes[row, col]
|
|
315
|
+
|
|
316
|
+
return ax._ax if hasattr(ax, "_ax") else ax
|
|
317
|
+
except (IndexError, AttributeError, KeyError, TypeError):
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _calculate_aligned_bbox(
|
|
322
|
+
bbox: Bbox,
|
|
323
|
+
ref_bbox: Bbox,
|
|
324
|
+
mode: AlignmentMode,
|
|
325
|
+
) -> Bbox:
|
|
326
|
+
"""Calculate new bbox aligned to reference.
|
|
327
|
+
|
|
328
|
+
Parameters
|
|
329
|
+
----------
|
|
330
|
+
bbox : Bbox
|
|
331
|
+
Current bounding box.
|
|
332
|
+
ref_bbox : Bbox
|
|
333
|
+
Reference bounding box.
|
|
334
|
+
mode : AlignmentMode
|
|
335
|
+
Alignment mode.
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
-------
|
|
339
|
+
Bbox
|
|
340
|
+
New aligned bounding box.
|
|
341
|
+
"""
|
|
342
|
+
x0, y0 = bbox.x0, bbox.y0
|
|
343
|
+
width, height = bbox.width, bbox.height
|
|
344
|
+
|
|
345
|
+
if mode == AlignmentMode.LEFT:
|
|
346
|
+
x0 = ref_bbox.x0
|
|
347
|
+
elif mode == AlignmentMode.RIGHT:
|
|
348
|
+
x0 = ref_bbox.x1 - width
|
|
349
|
+
elif mode == AlignmentMode.TOP:
|
|
350
|
+
y0 = ref_bbox.y1 - height
|
|
351
|
+
elif mode == AlignmentMode.BOTTOM:
|
|
352
|
+
y0 = ref_bbox.y0
|
|
353
|
+
elif mode == AlignmentMode.CENTER_H:
|
|
354
|
+
x0 = ref_bbox.x0 + (ref_bbox.width - width) / 2
|
|
355
|
+
elif mode == AlignmentMode.CENTER_V:
|
|
356
|
+
y0 = ref_bbox.y0 + (ref_bbox.height - height) / 2
|
|
357
|
+
elif mode == AlignmentMode.AXIS_X:
|
|
358
|
+
# Align bottom edges (x-axis position)
|
|
359
|
+
y0 = ref_bbox.y0
|
|
360
|
+
elif mode == AlignmentMode.AXIS_Y:
|
|
361
|
+
# Align left edges (y-axis position)
|
|
362
|
+
x0 = ref_bbox.x0
|
|
363
|
+
|
|
364
|
+
return Bbox.from_bounds(x0, y0, width, height)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _distribute_horizontal(
|
|
368
|
+
fig: RecordingFigure,
|
|
369
|
+
panels: List[Tuple[int, int]],
|
|
370
|
+
bboxes: List[Bbox],
|
|
371
|
+
spacing_mm: Optional[float],
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Distribute panels horizontally.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
fig : RecordingFigure
|
|
378
|
+
The figure.
|
|
379
|
+
panels : list of tuple
|
|
380
|
+
Panel positions (sorted).
|
|
381
|
+
bboxes : list of Bbox
|
|
382
|
+
Current bounding boxes.
|
|
383
|
+
spacing_mm : float or None
|
|
384
|
+
Fixed spacing in mm, or None for even distribution.
|
|
385
|
+
"""
|
|
386
|
+
if spacing_mm is not None:
|
|
387
|
+
from .._utils._units import mm_to_inch
|
|
388
|
+
|
|
389
|
+
fig_width = fig.fig.get_figwidth()
|
|
390
|
+
spacing = mm_to_inch(spacing_mm) / fig_width
|
|
391
|
+
else:
|
|
392
|
+
total_width = sum(b.width for b in bboxes)
|
|
393
|
+
available = bboxes[-1].x1 - bboxes[0].x0
|
|
394
|
+
spacing = (
|
|
395
|
+
(available - total_width) / (len(panels) - 1) if len(panels) > 1 else 0
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
x = bboxes[0].x0
|
|
399
|
+
for panel, bbox in zip(panels, bboxes):
|
|
400
|
+
ax = _get_mpl_axes(fig, panel)
|
|
401
|
+
if ax is not None:
|
|
402
|
+
new_bbox = Bbox.from_bounds(x, bbox.y0, bbox.width, bbox.height)
|
|
403
|
+
ax.set_position(new_bbox)
|
|
404
|
+
x += bbox.width + spacing
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _distribute_vertical(
|
|
408
|
+
fig: RecordingFigure,
|
|
409
|
+
panels: List[Tuple[int, int]],
|
|
410
|
+
bboxes: List[Bbox],
|
|
411
|
+
spacing_mm: Optional[float],
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Distribute panels vertically.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
fig : RecordingFigure
|
|
418
|
+
The figure.
|
|
419
|
+
panels : list of tuple
|
|
420
|
+
Panel positions (sorted).
|
|
421
|
+
bboxes : list of Bbox
|
|
422
|
+
Current bounding boxes.
|
|
423
|
+
spacing_mm : float or None
|
|
424
|
+
Fixed spacing in mm, or None for even distribution.
|
|
425
|
+
"""
|
|
426
|
+
if spacing_mm is not None:
|
|
427
|
+
from .._utils._units import mm_to_inch
|
|
428
|
+
|
|
429
|
+
fig_height = fig.fig.get_figheight()
|
|
430
|
+
spacing = mm_to_inch(spacing_mm) / fig_height
|
|
431
|
+
else:
|
|
432
|
+
total_height = sum(b.height for b in bboxes)
|
|
433
|
+
available = bboxes[-1].y1 - bboxes[0].y0
|
|
434
|
+
spacing = (
|
|
435
|
+
(available - total_height) / (len(panels) - 1) if len(panels) > 1 else 0
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
y = bboxes[0].y0
|
|
439
|
+
for panel, bbox in zip(panels, bboxes):
|
|
440
|
+
ax = _get_mpl_axes(fig, panel)
|
|
441
|
+
if ax is not None:
|
|
442
|
+
new_bbox = Bbox.from_bounds(bbox.x0, y, bbox.width, bbox.height)
|
|
443
|
+
ax.set_position(new_bbox)
|
|
444
|
+
y += bbox.height + spacing
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
__all__ = [
|
|
448
|
+
"AlignmentMode",
|
|
449
|
+
"align_panels",
|
|
450
|
+
"distribute_panels",
|
|
451
|
+
"smart_align",
|
|
452
|
+
]
|