diff --git a/src/plopp/backends/matplotlib/fast_image.py b/src/plopp/backends/matplotlib/fast_image.py deleted file mode 100644 index 89eeff457..000000000 --- a/src/plopp/backends/matplotlib/fast_image.py +++ /dev/null @@ -1,203 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) - -import uuid -import warnings -from typing import Literal - -import numpy as np -import scipp as sc - -from ...core.utils import coord_as_bin_edges, scalar_to_string -from ...graphics.bbox import BoundingBox, axis_bounds -from ...graphics.colormapper import ColorMapper -from ..common import check_ndim -from .canvas import Canvas - - -class FastImage: - """ - Artist to represent two-dimensional data. - - Parameters - ---------- - canvas: - The canvas that will display the image. - colormapper: - The colormapper to use for the image. - data: - The initial data to create the image from. - artist_number: - The canvas keeps track of how many images have been added to it. This is unused - by the FastImage artist. - uid: - The unique identifier of the artist. If None, a random UUID is generated. - **kwargs: - Additional arguments are forwarded to Matplotlib's ``imshow``. - """ - - def __init__( - self, - canvas: Canvas, - colormapper: ColorMapper, - data: sc.DataArray, - artist_number: int, - uid: str | None = None, - **kwargs, - ): - check_ndim(data, ndim=2, origin="FastImage") - self.uid = uid if uid is not None else uuid.uuid4().hex - self._canvas = canvas - self._colormapper = colormapper - self._ax = self._canvas.ax - self._data = data - - string_labels = {} - self._bin_edge_coords = {} - for i, k in enumerate("yx"): - self._bin_edge_coords[k] = coord_as_bin_edges( - self._data, self._data.dims[i] - ) - if self._data.coords[self._data.dims[i]].dtype == str: - string_labels[k] = self._data.coords[self._data.dims[i]] - - self._xmin, self._xmax = self._bin_edge_coords["x"].values[[0, -1]] - self._ymin, self._ymax = self._bin_edge_coords["y"].values[[0, -1]] - self._dx = np.diff(self._bin_edge_coords["x"].values[:2]) - self._dy = np.diff(self._bin_edge_coords["y"].values[:2]) - - # Calling imshow sets the aspect ratio to 'equal', which might not be what the - # user requested. We need to restore the original aspect ratio after making the - # image. - original_aspect = self._ax.get_aspect() - - # Because imshow sets the aspect, it may generate warnings when the axes scales - # are log. - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - category=UserWarning, - message="Attempt to set non-positive .* on a log-scaled axis", - ) - self._image = self._ax.imshow( - self._data.values, - origin="lower", - extent=(self._xmin, self._xmax, self._ymin, self._ymax), - **({"interpolation": "nearest"} | kwargs), - ) - - self._ax.set_aspect(original_aspect) - self._colormapper.add_artist(self.uid, self) - self._update_colors() - - for xy, var in string_labels.items(): - getattr(self._ax, f"set_{xy}ticks")(np.arange(float(var.shape[0]))) - getattr(self._ax, f"set_{xy}ticklabels")(var.values) - - self._canvas.register_format_coord(self.format_coord) - # We also hide the cursor hover values generated by the image, as values are - # included in our custom format_coord. - self._image.format_cursor_data = lambda _: "" - - @property - def data(self): - """ - Get the image's data in a form that may have been tweaked, compared to the - original data, in the case of a two-dimensional coordinate. - """ - return self._data - - def notify_artist(self, message: str) -> None: - """ - Receive notification from the colormapper that its state has changed. - We thus need to update the colors of the image. - - Parameters - ---------- - message: - The message from the colormapper. - """ - self._update_colors() - - def _update_colors(self): - """ - Update the image colors. - """ - rgba = self._colormapper.rgba(self.data) - self._image.set_data(rgba) - - def update(self, new_values: sc.DataArray): - """ - Update image array with new values. - - Parameters - ---------- - new_values: - New data to update the image values from. - """ - check_ndim(new_values, ndim=2, origin="FastImage") - self._data = new_values - self._update_colors() - - def format_coord( - self, xslice: tuple[str, sc.Variable], yslice: tuple[str, sc.Variable] - ) -> str: - """ - Format the coordinates of the mouse pointer to show the value of the - data at that point. - - Parameters - ---------- - xslice: - Dimension and x coordinate of the mouse pointer, as slice parameters. - yslice: - Dimension and y coordinate of the mouse pointer, as slice parameters. - """ - ind_x = int((xslice[1].value - self._xmin) / self._dx) - ind_y = int((yslice[1].value - self._ymin) / self._dy) - try: - val = self._data[yslice[0], ind_y][xslice[0], ind_x] - prefix = self._data.name - if prefix: - prefix += ": " - return prefix + scalar_to_string(val) - except IndexError: - return None - - @property - def visible(self) -> bool: - """ - The visibility of the image. - """ - return self._image.get_visible() - - @visible.setter - def visible(self, val: bool): - self._image.set_visible(val) - - @property - def opacity(self) -> float: - """ - The opacity of the image. - """ - return self._image.get_alpha() - - @opacity.setter - def opacity(self, val: float): - self._image.set_alpha(val) - - def bbox(self, xscale: Literal["linear", "log"], yscale: Literal["linear", "log"]): - """ - The bounding box of the image. - """ - return BoundingBox( - **{**axis_bounds(("xmin", "xmax"), self._bin_edge_coords["x"], xscale)}, - **{**axis_bounds(("ymin", "ymax"), self._bin_edge_coords["y"], yscale)}, - ) - - def remove(self): - """ - Remove the image artist from the canvas. - """ - self._image.remove() - self._colormapper.remove_artist(self.uid) diff --git a/src/plopp/backends/matplotlib/image.py b/src/plopp/backends/matplotlib/image.py index e1da9e96a..792cd3b75 100644 --- a/src/plopp/backends/matplotlib/image.py +++ b/src/plopp/backends/matplotlib/image.py @@ -1,36 +1,374 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +import uuid +import warnings +from typing import Literal + +import numpy as np import scipp as sc +from matplotlib.collections import QuadMesh +from matplotlib.image import AxesImage +from ...core.utils import coord_as_bin_edges, merge_masks, repeat, scalar_to_string +from ...graphics.bbox import BoundingBox, axis_bounds +from ...graphics.colormapper import ColorMapper +from ..common import check_ndim from .canvas import Canvas -from .fast_image import FastImage -from .mesh_image import MeshImage -def Image( - canvas: Canvas, - data: sc.DataArray, - **kwargs, -): +def _suitable_for_fast_image(canvas: Canvas, data: sc.DataArray) -> bool: + return (canvas.ax.name != 'polar') and all( + (data.coords[dim].ndim < 2) + and ((data.coords[dim].dtype == str) or (sc.islinspace(data.coords[dim]))) + for dim in data.dims + ) + + +def _find_dim_of_2d_coord(coords): + for xy, coord in coords.items(): + if coord['var'].ndim == 2: + return (xy, coord['dim']) + + +def _get_dims_of_1d_and_2d_coords(coords): + dim_2d = _find_dim_of_2d_coord(coords) + if dim_2d is None: + return None, None + axis_1d = 'xy'.replace(dim_2d[0], '') + dim_1d = (axis_1d, coords[axis_1d]['dim']) + return dim_1d, dim_2d + + +def _maybe_repeat_values(data, dim_1d, dim_2d): + if dim_2d is None: + return data + return repeat(data, dim=dim_1d[1], n=2)[dim_1d[1], :-1] + + +def _from_data_array_to_pcolormesh(data, coords, dim_1d, dim_2d): + z = _maybe_repeat_values(data=data, dim_1d=dim_1d, dim_2d=dim_2d) + if dim_2d is None: + return coords['x'], coords['y'], z + + # Broadcast 1d coord to 2d and repeat along 1d dim + # TODO: It may be more efficient here to first repeat and then broadcast, but + # the current order is simpler in implementation. + broadcasted_coord = repeat( + sc.broadcast( + coords[dim_1d[0]], + sizes={**coords[dim_2d[0]].sizes, **coords[dim_1d[0]].sizes}, + ).transpose(data.dims), + dim=dim_1d[1], + n=2, + ) + # Repeat 2d coord along 1d dim + repeated_coord = repeat(coords[dim_2d[0]].transpose(data.dims), dim=dim_1d[1], n=2) + out = {dim_1d[0]: broadcasted_coord[dim_1d[1], 1:-1], dim_2d[0]: repeated_coord} + return out['x'], out['y'], z + + +class Image: """ - Factory function to create an image artist. - If all the coordinates of the data are 1D and linearly spaced, - a `FastImage` is created. - Otherwise, a `MeshImage` is created. + Artist to represent two-dimensional data. Parameters ---------- canvas: The canvas that will display the image. + colormapper: + The colormapper to use for the image. data: - The data to create the image from. + The initial data to create the image from. + artist_number: + The canvas keeps track of how many images have been added to it. This is unused + by the MeshImage artist. + uid: + The unique identifier of the artist. If None, a random UUID is generated. + shading: + The shading to use for the ``pcolormesh``. + rasterized: + Rasterize the mesh/image if ``True``. + **kwargs: + Additional arguments are forwarded to Matplotlib's ``pcolormesh``. """ - if (canvas.ax.name != 'polar') and all( - (data.coords[dim].ndim < 2) - and ((data.coords[dim].dtype == str) or (sc.islinspace(data.coords[dim]))) - for dim in data.dims + + def __init__( + self, + canvas: Canvas, + colormapper: ColorMapper, + data: sc.DataArray, + artist_number: int, + uid: str | None = None, + shading: str = 'auto', + rasterized: bool = True, + **kwargs, ): - return FastImage(canvas=canvas, data=data, **kwargs) - else: - return MeshImage(canvas=canvas, data=data, **kwargs) + check_ndim(data, ndim=2, origin='MeshImage') + self.uid = uid if uid is not None else uuid.uuid4().hex + self._canvas = canvas + self._colormapper = colormapper + self._ax = self._canvas.ax + self._data = data + self._shading = shading + self._rasterized = rasterized + self._kwargs = kwargs + self._optimized_mode = _suitable_for_fast_image(self._canvas, self._data) + + self._string_labels = {} + self._bin_edge_coords = {} + self._raw_coords = {} + to_dim_search = { + k: {'dim': self._data.dims[i], 'var': self._data.coords[self._data.dims[i]]} + for i, k in enumerate('yx') + } + + self._update_coords() + self._dim_1d, self._dim_2d = _get_dims_of_1d_and_2d_coords(to_dim_search) + + self._image = self._make_image() + + self._colormapper.add_artist(self.uid, self) + self._update_colors() + + for xy, var in self._string_labels.items(): + getattr(self._ax, f'set_{xy}ticks')(np.arange(float(var.shape[0]))) + getattr(self._ax, f'set_{xy}ticklabels')(var.values) + + self._canvas.register_format_coord(self.format_coord) + + def _update_coords(self) -> None: + self._data_with_bin_edges = sc.DataArray(data=self._data.data) + for i, k in enumerate('yx'): + self._raw_coords[k] = self._data.coords[self._data.dims[i]] + self._bin_edge_coords[k] = coord_as_bin_edges( + self._data, self._data.dims[i] + ) + self._data_with_bin_edges.coords[self._data.dims[i]] = ( + self._bin_edge_coords[k] + ) + if self._data.coords[self._data.dims[i]].dtype == str: + self._string_labels[k] = self._data.coords[self._data.dims[i]] + + if self._optimized_mode: + self._xmin, self._xmax = self._bin_edge_coords["x"].values[[0, -1]] + self._ymin, self._ymax = self._bin_edge_coords["y"].values[[0, -1]] + self._dx = np.diff(self._bin_edge_coords["x"].values[:2]) + self._dy = np.diff(self._bin_edge_coords["y"].values[:2]) + + def _make_image(self) -> QuadMesh | AxesImage: + if self._optimized_mode: + # Calling imshow sets the aspect ratio to 'equal', which might not be what + # the user requested. We need to restore the original aspect ratio after + # making the image. + original_aspect = self._ax.get_aspect() + + # Because imshow sets the aspect, it may generate warnings when the axes + # scales are log. + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=UserWarning, + message="Attempt to set non-positive .* on a log-scaled axis", + ) + img = self._ax.imshow( + self._data.values, + origin="lower", + extent=(self._xmin, self._xmax, self._ymin, self._ymax), + **({"interpolation": "nearest"} | self._kwargs), + ) + + self._ax.set_aspect(original_aspect) + # We also hide the cursor hover values generated by the image, as values are + # included in our custom format_coord. + img.format_cursor_data = lambda _: "" + + else: + # If the grid is visible on the axes, we need to set that on again after we + # call pcolormesh, because that turns the grid off automatically. + # See https://github.com/matplotlib/matplotlib/issues/15600. + need_grid = self._ax.xaxis.get_gridlines()[0].get_visible() + + x, y, z = _from_data_array_to_pcolormesh( + data=self._data.data, + coords=self._bin_edge_coords, + dim_1d=self._dim_1d, + dim_2d=self._dim_2d, + ) + img = self._ax.pcolormesh( + x.values, + y.values, + z.values, + shading=self._shading, + rasterized=self._rasterized, + **self._kwargs, + ) + img.set_array(None) + if need_grid: + self._ax.grid(True) + + return img + + @property + def data(self): + """ + Get the Image's data in a form that may have been tweaked, compared to the + original data, in the case of a two-dimensional coordinate. + """ + if self._optimized_mode: + return self._data + out = sc.DataArray( + data=_maybe_repeat_values( + data=self._data.data, dim_1d=self._dim_1d, dim_2d=self._dim_2d + ) + ) + if self._data.masks: + out.masks['one_mask'] = _maybe_repeat_values( + data=sc.broadcast( + merge_masks(self._data.masks), sizes=self._data.sizes + ), + dim_1d=self._dim_1d, + dim_2d=self._dim_2d, + ) + return out + + def notify_artist(self, message: str) -> None: + """ + Receive notification from the colormapper that its state has changed. + We thus need to update the colors of the mesh. + + Parameters + ---------- + message: + The message from the colormapper. + """ + self._update_colors() + + def _update_colors(self): + """ + Update the mesh colors. + """ + rgba = self._colormapper.rgba(self.data) + if self._optimized_mode: + self._image.set_data(rgba) + else: + self._image.set_facecolors(rgba.reshape(np.prod(rgba.shape[:-1]), 4)) + + def update(self, new_values: sc.DataArray): + """ + Update image array with new values. + + Parameters + ---------- + new_values: + New data to update the mesh values from. + """ + check_ndim(new_values, ndim=2, origin='MeshImage') + old_shape = self._data.shape + old_mode = self._optimized_mode + self._data = new_values + self._optimized_mode = _suitable_for_fast_image(self._canvas, self._data) + if old_mode != self._optimized_mode: + self._update_coords() + self._image.remove() + self._image = self._make_image() + elif self._data.shape != old_shape: + self._update_coords() + if self._optimized_mode: + self._image.set_extent((self._xmin, self._xmax, self._ymin, self._ymax)) + else: + self._image.remove() + self._image = self._make_image() + elif any( + not sc.identical(new_values.coords[self._data.dims[i]], self._raw_coords[k]) + for i, k in enumerate('yx') + ): + # Update the coordinates of the existing mesh + self._update_coords() + if self._optimized_mode: + self._image.set_extent((self._xmin, self._xmax, self._ymin, self._ymax)) + else: + x, y, _ = _from_data_array_to_pcolormesh( + data=self._data.data, + coords=self._bin_edge_coords, + dim_1d=self._dim_1d, + dim_2d=self._dim_2d, + ) + m = QuadMesh(np.stack(np.meshgrid(x.values, y.values), axis=-1)) + # TODO: There is no public API to update the coordinates of a QuadMesh, + # so we have to access the protected member here. + self._image._coordinates = m._coordinates + self._image.stale = True # mark it for re-draw + else: + self._data_with_bin_edges.data = new_values.data + self._update_colors() + + def format_coord( + self, xslice: tuple[str, sc.Variable], yslice: tuple[str, sc.Variable] + ) -> str: + """ + Format the coordinates of the mouse pointer to show the value of the + data at that point. + + Parameters + ---------- + xslice: + Dimension and x coordinate of the mouse pointer, as slice parameters. + yslice: + Dimension and y coordinate of the mouse pointer, as slice parameters. + """ + try: + if self._optimized_mode: + ind_x = int((xslice[1].value - self._xmin) / self._dx) + ind_y = int((yslice[1].value - self._ymin) / self._dy) + val = self._data[yslice[0], ind_y][xslice[0], ind_x] + else: + val = self._data_with_bin_edges[yslice][xslice] + prefix = self._data.name + if prefix: + prefix += ': ' + return prefix + scalar_to_string(val) + except (IndexError, RuntimeError): + return None + + @property + def visible(self) -> bool: + """ + The visibility of the image. + """ + return self._image.get_visible() + + @visible.setter + def visible(self, val: bool): + self._image.set_visible(val) + + @property + def opacity(self) -> float: + """ + The opacity of the image. + """ + return self._image.get_alpha() + + @opacity.setter + def opacity(self, val: float): + self._image.set_alpha(val) + + def bbox(self, xscale: Literal['linear', 'log'], yscale: Literal['linear', 'log']): + """ + The bounding box of the image. + """ + ydim, xdim = self._data.dims + image_x = self._data_with_bin_edges.coords[xdim] + image_y = self._data_with_bin_edges.coords[ydim] + + return BoundingBox( + **{**axis_bounds(('xmin', 'xmax'), image_x, xscale)}, + **{**axis_bounds(('ymin', 'ymax'), image_y, yscale)}, + ) + + def remove(self): + """ + Remove the image artist from the canvas. + """ + self._image.remove() + self._colormapper.remove_artist(self.uid) diff --git a/src/plopp/backends/matplotlib/mesh_image.py b/src/plopp/backends/matplotlib/mesh_image.py deleted file mode 100644 index 2a2f4b324..000000000 --- a/src/plopp/backends/matplotlib/mesh_image.py +++ /dev/null @@ -1,269 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) - -import uuid -from typing import Literal - -import numpy as np -import scipp as sc - -from ...core.utils import coord_as_bin_edges, merge_masks, repeat, scalar_to_string -from ...graphics.bbox import BoundingBox, axis_bounds -from ...graphics.colormapper import ColorMapper -from ..common import check_ndim -from .canvas import Canvas - - -def _find_dim_of_2d_coord(coords): - for xy, coord in coords.items(): - if coord['var'].ndim == 2: - return (xy, coord['dim']) - - -def _get_dims_of_1d_and_2d_coords(coords): - dim_2d = _find_dim_of_2d_coord(coords) - if dim_2d is None: - return None, None - axis_1d = 'xy'.replace(dim_2d[0], '') - dim_1d = (axis_1d, coords[axis_1d]['dim']) - return dim_1d, dim_2d - - -def _maybe_repeat_values(data, dim_1d, dim_2d): - if dim_2d is None: - return data - return repeat(data, dim=dim_1d[1], n=2)[dim_1d[1], :-1] - - -def _from_data_array_to_pcolormesh(data, coords, dim_1d, dim_2d): - z = _maybe_repeat_values(data=data, dim_1d=dim_1d, dim_2d=dim_2d) - if dim_2d is None: - return coords['x'], coords['y'], z - - # Broadcast 1d coord to 2d and repeat along 1d dim - # TODO: It may be more efficient here to first repeat and then broadcast, but - # the current order is simpler in implementation. - broadcasted_coord = repeat( - sc.broadcast( - coords[dim_1d[0]], - sizes={**coords[dim_2d[0]].sizes, **coords[dim_1d[0]].sizes}, - ).transpose(data.dims), - dim=dim_1d[1], - n=2, - ) - # Repeat 2d coord along 1d dim - repeated_coord = repeat(coords[dim_2d[0]].transpose(data.dims), dim=dim_1d[1], n=2) - out = {dim_1d[0]: broadcasted_coord[dim_1d[1], 1:-1], dim_2d[0]: repeated_coord} - return out['x'], out['y'], z - - -class MeshImage: - """ - Artist to represent two-dimensional data. - - Parameters - ---------- - canvas: - The canvas that will display the image. - colormapper: - The colormapper to use for the image. - data: - The initial data to create the image from. - artist_number: - The canvas keeps track of how many images have been added to it. This is unused - by the MeshImage artist. - uid: - The unique identifier of the artist. If None, a random UUID is generated. - shading: - The shading to use for the ``pcolormesh``. - rasterized: - Rasterize the mesh/image if ``True``. - **kwargs: - Additional arguments are forwarded to Matplotlib's ``pcolormesh``. - """ - - def __init__( - self, - canvas: Canvas, - colormapper: ColorMapper, - data: sc.DataArray, - artist_number: int, - uid: str | None = None, - shading: str = 'auto', - rasterized: bool = True, - **kwargs, - ): - check_ndim(data, ndim=2, origin='MeshImage') - self.uid = uid if uid is not None else uuid.uuid4().hex - self._canvas = canvas - self._colormapper = colormapper - self._ax = self._canvas.ax - self._data = data - # If the grid is visible on the axes, we need to set that on again after we - # call pcolormesh, because that turns the grid off automatically. - # See https://github.com/matplotlib/matplotlib/issues/15600. - need_grid = self._ax.xaxis.get_gridlines()[0].get_visible() - - to_dim_search = {} - string_labels = {} - bin_edge_coords = {} - self._data_with_bin_edges = sc.DataArray(data=self._data.data) - for i, k in enumerate('yx'): - to_dim_search[k] = { - 'dim': self._data.dims[i], - 'var': self._data.coords[self._data.dims[i]], - } - bin_edge_coords[k] = coord_as_bin_edges(self._data, self._data.dims[i]) - self._data_with_bin_edges.coords[self._data.dims[i]] = bin_edge_coords[k] - if self._data.coords[self._data.dims[i]].dtype == str: - string_labels[k] = self._data.coords[self._data.dims[i]] - - self._dim_1d, self._dim_2d = _get_dims_of_1d_and_2d_coords(to_dim_search) - self._mesh = None - - x, y, z = _from_data_array_to_pcolormesh( - data=self._data.data, - coords=bin_edge_coords, - dim_1d=self._dim_1d, - dim_2d=self._dim_2d, - ) - self._mesh = self._ax.pcolormesh( - x.values, - y.values, - z.values, - shading=shading, - rasterized=rasterized, - **kwargs, - ) - - self._colormapper.add_artist(self.uid, self) - self._mesh.set_array(None) - self._update_colors() - - for xy, var in string_labels.items(): - getattr(self._ax, f'set_{xy}ticks')(np.arange(float(var.shape[0]))) - getattr(self._ax, f'set_{xy}ticklabels')(var.values) - - if need_grid: - self._ax.grid(True) - - self._canvas.register_format_coord(self.format_coord) - - @property - def data(self): - """ - Get the Mesh's data in a form that may have been tweaked, compared to the - original data, in the case of a two-dimensional coordinate. - """ - out = sc.DataArray( - data=_maybe_repeat_values( - data=self._data.data, dim_1d=self._dim_1d, dim_2d=self._dim_2d - ) - ) - if self._data.masks: - out.masks['one_mask'] = _maybe_repeat_values( - data=sc.broadcast( - merge_masks(self._data.masks), sizes=self._data.sizes - ), - dim_1d=self._dim_1d, - dim_2d=self._dim_2d, - ) - return out - - def notify_artist(self, message: str) -> None: - """ - Receive notification from the colormapper that its state has changed. - We thus need to update the colors of the mesh. - - Parameters - ---------- - message: - The message from the colormapper. - """ - self._update_colors() - - def _update_colors(self): - """ - Update the mesh colors. - """ - rgba = self._colormapper.rgba(self.data) - self._mesh.set_facecolors(rgba.reshape(np.prod(rgba.shape[:-1]), 4)) - - def update(self, new_values: sc.DataArray): - """ - Update image array with new values. - - Parameters - ---------- - new_values: - New data to update the mesh values from. - """ - check_ndim(new_values, ndim=2, origin='MeshImage') - self._data = new_values - self._data_with_bin_edges.data = new_values.data - self._update_colors() - - def format_coord( - self, xslice: tuple[str, sc.Variable], yslice: tuple[str, sc.Variable] - ) -> str: - """ - Format the coordinates of the mouse pointer to show the value of the - data at that point. - - Parameters - ---------- - xslice: - Dimension and x coordinate of the mouse pointer, as slice parameters. - yslice: - Dimension and y coordinate of the mouse pointer, as slice parameters. - """ - try: - val = self._data_with_bin_edges[yslice][xslice] - prefix = self._data.name - if prefix: - prefix += ': ' - return prefix + scalar_to_string(val) - except (IndexError, RuntimeError): - return None - - @property - def visible(self) -> bool: - """ - The visibility of the image. - """ - return self._mesh.get_visible() - - @visible.setter - def visible(self, val: bool): - self._mesh.set_visible(val) - - @property - def opacity(self) -> float: - """ - The opacity of the image. - """ - return self._mesh.get_alpha() - - @opacity.setter - def opacity(self, val: float): - self._mesh.set_alpha(val) - - def bbox(self, xscale: Literal['linear', 'log'], yscale: Literal['linear', 'log']): - """ - The bounding box of the image. - """ - ydim, xdim = self._data.dims - image_x = self._data_with_bin_edges.coords[xdim] - image_y = self._data_with_bin_edges.coords[ydim] - - return BoundingBox( - **{**axis_bounds(('xmin', 'xmax'), image_x, xscale)}, - **{**axis_bounds(('ymin', 'ymax'), image_y, yscale)}, - ) - - def remove(self): - """ - Remove the image artist from the canvas. - """ - self._mesh.remove() - self._colormapper.remove_artist(self.uid) diff --git a/tests/backends/matplotlib/mpl_image_test.py b/tests/backends/matplotlib/mpl_image_test.py index c0f7aa3b0..1edc4c0dd 100644 --- a/tests/backends/matplotlib/mpl_image_test.py +++ b/tests/backends/matplotlib/mpl_image_test.py @@ -12,52 +12,47 @@ pytestmark = pytest.mark.usefixtures("_parametrize_mpl_backends") -def test_update_on_one_mesh_changes_colors_on_second_mesh(): - da1 = data_array(ndim=2, linspace=False) - da2 = 3.0 * data_array(ndim=2, linspace=False) - da2.coords['xx'] += sc.scalar(50.0, unit='m') - a = Node(da1) - b = Node(da2) - f = imagefigure(a, b) - old_b_colors = f.artists[b.id]._mesh.get_facecolors() - a.func = lambda: da1 * 2.1 - a.notify_children('updated a') - f.view.colormapper.autoscale() # Autoscale the colorbar limits - # No change because the update did not change the colorbar limits - assert np.allclose(old_b_colors, f.artists[b.id]._mesh.get_facecolors()) - a.func = lambda: da1 * 5.0 - a.notify_children('updated a') - f.view.colormapper.autoscale() # Autoscale the colorbar limits - assert not np.allclose(old_b_colors, f.artists[b.id]._mesh.get_facecolors()) - - -def test_update_on_one_mesh_changes_colors_on_second_image(): - da1 = data_array(ndim=2, linspace=True) - da2 = 3.0 * data_array(ndim=2, linspace=True) +@pytest.mark.parametrize('linspace', [True, False]) +def test_update_on_one_image_changes_colors_on_second_image(linspace): + da1 = data_array(ndim=2, linspace=linspace) + da2 = 3.0 * data_array(ndim=2, linspace=linspace) da2.coords['xx'] += sc.scalar(50.0, unit='m') a = Node(da1) b = Node(da2) f = imagefigure(a, b) - old_b_colors = f.artists[b.id]._image.get_array() + old_b_colors = getattr( + f.artists[b.id]._image, "get_array" if linspace else "get_facecolors" + )() a.func = lambda: da1 * 2.1 a.notify_children('updated a') f.view.colormapper.autoscale() # Autoscale the colorbar limits # No change because the update did not change the colorbar limits - assert np.allclose(old_b_colors, f.artists[b.id]._image.get_array()) + # colors = f.artists[b.id]._image.get_array() if linspace else + assert np.allclose( + old_b_colors, + getattr( + f.artists[b.id]._image, "get_array" if linspace else "get_facecolors" + )(), + ) a.func = lambda: da1 * 5.0 a.notify_children('updated a') f.view.colormapper.autoscale() # Autoscale the colorbar limits - assert not np.allclose(old_b_colors, f.artists[b.id]._image.get_array()) + assert not np.allclose( + old_b_colors, + getattr( + f.artists[b.id]._image, "get_array" if linspace else "get_facecolors" + )(), + ) def test_kwargs_are_forwarded_to_artist(): da = data_array(ndim=2, linspace=False) fig = imagefigure(Node(da), rasterized=True) [artist] = fig.artists.values() - assert artist._mesh.get_rasterized() + assert artist._image.get_rasterized() fig = imagefigure(Node(da), rasterized=False) [artist] = fig.artists.values() - assert not artist._mesh.get_rasterized() + assert not artist._image.get_rasterized() @pytest.mark.parametrize('linspace', [True, False])