|
1 |
| -from typing import Optional |
| 1 | +from typing import Any, List, Optional, Tuple |
2 | 2 |
|
3 | 3 | import napari
|
4 | 4 | import numpy as np
|
5 |
| -from qtpy.QtWidgets import QWidget |
| 5 | +import numpy.typing as npt |
| 6 | +from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget |
6 | 7 |
|
7 | 8 | from .base import SingleAxesWidget
|
| 9 | +from .features import FEATURES_LAYER_TYPES |
8 | 10 | from .util import Interval
|
9 | 11 |
|
10 |
| -__all__ = ["HistogramWidget"] |
| 12 | +__all__ = ["HistogramWidget", "FeaturesHistogramWidget"] |
11 | 13 |
|
12 | 14 | _COLORS = {"r": "tab:red", "g": "tab:green", "b": "tab:blue"}
|
13 | 15 |
|
@@ -61,3 +63,112 @@ def draw(self) -> None:
|
61 | 63 | self.axes.hist(data.ravel(), bins=bins, label=layer.name)
|
62 | 64 |
|
63 | 65 | self.axes.legend()
|
| 66 | + |
| 67 | + |
| 68 | +class FeaturesHistogramWidget(SingleAxesWidget): |
| 69 | + """ |
| 70 | + Display a histogram of selected feature attached to selected layer. |
| 71 | + """ |
| 72 | + |
| 73 | + n_layers_input = Interval(1, 1) |
| 74 | + # All layers that have a .features attributes |
| 75 | + input_layer_types = FEATURES_LAYER_TYPES |
| 76 | + |
| 77 | + def __init__( |
| 78 | + self, |
| 79 | + napari_viewer: napari.viewer.Viewer, |
| 80 | + parent: Optional[QWidget] = None, |
| 81 | + ): |
| 82 | + super().__init__(napari_viewer, parent=parent) |
| 83 | + |
| 84 | + self.layout().addLayout(QVBoxLayout()) |
| 85 | + self._key_selection_widget = QComboBox() |
| 86 | + self.layout().addWidget(QLabel("Key:")) |
| 87 | + self.layout().addWidget(self._key_selection_widget) |
| 88 | + |
| 89 | + self._key_selection_widget.currentTextChanged.connect( |
| 90 | + self._set_axis_keys |
| 91 | + ) |
| 92 | + |
| 93 | + self._update_layers(None) |
| 94 | + |
| 95 | + @property |
| 96 | + def x_axis_key(self) -> Optional[str]: |
| 97 | + """Key to access x axis data from the FeaturesTable""" |
| 98 | + return self._x_axis_key |
| 99 | + |
| 100 | + @x_axis_key.setter |
| 101 | + def x_axis_key(self, key: Optional[str]) -> None: |
| 102 | + self._x_axis_key = key |
| 103 | + self._draw() |
| 104 | + |
| 105 | + def _set_axis_keys(self, x_axis_key: str) -> None: |
| 106 | + """Set both axis keys and then redraw the plot""" |
| 107 | + self._x_axis_key = x_axis_key |
| 108 | + self._draw() |
| 109 | + |
| 110 | + def _get_valid_axis_keys(self) -> List[str]: |
| 111 | + """ |
| 112 | + Get the valid axis keys from the layer FeatureTable. |
| 113 | +
|
| 114 | + Returns |
| 115 | + ------- |
| 116 | + axis_keys : List[str] |
| 117 | + The valid axis keys in the FeatureTable. If the table is empty |
| 118 | + or there isn't a table, returns an empty list. |
| 119 | + """ |
| 120 | + if len(self.layers) == 0 or not (hasattr(self.layers[0], "features")): |
| 121 | + return [] |
| 122 | + else: |
| 123 | + return self.layers[0].features.keys() |
| 124 | + |
| 125 | + def _get_data(self) -> Tuple[Optional[npt.NDArray[Any]], str]: |
| 126 | + """Get the plot data. |
| 127 | +
|
| 128 | + Returns |
| 129 | + ------- |
| 130 | + data : List[np.ndarray] |
| 131 | + List contains X and Y columns from the FeatureTable. Returns |
| 132 | + an empty array if nothing to plot. |
| 133 | + x_axis_name : str |
| 134 | + The title to display on the x axis. Returns |
| 135 | + an empty string if nothing to plot. |
| 136 | + """ |
| 137 | + if not hasattr(self.layers[0], "features"): |
| 138 | + # if the selected layer doesn't have a featuretable, |
| 139 | + # skip draw |
| 140 | + return None, "" |
| 141 | + |
| 142 | + feature_table = self.layers[0].features |
| 143 | + |
| 144 | + if (len(feature_table) == 0) or (self.x_axis_key is None): |
| 145 | + return None, "" |
| 146 | + |
| 147 | + data = feature_table[self.x_axis_key] |
| 148 | + x_axis_name = self.x_axis_key.replace("_", " ") |
| 149 | + |
| 150 | + return data, x_axis_name |
| 151 | + |
| 152 | + def on_update_layers(self) -> None: |
| 153 | + """ |
| 154 | + Called when the layer selection changes by ``self.update_layers()``. |
| 155 | + """ |
| 156 | + # reset the axis keys |
| 157 | + self._x_axis_key = None |
| 158 | + |
| 159 | + # Clear combobox |
| 160 | + self._key_selection_widget.clear() |
| 161 | + self._key_selection_widget.addItems(self._get_valid_axis_keys()) |
| 162 | + |
| 163 | + def draw(self) -> None: |
| 164 | + """Clear the axes and histogram the currently selected layer/slice.""" |
| 165 | + data, x_axis_name = self._get_data() |
| 166 | + |
| 167 | + if data is None: |
| 168 | + return |
| 169 | + |
| 170 | + self.axes.hist(data, bins=50, edgecolor="white", linewidth=0.3) |
| 171 | + |
| 172 | + # set ax labels |
| 173 | + self.axes.set_xlabel(x_axis_name) |
| 174 | + self.axes.set_ylabel("Counts [#]") |
0 commit comments