scitex 2.3.0__py3-none-any.whl → 2.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. scitex/ai/classification/reporters/reporter_utils/_Plotter.py +1 -1
  2. scitex/ai/plt/__init__.py +2 -2
  3. scitex/ai/plt/{_plot_conf_mat.py → _stx_conf_mat.py} +3 -3
  4. scitex/config/PriorityConfig.py +195 -0
  5. scitex/config/__init__.py +24 -0
  6. scitex/io/_save.py +125 -34
  7. scitex/io/_save_modules/_image.py +37 -20
  8. scitex/plt/__init__.py +470 -17
  9. scitex/plt/_subplots/_AxisWrapper.py +98 -50
  10. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +559 -124
  11. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +49 -8
  12. scitex/plt/_subplots/_SubplotsWrapper.py +76 -91
  13. scitex/plt/_subplots/_export_as_csv.py +127 -58
  14. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +25 -16
  15. scitex/plt/_subplots/_export_as_csv_formatters/_format_contourf.py +54 -0
  16. scitex/plt/_subplots/_export_as_csv_formatters/_format_hexbin.py +41 -0
  17. scitex/plt/_subplots/_export_as_csv_formatters/_format_hist2d.py +41 -0
  18. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow.py +59 -47
  19. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +42 -0
  20. scitex/plt/_subplots/_export_as_csv_formatters/_format_pie.py +42 -0
  21. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +72 -35
  22. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_box.py +1 -1
  23. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +2 -2
  24. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +53 -0
  25. scitex/plt/_subplots/_export_as_csv_formatters/_format_stem.py +42 -0
  26. scitex/plt/_subplots/_export_as_csv_formatters/_format_step.py +42 -0
  27. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +48 -0
  28. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_conf_mat.py → _format_stx_conf_mat.py} +2 -2
  29. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_ecdf.py → _format_stx_ecdf.py} +2 -2
  30. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_fillv.py → _format_stx_fillv.py} +2 -2
  31. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_heatmap.py → _format_stx_heatmap.py} +2 -2
  32. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_image.py → _format_stx_image.py} +2 -2
  33. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_joyplot.py → _format_stx_joyplot.py} +2 -2
  34. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_line.py → _format_stx_line.py} +3 -3
  35. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_ci.py → _format_stx_mean_ci.py} +2 -2
  36. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_mean_std.py → _format_stx_mean_std.py} +2 -2
  37. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_median_iqr.py → _format_stx_median_iqr.py} +2 -2
  38. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_raster.py → _format_stx_raster.py} +2 -2
  39. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_rectangle.py → _format_stx_rectangle.py} +1 -1
  40. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_scatter_hist.py → _format_stx_scatter_hist.py} +2 -2
  41. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_shaded_line.py → _format_stx_shaded_line.py} +2 -2
  42. scitex/plt/_subplots/_export_as_csv_formatters/{_format_plot_violin.py → _format_stx_violin.py} +2 -2
  43. scitex/plt/_subplots/_export_as_csv_formatters/verify_formatters.py +23 -23
  44. scitex/plt/ax/__init__.py +16 -15
  45. scitex/plt/ax/_plot/__init__.py +30 -30
  46. scitex/plt/ax/_plot/_add_fitted_line.py +65 -11
  47. scitex/plt/ax/_plot/_plot_statistical_shaded_line.py +104 -76
  48. scitex/plt/ax/_plot/{_plot_conf_mat.py → _stx_conf_mat.py} +10 -10
  49. scitex/plt/ax/_plot/_stx_ecdf.py +109 -0
  50. scitex/plt/ax/_plot/{_plot_fillv.py → _stx_fillv.py} +7 -7
  51. scitex/plt/ax/_plot/_stx_heatmap.py +366 -0
  52. scitex/plt/ax/_plot/{_plot_image.py → _stx_image.py} +1 -1
  53. scitex/plt/ax/_plot/_stx_joyplot.py +113 -0
  54. scitex/plt/ax/_plot/{_plot_raster.py → _stx_raster.py} +37 -25
  55. scitex/plt/ax/_plot/{_plot_rectangle.py → _stx_rectangle.py} +10 -9
  56. scitex/plt/ax/_plot/{_plot_scatter_hist.py → _stx_scatter_hist.py} +1 -1
  57. scitex/plt/ax/_plot/_stx_shaded_line.py +215 -0
  58. scitex/plt/ax/_plot/{_plot_violin.py → _stx_violin.py} +13 -6
  59. scitex/plt/ax/_style/__init__.py +3 -0
  60. scitex/plt/ax/_style/_style_barplot.py +13 -2
  61. scitex/plt/ax/_style/_style_boxplot.py +78 -32
  62. scitex/plt/ax/_style/_style_errorbar.py +17 -3
  63. scitex/plt/ax/_style/_style_scatter.py +17 -3
  64. scitex/plt/ax/_style/_style_violinplot.py +109 -0
  65. scitex/plt/color/_vizualize_colors.py +3 -3
  66. scitex/plt/styles/SCITEX_STYLE.yaml +104 -0
  67. scitex/plt/styles/__init__.py +57 -0
  68. scitex/plt/styles/_plot_defaults.py +209 -0
  69. scitex/plt/styles/_plot_postprocess.py +518 -0
  70. scitex/plt/styles/_style_loader.py +268 -0
  71. scitex/plt/styles/presets.py +208 -0
  72. scitex/plt/utils/_collect_figure_metadata.py +160 -18
  73. scitex/plt/utils/_colorbar.py +72 -10
  74. scitex/plt/utils/_configure_mpl.py +108 -52
  75. scitex/plt/utils/_crop.py +21 -7
  76. scitex/plt/utils/_figure_mm.py +21 -7
  77. scitex/stats/__init__.py +13 -1
  78. scitex/stats/_schema.py +578 -0
  79. scitex/stats/tests/__init__.py +13 -0
  80. scitex/stats/tests/correlation/__init__.py +13 -0
  81. scitex/stats/tests/correlation/_test_pearson.py +262 -0
  82. scitex/vis/__init__.py +6 -0
  83. scitex/vis/editor/__init__.py +23 -0
  84. scitex/vis/editor/_defaults.py +205 -0
  85. scitex/vis/editor/_edit.py +342 -0
  86. scitex/vis/editor/_mpl_editor.py +231 -0
  87. scitex/vis/editor/_tkinter_editor.py +466 -0
  88. scitex/vis/editor/_web_editor.py +1440 -0
  89. scitex/vis/model/plot_types.py +15 -15
  90. {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/METADATA +2 -1
  91. {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/RECORD +94 -67
  92. {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/WHEEL +1 -1
  93. scitex/plt/ax/_plot/_plot_ecdf.py +0 -84
  94. scitex/plt/ax/_plot/_plot_heatmap.py +0 -277
  95. scitex/plt/ax/_plot/_plot_joyplot.py +0 -77
  96. scitex/plt/ax/_plot/_plot_shaded_line.py +0 -142
  97. scitex/plt/presets.py +0 -224
  98. {scitex-2.3.0.dist-info → scitex-2.4.1.dist-info}/entry_points.txt +0 -0
  99. {scitex-2.3.0.dist-info → scitex-2.4.1.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