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,270 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Legend drag-to-move JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Detecting mousedown on legend elements
7
+ - Handling drag movement with visual feedback
8
+ - Updating legend position on drop
9
+
10
+ Legend coordinates are in axes-relative units (0-1 range).
11
+ """
12
+
13
+ SCRIPTS_LEGEND_DRAG = """
14
+ // ===== LEGEND DRAG-TO-MOVE =====
15
+
16
+ let isDraggingLegend = false;
17
+ let legendDragStartPos = null;
18
+ let legendDragStartBbox = null;
19
+ let legendDragOverlay = null;
20
+ let legendAxIndex = 0;
21
+
22
+ // Initialize legend drag functionality
23
+ function initLegendDrag() {
24
+ console.log('[LegendDrag] initLegendDrag called');
25
+ const zoomContainer = document.getElementById('zoom-container');
26
+ if (!zoomContainer) {
27
+ console.error('[LegendDrag] zoom-container not found!');
28
+ return;
29
+ }
30
+
31
+ // Create legend drag overlay element
32
+ legendDragOverlay = document.createElement('div');
33
+ legendDragOverlay.id = 'legend-drag-overlay';
34
+ legendDragOverlay.style.cssText = `
35
+ position: absolute;
36
+ border: 2px dashed #10b981;
37
+ background: rgba(16, 185, 129, 0.1);
38
+ pointer-events: none;
39
+ display: none;
40
+ z-index: 1001;
41
+ `;
42
+ zoomContainer.appendChild(legendDragOverlay);
43
+ console.log('[LegendDrag] Overlay created');
44
+ }
45
+
46
+ // Handle legend drag start (called from hitmap click handler)
47
+ function startLegendDrag(event, legendKey) {
48
+ console.log('[LegendDrag] startLegendDrag called for:', legendKey);
49
+
50
+ const img = document.getElementById('preview-image');
51
+ if (!img) return false;
52
+
53
+ const bbox = currentBboxes[legendKey];
54
+ if (!bbox) return false;
55
+
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+
59
+ // Capture state before drag for undo
60
+ if (typeof pushToHistory === 'function') {
61
+ pushToHistory();
62
+ }
63
+
64
+ isDraggingLegend = true;
65
+ legendDragStartPos = { x: event.clientX, y: event.clientY };
66
+ legendDragStartBbox = { ...bbox };
67
+
68
+ // Extract axis index from key (e.g., "legend_ax0" -> 0)
69
+ const match = legendKey.match(/ax(\\d+)/);
70
+ legendAxIndex = match ? parseInt(match[1], 10) : 0;
71
+
72
+ // Show drag overlay
73
+ if (legendDragOverlay) {
74
+ updateLegendDragOverlay(bbox);
75
+ legendDragOverlay.style.display = 'block';
76
+ }
77
+
78
+ // Add temporary event listeners
79
+ document.addEventListener('mousemove', handleLegendDragMove);
80
+ document.addEventListener('mouseup', handleLegendDragEnd);
81
+
82
+ document.body.style.cursor = 'move';
83
+ console.log('[LegendDrag] Started dragging legend');
84
+ return true;
85
+ }
86
+
87
+ // Handle mouse move during legend drag
88
+ function handleLegendDragMove(event) {
89
+ if (!isDraggingLegend) return;
90
+
91
+ event.preventDefault();
92
+
93
+ const img = document.getElementById('preview-image');
94
+ if (!img) return;
95
+
96
+ const rect = img.getBoundingClientRect();
97
+
98
+ // Calculate delta in pixels
99
+ const deltaX = event.clientX - legendDragStartPos.x;
100
+ const deltaY = event.clientY - legendDragStartPos.y;
101
+
102
+ // Calculate new position in image pixels
103
+ const scaleX = img.naturalWidth / rect.width;
104
+ const scaleY = img.naturalHeight / rect.height;
105
+
106
+ const newBbox = {
107
+ x: legendDragStartBbox.x + deltaX * scaleX,
108
+ y: legendDragStartBbox.y + deltaY * scaleY,
109
+ width: legendDragStartBbox.width,
110
+ height: legendDragStartBbox.height
111
+ };
112
+
113
+ // Update visual overlay
114
+ updateLegendDragOverlay(newBbox);
115
+ }
116
+
117
+ // Update the legend drag overlay position
118
+ function updateLegendDragOverlay(bbox) {
119
+ if (!legendDragOverlay) return;
120
+
121
+ const img = document.getElementById('preview-image');
122
+ if (!img) return;
123
+
124
+ const rect = img.getBoundingClientRect();
125
+ const scaleX = rect.width / img.naturalWidth;
126
+ const scaleY = rect.height / img.naturalHeight;
127
+
128
+ const left = bbox.x * scaleX;
129
+ const top = bbox.y * scaleY;
130
+ const width = bbox.width * scaleX;
131
+ const height = bbox.height * scaleY;
132
+
133
+ legendDragOverlay.style.left = `${left}px`;
134
+ legendDragOverlay.style.top = `${top}px`;
135
+ legendDragOverlay.style.width = `${width}px`;
136
+ legendDragOverlay.style.height = `${height}px`;
137
+ }
138
+
139
+ // Handle mouse up - complete the legend drag
140
+ async function handleLegendDragEnd(event) {
141
+ console.log('[LegendDrag] handleLegendDragEnd called');
142
+ if (!isDraggingLegend) return;
143
+
144
+ // Remove temporary event listeners
145
+ document.removeEventListener('mousemove', handleLegendDragMove);
146
+ document.removeEventListener('mouseup', handleLegendDragEnd);
147
+
148
+ // Hide overlay
149
+ if (legendDragOverlay) {
150
+ legendDragOverlay.style.display = 'none';
151
+ }
152
+ document.body.style.cursor = '';
153
+
154
+ const img = document.getElementById('preview-image');
155
+ if (!img) {
156
+ isDraggingLegend = false;
157
+ return;
158
+ }
159
+
160
+ const rect = img.getBoundingClientRect();
161
+
162
+ // Calculate delta in pixels
163
+ const deltaX = event.clientX - legendDragStartPos.x;
164
+ const deltaY = event.clientY - legendDragStartPos.y;
165
+
166
+ // Only update if moved significantly (5px threshold)
167
+ if (Math.abs(deltaX) < 5 && Math.abs(deltaY) < 5) {
168
+ console.log('[LegendDrag] Movement below threshold, not updating');
169
+ isDraggingLegend = false;
170
+ return;
171
+ }
172
+
173
+ // Convert to axes-relative coordinates (0-1 range)
174
+ // We need to get the axes position to calculate relative coords
175
+ const axKey = Object.keys(panelPositions).sort()[legendAxIndex];
176
+ const axPos = panelPositions[axKey];
177
+
178
+ if (!axPos || !figSize.width_mm || !figSize.height_mm) {
179
+ console.error('[LegendDrag] Cannot calculate axes-relative position');
180
+ isDraggingLegend = false;
181
+ return;
182
+ }
183
+
184
+ // Calculate scale factors: screen pixels to image pixels
185
+ const screenToImgX = img.naturalWidth / rect.width;
186
+ const screenToImgY = img.naturalHeight / rect.height;
187
+
188
+ // New legend upper-left corner in image pixels
189
+ const newImgX = legendDragStartBbox.x + deltaX * screenToImgX;
190
+ const newImgY = legendDragStartBbox.y + deltaY * screenToImgY;
191
+
192
+ // Convert image pixels to mm (upper-left origin)
193
+ const newMmX = newImgX / img.naturalWidth * figSize.width_mm;
194
+ const newMmY = newImgY / img.naturalHeight * figSize.height_mm;
195
+
196
+ // Convert to axes-relative (0-1) coordinates
197
+ // Use upper-left corner since we set _loc=2 (upper left) in backend
198
+ const relX = (newMmX - axPos.left) / axPos.width;
199
+ const relY = 1 - (newMmY - axPos.top) / axPos.height; // Flip Y (matplotlib uses bottom-left origin)
200
+
201
+ console.log('[LegendDrag] New legend position (rel):', relX.toFixed(3), relY.toFixed(3));
202
+
203
+ // Apply the new position
204
+ await applyLegendPosition(legendAxIndex, relX, relY);
205
+
206
+ // Reset state
207
+ isDraggingLegend = false;
208
+ legendDragStartPos = null;
209
+ legendDragStartBbox = null;
210
+ console.log('[LegendDrag] Drag state reset');
211
+ }
212
+
213
+ // Apply the dragged legend position to the server
214
+ async function applyLegendPosition(axIndex, x, y) {
215
+ document.body.classList.add('loading');
216
+
217
+ try {
218
+ const response = await fetch('/update_legend_position', {
219
+ method: 'POST',
220
+ headers: { 'Content-Type': 'application/json' },
221
+ body: JSON.stringify({
222
+ ax_index: axIndex,
223
+ loc: 'custom',
224
+ x: x,
225
+ y: y
226
+ })
227
+ });
228
+
229
+ const data = await response.json();
230
+
231
+ if (data.success) {
232
+ // Update preview image
233
+ const img = document.getElementById('preview-image');
234
+ if (img) {
235
+ await new Promise((resolve) => {
236
+ img.onload = resolve;
237
+ img.src = 'data:image/png;base64,' + data.image;
238
+ });
239
+ }
240
+
241
+ // Update bboxes and hitmap
242
+ if (data.bboxes) {
243
+ currentBboxes = data.bboxes;
244
+ loadHitmap();
245
+ updateHitRegions();
246
+ }
247
+
248
+ console.log('[LegendDrag] Legend position updated successfully');
249
+ } else {
250
+ console.error('[LegendDrag] Failed to update legend:', data.error);
251
+ }
252
+ } catch (error) {
253
+ console.error('[LegendDrag] Failed to update legend:', error);
254
+ }
255
+
256
+ document.body.classList.remove('loading');
257
+ }
258
+
259
+ // Check if a key refers to a legend element
260
+ function isLegendElement(key) {
261
+ return key && key.startsWith('legend_');
262
+ }
263
+
264
+ // Initialize on DOMContentLoaded
265
+ document.addEventListener('DOMContentLoaded', initLegendDrag);
266
+ """
267
+
268
+ __all__ = ["SCRIPTS_LEGEND_DRAG"]
269
+
270
+ # EOF
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Modal dialogs JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Theme modal (view, download, copy theme)
7
+ - Shortcuts modal
8
+ - Theme switching
9
+ """
10
+
11
+ SCRIPTS_MODALS = """
12
+ // ===== MODAL DIALOGS =====
13
+
14
+ // Initialize theme modal handlers
15
+ function initializeThemeModal() {
16
+ const modal = document.getElementById('theme-modal');
17
+ const themeSelector = document.getElementById('theme-selector');
18
+ const btnView = document.getElementById('btn-view-theme');
19
+ const btnDownload = document.getElementById('btn-download-theme');
20
+ const btnCopy = document.getElementById('btn-copy-theme');
21
+ const modalClose = document.getElementById('theme-modal-close');
22
+ const modalDownload = document.getElementById('theme-modal-download');
23
+ const modalCopy = document.getElementById('theme-modal-copy');
24
+
25
+ // Theme selector change handler
26
+ if (themeSelector) {
27
+ loadCurrentTheme();
28
+ themeSelector.addEventListener('change', function() {
29
+ switchTheme(this.value);
30
+ });
31
+ }
32
+
33
+ // View button opens modal
34
+ if (btnView) btnView.addEventListener('click', showThemeModal);
35
+
36
+ // Download and copy buttons
37
+ if (btnDownload) btnDownload.addEventListener('click', downloadTheme);
38
+ if (btnCopy) btnCopy.addEventListener('click', copyTheme);
39
+
40
+ // Modal close
41
+ if (modalClose) modalClose.addEventListener('click', hideThemeModal);
42
+
43
+ // Modal buttons
44
+ if (modalDownload) modalDownload.addEventListener('click', downloadTheme);
45
+ if (modalCopy) modalCopy.addEventListener('click', copyTheme);
46
+
47
+ // Close modal on outside click
48
+ if (modal) {
49
+ modal.addEventListener('click', function(e) {
50
+ if (e.target === modal) hideThemeModal();
51
+ });
52
+ }
53
+ }
54
+
55
+ // Show theme modal
56
+ async function showThemeModal() {
57
+ const modal = document.getElementById('theme-modal');
58
+ const themeContent = document.getElementById('theme-content');
59
+ const themeModalName = document.getElementById('theme-modal-name');
60
+ const themeSelector = document.getElementById('theme-selector');
61
+
62
+ try {
63
+ const response = await fetch('/theme');
64
+ const data = await response.json();
65
+
66
+ const themeName = themeSelector ? themeSelector.value : data.name;
67
+ if (themeModalName) themeModalName.textContent = themeName;
68
+ if (themeContent) themeContent.textContent = data.content;
69
+ if (modal) modal.style.display = 'flex';
70
+ } catch (error) {
71
+ console.error('Failed to load theme:', error);
72
+ }
73
+ }
74
+
75
+ // Hide theme modal
76
+ function hideThemeModal() {
77
+ const modal = document.getElementById('theme-modal');
78
+ if (modal) modal.style.display = 'none';
79
+ }
80
+
81
+ // Initialize shortcuts modal handlers
82
+ function initializeShortcutsModal() {
83
+ const modal = document.getElementById('shortcuts-modal');
84
+ const btnShortcuts = document.getElementById('btn-shortcuts');
85
+ const modalClose = document.getElementById('shortcuts-modal-close');
86
+
87
+ if (btnShortcuts) btnShortcuts.addEventListener('click', showShortcutsModal);
88
+ if (modalClose) modalClose.addEventListener('click', hideShortcutsModal);
89
+
90
+ if (modal) {
91
+ modal.addEventListener('click', function(e) {
92
+ if (e.target === modal) hideShortcutsModal();
93
+ });
94
+ }
95
+ }
96
+
97
+ // Show/hide shortcuts modal
98
+ function showShortcutsModal() {
99
+ const modal = document.getElementById('shortcuts-modal');
100
+ if (modal) modal.style.display = 'flex';
101
+ }
102
+
103
+ function hideShortcutsModal() {
104
+ const modal = document.getElementById('shortcuts-modal');
105
+ if (modal) modal.style.display = 'none';
106
+ }
107
+
108
+ // Download theme as YAML
109
+ async function downloadTheme() {
110
+ try {
111
+ const response = await fetch('/theme');
112
+ const data = await response.json();
113
+
114
+ const blob = new Blob([data.content], { type: 'text/yaml' });
115
+ const url = URL.createObjectURL(blob);
116
+ const a = document.createElement('a');
117
+ a.href = url;
118
+ a.download = data.name + '.yaml';
119
+ document.body.appendChild(a);
120
+ a.click();
121
+ document.body.removeChild(a);
122
+ URL.revokeObjectURL(url);
123
+ } catch (error) {
124
+ console.error('Failed to download theme:', error);
125
+ }
126
+ }
127
+
128
+ // Copy theme to clipboard
129
+ async function copyTheme() {
130
+ try {
131
+ const response = await fetch('/theme');
132
+ const data = await response.json();
133
+
134
+ await navigator.clipboard.writeText(data.content);
135
+
136
+ const btn = document.getElementById('btn-copy-theme');
137
+ const originalText = btn.textContent;
138
+ btn.textContent = 'Copied!';
139
+ setTimeout(() => { btn.textContent = originalText; }, 1500);
140
+ } catch (error) {
141
+ console.error('Failed to copy theme:', error);
142
+ }
143
+ }
144
+
145
+ // Load current theme and set selector
146
+ async function loadCurrentTheme() {
147
+ try {
148
+ const response = await fetch('/list_themes');
149
+ const data = await response.json();
150
+
151
+ const selector = document.getElementById('theme-selector');
152
+ if (selector && data.current) {
153
+ selector.value = data.current;
154
+ }
155
+ console.log('Current theme:', data.current);
156
+ } catch (error) {
157
+ console.error('Failed to load current theme:', error);
158
+ }
159
+ }
160
+
161
+ // Switch to a different theme preset
162
+ async function switchTheme(themeName) {
163
+ console.log('Switching theme to:', themeName);
164
+ document.body.classList.add('loading');
165
+
166
+ try {
167
+ const response = await fetch('/switch_theme', {
168
+ method: 'POST',
169
+ headers: { 'Content-Type': 'application/json' },
170
+ body: JSON.stringify({ theme: themeName })
171
+ });
172
+
173
+ const result = await response.json();
174
+
175
+ if (result.success) {
176
+ const previewImg = document.getElementById('preview-image');
177
+ previewImg.src = 'data:image/png;base64,' + result.image;
178
+
179
+ if (result.img_size) {
180
+ currentImgWidth = result.img_size.width;
181
+ currentImgHeight = result.img_size.height;
182
+ }
183
+
184
+ // Update form values from new theme
185
+ if (result.values) {
186
+ for (const [key, value] of Object.entries(result.values)) {
187
+ const element = document.getElementById(key);
188
+ if (element) {
189
+ if (element.type === 'checkbox') {
190
+ element.checked = Boolean(value);
191
+ } else {
192
+ element.value = value;
193
+ }
194
+ if (element.placeholder !== undefined) {
195
+ element.placeholder = value;
196
+ }
197
+ }
198
+ }
199
+ Object.assign(themeDefaults, result.values);
200
+ updateAllModifiedStates();
201
+ }
202
+
203
+ if (result.bboxes) {
204
+ currentBboxes = result.bboxes;
205
+ previewImg.onload = () => {
206
+ updateHitRegions();
207
+ loadHitmap();
208
+ };
209
+ }
210
+ console.log('Theme switched to:', themeName);
211
+ } else {
212
+ console.error('Theme switch failed:', result.error);
213
+ loadCurrentTheme();
214
+ }
215
+ } catch (error) {
216
+ console.error('Failed to switch theme:', error);
217
+ loadCurrentTheme();
218
+ } finally {
219
+ document.body.classList.remove('loading');
220
+ }
221
+ }
222
+ """
223
+
224
+ __all__ = ["SCRIPTS_MODALS"]
225
+
226
+ # EOF
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Multi-selection JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Ctrl+Click to add/remove elements from selection
7
+ - Managing multiple selected elements
8
+ - Drawing multi-selection highlights
9
+ """
10
+
11
+ SCRIPTS_MULTI_SELECT = """
12
+ // ===== MULTI-SELECTION (Ctrl+Click) =====
13
+
14
+ // Array of selected elements (each element is {key, type, x, y, width, height, ...})
15
+ let selectedElements = [];
16
+
17
+ // Check if multi-select mode is active (Ctrl or Cmd key held)
18
+ function isMultiSelectMode(event) {
19
+ return event && (event.ctrlKey || event.metaKey);
20
+ }
21
+
22
+ // Check if an element is currently selected
23
+ function isElementSelected(key) {
24
+ return selectedElements.some(el => el.key === key);
25
+ }
26
+
27
+ // Add element to selection (if not already selected)
28
+ function addToSelection(element) {
29
+ if (!element || !element.key) return;
30
+ if (isElementSelected(element.key)) return;
31
+
32
+ selectedElements.push(element);
33
+ console.log('[MultiSelect] Added to selection:', element.key, '- total:', selectedElements.length);
34
+ }
35
+
36
+ // Remove element from selection
37
+ function removeFromSelection(key) {
38
+ const idx = selectedElements.findIndex(el => el.key === key);
39
+ if (idx >= 0) {
40
+ selectedElements.splice(idx, 1);
41
+ console.log('[MultiSelect] Removed from selection:', key, '- total:', selectedElements.length);
42
+ }
43
+ }
44
+
45
+ // Toggle element in selection
46
+ function toggleInSelection(element) {
47
+ if (!element || !element.key) return;
48
+
49
+ if (isElementSelected(element.key)) {
50
+ removeFromSelection(element.key);
51
+ } else {
52
+ addToSelection(element);
53
+ }
54
+ }
55
+
56
+ // Clear all selections
57
+ function clearMultiSelection() {
58
+ selectedElements = [];
59
+ console.log('[MultiSelect] Selection cleared');
60
+
61
+ // Clear visual selection
62
+ const selOverlay = document.getElementById('selection-overlay');
63
+ if (selOverlay) {
64
+ const svg = selOverlay.querySelector('svg');
65
+ if (svg) {
66
+ // Remove multi-selection highlights
67
+ svg.querySelectorAll('.multi-select-highlight').forEach(el => el.remove());
68
+ }
69
+ }
70
+ }
71
+
72
+ // Draw multi-selection highlights on the selection overlay
73
+ function drawMultiSelection() {
74
+ const selOverlay = document.getElementById('selection-overlay');
75
+ if (!selOverlay) return;
76
+
77
+ let svg = selOverlay.querySelector('svg');
78
+ if (!svg) {
79
+ svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
80
+ svg.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;';
81
+ selOverlay.appendChild(svg);
82
+ }
83
+
84
+ // Remove existing multi-selection highlights
85
+ svg.querySelectorAll('.multi-select-highlight').forEach(el => el.remove());
86
+
87
+ const img = document.getElementById('preview-image');
88
+ if (!img || !figSize.width_mm || !figSize.height_mm) return;
89
+
90
+ const imgRect = img.getBoundingClientRect();
91
+ const containerRect = selOverlay.getBoundingClientRect();
92
+
93
+ // Update SVG viewBox to match image
94
+ svg.setAttribute('viewBox', `0 0 ${imgRect.width} ${imgRect.height}`);
95
+ svg.style.width = imgRect.width + 'px';
96
+ svg.style.height = imgRect.height + 'px';
97
+ svg.style.left = (imgRect.left - containerRect.left) + 'px';
98
+ svg.style.top = (imgRect.top - containerRect.top) + 'px';
99
+
100
+ // Draw highlight for each selected element
101
+ selectedElements.forEach(element => {
102
+ if (!element.x || !element.width) return;
103
+
104
+ // Convert from image pixels to display pixels
105
+ const scaleX = imgRect.width / img.naturalWidth;
106
+ const scaleY = imgRect.height / img.naturalHeight;
107
+
108
+ const x = element.x * scaleX;
109
+ const y = element.y * scaleY;
110
+ const w = element.width * scaleX;
111
+ const h = element.height * scaleY;
112
+
113
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
114
+ rect.setAttribute('class', 'multi-select-highlight');
115
+ rect.setAttribute('x', x);
116
+ rect.setAttribute('y', y);
117
+ rect.setAttribute('width', w);
118
+ rect.setAttribute('height', h);
119
+ rect.setAttribute('fill', 'rgba(37, 99, 235, 0.15)');
120
+ rect.setAttribute('stroke', '#2563eb');
121
+ rect.setAttribute('stroke-width', '2');
122
+ rect.setAttribute('stroke-dasharray', '4,2');
123
+ svg.appendChild(rect);
124
+ });
125
+
126
+ console.log('[MultiSelect] Drew selection for', selectedElements.length, 'elements');
127
+ }
128
+
129
+ // Select all elements of a specific type
130
+ function selectAllOfType(type) {
131
+ clearMultiSelection();
132
+
133
+ for (const [key, bbox] of Object.entries(currentBboxes)) {
134
+ if (key === '_meta') continue;
135
+ if (!bbox || typeof bbox.x === 'undefined') continue;
136
+
137
+ const info = (colorMap && colorMap[key]) || {};
138
+ if (info.type === type || bbox.type === type) {
139
+ addToSelection({ key, ...bbox, ...info });
140
+ }
141
+ }
142
+
143
+ drawMultiSelection();
144
+ console.log('[MultiSelect] Selected all', type, 'elements:', selectedElements.length);
145
+ }
146
+
147
+ // Get indices of selected panels (axes)
148
+ function getSelectedPanelIndices() {
149
+ return selectedElements
150
+ .filter(el => el.key && el.key.includes('_axes'))
151
+ .map(el => {
152
+ const match = el.key.match(/ax(\\d+)_axes/);
153
+ return match ? parseInt(match[1], 10) : -1;
154
+ })
155
+ .filter(idx => idx >= 0);
156
+ }
157
+
158
+ // Update UI to show multi-selection state
159
+ function updateMultiSelectionUI() {
160
+ const countEl = document.getElementById('multi-select-count');
161
+ if (countEl) {
162
+ countEl.textContent = selectedElements.length > 1
163
+ ? `${selectedElements.length} selected`
164
+ : '';
165
+ }
166
+ }
167
+
168
+ // Handle keyboard shortcuts for multi-selection
169
+ function handleMultiSelectKeyboard(event) {
170
+ // Ctrl+A: Select all panels
171
+ if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
172
+ // Only if not in an input field
173
+ if (event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
174
+ event.preventDefault();
175
+ selectAllOfType('axes');
176
+ }
177
+ }
178
+
179
+ // Escape: Clear selection
180
+ if (event.key === 'Escape') {
181
+ clearMultiSelection();
182
+ drawMultiSelection();
183
+ }
184
+ }
185
+
186
+ // Initialize multi-selection support
187
+ function initMultiSelect() {
188
+ console.log('[MultiSelect] Initializing multi-selection support');
189
+ document.addEventListener('keydown', handleMultiSelectKeyboard);
190
+ }
191
+
192
+ // Initialize on DOMContentLoaded
193
+ document.addEventListener('DOMContentLoaded', initMultiSelect);
194
+ """
195
+
196
+ __all__ = ["SCRIPTS_MULTI_SELECT"]
197
+
198
+ # EOF