figrecipe 0.5.0__py3-none-any.whl → 0.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +220 -819
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +258 -0
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +193 -170
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""API helper modules for figrecipe.
|
|
4
|
+
|
|
5
|
+
This package contains helper functions extracted from the main __init__.py
|
|
6
|
+
to reduce file size and improve maintainability.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._extract import DECORATION_FUNCS, extract_call_data, to_array
|
|
10
|
+
from ._panel import calculate_panel_position, get_panel_label_fontsize
|
|
11
|
+
from ._save import (
|
|
12
|
+
IMAGE_EXTENSIONS,
|
|
13
|
+
YAML_EXTENSIONS,
|
|
14
|
+
get_save_dpi,
|
|
15
|
+
get_save_transparency,
|
|
16
|
+
resolve_save_paths,
|
|
17
|
+
)
|
|
18
|
+
from ._subplots import (
|
|
19
|
+
_apply_mm_layout_to_figure,
|
|
20
|
+
_apply_style_to_axes,
|
|
21
|
+
_calculate_mm_layout,
|
|
22
|
+
_check_mm_layout,
|
|
23
|
+
_get_mm_value,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Subplots helpers
|
|
28
|
+
"_get_mm_value",
|
|
29
|
+
"_check_mm_layout",
|
|
30
|
+
"_calculate_mm_layout",
|
|
31
|
+
"_apply_mm_layout_to_figure",
|
|
32
|
+
"_apply_style_to_axes",
|
|
33
|
+
# Save helpers
|
|
34
|
+
"IMAGE_EXTENSIONS",
|
|
35
|
+
"YAML_EXTENSIONS",
|
|
36
|
+
"resolve_save_paths",
|
|
37
|
+
"get_save_dpi",
|
|
38
|
+
"get_save_transparency",
|
|
39
|
+
# Extract helpers
|
|
40
|
+
"DECORATION_FUNCS",
|
|
41
|
+
"to_array",
|
|
42
|
+
"extract_call_data",
|
|
43
|
+
# Panel helpers
|
|
44
|
+
"get_panel_label_fontsize",
|
|
45
|
+
"calculate_panel_position",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# EOF
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Data extraction helpers for the public API."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Set
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
# Decoration functions to skip when extracting data
|
|
10
|
+
DECORATION_FUNCS: Set[str] = {
|
|
11
|
+
"set_xlabel",
|
|
12
|
+
"set_ylabel",
|
|
13
|
+
"set_title",
|
|
14
|
+
"set_xlim",
|
|
15
|
+
"set_ylim",
|
|
16
|
+
"legend",
|
|
17
|
+
"grid",
|
|
18
|
+
"axhline",
|
|
19
|
+
"axvline",
|
|
20
|
+
"text",
|
|
21
|
+
"annotate",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def to_array(data: Any) -> np.ndarray:
|
|
26
|
+
"""Convert data to numpy array, handling YAML types.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
data : any
|
|
31
|
+
Data to convert.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
np.ndarray
|
|
36
|
+
Converted numpy array.
|
|
37
|
+
"""
|
|
38
|
+
# Handle dict with 'data' key (serialized array format)
|
|
39
|
+
if isinstance(data, dict) or (hasattr(data, "keys") and "data" in data):
|
|
40
|
+
return np.array(data["data"])
|
|
41
|
+
if hasattr(data, "tolist"): # Already array-like
|
|
42
|
+
return np.array(data)
|
|
43
|
+
return np.array(
|
|
44
|
+
list(data) if hasattr(data, "__iter__") and not isinstance(data, str) else data
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extract_call_data(call) -> Dict[str, Any]:
|
|
49
|
+
"""Extract data arrays from a single call record.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
call : CallRecord
|
|
54
|
+
The call to extract data from.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
dict
|
|
59
|
+
Dictionary with extracted data arrays.
|
|
60
|
+
"""
|
|
61
|
+
call_data = {}
|
|
62
|
+
|
|
63
|
+
# Extract positional arguments based on function type
|
|
64
|
+
if call.function in ("plot", "scatter", "fill_between"):
|
|
65
|
+
if len(call.args) >= 1:
|
|
66
|
+
call_data["x"] = to_array(call.args[0])
|
|
67
|
+
if len(call.args) >= 2:
|
|
68
|
+
call_data["y"] = to_array(call.args[1])
|
|
69
|
+
|
|
70
|
+
elif call.function == "bar":
|
|
71
|
+
if len(call.args) >= 1:
|
|
72
|
+
call_data["x"] = to_array(call.args[0])
|
|
73
|
+
if len(call.args) >= 2:
|
|
74
|
+
call_data["height"] = to_array(call.args[1])
|
|
75
|
+
|
|
76
|
+
elif call.function == "hist":
|
|
77
|
+
if len(call.args) >= 1:
|
|
78
|
+
call_data["x"] = to_array(call.args[0])
|
|
79
|
+
|
|
80
|
+
elif call.function == "errorbar":
|
|
81
|
+
if len(call.args) >= 1:
|
|
82
|
+
call_data["x"] = to_array(call.args[0])
|
|
83
|
+
if len(call.args) >= 2:
|
|
84
|
+
call_data["y"] = to_array(call.args[1])
|
|
85
|
+
|
|
86
|
+
# Extract relevant kwargs
|
|
87
|
+
for key in ("c", "s", "yerr", "xerr", "weights", "bins"):
|
|
88
|
+
if key in call.kwargs:
|
|
89
|
+
val = call.kwargs[key]
|
|
90
|
+
if (
|
|
91
|
+
isinstance(val, (list, tuple))
|
|
92
|
+
or hasattr(val, "__iter__")
|
|
93
|
+
and not isinstance(val, str)
|
|
94
|
+
):
|
|
95
|
+
call_data[key] = to_array(val)
|
|
96
|
+
else:
|
|
97
|
+
call_data[key] = val
|
|
98
|
+
|
|
99
|
+
return call_data
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
__all__ = [
|
|
103
|
+
"DECORATION_FUNCS",
|
|
104
|
+
"to_array",
|
|
105
|
+
"extract_call_data",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
# EOF
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Notebook utilities for figrecipe.
|
|
4
|
+
|
|
5
|
+
Provides SVG rendering for Jupyter notebooks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"enable_svg",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# Notebook display format flag (set once per session)
|
|
13
|
+
_notebook_format_set = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _enable_notebook_svg():
|
|
17
|
+
"""Enable SVG format for Jupyter notebook display.
|
|
18
|
+
|
|
19
|
+
This provides crisp vector graphics at any zoom level.
|
|
20
|
+
Called automatically when load_style() or subplots() is used.
|
|
21
|
+
"""
|
|
22
|
+
global _notebook_format_set
|
|
23
|
+
if _notebook_format_set:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Method 1: matplotlib_inline (IPython 7.0+, JupyterLab)
|
|
28
|
+
from matplotlib_inline.backend_inline import set_matplotlib_formats
|
|
29
|
+
|
|
30
|
+
set_matplotlib_formats("svg")
|
|
31
|
+
_notebook_format_set = True
|
|
32
|
+
except (ImportError, Exception):
|
|
33
|
+
try:
|
|
34
|
+
# Method 2: IPython config (older IPython)
|
|
35
|
+
from IPython import get_ipython
|
|
36
|
+
|
|
37
|
+
ipython = get_ipython()
|
|
38
|
+
if ipython is not None and hasattr(ipython, "kernel"):
|
|
39
|
+
# Only run in actual Jupyter kernel, not IPython console
|
|
40
|
+
ipython.run_line_magic(
|
|
41
|
+
"config", "InlineBackend.figure_formats = ['svg']"
|
|
42
|
+
)
|
|
43
|
+
_notebook_format_set = True
|
|
44
|
+
except Exception:
|
|
45
|
+
pass # Not in Jupyter environment or method not available
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def enable_svg():
|
|
49
|
+
"""Manually enable SVG format for Jupyter notebook display.
|
|
50
|
+
|
|
51
|
+
Call this if figures appear pixelated in notebooks.
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
>>> import figrecipe as fr
|
|
56
|
+
>>> fr.enable_svg() # Enable SVG rendering
|
|
57
|
+
>>> fig, ax = fr.subplots() # Now renders as crisp SVG
|
|
58
|
+
"""
|
|
59
|
+
global _notebook_format_set
|
|
60
|
+
_notebook_format_set = False # Force re-application
|
|
61
|
+
_enable_notebook_svg()
|
figrecipe/_api/_panel.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Panel label helper for the public API."""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_panel_label_fontsize(explicit_fontsize: Optional[float] = None) -> float:
|
|
9
|
+
"""Get fontsize for panel labels from style or default."""
|
|
10
|
+
if explicit_fontsize is not None:
|
|
11
|
+
return explicit_fontsize
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
15
|
+
|
|
16
|
+
if _STYLE_CACHE is not None:
|
|
17
|
+
return getattr(getattr(_STYLE_CACHE, "fonts", None), "panel_label_pt", 10)
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
return 10
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def calculate_panel_position(
|
|
24
|
+
loc: str,
|
|
25
|
+
offset: Tuple[float, float],
|
|
26
|
+
) -> Tuple[float, float]:
|
|
27
|
+
"""Calculate x, y position based on location and offset."""
|
|
28
|
+
if loc == "upper left":
|
|
29
|
+
x, y = offset
|
|
30
|
+
elif loc == "upper right":
|
|
31
|
+
x, y = 1.0 + abs(offset[0]), offset[1]
|
|
32
|
+
elif loc == "lower left":
|
|
33
|
+
x, y = offset[0], -abs(offset[1]) + 1.0
|
|
34
|
+
elif loc == "lower right":
|
|
35
|
+
x, y = 1.0 + abs(offset[0]), -abs(offset[1]) + 1.0
|
|
36
|
+
else:
|
|
37
|
+
x, y = offset
|
|
38
|
+
return x, y
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"get_panel_label_fontsize",
|
|
43
|
+
"calculate_panel_position",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# EOF
|
figrecipe/_api/_save.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Save function helpers for the public API."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
|
|
8
|
+
# Image extensions supported for saving
|
|
9
|
+
IMAGE_EXTENSIONS = {
|
|
10
|
+
".png",
|
|
11
|
+
".pdf",
|
|
12
|
+
".svg",
|
|
13
|
+
".jpg",
|
|
14
|
+
".jpeg",
|
|
15
|
+
".eps",
|
|
16
|
+
".tiff",
|
|
17
|
+
".tif",
|
|
18
|
+
}
|
|
19
|
+
YAML_EXTENSIONS = {".yaml", ".yml"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_save_paths(
|
|
23
|
+
path: Path,
|
|
24
|
+
image_format: Optional[str] = None,
|
|
25
|
+
) -> Tuple[Path, Path, str]:
|
|
26
|
+
"""Resolve image and YAML paths from the provided path.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
path : Path
|
|
31
|
+
User-provided output path.
|
|
32
|
+
image_format : str, optional
|
|
33
|
+
Explicit image format when path is YAML.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
tuple
|
|
38
|
+
(image_path, yaml_path, img_format)
|
|
39
|
+
"""
|
|
40
|
+
suffix_lower = path.suffix.lower()
|
|
41
|
+
|
|
42
|
+
if suffix_lower in IMAGE_EXTENSIONS:
|
|
43
|
+
# User provided image path
|
|
44
|
+
image_path = path
|
|
45
|
+
yaml_path = path.with_suffix(".yaml")
|
|
46
|
+
img_format = suffix_lower[1:] # Remove leading dot
|
|
47
|
+
elif suffix_lower in YAML_EXTENSIONS:
|
|
48
|
+
# User provided YAML path
|
|
49
|
+
yaml_path = path
|
|
50
|
+
img_format = _get_default_image_format(image_format)
|
|
51
|
+
image_path = path.with_suffix(f".{img_format}")
|
|
52
|
+
else:
|
|
53
|
+
# Unknown extension - treat as base name, add both extensions
|
|
54
|
+
yaml_path = path.with_suffix(".yaml")
|
|
55
|
+
img_format = _get_default_image_format(image_format)
|
|
56
|
+
image_path = path.with_suffix(f".{img_format}")
|
|
57
|
+
|
|
58
|
+
return image_path, yaml_path, img_format
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_default_image_format(explicit_format: Optional[str] = None) -> str:
|
|
62
|
+
"""Get default image format from style or fallback to png."""
|
|
63
|
+
if explicit_format is not None:
|
|
64
|
+
return explicit_format.lower().lstrip(".")
|
|
65
|
+
|
|
66
|
+
# Check global style for preferred format
|
|
67
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
68
|
+
|
|
69
|
+
if _STYLE_CACHE is not None:
|
|
70
|
+
try:
|
|
71
|
+
return _STYLE_CACHE.output.format.lower()
|
|
72
|
+
except (KeyError, AttributeError):
|
|
73
|
+
pass
|
|
74
|
+
return "png"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_save_dpi(explicit_dpi: Optional[int] = None) -> int:
|
|
78
|
+
"""Get DPI for saving, using style default if not specified."""
|
|
79
|
+
if explicit_dpi is not None:
|
|
80
|
+
return explicit_dpi
|
|
81
|
+
|
|
82
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
83
|
+
|
|
84
|
+
if _STYLE_CACHE is not None:
|
|
85
|
+
try:
|
|
86
|
+
return _STYLE_CACHE.output.dpi
|
|
87
|
+
except (KeyError, AttributeError):
|
|
88
|
+
pass
|
|
89
|
+
return 300
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_save_transparency() -> bool:
|
|
93
|
+
"""Get transparency setting from style."""
|
|
94
|
+
from ..styles._style_loader import _STYLE_CACHE
|
|
95
|
+
|
|
96
|
+
if _STYLE_CACHE is not None:
|
|
97
|
+
try:
|
|
98
|
+
return _STYLE_CACHE.output.transparent
|
|
99
|
+
except (KeyError, AttributeError):
|
|
100
|
+
pass
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def save_figure(
|
|
105
|
+
fig,
|
|
106
|
+
path,
|
|
107
|
+
include_data: bool = True,
|
|
108
|
+
data_format: str = "csv",
|
|
109
|
+
validate: bool = True,
|
|
110
|
+
validate_mse_threshold: float = 100.0,
|
|
111
|
+
validate_error_level: str = "error",
|
|
112
|
+
verbose: bool = True,
|
|
113
|
+
dpi: Optional[int] = None,
|
|
114
|
+
image_format: Optional[str] = None,
|
|
115
|
+
):
|
|
116
|
+
"""Core save implementation."""
|
|
117
|
+
from .._wrappers import RecordingFigure
|
|
118
|
+
|
|
119
|
+
path = Path(path)
|
|
120
|
+
|
|
121
|
+
if not isinstance(fig, RecordingFigure):
|
|
122
|
+
raise TypeError(
|
|
123
|
+
"Expected RecordingFigure. Use fr.subplots() to create "
|
|
124
|
+
"a recording-enabled figure."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Resolve paths
|
|
128
|
+
image_path, yaml_path, _ = resolve_save_paths(path, image_format)
|
|
129
|
+
|
|
130
|
+
# Get DPI and transparency from style if not specified
|
|
131
|
+
dpi = get_save_dpi(dpi)
|
|
132
|
+
transparent = get_save_transparency()
|
|
133
|
+
|
|
134
|
+
# Finalize tick configuration and special plot types for all axes
|
|
135
|
+
from ..styles._style_applier import finalize_special_plots, finalize_ticks
|
|
136
|
+
|
|
137
|
+
# Get style for special plot finalization
|
|
138
|
+
style_dict = {}
|
|
139
|
+
if hasattr(fig, "style") and fig.style:
|
|
140
|
+
from ..styles import get_style
|
|
141
|
+
|
|
142
|
+
style_dict = get_style(fig.style)
|
|
143
|
+
|
|
144
|
+
for ax in fig.fig.get_axes():
|
|
145
|
+
finalize_ticks(ax)
|
|
146
|
+
finalize_special_plots(ax, style_dict)
|
|
147
|
+
|
|
148
|
+
# Save the image
|
|
149
|
+
fig.fig.savefig(image_path, dpi=dpi, bbox_inches="tight", transparent=transparent)
|
|
150
|
+
|
|
151
|
+
# Save the recipe
|
|
152
|
+
saved_yaml = fig.save_recipe(
|
|
153
|
+
yaml_path, include_data=include_data, data_format=data_format
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Validate if requested
|
|
157
|
+
if validate:
|
|
158
|
+
from .._validator import validate_on_save
|
|
159
|
+
|
|
160
|
+
result = validate_on_save(fig, saved_yaml, mse_threshold=validate_mse_threshold)
|
|
161
|
+
status = "PASSED" if result.valid else "FAILED"
|
|
162
|
+
if verbose:
|
|
163
|
+
print(
|
|
164
|
+
f"Saved: {image_path} + {yaml_path} (Reproducible Validation: {status})"
|
|
165
|
+
)
|
|
166
|
+
if not result.valid:
|
|
167
|
+
msg = f"Reproducibility validation failed (MSE={result.mse:.1f}): {result.message}"
|
|
168
|
+
if validate_error_level == "error":
|
|
169
|
+
raise ValueError(msg)
|
|
170
|
+
elif validate_error_level == "warning":
|
|
171
|
+
import warnings
|
|
172
|
+
|
|
173
|
+
warnings.warn(msg, UserWarning)
|
|
174
|
+
# "debug" level: silent, just return the result
|
|
175
|
+
return image_path, yaml_path, result
|
|
176
|
+
|
|
177
|
+
if verbose:
|
|
178
|
+
print(f"Saved: {image_path} + {yaml_path}")
|
|
179
|
+
return image_path, yaml_path, None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
__all__ = [
|
|
183
|
+
"IMAGE_EXTENSIONS",
|
|
184
|
+
"YAML_EXTENSIONS",
|
|
185
|
+
"resolve_save_paths",
|
|
186
|
+
"get_save_dpi",
|
|
187
|
+
"get_save_transparency",
|
|
188
|
+
"save_figure",
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
# EOF
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Seaborn proxy for figrecipe.
|
|
4
|
+
|
|
5
|
+
Provides lazy seaborn integration via ps.sns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"sns",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# Lazy import for seaborn to avoid hard dependency
|
|
13
|
+
_sns_recorder = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_sns():
|
|
17
|
+
"""Get the seaborn recorder (lazy initialization)."""
|
|
18
|
+
global _sns_recorder
|
|
19
|
+
if _sns_recorder is None:
|
|
20
|
+
from .._seaborn import get_seaborn_recorder
|
|
21
|
+
|
|
22
|
+
_sns_recorder = get_seaborn_recorder()
|
|
23
|
+
return _sns_recorder
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _SeabornProxy:
|
|
27
|
+
"""Proxy object for seaborn access via ps.sns."""
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, name: str):
|
|
30
|
+
return getattr(_get_sns(), name)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Create seaborn proxy
|
|
34
|
+
sns = _SeabornProxy()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Style management API for figrecipe.
|
|
4
|
+
|
|
5
|
+
Provides style loading, unloading, and application functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"load_style",
|
|
10
|
+
"unload_style",
|
|
11
|
+
"list_presets",
|
|
12
|
+
"apply_style",
|
|
13
|
+
"STYLE",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_style(style="SCITEX", dark=False):
|
|
18
|
+
"""Load style configuration and apply it globally.
|
|
19
|
+
|
|
20
|
+
After calling this function, subsequent `subplots()` calls will
|
|
21
|
+
automatically use the loaded style (fonts, colors, theme, etc.).
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
style : str, Path, bool, or None
|
|
26
|
+
One of:
|
|
27
|
+
- "SCITEX" / "FIGRECIPE": Scientific publication style (default)
|
|
28
|
+
- "MATPLOTLIB": Vanilla matplotlib defaults
|
|
29
|
+
- Path to custom YAML file: "/path/to/my_style.yaml"
|
|
30
|
+
- None or False: Unload style (reset to matplotlib defaults)
|
|
31
|
+
dark : bool, optional
|
|
32
|
+
If True, apply dark theme transformation (default: False).
|
|
33
|
+
Equivalent to appending "_DARK" to preset name.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
DotDict or None
|
|
38
|
+
Style configuration with dot-notation access.
|
|
39
|
+
Returns None if style is unloaded.
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> import figrecipe as fr
|
|
44
|
+
|
|
45
|
+
>>> # Load scientific style (default)
|
|
46
|
+
>>> fr.load_style()
|
|
47
|
+
>>> fr.load_style("SCITEX") # explicit
|
|
48
|
+
|
|
49
|
+
>>> # Load dark theme
|
|
50
|
+
>>> fr.load_style("SCITEX_DARK")
|
|
51
|
+
>>> fr.load_style("SCITEX", dark=True) # equivalent
|
|
52
|
+
|
|
53
|
+
>>> # Reset to vanilla matplotlib
|
|
54
|
+
>>> fr.load_style(None) # unload
|
|
55
|
+
>>> fr.load_style(False) # unload
|
|
56
|
+
>>> fr.load_style("MATPLOTLIB") # explicit vanilla
|
|
57
|
+
|
|
58
|
+
>>> # Access style values
|
|
59
|
+
>>> style = fr.load_style("SCITEX")
|
|
60
|
+
>>> style.axes.width_mm
|
|
61
|
+
40
|
|
62
|
+
"""
|
|
63
|
+
from ..styles import load_style as _load_style
|
|
64
|
+
|
|
65
|
+
return _load_style(style, dark=dark)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def unload_style():
|
|
69
|
+
"""Unload the current style and reset to matplotlib defaults.
|
|
70
|
+
|
|
71
|
+
After calling this, subsequent `subplots()` calls will use vanilla
|
|
72
|
+
matplotlib behavior without FigRecipe styling.
|
|
73
|
+
|
|
74
|
+
Examples
|
|
75
|
+
--------
|
|
76
|
+
>>> import figrecipe as fr
|
|
77
|
+
>>> fr.load_style("SCITEX") # Apply scientific style
|
|
78
|
+
>>> fig, ax = fr.subplots() # Styled
|
|
79
|
+
>>> fr.unload_style() # Reset to matplotlib defaults
|
|
80
|
+
>>> fig, ax = fr.subplots() # Vanilla matplotlib
|
|
81
|
+
"""
|
|
82
|
+
from ..styles import unload_style as _unload_style
|
|
83
|
+
|
|
84
|
+
_unload_style()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def list_presets():
|
|
88
|
+
"""List available style presets.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
list of str
|
|
93
|
+
Names of available presets.
|
|
94
|
+
|
|
95
|
+
Examples
|
|
96
|
+
--------
|
|
97
|
+
>>> import figrecipe as ps
|
|
98
|
+
>>> ps.list_presets()
|
|
99
|
+
['MINIMAL', 'PRESENTATION', 'SCIENTIFIC']
|
|
100
|
+
"""
|
|
101
|
+
from ..styles import list_presets as _list_presets
|
|
102
|
+
|
|
103
|
+
return _list_presets()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def apply_style(ax, style=None):
|
|
107
|
+
"""Apply mm-based styling to an axes.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
ax : matplotlib.axes.Axes
|
|
112
|
+
Target axes to apply styling to.
|
|
113
|
+
style : dict or DotDict, optional
|
|
114
|
+
Style configuration. If None, uses default FIGRECIPE_STYLE.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
float
|
|
119
|
+
Trace line width in points.
|
|
120
|
+
|
|
121
|
+
Examples
|
|
122
|
+
--------
|
|
123
|
+
>>> import figrecipe as ps
|
|
124
|
+
>>> import matplotlib.pyplot as plt
|
|
125
|
+
>>> fig, ax = plt.subplots()
|
|
126
|
+
>>> trace_lw = ps.apply_style(ax)
|
|
127
|
+
>>> ax.plot(x, y, lw=trace_lw)
|
|
128
|
+
"""
|
|
129
|
+
from ..styles import apply_style_mm, get_style, to_subplots_kwargs
|
|
130
|
+
|
|
131
|
+
if style is None:
|
|
132
|
+
style = to_subplots_kwargs(get_style())
|
|
133
|
+
elif hasattr(style, "to_subplots_kwargs"):
|
|
134
|
+
style = style.to_subplots_kwargs()
|
|
135
|
+
return apply_style_mm(ax, style)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _StyleProxy:
|
|
139
|
+
"""Proxy object for lazy style loading."""
|
|
140
|
+
|
|
141
|
+
def __getattr__(self, name):
|
|
142
|
+
from ..styles import STYLE
|
|
143
|
+
|
|
144
|
+
return getattr(STYLE, name)
|
|
145
|
+
|
|
146
|
+
def to_subplots_kwargs(self):
|
|
147
|
+
from ..styles import to_subplots_kwargs
|
|
148
|
+
|
|
149
|
+
return to_subplots_kwargs()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Create style proxy
|
|
153
|
+
STYLE = _StyleProxy()
|