diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..af3b9f02
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+---
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 5b721d92..6a5d182f 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,4 +1,6 @@
name: Build docs
+permissions:
+ contents: read
on:
@@ -17,13 +19,15 @@ jobs:
name: Build & Upload Artifact
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
- - uses: tlambert03/setup-qt-libs@v1
+ - uses: tlambert03/setup-qt-libs@19e4ef2d781d81f5f067182e228b54ec90d23b76 # v1
- name: Install Dependencies
run: |
@@ -32,13 +36,13 @@ jobs:
sudo apt install graphviz --yes
- name: Build Docs
- uses: aganders3/headless-gui@v1
+ uses: aganders3/headless-gui@f85dd6316993505dfc5f21839d520ae440c84816 # v2
with:
run: make html
working-directory: ./docs
- name: Upload artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: docs
path: docs/_build
@@ -49,13 +53,15 @@ jobs:
needs: build-docs
if: contains(github.ref, 'tags')
steps:
- - uses: actions/checkout@v3
- - uses: actions/download-artifact@v3
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - uses: actions/download-artifact@v4.3.0
with:
name: docs
- name: Push to GitHub pages
- uses: JamesIves/github-pages-deploy-action@v4
+ uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4
with:
folder: html
ssh-key: ${{ secrets.DEPLOY_KEY }}
diff --git a/.github/workflows/napari_hub_preview.yml b/.github/workflows/napari_hub_preview.yml
deleted file mode 100644
index c204ac45..00000000
--- a/.github/workflows/napari_hub_preview.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-name: napari hub Preview Page # we use this name to find your preview page artifact, so don't change it!
-# For more info on this action, see https://github.com/chanzuckerberg/napari-hub-preview-action/blob/main/action.yml
-
-on:
- pull_request:
- types: [ labeled ]
-
-jobs:
- preview-page:
- if: ${{ github.event.label.name == 'napari hub preview' }}
- name: Preview Page Deploy
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repo
- uses: actions/checkout@v3
-
- - name: napari hub Preview Page Builder
- uses: chanzuckerberg/napari-hub-preview-action@v0.1
- with:
- hub-ref: main
diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml
index 8665e1d2..0521c9e6 100644
--- a/.github/workflows/test_and_deploy.yml
+++ b/.github/workflows/test_and_deploy.yml
@@ -1,4 +1,6 @@
name: tests
+permissions:
+ contents: read
on:
push:
@@ -12,7 +14,24 @@ on:
workflow_dispatch:
merge_group:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
+ pre-commit:
+ name: precommit
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
+ with:
+ python-version: "3.x"
+ - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
+ with:
+ extra_args: --hook-stage manual --all-files
test:
name: ${{ matrix.platform }} py${{ matrix.python-version }}
runs-on: ${{ matrix.platform }}
@@ -20,18 +39,20 @@ jobs:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ['3.9', '3.10', '3.11']
+ python-version: ['3.10', '3.11', '3.12']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v3
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
# these libraries enable testing on Qt on linux
- - uses: tlambert03/setup-qt-libs@v1
+ - uses: tlambert03/setup-qt-libs@19e4ef2d781d81f5f067182e228b54ec90d23b76 # v1
# strategy borrowed from vispy for installing opengl libs on windows
- name: Install Windows OpenGL
@@ -50,7 +71,7 @@ jobs:
run: python -m tox
- name: Upload pytest test results
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: pytest-results-${{ matrix.platform }} py${{ matrix.python-version }}
path: reports/
@@ -58,13 +79,15 @@ jobs:
if: ${{ always() }}
- name: Coverage
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
# Don't run coverage on merge queue CI to avoid duplicating reports
# to codecov. See https://github.com/matplotlib/napari-matplotlib/issues/155
if: github.event_name != 'merge_group'
with:
token: ${{ secrets.CODECOV_TOKEN }}
- fail_ci_if_error: true
+ fail_ci_if_error: false
+
+
deploy:
# this will run when you have tagged a commit, starting with "v*"
@@ -77,9 +100,11 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install build
@@ -93,4 +118,4 @@ jobs:
python -m build .
- name: Publish package
- uses: pypa/gh-action-pypi-publish@release/v1
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8df635ab..e592dea1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,13 +1,13 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
+ rev: v5.0.0
hooks:
- id: check-docstring-first
- id: end-of-file-fixer
- id: trailing-whitespace
- - repo: https://github.com/psf/black
- rev: 23.12.1
+ - repo: https://github.com/psf/black-pre-commit-mirror
+ rev: 25.1.0
hooks:
- id: black
@@ -17,14 +17,14 @@ repos:
- id: napari-plugin-checks
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.8.0
+ rev: v1.15.0
hooks:
- id: mypy
additional_dependencies: [numpy, matplotlib]
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
- rev: 'v0.1.11'
+ rev: 'v0.11.9'
hooks:
- id: ruff
diff --git a/README.md b/README.md
index e4551d23..fb7aa635 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 255982a3..60dd72ba 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,5 +1,31 @@
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
+~~~~~~~~~
+- Fix an error that happened when the histogram widget was open, but a layer that doesn't support
+ histogramming (e.g., a labels layer) was selected.
+
+2.0.2
+-----
+Dependencies
+~~~~~~~~~~~~
+napari-matplotlib now adheres to `SPEC 0 `_, and has:
+
+- Dropped support for Python 3.9
+- Added support for Python 3.12
+- Added a minimum required numpy verison of 1.23
+- Pinned the maximum napari version to ``< 0.5``.
+ Version 3.0 of ``napari-matplotlib`` will introduce support for ``napari`` version 0.5.
+
2.0.1
-----
Bug fixes
diff --git a/docs/conf.py b/docs/conf.py
index 2517a59c..f1533830 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -13,7 +13,7 @@
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
-import qtgallery
+from sphinx_gallery import scrapers
# -- Project information -----------------------------------------------------
@@ -35,18 +35,58 @@
"sphinx.ext.intersphinx",
]
+
+def reset_napari(gallery_conf, fname): # type: ignore[no-untyped-def]
+ from napari.settings import get_settings
+ from qtpy.QtWidgets import QApplication
+
+ settings = get_settings()
+ settings.appearance.theme = "dark"
+
+ # Disabling `QApplication.exec_` means example scripts can call `exec_`
+ # (scripts work when run normally) without blocking example execution by
+ # sphinx-gallery. (from qtgallery)
+ QApplication.exec_ = lambda _: None
+
+
+def napari_scraper(block, block_vars, gallery_conf): # type: ignore[no-untyped-def]
+ """Basic napari window scraper.
+
+ Looks for any QtMainWindow instances and takes a screenshot of them.
+
+ `app.processEvents()` allows Qt events to propagateo and prevents hanging.
+ """
+ import napari
+
+ imgpath_iter = block_vars["image_path_iterator"]
+
+ if app := napari.qt.get_app():
+ app.processEvents()
+ else:
+ return ""
+
+ img_paths = []
+ for win, img_path in zip(
+ reversed(napari._qt.qt_main_window._QtMainWindow._instances),
+ imgpath_iter,
+ strict=False,
+ ):
+ img_paths.append(img_path)
+ win._window.screenshot(img_path, canvas_only=False)
+
+ napari.Viewer.close_all()
+ app.processEvents()
+
+ return scrapers.figure_rst(img_paths, gallery_conf["src_dir"])
+
+
sphinx_gallery_conf = {
"filename_pattern": ".",
- "image_scrapers": (qtgallery.qtscraper,),
- "reset_modules": (qtgallery.reset_qapp,),
+ "image_scrapers": (napari_scraper,),
+ "reset_modules": (reset_napari,),
}
+suppress_warnings = ["config.cache"]
-qtgallery_conf = {
- "xvfb_size": (640, 480),
- "xvfb_color_depth": 24,
- "xfvb_use_xauth": False,
- "xfvb_extra_args": [],
-}
numpydoc_show_class_members = False
automodapi_inheritance_diagram = True
diff --git a/examples/histogram.py b/examples/histogram.py
index ccda491a..b9ceb377 100644
--- a/examples/histogram.py
+++ b/examples/histogram.py
@@ -2,6 +2,7 @@
Histograms
==========
"""
+
import napari
viewer = napari.Viewer()
diff --git a/examples/scatter.py b/examples/scatter.py
index cd812401..00e01ec9 100644
--- a/examples/scatter.py
+++ b/examples/scatter.py
@@ -2,6 +2,7 @@
Scatter plots
=============
"""
+
import napari
viewer = napari.Viewer()
diff --git a/examples/slice.py b/examples/slice.py
index 3e43443e..242a16cc 100644
--- a/examples/slice.py
+++ b/examples/slice.py
@@ -2,6 +2,7 @@
1D slices
=========
"""
+
import napari
viewer = napari.Viewer()
diff --git a/pyproject.toml b/pyproject.toml
index ba9f9e66..f76831a3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
[build-system]
-requires = ["setuptools", "wheel", "setuptools_scm"]
+requires = ["setuptools", "setuptools_scm"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
@@ -12,9 +12,23 @@ filterwarnings = [
# Coming from vispy
"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",
+ # 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"
+addopts = [
+ "--mpl",
+ "--mpl-baseline-relative",
+ "--strict-config",
+ "--strict-markers",
+ "-ra",
+]
+minversion = "7"
+testpaths = ["src/napari_matplotlib/tests"]
+log_cli_level = "INFO"
+xfail_strict = true
[tool.black]
line-length = 79
@@ -24,8 +38,11 @@ profile = "black"
line_length = 79
[tool.ruff]
-target-version = "py39"
-select = ["I", "UP", "F", "E", "W", "D"]
+target-version = "py310"
+fix = true
+
+[tool.ruff.lint]
+select = ["B", "I", "UP", "F", "E", "W", "D"]
ignore = [
"D100", # Missing docstring in public module
"D104", # Missing docstring in public package
@@ -35,26 +52,25 @@ ignore = [
"D401", # First line of docstring should be in imperative mood
]
-fix = true
-[tool.ruff.per-file-ignores]
+[tool.ruff.lint.per-file-ignores]
"docs/*" = ["D"]
"examples/*" = ["D"]
"src/napari_matplotlib/tests/*" = ["D"]
-[tool.ruff.pydocstyle]
+[tool.ruff.lint.pydocstyle]
convention = "numpy"
[tool.mypy]
-python_version = "3.9"
+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 41e4e34f..a3709e66 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -28,10 +28,10 @@ project_urls =
packages = find:
install_requires =
matplotlib
- napari
- numpy
+ napari>=0.5
+ numpy>=1.23
tinycss2
-python_requires = >=3.9
+python_requires = >=3.10
include_package_data = True
package_dir =
=src
@@ -47,11 +47,10 @@ napari.manifest =
[options.extras_require]
docs =
- napari[all]==0.4.19rc3
+ napari[all]
numpydoc
pydantic<2
pydata-sphinx-theme
- qtgallery
sphinx
sphinx-automodapi
sphinx-gallery
diff --git a/src/napari_matplotlib/base.py b/src/napari_matplotlib/base.py
index fb9e485c..ca69a548 100644
--- a/src/napari_matplotlib/base.py
+++ b/src/napari_matplotlib/base.py
@@ -1,6 +1,5 @@
import os
from pathlib import Path
-from typing import Optional
import matplotlib.style as mplstyle
import napari
@@ -38,12 +37,12 @@ class BaseNapariMPLWidget(QWidget):
def __init__(
self,
napari_viewer: napari.Viewer,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
):
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
@@ -85,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()
@@ -98,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
@@ -173,7 +172,7 @@ class NapariMPLWidget(BaseNapariMPLWidget):
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
):
super().__init__(napari_viewer=napari_viewer, parent=parent)
self._setup_callbacks()
@@ -225,6 +224,15 @@ def _setup_callbacks(self) -> None:
self._update_layers
)
+ @property
+ def _valid_layer_selection(self) -> bool:
+ """
+ Return `True` if layer selection is valid.
+ """
+ return self.n_selected_layers in self.n_layers_input and all(
+ isinstance(layer, self.input_layer_types) for layer in self.layers
+ )
+
def _update_layers(self, event: napari.utils.events.Event) -> None:
"""
Update the ``layers`` attribute with currently selected layers and re-draw.
@@ -232,7 +240,8 @@ def _update_layers(self, event: napari.utils.events.Event) -> None:
self.layers = list(self.viewer.layers.selection)
self.layers = sorted(self.layers, key=lambda layer: layer.name)
self.on_update_layers()
- self._draw()
+ if self._valid_layer_selection:
+ self._draw()
def _draw(self) -> None:
"""
@@ -244,10 +253,7 @@ def _draw(self) -> None:
with mplstyle.context(self.napari_theme_style_sheet):
# everything should be done in the style context
self.clear()
- if self.n_selected_layers in self.n_layers_input and all(
- isinstance(layer, self.input_layer_types)
- for layer in self.layers
- ):
+ if self._valid_layer_selection:
self.draw()
self.canvas.draw() # type: ignore[no-untyped-call]
@@ -282,7 +288,7 @@ class SingleAxesWidget(NapariMPLWidget):
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
):
super().__init__(napari_viewer=napari_viewer, parent=parent)
self.add_single_axes()
diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py
index 40765287..85bba9d2 100644
--- a/src/napari_matplotlib/histogram.py
+++ b/src/napari_matplotlib/histogram.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional, cast
+from typing import Any, cast
import napari
import numpy as np
@@ -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[np.floating]:
+ """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):
@@ -44,9 +64,33 @@ class HistogramWidget(SingleAxesWidget):
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
- parent: Optional[QWidget] = None,
+ 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)
@@ -55,22 +99,33 @@ def on_update_layers(self) -> None:
Called when the selected layers are updated.
"""
super().on_update_layers()
- for layer in self.viewer.layers:
- layer.events.contrast_limits.connect(self._update_contrast_lims)
+ if self._valid_layer_selection:
+ self.layers[0].events.contrast_limits.connect(
+ 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
+ self.layers[0].contrast_limits, self._contrast_lines, strict=False
):
line.set_xdata(lim)
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):
@@ -85,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
@@ -121,7 +188,7 @@ class FeaturesHistogramWidget(SingleAxesWidget):
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
):
super().__init__(napari_viewer, parent=parent)
@@ -137,12 +204,12 @@ def __init__(
self._update_layers(None)
@property
- def x_axis_key(self) -> Optional[str]:
+ def x_axis_key(self) -> str | None:
"""Key to access x axis data from the FeaturesTable"""
return self._x_axis_key
@x_axis_key.setter
- def x_axis_key(self, key: Optional[str]) -> None:
+ def x_axis_key(self, key: str | None) -> None:
self._x_axis_key = key
self._draw()
@@ -166,7 +233,7 @@ def _get_valid_axis_keys(self) -> list[str]:
else:
return self.layers[0].features.keys()
- def _get_data(self) -> tuple[Optional[npt.NDArray[Any]], str]:
+ def _get_data(self) -> tuple[npt.NDArray[Any] | None, str]:
"""Get the plot data.
Returns
@@ -209,10 +276,12 @@ def draw(self) -> None:
# get the colormap from the layer depending on its type
if isinstance(self.layers[0], napari.layers.Points):
colormap = self.layers[0].face_colormap
- self.layers[0].face_color = self.x_axis_key
+ if self.x_axis_key:
+ self.layers[0].face_color = self.x_axis_key
elif isinstance(self.layers[0], napari.layers.Vectors):
colormap = self.layers[0].edge_colormap
- self.layers[0].edge_color = self.x_axis_key
+ if self.x_axis_key:
+ self.layers[0].edge_color = self.x_axis_key
else:
colormap = None
diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py
index 67d6896c..98ebe928 100644
--- a/src/napari_matplotlib/scatter.py
+++ b/src/napari_matplotlib/scatter.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional, Union
+from typing import Any
import napari
import numpy.typing as npt
@@ -100,7 +100,7 @@ class FeaturesScatterWidget(ScatterBaseWidget):
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
):
super().__init__(napari_viewer, parent=parent)
@@ -118,7 +118,7 @@ def __init__(
self._update_layers(None)
@property
- def x_axis_key(self) -> Union[str, None]:
+ def x_axis_key(self) -> str | None:
"""
Key for the x-axis data.
"""
@@ -133,7 +133,7 @@ def x_axis_key(self, key: str) -> None:
self._draw()
@property
- def y_axis_key(self) -> Union[str, None]:
+ def y_axis_key(self) -> str | None:
"""
Key for the y-axis data.
"""
diff --git a/src/napari_matplotlib/slice.py b/src/napari_matplotlib/slice.py
index 9459fa97..1924bf2b 100644
--- a/src/napari_matplotlib/slice.py
+++ b/src/napari_matplotlib/slice.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional
+from typing import Any
import matplotlib.ticker as mticker
import napari
@@ -30,7 +30,7 @@ class SliceWidget(SingleAxesWidget):
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
- parent: Optional[QWidget] = None,
+ parent: QWidget | None = None,
):
# Setup figure/axes
super().__init__(napari_viewer, parent=parent)
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 00000000..98e3cde1
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 a225863d..0c60660c 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 1ceca519..435973ba 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 2310f32f..5fedc43d 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(
diff --git a/src/napari_matplotlib/tests/test_util.py b/src/napari_matplotlib/tests/test_util.py
index a8792d41..e966cc26 100644
--- a/src/napari_matplotlib/tests/test_util.py
+++ b/src/napari_matplotlib/tests/test_util.py
@@ -26,7 +26,7 @@ def test_interval():
assert 10 not in interval
with pytest.raises(ValueError, match="must be an integer"):
- "string" in interval # type: ignore
+ assert "string" in interval # type: ignore[operator]
with pytest.raises(ValueError, match="must be <= upper_bound"):
Interval(5, 3)
@@ -69,7 +69,10 @@ def test_fallback_if_missing_dimensions(mocker):
test_css = " Flobble { background-color: rgb(0, 97, 163); } "
mocker.patch("napari.qt.get_current_stylesheet").return_value = test_css
with pytest.warns(RuntimeWarning, match="Unable to find DimensionToken"):
- assert from_napari_css_get_size_of("Flobble", (1, 2)) == QSize(1, 2)
+ with pytest.warns(RuntimeWarning, match="Unable to find Flobble"):
+ assert from_napari_css_get_size_of("Flobble", (1, 2)) == QSize(
+ 1, 2
+ )
def test_fallback_if_prelude_not_in_css():
diff --git a/src/napari_matplotlib/util.py b/src/napari_matplotlib/util.py
index ed994256..8d4150c3 100644
--- a/src/napari_matplotlib/util.py
+++ b/src/napari_matplotlib/util.py
@@ -1,4 +1,3 @@
-from typing import Optional, Union
from warnings import warn
import napari.qt
@@ -12,7 +11,7 @@ class Interval:
An integer interval.
"""
- def __init__(self, lower_bound: Optional[int], upper_bound: Optional[int]):
+ def __init__(self, lower_bound: int | None, upper_bound: int | None):
"""
Parameters
----------
@@ -48,7 +47,7 @@ def __contains__(self, val: int) -> bool:
return True
@property
- def _helper_text(self) -> Optional[str]:
+ def _helper_text(self) -> str | None:
"""
Helper text for widgets.
"""
@@ -86,9 +85,7 @@ def _has_id(nodes: list[tinycss2.ast.Node], id_name: str) -> bool:
)
-def _get_dimension(
- nodes: list[tinycss2.ast.Node], id_name: str
-) -> Union[int, None]:
+def _get_dimension(nodes: list[tinycss2.ast.Node], id_name: str) -> int | None:
"""
Get the value of the DimensionToken for the IdentToken `id_name`.
@@ -97,14 +94,18 @@ def _get_dimension(
None if no IdentToken is found.
"""
cleaned_nodes = [node for node in nodes if node.type != "whitespace"]
- for name, _, value, _ in zip(*(iter(cleaned_nodes),) * 4):
+ for name, _, value, _ in zip(*(iter(cleaned_nodes),) * 4, strict=False):
if (
name.type == "ident"
and value.type == "dimension"
and name.value == id_name
):
return value.int_value
- warn(f"Unable to find DimensionToken for {id_name}", RuntimeWarning)
+ warn(
+ f"Unable to find DimensionToken for {id_name}",
+ RuntimeWarning,
+ stacklevel=1,
+ )
return None
@@ -137,6 +138,7 @@ def from_napari_css_get_size_of(
f"Unable to find {qt_element_name} or unable to find its size in "
f"the current Napari stylesheet, falling back to {fallback}",
RuntimeWarning,
+ stacklevel=1,
)
return QSize(*fallback)
diff --git a/tox.ini b/tox.ini
index 4ec0c702..f4aed6a8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,12 +1,12 @@
[tox]
-envlist = py{39,310,311}
+envlist = py{310,311,312}
isolated_build = true
[gh-actions]
python =
- 3.9: py39
3.10: py310
3.11: py311
+ 3.12: py312
[testenv]
extras = testing
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