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,464 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Label and axis controls JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Label input handlers (title, xlabel, ylabel, suptitle)
7
+ - Axis type toggles (numerical/categorical)
8
+ - Legend position controls
9
+ """
10
+
11
+ SCRIPTS_LABELS = """
12
+ // ===== LABEL AND AXIS CONTROLS =====
13
+
14
+ // Load current axis labels from server
15
+ async function loadLabels() {
16
+ try {
17
+ const response = await fetch('/get_labels');
18
+ const labels = await response.json();
19
+
20
+ const titleInput = document.getElementById('label_title');
21
+ const xlabelInput = document.getElementById('label_xlabel');
22
+ const ylabelInput = document.getElementById('label_ylabel');
23
+ const suptitleInput = document.getElementById('label_suptitle');
24
+
25
+ if (titleInput) titleInput.value = labels.title || '';
26
+ if (xlabelInput) xlabelInput.value = labels.xlabel || '';
27
+ if (ylabelInput) ylabelInput.value = labels.ylabel || '';
28
+ if (suptitleInput) suptitleInput.value = labels.suptitle || '';
29
+
30
+ console.log('Loaded labels:', labels);
31
+ } catch (error) {
32
+ console.error('Failed to load labels:', error);
33
+ }
34
+ }
35
+
36
+ // Update axis label on server
37
+ async function updateLabel(labelType, text) {
38
+ console.log(`Updating ${labelType} to: "${text}"`);
39
+ document.body.classList.add('loading');
40
+
41
+ try {
42
+ const response = await fetch('/update_label', {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify({ label_type: labelType, text: text })
46
+ });
47
+
48
+ const data = await response.json();
49
+
50
+ if (data.success) {
51
+ const img = document.getElementById('preview-image');
52
+ img.src = 'data:image/png;base64,' + data.image;
53
+
54
+ if (data.img_size) {
55
+ currentImgWidth = data.img_size.width;
56
+ currentImgHeight = data.img_size.height;
57
+ }
58
+
59
+ currentBboxes = data.bboxes;
60
+ updateHitRegions();
61
+ console.log('Label updated successfully');
62
+ } else {
63
+ console.error('Label update failed:', data.error);
64
+ alert('Update failed: ' + data.error);
65
+ }
66
+ } catch (error) {
67
+ console.error('Label update failed:', error);
68
+ alert('Update failed: ' + error.message);
69
+ }
70
+
71
+ document.body.classList.remove('loading');
72
+ }
73
+
74
+ // Initialize label input event handlers
75
+ function initializeLabelInputs() {
76
+ const labelMap = {
77
+ 'label_title': 'title',
78
+ 'label_xlabel': 'xlabel',
79
+ 'label_ylabel': 'ylabel',
80
+ 'label_suptitle': 'suptitle'
81
+ };
82
+
83
+ for (const [inputId, labelType] of Object.entries(labelMap)) {
84
+ const input = document.getElementById(inputId);
85
+ if (input) {
86
+ let timeout;
87
+ input.addEventListener('input', function() {
88
+ clearTimeout(timeout);
89
+ timeout = setTimeout(() => {
90
+ updateLabel(labelType, this.value);
91
+ }, UPDATE_DEBOUNCE);
92
+ });
93
+
94
+ input.addEventListener('keydown', function(e) {
95
+ if (e.key === 'Enter') {
96
+ clearTimeout(timeout);
97
+ updateLabel(labelType, this.value);
98
+ }
99
+ });
100
+
101
+ input.addEventListener('blur', function() {
102
+ clearTimeout(timeout);
103
+ updateLabel(labelType, this.value);
104
+ });
105
+ }
106
+ }
107
+
108
+ initializeAxisTypeToggles();
109
+ initializeLegendPosition();
110
+ }
111
+
112
+ // Initialize axis type toggle buttons
113
+ function initializeAxisTypeToggles() {
114
+ const xNumerical = document.getElementById('xaxis-numerical');
115
+ const xCategorical = document.getElementById('xaxis-categorical');
116
+ const yNumerical = document.getElementById('yaxis-numerical');
117
+ const yCategorical = document.getElementById('yaxis-categorical');
118
+ const xLabelsRow = document.getElementById('xaxis-labels-row');
119
+ const yLabelsRow = document.getElementById('yaxis-labels-row');
120
+ const xLabelsInput = document.getElementById('xaxis_labels');
121
+ const yLabelsInput = document.getElementById('yaxis_labels');
122
+
123
+ if (xNumerical) {
124
+ xNumerical.addEventListener('click', () => {
125
+ xNumerical.classList.add('active');
126
+ xCategorical.classList.remove('active');
127
+ xLabelsRow.style.display = 'none';
128
+ updateAxisType('x', 'numerical');
129
+ });
130
+ }
131
+
132
+ if (xCategorical) {
133
+ xCategorical.addEventListener('click', () => {
134
+ xCategorical.classList.add('active');
135
+ xNumerical.classList.remove('active');
136
+ xLabelsRow.style.display = 'flex';
137
+ });
138
+ }
139
+
140
+ if (yNumerical) {
141
+ yNumerical.addEventListener('click', () => {
142
+ yNumerical.classList.add('active');
143
+ yCategorical.classList.remove('active');
144
+ yLabelsRow.style.display = 'none';
145
+ updateAxisType('y', 'numerical');
146
+ });
147
+ }
148
+
149
+ if (yCategorical) {
150
+ yCategorical.addEventListener('click', () => {
151
+ yCategorical.classList.add('active');
152
+ yNumerical.classList.remove('active');
153
+ yLabelsRow.style.display = 'flex';
154
+ });
155
+ }
156
+
157
+ // Labels input handlers
158
+ [xLabelsInput, yLabelsInput].forEach((input, idx) => {
159
+ const axis = idx === 0 ? 'x' : 'y';
160
+ if (input) {
161
+ let timeout;
162
+ input.addEventListener('input', function() {
163
+ clearTimeout(timeout);
164
+ timeout = setTimeout(() => {
165
+ const labels = this.value.split(',').map(l => l.trim()).filter(l => l);
166
+ if (labels.length > 0) updateAxisType(axis, 'categorical', labels);
167
+ }, UPDATE_DEBOUNCE);
168
+ });
169
+
170
+ input.addEventListener('keydown', function(e) {
171
+ if (e.key === 'Enter') {
172
+ clearTimeout(timeout);
173
+ const labels = this.value.split(',').map(l => l.trim()).filter(l => l);
174
+ if (labels.length > 0) updateAxisType(axis, 'categorical', labels);
175
+ }
176
+ });
177
+ }
178
+ });
179
+
180
+ loadAxisInfo();
181
+ }
182
+
183
+ // Load current axis type info
184
+ async function loadAxisInfo() {
185
+ try {
186
+ const response = await fetch('/get_axis_info');
187
+ const info = await response.json();
188
+
189
+ if (info.x_type === 'categorical') {
190
+ document.getElementById('xaxis-categorical')?.classList.add('active');
191
+ document.getElementById('xaxis-numerical')?.classList.remove('active');
192
+ const xLabelsRow = document.getElementById('xaxis-labels-row');
193
+ if (xLabelsRow) xLabelsRow.style.display = 'flex';
194
+ if (info.x_labels?.length > 0) {
195
+ const input = document.getElementById('xaxis_labels');
196
+ if (input) input.value = info.x_labels.join(', ');
197
+ }
198
+ }
199
+
200
+ if (info.y_type === 'categorical') {
201
+ document.getElementById('yaxis-categorical')?.classList.add('active');
202
+ document.getElementById('yaxis-numerical')?.classList.remove('active');
203
+ const yLabelsRow = document.getElementById('yaxis-labels-row');
204
+ if (yLabelsRow) yLabelsRow.style.display = 'flex';
205
+ if (info.y_labels?.length > 0) {
206
+ const input = document.getElementById('yaxis_labels');
207
+ if (input) input.value = info.y_labels.join(', ');
208
+ }
209
+ }
210
+
211
+ console.log('Loaded axis info:', info);
212
+ } catch (error) {
213
+ console.error('Failed to load axis info:', error);
214
+ }
215
+ }
216
+
217
+ // Update axis type on server
218
+ async function updateAxisType(axis, type, labels = []) {
219
+ console.log(`Updating ${axis} axis to ${type}`, labels);
220
+ document.body.classList.add('loading');
221
+
222
+ try {
223
+ const response = await fetch('/update_axis_type', {
224
+ method: 'POST',
225
+ headers: { 'Content-Type': 'application/json' },
226
+ body: JSON.stringify({ axis, type, labels })
227
+ });
228
+
229
+ const data = await response.json();
230
+
231
+ if (data.success) {
232
+ const img = document.getElementById('preview-image');
233
+ img.src = 'data:image/png;base64,' + data.image;
234
+
235
+ if (data.img_size) {
236
+ currentImgWidth = data.img_size.width;
237
+ currentImgHeight = data.img_size.height;
238
+ }
239
+
240
+ currentBboxes = data.bboxes;
241
+ updateHitRegions();
242
+ console.log('Axis type updated successfully');
243
+ } else {
244
+ console.error('Axis type update failed:', data.error);
245
+ alert('Update failed: ' + data.error);
246
+ }
247
+ } catch (error) {
248
+ console.error('Axis type update failed:', error);
249
+ alert('Update failed: ' + error.message);
250
+ }
251
+
252
+ document.body.classList.remove('loading');
253
+ }
254
+
255
+ // Initialize legend position controls
256
+ function initializeLegendPosition() {
257
+ const locSelect = document.getElementById('legend_loc');
258
+ const customPosDiv = document.getElementById('legend-custom-pos');
259
+ const xInput = document.getElementById('legend_x');
260
+ const yInput = document.getElementById('legend_y');
261
+ const visibleCheckbox = document.getElementById('legend_visible');
262
+
263
+ if (!locSelect) return;
264
+
265
+ if (visibleCheckbox) {
266
+ visibleCheckbox.addEventListener('change', function() {
267
+ updateLegendVisibility(this.checked);
268
+ });
269
+ }
270
+
271
+ locSelect.addEventListener('change', function() {
272
+ if (this.value === 'custom') {
273
+ customPosDiv.style.display = 'block';
274
+ } else {
275
+ customPosDiv.style.display = 'none';
276
+ updateLegendPosition(this.value);
277
+ }
278
+ });
279
+
280
+ if (xInput && yInput) {
281
+ let timeout;
282
+ const updateCustomPos = () => {
283
+ clearTimeout(timeout);
284
+ timeout = setTimeout(() => {
285
+ const x = parseFloat(xInput.value);
286
+ const y = parseFloat(yInput.value);
287
+ if (!isNaN(x) && !isNaN(y)) updateLegendPosition('custom', x, y);
288
+ }, UPDATE_DEBOUNCE);
289
+ };
290
+
291
+ xInput.addEventListener('input', updateCustomPos);
292
+ yInput.addEventListener('input', updateCustomPos);
293
+
294
+ [xInput, yInput].forEach(input => {
295
+ input.addEventListener('keydown', (e) => {
296
+ if (e.key === 'Enter') {
297
+ clearTimeout(timeout);
298
+ const x = parseFloat(xInput.value);
299
+ const y = parseFloat(yInput.value);
300
+ if (!isNaN(x) && !isNaN(y)) updateLegendPosition('custom', x, y);
301
+ }
302
+ });
303
+ });
304
+ }
305
+
306
+ loadLegendInfo();
307
+ }
308
+
309
+ // Load current legend position info
310
+ async function loadLegendInfo() {
311
+ try {
312
+ const response = await fetch('/get_legend_info');
313
+ const info = await response.json();
314
+
315
+ if (!info.has_legend) {
316
+ console.log('No legend found');
317
+ return;
318
+ }
319
+
320
+ const locSelect = document.getElementById('legend_loc');
321
+ const customPosDiv = document.getElementById('legend-custom-pos');
322
+ const xInput = document.getElementById('legend_x');
323
+ const yInput = document.getElementById('legend_y');
324
+ const visibleCheckbox = document.getElementById('legend_visible');
325
+
326
+ if (visibleCheckbox) visibleCheckbox.checked = info.visible !== false;
327
+ if (locSelect) locSelect.value = info.loc;
328
+
329
+ if (info.loc === 'custom' && customPosDiv) {
330
+ customPosDiv.style.display = 'block';
331
+ if (xInput && info.x !== null) xInput.value = info.x;
332
+ if (yInput && info.y !== null) yInput.value = info.y;
333
+ }
334
+
335
+ console.log('Loaded legend info:', info);
336
+ } catch (error) {
337
+ console.error('Failed to load legend info:', error);
338
+ }
339
+ }
340
+
341
+ // Update legend visibility
342
+ async function updateLegendVisibility(visible) {
343
+ console.log('Updating legend visibility:', visible);
344
+
345
+ try {
346
+ const response = await fetch('/update_legend_position', {
347
+ method: 'POST',
348
+ headers: { 'Content-Type': 'application/json' },
349
+ body: JSON.stringify({ visible })
350
+ });
351
+
352
+ const result = await response.json();
353
+
354
+ if (result.success) {
355
+ const previewImg = document.getElementById('preview-image');
356
+ previewImg.src = 'data:image/png;base64,' + result.image;
357
+
358
+ if (result.img_size) {
359
+ currentImgWidth = result.img_size.width;
360
+ currentImgHeight = result.img_size.height;
361
+ }
362
+
363
+ if (result.bboxes) {
364
+ currentBboxes = result.bboxes;
365
+ previewImg.onload = () => {
366
+ updateHitRegions();
367
+ loadHitmap();
368
+ };
369
+ }
370
+ } else {
371
+ console.error('Legend visibility update failed:', result.error);
372
+ }
373
+ } catch (error) {
374
+ console.error('Failed to update legend visibility:', error);
375
+ }
376
+ }
377
+
378
+ // Update legend position on server
379
+ async function updateLegendPosition(loc, x = null, y = null) {
380
+ console.log(`Updating legend position: loc=${loc}, x=${x}, y=${y}`);
381
+ document.body.classList.add('loading');
382
+
383
+ try {
384
+ const body = { loc };
385
+ if (loc === 'custom' && x !== null && y !== null) {
386
+ body.x = x;
387
+ body.y = y;
388
+ }
389
+
390
+ const response = await fetch('/update_legend_position', {
391
+ method: 'POST',
392
+ headers: { 'Content-Type': 'application/json' },
393
+ body: JSON.stringify(body)
394
+ });
395
+
396
+ const data = await response.json();
397
+
398
+ if (data.success) {
399
+ const img = document.getElementById('preview-image');
400
+ img.src = 'data:image/png;base64,' + data.image;
401
+
402
+ if (data.img_size) {
403
+ currentImgWidth = data.img_size.width;
404
+ currentImgHeight = data.img_size.height;
405
+ }
406
+
407
+ currentBboxes = data.bboxes;
408
+ updateHitRegions();
409
+ console.log('Legend position updated successfully');
410
+ } else {
411
+ console.error('Legend position update failed:', data.error);
412
+ if (!data.error.includes('No legend')) {
413
+ alert('Update failed: ' + data.error);
414
+ }
415
+ }
416
+ } catch (error) {
417
+ console.error('Legend position update failed:', error);
418
+ }
419
+
420
+ document.body.classList.remove('loading');
421
+ }
422
+
423
+ // Initialize download dropdown
424
+ function initializeDownloadDropdown() {
425
+ const mainBtn = document.getElementById('btn-download-main');
426
+ const toggleBtn = document.getElementById('btn-download-toggle');
427
+ const menu = document.getElementById('download-menu');
428
+
429
+ // Download dropdown state
430
+ let currentDownloadFormat = 'png';
431
+
432
+ mainBtn?.addEventListener('click', () => downloadFigure(currentDownloadFormat));
433
+
434
+ toggleBtn?.addEventListener('click', (e) => {
435
+ e.stopPropagation();
436
+ menu.classList.toggle('open');
437
+ });
438
+
439
+ document.querySelectorAll('.download-option').forEach(option => {
440
+ option.addEventListener('click', () => {
441
+ const format = option.dataset.format;
442
+ currentDownloadFormat = format;
443
+ mainBtn.textContent = 'Download ' + format.toUpperCase();
444
+
445
+ document.querySelectorAll('.download-option').forEach(opt => {
446
+ opt.classList.toggle('active', opt.dataset.format === format);
447
+ });
448
+
449
+ menu.classList.remove('open');
450
+ downloadFigure(format);
451
+ });
452
+ });
453
+
454
+ document.addEventListener('click', (e) => {
455
+ if (!e.target.closest('.download-dropdown')) {
456
+ menu?.classList.remove('open');
457
+ }
458
+ });
459
+ }
460
+ """
461
+
462
+ __all__ = ["SCRIPTS_LABELS"]
463
+
464
+ # EOF