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,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,274 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Statistical annotation drawing utilities for comparison brackets and stars."""
4
+
5
+ from typing import Any, Dict, List, Literal, Optional
6
+
7
+ from matplotlib.axes import Axes
8
+
9
+
10
+ def get_theme_text_color(default: str = "black") -> str:
11
+ """Get text color from loaded style's theme settings."""
12
+ try:
13
+ from ..styles._style_loader import _STYLE_CACHE
14
+
15
+ if _STYLE_CACHE is not None:
16
+ theme = getattr(_STYLE_CACHE, "theme", None)
17
+ if theme is not None:
18
+ mode = getattr(theme, "mode", "light")
19
+ theme_colors = getattr(theme, mode, None)
20
+ if theme_colors is not None:
21
+ return getattr(theme_colors, "text", default)
22
+ except Exception:
23
+ pass
24
+ return default
25
+
26
+
27
+ def get_style_value(section: str, key: str, default: Any) -> Any:
28
+ """Get a value from loaded style settings.
29
+
30
+ Parameters
31
+ ----------
32
+ section : str
33
+ Style section (e.g., 'fonts', 'lines', 'stat_annotation')
34
+ key : str
35
+ Key within the section (e.g., 'annotation_pt', 'bracket_mm')
36
+ default : Any
37
+ Default value if not found
38
+ """
39
+ try:
40
+ from ..styles._style_loader import _STYLE_CACHE
41
+
42
+ if _STYLE_CACHE is not None:
43
+ section_obj = getattr(_STYLE_CACHE, section, None)
44
+ if section_obj is not None:
45
+ return getattr(section_obj, key, default)
46
+ except Exception:
47
+ pass
48
+ return default
49
+
50
+
51
+ def p_to_stars(p_value: float, ns_symbol: bool = True) -> str:
52
+ """Convert p-value to significance stars.
53
+
54
+ Parameters
55
+ ----------
56
+ p_value : float
57
+ The p-value to convert.
58
+ ns_symbol : bool
59
+ If True, return "n.s." for non-significant. If False, return "".
60
+
61
+ Returns
62
+ -------
63
+ str
64
+ Stars representation: "***" (p<0.001), "**" (p<0.01),
65
+ "*" (p<0.05), "n.s." or "" (p>=0.05).
66
+ """
67
+ if p_value < 0.001:
68
+ return "***"
69
+ elif p_value < 0.01:
70
+ return "**"
71
+ elif p_value < 0.05:
72
+ return "*"
73
+ else:
74
+ return "n.s." if ns_symbol else ""
75
+
76
+
77
+ def draw_stat_annotation(
78
+ ax: Axes,
79
+ x1: float,
80
+ x2: float,
81
+ y: Optional[float] = None,
82
+ text: Optional[str] = None,
83
+ p_value: Optional[float] = None,
84
+ style: Literal["stars", "p_value", "both", "bracket_only"] = "stars",
85
+ bracket_height: Optional[float] = None,
86
+ text_offset: Optional[float] = None,
87
+ color: Optional[str] = None,
88
+ linewidth: Optional[float] = None,
89
+ fontsize: Optional[float] = None,
90
+ fontweight: Optional[str] = None,
91
+ **kwargs,
92
+ ) -> List[Any]:
93
+ """Draw a statistical comparison bracket with annotation.
94
+
95
+ Parameters
96
+ ----------
97
+ ax : Axes
98
+ The matplotlib axes to draw on.
99
+ x1, x2 : float
100
+ X positions of the two groups being compared.
101
+ y : float, optional
102
+ Y position for the bracket. If None, auto-calculated from data.
103
+ text : str, optional
104
+ Custom text to display. Overrides p_value formatting.
105
+ p_value : float, optional
106
+ P-value for automatic star conversion.
107
+ style : str
108
+ Display style: "stars", "p_value", "both", "bracket_only".
109
+ bracket_height : float
110
+ Height of bracket tips as fraction of axes height.
111
+ text_offset : float
112
+ Offset of text above bracket as fraction of axes height.
113
+ color : str
114
+ Color for bracket and text.
115
+ linewidth : float
116
+ Line width for bracket.
117
+ fontsize : float
118
+ Font size for annotation text.
119
+ **kwargs
120
+ Additional kwargs passed to ax.text().
121
+
122
+ Returns
123
+ -------
124
+ list
125
+ List of matplotlib artists created (lines and text).
126
+ """
127
+ artists = []
128
+
129
+ from .._utils._units import mm_to_pt
130
+
131
+ # Resolve values from style if not explicitly provided
132
+ if color is None:
133
+ color = get_theme_text_color(default="black")
134
+ if bracket_height is None:
135
+ bracket_height = get_style_value("stat_annotation", "bracket_height", 0.03)
136
+ if text_offset is None:
137
+ text_offset = get_style_value("stat_annotation", "text_offset", 0.01)
138
+ if linewidth is None:
139
+ # Read mm value and convert to points
140
+ linewidth_mm = get_style_value("stat_annotation", "linewidth_mm", 0.2)
141
+ linewidth = mm_to_pt(linewidth_mm)
142
+
143
+ # Font settings from style: both stars and p-values use same fontsize_pt
144
+ # Stars are bold, p-values are normal weight
145
+ annotation_fontsize = get_style_value("stat_annotation", "fontsize_pt", 6)
146
+ stars_fontweight = get_style_value("stat_annotation", "stars_fontweight", "bold")
147
+
148
+ # Get axes limits for relative positioning
149
+ ylim = ax.get_ylim()
150
+ y_range = ylim[1] - ylim[0]
151
+
152
+ # Auto-calculate y position if not provided
153
+ if y is None:
154
+ # Find max y value in the x range and add padding
155
+ y = ylim[1] + y_range * 0.05
156
+
157
+ # Calculate bracket dimensions in data coordinates
158
+ tip_height = y_range * bracket_height
159
+ text_y_offset = y_range * text_offset
160
+
161
+ # Draw bracket: horizontal line with vertical tips
162
+ # Left tip
163
+ line1 = ax.plot(
164
+ [x1, x1], [y - tip_height, y], color=color, linewidth=linewidth, clip_on=False
165
+ )[0]
166
+ artists.append(line1)
167
+
168
+ # Horizontal bar
169
+ line2 = ax.plot([x1, x2], [y, y], color=color, linewidth=linewidth, clip_on=False)[
170
+ 0
171
+ ]
172
+ artists.append(line2)
173
+
174
+ # Right tip
175
+ line3 = ax.plot(
176
+ [x2, x2], [y, y - tip_height], color=color, linewidth=linewidth, clip_on=False
177
+ )[0]
178
+ artists.append(line3)
179
+
180
+ # Determine annotation text and whether it's stars-only
181
+ is_stars_only = False
182
+ if text is None and style != "bracket_only":
183
+ if p_value is not None:
184
+ if style == "stars":
185
+ text = p_to_stars(p_value)
186
+ # Only bold for actual stars, not for n.s.
187
+ is_stars_only = text not in ("n.s.", "")
188
+ elif style == "p_value":
189
+ # Use italic p with spaces around operators
190
+ if p_value < 0.001:
191
+ text = r"$\it{p}$ < 0.001"
192
+ else:
193
+ text = rf"$\it{{p}}$ = {p_value:.3f}"
194
+ elif style == "both":
195
+ stars = p_to_stars(p_value)
196
+ # Use italic p with spaces around operators
197
+ if p_value < 0.001:
198
+ text = rf"{stars} ($\it{{p}}$ < 0.001)"
199
+ else:
200
+ text = rf"{stars} ($\it{{p}}$ = {p_value:.3f})"
201
+
202
+ # Draw text if available
203
+ if text and style != "bracket_only":
204
+ text_x = (x1 + x2) / 2
205
+ text_y = y + text_y_offset
206
+
207
+ # Use same fontsize for stars and p-values, but stars are bold
208
+ effective_fontsize = fontsize if fontsize is not None else annotation_fontsize
209
+ if is_stars_only:
210
+ effective_fontweight = (
211
+ fontweight if fontweight is not None else stars_fontweight
212
+ )
213
+ else:
214
+ effective_fontweight = fontweight if fontweight is not None else "normal"
215
+
216
+ text_kwargs = {
217
+ "ha": "center",
218
+ "va": "bottom",
219
+ "fontsize": effective_fontsize,
220
+ "fontweight": effective_fontweight,
221
+ "color": color,
222
+ }
223
+ text_kwargs.update(kwargs)
224
+ txt = ax.text(text_x, text_y, text, **text_kwargs)
225
+ artists.append(txt)
226
+
227
+ return artists
228
+
229
+
230
+ def calculate_auto_y(
231
+ ax: Axes,
232
+ x1: float,
233
+ x2: float,
234
+ existing_annotations: List[Dict[str, Any]],
235
+ padding: float = 0.05,
236
+ ) -> float:
237
+ """Calculate automatic y position for a new annotation.
238
+
239
+ Avoids overlapping with existing annotations by stacking.
240
+
241
+ Parameters
242
+ ----------
243
+ ax : Axes
244
+ The matplotlib axes.
245
+ x1, x2 : float
246
+ X positions of the comparison.
247
+ existing_annotations : list
248
+ List of existing annotation info dicts with x1, x2, y keys.
249
+ padding : float
250
+ Padding as fraction of y range.
251
+
252
+ Returns
253
+ -------
254
+ float
255
+ Suggested y position for the new annotation.
256
+ """
257
+ ylim = ax.get_ylim()
258
+ y_range = ylim[1] - ylim[0]
259
+ pad = y_range * padding
260
+
261
+ # Start above the data
262
+ y = ylim[1] + pad
263
+
264
+ # Check for overlaps with existing annotations
265
+ for ann in existing_annotations:
266
+ ann_x1, ann_x2 = ann.get("x1", 0), ann.get("x2", 0)
267
+ ann_y = ann.get("y", 0)
268
+
269
+ # Check if x ranges overlap
270
+ if not (x2 < ann_x1 or x1 > ann_x2):
271
+ # Overlapping x range, need to stack
272
+ y = max(y, ann_y + pad * 2)
273
+
274
+ return y
@@ -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
  ]
@@ -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