plopp 25.9.0__py3-none-any.whl → 25.11.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/backends/matplotlib/canvas.py +89 -8
- plopp/backends/matplotlib/line.py +3 -1
- plopp/backends/matplotlib/scatter.py +2 -2
- plopp/backends/plotly/canvas.py +73 -8
- plopp/backends/pythreejs/canvas.py +22 -0
- plopp/backends/pythreejs/mesh3d.py +1 -0
- plopp/backends/pythreejs/scatter3d.py +3 -0
- plopp/core/node_class.py +23 -0
- plopp/graphics/colormapper.py +153 -55
- plopp/graphics/graphicalview.py +70 -21
- plopp/plotting/common.py +71 -3
- plopp/plotting/inspector.py +110 -6
- plopp/plotting/mesh3d.py +49 -16
- plopp/plotting/plot.py +106 -44
- plopp/plotting/scatter.py +125 -34
- plopp/plotting/scatter3d.py +52 -17
- plopp/plotting/slicer.py +137 -40
- plopp/plotting/superplot.py +102 -2
- plopp/plotting/xyplot.py +85 -2
- plopp/utils/__init__.py +6 -0
- plopp/utils/__init__.pyi +7 -0
- plopp/utils/arg_parse.py +24 -0
- plopp/{utils.py → utils/deprecation.py} +0 -3
- plopp/widgets/drawing.py +10 -3
- plopp/widgets/toolbar.py +2 -2
- {plopp-25.9.0.dist-info → plopp-25.11.0.dist-info}/METADATA +2 -1
- {plopp-25.9.0.dist-info → plopp-25.11.0.dist-info}/RECORD +30 -27
- {plopp-25.9.0.dist-info → plopp-25.11.0.dist-info}/WHEEL +0 -0
- {plopp-25.9.0.dist-info → plopp-25.11.0.dist-info}/licenses/LICENSE +0 -0
- {plopp-25.9.0.dist-info → plopp-25.11.0.dist-info}/top_level.txt +0 -0
|
@@ -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.
|
|
115
|
-
self.
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
plopp/backends/plotly/canvas.py
CHANGED
|
@@ -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.
|
|
74
|
-
self.
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
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
|
|
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
|
|
@@ -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)
|