Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
59e0d34
add new combined slider for slicer
nvaytet Feb 18, 2026
70118f5
add reduce nodes
nvaytet Feb 18, 2026
11ffc99
fix reduce dims
nvaytet Feb 18, 2026
15a9a69
remove maybe reduce function
nvaytet Feb 18, 2026
9ef00cf
add editable text box for bounds
nvaytet Feb 25, 2026
93a047a
add operations along sliced dimension
nvaytet Feb 26, 2026
d29c47d
start fixing tests
nvaytet Feb 26, 2026
4f6202a
use bounded text boxes for bounds
nvaytet Feb 27, 2026
7c74a9e
issues with linking bounds
nvaytet Feb 27, 2026
c77a432
use link instead of jslink to ensure everything is properly in sync o…
nvaytet Feb 27, 2026
6ac34fc
update docs thumbnail
nvaytet Feb 27, 2026
7a5405e
add tests
nvaytet Feb 27, 2026
a5e0431
spelling
nvaytet Feb 27, 2026
cb8cabd
properly use value widgets
nvaytet Feb 27, 2026
e996db4
fix single slider, still issues with range slider
nvaytet Feb 27, 2026
64ade6d
use Any as traitlet and try to debug bound limits
nvaytet Feb 27, 2026
9e439f8
do not go the ValueWidget way in the end, as the locking gets too messy
nvaytet Feb 27, 2026
1b7dbb1
new plan: drop the valuewidget thing and make a widget with a single …
nvaytet Mar 2, 2026
99a16d6
use custom text widget instead of in-built bounded float text
nvaytet Mar 2, 2026
3171166
debugging bin edges single widget
nvaytet Mar 3, 2026
10d50a4
fix bin edges and range widget
nvaytet Mar 3, 2026
5b55d97
squeeze if possible and fix bug in nanmean
nvaytet Mar 4, 2026
c2d0dcd
fixes for datetime types
nvaytet Mar 4, 2026
f430d54
add tests for datetime and binedges
nvaytet Mar 4, 2026
be6af6a
cleanup
nvaytet Mar 4, 2026
e35c8b8
fix slider mode
nvaytet Mar 4, 2026
1e8f5ba
Merge branch 'main' into combined-slider
nvaytet Mar 4, 2026
cddf732
Apply automatic formatting
pre-commit-ci-lite[bot] Mar 4, 2026
12d1790
fix one test, remove another because it would involve too many implem…
nvaytet Mar 4, 2026
87e9c78
revert removed tests
nvaytet Mar 4, 2026
d8646fd
fix mode arge
nvaytet Mar 4, 2026
1fb7d9c
change datetime unit in xarray data
nvaytet Mar 4, 2026
b236cd5
fix manually setting bounds with datetime binedge single slider
nvaytet Mar 5, 2026
6af4e72
Merge branch 'main' into combined-slider
nvaytet Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/_static/plotting/slicer-plot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion docs/getting-started/numpy-pandas-xarray.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@
"# The latitude and longitude coordinates have units unrecognised by Scipp\n",
"air.coords['lat'].attrs['units'] = 'degrees'\n",
"air.coords['lon'].attrs['units'] = 'degrees'\n",
"# Optional: change the datetimes from ns to s for better formatting\n",
"air.coords['time'] = air.coords['time'].astype('datetime64[s]')\n",
"air"
]
},
Expand Down Expand Up @@ -280,7 +282,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
"version": "3.12.7"
}
},
"nbformat": 4,
Expand Down
17 changes: 10 additions & 7 deletions docs/plotting/slicer-plot.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
"outputs": [],
"source": [
"%matplotlib widget\n",
"import scipp as sc\n",
"import plopp as pp\n",
"from plopp.data.examples import clusters3d\n",
"\n",
"da = pp.data.data3d()"
"da = clusters3d(nclusters=50, seed=12).hist(z=100, y=100, x=100)"
]
},
{
Expand All @@ -41,7 +43,7 @@
"metadata": {},
"outputs": [],
"source": [
"pp.slicer(da, keep=['x', 'y'])"
"pp.slicer(da, keep=['x', 'y'], logc=True)"
]
},
{
Expand Down Expand Up @@ -82,7 +84,7 @@
"metadata": {},
"outputs": [],
"source": [
"pp.slicer(da * da.coords['z'], keep=['x'], autoscale=False)"
"pp.slicer(da * sc.arange('z', 100), keep=['x'], autoscale=False)"
]
},
{
Expand All @@ -102,8 +104,9 @@
"\n",
"</div>\n",
"\n",
"It is possible to display some animation controls (play button) next to the slider,\n",
"by using the `enable_player=True` option:"
"It is possible to display some animation controls (play button) next to the slider, by using the `enable_player=True` option.\n",
"\n",
"Note that we need to use a slider with a single handle via `mode='single'` for this to work:"
]
},
{
Expand All @@ -113,7 +116,7 @@
"metadata": {},
"outputs": [],
"source": [
"pp.slicer(da, keep=['x', 'y'], enable_player=True)"
"pp.slicer(da, keep=['x', 'y'], logc=True, enable_player=True, mode='single')"
]
},
{
Expand Down Expand Up @@ -146,7 +149,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
"version": "3.12.12"
}
},
"nbformat": 4,
Expand Down
103 changes: 91 additions & 12 deletions src/plopp/plotting/slicer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from itertools import groupby
from typing import Literal

import numpy as np
import scipp as sc

from ..core import widget_node
from ..core import Node, widget_node
from ..core.typing import FigureLike, PlottableMulti
from ..graphics import imagefigure, linefigure
from .common import (
Expand All @@ -19,18 +20,42 @@
)


def _maybe_reduce_dim(da, dims, op):
to_be_reduced = set(dims) & set(da.dims)

# Small optimization: squeezing is much faster than reducing
to_be_squeezed = {dim for dim in to_be_reduced if da.sizes[dim] == 1}
if to_be_squeezed:
da = da.squeeze()
to_be_reduced -= to_be_squeezed

if not to_be_reduced:
return da

if 'mean' not in op:
return getattr(da, op)(to_be_reduced)

# If the operation is a mean, there is currently a bug in the implementation
# in scipp where doing a mean over a subset of the array's dimensions gives the
# wrong result: https://github.com/scipp/scipp/issues/3841
# Instead, we manually compute the mean
if 'nan' in op:
numerator = da.nansum(to_be_reduced)
denominator = (~sc.isnan(da)).to(dtype=int).sum(to_be_reduced)
else:
numerator = da.sum(to_be_reduced)
denominator = np.prod([da.sizes[dim] for dim in to_be_reduced])
return numerator / denominator


class Slicer:
"""
Class that slices out dimensions from the data and displays the resulting data as
either a 1D line or a 2D image.

Note:

This class primarily exists to facilitate unit testing. When running unit tests, we
are not in a Jupyter notebook, and the generated figures are not widgets that can
be placed in the `Box` widget container at the end of the `slicer` function.
We therefore place most of the code for creating a Slicer in this class, which is
under unit test coverage. The thin `slicer` wrapper is not covered by unit tests.
This class exists both for simplifying unit tests and for reuse by other plotting
functions that want to offer slicing functionality,
such as the :func:`superplot` function.

Parameters
----------
Expand All @@ -46,6 +71,11 @@ class Slicer:
be a list of dims. If no dims are provided, the last dim will be kept in the
case of a 2-dimensional input, while the last two dims will be kept in the case
of higher dimensional inputs.
mode:
The mode of the slicer. This can be 'single', 'range', or 'combined'.
operation:
The reduction operation to be applied to the sliced dimensions. This is ``sum``
by default.
**kwargs:
The additional arguments are forwarded to the underlying 1D or 2D figures.
"""
Expand All @@ -57,8 +87,17 @@ def __init__(
coords: list[str] | None = None,
enable_player: bool = False,
keep: list[str] | None = None,
mode: Literal['single', 'range', 'combined'] = 'combined',
operation: Literal[
'sum', 'mean', 'max', 'min', 'nansum', 'nanmean', 'nanmax', 'nanmin'
] = 'sum',
**kwargs,
):
if enable_player and mode != 'single':
raise ValueError(
'The play button cannot be used with range sliders. Please set '
'mode to "single" to use the play button.'
)
nodes = input_to_nodes(
obj,
processor=partial(preprocess, ignore_size=True, coords=coords),
Expand Down Expand Up @@ -93,15 +132,39 @@ def __init__(
f"were not found in the input's dimensions {dims}."
)

from ..widgets import SliceWidget, slice_dims
from ..widgets import (
CombinedSliceWidget,
RangeSliceWidget,
SliceWidget,
slice_dims,
)

other_dims = [dim for dim in dims if dim not in keep]

match mode:
case 'single':
slicer_constr = SliceWidget
case 'range':
slicer_constr = RangeSliceWidget
case 'combined':
slicer_constr = CombinedSliceWidget
case _:
raise ValueError(
f"Invalid mode: {mode}. Expected one of 'single', "
f"'range', or 'combined'."
)

self.slider = SliceWidget(
self.slider = slicer_constr(
nodes[0](),
dims=[dim for dim in dims if dim not in keep],
dims=other_dims,
enable_player=enable_player,
)
self.slider_node = widget_node(self.slider)
self.slice_nodes = [slice_dims(node, self.slider_node) for node in nodes]
self.reduce_nodes = [
Node(_maybe_reduce_dim, da=node, dims=other_dims, op=operation)
for node in self.slice_nodes
]

args = categorize_args(**kwargs)

Expand All @@ -118,7 +181,7 @@ def __init__(
f'but {ndims} were requested.'
)

self.figure = make_figure(*self.slice_nodes)
self.figure = make_figure(*self.reduce_nodes)
require_interactive_figure(self.figure, 'slicer')
self.figure.bottom_bar.add(self.slider)

Expand Down Expand Up @@ -147,7 +210,11 @@ def slicer(
mask_color: str | None = None,
nan_color: str | None = None,
norm: Literal['linear', 'log'] | None = None,
operation: Literal[
'sum', 'mean', 'max', 'min', 'nansum', 'nanmean', 'nanmax', 'nanmin'
] = 'sum',
scale: dict[str, str] | None = None,
mode: Literal['single', 'range', 'combined'] = 'combined',
title: str | None = None,
vmax: sc.Variable | float | None = None,
vmin: sc.Variable | float | None = None,
Expand Down Expand Up @@ -209,11 +276,20 @@ def slicer(
Colormap to use for masks in 2d plots.
mask_color:
Color of masks.
mode:
The type of slider to use for slicing. Can be either ``'single'`` for sliders
that select a single index along the sliced dimension, ``'range'`` for sliders
that select a range of indices along the sliced dimension, or ``'combined'`` for
sliders that allow both single index selection and range selection.
Defaults to ``'combined'``.
nan_color:
Color to use for NaN values in 2d plots.
norm:
Set to ``'log'`` for a logarithmic y-axis (1d plots) or logarithmic colorscale
(2d plots). Legacy, prefer ``logy`` and ``logc`` instead.
operation:
The reduction operation to be applied to the sliced dimensions. This is ``sum``
by default.
scale:
Change axis scaling between ``log`` and ``linear``. For example, specify
``scale={'time': 'log'}`` if you want log-scale for the ``time`` dimension.
Expand Down Expand Up @@ -261,8 +337,11 @@ def slicer(
logx=logx,
logy=logy,
mask_color=mask_color,
mask_cmap=mask_cmap,
mode=mode,
nan_color=nan_color,
norm=norm,
operation=operation,
scale=scale,
title=title,
vmax=vmax,
Expand Down
1 change: 1 addition & 0 deletions src/plopp/plotting/superplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def superplot(
slicer = Slicer(
obj,
keep=keep,
mode='single',
aspect=aspect,
autoscale=autoscale,
coords=coords,
Expand Down
3 changes: 2 additions & 1 deletion src/plopp/widgets/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from .checkboxes import Checkboxes
from .clip3d import Clip3dTool, ClippingManager
from .drawing import DrawingTool, PointsTool, PolygonTool, RectangleTool
from .linesave import LineSaveTool
from .slice import RangeSliceWidget, SliceWidget, slice_dims
from .slicing import CombinedSliceWidget, RangeSliceWidget, SliceWidget, slice_dims
from .toolbar import Toolbar, make_toolbar_canvas2d, make_toolbar_canvas3d
from .tools import ButtonTool, ColorTool, ToggleTool

Expand All @@ -17,6 +17,7 @@ __all__ = [
"Clip3dTool",
"ClippingManager",
"ColorTool",
"CombinedSliceWidget",
"DrawingTool",
"HBar",
"LineSaveTool",
Expand Down
Loading
Loading