figrecipe 0.5.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 (189) hide show
  1. figrecipe/__init__.py +220 -819
  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 +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  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/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Ruler, grid, and column overlay JavaScript for the figure editor."""
4
+
5
+ SCRIPTS_OVERLAYS = """
6
+ // ============================================
7
+ // MEASUREMENT OVERLAYS (Ruler, Grid, Columns)
8
+ // ============================================
9
+
10
+ // State for overlays
11
+ let rulerVisible = false;
12
+ let gridVisible = false;
13
+ let columnsVisible = false;
14
+
15
+ // Get figure DPI from server (default 300 for preview at 150)
16
+ // Preview DPI is 150, which is half of output DPI
17
+ const PREVIEW_DPI = 150;
18
+ const MM_PER_INCH = 25.4;
19
+
20
+ // Pixels per mm at preview resolution
21
+ function getPxPerMm() {
22
+ return PREVIEW_DPI / MM_PER_INCH;
23
+ }
24
+
25
+ // Unified ruler & grid state
26
+ let rulerGridVisible = false;
27
+
28
+ // Initialize overlay controls (single toggle button)
29
+ function initializeOverlayControls() {
30
+ const btn = document.getElementById('btn-ruler-grid');
31
+ if (btn) {
32
+ btn.addEventListener('click', toggleRulerGrid);
33
+ }
34
+ }
35
+
36
+ // Toggle all ruler/grid overlays together
37
+ function toggleRulerGrid() {
38
+ rulerGridVisible = !rulerGridVisible;
39
+
40
+ // Sync all three visibility states
41
+ rulerVisible = rulerGridVisible;
42
+ gridVisible = rulerGridVisible;
43
+ columnsVisible = rulerGridVisible;
44
+
45
+ // Update overlays
46
+ const rulerOverlay = document.getElementById('ruler-overlay');
47
+ const gridOverlay = document.getElementById('grid-overlay');
48
+ const columnOverlay = document.getElementById('column-overlay');
49
+
50
+ if (rulerOverlay) rulerOverlay.classList.toggle('visible', rulerGridVisible);
51
+ if (gridOverlay) gridOverlay.classList.toggle('visible', rulerGridVisible);
52
+ if (columnOverlay) columnOverlay.classList.toggle('visible', rulerGridVisible);
53
+
54
+ // Draw overlays if visible
55
+ if (rulerGridVisible) {
56
+ drawRuler();
57
+ drawGrid();
58
+ drawColumnGuides();
59
+ }
60
+
61
+ // Update button state
62
+ const btn = document.getElementById('btn-ruler-grid');
63
+ if (btn) {
64
+ btn.classList.toggle('active', rulerGridVisible);
65
+ }
66
+ }
67
+
68
+ // Legacy functions for backward compatibility
69
+ function toggleRuler() { toggleRulerGrid(); }
70
+ function toggleGrid() { toggleRulerGrid(); }
71
+ function toggleColumns() { toggleRulerGrid(); }
72
+
73
+ // Draw ruler overlay (top and left edges)
74
+ function drawRuler() {
75
+ const overlay = document.getElementById('ruler-overlay');
76
+ const img = document.getElementById('preview-image');
77
+ if (!overlay || !img) return;
78
+
79
+ // Clear existing
80
+ overlay.innerHTML = '';
81
+
82
+ const imgWidth = img.naturalWidth || img.width;
83
+ const imgHeight = img.naturalHeight || img.height;
84
+ const pxPerMm = getPxPerMm();
85
+
86
+ // Set SVG size
87
+ overlay.setAttribute('viewBox', `0 0 ${imgWidth} ${imgHeight}`);
88
+ overlay.style.width = imgWidth + 'px';
89
+ overlay.style.height = imgHeight + 'px';
90
+
91
+ const rulerThickness = 15; // pixels
92
+
93
+ // Background for horizontal ruler (top)
94
+ const hBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
95
+ hBg.setAttribute('x', 0);
96
+ hBg.setAttribute('y', 0);
97
+ hBg.setAttribute('width', imgWidth);
98
+ hBg.setAttribute('height', rulerThickness);
99
+ hBg.setAttribute('class', 'ruler-bg');
100
+ overlay.appendChild(hBg);
101
+
102
+ // Background for vertical ruler (left)
103
+ const vBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
104
+ vBg.setAttribute('x', 0);
105
+ vBg.setAttribute('y', 0);
106
+ vBg.setAttribute('width', rulerThickness);
107
+ vBg.setAttribute('height', imgHeight);
108
+ vBg.setAttribute('class', 'ruler-bg');
109
+ overlay.appendChild(vBg);
110
+
111
+ const widthMm = imgWidth / pxPerMm;
112
+ const heightMm = imgHeight / pxPerMm;
113
+
114
+ // Draw horizontal ruler ticks (every 1mm, labels every 5mm)
115
+ for (let mm = 0; mm <= widthMm; mm++) {
116
+ const x = mm * pxPerMm;
117
+ const isMajor = mm % 5 === 0;
118
+ const tickLen = isMajor ? 10 : 5;
119
+
120
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
121
+ line.setAttribute('x1', x);
122
+ line.setAttribute('y1', 0);
123
+ line.setAttribute('x2', x);
124
+ line.setAttribute('y2', tickLen);
125
+ line.setAttribute('class', isMajor ? 'ruler-line-major' : 'ruler-line');
126
+ overlay.appendChild(line);
127
+
128
+ // Label every 10mm
129
+ if (mm % 10 === 0 && mm > 0) {
130
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
131
+ text.setAttribute('x', x);
132
+ text.setAttribute('y', 13);
133
+ text.setAttribute('class', 'ruler-text');
134
+ text.setAttribute('text-anchor', 'middle');
135
+ text.textContent = mm.toString();
136
+ overlay.appendChild(text);
137
+ }
138
+ }
139
+
140
+ // Draw vertical ruler ticks (every 1mm, labels every 5mm)
141
+ for (let mm = 0; mm <= heightMm; mm++) {
142
+ const y = mm * pxPerMm;
143
+ const isMajor = mm % 5 === 0;
144
+ const tickLen = isMajor ? 10 : 5;
145
+
146
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
147
+ line.setAttribute('x1', 0);
148
+ line.setAttribute('y1', y);
149
+ line.setAttribute('x2', tickLen);
150
+ line.setAttribute('y2', y);
151
+ line.setAttribute('class', isMajor ? 'ruler-line-major' : 'ruler-line');
152
+ overlay.appendChild(line);
153
+
154
+ // Label every 10mm
155
+ if (mm % 10 === 0 && mm > 0) {
156
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
157
+ text.setAttribute('x', 2);
158
+ text.setAttribute('y', y + 3);
159
+ text.setAttribute('class', 'ruler-text');
160
+ text.setAttribute('text-anchor', 'start');
161
+ text.setAttribute('transform', `rotate(-90, 2, ${y})`);
162
+ text.textContent = mm.toString();
163
+ overlay.appendChild(text);
164
+ }
165
+ }
166
+ }
167
+
168
+ // Draw grid overlay (1mm and 5mm intervals)
169
+ function drawGrid() {
170
+ const overlay = document.getElementById('grid-overlay');
171
+ const img = document.getElementById('preview-image');
172
+ if (!overlay || !img) return;
173
+
174
+ // Clear existing
175
+ overlay.innerHTML = '';
176
+
177
+ const imgWidth = img.naturalWidth || img.width;
178
+ const imgHeight = img.naturalHeight || img.height;
179
+ const pxPerMm = getPxPerMm();
180
+
181
+ // Set SVG size
182
+ overlay.setAttribute('viewBox', `0 0 ${imgWidth} ${imgHeight}`);
183
+ overlay.style.width = imgWidth + 'px';
184
+ overlay.style.height = imgHeight + 'px';
185
+
186
+ const widthMm = imgWidth / pxPerMm;
187
+ const heightMm = imgHeight / pxPerMm;
188
+
189
+ // Draw vertical lines (every 1mm)
190
+ for (let mm = 0; mm <= widthMm; mm++) {
191
+ const x = mm * pxPerMm;
192
+ const is5mm = mm % 5 === 0;
193
+
194
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
195
+ line.setAttribute('x1', x);
196
+ line.setAttribute('y1', 0);
197
+ line.setAttribute('x2', x);
198
+ line.setAttribute('y2', imgHeight);
199
+ line.setAttribute('class', is5mm ? 'grid-line-5mm' : 'grid-line-1mm');
200
+ overlay.appendChild(line);
201
+ }
202
+
203
+ // Draw horizontal lines (every 1mm)
204
+ for (let mm = 0; mm <= heightMm; mm++) {
205
+ const y = mm * pxPerMm;
206
+ const is5mm = mm % 5 === 0;
207
+
208
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
209
+ line.setAttribute('x1', 0);
210
+ line.setAttribute('y1', y);
211
+ line.setAttribute('x2', imgWidth);
212
+ line.setAttribute('y2', y);
213
+ line.setAttribute('class', is5mm ? 'grid-line-5mm' : 'grid-line-1mm');
214
+ overlay.appendChild(line);
215
+ }
216
+ }
217
+
218
+ // Draw column guide lines (45mm intervals: 0, 45, 90, 135, 180mm)
219
+ function drawColumnGuides() {
220
+ const overlay = document.getElementById('column-overlay');
221
+ const img = document.getElementById('preview-image');
222
+ if (!overlay || !img) return;
223
+
224
+ // Clear existing
225
+ overlay.innerHTML = '';
226
+
227
+ const imgWidth = img.naturalWidth || img.width;
228
+ const imgHeight = img.naturalHeight || img.height;
229
+ const pxPerMm = getPxPerMm();
230
+
231
+ // Set SVG size
232
+ overlay.setAttribute('viewBox', `0 0 ${imgWidth} ${imgHeight}`);
233
+ overlay.style.width = imgWidth + 'px';
234
+ overlay.style.height = imgHeight + 'px';
235
+
236
+ // Journal column width: 45mm
237
+ // Lines at 0, 45, 90, 135, 180mm for 0, 1, 2, 3, 4 columns
238
+ const columnPositions = [
239
+ { mm: 0, label: '0' },
240
+ { mm: 45, label: '1 col' },
241
+ { mm: 90, label: '2 col' },
242
+ { mm: 135, label: '3 col' },
243
+ { mm: 180, label: '4 col' }
244
+ ];
245
+
246
+ for (const col of columnPositions) {
247
+ const x = col.mm * pxPerMm;
248
+
249
+ // Skip if beyond image width
250
+ if (x > imgWidth) continue;
251
+
252
+ // Draw vertical line
253
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
254
+ line.setAttribute('x1', x);
255
+ line.setAttribute('y1', 0);
256
+ line.setAttribute('x2', x);
257
+ line.setAttribute('y2', imgHeight);
258
+ line.setAttribute('class', 'column-line');
259
+ overlay.appendChild(line);
260
+
261
+ // Label background
262
+ const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
263
+ const labelText = `${col.label} (${col.mm}mm)`;
264
+ const labelWidth = labelText.length * 7 + 8;
265
+ labelBg.setAttribute('x', x + 3);
266
+ labelBg.setAttribute('y', 3);
267
+ labelBg.setAttribute('width', labelWidth);
268
+ labelBg.setAttribute('height', 18);
269
+ labelBg.setAttribute('rx', 3);
270
+ labelBg.setAttribute('class', 'column-bg');
271
+ overlay.appendChild(labelBg);
272
+
273
+ // Label
274
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
275
+ text.setAttribute('x', x + 6);
276
+ text.setAttribute('y', 16);
277
+ text.setAttribute('class', 'column-text');
278
+ text.setAttribute('text-anchor', 'start');
279
+ text.textContent = labelText;
280
+ overlay.appendChild(text);
281
+ }
282
+ }
283
+
284
+ // Update all overlays when image changes
285
+ function updateOverlays() {
286
+ if (rulerVisible) drawRuler();
287
+ if (gridVisible) drawGrid();
288
+ if (columnsVisible) drawColumnGuides();
289
+ }
290
+ """
291
+
292
+ __all__ = ["SCRIPTS_OVERLAYS"]
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel drag-to-move JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Detecting mousedown on axes/panel elements
7
+ - Handling drag movement with visual feedback
8
+ - Updating panel position on drop
9
+
10
+ Coordinates are in mm with upper-left origin.
11
+ """
12
+
13
+ SCRIPTS_PANEL_DRAG = """
14
+ // ===== PANEL DRAG-TO-MOVE (mm, upper-left origin) =====
15
+
16
+ let isDraggingPanel = false;
17
+ let draggedPanelIndex = null;
18
+ let dragStartPos = null;
19
+ let dragStartPanelPos = null;
20
+ let panelDragOverlay = null;
21
+
22
+ // Initialize panel drag functionality
23
+ function initPanelDrag() {
24
+ console.log('[PanelDrag] initPanelDrag called');
25
+ const zoomContainer = document.getElementById('zoom-container');
26
+ if (!zoomContainer) {
27
+ console.error('[PanelDrag] zoom-container not found!');
28
+ return;
29
+ }
30
+
31
+ // Add mouse event listeners to zoom container
32
+ zoomContainer.addEventListener('mousedown', handlePanelDragStart);
33
+ document.addEventListener('mousemove', handlePanelDragMove);
34
+ document.addEventListener('mouseup', handlePanelDragEnd);
35
+ console.log('[PanelDrag] Event listeners attached');
36
+
37
+ // Create drag overlay element
38
+ panelDragOverlay = document.createElement('div');
39
+ panelDragOverlay.id = 'panel-drag-overlay';
40
+ panelDragOverlay.style.cssText = `
41
+ position: absolute;
42
+ border: 2px dashed #2563eb;
43
+ background: rgba(37, 99, 235, 0.1);
44
+ pointer-events: none;
45
+ display: none;
46
+ z-index: 1000;
47
+ `;
48
+ zoomContainer.appendChild(panelDragOverlay);
49
+ console.log('[PanelDrag] Overlay created:', panelDragOverlay ? 'success' : 'failed');
50
+ }
51
+
52
+ // Handle mouse down - check if on a panel/axes (only drag from empty panel area)
53
+ function handlePanelDragStart(event) {
54
+ console.log('[PanelDrag] handlePanelDragStart called, button:', event.button);
55
+ // Skip if using modifier keys for other actions
56
+ if (event.ctrlKey || event.metaKey || event.altKey) {
57
+ console.log('[PanelDrag] Skipped - modifier key pressed');
58
+ return;
59
+ }
60
+
61
+ // Only start drag if clicking on axes element or empty panel area
62
+ // Skip if clicking on specific elements (they should be selected instead)
63
+ const target = event.target;
64
+ const targetKey = target.getAttribute ? target.getAttribute('data-key') : null;
65
+ if (targetKey && typeof currentBboxes !== 'undefined' && currentBboxes[targetKey]) {
66
+ const elemType = currentBboxes[targetKey].type;
67
+ // Only allow drag from axes bbox or if no specific element type
68
+ if (elemType && elemType !== 'axes') {
69
+ console.log('[PanelDrag] Skipped - clicked on element:', elemType);
70
+ return;
71
+ }
72
+ }
73
+
74
+ const img = document.getElementById('preview-image');
75
+ if (!img || !figSize.width_mm || !figSize.height_mm) {
76
+ console.log('[PanelDrag] Skipped - img or figSize not ready');
77
+ return;
78
+ }
79
+
80
+ const rect = img.getBoundingClientRect();
81
+ const x = event.clientX - rect.left;
82
+ const y = event.clientY - rect.top;
83
+
84
+ // Convert to mm coordinates (upper-left origin)
85
+ const mmX = (x / rect.width) * figSize.width_mm;
86
+ const mmY = (y / rect.height) * figSize.height_mm;
87
+
88
+ // Find which panel was clicked (using expanded bounds including labels)
89
+ const panelIndex = findPanelAtPositionMm(mmX, mmY);
90
+ console.log('[PanelDrag] Click at mm:', mmX.toFixed(1), mmY.toFixed(1), '-> panel:', panelIndex);
91
+
92
+ if (panelIndex !== null) {
93
+ event.preventDefault();
94
+ event.stopPropagation();
95
+
96
+ isDraggingPanel = true;
97
+ draggedPanelIndex = panelIndex;
98
+ dragStartPos = { x: event.clientX, y: event.clientY };
99
+
100
+ // Get current panel position (in mm)
101
+ const axKey = Object.keys(panelPositions).sort()[panelIndex];
102
+ const pos = panelPositions[axKey];
103
+ dragStartPanelPos = { ...pos };
104
+
105
+ // Create overlay if it doesn't exist
106
+ if (!panelDragOverlay) {
107
+ console.log('[PanelDrag] Creating overlay on-demand');
108
+ const zoomContainer = document.getElementById('zoom-container');
109
+ if (zoomContainer) {
110
+ panelDragOverlay = document.createElement('div');
111
+ panelDragOverlay.id = 'panel-drag-overlay';
112
+ panelDragOverlay.style.cssText = `
113
+ position: absolute;
114
+ border: 2px dashed #2563eb;
115
+ background: rgba(37, 99, 235, 0.1);
116
+ pointer-events: none;
117
+ display: none;
118
+ z-index: 1000;
119
+ `;
120
+ zoomContainer.appendChild(panelDragOverlay);
121
+ console.log('[PanelDrag] Overlay created on-demand');
122
+ }
123
+ }
124
+
125
+ // Show drag overlay
126
+ if (panelDragOverlay) {
127
+ updateDragOverlayMm(pos, rect);
128
+ panelDragOverlay.style.display = 'block';
129
+ console.log('[PanelDrag] Overlay shown');
130
+ } else {
131
+ console.warn('[PanelDrag] Overlay still null after creation attempt');
132
+ }
133
+
134
+ // Change cursor
135
+ document.body.style.cursor = 'move';
136
+
137
+ console.log('Started dragging panel', panelIndex);
138
+ }
139
+ }
140
+
141
+ // Find which panel contains the given position (in mm, upper-left origin)
142
+ // Uses expanded bounds to include title, labels, and tick areas
143
+ function findPanelAtPositionMm(mmX, mmY) {
144
+ const axKeys = Object.keys(panelPositions).sort();
145
+
146
+ // Margins in mm to expand panel bounds for labels/title/ticks
147
+ const marginLeft = 15; // Space for y-axis label and ticks
148
+ const marginRight = 5; // Small buffer on right
149
+ const marginTop = 8; // Space for title
150
+ const marginBottom = 12; // Space for x-axis label and ticks
151
+
152
+ for (let i = 0; i < axKeys.length; i++) {
153
+ const pos = panelPositions[axKeys[i]];
154
+
155
+ // Expanded bounds including label/title areas
156
+ const expandedLeft = Math.max(0, pos.left - marginLeft);
157
+ const expandedTop = Math.max(0, pos.top - marginTop);
158
+ const expandedRight = Math.min(figSize.width_mm, pos.left + pos.width + marginRight);
159
+ const expandedBottom = Math.min(figSize.height_mm, pos.top + pos.height + marginBottom);
160
+
161
+ // Check if point is within expanded panel bounds
162
+ if (mmX >= expandedLeft && mmX <= expandedRight &&
163
+ mmY >= expandedTop && mmY <= expandedBottom) {
164
+ return i;
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+
170
+ // Handle mouse move during drag
171
+ function handlePanelDragMove(event) {
172
+ if (!isDraggingPanel) return;
173
+
174
+ event.preventDefault();
175
+
176
+ const img = document.getElementById('preview-image');
177
+ if (!img) return;
178
+
179
+ const rect = img.getBoundingClientRect();
180
+
181
+ // Calculate delta in mm
182
+ const deltaMmX = (event.clientX - dragStartPos.x) / rect.width * figSize.width_mm;
183
+ const deltaMmY = (event.clientY - dragStartPos.y) / rect.height * figSize.height_mm;
184
+
185
+ // Calculate new position (clamped to figure bounds)
186
+ const newLeft = Math.max(0, Math.min(figSize.width_mm - dragStartPanelPos.width, dragStartPanelPos.left + deltaMmX));
187
+ const newTop = Math.max(0, Math.min(figSize.height_mm - dragStartPanelPos.height, dragStartPanelPos.top + deltaMmY));
188
+
189
+ const newPos = {
190
+ left: newLeft,
191
+ top: newTop,
192
+ width: dragStartPanelPos.width,
193
+ height: dragStartPanelPos.height
194
+ };
195
+
196
+ // Update visual overlay
197
+ updateDragOverlayMm(newPos, rect);
198
+ }
199
+
200
+ // Update the drag overlay position (pos in mm, upper-left origin)
201
+ function updateDragOverlayMm(pos, imgRect) {
202
+ if (!panelDragOverlay || !figSize.width_mm) return;
203
+
204
+ // Convert mm to screen pixels
205
+ const scaleX = imgRect.width / figSize.width_mm;
206
+ const scaleY = imgRect.height / figSize.height_mm;
207
+
208
+ const left = pos.left * scaleX;
209
+ const top = pos.top * scaleY;
210
+ const width = pos.width * scaleX;
211
+ const height = pos.height * scaleY;
212
+
213
+ panelDragOverlay.style.left = `${left}px`;
214
+ panelDragOverlay.style.top = `${top}px`;
215
+ panelDragOverlay.style.width = `${width}px`;
216
+ panelDragOverlay.style.height = `${height}px`;
217
+ }
218
+
219
+ // Handle mouse up - complete the drag
220
+ async function handlePanelDragEnd(event) {
221
+ console.log('[PanelDrag] handlePanelDragEnd called, isDraggingPanel:', isDraggingPanel);
222
+ if (!isDraggingPanel) return;
223
+
224
+ // Hide overlay (with null check)
225
+ if (panelDragOverlay) {
226
+ panelDragOverlay.style.display = 'none';
227
+ console.log('[PanelDrag] Overlay hidden');
228
+ }
229
+ document.body.style.cursor = '';
230
+
231
+ const img = document.getElementById('preview-image');
232
+ if (!img) {
233
+ isDraggingPanel = false;
234
+ return;
235
+ }
236
+
237
+ const rect = img.getBoundingClientRect();
238
+
239
+ // Calculate final position in mm
240
+ const deltaMmX = (event.clientX - dragStartPos.x) / rect.width * figSize.width_mm;
241
+ const deltaMmY = (event.clientY - dragStartPos.y) / rect.height * figSize.height_mm;
242
+
243
+ const newLeft = Math.max(0, Math.min(figSize.width_mm - dragStartPanelPos.width, dragStartPanelPos.left + deltaMmX));
244
+ const newTop = Math.max(0, Math.min(figSize.height_mm - dragStartPanelPos.height, dragStartPanelPos.top + deltaMmY));
245
+
246
+ // Only update if position actually changed (threshold in mm)
247
+ const threshold = 1.0; // 1mm threshold
248
+ const deltaLeft = Math.abs(newLeft - dragStartPanelPos.left);
249
+ const deltaTop = Math.abs(newTop - dragStartPanelPos.top);
250
+ console.log('[PanelDrag] Delta: left=', deltaLeft.toFixed(2), 'top=', deltaTop.toFixed(2), 'threshold=', threshold);
251
+
252
+ if (deltaLeft > threshold || deltaTop > threshold) {
253
+ console.log('[PanelDrag] Applying new position:', newLeft.toFixed(2), newTop.toFixed(2));
254
+ // Apply the new position (in mm)
255
+ await applyDraggedPanelPosition(
256
+ draggedPanelIndex,
257
+ newLeft,
258
+ newTop,
259
+ dragStartPanelPos.width,
260
+ dragStartPanelPos.height
261
+ );
262
+ } else {
263
+ console.log('[PanelDrag] Movement below threshold, not updating');
264
+ }
265
+
266
+ // Reset state
267
+ isDraggingPanel = false;
268
+ draggedPanelIndex = null;
269
+ dragStartPos = null;
270
+ dragStartPanelPos = null;
271
+ console.log('[PanelDrag] Drag state reset');
272
+ }
273
+
274
+ // Apply the dragged panel position to the server (values in mm)
275
+ async function applyDraggedPanelPosition(axIndex, left, top, width, height) {
276
+ document.body.classList.add('loading');
277
+
278
+ try {
279
+ const response = await fetch('/update_axes_position', {
280
+ method: 'POST',
281
+ headers: { 'Content-Type': 'application/json' },
282
+ body: JSON.stringify({
283
+ ax_index: axIndex,
284
+ left: left,
285
+ top: top,
286
+ width: width,
287
+ height: height
288
+ })
289
+ });
290
+
291
+ const data = await response.json();
292
+
293
+ if (data.success) {
294
+ // Update preview image and wait for it to load
295
+ const img = document.getElementById('preview-image');
296
+ if (img) {
297
+ await new Promise((resolve) => {
298
+ img.onload = resolve;
299
+ img.src = 'data:image/png;base64,' + data.image;
300
+ });
301
+ }
302
+
303
+ // Update image size
304
+ if (data.img_size) {
305
+ currentImgWidth = data.img_size.width;
306
+ currentImgHeight = data.img_size.height;
307
+ }
308
+
309
+ // Update bboxes and hitmap
310
+ currentBboxes = data.bboxes;
311
+ loadHitmap();
312
+ updateHitRegions();
313
+
314
+ // Reload positions (now with correct image dimensions)
315
+ await loadPanelPositions();
316
+
317
+ console.log('Panel position updated via drag');
318
+ } else {
319
+ console.error('Failed to update position:', data.error);
320
+ }
321
+ } catch (error) {
322
+ console.error('Failed to update position:', error);
323
+ }
324
+
325
+ document.body.classList.remove('loading');
326
+ }
327
+
328
+ // Initialize on DOMContentLoaded
329
+ document.addEventListener('DOMContentLoaded', initPanelDrag);
330
+ """
331
+
332
+ __all__ = ["SCRIPTS_PANEL_DRAG"]
333
+
334
+ # EOF