scitex 2.4.3__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 (45) hide show
  1. scitex/__version__.py +1 -1
  2. scitex/io/_load.py +5 -0
  3. scitex/io/_load_modules/_canvas.py +171 -0
  4. scitex/io/_save.py +8 -0
  5. scitex/io/_save_modules/_canvas.py +356 -0
  6. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
  7. scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
  8. scitex/plt/utils/__init__.py +10 -0
  9. scitex/plt/utils/_collect_figure_metadata.py +14 -12
  10. scitex/plt/utils/_csv_column_naming.py +237 -0
  11. scitex/session/_decorator.py +13 -1
  12. scitex/vis/README.md +246 -615
  13. scitex/vis/__init__.py +138 -78
  14. scitex/vis/canvas.py +423 -0
  15. scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
  16. scitex/vis/editor/__init__.py +1 -1
  17. scitex/vis/editor/_dearpygui_editor.py +1830 -0
  18. scitex/vis/editor/_defaults.py +40 -1
  19. scitex/vis/editor/_edit.py +54 -18
  20. scitex/vis/editor/_flask_editor.py +37 -0
  21. scitex/vis/editor/_qt_editor.py +865 -0
  22. scitex/vis/editor/flask_editor/__init__.py +21 -0
  23. scitex/vis/editor/flask_editor/bbox.py +216 -0
  24. scitex/vis/editor/flask_editor/core.py +152 -0
  25. scitex/vis/editor/flask_editor/plotter.py +130 -0
  26. scitex/vis/editor/flask_editor/renderer.py +184 -0
  27. scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
  28. scitex/vis/editor/flask_editor/templates/html.py +295 -0
  29. scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
  30. scitex/vis/editor/flask_editor/templates/styles.py +549 -0
  31. scitex/vis/editor/flask_editor/utils.py +81 -0
  32. scitex/vis/io/__init__.py +84 -21
  33. scitex/vis/io/canvas.py +226 -0
  34. scitex/vis/io/data.py +204 -0
  35. scitex/vis/io/directory.py +202 -0
  36. scitex/vis/io/export.py +460 -0
  37. scitex/vis/io/panel.py +424 -0
  38. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
  39. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/RECORD +42 -21
  40. scitex/vis/DJANGO_INTEGRATION.md +0 -677
  41. scitex/vis/editor/_web_editor.py +0 -1440
  42. scitex/vis/tmp.txt +0 -239
  43. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
  44. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
  45. {scitex-2.4.3.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
scitex/vis/__init__.py CHANGED
@@ -1,117 +1,177 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # Timestamp: 2025-12-08
3
4
  # File: ./src/scitex/vis/__init__.py
4
5
  """
5
6
  SciTeX Visualization Module (scitex.vis)
6
7
 
7
- A structured approach to creating publication-quality figures through JSON specifications.
8
- This module completes the SciTeX ecosystem as the third pillar alongside scholar and writer.
8
+ Canvas-based composition of publication-quality figures.
9
9
 
10
- Architecture:
11
- - model: JSON data models (FigureModel, AxesModel, PlotModel, etc.)
12
- - backend: JSON matplotlib rendering engine
13
- - io: Load/save figure JSON with project structure support
14
- - utils: Validation and publication format templates
10
+ Terminology:
11
+ - Canvas: A paper figure workspace (e.g., "Figure 1" in publication)
12
+ - Panel: A single component on canvas (stx.plt output or image)
13
+ - Figure: Reserved for matplotlib's fig object (see scitex.plt)
15
14
 
16
15
  Quick Start:
17
16
  -----------
18
17
  >>> import scitex as stx
19
18
  >>>
20
- >>> # Using a template
21
- >>> fig_json = stx.vis.utils.get_template("nature_single", height_mm=100)
22
- >>> fig_json["axes"] = [...] # Add axes configurations
19
+ >>> # Create canvas and add panels
20
+ >>> stx.vis.create_canvas("/output", "fig1")
21
+ >>> stx.vis.add_panel("/output", "fig1", "panel_a", source="plot.png",
22
+ ... position=(10, 10), size=(80, 60), label="A")
23
23
  >>>
24
- >>> # Save the spec
25
- >>> stx.vis.io.save_figure_json(fig_json, "figure.json")
26
- >>>
27
- >>> # Render to matplotlib
28
- >>> fig, axes = stx.vis.backend.build_figure_from_json(fig_json)
29
- >>>
30
- >>> # Export to image
31
- >>> stx.vis.backend.export_figure(fig_json, "figure.png", dpi=300)
32
-
33
- With Project Structure:
34
- ----------------------
35
- >>> # Save to project
36
- >>> stx.vis.io.save_figure_json_to_project(
37
- ... project_dir="/path/to/project",
38
- ... figure_id="fig-001",
39
- ... fig_json=fig_json
40
- ... )
41
- >>>
42
- >>> # Load from project
43
- >>> loaded = stx.vis.io.load_figure_json_from_project(
44
- ... project_dir="/path/to/project",
45
- ... figure_id="fig-001"
46
- ... )
47
- >>>
48
- >>> # Export from project
49
- >>> stx.vis.backend.export_figure(loaded, "output/fig-001.png")
24
+ >>> # Save with stx.io (auto-exports PNG/PDF/SVG)
25
+ >>> canvas = stx.io.load("/output/fig1.canvas")
26
+ >>> stx.io.save(canvas, "/output/fig1_copy.canvas")
27
+
28
+ Directory Structure:
29
+ -------------------
30
+ {parent_dir}/{canvas_name}.canvas/
31
+ ├── canvas.json # Layout, panels, composition
32
+ ├── panels/ # Panel directories
33
+ └── exports/ # canvas.png, canvas.pdf, canvas.svg
50
34
  """
51
35
 
52
- # Import submodules
36
+ # Submodules for advanced use
37
+ from . import io
53
38
  from . import model
54
39
  from . import backend
55
- from . import io
56
40
  from . import utils
57
41
  from . import editor
58
42
 
59
- # Convenient top-level imports for common use cases
60
- from .model import FigureModel, AxesModel, PlotModel, GuideModel, AnnotationModel
43
+ # Canvas class
44
+ from .canvas import Canvas
61
45
 
62
- from .backend import (
63
- build_figure_from_json,
64
- export_figure,
65
- export_figure_from_file,
66
- export_multiple_formats,
67
- )
46
+ # =============================================================================
47
+ # Primary API (minimal, reusable, flexible)
48
+ # =============================================================================
68
49
 
50
+ # Canvas operations
69
51
  from .io import (
70
- load_figure_json,
71
- save_figure_json,
72
- load_figure_json_from_project,
73
- save_figure_json_to_project,
52
+ ensure_canvas_directory as create_canvas,
53
+ get_canvas_directory_path as get_canvas_path,
54
+ canvas_directory_exists as canvas_exists,
55
+ list_canvas_directories as list_canvases,
56
+ delete_canvas_directory as delete_canvas,
74
57
  )
75
58
 
76
- from .utils import (
77
- get_template,
78
- list_templates,
79
- NATURE_SINGLE_COLUMN_MM,
80
- NATURE_DOUBLE_COLUMN_MM,
59
+ # Panel operations
60
+ from .io import (
61
+ add_panel_from_scitex,
62
+ add_panel_from_image,
63
+ update_panel,
64
+ remove_panel,
65
+ list_panels,
81
66
  )
82
67
 
68
+ # Export (usually handled by stx.io.save, but available for explicit use)
69
+ from .io import export_canvas_to_file as export_canvas
70
+
71
+ # Data integrity
72
+ from .io import verify_all_data_hashes as verify_data
73
+
74
+ # Editor
83
75
  from .editor import edit
84
76
 
77
+
78
+ # =============================================================================
79
+ # Convenience wrapper for add_panel
80
+ # =============================================================================
81
+ def add_panel(
82
+ parent_dir,
83
+ canvas_name,
84
+ panel_name,
85
+ source,
86
+ position=(0, 0),
87
+ size=(50, 50),
88
+ label="",
89
+ bundle=False,
90
+ **kwargs,
91
+ ):
92
+ """
93
+ Add a panel to canvas (auto-detects scitex vs image type).
94
+
95
+ Parameters
96
+ ----------
97
+ parent_dir : str or Path
98
+ Parent directory containing canvas
99
+ canvas_name : str
100
+ Canvas name
101
+ panel_name : str
102
+ Name for the panel
103
+ source : str or Path
104
+ Source file (PNG, JPG, SVG)
105
+ position : tuple
106
+ (x_mm, y_mm) position on canvas
107
+ size : tuple
108
+ (width_mm, height_mm) panel size
109
+ label : str
110
+ Panel label (A, B, C...)
111
+ bundle : bool
112
+ If True, copy files. If False (default), use symlinks.
113
+ **kwargs
114
+ Additional panel properties (rotation_deg, opacity, flip_h, etc.)
115
+ """
116
+ from pathlib import Path
117
+
118
+ source = Path(source)
119
+ panel_properties = {
120
+ "position": {"x_mm": position[0], "y_mm": position[1]},
121
+ "size": {"width_mm": size[0], "height_mm": size[1]},
122
+ **kwargs,
123
+ }
124
+ if label:
125
+ panel_properties["label"] = {"text": label, "position": "top-left"}
126
+
127
+ # Check if scitex output (has .json/.csv siblings)
128
+ json_sibling = source.parent / f"{source.stem}.json"
129
+ if json_sibling.exists():
130
+ return add_panel_from_scitex(
131
+ project_dir=parent_dir,
132
+ canvas_name=canvas_name,
133
+ panel_name=panel_name,
134
+ source_png=source,
135
+ panel_properties=panel_properties,
136
+ bundle=bundle,
137
+ )
138
+ else:
139
+ return add_panel_from_image(
140
+ project_dir=parent_dir,
141
+ canvas_name=canvas_name,
142
+ panel_name=panel_name,
143
+ source_image=source,
144
+ panel_properties=panel_properties,
145
+ bundle=bundle,
146
+ )
147
+
148
+
85
149
  __all__ = [
86
- # Submodules
150
+ # Canvas class
151
+ "Canvas",
152
+ # Submodules (advanced)
153
+ "io",
87
154
  "model",
88
155
  "backend",
89
- "io",
90
156
  "utils",
91
157
  "editor",
158
+ # Canvas operations
159
+ "create_canvas",
160
+ "get_canvas_path",
161
+ "canvas_exists",
162
+ "list_canvases",
163
+ "delete_canvas",
164
+ # Panel operations
165
+ "add_panel",
166
+ "update_panel",
167
+ "remove_panel",
168
+ "list_panels",
169
+ # Export
170
+ "export_canvas",
171
+ # Data integrity
172
+ "verify_data",
92
173
  # Editor
93
174
  "edit",
94
- # Models
95
- "FigureModel",
96
- "AxesModel",
97
- "PlotModel",
98
- "GuideModel",
99
- "AnnotationModel",
100
- # Backend
101
- "build_figure_from_json",
102
- "export_figure",
103
- "export_figure_from_file",
104
- "export_multiple_formats",
105
- # I/O
106
- "load_figure_json",
107
- "save_figure_json",
108
- "load_figure_json_from_project",
109
- "save_figure_json_to_project",
110
- # Utils
111
- "get_template",
112
- "list_templates",
113
- "NATURE_SINGLE_COLUMN_MM",
114
- "NATURE_DOUBLE_COLUMN_MM",
115
175
  ]
116
176
 
117
177
  # EOF
scitex/vis/canvas.py ADDED
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: 2025-12-08
4
+ # File: ./src/scitex/vis/canvas.py
5
+ """
6
+ Canvas class for scitex.vis.
7
+
8
+ Provides object-oriented interface to canvas operations.
9
+ """
10
+
11
+ from pathlib import Path
12
+ from typing import Dict, Any, Union, List, Optional
13
+ from datetime import datetime
14
+
15
+
16
+ class Canvas:
17
+ """
18
+ Canvas for composing publication-quality figures.
19
+
20
+ A Canvas represents a paper figure workspace containing multiple panels.
21
+ It can be saved to a .canvas directory bundle for portability.
22
+
23
+ Parameters
24
+ ----------
25
+ name : str
26
+ Canvas name (e.g., "fig1_neural_results")
27
+ width_mm : float
28
+ Canvas width in millimeters (default: 180)
29
+ height_mm : float
30
+ Canvas height in millimeters (default: 240)
31
+
32
+ Examples
33
+ --------
34
+ >>> import scitex as stx
35
+ >>> # Create canvas
36
+ >>> canvas = stx.vis.Canvas("fig1", width_mm=180, height_mm=120)
37
+ >>> # Add panels
38
+ >>> canvas.add_panel("panel_a", "plot.png", position=(10, 10), size=(80, 50), label="A")
39
+ >>> canvas.add_panel("panel_b", "chart.png", position=(100, 10), size=(80, 50), label="B")
40
+ >>> # Save (auto-exports PNG/PDF/SVG)
41
+ >>> stx.io.save(canvas, "/output/fig1.canvas")
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ name: str,
47
+ width_mm: float = 180,
48
+ height_mm: float = 240,
49
+ ):
50
+ self._name = name
51
+ self._width_mm = width_mm
52
+ self._height_mm = height_mm
53
+ self._panels: List[Dict[str, Any]] = []
54
+ self._annotations: List[Dict[str, Any]] = []
55
+ self._title: Dict[str, Any] = {"text": "", "position": {}, "fontsize": 14}
56
+ self._background: Dict[str, Any] = {"color": "#ffffff", "grid": False}
57
+ self._metadata: Dict[str, Any] = {
58
+ "created_at": datetime.now().isoformat(),
59
+ "updated_at": datetime.now().isoformat(),
60
+ "author": "",
61
+ "description": "",
62
+ }
63
+ self._caption: Dict[str, Any] = {"text": "", "render": False, "fontsize": 10, "width_mm": None}
64
+ self._source_files: Dict[str, Path] = {} # panel_name -> source_path
65
+
66
+ @property
67
+ def name(self) -> str:
68
+ """Canvas name."""
69
+ return self._name
70
+
71
+ @property
72
+ def panels(self) -> List[Dict[str, Any]]:
73
+ """List of panel configurations."""
74
+ return self._panels
75
+
76
+ def add_panel(
77
+ self,
78
+ panel_name: str,
79
+ source: Union[str, Path],
80
+ position: tuple = (0, 0),
81
+ size: tuple = (50, 50),
82
+ label: str = "",
83
+ **kwargs,
84
+ ) -> "Canvas":
85
+ """
86
+ Add a panel to the canvas.
87
+
88
+ Parameters
89
+ ----------
90
+ panel_name : str
91
+ Name for the panel
92
+ source : str or Path
93
+ Path to source file (PNG, JPG, SVG)
94
+ position : tuple
95
+ (x_mm, y_mm) position on canvas
96
+ size : tuple
97
+ (width_mm, height_mm) panel size
98
+ label : str
99
+ Panel label (A, B, C...)
100
+ **kwargs
101
+ Additional panel properties (rotation_deg, opacity, flip_h, etc.)
102
+
103
+ Returns
104
+ -------
105
+ Canvas
106
+ Self for method chaining
107
+ """
108
+ source = Path(source)
109
+
110
+ # Determine panel type
111
+ json_sibling = source.parent / f"{source.stem}.json"
112
+ panel_type = "scitex" if json_sibling.exists() else "image"
113
+
114
+ # Build panel entry
115
+ panel_entry = {
116
+ "name": panel_name,
117
+ "type": panel_type,
118
+ "position": {"x_mm": position[0], "y_mm": position[1]},
119
+ "size": {"width_mm": size[0], "height_mm": size[1]},
120
+ "z_index": len(self._panels),
121
+ "rotation_deg": kwargs.get("rotation_deg", 0),
122
+ "opacity": kwargs.get("opacity", 1.0),
123
+ "flip_h": kwargs.get("flip_h", False),
124
+ "flip_v": kwargs.get("flip_v", False),
125
+ "visible": kwargs.get("visible", True),
126
+ "clip": {
127
+ "enabled": False,
128
+ "x_mm": 0,
129
+ "y_mm": 0,
130
+ "width_mm": None,
131
+ "height_mm": None,
132
+ },
133
+ "label": {
134
+ "text": label,
135
+ "position": "top-left",
136
+ "fontsize": 12,
137
+ "fontweight": "bold",
138
+ },
139
+ "border": {
140
+ "visible": False,
141
+ "color": "#000000",
142
+ "width_mm": 0.2,
143
+ },
144
+ }
145
+
146
+ if panel_type == "image":
147
+ panel_entry["source"] = f"panel{source.suffix}"
148
+
149
+ # Apply any additional kwargs to nested dicts
150
+ for key, value in kwargs.items():
151
+ if key in panel_entry and isinstance(panel_entry[key], dict) and isinstance(value, dict):
152
+ panel_entry[key].update(value)
153
+ elif key not in ["rotation_deg", "opacity", "flip_h", "flip_v", "visible"]:
154
+ panel_entry[key] = value
155
+
156
+ # Remove existing panel with same name
157
+ self._panels = [p for p in self._panels if p.get("name") != panel_name]
158
+ self._panels.append(panel_entry)
159
+
160
+ # Store source file path for later
161
+ self._source_files[panel_name] = source.resolve()
162
+
163
+ return self
164
+
165
+ def update_panel(self, panel_name: str, updates: Dict[str, Any]) -> "Canvas":
166
+ """
167
+ Update panel properties.
168
+
169
+ Parameters
170
+ ----------
171
+ panel_name : str
172
+ Name of panel to update
173
+ updates : Dict[str, Any]
174
+ Properties to update
175
+
176
+ Returns
177
+ -------
178
+ Canvas
179
+ Self for method chaining
180
+ """
181
+ for panel in self._panels:
182
+ if panel.get("name") == panel_name:
183
+ _deep_merge(panel, updates)
184
+ break
185
+ return self
186
+
187
+ def remove_panel(self, panel_name: str) -> "Canvas":
188
+ """
189
+ Remove a panel from the canvas.
190
+
191
+ Parameters
192
+ ----------
193
+ panel_name : str
194
+ Name of panel to remove
195
+
196
+ Returns
197
+ -------
198
+ Canvas
199
+ Self for method chaining
200
+ """
201
+ self._panels = [p for p in self._panels if p.get("name") != panel_name]
202
+ self._source_files.pop(panel_name, None)
203
+ return self
204
+
205
+ def add_annotation(
206
+ self,
207
+ ann_type: str,
208
+ **kwargs,
209
+ ) -> "Canvas":
210
+ """
211
+ Add an annotation to the canvas.
212
+
213
+ Parameters
214
+ ----------
215
+ ann_type : str
216
+ Annotation type: "text", "arrow", "bracket", "line", "rectangle", "legend"
217
+ **kwargs
218
+ Type-specific properties
219
+
220
+ Returns
221
+ -------
222
+ Canvas
223
+ Self for method chaining
224
+ """
225
+ annotation = {"type": ann_type, **kwargs}
226
+ self._annotations.append(annotation)
227
+ return self
228
+
229
+ def set_caption(
230
+ self,
231
+ text: str,
232
+ render: bool = False,
233
+ position: tuple = None,
234
+ fontsize: int = 10,
235
+ width_mm: float = None,
236
+ ) -> "Canvas":
237
+ """
238
+ Set figure caption (legend in scientific sense).
239
+
240
+ Caption is stored as metadata by default. Use render=True to
241
+ include it in the exported image. "Figure X." numbering should
242
+ be handled by LaTeX/document side, not included in caption text.
243
+
244
+ Parameters
245
+ ----------
246
+ text : str
247
+ Caption text describing the figure, e.g.,
248
+ "Neural activity across conditions. (A) Control. (B) Treatment."
249
+ Do NOT include "Figure X." - that's handled by LaTeX.
250
+ render : bool
251
+ If True, render caption in exported image.
252
+ If False (default), store as metadata only.
253
+ position : tuple, optional
254
+ (x_mm, y_mm) position when render=True. Default: below canvas
255
+ fontsize : int
256
+ Font size when rendered (default: 10)
257
+ width_mm : float, optional
258
+ Text wrap width in mm. Default: canvas width - 20mm margins
259
+
260
+ Returns
261
+ -------
262
+ Canvas
263
+ Self for method chaining
264
+
265
+ Examples
266
+ --------
267
+ >>> # Caption as metadata only (for LaTeX)
268
+ >>> canvas.set_caption(
269
+ ... "Neural responses to visual stimuli. "
270
+ ... "(A) Raw signals. (B) Filtered signals."
271
+ ... )
272
+ >>>
273
+ >>> # Caption rendered in image
274
+ >>> canvas.set_caption(
275
+ ... "Neural responses to visual stimuli.",
276
+ ... render=True
277
+ ... )
278
+ """
279
+ self._caption = {
280
+ "text": text,
281
+ "render": render,
282
+ "fontsize": fontsize,
283
+ "width_mm": width_mm,
284
+ }
285
+ if position:
286
+ self._caption["position"] = {"x_mm": position[0], "y_mm": position[1]}
287
+
288
+ return self
289
+
290
+ def set_title(self, text: str, position: tuple = None, fontsize: int = 14) -> "Canvas":
291
+ """
292
+ Set canvas title.
293
+
294
+ Parameters
295
+ ----------
296
+ text : str
297
+ Title text
298
+ position : tuple, optional
299
+ (x_mm, y_mm) position
300
+ fontsize : int
301
+ Font size
302
+
303
+ Returns
304
+ -------
305
+ Canvas
306
+ Self for method chaining
307
+ """
308
+ self._title["text"] = text
309
+ if position:
310
+ self._title["position"] = {"x_mm": position[0], "y_mm": position[1]}
311
+ self._title["fontsize"] = fontsize
312
+ return self
313
+
314
+ def to_dict(self) -> Dict[str, Any]:
315
+ """
316
+ Convert canvas to dictionary representation.
317
+
318
+ Returns
319
+ -------
320
+ Dict[str, Any]
321
+ Canvas as dictionary (canvas.json structure)
322
+ """
323
+ return {
324
+ "schema_version": "2.0.0",
325
+ "canvas_name": self._name,
326
+ "size": {
327
+ "width_mm": self._width_mm,
328
+ "height_mm": self._height_mm,
329
+ },
330
+ "background": self._background,
331
+ "panels": self._panels,
332
+ "annotations": self._annotations,
333
+ "title": self._title,
334
+ "caption": self._caption,
335
+ "data_files": [],
336
+ "metadata": self._metadata,
337
+ "manual_overrides": {},
338
+ "_source_files": {k: str(v) for k, v in self._source_files.items()},
339
+ }
340
+
341
+ @classmethod
342
+ def from_dict(cls, data: Dict[str, Any]) -> "Canvas":
343
+ """
344
+ Create canvas from dictionary.
345
+
346
+ Parameters
347
+ ----------
348
+ data : Dict[str, Any]
349
+ Canvas dictionary (from canvas.json or to_dict())
350
+
351
+ Returns
352
+ -------
353
+ Canvas
354
+ New Canvas instance
355
+ """
356
+ size = data.get("size", {})
357
+ canvas = cls(
358
+ name=data.get("canvas_name", "untitled"),
359
+ width_mm=size.get("width_mm", 180),
360
+ height_mm=size.get("height_mm", 240),
361
+ )
362
+ canvas._panels = data.get("panels", [])
363
+ canvas._annotations = data.get("annotations", [])
364
+ canvas._title = data.get("title", canvas._title)
365
+ canvas._caption = data.get("caption", canvas._caption)
366
+ canvas._background = data.get("background", canvas._background)
367
+ canvas._metadata = data.get("metadata", canvas._metadata)
368
+
369
+ # Restore source files if present
370
+ source_files = data.get("_source_files", {})
371
+ for panel_name, path_str in source_files.items():
372
+ canvas._source_files[panel_name] = Path(path_str)
373
+
374
+ return canvas
375
+
376
+ def save(
377
+ self,
378
+ path: Union[str, Path],
379
+ bundle: bool = False,
380
+ **kwargs,
381
+ ) -> Path:
382
+ """
383
+ Save canvas to a .canvas directory.
384
+
385
+ Parameters
386
+ ----------
387
+ path : str or Path
388
+ Path where the .canvas directory should be created.
389
+ Must end with .canvas extension.
390
+ bundle : bool
391
+ If True, copy source files. If False (default), use symlinks.
392
+ **kwargs
393
+ Additional arguments passed to stx.io.save
394
+
395
+ Returns
396
+ -------
397
+ Path
398
+ Path to the created .canvas directory.
399
+
400
+ Examples
401
+ --------
402
+ >>> canvas = stx.vis.Canvas("fig1")
403
+ >>> canvas.add_panel("panel_a", "plot.png", ...)
404
+ >>> canvas.save("/output/fig1.canvas") # Uses symlinks
405
+ >>> canvas.save("/output/fig1.canvas", bundle=True) # Copies files
406
+ """
407
+ import scitex as stx
408
+ return stx.io.save(self, path, bundle=bundle, **kwargs)
409
+
410
+ def __repr__(self) -> str:
411
+ return f"Canvas(name='{self._name}', panels={len(self._panels)})"
412
+
413
+
414
+ def _deep_merge(base: Dict, updates: Dict) -> None:
415
+ """Deep merge updates into base dictionary (in-place)."""
416
+ for key, value in updates.items():
417
+ if key in base and isinstance(base[key], dict) and isinstance(value, dict):
418
+ _deep_merge(base[key], value)
419
+ else:
420
+ base[key] = value
421
+
422
+
423
+ # EOF