Skip to content

Commit 02860ff

Browse files
authored
Merge pull request raphaelquast#243 from raphaelquast/dev
merge for v8.2.1
2 parents b40a54b + d3c4183 commit 02860ff

File tree

8 files changed

+137
-34
lines changed

8 files changed

+137
-34
lines changed

eomaps/_blit_manager.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,11 @@ def get_bg_artists(self, layer):
574574
# artists that are only visible if both layers are visible! (e.g. "l1|l2")
575575
artists.extend(self._bg_artists.get(l, []))
576576

577-
if l == self._unmanaged_artists_layer:
577+
# make sure to also trigger drawing unmanaged artists on inset-maps!
578+
if l in (
579+
self._unmanaged_artists_layer,
580+
f"__inset_{self._unmanaged_artists_layer}",
581+
):
578582
artists.extend(self._get_unmanaged_artists())
579583

580584
# make the list unique but maintain order (dicts keep order for python>3.7)

eomaps/_data_manager.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ def redraw_required(self, layer):
681681

682682
# don't re-draw if the layer of the dataset is not requested
683683
# (note multi-layers trigger re-draws of individual layers as well)
684-
if layer not in ["all", self.layer]:
684+
if not self.m.BM._layer_is_subset(layer, self.layer):
685685
return False
686686

687687
# don't re-draw if the collection has been hidden in the companion-widget
@@ -891,7 +891,6 @@ def on_fetch_bg(self, layer=None, bbox=None, check_redraw=True):
891891

892892
s = self._get_datasize(**props)
893893
self._print_datasize_warnings(s)
894-
895894
# stop here in case we are dealing with a pick-only dataset
896895
if self._only_pick:
897896
return
@@ -940,9 +939,9 @@ def on_fetch_bg(self, layer=None, bbox=None, check_redraw=True):
940939

941940
self.m.cb.pick._set_artist(coll)
942941

943-
except Exception:
942+
except Exception as ex:
944943
_log.exception(
945-
f"EOmaps: Unable to plot the data for the layer '{layer}'!",
944+
f"EOmaps: Unable to plot the data for the layer '{layer}'!\n{ex}",
946945
exc_info=_log.getEffectiveLevel() <= logging.DEBUG,
947946
)
948947

eomaps/_maps_base.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,94 @@ def config(
144144
if log_level is not None:
145145
set_loglevel(log_level)
146146

147+
def apply_webagg_fix(cls):
148+
"""
149+
Apply fix to avoid slow updates and lags due to event-accumulation in webagg backend.
150+
151+
(e.g. when using `matplotlib.use("webagg")`)
152+
153+
- Events that occur while draws are pending are dropped and only the
154+
last event of each type that occured during the wait is finally executed.
155+
156+
Note
157+
----
158+
159+
Using this fix is **experimental** and will monkey-patch matplotlibs
160+
`FigureCanvasWebAggCore` and `FigureManagerWebAgg` to avoid event accumulation!
161+
162+
You MUST call this function at the very beginning of the script to ensure
163+
changes are applied correctly!
164+
165+
There might be unwanted side-effects for callbacks that require all events
166+
to be executed consecutively independent of the draw-state (e.g. typing text).
167+
168+
"""
169+
from matplotlib.backends.backend_webagg_core import (
170+
FigureCanvasWebAggCore,
171+
FigureManagerWebAgg,
172+
)
173+
174+
def handle_ack(self, event):
175+
self._ack_cnt += 1 # count the number of received images
176+
177+
def refresh_all(self):
178+
if self.web_sockets:
179+
diff = self.canvas.get_diff_image()
180+
if diff is not None:
181+
for s in self.web_sockets:
182+
s.send_binary(diff)
183+
184+
self._send_cnt += 1 # count the number of sent images
185+
186+
def handle_event(self, event):
187+
if not hasattr(self, "_event_cache"):
188+
self._event_cache = dict()
189+
190+
cnt_equal = self._ack_cnt == self.manager._send_cnt
191+
192+
# always process ack and draw events
193+
# process other events only if "ack count" equals "send count"
194+
# (e.g. if we received and handled all pending images)
195+
if cnt_equal or event["type"] in ["ack", "draw"]:
196+
# immediately process all cached events
197+
for cache_event_type, cache_event in self._event_cache.items():
198+
getattr(
199+
self,
200+
"handle_{0}".format(cache_event_type),
201+
self.handle_unknown_event,
202+
)(cache_event)
203+
self._event_cache.clear()
204+
205+
# reset counters to avoid overflows (just a precaution to avoid overflows)
206+
if cnt_equal:
207+
self._ack_cnt, self.manager._send_cnt = 0, 0
208+
209+
# process event
210+
e_type = event["type"]
211+
handler = getattr(
212+
self, "handle_{0}".format(e_type), self.handle_unknown_event
213+
)
214+
else:
215+
# ignore events in case we have a pending image that is on the way to be processed
216+
# cache the latest event of each type so we can process it once we are ready
217+
self._event_cache[event["type"]] = event
218+
219+
# a final savety precaution in case send count is lower than ack count
220+
# (e.g. we wait for an image but there was no image sent)
221+
if self.manager._send_cnt < self._ack_cnt:
222+
# reset counts... they seem to be incorrect
223+
self._ack_cnt, self.manager._send_cnt = 0, 0
224+
return
225+
226+
return handler(event)
227+
228+
FigureCanvasWebAggCore._ack_cnt = 0
229+
FigureCanvasWebAggCore.handle_ack = handle_ack
230+
FigureCanvasWebAggCore.handle_event = handle_event
231+
232+
FigureManagerWebAgg._send_cnt = 0
233+
FigureManagerWebAgg.refresh_all = refresh_all
234+
147235

148236
class MapsBase(metaclass=_MapsMeta):
149237
def __init__(

eomaps/colorbar.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,8 @@ class ColorBar(ColorBarBase):
631631
632632
"""
633633

634+
max_n_classify_bins_to_label = 30
635+
634636
def __init__(self, *args, inherit_position=True, layer=None, **kwargs):
635637
super().__init__(*args, **kwargs)
636638

@@ -1197,9 +1199,11 @@ def _set_tick_formatter(self):
11971199
return
11981200

11991201
if self._m._classified:
1200-
self.cb.set_ticks(
1201-
np.unique(np.clip(self._m.classify_specs._bins, self._vmin, self._vmax))
1202+
unique_bins = np.unique(
1203+
np.clip(self._m.classify_specs._bins, self._vmin, self._vmax)
12021204
)
1205+
if len(unique_bins) <= self.max_n_classify_bins_to_label:
1206+
self.cb.set_ticks(unique_bins)
12031207

12041208
if self.orientation == "horizontal":
12051209
if self._m._classified:

eomaps/eomaps.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,18 @@ def coll(self):
362362
"""The collection representing the dataset plotted by m.plot_map()."""
363363
return self._coll
364364

365+
@property
366+
def _shape_assigned(self):
367+
"""Return True if the shape is explicitly assigned and False otherwise"""
368+
# the shape is considered assigned if an explicit shape is set
369+
# or if the data has been plotted with the default shape
370+
371+
q = self._shape is None or (
372+
getattr(self._shape, "_is_default", False) and not self._data_plotted
373+
)
374+
375+
return not q
376+
365377
@property
366378
def shape(self):
367379
"""
@@ -372,8 +384,10 @@ def shape(self):
372384
for 2D datasets and "shade_points" is used for unstructured datasets.
373385
374386
"""
375-
if self._shape is None:
387+
388+
if not self._shape_assigned:
376389
self._set_default_shape()
390+
self._shape._is_default = True
377391

378392
return self._shape
379393

@@ -531,7 +545,7 @@ def new_map(
531545
m2.inherit_data(self)
532546
if inherit_classification:
533547
m2.inherit_classification(self)
534-
if inherit_shape:
548+
if inherit_shape and self._shape_assigned:
535549
getattr(m2.set_shape, self.shape.name)(**self.shape._initargs)
536550

537551
if np.allclose(self.ax.bbox.bounds, m2.ax.bbox.bounds):
@@ -662,7 +676,7 @@ def new_layer(
662676
m.inherit_data(self)
663677
if inherit_classification:
664678
m.inherit_classification(self)
665-
if inherit_shape:
679+
if inherit_shape and self._shape_assigned:
666680
getattr(m.set_shape, self.shape.name)(**self.shape._initargs)
667681

668682
# make sure the new layer does not attempt to reset the extent if
@@ -2765,8 +2779,9 @@ def plot_map(
27652779
else:
27662780
if "norm" in kwargs:
27672781
norm = kwargs.pop("norm")
2768-
norm.vmin = self._vmin
2769-
norm.vmax = self._vmax
2782+
if not isinstance(norm, str): # to allow datashader "eq_hist" norm
2783+
norm.vmin = self._vmin
2784+
norm.vmax = self._vmax
27702785
else:
27712786
norm = plt.Normalize(vmin=self._vmin, vmax=self._vmax)
27722787

@@ -3499,8 +3514,15 @@ def _classify_data(
34993514
if vmax > max(bins):
35003515
bins = [*bins, vmax]
35013516

3502-
cbcmap = cmap
3503-
norm = mpl.colors.BoundaryNorm(bins, cmap.N)
3517+
# TODO Always use resample once mpl>3.6 is pinned
3518+
if hasattr(cmap, "resampled") and len(bins) > cmap.N:
3519+
# Resample colormap to contain enough color-values
3520+
# as needed by the boundary-norm.
3521+
cbcmap = cmap.resampled(len(bins))
3522+
else:
3523+
cbcmap = cmap
3524+
3525+
norm = mpl.colors.BoundaryNorm(bins, cbcmap.N)
35043526

35053527
self._emit_signal("cmapsChanged")
35063528

eomaps/shapes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,11 @@ def _get_radius(m, radius, radius_crs):
426426
if m._data_manager.x0 is None:
427427
m._data_manager.set_props(None)
428428

429+
# check if the first element of x0 is nonzero...
430+
# (to avoid slow performance of np.any for large arrays)
431+
if not np.any(m._data_manager.x0.take(0)):
432+
return None
433+
429434
_log.info("EOmaps: Estimating shape radius...")
430435
radiusx, radiusy = Shapes._estimate_radius(m, radius_crs)
431436

eomaps/widgets.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from contextlib import contextmanager
22

33
import numpy as np
4-
import matplotlib.pyplot as plt
54

65
from . import _log
76
from ._blit_manager import LayerParser
@@ -12,16 +11,6 @@
1211
_log.warning("EOmaps-widgets are missing the required dependency 'ipywidgets'!")
1312

1413

15-
def _check_backend():
16-
backend = plt.get_backend()
17-
if "ipympl" not in backend.lower():
18-
_log.warning(
19-
"EOmaps-widgets only work with the 'ipympl (widget)' backend! "
20-
"Make sure you have 'ipympl' installed and use the magic-command "
21-
"'%matplotlib widget' to switch to the interactive jupyter backend!"
22-
)
23-
24-
2514
@contextmanager
2615
def _force_full(m):
2716
"""A contextmanager to force a full update of the figure (to avoid glitches)"""
@@ -100,8 +89,6 @@ class _LayerSelectionWidget:
10089
_widget_cls = None
10190

10291
def __init__(self, m, layers=None, **kwargs):
103-
_check_backend()
104-
10592
self._m = m
10693
self._set_layers_options(layers)
10794

@@ -336,8 +323,6 @@ class LayerButton(ipywidgets.Button):
336323

337324
def __init__(self, m, layer, **kwargs):
338325
self._m = m
339-
_check_backend()
340-
341326
self._layer = self._parse_layer(layer)
342327

343328
kwargs.setdefault("description", self._layer)
@@ -386,8 +371,6 @@ class LayerOverlaySlider(ipywidgets.FloatSlider):
386371

387372
def __init__(self, m, layer, **kwargs):
388373
self._m = m
389-
_check_backend()
390-
391374
self._layer = layer
392375

393376
kwargs.setdefault("value", 0)
@@ -449,8 +432,6 @@ class _CallbackWidget:
449432

450433
def __init__(self, m, widget_kwargs=None, **kwargs):
451434
self._m = m
452-
_check_backend()
453-
454435
self._kwargs = kwargs
455436

456437
if widget_kwargs is None:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ eomaps = ["logo.png", "NE_features.json", "qtcompanion/icons/*"]
1111

1212
[project]
1313
name = "eomaps"
14-
version = "8.2"
14+
version = "8.2.1"
1515
description = "A library to create interactive maps of geographical datasets."
1616
readme = "README.md"
1717
license = {file = "LICENSE"}

0 commit comments

Comments
 (0)