figrecipe 0.6.0__py3-none-any.whl → 0.7.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. figrecipe/__init__.py +106 -973
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,480 @@
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
+ - etc.
27
+ record : FigureRecord, optional
28
+ Recording record to access call IDs for grouping elements.
29
+ """
30
+ from ..styles._style_applier import apply_style_mm
31
+
32
+ axes_list = fig.get_axes()
33
+
34
+ for ax in axes_list:
35
+ # Apply mm-based styling
36
+ apply_style_mm(ax, overrides)
37
+
38
+ # Apply specific overrides that aren't handled by apply_style_mm
39
+ _apply_font_overrides(ax, overrides)
40
+ _apply_tick_overrides(ax, overrides)
41
+ _apply_behavior_overrides(ax, overrides)
42
+ _apply_legend_overrides(ax, overrides)
43
+ _apply_line_overrides(ax, overrides)
44
+ _apply_marker_overrides(ax, overrides)
45
+
46
+ # Apply color palette to existing elements
47
+ color_palette = overrides.get("color_palette")
48
+ if color_palette is not None:
49
+ ax_record = _find_ax_record(ax, axes_list, record)
50
+ apply_color_palette(ax, color_palette, ax_record)
51
+
52
+
53
+ def _apply_font_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
54
+ """Apply font-related overrides to axes."""
55
+ # Font sizes (YAML: fonts_axis_label_pt, legacy: axis_font_size_pt)
56
+ axis_fs = overrides.get("fonts_axis_label_pt", overrides.get("axis_font_size_pt"))
57
+ if axis_fs is not None:
58
+ ax.xaxis.label.set_fontsize(axis_fs)
59
+ ax.yaxis.label.set_fontsize(axis_fs)
60
+
61
+ tick_fs = overrides.get("fonts_tick_label_pt", overrides.get("tick_font_size_pt"))
62
+ if tick_fs is not None:
63
+ ax.tick_params(labelsize=tick_fs)
64
+
65
+ title_fs = overrides.get("fonts_title_pt", overrides.get("title_font_size_pt"))
66
+ if title_fs is not None:
67
+ ax.title.set_fontsize(title_fs)
68
+
69
+ family = overrides.get("fonts_family", overrides.get("font_family"))
70
+ if family is not None:
71
+ ax.xaxis.label.set_fontfamily(family)
72
+ ax.yaxis.label.set_fontfamily(family)
73
+ ax.title.set_fontfamily(family)
74
+ for label in ax.get_xticklabels() + ax.get_yticklabels():
75
+ label.set_fontfamily(family)
76
+
77
+
78
+ def _apply_tick_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
79
+ """Apply tick-related overrides to axes."""
80
+ # Ticks (YAML: ticks_direction, legacy: tick_direction)
81
+ tick_dir = overrides.get("ticks_direction", overrides.get("tick_direction"))
82
+ if tick_dir is not None and tick_dir in ("in", "out", "inout"):
83
+ ax.tick_params(direction=tick_dir)
84
+
85
+ tick_len = overrides.get("ticks_length_mm", overrides.get("tick_length_mm"))
86
+ if tick_len is not None:
87
+ from .._utils._units import mm_to_pt
88
+
89
+ length = mm_to_pt(tick_len)
90
+ ax.tick_params(length=length)
91
+
92
+
93
+ def _apply_behavior_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
94
+ """Apply behavior-related overrides (grid, spines)."""
95
+ # Grid (YAML: behavior_grid, legacy: grid)
96
+ grid_value = overrides.get("behavior_grid", overrides.get("grid"))
97
+ if grid_value is not None:
98
+ if grid_value:
99
+ ax.grid(True, alpha=0.3)
100
+ else:
101
+ ax.grid(False)
102
+
103
+ # Spines (YAML: behavior_hide_top_spine, legacy: hide_top_spine)
104
+ hide_top = overrides.get("behavior_hide_top_spine", overrides.get("hide_top_spine"))
105
+ if hide_top is not None:
106
+ ax.spines["top"].set_visible(not hide_top)
107
+
108
+ hide_right = overrides.get(
109
+ "behavior_hide_right_spine", overrides.get("hide_right_spine")
110
+ )
111
+ if hide_right is not None:
112
+ ax.spines["right"].set_visible(not hide_right)
113
+
114
+
115
+ def _apply_legend_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
116
+ """Apply legend-related overrides."""
117
+ legend = ax.get_legend()
118
+ if legend is not None:
119
+ if "legend_frameon" in overrides:
120
+ legend.set_frame_on(overrides["legend_frameon"])
121
+
122
+ if "legend_alpha" in overrides:
123
+ frame = legend.get_frame()
124
+ fc = frame.get_facecolor()
125
+ frame.set_facecolor((*fc[:3], overrides["legend_alpha"]))
126
+
127
+
128
+ def _apply_line_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
129
+ """Apply line-related overrides."""
130
+ # Line widths (YAML: lines_trace_mm, legacy: trace_thickness_mm)
131
+ trace_mm = overrides.get("lines_trace_mm", overrides.get("trace_thickness_mm"))
132
+ if trace_mm is not None:
133
+ from .._utils._units import mm_to_pt
134
+
135
+ lw = mm_to_pt(trace_mm)
136
+ for line in ax.get_lines():
137
+ line.set_linewidth(lw)
138
+
139
+
140
+ def _apply_marker_overrides(ax: Axes, overrides: Dict[str, Any]) -> None:
141
+ """Apply marker-related overrides."""
142
+ # Marker sizes (YAML: markers_scatter_mm, legacy: marker_size_mm)
143
+ # Only apply to PathCollection (scatter), not PolyCollection (violin/fill)
144
+ scatter_mm = overrides.get(
145
+ "markers_scatter_mm",
146
+ overrides.get("markers_size_mm", overrides.get("marker_size_mm")),
147
+ )
148
+ if scatter_mm is not None:
149
+ from matplotlib.collections import PathCollection
150
+
151
+ from .._utils._units import mm_to_scatter_size
152
+
153
+ size = mm_to_scatter_size(scatter_mm)
154
+ for coll in ax.collections:
155
+ # Only apply to scatter plots (PathCollection), not violin/fill (PolyCollection)
156
+ if isinstance(coll, PathCollection):
157
+ try:
158
+ coll.set_sizes([size])
159
+ except Exception:
160
+ pass
161
+
162
+
163
+ def _find_ax_record(ax: Axes, axes_list: List[Axes], record: Optional[Any]) -> Any:
164
+ """Find the AxesRecord for a given axes."""
165
+ if record is None or not hasattr(record, "axes"):
166
+ return None
167
+
168
+ # Find ax position in the figure's axes list
169
+ ax_idx = axes_list.index(ax)
170
+ # AxesRecord keys are position tuples like "(0, 0)", "(0, 1)", etc.
171
+ # Try to match by index order
172
+ ax_keys = sorted(record.axes.keys())
173
+ if ax_idx < len(ax_keys):
174
+ return record.axes.get(ax_keys[ax_idx])
175
+ return None
176
+
177
+
178
+ def apply_color_palette(
179
+ ax: Axes, color_palette: List[Any], ax_record: Optional[Any] = None
180
+ ) -> None:
181
+ """
182
+ Apply color palette to existing plot elements, grouping by call_id.
183
+
184
+ Parameters
185
+ ----------
186
+ ax : Axes
187
+ Matplotlib axes containing plot elements.
188
+ color_palette : list
189
+ List of colors (RGB tuples or color names).
190
+ ax_record : AxesRecord, optional
191
+ Record of calls for this axes (for grouping elements by call_id).
192
+ """
193
+
194
+ # Normalize colors (RGB 0-255 to 0-1)
195
+ normalized_palette = _normalize_color_palette(color_palette)
196
+ if not normalized_palette:
197
+ return
198
+
199
+ # Build call_id to color index mapping from record
200
+ call_color_map = _build_call_color_map(ax_record)
201
+
202
+ # Apply to different element types
203
+ _apply_colors_to_lines(ax, normalized_palette, ax_record, call_color_map)
204
+ _apply_colors_to_bars(ax, normalized_palette, ax_record, call_color_map)
205
+ _apply_colors_to_pie(ax, normalized_palette, ax_record)
206
+ _apply_colors_to_scatter(ax, normalized_palette, ax_record, call_color_map)
207
+ _apply_colors_to_poly(ax, normalized_palette)
208
+ _update_legend_colors(ax, normalized_palette)
209
+
210
+
211
+ def _normalize_color_palette(color_palette: List[Any]) -> List[Any]:
212
+ """Normalize color palette to 0-1 range."""
213
+ normalized = []
214
+ for c in color_palette:
215
+ if isinstance(c, (list, tuple)) and len(c) >= 3:
216
+ if all(v <= 1.0 for v in c):
217
+ normalized.append(tuple(c))
218
+ else:
219
+ normalized.append(tuple(v / 255.0 for v in c))
220
+ else:
221
+ normalized.append(c)
222
+ return normalized
223
+
224
+
225
+ def _build_call_color_map(ax_record: Optional[Any]) -> Dict[str, int]:
226
+ """Build mapping from call_id to color index."""
227
+ call_color_map = {}
228
+ if ax_record:
229
+ color_idx = 0
230
+ for call in ax_record.calls:
231
+ if call.function in (
232
+ "plot",
233
+ "scatter",
234
+ "bar",
235
+ "barh",
236
+ "hist",
237
+ "pie",
238
+ "fill",
239
+ "fill_between",
240
+ "fill_betweenx",
241
+ ):
242
+ if call.id not in call_color_map:
243
+ call_color_map[call.id] = color_idx
244
+ color_idx += 1
245
+ return call_color_map
246
+
247
+
248
+ def _apply_colors_to_lines(
249
+ ax: Axes,
250
+ palette: List[Any],
251
+ ax_record: Optional[Any],
252
+ call_color_map: Dict[str, int],
253
+ ) -> None:
254
+ """Apply colors to line elements."""
255
+ lines = ax.get_lines()
256
+ line_calls = [
257
+ c for c in (ax_record.calls if ax_record else []) if c.function == "plot"
258
+ ]
259
+ for i, line in enumerate(lines):
260
+ # Skip internal lines (boxplot whiskers, etc.)
261
+ label = line.get_label()
262
+ if label.startswith("_"):
263
+ continue
264
+ if i < len(line_calls) and line_calls[i].id in call_color_map:
265
+ color_idx = call_color_map[line_calls[i].id]
266
+ else:
267
+ color_idx = i
268
+ line.set_color(palette[color_idx % len(palette)])
269
+
270
+
271
+ def _apply_colors_to_bars(
272
+ ax: Axes,
273
+ palette: List[Any],
274
+ ax_record: Optional[Any],
275
+ call_color_map: Dict[str, int],
276
+ ) -> None:
277
+ """Apply colors to bar and histogram elements."""
278
+ from matplotlib.patches import Rectangle
279
+
280
+ rectangles = [p for p in ax.patches if isinstance(p, Rectangle)]
281
+ if not rectangles:
282
+ return
283
+
284
+ bar_calls = [
285
+ c
286
+ for c in (ax_record.calls if ax_record else [])
287
+ if c.function in ("bar", "barh", "hist")
288
+ ]
289
+ if bar_calls:
290
+ rect_per_call = len(rectangles) // len(bar_calls) if bar_calls else 1
291
+ for i, patch in enumerate(rectangles):
292
+ call_idx = (
293
+ min(i // rect_per_call, len(bar_calls) - 1) if rect_per_call > 0 else 0
294
+ )
295
+ if call_idx < len(bar_calls) and bar_calls[call_idx].id in call_color_map:
296
+ color_idx = call_color_map[bar_calls[call_idx].id]
297
+ else:
298
+ color_idx = call_idx
299
+ patch.set_facecolor(palette[color_idx % len(palette)])
300
+ else:
301
+ # No record, apply single color to all bars
302
+ color = palette[0]
303
+ for patch in rectangles:
304
+ patch.set_facecolor(color)
305
+
306
+
307
+ def _apply_colors_to_pie(
308
+ ax: Axes, palette: List[Any], ax_record: Optional[Any] = None
309
+ ) -> None:
310
+ """Apply colors to pie chart wedges.
311
+
312
+ If the pie call has custom colors in kwargs, those are used.
313
+ Otherwise, the theme palette is applied.
314
+ """
315
+ from matplotlib.patches import Wedge
316
+
317
+ wedges = [p for p in ax.patches if isinstance(p, Wedge)]
318
+ if not wedges:
319
+ return
320
+
321
+ # Check if pie call has custom colors
322
+ custom_colors = None
323
+ if ax_record:
324
+ for call in ax_record.calls:
325
+ if call.function == "pie" and "colors" in call.kwargs:
326
+ custom_colors = call.kwargs["colors"]
327
+ break
328
+
329
+ # Use custom colors if available, otherwise use palette
330
+ if custom_colors and isinstance(custom_colors, list):
331
+ # Normalize custom colors
332
+ colors_to_use = []
333
+ for c in custom_colors:
334
+ if isinstance(c, str):
335
+ colors_to_use.append(c)
336
+ elif isinstance(c, (list, tuple)) and len(c) >= 3:
337
+ if all(v <= 1.0 for v in c):
338
+ colors_to_use.append(tuple(c))
339
+ else:
340
+ colors_to_use.append(tuple(v / 255.0 for v in c))
341
+ else:
342
+ colors_to_use.append(c)
343
+ else:
344
+ colors_to_use = palette
345
+
346
+ for i, wedge in enumerate(wedges):
347
+ color = colors_to_use[i % len(colors_to_use)]
348
+ wedge.set_facecolor(color)
349
+ wedge.set_edgecolor("black")
350
+
351
+
352
+ def _apply_colors_to_scatter(
353
+ ax: Axes,
354
+ palette: List[Any],
355
+ ax_record: Optional[Any],
356
+ call_color_map: Dict[str, int],
357
+ ) -> None:
358
+ """Apply colors to scatter plot collections."""
359
+ from matplotlib.collections import PathCollection
360
+
361
+ scatter_collections = [c for c in ax.collections if isinstance(c, PathCollection)]
362
+ scatter_calls = [
363
+ c for c in (ax_record.calls if ax_record else []) if c.function == "scatter"
364
+ ]
365
+ for i, coll in enumerate(scatter_collections):
366
+ if i < len(scatter_calls) and scatter_calls[i].id in call_color_map:
367
+ color_idx = call_color_map[scatter_calls[i].id]
368
+ else:
369
+ color_idx = i
370
+ coll.set_facecolor(palette[color_idx % len(palette)])
371
+
372
+
373
+ def _apply_colors_to_poly(ax: Axes, palette: List[Any]) -> None:
374
+ """Apply colors to polygon collections (violin, fill)."""
375
+ from matplotlib.collections import PolyCollection
376
+
377
+ poly_collections = [c for c in ax.collections if isinstance(c, PolyCollection)]
378
+ for i, coll in enumerate(poly_collections):
379
+ color = palette[i % len(palette)]
380
+ coll.set_facecolor(color)
381
+
382
+
383
+ def _update_legend_colors(ax: Axes, palette: List[Any]) -> None:
384
+ """Update legend colors to reflect new palette."""
385
+ legend = ax.get_legend()
386
+ if legend is None:
387
+ return
388
+
389
+ handles = (
390
+ legend.legend_handles
391
+ if hasattr(legend, "legend_handles")
392
+ else legend.legendHandles
393
+ )
394
+ for i, handle in enumerate(handles):
395
+ if i < len(palette):
396
+ color = palette[i % len(palette)]
397
+ if hasattr(handle, "set_color"):
398
+ handle.set_color(color)
399
+ elif hasattr(handle, "set_facecolor"):
400
+ handle.set_facecolor(color)
401
+
402
+
403
+ def apply_dark_mode(fig: Figure) -> None:
404
+ """
405
+ Apply dark mode colors to figure.
406
+
407
+ Parameters
408
+ ----------
409
+ fig : Figure
410
+ Matplotlib figure.
411
+ """
412
+ # Dark theme colors
413
+ bg_color = "#1a1a1a"
414
+ text_color = "#e8e8e8"
415
+
416
+ # Update rcParams for dark mode (pie charts, panel labels)
417
+ import matplotlib as mpl
418
+
419
+ mpl.rcParams["text.color"] = text_color
420
+ mpl.rcParams["axes.labelcolor"] = text_color
421
+ mpl.rcParams["xtick.color"] = text_color
422
+ mpl.rcParams["ytick.color"] = text_color
423
+
424
+ # Figure background
425
+ fig.patch.set_facecolor(bg_color)
426
+
427
+ # Figure-level text elements (suptitle, supxlabel, supylabel)
428
+ if hasattr(fig, "_suptitle") and fig._suptitle is not None:
429
+ fig._suptitle.set_color(text_color)
430
+ if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
431
+ fig._supxlabel.set_color(text_color)
432
+ if hasattr(fig, "_supylabel") and fig._supylabel is not None:
433
+ fig._supylabel.set_color(text_color)
434
+
435
+ for ax in fig.get_axes():
436
+ _apply_dark_mode_to_axes(ax, bg_color, text_color)
437
+
438
+
439
+ def _apply_dark_mode_to_axes(ax: Axes, bg_color: str, text_color: str) -> None:
440
+ """Apply dark mode colors to a single axes."""
441
+ # Axes background
442
+ ax.set_facecolor(bg_color)
443
+
444
+ # Text colors
445
+ ax.xaxis.label.set_color(text_color)
446
+ ax.yaxis.label.set_color(text_color)
447
+ ax.title.set_color(text_color)
448
+
449
+ # Tick labels and tick marks
450
+ ax.tick_params(colors=text_color, which="both")
451
+
452
+ # Explicitly set tick label colors (for specgram and other plots)
453
+ for label in ax.get_xticklabels() + ax.get_yticklabels():
454
+ label.set_color(text_color)
455
+
456
+ # Spines
457
+ for spine in ax.spines.values():
458
+ spine.set_color(text_color)
459
+
460
+ # All text objects on axes (panel labels, pie labels, annotations)
461
+ for text in ax.texts:
462
+ text.set_color(text_color)
463
+
464
+ # Legend
465
+ legend = ax.get_legend()
466
+ if legend is not None:
467
+ frame = legend.get_frame()
468
+ frame.set_facecolor(bg_color)
469
+ frame.set_edgecolor(text_color)
470
+ for text in legend.get_texts():
471
+ text.set_color(text_color)
472
+
473
+
474
+ __all__ = [
475
+ "apply_overrides",
476
+ "apply_color_palette",
477
+ "apply_dark_mode",
478
+ ]
479
+
480
+ # EOF