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,251 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Helper functions for the figure editor.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional
8
+
9
+
10
+ def get_form_values_from_style(style: Dict[str, Any]) -> Dict[str, Any]:
11
+ """Extract form field values from a style dictionary.
12
+
13
+ Maps style dictionary values to HTML form input IDs.
14
+
15
+ Parameters
16
+ ----------
17
+ style : dict
18
+ Style configuration dictionary
19
+
20
+ Returns
21
+ -------
22
+ dict
23
+ Mapping of form input IDs to values
24
+ """
25
+ values = {}
26
+
27
+ # Axes dimensions
28
+ if "axes" in style:
29
+ values["axes_width_mm"] = style["axes"].get("width_mm", 80)
30
+ values["axes_height_mm"] = style["axes"].get("height_mm", 55)
31
+ values["axes_thickness_mm"] = style["axes"].get("thickness_mm", 0.2)
32
+
33
+ # Margins
34
+ if "margins" in style:
35
+ values["margins_left_mm"] = style["margins"].get("left_mm", 12)
36
+ values["margins_right_mm"] = style["margins"].get("right_mm", 3)
37
+ values["margins_bottom_mm"] = style["margins"].get("bottom_mm", 10)
38
+ values["margins_top_mm"] = style["margins"].get("top_mm", 3)
39
+
40
+ # Spacing
41
+ if "spacing" in style:
42
+ values["spacing_horizontal_mm"] = style["spacing"].get("horizontal_mm", 8)
43
+ values["spacing_vertical_mm"] = style["spacing"].get("vertical_mm", 8)
44
+
45
+ # Fonts
46
+ if "fonts" in style:
47
+ values["fonts_family"] = style["fonts"].get("family", "Arial")
48
+ values["fonts_axis_label_pt"] = style["fonts"].get("axis_label_pt", 7)
49
+ values["fonts_tick_label_pt"] = style["fonts"].get("tick_label_pt", 6)
50
+ values["fonts_title_pt"] = style["fonts"].get("title_pt", 8)
51
+ values["fonts_legend_pt"] = style["fonts"].get("legend_pt", 6)
52
+
53
+ # Ticks
54
+ if "ticks" in style:
55
+ values["ticks_length_mm"] = style["ticks"].get("length_mm", 1.0)
56
+ values["ticks_thickness_mm"] = style["ticks"].get("thickness_mm", 0.2)
57
+ values["ticks_direction"] = style["ticks"].get("direction", "out")
58
+
59
+ # Lines
60
+ if "lines" in style:
61
+ values["lines_trace_mm"] = style["lines"].get("trace_mm", 0.2)
62
+
63
+ # Markers
64
+ if "markers" in style:
65
+ values["markers_size_mm"] = style["markers"].get("size_mm", 0.8)
66
+
67
+ # Output
68
+ if "output" in style:
69
+ values["output_dpi"] = style["output"].get("dpi", 300)
70
+
71
+ # Behavior - spine visibility for all four sides
72
+ if "behavior" in style:
73
+ values["behavior_hide_top_spine"] = style["behavior"].get(
74
+ "hide_top_spine", True
75
+ )
76
+ values["behavior_hide_right_spine"] = style["behavior"].get(
77
+ "hide_right_spine", True
78
+ )
79
+ values["behavior_hide_bottom_spine"] = style["behavior"].get(
80
+ "hide_bottom_spine", False
81
+ )
82
+ values["behavior_hide_left_spine"] = style["behavior"].get(
83
+ "hide_left_spine", False
84
+ )
85
+ values["behavior_grid"] = style["behavior"].get("grid", False)
86
+
87
+ # Legend
88
+ if "legend" in style:
89
+ values["legend_frameon"] = style["legend"].get("frameon", True)
90
+
91
+ return values
92
+
93
+
94
+ def render_with_overrides(
95
+ fig, overrides: Optional[Dict[str, Any]], dark_mode: bool = False
96
+ ):
97
+ """
98
+ Re-render figure with overrides applied directly.
99
+
100
+ Applies style overrides directly to the existing figure for reliable rendering.
101
+ """
102
+ import base64
103
+ import io
104
+ import warnings
105
+
106
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
107
+ from PIL import Image
108
+
109
+ from ._bbox import extract_bboxes
110
+
111
+ # Get the underlying matplotlib figure
112
+ new_fig = fig.fig if hasattr(fig, "fig") else fig
113
+
114
+ # Safety check: validate figure size before rendering
115
+ fig_width, fig_height = new_fig.get_size_inches()
116
+ dpi = 150
117
+ pixel_width = fig_width * dpi
118
+ pixel_height = fig_height * dpi
119
+
120
+ # Sanity check: prevent enormous figures (max 10000x10000 pixels)
121
+ MAX_PIXELS = 10000
122
+ if pixel_width > MAX_PIXELS or pixel_height > MAX_PIXELS:
123
+ # Reset to reasonable size
124
+ new_fig.set_size_inches(
125
+ min(fig_width, MAX_PIXELS / dpi), min(fig_height, MAX_PIXELS / dpi)
126
+ )
127
+
128
+ # Switch to Agg backend to avoid Tkinter thread issues
129
+ new_fig.set_canvas(FigureCanvasAgg(new_fig))
130
+
131
+ # Disable constrained_layout if present (can cause rendering issues)
132
+ layout_engine = new_fig.get_layout_engine()
133
+ if layout_engine is not None and hasattr(layout_engine, "__class__"):
134
+ layout_name = layout_engine.__class__.__name__
135
+ if "Constrained" in layout_name:
136
+ new_fig.set_layout_engine("none")
137
+
138
+ # Apply overrides directly to existing figure
139
+ # Get record for call_id grouping (if fig is a RecordingFigure)
140
+ record = fig.record if hasattr(fig, "record") else None
141
+ if overrides:
142
+ from ._render_overrides import apply_overrides
143
+
144
+ apply_overrides(new_fig, overrides, record)
145
+
146
+ # Apply dark mode if requested
147
+ if dark_mode:
148
+ from ._render_overrides import apply_dark_mode
149
+
150
+ apply_dark_mode(new_fig)
151
+
152
+ # Validate axes bounds before rendering (prevent infinite/invalid extents)
153
+ for ax in new_fig.get_axes():
154
+ xlim = ax.get_xlim()
155
+ ylim = ax.get_ylim()
156
+ # Check for invalid limits (inf, nan, or extremely large)
157
+ if any(not (-1e10 < v < 1e10) for v in xlim + ylim):
158
+ ax.set_xlim(-1, 1)
159
+ ax.set_ylim(-1, 1)
160
+
161
+ # Save to PNG using same params as static save
162
+ # Re-create canvas to ensure clean state (avoids matplotlib renderer issues)
163
+ # Also save/restore the figure's draw method which can get corrupted by _get_renderer
164
+ original_draw = getattr(new_fig, "draw", None)
165
+ new_fig.set_canvas(FigureCanvasAgg(new_fig))
166
+
167
+ buf = io.BytesIO()
168
+ with warnings.catch_warnings():
169
+ warnings.filterwarnings("ignore", "constrained_layout not applied")
170
+ warnings.filterwarnings("ignore", category=UserWarning)
171
+ try:
172
+ # Standard render - layout handled by figure settings
173
+ # Use transparent background if specified in overrides
174
+ transparent = (
175
+ overrides.get("output_transparent", True) if overrides else True
176
+ )
177
+ new_fig.savefig(buf, format="png", dpi=150, transparent=transparent)
178
+ except Exception:
179
+ buf = io.BytesIO()
180
+ new_fig.set_canvas(FigureCanvasAgg(new_fig))
181
+ if original_draw is not None:
182
+ new_fig.draw = original_draw
183
+ try:
184
+ transparent = (
185
+ overrides.get("output_transparent", True) if overrides else True
186
+ )
187
+ new_fig.savefig(buf, format="png", dpi=150, transparent=transparent)
188
+ except Exception:
189
+ from PIL import Image as PILImage
190
+
191
+ placeholder = PILImage.new("RGB", (400, 300), color=(240, 240, 240))
192
+ placeholder.save(buf, format="PNG")
193
+ buf.seek(0)
194
+ finally:
195
+ if original_draw is not None and hasattr(new_fig, "draw"):
196
+ try:
197
+ new_fig.draw = original_draw
198
+ except AttributeError:
199
+ pass
200
+ buf.seek(0)
201
+ png_bytes = buf.read()
202
+ base64_str = base64.b64encode(png_bytes).decode("utf-8")
203
+
204
+ # Get image size
205
+ buf.seek(0)
206
+ img = Image.open(buf)
207
+ img_size = img.size
208
+
209
+ # Extract bboxes
210
+ # Ensure clean canvas state before drawing
211
+ new_fig.set_canvas(FigureCanvasAgg(new_fig))
212
+ original_dpi = new_fig.dpi
213
+ new_fig.set_dpi(150)
214
+ try:
215
+ new_fig.canvas.draw()
216
+ except Exception:
217
+ # Canvas draw failed, likely due to corrupted state - reset and retry
218
+ new_fig.set_canvas(FigureCanvasAgg(new_fig))
219
+ try:
220
+ new_fig.canvas.draw()
221
+ except Exception:
222
+ pass # If still fails, proceed with possibly stale bboxes
223
+ bboxes = extract_bboxes(new_fig, img_size[0], img_size[1])
224
+ new_fig.set_dpi(original_dpi)
225
+
226
+ return base64_str, bboxes, img_size
227
+
228
+
229
+ def to_json_serializable(obj):
230
+ """Convert numpy arrays and other non-serializable objects to JSON-safe types."""
231
+ import numpy as np
232
+
233
+ if isinstance(obj, np.ndarray):
234
+ return obj.tolist()
235
+ elif isinstance(obj, (np.integer, np.floating)):
236
+ return obj.item()
237
+ # Handle pandas Series
238
+ elif hasattr(obj, "values") and hasattr(obj, "tolist"):
239
+ return obj.tolist()
240
+ elif isinstance(obj, dict):
241
+ return {k: to_json_serializable(v) for k, v in obj.items()}
242
+ elif isinstance(obj, (list, tuple)):
243
+ return [to_json_serializable(item) for item in obj]
244
+ return obj
245
+
246
+
247
+ __all__ = [
248
+ "get_form_values_from_style",
249
+ "render_with_overrides",
250
+ "to_json_serializable",
251
+ ]
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Hitmap generation package.
4
+
5
+ This package provides hitmap generation for interactive element selection.
6
+ The main entry point is `generate_hitmap`.
7
+ """
8
+
9
+ # Re-export main functions from _hitmap_main
10
+ from .._hitmap_main import generate_hitmap, hitmap_to_base64
11
+
12
+ # Re-export from artist processing
13
+ from ._artists import (
14
+ process_collections,
15
+ process_figure_text,
16
+ process_images,
17
+ process_legend,
18
+ process_lines,
19
+ process_patches,
20
+ process_text,
21
+ )
22
+
23
+ # Re-export from color utilities
24
+ from ._colors import (
25
+ AXES_COLOR,
26
+ BACKGROUND_COLOR,
27
+ DISTINCT_COLORS,
28
+ hsv_to_rgb,
29
+ id_to_rgb,
30
+ mpl_color_to_hex,
31
+ normalize_color,
32
+ rgb_to_id,
33
+ )
34
+
35
+ # Re-export from detection utilities
36
+ from ._detect import detect_plot_types, is_boxplot_element, is_violin_element
37
+
38
+ # Re-export from restoration utilities
39
+ from ._restore import (
40
+ restore_axes_properties,
41
+ restore_backgrounds,
42
+ restore_figure_text,
43
+ )
44
+
45
+ __all__ = [
46
+ # Main functions
47
+ "generate_hitmap",
48
+ "hitmap_to_base64",
49
+ # Color utilities
50
+ "id_to_rgb",
51
+ "rgb_to_id",
52
+ "DISTINCT_COLORS",
53
+ "BACKGROUND_COLOR",
54
+ "AXES_COLOR",
55
+ "hsv_to_rgb",
56
+ "normalize_color",
57
+ "mpl_color_to_hex",
58
+ # Detection utilities
59
+ "detect_plot_types",
60
+ "is_boxplot_element",
61
+ "is_violin_element",
62
+ # Artist processing
63
+ "process_collections",
64
+ "process_figure_text",
65
+ "process_images",
66
+ "process_legend",
67
+ "process_lines",
68
+ "process_patches",
69
+ "process_text",
70
+ # Restoration utilities
71
+ "restore_axes_properties",
72
+ "restore_backgrounds",
73
+ "restore_figure_text",
74
+ ]
75
+
76
+ # EOF
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Artist processing package for hitmap generation."""
4
+
5
+ from ._collections import process_collections
6
+ from ._images import process_images
7
+ from ._lines import process_lines
8
+ from ._patches import process_patches
9
+ from ._text import process_figure_text, process_legend, process_text
10
+
11
+ __all__ = [
12
+ "process_lines",
13
+ "process_collections",
14
+ "process_patches",
15
+ "process_images",
16
+ "process_text",
17
+ "process_legend",
18
+ "process_figure_text",
19
+ ]
20
+
21
+ # EOF
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Collection processing for hitmap generation."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ from matplotlib.collections import (
8
+ LineCollection,
9
+ PathCollection,
10
+ PolyCollection,
11
+ )
12
+
13
+ from .._colors import id_to_rgb, mpl_color_to_hex, normalize_color
14
+ from .._detect import is_violin_element
15
+
16
+
17
+ def process_collections(
18
+ ax,
19
+ ax_idx: int,
20
+ element_id: int,
21
+ original_props: Dict[str, Any],
22
+ color_map: Dict[str, Any],
23
+ ax_info: Dict[str, Any],
24
+ ) -> int:
25
+ """Process collections (scatter, fills, etc.) on an axes.
26
+
27
+ Returns updated element_id.
28
+ """
29
+ from matplotlib.collections import QuadMesh
30
+ from matplotlib.contour import QuadContourSet
31
+ from matplotlib.quiver import Quiver
32
+
33
+ ax_plot_types = ax_info.get("types", set())
34
+ ax_call_ids = ax_info.get("call_ids", {})
35
+ has_violin = "violinplot" in ax_plot_types
36
+ has_record = len(ax_plot_types) > 0
37
+
38
+ violin_ids = list(ax_call_ids.get("violinplot", []))
39
+ scatter_ids = list(ax_call_ids.get("scatter", []))
40
+ quiver_ids = list(ax_call_ids.get("quiver", []))
41
+ fill_between_ids = list(ax_call_ids.get("fill_between", []))
42
+ pcolormesh_ids = list(ax_call_ids.get("pcolormesh", []))
43
+ contour_ids = list(ax_call_ids.get("contour", []))
44
+ contourf_ids = list(ax_call_ids.get("contourf", []))
45
+
46
+ violin_call_id = violin_ids[0] if violin_ids else None
47
+ pcolormesh_call_id = pcolormesh_ids[0] if pcolormesh_ids else None
48
+ contour_call_id = contour_ids[0] if contour_ids else None
49
+ contourf_call_id = contourf_ids[0] if contourf_ids else None
50
+ scatter_coll_idx = 0
51
+ fill_coll_idx = 0
52
+
53
+ for i, coll in enumerate(ax.collections):
54
+ if isinstance(coll, Quiver):
55
+ element_id = _process_quiver(
56
+ coll, i, ax_idx, element_id, original_props, color_map, quiver_ids
57
+ )
58
+ elif isinstance(coll, PathCollection):
59
+ element_id, scatter_coll_idx = _process_scatter(
60
+ coll,
61
+ i,
62
+ ax_idx,
63
+ element_id,
64
+ original_props,
65
+ color_map,
66
+ scatter_ids,
67
+ scatter_coll_idx,
68
+ )
69
+ elif isinstance(coll, PolyCollection):
70
+ element_id, fill_coll_idx = _process_polycoll(
71
+ coll,
72
+ i,
73
+ ax_idx,
74
+ element_id,
75
+ original_props,
76
+ color_map,
77
+ has_violin,
78
+ violin_call_id,
79
+ has_record,
80
+ ax,
81
+ fill_between_ids,
82
+ fill_coll_idx,
83
+ )
84
+ elif isinstance(coll, LineCollection):
85
+ element_id = _process_linecoll(
86
+ coll,
87
+ i,
88
+ ax_idx,
89
+ element_id,
90
+ original_props,
91
+ color_map,
92
+ has_violin,
93
+ violin_call_id,
94
+ )
95
+ elif isinstance(coll, QuadMesh):
96
+ element_id = _process_quadmesh(
97
+ coll, i, ax_idx, element_id, color_map, pcolormesh_call_id
98
+ )
99
+ elif isinstance(coll, QuadContourSet):
100
+ element_id = _process_contour(
101
+ coll,
102
+ i,
103
+ ax_idx,
104
+ element_id,
105
+ color_map,
106
+ contour_call_id,
107
+ contourf_call_id,
108
+ )
109
+
110
+ return element_id
111
+
112
+
113
+ def _process_quiver(coll, i, ax_idx, element_id, original_props, color_map, quiver_ids):
114
+ """Process Quiver collection."""
115
+ if not coll.get_visible():
116
+ return element_id
117
+
118
+ key = f"ax{ax_idx}_quiver{i}"
119
+ rgb = id_to_rgb(element_id)
120
+
121
+ original_props[key] = {"color": coll.get_facecolor().copy()}
122
+ coll.set_color(normalize_color(rgb))
123
+
124
+ call_id = quiver_ids[0] if quiver_ids else None
125
+ label = call_id or f"quiver_{i}"
126
+
127
+ color_map[key] = {
128
+ "id": element_id,
129
+ "type": "quiver",
130
+ "label": label,
131
+ "ax_index": ax_idx,
132
+ "rgb": list(rgb),
133
+ "call_id": call_id,
134
+ }
135
+ return element_id + 1
136
+
137
+
138
+ def _process_scatter(
139
+ coll,
140
+ i,
141
+ ax_idx,
142
+ element_id,
143
+ original_props,
144
+ color_map,
145
+ scatter_ids,
146
+ scatter_coll_idx,
147
+ ):
148
+ """Process PathCollection (scatter)."""
149
+ if not coll.get_visible():
150
+ return element_id, scatter_coll_idx
151
+
152
+ key = f"ax{ax_idx}_scatter{i}"
153
+ rgb = id_to_rgb(element_id)
154
+
155
+ original_props[key] = {
156
+ "facecolors": coll.get_facecolors().copy(),
157
+ "edgecolors": coll.get_edgecolors().copy(),
158
+ }
159
+
160
+ coll.set_facecolors([normalize_color(rgb)])
161
+ coll.set_edgecolors([normalize_color(rgb)])
162
+
163
+ orig_fc = original_props[key]["facecolors"]
164
+ orig_color = orig_fc[0] if len(orig_fc) > 0 else [0.5, 0.5, 0.5, 1]
165
+
166
+ orig_label = coll.get_label() or f"scatter_{i}"
167
+ if scatter_coll_idx < len(scatter_ids):
168
+ call_id = scatter_ids[scatter_coll_idx]
169
+ label = call_id
170
+ else:
171
+ call_id = f"scatter_{ax_idx}_{scatter_coll_idx}"
172
+ label = call_id if orig_label.startswith("_") else orig_label
173
+
174
+ color_map[key] = {
175
+ "id": element_id,
176
+ "type": "scatter",
177
+ "label": label,
178
+ "ax_index": ax_idx,
179
+ "rgb": list(rgb),
180
+ "original_color": mpl_color_to_hex(orig_color),
181
+ "call_id": call_id,
182
+ }
183
+ return element_id + 1, scatter_coll_idx + 1
184
+
185
+
186
+ def _process_polycoll(
187
+ coll,
188
+ i,
189
+ ax_idx,
190
+ element_id,
191
+ original_props,
192
+ color_map,
193
+ has_violin,
194
+ violin_call_id,
195
+ has_record,
196
+ ax,
197
+ fill_between_ids=None,
198
+ fill_coll_idx=0,
199
+ ):
200
+ """Process PolyCollection (fills, violin bodies)."""
201
+ if fill_between_ids is None:
202
+ fill_between_ids = []
203
+
204
+ if not coll.get_visible():
205
+ return element_id, fill_coll_idx
206
+
207
+ orig_label = coll.get_label() or ""
208
+
209
+ # Check if this is a fill_between element (should NOT be skipped)
210
+ has_fill_between = len(fill_between_ids) > 0
211
+
212
+ if has_record and not has_fill_between:
213
+ # Only skip _child/_nolegend if NOT from fill_between
214
+ if orig_label.startswith("_child") or orig_label.startswith("_nolegend"):
215
+ return element_id, fill_coll_idx
216
+
217
+ key = f"ax{ax_idx}_fill{i}"
218
+ rgb = id_to_rgb(element_id)
219
+
220
+ original_props[key] = {
221
+ "facecolors": coll.get_facecolors().copy(),
222
+ "edgecolors": coll.get_edgecolors().copy(),
223
+ }
224
+
225
+ coll.set_facecolors([normalize_color(rgb)])
226
+ coll.set_edgecolors([normalize_color(rgb)])
227
+
228
+ orig_fc = original_props[key]["facecolors"]
229
+ orig_color = orig_fc[0] if len(orig_fc) > 0 else [0.5, 0.5, 0.5, 1]
230
+
231
+ if has_violin and is_violin_element(coll, ax):
232
+ elem_type = "violin"
233
+ label = violin_call_id or "violin"
234
+ call_id = violin_call_id
235
+ else:
236
+ elem_type = "fill"
237
+ # Try to get fill_between call_id
238
+ if fill_coll_idx < len(fill_between_ids):
239
+ call_id = fill_between_ids[fill_coll_idx]
240
+ label = call_id
241
+ fill_coll_idx += 1
242
+ else:
243
+ label = orig_label if not orig_label.startswith("_") else f"fill_{i}"
244
+ call_id = None
245
+
246
+ color_map[key] = {
247
+ "id": element_id,
248
+ "type": elem_type,
249
+ "label": label,
250
+ "ax_index": ax_idx,
251
+ "rgb": list(rgb),
252
+ "original_color": mpl_color_to_hex(orig_color),
253
+ "call_id": call_id,
254
+ }
255
+ return element_id + 1, fill_coll_idx
256
+
257
+
258
+ def _process_linecoll(
259
+ coll, i, ax_idx, element_id, original_props, color_map, has_violin, violin_call_id
260
+ ):
261
+ """Process LineCollection (violin inner lines)."""
262
+ if not coll.get_visible():
263
+ return element_id
264
+
265
+ key = f"ax{ax_idx}_linecoll{i}"
266
+ rgb = id_to_rgb(element_id)
267
+
268
+ original_props[key] = {
269
+ "colors": coll.get_colors().copy() if hasattr(coll, "get_colors") else [],
270
+ "edgecolors": coll.get_edgecolors().copy(),
271
+ }
272
+
273
+ coll.set_color(normalize_color(rgb))
274
+
275
+ orig_colors = original_props[key]["colors"]
276
+ orig_color = orig_colors[0] if len(orig_colors) > 0 else [0.5, 0.5, 0.5, 1]
277
+
278
+ if has_violin:
279
+ elem_type = "violin"
280
+ label = violin_call_id or "violin"
281
+ call_id = violin_call_id
282
+ else:
283
+ elem_type = "linecollection"
284
+ label = f"linecoll_{i}"
285
+ call_id = None
286
+
287
+ color_map[key] = {
288
+ "id": element_id,
289
+ "type": elem_type,
290
+ "label": label,
291
+ "ax_index": ax_idx,
292
+ "rgb": list(rgb),
293
+ "original_color": mpl_color_to_hex(orig_color),
294
+ "call_id": call_id,
295
+ }
296
+ return element_id + 1
297
+
298
+
299
+ def _process_quadmesh(coll, i, ax_idx, element_id, color_map, pcolormesh_call_id=None):
300
+ """Process QuadMesh (pcolormesh, hexbin, hist2d)."""
301
+ if not coll.get_visible():
302
+ return element_id
303
+
304
+ key = f"ax{ax_idx}_quadmesh{i}"
305
+ rgb = id_to_rgb(element_id)
306
+
307
+ call_id = pcolormesh_call_id
308
+ label = call_id or f"quadmesh_{i}"
309
+
310
+ color_map[key] = {
311
+ "id": element_id,
312
+ "type": "quadmesh",
313
+ "label": label,
314
+ "ax_index": ax_idx,
315
+ "rgb": list(rgb),
316
+ "call_id": call_id,
317
+ }
318
+ return element_id + 1
319
+
320
+
321
+ def _process_contour(
322
+ coll, i, ax_idx, element_id, color_map, contour_call_id=None, contourf_call_id=None
323
+ ):
324
+ """Process QuadContourSet (contour, contourf)."""
325
+ key = f"ax{ax_idx}_contour{i}"
326
+ rgb = id_to_rgb(element_id)
327
+
328
+ # Try contourf first, then contour
329
+ call_id = contourf_call_id or contour_call_id
330
+ label = call_id or f"contour_{i}"
331
+
332
+ color_map[key] = {
333
+ "id": element_id,
334
+ "type": "contour",
335
+ "label": label,
336
+ "ax_index": ax_idx,
337
+ "rgb": list(rgb),
338
+ "call_id": call_id,
339
+ }
340
+ return element_id + 1
341
+
342
+
343
+ __all__ = ["process_collections"]
344
+
345
+ # EOF