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.
- scitex/__version__.py +1 -1
- 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/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.3.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/RECORD +42 -21
- scitex/vis/DJANGO_INTEGRATION.md +0 -677
- scitex/vis/editor/_web_editor.py +0 -1440
- scitex/vis/tmp.txt +0 -239
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: 2025-12-08
|
|
4
|
+
# File: ./src/scitex/vis/editor/_qt_editor.py
|
|
5
|
+
"""Qt-based figure editor with rich desktop UI."""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
import copy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_qt():
|
|
13
|
+
"""Get Qt bindings (PyQt6, PyQt5, PySide6, or PySide2)."""
|
|
14
|
+
try:
|
|
15
|
+
from PyQt6 import QtWidgets, QtCore, QtGui
|
|
16
|
+
return QtWidgets, QtCore, QtGui, "PyQt6"
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from PyQt5 import QtWidgets, QtCore, QtGui
|
|
22
|
+
return QtWidgets, QtCore, QtGui, "PyQt5"
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from PySide6 import QtWidgets, QtCore, QtGui
|
|
28
|
+
return QtWidgets, QtCore, QtGui, "PySide6"
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from PySide2 import QtWidgets, QtCore, QtGui
|
|
34
|
+
return QtWidgets, QtCore, QtGui, "PySide2"
|
|
35
|
+
except ImportError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"Qt backend requires PyQt6, PyQt5, PySide6, or PySide2. "
|
|
40
|
+
"Install with: pip install PyQt6"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class QtEditor:
|
|
45
|
+
"""
|
|
46
|
+
Rich desktop figure editor using Qt (PyQt6/5 or PySide6/2).
|
|
47
|
+
|
|
48
|
+
Features:
|
|
49
|
+
- Native desktop UI with dockable panels
|
|
50
|
+
- Embedded matplotlib canvas with navigation
|
|
51
|
+
- Property editors with spinboxes, sliders, color pickers
|
|
52
|
+
- Real-time preview updates
|
|
53
|
+
- Save to .manual.json
|
|
54
|
+
- SciTeX style defaults pre-filled
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
json_path: Path,
|
|
60
|
+
metadata: Dict[str, Any],
|
|
61
|
+
csv_data: Optional[Any] = None,
|
|
62
|
+
png_path: Optional[Path] = None,
|
|
63
|
+
manual_overrides: Optional[Dict[str, Any]] = None,
|
|
64
|
+
):
|
|
65
|
+
self.json_path = Path(json_path)
|
|
66
|
+
self.metadata = metadata
|
|
67
|
+
self.csv_data = csv_data
|
|
68
|
+
self.png_path = Path(png_path) if png_path else None
|
|
69
|
+
self.manual_overrides = manual_overrides or {}
|
|
70
|
+
|
|
71
|
+
# Get SciTeX defaults and merge with metadata
|
|
72
|
+
from ._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
73
|
+
self.scitex_defaults = get_scitex_defaults()
|
|
74
|
+
self.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
75
|
+
|
|
76
|
+
# Start with defaults, then overlay manual overrides
|
|
77
|
+
self.current_overrides = copy.deepcopy(self.scitex_defaults)
|
|
78
|
+
self.current_overrides.update(self.metadata_defaults)
|
|
79
|
+
self.current_overrides.update(self.manual_overrides)
|
|
80
|
+
|
|
81
|
+
# Track modifications
|
|
82
|
+
self._initial_overrides = copy.deepcopy(self.current_overrides)
|
|
83
|
+
self._user_modified = False
|
|
84
|
+
|
|
85
|
+
# Qt components (initialized in run())
|
|
86
|
+
self.app = None
|
|
87
|
+
self.main_window = None
|
|
88
|
+
self.canvas = None
|
|
89
|
+
self.fig = None
|
|
90
|
+
self.ax = None
|
|
91
|
+
|
|
92
|
+
def run(self):
|
|
93
|
+
"""Launch the Qt editor."""
|
|
94
|
+
QtWidgets, QtCore, QtGui, qt_version = _get_qt()
|
|
95
|
+
|
|
96
|
+
# Create application
|
|
97
|
+
self.app = QtWidgets.QApplication.instance()
|
|
98
|
+
if self.app is None:
|
|
99
|
+
self.app = QtWidgets.QApplication([])
|
|
100
|
+
|
|
101
|
+
# Create main window
|
|
102
|
+
self.main_window = QtEditorWindow(
|
|
103
|
+
self, QtWidgets, QtCore, QtGui, qt_version
|
|
104
|
+
)
|
|
105
|
+
self.main_window.show()
|
|
106
|
+
|
|
107
|
+
# Start event loop
|
|
108
|
+
self.app.exec() if hasattr(self.app, 'exec') else self.app.exec_()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class QtEditorWindow:
|
|
112
|
+
"""Qt main window for the editor."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, editor, QtWidgets, QtCore, QtGui, qt_version):
|
|
115
|
+
self.editor = editor
|
|
116
|
+
self.QtWidgets = QtWidgets
|
|
117
|
+
self.QtCore = QtCore
|
|
118
|
+
self.QtGui = QtGui
|
|
119
|
+
self.qt_version = qt_version
|
|
120
|
+
|
|
121
|
+
# Create main window
|
|
122
|
+
self.window = QtWidgets.QMainWindow()
|
|
123
|
+
self.window.setWindowTitle(f"SciTeX Editor - {editor.json_path.name}")
|
|
124
|
+
self.window.resize(1400, 900)
|
|
125
|
+
|
|
126
|
+
# Central widget with splitter
|
|
127
|
+
central_widget = QtWidgets.QWidget()
|
|
128
|
+
self.window.setCentralWidget(central_widget)
|
|
129
|
+
|
|
130
|
+
layout = QtWidgets.QHBoxLayout(central_widget)
|
|
131
|
+
|
|
132
|
+
# Splitter for resizable panels
|
|
133
|
+
splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal
|
|
134
|
+
if hasattr(QtCore.Qt, 'Orientation')
|
|
135
|
+
else QtCore.Qt.Horizontal)
|
|
136
|
+
layout.addWidget(splitter)
|
|
137
|
+
|
|
138
|
+
# Left panel: Canvas
|
|
139
|
+
self._create_canvas_panel(splitter)
|
|
140
|
+
|
|
141
|
+
# Right panel: Controls
|
|
142
|
+
self._create_control_panel(splitter)
|
|
143
|
+
|
|
144
|
+
# Set splitter sizes
|
|
145
|
+
splitter.setSizes([900, 400])
|
|
146
|
+
|
|
147
|
+
# Create toolbar
|
|
148
|
+
self._create_toolbar()
|
|
149
|
+
|
|
150
|
+
# Create status bar
|
|
151
|
+
self.status_bar = self.window.statusBar()
|
|
152
|
+
self.status_bar.showMessage("Ready")
|
|
153
|
+
|
|
154
|
+
# Initial render
|
|
155
|
+
self._render_figure()
|
|
156
|
+
|
|
157
|
+
def show(self):
|
|
158
|
+
"""Show the window."""
|
|
159
|
+
self.window.show()
|
|
160
|
+
|
|
161
|
+
def _create_canvas_panel(self, parent):
|
|
162
|
+
"""Create the matplotlib canvas panel."""
|
|
163
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
|
164
|
+
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT
|
|
165
|
+
from matplotlib.figure import Figure
|
|
166
|
+
|
|
167
|
+
canvas_widget = self.QtWidgets.QWidget()
|
|
168
|
+
canvas_layout = self.QtWidgets.QVBoxLayout(canvas_widget)
|
|
169
|
+
|
|
170
|
+
# Create matplotlib figure
|
|
171
|
+
self.fig = Figure(figsize=(8, 6), dpi=100)
|
|
172
|
+
self.ax = self.fig.add_subplot(111)
|
|
173
|
+
|
|
174
|
+
# Create canvas
|
|
175
|
+
self.canvas = FigureCanvasQTAgg(self.fig)
|
|
176
|
+
canvas_layout.addWidget(self.canvas)
|
|
177
|
+
|
|
178
|
+
# Navigation toolbar
|
|
179
|
+
toolbar = NavigationToolbar2QT(self.canvas, canvas_widget)
|
|
180
|
+
canvas_layout.addWidget(toolbar)
|
|
181
|
+
|
|
182
|
+
parent.addWidget(canvas_widget)
|
|
183
|
+
|
|
184
|
+
def _create_control_panel(self, parent):
|
|
185
|
+
"""Create the control panel with property editors."""
|
|
186
|
+
scroll_area = self.QtWidgets.QScrollArea()
|
|
187
|
+
scroll_area.setWidgetResizable(True)
|
|
188
|
+
scroll_area.setMinimumWidth(350)
|
|
189
|
+
|
|
190
|
+
control_widget = self.QtWidgets.QWidget()
|
|
191
|
+
control_layout = self.QtWidgets.QVBoxLayout(control_widget)
|
|
192
|
+
control_layout.setSpacing(10)
|
|
193
|
+
|
|
194
|
+
# Labels Section
|
|
195
|
+
self._create_labels_section(control_layout)
|
|
196
|
+
|
|
197
|
+
# Axis Limits Section
|
|
198
|
+
self._create_limits_section(control_layout)
|
|
199
|
+
|
|
200
|
+
# Line Style Section
|
|
201
|
+
self._create_line_style_section(control_layout)
|
|
202
|
+
|
|
203
|
+
# Font Settings Section
|
|
204
|
+
self._create_font_section(control_layout)
|
|
205
|
+
|
|
206
|
+
# Tick Settings Section
|
|
207
|
+
self._create_tick_section(control_layout)
|
|
208
|
+
|
|
209
|
+
# Style Section
|
|
210
|
+
self._create_style_section(control_layout)
|
|
211
|
+
|
|
212
|
+
# Legend Section
|
|
213
|
+
self._create_legend_section(control_layout)
|
|
214
|
+
|
|
215
|
+
# Dimensions Section
|
|
216
|
+
self._create_dimensions_section(control_layout)
|
|
217
|
+
|
|
218
|
+
# Annotations Section
|
|
219
|
+
self._create_annotations_section(control_layout)
|
|
220
|
+
|
|
221
|
+
# Add stretch at bottom
|
|
222
|
+
control_layout.addStretch()
|
|
223
|
+
|
|
224
|
+
scroll_area.setWidget(control_widget)
|
|
225
|
+
parent.addWidget(scroll_area)
|
|
226
|
+
|
|
227
|
+
def _create_group_box(self, title: str) -> "QtWidgets.QGroupBox":
|
|
228
|
+
"""Create a collapsible group box."""
|
|
229
|
+
group = self.QtWidgets.QGroupBox(title)
|
|
230
|
+
group.setCheckable(True)
|
|
231
|
+
group.setChecked(True)
|
|
232
|
+
return group
|
|
233
|
+
|
|
234
|
+
def _create_labels_section(self, parent_layout):
|
|
235
|
+
"""Create labels section."""
|
|
236
|
+
group = self._create_group_box("Labels")
|
|
237
|
+
layout = self.QtWidgets.QFormLayout(group)
|
|
238
|
+
|
|
239
|
+
self.title_edit = self.QtWidgets.QLineEdit(
|
|
240
|
+
self.editor.current_overrides.get('title', '')
|
|
241
|
+
)
|
|
242
|
+
self.title_edit.editingFinished.connect(self._on_value_change)
|
|
243
|
+
layout.addRow("Title:", self.title_edit)
|
|
244
|
+
|
|
245
|
+
self.xlabel_edit = self.QtWidgets.QLineEdit(
|
|
246
|
+
self.editor.current_overrides.get('xlabel', '')
|
|
247
|
+
)
|
|
248
|
+
self.xlabel_edit.editingFinished.connect(self._on_value_change)
|
|
249
|
+
layout.addRow("X Label:", self.xlabel_edit)
|
|
250
|
+
|
|
251
|
+
self.ylabel_edit = self.QtWidgets.QLineEdit(
|
|
252
|
+
self.editor.current_overrides.get('ylabel', '')
|
|
253
|
+
)
|
|
254
|
+
self.ylabel_edit.editingFinished.connect(self._on_value_change)
|
|
255
|
+
layout.addRow("Y Label:", self.ylabel_edit)
|
|
256
|
+
|
|
257
|
+
parent_layout.addWidget(group)
|
|
258
|
+
|
|
259
|
+
def _create_limits_section(self, parent_layout):
|
|
260
|
+
"""Create axis limits section."""
|
|
261
|
+
group = self._create_group_box("Axis Limits")
|
|
262
|
+
layout = self.QtWidgets.QGridLayout(group)
|
|
263
|
+
|
|
264
|
+
xlim = self.editor.current_overrides.get('xlim', [0, 1])
|
|
265
|
+
ylim = self.editor.current_overrides.get('ylim', [0, 1])
|
|
266
|
+
|
|
267
|
+
layout.addWidget(self.QtWidgets.QLabel("X Min:"), 0, 0)
|
|
268
|
+
self.xmin_spin = self.QtWidgets.QDoubleSpinBox()
|
|
269
|
+
self.xmin_spin.setRange(-1e9, 1e9)
|
|
270
|
+
self.xmin_spin.setDecimals(4)
|
|
271
|
+
self.xmin_spin.setValue(xlim[0] if xlim else 0)
|
|
272
|
+
layout.addWidget(self.xmin_spin, 0, 1)
|
|
273
|
+
|
|
274
|
+
layout.addWidget(self.QtWidgets.QLabel("X Max:"), 0, 2)
|
|
275
|
+
self.xmax_spin = self.QtWidgets.QDoubleSpinBox()
|
|
276
|
+
self.xmax_spin.setRange(-1e9, 1e9)
|
|
277
|
+
self.xmax_spin.setDecimals(4)
|
|
278
|
+
self.xmax_spin.setValue(xlim[1] if xlim else 1)
|
|
279
|
+
layout.addWidget(self.xmax_spin, 0, 3)
|
|
280
|
+
|
|
281
|
+
layout.addWidget(self.QtWidgets.QLabel("Y Min:"), 1, 0)
|
|
282
|
+
self.ymin_spin = self.QtWidgets.QDoubleSpinBox()
|
|
283
|
+
self.ymin_spin.setRange(-1e9, 1e9)
|
|
284
|
+
self.ymin_spin.setDecimals(4)
|
|
285
|
+
self.ymin_spin.setValue(ylim[0] if ylim else 0)
|
|
286
|
+
layout.addWidget(self.ymin_spin, 1, 1)
|
|
287
|
+
|
|
288
|
+
layout.addWidget(self.QtWidgets.QLabel("Y Max:"), 1, 2)
|
|
289
|
+
self.ymax_spin = self.QtWidgets.QDoubleSpinBox()
|
|
290
|
+
self.ymax_spin.setRange(-1e9, 1e9)
|
|
291
|
+
self.ymax_spin.setDecimals(4)
|
|
292
|
+
self.ymax_spin.setValue(ylim[1] if ylim else 1)
|
|
293
|
+
layout.addWidget(self.ymax_spin, 1, 3)
|
|
294
|
+
|
|
295
|
+
apply_btn = self.QtWidgets.QPushButton("Apply Limits")
|
|
296
|
+
apply_btn.clicked.connect(self._apply_limits)
|
|
297
|
+
layout.addWidget(apply_btn, 2, 0, 1, 4)
|
|
298
|
+
|
|
299
|
+
parent_layout.addWidget(group)
|
|
300
|
+
|
|
301
|
+
def _create_line_style_section(self, parent_layout):
|
|
302
|
+
"""Create line style section."""
|
|
303
|
+
group = self._create_group_box("Line Style")
|
|
304
|
+
layout = self.QtWidgets.QFormLayout(group)
|
|
305
|
+
|
|
306
|
+
self.linewidth_spin = self.QtWidgets.QDoubleSpinBox()
|
|
307
|
+
self.linewidth_spin.setRange(0.1, 10.0)
|
|
308
|
+
self.linewidth_spin.setSingleStep(0.1)
|
|
309
|
+
self.linewidth_spin.setValue(self.editor.current_overrides.get('linewidth', 1.0))
|
|
310
|
+
self.linewidth_spin.valueChanged.connect(self._on_value_change)
|
|
311
|
+
layout.addRow("Line Width (pt):", self.linewidth_spin)
|
|
312
|
+
|
|
313
|
+
parent_layout.addWidget(group)
|
|
314
|
+
|
|
315
|
+
def _create_font_section(self, parent_layout):
|
|
316
|
+
"""Create font settings section."""
|
|
317
|
+
group = self._create_group_box("Font Settings")
|
|
318
|
+
layout = self.QtWidgets.QFormLayout(group)
|
|
319
|
+
|
|
320
|
+
self.title_fontsize_spin = self.QtWidgets.QSpinBox()
|
|
321
|
+
self.title_fontsize_spin.setRange(6, 24)
|
|
322
|
+
self.title_fontsize_spin.setValue(self.editor.current_overrides.get('title_fontsize', 8))
|
|
323
|
+
self.title_fontsize_spin.valueChanged.connect(self._on_value_change)
|
|
324
|
+
layout.addRow("Title Font Size:", self.title_fontsize_spin)
|
|
325
|
+
|
|
326
|
+
self.axis_fontsize_spin = self.QtWidgets.QSpinBox()
|
|
327
|
+
self.axis_fontsize_spin.setRange(6, 24)
|
|
328
|
+
self.axis_fontsize_spin.setValue(self.editor.current_overrides.get('axis_fontsize', 7))
|
|
329
|
+
self.axis_fontsize_spin.valueChanged.connect(self._on_value_change)
|
|
330
|
+
layout.addRow("Axis Font Size:", self.axis_fontsize_spin)
|
|
331
|
+
|
|
332
|
+
self.tick_fontsize_spin = self.QtWidgets.QSpinBox()
|
|
333
|
+
self.tick_fontsize_spin.setRange(6, 24)
|
|
334
|
+
self.tick_fontsize_spin.setValue(self.editor.current_overrides.get('tick_fontsize', 7))
|
|
335
|
+
self.tick_fontsize_spin.valueChanged.connect(self._on_value_change)
|
|
336
|
+
layout.addRow("Tick Font Size:", self.tick_fontsize_spin)
|
|
337
|
+
|
|
338
|
+
self.legend_fontsize_spin = self.QtWidgets.QSpinBox()
|
|
339
|
+
self.legend_fontsize_spin.setRange(4, 20)
|
|
340
|
+
self.legend_fontsize_spin.setValue(self.editor.current_overrides.get('legend_fontsize', 6))
|
|
341
|
+
self.legend_fontsize_spin.valueChanged.connect(self._on_value_change)
|
|
342
|
+
layout.addRow("Legend Font Size:", self.legend_fontsize_spin)
|
|
343
|
+
|
|
344
|
+
parent_layout.addWidget(group)
|
|
345
|
+
|
|
346
|
+
def _create_tick_section(self, parent_layout):
|
|
347
|
+
"""Create tick settings section."""
|
|
348
|
+
group = self._create_group_box("Tick Settings")
|
|
349
|
+
layout = self.QtWidgets.QFormLayout(group)
|
|
350
|
+
|
|
351
|
+
self.n_ticks_spin = self.QtWidgets.QSpinBox()
|
|
352
|
+
self.n_ticks_spin.setRange(2, 15)
|
|
353
|
+
self.n_ticks_spin.setValue(self.editor.current_overrides.get('n_ticks', 4))
|
|
354
|
+
self.n_ticks_spin.valueChanged.connect(self._on_value_change)
|
|
355
|
+
layout.addRow("N Ticks:", self.n_ticks_spin)
|
|
356
|
+
|
|
357
|
+
self.tick_length_spin = self.QtWidgets.QDoubleSpinBox()
|
|
358
|
+
self.tick_length_spin.setRange(0.1, 5.0)
|
|
359
|
+
self.tick_length_spin.setSingleStep(0.1)
|
|
360
|
+
self.tick_length_spin.setValue(self.editor.current_overrides.get('tick_length', 0.8))
|
|
361
|
+
self.tick_length_spin.valueChanged.connect(self._on_value_change)
|
|
362
|
+
layout.addRow("Tick Length (mm):", self.tick_length_spin)
|
|
363
|
+
|
|
364
|
+
self.tick_width_spin = self.QtWidgets.QDoubleSpinBox()
|
|
365
|
+
self.tick_width_spin.setRange(0.05, 2.0)
|
|
366
|
+
self.tick_width_spin.setSingleStep(0.05)
|
|
367
|
+
self.tick_width_spin.setValue(self.editor.current_overrides.get('tick_width', 0.2))
|
|
368
|
+
self.tick_width_spin.valueChanged.connect(self._on_value_change)
|
|
369
|
+
layout.addRow("Tick Width (mm):", self.tick_width_spin)
|
|
370
|
+
|
|
371
|
+
self.tick_direction_combo = self.QtWidgets.QComboBox()
|
|
372
|
+
self.tick_direction_combo.addItems(["out", "in", "inout"])
|
|
373
|
+
self.tick_direction_combo.setCurrentText(
|
|
374
|
+
self.editor.current_overrides.get('tick_direction', 'out')
|
|
375
|
+
)
|
|
376
|
+
self.tick_direction_combo.currentTextChanged.connect(self._on_value_change)
|
|
377
|
+
layout.addRow("Tick Direction:", self.tick_direction_combo)
|
|
378
|
+
|
|
379
|
+
parent_layout.addWidget(group)
|
|
380
|
+
|
|
381
|
+
def _create_style_section(self, parent_layout):
|
|
382
|
+
"""Create style section."""
|
|
383
|
+
group = self._create_group_box("Style")
|
|
384
|
+
layout = self.QtWidgets.QFormLayout(group)
|
|
385
|
+
|
|
386
|
+
self.grid_check = self.QtWidgets.QCheckBox()
|
|
387
|
+
self.grid_check.setChecked(self.editor.current_overrides.get('grid', False))
|
|
388
|
+
self.grid_check.stateChanged.connect(self._on_value_change)
|
|
389
|
+
layout.addRow("Show Grid:", self.grid_check)
|
|
390
|
+
|
|
391
|
+
self.hide_top_spine_check = self.QtWidgets.QCheckBox()
|
|
392
|
+
self.hide_top_spine_check.setChecked(
|
|
393
|
+
self.editor.current_overrides.get('hide_top_spine', True)
|
|
394
|
+
)
|
|
395
|
+
self.hide_top_spine_check.stateChanged.connect(self._on_value_change)
|
|
396
|
+
layout.addRow("Hide Top Spine:", self.hide_top_spine_check)
|
|
397
|
+
|
|
398
|
+
self.hide_right_spine_check = self.QtWidgets.QCheckBox()
|
|
399
|
+
self.hide_right_spine_check.setChecked(
|
|
400
|
+
self.editor.current_overrides.get('hide_right_spine', True)
|
|
401
|
+
)
|
|
402
|
+
self.hide_right_spine_check.stateChanged.connect(self._on_value_change)
|
|
403
|
+
layout.addRow("Hide Right Spine:", self.hide_right_spine_check)
|
|
404
|
+
|
|
405
|
+
self.transparent_check = self.QtWidgets.QCheckBox()
|
|
406
|
+
self.transparent_check.setChecked(
|
|
407
|
+
self.editor.current_overrides.get('transparent', True)
|
|
408
|
+
)
|
|
409
|
+
self.transparent_check.stateChanged.connect(self._on_value_change)
|
|
410
|
+
layout.addRow("Transparent BG:", self.transparent_check)
|
|
411
|
+
|
|
412
|
+
self.axis_width_spin = self.QtWidgets.QDoubleSpinBox()
|
|
413
|
+
self.axis_width_spin.setRange(0.05, 2.0)
|
|
414
|
+
self.axis_width_spin.setSingleStep(0.05)
|
|
415
|
+
self.axis_width_spin.setValue(self.editor.current_overrides.get('axis_width', 0.2))
|
|
416
|
+
self.axis_width_spin.valueChanged.connect(self._on_value_change)
|
|
417
|
+
layout.addRow("Axis Width (mm):", self.axis_width_spin)
|
|
418
|
+
|
|
419
|
+
# Background color button
|
|
420
|
+
self.bg_color = self.editor.current_overrides.get('facecolor', '#ffffff')
|
|
421
|
+
self.bg_color_btn = self.QtWidgets.QPushButton("Choose...")
|
|
422
|
+
self.bg_color_btn.clicked.connect(self._choose_bg_color)
|
|
423
|
+
layout.addRow("Background Color:", self.bg_color_btn)
|
|
424
|
+
|
|
425
|
+
parent_layout.addWidget(group)
|
|
426
|
+
|
|
427
|
+
def _create_legend_section(self, parent_layout):
|
|
428
|
+
"""Create legend section."""
|
|
429
|
+
group = self._create_group_box("Legend")
|
|
430
|
+
layout = self.QtWidgets.QFormLayout(group)
|
|
431
|
+
|
|
432
|
+
self.legend_visible_check = self.QtWidgets.QCheckBox()
|
|
433
|
+
self.legend_visible_check.setChecked(
|
|
434
|
+
self.editor.current_overrides.get('legend_visible', True)
|
|
435
|
+
)
|
|
436
|
+
self.legend_visible_check.stateChanged.connect(self._on_value_change)
|
|
437
|
+
layout.addRow("Show Legend:", self.legend_visible_check)
|
|
438
|
+
|
|
439
|
+
self.legend_frameon_check = self.QtWidgets.QCheckBox()
|
|
440
|
+
self.legend_frameon_check.setChecked(
|
|
441
|
+
self.editor.current_overrides.get('legend_frameon', False)
|
|
442
|
+
)
|
|
443
|
+
self.legend_frameon_check.stateChanged.connect(self._on_value_change)
|
|
444
|
+
layout.addRow("Show Frame:", self.legend_frameon_check)
|
|
445
|
+
|
|
446
|
+
self.legend_loc_combo = self.QtWidgets.QComboBox()
|
|
447
|
+
self.legend_loc_combo.addItems([
|
|
448
|
+
"best", "upper right", "upper left", "lower right",
|
|
449
|
+
"lower left", "center right", "center left",
|
|
450
|
+
"upper center", "lower center", "center"
|
|
451
|
+
])
|
|
452
|
+
self.legend_loc_combo.setCurrentText(
|
|
453
|
+
self.editor.current_overrides.get('legend_loc', 'best')
|
|
454
|
+
)
|
|
455
|
+
self.legend_loc_combo.currentTextChanged.connect(self._on_value_change)
|
|
456
|
+
layout.addRow("Position:", self.legend_loc_combo)
|
|
457
|
+
|
|
458
|
+
parent_layout.addWidget(group)
|
|
459
|
+
|
|
460
|
+
def _create_dimensions_section(self, parent_layout):
|
|
461
|
+
"""Create dimensions section."""
|
|
462
|
+
group = self._create_group_box("Dimensions")
|
|
463
|
+
layout = self.QtWidgets.QFormLayout(group)
|
|
464
|
+
|
|
465
|
+
fig_size = self.editor.current_overrides.get('fig_size', [3.15, 2.68])
|
|
466
|
+
|
|
467
|
+
self.fig_width_spin = self.QtWidgets.QDoubleSpinBox()
|
|
468
|
+
self.fig_width_spin.setRange(1.0, 20.0)
|
|
469
|
+
self.fig_width_spin.setSingleStep(0.1)
|
|
470
|
+
self.fig_width_spin.setValue(fig_size[0])
|
|
471
|
+
layout.addRow("Width (in):", self.fig_width_spin)
|
|
472
|
+
|
|
473
|
+
self.fig_height_spin = self.QtWidgets.QDoubleSpinBox()
|
|
474
|
+
self.fig_height_spin.setRange(1.0, 20.0)
|
|
475
|
+
self.fig_height_spin.setSingleStep(0.1)
|
|
476
|
+
self.fig_height_spin.setValue(fig_size[1])
|
|
477
|
+
layout.addRow("Height (in):", self.fig_height_spin)
|
|
478
|
+
|
|
479
|
+
self.dpi_spin = self.QtWidgets.QSpinBox()
|
|
480
|
+
self.dpi_spin.setRange(72, 600)
|
|
481
|
+
self.dpi_spin.setValue(self.editor.current_overrides.get('dpi', 300))
|
|
482
|
+
self.dpi_spin.valueChanged.connect(self._on_value_change)
|
|
483
|
+
layout.addRow("DPI:", self.dpi_spin)
|
|
484
|
+
|
|
485
|
+
parent_layout.addWidget(group)
|
|
486
|
+
|
|
487
|
+
def _create_annotations_section(self, parent_layout):
|
|
488
|
+
"""Create annotations section."""
|
|
489
|
+
group = self._create_group_box("Annotations")
|
|
490
|
+
layout = self.QtWidgets.QVBoxLayout(group)
|
|
491
|
+
|
|
492
|
+
# Input fields
|
|
493
|
+
form_layout = self.QtWidgets.QFormLayout()
|
|
494
|
+
|
|
495
|
+
self.annot_text_edit = self.QtWidgets.QLineEdit()
|
|
496
|
+
form_layout.addRow("Text:", self.annot_text_edit)
|
|
497
|
+
|
|
498
|
+
pos_layout = self.QtWidgets.QHBoxLayout()
|
|
499
|
+
self.annot_x_spin = self.QtWidgets.QDoubleSpinBox()
|
|
500
|
+
self.annot_x_spin.setRange(0, 1)
|
|
501
|
+
self.annot_x_spin.setSingleStep(0.05)
|
|
502
|
+
self.annot_x_spin.setValue(0.5)
|
|
503
|
+
pos_layout.addWidget(self.QtWidgets.QLabel("X:"))
|
|
504
|
+
pos_layout.addWidget(self.annot_x_spin)
|
|
505
|
+
|
|
506
|
+
self.annot_y_spin = self.QtWidgets.QDoubleSpinBox()
|
|
507
|
+
self.annot_y_spin.setRange(0, 1)
|
|
508
|
+
self.annot_y_spin.setSingleStep(0.05)
|
|
509
|
+
self.annot_y_spin.setValue(0.5)
|
|
510
|
+
pos_layout.addWidget(self.QtWidgets.QLabel("Y:"))
|
|
511
|
+
pos_layout.addWidget(self.annot_y_spin)
|
|
512
|
+
|
|
513
|
+
form_layout.addRow("Position:", pos_layout)
|
|
514
|
+
layout.addLayout(form_layout)
|
|
515
|
+
|
|
516
|
+
add_btn = self.QtWidgets.QPushButton("Add Annotation")
|
|
517
|
+
add_btn.clicked.connect(self._add_annotation)
|
|
518
|
+
layout.addWidget(add_btn)
|
|
519
|
+
|
|
520
|
+
# Annotation list
|
|
521
|
+
self.annot_list = self.QtWidgets.QListWidget()
|
|
522
|
+
self.annot_list.setMaximumHeight(100)
|
|
523
|
+
layout.addWidget(self.annot_list)
|
|
524
|
+
|
|
525
|
+
remove_btn = self.QtWidgets.QPushButton("Remove Selected")
|
|
526
|
+
remove_btn.clicked.connect(self._remove_annotation)
|
|
527
|
+
layout.addWidget(remove_btn)
|
|
528
|
+
|
|
529
|
+
self._update_annotations_list()
|
|
530
|
+
|
|
531
|
+
parent_layout.addWidget(group)
|
|
532
|
+
|
|
533
|
+
def _create_toolbar(self):
|
|
534
|
+
"""Create the main toolbar."""
|
|
535
|
+
toolbar = self.window.addToolBar("Main")
|
|
536
|
+
|
|
537
|
+
# Update action
|
|
538
|
+
update_action = toolbar.addAction("Update Preview")
|
|
539
|
+
update_action.triggered.connect(self._render_figure)
|
|
540
|
+
|
|
541
|
+
# Save action
|
|
542
|
+
save_action = toolbar.addAction("Save")
|
|
543
|
+
save_action.triggered.connect(self._save_manual)
|
|
544
|
+
|
|
545
|
+
# Reset action
|
|
546
|
+
reset_action = toolbar.addAction("Reset")
|
|
547
|
+
reset_action.triggered.connect(self._reset_overrides)
|
|
548
|
+
|
|
549
|
+
toolbar.addSeparator()
|
|
550
|
+
|
|
551
|
+
# Export action
|
|
552
|
+
export_action = toolbar.addAction("Export PNG")
|
|
553
|
+
export_action.triggered.connect(self._export_png)
|
|
554
|
+
|
|
555
|
+
def _on_value_change(self, value=None):
|
|
556
|
+
"""Handle value changes from widgets."""
|
|
557
|
+
self.editor._user_modified = True
|
|
558
|
+
self._collect_overrides()
|
|
559
|
+
self._render_figure()
|
|
560
|
+
|
|
561
|
+
def _collect_overrides(self):
|
|
562
|
+
"""Collect current values from all widgets."""
|
|
563
|
+
o = self.editor.current_overrides
|
|
564
|
+
|
|
565
|
+
# Labels
|
|
566
|
+
o['title'] = self.title_edit.text()
|
|
567
|
+
o['xlabel'] = self.xlabel_edit.text()
|
|
568
|
+
o['ylabel'] = self.ylabel_edit.text()
|
|
569
|
+
|
|
570
|
+
# Line style
|
|
571
|
+
o['linewidth'] = self.linewidth_spin.value()
|
|
572
|
+
|
|
573
|
+
# Font settings
|
|
574
|
+
o['title_fontsize'] = self.title_fontsize_spin.value()
|
|
575
|
+
o['axis_fontsize'] = self.axis_fontsize_spin.value()
|
|
576
|
+
o['tick_fontsize'] = self.tick_fontsize_spin.value()
|
|
577
|
+
o['legend_fontsize'] = self.legend_fontsize_spin.value()
|
|
578
|
+
|
|
579
|
+
# Tick settings
|
|
580
|
+
o['n_ticks'] = self.n_ticks_spin.value()
|
|
581
|
+
o['tick_length'] = self.tick_length_spin.value()
|
|
582
|
+
o['tick_width'] = self.tick_width_spin.value()
|
|
583
|
+
o['tick_direction'] = self.tick_direction_combo.currentText()
|
|
584
|
+
|
|
585
|
+
# Style
|
|
586
|
+
o['grid'] = self.grid_check.isChecked()
|
|
587
|
+
o['hide_top_spine'] = self.hide_top_spine_check.isChecked()
|
|
588
|
+
o['hide_right_spine'] = self.hide_right_spine_check.isChecked()
|
|
589
|
+
o['transparent'] = self.transparent_check.isChecked()
|
|
590
|
+
o['axis_width'] = self.axis_width_spin.value()
|
|
591
|
+
o['facecolor'] = self.bg_color
|
|
592
|
+
|
|
593
|
+
# Legend
|
|
594
|
+
o['legend_visible'] = self.legend_visible_check.isChecked()
|
|
595
|
+
o['legend_frameon'] = self.legend_frameon_check.isChecked()
|
|
596
|
+
o['legend_loc'] = self.legend_loc_combo.currentText()
|
|
597
|
+
|
|
598
|
+
# Dimensions
|
|
599
|
+
o['fig_size'] = [self.fig_width_spin.value(), self.fig_height_spin.value()]
|
|
600
|
+
o['dpi'] = self.dpi_spin.value()
|
|
601
|
+
|
|
602
|
+
def _apply_limits(self):
|
|
603
|
+
"""Apply axis limits."""
|
|
604
|
+
xmin = self.xmin_spin.value()
|
|
605
|
+
xmax = self.xmax_spin.value()
|
|
606
|
+
ymin = self.ymin_spin.value()
|
|
607
|
+
ymax = self.ymax_spin.value()
|
|
608
|
+
|
|
609
|
+
if xmin < xmax:
|
|
610
|
+
self.editor.current_overrides['xlim'] = [xmin, xmax]
|
|
611
|
+
if ymin < ymax:
|
|
612
|
+
self.editor.current_overrides['ylim'] = [ymin, ymax]
|
|
613
|
+
|
|
614
|
+
self.editor._user_modified = True
|
|
615
|
+
self._render_figure()
|
|
616
|
+
|
|
617
|
+
def _choose_bg_color(self):
|
|
618
|
+
"""Open color dialog for background."""
|
|
619
|
+
color = self.QtWidgets.QColorDialog.getColor()
|
|
620
|
+
if color.isValid():
|
|
621
|
+
self.bg_color = color.name()
|
|
622
|
+
self.editor.current_overrides['facecolor'] = self.bg_color
|
|
623
|
+
self._render_figure()
|
|
624
|
+
|
|
625
|
+
def _add_annotation(self):
|
|
626
|
+
"""Add text annotation."""
|
|
627
|
+
text = self.annot_text_edit.text()
|
|
628
|
+
if not text:
|
|
629
|
+
return
|
|
630
|
+
|
|
631
|
+
x = self.annot_x_spin.value()
|
|
632
|
+
y = self.annot_y_spin.value()
|
|
633
|
+
|
|
634
|
+
if 'annotations' not in self.editor.current_overrides:
|
|
635
|
+
self.editor.current_overrides['annotations'] = []
|
|
636
|
+
|
|
637
|
+
self.editor.current_overrides['annotations'].append({
|
|
638
|
+
'type': 'text',
|
|
639
|
+
'text': text,
|
|
640
|
+
'x': x,
|
|
641
|
+
'y': y,
|
|
642
|
+
'fontsize': self.editor.current_overrides.get('axis_fontsize', 7),
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
self.annot_text_edit.clear()
|
|
646
|
+
self._update_annotations_list()
|
|
647
|
+
self.editor._user_modified = True
|
|
648
|
+
self._render_figure()
|
|
649
|
+
|
|
650
|
+
def _remove_annotation(self):
|
|
651
|
+
"""Remove selected annotation."""
|
|
652
|
+
row = self.annot_list.currentRow()
|
|
653
|
+
annotations = self.editor.current_overrides.get('annotations', [])
|
|
654
|
+
|
|
655
|
+
if row >= 0 and row < len(annotations):
|
|
656
|
+
del annotations[row]
|
|
657
|
+
self._update_annotations_list()
|
|
658
|
+
self.editor._user_modified = True
|
|
659
|
+
self._render_figure()
|
|
660
|
+
|
|
661
|
+
def _update_annotations_list(self):
|
|
662
|
+
"""Update the annotations list widget."""
|
|
663
|
+
self.annot_list.clear()
|
|
664
|
+
for ann in self.editor.current_overrides.get('annotations', []):
|
|
665
|
+
if ann.get('type') == 'text':
|
|
666
|
+
text = ann.get('text', '')[:20]
|
|
667
|
+
x = ann.get('x', 0)
|
|
668
|
+
y = ann.get('y', 0)
|
|
669
|
+
self.annot_list.addItem(f"{text} ({x:.2f}, {y:.2f})")
|
|
670
|
+
|
|
671
|
+
def _render_figure(self):
|
|
672
|
+
"""Render the figure with current overrides."""
|
|
673
|
+
from matplotlib.ticker import MaxNLocator
|
|
674
|
+
|
|
675
|
+
self.ax.clear()
|
|
676
|
+
o = self.editor.current_overrides
|
|
677
|
+
mm_to_pt = 2.83465
|
|
678
|
+
|
|
679
|
+
# Background
|
|
680
|
+
if o.get('transparent', True):
|
|
681
|
+
self.fig.patch.set_facecolor('none')
|
|
682
|
+
self.ax.patch.set_facecolor('none')
|
|
683
|
+
else:
|
|
684
|
+
self.fig.patch.set_facecolor(o.get('facecolor', '#ffffff'))
|
|
685
|
+
self.ax.patch.set_facecolor(o.get('facecolor', '#ffffff'))
|
|
686
|
+
|
|
687
|
+
# Plot from CSV
|
|
688
|
+
if self.editor.csv_data is not None:
|
|
689
|
+
self._plot_from_csv(o)
|
|
690
|
+
else:
|
|
691
|
+
self.ax.text(0.5, 0.5, "No plot data available\n(CSV not found)",
|
|
692
|
+
ha='center', va='center', transform=self.ax.transAxes)
|
|
693
|
+
|
|
694
|
+
# Labels
|
|
695
|
+
if o.get('title'):
|
|
696
|
+
self.ax.set_title(o['title'], fontsize=o.get('title_fontsize', 8))
|
|
697
|
+
if o.get('xlabel'):
|
|
698
|
+
self.ax.set_xlabel(o['xlabel'], fontsize=o.get('axis_fontsize', 7))
|
|
699
|
+
if o.get('ylabel'):
|
|
700
|
+
self.ax.set_ylabel(o['ylabel'], fontsize=o.get('axis_fontsize', 7))
|
|
701
|
+
|
|
702
|
+
# Ticks
|
|
703
|
+
self.ax.tick_params(
|
|
704
|
+
axis='both',
|
|
705
|
+
labelsize=o.get('tick_fontsize', 7),
|
|
706
|
+
length=o.get('tick_length', 0.8) * mm_to_pt,
|
|
707
|
+
width=o.get('tick_width', 0.2) * mm_to_pt,
|
|
708
|
+
direction=o.get('tick_direction', 'out'),
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
self.ax.xaxis.set_major_locator(MaxNLocator(nbins=o.get('n_ticks', 4)))
|
|
712
|
+
self.ax.yaxis.set_major_locator(MaxNLocator(nbins=o.get('n_ticks', 4)))
|
|
713
|
+
|
|
714
|
+
# Grid
|
|
715
|
+
if o.get('grid'):
|
|
716
|
+
self.ax.grid(True, linewidth=o.get('axis_width', 0.2) * mm_to_pt, alpha=0.3)
|
|
717
|
+
|
|
718
|
+
# Limits
|
|
719
|
+
if o.get('xlim'):
|
|
720
|
+
self.ax.set_xlim(o['xlim'])
|
|
721
|
+
if o.get('ylim'):
|
|
722
|
+
self.ax.set_ylim(o['ylim'])
|
|
723
|
+
|
|
724
|
+
# Spines
|
|
725
|
+
if o.get('hide_top_spine', True):
|
|
726
|
+
self.ax.spines['top'].set_visible(False)
|
|
727
|
+
if o.get('hide_right_spine', True):
|
|
728
|
+
self.ax.spines['right'].set_visible(False)
|
|
729
|
+
|
|
730
|
+
for spine in self.ax.spines.values():
|
|
731
|
+
spine.set_linewidth(o.get('axis_width', 0.2) * mm_to_pt)
|
|
732
|
+
|
|
733
|
+
# Annotations
|
|
734
|
+
for ann in o.get('annotations', []):
|
|
735
|
+
if ann.get('type') == 'text':
|
|
736
|
+
self.ax.text(
|
|
737
|
+
ann.get('x', 0.5),
|
|
738
|
+
ann.get('y', 0.5),
|
|
739
|
+
ann.get('text', ''),
|
|
740
|
+
transform=self.ax.transAxes,
|
|
741
|
+
fontsize=ann.get('fontsize', o.get('axis_fontsize', 7)),
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
self.fig.tight_layout()
|
|
745
|
+
self.canvas.draw()
|
|
746
|
+
self.status_bar.showMessage("Preview updated")
|
|
747
|
+
|
|
748
|
+
def _plot_from_csv(self, o):
|
|
749
|
+
"""Reconstruct plot from CSV data."""
|
|
750
|
+
import pandas as pd
|
|
751
|
+
|
|
752
|
+
if not isinstance(self.editor.csv_data, pd.DataFrame):
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
df = self.editor.csv_data
|
|
756
|
+
linewidth = o.get('linewidth', 1.0)
|
|
757
|
+
legend_visible = o.get('legend_visible', True)
|
|
758
|
+
legend_fontsize = o.get('legend_fontsize', 6)
|
|
759
|
+
legend_frameon = o.get('legend_frameon', False)
|
|
760
|
+
legend_loc = o.get('legend_loc', 'best')
|
|
761
|
+
|
|
762
|
+
traces = o.get('traces', [])
|
|
763
|
+
|
|
764
|
+
if traces:
|
|
765
|
+
for trace in traces:
|
|
766
|
+
csv_cols = trace.get('csv_columns', {})
|
|
767
|
+
x_col = csv_cols.get('x')
|
|
768
|
+
y_col = csv_cols.get('y')
|
|
769
|
+
|
|
770
|
+
if x_col in df.columns and y_col in df.columns:
|
|
771
|
+
self.ax.plot(
|
|
772
|
+
df[x_col],
|
|
773
|
+
df[y_col],
|
|
774
|
+
label=trace.get('label', trace.get('id', '')),
|
|
775
|
+
color=trace.get('color'),
|
|
776
|
+
linestyle=trace.get('linestyle', '-'),
|
|
777
|
+
linewidth=trace.get('linewidth', linewidth),
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
if legend_visible and any(t.get('label') for t in traces):
|
|
781
|
+
self.ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
|
|
782
|
+
else:
|
|
783
|
+
cols = df.columns.tolist()
|
|
784
|
+
if len(cols) >= 2:
|
|
785
|
+
x_col = cols[0]
|
|
786
|
+
for y_col in cols[1:]:
|
|
787
|
+
try:
|
|
788
|
+
self.ax.plot(df[x_col], df[y_col], label=str(y_col), linewidth=linewidth)
|
|
789
|
+
except Exception:
|
|
790
|
+
pass
|
|
791
|
+
if len(cols) > 2 and legend_visible:
|
|
792
|
+
self.ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
|
|
793
|
+
|
|
794
|
+
def _save_manual(self):
|
|
795
|
+
"""Save to .manual.json."""
|
|
796
|
+
from ._edit import save_manual_overrides
|
|
797
|
+
|
|
798
|
+
try:
|
|
799
|
+
self._collect_overrides()
|
|
800
|
+
manual_path = save_manual_overrides(
|
|
801
|
+
self.editor.json_path,
|
|
802
|
+
self.editor.current_overrides
|
|
803
|
+
)
|
|
804
|
+
self.status_bar.showMessage(f"Saved: {manual_path.name}")
|
|
805
|
+
self.QtWidgets.QMessageBox.information(
|
|
806
|
+
self.window, "Saved",
|
|
807
|
+
f"Manual overrides saved to:\n{manual_path}"
|
|
808
|
+
)
|
|
809
|
+
except Exception as e:
|
|
810
|
+
self.QtWidgets.QMessageBox.critical(
|
|
811
|
+
self.window, "Error",
|
|
812
|
+
f"Failed to save: {e}"
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
def _reset_overrides(self):
|
|
816
|
+
"""Reset to initial overrides."""
|
|
817
|
+
reply = self.QtWidgets.QMessageBox.question(
|
|
818
|
+
self.window, "Reset",
|
|
819
|
+
"Reset all changes to original values?",
|
|
820
|
+
self.QtWidgets.QMessageBox.StandardButton.Yes |
|
|
821
|
+
self.QtWidgets.QMessageBox.StandardButton.No
|
|
822
|
+
if hasattr(self.QtWidgets.QMessageBox, 'StandardButton')
|
|
823
|
+
else self.QtWidgets.QMessageBox.Yes | self.QtWidgets.QMessageBox.No
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
yes_val = (self.QtWidgets.QMessageBox.StandardButton.Yes
|
|
827
|
+
if hasattr(self.QtWidgets.QMessageBox, 'StandardButton')
|
|
828
|
+
else self.QtWidgets.QMessageBox.Yes)
|
|
829
|
+
|
|
830
|
+
if reply == yes_val:
|
|
831
|
+
self.editor.current_overrides = copy.deepcopy(self.editor._initial_overrides)
|
|
832
|
+
self.editor._user_modified = False
|
|
833
|
+
|
|
834
|
+
# Update UI
|
|
835
|
+
self.title_edit.setText(self.editor.current_overrides.get('title', ''))
|
|
836
|
+
self.xlabel_edit.setText(self.editor.current_overrides.get('xlabel', ''))
|
|
837
|
+
self.ylabel_edit.setText(self.editor.current_overrides.get('ylabel', ''))
|
|
838
|
+
self.linewidth_spin.setValue(self.editor.current_overrides.get('linewidth', 1.0))
|
|
839
|
+
self.grid_check.setChecked(self.editor.current_overrides.get('grid', False))
|
|
840
|
+
|
|
841
|
+
self._update_annotations_list()
|
|
842
|
+
self._render_figure()
|
|
843
|
+
self.status_bar.showMessage("Reset to original")
|
|
844
|
+
|
|
845
|
+
def _export_png(self):
|
|
846
|
+
"""Export current view to PNG."""
|
|
847
|
+
filepath, _ = self.QtWidgets.QFileDialog.getSaveFileName(
|
|
848
|
+
self.window, "Export PNG",
|
|
849
|
+
str(self.editor.json_path.with_suffix('.edited.png')),
|
|
850
|
+
"PNG files (*.png);;All files (*)"
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
if filepath:
|
|
854
|
+
self._collect_overrides()
|
|
855
|
+
o = self.editor.current_overrides
|
|
856
|
+
dpi = o.get('dpi', 300)
|
|
857
|
+
|
|
858
|
+
self.fig.savefig(
|
|
859
|
+
filepath, dpi=dpi, bbox_inches='tight',
|
|
860
|
+
transparent=o.get('transparent', True)
|
|
861
|
+
)
|
|
862
|
+
self.status_bar.showMessage(f"Exported: {Path(filepath).name}")
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
# EOF
|