Skip to content

Commit

Permalink
adjust sample widget
Browse files Browse the repository at this point in the history
  • Loading branch information
melonora committed Mar 12, 2024
1 parent 77c96a8 commit 8574e5e
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 50 deletions.
66 changes: 59 additions & 7 deletions src/cell_gater/model/data_model.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Sequence
Expand All @@ -8,15 +10,21 @@

@dataclass
class DataModel:
"""Model containing all necessary fields for gating."""

events: EmitterGroup = field(init=False, default=None, repr=True)
_samples: Sequence[str] = field(default_factory=list, init=False)
_regionprops_df: pd.DataFrame = field(
default_factory=pd.DataFrame, init=False
)
_regionprops_df: pd.DataFrame = field(default_factory=pd.DataFrame, init=False)
_regionprops_columns: Sequence[str] = field(default_factory=list, init=False)
_image_paths: Sequence[Path] = field(default_factory=list, init=False)
_mask_paths: Sequence[Path] = field(default_factory=list, init=False)
_lower_bound_marker: str | None = field(default=None, init=False)
_upper_bound_marker: str | None = field(default=None, init=False)
_markers: Sequence[str] = field(default_factory=list, init=False)
_active_marker: str | None = field(default=None, init=False)

def __post_init__(self) -> None:
"""Allow fields in the dataclass to emit events when changed."""
self.events = EmitterGroup(
source=self,
samples=Event,
Expand All @@ -25,33 +33,77 @@ def __post_init__(self) -> None:

@property
def samples(self):
"""Samples derived from the regionprops csv file names."""
return self._samples

@samples.setter
def samples(self, samples: Sequence[str]):
def samples(self, samples: Sequence[str]) -> None:
self._samples = samples

@property
def regionprops_df(self):
"""Regionprops dataframe derived from possibly multiple csv files."""
return self._regionprops_df

@regionprops_df.setter
def regionprops_df(self, regionprops: pd.DataFrame):
def regionprops_df(self, regionprops: pd.DataFrame) -> None:
self._regionprops_df = regionprops
self.events.regionprops_df()

@property
def image_paths(self):
"""The paths to the images."""
return self._image_paths

@image_paths.setter
def image_paths(self, image_paths: Sequence[Path]):
def image_paths(self, image_paths: Sequence[Path]) -> None:
self._image_paths = image_paths

@property
def mask_paths(self):
"""The paths to the mask images."""
return self._mask_paths

@mask_paths.setter
def mask_paths(self, mask_paths: Sequence[Path]):
def mask_paths(self, mask_paths: Sequence[Path]) -> None:
self._mask_paths = mask_paths

@property
def lower_bound_marker(self):
"""The lower bound column name of the marker columns to be included from regionprops_df."""
return self._lower_bound_marker

@lower_bound_marker.setter
def lower_bound_marker(self, marker: str) -> None:
self._lower_bound_marker = marker

@property
def upper_bound_marker(self):
"""The inclusive upper bound column name of the marker columns to be included from regionprops_df."""
return self._upper_bound_marker

@upper_bound_marker.setter
def upper_bound_marker(self, marker: str) -> None:
self._upper_bound_marker = marker

@property
def markers(self):
"""The markers included for gating."""
return self._markers

@markers.setter
def markers(self, markers: Sequence[str]) -> None:
self._markers = markers

@property
def active_marker(self):
"""The marker currently used on x-axis for gating."""
return self._active_marker

@active_marker.setter
def active_marker(self, marker: str) -> None:
self._active_marker = marker

def validate(self):
"""Validate the input data from the user provided through the SampleWidget."""
pass
87 changes: 44 additions & 43 deletions src/cell_gater/widgets/sample_widget.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from pathlib import Path
from typing import Any

from napari import Viewer
from napari.utils.history import (
get_open_history,
update_open_history,
)
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QComboBox,
QFileDialog,
Expand All @@ -25,10 +23,25 @@
class SampleWidget(QWidget):
"""Sample widget for loading required data."""

def __init__(self, viewer: Viewer) -> None:
def __init__(self, viewer: Viewer, model: DataModel | None = None) -> None:
"""
Create the QWidget visuals.
This widget sets up the QtWidget elements used to determine what the input is that the user wants to use
for cell gating. It also connects these elements to their appropriate callback functions.
Parameters
----------
viewer : napari.Viewer
The napari Viewer instance.
model : DataModel | None
The data model dataclass. If provided, this means that the plugin is used by means of a CLI to be
implemented.
"""
super().__init__()
self._viewer = viewer
self._model = DataModel()
self._model = DataModel() if model is None else model
self.setLayout(QGridLayout())
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

Expand All @@ -49,38 +62,30 @@ def __init__(self, viewer: Viewer) -> None:
self.load_mask_dir_button.clicked.connect(self._open_mask_dir_dialog)
self.layout().addWidget(self.load_mask_dir_button, 1, 2)

# The lower bound marker column
# The lower bound marker column dropdown
lower_col = QLabel("Select lowerbound marker column:")
self.lower_bound_marker_col = QComboBox()
if len(self.model.regionprops_df) > 0:
self.lower_bound_marker_col.addItems([None] + self.model.regionprops_df.columns)
self.lower_bound_marker_col.currentTextChanged.connect(self._update_marker_widget)
self.lower_bound_marker_col.currentTextChanged.connect(self._update_model_lowerbound)
self.layout().addWidget(lower_col, 2, 0)
self.layout().addWidget(self.lower_bound_marker_col, 3, 0)

# The upper bound marker column
# The upper bound marker column dropdown
upper_col = QLabel("Select upperbound marker column:")
self.upper_bound_marker_col = QComboBox()
if len(self.model.regionprops_df) > 0:
self.upper_bound_marker_col.addItems([None] + self.model.regionprops_df.columns)
self.upper_bound_marker_col.currentTextChanged.connect(self._update_marker_widget)
self.upper_bound_marker_col.currentTextChanged.connect(self._update_model_upperbound)
self.layout().addWidget(upper_col, 2, 1)
self.layout().addWidget(self.upper_bound_marker_col, 3, 1)

# Dropdown of samples once directory is loaded
selection_label = QLabel("Select sample:")
self.sample_selection_dropdown = QComboBox()
if len(self.model.samples) > 0:
self.sample_selection_dropdown.addItems([None])
self.sample_selection_dropdown.addItems(self.model.samples)
self.sample_selection_dropdown.currentTextChanged.connect(self._on_sample_changed)
self.validate_button = QPushButton("Validate input")
self.validate_button.clicked.connect(self.model.validate)
self.layout().addWidget(self.validate_button, 4, 0)

self.layout().addWidget(selection_label, 4, 0, Qt.AlignCenter)
self.layout().addWidget(self.sample_selection_dropdown, 4, 1)

self.model.events.regionprops_df.connect(self._set_samples_dropdown)
self.model.events.regionprops_df.connect(self._set_marker_lowerbound)
self.model.events.regionprops_df.connect(self._set_marker_upperbound)
self.model.events.regionprops_df.connect(self._set_dropdown_marker_lowerbound)
self.model.events.regionprops_df.connect(self._set_dropdown_marker_upperbound)

@property
def viewer(self) -> Viewer:
Expand All @@ -92,10 +97,8 @@ def model(self) -> DataModel:
"""Data model of the widget."""
return self._model

def _on_sample_changed(self):
pass

def _dir_dialog(self):
"""Open dialog for a user to pass on a directory."""
dlg = QFileDialog()
hist = get_open_history()
dlg.setHistory(hist)
Expand Down Expand Up @@ -128,38 +131,36 @@ def _open_mask_dir_dialog(self):
if folder not in {"", None}:
self._set_mask_paths(folder)

def _set_image_paths(self, folder):
def _set_image_paths(self, folder: str) -> None:
"""Set the image paths in the DataModel."""
self.model.image_paths = list(Path(folder).glob("*tif"))
napari_notification(f"{len(self.model.image_paths)} paths of images loaded.")

def _set_mask_paths(self, folder):
def _set_mask_paths(self, folder: str) -> None:
"""Set the paths of the masks in the DataModel."""
self.model.mask_paths = list(Path(folder).glob("*tif"))
napari_notification(f"{len(self.model.mask_paths)} paths of masks loaded.")

def _assign_regionprops_to_model(self, folder):
self.model.regionprops_df = stack_csv_files(folder)
def _assign_regionprops_to_model(self, folder: str) -> None:
"""Read the csv files in the directory and assign the resulting concatenated DataFrame in the DataModel."""
self.model.regionprops_df = stack_csv_files(Path(folder))

def _set_samples_dropdown(self, event: Any):
if (region_props := self.model.regionprops_df) is not None:
self.model.samples = list(region_props["sample_id"].cat.categories)

# New directory loaded so we reload the dropdown items
self.sample_selection_dropdown.clear()
if len(self.model.samples) > 0:
self.sample_selection_dropdown.addItems([None])
self.sample_selection_dropdown.addItems(self.model.samples)

def _update_marker_widget(self):
pass

def _set_marker_lowerbound(self):
def _set_dropdown_marker_lowerbound(self):
self.lower_bound_marker_col.clear()
region_props = self.model.regionprops_df
if region_props is not None and len(region_props) > 0:
self.lower_bound_marker_col.addItems(region_props.columns)

def _set_marker_upperbound(self):
def _set_dropdown_marker_upperbound(self):
self.upper_bound_marker_col.clear()
region_props = self.model.regionprops_df
if region_props is not None and len(region_props) > 0:
self.upper_bound_marker_col.addItems(region_props.columns)

def _update_model_lowerbound(self):
lower_bound_marker = self.lower_bound_marker_col.currentText()
self.model.lower_bound_marker = lower_bound_marker

def _update_model_upperbound(self):
upper_bound_marker = self.upper_bound_marker_col.currentText()
self.model.upper_bound_marker = upper_bound_marker

0 comments on commit 8574e5e

Please sign in to comment.