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,504 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Annotation drag-to-move JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Detecting mousedown on annotation elements (panel labels, text, arrows)
7
+ - Handling drag movement with visual feedback
8
+ - Updating annotation position on drop
9
+
10
+ Annotation coordinates are in axes-relative units (0-1 range).
11
+ """
12
+
13
+ SCRIPTS_ANNOTATION_DRAG = """
14
+ // ===== ANNOTATION DRAG-TO-MOVE =====
15
+
16
+ let isDraggingAnnotation = false;
17
+ let annotationDragStartPos = null;
18
+ let annotationDragStartBbox = null;
19
+ let annotationDragOverlay = null;
20
+ let annotationDragLabel = null;
21
+ let annotationKey = null;
22
+ let annotationAxIndex = 0;
23
+ let annotationDragOffset = null; // Offset from click to bbox corner for precise cursor following
24
+ let annotationOriginalRelPos = null; // Original axes-relative position (anchor point)
25
+ let annotationLastSnappedBbox = null; // Last snapped bbox position (used on mouse up)
26
+
27
+ // Track annotation positions for undo support (axes-relative 0-1 coordinates)
28
+ // Key format: "ax{axIndex}_text{textIndex}" or "ax{axIndex}_panel_label0"
29
+ let annotationPositions = {}; // {ax0_text0: {x, y}, ...}
30
+
31
+ // Initialize annotation positions from current bboxes
32
+ function initAnnotationPositions() {
33
+ if (typeof currentBboxes === 'undefined') return;
34
+
35
+ annotationPositions = {};
36
+ for (const [key, bbox] of Object.entries(currentBboxes)) {
37
+ if (isAnnotationElement(key) && bbox.rel_x !== undefined && bbox.rel_y !== undefined) {
38
+ annotationPositions[key] = {
39
+ x: bbox.rel_x,
40
+ y: bbox.rel_y,
41
+ type: bbox.type || 'text'
42
+ };
43
+ }
44
+ }
45
+ console.log('[AnnotationDrag] Initialized annotation positions:', Object.keys(annotationPositions).length);
46
+ }
47
+
48
+ // Snap configuration for annotations (uses same visual system as panel snap)
49
+ const ANNOTATION_SNAP = {
50
+ enabled: true,
51
+ threshold: 5, // mm - same as panel snap threshold
52
+ magneticZone: 10, // mm - distance where magnetic attraction starts
53
+ snapToEdges: true,
54
+ snapToOtherLabels: true // Snap to other panel labels
55
+ };
56
+
57
+ // Initialize annotation drag functionality
58
+ function initAnnotationDrag() {
59
+ console.log('[AnnotationDrag] initAnnotationDrag called');
60
+ const zoomContainer = document.getElementById('zoom-container');
61
+ if (!zoomContainer) {
62
+ console.error('[AnnotationDrag] zoom-container not found!');
63
+ return;
64
+ }
65
+
66
+ // Create annotation drag overlay element (bounding box)
67
+ annotationDragOverlay = document.createElement('div');
68
+ annotationDragOverlay.id = 'annotation-drag-overlay';
69
+ annotationDragOverlay.style.cssText = `
70
+ position: absolute;
71
+ border: 2px dashed #8b5cf6;
72
+ background: rgba(139, 92, 246, 0.15);
73
+ pointer-events: none;
74
+ display: none;
75
+ z-index: 1001;
76
+ box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3);
77
+ transition: box-shadow 0.1s ease;
78
+ `;
79
+ zoomContainer.appendChild(annotationDragOverlay);
80
+
81
+ // Create label showing text content during drag
82
+ annotationDragLabel = document.createElement('div');
83
+ annotationDragLabel.id = 'annotation-drag-label';
84
+ annotationDragLabel.style.cssText = `
85
+ position: absolute;
86
+ background: #8b5cf6;
87
+ color: white;
88
+ padding: 2px 6px;
89
+ border-radius: 3px;
90
+ font-size: 11px;
91
+ font-weight: bold;
92
+ pointer-events: none;
93
+ display: none;
94
+ z-index: 1002;
95
+ white-space: nowrap;
96
+ `;
97
+ zoomContainer.appendChild(annotationDragLabel);
98
+
99
+ console.log('[AnnotationDrag] Overlay and label created');
100
+ // Note: Snap guides are shared with panel snap (created in initPanelSnap)
101
+
102
+ // Initialize positions after a short delay to ensure bboxes are loaded
103
+ setTimeout(initAnnotationPositions, 500);
104
+ }
105
+
106
+ // Check if an element is an annotation (panel_label, text, arrow)
107
+ function isAnnotationElement(key) {
108
+ if (!key) return false;
109
+ return key.includes('_panel_label') ||
110
+ key.includes('_text_') ||
111
+ key.includes('_arrow_');
112
+ }
113
+
114
+ // Handle annotation drag start (called from hitmap click handler)
115
+ function startAnnotationDrag(event, elemKey) {
116
+ console.log('[AnnotationDrag] startAnnotationDrag called for:', elemKey);
117
+
118
+ const img = document.getElementById('preview-image');
119
+ if (!img) return false;
120
+
121
+ const bbox = currentBboxes[elemKey];
122
+ if (!bbox) return false;
123
+
124
+ event.preventDefault();
125
+ event.stopPropagation();
126
+
127
+ // Capture state before drag for undo
128
+ if (typeof pushToHistory === 'function') {
129
+ pushToHistory();
130
+ }
131
+
132
+ isDraggingAnnotation = true;
133
+ annotationKey = elemKey;
134
+ annotationDragStartPos = { x: event.clientX, y: event.clientY };
135
+ annotationDragStartBbox = { ...bbox };
136
+
137
+ // Store the original axes-relative position (anchor point, not bbox corner)
138
+ // This is the actual text position used by matplotlib
139
+ annotationOriginalRelPos = {
140
+ x: bbox.rel_x !== undefined ? bbox.rel_x : 0,
141
+ y: bbox.rel_y !== undefined ? bbox.rel_y : 0
142
+ };
143
+ console.log('[AnnotationDrag] Original anchor position (rel):', annotationOriginalRelPos.x.toFixed(3), annotationOriginalRelPos.y.toFixed(3));
144
+
145
+ // Calculate offset from click position to bbox corner for precise cursor following
146
+ // This ensures the overlay follows the cursor at exactly the click point
147
+ const rect = img.getBoundingClientRect();
148
+ const scaleX = rect.width / img.naturalWidth;
149
+ const scaleY = rect.height / img.naturalHeight;
150
+ const bboxScreenX = bbox.x * scaleX;
151
+ const bboxScreenY = bbox.y * scaleY;
152
+ // Offset is the distance from bbox corner to click point (in screen pixels)
153
+ annotationDragOffset = {
154
+ x: event.clientX - rect.left - bboxScreenX,
155
+ y: event.clientY - rect.top - bboxScreenY
156
+ };
157
+ console.log('[AnnotationDrag] Click offset from bbox corner:', annotationDragOffset.x.toFixed(1), annotationDragOffset.y.toFixed(1));
158
+
159
+ // Extract axis index from key (e.g., "ax0_panel_label" -> 0)
160
+ const match = elemKey.match(/ax(\\d+)_/);
161
+ annotationAxIndex = match ? parseInt(match[1], 10) : 0;
162
+
163
+ // Show drag overlay
164
+ if (annotationDragOverlay) {
165
+ updateAnnotationDragOverlay(bbox);
166
+ annotationDragOverlay.style.display = 'block';
167
+ // Add glow effect when dragging starts
168
+ annotationDragOverlay.style.boxShadow = '0 4px 12px rgba(139, 92, 246, 0.5)';
169
+ }
170
+
171
+ // Show label with text content
172
+ if (annotationDragLabel && bbox.text) {
173
+ annotationDragLabel.textContent = bbox.text;
174
+ annotationDragLabel.style.display = 'block';
175
+ }
176
+
177
+ // Add temporary event listeners
178
+ document.addEventListener('mousemove', handleAnnotationDragMove);
179
+ document.addEventListener('mouseup', handleAnnotationDragEnd);
180
+
181
+ document.body.style.cursor = 'move';
182
+ console.log('[AnnotationDrag] Started dragging annotation:', bbox.text || elemKey);
183
+ return true;
184
+ }
185
+
186
+ // Handle mouse move during annotation drag
187
+ function handleAnnotationDragMove(event) {
188
+ if (!isDraggingAnnotation) return;
189
+
190
+ event.preventDefault();
191
+
192
+ const img = document.getElementById('preview-image');
193
+ if (!img) return;
194
+
195
+ const rect = img.getBoundingClientRect();
196
+
197
+ // Calculate new bbox position directly from cursor position minus offset
198
+ // This ensures the overlay follows the cursor precisely at the click point
199
+ const screenToImgX = img.naturalWidth / rect.width;
200
+ const screenToImgY = img.naturalHeight / rect.height;
201
+
202
+ // Convert current mouse position to image coordinates, accounting for the click offset
203
+ const cursorScreenX = event.clientX - rect.left - annotationDragOffset.x;
204
+ const cursorScreenY = event.clientY - rect.top - annotationDragOffset.y;
205
+
206
+ let newBbox = {
207
+ x: cursorScreenX * screenToImgX,
208
+ y: cursorScreenY * screenToImgY,
209
+ width: annotationDragStartBbox.width,
210
+ height: annotationDragStartBbox.height
211
+ };
212
+
213
+ // Apply snap and update visual overlay
214
+ const snapResult = applyAnnotationSnap(newBbox, annotationAxIndex);
215
+ updateAnnotationDragOverlay(snapResult.bbox, snapResult.snapped);
216
+
217
+ // Store snapped position for use on mouse up
218
+ annotationLastSnappedBbox = snapResult.bbox;
219
+ }
220
+
221
+ // Update the annotation drag overlay position
222
+ function updateAnnotationDragOverlay(bbox, isSnapped = false) {
223
+ if (!annotationDragOverlay) return;
224
+
225
+ const img = document.getElementById('preview-image');
226
+ if (!img) return;
227
+
228
+ const rect = img.getBoundingClientRect();
229
+ const scaleX = rect.width / img.naturalWidth;
230
+ const scaleY = rect.height / img.naturalHeight;
231
+
232
+ const left = bbox.x * scaleX;
233
+ const top = bbox.y * scaleY;
234
+ const width = bbox.width * scaleX;
235
+ const height = bbox.height * scaleY;
236
+
237
+ annotationDragOverlay.style.left = `${left}px`;
238
+ annotationDragOverlay.style.top = `${top}px`;
239
+ annotationDragOverlay.style.width = `${width}px`;
240
+ annotationDragOverlay.style.height = `${height}px`;
241
+
242
+ // Visual feedback for snap
243
+ if (isSnapped) {
244
+ annotationDragOverlay.style.borderColor = '#22c55e'; // Green when snapped
245
+ annotationDragOverlay.style.boxShadow = '0 4px 12px rgba(34, 197, 94, 0.5)';
246
+ } else {
247
+ annotationDragOverlay.style.borderColor = '#8b5cf6'; // Purple when not snapped
248
+ annotationDragOverlay.style.boxShadow = '0 4px 12px rgba(139, 92, 246, 0.5)';
249
+ }
250
+
251
+ // Update label position (above the overlay)
252
+ if (annotationDragLabel && annotationDragLabel.style.display !== 'none') {
253
+ annotationDragLabel.style.left = `${left}px`;
254
+ annotationDragLabel.style.top = `${top - 20}px`;
255
+ }
256
+ }
257
+
258
+ // Get snap targets from other panel labels (for alignment)
259
+ function getAnnotationSnapTargets(excludeKey) {
260
+ const targets = { h: [], v: [] };
261
+ if (typeof currentBboxes === 'undefined' || !figSize.width_mm) return targets;
262
+ const img = document.getElementById('preview-image');
263
+ if (!img) return targets;
264
+ const pxToMmX = figSize.width_mm / img.naturalWidth;
265
+ const pxToMmY = figSize.height_mm / img.naturalHeight;
266
+ for (const [key, bbox] of Object.entries(currentBboxes)) {
267
+ if (key === excludeKey || !key.includes('_panel_label')) continue;
268
+ const cx = (bbox.x + bbox.width / 2) * pxToMmX;
269
+ const cy = (bbox.y + bbox.height / 2) * pxToMmY;
270
+ targets.v.push({ pos: cx, type: 'label-center' });
271
+ targets.h.push({ pos: cy, type: 'label-center' });
272
+ }
273
+ return targets;
274
+ }
275
+
276
+ // Apply snap using CENTER-TO-CENTER alignment for labels
277
+ // Always snaps to the CLOSEST target within threshold (not the first match)
278
+ function applyAnnotationSnap(newBbox, axIndex) {
279
+ const img = document.getElementById('preview-image');
280
+ const rect = img ? img.getBoundingClientRect() : null;
281
+ if (!ANNOTATION_SNAP.enabled || !img || !figSize.width_mm) {
282
+ if (typeof hideSnapGuides === 'function') hideSnapGuides();
283
+ return { bbox: newBbox, snapped: false, guides: [] };
284
+ }
285
+ const pxToMmX = figSize.width_mm / img.naturalWidth;
286
+ const pxToMmY = figSize.height_mm / img.naturalHeight;
287
+ const threshold = ANNOTATION_SNAP.threshold;
288
+ let snapped = false, snapX = newBbox.x, snapY = newBbox.y;
289
+ const guides = [];
290
+ // Dragged label's CENTER (in mm)
291
+ const cx = (newBbox.x + newBbox.width / 2) * pxToMmX;
292
+ const cy = (newBbox.y + newBbox.height / 2) * pxToMmY;
293
+ const hw = newBbox.width / 2, hh = newBbox.height / 2;
294
+
295
+ // Snap to CLOSEST panel label center (center-to-center alignment)
296
+ if (ANNOTATION_SNAP.snapToOtherLabels && annotationKey) {
297
+ const targets = getAnnotationSnapTargets(annotationKey);
298
+ // Find closest vertical target (X alignment)
299
+ let bestV = null, bestVDist = threshold;
300
+ for (const t of targets.v) {
301
+ const dist = Math.abs(cx - t.pos);
302
+ if (dist < bestVDist) { bestVDist = dist; bestV = t; }
303
+ }
304
+ if (bestV) {
305
+ snapX = bestV.pos / pxToMmX - hw;
306
+ snapped = true;
307
+ guides.push({type:'vertical',pos:bestV.pos,targetType:'label-center',strength:1});
308
+ console.log('[AnnotationSnap] X snap to', bestV.pos.toFixed(1), 'mm (dist:', bestVDist.toFixed(2), 'mm)');
309
+ }
310
+ // Find closest horizontal target (Y alignment)
311
+ let bestH = null, bestHDist = threshold;
312
+ for (const t of targets.h) {
313
+ const dist = Math.abs(cy - t.pos);
314
+ if (dist < bestHDist) { bestHDist = dist; bestH = t; }
315
+ }
316
+ if (bestH) {
317
+ snapY = bestH.pos / pxToMmY - hh;
318
+ snapped = true;
319
+ guides.push({type:'horizontal',pos:bestH.pos,targetType:'label-center',strength:1});
320
+ console.log('[AnnotationSnap] Y snap to', bestH.pos.toFixed(1), 'mm (dist:', bestHDist.toFixed(2), 'mm)');
321
+ }
322
+ }
323
+ // Show/hide snap guides
324
+ if (guides.length > 0 && typeof showSnapGuides === 'function' && rect) showSnapGuides(guides, rect);
325
+ else if (typeof hideSnapGuides === 'function') hideSnapGuides();
326
+ return { bbox: {...newBbox, x: snapX, y: snapY}, snapped: snapped, guides: guides };
327
+ }
328
+
329
+ // Handle mouse up - complete the annotation drag
330
+ async function handleAnnotationDragEnd(event) {
331
+ console.log('[AnnotationDrag] handleAnnotationDragEnd called');
332
+ if (!isDraggingAnnotation) return;
333
+
334
+ // Remove temporary event listeners
335
+ document.removeEventListener('mousemove', handleAnnotationDragMove);
336
+ document.removeEventListener('mouseup', handleAnnotationDragEnd);
337
+
338
+ // Hide overlay, label, and snap guides
339
+ if (annotationDragOverlay) {
340
+ annotationDragOverlay.style.display = 'none';
341
+ annotationDragOverlay.style.borderColor = '#8b5cf6'; // Reset color
342
+ }
343
+ if (annotationDragLabel) {
344
+ annotationDragLabel.style.display = 'none';
345
+ }
346
+ if (typeof hideSnapGuides === 'function') hideSnapGuides();
347
+ document.body.style.cursor = '';
348
+
349
+ const img = document.getElementById('preview-image');
350
+ if (!img) {
351
+ isDraggingAnnotation = false;
352
+ return;
353
+ }
354
+
355
+ const rect = img.getBoundingClientRect();
356
+
357
+ // Calculate delta in pixels (for threshold check only)
358
+ const deltaX = event.clientX - annotationDragStartPos.x;
359
+ const deltaY = event.clientY - annotationDragStartPos.y;
360
+
361
+ // Only update if moved significantly (5px threshold)
362
+ if (Math.abs(deltaX) < 5 && Math.abs(deltaY) < 5) {
363
+ console.log('[AnnotationDrag] Movement below threshold, not updating');
364
+ isDraggingAnnotation = false;
365
+ return;
366
+ }
367
+
368
+ const axKey = Object.keys(panelPositions).sort()[annotationAxIndex];
369
+ const axPos = panelPositions[axKey];
370
+ if (!axPos || !figSize.width_mm || !figSize.height_mm) {
371
+ console.error('[AnnotationDrag] Cannot calculate axes-relative position');
372
+ isDraggingAnnotation = false;
373
+ return;
374
+ }
375
+
376
+ // Calculate position using DELTA from original to snapped/current position
377
+ // This ensures coordinates stay valid even when snapping to labels from other axes
378
+ const pxToMmX = figSize.width_mm / img.naturalWidth;
379
+ const pxToMmY = figSize.height_mm / img.naturalHeight;
380
+
381
+ // Original bbox center in mm
382
+ const origCenterX_mm = (annotationDragStartBbox.x + annotationDragStartBbox.width / 2) * pxToMmX;
383
+ const origCenterY_mm = (annotationDragStartBbox.y + annotationDragStartBbox.height / 2) * pxToMmY;
384
+
385
+ let relX, relY;
386
+
387
+ if (annotationLastSnappedBbox) {
388
+ // Calculate delta from original to snapped position (in mm)
389
+ const snappedCenterX_mm = (annotationLastSnappedBbox.x + annotationLastSnappedBbox.width / 2) * pxToMmX;
390
+ const snappedCenterY_mm = (annotationLastSnappedBbox.y + annotationLastSnappedBbox.height / 2) * pxToMmY;
391
+ const deltaX_mm = snappedCenterX_mm - origCenterX_mm;
392
+ const deltaY_mm = snappedCenterY_mm - origCenterY_mm;
393
+
394
+ // Convert mm delta to axes-relative delta
395
+ const relDeltaX = deltaX_mm / axPos.width;
396
+ const relDeltaY = -deltaY_mm / axPos.height; // Y flipped (figure Y increases down, axes Y increases up)
397
+
398
+ // Apply delta to original axes-relative position
399
+ relX = annotationOriginalRelPos.x + relDeltaX;
400
+ relY = annotationOriginalRelPos.y + relDeltaY;
401
+
402
+ console.log('[AnnotationDrag] Using SNAPPED delta | Delta mm:', deltaX_mm.toFixed(2), deltaY_mm.toFixed(2),
403
+ '| Delta rel:', relDeltaX.toFixed(3), relDeltaY.toFixed(3), '| New pos:', relX.toFixed(3), relY.toFixed(3));
404
+ } else {
405
+ // Fallback: Calculate from cursor delta
406
+ const screenToImgScale = img.naturalWidth / rect.width;
407
+ const screenDeltaX = event.clientX - annotationDragStartPos.x;
408
+ const screenDeltaY = event.clientY - annotationDragStartPos.y;
409
+ const mmDeltaX = screenDeltaX * screenToImgScale * pxToMmX;
410
+ const mmDeltaY = screenDeltaY * screenToImgScale * pxToMmY;
411
+ const relDeltaX = mmDeltaX / axPos.width;
412
+ const relDeltaY = -mmDeltaY / axPos.height; // Y flipped
413
+
414
+ relX = annotationOriginalRelPos.x + relDeltaX;
415
+ relY = annotationOriginalRelPos.y + relDeltaY;
416
+ console.log('[AnnotationDrag] Using CURSOR delta | Delta:', relDeltaX.toFixed(3), relDeltaY.toFixed(3),
417
+ '| New pos:', relX.toFixed(3), relY.toFixed(3));
418
+ }
419
+
420
+ // Get annotation type and index from key
421
+ const bboxInfo = annotationDragStartBbox;
422
+ const annotationType = bboxInfo.type || 'text';
423
+ const textIndex = bboxInfo.text_index !== undefined ? bboxInfo.text_index : 0;
424
+
425
+ // Apply the new position
426
+ await applyAnnotationPosition(annotationAxIndex, annotationType, textIndex, relX, relY);
427
+
428
+ // Update annotationPositions for undo support
429
+ if (annotationKey) {
430
+ annotationPositions[annotationKey] = {
431
+ x: relX,
432
+ y: relY,
433
+ type: annotationType
434
+ };
435
+ console.log('[AnnotationDrag] Updated position:', annotationKey, relX.toFixed(3), relY.toFixed(3));
436
+ }
437
+
438
+ // Reset state
439
+ isDraggingAnnotation = false;
440
+ annotationDragStartPos = null;
441
+ annotationDragStartBbox = null;
442
+ annotationDragOffset = null;
443
+ annotationOriginalRelPos = null;
444
+ annotationLastSnappedBbox = null;
445
+ annotationKey = null;
446
+ console.log('[AnnotationDrag] Drag state reset');
447
+ }
448
+
449
+ // Apply the dragged annotation position to the server
450
+ async function applyAnnotationPosition(axIndex, annotationType, textIndex, x, y) {
451
+ document.body.classList.add('loading');
452
+
453
+ try {
454
+ const response = await fetch('/update_annotation_position', {
455
+ method: 'POST',
456
+ headers: { 'Content-Type': 'application/json' },
457
+ body: JSON.stringify({
458
+ ax_index: axIndex,
459
+ annotation_type: annotationType,
460
+ text_index: textIndex,
461
+ x: x,
462
+ y: y
463
+ })
464
+ });
465
+
466
+ const data = await response.json();
467
+
468
+ if (data.success) {
469
+ // Update preview image
470
+ const img = document.getElementById('preview-image');
471
+ if (img) {
472
+ await new Promise((resolve) => {
473
+ img.onload = resolve;
474
+ img.src = 'data:image/png;base64,' + data.image;
475
+ });
476
+ }
477
+
478
+ // Update bboxes and hitmap
479
+ if (data.bboxes) {
480
+ currentBboxes = data.bboxes;
481
+ loadHitmap();
482
+ updateHitRegions();
483
+ // Reinitialize annotation positions from new bboxes
484
+ initAnnotationPositions();
485
+ }
486
+
487
+ console.log('[AnnotationDrag] Annotation position updated successfully');
488
+ } else {
489
+ console.error('[AnnotationDrag] Failed to update annotation:', data.error);
490
+ }
491
+ } catch (error) {
492
+ console.error('[AnnotationDrag] Failed to update annotation:', error);
493
+ }
494
+
495
+ document.body.classList.remove('loading');
496
+ }
497
+
498
+ // Initialize on DOMContentLoaded
499
+ document.addEventListener('DOMContentLoaded', initAnnotationDrag);
500
+ """
501
+
502
+ __all__ = ["SCRIPTS_ANNOTATION_DRAG"]
503
+
504
+ # EOF