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.
Files changed (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -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"
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Convert style DotDict to subplots kwargs."""
4
+
5
+ from typing import TYPE_CHECKING, Any, Dict, Optional
6
+
7
+ if TYPE_CHECKING:
8
+ from ._dotdict import DotDict
9
+
10
+
11
+ def to_subplots_kwargs(style: Optional["DotDict"] = None) -> Dict[str, Any]:
12
+ """Convert style DotDict to kwargs for ps.subplots().
13
+
14
+ Uses YAML-compatible flattened key names as the single source of truth.
15
+ For example, YAML `fonts.axis_label_pt` becomes `fonts_axis_label_pt`.
16
+
17
+ Parameters
18
+ ----------
19
+ style : DotDict, optional
20
+ Style configuration. If None, uses current loaded style.
21
+
22
+ Returns
23
+ -------
24
+ dict
25
+ Keyword arguments for ps.subplots() with YAML-compatible flattened keys.
26
+
27
+ Examples
28
+ --------
29
+ >>> style = load_style()
30
+ >>> kwargs = to_subplots_kwargs(style)
31
+ >>> fig, ax = ps.subplots(**kwargs)
32
+ """
33
+ if style is None:
34
+ from ._style_loader import get_style
35
+
36
+ style = get_style()
37
+
38
+ # YAML-compatible flattened keys (single source of truth)
39
+ result = {
40
+ # Axes (axes.* in YAML)
41
+ "axes_width_mm": style.axes.width_mm,
42
+ "axes_height_mm": style.axes.height_mm,
43
+ "axes_thickness_mm": style.axes.thickness_mm,
44
+ # Margins (margins.* in YAML)
45
+ "margins_left_mm": style.margins.left_mm,
46
+ "margins_right_mm": style.margins.right_mm,
47
+ "margins_bottom_mm": style.margins.bottom_mm,
48
+ "margins_top_mm": style.margins.top_mm,
49
+ # Spacing (spacing.* in YAML)
50
+ "spacing_horizontal_mm": style.spacing.horizontal_mm,
51
+ "spacing_vertical_mm": style.spacing.vertical_mm,
52
+ # Ticks (ticks.* in YAML)
53
+ "ticks_length_mm": style.ticks.length_mm,
54
+ "ticks_thickness_mm": style.ticks.thickness_mm,
55
+ "ticks_direction": style.ticks.get("direction", "out"),
56
+ "ticks_n_ticks_min": style.ticks.get("n_ticks_min", 3),
57
+ "ticks_n_ticks_max": style.ticks.get("n_ticks_max", 4),
58
+ # Lines (lines.* in YAML)
59
+ "lines_trace_mm": style.lines.trace_mm,
60
+ "lines_errorbar_mm": style.lines.get("errorbar_mm", 0.2),
61
+ "lines_errorbar_cap_mm": style.lines.get("errorbar_cap_mm", 0.8),
62
+ # Markers (markers.* in YAML)
63
+ "markers_size_mm": style.markers.size_mm,
64
+ "markers_scatter_mm": style.markers.get("scatter_mm", style.markers.size_mm),
65
+ "markers_flier_mm": style.markers.get("flier_mm", style.markers.size_mm),
66
+ "markers_edge_width_mm": style.markers.get("edge_width_mm"),
67
+ # Boxplot (boxplot.* in YAML)
68
+ "boxplot_line_mm": style.get("boxplot", {}).get("line_mm", 0.2),
69
+ "boxplot_whisker_mm": style.get("boxplot", {}).get("whisker_mm", 0.2),
70
+ "boxplot_cap_mm": style.get("boxplot", {}).get("cap_mm", 0.2),
71
+ "boxplot_median_mm": style.get("boxplot", {}).get("median_mm", 0.2),
72
+ "boxplot_median_color": style.get("boxplot", {}).get("median_color", "black"),
73
+ "boxplot_flier_edge_mm": style.get("boxplot", {}).get("flier_edge_mm", 0.2),
74
+ # Violinplot (violinplot.* in YAML)
75
+ "violinplot_line_mm": style.get("violinplot", {}).get("line_mm", 0.2),
76
+ "violinplot_inner": style.get("violinplot", {}).get("inner", "box"),
77
+ "violinplot_box_width_mm": style.get("violinplot", {}).get("box_width_mm", 1.5),
78
+ "violinplot_whisker_mm": style.get("violinplot", {}).get("whisker_mm", 0.2),
79
+ "violinplot_median_mm": style.get("violinplot", {}).get("median_mm", 0.8),
80
+ # Barplot (barplot.* in YAML)
81
+ "barplot_edge_mm": style.get("barplot", {}).get("edge_mm", 0.2),
82
+ # Histogram (histogram.* in YAML)
83
+ "histogram_edge_mm": style.get("histogram", {}).get("edge_mm", 0.2),
84
+ # Pie chart (pie.* in YAML)
85
+ "pie_text_pt": style.get("pie", {}).get("text_pt", 6),
86
+ "pie_show_axes": style.get("pie", {}).get("show_axes", False),
87
+ # Imshow (imshow.* in YAML)
88
+ "imshow_show_axes": style.get("imshow", {}).get("show_axes", False),
89
+ "imshow_show_labels": style.get("imshow", {}).get("show_labels", False),
90
+ # Fonts (fonts.* in YAML)
91
+ "fonts_family": style.fonts.family,
92
+ "fonts_axis_label_pt": style.fonts.axis_label_pt,
93
+ "fonts_tick_label_pt": style.fonts.tick_label_pt,
94
+ "fonts_title_pt": style.fonts.title_pt,
95
+ "fonts_suptitle_pt": style.fonts.suptitle_pt,
96
+ "fonts_legend_pt": style.fonts.legend_pt,
97
+ "fonts_annotation_pt": style.fonts.get("annotation_pt", 6),
98
+ # Padding (padding.* in YAML)
99
+ "padding_label_pt": style.padding.label_pt,
100
+ "padding_tick_pt": style.padding.tick_pt,
101
+ "padding_title_pt": style.padding.title_pt,
102
+ # Output (output.* in YAML)
103
+ "output_dpi": style.output.dpi,
104
+ "output_transparent": style.output.get("transparent", True),
105
+ "output_format": style.output.get("format", "pdf"),
106
+ # Theme (theme.* in YAML)
107
+ "theme_mode": style.theme.mode,
108
+ }
109
+
110
+ # Add theme colors from preset if available
111
+ theme_mode = style.theme.mode
112
+ if "theme" in style and theme_mode in style.theme:
113
+ result["theme_colors"] = dict(style.theme[theme_mode])
114
+
115
+ # Add color palette if available
116
+ if "colors" in style and "palette" in style.colors:
117
+ result["color_palette"] = list(style.colors.palette)
118
+
119
+ # Add behavior settings (behavior.* in YAML)
120
+ if "behavior" in style:
121
+ behavior = style.behavior
122
+ if hasattr(behavior, "hide_top_spine"):
123
+ result["behavior_hide_top_spine"] = behavior.hide_top_spine
124
+ if hasattr(behavior, "hide_right_spine"):
125
+ result["behavior_hide_right_spine"] = behavior.hide_right_spine
126
+ if hasattr(behavior, "grid"):
127
+ result["behavior_grid"] = behavior.grid
128
+ if hasattr(behavior, "auto_scale_axes"):
129
+ result["behavior_auto_scale_axes"] = behavior.auto_scale_axes
130
+ if hasattr(behavior, "constrained_layout"):
131
+ result["behavior_constrained_layout"] = behavior.constrained_layout
132
+
133
+ # Legacy key aliases for backwards compatibility
134
+ result.update(_get_legacy_aliases(result))
135
+
136
+ return result
137
+
138
+
139
+ def _get_legacy_aliases(result: Dict[str, Any]) -> Dict[str, Any]:
140
+ """Get legacy key aliases for backwards compatibility.
141
+
142
+ These allow existing code using old keys to still work.
143
+ """
144
+ return {
145
+ "margin_left_mm": result["margins_left_mm"],
146
+ "margin_right_mm": result["margins_right_mm"],
147
+ "margin_bottom_mm": result["margins_bottom_mm"],
148
+ "margin_top_mm": result["margins_top_mm"],
149
+ "space_w_mm": result["spacing_horizontal_mm"],
150
+ "space_h_mm": result["spacing_vertical_mm"],
151
+ "tick_length_mm": result["ticks_length_mm"],
152
+ "tick_thickness_mm": result["ticks_thickness_mm"],
153
+ "n_ticks_min": result["ticks_n_ticks_min"],
154
+ "n_ticks_max": result["ticks_n_ticks_max"],
155
+ "trace_thickness_mm": result["lines_trace_mm"],
156
+ "marker_size_mm": result["markers_size_mm"],
157
+ "font_family": result["fonts_family"],
158
+ "axis_font_size_pt": result["fonts_axis_label_pt"],
159
+ "tick_font_size_pt": result["fonts_tick_label_pt"],
160
+ "title_font_size_pt": result["fonts_title_pt"],
161
+ "suptitle_font_size_pt": result["fonts_suptitle_pt"],
162
+ "legend_font_size_pt": result["fonts_legend_pt"],
163
+ "label_pad_pt": result["padding_label_pt"],
164
+ "tick_pad_pt": result["padding_tick_pt"],
165
+ "title_pad_pt": result["padding_title_pt"],
166
+ "dpi": result["output_dpi"],
167
+ "theme": result["theme_mode"],
168
+ "hide_top_spine": result.get("behavior_hide_top_spine", True),
169
+ "hide_right_spine": result.get("behavior_hide_right_spine", True),
170
+ "grid": result.get("behavior_grid", False),
171
+ "auto_scale_axes": result.get("behavior_auto_scale_axes", True),
172
+ "constrained_layout": result.get("behavior_constrained_layout", False),
173
+ }
174
+
175
+
176
+ __all__ = ["to_subplots_kwargs"]
177
+
178
+ # EOF
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Plot-specific style application for figrecipe."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ from matplotlib.axes import Axes
8
+
9
+ from .._utils._units import mm_to_pt
10
+ from ._fonts import check_font
11
+
12
+
13
+ def apply_boxplot_style(ax: Axes, style: Dict[str, Any]) -> None:
14
+ """Apply boxplot line width styling to existing boxplot elements.
15
+
16
+ Parameters
17
+ ----------
18
+ ax : matplotlib.axes.Axes
19
+ Target axes containing boxplot elements.
20
+ style : dict
21
+ Style dictionary with boxplot_* keys.
22
+ """
23
+ from matplotlib.lines import Line2D
24
+ from matplotlib.patches import PathPatch
25
+
26
+ box_lw = mm_to_pt(style.get("boxplot_line_mm", 0.2))
27
+ whisker_lw = mm_to_pt(style.get("boxplot_whisker_mm", 0.2))
28
+ cap_lw = mm_to_pt(style.get("boxplot_cap_mm", 0.2))
29
+ median_lw = mm_to_pt(style.get("boxplot_median_mm", 0.2))
30
+ median_color = style.get("boxplot_median_color", "black")
31
+ flier_edge_lw = mm_to_pt(style.get("boxplot_flier_edge_mm", 0.2))
32
+
33
+ for child in ax.get_children():
34
+ if isinstance(child, PathPatch):
35
+ if child.get_edgecolor() is not None:
36
+ child.set_linewidth(box_lw)
37
+
38
+ elif isinstance(child, Line2D):
39
+ xdata = child.get_xdata()
40
+ ydata = child.get_ydata()
41
+
42
+ marker = child.get_marker()
43
+ linestyle = child.get_linestyle()
44
+ if marker and marker != "None" and linestyle in ("None", "", " "):
45
+ child.set_markeredgewidth(flier_edge_lw)
46
+ elif len(xdata) == 2 and len(ydata) == 2:
47
+ if ydata[0] == ydata[1]:
48
+ if linestyle == "-":
49
+ child.set_linewidth(median_lw)
50
+ if median_color:
51
+ child.set_color(median_color)
52
+ else:
53
+ child.set_linewidth(cap_lw)
54
+ elif xdata[0] == xdata[1]:
55
+ child.set_linewidth(whisker_lw)
56
+
57
+
58
+ def apply_violinplot_style(ax: Axes, style: Dict[str, Any]) -> None:
59
+ """Apply violinplot line width styling to existing violinplot elements.
60
+
61
+ Parameters
62
+ ----------
63
+ ax : matplotlib.axes.Axes
64
+ Target axes containing violinplot elements.
65
+ style : dict
66
+ Style dictionary with violinplot_* keys.
67
+ """
68
+ from matplotlib.collections import LineCollection, PolyCollection
69
+
70
+ body_lw = mm_to_pt(style.get("violinplot_line_mm", 0.2))
71
+ whisker_lw = mm_to_pt(style.get("violinplot_whisker_mm", 0.2))
72
+
73
+ for child in ax.get_children():
74
+ if isinstance(child, PolyCollection):
75
+ child.set_linewidth(body_lw)
76
+ elif isinstance(child, LineCollection):
77
+ child.set_linewidth(whisker_lw)
78
+
79
+
80
+ def apply_barplot_style(ax: Axes, style: Dict[str, Any]) -> None:
81
+ """Apply barplot edge styling to existing bar elements.
82
+
83
+ Parameters
84
+ ----------
85
+ ax : matplotlib.axes.Axes
86
+ Target axes containing bar elements.
87
+ style : dict
88
+ Style dictionary with barplot_* keys.
89
+ """
90
+ from matplotlib.patches import Rectangle
91
+
92
+ edge_lw = mm_to_pt(style.get("barplot_edge_mm", 0.2))
93
+
94
+ for patch in ax.patches:
95
+ if isinstance(patch, Rectangle):
96
+ patch.set_linewidth(edge_lw)
97
+ patch.set_edgecolor("black")
98
+
99
+
100
+ def apply_histogram_style(ax: Axes, style: Dict[str, Any]) -> None:
101
+ """Apply histogram edge styling to existing histogram elements.
102
+
103
+ Parameters
104
+ ----------
105
+ ax : matplotlib.axes.Axes
106
+ Target axes containing histogram elements.
107
+ style : dict
108
+ Style dictionary with histogram_* keys.
109
+ """
110
+ from matplotlib.patches import Rectangle
111
+
112
+ edge_lw = mm_to_pt(style.get("histogram_edge_mm", 0.2))
113
+
114
+ for patch in ax.patches:
115
+ if isinstance(patch, Rectangle):
116
+ patch.set_linewidth(edge_lw)
117
+ patch.set_edgecolor("black")
118
+
119
+
120
+ def apply_pie_style(ax: Axes, style: Dict[str, Any]) -> None:
121
+ """Apply pie chart styling to existing pie elements.
122
+
123
+ Parameters
124
+ ----------
125
+ ax : matplotlib.axes.Axes
126
+ Target axes containing pie chart elements.
127
+ style : dict
128
+ Style dictionary with pie_* keys.
129
+ """
130
+ from matplotlib.patches import Wedge
131
+
132
+ has_pie = any(isinstance(p, Wedge) for p in ax.patches)
133
+ if not has_pie:
134
+ return
135
+
136
+ text_pt = style.get("pie_text_pt", 6)
137
+ show_axes = style.get("pie_show_axes", False)
138
+ font_family = check_font(style.get("font_family", "Arial"))
139
+
140
+ for text in ax.texts:
141
+ transform = text.get_transform()
142
+ if transform == ax.transAxes:
143
+ x, y = text.get_position()
144
+ if y > 1.0 or y < 0.0:
145
+ continue
146
+ text.set_fontsize(text_pt)
147
+ text.set_fontfamily(font_family)
148
+
149
+ if not show_axes:
150
+ ax.set_xticks([])
151
+ ax.set_yticks([])
152
+ ax.set_xticklabels([])
153
+ ax.set_yticklabels([])
154
+ for spine in ax.spines.values():
155
+ spine.set_visible(False)
156
+
157
+
158
+ def apply_matrix_style(ax: Axes, style: Dict[str, Any]) -> None:
159
+ """Apply imshow/matshow/spy styling (hide axes if configured).
160
+
161
+ Parameters
162
+ ----------
163
+ ax : matplotlib.axes.Axes
164
+ Target axes containing matrix plot elements.
165
+ style : dict
166
+ Style dictionary with imshow_*, matshow_*, spy_* keys.
167
+ """
168
+ from matplotlib.image import AxesImage
169
+
170
+ has_image = any(isinstance(c, AxesImage) for c in ax.get_children())
171
+ if not has_image:
172
+ return
173
+
174
+ # Check if this is specgram (has xlabel or ylabel)
175
+ # Specgram typically has "Time" and "Frequency" labels
176
+ xlabel = ax.get_xlabel()
177
+ ylabel = ax.get_ylabel()
178
+ is_specgram = bool(xlabel or ylabel)
179
+
180
+ # Don't hide axes for specgram - it needs visible ticks
181
+ if is_specgram:
182
+ return
183
+
184
+ show_axes = style.get("imshow_show_axes", True)
185
+ show_labels = style.get("imshow_show_labels", True)
186
+
187
+ if not show_axes:
188
+ ax.set_xticks([])
189
+ ax.set_yticks([])
190
+ ax.set_xticklabels([])
191
+ ax.set_yticklabels([])
192
+ for spine in ax.spines.values():
193
+ spine.set_visible(False)
194
+
195
+ if not show_labels:
196
+ ax.set_xlabel("")
197
+ ax.set_ylabel("")
198
+
199
+
200
+ __all__ = [
201
+ "apply_boxplot_style",
202
+ "apply_violinplot_style",
203
+ "apply_barplot_style",
204
+ "apply_histogram_style",
205
+ "apply_pie_style",
206
+ "apply_matrix_style",
207
+ ]
208
+
209
+ # EOF