figrecipe 0.6.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 (177) hide show
  1. figrecipe/__init__.py +106 -973
  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 +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  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/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -93,6 +93,22 @@ class RecordingFigure:
93
93
  pass
94
94
  return default
95
95
 
96
+ def _get_theme_text_color(self, default: str = "black") -> str:
97
+ """Get text color from loaded style's theme settings."""
98
+ try:
99
+ from ..styles._style_loader import _STYLE_CACHE
100
+
101
+ if _STYLE_CACHE is not None:
102
+ theme = getattr(_STYLE_CACHE, "theme", None)
103
+ if theme is not None:
104
+ mode = getattr(theme, "mode", "light")
105
+ theme_colors = getattr(theme, mode, None)
106
+ if theme_colors is not None:
107
+ return getattr(theme_colors, "text", default)
108
+ except Exception:
109
+ pass
110
+ return default
111
+
96
112
  def suptitle(self, t: str, **kwargs) -> Any:
97
113
  """Set super title for the figure and record it.
98
114
 
@@ -162,6 +178,131 @@ class RecordingFigure:
162
178
  # Call the underlying figure's supylabel
163
179
  return self._fig.supylabel(t, **kwargs)
164
180
 
181
+ def add_panel_labels(
182
+ self,
183
+ labels: Optional[List[str]] = None,
184
+ loc: str = "upper left",
185
+ offset: Tuple[float, float] = (-0.1, 1.05),
186
+ fontsize: Optional[float] = None,
187
+ fontweight: str = "bold",
188
+ **kwargs,
189
+ ) -> List[Any]:
190
+ """Add panel labels (A, B, C, D, etc.) to multi-panel figures.
191
+
192
+ Parameters
193
+ ----------
194
+ labels : list of str, optional
195
+ Custom labels. If None, uses uppercase letters (A, B, C, ...).
196
+ loc : str
197
+ Location hint: 'upper left' (default), 'upper right', 'lower left', 'lower right'.
198
+ offset : tuple of float
199
+ (x, y) offset in axes coordinates from the corner.
200
+ Default is (-0.1, 1.05) for upper left positioning.
201
+ fontsize : float, optional
202
+ Font size in points. If None, uses style's title_pt or 10.
203
+ fontweight : str
204
+ Font weight (default: 'bold').
205
+ **kwargs
206
+ Additional arguments passed to ax.text().
207
+
208
+ Returns
209
+ -------
210
+ list of Text
211
+ The matplotlib Text objects created.
212
+
213
+ Examples
214
+ --------
215
+ >>> fig, axes = fr.subplots(2, 2)
216
+ >>> fig.add_panel_labels() # Adds A, B, C, D
217
+ >>> fig.add_panel_labels(['i', 'ii', 'iii', 'iv']) # Custom labels
218
+ >>> fig.add_panel_labels(loc='upper right', offset=(1.05, 1.05))
219
+ """
220
+ from ._panel_labels import add_panel_labels as _add_panel_labels
221
+
222
+ # Get fontsize from style if not specified
223
+ if fontsize is None:
224
+ fontsize = self._get_style_fontsize("title_pt", 10)
225
+
226
+ # Get theme text color (unless user provided 'color' in kwargs)
227
+ if "color" not in kwargs:
228
+ text_color = self._get_theme_text_color()
229
+ else:
230
+ text_color = kwargs.pop("color")
231
+
232
+ def record_callback(info):
233
+ self._recorder.figure_record.panel_labels = info
234
+
235
+ return _add_panel_labels(
236
+ all_axes=self.flat,
237
+ labels=labels,
238
+ loc=loc,
239
+ offset=offset,
240
+ fontsize=fontsize,
241
+ fontweight=fontweight,
242
+ text_color=text_color,
243
+ record_callback=record_callback,
244
+ **kwargs,
245
+ )
246
+
247
+ def set_title_metadata(self, title: str) -> "RecordingFigure":
248
+ """Set figure title metadata (not rendered, stored in recipe).
249
+
250
+ This is for storing a publication/reference title for the figure,
251
+ separate from suptitle which is rendered on the figure.
252
+
253
+ Parameters
254
+ ----------
255
+ title : str
256
+ The figure title for publication/reference.
257
+
258
+ Returns
259
+ -------
260
+ RecordingFigure
261
+ Self for method chaining.
262
+
263
+ Examples
264
+ --------
265
+ >>> fig, ax = fr.subplots()
266
+ >>> fig.set_title_metadata("Effect of temperature on reaction rate")
267
+ >>> fig.set_caption("Figure 1. Reaction rates measured at various temperatures.")
268
+ """
269
+ self._recorder.figure_record.title_metadata = title
270
+ return self
271
+
272
+ def set_caption(self, caption: str) -> "RecordingFigure":
273
+ """Set figure caption metadata (not rendered, stored in recipe).
274
+
275
+ This is for storing a publication caption for the figure,
276
+ typically used in scientific papers (e.g., "Fig. 1. Description...").
277
+
278
+ Parameters
279
+ ----------
280
+ caption : str
281
+ The figure caption text.
282
+
283
+ Returns
284
+ -------
285
+ RecordingFigure
286
+ Self for method chaining.
287
+
288
+ Examples
289
+ --------
290
+ >>> fig, ax = fr.subplots()
291
+ >>> fig.set_caption("Figure 1. Temperature dependence of reaction rates.")
292
+ """
293
+ self._recorder.figure_record.caption = caption
294
+ return self
295
+
296
+ @property
297
+ def title_metadata(self) -> Optional[str]:
298
+ """Get the figure title metadata."""
299
+ return self._recorder.figure_record.title_metadata
300
+
301
+ @property
302
+ def caption(self) -> Optional[str]:
303
+ """Get the figure caption metadata."""
304
+ return self._recorder.figure_record.caption
305
+
165
306
  def __getattr__(self, name: str) -> Any:
166
307
  """Delegate attribute access to underlying figure."""
167
308
  return getattr(self._fig, name)
@@ -203,6 +344,15 @@ class RecordingFigure:
203
344
  >>> fig.savefig('figure.png') # Saves both figure.png and figure.yaml
204
345
  >>> fig.savefig('figure.png', save_recipe=False) # Image only
205
346
  """
347
+ # Finalize ticks and special plots before saving
348
+ from ..styles._style_applier import finalize_special_plots, finalize_ticks
349
+ from ..styles._style_loader import get_current_style_dict
350
+
351
+ style_dict = get_current_style_dict()
352
+ for ax in self._fig.get_axes():
353
+ finalize_ticks(ax)
354
+ finalize_special_plots(ax, style_dict)
355
+
206
356
  # Handle file-like objects (BytesIO, etc.) - just pass through
207
357
  if hasattr(fname, "write"):
208
358
  self._fig.savefig(fname, **kwargs)
@@ -255,6 +405,7 @@ def create_recording_subplots(
255
405
  nrows: int = 1,
256
406
  ncols: int = 1,
257
407
  recorder: Optional["Recorder"] = None,
408
+ panel_labels: bool = False,
258
409
  **kwargs,
259
410
  ) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
260
411
  """Create a figure with recording-enabled axes.
@@ -267,6 +418,9 @@ def create_recording_subplots(
267
418
  Number of columns.
268
419
  recorder : Recorder, optional
269
420
  Recorder instance. Created if not provided.
421
+ panel_labels : bool
422
+ If True and figure has multiple panels, automatically add
423
+ panel labels (A, B, C, D, ...). Default is False.
270
424
  **kwargs
271
425
  Passed to plt.subplots().
272
426
 
@@ -276,6 +430,10 @@ def create_recording_subplots(
276
430
  Wrapped figure.
277
431
  axes : RecordingAxes or ndarray
278
432
  Wrapped axes (single if 1x1, otherwise numpy array matching matplotlib).
433
+
434
+ Examples
435
+ --------
436
+ >>> fig, axes = fr.subplots(2, 2, panel_labels=True) # Auto-adds A, B, C, D
279
437
  """
280
438
  from .._recorder import Recorder
281
439
 
@@ -312,6 +470,10 @@ def create_recording_subplots(
312
470
 
313
471
  wrapped_fig = RecordingFigure(fig, recorder, wrapped_axes)
314
472
 
473
+ # Add panel labels if requested (multi-panel figures only)
474
+ if panel_labels:
475
+ wrapped_fig.add_panel_labels()
476
+
315
477
  # Return in same shape as matplotlib (numpy arrays for consistency)
316
478
  if nrows == 1:
317
479
  # 1xN -> 1D array of shape (N,)
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel label utilities for multi-panel figures."""
4
+
5
+ import string
6
+ from typing import TYPE_CHECKING, Any, List, Optional, Tuple
7
+
8
+ if TYPE_CHECKING:
9
+ from ._axes import RecordingAxes
10
+
11
+
12
+ def add_panel_labels(
13
+ all_axes: List["RecordingAxes"],
14
+ labels: Optional[List[str]],
15
+ loc: str,
16
+ offset: Tuple[float, float],
17
+ fontsize: float,
18
+ fontweight: str,
19
+ text_color: str,
20
+ record_callback: Any,
21
+ **kwargs,
22
+ ) -> List[Any]:
23
+ """Add panel labels (A, B, C, D, etc.) to axes.
24
+
25
+ Parameters
26
+ ----------
27
+ all_axes : list of RecordingAxes
28
+ Flattened list of all axes.
29
+ labels : list of str or None
30
+ Custom labels. If None, uses uppercase letters.
31
+ loc : str
32
+ Location hint: 'upper left', 'upper right', 'lower left', 'lower right'.
33
+ offset : tuple of float
34
+ (x, y) offset in axes coordinates.
35
+ fontsize : float
36
+ Font size in points.
37
+ fontweight : str
38
+ Font weight.
39
+ text_color : str
40
+ Text color.
41
+ record_callback : callable
42
+ Callback to record panel labels info.
43
+ **kwargs
44
+ Additional arguments passed to ax.text().
45
+
46
+ Returns
47
+ -------
48
+ list of Text
49
+ The matplotlib Text objects created.
50
+ """
51
+ n_axes = len(all_axes)
52
+
53
+ # Generate default labels if not provided
54
+ if labels is None:
55
+ labels = list(string.ascii_uppercase[:n_axes])
56
+ elif len(labels) < n_axes:
57
+ # Extend with letters if not enough labels provided
58
+ labels = list(labels) + list(string.ascii_uppercase[len(labels) : n_axes])
59
+
60
+ # Calculate position based on loc
61
+ x, y, ha, va = _calculate_position(loc, offset)
62
+
63
+ # Record panel labels
64
+ record_callback(
65
+ {
66
+ "labels": labels[:n_axes],
67
+ "loc": loc,
68
+ "offset": offset,
69
+ "fontsize": fontsize,
70
+ "fontweight": fontweight,
71
+ "color": text_color,
72
+ "kwargs": kwargs,
73
+ }
74
+ )
75
+
76
+ # Add labels to each axes
77
+ text_objects = []
78
+ for ax, label in zip(all_axes, labels[:n_axes]):
79
+ text = ax.ax.text(
80
+ x,
81
+ y,
82
+ label,
83
+ transform=ax.ax.transAxes,
84
+ fontsize=fontsize,
85
+ fontweight=fontweight,
86
+ color=text_color,
87
+ ha=ha,
88
+ va=va,
89
+ **kwargs,
90
+ )
91
+ text_objects.append(text)
92
+
93
+ return text_objects
94
+
95
+
96
+ def _calculate_position(
97
+ loc: str, offset: Tuple[float, float]
98
+ ) -> Tuple[float, float, str, str]:
99
+ """Calculate text position and alignment based on location.
100
+
101
+ Returns
102
+ -------
103
+ tuple
104
+ (x, y, ha, va) where ha/va are horizontal/vertical alignment.
105
+ """
106
+ if loc == "upper left":
107
+ x, y = offset
108
+ ha, va = "right", "bottom"
109
+ elif loc == "upper right":
110
+ x, y = offset
111
+ ha, va = "left", "bottom"
112
+ elif loc == "lower left":
113
+ x, y = offset[0], -offset[1] + 1.0
114
+ ha, va = "right", "top"
115
+ elif loc == "lower right":
116
+ x, y = offset
117
+ ha, va = "left", "top"
118
+ else:
119
+ x, y = offset
120
+ ha, va = "right", "bottom"
121
+
122
+ return x, y, ha, va
123
+
124
+
125
+ __all__ = ["add_panel_labels"]
126
+
127
+ # EOF
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Helper functions for custom plot methods in RecordingAxes."""
4
+
5
+ from typing import List, Tuple
6
+
7
+ import numpy as np
8
+
9
+
10
+ def get_colors_from_style(n_colors: int, explicit_colors=None) -> List:
11
+ """Get colors from style or matplotlib defaults.
12
+
13
+ Parameters
14
+ ----------
15
+ n_colors : int
16
+ Number of colors needed.
17
+ explicit_colors : list or color, optional
18
+ Explicitly provided colors.
19
+
20
+ Returns
21
+ -------
22
+ list
23
+ List of colors.
24
+ """
25
+ if explicit_colors is not None:
26
+ if isinstance(explicit_colors, list):
27
+ return explicit_colors
28
+ return [explicit_colors] * n_colors
29
+
30
+ from ..styles import get_style
31
+
32
+ style = get_style()
33
+ if style and "colors" in style and "palette" in style.colors:
34
+ palette = list(style.colors.palette)
35
+ colors = []
36
+ for c in palette:
37
+ if isinstance(c, (list, tuple)) and len(c) >= 3:
38
+ if all(v <= 1.0 for v in c):
39
+ colors.append(tuple(c))
40
+ else:
41
+ colors.append(tuple(v / 255.0 for v in c))
42
+ else:
43
+ colors.append(c)
44
+ return colors
45
+
46
+ # Matplotlib default color cycle
47
+ import matplotlib.pyplot as plt
48
+
49
+ return [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
50
+
51
+
52
+ def beeswarm_positions(
53
+ data: np.ndarray,
54
+ width: float,
55
+ rng: np.random.Generator,
56
+ ) -> np.ndarray:
57
+ """Calculate beeswarm-style x positions to minimize overlap.
58
+
59
+ This is a simplified beeswarm that uses binning and jittering.
60
+
61
+ Parameters
62
+ ----------
63
+ data : array
64
+ Y values of points.
65
+ width : float
66
+ Maximum jitter width.
67
+ rng : Generator
68
+ Random number generator.
69
+
70
+ Returns
71
+ -------
72
+ array
73
+ X offsets for each point.
74
+ """
75
+ n = len(data)
76
+ if n == 0:
77
+ return np.array([])
78
+
79
+ # Sort data and get order
80
+ order = np.argsort(data)
81
+ sorted_data = data[order]
82
+
83
+ # Group nearby points and offset them
84
+ x_offsets = np.zeros(n)
85
+
86
+ # Simple approach: bin by quantiles and spread within each bin
87
+ n_bins = max(1, int(np.sqrt(n)))
88
+ bin_edges = np.percentile(sorted_data, np.linspace(0, 100, n_bins + 1))
89
+
90
+ for i in range(n_bins):
91
+ mask = (sorted_data >= bin_edges[i]) & (sorted_data <= bin_edges[i + 1])
92
+ n_in_bin = mask.sum()
93
+ if n_in_bin > 0:
94
+ # Spread points evenly within bin width
95
+ offsets = np.linspace(-width / 2, width / 2, n_in_bin)
96
+ # Add small random noise
97
+ offsets += rng.uniform(-width * 0.1, width * 0.1, n_in_bin)
98
+ x_offsets[mask] = offsets
99
+
100
+ # Restore original order
101
+ result = np.zeros(n)
102
+ result[order] = x_offsets
103
+ return result
104
+
105
+
106
+ def compute_joyplot_kdes(arrays: List[np.ndarray], x: np.ndarray) -> Tuple[List, float]:
107
+ """Compute KDEs for joyplot ridges.
108
+
109
+ Parameters
110
+ ----------
111
+ arrays : list
112
+ List of data arrays.
113
+ x : array
114
+ X values for KDE evaluation.
115
+
116
+ Returns
117
+ -------
118
+ tuple
119
+ (kdes, max_density)
120
+ """
121
+ from scipy import stats
122
+
123
+ kdes = []
124
+ max_density = 0
125
+ for arr in arrays:
126
+ arr = np.asarray(arr)
127
+ if len(arr) > 1:
128
+ kde = stats.gaussian_kde(arr)
129
+ density = kde(x)
130
+ kdes.append(density)
131
+ max_density = max(max_density, np.max(density))
132
+ else:
133
+ kdes.append(np.zeros_like(x))
134
+ return kdes, max_density
135
+
136
+
137
+ __all__ = [
138
+ "get_colors_from_style",
139
+ "beeswarm_positions",
140
+ "compute_joyplot_kdes",
141
+ ]
142
+
143
+ # EOF
@@ -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
@@ -18,12 +18,10 @@ Usage:
18
18
  fig, ax = ps.subplots(**style.to_subplots_kwargs())
19
19
  """
20
20
 
21
- from ._style_applier import (
22
- apply_style_mm,
23
- apply_theme_colors,
24
- check_font,
25
- list_available_fonts,
26
- )
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
27
25
  from ._style_loader import (
28
26
  STYLE,
29
27
  get_style,
@@ -33,8 +31,10 @@ from ._style_loader import (
33
31
  to_subplots_kwargs,
34
32
  unload_style,
35
33
  )
34
+ from ._themes import apply_theme_colors
36
35
 
37
36
  __all__ = [
37
+ "DotDict",
38
38
  "load_style",
39
39
  "unload_style",
40
40
  "get_style",
@@ -46,4 +46,6 @@ __all__ = [
46
46
  "apply_theme_colors",
47
47
  "check_font",
48
48
  "list_available_fonts",
49
+ "finalize_ticks",
50
+ "finalize_special_plots",
49
51
  ]