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,505 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel drag-to-move JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Detecting mousedown on axes/panel elements
7
+ - Handling drag movement with visual feedback
8
+ - Updating panel position on drop
9
+
10
+ Coordinates are in mm with upper-left origin.
11
+ """
12
+
13
+ SCRIPTS_PANEL_DRAG = """
14
+ // ===== PANEL DRAG-TO-MOVE (mm, upper-left origin) =====
15
+
16
+ let isDraggingPanel = false;
17
+ let draggedPanelIndex = null;
18
+ let dragStartPos = null;
19
+ let dragStartPanelPos = null;
20
+ let panelDragOverlay = null;
21
+ let panelBboxDragOverlay = null; // Outer panel bbox overlay
22
+ let panelHoverOverlay = null;
23
+ let hoveredPanelIndex = null;
24
+ let dragStartPanelBbox = null; // Initial panel bbox in pixels
25
+ let panelBboxOffset = null; // Offset from axis to panel bbox (in mm)
26
+
27
+ // Initialize panel drag functionality
28
+ function initPanelDrag() {
29
+ console.log('[PanelDrag] initPanelDrag called');
30
+ const zoomContainer = document.getElementById('zoom-container');
31
+ if (!zoomContainer) {
32
+ console.error('[PanelDrag] zoom-container not found!');
33
+ return;
34
+ }
35
+
36
+ // Add mouse event listeners to zoom container
37
+ zoomContainer.addEventListener('mousedown', handlePanelDragStart);
38
+ document.addEventListener('mousemove', handlePanelDragMove);
39
+ document.addEventListener('mouseup', handlePanelDragEnd);
40
+ console.log('[PanelDrag] Event listeners attached');
41
+
42
+ // Create drag overlay for axis bbox (inner, subtle orange)
43
+ panelDragOverlay = document.createElement('div');
44
+ panelDragOverlay.id = 'panel-drag-overlay';
45
+ panelDragOverlay.style.cssText = `
46
+ position: absolute;
47
+ border: 2px dashed #f59e0b;
48
+ background: rgba(245, 158, 11, 0.08);
49
+ pointer-events: none;
50
+ display: none;
51
+ z-index: 999;
52
+ `;
53
+ zoomContainer.appendChild(panelDragOverlay);
54
+
55
+ // Create outer panel bbox overlay (prominent blue)
56
+ panelBboxDragOverlay = document.createElement('div');
57
+ panelBboxDragOverlay.id = 'panel-bbox-drag-overlay';
58
+ panelBboxDragOverlay.style.cssText = `
59
+ position: absolute;
60
+ border: 2px dashed #2563eb;
61
+ background: rgba(37, 99, 235, 0.05);
62
+ pointer-events: none;
63
+ display: none;
64
+ z-index: 1000;
65
+ `;
66
+ zoomContainer.appendChild(panelBboxDragOverlay);
67
+
68
+ // Create hover overlay element for visual feedback
69
+ panelHoverOverlay = document.createElement('div');
70
+ panelHoverOverlay.id = 'panel-hover-overlay';
71
+ panelHoverOverlay.style.cssText = `
72
+ position: absolute;
73
+ border: 2px solid rgba(37, 99, 235, 0.5);
74
+ background: rgba(37, 99, 235, 0.05);
75
+ pointer-events: none;
76
+ display: none;
77
+ z-index: 999;
78
+ transition: opacity 0.15s ease-in-out;
79
+ `;
80
+ zoomContainer.appendChild(panelHoverOverlay);
81
+
82
+ // Add hover detection on zoom container
83
+ zoomContainer.addEventListener('mousemove', handlePanelHover);
84
+ zoomContainer.addEventListener('mouseleave', hidePanelHover);
85
+
86
+ console.log('[PanelDrag] Overlays created');
87
+ }
88
+
89
+ // Handle mouse down - check if on a panel/axes (only drag from empty panel area)
90
+ function handlePanelDragStart(event) {
91
+ if (event.ctrlKey || event.metaKey || event.altKey) return; // Skip modifier keys
92
+
93
+ // Only allow drag from axes/imshow/contour/quadmesh/quiver (fills panel area)
94
+ const target = event.target;
95
+ const targetKey = target.getAttribute ? target.getAttribute('data-key') : null;
96
+ if (targetKey && typeof currentBboxes !== 'undefined' && currentBboxes[targetKey]) {
97
+ const elemType = currentBboxes[targetKey].type;
98
+ const dragAllowedTypes = ['axes', 'image', 'contour', 'quadmesh', 'quiver'];
99
+ if (elemType && !dragAllowedTypes.includes(elemType)) return;
100
+ }
101
+
102
+ const img = document.getElementById('preview-image');
103
+ if (!img || !figSize.width_mm || !figSize.height_mm) return;
104
+
105
+ const rect = img.getBoundingClientRect();
106
+ const x = event.clientX - rect.left;
107
+ const y = event.clientY - rect.top;
108
+
109
+ // Convert to mm coordinates (upper-left origin)
110
+ const mmX = (x / rect.width) * figSize.width_mm;
111
+ const mmY = (y / rect.height) * figSize.height_mm;
112
+
113
+ // Find which panel was clicked (using expanded bounds including labels)
114
+ const panelIndex = findPanelAtPositionMm(mmX, mmY);
115
+ console.log('[PanelDrag] Click at mm:', mmX.toFixed(1), mmY.toFixed(1), '-> panel:', panelIndex);
116
+
117
+ if (panelIndex !== null) {
118
+ event.preventDefault();
119
+ event.stopPropagation();
120
+
121
+ // Capture state before drag for undo
122
+ if (typeof pushToHistory === 'function') {
123
+ pushToHistory();
124
+ }
125
+
126
+ isDraggingPanel = true;
127
+ draggedPanelIndex = panelIndex;
128
+ dragStartPos = { x: event.clientX, y: event.clientY };
129
+
130
+ // Hide hover overlay when starting drag
131
+ hidePanelHover();
132
+
133
+ // Get current panel position (in mm)
134
+ const axKey = Object.keys(panelPositions).sort()[panelIndex];
135
+ const pos = panelPositions[axKey];
136
+ dragStartPanelPos = { ...pos };
137
+
138
+ // Get panel bbox (outer bounds including labels) and calculate offset from axis
139
+ const panelBboxes = currentBboxes?._meta?.panel_bboxes;
140
+ if (panelBboxes && panelBboxes[panelIndex] && img.naturalWidth) {
141
+ dragStartPanelBbox = { ...panelBboxes[panelIndex] };
142
+ // Convert panel bbox to mm and calculate offset from axis position
143
+ const pxToMmX = figSize.width_mm / img.naturalWidth;
144
+ const pxToMmY = figSize.height_mm / img.naturalHeight;
145
+ panelBboxOffset = {
146
+ left: dragStartPanelBbox.x * pxToMmX - pos.left,
147
+ top: dragStartPanelBbox.y * pxToMmY - pos.top,
148
+ width: dragStartPanelBbox.width * pxToMmX,
149
+ height: dragStartPanelBbox.height * pxToMmY
150
+ };
151
+ } else {
152
+ dragStartPanelBbox = null;
153
+ panelBboxOffset = null;
154
+ }
155
+
156
+ // Create overlay if it doesn't exist
157
+ if (!panelDragOverlay) {
158
+ console.log('[PanelDrag] Creating overlay on-demand');
159
+ const zoomContainer = document.getElementById('zoom-container');
160
+ if (zoomContainer) {
161
+ panelDragOverlay = document.createElement('div');
162
+ panelDragOverlay.id = 'panel-drag-overlay';
163
+ panelDragOverlay.style.cssText = `
164
+ position: absolute;
165
+ border: 2px dashed #2563eb;
166
+ background: rgba(37, 99, 235, 0.1);
167
+ pointer-events: none;
168
+ display: none;
169
+ z-index: 1000;
170
+ `;
171
+ zoomContainer.appendChild(panelDragOverlay);
172
+ console.log('[PanelDrag] Overlay created on-demand');
173
+ }
174
+ }
175
+
176
+ // Show drag overlay (axis bbox)
177
+ if (panelDragOverlay) {
178
+ updateDragOverlayMm(pos, rect);
179
+ panelDragOverlay.style.display = 'block';
180
+ console.log('[PanelDrag] Axis overlay shown');
181
+ } else {
182
+ console.warn('[PanelDrag] Overlay still null after creation attempt');
183
+ }
184
+
185
+ // Show panel bbox overlay (outer bounds) - follows snapped axis position
186
+ if (panelBboxDragOverlay && panelBboxOffset) {
187
+ updatePanelBboxDragOverlayMm(pos, rect);
188
+ panelBboxDragOverlay.style.display = 'block';
189
+ console.log('[PanelDrag] Panel bbox overlay shown');
190
+ }
191
+
192
+ // Create and show panel snapshot for visual feedback
193
+ if (typeof startSnapshotDrag === 'function') {
194
+ startSnapshotDrag(panelIndex, rect, pos);
195
+ }
196
+
197
+ // Change cursor
198
+ document.body.style.cursor = 'move';
199
+
200
+ console.log('Started dragging panel', panelIndex);
201
+ }
202
+ }
203
+
204
+ // Find which panel contains the given position (in mm, upper-left origin)
205
+ // Uses expanded bounds to include title, labels, and tick areas
206
+ function findPanelAtPositionMm(mmX, mmY) {
207
+ const axKeys = Object.keys(panelPositions).sort();
208
+
209
+ // Margins in mm to expand panel bounds for labels/title/ticks
210
+ const marginLeft = 15; // Space for y-axis label and ticks
211
+ const marginRight = 5; // Small buffer on right
212
+ const marginTop = 8; // Space for title
213
+ const marginBottom = 12; // Space for x-axis label and ticks
214
+
215
+ for (let i = 0; i < axKeys.length; i++) {
216
+ const pos = panelPositions[axKeys[i]];
217
+
218
+ // Expanded bounds including label/title areas
219
+ const expandedLeft = Math.max(0, pos.left - marginLeft);
220
+ const expandedTop = Math.max(0, pos.top - marginTop);
221
+ const expandedRight = Math.min(figSize.width_mm, pos.left + pos.width + marginRight);
222
+ const expandedBottom = Math.min(figSize.height_mm, pos.top + pos.height + marginBottom);
223
+
224
+ // Check if point is within expanded panel bounds
225
+ if (mmX >= expandedLeft && mmX <= expandedRight &&
226
+ mmY >= expandedTop && mmY <= expandedBottom) {
227
+ return i;
228
+ }
229
+ }
230
+ return null;
231
+ }
232
+
233
+ // Handle mouse hover over panels - show visual feedback
234
+ function handlePanelHover(event) {
235
+ // Skip if dragging
236
+ if (isDraggingPanel) {
237
+ hidePanelHover();
238
+ return;
239
+ }
240
+
241
+ const img = document.getElementById('preview-image');
242
+ if (!img || !figSize.width_mm || !figSize.height_mm) return;
243
+
244
+ const rect = img.getBoundingClientRect();
245
+ const x = event.clientX - rect.left;
246
+ const y = event.clientY - rect.top;
247
+
248
+ // Check if mouse is within image bounds
249
+ if (x < 0 || x > rect.width || y < 0 || y > rect.height) {
250
+ hidePanelHover();
251
+ return;
252
+ }
253
+
254
+ // Convert to mm coordinates
255
+ const mmX = (x / rect.width) * figSize.width_mm;
256
+ const mmY = (y / rect.height) * figSize.height_mm;
257
+
258
+ // Find panel at position
259
+ const panelIndex = findPanelAtPositionMm(mmX, mmY);
260
+
261
+ if (panelIndex !== null && panelIndex !== hoveredPanelIndex) {
262
+ showPanelHover(panelIndex, rect);
263
+ } else if (panelIndex === null) {
264
+ hidePanelHover();
265
+ }
266
+ }
267
+
268
+ // Show hover feedback for a panel
269
+ function showPanelHover(panelIndex, imgRect) {
270
+ if (!panelHoverOverlay) return;
271
+
272
+ hoveredPanelIndex = panelIndex;
273
+
274
+ // Get panel position
275
+ const axKey = Object.keys(panelPositions).sort()[panelIndex];
276
+ const pos = panelPositions[axKey];
277
+ if (!pos) return;
278
+
279
+ // Convert mm to screen pixels
280
+ const scaleX = imgRect.width / figSize.width_mm;
281
+ const scaleY = imgRect.height / figSize.height_mm;
282
+
283
+ const left = pos.left * scaleX;
284
+ const top = pos.top * scaleY;
285
+ const width = pos.width * scaleX;
286
+ const height = pos.height * scaleY;
287
+
288
+ panelHoverOverlay.style.left = `${left}px`;
289
+ panelHoverOverlay.style.top = `${top}px`;
290
+ panelHoverOverlay.style.width = `${width}px`;
291
+ panelHoverOverlay.style.height = `${height}px`;
292
+ panelHoverOverlay.style.display = 'block';
293
+
294
+ // Change cursor to indicate draggable
295
+ document.body.style.cursor = 'move';
296
+ }
297
+
298
+ // Hide hover feedback
299
+ function hidePanelHover() {
300
+ if (panelHoverOverlay) {
301
+ panelHoverOverlay.style.display = 'none';
302
+ }
303
+ hoveredPanelIndex = null;
304
+
305
+ // Reset cursor if not dragging
306
+ if (!isDraggingPanel) {
307
+ document.body.style.cursor = '';
308
+ }
309
+ }
310
+
311
+ // Handle mouse move during drag
312
+ function handlePanelDragMove(event) {
313
+ if (!isDraggingPanel) return;
314
+
315
+ event.preventDefault();
316
+
317
+ const img = document.getElementById('preview-image');
318
+ if (!img) return;
319
+
320
+ const rect = img.getBoundingClientRect();
321
+
322
+ // Calculate delta in mm
323
+ const deltaMmX = (event.clientX - dragStartPos.x) / rect.width * figSize.width_mm;
324
+ const deltaMmY = (event.clientY - dragStartPos.y) / rect.height * figSize.height_mm;
325
+
326
+ // Calculate raw new position (clamped to figure bounds)
327
+ let newLeft = Math.max(0, Math.min(figSize.width_mm - dragStartPanelPos.width, dragStartPanelPos.left + deltaMmX));
328
+ let newTop = Math.max(0, Math.min(figSize.height_mm - dragStartPanelPos.height, dragStartPanelPos.top + deltaMmY));
329
+
330
+ // Apply snapping (Alt key disables snapping for fine control)
331
+ let snapResult = { pos: { left: newLeft, top: newTop }, guides: [] };
332
+ if (typeof applySnapping === 'function' && !event.altKey) {
333
+ snapResult = applySnapping(newLeft, newTop, dragStartPanelPos.width, dragStartPanelPos.height, draggedPanelIndex);
334
+ newLeft = snapResult.pos.left;
335
+ newTop = snapResult.pos.top;
336
+ }
337
+
338
+ const newPos = {
339
+ left: newLeft,
340
+ top: newTop,
341
+ width: dragStartPanelPos.width,
342
+ height: dragStartPanelPos.height
343
+ };
344
+
345
+ // Update visual overlays - both use snapped position in mm
346
+ updateDragOverlayMm(newPos, rect);
347
+ updatePanelBboxDragOverlayMm(newPos, rect);
348
+
349
+ // Update snapshot position
350
+ if (typeof updateSnapshotPosition === 'function') {
351
+ updateSnapshotPosition(newPos, rect);
352
+ }
353
+
354
+ // Show/hide alignment guides
355
+ if (typeof showSnapGuides === 'function') {
356
+ showSnapGuides(snapResult.guides, rect);
357
+ }
358
+ }
359
+
360
+ // Update axis drag overlay (mm to screen pixels)
361
+ function updateDragOverlayMm(pos, imgRect) {
362
+ if (!panelDragOverlay || !figSize.width_mm) return;
363
+ const scaleX = imgRect.width / figSize.width_mm, scaleY = imgRect.height / figSize.height_mm;
364
+ panelDragOverlay.style.left = `${pos.left * scaleX}px`;
365
+ panelDragOverlay.style.top = `${pos.top * scaleY}px`;
366
+ panelDragOverlay.style.width = `${pos.width * scaleX}px`;
367
+ panelDragOverlay.style.height = `${pos.height * scaleY}px`;
368
+ }
369
+
370
+ // Update panel bbox overlay based on snapped axis position (in mm)
371
+ function updatePanelBboxDragOverlayMm(axisPos, imgRect) {
372
+ if (!panelBboxDragOverlay || !panelBboxOffset || !figSize.width_mm) return;
373
+ const scaleX = imgRect.width / figSize.width_mm, scaleY = imgRect.height / figSize.height_mm;
374
+ // Panel bbox position = axis position + offset (both in mm, converted to screen pixels)
375
+ panelBboxDragOverlay.style.left = `${(axisPos.left + panelBboxOffset.left) * scaleX}px`;
376
+ panelBboxDragOverlay.style.top = `${(axisPos.top + panelBboxOffset.top) * scaleY}px`;
377
+ panelBboxDragOverlay.style.width = `${panelBboxOffset.width * scaleX}px`;
378
+ panelBboxDragOverlay.style.height = `${panelBboxOffset.height * scaleY}px`;
379
+ }
380
+
381
+ // Handle mouse up - complete the drag
382
+ async function handlePanelDragEnd(event) {
383
+ console.log('[PanelDrag] handlePanelDragEnd called, isDraggingPanel:', isDraggingPanel);
384
+ if (!isDraggingPanel) return;
385
+
386
+ // Hide overlays, snapshot, and snap guides
387
+ if (panelDragOverlay) panelDragOverlay.style.display = 'none';
388
+ if (panelBboxDragOverlay) panelBboxDragOverlay.style.display = 'none';
389
+ if (typeof endSnapshotDrag === 'function') endSnapshotDrag();
390
+ if (typeof hideSnapGuides === 'function') hideSnapGuides();
391
+ document.body.style.cursor = '';
392
+ dragStartPanelBbox = null;
393
+ panelBboxOffset = null;
394
+
395
+ const img = document.getElementById('preview-image');
396
+ if (!img) {
397
+ isDraggingPanel = false;
398
+ return;
399
+ }
400
+
401
+ const rect = img.getBoundingClientRect();
402
+
403
+ // Calculate final position in mm
404
+ const deltaMmX = (event.clientX - dragStartPos.x) / rect.width * figSize.width_mm;
405
+ const deltaMmY = (event.clientY - dragStartPos.y) / rect.height * figSize.height_mm;
406
+
407
+ let newLeft = Math.max(0, Math.min(figSize.width_mm - dragStartPanelPos.width, dragStartPanelPos.left + deltaMmX));
408
+ let newTop = Math.max(0, Math.min(figSize.height_mm - dragStartPanelPos.height, dragStartPanelPos.top + deltaMmY));
409
+
410
+ // Apply snapping to final position (unless Alt was held)
411
+ if (typeof applySnapping === 'function' && !event.altKey) {
412
+ const snapResult = applySnapping(newLeft, newTop, dragStartPanelPos.width, dragStartPanelPos.height, draggedPanelIndex);
413
+ newLeft = snapResult.pos.left;
414
+ newTop = snapResult.pos.top;
415
+ }
416
+
417
+ // Only update if position actually changed (threshold in mm)
418
+ const threshold = 1.0; // 1mm threshold
419
+ const deltaLeft = Math.abs(newLeft - dragStartPanelPos.left);
420
+ const deltaTop = Math.abs(newTop - dragStartPanelPos.top);
421
+ console.log('[PanelDrag] Delta: left=', deltaLeft.toFixed(2), 'top=', deltaTop.toFixed(2), 'threshold=', threshold);
422
+
423
+ if (deltaLeft > threshold || deltaTop > threshold) {
424
+ console.log('[PanelDrag] Applying new position:', newLeft.toFixed(2), newTop.toFixed(2));
425
+ // Apply the new position (in mm)
426
+ await applyDraggedPanelPosition(
427
+ draggedPanelIndex,
428
+ newLeft,
429
+ newTop,
430
+ dragStartPanelPos.width,
431
+ dragStartPanelPos.height
432
+ );
433
+ } else {
434
+ console.log('[PanelDrag] Movement below threshold, not updating');
435
+ }
436
+
437
+ // Reset state
438
+ isDraggingPanel = false;
439
+ draggedPanelIndex = null;
440
+ dragStartPos = null;
441
+ dragStartPanelPos = null;
442
+ console.log('[PanelDrag] Drag state reset');
443
+ }
444
+
445
+ // Apply the dragged panel position to the server (values in mm)
446
+ async function applyDraggedPanelPosition(axIndex, left, top, width, height) {
447
+ document.body.classList.add('loading');
448
+
449
+ try {
450
+ const response = await fetch('/update_axes_position', {
451
+ method: 'POST',
452
+ headers: { 'Content-Type': 'application/json' },
453
+ body: JSON.stringify({
454
+ ax_index: axIndex,
455
+ left: left,
456
+ top: top,
457
+ width: width,
458
+ height: height
459
+ })
460
+ });
461
+
462
+ const data = await response.json();
463
+
464
+ if (data.success) {
465
+ // Update preview image and wait for it to load
466
+ const img = document.getElementById('preview-image');
467
+ if (img) {
468
+ await new Promise((resolve) => {
469
+ img.onload = resolve;
470
+ img.src = 'data:image/png;base64,' + data.image;
471
+ });
472
+ }
473
+
474
+ // Update image size
475
+ if (data.img_size) {
476
+ currentImgWidth = data.img_size.width;
477
+ currentImgHeight = data.img_size.height;
478
+ }
479
+
480
+ // Update bboxes and hitmap
481
+ currentBboxes = data.bboxes;
482
+ loadHitmap();
483
+ updateHitRegions();
484
+
485
+ // Reload positions (now with correct image dimensions)
486
+ await loadPanelPositions();
487
+
488
+ console.log('Panel position updated via drag');
489
+ } else {
490
+ console.error('Failed to update position:', data.error);
491
+ }
492
+ } catch (error) {
493
+ console.error('Failed to update position:', error);
494
+ }
495
+
496
+ document.body.classList.remove('loading');
497
+ }
498
+
499
+ // Initialize on DOMContentLoaded
500
+ document.addEventListener('DOMContentLoaded', initPanelDrag);
501
+ """
502
+
503
+ __all__ = ["SCRIPTS_PANEL_DRAG"]
504
+
505
+ # EOF
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel drag snapshot functionality with server-side isolated rendering.
4
+
5
+ This module provides clean panel snapshots rendered in isolation (no overlap)
6
+ by fetching from the server, with async caching for smooth UX.
7
+ """
8
+
9
+ SCRIPTS_PANEL_DRAG_SNAPSHOT = """
10
+ // ===== PANEL DRAG SNAPSHOT (DISABLED - corrupts figure state) =====
11
+ // Server-side snapshot rendering was disabled because matplotlib figures
12
+ // are not thread-safe. Modifying visibility to render isolated panels
13
+ // corrupts the shared figure state in Flask's threaded mode.
14
+
15
+ // No-op stubs to prevent errors from panel_drag.py calls
16
+ function startSnapshotDrag(panelIndex, imgRect, initialPos) {
17
+ // Disabled - no snapshot during drag
18
+ }
19
+
20
+ function updateSnapshotPosition(pos, imgRect) {
21
+ // Disabled - no snapshot during drag
22
+ }
23
+
24
+ function endSnapshotDrag() {
25
+ // Disabled - no snapshot during drag
26
+ }
27
+
28
+ // No initialization needed
29
+ """
30
+
31
+ __all__ = ["SCRIPTS_PANEL_DRAG_SNAPSHOT"]
32
+
33
+ # EOF