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,194 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Plot type detection utilities for hitmap generation."""
4
+
5
+ from typing import Any, Dict
6
+
7
+
8
+ def detect_plot_types(fig, debug: bool = False) -> Dict[int, Dict[str, Any]]:
9
+ """Detect plot types from recorded calls in figure.
10
+
11
+ Parameters
12
+ ----------
13
+ fig : Figure
14
+ The figure to analyze.
15
+ debug : bool
16
+ If True, print debug information.
17
+
18
+ Returns
19
+ -------
20
+ dict
21
+ Mapping from ax_index (matching fig.get_axes() order) to plot type info.
22
+ """
23
+ # Get figure record if available
24
+ if hasattr(fig, "record"):
25
+ record = fig.record
26
+ elif hasattr(fig, "fig") and hasattr(fig.fig, "_record"):
27
+ record = fig.fig._record
28
+ else:
29
+ if debug:
30
+ print("[detect_plot_types] No record found")
31
+ return {}
32
+
33
+ # Get the actual matplotlib figure and its axes
34
+ mpl_fig = fig.fig if hasattr(fig, "fig") else fig
35
+ axes_list = mpl_fig.get_axes()
36
+
37
+ result = {}
38
+
39
+ # Process each axes in the record
40
+ if hasattr(record, "axes"):
41
+ # Build mapping from ax_key to ax_record's plot info
42
+ ax_key_to_info = {}
43
+ for ax_key, ax_record in record.axes.items():
44
+ info = {"types": set(), "call_ids": {}}
45
+
46
+ if hasattr(ax_record, "calls"):
47
+ for call in ax_record.calls:
48
+ func_name = call.function
49
+ call_id = call.id
50
+
51
+ info["types"].add(func_name)
52
+
53
+ if func_name not in info["call_ids"]:
54
+ info["call_ids"][func_name] = []
55
+ info["call_ids"][func_name].append(call_id)
56
+
57
+ ax_key_to_info[ax_key] = info
58
+
59
+ # Map ax_keys to current axes positions using position matching
60
+ # This handles the case where panels have been dragged to new positions
61
+ ax_keys_sorted = sorted(record.axes.keys())
62
+
63
+ # Debug: Check which ax_keys have position_override
64
+ overrides = {
65
+ k: getattr(record.axes[k], "position_override", None)
66
+ for k in ax_keys_sorted
67
+ if hasattr(record.axes[k], "position_override")
68
+ and record.axes[k].position_override
69
+ }
70
+ if overrides:
71
+ print(f"[detect_plot_types] Position overrides: {overrides}")
72
+
73
+ for ax_idx, ax in enumerate(axes_list):
74
+ # Try to find the matching ax_record by comparing positions
75
+ # or fall back to index-based matching
76
+ matched = False
77
+ ax_pos = ax.get_position()
78
+
79
+ for ax_key in ax_keys_sorted:
80
+ ax_record = record.axes[ax_key]
81
+ # Check if there's a position_override that matches
82
+ # Must check ALL 4 coordinates to avoid false matches
83
+ if (
84
+ hasattr(ax_record, "position_override")
85
+ and ax_record.position_override
86
+ ):
87
+ rec_pos = ax_record.position_override
88
+ # Position override is [x0, y0, width, height]
89
+ if len(rec_pos) >= 4:
90
+ if (
91
+ abs(rec_pos[0] - ax_pos.x0) < 0.01
92
+ and abs(rec_pos[1] - ax_pos.y0) < 0.01
93
+ and abs(rec_pos[2] - ax_pos.width) < 0.01
94
+ and abs(rec_pos[3] - ax_pos.height) < 0.01
95
+ ):
96
+ print(
97
+ f"[detect_plot_types] ax_idx={ax_idx} matched {ax_key} "
98
+ f"via position_override"
99
+ )
100
+ result[ax_idx] = ax_key_to_info.get(
101
+ ax_key, {"types": set(), "call_ids": {}}
102
+ )
103
+ matched = True
104
+ break
105
+ else:
106
+ # Fallback for old format with only x0, y0
107
+ if (
108
+ abs(rec_pos[0] - ax_pos.x0) < 0.01
109
+ and abs(rec_pos[1] - ax_pos.y0) < 0.01
110
+ ):
111
+ result[ax_idx] = ax_key_to_info.get(
112
+ ax_key, {"types": set(), "call_ids": {}}
113
+ )
114
+ matched = True
115
+ break
116
+
117
+ # Fall back to index-based matching if position match failed
118
+ if not matched and ax_idx < len(ax_keys_sorted):
119
+ ax_key = ax_keys_sorted[ax_idx]
120
+ info = ax_key_to_info.get(ax_key, {"types": set(), "call_ids": {}})
121
+ print(
122
+ f"[detect_plot_types] ax_idx={ax_idx} fallback to {ax_key}, "
123
+ f"types={info.get('types', set())}"
124
+ )
125
+ result[ax_idx] = info
126
+
127
+ return result
128
+
129
+
130
+ def is_boxplot_element(line, ax) -> bool:
131
+ """Check if a line element belongs to a boxplot.
132
+
133
+ Parameters
134
+ ----------
135
+ line : Line2D
136
+ The line to check.
137
+ ax : Axes
138
+ The axes containing the line.
139
+
140
+ Returns
141
+ -------
142
+ bool
143
+ True if line is a boxplot element.
144
+ """
145
+ label = line.get_label() or ""
146
+
147
+ # Boxplot whisker/median lines have specific patterns
148
+ if label.startswith("_line"):
149
+ return True
150
+
151
+ # Check if line is horizontal (median) or vertical (whisker)
152
+ xdata = line.get_xdata()
153
+ ydata = line.get_ydata()
154
+
155
+ if len(xdata) == 2 and len(ydata) == 2:
156
+ # Horizontal or vertical line segments
157
+ is_horizontal = ydata[0] == ydata[1]
158
+ is_vertical = xdata[0] == xdata[1]
159
+ if is_horizontal or is_vertical:
160
+ return True
161
+
162
+ return False
163
+
164
+
165
+ def is_violin_element(coll, ax) -> bool:
166
+ """Check if a collection element belongs to a violin plot.
167
+
168
+ Parameters
169
+ ----------
170
+ coll : Collection
171
+ The collection to check.
172
+ ax : Axes
173
+ The axes containing the collection.
174
+
175
+ Returns
176
+ -------
177
+ bool
178
+ True if collection is a violin element.
179
+ """
180
+ from matplotlib.collections import PolyCollection
181
+
182
+ if isinstance(coll, PolyCollection):
183
+ # Violin bodies are PolyCollections
184
+ return True
185
+ return False
186
+
187
+
188
+ __all__ = [
189
+ "detect_plot_types",
190
+ "is_boxplot_element",
191
+ "is_violin_element",
192
+ ]
193
+
194
+ # EOF
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Property restoration for hitmap generation."""
4
+
5
+ from typing import Any, Dict, List
6
+
7
+ from matplotlib.collections import (
8
+ LineCollection,
9
+ PathCollection,
10
+ PolyCollection,
11
+ )
12
+
13
+
14
+ def restore_axes_properties(
15
+ axes_list: List,
16
+ original_props: Dict[str, Any],
17
+ include_text: bool = True,
18
+ ) -> None:
19
+ """Restore original properties to axes elements.
20
+
21
+ Parameters
22
+ ----------
23
+ axes_list : list
24
+ List of axes to restore.
25
+ original_props : dict
26
+ Dictionary of original properties keyed by element key.
27
+ include_text : bool
28
+ Whether text elements were modified.
29
+ """
30
+ for ax_idx, ax in enumerate(axes_list):
31
+ # Restore lines
32
+ for i, line in enumerate(ax.get_lines()):
33
+ key = f"ax{ax_idx}_line{i}"
34
+ if key in original_props:
35
+ props = original_props[key]
36
+ line.set_color(props["color"])
37
+ line.set_markerfacecolor(props["markerfacecolor"])
38
+ line.set_markeredgecolor(props["markeredgecolor"])
39
+
40
+ # Restore collections
41
+ for i, coll in enumerate(ax.collections):
42
+ if isinstance(coll, PathCollection):
43
+ key = f"ax{ax_idx}_scatter{i}"
44
+ if key in original_props:
45
+ props = original_props[key]
46
+ coll.set_facecolors(props["facecolors"])
47
+ coll.set_edgecolors(props["edgecolors"])
48
+ elif isinstance(coll, PolyCollection):
49
+ key = f"ax{ax_idx}_fill{i}"
50
+ if key in original_props:
51
+ props = original_props[key]
52
+ coll.set_facecolors(props["facecolors"])
53
+ coll.set_edgecolors(props["edgecolors"])
54
+ elif isinstance(coll, LineCollection):
55
+ key = f"ax{ax_idx}_linecoll{i}"
56
+ if key in original_props:
57
+ props = original_props[key]
58
+ if len(props["colors"]) > 0:
59
+ coll.set_color(props["colors"])
60
+
61
+ # Restore patches
62
+ for i, patch in enumerate(ax.patches):
63
+ key = f"ax{ax_idx}_bar{i}"
64
+ if key in original_props:
65
+ props = original_props[key]
66
+ patch.set_facecolor(props["facecolor"])
67
+ patch.set_edgecolor(props["edgecolor"])
68
+
69
+ # Restore text
70
+ if include_text:
71
+ key = f"ax{ax_idx}_title"
72
+ if key in original_props:
73
+ ax.title.set_color(original_props[key]["color"])
74
+
75
+ key = f"ax{ax_idx}_xlabel"
76
+ if key in original_props:
77
+ ax.xaxis.label.set_color(original_props[key]["color"])
78
+
79
+ key = f"ax{ax_idx}_ylabel"
80
+ if key in original_props:
81
+ ax.yaxis.label.set_color(original_props[key]["color"])
82
+
83
+ # Restore legend
84
+ key = f"ax{ax_idx}_legend"
85
+ if key in original_props:
86
+ legend = ax.get_legend()
87
+ if legend:
88
+ frame = legend.get_frame()
89
+ props = original_props[key]
90
+ frame.set_facecolor(props["facecolor"])
91
+ frame.set_edgecolor(props["edgecolor"])
92
+
93
+ # Restore spines
94
+ for spine in ax.spines.values():
95
+ spine.set_color("black")
96
+
97
+ # Restore tick colors
98
+ ax.tick_params(colors="black")
99
+
100
+
101
+ def restore_figure_text(
102
+ mpl_fig,
103
+ original_props: Dict[str, Any],
104
+ include_text: bool = True,
105
+ ) -> None:
106
+ """Restore figure-level text properties.
107
+
108
+ Parameters
109
+ ----------
110
+ mpl_fig : Figure
111
+ The matplotlib figure.
112
+ original_props : dict
113
+ Dictionary of original properties.
114
+ include_text : bool
115
+ Whether text elements were modified.
116
+ """
117
+ if not include_text:
118
+ return
119
+
120
+ key = "fig_suptitle"
121
+ if key in original_props and hasattr(mpl_fig, "_suptitle") and mpl_fig._suptitle:
122
+ mpl_fig._suptitle.set_color(original_props[key]["color"])
123
+
124
+ key = "fig_supxlabel"
125
+ if key in original_props and hasattr(mpl_fig, "_supxlabel") and mpl_fig._supxlabel:
126
+ mpl_fig._supxlabel.set_color(original_props[key]["color"])
127
+
128
+ key = "fig_supylabel"
129
+ if key in original_props and hasattr(mpl_fig, "_supylabel") and mpl_fig._supylabel:
130
+ mpl_fig._supylabel.set_color(original_props[key]["color"])
131
+
132
+
133
+ def restore_backgrounds(fig, axes_list: List) -> None:
134
+ """Restore background colors.
135
+
136
+ Parameters
137
+ ----------
138
+ fig : Figure
139
+ The figure.
140
+ axes_list : list
141
+ List of axes.
142
+ """
143
+ fig.patch.set_facecolor("white")
144
+ for ax in axes_list:
145
+ ax.set_facecolor("white")
146
+
147
+
148
+ __all__ = [
149
+ "restore_axes_properties",
150
+ "restore_figure_text",
151
+ "restore_backgrounds",
152
+ ]
153
+
154
+ # EOF
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Hitmap generation for interactive element selection.
5
+
6
+ This module generates color-coded images where each figure element
7
+ (line, scatter, bar, text, etc.) is rendered with a unique RGB color.
8
+ This enables precise pixel-based element detection when users click
9
+ on the figure preview.
10
+
11
+ The color encoding uses 24-bit RGB:
12
+ - First 12 elements: hand-picked visually distinct colors
13
+ - Elements 13+: HSV-based generation for deterministic uniqueness
14
+ """
15
+
16
+ import io
17
+ from typing import Any, Dict, Tuple
18
+
19
+ from matplotlib.figure import Figure
20
+ from PIL import Image
21
+
22
+
23
+ def generate_hitmap(
24
+ fig: Figure,
25
+ dpi: int = 150,
26
+ include_text: bool = True,
27
+ ) -> Tuple[Image.Image, Dict[str, Any]]:
28
+ """
29
+ Generate hitmap with unique colors per element.
30
+
31
+ Parameters
32
+ ----------
33
+ fig : matplotlib.figure.Figure
34
+ Figure to generate hitmap for.
35
+ dpi : int, optional
36
+ Resolution for hitmap rendering (default: 150).
37
+ include_text : bool, optional
38
+ Whether to include text elements like labels (default: True).
39
+
40
+ Returns
41
+ -------
42
+ hitmap : PIL.Image.Image
43
+ RGB image where each element has unique color.
44
+ color_map : dict
45
+ Mapping from element key to metadata:
46
+ {
47
+ 'element_key': {
48
+ 'id': int,
49
+ 'type': str, # 'line', 'scatter', 'bar', 'boxplot', 'violin', etc.
50
+ 'label': str,
51
+ 'ax_index': int,
52
+ 'rgb': [r, g, b],
53
+ }
54
+ }
55
+ """
56
+ # Import from helper modules (inside function to avoid circular imports)
57
+ from ._hitmap._artists import (
58
+ process_collections,
59
+ process_figure_text,
60
+ process_images,
61
+ process_legend,
62
+ process_lines,
63
+ process_patches,
64
+ process_text,
65
+ )
66
+ from ._hitmap._colors import (
67
+ AXES_COLOR,
68
+ BACKGROUND_COLOR,
69
+ normalize_color,
70
+ )
71
+ from ._hitmap._detect import detect_plot_types
72
+ from ._hitmap._restore import (
73
+ restore_axes_properties,
74
+ restore_backgrounds,
75
+ restore_figure_text,
76
+ )
77
+
78
+ # Store original properties for restoration
79
+ original_props = {}
80
+ color_map = {}
81
+ element_id = 1
82
+
83
+ # Detect plot types from record
84
+ plot_types = detect_plot_types(fig, debug=False)
85
+
86
+ # Get all axes (handle RecordingFigure wrapper)
87
+ if hasattr(fig, "fig"):
88
+ mpl_fig = fig.fig
89
+ else:
90
+ mpl_fig = fig
91
+ axes_list = mpl_fig.get_axes()
92
+
93
+ # Process all artists and assign colors
94
+ for ax_idx, ax in enumerate(axes_list):
95
+ ax_info = plot_types.get(ax_idx, {"types": set(), "call_ids": {}})
96
+
97
+ # Process lines
98
+ element_id = process_lines(
99
+ ax, ax_idx, element_id, original_props, color_map, ax_info
100
+ )
101
+
102
+ # Process collections (scatter, fills, etc.)
103
+ element_id = process_collections(
104
+ ax, ax_idx, element_id, original_props, color_map, ax_info
105
+ )
106
+
107
+ # Process patches (bars, wedges, polygons)
108
+ element_id = process_patches(
109
+ ax, ax_idx, element_id, original_props, color_map, ax_info
110
+ )
111
+
112
+ # Process images
113
+ element_id = process_images(ax, ax_idx, element_id, color_map, ax_info)
114
+
115
+ # Process text elements
116
+ if include_text:
117
+ element_id = process_text(ax, ax_idx, element_id, original_props, color_map)
118
+
119
+ # Process legend
120
+ element_id = process_legend(ax, ax_idx, element_id, original_props, color_map)
121
+
122
+ # Process figure-level text elements
123
+ if include_text:
124
+ element_id = process_figure_text(mpl_fig, element_id, original_props, color_map)
125
+
126
+ # Set non-selectable elements to axes color
127
+ for ax in axes_list:
128
+ for spine in ax.spines.values():
129
+ spine.set_color(normalize_color(AXES_COLOR))
130
+ ax.tick_params(colors=normalize_color(AXES_COLOR))
131
+
132
+ # Set figure background
133
+ fig.patch.set_facecolor(normalize_color(BACKGROUND_COLOR))
134
+ for ax in axes_list:
135
+ ax.set_facecolor(normalize_color(BACKGROUND_COLOR))
136
+
137
+ # Render to buffer
138
+ # IMPORTANT: Do NOT use bbox_inches="tight" - it causes dimension changes
139
+ # between renders when elements change (e.g., color). Must match main render.
140
+ buf = io.BytesIO()
141
+ fig.savefig(buf, format="png", dpi=dpi, facecolor=fig.get_facecolor())
142
+ buf.seek(0)
143
+
144
+ # Load as PIL Image
145
+ hitmap = Image.open(buf).convert("RGB")
146
+
147
+ # Restore original properties
148
+ restore_axes_properties(axes_list, original_props, include_text)
149
+ restore_figure_text(mpl_fig, original_props, include_text)
150
+ restore_backgrounds(fig, axes_list)
151
+
152
+ return hitmap, color_map
153
+
154
+
155
+ def hitmap_to_base64(hitmap: Image.Image) -> str:
156
+ """
157
+ Convert hitmap image to base64 string.
158
+
159
+ Parameters
160
+ ----------
161
+ hitmap : PIL.Image.Image
162
+ Hitmap image.
163
+
164
+ Returns
165
+ -------
166
+ str
167
+ Base64-encoded PNG string.
168
+ """
169
+ import base64
170
+
171
+ buf = io.BytesIO()
172
+ hitmap.save(buf, format="PNG")
173
+ buf.seek(0)
174
+ return base64.b64encode(buf.read()).decode("utf-8")
175
+
176
+
177
+ __all__ = [
178
+ "generate_hitmap",
179
+ "hitmap_to_base64",
180
+ ]
181
+
182
+ # EOF
@@ -58,12 +58,15 @@ class StyleOverrides:
58
58
  Returns
59
59
  -------
60
60
  dict
61
- Merged style dictionary.
61
+ Merged style dictionary including call_overrides.
62
62
  """
63
63
  result = {}
64
64
  result.update(self.base_style)
65
65
  result.update(self.programmatic_style)
66
66
  result.update(self.manual_overrides)
67
+ # Include call_overrides for apply_overrides to use
68
+ if self.call_overrides:
69
+ result["call_overrides"] = self.call_overrides
67
70
  return result
68
71
 
69
72
  def get_original_style(self) -> Dict[str, Any]: