Skip to content

Commit

Permalink
Merge pull request #311 from scipp/disable-custom-hover-for-datetimes
Browse files Browse the repository at this point in the history
Fix cursor hover when datetime axis is present in figure
  • Loading branch information
nvaytet authored Mar 13, 2024
2 parents abac0a1 + 382107e commit 14d037e
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 61 deletions.
51 changes: 41 additions & 10 deletions src/plopp/backends/matplotlib/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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) + "}"
Expand Down
52 changes: 9 additions & 43 deletions src/plopp/backends/matplotlib/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 += ': '
Expand Down
7 changes: 5 additions & 2 deletions src/plopp/backends/plotly/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion src/plopp/backends/pythreejs/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]):
"""
Expand Down
7 changes: 4 additions & 3 deletions src/plopp/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/plopp/graphics/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/plopp/graphics/lineview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="")
Expand Down
1 change: 1 addition & 0 deletions src/plopp/graphics/scatter3dview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 14d037e

Please sign in to comment.