plopp 25.7.1__py3-none-any.whl → 25.10.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.
@@ -12,6 +12,7 @@ from mpl_toolkits.axes_grid1 import make_axes_locatable
12
12
 
13
13
  from ...core.utils import maybe_variable_to_number, scalar_to_string
14
14
  from ...graphics.bbox import BoundingBox
15
+ from ...utils import parse_mutually_exclusive
15
16
  from .utils import fig_to_bytes, is_sphinx_build, make_figure, make_legend
16
17
 
17
18
 
@@ -82,6 +83,24 @@ class Canvas:
82
83
  legend:
83
84
  Show legend if ``True``. If ``legend`` is a tuple, it should contain the
84
85
  ``(x, y)`` coordinates of the legend's anchor point in axes coordinates.
86
+ xmin:
87
+ The minimum value for the x axis.
88
+ xmax:
89
+ The maximum value for the x axis.
90
+ ymin:
91
+ The minimum value for the y axis.
92
+ ymax:
93
+ The maximum value for the y axis.
94
+ logx:
95
+ If ``True``, use a logarithmic scale for the x axis.
96
+ logy:
97
+ If ``True``, use a logarithmic scale for the y axis.
98
+ xlabel:
99
+ The label for the x axis.
100
+ ylabel:
101
+ The label for the y axis.
102
+ norm:
103
+ Set to ``'log'`` for a logarithmic y-axis (legacy, prefer ``logy`` instead).
85
104
  """
86
105
 
87
106
  def __init__(
@@ -91,11 +110,20 @@ class Canvas:
91
110
  figsize: tuple[float, float] | None = None,
92
111
  title: str | None = None,
93
112
  grid: bool = False,
94
- user_vmin: sc.Variable | float = None,
95
- user_vmax: sc.Variable | float = None,
113
+ user_vmin: sc.Variable | float | None = None,
114
+ user_vmax: sc.Variable | float | None = None,
96
115
  aspect: Literal['auto', 'equal', None] = None,
97
116
  cbar: bool = False,
98
117
  legend: bool | tuple[float, float] = True,
118
+ xmin: sc.Variable | float | None = None,
119
+ xmax: sc.Variable | float | None = None,
120
+ ymin: sc.Variable | float | None = None,
121
+ ymax: sc.Variable | float | None = None,
122
+ logx: bool = False,
123
+ logy: bool = False,
124
+ xlabel: str | None = None,
125
+ ylabel: str | None = None,
126
+ norm: Literal['linear', 'log', None] = None,
99
127
  **ignored,
100
128
  ):
101
129
  # Note on the `**ignored`` keyword arguments: the figure which owns the canvas
@@ -107,12 +135,20 @@ class Canvas:
107
135
  # Instead, we forward all the kwargs from the figure to both the canvas and the
108
136
  # artist, and filter out the artist kwargs with `**ignored`.
109
137
 
138
+ ymin = parse_mutually_exclusive(vmin=user_vmin, ymin=ymin)
139
+ ymax = parse_mutually_exclusive(vmax=user_vmax, ymax=ymax)
140
+ logy = parse_mutually_exclusive(norm=norm, logy=logy)
141
+
110
142
  self.fig = None
111
143
  self.ax = ax
112
144
  self.cax = cax
113
145
  self.bbox = BoundingBox()
114
- self._user_vmin = user_vmin
115
- self._user_vmax = user_vmax
146
+ self._xmin = xmin
147
+ self._xmax = xmax
148
+ self._ymin = ymin
149
+ self._ymax = ymax
150
+ self._xlabel = xlabel
151
+ self._ylabel = ylabel
116
152
  self.units = {}
117
153
  self.dims = {}
118
154
  self._legend = legend
@@ -143,6 +179,15 @@ class Canvas:
143
179
  self.ax.set_title(title)
144
180
  self._coord_formatters = []
145
181
 
182
+ if logx:
183
+ self.xscale = 'log'
184
+ if logy:
185
+ self.yscale = 'log'
186
+ if xlabel is not None:
187
+ self.xlabel = xlabel
188
+ if ylabel is not None:
189
+ self.ylabel = ylabel
190
+
146
191
  def is_widget(self):
147
192
  return hasattr(self.fig.canvas, "on_widget_constructed")
148
193
 
@@ -225,8 +270,10 @@ class Canvas:
225
270
  self.ax.format_coord = self.format_coord
226
271
  key = 'y' if 'y' in self.units else 'data'
227
272
  self.bbox = BoundingBox(
228
- ymin=maybe_variable_to_number(self._user_vmin, unit=self.units[key]),
229
- ymax=maybe_variable_to_number(self._user_vmax, unit=self.units[key]),
273
+ xmin=maybe_variable_to_number(self._xmin, unit=self.units['x']),
274
+ xmax=maybe_variable_to_number(self._xmax, unit=self.units['x']),
275
+ ymin=maybe_variable_to_number(self._ymin, unit=self.units[key]),
276
+ ymax=maybe_variable_to_number(self._ymax, unit=self.units[key]),
230
277
  )
231
278
 
232
279
  def register_format_coord(self, formatter):
@@ -414,6 +461,28 @@ class Canvas:
414
461
  def yrange(self, value: tuple[float, float]):
415
462
  self.ax.set_ylim(value)
416
463
 
464
+ @property
465
+ def logx(self) -> bool:
466
+ """
467
+ Get or set whether the x-axis is in logarithmic scale.
468
+ """
469
+ return self.xscale == 'log'
470
+
471
+ @logx.setter
472
+ def logx(self, value: bool):
473
+ self.xscale = 'log' if value else 'linear'
474
+
475
+ @property
476
+ def logy(self) -> bool:
477
+ """
478
+ Get or set whether the y-axis is in logarithmic scale.
479
+ """
480
+ return self.yscale == 'log'
481
+
482
+ @logy.setter
483
+ def logy(self, value: bool):
484
+ self.yscale = 'log' if value else 'linear'
485
+
417
486
  @property
418
487
  def grid(self) -> bool:
419
488
  """
@@ -463,14 +532,26 @@ class Canvas:
463
532
  """
464
533
  self.fig.canvas.toolbar.save_figure()
465
534
 
466
- def logx(self):
535
+ def toggle_logx(self):
467
536
  """
468
537
  Toggle the scale between ``linear`` and ``log`` along the horizontal axis.
469
538
  """
470
539
  self.xscale = 'log' if self.xscale == 'linear' else 'linear'
471
540
 
472
- def logy(self):
541
+ def toggle_logy(self):
473
542
  """
474
543
  Toggle the scale between ``linear`` and ``log`` along the vertical axis.
475
544
  """
476
545
  self.yscale = 'log' if self.yscale == 'linear' else 'linear'
546
+
547
+ def has_user_xlabel(self) -> bool:
548
+ """
549
+ Return ``True`` if the user has set an x-axis label.
550
+ """
551
+ return self._xlabel is not None
552
+
553
+ def has_user_ylabel(self) -> bool:
554
+ """
555
+ Return ``True`` if the user has set a y-axis label.
556
+ """
557
+ return self._ylabel is not None
@@ -50,7 +50,7 @@ class Line:
50
50
  uid: str | None = None,
51
51
  artist_number: int = 0,
52
52
  errorbars: bool = True,
53
- mask_color: str = 'black',
53
+ mask_color: str | None = None,
54
54
  **kwargs,
55
55
  ):
56
56
  check_ndim(data, ndim=1, origin='Line')
@@ -69,6 +69,8 @@ class Line:
69
69
  self._dim = self._data.dim
70
70
  self._unit = self._data.unit
71
71
  self._coord = self._data.coords[self._dim]
72
+ if mask_color is None:
73
+ mask_color = 'black'
72
74
 
73
75
  aliases = {'ls': 'linestyle', 'lw': 'linewidth', 'c': 'color'}
74
76
  for key, alias in aliases.items():
@@ -56,7 +56,7 @@ class Scatter:
56
56
  size: str | float | None = None,
57
57
  artist_number: int = 0,
58
58
  colormapper: ColorMapper | None = None,
59
- mask_color: str = 'black',
59
+ mask_color: str | None = None,
60
60
  cbar: bool = False,
61
61
  **kwargs,
62
62
  ):
@@ -117,7 +117,7 @@ class Scatter:
117
117
  ymask,
118
118
  s=marker_size,
119
119
  marker=merged_kwargs['marker'],
120
- edgecolors=mask_color,
120
+ edgecolors=mask_color or 'black',
121
121
  facecolor="None",
122
122
  linewidth=3.0,
123
123
  zorder=self._scatter.get_zorder() + 1,
@@ -68,6 +68,8 @@ class Tiled:
68
68
  nrows: int,
69
69
  ncols: int,
70
70
  figsize: tuple[float, float] | None = None,
71
+ hspace: float = 0.05,
72
+ wspace: float = 0.1,
71
73
  **kwargs: Any,
72
74
  ) -> None:
73
75
  self.nrows = nrows
@@ -81,7 +83,9 @@ class Tiled:
81
83
  layout='constrained',
82
84
  )
83
85
 
84
- self.gs = gridspec.GridSpec(nrows, ncols, figure=self.fig, **kwargs)
86
+ self.gs = gridspec.GridSpec(
87
+ nrows, ncols, figure=self.fig, wspace=wspace, hspace=hspace, **kwargs
88
+ )
85
89
  self.figures = np.full((nrows, ncols), None)
86
90
  self._history = []
87
91
 
@@ -7,6 +7,7 @@ import scipp as sc
7
7
 
8
8
  from ...core.utils import maybe_variable_to_number
9
9
  from ...graphics.bbox import BoundingBox
10
+ from ...utils import parse_mutually_exclusive
10
11
 
11
12
 
12
13
  class Canvas:
@@ -35,8 +36,17 @@ class Canvas:
35
36
  self,
36
37
  figsize: tuple[float, float] | None = None,
37
38
  title: str | None = None,
38
- user_vmin: sc.Variable | float = None,
39
- user_vmax: sc.Variable | float = None,
39
+ user_vmin: sc.Variable | float | None = None,
40
+ user_vmax: sc.Variable | float | None = None,
41
+ xmin: sc.Variable | float | None = None,
42
+ xmax: sc.Variable | float | None = None,
43
+ ymin: sc.Variable | float | None = None,
44
+ ymax: sc.Variable | float | None = None,
45
+ logx: bool | None = None,
46
+ logy: bool | None = None,
47
+ xlabel: str | None = None,
48
+ ylabel: str | None = None,
49
+ norm: Literal['linear', 'log', None] = None,
40
50
  **ignored,
41
51
  ):
42
52
  # Note on the `**ignored`` keyword arguments: the figure which owns the canvas
@@ -48,6 +58,10 @@ class Canvas:
48
58
  # Instead, we forward all the kwargs from the figure to both the canvas and the
49
59
  # artist, and filter out the artist kwargs with `**ignored`.
50
60
 
61
+ ymin = parse_mutually_exclusive(vmin=user_vmin, ymin=ymin)
62
+ ymax = parse_mutually_exclusive(vmax=user_vmax, ymax=ymax)
63
+ logy = parse_mutually_exclusive(norm=norm, logy=logy)
64
+
51
65
  import plotly.graph_objects as go
52
66
 
53
67
  self.fig = go.FigureWidget(
@@ -70,8 +84,12 @@ class Canvas:
70
84
  }
71
85
  )
72
86
  self.figsize = figsize
73
- self._user_vmin = user_vmin
74
- self._user_vmax = user_vmax
87
+ self._xmin = xmin
88
+ self._xmax = xmax
89
+ self._ymin = ymin
90
+ self._ymax = ymax
91
+ self._xlabel = xlabel
92
+ self._ylabel = ylabel
75
93
  self.units = {}
76
94
  self.dims = {}
77
95
  self._own_axes = False
@@ -79,6 +97,17 @@ class Canvas:
79
97
  self.title = title
80
98
  self.bbox = BoundingBox()
81
99
 
100
+ logx = False if logx is None else logx
101
+ logy = False if logy is None else logy
102
+ if logx:
103
+ self.xscale = 'log'
104
+ if logy:
105
+ self.yscale = 'log'
106
+ if xlabel is not None:
107
+ self.xlabel = xlabel
108
+ if ylabel is not None:
109
+ self.ylabel = ylabel
110
+
82
111
  def to_widget(self):
83
112
  return self.fig
84
113
 
@@ -118,8 +147,10 @@ class Canvas:
118
147
  self.dtypes = dtypes
119
148
  key = 'y' if 'y' in self.units else 'data'
120
149
  self.bbox = BoundingBox(
121
- ymin=maybe_variable_to_number(self._user_vmin, unit=self.units[key]),
122
- ymax=maybe_variable_to_number(self._user_vmax, unit=self.units[key]),
150
+ xmin=maybe_variable_to_number(self._xmin, unit=self.units['x']),
151
+ xmax=maybe_variable_to_number(self._xmax, unit=self.units['x']),
152
+ ymin=maybe_variable_to_number(self._ymin, unit=self.units[key]),
153
+ ymax=maybe_variable_to_number(self._ymax, unit=self.units[key]),
123
154
  )
124
155
 
125
156
  @property
@@ -255,6 +286,28 @@ class Canvas:
255
286
  def yrange(self, value: tuple[float, float]):
256
287
  self.fig.layout.yaxis.range = value
257
288
 
289
+ @property
290
+ def logx(self) -> bool:
291
+ """
292
+ Get or set whether the x-axis is in logarithmic scale.
293
+ """
294
+ return self.xscale == 'log'
295
+
296
+ @logx.setter
297
+ def logx(self, value: bool):
298
+ self.xscale = 'log' if value else 'linear'
299
+
300
+ @property
301
+ def logy(self) -> bool:
302
+ """
303
+ Get or set whether the y-axis is in logarithmic scale.
304
+ """
305
+ return self.yscale == 'log'
306
+
307
+ @logy.setter
308
+ def logy(self, value: bool):
309
+ self.yscale = 'log' if value else 'linear'
310
+
258
311
  def reset_mode(self):
259
312
  """
260
313
  Reset the modebar mode to nothing, to disable all zoom/pan tools.
@@ -290,13 +343,13 @@ class Canvas:
290
343
  """
291
344
  self.fig.write_image('figure.png')
292
345
 
293
- def logx(self):
346
+ def toggle_logx(self):
294
347
  """
295
348
  Toggle the scale between ``linear`` and ``log`` along the horizontal axis.
296
349
  """
297
350
  self.xscale = 'log' if self.xscale in ('linear', None) else 'linear'
298
351
 
299
- def logy(self):
352
+ def toggle_logy(self):
300
353
  """
301
354
  Toggle the scale between ``linear`` and ``log`` along the vertical axis.
302
355
  """
@@ -307,3 +360,15 @@ class Canvas:
307
360
 
308
361
  def update_legend(self):
309
362
  pass
363
+
364
+ def has_user_xlabel(self) -> bool:
365
+ """
366
+ Return ``True`` if the user has set an x-axis label.
367
+ """
368
+ return self._xlabel is not None
369
+
370
+ def has_user_ylabel(self) -> bool:
371
+ """
372
+ Return ``True`` if the user has set a y-axis label.
373
+ """
374
+ return self._ylabel is not None
@@ -47,6 +47,10 @@ class Canvas:
47
47
  self.xscale = 'linear'
48
48
  self.yscale = 'linear'
49
49
  self.zscale = 'linear'
50
+ # TODO: Support setting labels on axes via xlabel, ylabel, zlabel args.
51
+ self._xlabel = None
52
+ self._ylabel = None
53
+ self._zlabel = None
50
54
  self.outline = None
51
55
  self.axticks = None
52
56
  self.figsize = np.asarray(figsize if figsize is not None else (600, 400))
@@ -398,3 +402,21 @@ class Canvas:
398
402
 
399
403
  def update_legend(self):
400
404
  pass
405
+
406
+ def has_user_xlabel(self) -> bool:
407
+ """
408
+ Return ``True`` if the user has set an x-axis label.
409
+ """
410
+ return self._xlabel is not None
411
+
412
+ def has_user_ylabel(self) -> bool:
413
+ """
414
+ Return ``True`` if the user has set a y-axis label.
415
+ """
416
+ return self._ylabel is not None
417
+
418
+ def has_user_zlabel(self) -> bool:
419
+ """
420
+ Return ``True`` if the user has set a z-axis label.
421
+ """
422
+ return self._zlabel is not None
@@ -57,6 +57,7 @@ class Mesh3d:
57
57
  opacity: float = 1,
58
58
  edgecolor: str | None = None,
59
59
  artist_number: int = 0,
60
+ **ignored,
60
61
  ):
61
62
  import pythreejs as p3
62
63
 
@@ -45,6 +45,8 @@ class Scatter3d:
45
45
  The opacity of the points.
46
46
  pixel_size:
47
47
  The size of the pixels in the plot. Deprecated (use size instead).
48
+ mask_color:
49
+ The color of the masked points. TODO: not yet implemented.
48
50
  """
49
51
 
50
52
  def __init__(
@@ -62,6 +64,7 @@ class Scatter3d:
62
64
  artist_number: int = 0,
63
65
  opacity: float = 1,
64
66
  pixel_size: sc.Variable | float | None = None,
67
+ mask_color: str | None = None,
65
68
  ):
66
69
  import pythreejs as p3
67
70
 
plopp/core/node_class.py CHANGED
@@ -26,6 +26,18 @@ class Node:
26
26
  A node can be constructed from a callable ``func``, or a raw object. In the case
27
27
  of a raw object, a node wrapping the object will be created.
28
28
 
29
+ Nodes can have views attached to them, which are instances of :class:`View`. Views
30
+ can be notified when the node's data changes. When this happens, the view typically
31
+ requests data from its parent nodes, who in-turn request data from their parents,
32
+ traversing the graph from bottom to top.
33
+
34
+ Caching is used to avoid traversing the graph multiple times when data is
35
+ requested multiple times without any changes to the graph.
36
+
37
+ Leaf nodes are nodes that have neither children nor views. When such nodes are
38
+ notified of changes, they will call their ``func`` to ensure any side effects are
39
+ executed.
40
+
29
41
  Parameters
30
42
  ----------
31
43
  func:
@@ -160,6 +172,12 @@ class Node:
160
172
  _no_replace_append(self.views, view, 'view')
161
173
  view.graph_nodes[self.id] = self
162
174
 
175
+ def is_leaf(self) -> bool:
176
+ """
177
+ Whether the node is a leaf node (i.e. has neither children nor views).
178
+ """
179
+ return (not self.children) and (not self.views)
180
+
163
181
  def notify_children(self, message: Any) -> None:
164
182
  """
165
183
  Notify all of the node's children with ``message``.
@@ -172,6 +190,11 @@ class Node:
172
190
  The message to pass to the children.
173
191
  """
174
192
  self._data = None
193
+ if self.is_leaf():
194
+ # Special case: leaf nodes have no children nor views, so we always request
195
+ # data from parents and call ``self.func``.
196
+ self.request_data()
197
+ return
175
198
  self.notify_views(message)
176
199
  for child in self.children:
177
200
  child.notify_children(message)
plopp/data/examples.py CHANGED
@@ -98,7 +98,9 @@ def three_bands(npeaks=200, per_peak=500, spread=30.0):
98
98
  data=sc.ones(sizes=xcoord.sizes, unit='counts'),
99
99
  coords={'x': xcoord, 'y': ycoord},
100
100
  )
101
- return table.hist(y=300, x=300) + sc.scalar(1.0, unit='counts')
101
+ xedges = sc.linspace('x', -50, 350, num=nx + 1, unit='cm')
102
+ yedges = sc.linspace('y', -50, 350, num=ny + 1, unit='cm')
103
+ return table.hist(y=yedges, x=xedges) + sc.scalar(1.0, unit='counts')
102
104
 
103
105
 
104
106
  def clusters3d(nclusters=100, npercluster=2000):