Skip to content

Add widgets for setting histogram bins #242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a7b09d4
Add widgets to set bin parameters for histograms
p-j-smith Jan 11, 2024
96b2f0c
Add test for histogram widget when setting bin parameters
p-j-smith Jan 11, 2024
b8623ed
Update changelog
p-j-smith Jan 11, 2024
4e4fb84
Make linters happy
p-j-smith Jan 11, 2024
5ac1f71
Fix type hints
p-j-smith Jan 11, 2024
fab2906
Don't allow bins lower than 0 if dtype is unisgned
p-j-smith Jan 11, 2024
5553174
Update changelog
p-j-smith Jan 11, 2024
e86d4f6
Undo changes to example of HistogramWidget
p-j-smith Jan 11, 2024
c5e0886
Fix autosetting bins from data
p-j-smith Jan 15, 2024
127d325
remove duplicate on_update_layers method
p-j-smith Jan 15, 2024
b8ffdb7
Make linters happy
p-j-smith Jan 15, 2024
88760cf
Add HistogramWidget._bin_widgets attribute for storing bin widgets
p-j-smith Jan 15, 2024
d56942b
Fix calculation of bins from widget values
p-j-smith Jan 15, 2024
11590b2
Calculate step using n_bins-1
p-j-smith Jan 15, 2024
08a5086
cannot use negative start bin for uint data
p-j-smith Jan 15, 2024
65e84f8
Make HistogramWidget bins_num widget correspond to number of bins rat…
p-j-smith Jan 15, 2024
37d33ae
Merge branch 'main' into feat/hist-bin-params
p-j-smith Jan 15, 2024
8a7d9e4
fix typo in comment about using 128 bins for float data
p-j-smith Jan 15, 2024
fbd929e
Merge branch 'main' into feat/hist-bin-params
p-j-smith Jan 15, 2024
7c4cdc8
Update changelog
p-j-smith Jan 15, 2024
c6b5d8e
Merge branch 'main' into feat/hist-bin-params
p-j-smith Feb 14, 2024
6261f4c
Add 'num_bins, 'start', and 'stop' parameters to '_get_bins'
p-j-smith Feb 14, 2024
457bc1b
Merge branch 'main' into feat/hist-bin-params
p-j-smith May 25, 2024
426a0f5
use '| None' rather than Optional[Union[...]] for type hints
p-j-smith May 25, 2024
67a8641
remove widgest to set start and stop values for histogram bins
p-j-smith May 25, 2024
208af97
Merge branch 'main' into feat/hist-bin-params
dstansby Jul 12, 2024
8fe7c7f
Update changelog
dstansby Jul 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changelog
=========

2.1.0
-----
New features
~~~~~~~~~~~~
- Added a GUI element to manually set the number of bins in the histogram widgets.

2.0.3
-----
Bug fixes
Expand Down
87 changes: 76 additions & 11 deletions src/napari_matplotlib/histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from napari.layers._multiscale_data import MultiScaleData
from qtpy.QtWidgets import (
QComboBox,
QFormLayout,
QGroupBox,
QLabel,
QSpinBox,
QVBoxLayout,
QWidget,
)
Expand All @@ -22,15 +25,32 @@
_COLORS = {"r": "tab:red", "g": "tab:green", "b": "tab:blue"}


def _get_bins(data: npt.NDArray[Any]) -> npt.NDArray[Any]:
def _get_bins(
data: npt.NDArray[Any],
num_bins: int = 100,
) -> npt.NDArray[Any]:
"""Create evenly spaced bins with a given interval.

Parameters
----------
data : napari.layers.Layer.data
Napari layer data.
num_bins : integer, optional
Number of evenly-spaced bins to create. Defaults to 100.

Returns
-------
bin_edges : numpy.ndarray
Array of evenly spaced bin edges.
"""
if data.dtype.kind in {"i", "u"}:
# Make sure integer data types have integer sized bins
step = np.ceil(np.ptp(data) / 100)
step = np.ceil(np.ptp(data) / num_bins)
return np.arange(np.min(data), np.max(data) + step, step)
else:
# For other data types, just have 100 evenly spaced bins
# (and 101 bin edges)
return np.linspace(np.min(data), np.max(data), 101)
# For other data types we can use exactly `num_bins` bins
# (and `num_bins` + 1 bin edges)
return np.linspace(np.min(data), np.max(data), num_bins + 1)


class HistogramWidget(SingleAxesWidget):
Expand All @@ -47,6 +67,30 @@ def __init__(
parent: QWidget | None = None,
):
super().__init__(napari_viewer, parent=parent)

num_bins_widget = QSpinBox()
num_bins_widget.setRange(1, 100_000)
num_bins_widget.setValue(101)
num_bins_widget.setWrapping(False)
num_bins_widget.setKeyboardTracking(False)

# Set bins widget layout
bins_selection_layout = QFormLayout()
bins_selection_layout.addRow("num bins", num_bins_widget)

# Group the widgets and add to main layout
params_widget_group = QGroupBox("Params")
params_widget_group_layout = QVBoxLayout()
params_widget_group_layout.addLayout(bins_selection_layout)
params_widget_group.setLayout(params_widget_group_layout)
self.layout().addWidget(params_widget_group)

# Add callbacks
num_bins_widget.valueChanged.connect(self._draw)

# Store widgets for later usage
self.num_bins_widget = num_bins_widget

self._update_layers(None)
self.viewer.events.theme.connect(self._on_napari_theme_changed)

Expand All @@ -60,6 +104,13 @@ def on_update_layers(self) -> None:
self._update_contrast_lims
)

if not self.layers:
return

# Reset the num bins based on new layer data
layer_data = self._get_layer_data(self.layers[0])
self._set_widget_nums_bins(data=layer_data)

def _update_contrast_lims(self) -> None:
for lim, line in zip(
self.layers[0].contrast_limits, self._contrast_lines, strict=False
Expand All @@ -68,11 +119,13 @@ def _update_contrast_lims(self) -> None:

self.figure.canvas.draw()

def draw(self) -> None:
"""
Clear the axes and histogram the currently selected layer/slice.
"""
layer: Image = self.layers[0]
def _set_widget_nums_bins(self, data: npt.NDArray[Any]) -> None:
"""Update num_bins widget with bins determined from the image data"""
bins = _get_bins(data)
self.num_bins_widget.setValue(bins.size - 1)

def _get_layer_data(self, layer: napari.layers.Layer) -> npt.NDArray[Any]:
"""Get the data associated with a given layer"""
data = layer.data

if isinstance(layer.data, MultiScaleData):
Expand All @@ -87,9 +140,21 @@ def draw(self) -> None:
# Read data into memory if it's a dask array
data = np.asarray(data)

return data

def draw(self) -> None:
"""
Clear the axes and histogram the currently selected layer/slice.
"""
layer: Image = self.layers[0]
data = self._get_layer_data(layer)

# Important to calculate bins after slicing 3D data, to avoid reading
# whole cube into memory.
bins = _get_bins(data)
bins = _get_bins(
data,
num_bins=self.num_bins_widget.value(),
)

if layer.rgb:
# Histogram RGB channels independently
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/napari_matplotlib/tests/test_histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
)


@pytest.mark.mpl_image_compare
def test_histogram_2D_bins(make_napari_viewer, astronaut_data):
viewer = make_napari_viewer()
viewer.theme = "light"
viewer.add_image(astronaut_data[0], **astronaut_data[1])
widget = HistogramWidget(viewer)
viewer.window.add_dock_widget(widget)
widget.num_bins_widget.setValue(25)
fig = widget.figure
# Need to return a copy, as original figure is too eagerley garbage
# collected by the widget
return deepcopy(fig)


@pytest.mark.mpl_image_compare
def test_histogram_2D(make_napari_viewer, astronaut_data):
viewer = make_napari_viewer()
Expand Down
Loading