Skip to content

Commit

Permalink
Merge pull request #243 from raphaelquast/dev
Browse files Browse the repository at this point in the history
merge for v8.2.1
  • Loading branch information
raphaelquast committed May 20, 2024
2 parents b40a54b + d3c4183 commit 02860ff
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 34 deletions.
6 changes: 5 additions & 1 deletion eomaps/_blit_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,11 @@ def get_bg_artists(self, layer):
# artists that are only visible if both layers are visible! (e.g. "l1|l2")
artists.extend(self._bg_artists.get(l, []))

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

# make the list unique but maintain order (dicts keep order for python>3.7)
Expand Down
7 changes: 3 additions & 4 deletions eomaps/_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ def redraw_required(self, layer):

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

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

s = self._get_datasize(**props)
self._print_datasize_warnings(s)

# stop here in case we are dealing with a pick-only dataset
if self._only_pick:
return
Expand Down Expand Up @@ -940,9 +939,9 @@ def on_fetch_bg(self, layer=None, bbox=None, check_redraw=True):

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

except Exception:
except Exception as ex:
_log.exception(
f"EOmaps: Unable to plot the data for the layer '{layer}'!",
f"EOmaps: Unable to plot the data for the layer '{layer}'!\n{ex}",
exc_info=_log.getEffectiveLevel() <= logging.DEBUG,
)

Expand Down
88 changes: 88 additions & 0 deletions eomaps/_maps_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,94 @@ def config(
if log_level is not None:
set_loglevel(log_level)

def apply_webagg_fix(cls):
"""
Apply fix to avoid slow updates and lags due to event-accumulation in webagg backend.
(e.g. when using `matplotlib.use("webagg")`)
- Events that occur while draws are pending are dropped and only the
last event of each type that occured during the wait is finally executed.
Note
----
Using this fix is **experimental** and will monkey-patch matplotlibs
`FigureCanvasWebAggCore` and `FigureManagerWebAgg` to avoid event accumulation!
You MUST call this function at the very beginning of the script to ensure
changes are applied correctly!
There might be unwanted side-effects for callbacks that require all events
to be executed consecutively independent of the draw-state (e.g. typing text).
"""
from matplotlib.backends.backend_webagg_core import (
FigureCanvasWebAggCore,
FigureManagerWebAgg,
)

def handle_ack(self, event):
self._ack_cnt += 1 # count the number of received images

def refresh_all(self):
if self.web_sockets:
diff = self.canvas.get_diff_image()
if diff is not None:
for s in self.web_sockets:
s.send_binary(diff)

self._send_cnt += 1 # count the number of sent images

def handle_event(self, event):
if not hasattr(self, "_event_cache"):
self._event_cache = dict()

cnt_equal = self._ack_cnt == self.manager._send_cnt

# always process ack and draw events
# process other events only if "ack count" equals "send count"
# (e.g. if we received and handled all pending images)
if cnt_equal or event["type"] in ["ack", "draw"]:
# immediately process all cached events
for cache_event_type, cache_event in self._event_cache.items():
getattr(
self,
"handle_{0}".format(cache_event_type),
self.handle_unknown_event,
)(cache_event)
self._event_cache.clear()

# reset counters to avoid overflows (just a precaution to avoid overflows)
if cnt_equal:
self._ack_cnt, self.manager._send_cnt = 0, 0

# process event
e_type = event["type"]
handler = getattr(
self, "handle_{0}".format(e_type), self.handle_unknown_event
)
else:
# ignore events in case we have a pending image that is on the way to be processed
# cache the latest event of each type so we can process it once we are ready
self._event_cache[event["type"]] = event

# a final savety precaution in case send count is lower than ack count
# (e.g. we wait for an image but there was no image sent)
if self.manager._send_cnt < self._ack_cnt:
# reset counts... they seem to be incorrect
self._ack_cnt, self.manager._send_cnt = 0, 0
return

return handler(event)

FigureCanvasWebAggCore._ack_cnt = 0
FigureCanvasWebAggCore.handle_ack = handle_ack
FigureCanvasWebAggCore.handle_event = handle_event

FigureManagerWebAgg._send_cnt = 0
FigureManagerWebAgg.refresh_all = refresh_all


class MapsBase(metaclass=_MapsMeta):
def __init__(
Expand Down
8 changes: 6 additions & 2 deletions eomaps/colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,8 @@ class ColorBar(ColorBarBase):
"""

max_n_classify_bins_to_label = 30

def __init__(self, *args, inherit_position=True, layer=None, **kwargs):
super().__init__(*args, **kwargs)

Expand Down Expand Up @@ -1197,9 +1199,11 @@ def _set_tick_formatter(self):
return

if self._m._classified:
self.cb.set_ticks(
np.unique(np.clip(self._m.classify_specs._bins, self._vmin, self._vmax))
unique_bins = np.unique(
np.clip(self._m.classify_specs._bins, self._vmin, self._vmax)
)
if len(unique_bins) <= self.max_n_classify_bins_to_label:
self.cb.set_ticks(unique_bins)

if self.orientation == "horizontal":
if self._m._classified:
Expand Down
36 changes: 29 additions & 7 deletions eomaps/eomaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,18 @@ def coll(self):
"""The collection representing the dataset plotted by m.plot_map()."""
return self._coll

@property
def _shape_assigned(self):
"""Return True if the shape is explicitly assigned and False otherwise"""
# the shape is considered assigned if an explicit shape is set
# or if the data has been plotted with the default shape

q = self._shape is None or (
getattr(self._shape, "_is_default", False) and not self._data_plotted
)

return not q

@property
def shape(self):
"""
Expand All @@ -372,8 +384,10 @@ def shape(self):
for 2D datasets and "shade_points" is used for unstructured datasets.
"""
if self._shape is None:

if not self._shape_assigned:
self._set_default_shape()
self._shape._is_default = True

return self._shape

Expand Down Expand Up @@ -531,7 +545,7 @@ def new_map(
m2.inherit_data(self)
if inherit_classification:
m2.inherit_classification(self)
if inherit_shape:
if inherit_shape and self._shape_assigned:
getattr(m2.set_shape, self.shape.name)(**self.shape._initargs)

if np.allclose(self.ax.bbox.bounds, m2.ax.bbox.bounds):
Expand Down Expand Up @@ -662,7 +676,7 @@ def new_layer(
m.inherit_data(self)
if inherit_classification:
m.inherit_classification(self)
if inherit_shape:
if inherit_shape and self._shape_assigned:
getattr(m.set_shape, self.shape.name)(**self.shape._initargs)

# make sure the new layer does not attempt to reset the extent if
Expand Down Expand Up @@ -2765,8 +2779,9 @@ def plot_map(
else:
if "norm" in kwargs:
norm = kwargs.pop("norm")
norm.vmin = self._vmin
norm.vmax = self._vmax
if not isinstance(norm, str): # to allow datashader "eq_hist" norm
norm.vmin = self._vmin
norm.vmax = self._vmax
else:
norm = plt.Normalize(vmin=self._vmin, vmax=self._vmax)

Expand Down Expand Up @@ -3499,8 +3514,15 @@ def _classify_data(
if vmax > max(bins):
bins = [*bins, vmax]

cbcmap = cmap
norm = mpl.colors.BoundaryNorm(bins, cmap.N)
# TODO Always use resample once mpl>3.6 is pinned
if hasattr(cmap, "resampled") and len(bins) > cmap.N:
# Resample colormap to contain enough color-values
# as needed by the boundary-norm.
cbcmap = cmap.resampled(len(bins))
else:
cbcmap = cmap

norm = mpl.colors.BoundaryNorm(bins, cbcmap.N)

self._emit_signal("cmapsChanged")

Expand Down
5 changes: 5 additions & 0 deletions eomaps/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,11 @@ def _get_radius(m, radius, radius_crs):
if m._data_manager.x0 is None:
m._data_manager.set_props(None)

# check if the first element of x0 is nonzero...
# (to avoid slow performance of np.any for large arrays)
if not np.any(m._data_manager.x0.take(0)):
return None

_log.info("EOmaps: Estimating shape radius...")
radiusx, radiusy = Shapes._estimate_radius(m, radius_crs)

Expand Down
19 changes: 0 additions & 19 deletions eomaps/widgets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from contextlib import contextmanager

import numpy as np
import matplotlib.pyplot as plt

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


def _check_backend():
backend = plt.get_backend()
if "ipympl" not in backend.lower():
_log.warning(
"EOmaps-widgets only work with the 'ipympl (widget)' backend! "
"Make sure you have 'ipympl' installed and use the magic-command "
"'%matplotlib widget' to switch to the interactive jupyter backend!"
)


@contextmanager
def _force_full(m):
"""A contextmanager to force a full update of the figure (to avoid glitches)"""
Expand Down Expand Up @@ -100,8 +89,6 @@ class _LayerSelectionWidget:
_widget_cls = None

def __init__(self, m, layers=None, **kwargs):
_check_backend()

self._m = m
self._set_layers_options(layers)

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

def __init__(self, m, layer, **kwargs):
self._m = m
_check_backend()

self._layer = self._parse_layer(layer)

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

def __init__(self, m, layer, **kwargs):
self._m = m
_check_backend()

self._layer = layer

kwargs.setdefault("value", 0)
Expand Down Expand Up @@ -449,8 +432,6 @@ class _CallbackWidget:

def __init__(self, m, widget_kwargs=None, **kwargs):
self._m = m
_check_backend()

self._kwargs = kwargs

if widget_kwargs is None:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ eomaps = ["logo.png", "NE_features.json", "qtcompanion/icons/*"]

[project]
name = "eomaps"
version = "8.2"
version = "8.2.1"
description = "A library to create interactive maps of geographical datasets."
readme = "README.md"
license = {file = "LICENSE"}
Expand Down

0 comments on commit 02860ff

Please sign in to comment.