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
@@ -1,1440 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # File: ./src/scitex/vis/editor/_web_editor.py
4
- """Web-based figure editor using Flask."""
5
-
6
- from pathlib import Path
7
- from typing import Dict, Any, Optional
8
- import copy
9
- import json
10
- import io
11
- import base64
12
- import webbrowser
13
- import threading
14
-
15
-
16
- def _find_available_port(start_port: int = 5050, max_attempts: int = 10) -> int:
17
- """Find an available port, starting from start_port."""
18
- import socket
19
-
20
- for offset in range(max_attempts):
21
- port = start_port + offset
22
- try:
23
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
24
- s.bind(('127.0.0.1', port))
25
- return port
26
- except OSError:
27
- continue
28
-
29
- raise RuntimeError(f"Could not find available port in range {start_port}-{start_port + max_attempts}")
30
-
31
-
32
- def _kill_process_on_port(port: int) -> bool:
33
- """Try to kill process using the specified port. Returns True if successful."""
34
- import subprocess
35
- import sys
36
-
37
- try:
38
- if sys.platform == 'win32':
39
- # Windows: netstat + taskkill
40
- result = subprocess.run(
41
- f'netstat -ano | findstr :{port}',
42
- shell=True, capture_output=True, text=True
43
- )
44
- if result.stdout:
45
- for line in result.stdout.strip().split('\n'):
46
- parts = line.split()
47
- if len(parts) >= 5:
48
- pid = parts[-1]
49
- subprocess.run(f'taskkill /F /PID {pid}', shell=True, capture_output=True)
50
- return True
51
- else:
52
- # Linux/Mac: fuser or lsof
53
- result = subprocess.run(
54
- ['fuser', '-k', f'{port}/tcp'],
55
- capture_output=True, text=True
56
- )
57
- if result.returncode == 0:
58
- return True
59
-
60
- # Fallback to lsof
61
- result = subprocess.run(
62
- ['lsof', '-t', f'-i:{port}'],
63
- capture_output=True, text=True
64
- )
65
- if result.stdout:
66
- for pid in result.stdout.strip().split('\n'):
67
- if pid:
68
- subprocess.run(['kill', '-9', pid], capture_output=True)
69
- return True
70
- except Exception:
71
- pass
72
-
73
- return False
74
-
75
-
76
- class WebEditor:
77
- """
78
- Browser-based figure editor using Flask.
79
-
80
- Features:
81
- - Modern responsive UI
82
- - Real-time preview via WebSocket or polling
83
- - Property editors with sliders and color pickers
84
- - Save to .manual.json
85
- - SciTeX style defaults pre-filled
86
- - Auto-finds available port if default is in use
87
- """
88
-
89
- def __init__(
90
- self,
91
- json_path: Path,
92
- metadata: Dict[str, Any],
93
- csv_data: Optional[Any] = None,
94
- png_path: Optional[Path] = None,
95
- manual_overrides: Optional[Dict[str, Any]] = None,
96
- port: int = 5050,
97
- ):
98
- self.json_path = Path(json_path)
99
- self.metadata = metadata
100
- self.csv_data = csv_data
101
- self.png_path = Path(png_path) if png_path else None
102
- self.manual_overrides = manual_overrides or {}
103
- self._requested_port = port
104
- self.port = port # Will be updated in run() if needed
105
-
106
- # Get SciTeX defaults and merge with metadata
107
- from ._defaults import get_scitex_defaults, extract_defaults_from_metadata
108
- self.scitex_defaults = get_scitex_defaults()
109
- self.metadata_defaults = extract_defaults_from_metadata(metadata)
110
-
111
- # Start with defaults, then overlay manual overrides
112
- self.current_overrides = copy.deepcopy(self.scitex_defaults)
113
- self.current_overrides.update(self.metadata_defaults)
114
- self.current_overrides.update(self.manual_overrides)
115
-
116
- # Track initial state to detect modifications
117
- self._initial_overrides = copy.deepcopy(self.current_overrides)
118
- self._user_modified = False # Set to True when user makes changes
119
-
120
- def run(self):
121
- """Launch the web editor."""
122
- try:
123
- from flask import Flask, render_template_string, request, jsonify
124
- except ImportError:
125
- raise ImportError("Flask is required for web editor. Install: pip install flask")
126
-
127
- # Handle port conflicts: try to find available port or kill existing process
128
- import socket
129
- try:
130
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
131
- s.bind(('127.0.0.1', self._requested_port))
132
- self.port = self._requested_port
133
- except OSError:
134
- # Port in use - try to kill existing process first
135
- print(f"Port {self._requested_port} is in use. Attempting to free it...")
136
- if _kill_process_on_port(self._requested_port):
137
- import time
138
- time.sleep(0.5) # Give process time to release port
139
- self.port = self._requested_port
140
- print(f"Successfully freed port {self.port}")
141
- else:
142
- # Find an alternative port
143
- self.port = _find_available_port(self._requested_port + 1)
144
- print(f"Using alternative port: {self.port}")
145
-
146
- app = Flask(__name__)
147
-
148
- # Store reference to self for routes
149
- editor = self
150
-
151
- @app.route('/')
152
- def index():
153
- return render_template_string(HTML_TEMPLATE,
154
- filename=editor.json_path.name,
155
- overrides=json.dumps(editor.current_overrides))
156
-
157
- @app.route('/preview')
158
- def preview():
159
- """Generate figure preview as base64 PNG."""
160
- img_data = editor._render_preview()
161
- return jsonify({'image': img_data})
162
-
163
- @app.route('/update', methods=['POST'])
164
- def update():
165
- """Update overrides and return new preview."""
166
- data = request.json
167
- editor.current_overrides.update(data.get('overrides', {}))
168
- editor._user_modified = True # Mark as modified to regenerate from CSV
169
- img_data = editor._render_preview()
170
- return jsonify({'image': img_data, 'status': 'updated'})
171
-
172
- @app.route('/save', methods=['POST'])
173
- def save():
174
- """Save to .manual.json."""
175
- from ._edit import save_manual_overrides
176
- try:
177
- manual_path = save_manual_overrides(editor.json_path, editor.current_overrides)
178
- return jsonify({'status': 'saved', 'path': str(manual_path)})
179
- except Exception as e:
180
- return jsonify({'status': 'error', 'message': str(e)}), 500
181
-
182
- @app.route('/shutdown', methods=['POST'])
183
- def shutdown():
184
- """Shutdown the server."""
185
- func = request.environ.get('werkzeug.server.shutdown')
186
- if func is None:
187
- raise RuntimeError('Not running with Werkzeug Server')
188
- func()
189
- return jsonify({'status': 'shutdown'})
190
-
191
- # Open browser after short delay
192
- def open_browser():
193
- import time
194
- time.sleep(0.5)
195
- webbrowser.open(f'http://127.0.0.1:{self.port}')
196
-
197
- threading.Thread(target=open_browser, daemon=True).start()
198
-
199
- print(f"Starting SciTeX Editor at http://127.0.0.1:{self.port}")
200
- print("Press Ctrl+C to stop")
201
-
202
- app.run(host='127.0.0.1', port=self.port, debug=False, use_reloader=False)
203
-
204
- def _has_modifications(self) -> bool:
205
- """Check if user has made any modifications to the figure."""
206
- return self._user_modified
207
-
208
- def _render_preview(self) -> str:
209
- """Render figure and return as base64 PNG.
210
-
211
- If original PNG exists and no overrides have been applied, return the original.
212
- Otherwise, regenerate from CSV with applied overrides using exact metadata.
213
- """
214
- # If PNG exists and this is initial load (no modifications), show original
215
- if self.png_path and self.png_path.exists() and not self._has_modifications():
216
- with open(self.png_path, 'rb') as f:
217
- return base64.b64encode(f.read()).decode('utf-8')
218
-
219
- import matplotlib
220
- matplotlib.use('Agg')
221
- import matplotlib.pyplot as plt
222
- from matplotlib.ticker import MaxNLocator
223
-
224
- # mm to pt conversion
225
- mm_to_pt = 2.83465
226
-
227
- # Get values from overrides (which includes metadata defaults)
228
- o = self.current_overrides
229
-
230
- # Dimensions
231
- dpi = o.get('dpi', 300)
232
- fig_size = o.get('fig_size', [3.15, 2.68])
233
-
234
- # Font sizes
235
- axis_fontsize = o.get('axis_fontsize', 7)
236
- tick_fontsize = o.get('tick_fontsize', 7)
237
- title_fontsize = o.get('title_fontsize', 8)
238
- legend_fontsize = o.get('legend_fontsize', 6)
239
-
240
- # Line/axis thickness (convert mm to pt)
241
- linewidth_pt = o.get('linewidth', 0.57) # Already in pt or convert
242
- axis_width_pt = o.get('axis_width', 0.2) * mm_to_pt
243
- tick_length_pt = o.get('tick_length', 0.8) * mm_to_pt
244
- tick_width_pt = o.get('tick_width', 0.2) * mm_to_pt
245
- tick_direction = o.get('tick_direction', 'out')
246
- n_ticks = o.get('n_ticks', 4)
247
-
248
- # Transparent background
249
- transparent = o.get('transparent', True)
250
-
251
- # Create figure with dimensions from overrides
252
- fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
253
- if transparent:
254
- fig.patch.set_facecolor('none')
255
- ax.patch.set_facecolor('none')
256
- elif o.get('facecolor'):
257
- fig.patch.set_facecolor(o['facecolor'])
258
- ax.patch.set_facecolor(o['facecolor'])
259
-
260
- # Plot from CSV data
261
- if self.csv_data is not None:
262
- self._plot_from_csv(ax, linewidth=linewidth_pt)
263
- else:
264
- ax.text(0.5, 0.5, "No plot data available\n(CSV not found)",
265
- ha='center', va='center', transform=ax.transAxes,
266
- fontsize=axis_fontsize)
267
-
268
- # Apply labels
269
- if o.get('title'):
270
- ax.set_title(o['title'], fontsize=title_fontsize)
271
- if o.get('xlabel'):
272
- ax.set_xlabel(o['xlabel'], fontsize=axis_fontsize)
273
- if o.get('ylabel'):
274
- ax.set_ylabel(o['ylabel'], fontsize=axis_fontsize)
275
-
276
- # Tick styling
277
- ax.tick_params(
278
- axis='both',
279
- labelsize=tick_fontsize,
280
- length=tick_length_pt,
281
- width=tick_width_pt,
282
- direction=tick_direction,
283
- )
284
-
285
- # Number of ticks
286
- ax.xaxis.set_major_locator(MaxNLocator(nbins=n_ticks))
287
- ax.yaxis.set_major_locator(MaxNLocator(nbins=n_ticks))
288
-
289
- # Grid
290
- if o.get('grid'):
291
- ax.grid(True, linewidth=axis_width_pt, alpha=0.3)
292
-
293
- # Axis limits
294
- if o.get('xlim'):
295
- ax.set_xlim(o['xlim'])
296
- if o.get('ylim'):
297
- ax.set_ylim(o['ylim'])
298
-
299
- # Spines visibility
300
- if o.get('hide_top_spine', True):
301
- ax.spines['top'].set_visible(False)
302
- if o.get('hide_right_spine', True):
303
- ax.spines['right'].set_visible(False)
304
-
305
- # Spine line width
306
- for spine in ax.spines.values():
307
- spine.set_linewidth(axis_width_pt)
308
-
309
- # Apply annotations
310
- for annot in o.get('annotations', []):
311
- if annot.get('type') == 'text':
312
- ax.text(
313
- annot.get('x', 0.5),
314
- annot.get('y', 0.5),
315
- annot.get('text', ''),
316
- transform=ax.transAxes,
317
- fontsize=annot.get('fontsize', axis_fontsize),
318
- )
319
-
320
- fig.tight_layout()
321
-
322
- # Convert to base64
323
- buf = io.BytesIO()
324
- fig.savefig(buf, format='png', dpi=dpi, bbox_inches='tight', transparent=transparent)
325
- buf.seek(0)
326
- img_data = base64.b64encode(buf.read()).decode('utf-8')
327
- plt.close(fig)
328
-
329
- return img_data
330
-
331
- def _plot_from_csv(self, ax, linewidth=1.0):
332
- """Reconstruct plot from CSV data using trace info from overrides."""
333
- import pandas as pd
334
-
335
- if not isinstance(self.csv_data, pd.DataFrame):
336
- return
337
-
338
- df = self.csv_data
339
- o = self.current_overrides
340
-
341
- # Get legend settings from overrides
342
- legend_fontsize = o.get('legend_fontsize', 6)
343
- legend_visible = o.get('legend_visible', True)
344
- legend_frameon = o.get('legend_frameon', False)
345
- legend_loc = o.get('legend_loc', 'best')
346
-
347
- # Get traces from overrides (which may have been edited by user)
348
- traces = o.get('traces', [])
349
-
350
- if traces:
351
- # Use trace information to reconstruct plot correctly
352
- for trace in traces:
353
- csv_cols = trace.get('csv_columns', {})
354
- x_col = csv_cols.get('x')
355
- y_col = csv_cols.get('y')
356
-
357
- if x_col in df.columns and y_col in df.columns:
358
- ax.plot(
359
- df[x_col],
360
- df[y_col],
361
- label=trace.get('label', trace.get('id', '')),
362
- color=trace.get('color'),
363
- linestyle=trace.get('linestyle', '-'),
364
- linewidth=trace.get('linewidth', linewidth),
365
- marker=trace.get('marker', None),
366
- markersize=trace.get('markersize', 6),
367
- )
368
-
369
- # Add legend if there are labeled traces
370
- if legend_visible and any(t.get('label') for t in traces):
371
- ax.legend(
372
- fontsize=legend_fontsize,
373
- frameon=legend_frameon,
374
- loc=legend_loc,
375
- )
376
- else:
377
- # Fallback: smart parsing of CSV column names
378
- # Format: ax_00_{id}_plot_x, ax_00_{id}_plot_y
379
- cols = df.columns.tolist()
380
-
381
- # Group columns by trace ID
382
- trace_groups = {}
383
- for col in cols:
384
- if col.endswith('_x'):
385
- trace_id = col[:-2] # Remove '_x'
386
- y_col = trace_id + '_y'
387
- if y_col in cols:
388
- # Extract label from column name (e.g., ax_00_sine_plot -> sine)
389
- parts = trace_id.split('_')
390
- label = parts[2] if len(parts) > 2 else trace_id
391
- trace_groups[trace_id] = {
392
- 'x_col': col,
393
- 'y_col': y_col,
394
- 'label': label,
395
- }
396
-
397
- if trace_groups:
398
- for trace_id, info in trace_groups.items():
399
- ax.plot(
400
- df[info['x_col']],
401
- df[info['y_col']],
402
- label=info['label'],
403
- linewidth=linewidth,
404
- )
405
- if legend_visible:
406
- ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
407
- elif len(cols) >= 2:
408
- # Last resort: assume first column is x, rest are y
409
- x_col = cols[0]
410
- for y_col in cols[1:]:
411
- try:
412
- ax.plot(df[x_col], df[y_col], label=str(y_col), linewidth=linewidth)
413
- except Exception:
414
- pass
415
- if len(cols) > 2 and legend_visible:
416
- ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
417
-
418
-
419
- # HTML template for web editor with light/dark mode based on scitex-cloud
420
- HTML_TEMPLATE = '''
421
- <!DOCTYPE html>
422
- <html lang="en" data-theme="dark">
423
- <head>
424
- <meta charset="UTF-8">
425
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
426
- <title>SciTeX Editor - {{ filename }}</title>
427
- <style>
428
- /* =============================================================================
429
- * SciTeX Color System - Based on scitex-cloud/static/shared/css
430
- * ============================================================================= */
431
- :root, [data-theme="light"] {
432
- /* Brand colors (light mode) */
433
- --scitex-01: #1a2a40;
434
- --scitex-02: #34495e;
435
- --scitex-03: #506b7a;
436
- --scitex-04: #6c8ba0;
437
- --scitex-05: #8fa4b0;
438
- --scitex-06: #b5c7d1;
439
- --scitex-07: #d4e1e8;
440
- --white: #fafbfc;
441
- --gray-subtle: #f6f8fa;
442
-
443
- /* Semantic tokens */
444
- --text-primary: var(--scitex-01);
445
- --text-secondary: var(--scitex-02);
446
- --text-muted: var(--scitex-04);
447
- --text-inverse: var(--white);
448
-
449
- --bg-page: #fefefe;
450
- --bg-surface: var(--white);
451
- --bg-muted: var(--gray-subtle);
452
-
453
- --border-default: var(--scitex-05);
454
- --border-muted: var(--scitex-06);
455
-
456
- /* Workspace colors */
457
- --workspace-bg-primary: #f8f9fa;
458
- --workspace-bg-secondary: #f3f4f6;
459
- --workspace-bg-tertiary: #ebedef;
460
- --workspace-bg-elevated: #ffffff;
461
- --workspace-border-subtle: #e0e4e8;
462
- --workspace-border-default: #b5c7d1;
463
-
464
- /* Status */
465
- --status-success: #4a9b7e;
466
- --status-warning: #b8956a;
467
- --status-error: #a67373;
468
-
469
- /* CTA */
470
- --color-cta: #3b82f6;
471
- --color-cta-hover: #2563eb;
472
-
473
- /* Preview background (checkered for transparency) */
474
- --preview-bg: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
475
- linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
476
- linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
477
- linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
478
- }
479
-
480
- [data-theme="dark"] {
481
- /* Semantic tokens (dark mode) */
482
- --text-primary: var(--scitex-07);
483
- --text-secondary: var(--scitex-05);
484
- --text-muted: var(--scitex-04);
485
- --text-inverse: var(--scitex-01);
486
-
487
- --bg-page: #0f1419;
488
- --bg-surface: var(--scitex-01);
489
- --bg-muted: var(--scitex-02);
490
-
491
- --border-default: var(--scitex-03);
492
- --border-muted: var(--scitex-02);
493
-
494
- /* Workspace colors */
495
- --workspace-bg-primary: #0d0d0d;
496
- --workspace-bg-secondary: #151515;
497
- --workspace-bg-tertiary: #1a1a1a;
498
- --workspace-bg-elevated: #1f1f1f;
499
- --workspace-border-subtle: #1a1a1a;
500
- --workspace-border-default: #3a3a3a;
501
-
502
- /* Status */
503
- --status-success: #6ba89a;
504
- --status-warning: #d4a87a;
505
- --status-error: #c08888;
506
-
507
- /* Preview background (darker checkered) */
508
- --preview-bg: linear-gradient(45deg, #2a2a2a 25%, transparent 25%),
509
- linear-gradient(-45deg, #2a2a2a 25%, transparent 25%),
510
- linear-gradient(45deg, transparent 75%, #2a2a2a 75%),
511
- linear-gradient(-45deg, transparent 75%, #2a2a2a 75%);
512
- }
513
-
514
- /* =============================================================================
515
- * Base Styles
516
- * ============================================================================= */
517
- * { box-sizing: border-box; margin: 0; padding: 0; }
518
-
519
- body {
520
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
521
- background: var(--workspace-bg-primary);
522
- color: var(--text-primary);
523
- transition: background 0.3s, color 0.3s;
524
- }
525
-
526
- .container { display: flex; height: 100vh; }
527
-
528
- /* =============================================================================
529
- * Preview Panel
530
- * ============================================================================= */
531
- .preview {
532
- flex: 2;
533
- padding: 20px;
534
- display: flex;
535
- align-items: center;
536
- justify-content: center;
537
- background: var(--workspace-bg-secondary);
538
- }
539
-
540
- .preview-wrapper {
541
- background: var(--preview-bg);
542
- background-size: 20px 20px;
543
- background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
544
- border-radius: 8px;
545
- padding: 10px;
546
- box-shadow: 0 4px 20px rgba(0,0,0,0.15);
547
- }
548
-
549
- .preview img {
550
- max-width: 100%;
551
- max-height: calc(100vh - 80px);
552
- display: block;
553
- }
554
-
555
- /* =============================================================================
556
- * Controls Panel
557
- * ============================================================================= */
558
- .controls {
559
- flex: 1;
560
- min-width: 320px;
561
- max-width: 420px;
562
- background: var(--workspace-bg-elevated);
563
- border-left: 1px solid var(--workspace-border-default);
564
- overflow-y: auto;
565
- display: flex;
566
- flex-direction: column;
567
- }
568
-
569
- .controls-header {
570
- padding: 16px 20px;
571
- border-bottom: 1px solid var(--workspace-border-subtle);
572
- display: flex;
573
- justify-content: space-between;
574
- align-items: center;
575
- background: var(--bg-surface);
576
- position: sticky;
577
- top: 0;
578
- z-index: 10;
579
- }
580
-
581
- .controls-header h1 {
582
- font-size: 1.1em;
583
- font-weight: 600;
584
- color: var(--status-success);
585
- }
586
-
587
- .controls-body {
588
- padding: 0 20px 20px;
589
- flex: 1;
590
- }
591
-
592
- .filename {
593
- font-size: 0.8em;
594
- color: var(--text-muted);
595
- margin-top: 4px;
596
- word-break: break-all;
597
- }
598
-
599
- /* Theme toggle */
600
- .theme-toggle {
601
- background: transparent;
602
- border: 1px solid var(--border-muted);
603
- color: var(--text-secondary);
604
- cursor: pointer;
605
- font-size: 16px;
606
- padding: 6px 10px;
607
- border-radius: 6px;
608
- transition: all 0.2s;
609
- display: flex;
610
- align-items: center;
611
- gap: 6px;
612
- }
613
-
614
- .theme-toggle:hover {
615
- background: var(--bg-muted);
616
- border-color: var(--border-default);
617
- }
618
-
619
- /* =============================================================================
620
- * Section Headers
621
- * ============================================================================= */
622
- .section {
623
- margin-top: 16px;
624
- }
625
-
626
- .section-header {
627
- font-size: 0.75em;
628
- font-weight: 600;
629
- text-transform: uppercase;
630
- letter-spacing: 0.5px;
631
- color: var(--text-inverse);
632
- background: var(--status-success);
633
- padding: 8px 12px;
634
- border-radius: 4px;
635
- margin-bottom: 12px;
636
- }
637
-
638
- /* =============================================================================
639
- * Form Fields
640
- * ============================================================================= */
641
- .field { margin-bottom: 12px; }
642
-
643
- .field label {
644
- display: block;
645
- font-size: 0.8em;
646
- font-weight: 500;
647
- margin-bottom: 4px;
648
- color: var(--text-secondary);
649
- }
650
-
651
- .field input[type="text"],
652
- .field input[type="number"],
653
- .field select {
654
- width: 100%;
655
- padding: 8px 10px;
656
- border: 1px solid var(--border-muted);
657
- border-radius: 4px;
658
- background: var(--bg-surface);
659
- color: var(--text-primary);
660
- font-size: 0.85em;
661
- transition: border-color 0.2s;
662
- }
663
-
664
- .field input:focus,
665
- .field select:focus {
666
- outline: none;
667
- border-color: var(--status-success);
668
- }
669
-
670
- .field input[type="color"] {
671
- width: 40px;
672
- height: 32px;
673
- padding: 2px;
674
- border: 1px solid var(--border-muted);
675
- border-radius: 4px;
676
- cursor: pointer;
677
- background: var(--bg-surface);
678
- }
679
-
680
- .field-row {
681
- display: flex;
682
- gap: 10px;
683
- }
684
-
685
- .field-row .field { flex: 1; }
686
-
687
- /* Checkbox styling */
688
- .checkbox-field {
689
- display: flex;
690
- align-items: center;
691
- gap: 8px;
692
- cursor: pointer;
693
- padding: 6px 0;
694
- }
695
-
696
- .checkbox-field input[type="checkbox"] {
697
- width: 16px;
698
- height: 16px;
699
- accent-color: var(--status-success);
700
- }
701
-
702
- .checkbox-field span {
703
- font-size: 0.85em;
704
- color: var(--text-primary);
705
- }
706
-
707
- /* Color field with input */
708
- .color-field {
709
- display: flex;
710
- align-items: center;
711
- gap: 8px;
712
- }
713
-
714
- .color-field input[type="text"] {
715
- flex: 1;
716
- }
717
-
718
- /* =============================================================================
719
- * Traces Section
720
- * ============================================================================= */
721
- .traces-list {
722
- max-height: 200px;
723
- overflow-y: auto;
724
- border: 1px solid var(--border-muted);
725
- border-radius: 4px;
726
- background: var(--bg-muted);
727
- }
728
-
729
- .trace-item {
730
- display: flex;
731
- align-items: center;
732
- gap: 8px;
733
- padding: 8px 10px;
734
- border-bottom: 1px solid var(--border-muted);
735
- font-size: 0.85em;
736
- }
737
-
738
- .trace-item:last-child { border-bottom: none; }
739
-
740
- .trace-color {
741
- width: 24px;
742
- height: 24px;
743
- border-radius: 4px;
744
- border: 1px solid var(--border-default);
745
- cursor: pointer;
746
- }
747
-
748
- .trace-label {
749
- flex: 1;
750
- color: var(--text-primary);
751
- }
752
-
753
- .trace-style select {
754
- padding: 4px 6px;
755
- font-size: 0.8em;
756
- border: 1px solid var(--border-muted);
757
- border-radius: 3px;
758
- background: var(--bg-surface);
759
- color: var(--text-primary);
760
- }
761
-
762
- /* =============================================================================
763
- * Annotations
764
- * ============================================================================= */
765
- .annotations-list {
766
- margin-top: 10px;
767
- max-height: 120px;
768
- overflow-y: auto;
769
- }
770
-
771
- .annotation-item {
772
- display: flex;
773
- justify-content: space-between;
774
- align-items: center;
775
- padding: 6px 10px;
776
- background: var(--bg-muted);
777
- border-radius: 4px;
778
- margin-bottom: 5px;
779
- font-size: 0.85em;
780
- }
781
-
782
- .annotation-item span { color: var(--text-primary); }
783
-
784
- .annotation-item button {
785
- padding: 3px 8px;
786
- font-size: 0.75em;
787
- background: var(--status-error);
788
- border: none;
789
- border-radius: 3px;
790
- color: white;
791
- cursor: pointer;
792
- }
793
-
794
- /* =============================================================================
795
- * Buttons
796
- * ============================================================================= */
797
- .btn {
798
- width: 100%;
799
- padding: 10px 16px;
800
- margin-top: 8px;
801
- border: none;
802
- border-radius: 4px;
803
- cursor: pointer;
804
- font-size: 0.9em;
805
- font-weight: 500;
806
- transition: all 0.2s;
807
- }
808
-
809
- .btn-primary {
810
- background: var(--status-success);
811
- color: white;
812
- }
813
-
814
- .btn-primary:hover {
815
- filter: brightness(1.1);
816
- }
817
-
818
- .btn-secondary {
819
- background: var(--bg-muted);
820
- color: var(--text-primary);
821
- border: 1px solid var(--border-muted);
822
- }
823
-
824
- .btn-secondary:hover {
825
- background: var(--workspace-bg-tertiary);
826
- }
827
-
828
- .btn-cta {
829
- background: var(--color-cta);
830
- color: white;
831
- }
832
-
833
- .btn-cta:hover {
834
- background: var(--color-cta-hover);
835
- }
836
-
837
- /* =============================================================================
838
- * Status Bar
839
- * ============================================================================= */
840
- .status-bar {
841
- margin-top: 16px;
842
- padding: 10px 12px;
843
- border-radius: 4px;
844
- background: var(--bg-muted);
845
- font-size: 0.8em;
846
- color: var(--text-secondary);
847
- border-left: 3px solid var(--status-success);
848
- }
849
-
850
- .status-bar.error {
851
- border-left-color: var(--status-error);
852
- }
853
-
854
- /* =============================================================================
855
- * Collapsible Sections
856
- * ============================================================================= */
857
- .section-toggle {
858
- cursor: pointer;
859
- display: flex;
860
- align-items: center;
861
- gap: 8px;
862
- }
863
-
864
- .section-toggle::before {
865
- content: "\\25BC";
866
- font-size: 0.7em;
867
- transition: transform 0.2s;
868
- }
869
-
870
- .section-toggle.collapsed::before {
871
- transform: rotate(-90deg);
872
- }
873
-
874
- .section-content {
875
- overflow: hidden;
876
- transition: max-height 0.3s ease;
877
- }
878
-
879
- .section-content.collapsed {
880
- max-height: 0 !important;
881
- }
882
-
883
- /* =============================================================================
884
- * Scrollbar Styling
885
- * ============================================================================= */
886
- .controls::-webkit-scrollbar,
887
- .traces-list::-webkit-scrollbar,
888
- .annotations-list::-webkit-scrollbar {
889
- width: 6px;
890
- }
891
-
892
- .controls::-webkit-scrollbar-track,
893
- .traces-list::-webkit-scrollbar-track,
894
- .annotations-list::-webkit-scrollbar-track {
895
- background: var(--bg-muted);
896
- }
897
-
898
- .controls::-webkit-scrollbar-thumb,
899
- .traces-list::-webkit-scrollbar-thumb,
900
- .annotations-list::-webkit-scrollbar-thumb {
901
- background: var(--border-default);
902
- border-radius: 3px;
903
- }
904
-
905
- .controls::-webkit-scrollbar-thumb:hover,
906
- .traces-list::-webkit-scrollbar-thumb:hover,
907
- .annotations-list::-webkit-scrollbar-thumb:hover {
908
- background: var(--text-muted);
909
- }
910
- </style>
911
- </head>
912
- <body>
913
- <div class="container">
914
- <div class="preview">
915
- <div class="preview-wrapper">
916
- <img id="preview-img" src="" alt="Figure Preview">
917
- </div>
918
- </div>
919
- <div class="controls">
920
- <div class="controls-header">
921
- <div>
922
- <h1>SciTeX Editor</h1>
923
- <div class="filename">{{ filename }}</div>
924
- </div>
925
- <button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode">
926
- <span id="theme-icon">&#9790;</span>
927
- </button>
928
- </div>
929
-
930
- <div class="controls-body">
931
- <!-- Labels Section -->
932
- <div class="section">
933
- <div class="section-header section-toggle" onclick="toggleSection(this)">Labels</div>
934
- <div class="section-content">
935
- <div class="field">
936
- <label>Title</label>
937
- <input type="text" id="title" placeholder="Figure title">
938
- </div>
939
- <div class="field">
940
- <label>X Label</label>
941
- <input type="text" id="xlabel" placeholder="X axis label">
942
- </div>
943
- <div class="field">
944
- <label>Y Label</label>
945
- <input type="text" id="ylabel" placeholder="Y axis label">
946
- </div>
947
- </div>
948
- </div>
949
-
950
- <!-- Axis Limits Section -->
951
- <div class="section">
952
- <div class="section-header section-toggle" onclick="toggleSection(this)">Axis Limits</div>
953
- <div class="section-content">
954
- <div class="field-row">
955
- <div class="field">
956
- <label>X Min</label>
957
- <input type="number" id="xmin" step="any">
958
- </div>
959
- <div class="field">
960
- <label>X Max</label>
961
- <input type="number" id="xmax" step="any">
962
- </div>
963
- </div>
964
- <div class="field-row">
965
- <div class="field">
966
- <label>Y Min</label>
967
- <input type="number" id="ymin" step="any">
968
- </div>
969
- <div class="field">
970
- <label>Y Max</label>
971
- <input type="number" id="ymax" step="any">
972
- </div>
973
- </div>
974
- </div>
975
- </div>
976
-
977
- <!-- Traces Section -->
978
- <div class="section">
979
- <div class="section-header section-toggle" onclick="toggleSection(this)">Traces</div>
980
- <div class="section-content">
981
- <div class="traces-list" id="traces-list">
982
- <!-- Dynamically populated -->
983
- </div>
984
- <div class="field" style="margin-top: 10px;">
985
- <label>Default Line Width (pt)</label>
986
- <input type="number" id="linewidth" value="1.0" min="0.1" max="5" step="0.1">
987
- </div>
988
- </div>
989
- </div>
990
-
991
- <!-- Legend Section -->
992
- <div class="section">
993
- <div class="section-header section-toggle" onclick="toggleSection(this)">Legend</div>
994
- <div class="section-content">
995
- <label class="checkbox-field">
996
- <input type="checkbox" id="legend_visible" checked>
997
- <span>Show Legend</span>
998
- </label>
999
- <div class="field">
1000
- <label>Position</label>
1001
- <select id="legend_loc">
1002
- <option value="best">Best</option>
1003
- <option value="upper right">Upper Right</option>
1004
- <option value="upper left">Upper Left</option>
1005
- <option value="lower right">Lower Right</option>
1006
- <option value="lower left">Lower Left</option>
1007
- <option value="center right">Center Right</option>
1008
- <option value="center left">Center Left</option>
1009
- <option value="upper center">Upper Center</option>
1010
- <option value="lower center">Lower Center</option>
1011
- <option value="center">Center</option>
1012
- </select>
1013
- </div>
1014
- <label class="checkbox-field">
1015
- <input type="checkbox" id="legend_frameon">
1016
- <span>Show Frame</span>
1017
- </label>
1018
- <div class="field">
1019
- <label>Font Size (pt)</label>
1020
- <input type="number" id="legend_fontsize" value="6" min="4" max="16" step="1">
1021
- </div>
1022
- </div>
1023
- </div>
1024
-
1025
- <!-- Ticks Section -->
1026
- <div class="section">
1027
- <div class="section-header section-toggle" onclick="toggleSection(this)">Ticks</div>
1028
- <div class="section-content">
1029
- <div class="field-row">
1030
- <div class="field">
1031
- <label>N Ticks</label>
1032
- <input type="number" id="n_ticks" value="4" min="2" max="10" step="1">
1033
- </div>
1034
- <div class="field">
1035
- <label>Font Size (pt)</label>
1036
- <input type="number" id="tick_fontsize" value="7" min="4" max="16" step="1">
1037
- </div>
1038
- </div>
1039
- <div class="field-row">
1040
- <div class="field">
1041
- <label>Tick Length (mm)</label>
1042
- <input type="number" id="tick_length" value="0.8" min="0.1" max="3" step="0.1">
1043
- </div>
1044
- <div class="field">
1045
- <label>Tick Width (mm)</label>
1046
- <input type="number" id="tick_width" value="0.2" min="0.05" max="1" step="0.05">
1047
- </div>
1048
- </div>
1049
- <div class="field">
1050
- <label>Direction</label>
1051
- <select id="tick_direction">
1052
- <option value="out">Out</option>
1053
- <option value="in">In</option>
1054
- <option value="inout">Both</option>
1055
- </select>
1056
- </div>
1057
- </div>
1058
- </div>
1059
-
1060
- <!-- Style Section -->
1061
- <div class="section">
1062
- <div class="section-header section-toggle" onclick="toggleSection(this)">Style</div>
1063
- <div class="section-content">
1064
- <label class="checkbox-field">
1065
- <input type="checkbox" id="grid">
1066
- <span>Show Grid</span>
1067
- </label>
1068
- <label class="checkbox-field">
1069
- <input type="checkbox" id="hide_top_spine" checked>
1070
- <span>Hide Top Spine</span>
1071
- </label>
1072
- <label class="checkbox-field">
1073
- <input type="checkbox" id="hide_right_spine" checked>
1074
- <span>Hide Right Spine</span>
1075
- </label>
1076
- <div class="field-row">
1077
- <div class="field">
1078
- <label>Axis Width (mm)</label>
1079
- <input type="number" id="axis_width" value="0.2" min="0.05" max="1" step="0.05">
1080
- </div>
1081
- <div class="field">
1082
- <label>Label Size (pt)</label>
1083
- <input type="number" id="axis_fontsize" value="7" min="4" max="16" step="1">
1084
- </div>
1085
- </div>
1086
- <div class="field">
1087
- <label>Background Color</label>
1088
- <div class="color-field">
1089
- <input type="color" id="facecolor" value="#ffffff">
1090
- <input type="text" id="facecolor_text" value="#ffffff" placeholder="#ffffff">
1091
- </div>
1092
- </div>
1093
- <label class="checkbox-field">
1094
- <input type="checkbox" id="transparent" checked>
1095
- <span>Transparent Background</span>
1096
- </label>
1097
- </div>
1098
- </div>
1099
-
1100
- <!-- Dimensions Section -->
1101
- <div class="section">
1102
- <div class="section-header section-toggle" onclick="toggleSection(this)">Dimensions</div>
1103
- <div class="section-content">
1104
- <div class="field-row">
1105
- <div class="field">
1106
- <label>Width (inch)</label>
1107
- <input type="number" id="fig_width" value="3.15" min="1" max="12" step="0.1">
1108
- </div>
1109
- <div class="field">
1110
- <label>Height (inch)</label>
1111
- <input type="number" id="fig_height" value="2.68" min="1" max="12" step="0.1">
1112
- </div>
1113
- </div>
1114
- <div class="field">
1115
- <label>DPI</label>
1116
- <input type="number" id="dpi" value="300" min="72" max="600" step="1">
1117
- </div>
1118
- </div>
1119
- </div>
1120
-
1121
- <!-- Annotations Section -->
1122
- <div class="section">
1123
- <div class="section-header section-toggle" onclick="toggleSection(this)">Annotations</div>
1124
- <div class="section-content">
1125
- <div class="field">
1126
- <label>Text</label>
1127
- <input type="text" id="annot-text" placeholder="Annotation text">
1128
- </div>
1129
- <div class="field-row">
1130
- <div class="field">
1131
- <label>X (0-1)</label>
1132
- <input type="number" id="annot-x" value="0.5" min="0" max="1" step="0.05">
1133
- </div>
1134
- <div class="field">
1135
- <label>Y (0-1)</label>
1136
- <input type="number" id="annot-y" value="0.5" min="0" max="1" step="0.05">
1137
- </div>
1138
- <div class="field">
1139
- <label>Size</label>
1140
- <input type="number" id="annot-size" value="8" min="4" max="24" step="1">
1141
- </div>
1142
- </div>
1143
- <button class="btn btn-secondary" onclick="addAnnotation()">Add Annotation</button>
1144
- <div class="annotations-list" id="annotations-list"></div>
1145
- </div>
1146
- </div>
1147
-
1148
- <!-- Actions Section -->
1149
- <div class="section">
1150
- <div class="section-header">Actions</div>
1151
- <button class="btn btn-cta" onclick="updatePreview()">Update Preview</button>
1152
- <button class="btn btn-primary" onclick="saveManual()">Save to .manual.json</button>
1153
- <button class="btn btn-secondary" onclick="resetOverrides()">Reset to Original</button>
1154
- </div>
1155
-
1156
- <div class="status-bar" id="status">Ready</div>
1157
- </div>
1158
- </div>
1159
- </div>
1160
-
1161
- <script>
1162
- let overrides = {{ overrides|safe }};
1163
- let traces = overrides.traces || [];
1164
-
1165
- // Theme management
1166
- function toggleTheme() {
1167
- const html = document.documentElement;
1168
- const current = html.getAttribute('data-theme');
1169
- const next = current === 'dark' ? 'light' : 'dark';
1170
- html.setAttribute('data-theme', next);
1171
- document.getElementById('theme-icon').innerHTML = next === 'dark' ? '&#9790;' : '&#9788;';
1172
- localStorage.setItem('scitex-editor-theme', next);
1173
- }
1174
-
1175
- // Load saved theme
1176
- const savedTheme = localStorage.getItem('scitex-editor-theme');
1177
- if (savedTheme) {
1178
- document.documentElement.setAttribute('data-theme', savedTheme);
1179
- document.getElementById('theme-icon').innerHTML = savedTheme === 'dark' ? '&#9790;' : '&#9788;';
1180
- }
1181
-
1182
- // Collapsible sections
1183
- function toggleSection(header) {
1184
- header.classList.toggle('collapsed');
1185
- const content = header.nextElementSibling;
1186
- content.classList.toggle('collapsed');
1187
- }
1188
-
1189
- // Initialize fields
1190
- document.addEventListener('DOMContentLoaded', () => {
1191
- // Labels
1192
- if (overrides.title) document.getElementById('title').value = overrides.title;
1193
- if (overrides.xlabel) document.getElementById('xlabel').value = overrides.xlabel;
1194
- if (overrides.ylabel) document.getElementById('ylabel').value = overrides.ylabel;
1195
-
1196
- // Axis limits
1197
- if (overrides.xlim) {
1198
- document.getElementById('xmin').value = overrides.xlim[0];
1199
- document.getElementById('xmax').value = overrides.xlim[1];
1200
- }
1201
- if (overrides.ylim) {
1202
- document.getElementById('ymin').value = overrides.ylim[0];
1203
- document.getElementById('ymax').value = overrides.ylim[1];
1204
- }
1205
-
1206
- // Traces
1207
- document.getElementById('linewidth').value = overrides.linewidth || 1.0;
1208
- updateTracesList();
1209
-
1210
- // Legend
1211
- document.getElementById('legend_visible').checked = overrides.legend_visible !== false;
1212
- document.getElementById('legend_loc').value = overrides.legend_loc || 'best';
1213
- document.getElementById('legend_frameon').checked = overrides.legend_frameon || false;
1214
- document.getElementById('legend_fontsize').value = overrides.legend_fontsize || 6;
1215
-
1216
- // Ticks
1217
- document.getElementById('n_ticks').value = overrides.n_ticks || 4;
1218
- document.getElementById('tick_fontsize').value = overrides.tick_fontsize || 7;
1219
- document.getElementById('tick_length').value = overrides.tick_length || 0.8;
1220
- document.getElementById('tick_width').value = overrides.tick_width || 0.2;
1221
- document.getElementById('tick_direction').value = overrides.tick_direction || 'out';
1222
-
1223
- // Style
1224
- document.getElementById('grid').checked = overrides.grid || false;
1225
- document.getElementById('hide_top_spine').checked = overrides.hide_top_spine !== false;
1226
- document.getElementById('hide_right_spine').checked = overrides.hide_right_spine !== false;
1227
- document.getElementById('axis_width').value = overrides.axis_width || 0.2;
1228
- document.getElementById('axis_fontsize').value = overrides.axis_fontsize || 7;
1229
- document.getElementById('facecolor').value = overrides.facecolor || '#ffffff';
1230
- document.getElementById('facecolor_text').value = overrides.facecolor || '#ffffff';
1231
- document.getElementById('transparent').checked = overrides.transparent !== false;
1232
-
1233
- // Dimensions
1234
- if (overrides.fig_size) {
1235
- document.getElementById('fig_width').value = overrides.fig_size[0];
1236
- document.getElementById('fig_height').value = overrides.fig_size[1];
1237
- }
1238
- document.getElementById('dpi').value = overrides.dpi || 300;
1239
-
1240
- // Sync color inputs
1241
- document.getElementById('facecolor').addEventListener('input', (e) => {
1242
- document.getElementById('facecolor_text').value = e.target.value;
1243
- });
1244
- document.getElementById('facecolor_text').addEventListener('change', (e) => {
1245
- document.getElementById('facecolor').value = e.target.value;
1246
- });
1247
-
1248
- updateAnnotationsList();
1249
- updatePreview();
1250
- });
1251
-
1252
- // Traces list management
1253
- function updateTracesList() {
1254
- const list = document.getElementById('traces-list');
1255
- if (!traces || traces.length === 0) {
1256
- list.innerHTML = '<div style="padding: 10px; color: var(--text-muted); font-size: 0.85em;">No traces found in metadata</div>';
1257
- return;
1258
- }
1259
-
1260
- list.innerHTML = traces.map((t, i) => `
1261
- <div class="trace-item">
1262
- <input type="color" class="trace-color" value="${t.color || '#1f77b4'}"
1263
- onchange="updateTraceColor(${i}, this.value)">
1264
- <span class="trace-label">${t.label || t.id || 'Trace ' + (i+1)}</span>
1265
- <div class="trace-style">
1266
- <select onchange="updateTraceStyle(${i}, this.value)">
1267
- <option value="-" ${t.linestyle === '-' ? 'selected' : ''}>Solid</option>
1268
- <option value="--" ${t.linestyle === '--' ? 'selected' : ''}>Dashed</option>
1269
- <option value=":" ${t.linestyle === ':' ? 'selected' : ''}>Dotted</option>
1270
- <option value="-." ${t.linestyle === '-.' ? 'selected' : ''}>Dash-dot</option>
1271
- </select>
1272
- </div>
1273
- </div>
1274
- `).join('');
1275
- }
1276
-
1277
- function updateTraceColor(idx, color) {
1278
- if (traces[idx]) {
1279
- traces[idx].color = color;
1280
- }
1281
- }
1282
-
1283
- function updateTraceStyle(idx, style) {
1284
- if (traces[idx]) {
1285
- traces[idx].linestyle = style;
1286
- }
1287
- }
1288
-
1289
- function collectOverrides() {
1290
- const o = {};
1291
-
1292
- // Labels
1293
- const title = document.getElementById('title').value;
1294
- const xlabel = document.getElementById('xlabel').value;
1295
- const ylabel = document.getElementById('ylabel').value;
1296
- if (title) o.title = title;
1297
- if (xlabel) o.xlabel = xlabel;
1298
- if (ylabel) o.ylabel = ylabel;
1299
-
1300
- // Axis limits
1301
- const xmin = document.getElementById('xmin').value;
1302
- const xmax = document.getElementById('xmax').value;
1303
- if (xmin !== '' && xmax !== '') o.xlim = [parseFloat(xmin), parseFloat(xmax)];
1304
-
1305
- const ymin = document.getElementById('ymin').value;
1306
- const ymax = document.getElementById('ymax').value;
1307
- if (ymin !== '' && ymax !== '') o.ylim = [parseFloat(ymin), parseFloat(ymax)];
1308
-
1309
- // Traces
1310
- o.linewidth = parseFloat(document.getElementById('linewidth').value) || 1.0;
1311
- o.traces = traces;
1312
-
1313
- // Legend
1314
- o.legend_visible = document.getElementById('legend_visible').checked;
1315
- o.legend_loc = document.getElementById('legend_loc').value;
1316
- o.legend_frameon = document.getElementById('legend_frameon').checked;
1317
- o.legend_fontsize = parseInt(document.getElementById('legend_fontsize').value) || 6;
1318
-
1319
- // Ticks
1320
- o.n_ticks = parseInt(document.getElementById('n_ticks').value) || 4;
1321
- o.tick_fontsize = parseInt(document.getElementById('tick_fontsize').value) || 7;
1322
- o.tick_length = parseFloat(document.getElementById('tick_length').value) || 0.8;
1323
- o.tick_width = parseFloat(document.getElementById('tick_width').value) || 0.2;
1324
- o.tick_direction = document.getElementById('tick_direction').value;
1325
-
1326
- // Style
1327
- o.grid = document.getElementById('grid').checked;
1328
- o.hide_top_spine = document.getElementById('hide_top_spine').checked;
1329
- o.hide_right_spine = document.getElementById('hide_right_spine').checked;
1330
- o.axis_width = parseFloat(document.getElementById('axis_width').value) || 0.2;
1331
- o.axis_fontsize = parseInt(document.getElementById('axis_fontsize').value) || 7;
1332
- o.facecolor = document.getElementById('facecolor').value;
1333
- o.transparent = document.getElementById('transparent').checked;
1334
-
1335
- // Dimensions
1336
- o.fig_size = [
1337
- parseFloat(document.getElementById('fig_width').value) || 3.15,
1338
- parseFloat(document.getElementById('fig_height').value) || 2.68
1339
- ];
1340
- o.dpi = parseInt(document.getElementById('dpi').value) || 300;
1341
-
1342
- // Annotations
1343
- o.annotations = overrides.annotations || [];
1344
-
1345
- return o;
1346
- }
1347
-
1348
- async function updatePreview() {
1349
- setStatus('Updating...', false);
1350
- overrides = collectOverrides();
1351
- try {
1352
- const resp = await fetch('/update', {
1353
- method: 'POST',
1354
- headers: {'Content-Type': 'application/json'},
1355
- body: JSON.stringify({overrides})
1356
- });
1357
- const data = await resp.json();
1358
- document.getElementById('preview-img').src = 'data:image/png;base64,' + data.image;
1359
- setStatus('Preview updated', false);
1360
- } catch (e) {
1361
- setStatus('Error: ' + e.message, true);
1362
- }
1363
- }
1364
-
1365
- async function saveManual() {
1366
- setStatus('Saving...', false);
1367
- try {
1368
- const resp = await fetch('/save', {
1369
- method: 'POST',
1370
- headers: {'Content-Type': 'application/json'}
1371
- });
1372
- const data = await resp.json();
1373
- if (data.status === 'saved') {
1374
- setStatus('Saved: ' + data.path.split('/').pop(), false);
1375
- } else {
1376
- setStatus('Error: ' + data.message, true);
1377
- }
1378
- } catch (e) {
1379
- setStatus('Error: ' + e.message, true);
1380
- }
1381
- }
1382
-
1383
- function resetOverrides() {
1384
- if (confirm('Reset all changes to original values?')) {
1385
- location.reload();
1386
- }
1387
- }
1388
-
1389
- function addAnnotation() {
1390
- const text = document.getElementById('annot-text').value;
1391
- if (!text) return;
1392
- const x = parseFloat(document.getElementById('annot-x').value) || 0.5;
1393
- const y = parseFloat(document.getElementById('annot-y').value) || 0.5;
1394
- const size = parseInt(document.getElementById('annot-size').value) || 8;
1395
- if (!overrides.annotations) overrides.annotations = [];
1396
- overrides.annotations.push({type: 'text', text, x, y, fontsize: size});
1397
- document.getElementById('annot-text').value = '';
1398
- updateAnnotationsList();
1399
- updatePreview();
1400
- }
1401
-
1402
- function removeAnnotation(idx) {
1403
- overrides.annotations.splice(idx, 1);
1404
- updateAnnotationsList();
1405
- updatePreview();
1406
- }
1407
-
1408
- function updateAnnotationsList() {
1409
- const list = document.getElementById('annotations-list');
1410
- const annotations = overrides.annotations || [];
1411
- if (annotations.length === 0) {
1412
- list.innerHTML = '';
1413
- return;
1414
- }
1415
- list.innerHTML = annotations.map((a, i) =>
1416
- `<div class="annotation-item">
1417
- <span>${a.text.substring(0, 25)}${a.text.length > 25 ? '...' : ''} (${a.x.toFixed(2)}, ${a.y.toFixed(2)})</span>
1418
- <button onclick="removeAnnotation(${i})">Remove</button>
1419
- </div>`
1420
- ).join('');
1421
- }
1422
-
1423
- function setStatus(msg, isError = false) {
1424
- const el = document.getElementById('status');
1425
- el.textContent = msg;
1426
- el.classList.toggle('error', isError);
1427
- }
1428
-
1429
- // Auto-update on Enter key in input fields
1430
- document.querySelectorAll('input[type="text"], input[type="number"]').forEach(el => {
1431
- el.addEventListener('keypress', (e) => {
1432
- if (e.key === 'Enter') updatePreview();
1433
- });
1434
- });
1435
- </script>
1436
- </body>
1437
- </html>
1438
- '''
1439
-
1440
- # EOF