diff --git a/src/plopp/backends/matplotlib/canvas.py b/src/plopp/backends/matplotlib/canvas.py index d5a82c34..ffa9027a 100644 --- a/src/plopp/backends/matplotlib/canvas.py +++ b/src/plopp/backends/matplotlib/canvas.py @@ -15,7 +15,7 @@ from .utils import fig_to_bytes, is_sphinx_build, make_figure -def _to_floats(x): +def _to_floats(x: np.ndarray) -> np.ndarray: return mdates.date2num(x) if np.issubdtype(x.dtype, np.datetime64) else x @@ -25,6 +25,25 @@ def _none_if_not_finite(x: Union[float, int, None]) -> Union[float, int, None]: return x if np.isfinite(x) else None +def _cursor_value_to_variable( + x: Union[float, int], dtype: sc.DType, unit: str +) -> sc.Variable: + if dtype == sc.DType.datetime64: + # Annoying chain of conversion but matplotlib has its own way of converting + # dates to numbers (number of days since epoch), and num2date returns a python + # datetime object, while scipp expects a numpy datetime64. + return sc.scalar(np.datetime64(mdates.num2date(x).replace(tzinfo=None))).to( + unit=unit + ) + return sc.scalar(x, unit=unit) + + +def _cursor_formatter(x: Union[float, int], dtype: sc.DType, unit: str) -> str: + if dtype == sc.DType.datetime64: + return mdates.num2date(x).replace(tzinfo=None).isoformat() + return scalar_to_string(sc.scalar(x, unit=unit)) + + class Canvas: """ Matplotlib-based canvas used to render 2D graphics. @@ -246,7 +265,7 @@ def show(self): """ self.fig.show() - def set_axes(self, dims, units): + def set_axes(self, dims, units, dtypes): """ Set the axes dimensions and units. @@ -256,11 +275,12 @@ def set_axes(self, dims, units): The dimensions of the data. units: The units of the data. + dtypes: + The data types of the data. """ self.units = units self.dims = dims - self._cursor_x_placeholder = sc.scalar(0.0, unit=self.units['x']) - self._cursor_y_placeholder = sc.scalar(0.0, unit=self.units['y']) + self.dtypes = dtypes self._cursor_x_prefix = '' self._cursor_y_prefix = '' if 'y' in self.dims: @@ -286,13 +306,24 @@ def format_coord(self, x: float, y: float) -> str: y: The y coordinate of the mouse pointer. """ - self._cursor_x_placeholder.value = x - self._cursor_y_placeholder.value = y - out = ( - f"({self._cursor_x_prefix}{scalar_to_string(self._cursor_x_placeholder)}, " - f"{self._cursor_y_prefix}{scalar_to_string(self._cursor_y_placeholder)})" + xstr = _cursor_formatter(x, self.dtypes['x'], self.units['x']) + ystr = _cursor_formatter(y, self.dtypes['y'], self.units['y']) + out = f"({self._cursor_x_prefix}{xstr}, {self._cursor_y_prefix}{ystr})" + if not self._coord_formatters: + return out + xpos = ( + self.dims['x'], + _cursor_value_to_variable(x, self.dtypes['x'], self.units['x']), + ) + ypos = ( + ( + self.dims['y'], + _cursor_value_to_variable(y, self.dtypes['y'], self.units['y']), + ) + if 'y' in self.dims + else None ) - extra = [formatter(x, y) for formatter in self._coord_formatters] + extra = [formatter(xpos, ypos) for formatter in self._coord_formatters] extra = [e for e in extra if e is not None] if extra: out += ": {" + ", ".join(extra) + "}" diff --git a/src/plopp/backends/matplotlib/image.py b/src/plopp/backends/matplotlib/image.py index c3b641da..7aed9697 100644 --- a/src/plopp/backends/matplotlib/image.py +++ b/src/plopp/backends/matplotlib/image.py @@ -2,6 +2,7 @@ # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) import uuid +from typing import Tuple import numpy as np import scipp as sc @@ -132,29 +133,6 @@ def __init__( if need_grid: self._ax.grid(True) - # Cache slicing order for hover values - if self._dim_1d is not None: - # If there is a 2d coord, we first slice the 1d coord, and then the - # dimension that is left should also be 1d, making value-based slicing - # possible. - self._hover_slicing = { - 'dir': (self._dim_1d[0], self._dim_2d[0]), - 'dim': (self._dim_1d[1], self._dim_2d[1]), - 'unit': ( - self._data.coords[self._dim_1d[1]].unit, - self._data.coords[self._dim_2d[1]].unit, - ), - } - else: - self._hover_slicing = { - 'dir': ('y', 'x'), - 'dim': (self._data.dims[0], self._data.dims[1]), - 'unit': ( - self._data.coords[self._data.dims[0]].unit, - self._data.coords[self._data.dims[1]].unit, - ), - } - self._canvas.register_format_coord(self.format_coord) @property @@ -204,34 +182,22 @@ def update(self, new_values: sc.DataArray): self._data = new_values self._data_with_bin_edges.data = new_values.data - def format_coord(self, x: float, y: float) -> str: + 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 ---------- - x: - The x coordinate of the mouse pointer. - y: - The y coordinate of the mouse pointer. + xslice: + Dimension and x coordinate of the mouse pointer, as slice parameters. + yslice: + Dimension and y coordinate of the mouse pointer, as slice parameters. """ - xy = {'x': x, 'y': y} try: - val = self._data_with_bin_edges[ - self._hover_slicing['dim'][0], - sc.scalar( - xy[self._hover_slicing['dir'][0]], - unit=self._hover_slicing['unit'][0], - ), - ] - val = val[ - self._hover_slicing['dim'][1], - sc.scalar( - xy[self._hover_slicing['dir'][1]], - unit=self._hover_slicing['unit'][1], - ), - ] + val = self._data_with_bin_edges[yslice][xslice] prefix = self._data.name if prefix: prefix += ': ' diff --git a/src/plopp/backends/plotly/canvas.py b/src/plopp/backends/plotly/canvas.py index 18e10bdc..e7f9d8ef 100644 --- a/src/plopp/backends/plotly/canvas.py +++ b/src/plopp/backends/plotly/canvas.py @@ -151,7 +151,7 @@ def save(self, filename: str): else: self.fig.write_image(filename) - def set_axes(self, dims, units): + def set_axes(self, dims, units, dtypes): """ Set the axes dimensions and units. @@ -161,9 +161,12 @@ def set_axes(self, dims, units): The dimensions of the data. units: The units of the data. + dtypes: + The data types of the data. """ - self.units = units self.dims = dims + self.units = units + self.dtypes = dtypes @property def empty(self) -> bool: diff --git a/src/plopp/backends/pythreejs/canvas.py b/src/plopp/backends/pythreejs/canvas.py index e78e140d..43809391 100644 --- a/src/plopp/backends/pythreejs/canvas.py +++ b/src/plopp/backends/pythreejs/canvas.py @@ -66,7 +66,7 @@ def __init__( def to_widget(self): return self.renderer - def set_axes(self, dims, units): + def set_axes(self, dims, units, dtypes): """ Set the axes dimensions and units. @@ -76,9 +76,12 @@ def set_axes(self, dims, units): The dimensions of the data. units: The units of the data. + dtypes: + The data types of the data. """ self.units = units self.dims = dims + self.dtypes = dtypes def make_outline(self, limits: Tuple[sc.Variable, sc.Variable, sc.Variable]): """ diff --git a/src/plopp/core/utils.py b/src/plopp/core/utils.py index 8e042e33..50333222 100644 --- a/src/plopp/core/utils.py +++ b/src/plopp/core/utils.py @@ -171,9 +171,10 @@ def scalar_to_string(var: sc.Variable, precision: int = 3) -> str: precision: The number of decimal places to use for the string output. """ - return value_to_string(var.value, precision=precision) + ( - f" {var.unit}" if var.unit not in (None, "") else "" - ) + out = value_to_string(var.value, precision=precision) + if (var.unit not in (None, "")) and var.dtype != sc.DType.datetime64: + out += f" {var.unit}" + return out def merge_masks(masks: Dict[str, sc.Variable]) -> sc.Variable: diff --git a/src/plopp/graphics/imageview.py b/src/plopp/graphics/imageview.py index a1b7090c..bb232cbd 100644 --- a/src/plopp/graphics/imageview.py +++ b/src/plopp/graphics/imageview.py @@ -128,7 +128,9 @@ def update(self, new_values: sc.DataArray, key: str): ycoord = new_values.coords[ydim] if self.canvas.empty: self.canvas.set_axes( - dims={'x': xdim, 'y': ydim}, units={'x': xcoord.unit, 'y': ycoord.unit} + dims={'x': xdim, 'y': ydim}, + units={'x': xcoord.unit, 'y': ycoord.unit}, + dtypes={'x': xcoord.dtype, 'y': ycoord.dtype}, ) self.canvas.xlabel = name_with_unit(var=xcoord, name=xdim) self.canvas.ylabel = name_with_unit(var=ycoord, name=ydim) diff --git a/src/plopp/graphics/lineview.py b/src/plopp/graphics/lineview.py index ad8c2a10..11f334c0 100644 --- a/src/plopp/graphics/lineview.py +++ b/src/plopp/graphics/lineview.py @@ -131,7 +131,9 @@ def update(self, new_values: sc.DataArray, key: str): xcoord = new_values.coords[xdim] if self.canvas.empty: self.canvas.set_axes( - dims={'x': xdim}, units={'x': xcoord.unit, 'y': new_values.unit} + dims={'x': xdim}, + units={'x': xcoord.unit, 'y': new_values.unit}, + dtypes={'x': xcoord.dtype, 'y': new_values.dtype}, ) self.canvas.xlabel = name_with_unit(var=xcoord) self.canvas.ylabel = name_with_unit(var=new_values.data, name="") diff --git a/src/plopp/graphics/scatter3dview.py b/src/plopp/graphics/scatter3dview.py index f3f450a4..71dff2be 100644 --- a/src/plopp/graphics/scatter3dview.py +++ b/src/plopp/graphics/scatter3dview.py @@ -106,6 +106,7 @@ def update(self, new_values: sc.DataArray, key: str): self.canvas.set_axes( dims=mapping, units={x: new_values.coords[dim].unit for x, dim in mapping.items()}, + dtypes={x: new_values.coords[dim].dtype for x, dim in mapping.items()}, ) self.colormapper.unit = new_values.unit else: