plopp 25.11.0__py3-none-any.whl → 26.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plopp/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # SPDX-License-Identifier: BSD-3-Clause
2
2
  # Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3
- # ruff: noqa: E402, F401, I
3
+ # ruff: noqa: RUF100, E402, F401, I
4
4
 
5
5
  import importlib.metadata
6
6
 
@@ -112,7 +112,7 @@ class Canvas:
112
112
  grid: bool = False,
113
113
  user_vmin: sc.Variable | float | None = None,
114
114
  user_vmax: sc.Variable | float | None = None,
115
- aspect: Literal['auto', 'equal', None] = None,
115
+ aspect: Literal['auto', 'equal'] | None = None,
116
116
  cbar: bool = False,
117
117
  legend: bool | tuple[float, float] = True,
118
118
  xmin: sc.Variable | float | None = None,
@@ -123,7 +123,7 @@ class Canvas:
123
123
  logy: bool = False,
124
124
  xlabel: str | None = None,
125
125
  ylabel: str | None = None,
126
- norm: Literal['linear', 'log', None] = None,
126
+ norm: Literal['linear', 'log'] | None = None,
127
127
  **ignored,
128
128
  ):
129
129
  # Note on the `**ignored`` keyword arguments: the figure which owns the canvas
@@ -515,7 +515,7 @@ class Canvas:
515
515
  """
516
516
  self.fig.canvas.toolbar.pan()
517
517
 
518
- def panzoom(self, value: Literal['pan', 'zoom', None]):
518
+ def panzoom(self, value: Literal['pan', 'zoom'] | None):
519
519
  """
520
520
  Activate or deactivate the pan or zoom tool, depending on the input value.
521
521
  """
@@ -46,7 +46,7 @@ class Canvas:
46
46
  logy: bool | None = None,
47
47
  xlabel: str | None = None,
48
48
  ylabel: str | None = None,
49
- norm: Literal['linear', 'log', None] = None,
49
+ norm: Literal['linear', 'log'] | None = None,
50
50
  **ignored,
51
51
  ):
52
52
  # Note on the `**ignored`` keyword arguments: the figure which owns the canvas
@@ -326,7 +326,7 @@ class Canvas:
326
326
  """
327
327
  self.fig.update_layout(dragmode='pan')
328
328
 
329
- def panzoom(self, value: Literal['pan', 'zoom', None]):
329
+ def panzoom(self, value: Literal['pan', 'zoom'] | None):
330
330
  """
331
331
  Activate or deactivate the pan or zoom tool, depending on the input value.
332
332
  """
@@ -55,7 +55,12 @@ def _get_cmap(colormap: str | Colormap, nan_color: str | None = None) -> Colorma
55
55
  else:
56
56
  cmap = mpl.cm.get_cmap(colormap)
57
57
  except (KeyError, ValueError):
58
+ # Case where we have just a single color
58
59
  cmap = LinearSegmentedColormap.from_list('tmp', [colormap, colormap])
60
+ cmap.set_over(colormap)
61
+ cmap.set_under(colormap)
62
+ cmap.set_bad(colormap)
63
+ return cmap
59
64
 
60
65
  # Add under and over values to the cmap
61
66
  delta = 0.15
@@ -141,7 +146,7 @@ class ColorMapper:
141
146
  clabel: str | None = None,
142
147
  nan_color: str | None = None,
143
148
  figsize: tuple[float, float] | None = None,
144
- norm: Literal['linear', 'log', None] = None,
149
+ norm: Literal['linear', 'log'] | None = None,
145
150
  vmin: sc.Variable | float | None = None,
146
151
  vmax: sc.Variable | float | None = None,
147
152
  ):
@@ -152,9 +157,14 @@ class ColorMapper:
152
157
  self._canvas = canvas
153
158
  self.cax = self._canvas.cax if hasattr(self._canvas, 'cax') else None
154
159
  self.cmap = _get_cmap(cmap, nan_color=nan_color)
155
- self.mask_cmap = _get_cmap(
156
- mask_cmap if mask_color is None else mask_color, nan_color=nan_color
157
- )
160
+ self.mask_cmap = _get_cmap(mask_cmap if mask_color is None else mask_color)
161
+ if mask_color is None:
162
+ # If no single color was used for masks, it means a colormap was used
163
+ # instead. However, `nan_color` was not passed to get_cmap for the masks
164
+ # (it only applied to non-masked data), so we still need to set the 'bad'
165
+ # color here. We choose to set it to the 'over' color for a lack of a
166
+ # better idea.
167
+ self.mask_cmap.set_bad(self.mask_cmap.get_over())
158
168
 
159
169
  # Inside the autoscale, we need to distinguish between a min value that was set
160
170
  # by the user and one that was found by looping over all the data.
@@ -51,11 +51,11 @@ class GraphicalView(View):
51
51
  mask_cmap: str = 'gray',
52
52
  mask_color: str | None = None,
53
53
  cbar: bool = False,
54
- norm: Literal['linear', 'log', None] = None,
54
+ norm: Literal['linear', 'log'] | None = None,
55
55
  vmin: sc.Variable | float | None = None,
56
56
  vmax: sc.Variable | float | None = None,
57
57
  scale: dict[str, str] | None = None,
58
- aspect: Literal['auto', 'equal', None] = None,
58
+ aspect: Literal['auto', 'equal'] | None = None,
59
59
  grid: bool = False,
60
60
  title: str | None = None,
61
61
  figsize: tuple[float, float] | None = None,
@@ -312,5 +312,6 @@ class GraphicalView(View):
312
312
  self.artists[key].remove()
313
313
  del self.artists[key]
314
314
  self.canvas.update_legend()
315
- self.fit_to_data()
316
- self.canvas.draw()
315
+ if self._autoscale:
316
+ self.fit_to_data()
317
+ self.canvas.draw()
plopp/plotting/common.py CHANGED
@@ -319,7 +319,7 @@ def input_to_nodes(obj: PlottableMulti, processor: Callable) -> list[Node]:
319
319
  to_nodes = obj.items()
320
320
  else:
321
321
  to_nodes = [(getattr(obj, "name", None), obj)]
322
- nodes = [Node(processor, inp, name=name) for name, inp in to_nodes]
322
+ nodes = [Node(processor, inp, name=str(name)) for name, inp in to_nodes]
323
323
  for node in nodes:
324
324
  if hasattr(processor, 'func'):
325
325
  node.pretty_name = processor.func.__name__
@@ -347,7 +347,7 @@ def raise_multiple_inputs_for_2d_plot_error(origin):
347
347
 
348
348
 
349
349
  def categorize_args(
350
- aspect: Literal['auto', 'equal', None] = None,
350
+ aspect: Literal['auto', 'equal'] | None = None,
351
351
  autoscale: bool = True,
352
352
  cbar: bool = True,
353
353
  clabel: str | None = None,
@@ -364,7 +364,7 @@ def categorize_args(
364
364
  mask_cmap: str = 'gray',
365
365
  mask_color: str = 'black',
366
366
  nan_color: str | None = None,
367
- norm: Literal['linear', 'log', None] = None,
367
+ norm: Literal['linear', 'log'] | None = None,
368
368
  scale: dict[str, str] | None = None,
369
369
  title: str | None = None,
370
370
  vmax: sc.Variable | float | None = None,
@@ -3,6 +3,7 @@
3
3
 
4
4
  from typing import Literal
5
5
 
6
+ import numpy as np
6
7
  import scipp as sc
7
8
 
8
9
  from ..core import Node
@@ -34,11 +35,40 @@ def _slice_xy(da: sc.DataArray, xy: dict[str, dict[str, int]]) -> sc.DataArray:
34
35
  return da[y['dim'], y['value']][x['dim'], x['value']]
35
36
 
36
37
 
38
+ def _coord_to_centers(da: sc.DataArray, dim: str) -> sc.Variable:
39
+ coord = da.coords[dim]
40
+ if da.coords.is_edges(dim, dim=dim):
41
+ return sc.midpoints(coord, dim=dim)
42
+ return coord
43
+
44
+
45
+ def _mask_outside_polygon(
46
+ da: sc.DataArray, poly: dict[str, dict[str, sc.Variable]]
47
+ ) -> sc.DataArray:
48
+ from matplotlib.path import Path
49
+
50
+ xdim = poly['x']['dim']
51
+ ydim = poly['y']['dim']
52
+ x = _coord_to_centers(da, xdim)
53
+ y = _coord_to_centers(da, ydim)
54
+ vx = poly['x']['value'].to(unit=x.unit).values
55
+ vy = poly['y']['value'].to(unit=y.unit).values
56
+ verts = np.column_stack([vx, vy])
57
+ path = Path(verts)
58
+ xx = sc.broadcast(x, sizes={**x.sizes, **y.sizes})
59
+ yy = sc.broadcast(y, sizes={**x.sizes, **y.sizes})
60
+ points = np.column_stack([xx.values.ravel(), yy.values.ravel()])
61
+ inside = sc.array(
62
+ dims=yy.dims, values=path.contains_points(points).reshape(yy.shape)
63
+ )
64
+ return da.assign_masks(__inside_polygon=~inside).sum({*x.dims, *y.dims})
65
+
66
+
37
67
  def inspector(
38
68
  obj: Plottable,
39
69
  dim: str | None = None,
40
70
  *,
41
- aspect: Literal['auto', 'equal', None] = None,
71
+ aspect: Literal['auto', 'equal'] | None = None,
42
72
  autoscale: bool = True,
43
73
  cbar: bool = True,
44
74
  clabel: str | None = None,
@@ -49,17 +79,19 @@ def inspector(
49
79
  grid: bool = False,
50
80
  legend: bool | tuple[float, float] = True,
51
81
  logc: bool | None = None,
52
- logy: bool | None = None,
53
82
  mask_cmap: str = 'gray',
54
83
  mask_color: str = 'black',
84
+ mode: Literal['point', 'polygon'] = 'point',
55
85
  nan_color: str | None = None,
56
- norm: Literal['linear', 'log', None] = None,
86
+ norm: Literal['linear', 'log'] | None = None,
57
87
  operation: Literal['sum', 'mean', 'min', 'max'] = 'sum',
58
88
  orientation: Literal['horizontal', 'vertical'] = 'horizontal',
59
89
  title: str | None = None,
60
90
  vmax: sc.Variable | float | None = None,
61
91
  vmin: sc.Variable | float | None = None,
62
92
  xlabel: str | None = None,
93
+ xmax: sc.Variable | float | None = None,
94
+ xmin: sc.Variable | float | None = None,
63
95
  ylabel: str | None = None,
64
96
  ymax: sc.Variable | float | None = None,
65
97
  ymin: sc.Variable | float | None = None,
@@ -69,16 +101,29 @@ def inspector(
69
101
  Inspector takes in a three-dimensional input and applies a reduction operation
70
102
  (``'sum'`` by default) along one of the dimensions specified by ``dim``.
71
103
  It displays the result as a two-dimensional image.
72
- In addition, an 'inspection' tool is available in the toolbar which allows to place
73
- markers on the image which perform slicing at that position to retain only the third
74
- dimension and displays the resulting one-dimensional slice on the right hand side
75
- figure.
76
-
77
- Controls:
78
- - Click to make new point
79
- - Drag existing point to move it
104
+ In addition, an 'inspection' tool is available in the toolbar. In ``mode='point'``
105
+ it allows placing point markers on the image to slice at that position, retaining
106
+ only the third dimension and displaying the resulting one-dimensional slice in the
107
+ right-hand side figure. In ``mode='polygon'`` it allows drawing a polygon to compute
108
+ the total intensity inside the polygon as a function of the third dimension.
109
+
110
+ Controls (point mode):
111
+ - Left-click to make new points
112
+ - Left-click and hold on point to move point
80
113
  - Middle-click to delete point
81
114
 
115
+ Controls (polygon mode):
116
+ - Left-click to make new polygons
117
+ - Left-click and hold on polygon vertex to move vertex
118
+ - Right-click and hold to drag/move the entire polygon
119
+ - Middle-click to delete polygon
120
+
121
+ Notes
122
+ -----
123
+
124
+ Almost all the arguments for plot customization apply to the two-dimensional image
125
+ (unless specified).
126
+
82
127
  Parameters
83
128
  ----------
84
129
  obj:
@@ -93,13 +138,13 @@ def inspector(
93
138
  autoscale:
94
139
  Automatically scale the axes/colormap on updates if ``True``.
95
140
  cbar:
96
- Show colorbar if ``True`` (2d figure).
141
+ Show colorbar if ``True``.
97
142
  clabel:
98
- Label for colorscale (2d figure).
143
+ Label for colorscale.
99
144
  cmax:
100
- Upper limit for colorscale (2d figure).
145
+ Upper limit for colorscale.
101
146
  cmin:
102
- Lower limit for colorscale (2d figure).
147
+ Lower limit for colorscale.
103
148
  errorbars:
104
149
  Show errorbars if ``True`` (1d figure).
105
150
  figsize:
@@ -111,18 +156,18 @@ def inspector(
111
156
  ``(x, y)`` coordinates of the legend's anchor point in axes coordinates
112
157
  (1d figure).
113
158
  logc:
114
- If ``True``, use logarithmic scale for colorscale (2d figure).
115
- logy:
116
- If ``True``, use logarithmic scale for y-axis (1d figure).
159
+ If ``True``, use logarithmic scale for colorscale.
117
160
  mask_cmap:
118
- Colormap to use for masks in 2d figure.
161
+ Colormap to use for masks.
119
162
  mask_color:
120
163
  Color of masks (overrides ``mask_cmap``).
164
+ mode:
165
+ Select ``'point'`` for point inspection or ``'polygon'`` for polygon selection
166
+ with total intensity inside the polygon plotted as a function of ``dim``.
121
167
  nan_color:
122
- Color to use for NaN values in 2d figure.
168
+ Color to use for NaN values.
123
169
  norm:
124
- Set to ``'log'`` for a logarithmic y-axis (1d figure) or logarithmic colorscale
125
- (2d figure). Legacy, prefer ``logy`` and ``logc`` instead.
170
+ Set to ``'log'`` for a logarithmic colorscale. Legacy, prefer ``logc`` instead.
126
171
  operation:
127
172
  The operation to apply along the third (undisplayed) dimension specified by
128
173
  ``dim``.
@@ -132,15 +177,19 @@ def inspector(
132
177
  title:
133
178
  The figure title.
134
179
  vmax:
135
- Upper limit for data to be displayed (y-axis for 1d figure, colorscale for
136
- 2d figure). Legacy, prefer ``ymax`` and ``cmax`` instead.
180
+ Upper limit for data colorscale to be displayed.
181
+ Legacy, prefer ``cmax`` instead.
137
182
  vmin:
138
- Lower limit for data to be displayed (y-axis for 1d figure, colorscale for
139
- 2d figure). Legacy, prefer ``ymin`` and ``cmin`` instead.
183
+ Lower limit for data colorscale to be displayed.
184
+ Legacy, prefer ``cmin`` instead.
140
185
  xlabel:
141
- Label for x-axis (2d figure).
186
+ Label for x-axis.
187
+ xmax:
188
+ Upper limit for x-axis (1d figure)
189
+ xmin:
190
+ Lower limit for x-axis (1d figure)
142
191
  ylabel:
143
- Label for y-axis (2d figure).
192
+ Label for y-axis.
144
193
  ymax:
145
194
  Upper limit for y-axis (1d figure).
146
195
  ymin:
@@ -159,11 +208,9 @@ def inspector(
159
208
  errorbars=errorbars,
160
209
  grid=grid,
161
210
  legend=legend,
162
- logy=logy,
163
211
  mask_color=mask_color,
164
- norm=norm,
165
- vmax=vmax,
166
- vmin=vmin,
212
+ xmax=xmax,
213
+ xmin=xmin,
167
214
  ymax=ymax,
168
215
  ymin=ymin,
169
216
  )
@@ -190,6 +237,7 @@ def inspector(
190
237
  figsize=figsize,
191
238
  grid=grid,
192
239
  logc=logc,
240
+ mask_cmap=mask_cmap,
193
241
  mask_color=mask_color,
194
242
  nan_color=nan_color,
195
243
  norm=norm,
@@ -198,21 +246,30 @@ def inspector(
198
246
  vmin=vmin,
199
247
  xlabel=xlabel,
200
248
  ylabel=ylabel,
201
- ymax=ymax,
202
- ymin=ymin,
203
249
  **kwargs,
204
250
  )
251
+ from ..widgets import Box, PointsTool, PolygonTool
205
252
 
206
- from ..widgets import Box, PointsTool
253
+ if mode == 'point':
254
+ tool = PointsTool(
255
+ figure=f2d,
256
+ input_node=bin_edges_node,
257
+ func=_slice_xy,
258
+ destination=f1d,
259
+ tooltip="Activate inspector tool",
260
+ )
261
+ elif mode == 'polygon':
262
+ tool = PolygonTool(
263
+ figure=f2d,
264
+ input_node=bin_edges_node,
265
+ func=_mask_outside_polygon,
266
+ destination=f1d,
267
+ tooltip="Activate polygon inspector tool",
268
+ )
269
+ else:
270
+ raise ValueError(f'Mode "{mode}" is unknown.')
207
271
 
208
- pts = PointsTool(
209
- figure=f2d,
210
- input_node=bin_edges_node,
211
- func=_slice_xy,
212
- destination=f1d,
213
- tooltip="Activate inspector tool",
214
- )
215
- f2d.toolbar['inspect'] = pts
272
+ f2d.toolbar['inspect'] = tool
216
273
  out = [f2d, f1d]
217
274
  if orientation == 'horizontal':
218
275
  out = [out]
plopp/plotting/mesh3d.py CHANGED
@@ -53,7 +53,7 @@ def mesh3d(
53
53
  figsize: tuple[int, int] = (600, 400),
54
54
  logc: bool | None = None,
55
55
  nan_color: str | None = None,
56
- norm: Literal['linear', 'log', None] = None,
56
+ norm: Literal['linear', 'log'] | None = None,
57
57
  title: str | None = None,
58
58
  vmax: sc.Variable | float = None,
59
59
  vmin: sc.Variable | float = None,
plopp/plotting/plot.py CHANGED
@@ -19,7 +19,7 @@ from .common import (
19
19
  def plot(
20
20
  obj: PlottableMulti,
21
21
  *,
22
- aspect: Literal['auto', 'equal', None] = None,
22
+ aspect: Literal['auto', 'equal'] | None = None,
23
23
  autoscale: bool = True,
24
24
  cbar: bool = True,
25
25
  clabel: str | None = None,
@@ -38,7 +38,7 @@ def plot(
38
38
  mask_cmap: str = 'gray',
39
39
  mask_color: str | None = None,
40
40
  nan_color: str | None = None,
41
- norm: Literal['linear', 'log', None] = None,
41
+ norm: Literal['linear', 'log'] | None = None,
42
42
  scale: dict[str, str] | None = None,
43
43
  title: str | None = None,
44
44
  vmax: sc.Variable | float | None = None,
plopp/plotting/scatter.py CHANGED
@@ -46,9 +46,9 @@ def scatter(
46
46
  x: str = 'x',
47
47
  y: str = 'y',
48
48
  pos: str | None = None,
49
- aspect: Literal['auto', 'equal', None] = None,
49
+ aspect: Literal['auto', 'equal'] | None = None,
50
50
  autoscale: bool = True,
51
- cbar: bool = True,
51
+ cbar: bool = False,
52
52
  clabel: str | None = None,
53
53
  cmap: str = 'viridis',
54
54
  cmax: sc.Variable | float | None = None,
@@ -62,7 +62,7 @@ def scatter(
62
62
  logy: bool | None = None,
63
63
  mask_color: str = 'black',
64
64
  nan_color: str | None = None,
65
- norm: Literal['linear', 'log', None] = None,
65
+ norm: Literal['linear', 'log'] | None = None,
66
66
  scale: dict[str, str] | None = None,
67
67
  size: str | float | None = None,
68
68
  title: str | None = None,
@@ -53,7 +53,7 @@ def scatter3d(
53
53
  figsize: tuple[int, int] = (600, 400),
54
54
  logc: bool | None = None,
55
55
  nan_color: str | None = None,
56
- norm: Literal['linear', 'log', None] = None,
56
+ norm: Literal['linear', 'log'] | None = None,
57
57
  title: str | None = None,
58
58
  vmax: sc.Variable | float = None,
59
59
  vmin: sc.Variable | float = None,
@@ -126,7 +126,7 @@ def scatter3d(
126
126
  A three-dimensional interactive scatter plot.
127
127
  """
128
128
  from ..graphics import scatter3dfigure
129
- from ..widgets import ClippingPlanes, ToggleTool
129
+ from ..widgets import ClippingManager, ToggleTool
130
130
 
131
131
  if 'ax' in kwargs:
132
132
  raise ValueError(
@@ -160,11 +160,11 @@ def scatter3d(
160
160
  vmin=vmin,
161
161
  **kwargs,
162
162
  )
163
- clip_planes = ClippingPlanes(fig)
163
+ clip_manager = ClippingManager(fig)
164
164
  fig.toolbar['cut3d'] = ToggleTool(
165
- callback=clip_planes.toggle_visibility,
165
+ callback=clip_manager.toggle_visibility,
166
166
  icon='layer-group',
167
167
  tooltip='Hide/show spatial cutting tool',
168
168
  )
169
- fig.bottom_bar.add(clip_planes)
169
+ fig.bottom_bar.add(clip_manager)
170
170
  return fig
plopp/plotting/slicer.py CHANGED
@@ -127,7 +127,7 @@ def slicer(
127
127
  obj: PlottableMulti,
128
128
  keep: list[str] | None = None,
129
129
  *,
130
- aspect: Literal['auto', 'equal', None] = None,
130
+ aspect: Literal['auto', 'equal'] | None = None,
131
131
  autoscale: bool = True,
132
132
  cbar: bool = True,
133
133
  clabel: str | None = None,
@@ -146,7 +146,7 @@ def slicer(
146
146
  mask_cmap: str = 'gray',
147
147
  mask_color: str | None = None,
148
148
  nan_color: str | None = None,
149
- norm: Literal['linear', 'log', None] = None,
149
+ norm: Literal['linear', 'log'] | None = None,
150
150
  scale: dict[str, str] | None = None,
151
151
  title: str | None = None,
152
152
  vmax: sc.Variable | float | None = None,
@@ -13,7 +13,7 @@ def superplot(
13
13
  obj: Plottable,
14
14
  keep: str | None = None,
15
15
  *,
16
- aspect: Literal['auto', 'equal', None] = None,
16
+ aspect: Literal['auto', 'equal'] | None = None,
17
17
  autoscale: bool = True,
18
18
  coords: list[str] | None = None,
19
19
  enable_player: bool = False,
@@ -24,7 +24,7 @@ def superplot(
24
24
  logx: bool | None = None,
25
25
  logy: bool | None = None,
26
26
  mask_color: str = 'black',
27
- norm: Literal['linear', 'log', None] = None,
27
+ norm: Literal['linear', 'log'] | None = None,
28
28
  scale: dict[str, str] | None = None,
29
29
  title: str | None = None,
30
30
  vmax: sc.Variable | float | None = None,
plopp/plotting/xyplot.py CHANGED
@@ -40,7 +40,7 @@ def _make_data_array(x: sc.Variable, y: sc.Variable) -> sc.DataArray:
40
40
  def xyplot(
41
41
  x: sc.Variable | ndarray | list | Node,
42
42
  y: sc.Variable | ndarray | list | Node,
43
- aspect: Literal['auto', 'equal', None] = None,
43
+ aspect: Literal['auto', 'equal'] | None = None,
44
44
  autoscale: bool = True,
45
45
  errorbars: bool = True,
46
46
  figsize: tuple[float, float] | None = None,
@@ -48,7 +48,7 @@ def xyplot(
48
48
  legend: bool | tuple[float, float] = True,
49
49
  logx: bool | None = None,
50
50
  logy: bool | None = None,
51
- norm: Literal['linear', 'log', None] = None,
51
+ norm: Literal['linear', 'log'] | None = None,
52
52
  scale: dict[str, str] | None = None,
53
53
  title: str | None = None,
54
54
  vmax: sc.Variable | float | None = None,
@@ -3,8 +3,8 @@
3
3
 
4
4
  from .box import Box, HBar, VBar
5
5
  from .checkboxes import Checkboxes
6
- from .clip3d import Clip3dTool, ClippingPlanes
7
- from .drawing import DrawingTool, PointsTool
6
+ from .clip3d import Clip3dTool, ClippingManager
7
+ from .drawing import DrawingTool, PointsTool, PolygonTool
8
8
  from .linesave import LineSaveTool
9
9
  from .slice import RangeSliceWidget, SliceWidget, slice_dims
10
10
  from .toolbar import Toolbar, make_toolbar_canvas2d, make_toolbar_canvas3d
@@ -15,12 +15,13 @@ __all__ = [
15
15
  "ButtonTool",
16
16
  "Checkboxes",
17
17
  "Clip3dTool",
18
- "ClippingPlanes",
18
+ "ClippingManager",
19
19
  "ColorTool",
20
20
  "DrawingTool",
21
21
  "HBar",
22
22
  "LineSaveTool",
23
23
  "PointsTool",
24
+ "PolygonTool",
24
25
  "RangeSliceWidget",
25
26
  "SliceWidget",
26
27
  "ToggleTool",
plopp/widgets/clip3d.py CHANGED
@@ -46,7 +46,7 @@ class Clip3dTool(ipw.HBox):
46
46
  ----------
47
47
  limits:
48
48
  The spatial extent of the points in the 3d figure in the XYZ directions.
49
- direction:
49
+ kind:
50
50
  The direction normal to the slice.
51
51
  update:
52
52
  A function to update the scene.
@@ -59,23 +59,23 @@ class Clip3dTool(ipw.HBox):
59
59
  def __init__(
60
60
  self,
61
61
  limits: tuple[sc.Variable, sc.Variable, sc.Variable],
62
- direction: Literal['x', 'y', 'z'],
62
+ kind: Literal['x', 'y', 'z'],
63
63
  update: Callable,
64
64
  color: str = 'red',
65
65
  linewidth: float = 1.5,
66
66
  border_visible: bool = True,
67
67
  ):
68
68
  self._limits = limits
69
- self._direction = direction
70
- axis = 'xyz'.index(self._direction)
69
+ self.kind = kind
70
+ axis = 'xyz'.index(self.kind)
71
71
  self.dim = self._limits[axis].dim
72
72
  self._unit = self._limits[axis].unit
73
73
  self.visible = True
74
74
  self._update = update
75
75
  self._border_visible = border_visible
76
76
 
77
- w_axis = 2 if self._direction == 'x' else 0
78
- h_axis = 2 if self._direction == 'y' else 1
77
+ w_axis = 2 if self.kind == 'x' else 0
78
+ h_axis = 2 if self.kind == 'y' else 1
79
79
  width = (self._limits[w_axis][1] - self._limits[w_axis][0]).value
80
80
  height = (self._limits[h_axis][1] - self._limits[h_axis][0]).value
81
81
 
@@ -95,10 +95,10 @@ class Clip3dTool(ipw.HBox):
95
95
  material=p3.LineBasicMaterial(color=color, linewidth=linewidth),
96
96
  ),
97
97
  ]
98
- if self._direction == 'x':
98
+ if self.kind == 'x':
99
99
  for outline in self.outlines:
100
100
  outline.rotateY(0.5 * np.pi)
101
- if self._direction == 'y':
101
+ if self.kind == 'y':
102
102
  for outline in self.outlines:
103
103
  outline.rotateX(0.5 * np.pi)
104
104
 
@@ -112,15 +112,15 @@ class Clip3dTool(ipw.HBox):
112
112
  max=vmax,
113
113
  value=[center[axis] - delta, center[axis] + delta],
114
114
  step=dx * 0.01,
115
- description=direction.upper(),
115
+ description=self.kind.upper(),
116
116
  style={'description_width': 'initial'},
117
- layout={'width': '470px', 'padding': '0px'},
117
+ layout={'width': '35.6em', 'padding': '0px'},
118
118
  )
119
119
 
120
120
  self.cut_visible = ipw.Button(
121
121
  icon='eye-slash',
122
122
  tooltip='Hide cut',
123
- layout={'width': '16px', 'padding': '0px'},
123
+ layout={'width': '1.23em', 'padding': '0px'},
124
124
  )
125
125
 
126
126
  for outline, val in zip(self.outlines, self.slider.value, strict=True):
@@ -158,20 +158,20 @@ class Clip3dTool(ipw.HBox):
158
158
  # the cut, the border visibility is in sync with the parent button.
159
159
  self._border_visible = value
160
160
 
161
- def move(self, value: dict[str, Any]):
161
+ def move(self, change: dict[str, Any]):
162
162
  """
163
163
  Move the outline of the cut according to new position given by the slider.
164
164
  """
165
165
  # Early return if relative difference between new and old value is small.
166
166
  # This also prevents flickering of an existing cut when a new cut is added.
167
167
  if (
168
- np.abs(np.array(value['new']) - np.array(value['old'])).max()
168
+ np.abs(np.array(change['new']) - np.array(change['old'])).max()
169
169
  < 0.01 * self.slider.step
170
170
  ):
171
171
  return
172
- for outline, val in zip(self.outlines, value['new'], strict=True):
172
+ for outline, val in zip(self.outlines, change['new'], strict=True):
173
173
  pos = list(outline.position)
174
- axis = 'xyz'.index(self._direction)
174
+ axis = 'xyz'.index(self.kind)
175
175
  pos[axis] = val
176
176
  outline.position = pos
177
177
  self._throttled_update()
@@ -182,15 +182,114 @@ class Clip3dTool(ipw.HBox):
182
182
  self.slider.value[1], unit=self._unit
183
183
  )
184
184
 
185
+ def make_selection(self, da: sc.DataArray) -> sc.Variable:
186
+ """
187
+ Make a selection variable based on the current slider range.
188
+ """
189
+ xmin, xmax = self.range
190
+ return (da.coords[self.dim] >= xmin) & (da.coords[self.dim] < xmax)
191
+
192
+ @debounce(0.3)
193
+ def _throttled_update(self):
194
+ self._update()
195
+
196
+
197
+ class ClipValueTool(ipw.HBox):
198
+ """
199
+ A tool that provides a slider to extract a points in a three-dimensional
200
+ scatter plot based on a value selection criterion, and adds it to the scene as an
201
+ opaque cut. The slider controls the range of the selection.
202
+
203
+ .. versionadded:: 26.2.0
204
+
205
+ Parameters
206
+ ----------
207
+ limits:
208
+ The range of values in the original data.
209
+ update:
210
+ A function to update the scene.
211
+ """
212
+
213
+ def __init__(self, limits: sc.Variable, update: Callable):
214
+ self._limits = limits
215
+ self._unit = self._limits.unit
216
+ self.visible = True
217
+ self._update = update
218
+ self.kind = 'v'
219
+
220
+ center = self._limits.mean().value
221
+ vmin = self._limits[0].value
222
+ vmax = self._limits[1].value
223
+ dx = vmax - vmin
224
+ delta = 0.05 * dx
225
+ self.slider = ipw.FloatRangeSlider(
226
+ min=vmin,
227
+ max=vmax,
228
+ value=[center - delta, center + delta],
229
+ step=dx * 0.01,
230
+ description="Values",
231
+ style={'description_width': 'initial'},
232
+ layout={'width': '35.6em', 'padding': '0px'},
233
+ )
234
+
235
+ self.cut_visible = ipw.Button(
236
+ icon='eye-slash',
237
+ tooltip='Hide cut',
238
+ layout={'width': '1.23em', 'padding': '0px'},
239
+ )
240
+
241
+ self.unit_label = ipw.Label(f'[{self._unit}]')
242
+ self.cut_visible.on_click(self.toggle)
243
+ self.slider.observe(self.move, names='value')
244
+
245
+ super().__init__([self.slider, ipw.Label(f'[{self._unit}]'), self.cut_visible])
246
+
247
+ def toggle(self, owner: ipw.Button):
248
+ """
249
+ Toggle the visibility of the cut on and off.
250
+ """
251
+ self.visible = not self.visible
252
+ self.slider.disabled = not self.visible
253
+ owner.icon = 'eye-slash' if self.visible else 'eye'
254
+ owner.tooltip = 'Hide cut' if self.visible else 'Show cut'
255
+ self._update()
256
+
257
+ def toggle_border(self, _):
258
+ return
259
+
260
+ @property
261
+ def range(self):
262
+ return sc.scalar(self.slider.value[0], unit=self._unit), sc.scalar(
263
+ self.slider.value[1], unit=self._unit
264
+ )
265
+
266
+ def make_selection(self, da: sc.DataArray) -> sc.Variable:
267
+ """
268
+ Make a selection variable based on the current slider range.
269
+ """
270
+ xmin, xmax = self.range
271
+ return (da.data >= xmin) & (da.data < xmax)
272
+
273
+ def move(self, change: dict[str, Any]):
274
+ # Early return if relative difference between new and old value is small.
275
+ # This also prevents flickering of an existing cut when a new cut is added.
276
+ if (
277
+ np.abs(np.array(change['new']) - np.array(change['old'])).max()
278
+ < 0.01 * self.slider.step
279
+ ):
280
+ return
281
+ self._throttled_update()
282
+
185
283
  @debounce(0.3)
186
284
  def _throttled_update(self):
187
285
  self._update()
188
286
 
189
287
 
190
- class ClippingPlanes(ipw.HBox):
288
+ class ClippingManager(ipw.HBox):
191
289
  """
192
290
  A widget to make clipping planes for spatial cutting (see :class:`Clip3dTool`) to
193
291
  make spatial cuts in the X, Y, and Z directions on a three-dimensional scatter plot.
292
+ The widget also allows to make value-based cuts using :class:`ClipValueTool`.
194
293
 
195
294
  .. versionadded:: 24.04.0
196
295
 
@@ -226,33 +325,47 @@ class ClippingPlanes(ipw.HBox):
226
325
  self.cuts = []
227
326
  self._operation = 'or'
228
327
 
229
- self.tabs = ipw.Tab(layout={'width': '550px'})
328
+ self.tabs = ipw.Tab(layout={'width': '41.6em'})
230
329
  self._original_nodes = list(self._view.graph_nodes.values())
231
330
  self._nodes = {}
232
331
 
332
+ self._value_limits = sc.concat(
333
+ [
334
+ min(n().data.min() for n in self._original_nodes),
335
+ max(n().data.max() for n in self._original_nodes),
336
+ ],
337
+ dim="dummy",
338
+ )
339
+
233
340
  self.add_cut_label = ipw.Label('Add cut:')
234
- layout = {'width': '45px', 'padding': '0px 0px 0px 0px'}
235
341
  self.add_x_cut = ipw.Button(
236
342
  description='X',
237
343
  icon='plus',
238
344
  tooltip='Add X cut',
239
- layout=layout,
345
+ **BUTTON_LAYOUT,
240
346
  )
241
347
  self.add_y_cut = ipw.Button(
242
348
  description='Y',
243
349
  icon='plus',
244
350
  tooltip='Add Y cut',
245
- layout=layout,
351
+ **BUTTON_LAYOUT,
246
352
  )
247
353
  self.add_z_cut = ipw.Button(
248
354
  description='Z',
249
355
  icon='plus',
250
356
  tooltip='Add Z cut',
251
- layout=layout,
357
+ **BUTTON_LAYOUT,
358
+ )
359
+ self.add_v_cut = ipw.Button(
360
+ description='V',
361
+ icon='plus',
362
+ tooltip='Add Value cut',
363
+ **BUTTON_LAYOUT,
252
364
  )
253
365
  self.add_x_cut.on_click(lambda _: self._add_cut('x'))
254
366
  self.add_y_cut.on_click(lambda _: self._add_cut('y'))
255
367
  self.add_z_cut.on_click(lambda _: self._add_cut('z'))
368
+ self.add_v_cut.on_click(lambda _: self._add_cut('v'))
256
369
 
257
370
  self.opacity = ipw.BoundedFloatText(
258
371
  min=0,
@@ -263,7 +376,7 @@ class ClippingPlanes(ipw.HBox):
263
376
  description='Opacity:',
264
377
  tooltip='Set the opacity of the background',
265
378
  style={'description_width': 'initial'},
266
- layout={'width': '142px', 'padding': '0px 0px 0px 0px'},
379
+ layout={'width': '12.1em', 'padding': '0px 0px 0px 0px'},
267
380
  )
268
381
  self.opacity.observe(self._set_opacity, names='value')
269
382
 
@@ -283,7 +396,7 @@ class ClippingPlanes(ipw.HBox):
283
396
  value='OR',
284
397
  disabled=True,
285
398
  tooltip='Operation to combine multiple cuts',
286
- layout={'width': '60px', 'padding': '0px 0px 0px 0px'},
399
+ layout={'width': '5.9em', 'padding': '0px 0px 0px 0px'},
287
400
  )
288
401
  self.cut_operation.observe(self.change_operation, names='value')
289
402
 
@@ -300,7 +413,14 @@ class ClippingPlanes(ipw.HBox):
300
413
  self.tabs,
301
414
  ipw.VBox(
302
415
  [
303
- ipw.HBox([self.add_x_cut, self.add_y_cut, self.add_z_cut]),
416
+ ipw.HBox(
417
+ [
418
+ self.add_x_cut,
419
+ self.add_y_cut,
420
+ self.add_z_cut,
421
+ self.add_v_cut,
422
+ ]
423
+ ),
304
424
  self.opacity,
305
425
  ipw.HBox(
306
426
  [
@@ -316,17 +436,20 @@ class ClippingPlanes(ipw.HBox):
316
436
 
317
437
  self.layout.display = 'none'
318
438
 
319
- def _add_cut(self, direction: Literal['x', 'y', 'z']):
439
+ def _add_cut(self, kind: Literal['x', 'y', 'z', 'v']):
320
440
  """
321
- Add a cut in the specified direction.
441
+ Add a cut of the specified kind.
322
442
  """
323
- cut = Clip3dTool(
324
- direction=direction,
325
- limits=self._limits,
326
- update=self.update_state,
327
- border_visible=self.cut_borders_visibility.value,
328
- )
329
- self._view.canvas.add(cut.outlines)
443
+ if kind == 'v':
444
+ cut = ClipValueTool(limits=self._value_limits, update=self.update_state)
445
+ else:
446
+ cut = Clip3dTool(
447
+ kind=kind,
448
+ limits=self._limits,
449
+ update=self.update_state,
450
+ border_visible=self.cut_borders_visibility.value,
451
+ )
452
+ self._view.canvas.add(cut.outlines)
330
453
  self.cuts.append(cut)
331
454
  self.tabs.children = [*self.tabs.children, cut]
332
455
  self.tabs.selected_index = len(self.cuts) - 1
@@ -335,7 +458,8 @@ class ClippingPlanes(ipw.HBox):
335
458
 
336
459
  def _remove_cut(self, _):
337
460
  cut = self.cuts.pop(self.tabs.selected_index)
338
- self._view.canvas.remove(cut.outlines)
461
+ if cut.kind != 'v':
462
+ self._view.canvas.remove(cut.outlines)
339
463
  self.tabs.children = self.cuts
340
464
  self.update_state()
341
465
  self.update_controls()
@@ -350,7 +474,7 @@ class ClippingPlanes(ipw.HBox):
350
474
  self.delete_cut.disabled = not at_least_one_cut
351
475
  self.cut_borders_visibility.disabled = not at_least_one_cut
352
476
  self.cut_operation.disabled = not at_least_one_cut
353
- self.tabs.titles = [cut._direction.upper() for cut in self.cuts]
477
+ self.tabs.titles = [cut.kind.upper() for cut in self.cuts]
354
478
  self.opacity.disabled = not at_least_one_cut
355
479
  opacity = self.opacity.value if at_least_one_cut else 1.0
356
480
  self._set_opacity({'new': opacity})
@@ -403,12 +527,7 @@ class ClippingPlanes(ipw.HBox):
403
527
 
404
528
  for n in self._original_nodes:
405
529
  da = n.request_data()
406
- selections = []
407
- for cut in visible_cuts:
408
- xmin, xmax = cut.range
409
- selections.append(
410
- (da.coords[cut.dim] >= xmin) & (da.coords[cut.dim] < xmax)
411
- )
530
+ selections = [cut.make_selection(da) for cut in visible_cuts]
412
531
  selection = OPERATIONS[self._operation](selections)
413
532
  if selection.sum().value > 0:
414
533
  if n.id not in self._nodes:
plopp/widgets/drawing.py CHANGED
@@ -189,3 +189,73 @@ value:
189
189
  **kwargs:
190
190
  Additional arguments are forwarded to the ``ToggleTool`` constructor.
191
191
  """
192
+
193
+
194
+ def _get_polygon_info(artist, figure):
195
+ """
196
+ Convert the raw polygon vertices to a dict containing the dimensions of
197
+ each axis, and arrays with units.
198
+ """
199
+ xs, ys = artist.x, artist.y
200
+ return {
201
+ 'x': {
202
+ 'dim': figure.canvas.dims['x'],
203
+ 'value': sc.array(
204
+ dims=['vertex'],
205
+ values=xs,
206
+ unit=figure.canvas.units['x'],
207
+ ),
208
+ },
209
+ 'y': {
210
+ 'dim': figure.canvas.dims['y'],
211
+ 'value': sc.array(
212
+ dims=['vertex'],
213
+ values=ys,
214
+ unit=figure.canvas.units['y'],
215
+ ),
216
+ },
217
+ }
218
+
219
+
220
+ def _make_polygons(**kwargs):
221
+ """
222
+ Intermediate function needed for giving to `partial` to avoid making mpltoolbox a
223
+ hard dependency.
224
+ """
225
+ from mpltoolbox import Polygons
226
+
227
+ return Polygons(**kwargs)
228
+
229
+
230
+ PolygonTool = partial(
231
+ DrawingTool,
232
+ tool=partial(_make_polygons, mec='w'),
233
+ get_artist_info=_get_polygon_info,
234
+ icon='draw-polygon',
235
+ )
236
+ """
237
+ Tool to draw polygon selections on a figure.
238
+
239
+ Controls:
240
+ - Left-click to make new polygons
241
+ - Left-click and hold on polygon vertex to move vertex
242
+ - Right-click and hold to drag/move the entire polygon
243
+ - Middle-click to delete polygon
244
+
245
+ Parameters
246
+ ----------
247
+ figure:
248
+ The figure where the tool will draw the polygon.
249
+ input_node:
250
+ The node that provides the raw data which is shown in ``figure``.
251
+ func:
252
+ The function to be used to make a node whose parents will be the ``input_node``
253
+ and a node yielding the current state of the tool (current position, size).
254
+ destination:
255
+ Where the output from the ``func`` node will be then sent on. This can either
256
+ be a figure, or another graph node.
257
+ value:
258
+ Activate the tool upon creation if ``True``.
259
+ **kwargs:
260
+ Additional arguments are forwarded to the ``ToggleTool`` constructor.
261
+ """
plopp/widgets/linesave.py CHANGED
@@ -35,7 +35,7 @@ class LineSaveTool(VBar):
35
35
  self.button = ipw.Button(description='Save line')
36
36
  self.button.on_click(self.save_line)
37
37
  self.container = VBar()
38
- super().__init__([self.button, self.container], layout={'width': '350px'})
38
+ super().__init__([self.button, self.container], layout={'width': '26.5em'})
39
39
 
40
40
  def _update_container(self):
41
41
  self.container.children = [line['tool'] for line in self._lines.values()]
plopp/widgets/slice.py CHANGED
@@ -32,7 +32,7 @@ class DimSlicer(ipw.VBox):
32
32
  'value': (size - 1) // 2 if slider_constr is ipw.IntSlider else None,
33
33
  'continuous_update': True,
34
34
  'readout': False,
35
- 'layout': {"width": "200px", "margin": "0px 0px 0px 10px"},
35
+ 'layout': {"width": "15.2em", "margin": "0px 0px 0px 10px"},
36
36
  }
37
37
  self.dim_label = ipw.Label(value=dim)
38
38
  self.slider = slider_constr(**widget_args)
@@ -40,7 +40,7 @@ class DimSlicer(ipw.VBox):
40
40
  value=True,
41
41
  tooltip="Continuous update",
42
42
  indent=False,
43
- layout={"width": "20px"},
43
+ layout={"width": "1.52em"},
44
44
  )
45
45
  self.label = ipw.Label(
46
46
  value=coord_element_to_string(coord[dim, self.slider.value])
plopp/widgets/style.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-License-Identifier: BSD-3-Clause
2
2
  # Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3
3
 
4
- BUTTON_LAYOUT = {"layout": {"width": "37px", "padding": "0px 0px 0px 0px"}}
4
+ BUTTON_LAYOUT = {"layout": {"width": "2.8em", "padding": "0px 0px 0px 0px"}}
plopp/widgets/tools.py CHANGED
@@ -54,7 +54,7 @@ class ToggleTool(ipw.ToggleButton):
54
54
 
55
55
  class PlusMinusTool(ipw.HBox):
56
56
  def __init__(self, plus, minus):
57
- layout = {'width': '16px', 'padding': '0px'}
57
+ layout = {'width': '1.23em', 'padding': '0px'}
58
58
  self.callback = {'plus': plus.pop('callback'), 'minus': minus.pop('callback')}
59
59
  self._plus = ipw.Button(icon='plus', **{**{'layout': layout}, **plus})
60
60
  self._minus = ipw.Button(icon='minus', **{**{'layout': layout}, **minus})
@@ -230,10 +230,10 @@ class ColorTool(ipw.HBox):
230
230
  """
231
231
 
232
232
  def __init__(self, text: str, color: str):
233
- layout = ipw.Layout(display="flex", justify_content="flex-end", width='150px')
233
+ layout = ipw.Layout(display="flex", justify_content="flex-end", width='11.4em')
234
234
  self.text = ipw.Label(value=text, layout=layout)
235
235
  self.color = ipw.ColorPicker(
236
- concise=True, value=color, description='', layout={'width': "30px"}
236
+ concise=True, value=color, description='', layout={'width': "2.3em"}
237
237
  )
238
238
  self.button = ipw.Button(icon='times', **BUTTON_LAYOUT)
239
239
  super().__init__([self.text, self.color, self.button])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plopp
3
- Version: 25.11.0
3
+ Version: 26.2.0
4
4
  Summary: Visualization library for Scipp
5
5
  Author: Scipp contributors
6
6
  License-Expression: BSD-3-Clause
@@ -36,13 +36,13 @@ Requires-Dist: graphviz>=0.20.3; extra == "test"
36
36
  Requires-Dist: h5py>=3.12; extra == "test"
37
37
  Requires-Dist: ipympl>=0.8.4; extra == "test"
38
38
  Requires-Dist: ipywidgets>=8.1.0; extra == "test"
39
- Requires-Dist: ipykernel<7; extra == "test"
39
+ Requires-Dist: ipykernel<7,>=6.26; extra == "test"
40
40
  Requires-Dist: mpltoolbox>=24.6.0; extra == "test"
41
41
  Requires-Dist: pandas>=2.2.2; extra == "test"
42
42
  Requires-Dist: plotly>=5.15.0; extra == "test"
43
43
  Requires-Dist: pooch>=1.5; extra == "test"
44
44
  Requires-Dist: pyarrow>=13.0.0; extra == "test"
45
- Requires-Dist: pytest>=7.0; extra == "test"
45
+ Requires-Dist: pytest>=8.0; extra == "test"
46
46
  Requires-Dist: pythreejs>=2.4.1; extra == "test"
47
47
  Requires-Dist: scipp>=25.5.0; extra == "test"
48
48
  Requires-Dist: scipy>=1.10.0; extra == "test"
@@ -1,9 +1,9 @@
1
- plopp/__init__.py,sha256=gyb7FdEP0biNaLQEBZFJKTQSf2pAghTK051eN-yJLxM,1084
1
+ plopp/__init__.py,sha256=sDy1Bzh9cCQvH-JQ1m3e1V0aK-ZrDGycaE791H-nryA,1092
2
2
  plopp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  plopp/backends/__init__.py,sha256=hMvOWD3ua_34nUgcb_jITMnRvjRUUqHSUYuXxkxSIMM,1491
4
4
  plopp/backends/common.py,sha256=xz4iMmgE4UtRzQJQlX4Sc9sbTCmjPrd7b4dnpCbWuQs,3422
5
5
  plopp/backends/matplotlib/__init__.py,sha256=QNPZgWV3wO8psaMkSB-GtTOP-OIe9jSfwDdEt2xgbZg,107
6
- plopp/backends/matplotlib/canvas.py,sha256=xWwe6RY8qVRSNqpO_4rRCwy_zvlUafD0sOFuadU_9jE,17213
6
+ plopp/backends/matplotlib/canvas.py,sha256=0fRyNkreWEBxdjTr8LJ-OlYs9Z1IHGCiqMS76H07DTs,17216
7
7
  plopp/backends/matplotlib/fast_image.py,sha256=1tF4G8kTmgmiybdD-KPIatU2dWwv0dgv4r8IMDQTTGE,6532
8
8
  plopp/backends/matplotlib/figure.py,sha256=Emj8KCIFvesEfAoKIWn4M4TcgZhq6zFTfRTFnLZHGxE,5515
9
9
  plopp/backends/matplotlib/image.py,sha256=YnczwdxsGmh0Nvrk1A8xL93ap2rzKI-0G428OCQmSCc,974
@@ -13,7 +13,7 @@ plopp/backends/matplotlib/scatter.py,sha256=DH-uvlGCWXrCd9qtBqUmg_qwgACnPTEjaUhT
13
13
  plopp/backends/matplotlib/tiled.py,sha256=dy5bKixJP7MYoFJ-bAU4vc6baZaw6aKenD-WoahF_ZM,6238
14
14
  plopp/backends/matplotlib/utils.py,sha256=24gmT2sBHS_wUitUThSLavwSK95uqbLAhadNdxC19I0,3479
15
15
  plopp/backends/plotly/__init__.py,sha256=QNPZgWV3wO8psaMkSB-GtTOP-OIe9jSfwDdEt2xgbZg,107
16
- plopp/backends/plotly/canvas.py,sha256=637AZaHvST95WZUO14GkpE_-hoQuWAItex2IK1otHbM,11023
16
+ plopp/backends/plotly/canvas.py,sha256=Zcopn9avEjGBFUnBxgKFy7uIi_8GiDeyPCIAAF6L6CI,11025
17
17
  plopp/backends/plotly/figure.py,sha256=60QsXUFjr3CqXjxVpAbvd1c-wtq7T5fptotwybgUEcI,1356
18
18
  plopp/backends/plotly/line.py,sha256=yVpJW-xWa0Wy_oMdmvDn2ESIJymT9Fg6wN7RhIRK6Dw,9533
19
19
  plopp/backends/pythreejs/__init__.py,sha256=XvSD58Tj5WBrrOjkePcCrRW6RbbyrvhhWI_S8-AR92U,107
@@ -40,39 +40,39 @@ plopp/graphics/__init__.pyi,sha256=yZQkWxUfP9eKHSP47WjrBiu59Z-uhP5gE2eUrPDa6jU,6
40
40
  plopp/graphics/basefig.py,sha256=N5gBQE8w2968biRV7a3wkxxq-BM14ncfX52y5HziWiQ,824
41
41
  plopp/graphics/bbox.py,sha256=3WeMgu5LgWIXLJTYVT7_USwaAcS7cQPNCk6B0gvuNGo,3596
42
42
  plopp/graphics/camera.py,sha256=fXc1YH7Tp-rwKkcYeE-mHrxMz3SKauj5xJCRuKv-QPs,5323
43
- plopp/graphics/colormapper.py,sha256=dP3sC5IALguLK5UXI6ky1-K0ikX0OYgQurL7H5S6ONo,14384
43
+ plopp/graphics/colormapper.py,sha256=Owe3l5cKNExJfh8_RPWGqhymhWrbmny7aDX9sx3yZP4,14962
44
44
  plopp/graphics/figures.py,sha256=PLWQLnQNWz8epubYlK7HLSkvPGJmTAmHjJM2DqpvLe8,2735
45
- plopp/graphics/graphicalview.py,sha256=BcCM1KuqyfWoJNVtpB8G-K2aywx9dXmZqtLuho_tuLI,11622
45
+ plopp/graphics/graphicalview.py,sha256=LpfQIYN-OQeg_manrMskfpjH6XYsOCIQuZnMNEb0BKw,11660
46
46
  plopp/graphics/tiled.py,sha256=gXYZVAs6wRFKezzN3VlEvm7_z2zx3IDYfGZQZqrpL80,1386
47
47
  plopp/plotting/__init__.py,sha256=s25RIbKplHSRn0jjtLADPQl72JLf_KLRMRCfeMDI_UA,205
48
48
  plopp/plotting/__init__.pyi,sha256=95wYGfM5PT0Y0Ov_ue7rBrHvHxrtex-2WEYtfFzBvE0,475
49
- plopp/plotting/common.py,sha256=LxBrLdUrHqjgKde7KHnf4y09JEzfO4fbphXm5pjPoAs,13132
50
- plopp/plotting/inspector.py,sha256=hz15i9kp9s_Txfi-6wYPCS8CLIQbyDoEcVOTs3dAW4A,6803
51
- plopp/plotting/mesh3d.py,sha256=SaJCaKPIvhMXflOyGWxRlSj1o93bocrQ7OTYY5dNnHY,4186
52
- plopp/plotting/plot.py,sha256=oSM1rQr3nbO0yhIxsMexD5GCuee0lj3lc4rpE6LgpDQ,5655
53
- plopp/plotting/scatter.py,sha256=kjYmYcam651ELuN7AsED3NlSjU-9g6hOgpN399SghDM,6018
54
- plopp/plotting/scatter3d.py,sha256=GlEisMiCeR1ejS4qWIsiYW1iUWH8XDkipeZ1fwDDwP0,5305
55
- plopp/plotting/slicer.py,sha256=VX5kFaBGCF6qJZdew0pbVTVdeJW6rRejChyuyngd3Zs,9069
56
- plopp/plotting/superplot.py,sha256=XaRbNWMv9-nVQwVwglyndNZPdbfrQ736-Nzkm7I7eak,4484
57
- plopp/plotting/xyplot.py,sha256=o-0Yrysda0TAxImw0rNn7TKDJ6x21zuaz8iFYoS52mA,4260
49
+ plopp/plotting/common.py,sha256=f9YxSNGYqisOTLit6ls-iQjx2bgs9xIaf3WEMlorNCE,13139
50
+ plopp/plotting/inspector.py,sha256=5XKvz1RfSTpTzRbTSUJ_9oa8SpPtaAht4eixisb0hDQ,8739
51
+ plopp/plotting/mesh3d.py,sha256=4QQPHuYVyur8nKkbTg7zQMQgFU3MjlsyAPFfpQSJMrU,4187
52
+ plopp/plotting/plot.py,sha256=kAjKSH0QZGgWJVpyfk3GCenrLOUx26D5jbvnZscAueU,5657
53
+ plopp/plotting/scatter.py,sha256=soKks05Fe5FafVrnJxhIj6OfFPhkG9zYrq2tADopi_I,6021
54
+ plopp/plotting/scatter3d.py,sha256=7_mgE5apgkVvseLLMUJSfUQWIwW5P50w589AgrCNUvo,5311
55
+ plopp/plotting/slicer.py,sha256=wdRdrvl0_f32U4yhe_7mbHT9ijjPPj9mW1KzAd_xupc,9071
56
+ plopp/plotting/superplot.py,sha256=htLBssBpuoMKlhP7gsNHbjsyLJWLF27DO3HXpmnpauw,4486
57
+ plopp/plotting/xyplot.py,sha256=3XyhLjWaIqpXOjBQONnD7VehO2cLoWkUiJFk471I_Mw,4262
58
58
  plopp/utils/__init__.py,sha256=s25RIbKplHSRn0jjtLADPQl72JLf_KLRMRCfeMDI_UA,205
59
59
  plopp/utils/__init__.pyi,sha256=n6xB4g9xWLz1tPI2gbX1N_hLKJhL0pbHOqarM4HNUdQ,246
60
60
  plopp/utils/arg_parse.py,sha256=ucBYyOoJfFNEyc2XdnmHLLIbhV-eyOW-UY4JSnMKE-4,783
61
61
  plopp/utils/deprecation.py,sha256=TW02FJht8_f3pHHRZYr2VurMrKniFQrmNpj4-z5ahKs,673
62
62
  plopp/widgets/__init__.py,sha256=s25RIbKplHSRn0jjtLADPQl72JLf_KLRMRCfeMDI_UA,205
63
- plopp/widgets/__init__.pyi,sha256=4hVX7I3VEjE3oy2AGVvguYsIgg7cVrhKGpX7xVgGe9Y,844
63
+ plopp/widgets/__init__.pyi,sha256=0mbhLTfA-sULqa9kzmhV9WN6OJr9n-9W5Qb-aKQjIW4,878
64
64
  plopp/widgets/box.py,sha256=v5Gd3sY0WXcc3UfkcXxZboPG02sfOMXIo50tmpxPwVs,1806
65
65
  plopp/widgets/checkboxes.py,sha256=52yJryEu1upYafFxqoTOdAZmHeKzCI_CJiCWSezCtRA,2430
66
- plopp/widgets/clip3d.py,sha256=L90HUd3bjEU3BaPdK8DnUbeeFNqfFyBWDEwaG9nP-M8,14602
66
+ plopp/widgets/clip3d.py,sha256=NFJGCPsaGcTnlsP5ruW_jfFo4Ywx3zafuPiewpBSbYI,18409
67
67
  plopp/widgets/debounce.py,sha256=V5aIcnb465oqXZX-UG_DR7GjvoG6D_C2jjEUBmguGDA,1292
68
- plopp/widgets/drawing.py,sha256=pfbMtG078LPkZc5M_UGdo0kusIKFdLNlhihpO9z0Hv0,6582
69
- plopp/widgets/linesave.py,sha256=s5QcY-31x7Hvf9iLHmzfFIzodXTD9BX0Av3NlG8GS88,2458
70
- plopp/widgets/slice.py,sha256=rWBxvNru7m9PDWSjUUb-WGPCTDfOcNDQBjP88HFEqbc,5474
71
- plopp/widgets/style.py,sha256=Fw4S8te0xDyT8-kWu4Ut35aLgyr98qMlWZRbXbd1-dk,184
68
+ plopp/widgets/drawing.py,sha256=z8gzzD4pN52V0BxHc8lw-CArGQGk0POS-ZGAPQqCAEw,8483
69
+ plopp/widgets/linesave.py,sha256=gnxTT31xsSEroYMRriQNaxokM8cQPjhLPKwujoQTGVE,2459
70
+ plopp/widgets/slice.py,sha256=rL_BiryBB_pITin1N3o7wRTGQaQksI3EEaJTG_040Jw,5477
71
+ plopp/widgets/style.py,sha256=3qEdXNsat4bqk5BDqZie0Sqg2wvHn2zMrWvfFMq9W9M,185
72
72
  plopp/widgets/toolbar.py,sha256=GH4KKsPGBnNl-bllGD8gF__PgmKpr_QS7zL81GYXUWE,4697
73
- plopp/widgets/tools.py,sha256=9l7LmB8qG0OTt5ZJihJzqRna1ouxJthO_Ahnq7WKTtY,7172
74
- plopp-25.11.0.dist-info/licenses/LICENSE,sha256=fCqnkzCwkGneGtgOidkO2b3ed6SGZT0yhuworuJG0K0,1553
75
- plopp-25.11.0.dist-info/METADATA,sha256=tihTwwjYfb-A1gFpWVUiWjRxeR7iE2eL8dd98N3FLFI,2894
76
- plopp-25.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
- plopp-25.11.0.dist-info/top_level.txt,sha256=Lk_GLFh37eX-3PO3c6bPkDpir7GjJmbQHYza3IpIphc,6
78
- plopp-25.11.0.dist-info/RECORD,,
73
+ plopp/widgets/tools.py,sha256=01JW4RSR72L2fs-2FkuSMVgBc1Eyq-NSNFIuPx7OWao,7176
74
+ plopp-26.2.0.dist-info/licenses/LICENSE,sha256=fCqnkzCwkGneGtgOidkO2b3ed6SGZT0yhuworuJG0K0,1553
75
+ plopp-26.2.0.dist-info/METADATA,sha256=JoXJTsXScaR-7BiSPMPBQvRIwslxSQ1gFJRO6LyNR-A,2900
76
+ plopp-26.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
77
+ plopp-26.2.0.dist-info/top_level.txt,sha256=Lk_GLFh37eX-3PO3c6bPkDpir7GjJmbQHYza3IpIphc,6
78
+ plopp-26.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5