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
@@ -26,10 +26,14 @@ from ._flask_app import FigureEditor
26
26
 
27
27
 
28
28
  def edit(
29
- source: Union[RecordingFigure, str, Path],
29
+ source: Optional[Union[RecordingFigure, str, Path]] = None,
30
30
  style: Optional[Union[str, Dict[str, Any]]] = None,
31
31
  port: int = 5050,
32
+ host: str = "127.0.0.1",
32
33
  open_browser: bool = True,
34
+ hot_reload: bool = False,
35
+ working_dir: Optional[Union[str, Path]] = None,
36
+ desktop: bool = False,
33
37
  ) -> Dict[str, Any]:
34
38
  """
35
39
  Launch interactive GUI editor for figure styling.
@@ -39,8 +43,14 @@ def edit(
39
43
 
40
44
  Parameters
41
45
  ----------
42
- source : RecordingFigure, str, or Path
43
- Either a live RecordingFigure object or path to a .yaml recipe file.
46
+ source : RecordingFigure, str, Path, or None
47
+ Figure source. Supports multiple formats:
48
+ - RecordingFigure: Live figure object
49
+ - .yaml/.yml: Direct recipe file
50
+ - .png/.jpg/etc: Image with associated .yaml
51
+ - Directory: Bundle containing recipe.yaml
52
+ - .zip: ZIP archive containing recipe.yaml
53
+ - None: Create new blank figure
44
54
  style : str or dict, optional
45
55
  Style preset name (e.g., 'SCITEX', 'SCITEX_DARK') or style dict.
46
56
  If None, uses the currently loaded global style.
@@ -48,6 +58,15 @@ def edit(
48
58
  Flask server port (default: 5050). Auto-finds available port if occupied.
49
59
  open_browser : bool, optional
50
60
  Whether to open browser automatically (default: True).
61
+ hot_reload : bool, optional
62
+ Enable hot reload - server restarts when source files change (default: False).
63
+ Like Django's development server. Browser auto-refreshes on reconnect.
64
+ working_dir : str or Path, optional
65
+ Working directory for file switching feature (default: current directory).
66
+ The file switcher will list recipe files from this directory.
67
+ desktop : bool, optional
68
+ Launch as native desktop window using pywebview (default: False).
69
+ Requires: pip install figrecipe[desktop]
51
70
 
52
71
  Returns
53
72
  -------
@@ -109,34 +128,65 @@ def edit(
109
128
  hitmap, color_map = generate_hitmap(fig)
110
129
  hitmap_base64 = hitmap_to_base64(hitmap)
111
130
 
131
+ # Resolve working directory
132
+ resolved_working_dir = Path(working_dir) if working_dir else Path.cwd()
133
+
112
134
  # Create and run editor with pre-rendered static PNG
113
135
  editor = FigureEditor(
114
136
  fig=fig,
115
137
  recipe_path=recipe_path,
116
138
  style=style_dict,
117
139
  port=port,
140
+ host=host,
118
141
  static_png_path=static_png_path,
119
142
  hitmap_base64=hitmap_base64,
120
143
  color_map=color_map,
144
+ hot_reload=hot_reload,
145
+ working_dir=resolved_working_dir,
146
+ desktop=desktop,
121
147
  )
122
148
 
123
149
  return editor.run(open_browser=open_browser)
124
150
 
125
151
 
126
- def _resolve_source(source: Union[RecordingFigure, str, Path]):
152
+ def _check_figure_has_content(fig: RecordingFigure) -> bool:
153
+ """Check if figure has any plot content."""
154
+ for ax_row in fig._axes:
155
+ for ax in ax_row:
156
+ # Check for lines, patches, images, collections
157
+ if ax.lines or ax.patches or ax.images or ax.collections or ax.texts:
158
+ return True
159
+ return False
160
+
161
+
162
+ def _resolve_source(source: Optional[Union[RecordingFigure, str, Path]]):
127
163
  """
128
164
  Resolve source to figure and optional recipe path.
129
165
 
130
166
  Parameters
131
167
  ----------
132
- source : RecordingFigure, str, or Path
133
- Input source.
168
+ source : RecordingFigure, str, Path, or None
169
+ Input source. Supports:
170
+ - None: Creates new blank figure
171
+ - RecordingFigure: Uses directly
172
+ - .yaml/.yml: Direct recipe file
173
+ - .png/.jpg/etc: Image with associated YAML
174
+ - Directory: Bundle containing recipe.yaml
175
+ - .zip: ZIP archive containing recipe.yaml
134
176
 
135
177
  Returns
136
178
  -------
137
179
  tuple
138
- (RecordingFigure or None, Path or None)
180
+ (RecordingFigure, Path or None)
139
181
  """
182
+ # Handle None - create new blank figure
183
+ if source is None:
184
+ from .. import subplots
185
+
186
+ fig, ax = subplots()
187
+ ax.set_title("New Figure")
188
+ return fig, None
189
+
140
190
  if isinstance(source, RecordingFigure):
141
191
  return source, None
142
192
 
@@ -157,13 +207,11 @@ def _resolve_source(source: Union[RecordingFigure, str, Path]):
157
207
  )
158
208
  return wrapped_fig, None
159
209
 
160
- # Assume it's a path
161
- path = Path(source)
162
- if not path.exists():
163
- raise FileNotFoundError(f"Recipe file not found: {path}")
210
+ # Assume it's a path - use bundle resolution (handles dir, zip, yaml, png)
211
+ from .._utils._bundle import resolve_recipe_path
164
212
 
165
- if path.suffix.lower() not in (".yaml", ".yml"):
166
- raise ValueError(f"Expected .yaml or .yml file, got: {path.suffix}")
213
+ path, _temp_dir = resolve_recipe_path(source)
214
+ # Note: temp_dir cleanup handled by reproduce() if ZIP was extracted
167
215
 
168
216
  # Load recipe and reproduce figure
169
217
  from .._reproducer import reproduce
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Modular bbox extraction for figure elements.
5
+
6
+ This package provides functions for extracting bounding boxes from
7
+ matplotlib figure elements for hit detection in the GUI editor.
8
+
9
+ The main function `extract_bboxes` is re-exported from _bbox_main.py
10
+ for backward compatibility.
11
+
12
+ Modules:
13
+ - _transforms: Coordinate transformation utilities
14
+ - _elements: General element, text, and tick bbox extraction
15
+ - _lines: Line and quiver bbox extraction
16
+ - _collections: Collection (scatter, fill) and patch bbox extraction
17
+ """
18
+
19
+ # Re-export main function from _extract.py
20
+ # Import modular helpers
21
+ from ._collections import get_collection_bbox, get_patch_bbox
22
+ from ._elements import get_element_bbox, get_text_bbox, get_tick_labels_bbox
23
+ from ._extract import extract_bboxes
24
+ from ._lines import get_line_bbox, get_quiver_bbox
25
+ from ._transforms import display_to_image, transform_bbox
26
+
27
+ __all__ = [
28
+ # Main function
29
+ "extract_bboxes",
30
+ # Transforms
31
+ "transform_bbox",
32
+ "display_to_image",
33
+ # Elements
34
+ "get_element_bbox",
35
+ "get_text_bbox",
36
+ "get_tick_labels_bbox",
37
+ # Lines
38
+ "get_line_bbox",
39
+ "get_quiver_bbox",
40
+ # Collections
41
+ "get_collection_bbox",
42
+ "get_patch_bbox",
43
+ ]
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Collection bbox extraction for scatter, fill, and patch elements.
5
+
6
+ This module handles bbox extraction for matplotlib collections
7
+ (scatter plots, fills, bars, etc.).
8
+ """
9
+
10
+ import math
11
+ from typing import Any, Dict, Optional
12
+
13
+ from matplotlib.axes import Axes
14
+ from matplotlib.collections import PathCollection
15
+ from matplotlib.figure import Figure
16
+ from matplotlib.transforms import Bbox
17
+
18
+ from ._elements import get_element_bbox
19
+ from ._transforms import display_to_image, transform_bbox
20
+
21
+
22
+ def get_collection_bbox(
23
+ coll,
24
+ ax: Axes,
25
+ fig: Figure,
26
+ renderer,
27
+ tight_bbox: Bbox,
28
+ img_width: int,
29
+ img_height: int,
30
+ scale_x: float,
31
+ scale_y: float,
32
+ pad_inches: float,
33
+ saved_height_inches: float,
34
+ include_points: bool = True,
35
+ ) -> Optional[Dict[str, Any]]:
36
+ """Get bbox and points for a collection (scatter, fill)."""
37
+ try:
38
+ bbox = None
39
+
40
+ # For scatter plots, get_window_extent() can fail or return empty
41
+ # So we calculate bbox from data points as fallback
42
+ if isinstance(coll, PathCollection):
43
+ offsets = coll.get_offsets()
44
+ if len(offsets) > 0:
45
+ transform = ax.transData
46
+ points = []
47
+
48
+ # Limit to reasonable number of points
49
+ max_points = 200
50
+ step = max(1, len(offsets) // max_points)
51
+
52
+ for i in range(0, len(offsets), step):
53
+ try:
54
+ offset = offsets[i]
55
+ display_coords = transform.transform(offset)
56
+ img_coords = display_to_image(
57
+ display_coords[0],
58
+ display_coords[1],
59
+ fig,
60
+ tight_bbox,
61
+ img_width,
62
+ img_height,
63
+ scale_x,
64
+ scale_y,
65
+ pad_inches,
66
+ saved_height_inches,
67
+ )
68
+ if img_coords:
69
+ points.append(img_coords)
70
+ except Exception:
71
+ continue
72
+
73
+ # Calculate bbox from points
74
+ if points:
75
+ xs = [p[0] for p in points]
76
+ ys = [p[1] for p in points]
77
+ # Add padding around scatter points for easier clicking
78
+ padding = 10 # pixels
79
+ bbox = {
80
+ "x": float(min(xs) - padding),
81
+ "y": float(min(ys) - padding),
82
+ "width": float(max(xs) - min(xs) + 2 * padding),
83
+ "height": float(max(ys) - min(ys) + 2 * padding),
84
+ "points": points,
85
+ }
86
+ return bbox
87
+
88
+ # Fallback: try standard window extent
89
+ window_extent = coll.get_window_extent(renderer)
90
+ if window_extent is None:
91
+ # Use axes extent as fallback
92
+ return get_element_bbox(
93
+ ax,
94
+ fig,
95
+ renderer,
96
+ tight_bbox,
97
+ img_width,
98
+ img_height,
99
+ scale_x,
100
+ scale_y,
101
+ pad_inches,
102
+ saved_height_inches,
103
+ )
104
+
105
+ # Check if window_extent is valid (not inf)
106
+ if (
107
+ math.isinf(window_extent.x0)
108
+ or math.isinf(window_extent.y0)
109
+ or math.isinf(window_extent.x1)
110
+ or math.isinf(window_extent.y1)
111
+ ):
112
+ # Invalid extent - use axes extent as fallback
113
+ return get_element_bbox(
114
+ ax,
115
+ fig,
116
+ renderer,
117
+ tight_bbox,
118
+ img_width,
119
+ img_height,
120
+ scale_x,
121
+ scale_y,
122
+ pad_inches,
123
+ saved_height_inches,
124
+ )
125
+
126
+ bbox = transform_bbox(
127
+ window_extent,
128
+ fig,
129
+ tight_bbox,
130
+ img_width,
131
+ img_height,
132
+ scale_x,
133
+ scale_y,
134
+ pad_inches,
135
+ saved_height_inches,
136
+ )
137
+
138
+ return bbox
139
+
140
+ except Exception:
141
+ return None
142
+
143
+
144
+ def get_patch_bbox(
145
+ patch,
146
+ ax: Axes,
147
+ fig: Figure,
148
+ renderer,
149
+ tight_bbox: Bbox,
150
+ img_width: int,
151
+ img_height: int,
152
+ scale_x: float,
153
+ scale_y: float,
154
+ pad_inches: float,
155
+ saved_height_inches: float,
156
+ ) -> Optional[Dict[str, float]]:
157
+ """Get bbox for a patch (bar, rectangle)."""
158
+ try:
159
+ window_extent = patch.get_window_extent(renderer)
160
+ if window_extent is None:
161
+ return None
162
+ return transform_bbox(
163
+ window_extent,
164
+ fig,
165
+ tight_bbox,
166
+ img_width,
167
+ img_height,
168
+ scale_x,
169
+ scale_y,
170
+ pad_inches,
171
+ saved_height_inches,
172
+ )
173
+ except Exception:
174
+ return None
175
+
176
+
177
+ __all__ = ["get_collection_bbox", "get_patch_bbox"]
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Element bbox extraction for general elements, text, and ticks.
5
+
6
+ This module handles bbox extraction for axes, text labels, and tick marks.
7
+ """
8
+
9
+ from typing import Dict, Optional
10
+
11
+ from matplotlib.figure import Figure
12
+ from matplotlib.transforms import Bbox
13
+
14
+ from ._transforms import transform_bbox
15
+
16
+
17
+ def get_element_bbox(
18
+ element,
19
+ fig: Figure,
20
+ renderer,
21
+ tight_bbox: Bbox,
22
+ img_width: int,
23
+ img_height: int,
24
+ scale_x: float,
25
+ scale_y: float,
26
+ pad_inches: float,
27
+ saved_height_inches: float,
28
+ ) -> Optional[Dict[str, float]]:
29
+ """Get bbox for a general element."""
30
+ try:
31
+ window_extent = element.get_window_extent(renderer)
32
+ if window_extent is None:
33
+ return None
34
+ return transform_bbox(
35
+ window_extent,
36
+ fig,
37
+ tight_bbox,
38
+ img_width,
39
+ img_height,
40
+ scale_x,
41
+ scale_y,
42
+ pad_inches,
43
+ saved_height_inches,
44
+ )
45
+ except Exception:
46
+ return None
47
+
48
+
49
+ def get_text_bbox(
50
+ text,
51
+ fig: Figure,
52
+ renderer,
53
+ tight_bbox: Bbox,
54
+ img_width: int,
55
+ img_height: int,
56
+ scale_x: float,
57
+ scale_y: float,
58
+ pad_inches: float,
59
+ saved_height_inches: float,
60
+ ) -> Optional[Dict[str, float]]:
61
+ """Get bbox for a text element."""
62
+ try:
63
+ window_extent = text.get_window_extent(renderer)
64
+ if window_extent is None:
65
+ return None
66
+ return transform_bbox(
67
+ window_extent,
68
+ fig,
69
+ tight_bbox,
70
+ img_width,
71
+ img_height,
72
+ scale_x,
73
+ scale_y,
74
+ pad_inches,
75
+ saved_height_inches,
76
+ )
77
+ except Exception:
78
+ return None
79
+
80
+
81
+ def get_tick_labels_bbox(
82
+ axis,
83
+ axis_type: str, # 'x' or 'y'
84
+ fig: Figure,
85
+ renderer,
86
+ tight_bbox: Bbox,
87
+ img_width: int,
88
+ img_height: int,
89
+ scale_x: float,
90
+ scale_y: float,
91
+ pad_inches: float,
92
+ saved_height_inches: float,
93
+ ) -> Optional[Dict[str, float]]:
94
+ """
95
+ Get bbox for tick labels, extended to span the full axis dimension.
96
+
97
+ For x-axis: tick labels bbox spans the full width of the plot area.
98
+ For y-axis: tick labels bbox spans the full height of the plot area.
99
+ """
100
+ try:
101
+ all_bboxes = []
102
+
103
+ # Get all tick label bboxes
104
+ for tick in axis.get_major_ticks():
105
+ tick_label = tick.label1 if hasattr(tick, "label1") else tick.label
106
+ if tick_label and tick_label.get_visible():
107
+ try:
108
+ tick_extent = tick_label.get_window_extent(renderer)
109
+ if tick_extent is not None and tick_extent.width > 0:
110
+ all_bboxes.append(tick_extent)
111
+ except Exception:
112
+ pass
113
+
114
+ if not all_bboxes:
115
+ return None
116
+
117
+ # Merge all tick label bboxes
118
+ merged = all_bboxes[0]
119
+ for bbox in all_bboxes[1:]:
120
+ merged = Bbox.union([merged, bbox])
121
+
122
+ # Get the axes extent to extend the tick labels region
123
+ ax = axis.axes
124
+ ax_bbox = ax.get_window_extent(renderer)
125
+
126
+ if axis_type == "x":
127
+ # For x-axis: extend width to match axes width, keep tick labels height
128
+ merged = Bbox.from_extents(
129
+ ax_bbox.x0, # Align left with axes
130
+ merged.y0, # Keep tick labels y position
131
+ ax_bbox.x1, # Align right with axes
132
+ merged.y1, # Keep tick labels height
133
+ )
134
+ else: # y-axis
135
+ # For y-axis: extend height to match axes height, keep tick labels width
136
+ merged = Bbox.from_extents(
137
+ merged.x0, # Keep tick labels x position
138
+ ax_bbox.y0, # Align bottom with axes
139
+ merged.x1, # Keep tick labels width
140
+ ax_bbox.y1, # Align top with axes
141
+ )
142
+
143
+ return transform_bbox(
144
+ merged,
145
+ fig,
146
+ tight_bbox,
147
+ img_width,
148
+ img_height,
149
+ scale_x,
150
+ scale_y,
151
+ pad_inches,
152
+ saved_height_inches,
153
+ )
154
+
155
+ except Exception:
156
+ return None
157
+
158
+
159
+ __all__ = ["get_element_bbox", "get_text_bbox", "get_tick_labels_bbox"]