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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ./src/scitex/vis/editor/flask_editor/__init__.py
|
|
4
|
+
"""Flask-based web editor for SciTeX figures."""
|
|
5
|
+
|
|
6
|
+
from .core import WebEditor
|
|
7
|
+
from .utils import find_available_port, kill_process_on_port, check_port_available
|
|
8
|
+
from .renderer import render_preview_with_bboxes
|
|
9
|
+
from .plotter import plot_from_csv
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'WebEditor',
|
|
13
|
+
'find_available_port',
|
|
14
|
+
'kill_process_on_port',
|
|
15
|
+
'check_port_available',
|
|
16
|
+
'render_preview_with_bboxes',
|
|
17
|
+
'plot_from_csv',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# EOF
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ./src/scitex/vis/editor/flask_editor/bbox.py
|
|
4
|
+
"""Bounding box extraction for figure elements."""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_bboxes(fig, ax, renderer, img_width: int, img_height: int) -> Dict[str, Any]:
|
|
10
|
+
"""Extract bounding boxes for all figure elements."""
|
|
11
|
+
from matplotlib.transforms import Bbox
|
|
12
|
+
|
|
13
|
+
# Get figure tight bbox in inches
|
|
14
|
+
fig_bbox = fig.get_tightbbox(renderer)
|
|
15
|
+
tight_x0 = fig_bbox.x0
|
|
16
|
+
tight_y0 = fig_bbox.y0
|
|
17
|
+
tight_width = fig_bbox.width
|
|
18
|
+
tight_height = fig_bbox.height
|
|
19
|
+
|
|
20
|
+
# bbox_inches='tight' adds pad_inches (default 0.1) around the tight bbox
|
|
21
|
+
pad_inches = 0.1
|
|
22
|
+
saved_width_inches = tight_width + 2 * pad_inches
|
|
23
|
+
saved_height_inches = tight_height + 2 * pad_inches
|
|
24
|
+
|
|
25
|
+
# Scale factors for converting inches to pixels
|
|
26
|
+
scale_x = img_width / saved_width_inches
|
|
27
|
+
scale_y = img_height / saved_height_inches
|
|
28
|
+
|
|
29
|
+
bboxes = {}
|
|
30
|
+
|
|
31
|
+
def get_element_bbox(element, name):
|
|
32
|
+
"""Get element bbox in image pixel coordinates."""
|
|
33
|
+
try:
|
|
34
|
+
bbox = element.get_window_extent(renderer)
|
|
35
|
+
|
|
36
|
+
elem_x0_inches = bbox.x0 / fig.dpi
|
|
37
|
+
elem_x1_inches = bbox.x1 / fig.dpi
|
|
38
|
+
elem_y0_inches = bbox.y0 / fig.dpi
|
|
39
|
+
elem_y1_inches = bbox.y1 / fig.dpi
|
|
40
|
+
|
|
41
|
+
x0_rel = elem_x0_inches - tight_x0 + pad_inches
|
|
42
|
+
x1_rel = elem_x1_inches - tight_x0 + pad_inches
|
|
43
|
+
y0_rel = saved_height_inches - (elem_y1_inches - tight_y0 + pad_inches)
|
|
44
|
+
y1_rel = saved_height_inches - (elem_y0_inches - tight_y0 + pad_inches)
|
|
45
|
+
|
|
46
|
+
bboxes[name] = {
|
|
47
|
+
'x0': max(0, int(x0_rel * scale_x)),
|
|
48
|
+
'y0': max(0, int(y0_rel * scale_y)),
|
|
49
|
+
'x1': min(img_width, int(x1_rel * scale_x)),
|
|
50
|
+
'y1': min(img_height, int(y1_rel * scale_y)),
|
|
51
|
+
'label': name.replace('_', ' ').title()
|
|
52
|
+
}
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"Error getting bbox for {name}: {e}")
|
|
55
|
+
|
|
56
|
+
def bbox_to_img_coords(bbox):
|
|
57
|
+
"""Convert matplotlib bbox to image pixel coordinates."""
|
|
58
|
+
x0_inches = bbox.x0 / fig.dpi
|
|
59
|
+
y0_inches = bbox.y0 / fig.dpi
|
|
60
|
+
x1_inches = bbox.x1 / fig.dpi
|
|
61
|
+
y1_inches = bbox.y1 / fig.dpi
|
|
62
|
+
x0_rel = x0_inches - tight_x0 + pad_inches
|
|
63
|
+
y0_rel = y0_inches - tight_y0 + pad_inches
|
|
64
|
+
x1_rel = x1_inches - tight_x0 + pad_inches
|
|
65
|
+
y1_rel = y1_inches - tight_y0 + pad_inches
|
|
66
|
+
return {
|
|
67
|
+
'x0': int(x0_rel * scale_x),
|
|
68
|
+
'y0': int((saved_height_inches - y1_rel) * scale_y),
|
|
69
|
+
'x1': int(x1_rel * scale_x),
|
|
70
|
+
'y1': int((saved_height_inches - y0_rel) * scale_y),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Get bboxes for title, labels
|
|
74
|
+
if ax.title.get_text():
|
|
75
|
+
get_element_bbox(ax.title, 'title')
|
|
76
|
+
if ax.xaxis.label.get_text():
|
|
77
|
+
get_element_bbox(ax.xaxis.label, 'xlabel')
|
|
78
|
+
if ax.yaxis.label.get_text():
|
|
79
|
+
get_element_bbox(ax.yaxis.label, 'ylabel')
|
|
80
|
+
|
|
81
|
+
# Get axis bboxes
|
|
82
|
+
_extract_axis_bboxes(ax, renderer, bboxes, bbox_to_img_coords, Bbox)
|
|
83
|
+
|
|
84
|
+
# Get legend bbox
|
|
85
|
+
legend = ax.get_legend()
|
|
86
|
+
if legend:
|
|
87
|
+
get_element_bbox(legend, 'legend')
|
|
88
|
+
|
|
89
|
+
# Get trace (line) bboxes
|
|
90
|
+
_extract_trace_bboxes(ax, fig, renderer, bboxes, get_element_bbox,
|
|
91
|
+
tight_x0, tight_y0, saved_height_inches, scale_x, scale_y, pad_inches)
|
|
92
|
+
|
|
93
|
+
return bboxes
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _extract_axis_bboxes(ax, renderer, bboxes, bbox_to_img_coords, Bbox):
|
|
97
|
+
"""Extract bboxes for X and Y axis elements."""
|
|
98
|
+
try:
|
|
99
|
+
# X-axis: combine spine and tick labels into one bbox
|
|
100
|
+
x_axis_bboxes = []
|
|
101
|
+
for ticklabel in ax.xaxis.get_ticklabels():
|
|
102
|
+
if ticklabel.get_visible():
|
|
103
|
+
try:
|
|
104
|
+
tb = ticklabel.get_window_extent(renderer)
|
|
105
|
+
if tb.width > 0:
|
|
106
|
+
x_axis_bboxes.append(tb)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
for tick in ax.xaxis.get_major_ticks():
|
|
110
|
+
if tick.tick1line.get_visible():
|
|
111
|
+
try:
|
|
112
|
+
tb = tick.tick1line.get_window_extent(renderer)
|
|
113
|
+
if tb.width > 0 or tb.height > 0:
|
|
114
|
+
x_axis_bboxes.append(tb)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
spine_bbox = ax.spines['bottom'].get_window_extent(renderer)
|
|
118
|
+
if spine_bbox.width > 0:
|
|
119
|
+
if x_axis_bboxes:
|
|
120
|
+
tick_union = Bbox.union(x_axis_bboxes)
|
|
121
|
+
constrained_spine = Bbox.from_extents(
|
|
122
|
+
tick_union.x0, spine_bbox.y0,
|
|
123
|
+
tick_union.x1, spine_bbox.y1
|
|
124
|
+
)
|
|
125
|
+
x_axis_bboxes.append(constrained_spine)
|
|
126
|
+
else:
|
|
127
|
+
x_axis_bboxes.append(spine_bbox)
|
|
128
|
+
if x_axis_bboxes:
|
|
129
|
+
combined = Bbox.union(x_axis_bboxes)
|
|
130
|
+
bboxes['xaxis_ticks'] = bbox_to_img_coords(combined)
|
|
131
|
+
bboxes['xaxis_ticks']['label'] = 'X Spine & Ticks'
|
|
132
|
+
|
|
133
|
+
# Y-axis: combine spine and tick labels into one bbox
|
|
134
|
+
y_axis_bboxes = []
|
|
135
|
+
for ticklabel in ax.yaxis.get_ticklabels():
|
|
136
|
+
if ticklabel.get_visible():
|
|
137
|
+
try:
|
|
138
|
+
tb = ticklabel.get_window_extent(renderer)
|
|
139
|
+
if tb.width > 0:
|
|
140
|
+
y_axis_bboxes.append(tb)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
for tick in ax.yaxis.get_major_ticks():
|
|
144
|
+
if tick.tick1line.get_visible():
|
|
145
|
+
try:
|
|
146
|
+
tb = tick.tick1line.get_window_extent(renderer)
|
|
147
|
+
if tb.width > 0 or tb.height > 0:
|
|
148
|
+
y_axis_bboxes.append(tb)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
spine_bbox = ax.spines['left'].get_window_extent(renderer)
|
|
152
|
+
if spine_bbox.height > 0:
|
|
153
|
+
if y_axis_bboxes:
|
|
154
|
+
tick_union = Bbox.union(y_axis_bboxes)
|
|
155
|
+
constrained_spine = Bbox.from_extents(
|
|
156
|
+
spine_bbox.x0, tick_union.y0,
|
|
157
|
+
spine_bbox.x1, tick_union.y1
|
|
158
|
+
)
|
|
159
|
+
y_axis_bboxes.append(constrained_spine)
|
|
160
|
+
else:
|
|
161
|
+
y_axis_bboxes.append(spine_bbox)
|
|
162
|
+
if y_axis_bboxes:
|
|
163
|
+
combined = Bbox.union(y_axis_bboxes)
|
|
164
|
+
padded = Bbox.from_extents(
|
|
165
|
+
combined.x0 - 10,
|
|
166
|
+
combined.y0 - 5,
|
|
167
|
+
combined.x1 + 5,
|
|
168
|
+
combined.y1 + 5
|
|
169
|
+
)
|
|
170
|
+
bboxes['yaxis_ticks'] = bbox_to_img_coords(padded)
|
|
171
|
+
bboxes['yaxis_ticks']['label'] = 'Y Spine & Ticks'
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(f"Error getting axis bboxes: {e}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_trace_bboxes(ax, fig, renderer, bboxes, get_element_bbox,
|
|
178
|
+
tight_x0, tight_y0, saved_height_inches, scale_x, scale_y, pad_inches):
|
|
179
|
+
"""Extract bboxes for trace (line) elements with proximity detection points."""
|
|
180
|
+
for idx, line in enumerate(ax.get_lines()):
|
|
181
|
+
try:
|
|
182
|
+
label = line.get_label()
|
|
183
|
+
if label.startswith('_'):
|
|
184
|
+
continue
|
|
185
|
+
get_element_bbox(line, f'trace_{idx}')
|
|
186
|
+
if f'trace_{idx}' in bboxes:
|
|
187
|
+
bboxes[f'trace_{idx}']['label'] = label or f'Trace {idx}'
|
|
188
|
+
bboxes[f'trace_{idx}']['trace_idx'] = idx
|
|
189
|
+
|
|
190
|
+
# Get line data points in pixel coordinates for proximity detection
|
|
191
|
+
xdata, ydata = line.get_xdata(), line.get_ydata()
|
|
192
|
+
if len(xdata) > 0:
|
|
193
|
+
transform = ax.transData
|
|
194
|
+
points_display = transform.transform(list(zip(xdata, ydata)))
|
|
195
|
+
|
|
196
|
+
points_img = []
|
|
197
|
+
for px, py in points_display:
|
|
198
|
+
px_inches = px / fig.dpi
|
|
199
|
+
py_inches = py / fig.dpi
|
|
200
|
+
x_rel = px_inches - tight_x0 + pad_inches
|
|
201
|
+
y_rel = saved_height_inches - (py_inches - tight_y0 + pad_inches)
|
|
202
|
+
x_img = int(x_rel * scale_x)
|
|
203
|
+
y_img = int(y_rel * scale_y)
|
|
204
|
+
points_img.append([x_img, y_img])
|
|
205
|
+
|
|
206
|
+
# Downsample points if too many
|
|
207
|
+
if len(points_img) > 100:
|
|
208
|
+
step = len(points_img) // 100
|
|
209
|
+
points_img = points_img[::step]
|
|
210
|
+
|
|
211
|
+
bboxes[f'trace_{idx}']['points'] = points_img
|
|
212
|
+
except Exception as e:
|
|
213
|
+
print(f"Error getting trace bbox: {e}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# EOF
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ./src/scitex/vis/editor/flask_editor/core.py
|
|
4
|
+
"""Core WebEditor class for Flask-based figure editing."""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
import copy
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
import webbrowser
|
|
12
|
+
|
|
13
|
+
from .utils import find_available_port, kill_process_on_port, check_port_available
|
|
14
|
+
from .renderer import render_preview_with_bboxes
|
|
15
|
+
from .templates import build_html_template
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WebEditor:
|
|
19
|
+
"""
|
|
20
|
+
Browser-based figure editor using Flask.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Modern responsive UI
|
|
24
|
+
- Real-time preview via WebSocket or polling
|
|
25
|
+
- Property editors with sliders and color pickers
|
|
26
|
+
- Save to .manual.json
|
|
27
|
+
- SciTeX style defaults pre-filled
|
|
28
|
+
- Auto-finds available port if default is in use
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
json_path: Path,
|
|
34
|
+
metadata: Dict[str, Any],
|
|
35
|
+
csv_data: Optional[Any] = None,
|
|
36
|
+
png_path: Optional[Path] = None,
|
|
37
|
+
manual_overrides: Optional[Dict[str, Any]] = None,
|
|
38
|
+
port: int = 5050,
|
|
39
|
+
):
|
|
40
|
+
self.json_path = Path(json_path)
|
|
41
|
+
self.metadata = metadata
|
|
42
|
+
self.csv_data = csv_data
|
|
43
|
+
self.png_path = Path(png_path) if png_path else None
|
|
44
|
+
self.manual_overrides = manual_overrides or {}
|
|
45
|
+
self._requested_port = port
|
|
46
|
+
self.port = port
|
|
47
|
+
|
|
48
|
+
# Get SciTeX defaults and merge with metadata
|
|
49
|
+
from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
50
|
+
self.scitex_defaults = get_scitex_defaults()
|
|
51
|
+
self.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
52
|
+
|
|
53
|
+
# Start with defaults, then overlay manual overrides
|
|
54
|
+
self.current_overrides = copy.deepcopy(self.scitex_defaults)
|
|
55
|
+
self.current_overrides.update(self.metadata_defaults)
|
|
56
|
+
self.current_overrides.update(self.manual_overrides)
|
|
57
|
+
|
|
58
|
+
# Track initial state to detect modifications
|
|
59
|
+
self._initial_overrides = copy.deepcopy(self.current_overrides)
|
|
60
|
+
self._user_modified = False
|
|
61
|
+
|
|
62
|
+
def run(self):
|
|
63
|
+
"""Launch the web editor."""
|
|
64
|
+
try:
|
|
65
|
+
from flask import Flask, render_template_string, request, jsonify
|
|
66
|
+
except ImportError:
|
|
67
|
+
raise ImportError("Flask is required for web editor. Install: pip install flask")
|
|
68
|
+
|
|
69
|
+
# Handle port conflicts
|
|
70
|
+
if not check_port_available(self._requested_port):
|
|
71
|
+
print(f"Port {self._requested_port} is in use. Attempting to free it...")
|
|
72
|
+
if kill_process_on_port(self._requested_port):
|
|
73
|
+
import time
|
|
74
|
+
time.sleep(0.5)
|
|
75
|
+
self.port = self._requested_port
|
|
76
|
+
print(f"Successfully freed port {self.port}")
|
|
77
|
+
else:
|
|
78
|
+
self.port = find_available_port(self._requested_port + 1)
|
|
79
|
+
print(f"Using alternative port: {self.port}")
|
|
80
|
+
else:
|
|
81
|
+
self.port = self._requested_port
|
|
82
|
+
|
|
83
|
+
app = Flask(__name__)
|
|
84
|
+
editor = self
|
|
85
|
+
html_template = build_html_template()
|
|
86
|
+
|
|
87
|
+
@app.route('/')
|
|
88
|
+
def index():
|
|
89
|
+
return render_template_string(
|
|
90
|
+
html_template,
|
|
91
|
+
filename=str(editor.json_path.resolve()),
|
|
92
|
+
overrides=json.dumps(editor.current_overrides)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@app.route('/preview')
|
|
96
|
+
def preview():
|
|
97
|
+
"""Generate figure preview as base64 PNG with element bboxes."""
|
|
98
|
+
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
99
|
+
editor.csv_data, editor.current_overrides
|
|
100
|
+
)
|
|
101
|
+
return jsonify({'image': img_data, 'bboxes': bboxes, 'img_size': img_size})
|
|
102
|
+
|
|
103
|
+
@app.route('/update', methods=['POST'])
|
|
104
|
+
def update():
|
|
105
|
+
"""Update overrides and return new preview."""
|
|
106
|
+
data = request.json
|
|
107
|
+
editor.current_overrides.update(data.get('overrides', {}))
|
|
108
|
+
editor._user_modified = True
|
|
109
|
+
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
110
|
+
editor.csv_data, editor.current_overrides
|
|
111
|
+
)
|
|
112
|
+
return jsonify({
|
|
113
|
+
'image': img_data,
|
|
114
|
+
'bboxes': bboxes,
|
|
115
|
+
'img_size': img_size,
|
|
116
|
+
'status': 'updated'
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
@app.route('/save', methods=['POST'])
|
|
120
|
+
def save():
|
|
121
|
+
"""Save to .manual.json."""
|
|
122
|
+
from .._edit import save_manual_overrides
|
|
123
|
+
try:
|
|
124
|
+
manual_path = save_manual_overrides(editor.json_path, editor.current_overrides)
|
|
125
|
+
return jsonify({'status': 'saved', 'path': str(manual_path)})
|
|
126
|
+
except Exception as e:
|
|
127
|
+
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
128
|
+
|
|
129
|
+
@app.route('/shutdown', methods=['POST'])
|
|
130
|
+
def shutdown():
|
|
131
|
+
"""Shutdown the server."""
|
|
132
|
+
func = request.environ.get('werkzeug.server.shutdown')
|
|
133
|
+
if func is None:
|
|
134
|
+
raise RuntimeError('Not running with Werkzeug Server')
|
|
135
|
+
func()
|
|
136
|
+
return jsonify({'status': 'shutdown'})
|
|
137
|
+
|
|
138
|
+
# Open browser after short delay
|
|
139
|
+
def open_browser():
|
|
140
|
+
import time
|
|
141
|
+
time.sleep(0.5)
|
|
142
|
+
webbrowser.open(f'http://127.0.0.1:{self.port}')
|
|
143
|
+
|
|
144
|
+
threading.Thread(target=open_browser, daemon=True).start()
|
|
145
|
+
|
|
146
|
+
print(f"Starting SciTeX Editor at http://127.0.0.1:{self.port}")
|
|
147
|
+
print("Press Ctrl+C to stop")
|
|
148
|
+
|
|
149
|
+
app.run(host='127.0.0.1', port=self.port, debug=False, use_reloader=False)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# EOF
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ./src/scitex/vis/editor/flask_editor/plotter.py
|
|
4
|
+
"""CSV plotting functionality for Flask editor."""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def plot_from_csv(ax, csv_data: Optional[pd.DataFrame], overrides: Dict[str, Any], linewidth: float = 1.0):
|
|
11
|
+
"""Reconstruct plot from CSV data using trace info from overrides.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
ax: Matplotlib axes object
|
|
15
|
+
csv_data: DataFrame containing CSV data
|
|
16
|
+
overrides: Dictionary with override settings including traces
|
|
17
|
+
linewidth: Default line width in points
|
|
18
|
+
"""
|
|
19
|
+
if csv_data is None or not isinstance(csv_data, pd.DataFrame):
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
df = csv_data
|
|
23
|
+
o = overrides
|
|
24
|
+
|
|
25
|
+
# Get legend settings from overrides
|
|
26
|
+
legend_fontsize = o.get('legend_fontsize', 6)
|
|
27
|
+
legend_visible = o.get('legend_visible', True)
|
|
28
|
+
legend_frameon = o.get('legend_frameon', False)
|
|
29
|
+
legend_loc = o.get('legend_loc', 'best')
|
|
30
|
+
legend_x = o.get('legend_x', 0.5)
|
|
31
|
+
legend_y = o.get('legend_y', 0.5)
|
|
32
|
+
|
|
33
|
+
# Get traces from overrides (which may have been edited by user)
|
|
34
|
+
traces = o.get('traces', [])
|
|
35
|
+
|
|
36
|
+
if traces:
|
|
37
|
+
_plot_with_traces(ax, df, traces, linewidth, legend_fontsize, legend_visible,
|
|
38
|
+
legend_frameon, legend_loc, legend_x, legend_y)
|
|
39
|
+
else:
|
|
40
|
+
_plot_fallback(ax, df, linewidth, legend_fontsize, legend_visible,
|
|
41
|
+
legend_frameon, legend_loc, legend_x, legend_y)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _plot_with_traces(ax, df, traces, linewidth, legend_fontsize, legend_visible,
|
|
45
|
+
legend_frameon, legend_loc, legend_x, legend_y):
|
|
46
|
+
"""Plot using trace information from overrides."""
|
|
47
|
+
for trace in traces:
|
|
48
|
+
csv_cols = trace.get('csv_columns', {})
|
|
49
|
+
x_col = csv_cols.get('x')
|
|
50
|
+
y_col = csv_cols.get('y')
|
|
51
|
+
|
|
52
|
+
if x_col in df.columns and y_col in df.columns:
|
|
53
|
+
ax.plot(
|
|
54
|
+
df[x_col],
|
|
55
|
+
df[y_col],
|
|
56
|
+
label=trace.get('label', trace.get('id', '')),
|
|
57
|
+
color=trace.get('color'),
|
|
58
|
+
linestyle=trace.get('linestyle', '-'),
|
|
59
|
+
linewidth=trace.get('linewidth', linewidth),
|
|
60
|
+
marker=trace.get('marker', None),
|
|
61
|
+
markersize=trace.get('markersize', 6),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Add legend if there are labeled traces
|
|
65
|
+
if legend_visible and any(t.get('label') for t in traces):
|
|
66
|
+
_add_legend(ax, legend_fontsize, legend_frameon, legend_loc, legend_x, legend_y)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _plot_fallback(ax, df, linewidth, legend_fontsize, legend_visible,
|
|
70
|
+
legend_frameon, legend_loc, legend_x, legend_y):
|
|
71
|
+
"""Fallback plotting when no trace info available - smart parsing of column names."""
|
|
72
|
+
cols = df.columns.tolist()
|
|
73
|
+
|
|
74
|
+
# Group columns by trace ID
|
|
75
|
+
trace_groups = {}
|
|
76
|
+
for col in cols:
|
|
77
|
+
if col.endswith('_x'):
|
|
78
|
+
trace_id = col[:-2] # Remove '_x'
|
|
79
|
+
y_col = trace_id + '_y'
|
|
80
|
+
if y_col in cols:
|
|
81
|
+
# Extract label from column name (e.g., ax_00_sine_plot -> sine)
|
|
82
|
+
parts = trace_id.split('_')
|
|
83
|
+
label = parts[2] if len(parts) > 2 else trace_id
|
|
84
|
+
trace_groups[trace_id] = {
|
|
85
|
+
'x_col': col,
|
|
86
|
+
'y_col': y_col,
|
|
87
|
+
'label': label,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if trace_groups:
|
|
91
|
+
for trace_id, info in trace_groups.items():
|
|
92
|
+
ax.plot(
|
|
93
|
+
df[info['x_col']],
|
|
94
|
+
df[info['y_col']],
|
|
95
|
+
label=info['label'],
|
|
96
|
+
linewidth=linewidth,
|
|
97
|
+
)
|
|
98
|
+
if legend_visible:
|
|
99
|
+
_add_legend(ax, legend_fontsize, legend_frameon, legend_loc, legend_x, legend_y)
|
|
100
|
+
|
|
101
|
+
elif len(cols) >= 2:
|
|
102
|
+
# Last resort: assume first column is x, rest are y
|
|
103
|
+
x_col = cols[0]
|
|
104
|
+
for y_col in cols[1:]:
|
|
105
|
+
try:
|
|
106
|
+
ax.plot(df[x_col], df[y_col], label=str(y_col), linewidth=linewidth)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
if len(cols) > 2 and legend_visible:
|
|
110
|
+
_add_legend(ax, legend_fontsize, legend_frameon, legend_loc, legend_x, legend_y)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _add_legend(ax, fontsize, frameon, loc, x, y):
|
|
114
|
+
"""Add legend to axes with specified settings."""
|
|
115
|
+
if loc == 'custom':
|
|
116
|
+
ax.legend(
|
|
117
|
+
fontsize=fontsize,
|
|
118
|
+
frameon=frameon,
|
|
119
|
+
loc='upper left',
|
|
120
|
+
bbox_to_anchor=(x, y),
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
ax.legend(
|
|
124
|
+
fontsize=fontsize,
|
|
125
|
+
frameon=frameon,
|
|
126
|
+
loc=loc,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# EOF
|