scitex 2.4.2__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scitex/__version__.py +1 -1
- scitex/browser/__init__.py +53 -0
- scitex/browser/debugging/__init__.py +56 -0
- scitex/browser/debugging/_failure_capture.py +372 -0
- scitex/browser/debugging/_sync_session.py +259 -0
- scitex/browser/debugging/_test_monitor.py +284 -0
- scitex/browser/debugging/_visual_cursor.py +432 -0
- scitex/io/_load.py +5 -0
- scitex/io/_load_modules/_canvas.py +171 -0
- scitex/io/_save.py +8 -0
- scitex/io/_save_modules/_canvas.py +356 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
- scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
- scitex/plt/utils/__init__.py +10 -0
- scitex/plt/utils/_collect_figure_metadata.py +14 -12
- scitex/plt/utils/_csv_column_naming.py +237 -0
- scitex/scholar/citation_graph/database.py +9 -2
- scitex/scholar/config/ScholarConfig.py +23 -3
- scitex/scholar/config/default.yaml +55 -0
- scitex/scholar/core/Paper.py +102 -0
- scitex/scholar/core/__init__.py +44 -0
- scitex/scholar/core/journal_normalizer.py +524 -0
- scitex/scholar/core/oa_cache.py +285 -0
- scitex/scholar/core/open_access.py +457 -0
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +137 -0
- scitex/scholar/pdf_download/strategies/__init__.py +6 -0
- scitex/scholar/pdf_download/strategies/open_access_download.py +186 -0
- scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +18 -3
- scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +15 -2
- scitex/session/_decorator.py +13 -1
- scitex/vis/README.md +246 -615
- scitex/vis/__init__.py +138 -78
- scitex/vis/canvas.py +423 -0
- scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
- scitex/vis/editor/__init__.py +1 -1
- scitex/vis/editor/_dearpygui_editor.py +1830 -0
- scitex/vis/editor/_defaults.py +40 -1
- scitex/vis/editor/_edit.py +54 -18
- scitex/vis/editor/_flask_editor.py +37 -0
- scitex/vis/editor/_qt_editor.py +865 -0
- scitex/vis/editor/flask_editor/__init__.py +21 -0
- scitex/vis/editor/flask_editor/bbox.py +216 -0
- scitex/vis/editor/flask_editor/core.py +152 -0
- scitex/vis/editor/flask_editor/plotter.py +130 -0
- scitex/vis/editor/flask_editor/renderer.py +184 -0
- scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
- scitex/vis/editor/flask_editor/templates/html.py +295 -0
- scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
- scitex/vis/editor/flask_editor/templates/styles.py +549 -0
- scitex/vis/editor/flask_editor/utils.py +81 -0
- scitex/vis/io/__init__.py +84 -21
- scitex/vis/io/canvas.py +226 -0
- scitex/vis/io/data.py +204 -0
- scitex/vis/io/directory.py +202 -0
- scitex/vis/io/export.py +460 -0
- scitex/vis/io/panel.py +424 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/RECORD +61 -32
- scitex/vis/DJANGO_INTEGRATION.md +0 -677
- scitex/vis/editor/_web_editor.py +0 -1440
- scitex/vis/tmp.txt +0 -239
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: 2025-12-08
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/io/_save_modules/_canvas.py
|
|
5
|
+
"""
|
|
6
|
+
Save canvas directory (.canvas) for scitex.vis.
|
|
7
|
+
|
|
8
|
+
Canvas directories are portable figure bundles containing:
|
|
9
|
+
- canvas.json: Layout, panels, composition settings
|
|
10
|
+
- panels/: Panel directories (scitex or image type)
|
|
11
|
+
- exports/: Composed outputs (PNG, PDF, SVG)
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
>>> import scitex as stx
|
|
15
|
+
>>> # Create canvas object
|
|
16
|
+
>>> canvas = stx.vis.Canvas(name="fig1_results")
|
|
17
|
+
>>> canvas.add_panel("panel_a", "plot.png", ...)
|
|
18
|
+
>>> # Save canvas to directory
|
|
19
|
+
>>> stx.io.save(canvas, "/path/to/fig1_results.canvas")
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Or save existing canvas directory to new location
|
|
22
|
+
>>> stx.io.save(canvas_json_dict, "/path/to/new_location.canvas")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import shutil
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Dict, Union
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def save_canvas(
|
|
32
|
+
obj: Any,
|
|
33
|
+
spath: Union[str, Path],
|
|
34
|
+
**kwargs,
|
|
35
|
+
) -> Path:
|
|
36
|
+
"""
|
|
37
|
+
Save a canvas object or dictionary to a .canvas directory.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
obj : Any
|
|
42
|
+
Canvas object or dictionary containing canvas data.
|
|
43
|
+
Can be:
|
|
44
|
+
- Dict with canvas.json structure
|
|
45
|
+
- Canvas object with to_dict() method
|
|
46
|
+
- Path to existing .canvas directory (for copy/move)
|
|
47
|
+
spath : str or Path
|
|
48
|
+
Path where the .canvas directory should be created.
|
|
49
|
+
Must end with .canvas extension.
|
|
50
|
+
**kwargs
|
|
51
|
+
Additional arguments (reserved for future use).
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
Path
|
|
56
|
+
Path to the created .canvas directory.
|
|
57
|
+
|
|
58
|
+
Raises
|
|
59
|
+
------
|
|
60
|
+
ValueError
|
|
61
|
+
If path doesn't end with .canvas extension.
|
|
62
|
+
"""
|
|
63
|
+
spath = Path(spath)
|
|
64
|
+
|
|
65
|
+
# Validate extension
|
|
66
|
+
if not str(spath).endswith(".canvas"):
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Canvas path must end with .canvas extension: {spath}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Handle different object types
|
|
72
|
+
if isinstance(obj, (str, Path)):
|
|
73
|
+
# Source is an existing canvas directory - copy it
|
|
74
|
+
_copy_canvas_directory(Path(obj), spath)
|
|
75
|
+
elif isinstance(obj, dict):
|
|
76
|
+
# Object is a canvas JSON dictionary
|
|
77
|
+
_save_canvas_from_dict(obj, spath)
|
|
78
|
+
elif hasattr(obj, "to_dict"):
|
|
79
|
+
# Object has to_dict method (Canvas object)
|
|
80
|
+
canvas_dict = obj.to_dict()
|
|
81
|
+
# Check if this Canvas was loaded from disk (has _canvas_dir)
|
|
82
|
+
if hasattr(obj, "_canvas_dir") and obj._canvas_dir:
|
|
83
|
+
canvas_dict["_canvas_dir"] = obj._canvas_dir
|
|
84
|
+
# Pass bundle option
|
|
85
|
+
if "bundle" in kwargs:
|
|
86
|
+
canvas_dict["_bundle"] = kwargs.pop("bundle")
|
|
87
|
+
_save_canvas_from_dict(canvas_dict, spath)
|
|
88
|
+
elif hasattr(obj, "_canvas_json"):
|
|
89
|
+
# Object has internal canvas JSON (Canvas object variant)
|
|
90
|
+
_save_canvas_from_dict(obj._canvas_json, spath)
|
|
91
|
+
else:
|
|
92
|
+
raise TypeError(
|
|
93
|
+
f"Cannot save object of type {type(obj).__name__} as canvas. "
|
|
94
|
+
"Expected dict, Canvas object, or path to existing canvas."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Export figures to exports/ directory
|
|
98
|
+
_export_canvas_figures(spath, **kwargs)
|
|
99
|
+
|
|
100
|
+
return spath
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _export_canvas_figures(
|
|
104
|
+
canvas_dir: Path,
|
|
105
|
+
formats: list = None,
|
|
106
|
+
dpi: int = 300,
|
|
107
|
+
**kwargs,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Export canvas figures directly to canvas directory.
|
|
111
|
+
|
|
112
|
+
Automatically exports PNG, PDF, and SVG formats.
|
|
113
|
+
"""
|
|
114
|
+
if formats is None:
|
|
115
|
+
formats = ["png", "pdf", "svg"]
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from scitex.vis.io.export import _compose_and_export
|
|
119
|
+
import json
|
|
120
|
+
|
|
121
|
+
# Load canvas.json
|
|
122
|
+
json_path = canvas_dir / "canvas.json"
|
|
123
|
+
if not json_path.exists():
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
with open(json_path, "r") as f:
|
|
127
|
+
canvas_json = json.load(f)
|
|
128
|
+
|
|
129
|
+
# Export directly to canvas directory (no exports/ subdirectory)
|
|
130
|
+
for fmt in formats:
|
|
131
|
+
output_path = canvas_dir / f"canvas.{fmt}"
|
|
132
|
+
_compose_and_export(
|
|
133
|
+
canvas_dir=canvas_dir,
|
|
134
|
+
canvas_json=canvas_json,
|
|
135
|
+
output_path=output_path,
|
|
136
|
+
output_format=fmt,
|
|
137
|
+
dpi=dpi,
|
|
138
|
+
transparent=False,
|
|
139
|
+
)
|
|
140
|
+
except ImportError:
|
|
141
|
+
# scitex.vis not available
|
|
142
|
+
pass
|
|
143
|
+
except Exception as e:
|
|
144
|
+
# Log but don't fail save if export fails
|
|
145
|
+
import sys
|
|
146
|
+
print(f"Warning: Canvas export failed: {e}", file=sys.stderr)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _copy_canvas_directory(source: Path, dest: Path) -> None:
|
|
150
|
+
"""Copy an existing canvas directory to a new location."""
|
|
151
|
+
if not source.exists():
|
|
152
|
+
raise FileNotFoundError(f"Source canvas directory not found: {source}")
|
|
153
|
+
|
|
154
|
+
if not (source / "canvas.json").exists():
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Invalid canvas directory (missing canvas.json): {source}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Remove destination if exists
|
|
160
|
+
if dest.exists():
|
|
161
|
+
shutil.rmtree(dest)
|
|
162
|
+
|
|
163
|
+
# Copy entire directory tree
|
|
164
|
+
shutil.copytree(source, dest)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _save_canvas_from_dict(canvas_dict: Dict[str, Any], dest: Path) -> None:
|
|
168
|
+
"""Create a canvas directory from a dictionary."""
|
|
169
|
+
import json
|
|
170
|
+
|
|
171
|
+
# Create directory structure (no exports/ - files go directly in canvas dir)
|
|
172
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
(dest / "panels").mkdir(exist_ok=True)
|
|
174
|
+
|
|
175
|
+
# Check if this dict was loaded from an existing canvas (has _canvas_dir)
|
|
176
|
+
source_canvas_dir = canvas_dict.get("_canvas_dir")
|
|
177
|
+
|
|
178
|
+
# Check if this dict has source files (from Canvas object)
|
|
179
|
+
source_files = canvas_dict.get("_source_files", {})
|
|
180
|
+
|
|
181
|
+
# Get bundle option
|
|
182
|
+
bundle = canvas_dict.get("_bundle", False)
|
|
183
|
+
|
|
184
|
+
# Create a clean copy of canvas_dict without internal keys for saving
|
|
185
|
+
save_dict = {k: v for k, v in canvas_dict.items() if not k.startswith("_")}
|
|
186
|
+
|
|
187
|
+
# Save canvas.json
|
|
188
|
+
json_path = dest / "canvas.json"
|
|
189
|
+
with open(json_path, "w") as f:
|
|
190
|
+
json.dump(save_dict, f, indent=2, default=str)
|
|
191
|
+
|
|
192
|
+
# If canvas_dict was loaded from an existing canvas, copy panel files
|
|
193
|
+
if source_canvas_dir:
|
|
194
|
+
_copy_panels_from_source(Path(source_canvas_dir), dest, canvas_dict)
|
|
195
|
+
|
|
196
|
+
# If canvas_dict has source files (from Canvas object), create panel dirs
|
|
197
|
+
if source_files:
|
|
198
|
+
_create_panels_from_source_files(source_files, dest, canvas_dict, bundle=bundle)
|
|
199
|
+
|
|
200
|
+
# If canvas_dict contains embedded panel data, extract it
|
|
201
|
+
_extract_embedded_panels(canvas_dict, dest)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _copy_panels_from_source(
|
|
205
|
+
source_canvas_dir: Path,
|
|
206
|
+
dest: Path,
|
|
207
|
+
canvas_dict: Dict[str, Any],
|
|
208
|
+
) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Copy panel files from source canvas directory to destination.
|
|
211
|
+
|
|
212
|
+
When a canvas dict was loaded from an existing canvas directory,
|
|
213
|
+
this function copies the panel files to the new location.
|
|
214
|
+
Skips copying if source and destination are the same.
|
|
215
|
+
"""
|
|
216
|
+
source_panels_dir = source_canvas_dir / "panels"
|
|
217
|
+
dest_panels_dir = dest / "panels"
|
|
218
|
+
|
|
219
|
+
if not source_panels_dir.exists():
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# Skip if source and dest are the same (saving back to same location)
|
|
223
|
+
try:
|
|
224
|
+
if source_canvas_dir.resolve() == dest.resolve():
|
|
225
|
+
return
|
|
226
|
+
except (OSError, ValueError):
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
for panel in canvas_dict.get("panels", []):
|
|
230
|
+
panel_name = panel.get("name", "")
|
|
231
|
+
if not panel_name:
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
source_panel_dir = source_panels_dir / panel_name
|
|
235
|
+
dest_panel_dir = dest_panels_dir / panel_name
|
|
236
|
+
|
|
237
|
+
if source_panel_dir.exists() and source_panel_dir.is_dir():
|
|
238
|
+
# Copy entire panel directory (follow symlinks to get actual content)
|
|
239
|
+
if dest_panel_dir.exists():
|
|
240
|
+
shutil.rmtree(dest_panel_dir)
|
|
241
|
+
shutil.copytree(source_panel_dir, dest_panel_dir, symlinks=False)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _create_panels_from_source_files(
|
|
245
|
+
source_files: Dict[str, str],
|
|
246
|
+
dest: Path,
|
|
247
|
+
canvas_dict: Dict[str, Any],
|
|
248
|
+
bundle: bool = False,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Create panel directories from source files.
|
|
252
|
+
|
|
253
|
+
When a Canvas object is saved, this creates the panel directories
|
|
254
|
+
with symlinks (default) or copies of the source files.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
source_files : Dict[str, str]
|
|
259
|
+
Mapping of panel_name -> source_file_path
|
|
260
|
+
dest : Path
|
|
261
|
+
Destination canvas directory
|
|
262
|
+
canvas_dict : Dict[str, Any]
|
|
263
|
+
Canvas dictionary (to get panel types)
|
|
264
|
+
bundle : bool
|
|
265
|
+
If True, copy files. If False (default), create symlinks.
|
|
266
|
+
"""
|
|
267
|
+
dest_panels_dir = dest / "panels"
|
|
268
|
+
|
|
269
|
+
for panel in canvas_dict.get("panels", []):
|
|
270
|
+
panel_name = panel.get("name", "")
|
|
271
|
+
if not panel_name or panel_name not in source_files:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
source_path = Path(source_files[panel_name])
|
|
275
|
+
if not source_path.exists():
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
panel_type = panel.get("type", "image")
|
|
279
|
+
panel_dir = dest_panels_dir / panel_name
|
|
280
|
+
panel_dir.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
|
|
282
|
+
# Symlink or copy panel files
|
|
283
|
+
if panel_type == "scitex":
|
|
284
|
+
# Scitex panel: PNG, JSON, CSV
|
|
285
|
+
_link_or_copy(source_path, panel_dir / "panel.png", bundle)
|
|
286
|
+
json_sibling = source_path.parent / f"{source_path.stem}.json"
|
|
287
|
+
if json_sibling.exists():
|
|
288
|
+
_link_or_copy(json_sibling, panel_dir / "panel.json", bundle)
|
|
289
|
+
csv_sibling = source_path.parent / f"{source_path.stem}.csv"
|
|
290
|
+
if csv_sibling.exists():
|
|
291
|
+
_link_or_copy(csv_sibling, panel_dir / "panel.csv", bundle)
|
|
292
|
+
else:
|
|
293
|
+
# Image panel: just the image
|
|
294
|
+
dest_name = f"panel{source_path.suffix}"
|
|
295
|
+
_link_or_copy(source_path, panel_dir / dest_name, bundle)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _link_or_copy(source: Path, dest: Path, bundle: bool = False) -> None:
|
|
299
|
+
"""Create relative symlink or copy file based on bundle flag."""
|
|
300
|
+
if dest.exists() or dest.is_symlink():
|
|
301
|
+
dest.unlink()
|
|
302
|
+
|
|
303
|
+
if bundle:
|
|
304
|
+
shutil.copy2(source, dest)
|
|
305
|
+
else:
|
|
306
|
+
try:
|
|
307
|
+
# Use relative symlink for portability
|
|
308
|
+
import os
|
|
309
|
+
rel_path = os.path.relpath(source.resolve(), dest.parent.resolve())
|
|
310
|
+
dest.symlink_to(rel_path)
|
|
311
|
+
except (OSError, ValueError):
|
|
312
|
+
# Fallback to copy if symlink fails
|
|
313
|
+
shutil.copy2(source, dest)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _extract_embedded_panels(canvas_dict: Dict[str, Any], dest: Path) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Extract embedded panel data from canvas dictionary.
|
|
319
|
+
|
|
320
|
+
Some Canvas objects may embed panel image data (base64) in the dict.
|
|
321
|
+
This function extracts them to the panels/ directory.
|
|
322
|
+
"""
|
|
323
|
+
import base64
|
|
324
|
+
|
|
325
|
+
for panel in canvas_dict.get("panels", []):
|
|
326
|
+
panel_name = panel.get("name", "")
|
|
327
|
+
if not panel_name:
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
panel_dir = dest / "panels" / panel_name
|
|
331
|
+
panel_dir.mkdir(parents=True, exist_ok=True)
|
|
332
|
+
|
|
333
|
+
# Check for embedded image data
|
|
334
|
+
if "image_data" in panel:
|
|
335
|
+
# Decode base64 image data
|
|
336
|
+
img_data = base64.b64decode(panel["image_data"])
|
|
337
|
+
img_ext = panel.get("image_ext", "png")
|
|
338
|
+
img_path = panel_dir / f"panel.{img_ext}"
|
|
339
|
+
with open(img_path, "wb") as f:
|
|
340
|
+
f.write(img_data)
|
|
341
|
+
|
|
342
|
+
# Check for embedded JSON data (scitex type panels)
|
|
343
|
+
if "panel_json" in panel:
|
|
344
|
+
import json
|
|
345
|
+
json_path = panel_dir / "panel.json"
|
|
346
|
+
with open(json_path, "w") as f:
|
|
347
|
+
json.dump(panel["panel_json"], f, indent=2, default=str)
|
|
348
|
+
|
|
349
|
+
# Check for embedded CSV data (scitex type panels)
|
|
350
|
+
if "panel_csv" in panel:
|
|
351
|
+
csv_path = panel_dir / "panel.csv"
|
|
352
|
+
with open(csv_path, "w") as f:
|
|
353
|
+
f.write(panel["panel_csv"])
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# EOF
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: "2025-12-
|
|
3
|
+
# Timestamp: "2025-12-08 18:45:00 (ywatanabe)"
|
|
4
4
|
# File: ./src/scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py
|
|
5
5
|
|
|
6
6
|
"""CSV formatter for matplotlib plot() calls."""
|
|
@@ -12,6 +12,48 @@ import numpy as np
|
|
|
12
12
|
import pandas as pd
|
|
13
13
|
import xarray as xr
|
|
14
14
|
|
|
15
|
+
from scitex.plt.utils._csv_column_naming import get_csv_column_name
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_tracking_id(id: str) -> tuple:
|
|
19
|
+
"""Parse tracking ID to extract axes position and trace index.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
id : str
|
|
24
|
+
Tracking ID like "ax_00_plot_0" or "plot_0"
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
tuple
|
|
29
|
+
(ax_row, ax_col, trace_index)
|
|
30
|
+
"""
|
|
31
|
+
ax_row, ax_col, trace_index = 0, 0, 0
|
|
32
|
+
|
|
33
|
+
if id.startswith('ax_'):
|
|
34
|
+
parts = id.split('_')
|
|
35
|
+
if len(parts) >= 2:
|
|
36
|
+
ax_pos = parts[1]
|
|
37
|
+
if len(ax_pos) >= 2:
|
|
38
|
+
try:
|
|
39
|
+
ax_row = int(ax_pos[0])
|
|
40
|
+
ax_col = int(ax_pos[1])
|
|
41
|
+
except ValueError:
|
|
42
|
+
pass
|
|
43
|
+
# Extract trace index from the rest (e.g., "plot_0" -> 0)
|
|
44
|
+
if len(parts) >= 4 and parts[2] == 'plot':
|
|
45
|
+
try:
|
|
46
|
+
trace_index = int(parts[3])
|
|
47
|
+
except ValueError:
|
|
48
|
+
pass
|
|
49
|
+
elif id.startswith('plot_'):
|
|
50
|
+
try:
|
|
51
|
+
trace_index = int(id.split('_')[1])
|
|
52
|
+
except (ValueError, IndexError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
return ax_row, ax_col, trace_index
|
|
56
|
+
|
|
15
57
|
|
|
16
58
|
def _format_plot(
|
|
17
59
|
id: str,
|
|
@@ -30,7 +72,7 @@ def _format_plot(
|
|
|
30
72
|
Parameters
|
|
31
73
|
----------
|
|
32
74
|
id : str
|
|
33
|
-
Identifier prefix for the output columns (e.g., "
|
|
75
|
+
Identifier prefix for the output columns (e.g., "ax_00_plot_0").
|
|
34
76
|
tracked_dict : dict or None
|
|
35
77
|
Dictionary containing tracked data. May include:
|
|
36
78
|
- 'plot_df': Pre-formatted DataFrame from wrapper
|
|
@@ -41,25 +83,40 @@ def _format_plot(
|
|
|
41
83
|
Returns
|
|
42
84
|
-------
|
|
43
85
|
pd.DataFrame
|
|
44
|
-
Formatted data with columns
|
|
45
|
-
For 1D data:
|
|
46
|
-
For 2D data: {id}_plot_x00, {id}_plot_y00, {id}_plot_x01, ...
|
|
86
|
+
Formatted data with columns using single source of truth naming.
|
|
87
|
+
For 1D data: ax_00_plot_0_plot_x, ax_00_plot_0_plot_y
|
|
47
88
|
"""
|
|
48
89
|
# Check if tracked_dict is empty or not a dictionary
|
|
49
90
|
if not tracked_dict or not isinstance(tracked_dict, dict):
|
|
50
91
|
return pd.DataFrame()
|
|
51
92
|
|
|
93
|
+
# Parse the tracking ID to get axes position and trace index
|
|
94
|
+
ax_row, ax_col, trace_index = _parse_tracking_id(id)
|
|
95
|
+
|
|
52
96
|
# For stx_line, we expect a 'plot_df' key
|
|
53
97
|
if 'plot_df' in tracked_dict:
|
|
54
98
|
plot_df = tracked_dict['plot_df']
|
|
55
99
|
if isinstance(plot_df, pd.DataFrame):
|
|
56
|
-
#
|
|
57
|
-
|
|
100
|
+
# Rename columns using single source of truth
|
|
101
|
+
renamed = {}
|
|
102
|
+
for col in plot_df.columns:
|
|
103
|
+
if col == 'plot_x':
|
|
104
|
+
renamed[col] = get_csv_column_name('plot_x', ax_row, ax_col, trace_index=trace_index)
|
|
105
|
+
elif col == 'plot_y':
|
|
106
|
+
renamed[col] = get_csv_column_name('plot_y', ax_row, ax_col, trace_index=trace_index)
|
|
107
|
+
else:
|
|
108
|
+
# For other columns, just prefix with id
|
|
109
|
+
renamed[col] = f"{id}_{col}"
|
|
110
|
+
return plot_df.rename(columns=renamed)
|
|
58
111
|
|
|
59
112
|
# Handle raw args from __getattr__ proxied calls
|
|
60
113
|
if 'args' in tracked_dict:
|
|
61
114
|
args = tracked_dict['args']
|
|
62
115
|
if isinstance(args, tuple) and len(args) > 0:
|
|
116
|
+
# Get column names from single source of truth
|
|
117
|
+
x_col = get_csv_column_name('plot_x', ax_row, ax_col, trace_index=trace_index)
|
|
118
|
+
y_col = get_csv_column_name('plot_y', ax_row, ax_col, trace_index=trace_index)
|
|
119
|
+
|
|
63
120
|
# Handle single argument: plot(y) or plot(data_2d)
|
|
64
121
|
if len(args) == 1:
|
|
65
122
|
args_value = args[0]
|
|
@@ -72,14 +129,14 @@ def _format_plot(
|
|
|
72
129
|
# 2D array: extract x and y columns
|
|
73
130
|
if hasattr(args_value, 'ndim') and args_value.ndim == 2:
|
|
74
131
|
x, y = args_value[:, 0], args_value[:, 1]
|
|
75
|
-
df = pd.DataFrame({
|
|
132
|
+
df = pd.DataFrame({x_col: x, y_col: y})
|
|
76
133
|
return df
|
|
77
134
|
|
|
78
135
|
# 1D array: generate x from indices (common case: plot(y))
|
|
79
136
|
elif hasattr(args_value, 'ndim') and args_value.ndim == 1:
|
|
80
137
|
x = np.arange(len(args_value))
|
|
81
138
|
y = args_value
|
|
82
|
-
df = pd.DataFrame({
|
|
139
|
+
df = pd.DataFrame({x_col: x, y_col: y})
|
|
83
140
|
return df
|
|
84
141
|
|
|
85
142
|
# Handle two arguments: plot(x, y)
|
|
@@ -94,22 +151,20 @@ def _format_plot(
|
|
|
94
151
|
if hasattr(y, 'ndim') and y.ndim == 2:
|
|
95
152
|
out = OrderedDict()
|
|
96
153
|
for ii in range(y.shape[1]):
|
|
97
|
-
|
|
98
|
-
|
|
154
|
+
x_col_i = get_csv_column_name(f'plot_x{ii:02d}', ax_row, ax_col, trace_index=trace_index)
|
|
155
|
+
y_col_i = get_csv_column_name(f'plot_y{ii:02d}', ax_row, ax_col, trace_index=trace_index)
|
|
156
|
+
out[x_col_i] = x
|
|
157
|
+
out[y_col_i] = y[:, ii]
|
|
99
158
|
df = pd.DataFrame(out)
|
|
100
159
|
return df
|
|
101
160
|
|
|
102
161
|
# Handle DataFrame y
|
|
103
162
|
if isinstance(y_arg, pd.DataFrame):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
for ii, col in enumerate(y_arg.columns)
|
|
110
|
-
},
|
|
111
|
-
}
|
|
112
|
-
)
|
|
163
|
+
result = {x_col: x}
|
|
164
|
+
for ii, col in enumerate(y_arg.columns):
|
|
165
|
+
y_col_i = get_csv_column_name(f'plot_y{ii:02d}', ax_row, ax_col, trace_index=trace_index)
|
|
166
|
+
result[y_col_i] = np.array(y_arg[col])
|
|
167
|
+
df = pd.DataFrame(result)
|
|
113
168
|
return df
|
|
114
169
|
|
|
115
170
|
# Handle 1D arrays (most common case: plot(x, y))
|
|
@@ -117,11 +172,11 @@ def _format_plot(
|
|
|
117
172
|
# Flatten x if needed
|
|
118
173
|
x_flat = np.ravel(x)
|
|
119
174
|
y_flat = np.ravel(y)
|
|
120
|
-
df = pd.DataFrame({
|
|
175
|
+
df = pd.DataFrame({x_col: x_flat, y_col: y_flat})
|
|
121
176
|
return df
|
|
122
177
|
|
|
123
178
|
# Fallback for list-like y
|
|
124
|
-
df = pd.DataFrame({
|
|
179
|
+
df = pd.DataFrame({x_col: np.ravel(x), y_col: np.ravel(y)})
|
|
125
180
|
return df
|
|
126
181
|
|
|
127
182
|
# Default empty DataFrame if we can't process the input
|