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
@@ -10,10 +10,51 @@ applied, enabling real-time preview updates in the GUI editor.
10
10
  import io
11
11
  from typing import Any, Dict, Optional, Tuple
12
12
 
13
- from matplotlib.figure import Figure
14
-
15
13
  from .._wrappers import RecordingFigure
16
14
  from ._bbox import extract_bboxes
15
+ from ._render_overrides import apply_dark_mode, apply_overrides
16
+
17
+
18
+ def _restore_light_mode(fig) -> None:
19
+ """Restore light mode colors to figure (undo dark mode changes)."""
20
+ text_color = "black"
21
+
22
+ # Figure background (transparent)
23
+ fig.patch.set_facecolor("none")
24
+
25
+ # Figure-level text
26
+ if hasattr(fig, "_suptitle") and fig._suptitle is not None:
27
+ fig._suptitle.set_color(text_color)
28
+ if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
29
+ fig._supxlabel.set_color(text_color)
30
+ if hasattr(fig, "_supylabel") and fig._supylabel is not None:
31
+ fig._supylabel.set_color(text_color)
32
+
33
+ for ax in fig.get_axes():
34
+ # Axes background (transparent)
35
+ ax.set_facecolor("none")
36
+ # Text colors
37
+ ax.xaxis.label.set_color(text_color)
38
+ ax.yaxis.label.set_color(text_color)
39
+ ax.title.set_color(text_color)
40
+ ax.tick_params(colors=text_color)
41
+ # Spines
42
+ for spine in ax.spines.values():
43
+ spine.set_edgecolor(text_color)
44
+ # Text objects (panel labels, annotations)
45
+ for text in ax.texts:
46
+ text.set_color(text_color)
47
+ # Bracket lines (Line2D with clip_on=False)
48
+ for line in ax.get_lines():
49
+ if not line.get_clip_on():
50
+ line.set_color(text_color)
51
+ # Legend
52
+ legend = ax.get_legend()
53
+ if legend is not None:
54
+ legend.get_frame().set_facecolor("none")
55
+ legend.get_frame().set_edgecolor(text_color)
56
+ for text in legend.get_texts():
57
+ text.set_color(text_color)
17
58
 
18
59
 
19
60
  def render_preview(
@@ -48,13 +89,22 @@ def render_preview(
48
89
  # Get underlying matplotlib figure
49
90
  mpl_fig = fig.fig if hasattr(fig, "fig") else fig
50
91
 
92
+ # Get record for call_id grouping (if fig is a RecordingFigure)
93
+ record = fig.record if hasattr(fig, "record") else None
94
+
51
95
  # Apply style overrides
52
96
  if overrides:
53
- _apply_overrides(mpl_fig, overrides)
97
+ apply_overrides(mpl_fig, overrides, record)
54
98
 
55
- # Apply dark mode if requested
99
+ # Apply dark mode if requested, or restore light mode colors
56
100
  if dark_mode:
57
- _apply_dark_mode(mpl_fig)
101
+ apply_dark_mode(mpl_fig)
102
+ else:
103
+ # Restore light mode colors (needed because figure is reused)
104
+ _restore_light_mode(mpl_fig)
105
+
106
+ # Finalize ticks and special plots (must be done after all plotting)
107
+ _finalize_figure(fig, mpl_fig)
58
108
 
59
109
  # Render to buffer first
60
110
  buf = io.BytesIO()
@@ -120,185 +170,6 @@ def render_to_base64(
120
170
  return base64_str, bboxes, img_size
121
171
 
122
172
 
123
- def _apply_overrides(fig: Figure, overrides: Dict[str, Any]) -> None:
124
- """
125
- Apply style overrides to figure.
126
-
127
- Parameters
128
- ----------
129
- fig : Figure
130
- Matplotlib figure.
131
- overrides : dict
132
- Style overrides with keys like:
133
- - axes_width_mm, axes_height_mm
134
- - fonts_axis_label_pt, fonts_tick_label_pt
135
- - lines_trace_mm
136
- - etc.
137
- """
138
- from ..styles._style_applier import apply_style_mm
139
-
140
- axes_list = fig.get_axes()
141
-
142
- for ax in axes_list:
143
- # Apply mm-based styling
144
- apply_style_mm(ax, overrides)
145
-
146
- # Apply specific overrides that aren't handled by apply_style_mm
147
- # YAML-compatible keys are canonical, legacy keys supported for backwards compatibility
148
-
149
- # Font sizes (YAML: fonts_axis_label_pt, legacy: axis_font_size_pt)
150
- axis_fs = overrides.get(
151
- "fonts_axis_label_pt", overrides.get("axis_font_size_pt")
152
- )
153
- if axis_fs is not None:
154
- ax.xaxis.label.set_fontsize(axis_fs)
155
- ax.yaxis.label.set_fontsize(axis_fs)
156
-
157
- tick_fs = overrides.get(
158
- "fonts_tick_label_pt", overrides.get("tick_font_size_pt")
159
- )
160
- if tick_fs is not None:
161
- ax.tick_params(labelsize=tick_fs)
162
-
163
- title_fs = overrides.get("fonts_title_pt", overrides.get("title_font_size_pt"))
164
- if title_fs is not None:
165
- ax.title.set_fontsize(title_fs)
166
-
167
- family = overrides.get("fonts_family", overrides.get("font_family"))
168
- if family is not None:
169
- ax.xaxis.label.set_fontfamily(family)
170
- ax.yaxis.label.set_fontfamily(family)
171
- ax.title.set_fontfamily(family)
172
- for label in ax.get_xticklabels() + ax.get_yticklabels():
173
- label.set_fontfamily(family)
174
-
175
- # Ticks (YAML: ticks_direction, legacy: tick_direction)
176
- tick_dir = overrides.get("ticks_direction", overrides.get("tick_direction"))
177
- if tick_dir is not None and tick_dir in ("in", "out", "inout"):
178
- ax.tick_params(direction=tick_dir)
179
-
180
- tick_len = overrides.get("ticks_length_mm", overrides.get("tick_length_mm"))
181
- if tick_len is not None:
182
- from .._utils._units import mm_to_pt
183
-
184
- length = mm_to_pt(tick_len)
185
- ax.tick_params(length=length)
186
-
187
- # Grid (YAML: behavior_grid, legacy: grid)
188
- grid_value = overrides.get("behavior_grid", overrides.get("grid"))
189
- if grid_value is not None:
190
- if grid_value:
191
- ax.grid(True, alpha=0.3)
192
- else:
193
- ax.grid(False)
194
-
195
- # Spines (YAML: behavior_hide_top_spine, legacy: hide_top_spine)
196
- hide_top = overrides.get(
197
- "behavior_hide_top_spine", overrides.get("hide_top_spine")
198
- )
199
- if hide_top is not None:
200
- ax.spines["top"].set_visible(not hide_top)
201
-
202
- hide_right = overrides.get(
203
- "behavior_hide_right_spine", overrides.get("hide_right_spine")
204
- )
205
- if hide_right is not None:
206
- ax.spines["right"].set_visible(not hide_right)
207
-
208
- # Legend
209
- legend = ax.get_legend()
210
- if legend is not None:
211
- if "legend_frameon" in overrides:
212
- legend.set_frame_on(overrides["legend_frameon"])
213
-
214
- if "legend_alpha" in overrides:
215
- frame = legend.get_frame()
216
- fc = frame.get_facecolor()
217
- frame.set_facecolor((*fc[:3], overrides["legend_alpha"]))
218
-
219
- # Line widths (YAML: lines_trace_mm, legacy: trace_thickness_mm)
220
- trace_mm = overrides.get("lines_trace_mm", overrides.get("trace_thickness_mm"))
221
- if trace_mm is not None:
222
- from .._utils._units import mm_to_pt
223
-
224
- lw = mm_to_pt(trace_mm)
225
- for line in ax.get_lines():
226
- line.set_linewidth(lw)
227
-
228
- # Marker sizes (YAML: markers_scatter_mm, legacy: marker_size_mm)
229
- # Only apply to PathCollection (scatter), not PolyCollection (violin/fill)
230
- scatter_mm = overrides.get(
231
- "markers_scatter_mm",
232
- overrides.get("markers_size_mm", overrides.get("marker_size_mm")),
233
- )
234
- if scatter_mm is not None:
235
- from matplotlib.collections import PathCollection
236
-
237
- from .._utils._units import mm_to_scatter_size
238
-
239
- size = mm_to_scatter_size(scatter_mm)
240
- for coll in ax.collections:
241
- # Only apply to scatter plots (PathCollection), not violin/fill (PolyCollection)
242
- if isinstance(coll, PathCollection):
243
- try:
244
- coll.set_sizes([size])
245
- except Exception:
246
- pass
247
-
248
-
249
- def _apply_dark_mode(fig: Figure) -> None:
250
- """
251
- Apply dark mode colors to figure.
252
-
253
- Parameters
254
- ----------
255
- fig : Figure
256
- Matplotlib figure.
257
- """
258
- # Dark theme colors
259
- bg_color = "#1a1a1a"
260
- text_color = "#e8e8e8"
261
-
262
- # Figure background
263
- fig.patch.set_facecolor(bg_color)
264
-
265
- # Figure-level text elements (suptitle, supxlabel, supylabel)
266
- if hasattr(fig, "_suptitle") and fig._suptitle is not None:
267
- fig._suptitle.set_color(text_color)
268
- if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
269
- fig._supxlabel.set_color(text_color)
270
- if hasattr(fig, "_supylabel") and fig._supylabel is not None:
271
- fig._supylabel.set_color(text_color)
272
-
273
- for ax in fig.get_axes():
274
- # Axes background
275
- ax.set_facecolor(bg_color)
276
-
277
- # Text colors
278
- ax.xaxis.label.set_color(text_color)
279
- ax.yaxis.label.set_color(text_color)
280
- ax.title.set_color(text_color)
281
-
282
- # Tick labels
283
- ax.tick_params(colors=text_color)
284
-
285
- # Spines
286
- for spine in ax.spines.values():
287
- spine.set_color(text_color)
288
-
289
- # Grid
290
- ax.tick_params(color=text_color)
291
-
292
- # Legend
293
- legend = ax.get_legend()
294
- if legend is not None:
295
- frame = legend.get_frame()
296
- frame.set_facecolor(bg_color)
297
- frame.set_edgecolor(text_color)
298
- for text in legend.get_texts():
299
- text.set_color(text_color)
300
-
301
-
302
173
  def render_download(
303
174
  fig: RecordingFigure,
304
175
  fmt: str = "png",
@@ -329,11 +200,17 @@ def render_download(
329
200
  """
330
201
  mpl_fig = fig.fig if hasattr(fig, "fig") else fig
331
202
 
203
+ # Get record for call_id grouping (if fig is a RecordingFigure)
204
+ record = fig.record if hasattr(fig, "record") else None
205
+
332
206
  if overrides:
333
- _apply_overrides(mpl_fig, overrides)
207
+ apply_overrides(mpl_fig, overrides, record)
334
208
 
335
209
  if dark_mode:
336
- _apply_dark_mode(mpl_fig)
210
+ apply_dark_mode(mpl_fig)
211
+
212
+ # Finalize ticks and special plots (must be done after all plotting)
213
+ _finalize_figure(fig, mpl_fig)
337
214
 
338
215
  buf = io.BytesIO()
339
216
  mpl_fig.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight")
@@ -342,8 +219,26 @@ def render_download(
342
219
  return buf.read()
343
220
 
344
221
 
222
+ def _finalize_figure(fig: RecordingFigure, mpl_fig: Any) -> None:
223
+ """Finalize ticks and special plots for all axes in the figure."""
224
+ from ..styles._style_applier import finalize_special_plots, finalize_ticks
225
+
226
+ # Get style dict for finalization
227
+ style_dict = {}
228
+ if hasattr(fig, "style") and fig.style:
229
+ from ..styles import get_style
230
+
231
+ style_dict = get_style(fig.style)
232
+
233
+ for ax in mpl_fig.get_axes():
234
+ finalize_ticks(ax)
235
+ finalize_special_plots(ax, style_dict)
236
+
237
+
345
238
  __all__ = [
346
239
  "render_preview",
347
240
  "render_to_base64",
348
241
  "render_download",
349
242
  ]
243
+
244
+ # EOF
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Annotation-related Flask route handlers for the figure editor.
4
+
5
+ Handles updating positions of decorative elements like panel labels,
6
+ text annotations, and arrows.
7
+ """
8
+
9
+ from flask import jsonify, request
10
+
11
+ from ._helpers import render_with_overrides
12
+
13
+
14
+ def register_annotation_routes(app, editor):
15
+ """Register annotation-related routes with the Flask app."""
16
+
17
+ @app.route("/update_annotation_position", methods=["POST"])
18
+ def update_annotation_position():
19
+ """Update annotation position (panel label, text, arrow).
20
+
21
+ Expects JSON: {
22
+ ax_index: int,
23
+ annotation_type: str ('panel_label', 'text', 'arrow'),
24
+ text_index: int,
25
+ x: float (axes-relative 0-1),
26
+ y: float (axes-relative 0-1)
27
+ }
28
+ """
29
+ data = request.get_json() or {}
30
+ ax_index = data.get("ax_index", 0)
31
+ annotation_type = data.get("annotation_type", "text")
32
+ text_index = data.get("text_index", 0)
33
+ new_x = data.get("x")
34
+ new_y = data.get("y")
35
+
36
+ if new_x is None or new_y is None:
37
+ return jsonify({"error": "Missing x or y coordinate"}), 400
38
+
39
+ try:
40
+ # Get the matplotlib figure
41
+ mpl_fig = editor.fig.fig if hasattr(editor.fig, "fig") else editor.fig
42
+ axes_list = mpl_fig.get_axes()
43
+
44
+ if ax_index >= len(axes_list):
45
+ return jsonify({"error": f"Invalid axis index: {ax_index}"}), 400
46
+
47
+ ax = axes_list[ax_index]
48
+
49
+ if annotation_type in ("panel_label", "text"):
50
+ # Update text position
51
+ if text_index >= len(ax.texts):
52
+ return jsonify({"error": f"Invalid text index: {text_index}"}), 400
53
+
54
+ text_obj = ax.texts[text_index]
55
+
56
+ # Set new position (in axes coordinates for transAxes transform)
57
+ if text_obj.get_transform() == ax.transAxes:
58
+ text_obj.set_position((new_x, new_y))
59
+ else:
60
+ # For data coordinates, convert from axes-relative
61
+ xlim = ax.get_xlim()
62
+ ylim = ax.get_ylim()
63
+ data_x = xlim[0] + new_x * (xlim[1] - xlim[0])
64
+ data_y = ylim[0] + new_y * (ylim[1] - ylim[0])
65
+ text_obj.set_position((data_x, data_y))
66
+
67
+ # Store in manual_overrides for persistence and undo support
68
+ override_key = f"annotation_pos_ax{ax_index}_text{text_index}"
69
+ editor.style_overrides.manual_overrides[override_key] = {
70
+ "x": new_x,
71
+ "y": new_y,
72
+ "type": annotation_type,
73
+ }
74
+
75
+ elif annotation_type == "arrow":
76
+ # Update arrow position (FancyArrowPatch)
77
+ from matplotlib.patches import FancyArrowPatch
78
+
79
+ arrow_patches = [
80
+ p for p in ax.patches if isinstance(p, FancyArrowPatch)
81
+ ]
82
+ arrow_index = data.get("arrow_index", 0)
83
+
84
+ if arrow_index >= len(arrow_patches):
85
+ return jsonify(
86
+ {"error": f"Invalid arrow index: {arrow_index}"}
87
+ ), 400
88
+
89
+ # Arrow position update is more complex - skip for now
90
+ return jsonify({"error": "Arrow drag not yet implemented"}), 501
91
+
92
+ # Re-render the figure
93
+ base64_img, bboxes, img_size = render_with_overrides(
94
+ editor.fig,
95
+ editor.get_effective_style(),
96
+ editor.dark_mode,
97
+ )
98
+
99
+ return jsonify(
100
+ {
101
+ "success": True,
102
+ "image": base64_img,
103
+ "bboxes": bboxes,
104
+ }
105
+ )
106
+
107
+ except Exception as e:
108
+ import traceback
109
+
110
+ traceback.print_exc()
111
+ return jsonify({"error": str(e)}), 500
112
+
113
+
114
+ __all__ = ["register_annotation_routes"]