diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 39c1ab5..e1a8621 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -50,7 +50,7 @@ jobs: if: contains(github.ref, 'tags') steps: - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 with: name: docs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ebae2d3..e807909 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black @@ -17,14 +17,14 @@ repos: - id: napari-plugin-checks - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.1 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: [numpy, matplotlib] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: 'v0.5.1' + rev: 'v0.6.8' hooks: - id: ruff diff --git a/README.md b/README.md index e4551d2..fb7aa63 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A plugin to create Matplotlib plots from napari layers ## Introduction `napari-matplotlib` is a bridge between `napari` and `matplotlib`, making it easy to create publication quality `Matplotlib` plots based on the data loaded in `napari` layers. -Documentaiton can be found at https://napari-matplotlib.github.io/ +Documentation can be found at https://napari-matplotlib.github.io/ ## Contributing diff --git a/docs/changelog.rst b/docs/changelog.rst index 54e6bba..60dd72b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 05f7df6..f76831a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,10 +13,18 @@ filterwarnings = [ "ignore:distutils Version classes are deprecated:DeprecationWarning", "ignore:`np.bool8` is a deprecated alias for `np.bool_`:DeprecationWarning", # Coming from pydantic via napari - "ignore:Pickle, copy, and deepcopy support will be removed from itertools in Python 3.14.:DeprecationWarning" + "ignore:Pickle, copy, and deepcopy support will be removed from itertools in Python 3.14.:DeprecationWarning", + # Until we stop supporting older numpy versions (<2.1) + "ignore:(?s).*`newshape` keyword argument is deprecated.*$:DeprecationWarning", ] qt_api = "pyqt6" -addopts = ["--mpl", "--mpl-baseline-relative", "--strict-config", "--strict-markers", "-ra"] +addopts = [ + "--mpl", + "--mpl-baseline-relative", + "--strict-config", + "--strict-markers", + "-ra", +] minversion = "7" testpaths = ["src/napari_matplotlib/tests"] log_cli_level = "INFO" @@ -54,17 +62,15 @@ ignore = [ convention = "numpy" [tool.mypy] -python_version = "3.10" +python_version = "3.12" # Block below are checks that form part of mypy 'strict' mode strict = true disallow_subclassing_any = false # TODO: fix -warn_return_any = false # TODO: fix +warn_return_any = false # TODO: fix ignore_missing_imports = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] [[tool.mypy.overrides]] -module = [ - "napari_matplotlib/tests/*", -] +module = ["napari_matplotlib/tests/*"] disallow_untyped_defs = false diff --git a/setup.cfg b/setup.cfg index 073478a..a3709e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ project_urls = packages = find: install_requires = matplotlib - napari<0.5 + napari>=0.5 numpy>=1.23 tinycss2 python_requires = >=3.10 diff --git a/src/napari_matplotlib/base.py b/src/napari_matplotlib/base.py index 720333e..ca69a54 100644 --- a/src/napari_matplotlib/base.py +++ b/src/napari_matplotlib/base.py @@ -42,7 +42,7 @@ def __init__( super().__init__(parent=parent) self.viewer = napari_viewer self.napari_theme_style_sheet = style_sheet_from_theme( - get_theme(napari_viewer.theme, as_dict=False) + get_theme(napari_viewer.theme) ) # Sets figure.* style @@ -84,7 +84,7 @@ def _on_napari_theme_changed(self, event: Event) -> None: Event that triggered the callback. """ self.napari_theme_style_sheet = style_sheet_from_theme( - get_theme(event.value, as_dict=False) + get_theme(event.value) ) self._replace_toolbar_icons() @@ -97,7 +97,7 @@ def _napari_theme_has_light_bg(self) -> bool: bool True if theme's background colour has hsl lighter than 50%, False if darker. """ - theme = napari.utils.theme.get_theme(self.viewer.theme, as_dict=False) + theme = napari.utils.theme.get_theme(self.viewer.theme) _, _, bg_lightness = theme.background.as_hsl_tuple() return bg_lightness > 0.5 diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 2881cf7..acdd840 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -8,7 +8,10 @@ from napari.layers._multiscale_data import MultiScaleData from qtpy.QtWidgets import ( QComboBox, + QFormLayout, + QGroupBox, QLabel, + QSpinBox, QVBoxLayout, QWidget, ) @@ -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): @@ -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) @@ -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 @@ -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): @@ -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 diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png b/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png new file mode 100644 index 0000000..98e3cde Binary files /dev/null and b/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png differ diff --git a/src/napari_matplotlib/tests/scatter/test_scatter.py b/src/napari_matplotlib/tests/scatter/test_scatter.py index a225863..0c60660 100644 --- a/src/napari_matplotlib/tests/scatter/test_scatter.py +++ b/src/napari_matplotlib/tests/scatter/test_scatter.py @@ -15,7 +15,9 @@ def test_scatter_2D(make_napari_viewer, astronaut_data): viewer.add_image(astronaut_data[0], **astronaut_data[1], name="astronaut") viewer.add_image( - astronaut_data[0] * -1, **astronaut_data[1], name="astronaut_reversed" + astronaut_data[0] * -1.0, + **astronaut_data[1], + name="astronaut_reversed", ) # De-select existing selection viewer.layers.selection.clear() @@ -36,7 +38,7 @@ def test_scatter_3D(make_napari_viewer, brain_data): viewer.add_image(brain_data[0], **brain_data[1], name="brain") viewer.add_image( - brain_data[0] * -1, **brain_data[1], name="brain_reversed" + brain_data[0] * -1.0, **brain_data[1], name="brain_reversed" ) # De-select existing selection viewer.layers.selection.clear() diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index 1ceca51..435973b 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -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() diff --git a/src/napari_matplotlib/tests/test_theme.py b/src/napari_matplotlib/tests/test_theme.py index 2310f32..5fedc43 100644 --- a/src/napari_matplotlib/tests/test_theme.py +++ b/src/napari_matplotlib/tests/test_theme.py @@ -29,7 +29,7 @@ def _mock_up_theme() -> None: Based on: https://napari.org/stable/gallery/new_theme.html """ - blue_theme = napari.utils.theme.get_theme("dark", False) + blue_theme = napari.utils.theme.get_theme("dark") blue_theme.label = "blue" blue_theme.background = "#4169e1" # my favourite shade of blue napari.utils.theme.register_theme( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy