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,279 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Custom plot replay functions (joyplot, swarmplot) for figure reproduction."""
4
+
5
+ from typing import Any, List
6
+
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ from matplotlib.axes import Axes
10
+
11
+ from .._recorder import CallRecord
12
+
13
+
14
+ def replay_joyplot_call(ax: Axes, call: CallRecord) -> Any:
15
+ """Replay a joyplot call on an axes.
16
+
17
+ Parameters
18
+ ----------
19
+ ax : Axes
20
+ The matplotlib axes.
21
+ call : CallRecord
22
+ The joyplot call to replay.
23
+
24
+ Returns
25
+ -------
26
+ Any
27
+ Result of the joyplot call.
28
+ """
29
+ from scipy import stats
30
+
31
+ from ._core import _reconstruct_kwargs, _reconstruct_value
32
+
33
+ # Reconstruct args
34
+ arrays = []
35
+ for arg_data in call.args:
36
+ value = _reconstruct_value(arg_data)
37
+ if isinstance(value, list):
38
+ # Could be a list of arrays
39
+ arrays = [np.asarray(arr) for arr in value]
40
+ else:
41
+ arrays.append(np.asarray(value))
42
+
43
+ if not arrays:
44
+ return None
45
+
46
+ # Get kwargs
47
+ kwargs = _reconstruct_kwargs(call.kwargs)
48
+ overlap = kwargs.get("overlap", 0.5)
49
+ fill_alpha = kwargs.get("fill_alpha", 0.7)
50
+ line_alpha = kwargs.get("line_alpha", 1.0)
51
+ labels = kwargs.get("labels")
52
+
53
+ n_ridges = len(arrays)
54
+
55
+ # Get colors from style
56
+ from ..styles import get_style
57
+
58
+ style = get_style()
59
+ if style and "colors" in style and "palette" in style.colors:
60
+ palette = list(style.colors.palette)
61
+ colors = []
62
+ for c in palette:
63
+ if isinstance(c, (list, tuple)) and len(c) >= 3:
64
+ if all(v <= 1.0 for v in c):
65
+ colors.append(tuple(c))
66
+ else:
67
+ colors.append(tuple(v / 255.0 for v in c))
68
+ else:
69
+ colors.append(c)
70
+ else:
71
+ colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
72
+
73
+ # Calculate global x range
74
+ all_data = np.concatenate([np.asarray(arr) for arr in arrays])
75
+ x_min, x_max = np.min(all_data), np.max(all_data)
76
+ x_range = x_max - x_min
77
+ x_padding = x_range * 0.1
78
+ x = np.linspace(x_min - x_padding, x_max + x_padding, 200)
79
+
80
+ # Calculate KDEs and find max density for scaling
81
+ kdes = []
82
+ max_density = 0
83
+ for arr in arrays:
84
+ arr = np.asarray(arr)
85
+ if len(arr) > 1:
86
+ kde = stats.gaussian_kde(arr)
87
+ density = kde(x)
88
+ kdes.append(density)
89
+ max_density = max(max_density, np.max(density))
90
+ else:
91
+ kdes.append(np.zeros_like(x))
92
+
93
+ # Scale factor for ridge height
94
+ ridge_height = 1.0 / (1.0 - overlap * 0.5) if overlap < 1 else 2.0
95
+
96
+ # Get line width from style
97
+ from .._utils._units import mm_to_pt
98
+
99
+ lw = mm_to_pt(0.2) # Default
100
+ if style and "lines" in style:
101
+ lw = mm_to_pt(style.lines.get("trace_mm", 0.2))
102
+
103
+ # Plot each ridge from back to front
104
+ for i in range(n_ridges - 1, -1, -1):
105
+ color = colors[i % len(colors)]
106
+ baseline = i * (1.0 - overlap)
107
+
108
+ # Scale density to fit nicely
109
+ scaled_density = (
110
+ kdes[i] / max_density * ridge_height if max_density > 0 else kdes[i]
111
+ )
112
+
113
+ # Fill
114
+ ax.fill_between(
115
+ x,
116
+ baseline,
117
+ baseline + scaled_density,
118
+ facecolor=color,
119
+ edgecolor="none",
120
+ alpha=fill_alpha,
121
+ )
122
+ # Line on top
123
+ ax.plot(
124
+ x, baseline + scaled_density, color=color, alpha=line_alpha, linewidth=lw
125
+ )
126
+
127
+ # Set y limits
128
+ ax.set_ylim(-0.1, n_ridges * (1.0 - overlap) + ridge_height)
129
+
130
+ # Set y-axis labels if provided
131
+ if labels:
132
+ y_positions = [(i * (1.0 - overlap)) + 0.3 for i in range(n_ridges)]
133
+ ax.set_yticks(y_positions)
134
+ ax.set_yticklabels(labels)
135
+ else:
136
+ ax.set_yticks([])
137
+
138
+ return ax
139
+
140
+
141
+ def replay_swarmplot_call(ax: Axes, call: CallRecord) -> List[Any]:
142
+ """Replay a swarmplot call on an axes.
143
+
144
+ Parameters
145
+ ----------
146
+ ax : Axes
147
+ The matplotlib axes.
148
+ call : CallRecord
149
+ The swarmplot call to replay.
150
+
151
+ Returns
152
+ -------
153
+ list
154
+ List of PathCollection objects.
155
+ """
156
+ from ._core import _reconstruct_kwargs, _reconstruct_value
157
+
158
+ # Reconstruct args
159
+ data = []
160
+ for arg_data in call.args:
161
+ value = _reconstruct_value(arg_data)
162
+ if isinstance(value, list):
163
+ # Could be a list of arrays
164
+ data = [np.asarray(arr) for arr in value]
165
+ else:
166
+ data.append(np.asarray(value))
167
+
168
+ if not data:
169
+ return []
170
+
171
+ # Get kwargs
172
+ kwargs = _reconstruct_kwargs(call.kwargs)
173
+ positions = kwargs.get("positions")
174
+ size = kwargs.get("size", 0.8)
175
+ alpha = kwargs.get("alpha", 0.7)
176
+ jitter = kwargs.get("jitter", 0.3)
177
+
178
+ if positions is None:
179
+ positions = list(range(1, len(data) + 1))
180
+
181
+ # Get style
182
+ from .._utils._units import mm_to_pt
183
+ from ..styles import get_style
184
+
185
+ style = get_style()
186
+ size_pt = mm_to_pt(size) ** 2 # matplotlib uses area
187
+
188
+ # Get colors
189
+ if style and "colors" in style and "palette" in style.colors:
190
+ palette = list(style.colors.palette)
191
+ colors = []
192
+ for c in palette:
193
+ if isinstance(c, (list, tuple)) and len(c) >= 3:
194
+ if all(v <= 1.0 for v in c):
195
+ colors.append(tuple(c))
196
+ else:
197
+ colors.append(tuple(v / 255.0 for v in c))
198
+ else:
199
+ colors.append(c)
200
+ else:
201
+ colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
202
+
203
+ # Random generator for reproducible jitter
204
+ rng = np.random.default_rng(42)
205
+
206
+ results = []
207
+ for i, (arr, pos) in enumerate(zip(data, positions)):
208
+ arr = np.asarray(arr)
209
+
210
+ # Create jittered x positions using simplified beeswarm
211
+ x_offsets = _beeswarm_positions(arr, jitter, rng)
212
+ x_positions = pos + x_offsets
213
+
214
+ c = colors[i % len(colors)]
215
+ result = ax.scatter(
216
+ x_positions,
217
+ arr,
218
+ s=size_pt,
219
+ c=[c],
220
+ alpha=alpha,
221
+ )
222
+ results.append(result)
223
+
224
+ return results
225
+
226
+
227
+ def _beeswarm_positions(
228
+ data: np.ndarray,
229
+ width: float,
230
+ rng: np.random.Generator,
231
+ ) -> np.ndarray:
232
+ """Calculate beeswarm-style x positions to minimize overlap.
233
+
234
+ Parameters
235
+ ----------
236
+ data : array
237
+ Y values of points.
238
+ width : float
239
+ Maximum jitter width.
240
+ rng : Generator
241
+ Random number generator.
242
+
243
+ Returns
244
+ -------
245
+ array
246
+ X offsets for each point.
247
+ """
248
+ n = len(data)
249
+ if n == 0:
250
+ return np.array([])
251
+
252
+ # Sort data and get order
253
+ order = np.argsort(data)
254
+ sorted_data = data[order]
255
+
256
+ # Group nearby points and offset them
257
+ x_offsets = np.zeros(n)
258
+
259
+ # Simple approach: bin by quantiles and spread within each bin
260
+ n_bins = max(1, int(np.sqrt(n)))
261
+ bin_edges = np.percentile(sorted_data, np.linspace(0, 100, n_bins + 1))
262
+
263
+ for i in range(n_bins):
264
+ mask = (sorted_data >= bin_edges[i]) & (sorted_data <= bin_edges[i + 1])
265
+ n_in_bin = mask.sum()
266
+ if n_in_bin > 0:
267
+ # Spread points evenly within bin width
268
+ offsets = np.linspace(-width / 2, width / 2, n_in_bin)
269
+ # Add small random noise
270
+ offsets += rng.uniform(-width * 0.1, width * 0.1, n_in_bin)
271
+ x_offsets[mask] = offsets
272
+
273
+ # Restore original order
274
+ result = np.zeros(n)
275
+ result[order] = x_offsets
276
+ return result
277
+
278
+
279
+ __all__ = ["replay_joyplot_call", "replay_swarmplot_call"]
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Seaborn plot replay for figure reproduction."""
4
+
5
+ from typing import Any
6
+
7
+ from matplotlib.axes import Axes
8
+
9
+ from .._recorder import CallRecord
10
+
11
+
12
+ def replay_seaborn_call(ax: Axes, call: CallRecord) -> Any:
13
+ """Replay a seaborn call on an axes.
14
+
15
+ Parameters
16
+ ----------
17
+ ax : Axes
18
+ The matplotlib axes.
19
+ call : CallRecord
20
+ The seaborn call to replay.
21
+
22
+ Returns
23
+ -------
24
+ Any
25
+ Result of the seaborn call.
26
+ """
27
+ try:
28
+ import pandas as pd
29
+ import seaborn as sns
30
+ except ImportError:
31
+ import warnings
32
+
33
+ warnings.warn("seaborn/pandas required to replay seaborn calls")
34
+ return None
35
+
36
+ from ._core import _reconstruct_value
37
+
38
+ # Get the seaborn function name (remove "sns." prefix)
39
+ func_name = call.function[4:] # Remove "sns."
40
+ func = getattr(sns, func_name, None)
41
+
42
+ if func is None:
43
+ import warnings
44
+
45
+ warnings.warn(f"Seaborn function {func_name} not found")
46
+ return None
47
+
48
+ # Reconstruct data from args
49
+ # Args contain column data with "param" field indicating the parameter name
50
+ data_dict = {}
51
+ param_mapping = {} # Maps param name to column name
52
+
53
+ for arg_data in call.args:
54
+ param = arg_data.get("param")
55
+ name = arg_data.get("name")
56
+ value = _reconstruct_value(arg_data)
57
+
58
+ if param is not None:
59
+ # This is a DataFrame column
60
+ col_name = name if name else param
61
+ data_dict[col_name] = value
62
+ param_mapping[param] = col_name
63
+
64
+ # Build kwargs
65
+ kwargs = call.kwargs.copy()
66
+
67
+ # Remove internal keys
68
+ internal_keys = [k for k in kwargs.keys() if k.startswith("_")]
69
+ for key in internal_keys:
70
+ kwargs.pop(key, None)
71
+
72
+ # If we have data columns, create a DataFrame
73
+ if data_dict:
74
+ df = pd.DataFrame(data_dict)
75
+ kwargs["data"] = df
76
+
77
+ # Update column name references in kwargs
78
+ for param, col_name in param_mapping.items():
79
+ if param in ["x", "y", "hue", "size", "style", "row", "col"]:
80
+ kwargs[param] = col_name
81
+
82
+ # Add the axes
83
+ kwargs["ax"] = ax
84
+
85
+ # Convert certain list parameters back to tuples (YAML serializes tuples as lists)
86
+ # 'sizes' in seaborn expects a tuple (min, max) for range, not a list
87
+ if "sizes" in kwargs and isinstance(kwargs["sizes"], list):
88
+ kwargs["sizes"] = tuple(kwargs["sizes"])
89
+
90
+ # Call the seaborn function
91
+ try:
92
+ return func(**kwargs)
93
+ except Exception as e:
94
+ import warnings
95
+
96
+ warnings.warn(f"Failed to replay sns.{func_name}: {e}")
97
+ return None
98
+
99
+
100
+ __all__ = ["replay_seaborn_call"]
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Violin plot replay for figure reproduction."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ import numpy as np
8
+ from matplotlib.axes import Axes
9
+
10
+ from .._recorder import CallRecord
11
+
12
+
13
+ def replay_violinplot_call(ax: Axes, call: CallRecord) -> Any:
14
+ """Replay a violinplot call with inner option support.
15
+
16
+ Parameters
17
+ ----------
18
+ ax : Axes
19
+ The matplotlib axes.
20
+ call : CallRecord
21
+ The violinplot call to replay.
22
+
23
+ Returns
24
+ -------
25
+ Any
26
+ Result of the violinplot call.
27
+ """
28
+ # Import from _core module (will be available after full package setup)
29
+ from ._core import _reconstruct_kwargs, _reconstruct_value
30
+
31
+ # Reconstruct args
32
+ args = []
33
+ for arg_data in call.args:
34
+ value = _reconstruct_value(arg_data)
35
+ args.append(value)
36
+
37
+ # Get kwargs and reconstruct arrays
38
+ kwargs = _reconstruct_kwargs(call.kwargs)
39
+
40
+ # Extract inner option (not a matplotlib kwarg)
41
+ inner = kwargs.pop("inner", "box")
42
+
43
+ # Get display options
44
+ showmeans = kwargs.pop("showmeans", False)
45
+ showmedians = kwargs.pop("showmedians", True)
46
+ showextrema = kwargs.pop("showextrema", False)
47
+
48
+ # When using inner box/swarm, suppress default median/extrema lines
49
+ if inner in ("box", "swarm"):
50
+ showmedians = False
51
+ showextrema = False
52
+
53
+ # Call matplotlib's violinplot
54
+ try:
55
+ result = ax.violinplot(
56
+ *args,
57
+ showmeans=showmeans,
58
+ showmedians=showmedians,
59
+ showextrema=showextrema,
60
+ **kwargs,
61
+ )
62
+
63
+ # Get style settings for inner display
64
+ from ..styles import get_style
65
+
66
+ style = get_style()
67
+ violin_style = style.get("violinplot", {}) if style else {}
68
+
69
+ # Apply alpha from style to violin bodies
70
+ alpha = violin_style.get("alpha", 0.7)
71
+ if "bodies" in result:
72
+ for body in result["bodies"]:
73
+ body.set_alpha(alpha)
74
+
75
+ # Determine positions
76
+ dataset = args[0] if args else []
77
+ positions = kwargs.get("positions")
78
+ if positions is None:
79
+ positions = list(range(1, len(dataset) + 1))
80
+
81
+ # Overlay inner elements based on inner type
82
+ if inner == "box":
83
+ _add_violin_inner_box(ax, dataset, positions, violin_style)
84
+ elif inner == "swarm":
85
+ _add_violin_inner_swarm(ax, dataset, positions, violin_style)
86
+ elif inner == "stick":
87
+ _add_violin_inner_stick(ax, dataset, positions, violin_style)
88
+ elif inner == "point":
89
+ _add_violin_inner_point(ax, dataset, positions, violin_style)
90
+
91
+ return result
92
+ except Exception as e:
93
+ import warnings
94
+
95
+ warnings.warn(f"Failed to replay violinplot: {e}")
96
+ return None
97
+
98
+
99
+ def _add_violin_inner_box(ax: Axes, dataset, positions, style: Dict[str, Any]) -> None:
100
+ """Add box plot inside violin for reproduction."""
101
+ from ..styles._style_applier import mm_to_pt
102
+
103
+ whisker_lw = mm_to_pt(style.get("whisker_mm", 0.2))
104
+ median_size = mm_to_pt(style.get("median_mm", 0.8))
105
+
106
+ for data, pos in zip(dataset, positions):
107
+ data = np.asarray(data)
108
+ q1, median, q3 = np.percentile(data, [25, 50, 75])
109
+ iqr = q3 - q1
110
+ whisker_low = max(data.min(), q1 - 1.5 * iqr)
111
+ whisker_high = min(data.max(), q3 + 1.5 * iqr)
112
+
113
+ # Draw box (Q1 to Q3)
114
+ ax.vlines(pos, q1, q3, colors="black", linewidths=whisker_lw, zorder=3)
115
+ # Draw whiskers
116
+ ax.vlines(
117
+ pos, whisker_low, q1, colors="black", linewidths=whisker_lw * 0.5, zorder=3
118
+ )
119
+ ax.vlines(
120
+ pos, q3, whisker_high, colors="black", linewidths=whisker_lw * 0.5, zorder=3
121
+ )
122
+ # Draw median as a white dot with black edge
123
+ ax.scatter(
124
+ [pos],
125
+ [median],
126
+ s=median_size**2,
127
+ c="white",
128
+ edgecolors="black",
129
+ linewidths=whisker_lw,
130
+ zorder=4,
131
+ )
132
+
133
+
134
+ def _add_violin_inner_swarm(
135
+ ax: Axes, dataset, positions, style: Dict[str, Any]
136
+ ) -> None:
137
+ """Add swarm points inside violin for reproduction."""
138
+ from ..styles._style_applier import mm_to_pt
139
+
140
+ point_size = mm_to_pt(style.get("median_mm", 0.8))
141
+
142
+ for data, pos in zip(dataset, positions):
143
+ data = np.asarray(data)
144
+ n = len(data)
145
+ jitter = np.random.default_rng(42).uniform(-0.15, 0.15, n)
146
+ x_positions = pos + jitter
147
+ ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.5, zorder=3)
148
+
149
+
150
+ def _add_violin_inner_stick(
151
+ ax: Axes, dataset, positions, style: Dict[str, Any]
152
+ ) -> None:
153
+ """Add stick markers inside violin for reproduction."""
154
+ from ..styles._style_applier import mm_to_pt
155
+
156
+ lw = mm_to_pt(style.get("whisker_mm", 0.2))
157
+
158
+ for data, pos in zip(dataset, positions):
159
+ data = np.asarray(data)
160
+ for val in data:
161
+ ax.hlines(
162
+ val,
163
+ pos - 0.05,
164
+ pos + 0.05,
165
+ colors="black",
166
+ linewidths=lw * 0.5,
167
+ alpha=0.3,
168
+ zorder=3,
169
+ )
170
+
171
+
172
+ def _add_violin_inner_point(
173
+ ax: Axes, dataset, positions, style: Dict[str, Any]
174
+ ) -> None:
175
+ """Add point markers inside violin for reproduction."""
176
+ from ..styles._style_applier import mm_to_pt
177
+
178
+ point_size = mm_to_pt(style.get("median_mm", 0.8)) * 0.5
179
+
180
+ for data, pos in zip(dataset, positions):
181
+ data = np.asarray(data)
182
+ x_positions = np.full_like(data, pos)
183
+ ax.scatter(x_positions, data, s=point_size**2, c="black", alpha=0.3, zorder=3)
184
+
185
+
186
+ __all__ = ["replay_violinplot_call"]