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.
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 +254 -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.0.dist-info}/METADATA +2 -1
  91. {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/RECORD +94 -67
  92. {scitex-2.3.0.dist-info → scitex-2.4.0.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.0.dist-info}/entry_points.txt +0 -0
  99. {scitex-2.3.0.dist-info → scitex-2.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # File: ./src/scitex/vis/editor/_edit.py
4
+ """Main edit function for launching visual editor."""
5
+
6
+ from pathlib import Path
7
+ from typing import Union, Optional, Literal
8
+ import hashlib
9
+ import json
10
+
11
+
12
+ def edit(
13
+ path: Union[str, Path],
14
+ backend: Literal["auto", "web", "dearpygui", "qt", "tkinter", "mpl"] = "auto",
15
+ apply_manual: bool = True,
16
+ ) -> None:
17
+ """
18
+ Launch interactive editor for figure style/annotation editing.
19
+
20
+ Parameters
21
+ ----------
22
+ path : str or Path
23
+ Path to figure file. Can be:
24
+ - JSON file (figure.json or figure.manual.json)
25
+ - CSV file (figure.csv) - for data-only start
26
+ - PNG file (figure.png)
27
+ Will auto-detect sibling files in same directory or organized subdirectories.
28
+ backend : str, optional
29
+ GUI backend to use (default: "auto"):
30
+ - "auto": Pick best available with graceful degradation
31
+ (web -> dearpygui -> qt -> tkinter -> mpl)
32
+ - "web": Browser-based editor (Flask/FastAPI, modern UI)
33
+ - "dearpygui": GPU-accelerated modern GUI (fast, requires dearpygui)
34
+ - "qt": Rich desktop editor (requires PyQt5/6 or PySide2/6)
35
+ - "tkinter": Built-in Python GUI (works everywhere)
36
+ - "mpl": Minimal matplotlib interactive mode (always works)
37
+ apply_manual : bool, optional
38
+ If True, load .manual.json overrides if exists (default: True)
39
+
40
+ Returns
41
+ -------
42
+ None
43
+ Editor runs in GUI event loop. Changes saved to .manual.json.
44
+
45
+ Examples
46
+ --------
47
+ >>> import scitex as stx
48
+ >>> stx.vis.edit("output/figure.json") # Auto-select best backend
49
+ >>> stx.vis.edit("output/figure.png", backend="web") # Force web editor
50
+ >>> stx.vis.edit("output/figure.json", backend="tkinter") # Force tkinter
51
+
52
+ Notes
53
+ -----
54
+ - Changes are saved to `{basename}.manual.json` alongside the original
55
+ - Manual JSON includes hash of base JSON for staleness detection
56
+ - Original JSON/CSV files are never modified
57
+ - Backend auto-detection order: web > dearpygui > qt > tkinter > mpl
58
+ """
59
+ path = Path(path)
60
+
61
+ # Resolve paths (JSON, CSV, PNG)
62
+ json_path, csv_path, png_path = _resolve_figure_paths(path)
63
+
64
+ if not json_path.exists():
65
+ raise FileNotFoundError(f"JSON file not found: {json_path}")
66
+
67
+ # Load data
68
+ import scitex as stx
69
+ metadata = stx.io.load(json_path)
70
+ csv_data = None
71
+ if csv_path and csv_path.exists():
72
+ csv_data = stx.io.load(csv_path)
73
+
74
+ # Load manual overrides if exists
75
+ manual_path = json_path.with_suffix('.manual.json')
76
+ manual_overrides = None
77
+ if apply_manual and manual_path.exists():
78
+ manual_data = stx.io.load(manual_path)
79
+ manual_overrides = manual_data.get('overrides', {})
80
+
81
+ # Resolve backend if "auto"
82
+ if backend == "auto":
83
+ backend = _detect_best_backend()
84
+
85
+ # Launch appropriate backend
86
+ if backend == "web":
87
+ try:
88
+ from ._web_editor import WebEditor
89
+ editor = WebEditor(
90
+ json_path=json_path,
91
+ metadata=metadata,
92
+ csv_data=csv_data,
93
+ png_path=png_path,
94
+ manual_overrides=manual_overrides,
95
+ )
96
+ editor.run()
97
+ except ImportError as e:
98
+ raise ImportError(
99
+ "Web backend requires Flask or FastAPI. "
100
+ "Install with: pip install flask"
101
+ ) from e
102
+ elif backend == "dearpygui":
103
+ try:
104
+ from ._dearpygui_editor import DearPyGuiEditor
105
+ editor = DearPyGuiEditor(
106
+ json_path=json_path,
107
+ metadata=metadata,
108
+ csv_data=csv_data,
109
+ manual_overrides=manual_overrides,
110
+ )
111
+ editor.run()
112
+ except ImportError as e:
113
+ raise ImportError(
114
+ "DearPyGui backend requires dearpygui. "
115
+ "Install with: pip install dearpygui"
116
+ ) from e
117
+ elif backend == "qt":
118
+ try:
119
+ from ._qt_editor import QtEditor
120
+ editor = QtEditor(
121
+ json_path=json_path,
122
+ metadata=metadata,
123
+ csv_data=csv_data,
124
+ manual_overrides=manual_overrides,
125
+ )
126
+ editor.run()
127
+ except ImportError as e:
128
+ raise ImportError(
129
+ "Qt backend requires PyQt5/PyQt6 or PySide2/PySide6. "
130
+ "Install with: pip install PyQt6"
131
+ ) from e
132
+ elif backend == "tkinter":
133
+ from ._tkinter_editor import TkinterEditor
134
+ editor = TkinterEditor(
135
+ json_path=json_path,
136
+ metadata=metadata,
137
+ csv_data=csv_data,
138
+ manual_overrides=manual_overrides,
139
+ )
140
+ editor.run()
141
+ elif backend == "mpl":
142
+ from ._mpl_editor import MplEditor
143
+ editor = MplEditor(
144
+ json_path=json_path,
145
+ metadata=metadata,
146
+ csv_data=csv_data,
147
+ manual_overrides=manual_overrides,
148
+ )
149
+ editor.run()
150
+ else:
151
+ raise ValueError(
152
+ f"Unknown backend: {backend}. "
153
+ "Use 'auto', 'web', 'dearpygui', 'qt', 'tkinter', or 'mpl'."
154
+ )
155
+
156
+
157
+ def _detect_best_backend() -> str:
158
+ """
159
+ Detect the best available GUI backend with graceful degradation.
160
+
161
+ Order: web > dearpygui > qt > tkinter > mpl
162
+ Shows warnings when falling back to less capable backends.
163
+ """
164
+ import warnings
165
+
166
+ # Try Web (Flask/FastAPI) - best for modern UI
167
+ try:
168
+ import flask
169
+ return "web"
170
+ except ImportError:
171
+ pass
172
+ try:
173
+ import fastapi
174
+ return "web"
175
+ except ImportError:
176
+ pass
177
+
178
+ # Try DearPyGui - GPU-accelerated, modern
179
+ try:
180
+ import dearpygui
181
+ return "dearpygui"
182
+ except ImportError:
183
+ warnings.warn(
184
+ "Web/Flask not available. Consider: pip install flask\n"
185
+ "Trying DearPyGui..."
186
+ )
187
+
188
+ # Try DearPyGui
189
+ try:
190
+ import dearpygui
191
+ return "dearpygui"
192
+ except ImportError:
193
+ pass
194
+
195
+ # Try Qt (richest desktop features)
196
+ qt_available = False
197
+ try:
198
+ import PyQt6
199
+ qt_available = True
200
+ except ImportError:
201
+ pass
202
+ if not qt_available:
203
+ try:
204
+ import PyQt5
205
+ qt_available = True
206
+ except ImportError:
207
+ pass
208
+ if not qt_available:
209
+ try:
210
+ import PySide6
211
+ qt_available = True
212
+ except ImportError:
213
+ pass
214
+ if not qt_available:
215
+ try:
216
+ import PySide2
217
+ qt_available = True
218
+ except ImportError:
219
+ pass
220
+
221
+ if qt_available:
222
+ warnings.warn(
223
+ "DearPyGui not available. Consider: pip install dearpygui\n"
224
+ "Using Qt backend instead."
225
+ )
226
+ return "qt"
227
+
228
+ # Try Tkinter (built-in, good features)
229
+ try:
230
+ import tkinter
231
+ warnings.warn(
232
+ "Qt not available. Consider: pip install PyQt6\n"
233
+ "Using Tkinter backend (basic features)."
234
+ )
235
+ return "tkinter"
236
+ except ImportError:
237
+ pass
238
+
239
+ # Fall back to matplotlib interactive (always works)
240
+ warnings.warn(
241
+ "No GUI toolkit found. Using minimal matplotlib editor.\n"
242
+ "For better experience, install: pip install flask (web) or pip install PyQt6 (desktop)"
243
+ )
244
+ return "mpl"
245
+
246
+
247
+ def _resolve_figure_paths(path: Path) -> tuple:
248
+ """
249
+ Resolve JSON, CSV, and PNG paths from any input file path.
250
+
251
+ Handles two patterns:
252
+ 1. Flat (sibling): path/to/figure.{json,csv,png}
253
+ 2. Organized (subdirs): path/to/{json,csv,png}/figure.{ext}
254
+
255
+ Parameters
256
+ ----------
257
+ path : Path
258
+ Input path (can be JSON, CSV, or PNG)
259
+
260
+ Returns
261
+ -------
262
+ tuple
263
+ (json_path, csv_path, png_path) - csv_path/png_path may be None if not found
264
+ """
265
+ path = Path(path)
266
+ stem = path.stem
267
+ parent = path.parent
268
+
269
+ # Check if this is organized pattern (parent is json/, csv/, png/)
270
+ if parent.name in ('json', 'csv', 'png'):
271
+ base_dir = parent.parent
272
+ json_path = base_dir / 'json' / f'{stem}.json'
273
+ csv_path = base_dir / 'csv' / f'{stem}.csv'
274
+ png_path = base_dir / 'png' / f'{stem}.png'
275
+ else:
276
+ # Flat pattern - sibling files
277
+ json_path = parent / f'{stem}.json'
278
+ csv_path = parent / f'{stem}.csv'
279
+ png_path = parent / f'{stem}.png'
280
+
281
+ # If input was .manual.json, get base json
282
+ if stem.endswith('.manual'):
283
+ base_stem = stem[:-7] # Remove '.manual'
284
+ if parent.name == 'json':
285
+ json_path = parent / f'{base_stem}.json'
286
+ csv_path = parent.parent / 'csv' / f'{base_stem}.csv'
287
+ png_path = parent.parent / 'png' / f'{base_stem}.png'
288
+ else:
289
+ json_path = parent / f'{base_stem}.json'
290
+ csv_path = parent / f'{base_stem}.csv'
291
+ png_path = parent / f'{base_stem}.png'
292
+
293
+ return (
294
+ json_path,
295
+ csv_path if csv_path.exists() else None,
296
+ png_path if png_path.exists() else None,
297
+ )
298
+
299
+
300
+ def _compute_file_hash(path: Path) -> str:
301
+ """Compute SHA256 hash of file contents."""
302
+ with open(path, 'rb') as f:
303
+ return hashlib.sha256(f.read()).hexdigest()
304
+
305
+
306
+ def save_manual_overrides(
307
+ json_path: Path,
308
+ overrides: dict,
309
+ ) -> Path:
310
+ """
311
+ Save manual overrides to .manual.json file.
312
+
313
+ Parameters
314
+ ----------
315
+ json_path : Path
316
+ Path to base JSON file
317
+ overrides : dict
318
+ Override settings (styles, annotations, etc.)
319
+
320
+ Returns
321
+ -------
322
+ Path
323
+ Path to saved manual.json file
324
+ """
325
+ import scitex as stx
326
+
327
+ manual_path = json_path.with_suffix('.manual.json')
328
+
329
+ # Compute hash of base JSON for staleness detection
330
+ base_hash = _compute_file_hash(json_path)
331
+
332
+ manual_data = {
333
+ 'base_file': json_path.name,
334
+ 'base_hash': base_hash,
335
+ 'overrides': overrides,
336
+ }
337
+
338
+ stx.io.save(manual_data, manual_path)
339
+ return manual_path
340
+
341
+
342
+ # EOF
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # File: ./src/scitex/vis/editor/_mpl_editor.py
4
+ """Minimal matplotlib-based figure editor."""
5
+
6
+ from pathlib import Path
7
+ from typing import Dict, Any, Optional
8
+ import copy
9
+
10
+
11
+ class MplEditor:
12
+ """
13
+ Minimal interactive figure editor using matplotlib's built-in interactivity.
14
+
15
+ Features:
16
+ - Basic figure display with navigation toolbar
17
+ - Text-based property editing via console
18
+ - Save to .manual.json
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ json_path: Path,
24
+ metadata: Dict[str, Any],
25
+ csv_data: Optional[Any] = None,
26
+ manual_overrides: Optional[Dict[str, Any]] = None,
27
+ ):
28
+ self.json_path = Path(json_path)
29
+ self.metadata = metadata
30
+ self.csv_data = csv_data
31
+ self.manual_overrides = manual_overrides or {}
32
+ self.current_overrides = copy.deepcopy(self.manual_overrides)
33
+
34
+ def run(self):
35
+ """Launch the matplotlib editor."""
36
+ import matplotlib
37
+ matplotlib.use('TkAgg') # Use interactive backend
38
+ import matplotlib.pyplot as plt
39
+ from matplotlib.widgets import Button, TextBox
40
+
41
+ # Create figure with extra space for controls
42
+ self.fig = plt.figure(figsize=(12, 8))
43
+
44
+ # Main axes for plot
45
+ self.ax = self.fig.add_axes([0.1, 0.25, 0.85, 0.65])
46
+
47
+ # Initial render
48
+ self._render()
49
+
50
+ # Add control buttons
51
+ self._add_controls()
52
+
53
+ # Show instructions
54
+ print("\n" + "="*50)
55
+ print("SciTeX Matplotlib Editor")
56
+ print("="*50)
57
+ print(f"Editing: {self.json_path.name}")
58
+ print("\nControls:")
59
+ print(" - Use navigation toolbar for zoom/pan")
60
+ print(" - Click buttons below figure for actions")
61
+ print(" - Close window when done")
62
+ print("="*50 + "\n")
63
+
64
+ plt.show()
65
+
66
+ def _render(self):
67
+ """Render the figure."""
68
+ self.ax.clear()
69
+
70
+ # Plot from CSV data
71
+ if self.csv_data is not None:
72
+ self._plot_from_csv()
73
+ else:
74
+ self.ax.text(0.5, 0.5, "No plot data available\n(CSV not found)",
75
+ ha='center', va='center', transform=self.ax.transAxes)
76
+
77
+ # Apply overrides
78
+ if self.current_overrides.get('title'):
79
+ self.ax.set_title(self.current_overrides['title'])
80
+ if self.current_overrides.get('xlabel'):
81
+ self.ax.set_xlabel(self.current_overrides['xlabel'])
82
+ if self.current_overrides.get('ylabel'):
83
+ self.ax.set_ylabel(self.current_overrides['ylabel'])
84
+ if self.current_overrides.get('grid'):
85
+ self.ax.grid(True)
86
+ if self.current_overrides.get('xlim'):
87
+ self.ax.set_xlim(self.current_overrides['xlim'])
88
+ if self.current_overrides.get('ylim'):
89
+ self.ax.set_ylim(self.current_overrides['ylim'])
90
+ if self.current_overrides.get('facecolor'):
91
+ self.ax.set_facecolor(self.current_overrides['facecolor'])
92
+
93
+ # Apply annotations
94
+ for annot in self.current_overrides.get('annotations', []):
95
+ if annot.get('type') == 'text':
96
+ self.ax.text(
97
+ annot.get('x', 0.5),
98
+ annot.get('y', 0.5),
99
+ annot.get('text', ''),
100
+ transform=self.ax.transAxes,
101
+ fontsize=annot.get('fontsize', 10),
102
+ )
103
+
104
+ self.fig.canvas.draw()
105
+
106
+ def _plot_from_csv(self):
107
+ """Reconstruct plot from CSV data."""
108
+ import pandas as pd
109
+
110
+ if isinstance(self.csv_data, pd.DataFrame):
111
+ df = self.csv_data
112
+ else:
113
+ return
114
+
115
+ cols = df.columns.tolist()
116
+ if len(cols) >= 2:
117
+ x_col = cols[0]
118
+ for y_col in cols[1:]:
119
+ try:
120
+ self.ax.plot(df[x_col], df[y_col], label=str(y_col))
121
+ except Exception:
122
+ pass
123
+ if len(cols) > 2:
124
+ self.ax.legend()
125
+ elif len(cols) == 1:
126
+ self.ax.plot(df[cols[0]])
127
+
128
+ def _add_controls(self):
129
+ """Add control buttons."""
130
+ from matplotlib.widgets import Button, TextBox
131
+
132
+ # Title text box
133
+ ax_title = self.fig.add_axes([0.15, 0.12, 0.3, 0.04])
134
+ self.title_box = TextBox(ax_title, 'Title:', initial=self.current_overrides.get('title', ''))
135
+ self.title_box.on_submit(self._on_title_change)
136
+
137
+ # Grid toggle button
138
+ ax_grid = self.fig.add_axes([0.55, 0.12, 0.1, 0.04])
139
+ self.grid_btn = Button(ax_grid, 'Toggle Grid')
140
+ self.grid_btn.on_clicked(self._toggle_grid)
141
+
142
+ # Save button
143
+ ax_save = self.fig.add_axes([0.7, 0.12, 0.1, 0.04])
144
+ self.save_btn = Button(ax_save, 'Save')
145
+ self.save_btn.on_clicked(self._save)
146
+
147
+ # Edit labels button
148
+ ax_labels = self.fig.add_axes([0.15, 0.05, 0.15, 0.04])
149
+ self.labels_btn = Button(ax_labels, 'Edit Labels')
150
+ self.labels_btn.on_clicked(self._edit_labels)
151
+
152
+ # Add annotation button
153
+ ax_annot = self.fig.add_axes([0.35, 0.05, 0.15, 0.04])
154
+ self.annot_btn = Button(ax_annot, 'Add Text')
155
+ self.annot_btn.on_clicked(self._add_annotation)
156
+
157
+ # Export PNG button
158
+ ax_export = self.fig.add_axes([0.55, 0.05, 0.12, 0.04])
159
+ self.export_btn = Button(ax_export, 'Export PNG')
160
+ self.export_btn.on_clicked(self._export_png)
161
+
162
+ def _on_title_change(self, text):
163
+ """Handle title change."""
164
+ self.current_overrides['title'] = text
165
+ self._render()
166
+
167
+ def _toggle_grid(self, event):
168
+ """Toggle grid visibility."""
169
+ self.current_overrides['grid'] = not self.current_overrides.get('grid', False)
170
+ self._render()
171
+
172
+ def _edit_labels(self, event):
173
+ """Edit axis labels via console."""
174
+ print("\n--- Edit Labels ---")
175
+ xlabel = input(f"X Label [{self.current_overrides.get('xlabel', '')}]: ").strip()
176
+ if xlabel:
177
+ self.current_overrides['xlabel'] = xlabel
178
+
179
+ ylabel = input(f"Y Label [{self.current_overrides.get('ylabel', '')}]: ").strip()
180
+ if ylabel:
181
+ self.current_overrides['ylabel'] = ylabel
182
+
183
+ self._render()
184
+ print("Labels updated!")
185
+
186
+ def _add_annotation(self, event):
187
+ """Add text annotation via console."""
188
+ print("\n--- Add Text Annotation ---")
189
+ text = input("Text: ").strip()
190
+ if not text:
191
+ return
192
+
193
+ try:
194
+ x = float(input("X position (0-1) [0.5]: ").strip() or "0.5")
195
+ y = float(input("Y position (0-1) [0.5]: ").strip() or "0.5")
196
+ except ValueError:
197
+ print("Invalid position, using defaults")
198
+ x, y = 0.5, 0.5
199
+
200
+ if 'annotations' not in self.current_overrides:
201
+ self.current_overrides['annotations'] = []
202
+
203
+ self.current_overrides['annotations'].append({
204
+ 'type': 'text',
205
+ 'text': text,
206
+ 'x': x,
207
+ 'y': y,
208
+ 'fontsize': 10,
209
+ })
210
+
211
+ self._render()
212
+ print("Annotation added!")
213
+
214
+ def _save(self, event):
215
+ """Save to .manual.json."""
216
+ from ._edit import save_manual_overrides
217
+
218
+ try:
219
+ manual_path = save_manual_overrides(self.json_path, self.current_overrides)
220
+ print(f"\nSaved: {manual_path}")
221
+ except Exception as e:
222
+ print(f"\nError saving: {e}")
223
+
224
+ def _export_png(self, event):
225
+ """Export current view to PNG."""
226
+ output_path = self.json_path.with_suffix('.edited.png')
227
+ self.fig.savefig(output_path, dpi=300, bbox_inches='tight')
228
+ print(f"\nExported: {output_path}")
229
+
230
+
231
+ # EOF