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,180 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Violin plot helper functions for RecordingAxes."""
4
+
5
+ from typing import Any, Dict, List
6
+
7
+ import numpy as np
8
+
9
+
10
+ def add_violin_inner_box(
11
+ ax,
12
+ dataset: List,
13
+ positions: List,
14
+ style: Dict[str, Any],
15
+ ) -> None:
16
+ """Add box plot inside violin.
17
+
18
+ Parameters
19
+ ----------
20
+ ax : matplotlib.axes.Axes
21
+ The axes to draw on.
22
+ dataset : array-like
23
+ Data arrays for each violin.
24
+ positions : array-like
25
+ X positions of violins.
26
+ style : dict
27
+ Violin style configuration.
28
+ """
29
+ from ..styles._style_applier import mm_to_pt
30
+
31
+ whisker_lw = mm_to_pt(style.get("whisker_mm", 0.2))
32
+ median_size = mm_to_pt(style.get("median_mm", 0.8))
33
+
34
+ for data, pos in zip(dataset, positions):
35
+ data = np.asarray(data)
36
+ q1, median, q3 = np.percentile(data, [25, 50, 75])
37
+ iqr = q3 - q1
38
+ whisker_low = max(data.min(), q1 - 1.5 * iqr)
39
+ whisker_high = min(data.max(), q3 + 1.5 * iqr)
40
+
41
+ # Draw box (Q1 to Q3)
42
+ ax.vlines(pos, q1, q3, colors="black", linewidths=whisker_lw, zorder=3)
43
+ # Draw whiskers
44
+ ax.vlines(
45
+ pos,
46
+ whisker_low,
47
+ q1,
48
+ colors="black",
49
+ linewidths=whisker_lw * 0.5,
50
+ zorder=3,
51
+ )
52
+ ax.vlines(
53
+ pos,
54
+ q3,
55
+ whisker_high,
56
+ colors="black",
57
+ linewidths=whisker_lw * 0.5,
58
+ zorder=3,
59
+ )
60
+ # Draw median as a white dot with black edge
61
+ ax.scatter(
62
+ [pos],
63
+ [median],
64
+ s=median_size**2,
65
+ c="white",
66
+ edgecolors="black",
67
+ linewidths=whisker_lw,
68
+ zorder=4,
69
+ )
70
+
71
+
72
+ def add_violin_inner_swarm(
73
+ ax,
74
+ dataset: List,
75
+ positions: List,
76
+ style: Dict[str, Any],
77
+ ) -> None:
78
+ """Add swarm points inside violin.
79
+
80
+ Parameters
81
+ ----------
82
+ ax : matplotlib.axes.Axes
83
+ The axes to draw on.
84
+ dataset : array-like
85
+ Data arrays for each violin.
86
+ positions : array-like
87
+ X positions of violins.
88
+ style : dict
89
+ Violin style configuration.
90
+ """
91
+ from ..styles._style_applier import mm_to_pt
92
+
93
+ point_size = mm_to_pt(style.get("median_mm", 0.8))
94
+
95
+ for data, pos in zip(dataset, positions):
96
+ data = np.asarray(data)
97
+ n = len(data)
98
+
99
+ # Simple swarm: jitter x positions
100
+ jitter = np.random.default_rng(42).uniform(-0.15, 0.15, n)
101
+ x_positions = pos + jitter
102
+
103
+ ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.5, zorder=3)
104
+
105
+
106
+ def add_violin_inner_stick(
107
+ ax,
108
+ dataset: List,
109
+ positions: List,
110
+ style: Dict[str, Any],
111
+ ) -> None:
112
+ """Add stick (line) markers inside violin for each data point.
113
+
114
+ Parameters
115
+ ----------
116
+ ax : matplotlib.axes.Axes
117
+ The axes to draw on.
118
+ dataset : array-like
119
+ Data arrays for each violin.
120
+ positions : array-like
121
+ X positions of violins.
122
+ style : dict
123
+ Violin style configuration.
124
+ """
125
+ from ..styles._style_applier import mm_to_pt
126
+
127
+ lw = mm_to_pt(style.get("whisker_mm", 0.2))
128
+
129
+ for data, pos in zip(dataset, positions):
130
+ data = np.asarray(data)
131
+ # Draw short horizontal lines at each data point
132
+ for val in data:
133
+ ax.hlines(
134
+ val,
135
+ pos - 0.05,
136
+ pos + 0.05,
137
+ colors="black",
138
+ linewidths=lw * 0.5,
139
+ alpha=0.3,
140
+ zorder=3,
141
+ )
142
+
143
+
144
+ def add_violin_inner_point(
145
+ ax,
146
+ dataset: List,
147
+ positions: List,
148
+ style: Dict[str, Any],
149
+ ) -> None:
150
+ """Add point markers inside violin for each data point.
151
+
152
+ Parameters
153
+ ----------
154
+ ax : matplotlib.axes.Axes
155
+ The axes to draw on.
156
+ dataset : array-like
157
+ Data arrays for each violin.
158
+ positions : array-like
159
+ X positions of violins.
160
+ style : dict
161
+ Violin style configuration.
162
+ """
163
+ from ..styles._style_applier import mm_to_pt
164
+
165
+ point_size = mm_to_pt(style.get("median_mm", 0.8)) * 0.5
166
+
167
+ for data, pos in zip(dataset, positions):
168
+ data = np.asarray(data)
169
+ x_positions = np.full_like(data, pos)
170
+ ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.3, zorder=3)
171
+
172
+
173
+ __all__ = [
174
+ "add_violin_inner_box",
175
+ "add_violin_inner_swarm",
176
+ "add_violin_inner_stick",
177
+ "add_violin_inner_point",
178
+ ]
179
+
180
+ # EOF
figrecipe/plt.py CHANGED
@@ -9,4 +9,3 @@ Usage:
9
9
  """
10
10
 
11
11
  from .pyplot import * # noqa: F401, F403
12
- from .pyplot import __all__
figrecipe/pyplot.py CHANGED
@@ -34,9 +34,10 @@ Examples
34
34
  import matplotlib.pyplot as _plt
35
35
  from matplotlib.pyplot import * # noqa: F401, F403
36
36
 
37
+ from . import save as _ps_save
38
+
37
39
  # Import figrecipe functionality
38
40
  from . import subplots as _ps_subplots
39
- from . import save as _ps_save
40
41
  from ._wrappers import RecordingFigure
41
42
 
42
43
  # Override subplots with recording-enabled version
@@ -18,24 +18,23 @@ Usage:
18
18
  fig, ax = ps.subplots(**style.to_subplots_kwargs())
19
19
  """
20
20
 
21
+ from ._dotdict import DotDict
22
+ from ._finalize import finalize_special_plots, finalize_ticks
23
+ from ._fonts import check_font, list_available_fonts
24
+ from ._style_applier import apply_style_mm
21
25
  from ._style_loader import (
22
- load_style,
23
- unload_style,
26
+ STYLE,
24
27
  get_style,
25
- reload_style,
26
28
  list_presets,
27
- STYLE,
29
+ load_style,
30
+ reload_style,
28
31
  to_subplots_kwargs,
32
+ unload_style,
29
33
  )
30
-
31
- from ._style_applier import (
32
- apply_style_mm,
33
- apply_theme_colors,
34
- check_font,
35
- list_available_fonts,
36
- )
34
+ from ._themes import apply_theme_colors
37
35
 
38
36
  __all__ = [
37
+ "DotDict",
39
38
  "load_style",
40
39
  "unload_style",
41
40
  "get_style",
@@ -47,4 +46,6 @@ __all__ = [
47
46
  "apply_theme_colors",
48
47
  "check_font",
49
48
  "list_available_fonts",
49
+ "finalize_ticks",
50
+ "finalize_special_plots",
50
51
  ]
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """DotDict class for nested dictionary access with dot notation."""
4
+
5
+ from typing import Any
6
+
7
+
8
+ class DotDict(dict):
9
+ """Dictionary with dot-notation access to nested keys.
10
+
11
+ Examples
12
+ --------
13
+ >>> d = DotDict({"axes": {"width_mm": 40}})
14
+ >>> d.axes.width_mm
15
+ 40
16
+ """
17
+
18
+ def __getattr__(self, key: str) -> Any:
19
+ # Handle special methods first
20
+ if key == "to_subplots_kwargs":
21
+ from ._style_loader import to_subplots_kwargs
22
+
23
+ return lambda: to_subplots_kwargs(self)
24
+ try:
25
+ value = self[key]
26
+ if isinstance(value, dict) and not isinstance(value, DotDict):
27
+ value = DotDict(value)
28
+ self[key] = value
29
+ return value
30
+ except KeyError:
31
+ raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
32
+
33
+ def __setattr__(self, key: str, value: Any) -> None:
34
+ self[key] = value
35
+
36
+ def __delattr__(self, key: str) -> None:
37
+ try:
38
+ del self[key]
39
+ except KeyError:
40
+ raise AttributeError(key)
41
+
42
+ def __repr__(self) -> str:
43
+ return f"DotDict({super().__repr__()})"
44
+
45
+ def get(self, key: str, default: Any = None) -> Any:
46
+ """Get value with default, supporting nested keys with dots."""
47
+ if "." in key:
48
+ parts = key.split(".")
49
+ value = self
50
+ for part in parts:
51
+ if isinstance(value, dict) and part in value:
52
+ value = value[part]
53
+ else:
54
+ return default
55
+ return value
56
+ return super().get(key, default)
57
+
58
+ def flatten(self, prefix: str = "") -> dict:
59
+ """Flatten nested dict to single level with underscore-joined keys."""
60
+ result = {}
61
+ for k, v in self.items():
62
+ new_key = f"{prefix}_{k}" if prefix else k
63
+ if isinstance(v, dict):
64
+ result.update(DotDict(v).flatten(new_key))
65
+ else:
66
+ result[new_key] = v
67
+ return result
68
+
69
+
70
+ __all__ = ["DotDict"]
71
+
72
+ # EOF
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Finalization utilities for figrecipe styles."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ from matplotlib.axes import Axes
8
+
9
+ from ._fonts import check_font
10
+
11
+
12
+ def finalize_ticks(ax: Axes) -> None:
13
+ """
14
+ Apply deferred tick configuration after all plotting is done.
15
+
16
+ This function applies the n_ticks setting stored by apply_style_mm(),
17
+ but only to numeric axes (not categorical). Skips pie charts and other
18
+ plot types that should have hidden axes.
19
+
20
+ Parameters
21
+ ----------
22
+ ax : matplotlib.axes.Axes
23
+ The axes to finalize.
24
+ """
25
+ from matplotlib.patches import Wedge
26
+ from matplotlib.ticker import MaxNLocator
27
+
28
+ # Skip pie charts - they should have no ticks
29
+ has_pie = any(isinstance(p, Wedge) for p in ax.patches)
30
+ if has_pie:
31
+ return
32
+
33
+ # Get tick count preferences (new format: min/max)
34
+ n_ticks_min = getattr(ax, "_figrecipe_n_ticks_min", None)
35
+ n_ticks_max = getattr(ax, "_figrecipe_n_ticks_max", None)
36
+
37
+ if n_ticks_min is None and n_ticks_max is None:
38
+ return
39
+
40
+ # Default values - minimum 3 ticks required
41
+ n_ticks_min = max(3, n_ticks_min or 3)
42
+ n_ticks_max = max(n_ticks_min, n_ticks_max or 4)
43
+
44
+ nbins = n_ticks_max
45
+
46
+ def _is_numeric_label(lbl: str) -> bool:
47
+ """Check if a tick label represents a numeric value."""
48
+ if not lbl:
49
+ return True
50
+ stripped = lbl.replace(".", "").replace("-", "").replace("+", "")
51
+ stripped = stripped.replace("−", "") # Unicode minus sign
52
+ stripped = stripped.replace("e", "").replace("E", "")
53
+ return stripped.isdigit() or stripped == ""
54
+
55
+ # Check if x-axis is categorical
56
+ x_labels = [t.get_text() for t in ax.get_xticklabels()]
57
+ x_is_categorical = any(not _is_numeric_label(lbl) for lbl in x_labels)
58
+ if not x_is_categorical:
59
+ ax.xaxis.set_major_locator(MaxNLocator(nbins=nbins, min_n_ticks=n_ticks_min))
60
+
61
+ # Check if y-axis is categorical
62
+ y_labels = [t.get_text() for t in ax.get_yticklabels()]
63
+ y_is_categorical = any(not _is_numeric_label(lbl) for lbl in y_labels)
64
+ if not y_is_categorical:
65
+ ax.yaxis.set_major_locator(MaxNLocator(nbins=nbins, min_n_ticks=n_ticks_min))
66
+
67
+
68
+ def finalize_special_plots(ax: Axes, style: Dict[str, Any] = None) -> None:
69
+ """
70
+ Finalize axes visibility for special plot types (pie, imshow, etc.).
71
+
72
+ This should be called after all plotting is done, before saving.
73
+ It handles plot types that need axes/ticks hidden.
74
+
75
+ Parameters
76
+ ----------
77
+ ax : matplotlib.axes.Axes
78
+ The axes to finalize.
79
+ style : dict, optional
80
+ Style dictionary. If None, uses defaults.
81
+ """
82
+ from matplotlib.image import AxesImage
83
+ from matplotlib.patches import Wedge
84
+
85
+ if style is None:
86
+ style = {}
87
+
88
+ # Check for pie chart
89
+ has_pie = any(isinstance(p, Wedge) for p in ax.patches)
90
+ if has_pie:
91
+ show_axes = style.get("pie_show_axes", False)
92
+ text_pt = style.get("pie_text_pt", 6)
93
+ font_family = check_font(style.get("font_family", "Arial"))
94
+
95
+ for text in ax.texts:
96
+ transform = text.get_transform()
97
+ if transform == ax.transAxes:
98
+ x, y = text.get_position()
99
+ if y > 1.0 or y < 0.0:
100
+ continue
101
+ text.set_fontsize(text_pt)
102
+ text.set_fontfamily(font_family)
103
+
104
+ if not show_axes:
105
+ ax.set_xticks([])
106
+ ax.set_yticks([])
107
+ ax.set_xticklabels([])
108
+ ax.set_yticklabels([])
109
+ ax.set_xlabel("")
110
+ ax.set_ylabel("")
111
+ for spine in ax.spines.values():
112
+ spine.set_visible(False)
113
+
114
+ # Check for imshow/matshow (has AxesImage)
115
+ has_image = any(isinstance(c, AxesImage) for c in ax.get_children())
116
+ if has_image:
117
+ xlabel = ax.get_xlabel()
118
+ ylabel = ax.get_ylabel()
119
+ is_specgram = xlabel or ylabel
120
+
121
+ if not is_specgram:
122
+ show_axes = style.get("imshow_show_axes", False)
123
+ if not show_axes:
124
+ ax.set_xticks([])
125
+ ax.set_yticks([])
126
+ ax.set_xticklabels([])
127
+ ax.set_yticklabels([])
128
+ for spine in ax.spines.values():
129
+ spine.set_visible(False)
130
+
131
+
132
+ __all__ = ["finalize_ticks", "finalize_special_plots"]
133
+
134
+ # EOF
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Font utilities for figrecipe.
4
+
5
+ Provides font availability checking and listing for publication-quality figures.
6
+ """
7
+
8
+ __all__ = [
9
+ "list_available_fonts",
10
+ "check_font",
11
+ ]
12
+
13
+ import warnings
14
+ from typing import List
15
+
16
+
17
+ def list_available_fonts() -> List[str]:
18
+ """List all available font families.
19
+
20
+ Returns
21
+ -------
22
+ list of str
23
+ Sorted list of available font family names.
24
+
25
+ Examples
26
+ --------
27
+ >>> fonts = ps.list_available_fonts()
28
+ >>> print(fonts[:5])
29
+ ['Arial', 'Courier New', 'DejaVu Sans', ...]
30
+ """
31
+ import matplotlib.font_manager as fm
32
+
33
+ fonts = set()
34
+ for font in fm.fontManager.ttflist:
35
+ fonts.add(font.name)
36
+ return sorted(fonts)
37
+
38
+
39
+ def check_font(font_family: str, fallback: str = "DejaVu Sans") -> str:
40
+ """Check if font is available, with fallback and helpful error message.
41
+
42
+ Parameters
43
+ ----------
44
+ font_family : str
45
+ Requested font family name.
46
+ fallback : str
47
+ Fallback font if requested font is not available.
48
+
49
+ Returns
50
+ -------
51
+ str
52
+ The font to use (original if available, fallback otherwise).
53
+
54
+ Examples
55
+ --------
56
+ >>> font = check_font("Arial") # Returns "Arial" if available
57
+ >>> font = check_font("NonExistentFont") # Returns fallback with warning
58
+ """
59
+
60
+ available = list_available_fonts()
61
+
62
+ if font_family in available:
63
+ return font_family
64
+
65
+ # Font not found - show helpful message
66
+ similar = [f for f in available if font_family.lower() in f.lower()]
67
+
68
+ msg = f"Font '{font_family}' not found.\n"
69
+ if similar:
70
+ msg += f" Similar fonts available: {similar[:5]}\n"
71
+ msg += f" Using fallback: '{fallback}'\n"
72
+ msg += " To see all available fonts: ps.list_available_fonts()\n"
73
+ msg += " To install Arial on Linux: sudo apt install ttf-mscorefonts-installer"
74
+
75
+ warnings.warn(msg, UserWarning)
76
+
77
+ return fallback if fallback in available else "DejaVu Sans"