scitex 2.3.0__py3-none-any.whl → 2.4.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/ai/classification/reporters/reporter_utils/_Plotter.py +1 -1
- scitex/ai/plt/__init__.py +2 -2
- scitex/ai/plt/{_plot_conf_mat.py → _stx_conf_mat.py} +3 -3
- scitex/config/PriorityConfig.py +195 -0
- scitex/config/__init__.py +24 -0
- scitex/io/_save.py +125 -34
- scitex/io/_save_modules/_image.py +37 -20
- scitex/plt/__init__.py +470 -17
- scitex/plt/_subplots/_AxisWrapper.py +98 -50
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +254 -124
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +49 -8
- scitex/plt/_subplots/_SubplotsWrapper.py +76 -91
- scitex/plt/_subplots/_export_as_csv.py +127 -58
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +25 -16
- scitex/plt/_subplots/_export_as_csv_formatters/_format_contourf.py +54 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_hexbin.py +41 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_hist2d.py +41 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow.py +59 -47
- scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_pie.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +72 -35
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_box.py +1 -1
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +53 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stem.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_step.py +42 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +48 -0
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_conf_mat.py → _format_stx_conf_mat.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_ecdf.py → _format_stx_ecdf.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_fillv.py → _format_stx_fillv.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_heatmap.py → _format_stx_heatmap.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_image.py → _format_stx_image.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_joyplot.py → _format_stx_joyplot.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_line.py → _format_stx_line.py} +3 -3
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_ci.py → _format_stx_mean_ci.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_std.py → _format_stx_mean_std.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_median_iqr.py → _format_stx_median_iqr.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_raster.py → _format_stx_raster.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_rectangle.py → _format_stx_rectangle.py} +1 -1
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_scatter_hist.py → _format_stx_scatter_hist.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_shaded_line.py → _format_stx_shaded_line.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_violin.py → _format_stx_violin.py} +2 -2
- scitex/plt/_subplots/_export_as_csv_formatters/verify_formatters.py +23 -23
- scitex/plt/ax/__init__.py +16 -15
- scitex/plt/ax/_plot/__init__.py +30 -30
- scitex/plt/ax/_plot/_add_fitted_line.py +65 -11
- scitex/plt/ax/_plot/_plot_statistical_shaded_line.py +104 -76
- scitex/plt/ax/_plot/{_plot_conf_mat.py → _stx_conf_mat.py} +10 -10
- scitex/plt/ax/_plot/_stx_ecdf.py +109 -0
- scitex/plt/ax/_plot/{_plot_fillv.py → _stx_fillv.py} +7 -7
- scitex/plt/ax/_plot/_stx_heatmap.py +366 -0
- scitex/plt/ax/_plot/{_plot_image.py → _stx_image.py} +1 -1
- scitex/plt/ax/_plot/_stx_joyplot.py +113 -0
- scitex/plt/ax/_plot/{_plot_raster.py → _stx_raster.py} +37 -25
- scitex/plt/ax/_plot/{_plot_rectangle.py → _stx_rectangle.py} +10 -9
- scitex/plt/ax/_plot/{_plot_scatter_hist.py → _stx_scatter_hist.py} +1 -1
- scitex/plt/ax/_plot/_stx_shaded_line.py +215 -0
- scitex/plt/ax/_plot/{_plot_violin.py → _stx_violin.py} +13 -6
- scitex/plt/ax/_style/__init__.py +3 -0
- scitex/plt/ax/_style/_style_barplot.py +13 -2
- scitex/plt/ax/_style/_style_boxplot.py +78 -32
- scitex/plt/ax/_style/_style_errorbar.py +17 -3
- scitex/plt/ax/_style/_style_scatter.py +17 -3
- scitex/plt/ax/_style/_style_violinplot.py +109 -0
- scitex/plt/color/_vizualize_colors.py +3 -3
- scitex/plt/styles/SCITEX_STYLE.yaml +104 -0
- scitex/plt/styles/__init__.py +57 -0
- scitex/plt/styles/_plot_defaults.py +209 -0
- scitex/plt/styles/_plot_postprocess.py +518 -0
- scitex/plt/styles/_style_loader.py +268 -0
- scitex/plt/styles/presets.py +208 -0
- scitex/plt/utils/_collect_figure_metadata.py +160 -18
- scitex/plt/utils/_colorbar.py +72 -10
- scitex/plt/utils/_configure_mpl.py +108 -52
- scitex/plt/utils/_crop.py +21 -7
- scitex/plt/utils/_figure_mm.py +21 -7
- scitex/stats/__init__.py +13 -1
- scitex/stats/_schema.py +578 -0
- scitex/stats/tests/__init__.py +13 -0
- scitex/stats/tests/correlation/__init__.py +13 -0
- scitex/stats/tests/correlation/_test_pearson.py +262 -0
- scitex/vis/__init__.py +6 -0
- scitex/vis/editor/__init__.py +23 -0
- scitex/vis/editor/_defaults.py +205 -0
- scitex/vis/editor/_edit.py +342 -0
- scitex/vis/editor/_mpl_editor.py +231 -0
- scitex/vis/editor/_tkinter_editor.py +466 -0
- scitex/vis/editor/_web_editor.py +1440 -0
- scitex/vis/model/plot_types.py +15 -15
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/METADATA +2 -1
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/RECORD +94 -67
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/WHEEL +1 -1
- scitex/plt/ax/_plot/_plot_ecdf.py +0 -84
- scitex/plt/ax/_plot/_plot_heatmap.py +0 -277
- scitex/plt/ax/_plot/_plot_joyplot.py +0 -77
- scitex/plt/ax/_plot/_plot_shaded_line.py +0 -142
- scitex/plt/presets.py +0 -224
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ./src/scitex/vis/editor/_tkinter_editor.py
|
|
4
|
+
"""Tkinter-based figure editor with matplotlib canvas."""
|
|
5
|
+
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
from tkinter import ttk, colorchooser, messagebox
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
import copy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TkinterEditor:
|
|
14
|
+
"""
|
|
15
|
+
Interactive figure editor using Tkinter GUI.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
- Figure preview with embedded matplotlib canvas
|
|
19
|
+
- Property editors for colors, line widths, fonts, labels
|
|
20
|
+
- Real-time preview updates
|
|
21
|
+
- Save to .manual.json
|
|
22
|
+
- SciTeX style defaults pre-filled
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
json_path: Path,
|
|
28
|
+
metadata: Dict[str, Any],
|
|
29
|
+
csv_data: Optional[Any] = None,
|
|
30
|
+
manual_overrides: Optional[Dict[str, Any]] = None,
|
|
31
|
+
):
|
|
32
|
+
self.json_path = Path(json_path)
|
|
33
|
+
self.metadata = metadata
|
|
34
|
+
self.csv_data = csv_data
|
|
35
|
+
self.manual_overrides = manual_overrides or {}
|
|
36
|
+
|
|
37
|
+
# Get SciTeX defaults and merge with metadata
|
|
38
|
+
from ._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
39
|
+
self.scitex_defaults = get_scitex_defaults()
|
|
40
|
+
self.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
41
|
+
|
|
42
|
+
# Track current overrides (modifications during session)
|
|
43
|
+
# Start with defaults, then overlay manual overrides
|
|
44
|
+
self.current_overrides = copy.deepcopy(self.scitex_defaults)
|
|
45
|
+
self.current_overrides.update(self.metadata_defaults)
|
|
46
|
+
self.current_overrides.update(self.manual_overrides)
|
|
47
|
+
|
|
48
|
+
# UI state
|
|
49
|
+
self.root = None
|
|
50
|
+
self.canvas = None
|
|
51
|
+
self.fig = None
|
|
52
|
+
self.ax = None
|
|
53
|
+
|
|
54
|
+
def run(self):
|
|
55
|
+
"""Launch the editor GUI."""
|
|
56
|
+
self.root = tk.Tk()
|
|
57
|
+
self.root.title(f"SciTeX Editor - {self.json_path.name}")
|
|
58
|
+
self.root.geometry("1200x800")
|
|
59
|
+
|
|
60
|
+
# Configure grid
|
|
61
|
+
self.root.columnconfigure(0, weight=3) # Canvas area
|
|
62
|
+
self.root.columnconfigure(1, weight=1) # Control panel
|
|
63
|
+
self.root.rowconfigure(0, weight=1)
|
|
64
|
+
|
|
65
|
+
# Create main frames
|
|
66
|
+
self._create_canvas_frame()
|
|
67
|
+
self._create_control_panel()
|
|
68
|
+
self._create_toolbar()
|
|
69
|
+
|
|
70
|
+
# Initial render
|
|
71
|
+
self._render_figure()
|
|
72
|
+
|
|
73
|
+
# Start main loop
|
|
74
|
+
self.root.mainloop()
|
|
75
|
+
|
|
76
|
+
def _create_canvas_frame(self):
|
|
77
|
+
"""Create the matplotlib canvas frame."""
|
|
78
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
79
|
+
from matplotlib.figure import Figure
|
|
80
|
+
|
|
81
|
+
canvas_frame = ttk.Frame(self.root)
|
|
82
|
+
canvas_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
|
83
|
+
canvas_frame.columnconfigure(0, weight=1)
|
|
84
|
+
canvas_frame.rowconfigure(0, weight=1)
|
|
85
|
+
|
|
86
|
+
# Create matplotlib figure
|
|
87
|
+
self.fig = Figure(figsize=(8, 6), dpi=100)
|
|
88
|
+
self.ax = self.fig.add_subplot(111)
|
|
89
|
+
|
|
90
|
+
# Create canvas
|
|
91
|
+
self.canvas = FigureCanvasTkAgg(self.fig, master=canvas_frame)
|
|
92
|
+
self.canvas.draw()
|
|
93
|
+
self.canvas.get_tk_widget().grid(row=0, column=0, sticky="nsew")
|
|
94
|
+
|
|
95
|
+
# Navigation toolbar
|
|
96
|
+
toolbar_frame = ttk.Frame(canvas_frame)
|
|
97
|
+
toolbar_frame.grid(row=1, column=0, sticky="ew")
|
|
98
|
+
NavigationToolbar2Tk(self.canvas, toolbar_frame)
|
|
99
|
+
|
|
100
|
+
def _create_control_panel(self):
|
|
101
|
+
"""Create the property control panel."""
|
|
102
|
+
panel = ttk.Frame(self.root, padding=10)
|
|
103
|
+
panel.grid(row=0, column=1, sticky="nsew")
|
|
104
|
+
|
|
105
|
+
# Notebook for tabbed controls
|
|
106
|
+
notebook = ttk.Notebook(panel)
|
|
107
|
+
notebook.pack(fill="both", expand=True)
|
|
108
|
+
|
|
109
|
+
# Tab 1: Figure settings
|
|
110
|
+
fig_tab = ttk.Frame(notebook, padding=10)
|
|
111
|
+
notebook.add(fig_tab, text="Figure")
|
|
112
|
+
self._create_figure_controls(fig_tab)
|
|
113
|
+
|
|
114
|
+
# Tab 2: Axes settings
|
|
115
|
+
axes_tab = ttk.Frame(notebook, padding=10)
|
|
116
|
+
notebook.add(axes_tab, text="Axes")
|
|
117
|
+
self._create_axes_controls(axes_tab)
|
|
118
|
+
|
|
119
|
+
# Tab 3: Style settings
|
|
120
|
+
style_tab = ttk.Frame(notebook, padding=10)
|
|
121
|
+
notebook.add(style_tab, text="Style")
|
|
122
|
+
self._create_style_controls(style_tab)
|
|
123
|
+
|
|
124
|
+
# Tab 4: Annotations
|
|
125
|
+
annot_tab = ttk.Frame(notebook, padding=10)
|
|
126
|
+
notebook.add(annot_tab, text="Annotations")
|
|
127
|
+
self._create_annotation_controls(annot_tab)
|
|
128
|
+
|
|
129
|
+
def _create_figure_controls(self, parent):
|
|
130
|
+
"""Create figure-level controls."""
|
|
131
|
+
# Title
|
|
132
|
+
ttk.Label(parent, text="Title:").grid(row=0, column=0, sticky="w", pady=2)
|
|
133
|
+
self.title_var = tk.StringVar(value=self._get_override('title', ''))
|
|
134
|
+
title_entry = ttk.Entry(parent, textvariable=self.title_var, width=30)
|
|
135
|
+
title_entry.grid(row=0, column=1, sticky="ew", pady=2)
|
|
136
|
+
title_entry.bind('<Return>', lambda e: self._update_and_render())
|
|
137
|
+
|
|
138
|
+
# X Label
|
|
139
|
+
ttk.Label(parent, text="X Label:").grid(row=1, column=0, sticky="w", pady=2)
|
|
140
|
+
self.xlabel_var = tk.StringVar(value=self._get_override('xlabel', ''))
|
|
141
|
+
xlabel_entry = ttk.Entry(parent, textvariable=self.xlabel_var, width=30)
|
|
142
|
+
xlabel_entry.grid(row=1, column=1, sticky="ew", pady=2)
|
|
143
|
+
xlabel_entry.bind('<Return>', lambda e: self._update_and_render())
|
|
144
|
+
|
|
145
|
+
# Y Label
|
|
146
|
+
ttk.Label(parent, text="Y Label:").grid(row=2, column=0, sticky="w", pady=2)
|
|
147
|
+
self.ylabel_var = tk.StringVar(value=self._get_override('ylabel', ''))
|
|
148
|
+
ylabel_entry = ttk.Entry(parent, textvariable=self.ylabel_var, width=30)
|
|
149
|
+
ylabel_entry.grid(row=2, column=1, sticky="ew", pady=2)
|
|
150
|
+
ylabel_entry.bind('<Return>', lambda e: self._update_and_render())
|
|
151
|
+
|
|
152
|
+
# Apply button
|
|
153
|
+
ttk.Button(parent, text="Apply", command=self._update_and_render).grid(
|
|
154
|
+
row=3, column=0, columnspan=2, pady=10
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
parent.columnconfigure(1, weight=1)
|
|
158
|
+
|
|
159
|
+
def _create_axes_controls(self, parent):
|
|
160
|
+
"""Create axes-level controls."""
|
|
161
|
+
# X limits
|
|
162
|
+
ttk.Label(parent, text="X Min:").grid(row=0, column=0, sticky="w", pady=2)
|
|
163
|
+
self.xmin_var = tk.StringVar(value="")
|
|
164
|
+
ttk.Entry(parent, textvariable=self.xmin_var, width=10).grid(row=0, column=1, pady=2)
|
|
165
|
+
|
|
166
|
+
ttk.Label(parent, text="X Max:").grid(row=0, column=2, sticky="w", pady=2, padx=(10, 0))
|
|
167
|
+
self.xmax_var = tk.StringVar(value="")
|
|
168
|
+
ttk.Entry(parent, textvariable=self.xmax_var, width=10).grid(row=0, column=3, pady=2)
|
|
169
|
+
|
|
170
|
+
# Y limits
|
|
171
|
+
ttk.Label(parent, text="Y Min:").grid(row=1, column=0, sticky="w", pady=2)
|
|
172
|
+
self.ymin_var = tk.StringVar(value="")
|
|
173
|
+
ttk.Entry(parent, textvariable=self.ymin_var, width=10).grid(row=1, column=1, pady=2)
|
|
174
|
+
|
|
175
|
+
ttk.Label(parent, text="Y Max:").grid(row=1, column=2, sticky="w", pady=2, padx=(10, 0))
|
|
176
|
+
self.ymax_var = tk.StringVar(value="")
|
|
177
|
+
ttk.Entry(parent, textvariable=self.ymax_var, width=10).grid(row=1, column=3, pady=2)
|
|
178
|
+
|
|
179
|
+
# Grid toggle
|
|
180
|
+
self.grid_var = tk.BooleanVar(value=self._get_override('grid', False))
|
|
181
|
+
ttk.Checkbutton(parent, text="Show Grid", variable=self.grid_var,
|
|
182
|
+
command=self._update_and_render).grid(row=2, column=0, columnspan=2, pady=5)
|
|
183
|
+
|
|
184
|
+
# Apply button
|
|
185
|
+
ttk.Button(parent, text="Apply Limits", command=self._apply_limits).grid(
|
|
186
|
+
row=3, column=0, columnspan=4, pady=10
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _create_style_controls(self, parent):
|
|
190
|
+
"""Create style controls."""
|
|
191
|
+
# Line width
|
|
192
|
+
ttk.Label(parent, text="Line Width:").grid(row=0, column=0, sticky="w", pady=2)
|
|
193
|
+
self.linewidth_var = tk.DoubleVar(value=self._get_override('linewidth', 1.5))
|
|
194
|
+
lw_spin = ttk.Spinbox(parent, from_=0.1, to=10, increment=0.1,
|
|
195
|
+
textvariable=self.linewidth_var, width=8)
|
|
196
|
+
lw_spin.grid(row=0, column=1, sticky="w", pady=2)
|
|
197
|
+
|
|
198
|
+
# Font size
|
|
199
|
+
ttk.Label(parent, text="Font Size:").grid(row=1, column=0, sticky="w", pady=2)
|
|
200
|
+
self.fontsize_var = tk.IntVar(value=self._get_override('fontsize', 10))
|
|
201
|
+
fs_spin = ttk.Spinbox(parent, from_=6, to=24, increment=1,
|
|
202
|
+
textvariable=self.fontsize_var, width=8)
|
|
203
|
+
fs_spin.grid(row=1, column=1, sticky="w", pady=2)
|
|
204
|
+
|
|
205
|
+
# Background color
|
|
206
|
+
ttk.Label(parent, text="Background:").grid(row=2, column=0, sticky="w", pady=2)
|
|
207
|
+
self.bg_color = self._get_override('facecolor', 'white')
|
|
208
|
+
self.bg_btn = ttk.Button(parent, text="Choose...", command=self._choose_bg_color)
|
|
209
|
+
self.bg_btn.grid(row=2, column=1, sticky="w", pady=2)
|
|
210
|
+
|
|
211
|
+
# Color frame to show current color
|
|
212
|
+
self.bg_preview = tk.Frame(parent, width=20, height=20, bg=self.bg_color)
|
|
213
|
+
self.bg_preview.grid(row=2, column=2, padx=5)
|
|
214
|
+
|
|
215
|
+
# Apply button
|
|
216
|
+
ttk.Button(parent, text="Apply Style", command=self._update_and_render).grid(
|
|
217
|
+
row=5, column=0, columnspan=3, pady=10
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _create_annotation_controls(self, parent):
|
|
221
|
+
"""Create annotation controls."""
|
|
222
|
+
ttk.Label(parent, text="Add annotations:").grid(row=0, column=0, columnspan=2, sticky="w")
|
|
223
|
+
|
|
224
|
+
# Text annotation
|
|
225
|
+
ttk.Label(parent, text="Text:").grid(row=1, column=0, sticky="w", pady=2)
|
|
226
|
+
self.annot_text_var = tk.StringVar()
|
|
227
|
+
ttk.Entry(parent, textvariable=self.annot_text_var, width=25).grid(row=1, column=1, pady=2)
|
|
228
|
+
|
|
229
|
+
ttk.Label(parent, text="X:").grid(row=2, column=0, sticky="w", pady=2)
|
|
230
|
+
self.annot_x_var = tk.StringVar(value="0.5")
|
|
231
|
+
ttk.Entry(parent, textvariable=self.annot_x_var, width=10).grid(row=2, column=1, sticky="w", pady=2)
|
|
232
|
+
|
|
233
|
+
ttk.Label(parent, text="Y:").grid(row=3, column=0, sticky="w", pady=2)
|
|
234
|
+
self.annot_y_var = tk.StringVar(value="0.5")
|
|
235
|
+
ttk.Entry(parent, textvariable=self.annot_y_var, width=10).grid(row=3, column=1, sticky="w", pady=2)
|
|
236
|
+
|
|
237
|
+
ttk.Button(parent, text="Add Text", command=self._add_text_annotation).grid(
|
|
238
|
+
row=4, column=0, columnspan=2, pady=5
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Annotation list
|
|
242
|
+
ttk.Label(parent, text="Current annotations:").grid(row=5, column=0, columnspan=2, sticky="w", pady=(10, 2))
|
|
243
|
+
self.annot_listbox = tk.Listbox(parent, height=5, width=30)
|
|
244
|
+
self.annot_listbox.grid(row=6, column=0, columnspan=2, sticky="ew")
|
|
245
|
+
|
|
246
|
+
ttk.Button(parent, text="Remove Selected", command=self._remove_annotation).grid(
|
|
247
|
+
row=7, column=0, columnspan=2, pady=5
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
self._update_annotation_list()
|
|
251
|
+
|
|
252
|
+
def _create_toolbar(self):
|
|
253
|
+
"""Create the main toolbar."""
|
|
254
|
+
toolbar = ttk.Frame(self.root)
|
|
255
|
+
toolbar.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
|
256
|
+
|
|
257
|
+
ttk.Button(toolbar, text="Save", command=self._save_manual).pack(side="left", padx=2)
|
|
258
|
+
ttk.Button(toolbar, text="Reset", command=self._reset_overrides).pack(side="left", padx=2)
|
|
259
|
+
ttk.Button(toolbar, text="Export PNG", command=self._export_png).pack(side="left", padx=2)
|
|
260
|
+
|
|
261
|
+
# Status label
|
|
262
|
+
self.status_var = tk.StringVar(value="Ready")
|
|
263
|
+
ttk.Label(toolbar, textvariable=self.status_var).pack(side="right", padx=10)
|
|
264
|
+
|
|
265
|
+
def _get_override(self, key, default=None):
|
|
266
|
+
"""Get value from current overrides or default."""
|
|
267
|
+
return self.current_overrides.get(key, default)
|
|
268
|
+
|
|
269
|
+
def _render_figure(self):
|
|
270
|
+
"""Render the figure with current data and overrides."""
|
|
271
|
+
import scitex as stx
|
|
272
|
+
|
|
273
|
+
self.ax.clear()
|
|
274
|
+
|
|
275
|
+
# Try to reconstruct from CSV data
|
|
276
|
+
if self.csv_data is not None:
|
|
277
|
+
self._plot_from_csv()
|
|
278
|
+
else:
|
|
279
|
+
# Show placeholder
|
|
280
|
+
self.ax.text(0.5, 0.5, "No plot data available\n(CSV not found)",
|
|
281
|
+
ha='center', va='center', transform=self.ax.transAxes)
|
|
282
|
+
|
|
283
|
+
# Apply overrides
|
|
284
|
+
if self.current_overrides.get('title'):
|
|
285
|
+
self.ax.set_title(self.current_overrides['title'])
|
|
286
|
+
if self.current_overrides.get('xlabel'):
|
|
287
|
+
self.ax.set_xlabel(self.current_overrides['xlabel'])
|
|
288
|
+
if self.current_overrides.get('ylabel'):
|
|
289
|
+
self.ax.set_ylabel(self.current_overrides['ylabel'])
|
|
290
|
+
if self.current_overrides.get('grid'):
|
|
291
|
+
self.ax.grid(True)
|
|
292
|
+
if self.current_overrides.get('xlim'):
|
|
293
|
+
self.ax.set_xlim(self.current_overrides['xlim'])
|
|
294
|
+
if self.current_overrides.get('ylim'):
|
|
295
|
+
self.ax.set_ylim(self.current_overrides['ylim'])
|
|
296
|
+
if self.current_overrides.get('facecolor'):
|
|
297
|
+
self.ax.set_facecolor(self.current_overrides['facecolor'])
|
|
298
|
+
|
|
299
|
+
# Apply annotations
|
|
300
|
+
for annot in self.current_overrides.get('annotations', []):
|
|
301
|
+
if annot.get('type') == 'text':
|
|
302
|
+
self.ax.text(
|
|
303
|
+
annot.get('x', 0.5),
|
|
304
|
+
annot.get('y', 0.5),
|
|
305
|
+
annot.get('text', ''),
|
|
306
|
+
transform=self.ax.transAxes,
|
|
307
|
+
fontsize=annot.get('fontsize', 10),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
self.fig.tight_layout()
|
|
311
|
+
self.canvas.draw()
|
|
312
|
+
|
|
313
|
+
def _plot_from_csv(self):
|
|
314
|
+
"""Reconstruct plot from CSV data."""
|
|
315
|
+
import pandas as pd
|
|
316
|
+
|
|
317
|
+
if isinstance(self.csv_data, pd.DataFrame):
|
|
318
|
+
df = self.csv_data
|
|
319
|
+
else:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
# Try to identify x and y columns
|
|
323
|
+
cols = df.columns.tolist()
|
|
324
|
+
|
|
325
|
+
# Simple heuristic: first column is x, rest are y series
|
|
326
|
+
if len(cols) >= 2:
|
|
327
|
+
x_col = cols[0]
|
|
328
|
+
for y_col in cols[1:]:
|
|
329
|
+
try:
|
|
330
|
+
self.ax.plot(df[x_col], df[y_col], label=str(y_col))
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
if len(cols) > 2:
|
|
334
|
+
self.ax.legend()
|
|
335
|
+
elif len(cols) == 1:
|
|
336
|
+
self.ax.plot(df[cols[0]])
|
|
337
|
+
|
|
338
|
+
def _update_and_render(self):
|
|
339
|
+
"""Update overrides from UI and re-render."""
|
|
340
|
+
# Collect values from UI
|
|
341
|
+
if hasattr(self, 'title_var') and self.title_var.get():
|
|
342
|
+
self.current_overrides['title'] = self.title_var.get()
|
|
343
|
+
if hasattr(self, 'xlabel_var') and self.xlabel_var.get():
|
|
344
|
+
self.current_overrides['xlabel'] = self.xlabel_var.get()
|
|
345
|
+
if hasattr(self, 'ylabel_var') and self.ylabel_var.get():
|
|
346
|
+
self.current_overrides['ylabel'] = self.ylabel_var.get()
|
|
347
|
+
if hasattr(self, 'grid_var'):
|
|
348
|
+
self.current_overrides['grid'] = self.grid_var.get()
|
|
349
|
+
if hasattr(self, 'linewidth_var'):
|
|
350
|
+
self.current_overrides['linewidth'] = self.linewidth_var.get()
|
|
351
|
+
if hasattr(self, 'fontsize_var'):
|
|
352
|
+
self.current_overrides['fontsize'] = self.fontsize_var.get()
|
|
353
|
+
|
|
354
|
+
self._render_figure()
|
|
355
|
+
self.status_var.set("Preview updated")
|
|
356
|
+
|
|
357
|
+
def _apply_limits(self):
|
|
358
|
+
"""Apply axis limits."""
|
|
359
|
+
try:
|
|
360
|
+
if self.xmin_var.get() and self.xmax_var.get():
|
|
361
|
+
self.current_overrides['xlim'] = [
|
|
362
|
+
float(self.xmin_var.get()),
|
|
363
|
+
float(self.xmax_var.get())
|
|
364
|
+
]
|
|
365
|
+
if self.ymin_var.get() and self.ymax_var.get():
|
|
366
|
+
self.current_overrides['ylim'] = [
|
|
367
|
+
float(self.ymin_var.get()),
|
|
368
|
+
float(self.ymax_var.get())
|
|
369
|
+
]
|
|
370
|
+
self._render_figure()
|
|
371
|
+
self.status_var.set("Limits applied")
|
|
372
|
+
except ValueError:
|
|
373
|
+
messagebox.showerror("Error", "Invalid limit values")
|
|
374
|
+
|
|
375
|
+
def _choose_bg_color(self):
|
|
376
|
+
"""Open color chooser for background."""
|
|
377
|
+
color = colorchooser.askcolor(title="Choose Background Color", color=self.bg_color)
|
|
378
|
+
if color[1]:
|
|
379
|
+
self.bg_color = color[1]
|
|
380
|
+
self.bg_preview.configure(bg=self.bg_color)
|
|
381
|
+
self.current_overrides['facecolor'] = self.bg_color
|
|
382
|
+
self._render_figure()
|
|
383
|
+
|
|
384
|
+
def _add_text_annotation(self):
|
|
385
|
+
"""Add a text annotation."""
|
|
386
|
+
text = self.annot_text_var.get()
|
|
387
|
+
if not text:
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
x = float(self.annot_x_var.get())
|
|
392
|
+
y = float(self.annot_y_var.get())
|
|
393
|
+
except ValueError:
|
|
394
|
+
messagebox.showerror("Error", "Invalid X or Y position")
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
if 'annotations' not in self.current_overrides:
|
|
398
|
+
self.current_overrides['annotations'] = []
|
|
399
|
+
|
|
400
|
+
self.current_overrides['annotations'].append({
|
|
401
|
+
'type': 'text',
|
|
402
|
+
'text': text,
|
|
403
|
+
'x': x,
|
|
404
|
+
'y': y,
|
|
405
|
+
'fontsize': self.fontsize_var.get() if hasattr(self, 'fontsize_var') else 10,
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
self.annot_text_var.set("")
|
|
409
|
+
self._update_annotation_list()
|
|
410
|
+
self._render_figure()
|
|
411
|
+
self.status_var.set("Annotation added")
|
|
412
|
+
|
|
413
|
+
def _remove_annotation(self):
|
|
414
|
+
"""Remove selected annotation."""
|
|
415
|
+
selection = self.annot_listbox.curselection()
|
|
416
|
+
if not selection:
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
idx = selection[0]
|
|
420
|
+
annotations = self.current_overrides.get('annotations', [])
|
|
421
|
+
if idx < len(annotations):
|
|
422
|
+
del annotations[idx]
|
|
423
|
+
self._update_annotation_list()
|
|
424
|
+
self._render_figure()
|
|
425
|
+
self.status_var.set("Annotation removed")
|
|
426
|
+
|
|
427
|
+
def _update_annotation_list(self):
|
|
428
|
+
"""Update the annotation listbox."""
|
|
429
|
+
self.annot_listbox.delete(0, tk.END)
|
|
430
|
+
for annot in self.current_overrides.get('annotations', []):
|
|
431
|
+
if annot.get('type') == 'text':
|
|
432
|
+
self.annot_listbox.insert(tk.END, f"Text: {annot.get('text', '')[:20]}")
|
|
433
|
+
|
|
434
|
+
def _save_manual(self):
|
|
435
|
+
"""Save current overrides to .manual.json."""
|
|
436
|
+
from ._edit import save_manual_overrides
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
manual_path = save_manual_overrides(self.json_path, self.current_overrides)
|
|
440
|
+
self.status_var.set(f"Saved: {manual_path.name}")
|
|
441
|
+
messagebox.showinfo("Saved", f"Manual overrides saved to:\n{manual_path}")
|
|
442
|
+
except Exception as e:
|
|
443
|
+
messagebox.showerror("Error", f"Failed to save: {e}")
|
|
444
|
+
|
|
445
|
+
def _reset_overrides(self):
|
|
446
|
+
"""Reset to original overrides."""
|
|
447
|
+
if messagebox.askyesno("Reset", "Reset all changes?"):
|
|
448
|
+
self.current_overrides = copy.deepcopy(self.manual_overrides)
|
|
449
|
+
self._render_figure()
|
|
450
|
+
self.status_var.set("Reset to original")
|
|
451
|
+
|
|
452
|
+
def _export_png(self):
|
|
453
|
+
"""Export current view to PNG."""
|
|
454
|
+
from tkinter import filedialog
|
|
455
|
+
|
|
456
|
+
filepath = filedialog.asksaveasfilename(
|
|
457
|
+
defaultextension=".png",
|
|
458
|
+
filetypes=[("PNG files", "*.png"), ("All files", "*.*")],
|
|
459
|
+
initialfile=f"{self.json_path.stem}_edited.png"
|
|
460
|
+
)
|
|
461
|
+
if filepath:
|
|
462
|
+
self.fig.savefig(filepath, dpi=300, bbox_inches='tight')
|
|
463
|
+
self.status_var.set(f"Exported: {Path(filepath).name}")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# EOF
|