scitex 2.4.3__py3-none-any.whl → 2.5.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 (45) hide show
  1. scitex/__version__.py +1 -1
  2. scitex/io/_load.py +5 -0
  3. scitex/io/_load_modules/_canvas.py +171 -0
  4. scitex/io/_save.py +8 -0
  5. scitex/io/_save_modules/_canvas.py +356 -0
  6. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
  7. scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
  8. scitex/plt/utils/__init__.py +10 -0
  9. scitex/plt/utils/_collect_figure_metadata.py +14 -12
  10. scitex/plt/utils/_csv_column_naming.py +237 -0
  11. scitex/session/_decorator.py +13 -1
  12. scitex/vis/README.md +246 -615
  13. scitex/vis/__init__.py +138 -78
  14. scitex/vis/canvas.py +423 -0
  15. scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
  16. scitex/vis/editor/__init__.py +1 -1
  17. scitex/vis/editor/_dearpygui_editor.py +1830 -0
  18. scitex/vis/editor/_defaults.py +40 -1
  19. scitex/vis/editor/_edit.py +54 -18
  20. scitex/vis/editor/_flask_editor.py +37 -0
  21. scitex/vis/editor/_qt_editor.py +865 -0
  22. scitex/vis/editor/flask_editor/__init__.py +21 -0
  23. scitex/vis/editor/flask_editor/bbox.py +216 -0
  24. scitex/vis/editor/flask_editor/core.py +152 -0
  25. scitex/vis/editor/flask_editor/plotter.py +130 -0
  26. scitex/vis/editor/flask_editor/renderer.py +184 -0
  27. scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
  28. scitex/vis/editor/flask_editor/templates/html.py +295 -0
  29. scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
  30. scitex/vis/editor/flask_editor/templates/styles.py +549 -0
  31. scitex/vis/editor/flask_editor/utils.py +81 -0
  32. scitex/vis/io/__init__.py +84 -21
  33. scitex/vis/io/canvas.py +226 -0
  34. scitex/vis/io/data.py +204 -0
  35. scitex/vis/io/directory.py +202 -0
  36. scitex/vis/io/export.py +460 -0
  37. scitex/vis/io/panel.py +424 -0
  38. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
  39. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/RECORD +42 -21
  40. scitex/vis/DJANGO_INTEGRATION.md +0 -677
  41. scitex/vis/editor/_web_editor.py +0 -1440
  42. scitex/vis/tmp.txt +0 -239
  43. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
  44. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
  45. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,614 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # File: ./src/scitex/vis/editor/flask_editor/templates/scripts.py
4
+ """JavaScript for the Flask editor UI."""
5
+
6
+ JS_SCRIPTS = '''
7
+ let overrides = {{ overrides|safe }};
8
+ let traces = overrides.traces || [];
9
+ let elementBboxes = {};
10
+ let imgSize = {width: 0, height: 0};
11
+ let hoveredElement = null;
12
+ let selectedElement = null;
13
+
14
+ // Hover system - client-side hit testing
15
+ function initHoverSystem() {
16
+ const container = document.getElementById('preview-container');
17
+ const img = document.getElementById('preview-img');
18
+
19
+ img.addEventListener('mousemove', (e) => {
20
+ if (imgSize.width === 0 || imgSize.height === 0) return;
21
+
22
+ const rect = img.getBoundingClientRect();
23
+ const x = e.clientX - rect.left;
24
+ const y = e.clientY - rect.top;
25
+
26
+ const scaleX = imgSize.width / rect.width;
27
+ const scaleY = imgSize.height / rect.height;
28
+ const imgX = x * scaleX;
29
+ const imgY = y * scaleY;
30
+
31
+ const element = findElementAt(imgX, imgY);
32
+ if (element !== hoveredElement) {
33
+ hoveredElement = element;
34
+ updateOverlay();
35
+ }
36
+ });
37
+
38
+ img.addEventListener('mouseleave', () => {
39
+ hoveredElement = null;
40
+ updateOverlay();
41
+ });
42
+
43
+ img.addEventListener('click', (e) => {
44
+ if (hoveredElement) {
45
+ selectedElement = hoveredElement;
46
+ updateOverlay();
47
+ scrollToSection(selectedElement);
48
+ }
49
+ });
50
+
51
+ img.addEventListener('load', () => {
52
+ updateOverlay();
53
+ });
54
+ }
55
+
56
+ function findElementAt(x, y) {
57
+ const matches = [];
58
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
59
+ if (!name.startsWith('trace_')) {
60
+ if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
61
+ const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
62
+ matches.push({name, area});
63
+ }
64
+ }
65
+ }
66
+ if (matches.length > 0) {
67
+ matches.sort((a, b) => a.area - b.area);
68
+ return matches[0].name;
69
+ }
70
+
71
+ const PROXIMITY_THRESHOLD = 15;
72
+ let closestTrace = null;
73
+ let minDistance = Infinity;
74
+
75
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
76
+ if (name.startsWith('trace_') && bbox.points) {
77
+ if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
78
+ const dist = distanceToLine(x, y, bbox.points);
79
+ if (dist < minDistance) {
80
+ minDistance = dist;
81
+ closestTrace = name;
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ if (closestTrace && minDistance <= PROXIMITY_THRESHOLD) {
88
+ return closestTrace;
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ function distanceToLine(px, py, points) {
95
+ let minDist = Infinity;
96
+ for (let i = 0; i < points.length - 1; i++) {
97
+ const [x1, y1] = points[i];
98
+ const [x2, y2] = points[i + 1];
99
+ const dist = distanceToSegment(px, py, x1, y1, x2, y2);
100
+ if (dist < minDist) minDist = dist;
101
+ }
102
+ return minDist;
103
+ }
104
+
105
+ function distanceToSegment(px, py, x1, y1, x2, y2) {
106
+ const dx = x2 - x1;
107
+ const dy = y2 - y1;
108
+ const lenSq = dx * dx + dy * dy;
109
+
110
+ if (lenSq === 0) {
111
+ return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2);
112
+ }
113
+
114
+ let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;
115
+ t = Math.max(0, Math.min(1, t));
116
+
117
+ const projX = x1 + t * dx;
118
+ const projY = y1 + t * dy;
119
+
120
+ return Math.sqrt((px - projX) ** 2 + (py - projY) ** 2);
121
+ }
122
+
123
+ function drawTracePath(bbox, scaleX, scaleY, type) {
124
+ if (!bbox.points || bbox.points.length < 2) return '';
125
+
126
+ const points = bbox.points;
127
+ let pathD = `M ${points[0][0] * scaleX} ${points[0][1] * scaleY}`;
128
+ for (let i = 1; i < points.length; i++) {
129
+ pathD += ` L ${points[i][0] * scaleX} ${points[i][1] * scaleY}`;
130
+ }
131
+
132
+ const className = type === 'hover' ? 'hover-path' : 'selected-path';
133
+ const labelX = points[0][0] * scaleX;
134
+ const labelY = points[0][1] * scaleY - 8;
135
+ const labelClass = type === 'hover' ? 'hover-label' : 'selected-label';
136
+
137
+ return `<path class="${className}" d="${pathD}"/>` +
138
+ `<text class="${labelClass}" x="${labelX}" y="${labelY}">${bbox.label}</text>`;
139
+ }
140
+
141
+ function updateOverlay() {
142
+ const overlay = document.getElementById('hover-overlay');
143
+ const img = document.getElementById('preview-img');
144
+ const rect = img.getBoundingClientRect();
145
+
146
+ overlay.setAttribute('width', rect.width);
147
+ overlay.setAttribute('height', rect.height);
148
+
149
+ const scaleX = rect.width / imgSize.width;
150
+ const scaleY = rect.height / imgSize.height;
151
+
152
+ let svg = '';
153
+
154
+ if (hoveredElement && hoveredElement !== selectedElement) {
155
+ const bbox = elementBboxes[hoveredElement];
156
+ if (bbox) {
157
+ if (hoveredElement.startsWith('trace_') && bbox.points) {
158
+ svg += drawTracePath(bbox, scaleX, scaleY, 'hover');
159
+ } else {
160
+ const x = bbox.x0 * scaleX - 2;
161
+ const y = bbox.y0 * scaleY - 2;
162
+ const w = (bbox.x1 - bbox.x0) * scaleX + 4;
163
+ const h = (bbox.y1 - bbox.y0) * scaleY + 4;
164
+ svg += `<rect class="hover-rect" x="${x}" y="${y}" width="${w}" height="${h}" rx="2"/>`;
165
+ svg += `<text class="hover-label" x="${x}" y="${y - 4}">${bbox.label}</text>`;
166
+ }
167
+ }
168
+ }
169
+
170
+ if (selectedElement) {
171
+ const bbox = elementBboxes[selectedElement];
172
+ if (bbox) {
173
+ if (selectedElement.startsWith('trace_') && bbox.points) {
174
+ svg += drawTracePath(bbox, scaleX, scaleY, 'selected');
175
+ } else {
176
+ const x = bbox.x0 * scaleX - 2;
177
+ const y = bbox.y0 * scaleY - 2;
178
+ const w = (bbox.x1 - bbox.x0) * scaleX + 4;
179
+ const h = (bbox.y1 - bbox.y0) * scaleY + 4;
180
+ svg += `<rect class="selected-rect" x="${x}" y="${y}" width="${w}" height="${h}" rx="2"/>`;
181
+ svg += `<text class="selected-label" x="${x}" y="${y - 4}">${bbox.label}</text>`;
182
+ }
183
+ }
184
+ }
185
+
186
+ overlay.innerHTML = svg;
187
+ }
188
+
189
+ function expandSection(sectionId) {
190
+ document.querySelectorAll('.section').forEach(section => {
191
+ const header = section.querySelector('.section-header');
192
+ const content = section.querySelector('.section-content');
193
+ if (section.id === sectionId) {
194
+ header?.classList.remove('collapsed');
195
+ content?.classList.remove('collapsed');
196
+ } else if (header?.classList.contains('section-toggle')) {
197
+ header?.classList.add('collapsed');
198
+ content?.classList.add('collapsed');
199
+ }
200
+ });
201
+ }
202
+
203
+ function scrollToSection(elementName) {
204
+ const elementToSection = {
205
+ 'title': 'section-labels',
206
+ 'xlabel': 'section-labels',
207
+ 'ylabel': 'section-labels',
208
+ 'xaxis_ticks': 'section-ticks',
209
+ 'yaxis_ticks': 'section-ticks',
210
+ 'legend': 'section-legend'
211
+ };
212
+
213
+ const fieldMap = {
214
+ 'title': 'title',
215
+ 'xlabel': 'xlabel',
216
+ 'ylabel': 'ylabel',
217
+ 'xaxis_ticks': 'x_n_ticks',
218
+ 'yaxis_ticks': 'y_n_ticks',
219
+ 'legend': 'legend_visible'
220
+ };
221
+
222
+ if (elementName.startsWith('trace_')) {
223
+ expandSection('section-traces');
224
+ const traceIdx = elementBboxes[elementName]?.trace_idx;
225
+ if (traceIdx !== undefined) {
226
+ const traceColors = document.querySelectorAll('.trace-color');
227
+ if (traceColors[traceIdx]) {
228
+ setTimeout(() => {
229
+ traceColors[traceIdx].scrollIntoView({behavior: 'smooth', block: 'center'});
230
+ traceColors[traceIdx].click();
231
+ }, 100);
232
+ }
233
+ }
234
+ return;
235
+ }
236
+
237
+ const sectionId = elementToSection[elementName];
238
+ if (sectionId) {
239
+ expandSection(sectionId);
240
+ }
241
+
242
+ const fieldId = fieldMap[elementName];
243
+ if (fieldId) {
244
+ const field = document.getElementById(fieldId);
245
+ if (field) {
246
+ setTimeout(() => {
247
+ field.scrollIntoView({behavior: 'smooth', block: 'center'});
248
+ field.focus();
249
+ }, 100);
250
+ }
251
+ }
252
+ }
253
+
254
+ // Theme management
255
+ function toggleTheme() {
256
+ const html = document.documentElement;
257
+ const current = html.getAttribute('data-theme');
258
+ const next = current === 'dark' ? 'light' : 'dark';
259
+ html.setAttribute('data-theme', next);
260
+ document.getElementById('theme-icon').innerHTML = next === 'dark' ? '&#9790;' : '&#9788;';
261
+ localStorage.setItem('scitex-editor-theme', next);
262
+ }
263
+
264
+ // Collapsible sections
265
+ function toggleSection(header) {
266
+ header.classList.toggle('collapsed');
267
+ const content = header.nextElementSibling;
268
+ content.classList.toggle('collapsed');
269
+ }
270
+
271
+ function toggleCustomLegendPosition() {
272
+ const legendLoc = document.getElementById('legend_loc').value;
273
+ const customCoordsDiv = document.getElementById('custom-legend-coords');
274
+ customCoordsDiv.style.display = legendLoc === 'custom' ? 'flex' : 'none';
275
+ }
276
+
277
+ // Initialize fields
278
+ document.addEventListener('DOMContentLoaded', () => {
279
+ // Load saved theme
280
+ const savedTheme = localStorage.getItem('scitex-editor-theme');
281
+ if (savedTheme) {
282
+ document.documentElement.setAttribute('data-theme', savedTheme);
283
+ document.getElementById('theme-icon').innerHTML = savedTheme === 'dark' ? '&#9790;' : '&#9788;';
284
+ }
285
+
286
+ // Labels
287
+ if (overrides.title) document.getElementById('title').value = overrides.title;
288
+ if (overrides.xlabel) document.getElementById('xlabel').value = overrides.xlabel;
289
+ if (overrides.ylabel) document.getElementById('ylabel').value = overrides.ylabel;
290
+
291
+ // Axis limits
292
+ if (overrides.xlim) {
293
+ document.getElementById('xmin').value = overrides.xlim[0];
294
+ document.getElementById('xmax').value = overrides.xlim[1];
295
+ }
296
+ if (overrides.ylim) {
297
+ document.getElementById('ymin').value = overrides.ylim[0];
298
+ document.getElementById('ymax').value = overrides.ylim[1];
299
+ }
300
+
301
+ // Traces
302
+ document.getElementById('linewidth').value = overrides.linewidth || 1.0;
303
+ updateTracesList();
304
+
305
+ // Legend
306
+ document.getElementById('legend_visible').checked = overrides.legend_visible !== false;
307
+ document.getElementById('legend_loc').value = overrides.legend_loc || 'best';
308
+ document.getElementById('legend_frameon').checked = overrides.legend_frameon || false;
309
+ document.getElementById('legend_fontsize').value = overrides.legend_fontsize || 6;
310
+ document.getElementById('legend_x').value = overrides.legend_x !== undefined ? overrides.legend_x : 0.5;
311
+ document.getElementById('legend_y').value = overrides.legend_y !== undefined ? overrides.legend_y : 0.5;
312
+ toggleCustomLegendPosition();
313
+
314
+ // Ticks
315
+ document.getElementById('x_n_ticks').value = overrides.x_n_ticks || overrides.n_ticks || 4;
316
+ document.getElementById('y_n_ticks').value = overrides.y_n_ticks || overrides.n_ticks || 4;
317
+ document.getElementById('hide_x_ticks').checked = overrides.hide_x_ticks || false;
318
+ document.getElementById('hide_y_ticks').checked = overrides.hide_y_ticks || false;
319
+ document.getElementById('tick_fontsize').value = overrides.tick_fontsize || 7;
320
+ document.getElementById('tick_length').value = overrides.tick_length || 0.8;
321
+ document.getElementById('tick_width').value = overrides.tick_width || 0.2;
322
+ document.getElementById('tick_direction').value = overrides.tick_direction || 'out';
323
+
324
+ // Style
325
+ document.getElementById('grid').checked = overrides.grid || false;
326
+ document.getElementById('hide_top_spine').checked = overrides.hide_top_spine !== false;
327
+ document.getElementById('hide_right_spine').checked = overrides.hide_right_spine !== false;
328
+ document.getElementById('axis_width').value = overrides.axis_width || 0.2;
329
+ document.getElementById('axis_fontsize').value = overrides.axis_fontsize || 7;
330
+ document.getElementById('facecolor').value = overrides.facecolor || '#ffffff';
331
+ document.getElementById('facecolor_text').value = overrides.facecolor || '#ffffff';
332
+ document.getElementById('transparent').checked = overrides.transparent !== false;
333
+
334
+ // Dimensions
335
+ if (overrides.fig_size) {
336
+ document.getElementById('fig_width').value = Math.round(overrides.fig_size[0] * 100) / 100;
337
+ document.getElementById('fig_height').value = Math.round(overrides.fig_size[1] * 100) / 100;
338
+ }
339
+ document.getElementById('dpi').value = overrides.dpi || 300;
340
+
341
+ // Sync color inputs
342
+ document.getElementById('facecolor').addEventListener('input', (e) => {
343
+ document.getElementById('facecolor_text').value = e.target.value;
344
+ });
345
+ document.getElementById('facecolor_text').addEventListener('change', (e) => {
346
+ document.getElementById('facecolor').value = e.target.value;
347
+ });
348
+
349
+ updateAnnotationsList();
350
+ updatePreview();
351
+ initHoverSystem();
352
+ setAutoUpdateInterval();
353
+ });
354
+
355
+ // Traces list management
356
+ function updateTracesList() {
357
+ const list = document.getElementById('traces-list');
358
+ if (!traces || traces.length === 0) {
359
+ list.innerHTML = '<div style="padding: 10px; color: var(--text-muted); font-size: 0.85em;">No traces found in metadata</div>';
360
+ return;
361
+ }
362
+
363
+ list.innerHTML = traces.map((t, i) => `
364
+ <div class="trace-item">
365
+ <input type="color" class="trace-color" value="${t.color || '#1f77b4'}"
366
+ onchange="updateTraceColor(${i}, this.value)">
367
+ <span class="trace-label">${t.label || t.id || 'Trace ' + (i+1)}</span>
368
+ <div class="trace-style">
369
+ <select onchange="updateTraceStyle(${i}, this.value)">
370
+ <option value="-" ${t.linestyle === '-' ? 'selected' : ''}>Solid</option>
371
+ <option value="--" ${t.linestyle === '--' ? 'selected' : ''}>Dashed</option>
372
+ <option value=":" ${t.linestyle === ':' ? 'selected' : ''}>Dotted</option>
373
+ <option value="-." ${t.linestyle === '-.' ? 'selected' : ''}>Dash-dot</option>
374
+ </select>
375
+ </div>
376
+ </div>
377
+ `).join('');
378
+ }
379
+
380
+ function updateTraceColor(idx, color) {
381
+ if (traces[idx]) {
382
+ traces[idx].color = color;
383
+ scheduleUpdate();
384
+ }
385
+ }
386
+
387
+ function updateTraceStyle(idx, style) {
388
+ if (traces[idx]) {
389
+ traces[idx].linestyle = style;
390
+ scheduleUpdate();
391
+ }
392
+ }
393
+
394
+ function collectOverrides() {
395
+ const o = {};
396
+
397
+ // Labels
398
+ const title = document.getElementById('title').value;
399
+ const xlabel = document.getElementById('xlabel').value;
400
+ const ylabel = document.getElementById('ylabel').value;
401
+ if (title) o.title = title;
402
+ if (xlabel) o.xlabel = xlabel;
403
+ if (ylabel) o.ylabel = ylabel;
404
+
405
+ // Axis limits
406
+ const xmin = document.getElementById('xmin').value;
407
+ const xmax = document.getElementById('xmax').value;
408
+ if (xmin !== '' && xmax !== '') o.xlim = [parseFloat(xmin), parseFloat(xmax)];
409
+
410
+ const ymin = document.getElementById('ymin').value;
411
+ const ymax = document.getElementById('ymax').value;
412
+ if (ymin !== '' && ymax !== '') o.ylim = [parseFloat(ymin), parseFloat(ymax)];
413
+
414
+ // Traces
415
+ o.linewidth = parseFloat(document.getElementById('linewidth').value) || 1.0;
416
+ o.traces = traces;
417
+
418
+ // Legend
419
+ o.legend_visible = document.getElementById('legend_visible').checked;
420
+ o.legend_loc = document.getElementById('legend_loc').value;
421
+ o.legend_frameon = document.getElementById('legend_frameon').checked;
422
+ o.legend_fontsize = parseInt(document.getElementById('legend_fontsize').value) || 6;
423
+ o.legend_x = parseFloat(document.getElementById('legend_x').value) || 0.5;
424
+ o.legend_y = parseFloat(document.getElementById('legend_y').value) || 0.5;
425
+
426
+ // Ticks
427
+ o.x_n_ticks = parseInt(document.getElementById('x_n_ticks').value) || 4;
428
+ o.y_n_ticks = parseInt(document.getElementById('y_n_ticks').value) || 4;
429
+ o.hide_x_ticks = document.getElementById('hide_x_ticks').checked;
430
+ o.hide_y_ticks = document.getElementById('hide_y_ticks').checked;
431
+ o.tick_fontsize = parseInt(document.getElementById('tick_fontsize').value) || 7;
432
+ o.tick_length = parseFloat(document.getElementById('tick_length').value) || 0.8;
433
+ o.tick_width = parseFloat(document.getElementById('tick_width').value) || 0.2;
434
+ o.tick_direction = document.getElementById('tick_direction').value;
435
+
436
+ // Style
437
+ o.grid = document.getElementById('grid').checked;
438
+ o.hide_top_spine = document.getElementById('hide_top_spine').checked;
439
+ o.hide_right_spine = document.getElementById('hide_right_spine').checked;
440
+ o.axis_width = parseFloat(document.getElementById('axis_width').value) || 0.2;
441
+ o.axis_fontsize = parseInt(document.getElementById('axis_fontsize').value) || 7;
442
+ o.facecolor = document.getElementById('facecolor').value;
443
+ o.transparent = document.getElementById('transparent').checked;
444
+
445
+ // Dimensions
446
+ o.fig_size = [
447
+ parseFloat(document.getElementById('fig_width').value) || 3.15,
448
+ parseFloat(document.getElementById('fig_height').value) || 2.68
449
+ ];
450
+ o.dpi = parseInt(document.getElementById('dpi').value) || 300;
451
+
452
+ // Annotations
453
+ o.annotations = overrides.annotations || [];
454
+
455
+ return o;
456
+ }
457
+
458
+ async function updatePreview() {
459
+ setStatus('Updating...', false);
460
+ overrides = collectOverrides();
461
+ try {
462
+ const resp = await fetch('/update', {
463
+ method: 'POST',
464
+ headers: {'Content-Type': 'application/json'},
465
+ body: JSON.stringify({overrides})
466
+ });
467
+ const data = await resp.json();
468
+ document.getElementById('preview-img').src = 'data:image/png;base64,' + data.image;
469
+
470
+ if (data.bboxes) {
471
+ elementBboxes = data.bboxes;
472
+ }
473
+ if (data.img_size) {
474
+ imgSize = data.img_size;
475
+ }
476
+
477
+ selectedElement = null;
478
+ hoveredElement = null;
479
+ updateOverlay();
480
+
481
+ setStatus('Preview updated', false);
482
+ } catch (e) {
483
+ setStatus('Error: ' + e.message, true);
484
+ }
485
+ }
486
+
487
+ async function saveManual() {
488
+ setStatus('Saving...', false);
489
+ try {
490
+ const resp = await fetch('/save', {
491
+ method: 'POST',
492
+ headers: {'Content-Type': 'application/json'}
493
+ });
494
+ const data = await resp.json();
495
+ if (data.status === 'saved') {
496
+ setStatus('Saved: ' + data.path.split('/').pop(), false);
497
+ } else {
498
+ setStatus('Error: ' + data.message, true);
499
+ }
500
+ } catch (e) {
501
+ setStatus('Error: ' + e.message, true);
502
+ }
503
+ }
504
+
505
+ function resetOverrides() {
506
+ if (confirm('Reset all changes to original values?')) {
507
+ location.reload();
508
+ }
509
+ }
510
+
511
+ function addAnnotation() {
512
+ const text = document.getElementById('annot-text').value;
513
+ if (!text) return;
514
+ const x = parseFloat(document.getElementById('annot-x').value) || 0.5;
515
+ const y = parseFloat(document.getElementById('annot-y').value) || 0.5;
516
+ const size = parseInt(document.getElementById('annot-size').value) || 8;
517
+ if (!overrides.annotations) overrides.annotations = [];
518
+ overrides.annotations.push({type: 'text', text, x, y, fontsize: size});
519
+ document.getElementById('annot-text').value = '';
520
+ updateAnnotationsList();
521
+ updatePreview();
522
+ }
523
+
524
+ function removeAnnotation(idx) {
525
+ overrides.annotations.splice(idx, 1);
526
+ updateAnnotationsList();
527
+ updatePreview();
528
+ }
529
+
530
+ function updateAnnotationsList() {
531
+ const list = document.getElementById('annotations-list');
532
+ const annotations = overrides.annotations || [];
533
+ if (annotations.length === 0) {
534
+ list.innerHTML = '';
535
+ return;
536
+ }
537
+ list.innerHTML = annotations.map((a, i) =>
538
+ `<div class="annotation-item">
539
+ <span>${a.text.substring(0, 25)}${a.text.length > 25 ? '...' : ''} (${a.x.toFixed(2)}, ${a.y.toFixed(2)})</span>
540
+ <button onclick="removeAnnotation(${i})">Remove</button>
541
+ </div>`
542
+ ).join('');
543
+ }
544
+
545
+ function setStatus(msg, isError = false) {
546
+ const el = document.getElementById('status');
547
+ el.textContent = msg;
548
+ el.classList.toggle('error', isError);
549
+ }
550
+
551
+ // Debounced auto-update
552
+ let updateTimer = null;
553
+ const DEBOUNCE_DELAY = 500;
554
+
555
+ function scheduleUpdate() {
556
+ if (updateTimer) clearTimeout(updateTimer);
557
+ updateTimer = setTimeout(() => {
558
+ updatePreview();
559
+ }, DEBOUNCE_DELAY);
560
+ }
561
+
562
+ // Auto-update on input changes
563
+ document.querySelectorAll('input[type="text"], input[type="number"]').forEach(el => {
564
+ el.addEventListener('input', scheduleUpdate);
565
+ el.addEventListener('keypress', (e) => {
566
+ if (e.key === 'Enter') {
567
+ if (updateTimer) clearTimeout(updateTimer);
568
+ updatePreview();
569
+ }
570
+ });
571
+ });
572
+
573
+ document.querySelectorAll('input[type="checkbox"], select').forEach(el => {
574
+ el.addEventListener('change', () => {
575
+ if (updateTimer) clearTimeout(updateTimer);
576
+ updatePreview();
577
+ });
578
+ });
579
+
580
+ document.querySelectorAll('input[type="color"]').forEach(el => {
581
+ el.addEventListener('change', () => {
582
+ if (updateTimer) clearTimeout(updateTimer);
583
+ updatePreview();
584
+ });
585
+ });
586
+
587
+ // Ctrl+S keyboard shortcut to save
588
+ document.addEventListener('keydown', (e) => {
589
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
590
+ e.preventDefault();
591
+ saveManual();
592
+ }
593
+ });
594
+
595
+ // Auto-update interval system
596
+ let autoUpdateIntervalId = null;
597
+
598
+ function setAutoUpdateInterval() {
599
+ if (autoUpdateIntervalId) {
600
+ clearInterval(autoUpdateIntervalId);
601
+ autoUpdateIntervalId = null;
602
+ }
603
+
604
+ const intervalMs = parseInt(document.getElementById('auto_update_interval').value);
605
+ if (intervalMs > 0) {
606
+ autoUpdateIntervalId = setInterval(() => {
607
+ updatePreview();
608
+ }, intervalMs);
609
+ }
610
+ }
611
+ '''
612
+
613
+
614
+ # EOF