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.
Files changed (189) hide show
  1. figrecipe/__init__.py +220 -819
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {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()
@@ -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
@@ -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()