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
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
|
-
|
|
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
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
-
>>> #
|
|
21
|
-
>>>
|
|
22
|
-
>>>
|
|
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
|
|
25
|
-
>>> stx.
|
|
26
|
-
>>>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
60
|
-
from .
|
|
43
|
+
# Canvas class
|
|
44
|
+
from .canvas import Canvas
|
|
61
45
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
#
|
|
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
|