plopp 25.3.0__py3-none-any.whl → 25.4.1__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/common.py +10 -8
- plopp/backends/matplotlib/fast_image.py +203 -0
- plopp/backends/matplotlib/image.py +20 -235
- plopp/backends/matplotlib/line.py +89 -27
- plopp/backends/matplotlib/mesh_image.py +269 -0
- plopp/backends/matplotlib/scatter.py +36 -3
- plopp/backends/plotly/line.py +76 -13
- plopp/backends/pythreejs/mesh3d.py +18 -6
- plopp/backends/pythreejs/scatter3d.py +14 -6
- plopp/core/helpers.py +1 -7
- plopp/core/limits.py +9 -5
- plopp/data/factory.py +8 -1
- plopp/graphics/colormapper.py +1 -1
- plopp/plotting/common.py +10 -0
- plopp/plotting/plot.py +2 -6
- plopp/widgets/checkboxes.py +29 -19
- plopp/widgets/slice.py +49 -61
- plopp/widgets/toolbar.py +2 -0
- {plopp-25.3.0.dist-info → plopp-25.4.1.dist-info}/METADATA +3 -2
- {plopp-25.3.0.dist-info → plopp-25.4.1.dist-info}/RECORD +23 -21
- {plopp-25.3.0.dist-info → plopp-25.4.1.dist-info}/WHEEL +1 -1
- {plopp-25.3.0.dist-info → plopp-25.4.1.dist-info/licenses}/LICENSE +0 -0
- {plopp-25.3.0.dist-info → plopp-25.4.1.dist-info}/top_level.txt +0 -0
plopp/backends/common.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
2
|
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
|
|
3
3
|
|
|
4
|
+
import uuid
|
|
4
5
|
from typing import Literal
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
@@ -98,15 +99,16 @@ def make_line_bbox(
|
|
|
98
99
|
The scale of the y-axis.
|
|
99
100
|
"""
|
|
100
101
|
line_x = data.coords[dim]
|
|
101
|
-
sel = slice(None)
|
|
102
|
-
if data.masks:
|
|
103
|
-
sel = ~merge_masks(data.masks)
|
|
104
|
-
if set(sel.dims) != set(data.data.dims):
|
|
105
|
-
sel = sc.broadcast(sel, sizes=data.data.sizes).copy()
|
|
106
|
-
line_y = data.data[sel]
|
|
107
102
|
if errorbars:
|
|
108
|
-
stddevs = sc.stddevs(data.data
|
|
109
|
-
line_y = sc.
|
|
103
|
+
stddevs = sc.stddevs(data.data)
|
|
104
|
+
line_y = sc.DataArray(
|
|
105
|
+
data=sc.concat(
|
|
106
|
+
[data.data - stddevs, data.data + stddevs], dim=uuid.uuid4().hex
|
|
107
|
+
),
|
|
108
|
+
masks=data.masks,
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
line_y = data
|
|
110
112
|
|
|
111
113
|
return BoundingBox(
|
|
112
114
|
**{**axis_bounds(('xmin', 'xmax'), line_x, xscale, pad=True)},
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
|
|
3
|
+
|
|
4
|
+
import uuid
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import scipp as sc
|
|
10
|
+
|
|
11
|
+
from ...core.utils import coord_as_bin_edges, scalar_to_string
|
|
12
|
+
from ...graphics.bbox import BoundingBox, axis_bounds
|
|
13
|
+
from ...graphics.colormapper import ColorMapper
|
|
14
|
+
from ..common import check_ndim
|
|
15
|
+
from .canvas import Canvas
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FastImage:
|
|
19
|
+
"""
|
|
20
|
+
Artist to represent two-dimensional data.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
canvas:
|
|
25
|
+
The canvas that will display the image.
|
|
26
|
+
colormapper:
|
|
27
|
+
The colormapper to use for the image.
|
|
28
|
+
data:
|
|
29
|
+
The initial data to create the image from.
|
|
30
|
+
artist_number:
|
|
31
|
+
The canvas keeps track of how many images have been added to it. This is unused
|
|
32
|
+
by the FastImage artist.
|
|
33
|
+
uid:
|
|
34
|
+
The unique identifier of the artist. If None, a random UUID is generated.
|
|
35
|
+
**kwargs:
|
|
36
|
+
Additional arguments are forwarded to Matplotlib's ``imshow``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
canvas: Canvas,
|
|
42
|
+
colormapper: ColorMapper,
|
|
43
|
+
data: sc.DataArray,
|
|
44
|
+
artist_number: int,
|
|
45
|
+
uid: str | None = None,
|
|
46
|
+
**kwargs,
|
|
47
|
+
):
|
|
48
|
+
check_ndim(data, ndim=2, origin="FastImage")
|
|
49
|
+
self.uid = uid if uid is not None else uuid.uuid4().hex
|
|
50
|
+
self._canvas = canvas
|
|
51
|
+
self._colormapper = colormapper
|
|
52
|
+
self._ax = self._canvas.ax
|
|
53
|
+
self._data = data
|
|
54
|
+
|
|
55
|
+
string_labels = {}
|
|
56
|
+
self._bin_edge_coords = {}
|
|
57
|
+
for i, k in enumerate("yx"):
|
|
58
|
+
self._bin_edge_coords[k] = coord_as_bin_edges(
|
|
59
|
+
self._data, self._data.dims[i]
|
|
60
|
+
)
|
|
61
|
+
if self._data.coords[self._data.dims[i]].dtype == str:
|
|
62
|
+
string_labels[k] = self._data.coords[self._data.dims[i]]
|
|
63
|
+
|
|
64
|
+
self._xmin, self._xmax = self._bin_edge_coords["x"].values[[0, -1]]
|
|
65
|
+
self._ymin, self._ymax = self._bin_edge_coords["y"].values[[0, -1]]
|
|
66
|
+
self._dx = np.diff(self._bin_edge_coords["x"].values[:2])
|
|
67
|
+
self._dy = np.diff(self._bin_edge_coords["y"].values[:2])
|
|
68
|
+
|
|
69
|
+
# Calling imshow sets the aspect ratio to 'equal', which might not be what the
|
|
70
|
+
# user requested. We need to restore the original aspect ratio after making the
|
|
71
|
+
# image.
|
|
72
|
+
original_aspect = self._ax.get_aspect()
|
|
73
|
+
|
|
74
|
+
# Because imshow sets the aspect, it may generate warnings when the axes scales
|
|
75
|
+
# are log.
|
|
76
|
+
with warnings.catch_warnings():
|
|
77
|
+
warnings.filterwarnings(
|
|
78
|
+
"ignore",
|
|
79
|
+
category=UserWarning,
|
|
80
|
+
message="Attempt to set non-positive .* on a log-scaled axis",
|
|
81
|
+
)
|
|
82
|
+
self._image = self._ax.imshow(
|
|
83
|
+
self._data.values,
|
|
84
|
+
origin="lower",
|
|
85
|
+
extent=(self._xmin, self._xmax, self._ymin, self._ymax),
|
|
86
|
+
**({"interpolation": "nearest"} | kwargs),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self._ax.set_aspect(original_aspect)
|
|
90
|
+
self._colormapper.add_artist(self.uid, self)
|
|
91
|
+
self._update_colors()
|
|
92
|
+
|
|
93
|
+
for xy, var in string_labels.items():
|
|
94
|
+
getattr(self._ax, f"set_{xy}ticks")(np.arange(float(var.shape[0])))
|
|
95
|
+
getattr(self._ax, f"set_{xy}ticklabels")(var.values)
|
|
96
|
+
|
|
97
|
+
self._canvas.register_format_coord(self.format_coord)
|
|
98
|
+
# We also hide the cursor hover values generated by the image, as values are
|
|
99
|
+
# included in our custom format_coord.
|
|
100
|
+
self._image.format_cursor_data = lambda _: ""
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def data(self):
|
|
104
|
+
"""
|
|
105
|
+
Get the image's data in a form that may have been tweaked, compared to the
|
|
106
|
+
original data, in the case of a two-dimensional coordinate.
|
|
107
|
+
"""
|
|
108
|
+
return self._data
|
|
109
|
+
|
|
110
|
+
def notify_artist(self, message: str) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Receive notification from the colormapper that its state has changed.
|
|
113
|
+
We thus need to update the colors of the image.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
message:
|
|
118
|
+
The message from the colormapper.
|
|
119
|
+
"""
|
|
120
|
+
self._update_colors()
|
|
121
|
+
|
|
122
|
+
def _update_colors(self):
|
|
123
|
+
"""
|
|
124
|
+
Update the image colors.
|
|
125
|
+
"""
|
|
126
|
+
rgba = self._colormapper.rgba(self.data)
|
|
127
|
+
self._image.set_data(rgba)
|
|
128
|
+
|
|
129
|
+
def update(self, new_values: sc.DataArray):
|
|
130
|
+
"""
|
|
131
|
+
Update image array with new values.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
new_values:
|
|
136
|
+
New data to update the image values from.
|
|
137
|
+
"""
|
|
138
|
+
check_ndim(new_values, ndim=2, origin="FastImage")
|
|
139
|
+
self._data = new_values
|
|
140
|
+
self._update_colors()
|
|
141
|
+
|
|
142
|
+
def format_coord(
|
|
143
|
+
self, xslice: tuple[str, sc.Variable], yslice: tuple[str, sc.Variable]
|
|
144
|
+
) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Format the coordinates of the mouse pointer to show the value of the
|
|
147
|
+
data at that point.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
xslice:
|
|
152
|
+
Dimension and x coordinate of the mouse pointer, as slice parameters.
|
|
153
|
+
yslice:
|
|
154
|
+
Dimension and y coordinate of the mouse pointer, as slice parameters.
|
|
155
|
+
"""
|
|
156
|
+
ind_x = int((xslice[1].value - self._xmin) / self._dx)
|
|
157
|
+
ind_y = int((yslice[1].value - self._ymin) / self._dy)
|
|
158
|
+
try:
|
|
159
|
+
val = self._data[yslice[0], ind_y][xslice[0], ind_x]
|
|
160
|
+
prefix = self._data.name
|
|
161
|
+
if prefix:
|
|
162
|
+
prefix += ": "
|
|
163
|
+
return prefix + scalar_to_string(val)
|
|
164
|
+
except IndexError:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def visible(self) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
The visibility of the image.
|
|
171
|
+
"""
|
|
172
|
+
return self._image.get_visible()
|
|
173
|
+
|
|
174
|
+
@visible.setter
|
|
175
|
+
def visible(self, val: bool):
|
|
176
|
+
self._image.set_visible(val)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def opacity(self) -> float:
|
|
180
|
+
"""
|
|
181
|
+
The opacity of the image.
|
|
182
|
+
"""
|
|
183
|
+
return self._image.get_alpha()
|
|
184
|
+
|
|
185
|
+
@opacity.setter
|
|
186
|
+
def opacity(self, val: float):
|
|
187
|
+
self._image.set_alpha(val)
|
|
188
|
+
|
|
189
|
+
def bbox(self, xscale: Literal["linear", "log"], yscale: Literal["linear", "log"]):
|
|
190
|
+
"""
|
|
191
|
+
The bounding box of the image.
|
|
192
|
+
"""
|
|
193
|
+
return BoundingBox(
|
|
194
|
+
**{**axis_bounds(("xmin", "xmax"), self._bin_edge_coords["x"], xscale)},
|
|
195
|
+
**{**axis_bounds(("ymin", "ymax"), self._bin_edge_coords["y"], yscale)},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def remove(self):
|
|
199
|
+
"""
|
|
200
|
+
Remove the image artist from the canvas.
|
|
201
|
+
"""
|
|
202
|
+
self._image.remove()
|
|
203
|
+
self._colormapper.remove_artist(self.uid)
|
|
@@ -1,251 +1,36 @@
|
|
|
1
1
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
-
# Copyright (c)
|
|
2
|
+
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
|
|
3
3
|
|
|
4
|
-
import uuid
|
|
5
|
-
from typing import Literal
|
|
6
|
-
|
|
7
|
-
import numpy as np
|
|
8
4
|
import scipp as sc
|
|
9
5
|
|
|
10
|
-
from ...core.utils import coord_as_bin_edges, merge_masks, repeat, scalar_to_string
|
|
11
|
-
from ...graphics.bbox import BoundingBox, axis_bounds
|
|
12
|
-
from ...graphics.colormapper import ColorMapper
|
|
13
|
-
from ..common import check_ndim
|
|
14
6
|
from .canvas import Canvas
|
|
7
|
+
from .fast_image import FastImage
|
|
8
|
+
from .mesh_image import MeshImage
|
|
15
9
|
|
|
16
10
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _get_dims_of_1d_and_2d_coords(coords):
|
|
24
|
-
dim_2d = _find_dim_of_2d_coord(coords)
|
|
25
|
-
if dim_2d is None:
|
|
26
|
-
return None, None
|
|
27
|
-
axis_1d = 'xy'.replace(dim_2d[0], '')
|
|
28
|
-
dim_1d = (axis_1d, coords[axis_1d]['dim'])
|
|
29
|
-
return dim_1d, dim_2d
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _maybe_repeat_values(data, dim_1d, dim_2d):
|
|
33
|
-
if dim_2d is None:
|
|
34
|
-
return data
|
|
35
|
-
return repeat(data, dim=dim_1d[1], n=2)[dim_1d[1], :-1]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _from_data_array_to_pcolormesh(data, coords, dim_1d, dim_2d):
|
|
39
|
-
z = _maybe_repeat_values(data=data, dim_1d=dim_1d, dim_2d=dim_2d)
|
|
40
|
-
if dim_2d is None:
|
|
41
|
-
return coords['x'], coords['y'], z
|
|
42
|
-
|
|
43
|
-
# Broadcast 1d coord to 2d and repeat along 1d dim
|
|
44
|
-
# TODO: It may be more efficient here to first repeat and then broadcast, but
|
|
45
|
-
# the current order is simpler in implementation.
|
|
46
|
-
broadcasted_coord = repeat(
|
|
47
|
-
sc.broadcast(
|
|
48
|
-
coords[dim_1d[0]],
|
|
49
|
-
sizes={**coords[dim_2d[0]].sizes, **coords[dim_1d[0]].sizes},
|
|
50
|
-
).transpose(data.dims),
|
|
51
|
-
dim=dim_1d[1],
|
|
52
|
-
n=2,
|
|
53
|
-
)
|
|
54
|
-
# Repeat 2d coord along 1d dim
|
|
55
|
-
repeated_coord = repeat(coords[dim_2d[0]].transpose(data.dims), dim=dim_1d[1], n=2)
|
|
56
|
-
out = {dim_1d[0]: broadcasted_coord[dim_1d[1], 1:-1], dim_2d[0]: repeated_coord}
|
|
57
|
-
return out['x'], out['y'], z
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class Image:
|
|
11
|
+
def Image(
|
|
12
|
+
canvas: Canvas,
|
|
13
|
+
data: sc.DataArray,
|
|
14
|
+
**kwargs,
|
|
15
|
+
):
|
|
61
16
|
"""
|
|
62
|
-
|
|
17
|
+
Factory function to create an image artist.
|
|
18
|
+
If all the coordinates of the data are 1D and linearly spaced,
|
|
19
|
+
a `FastImage` is created.
|
|
20
|
+
Otherwise, a `MeshImage` is created.
|
|
63
21
|
|
|
64
22
|
Parameters
|
|
65
23
|
----------
|
|
66
24
|
canvas:
|
|
67
25
|
The canvas that will display the image.
|
|
68
|
-
colormapper:
|
|
69
|
-
The colormapper to use for the image.
|
|
70
26
|
data:
|
|
71
|
-
The
|
|
72
|
-
uid:
|
|
73
|
-
The unique identifier of the artist. If None, a random UUID is generated.
|
|
74
|
-
shading:
|
|
75
|
-
The shading to use for the ``pcolormesh``.
|
|
76
|
-
rasterized:
|
|
77
|
-
Rasterize the mesh/image if ``True``.
|
|
78
|
-
**kwargs:
|
|
79
|
-
Additional arguments are forwarded to Matplotlib's ``pcolormesh``.
|
|
27
|
+
The data to create the image from.
|
|
80
28
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
colormapper: ColorMapper,
|
|
86
|
-
data: sc.DataArray,
|
|
87
|
-
uid: str | None = None,
|
|
88
|
-
shading: str = 'auto',
|
|
89
|
-
rasterized: bool = True,
|
|
90
|
-
**kwargs,
|
|
29
|
+
if (canvas.ax.name != 'polar') and all(
|
|
30
|
+
(data.coords[dim].ndim < 2)
|
|
31
|
+
and ((data.coords[dim].dtype == str) or (sc.islinspace(data.coords[dim])))
|
|
32
|
+
for dim in data.dims
|
|
91
33
|
):
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
self._colormapper = colormapper
|
|
96
|
-
self._ax = self._canvas.ax
|
|
97
|
-
self._data = data
|
|
98
|
-
# Because all keyword arguments from the figure are forwarded to both the canvas
|
|
99
|
-
# and the line, we need to remove the arguments that belong to the canvas.
|
|
100
|
-
kwargs.pop('ax', None)
|
|
101
|
-
kwargs.pop('cax', None)
|
|
102
|
-
# An artist number is passed to the artist, but is unused for the image.
|
|
103
|
-
kwargs.pop('artist_number', None)
|
|
104
|
-
# If the grid is visible on the axes, we need to set that on again after we
|
|
105
|
-
# call pcolormesh, because that turns the grid off automatically.
|
|
106
|
-
# See https://github.com/matplotlib/matplotlib/issues/15600.
|
|
107
|
-
need_grid = self._ax.xaxis.get_gridlines()[0].get_visible()
|
|
108
|
-
|
|
109
|
-
to_dim_search = {}
|
|
110
|
-
string_labels = {}
|
|
111
|
-
bin_edge_coords = {}
|
|
112
|
-
self._data_with_bin_edges = sc.DataArray(data=self._data.data)
|
|
113
|
-
for i, k in enumerate('yx'):
|
|
114
|
-
to_dim_search[k] = {
|
|
115
|
-
'dim': self._data.dims[i],
|
|
116
|
-
'var': self._data.coords[self._data.dims[i]],
|
|
117
|
-
}
|
|
118
|
-
bin_edge_coords[k] = coord_as_bin_edges(self._data, self._data.dims[i])
|
|
119
|
-
self._data_with_bin_edges.coords[self._data.dims[i]] = bin_edge_coords[k]
|
|
120
|
-
if self._data.coords[self._data.dims[i]].dtype == str:
|
|
121
|
-
string_labels[k] = self._data.coords[self._data.dims[i]]
|
|
122
|
-
|
|
123
|
-
self._dim_1d, self._dim_2d = _get_dims_of_1d_and_2d_coords(to_dim_search)
|
|
124
|
-
self._mesh = None
|
|
125
|
-
|
|
126
|
-
x, y, z = _from_data_array_to_pcolormesh(
|
|
127
|
-
data=self._data.data,
|
|
128
|
-
coords=bin_edge_coords,
|
|
129
|
-
dim_1d=self._dim_1d,
|
|
130
|
-
dim_2d=self._dim_2d,
|
|
131
|
-
)
|
|
132
|
-
self._mesh = self._ax.pcolormesh(
|
|
133
|
-
x.values,
|
|
134
|
-
y.values,
|
|
135
|
-
z.values,
|
|
136
|
-
shading=shading,
|
|
137
|
-
rasterized=rasterized,
|
|
138
|
-
**kwargs,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
self._colormapper.add_artist(self.uid, self)
|
|
142
|
-
self._mesh.set_array(None)
|
|
143
|
-
self._update_colors()
|
|
144
|
-
|
|
145
|
-
for xy, var in string_labels.items():
|
|
146
|
-
getattr(self._ax, f'set_{xy}ticks')(np.arange(float(var.shape[0])))
|
|
147
|
-
getattr(self._ax, f'set_{xy}ticklabels')(var.values)
|
|
148
|
-
|
|
149
|
-
if need_grid:
|
|
150
|
-
self._ax.grid(True)
|
|
151
|
-
|
|
152
|
-
self._canvas.register_format_coord(self.format_coord)
|
|
153
|
-
|
|
154
|
-
@property
|
|
155
|
-
def data(self):
|
|
156
|
-
"""
|
|
157
|
-
Get the Mesh's data in a form that may have been tweaked, compared to the
|
|
158
|
-
original data, in the case of a two-dimensional coordinate.
|
|
159
|
-
"""
|
|
160
|
-
out = sc.DataArray(
|
|
161
|
-
data=_maybe_repeat_values(
|
|
162
|
-
data=self._data.data, dim_1d=self._dim_1d, dim_2d=self._dim_2d
|
|
163
|
-
)
|
|
164
|
-
)
|
|
165
|
-
if self._data.masks:
|
|
166
|
-
out.masks['one_mask'] = _maybe_repeat_values(
|
|
167
|
-
data=sc.broadcast(
|
|
168
|
-
merge_masks(self._data.masks),
|
|
169
|
-
dims=self._data.dims,
|
|
170
|
-
shape=self._data.shape,
|
|
171
|
-
),
|
|
172
|
-
dim_1d=self._dim_1d,
|
|
173
|
-
dim_2d=self._dim_2d,
|
|
174
|
-
)
|
|
175
|
-
return out
|
|
176
|
-
|
|
177
|
-
def notify_artist(self, message: str) -> None:
|
|
178
|
-
"""
|
|
179
|
-
Receive notification from the colormapper that its state has changed.
|
|
180
|
-
We thus need to update the colors of the mesh.
|
|
181
|
-
|
|
182
|
-
Parameters
|
|
183
|
-
----------
|
|
184
|
-
message:
|
|
185
|
-
The message from the colormapper.
|
|
186
|
-
"""
|
|
187
|
-
self._update_colors()
|
|
188
|
-
|
|
189
|
-
def _update_colors(self):
|
|
190
|
-
"""
|
|
191
|
-
Update the mesh colors.
|
|
192
|
-
"""
|
|
193
|
-
rgba = self._colormapper.rgba(self.data)
|
|
194
|
-
self._mesh.set_facecolors(rgba.reshape(np.prod(rgba.shape[:-1]), 4))
|
|
195
|
-
|
|
196
|
-
def update(self, new_values: sc.DataArray):
|
|
197
|
-
"""
|
|
198
|
-
Update image array with new values.
|
|
199
|
-
|
|
200
|
-
Parameters
|
|
201
|
-
----------
|
|
202
|
-
new_values:
|
|
203
|
-
New data to update the mesh values from.
|
|
204
|
-
"""
|
|
205
|
-
check_ndim(new_values, ndim=2, origin='Image')
|
|
206
|
-
self._data = new_values
|
|
207
|
-
self._data_with_bin_edges.data = new_values.data
|
|
208
|
-
self._update_colors()
|
|
209
|
-
|
|
210
|
-
def format_coord(
|
|
211
|
-
self, xslice: tuple[str, sc.Variable], yslice: tuple[str, sc.Variable]
|
|
212
|
-
) -> str:
|
|
213
|
-
"""
|
|
214
|
-
Format the coordinates of the mouse pointer to show the value of the
|
|
215
|
-
data at that point.
|
|
216
|
-
|
|
217
|
-
Parameters
|
|
218
|
-
----------
|
|
219
|
-
xslice:
|
|
220
|
-
Dimension and x coordinate of the mouse pointer, as slice parameters.
|
|
221
|
-
yslice:
|
|
222
|
-
Dimension and y coordinate of the mouse pointer, as slice parameters.
|
|
223
|
-
"""
|
|
224
|
-
try:
|
|
225
|
-
val = self._data_with_bin_edges[yslice][xslice]
|
|
226
|
-
prefix = self._data.name
|
|
227
|
-
if prefix:
|
|
228
|
-
prefix += ': '
|
|
229
|
-
return prefix + scalar_to_string(val)
|
|
230
|
-
except (IndexError, RuntimeError):
|
|
231
|
-
return None
|
|
232
|
-
|
|
233
|
-
def bbox(self, xscale: Literal['linear', 'log'], yscale: Literal['linear', 'log']):
|
|
234
|
-
"""
|
|
235
|
-
The bounding box of the image.
|
|
236
|
-
"""
|
|
237
|
-
ydim, xdim = self._data.dims
|
|
238
|
-
image_x = self._data_with_bin_edges.coords[xdim]
|
|
239
|
-
image_y = self._data_with_bin_edges.coords[ydim]
|
|
240
|
-
|
|
241
|
-
return BoundingBox(
|
|
242
|
-
**{**axis_bounds(('xmin', 'xmax'), image_x, xscale)},
|
|
243
|
-
**{**axis_bounds(('ymin', 'ymax'), image_y, yscale)},
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
def remove(self):
|
|
247
|
-
"""
|
|
248
|
-
Remove the image artist from the canvas.
|
|
249
|
-
"""
|
|
250
|
-
self._mesh.remove()
|
|
251
|
-
self._colormapper.remove_artist(self.uid)
|
|
34
|
+
return FastImage(canvas=canvas, data=data, **kwargs)
|
|
35
|
+
else:
|
|
36
|
+
return MeshImage(canvas=canvas, data=data, **kwargs)
|
|
@@ -58,9 +58,6 @@ class Line:
|
|
|
58
58
|
self._canvas = canvas
|
|
59
59
|
self._ax = self._canvas.ax
|
|
60
60
|
self._data = data
|
|
61
|
-
# Because all keyword arguments from the figure are forwarded to both the canvas
|
|
62
|
-
# and the line, we need to remove the arguments that belong to the canvas.
|
|
63
|
-
kwargs.pop('ax', None)
|
|
64
61
|
|
|
65
62
|
line_args = parse_dicts_in_kwargs(kwargs, name=data.name)
|
|
66
63
|
|
|
@@ -84,6 +81,7 @@ class Line:
|
|
|
84
81
|
'linestyle': 'solid',
|
|
85
82
|
'linewidth': 1.5,
|
|
86
83
|
'color': f'C{artist_number}',
|
|
84
|
+
'zorder': 2,
|
|
87
85
|
}
|
|
88
86
|
markers = list(Line2D.markers.keys())
|
|
89
87
|
default_plot_style = {
|
|
@@ -91,6 +89,7 @@ class Line:
|
|
|
91
89
|
'linewidth': 1.5,
|
|
92
90
|
'marker': markers[(artist_number + 2) % len(markers)],
|
|
93
91
|
'color': f'C{artist_number}',
|
|
92
|
+
'zorder': 2,
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
if line_data["hist"]:
|
|
@@ -98,40 +97,35 @@ class Line:
|
|
|
98
97
|
line_data['values']['x'],
|
|
99
98
|
line_data['values']['y'],
|
|
100
99
|
label=self.label,
|
|
101
|
-
zorder=10,
|
|
102
100
|
**{**default_step_style, **line_args},
|
|
103
101
|
)[0]
|
|
104
102
|
|
|
105
103
|
self._mask = self._ax.step(line_data['mask']['x'], line_data['mask']['y'])[
|
|
106
104
|
0
|
|
107
105
|
]
|
|
108
|
-
self._mask.update_from(self._line)
|
|
109
|
-
self._mask.set_color(mask_color)
|
|
110
|
-
self._mask.set_label(None)
|
|
111
|
-
self._mask.set_linewidth(self._mask.get_linewidth() * 3)
|
|
112
|
-
self._mask.set_zorder(self._mask.get_zorder() - 1)
|
|
113
|
-
self._mask.set_visible(line_data['mask']['visible'])
|
|
114
106
|
else:
|
|
115
107
|
self._line = self._ax.plot(
|
|
116
108
|
line_data['values']['x'],
|
|
117
109
|
line_data['values']['y'],
|
|
118
110
|
label=self.label,
|
|
119
|
-
zorder=10,
|
|
120
111
|
**{**default_plot_style, **line_args},
|
|
121
112
|
)[0]
|
|
122
|
-
self._mask = self._ax.plot(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
zorder=11,
|
|
126
|
-
mec=mask_color,
|
|
127
|
-
mfc="None",
|
|
128
|
-
mew=3.0,
|
|
129
|
-
linestyle="none",
|
|
130
|
-
marker=self._line.get_marker(),
|
|
131
|
-
visible=line_data['mask']['visible'],
|
|
132
|
-
)[0]
|
|
113
|
+
self._mask = self._ax.plot(line_data['mask']['x'], line_data['mask']['y'])[
|
|
114
|
+
0
|
|
115
|
+
]
|
|
133
116
|
|
|
134
|
-
self.
|
|
117
|
+
self._mask.update_from(self._line)
|
|
118
|
+
self._mask.set_color(mask_color)
|
|
119
|
+
self._mask.set_label(None)
|
|
120
|
+
self._mask.set_visible(line_data['mask']['visible'])
|
|
121
|
+
if self._line.get_marker().lower() != 'none':
|
|
122
|
+
self._mask.set(
|
|
123
|
+
mec=mask_color, mfc='None', mew=3.0, zorder=self._line.get_zorder() + 1
|
|
124
|
+
)
|
|
125
|
+
if self._line.get_linestyle().lower() != 'none':
|
|
126
|
+
self._mask.set(
|
|
127
|
+
lw=self._line.get_linewidth() * 3, zorder=self._line.get_zorder() - 1
|
|
128
|
+
)
|
|
135
129
|
|
|
136
130
|
# Add error bars
|
|
137
131
|
if errorbars and (line_data['stddevs'] is not None):
|
|
@@ -140,7 +134,7 @@ class Line:
|
|
|
140
134
|
line_data['stddevs']['y'],
|
|
141
135
|
yerr=line_data['stddevs']['e'],
|
|
142
136
|
color=self._line.get_color(),
|
|
143
|
-
zorder=
|
|
137
|
+
zorder=self._line.get_zorder(),
|
|
144
138
|
fmt="none",
|
|
145
139
|
)
|
|
146
140
|
|
|
@@ -156,7 +150,6 @@ class Line:
|
|
|
156
150
|
check_ndim(new_values, ndim=1, origin='Line')
|
|
157
151
|
self._data = new_values
|
|
158
152
|
line_data = make_line_data(data=self._data, dim=self._dim)
|
|
159
|
-
self.line_mask = sc.array(dims=['x'], values=~np.isnan(line_data['mask']['y']))
|
|
160
153
|
|
|
161
154
|
self._line.set_data(line_data['values']['x'], line_data['values']['y'])
|
|
162
155
|
self._mask.set_data(line_data['mask']['x'], line_data['mask']['y'])
|
|
@@ -191,20 +184,89 @@ class Line:
|
|
|
191
184
|
self._canvas.draw()
|
|
192
185
|
|
|
193
186
|
@property
|
|
194
|
-
def color(self):
|
|
187
|
+
def color(self) -> str:
|
|
195
188
|
"""
|
|
196
189
|
The line color.
|
|
197
190
|
"""
|
|
198
191
|
return self._line.get_color()
|
|
199
192
|
|
|
200
193
|
@color.setter
|
|
201
|
-
def color(self, val):
|
|
194
|
+
def color(self, val: str):
|
|
202
195
|
self._line.set_color(val)
|
|
203
196
|
if self._error is not None:
|
|
204
197
|
for artist in self._error.get_children():
|
|
205
198
|
artist.set_color(val)
|
|
206
199
|
self._canvas.draw()
|
|
207
200
|
|
|
201
|
+
@property
|
|
202
|
+
def style(self) -> str:
|
|
203
|
+
"""
|
|
204
|
+
The line style.
|
|
205
|
+
"""
|
|
206
|
+
return self._line.get_linestyle()
|
|
207
|
+
|
|
208
|
+
@style.setter
|
|
209
|
+
def style(self, val: str):
|
|
210
|
+
self._line.set_linestyle(val)
|
|
211
|
+
self._canvas.draw()
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def width(self) -> float:
|
|
215
|
+
"""
|
|
216
|
+
The line width.
|
|
217
|
+
"""
|
|
218
|
+
return self._line.get_linewidth()
|
|
219
|
+
|
|
220
|
+
@width.setter
|
|
221
|
+
def width(self, val: float):
|
|
222
|
+
self._line.set_linewidth(val)
|
|
223
|
+
self._canvas.draw()
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def marker(self) -> str:
|
|
227
|
+
"""
|
|
228
|
+
The line marker.
|
|
229
|
+
"""
|
|
230
|
+
return self._line.get_marker()
|
|
231
|
+
|
|
232
|
+
@marker.setter
|
|
233
|
+
def marker(self, val: str):
|
|
234
|
+
self._line.set_marker(val)
|
|
235
|
+
self._mask.set_marker(val)
|
|
236
|
+
self._canvas.draw()
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def visible(self) -> bool:
|
|
240
|
+
"""
|
|
241
|
+
Whether the line is visible.
|
|
242
|
+
"""
|
|
243
|
+
return self._line.get_visible()
|
|
244
|
+
|
|
245
|
+
@visible.setter
|
|
246
|
+
def visible(self, val: bool):
|
|
247
|
+
self._line.set_visible(val)
|
|
248
|
+
self._mask.set_visible(val)
|
|
249
|
+
if self._error is not None:
|
|
250
|
+
for artist in self._error.get_children():
|
|
251
|
+
artist.set_visible(val)
|
|
252
|
+
self._canvas.draw()
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def opacity(self) -> float:
|
|
256
|
+
"""
|
|
257
|
+
The line opacity.
|
|
258
|
+
"""
|
|
259
|
+
return self._line.get_alpha()
|
|
260
|
+
|
|
261
|
+
@opacity.setter
|
|
262
|
+
def opacity(self, val: float):
|
|
263
|
+
self._line.set_alpha(val)
|
|
264
|
+
self._mask.set_alpha(val)
|
|
265
|
+
if self._error is not None:
|
|
266
|
+
for artist in self._error.get_children():
|
|
267
|
+
artist.set_alpha(val)
|
|
268
|
+
self._canvas.draw()
|
|
269
|
+
|
|
208
270
|
def bbox(
|
|
209
271
|
self, xscale: Literal['linear', 'log'], yscale: Literal['linear', 'log']
|
|
210
272
|
) -> BoundingBox:
|