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.
Files changed (64) hide show
  1. scitex/__version__.py +1 -1
  2. scitex/browser/__init__.py +53 -0
  3. scitex/browser/debugging/__init__.py +56 -0
  4. scitex/browser/debugging/_failure_capture.py +372 -0
  5. scitex/browser/debugging/_sync_session.py +259 -0
  6. scitex/browser/debugging/_test_monitor.py +284 -0
  7. scitex/browser/debugging/_visual_cursor.py +432 -0
  8. scitex/io/_load.py +5 -0
  9. scitex/io/_load_modules/_canvas.py +171 -0
  10. scitex/io/_save.py +8 -0
  11. scitex/io/_save_modules/_canvas.py +356 -0
  12. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
  13. scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
  14. scitex/plt/utils/__init__.py +10 -0
  15. scitex/plt/utils/_collect_figure_metadata.py +14 -12
  16. scitex/plt/utils/_csv_column_naming.py +237 -0
  17. scitex/scholar/citation_graph/database.py +9 -2
  18. scitex/scholar/config/ScholarConfig.py +23 -3
  19. scitex/scholar/config/default.yaml +55 -0
  20. scitex/scholar/core/Paper.py +102 -0
  21. scitex/scholar/core/__init__.py +44 -0
  22. scitex/scholar/core/journal_normalizer.py +524 -0
  23. scitex/scholar/core/oa_cache.py +285 -0
  24. scitex/scholar/core/open_access.py +457 -0
  25. scitex/scholar/pdf_download/ScholarPDFDownloader.py +137 -0
  26. scitex/scholar/pdf_download/strategies/__init__.py +6 -0
  27. scitex/scholar/pdf_download/strategies/open_access_download.py +186 -0
  28. scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +18 -3
  29. scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +15 -2
  30. scitex/session/_decorator.py +13 -1
  31. scitex/vis/README.md +246 -615
  32. scitex/vis/__init__.py +138 -78
  33. scitex/vis/canvas.py +423 -0
  34. scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
  35. scitex/vis/editor/__init__.py +1 -1
  36. scitex/vis/editor/_dearpygui_editor.py +1830 -0
  37. scitex/vis/editor/_defaults.py +40 -1
  38. scitex/vis/editor/_edit.py +54 -18
  39. scitex/vis/editor/_flask_editor.py +37 -0
  40. scitex/vis/editor/_qt_editor.py +865 -0
  41. scitex/vis/editor/flask_editor/__init__.py +21 -0
  42. scitex/vis/editor/flask_editor/bbox.py +216 -0
  43. scitex/vis/editor/flask_editor/core.py +152 -0
  44. scitex/vis/editor/flask_editor/plotter.py +130 -0
  45. scitex/vis/editor/flask_editor/renderer.py +184 -0
  46. scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
  47. scitex/vis/editor/flask_editor/templates/html.py +295 -0
  48. scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
  49. scitex/vis/editor/flask_editor/templates/styles.py +549 -0
  50. scitex/vis/editor/flask_editor/utils.py +81 -0
  51. scitex/vis/io/__init__.py +84 -21
  52. scitex/vis/io/canvas.py +226 -0
  53. scitex/vis/io/data.py +204 -0
  54. scitex/vis/io/directory.py +202 -0
  55. scitex/vis/io/export.py +460 -0
  56. scitex/vis/io/panel.py +424 -0
  57. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
  58. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/RECORD +61 -32
  59. scitex/vis/DJANGO_INTEGRATION.md +0 -677
  60. scitex/vis/editor/_web_editor.py +0 -1440
  61. scitex/vis/tmp.txt +0 -239
  62. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
  63. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
  64. {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-01 13:30:00 (ywatanabe)"
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., "ax_00").
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 prefixed by id.
45
- For 1D data: {id}_plot_x, {id}_plot_y
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
- # Add the id prefix to all columns
57
- return plot_df.add_prefix(f"{id}_")
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({f"{id}_plot_x": x, f"{id}_plot_y": y})
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({f"{id}_plot_x": x, f"{id}_plot_y": y})
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
- out[f"{id}_plot_x{ii:02d}"] = x
98
- out[f"{id}_plot_y{ii:02d}"] = y[:, ii]
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
- df = pd.DataFrame(
105
- {
106
- f"{id}_plot_x": x,
107
- **{
108
- f"{id}_plot_y{ii:02d}": np.array(y_arg[col])
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({f"{id}_plot_x": x_flat, f"{id}_plot_y": y_flat})
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({f"{id}_plot_x": np.ravel(x), f"{id}_plot_y": np.ravel(y)})
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