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,146 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Coordinate transformation utilities for bbox extraction.
5
+
6
+ This module handles the transformation from matplotlib display coordinates
7
+ to image pixel coordinates for hit detection.
8
+ """
9
+
10
+ from typing import Dict, List, Optional
11
+
12
+ from matplotlib.figure import Figure
13
+ from matplotlib.transforms import Bbox
14
+
15
+
16
+ def transform_bbox(
17
+ window_extent: Bbox,
18
+ fig: Figure,
19
+ tight_bbox: Bbox,
20
+ img_width: int,
21
+ img_height: int,
22
+ scale_x: float,
23
+ scale_y: float,
24
+ pad_inches: float,
25
+ saved_height_inches: float,
26
+ ) -> Optional[Dict[str, float]]:
27
+ """
28
+ Transform matplotlib window extent to image pixel coordinates.
29
+
30
+ Parameters
31
+ ----------
32
+ window_extent : Bbox
33
+ Bbox in display coordinates (points).
34
+ fig : Figure
35
+ Matplotlib figure.
36
+ tight_bbox : Bbox
37
+ Tight bbox of figure in inches.
38
+ img_width, img_height : int
39
+ Output image dimensions.
40
+ scale_x, scale_y : float
41
+ Scale factors from inches to pixels.
42
+ pad_inches : float
43
+ Padding added by bbox_inches='tight' (default 0.1).
44
+ saved_height_inches : float
45
+ Total saved image height including padding.
46
+
47
+ Returns
48
+ -------
49
+ dict or None
50
+ {x, y, width, height} in image pixels.
51
+ """
52
+ try:
53
+ dpi = fig.dpi
54
+
55
+ # Convert display coords to inches
56
+ x0_inches = window_extent.x0 / dpi
57
+ y0_inches = window_extent.y0 / dpi
58
+ x1_inches = window_extent.x1 / dpi
59
+ y1_inches = window_extent.y1 / dpi
60
+
61
+ # Transform to saved image coordinates
62
+ # Account for tight bbox origin and padding
63
+ x0_rel = x0_inches - tight_bbox.x0 + pad_inches
64
+ x1_rel = x1_inches - tight_bbox.x0 + pad_inches
65
+
66
+ # Y coordinate flip: matplotlib Y=0 at bottom, image Y=0 at top
67
+ y0_rel = saved_height_inches - (y1_inches - tight_bbox.y0 + pad_inches)
68
+ y1_rel = saved_height_inches - (y0_inches - tight_bbox.y0 + pad_inches)
69
+
70
+ # Scale to image pixels
71
+ x0_px = x0_rel * scale_x
72
+ y0_px = y0_rel * scale_y
73
+ x1_px = x1_rel * scale_x
74
+ y1_px = y1_rel * scale_y
75
+
76
+ # Clamp to bounds
77
+ x0_px = max(0, min(x0_px, img_width))
78
+ x1_px = max(0, min(x1_px, img_width))
79
+ y0_px = max(0, min(y0_px, img_height))
80
+ y1_px = max(0, min(y1_px, img_height))
81
+
82
+ width = x1_px - x0_px
83
+ height = y1_px - y0_px
84
+
85
+ if width <= 0 or height <= 0:
86
+ return None
87
+
88
+ return {
89
+ "x": float(x0_px),
90
+ "y": float(y0_px),
91
+ "width": float(width),
92
+ "height": float(height),
93
+ }
94
+
95
+ except Exception:
96
+ return None
97
+
98
+
99
+ def display_to_image(
100
+ display_x: float,
101
+ display_y: float,
102
+ fig: Figure,
103
+ tight_bbox: Bbox,
104
+ img_width: int,
105
+ img_height: int,
106
+ scale_x: float,
107
+ scale_y: float,
108
+ pad_inches: float,
109
+ saved_height_inches: float,
110
+ ) -> Optional[List[float]]:
111
+ """
112
+ Transform display coordinates to image pixel coordinates.
113
+
114
+ Returns
115
+ -------
116
+ list or None
117
+ [x, y] in image pixels.
118
+ """
119
+ try:
120
+ dpi = fig.dpi
121
+
122
+ # Convert to inches
123
+ x_inches = display_x / dpi
124
+ y_inches = display_y / dpi
125
+
126
+ # Transform to saved image coordinates with padding
127
+ x_rel = x_inches - tight_bbox.x0 + pad_inches
128
+
129
+ # Y coordinate flip: matplotlib Y=0 at bottom, image Y=0 at top
130
+ y_rel = saved_height_inches - (y_inches - tight_bbox.y0 + pad_inches)
131
+
132
+ # Scale to image pixels
133
+ x_px = x_rel * scale_x
134
+ y_px = y_rel * scale_y
135
+
136
+ # Clamp
137
+ x_px = max(0, min(x_px, img_width))
138
+ y_px = max(0, min(y_px, img_height))
139
+
140
+ return [float(x_px), float(y_px)]
141
+
142
+ except Exception:
143
+ return None
144
+
145
+
146
+ __all__ = ["transform_bbox", "display_to_image"]
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Call-specific override application for element properties.
5
+
6
+ This is the SINGLE SOURCE OF TRUTH for applying element-level changes.
7
+ Both initial render and re-render use this same function through apply_overrides().
8
+ """
9
+
10
+ from typing import Any, Dict
11
+
12
+ from matplotlib.figure import Figure
13
+
14
+
15
+ def apply_call_overrides(
16
+ fig: Figure, call_overrides: Dict[str, Dict[str, Any]], record: Any
17
+ ) -> None:
18
+ """Apply call-specific overrides to figure elements.
19
+
20
+ Parameters
21
+ ----------
22
+ fig : Figure
23
+ Matplotlib figure.
24
+ call_overrides : dict
25
+ Mapping from call_id to {param: value} overrides.
26
+ record : FigureRecord
27
+ Recording record to find call metadata.
28
+ """
29
+ from matplotlib.patches import Wedge
30
+
31
+ axes_list = fig.get_axes()
32
+
33
+ # Build mapping from ax_key to axes index
34
+ # ax_keys are in format "ax_{row}_{col}", need to map to actual axes indices
35
+ ax_keys_sorted = sorted(record.axes.keys())
36
+ ax_key_to_index = {key: idx for idx, key in enumerate(ax_keys_sorted)}
37
+
38
+ for call_id, params in call_overrides.items():
39
+ # Find the call in record to get function type, ax_index, and call position
40
+ call_function = None
41
+ ax_index = None
42
+ ax_record_found = None
43
+ for ax_key, ax_record in record.axes.items():
44
+ for call in ax_record.calls:
45
+ if call.id == call_id:
46
+ call_function = call.function
47
+ ax_record_found = ax_record
48
+ # Use sorted key order to get correct axes index
49
+ ax_index = ax_key_to_index.get(ax_key, 0)
50
+ break
51
+ if call_function:
52
+ break
53
+
54
+ if call_function is None or ax_index is None or ax_index >= len(axes_list):
55
+ continue
56
+
57
+ ax = axes_list[ax_index]
58
+
59
+ # Apply overrides based on plot type
60
+ for param, value in params.items():
61
+ if call_function in ("bar", "barh", "hist"):
62
+ # Bar/hist creates multiple patches per call - apply to ALL
63
+ _apply_bar_override(ax, ax_record_found, call_id, param, value)
64
+ elif call_function == "plot":
65
+ _apply_line_override(ax, ax_record_found, call_id, param, value)
66
+ elif call_function == "scatter":
67
+ _apply_scatter_override(ax, ax_record_found, call_id, param, value)
68
+ elif call_function == "pie":
69
+ # Pie wedges - apply to all wedges for this call
70
+ wedges = [p for p in ax.patches if isinstance(p, Wedge)]
71
+ for wedge in wedges:
72
+ _apply_patch_param(wedge, param, value)
73
+ elif call_function in ("fill_between", "fill_betweenx"):
74
+ _apply_fill_override(ax, ax_record_found, call_id, param, value)
75
+
76
+
77
+ def _apply_bar_override(ax, ax_record, call_id, param, value):
78
+ """Apply override to bar/hist patches for a specific call."""
79
+ from matplotlib.patches import Rectangle
80
+
81
+ rectangles = [p for p in ax.patches if isinstance(p, Rectangle)]
82
+ if not rectangles:
83
+ return
84
+
85
+ # Find all bar/hist calls to determine grouping
86
+ bar_calls = [c for c in ax_record.calls if c.function in ("bar", "barh", "hist")]
87
+ if not bar_calls:
88
+ return
89
+
90
+ # Find which call index this is
91
+ call_idx = next((i for i, c in enumerate(bar_calls) if c.id == call_id), None)
92
+ if call_idx is None:
93
+ return
94
+
95
+ # Distribute patches among calls
96
+ patches_per_call = len(rectangles) // len(bar_calls) if bar_calls else 1
97
+ start_idx = call_idx * patches_per_call
98
+ end_idx = start_idx + patches_per_call
99
+
100
+ # Apply to all patches for this call
101
+ for patch in rectangles[start_idx:end_idx]:
102
+ _apply_patch_param(patch, param, value)
103
+
104
+
105
+ def _apply_line_override(ax, ax_record, call_id, param, value):
106
+ """Apply override to line for a specific call."""
107
+ lines = [line for line in ax.get_lines() if not line.get_label().startswith("_")]
108
+ line_calls = [c for c in ax_record.calls if c.function == "plot"]
109
+
110
+ call_idx = next((i for i, c in enumerate(line_calls) if c.id == call_id), None)
111
+ if call_idx is not None and call_idx < len(lines):
112
+ _apply_line_param(lines[call_idx], param, value)
113
+
114
+
115
+ def _apply_scatter_override(ax, ax_record, call_id, param, value):
116
+ """Apply override to scatter collection for a specific call."""
117
+ from matplotlib.collections import PathCollection
118
+
119
+ collections = [c for c in ax.collections if isinstance(c, PathCollection)]
120
+ scatter_calls = [c for c in ax_record.calls if c.function == "scatter"]
121
+
122
+ call_idx = next((i for i, c in enumerate(scatter_calls) if c.id == call_id), None)
123
+ if call_idx is not None and call_idx < len(collections):
124
+ _apply_collection_param(collections[call_idx], param, value)
125
+
126
+
127
+ def _apply_fill_override(ax, ax_record, call_id, param, value):
128
+ """Apply override to fill_between for a specific call."""
129
+ from matplotlib.collections import PolyCollection
130
+
131
+ fills = [c for c in ax.collections if isinstance(c, PolyCollection)]
132
+ fill_calls = [
133
+ c for c in ax_record.calls if c.function in ("fill_between", "fill_betweenx")
134
+ ]
135
+
136
+ call_idx = next((i for i, c in enumerate(fill_calls) if c.id == call_id), None)
137
+ if call_idx is not None and call_idx < len(fills):
138
+ _apply_collection_param(fills[call_idx], param, value)
139
+
140
+
141
+ def _apply_patch_param(patch: Any, param: str, value: Any) -> None:
142
+ """Apply parameter to a patch (bar, wedge, etc.)."""
143
+ if param == "color":
144
+ patch.set_facecolor(value)
145
+ elif param == "edgecolor":
146
+ patch.set_edgecolor(value)
147
+ elif param == "linewidth":
148
+ patch.set_linewidth(value)
149
+ elif param == "alpha":
150
+ patch.set_alpha(value)
151
+
152
+
153
+ def _apply_line_param(line: Any, param: str, value: Any) -> None:
154
+ """Apply parameter to a line."""
155
+ if param == "color":
156
+ line.set_color(value)
157
+ elif param == "linewidth":
158
+ line.set_linewidth(value)
159
+ elif param == "linestyle":
160
+ line.set_linestyle(value)
161
+ elif param == "alpha":
162
+ line.set_alpha(value)
163
+ elif param == "marker":
164
+ line.set_marker(value)
165
+ elif param == "markersize":
166
+ line.set_markersize(value)
167
+
168
+
169
+ def _apply_collection_param(coll: Any, param: str, value: Any) -> None:
170
+ """Apply parameter to a collection (scatter, fill)."""
171
+ if param in ("color", "c"):
172
+ coll.set_facecolors(value)
173
+ elif param == "edgecolor":
174
+ coll.set_edgecolors(value)
175
+ elif param == "s":
176
+ coll.set_sizes([value] if not hasattr(value, "__len__") else value)
177
+ elif param == "alpha":
178
+ coll.set_alpha(value)
179
+
180
+
181
+ __all__ = ["apply_call_overrides"]
182
+
183
+ # EOF
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Plot handlers for datatable plotting functionality."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def dispatch_plot(ax, plot_type, plot_data, columns):
9
+ """Dispatch plot based on type and data.
10
+
11
+ Args:
12
+ ax: Matplotlib axes object
13
+ plot_type: Frontend plot type name
14
+ plot_data: Dict mapping column names to data arrays
15
+ columns: List of column names in order
16
+
17
+ Returns:
18
+ True on success
19
+
20
+ Raises:
21
+ ValueError: If plot type is unknown
22
+ """
23
+ # Map frontend names to matplotlib method names
24
+ method_name = plot_type
25
+ if plot_type == "line":
26
+ method_name = "plot"
27
+ elif plot_type == "histogram":
28
+ method_name = "hist"
29
+
30
+ # Get the plotting method
31
+ plot_method = getattr(ax, method_name, None)
32
+ if plot_method is None:
33
+ raise ValueError(f"Unknown plot type: {plot_type}")
34
+
35
+ # Prepare data arrays
36
+ data_arrays = [np.array(plot_data.get(c, [])) for c in columns]
37
+
38
+ # Handle decoration methods
39
+ if _handle_decoration(ax, method_name, data_arrays):
40
+ return True
41
+
42
+ # Handle specialized plot types
43
+ if _handle_specialized(ax, plot_method, method_name, data_arrays, columns):
44
+ return True
45
+
46
+ # Handle standard xy plots
47
+ _handle_standard_xy(ax, plot_method, method_name, data_arrays, columns)
48
+ return True
49
+
50
+
51
+ def _handle_decoration(ax, method_name, data_arrays):
52
+ """Handle decoration methods (scalar-based, iterate over rows)."""
53
+ decoration_methods = {
54
+ "text",
55
+ "annotate",
56
+ "arrow",
57
+ "axhline",
58
+ "axvline",
59
+ "axhspan",
60
+ "axvspan",
61
+ }
62
+
63
+ if method_name not in decoration_methods:
64
+ return False
65
+
66
+ n_rows = len(data_arrays[0]) if data_arrays else 0
67
+
68
+ for row_idx in range(n_rows):
69
+ row_vals = [arr[row_idx] for arr in data_arrays]
70
+
71
+ if method_name == "text" and len(row_vals) >= 2:
72
+ s = str(row_vals[2]) if len(row_vals) >= 3 else ""
73
+ ax.text(row_vals[0], row_vals[1], s)
74
+
75
+ elif method_name == "annotate":
76
+ if len(row_vals) >= 3:
77
+ ax.annotate(str(row_vals[0]), xy=(row_vals[1], row_vals[2]))
78
+ elif len(row_vals) == 2:
79
+ ax.annotate("", xy=(row_vals[0], row_vals[1]))
80
+
81
+ elif method_name == "arrow" and len(row_vals) >= 4:
82
+ ax.arrow(
83
+ row_vals[0],
84
+ row_vals[1],
85
+ row_vals[2],
86
+ row_vals[3],
87
+ head_width=0.1,
88
+ head_length=0.05,
89
+ )
90
+
91
+ elif method_name == "axhline" and len(row_vals) >= 1:
92
+ ax.axhline(y=row_vals[0])
93
+
94
+ elif method_name == "axvline" and len(row_vals) >= 1:
95
+ ax.axvline(x=row_vals[0])
96
+
97
+ elif method_name == "axhspan" and len(row_vals) >= 2:
98
+ ax.axhspan(row_vals[0], row_vals[1], alpha=0.3)
99
+
100
+ elif method_name == "axvspan" and len(row_vals) >= 2:
101
+ ax.axvspan(row_vals[0], row_vals[1], alpha=0.3)
102
+
103
+ return True
104
+
105
+
106
+ def _handle_specialized(ax, plot_method, method_name, data_arrays, columns):
107
+ """Handle specialized plot types that need custom argument handling."""
108
+ if method_name in ("boxplot", "violinplot"):
109
+ if method_name == "boxplot":
110
+ plot_method(data_arrays, labels=columns)
111
+ else:
112
+ plot_method(data_arrays)
113
+ ax.set_xticks(range(1, len(columns) + 1))
114
+ ax.set_xticklabels(columns)
115
+ return True
116
+
117
+ if method_name == "pie":
118
+ labels = columns[1:] if len(columns) > 1 else None
119
+ plot_method(data_arrays[0], labels=labels, autopct="%1.1f%%")
120
+ return True
121
+
122
+ if method_name in (
123
+ "hist",
124
+ "acorr",
125
+ "psd",
126
+ "specgram",
127
+ "angle_spectrum",
128
+ "phase_spectrum",
129
+ "magnitude_spectrum",
130
+ ):
131
+ for i, arr in enumerate(data_arrays):
132
+ plot_method(arr, label=columns[i])
133
+ return True
134
+
135
+ if method_name in ("hist2d", "hexbin", "xcorr", "csd", "cohere"):
136
+ if len(data_arrays) >= 2:
137
+ plot_method(data_arrays[0], data_arrays[1])
138
+ return True
139
+
140
+ if method_name in ("fill_between", "fill_betweenx"):
141
+ if len(data_arrays) >= 3:
142
+ plot_method(data_arrays[0], data_arrays[1], data_arrays[2], alpha=0.5)
143
+ elif len(data_arrays) >= 2:
144
+ plot_method(data_arrays[0], data_arrays[1], alpha=0.5)
145
+ return True
146
+
147
+ if method_name == "errorbar" and len(data_arrays) >= 3:
148
+ plot_method(
149
+ data_arrays[0], data_arrays[1], yerr=data_arrays[2], fmt="o-", capsize=3
150
+ )
151
+ return True
152
+
153
+ if method_name in ("imshow", "matshow"):
154
+ if len(data_arrays) == 1:
155
+ arr = data_arrays[0]
156
+ plot_method(arr.reshape(-1, 1) if arr.ndim == 1 else arr)
157
+ else:
158
+ plot_method(np.column_stack(data_arrays))
159
+ return True
160
+
161
+ if method_name in ("contour", "contourf", "pcolor", "pcolormesh"):
162
+ if len(data_arrays) == 1:
163
+ arr = data_arrays[0]
164
+ plot_method(arr.reshape(-1, 1) if arr.ndim == 1 else arr)
165
+ else:
166
+ plot_method(np.column_stack(data_arrays))
167
+ return True
168
+
169
+ if method_name in ("quiver", "barbs", "streamplot"):
170
+ if len(data_arrays) >= 4:
171
+ plot_method(data_arrays[0], data_arrays[1], data_arrays[2], data_arrays[3])
172
+ elif len(data_arrays) >= 2:
173
+ x = np.arange(len(data_arrays[0]))
174
+ y = np.arange(len(data_arrays[0]))
175
+ plot_method(x, y, data_arrays[0], data_arrays[1])
176
+ return True
177
+
178
+ if method_name == "eventplot":
179
+ plot_method(data_arrays)
180
+ return True
181
+
182
+ return False
183
+
184
+
185
+ def _handle_standard_xy(ax, plot_method, method_name, data_arrays, columns):
186
+ """Handle standard x, y plots."""
187
+ # Detect x and y columns
188
+ x_idx = None
189
+ y_indices = []
190
+ for i, col in enumerate(columns):
191
+ if col.endswith("_x") or col.lower() == "x":
192
+ x_idx = i
193
+ else:
194
+ y_indices.append(i)
195
+
196
+ if x_idx is not None and y_indices:
197
+ x_data = data_arrays[x_idx]
198
+ y_arrays = [data_arrays[i] for i in y_indices]
199
+ y_cols = [columns[i] for i in y_indices]
200
+ elif len(data_arrays) >= 2:
201
+ x_data = data_arrays[0]
202
+ y_arrays = data_arrays[1:]
203
+ y_cols = columns[1:]
204
+ else:
205
+ x_data = np.arange(len(data_arrays[0]))
206
+ y_arrays = data_arrays
207
+ y_cols = columns
208
+
209
+ for i, y_data in enumerate(y_arrays):
210
+ if method_name == "bar" and len(y_arrays) > 1:
211
+ width = 0.8 / len(y_arrays)
212
+ offset = (i - len(y_arrays) / 2 + 0.5) * width
213
+ plot_method(x_data + offset, y_data, width=width, label=y_cols[i])
214
+ elif method_name == "barh":
215
+ plot_method(x_data, y_data, label=y_cols[i])
216
+ elif method_name in (
217
+ "plot",
218
+ "scatter",
219
+ "step",
220
+ "loglog",
221
+ "semilogx",
222
+ "semilogy",
223
+ ):
224
+ plot_method(x_data, y_data, label=y_cols[i])
225
+ elif method_name == "stem":
226
+ plot_method(x_data, y_data, label=y_cols[i])
227
+ elif method_name == "fill":
228
+ plot_method(x_data, y_data, alpha=0.5, label=y_cols[i])
229
+ elif method_name == "stairs":
230
+ plot_method(y_data, label=y_cols[i])
231
+ elif method_name == "stackplot":
232
+ plot_method(x_data, *y_arrays, labels=y_cols)
233
+ break
234
+ else:
235
+ try:
236
+ plot_method(x_data, y_data, label=y_cols[i])
237
+ except TypeError:
238
+ plot_method(y_data, label=y_cols[i])
239
+
240
+ # Set labels
241
+ if len(columns) >= 2:
242
+ ax.set_xlabel(columns[0])
243
+ if len(y_cols) == 1:
244
+ ax.set_ylabel(y_cols[0])
245
+ if len(y_cols) > 1:
246
+ ax.legend()
247
+
248
+
249
+ __all__ = ["dispatch_plot"]