scitex 2.4.2__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.
- scitex/__version__.py +1 -1
- scitex/browser/__init__.py +53 -0
- scitex/browser/debugging/__init__.py +56 -0
- scitex/browser/debugging/_failure_capture.py +372 -0
- scitex/browser/debugging/_sync_session.py +259 -0
- scitex/browser/debugging/_test_monitor.py +284 -0
- scitex/browser/debugging/_visual_cursor.py +432 -0
- scitex/io/_load.py +5 -0
- scitex/io/_load_modules/_canvas.py +171 -0
- scitex/io/_save.py +8 -0
- scitex/io/_save_modules/_canvas.py +356 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
- scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
- scitex/plt/utils/__init__.py +10 -0
- scitex/plt/utils/_collect_figure_metadata.py +14 -12
- scitex/plt/utils/_csv_column_naming.py +237 -0
- scitex/scholar/citation_graph/database.py +9 -2
- scitex/scholar/config/ScholarConfig.py +23 -3
- scitex/scholar/config/default.yaml +55 -0
- scitex/scholar/core/Paper.py +102 -0
- scitex/scholar/core/__init__.py +44 -0
- scitex/scholar/core/journal_normalizer.py +524 -0
- scitex/scholar/core/oa_cache.py +285 -0
- scitex/scholar/core/open_access.py +457 -0
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +137 -0
- scitex/scholar/pdf_download/strategies/__init__.py +6 -0
- scitex/scholar/pdf_download/strategies/open_access_download.py +186 -0
- scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +18 -3
- scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +15 -2
- scitex/session/_decorator.py +13 -1
- scitex/vis/README.md +246 -615
- scitex/vis/__init__.py +138 -78
- scitex/vis/canvas.py +423 -0
- scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
- scitex/vis/editor/__init__.py +1 -1
- scitex/vis/editor/_dearpygui_editor.py +1830 -0
- scitex/vis/editor/_defaults.py +40 -1
- scitex/vis/editor/_edit.py +54 -18
- scitex/vis/editor/_flask_editor.py +37 -0
- scitex/vis/editor/_qt_editor.py +865 -0
- scitex/vis/editor/flask_editor/__init__.py +21 -0
- scitex/vis/editor/flask_editor/bbox.py +216 -0
- scitex/vis/editor/flask_editor/core.py +152 -0
- scitex/vis/editor/flask_editor/plotter.py +130 -0
- scitex/vis/editor/flask_editor/renderer.py +184 -0
- scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
- scitex/vis/editor/flask_editor/templates/html.py +295 -0
- scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
- scitex/vis/editor/flask_editor/templates/styles.py +549 -0
- scitex/vis/editor/flask_editor/utils.py +81 -0
- scitex/vis/io/__init__.py +84 -21
- scitex/vis/io/canvas.py +226 -0
- scitex/vis/io/data.py +204 -0
- scitex/vis/io/directory.py +202 -0
- scitex/vis/io/export.py +460 -0
- scitex/vis/io/panel.py +424 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/RECORD +61 -32
- scitex/vis/DJANGO_INTEGRATION.md +0 -677
- scitex/vis/editor/_web_editor.py +0 -1440
- scitex/vis/tmp.txt +0 -239
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
scitex/vis/editor/_web_editor.py
DELETED
|
@@ -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">☾</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' ? '☾' : '☼';
|
|
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' ? '☾' : '☼';
|
|
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
|