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,507 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Style override application for preview rendering."""
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from matplotlib.axes import Axes
8
+ from matplotlib.figure import Figure
9
+
10
+
11
+ def apply_overrides(
12
+ fig: Figure, overrides: Dict[str, Any], record: Optional[Any] = None
13
+ ) -> None:
14
+ """
15
+ Apply style overrides to figure.
16
+
17
+ Parameters
18
+ ----------
19
+ fig : Figure
20
+ Matplotlib figure.
21
+ overrides : dict
22
+ Style overrides with keys like:
23
+ - axes_width_mm, axes_height_mm
24
+ - fonts_axis_label_pt, fonts_tick_label_pt
25
+ - lines_trace_mm
26
+ - spacing_horizontal_mm, spacing_vertical_mm
27
+ - margins_left_mm, margins_right_mm, margins_bottom_mm, margins_top_mm
28
+ - call_overrides: dict mapping call_id -> {param: value}
29
+ - etc.
30
+ record : FigureRecord, optional
31
+ Recording record to access call IDs for grouping elements.
32
+ """
33
+ from ..styles._style_applier import apply_style_mm
34
+ from ._figure_layout import apply_figure_layout_overrides
35
+
36
+ # Apply figure-level layout overrides (spacing, margins)
37
+ apply_figure_layout_overrides(fig, overrides, record)
38
+
39
+ axes_list = fig.get_axes()
40
+
41
+ for ax in axes_list:
42
+ # Apply mm-based styling
43
+ apply_style_mm(ax, overrides)
44
+
45
+ # Apply specific overrides that aren't handled by apply_style_mm
46
+ _apply_font_overrides(ax, overrides)
47
+ _apply_tick_overrides(ax, overrides)
48
+ _apply_behavior_overrides(ax, overrides)
49
+ _apply_legend_overrides(ax, overrides)
50
+ _apply_line_overrides(ax, overrides)
51
+ _apply_marker_overrides(ax, overrides)
52
+
53
+ # Apply color palette to existing elements
54
+ color_palette = overrides.get("color_palette")
55
+ if color_palette is not None:
56
+ ax_record = _find_ax_record(ax, axes_list, record)
57
+ apply_color_palette(ax, color_palette, ax_record)
58
+
59
+ # Apply call-specific overrides (from Element tab edits)
60
+ # Single source of truth - same function for initial and re-render
61
+ call_overrides = overrides.get("call_overrides", {})
62
+ if call_overrides and record:
63
+ from ._call_overrides import apply_call_overrides
64
+
65
+ apply_call_overrides(fig, call_overrides, record)
66
+
67
+
68
+ def _apply_font_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
69
+ """Apply font-related overrides to axes."""
70
+ # Font sizes (YAML: fonts_axis_label_pt, legacy: axis_font_size_pt)
71
+ axis_fs = overrides.get("fonts_axis_label_pt", overrides.get("axis_font_size_pt"))
72
+ if axis_fs is not None:
73
+ ax.xaxis.label.set_fontsize(axis_fs)
74
+ ax.yaxis.label.set_fontsize(axis_fs)
75
+
76
+ tick_fs = overrides.get("fonts_tick_label_pt", overrides.get("tick_font_size_pt"))
77
+ if tick_fs is not None:
78
+ ax.tick_params(labelsize=tick_fs)
79
+
80
+ title_fs = overrides.get("fonts_title_pt", overrides.get("title_font_size_pt"))
81
+ if title_fs is not None:
82
+ ax.title.set_fontsize(title_fs)
83
+
84
+ family = overrides.get("fonts_family", overrides.get("font_family"))
85
+ if family is not None:
86
+ ax.xaxis.label.set_fontfamily(family)
87
+ ax.yaxis.label.set_fontfamily(family)
88
+ ax.title.set_fontfamily(family)
89
+ for label in ax.get_xticklabels() + ax.get_yticklabels():
90
+ label.set_fontfamily(family)
91
+
92
+
93
+ def _apply_tick_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
94
+ """Apply tick-related overrides to axes."""
95
+ # Ticks (YAML: ticks_direction, legacy: tick_direction)
96
+ tick_dir = overrides.get("ticks_direction", overrides.get("tick_direction"))
97
+ if tick_dir is not None and tick_dir in ("in", "out", "inout"):
98
+ ax.tick_params(direction=tick_dir)
99
+
100
+ tick_len = overrides.get("ticks_length_mm", overrides.get("tick_length_mm"))
101
+ if tick_len is not None:
102
+ from .._utils._units import mm_to_pt
103
+
104
+ length = mm_to_pt(tick_len)
105
+ ax.tick_params(length=length)
106
+
107
+
108
+ def _apply_behavior_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
109
+ """Apply behavior-related overrides (grid, spines)."""
110
+ # Grid (YAML: behavior_grid, legacy: grid)
111
+ grid_value = overrides.get("behavior_grid", overrides.get("grid"))
112
+ if grid_value is not None:
113
+ if grid_value:
114
+ ax.grid(True, alpha=0.3)
115
+ else:
116
+ ax.grid(False)
117
+
118
+ # Spines visibility (all four directions)
119
+ for side in ["top", "right", "bottom", "left"]:
120
+ key = f"behavior_hide_{side}_spine"
121
+ legacy_key = f"hide_{side}_spine"
122
+ hide_value = overrides.get(key, overrides.get(legacy_key))
123
+ if hide_value is not None:
124
+ ax.spines[side].set_visible(not hide_value)
125
+
126
+
127
+ def _apply_legend_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
128
+ """Apply legend-related overrides."""
129
+ legend = ax.get_legend()
130
+ if legend is not None:
131
+ if "legend_frameon" in overrides:
132
+ legend.set_frame_on(overrides["legend_frameon"])
133
+
134
+ if "legend_alpha" in overrides:
135
+ frame = legend.get_frame()
136
+ fc = frame.get_facecolor()
137
+ frame.set_facecolor((*fc[:3], overrides["legend_alpha"]))
138
+
139
+
140
+ def _apply_line_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
141
+ """Apply line-related overrides."""
142
+ # Line widths (YAML: lines_trace_mm, legacy: trace_thickness_mm)
143
+ trace_mm = overrides.get("lines_trace_mm", overrides.get("trace_thickness_mm"))
144
+ if trace_mm is not None:
145
+ from .._utils._units import mm_to_pt
146
+
147
+ lw = mm_to_pt(trace_mm)
148
+ for line in ax.get_lines():
149
+ line.set_linewidth(lw)
150
+
151
+
152
+ def _apply_marker_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
153
+ """Apply marker-related overrides."""
154
+ # Marker sizes (YAML: markers_scatter_mm, legacy: marker_size_mm)
155
+ # Only apply to PathCollection (scatter), not PolyCollection (violin/fill)
156
+ scatter_mm = overrides.get(
157
+ "markers_scatter_mm",
158
+ overrides.get("markers_size_mm", overrides.get("marker_size_mm")),
159
+ )
160
+ if scatter_mm is not None:
161
+ from matplotlib.collections import PathCollection
162
+
163
+ from .._utils._units import mm_to_scatter_size
164
+
165
+ size = mm_to_scatter_size(scatter_mm)
166
+ for coll in ax.collections:
167
+ # Only apply to scatter plots (PathCollection), not violin/fill (PolyCollection)
168
+ if isinstance(coll, PathCollection):
169
+ try:
170
+ coll.set_sizes([size])
171
+ except Exception:
172
+ pass
173
+
174
+
175
+ def _find_ax_record(ax: Axes, axes_list: List[Axes], record: Optional[Any]) -> Any:
176
+ """Find the AxesRecord for a given axes."""
177
+ if record is None or not hasattr(record, "axes"):
178
+ return None
179
+
180
+ # Find ax position in the figure's axes list
181
+ ax_idx = axes_list.index(ax)
182
+ # AxesRecord keys are position tuples like "(0, 0)", "(0, 1)", etc.
183
+ # Try to match by index order
184
+ ax_keys = sorted(record.axes.keys())
185
+ if ax_idx < len(ax_keys):
186
+ return record.axes.get(ax_keys[ax_idx])
187
+ return None
188
+
189
+
190
+ def apply_color_palette(
191
+ ax: Axes, color_palette: List[Any], ax_record: Optional[Any] = None
192
+ ) -> None:
193
+ """
194
+ Apply color palette to existing plot elements, grouping by call_id.
195
+
196
+ Parameters
197
+ ----------
198
+ ax : Axes
199
+ Matplotlib axes containing plot elements.
200
+ color_palette : list
201
+ List of colors (RGB tuples or color names).
202
+ ax_record : AxesRecord, optional
203
+ Record of calls for this axes (for grouping elements by call_id).
204
+ """
205
+
206
+ # Normalize colors (RGB 0-255 to 0-1)
207
+ normalized_palette = _normalize_color_palette(color_palette)
208
+ if not normalized_palette:
209
+ return
210
+
211
+ # Build call_id to color index mapping from record
212
+ call_color_map = _build_call_color_map(ax_record)
213
+
214
+ # Apply to different element types
215
+ _apply_colors_to_lines(ax, normalized_palette, ax_record, call_color_map)
216
+ _apply_colors_to_bars(ax, normalized_palette, ax_record, call_color_map)
217
+ _apply_colors_to_pie(ax, normalized_palette, ax_record)
218
+ _apply_colors_to_scatter(ax, normalized_palette, ax_record, call_color_map)
219
+ _apply_colors_to_poly(ax, normalized_palette)
220
+ _update_legend_colors(ax, normalized_palette)
221
+
222
+
223
+ def _normalize_color_palette(color_palette: List[Any]) -> List[Any]:
224
+ """Normalize color palette to 0-1 range."""
225
+ normalized = []
226
+ for c in color_palette:
227
+ if isinstance(c, (list, tuple)) and len(c) >= 3:
228
+ if all(v <= 1.0 for v in c):
229
+ normalized.append(tuple(c))
230
+ else:
231
+ normalized.append(tuple(v / 255.0 for v in c))
232
+ else:
233
+ normalized.append(c)
234
+ return normalized
235
+
236
+
237
+ def _build_call_color_map(ax_record: Optional[Any]) -> Dict[str, int]:
238
+ """Build mapping from call_id to color index."""
239
+ call_color_map = {}
240
+ if ax_record:
241
+ color_idx = 0
242
+ for call in ax_record.calls:
243
+ if call.function in (
244
+ "plot",
245
+ "scatter",
246
+ "bar",
247
+ "barh",
248
+ "hist",
249
+ "pie",
250
+ "fill",
251
+ "fill_between",
252
+ "fill_betweenx",
253
+ ):
254
+ if call.id not in call_color_map:
255
+ call_color_map[call.id] = color_idx
256
+ color_idx += 1
257
+ return call_color_map
258
+
259
+
260
+ def _apply_colors_to_lines(
261
+ ax: Axes,
262
+ palette: List[Any],
263
+ ax_record: Optional[Any],
264
+ call_color_map: Dict[str, int],
265
+ ) -> None:
266
+ """Apply colors to line elements."""
267
+ lines = ax.get_lines()
268
+ line_calls = [
269
+ c for c in (ax_record.calls if ax_record else []) if c.function == "plot"
270
+ ]
271
+ for i, line in enumerate(lines):
272
+ # Skip internal lines (boxplot whiskers, etc.)
273
+ label = line.get_label()
274
+ if label.startswith("_"):
275
+ continue
276
+ if i < len(line_calls) and line_calls[i].id in call_color_map:
277
+ color_idx = call_color_map[line_calls[i].id]
278
+ else:
279
+ color_idx = i
280
+ line.set_color(palette[color_idx % len(palette)])
281
+
282
+
283
+ def _apply_colors_to_bars(
284
+ ax: Axes,
285
+ palette: List[Any],
286
+ ax_record: Optional[Any],
287
+ call_color_map: Dict[str, int],
288
+ ) -> None:
289
+ """Apply colors to bar and histogram elements.
290
+
291
+ Skips bars that have an explicit 'color' kwarg set (from Element tab edits).
292
+ """
293
+ from matplotlib.patches import Rectangle
294
+
295
+ rectangles = [p for p in ax.patches if isinstance(p, Rectangle)]
296
+ if not rectangles:
297
+ return
298
+
299
+ bar_calls = [
300
+ c
301
+ for c in (ax_record.calls if ax_record else [])
302
+ if c.function in ("bar", "barh", "hist")
303
+ ]
304
+ if bar_calls:
305
+ rect_per_call = len(rectangles) // len(bar_calls) if bar_calls else 1
306
+ for i, patch in enumerate(rectangles):
307
+ call_idx = (
308
+ min(i // rect_per_call, len(bar_calls) - 1) if rect_per_call > 0 else 0
309
+ )
310
+ # Skip if this call has an explicit color set (user override)
311
+ if call_idx < len(bar_calls) and "color" in bar_calls[call_idx].kwargs:
312
+ continue
313
+ if call_idx < len(bar_calls) and bar_calls[call_idx].id in call_color_map:
314
+ color_idx = call_color_map[bar_calls[call_idx].id]
315
+ else:
316
+ color_idx = call_idx
317
+ patch.set_facecolor(palette[color_idx % len(palette)])
318
+ else:
319
+ # No record, apply single color to all bars
320
+ color = palette[0]
321
+ for patch in rectangles:
322
+ patch.set_facecolor(color)
323
+
324
+
325
+ def _apply_colors_to_pie(
326
+ ax: Axes, palette: List[Any], ax_record: Optional[Any] = None
327
+ ) -> None:
328
+ """Apply colors to pie chart wedges.
329
+
330
+ If the pie call has custom colors in kwargs, those are used.
331
+ Otherwise, the theme palette is applied.
332
+ """
333
+ from matplotlib.patches import Wedge
334
+
335
+ wedges = [p for p in ax.patches if isinstance(p, Wedge)]
336
+ if not wedges:
337
+ return
338
+
339
+ # Check if pie call has custom colors
340
+ custom_colors = None
341
+ if ax_record:
342
+ for call in ax_record.calls:
343
+ if call.function == "pie" and "colors" in call.kwargs:
344
+ custom_colors = call.kwargs["colors"]
345
+ break
346
+
347
+ # Use custom colors if available, otherwise use palette
348
+ if custom_colors and isinstance(custom_colors, list):
349
+ # Normalize custom colors
350
+ colors_to_use = []
351
+ for c in custom_colors:
352
+ if isinstance(c, str):
353
+ colors_to_use.append(c)
354
+ elif isinstance(c, (list, tuple)) and len(c) >= 3:
355
+ if all(v <= 1.0 for v in c):
356
+ colors_to_use.append(tuple(c))
357
+ else:
358
+ colors_to_use.append(tuple(v / 255.0 for v in c))
359
+ else:
360
+ colors_to_use.append(c)
361
+ else:
362
+ colors_to_use = palette
363
+
364
+ for i, wedge in enumerate(wedges):
365
+ color = colors_to_use[i % len(colors_to_use)]
366
+ wedge.set_facecolor(color)
367
+ wedge.set_edgecolor("black")
368
+
369
+
370
+ def _apply_colors_to_scatter(
371
+ ax: Axes,
372
+ palette: List[Any],
373
+ ax_record: Optional[Any],
374
+ call_color_map: Dict[str, int],
375
+ ) -> None:
376
+ """Apply colors to scatter plot collections."""
377
+ from matplotlib.collections import PathCollection
378
+
379
+ scatter_collections = [c for c in ax.collections if isinstance(c, PathCollection)]
380
+ scatter_calls = [
381
+ c for c in (ax_record.calls if ax_record else []) if c.function == "scatter"
382
+ ]
383
+ for i, coll in enumerate(scatter_collections):
384
+ if i < len(scatter_calls) and scatter_calls[i].id in call_color_map:
385
+ color_idx = call_color_map[scatter_calls[i].id]
386
+ else:
387
+ color_idx = i
388
+ coll.set_facecolor(palette[color_idx % len(palette)])
389
+
390
+
391
+ def _apply_colors_to_poly(ax: Axes, palette: List[Any]) -> None:
392
+ """Apply colors to polygon collections (violin, fill)."""
393
+ from matplotlib.collections import PolyCollection
394
+
395
+ poly_collections = [c for c in ax.collections if isinstance(c, PolyCollection)]
396
+ for i, coll in enumerate(poly_collections):
397
+ color = palette[i % len(palette)]
398
+ coll.set_facecolor(color)
399
+
400
+
401
+ def _update_legend_colors(ax: Axes, palette: List[Any]) -> None:
402
+ """Update legend colors to reflect new palette."""
403
+ legend = ax.get_legend()
404
+ if legend is None:
405
+ return
406
+
407
+ handles = (
408
+ legend.legend_handles
409
+ if hasattr(legend, "legend_handles")
410
+ else legend.legendHandles
411
+ )
412
+ for i, handle in enumerate(handles):
413
+ if i < len(palette):
414
+ color = palette[i % len(palette)]
415
+ if hasattr(handle, "set_color"):
416
+ handle.set_color(color)
417
+ elif hasattr(handle, "set_facecolor"):
418
+ handle.set_facecolor(color)
419
+
420
+
421
+ def apply_dark_mode(fig: Figure) -> None:
422
+ """
423
+ Apply dark mode colors to figure.
424
+
425
+ Parameters
426
+ ----------
427
+ fig : Figure
428
+ Matplotlib figure.
429
+ """
430
+ # Dark theme colors
431
+ bg_color = "#1a1a1a"
432
+ text_color = "#e8e8e8"
433
+
434
+ # Update rcParams for dark mode (pie charts, panel labels)
435
+ import matplotlib as mpl
436
+
437
+ mpl.rcParams["text.color"] = text_color
438
+ mpl.rcParams["axes.labelcolor"] = text_color
439
+ mpl.rcParams["xtick.color"] = text_color
440
+ mpl.rcParams["ytick.color"] = text_color
441
+
442
+ # Figure background
443
+ fig.patch.set_facecolor(bg_color)
444
+
445
+ # Figure-level text elements (suptitle, supxlabel, supylabel)
446
+ if hasattr(fig, "_suptitle") and fig._suptitle is not None:
447
+ fig._suptitle.set_color(text_color)
448
+ if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
449
+ fig._supxlabel.set_color(text_color)
450
+ if hasattr(fig, "_supylabel") and fig._supylabel is not None:
451
+ fig._supylabel.set_color(text_color)
452
+
453
+ for ax in fig.get_axes():
454
+ _apply_dark_mode_to_axes(ax, bg_color, text_color)
455
+
456
+
457
+ def _apply_dark_mode_to_axes(ax: Axes, bg_color: str, text_color: str) -> None:
458
+ """Apply dark mode colors to a single axes."""
459
+ # Axes background
460
+ ax.set_facecolor(bg_color)
461
+
462
+ # Text colors
463
+ ax.xaxis.label.set_color(text_color)
464
+ ax.yaxis.label.set_color(text_color)
465
+ ax.title.set_color(text_color)
466
+
467
+ # Tick labels and tick marks
468
+ ax.tick_params(colors=text_color, which="both")
469
+
470
+ # Explicitly set tick label colors (for specgram and other plots)
471
+ for label in ax.get_xticklabels() + ax.get_yticklabels():
472
+ label.set_color(text_color)
473
+
474
+ # Spines
475
+ for spine in ax.spines.values():
476
+ spine.set_color(text_color)
477
+
478
+ # All text objects on axes (panel labels, pie labels, annotations)
479
+ for text in ax.texts:
480
+ text.set_color(text_color)
481
+
482
+ # Stat annotation bracket lines (Line2D with clip_on=False)
483
+ for line in ax.get_lines():
484
+ # Bracket lines have clip_on=False and are typically black
485
+ if not line.get_clip_on():
486
+ current_color = line.get_color()
487
+ # Only update if it's a dark color (black or near-black)
488
+ if current_color in ["black", "k", "#000000", (0, 0, 0), (0.0, 0.0, 0.0)]:
489
+ line.set_color(text_color)
490
+
491
+ # Legend
492
+ legend = ax.get_legend()
493
+ if legend is not None:
494
+ frame = legend.get_frame()
495
+ frame.set_facecolor(bg_color)
496
+ frame.set_edgecolor(text_color)
497
+ for text in legend.get_texts():
498
+ text.set_color(text_color)
499
+
500
+
501
+ __all__ = [
502
+ "apply_overrides",
503
+ "apply_color_palette",
504
+ "apply_dark_mode",
505
+ ]
506
+
507
+ # EOF