figrecipe 0.6.0__py3-none-any.whl → 0.7.4__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 (177) hide show
  1. figrecipe/__init__.py +106 -973
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,265 @@
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
+ isDraggingLegend = true;
60
+ legendDragStartPos = { x: event.clientX, y: event.clientY };
61
+ legendDragStartBbox = { ...bbox };
62
+
63
+ // Extract axis index from key (e.g., "legend_ax0" -> 0)
64
+ const match = legendKey.match(/ax(\\d+)/);
65
+ legendAxIndex = match ? parseInt(match[1], 10) : 0;
66
+
67
+ // Show drag overlay
68
+ if (legendDragOverlay) {
69
+ updateLegendDragOverlay(bbox);
70
+ legendDragOverlay.style.display = 'block';
71
+ }
72
+
73
+ // Add temporary event listeners
74
+ document.addEventListener('mousemove', handleLegendDragMove);
75
+ document.addEventListener('mouseup', handleLegendDragEnd);
76
+
77
+ document.body.style.cursor = 'move';
78
+ console.log('[LegendDrag] Started dragging legend');
79
+ return true;
80
+ }
81
+
82
+ // Handle mouse move during legend drag
83
+ function handleLegendDragMove(event) {
84
+ if (!isDraggingLegend) return;
85
+
86
+ event.preventDefault();
87
+
88
+ const img = document.getElementById('preview-image');
89
+ if (!img) return;
90
+
91
+ const rect = img.getBoundingClientRect();
92
+
93
+ // Calculate delta in pixels
94
+ const deltaX = event.clientX - legendDragStartPos.x;
95
+ const deltaY = event.clientY - legendDragStartPos.y;
96
+
97
+ // Calculate new position in image pixels
98
+ const scaleX = img.naturalWidth / rect.width;
99
+ const scaleY = img.naturalHeight / rect.height;
100
+
101
+ const newBbox = {
102
+ x: legendDragStartBbox.x + deltaX * scaleX,
103
+ y: legendDragStartBbox.y + deltaY * scaleY,
104
+ width: legendDragStartBbox.width,
105
+ height: legendDragStartBbox.height
106
+ };
107
+
108
+ // Update visual overlay
109
+ updateLegendDragOverlay(newBbox);
110
+ }
111
+
112
+ // Update the legend drag overlay position
113
+ function updateLegendDragOverlay(bbox) {
114
+ if (!legendDragOverlay) return;
115
+
116
+ const img = document.getElementById('preview-image');
117
+ if (!img) return;
118
+
119
+ const rect = img.getBoundingClientRect();
120
+ const scaleX = rect.width / img.naturalWidth;
121
+ const scaleY = rect.height / img.naturalHeight;
122
+
123
+ const left = bbox.x * scaleX;
124
+ const top = bbox.y * scaleY;
125
+ const width = bbox.width * scaleX;
126
+ const height = bbox.height * scaleY;
127
+
128
+ legendDragOverlay.style.left = `${left}px`;
129
+ legendDragOverlay.style.top = `${top}px`;
130
+ legendDragOverlay.style.width = `${width}px`;
131
+ legendDragOverlay.style.height = `${height}px`;
132
+ }
133
+
134
+ // Handle mouse up - complete the legend drag
135
+ async function handleLegendDragEnd(event) {
136
+ console.log('[LegendDrag] handleLegendDragEnd called');
137
+ if (!isDraggingLegend) return;
138
+
139
+ // Remove temporary event listeners
140
+ document.removeEventListener('mousemove', handleLegendDragMove);
141
+ document.removeEventListener('mouseup', handleLegendDragEnd);
142
+
143
+ // Hide overlay
144
+ if (legendDragOverlay) {
145
+ legendDragOverlay.style.display = 'none';
146
+ }
147
+ document.body.style.cursor = '';
148
+
149
+ const img = document.getElementById('preview-image');
150
+ if (!img) {
151
+ isDraggingLegend = false;
152
+ return;
153
+ }
154
+
155
+ const rect = img.getBoundingClientRect();
156
+
157
+ // Calculate delta in pixels
158
+ const deltaX = event.clientX - legendDragStartPos.x;
159
+ const deltaY = event.clientY - legendDragStartPos.y;
160
+
161
+ // Only update if moved significantly (5px threshold)
162
+ if (Math.abs(deltaX) < 5 && Math.abs(deltaY) < 5) {
163
+ console.log('[LegendDrag] Movement below threshold, not updating');
164
+ isDraggingLegend = false;
165
+ return;
166
+ }
167
+
168
+ // Convert to axes-relative coordinates (0-1 range)
169
+ // We need to get the axes position to calculate relative coords
170
+ const axKey = Object.keys(panelPositions).sort()[legendAxIndex];
171
+ const axPos = panelPositions[axKey];
172
+
173
+ if (!axPos || !figSize.width_mm || !figSize.height_mm) {
174
+ console.error('[LegendDrag] Cannot calculate axes-relative position');
175
+ isDraggingLegend = false;
176
+ return;
177
+ }
178
+
179
+ // Calculate scale factors: screen pixels to image pixels
180
+ const screenToImgX = img.naturalWidth / rect.width;
181
+ const screenToImgY = img.naturalHeight / rect.height;
182
+
183
+ // New legend upper-left corner in image pixels
184
+ const newImgX = legendDragStartBbox.x + deltaX * screenToImgX;
185
+ const newImgY = legendDragStartBbox.y + deltaY * screenToImgY;
186
+
187
+ // Convert image pixels to mm (upper-left origin)
188
+ const newMmX = newImgX / img.naturalWidth * figSize.width_mm;
189
+ const newMmY = newImgY / img.naturalHeight * figSize.height_mm;
190
+
191
+ // Convert to axes-relative (0-1) coordinates
192
+ // Use upper-left corner since we set _loc=2 (upper left) in backend
193
+ const relX = (newMmX - axPos.left) / axPos.width;
194
+ const relY = 1 - (newMmY - axPos.top) / axPos.height; // Flip Y (matplotlib uses bottom-left origin)
195
+
196
+ console.log('[LegendDrag] New legend position (rel):', relX.toFixed(3), relY.toFixed(3));
197
+
198
+ // Apply the new position
199
+ await applyLegendPosition(legendAxIndex, relX, relY);
200
+
201
+ // Reset state
202
+ isDraggingLegend = false;
203
+ legendDragStartPos = null;
204
+ legendDragStartBbox = null;
205
+ console.log('[LegendDrag] Drag state reset');
206
+ }
207
+
208
+ // Apply the dragged legend position to the server
209
+ async function applyLegendPosition(axIndex, x, y) {
210
+ document.body.classList.add('loading');
211
+
212
+ try {
213
+ const response = await fetch('/update_legend_position', {
214
+ method: 'POST',
215
+ headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify({
217
+ ax_index: axIndex,
218
+ loc: 'custom',
219
+ x: x,
220
+ y: y
221
+ })
222
+ });
223
+
224
+ const data = await response.json();
225
+
226
+ if (data.success) {
227
+ // Update preview image
228
+ const img = document.getElementById('preview-image');
229
+ if (img) {
230
+ await new Promise((resolve) => {
231
+ img.onload = resolve;
232
+ img.src = 'data:image/png;base64,' + data.image;
233
+ });
234
+ }
235
+
236
+ // Update bboxes and hitmap
237
+ if (data.bboxes) {
238
+ currentBboxes = data.bboxes;
239
+ loadHitmap();
240
+ updateHitRegions();
241
+ }
242
+
243
+ console.log('[LegendDrag] Legend position updated successfully');
244
+ } else {
245
+ console.error('[LegendDrag] Failed to update legend:', data.error);
246
+ }
247
+ } catch (error) {
248
+ console.error('[LegendDrag] Failed to update legend:', error);
249
+ }
250
+
251
+ document.body.classList.remove('loading');
252
+ }
253
+
254
+ // Check if a key refers to a legend element
255
+ function isLegendElement(key) {
256
+ return key && key.startsWith('legend_');
257
+ }
258
+
259
+ // Initialize on DOMContentLoaded
260
+ document.addEventListener('DOMContentLoaded', initLegendDrag);
261
+ """
262
+
263
+ __all__ = ["SCRIPTS_LEGEND_DRAG"]
264
+
265
+ # 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