senoquant 1.0.0b3__py3-none-any.whl → 1.0.0b4__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.
- senoquant/__init__.py +1 -1
- senoquant/_widget.py +9 -1
- senoquant/tabs/__init__.py +2 -0
- senoquant/tabs/batch/backend.py +58 -24
- senoquant/tabs/batch/frontend.py +119 -21
- senoquant/tabs/spots/frontend.py +54 -0
- senoquant/tabs/spots/models/rmp/details.json +55 -0
- senoquant/tabs/spots/models/rmp/model.py +574 -0
- senoquant/tabs/spots/models/ufish/details.json +16 -1
- senoquant/tabs/spots/models/ufish/model.py +211 -13
- senoquant/tabs/spots/ufish_utils/core.py +31 -1
- senoquant/tabs/visualization/__init__.py +1 -0
- senoquant/tabs/visualization/backend.py +306 -0
- senoquant/tabs/visualization/frontend.py +1113 -0
- senoquant/tabs/visualization/plots/__init__.py +80 -0
- senoquant/tabs/visualization/plots/base.py +152 -0
- senoquant/tabs/visualization/plots/double_expression.py +187 -0
- senoquant/tabs/visualization/plots/spatialplot.py +156 -0
- senoquant/tabs/visualization/plots/umap.py +140 -0
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/METADATA +7 -6
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/RECORD +25 -15
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/WHEEL +0 -0
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/entry_points.txt +0 -0
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Visualization plot UI components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import pkgutil
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from .base import PlotConfig, PlotData, SenoQuantPlot
|
|
10
|
+
from .spatialplot import SpatialPlotData
|
|
11
|
+
from .umap import UMAPData
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _iter_subclasses(cls: type[SenoQuantPlot]) -> Iterable[type[SenoQuantPlot]]:
|
|
15
|
+
"""Yield all subclasses of a plot class recursively.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
cls : type[SenoQuantPlot]
|
|
20
|
+
Base class whose subclasses should be discovered.
|
|
21
|
+
|
|
22
|
+
Yields
|
|
23
|
+
------
|
|
24
|
+
type[SenoQuantPlot]
|
|
25
|
+
Plot subclass types.
|
|
26
|
+
"""
|
|
27
|
+
for subclass in cls.__subclasses__():
|
|
28
|
+
yield subclass
|
|
29
|
+
yield from _iter_subclasses(subclass)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_feature_registry() -> dict[str, type[SenoQuantPlot]]:
|
|
33
|
+
"""Discover plot classes and return a registry by name."""
|
|
34
|
+
for module in pkgutil.walk_packages(__path__, f"{__name__}."):
|
|
35
|
+
importlib.import_module(module.name)
|
|
36
|
+
|
|
37
|
+
registry: dict[str, type[SenoQuantPlot]] = {}
|
|
38
|
+
for plot_cls in _iter_subclasses(SenoQuantPlot):
|
|
39
|
+
feature_type = getattr(plot_cls, "feature_type", "")
|
|
40
|
+
if not feature_type:
|
|
41
|
+
continue
|
|
42
|
+
registry[feature_type] = plot_cls
|
|
43
|
+
|
|
44
|
+
return dict(
|
|
45
|
+
sorted(
|
|
46
|
+
registry.items(),
|
|
47
|
+
key=lambda item: getattr(item[1], "order", 0),
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
FEATURE_DATA_FACTORY: dict[str, type[PlotData]] = {
|
|
52
|
+
"UMAP": UMAPData,
|
|
53
|
+
"Spatial Plot": SpatialPlotData,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_feature_data(feature_type: str) -> PlotData:
|
|
58
|
+
"""Create a plot data instance for the specified plot type.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
feature_type : str
|
|
63
|
+
Plot type name.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
PlotData
|
|
68
|
+
Plot-specific configuration instance.
|
|
69
|
+
"""
|
|
70
|
+
data_cls = FEATURE_DATA_FACTORY.get(feature_type, PlotData)
|
|
71
|
+
return data_cls()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"PlotConfig",
|
|
76
|
+
"PlotData",
|
|
77
|
+
"SenoQuantPlot",
|
|
78
|
+
"build_feature_data",
|
|
79
|
+
"get_feature_registry",
|
|
80
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Feature UI base classes for visualization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Iterable
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
from qtpy.QtWidgets import QComboBox
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..frontend import VisualizationTab
|
|
14
|
+
from ..frontend import PlotUIContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlotData:
|
|
18
|
+
"""Base class for plot-specific configuration data.
|
|
19
|
+
|
|
20
|
+
Notes
|
|
21
|
+
-----
|
|
22
|
+
Concrete plot data classes should inherit from this class so they can
|
|
23
|
+
be stored on :class:`PlotConfig`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PlotConfig:
|
|
29
|
+
"""Configuration for a single visualization plot.
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
plot_id : str
|
|
34
|
+
Unique identifier for the plot instance.
|
|
35
|
+
type_name : str
|
|
36
|
+
Plot type name (e.g., ``"UMAP"``).
|
|
37
|
+
data : PlotData
|
|
38
|
+
Plot-specific configuration payload.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
plot_id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
42
|
+
type_name: str = ""
|
|
43
|
+
data: PlotData = field(default_factory=PlotData)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SenoQuantPlot:
|
|
47
|
+
"""Base class for visualization plot UI."""
|
|
48
|
+
|
|
49
|
+
feature_type: str = ""
|
|
50
|
+
order: int = 0
|
|
51
|
+
|
|
52
|
+
def __init__(self, tab: "VisualizationTab", context: "PlotUIContext") -> None:
|
|
53
|
+
"""Initialize a plot with shared tab context.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
tab : VisualizationTab
|
|
58
|
+
Parent visualization tab instance.
|
|
59
|
+
context : PlotUIContext
|
|
60
|
+
Plot UI context with configuration state.
|
|
61
|
+
"""
|
|
62
|
+
self._tab = tab
|
|
63
|
+
self._context = context
|
|
64
|
+
self._state = context.state
|
|
65
|
+
self._ui: dict[str, object] = {}
|
|
66
|
+
|
|
67
|
+
def build(self) -> None:
|
|
68
|
+
"""Build the UI for this plot."""
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
|
|
71
|
+
def plot(
|
|
72
|
+
self,
|
|
73
|
+
temp_dir: Path,
|
|
74
|
+
input_path: Path,
|
|
75
|
+
export_format: str,
|
|
76
|
+
markers: list[str] | None = None,
|
|
77
|
+
thresholds: dict[str, float] | None = None,
|
|
78
|
+
) -> Iterable[Path]:
|
|
79
|
+
"""Generate plot outputs into a temporary directory.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
temp_dir : Path
|
|
84
|
+
Temporary directory where outputs should be written.
|
|
85
|
+
input_path : Path
|
|
86
|
+
Path to the folder containing CSV or Excel files for plotting.
|
|
87
|
+
export_format : str
|
|
88
|
+
File format requested by the user (``"png"``, ``"svg"``, or ``"pdf"``).
|
|
89
|
+
markers : list of str, optional
|
|
90
|
+
List of selected markers to include.
|
|
91
|
+
thresholds : dict, optional
|
|
92
|
+
Dictionary of {marker_name: threshold_value} for filtering.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
iterable of Path
|
|
97
|
+
Paths to files produced by the plot routine.
|
|
98
|
+
|
|
99
|
+
Notes
|
|
100
|
+
-----
|
|
101
|
+
Implementations may either return explicit file paths or simply
|
|
102
|
+
write outputs into ``temp_dir`` and return an empty iterable.
|
|
103
|
+
"""
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
def on_features_changed(self, configs: list["PlotUIContext"]) -> None:
|
|
107
|
+
"""Handle updates when the plot list changes.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
configs : list of PlotUIContext
|
|
112
|
+
Current plot contexts.
|
|
113
|
+
"""
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def update_type_options(
|
|
118
|
+
cls, tab: "VisualizationTab", configs: list["PlotUIContext"]
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Update type availability in plot selectors.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
tab : VisualizationTab
|
|
125
|
+
Parent visualization tab instance.
|
|
126
|
+
configs : list of PlotUIContext
|
|
127
|
+
Current plot contexts.
|
|
128
|
+
"""
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class RefreshingComboBox(QComboBox):
|
|
133
|
+
"""Combo box that refreshes its items when opened."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, refresh_callback=None, parent=None) -> None:
|
|
136
|
+
"""Create a combo box that refreshes before showing its popup.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
refresh_callback : callable or None
|
|
141
|
+
Callback invoked before showing the popup.
|
|
142
|
+
parent : QWidget or None
|
|
143
|
+
Optional parent widget.
|
|
144
|
+
"""
|
|
145
|
+
super().__init__(parent)
|
|
146
|
+
self._refresh_callback = refresh_callback
|
|
147
|
+
|
|
148
|
+
def showPopup(self) -> None:
|
|
149
|
+
"""Refresh items before showing the popup."""
|
|
150
|
+
if self._refresh_callback is not None:
|
|
151
|
+
self._refresh_callback()
|
|
152
|
+
super().showPopup()
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Double expression plot handler for visualization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from napari.utils.notifications import show_error
|
|
10
|
+
except ImportError:
|
|
11
|
+
def show_error(message: str) -> None:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
from .base import PlotData, SenoQuantPlot
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DoubleExpressionData(PlotData):
|
|
18
|
+
"""Configuration data for double expression plot."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DoubleExpressionPlot(SenoQuantPlot):
|
|
24
|
+
"""Spatial scatter plot highlighting double positive cells."""
|
|
25
|
+
|
|
26
|
+
feature_type = "Double Expression"
|
|
27
|
+
order = 2
|
|
28
|
+
|
|
29
|
+
def build(self) -> None:
|
|
30
|
+
"""Build the UI for double expression plot configuration."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def plot(
|
|
34
|
+
self,
|
|
35
|
+
temp_dir: Path,
|
|
36
|
+
input_path: Path,
|
|
37
|
+
export_format: str,
|
|
38
|
+
markers: list[str] | None = None,
|
|
39
|
+
thresholds: dict[str, float] | None = None,
|
|
40
|
+
) -> Iterable[Path]:
|
|
41
|
+
"""Generate double expression plot from input CSV.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
temp_dir : Path
|
|
46
|
+
Temporary directory to write plot output.
|
|
47
|
+
input_path : Path
|
|
48
|
+
Path to input CSV file or folder containing CSV files.
|
|
49
|
+
export_format : str
|
|
50
|
+
Output format ("png", "svg", or "pdf").
|
|
51
|
+
markers : list of str, optional
|
|
52
|
+
List of selected markers. Must contain exactly 2 markers.
|
|
53
|
+
thresholds : dict, optional
|
|
54
|
+
Dictionary of {marker_name: threshold_value}.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
iterable of Path
|
|
59
|
+
Paths to generated plot files.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
try:
|
|
63
|
+
import pandas as pd
|
|
64
|
+
except ImportError:
|
|
65
|
+
msg = (
|
|
66
|
+
"[DoubleExpressionPlot] pandas is not installed; "
|
|
67
|
+
"skipping plot generation."
|
|
68
|
+
)
|
|
69
|
+
print(msg)
|
|
70
|
+
show_error(msg)
|
|
71
|
+
return []
|
|
72
|
+
try:
|
|
73
|
+
import matplotlib.pyplot as plt
|
|
74
|
+
except ImportError:
|
|
75
|
+
msg = (
|
|
76
|
+
"[DoubleExpressionPlot] matplotlib is not installed; "
|
|
77
|
+
"skipping plot generation."
|
|
78
|
+
)
|
|
79
|
+
print(msg)
|
|
80
|
+
show_error(msg)
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
print(f"[DoubleExpressionPlot] Starting with input_path={input_path}")
|
|
84
|
+
|
|
85
|
+
if not markers or len(markers) != 2:
|
|
86
|
+
msg = f"Double Expression Plot requires exactly 2 markers. Got {len(markers) if markers else 0}."
|
|
87
|
+
print(f"[DoubleExpressionPlot] {msg}")
|
|
88
|
+
show_error(msg)
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
# Find data file
|
|
92
|
+
data_files = list(input_path.glob("*.csv")) + list(input_path.glob("*.xlsx")) + list(input_path.glob("*.xls"))
|
|
93
|
+
if not data_files:
|
|
94
|
+
print(f"[DoubleExpressionPlot] No data files found")
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
data_file = data_files[0]
|
|
98
|
+
if data_file.suffix.lower() in ('.xlsx', '.xls'):
|
|
99
|
+
df = pd.read_excel(data_file)
|
|
100
|
+
else:
|
|
101
|
+
df = pd.read_csv(data_file)
|
|
102
|
+
|
|
103
|
+
if df.empty:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
# Identify columns (alphabetical order from frontend)
|
|
107
|
+
m1, m2 = markers[0], markers[1]
|
|
108
|
+
col1 = f"{m1}_mean_intensity"
|
|
109
|
+
col2 = f"{m2}_mean_intensity"
|
|
110
|
+
|
|
111
|
+
if col1 not in df.columns or col2 not in df.columns:
|
|
112
|
+
msg = f"Missing columns for markers: {m1}, {m2}"
|
|
113
|
+
print(f"[DoubleExpressionPlot] {msg}")
|
|
114
|
+
show_error(msg)
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
# Get thresholds
|
|
118
|
+
t1 = thresholds.get(m1, 0.0) if thresholds else 0.0
|
|
119
|
+
t2 = thresholds.get(m2, 0.0) if thresholds else 0.0
|
|
120
|
+
|
|
121
|
+
print(f"[DoubleExpressionPlot] Using thresholds: {m1}>{t1}, {m2}>{t2}")
|
|
122
|
+
|
|
123
|
+
# Find X, Y
|
|
124
|
+
x_col = None
|
|
125
|
+
y_col = None
|
|
126
|
+
for col in df.columns:
|
|
127
|
+
col_lower = col.lower()
|
|
128
|
+
if "x" in col_lower and x_col is None:
|
|
129
|
+
x_col = col
|
|
130
|
+
elif "y" in col_lower and y_col is None:
|
|
131
|
+
y_col = col
|
|
132
|
+
|
|
133
|
+
if x_col is None or y_col is None:
|
|
134
|
+
msg = "[DoubleExpressionPlot] Could not find X/Y columns in the data file."
|
|
135
|
+
print(msg)
|
|
136
|
+
show_error(msg)
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
# Plotting
|
|
140
|
+
fig, ax = plt.subplots(figsize=(10, 10))
|
|
141
|
+
|
|
142
|
+
# 1. Background (All cells - Negative appearance)
|
|
143
|
+
ax.scatter(df[x_col], df[y_col], c="#f0f0f0", s=1, label="Negative")
|
|
144
|
+
|
|
145
|
+
# 2. Layer 1: M1 ONLY (Red)
|
|
146
|
+
# Logic: (M1 > T1) AND (M2 <= T2)
|
|
147
|
+
m1_only = df[(df[col1] > t1) & (df[col2] <= t2)]
|
|
148
|
+
ax.scatter(m1_only[x_col], m1_only[y_col], c="red", s=3, alpha=0.8, label=f"{m1}+ only")
|
|
149
|
+
|
|
150
|
+
# 3. Layer 2: M2 ONLY (Blue)
|
|
151
|
+
# Logic: (M2 > T2) AND (M1 <= T1)
|
|
152
|
+
m2_only = df[(df[col2] > t2) & (df[col1] <= t1)]
|
|
153
|
+
ax.scatter(m2_only[x_col], m2_only[y_col], c="blue", s=3, alpha=0.8, label=f"{m2}+ only")
|
|
154
|
+
|
|
155
|
+
# 4. Layer 3: DOUBLE POSITIVE (Green)
|
|
156
|
+
# Logic: (M1 > T1) AND (M2 > T2)
|
|
157
|
+
both_pos = df[(df[col1] > t1) & (df[col2] > t2)]
|
|
158
|
+
ax.scatter(both_pos[x_col], both_pos[y_col], c="green", s=4, alpha=1.0, label="Double Positive")
|
|
159
|
+
|
|
160
|
+
ax.set_aspect('equal')
|
|
161
|
+
ax.set_title(f"Spatial Distribution\n{m1} (Red) | {m2} (Blue) | Both (Green)", fontsize=15)
|
|
162
|
+
ax.set_xlabel(x_col)
|
|
163
|
+
ax.set_ylabel(y_col)
|
|
164
|
+
|
|
165
|
+
# Custom Legend
|
|
166
|
+
ax.legend(markerscale=4, loc='upper right', frameon=False)
|
|
167
|
+
|
|
168
|
+
# Print Counts
|
|
169
|
+
print(f"[DoubleExpressionPlot] {m1}+ only: {len(m1_only)}")
|
|
170
|
+
print(f"[DoubleExpressionPlot] {m2}+ only: {len(m2_only)}")
|
|
171
|
+
print(f"[DoubleExpressionPlot] Double + : {len(both_pos)}")
|
|
172
|
+
|
|
173
|
+
# Save
|
|
174
|
+
safe_name = f"{m1}_{m2}_double_expression"
|
|
175
|
+
safe_name = "".join(c if c.isalnum() else "_" for c in safe_name)
|
|
176
|
+
output_file = temp_dir / f"{safe_name}.{export_format}"
|
|
177
|
+
fig.savefig(str(output_file), dpi=150, bbox_inches="tight")
|
|
178
|
+
plt.close(fig)
|
|
179
|
+
|
|
180
|
+
return [output_file]
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
import traceback
|
|
184
|
+
print(f"[DoubleExpressionPlot] Error: {e}")
|
|
185
|
+
print(traceback.format_exc())
|
|
186
|
+
show_error(f"Error in Double Expression Plot: {e}")
|
|
187
|
+
return []
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Spatial plot handler for visualization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from .base import PlotData, SenoQuantPlot
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpatialPlotData(PlotData):
|
|
12
|
+
"""Configuration data for spatial plot."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SpatialPlot(SenoQuantPlot):
|
|
18
|
+
"""Spatial scatter plot handler for coordinate and intensity data."""
|
|
19
|
+
|
|
20
|
+
feature_type = "Spatial Plot"
|
|
21
|
+
order = 0
|
|
22
|
+
|
|
23
|
+
def build(self) -> None:
|
|
24
|
+
"""Build the UI for spatial plot configuration."""
|
|
25
|
+
# Minimal UI for now; can add controls for marker selection, colormaps, etc. later
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def plot(
|
|
29
|
+
self,
|
|
30
|
+
temp_dir: Path,
|
|
31
|
+
input_path: Path,
|
|
32
|
+
export_format: str,
|
|
33
|
+
markers: list[str] | None = None,
|
|
34
|
+
thresholds: dict[str, float] | None = None,
|
|
35
|
+
) -> Iterable[Path]:
|
|
36
|
+
"""Generate spatial plot from input CSV.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
temp_dir : Path
|
|
41
|
+
Temporary directory to write plot output.
|
|
42
|
+
input_path : Path
|
|
43
|
+
Path to input CSV file or folder containing CSV files.
|
|
44
|
+
export_format : str
|
|
45
|
+
Output format ("png", "svg", or "pdf").
|
|
46
|
+
markers : list of str, optional
|
|
47
|
+
List of selected markers to include.
|
|
48
|
+
thresholds : dict, optional
|
|
49
|
+
Dictionary of {marker_name: threshold_value} for filtering.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
iterable of Path
|
|
54
|
+
Paths to generated plot files.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
try:
|
|
58
|
+
import pandas as pd
|
|
59
|
+
except ImportError:
|
|
60
|
+
print("[SpatialPlot] pandas is not installed; skipping plot generation.")
|
|
61
|
+
return []
|
|
62
|
+
try:
|
|
63
|
+
import matplotlib.pyplot as plt
|
|
64
|
+
except ImportError:
|
|
65
|
+
print(
|
|
66
|
+
"[SpatialPlot] matplotlib is not installed; skipping plot generation."
|
|
67
|
+
)
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
print(f"[SpatialPlot] Starting with input_path={input_path}")
|
|
71
|
+
# Find the first data file (CSV or Excel) in the input folder
|
|
72
|
+
data_files = list(input_path.glob("*.csv")) + list(input_path.glob("*.xlsx")) + list(input_path.glob("*.xls"))
|
|
73
|
+
print(f"[SpatialPlot] Found {len(data_files)} data files")
|
|
74
|
+
if not data_files:
|
|
75
|
+
print(f"[SpatialPlot] No CSV/Excel files found in {input_path}")
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
data_file = data_files[0]
|
|
79
|
+
print(f"[SpatialPlot] Reading {data_file}")
|
|
80
|
+
if data_file.suffix.lower() in ('.xlsx', '.xls'):
|
|
81
|
+
df = pd.read_excel(data_file)
|
|
82
|
+
else:
|
|
83
|
+
df = pd.read_csv(data_file)
|
|
84
|
+
print(f"[SpatialPlot] Loaded dataframe with shape {df.shape}")
|
|
85
|
+
if df.empty:
|
|
86
|
+
print(f"[SpatialPlot] DataFrame is empty")
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
print(df.head())
|
|
90
|
+
|
|
91
|
+
# Apply thresholds if provided
|
|
92
|
+
if thresholds:
|
|
93
|
+
for marker, thresh in thresholds.items():
|
|
94
|
+
col_name = f"{marker}_mean_intensity"
|
|
95
|
+
if col_name in df.columns:
|
|
96
|
+
# Clip values below threshold to 0
|
|
97
|
+
df.loc[df[col_name] < thresh, col_name] = 0
|
|
98
|
+
|
|
99
|
+
# Filter columns based on selected markers (optional, but good for cleanup)
|
|
100
|
+
if markers is not None:
|
|
101
|
+
# We want to ensure we don't pick a deselected marker as the intensity column
|
|
102
|
+
valid_marker_cols = [f"{m}_mean_intensity" for m in markers]
|
|
103
|
+
# Keep non-marker columns (like coords) + valid marker columns
|
|
104
|
+
cols_to_keep = [c for c in df.columns if "_mean_intensity" not in c or c in valid_marker_cols]
|
|
105
|
+
df = df[cols_to_keep]
|
|
106
|
+
print(f"[SpatialPlot] Filtered columns using {len(valid_marker_cols)} selected markers")
|
|
107
|
+
|
|
108
|
+
# Look for X, Y coordinate columns
|
|
109
|
+
x_col = None
|
|
110
|
+
y_col = None
|
|
111
|
+
for col in df.columns:
|
|
112
|
+
col_lower = col.lower()
|
|
113
|
+
if "x" in col_lower and x_col is None:
|
|
114
|
+
x_col = col
|
|
115
|
+
elif "y" in col_lower and y_col is None:
|
|
116
|
+
y_col = col
|
|
117
|
+
|
|
118
|
+
if x_col is None or y_col is None:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
x = df[x_col].values
|
|
122
|
+
y = df[y_col].values
|
|
123
|
+
|
|
124
|
+
# Get first numeric column (intensity) for coloring
|
|
125
|
+
numeric_cols = df.select_dtypes(include=["number"]).columns
|
|
126
|
+
intensity_col = None
|
|
127
|
+
for col in numeric_cols:
|
|
128
|
+
if col not in [x_col, y_col]:
|
|
129
|
+
intensity_col = col
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
# Create plot
|
|
133
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
134
|
+
if intensity_col is not None:
|
|
135
|
+
c = df[intensity_col].values
|
|
136
|
+
scatter = ax.scatter(x, y, c=c, cmap="viridis", alpha=0.6, s=20)
|
|
137
|
+
plt.colorbar(scatter, ax=ax, label=intensity_col)
|
|
138
|
+
else:
|
|
139
|
+
ax.scatter(x, y, alpha=0.6, s=20)
|
|
140
|
+
|
|
141
|
+
ax.set_xlabel(x_col)
|
|
142
|
+
ax.set_ylabel(y_col)
|
|
143
|
+
ax.set_title("Spatial Distribution")
|
|
144
|
+
|
|
145
|
+
# Save plot
|
|
146
|
+
output_file = temp_dir / f"spatial_plot.{export_format}"
|
|
147
|
+
fig.savefig(str(output_file), dpi=150, bbox_inches="tight")
|
|
148
|
+
plt.close(fig)
|
|
149
|
+
|
|
150
|
+
return [output_file]
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
import traceback
|
|
154
|
+
print(f"[SpatialPlot] ERROR generating spatial plot: {e}")
|
|
155
|
+
print(traceback.format_exc())
|
|
156
|
+
return []
|