diff --git a/eomaps/_data_manager.py b/eomaps/_data_manager.py index 1a3f0cde5..61b32fd29 100644 --- a/eomaps/_data_manager.py +++ b/eomaps/_data_manager.py @@ -652,7 +652,12 @@ def _select_vals(self, val, qs, slices=None): else: val = np.asanyarray(val) - if len(val.shape) == 2 and qx is not None and qy is not None: + if ( + len(val.shape) == 2 + and qx is not None + and qy is not None + and slices is not None + ): (x0, x1, y0, y1) = slices ret = val[y0:y1, x0:x1] else: @@ -895,6 +900,11 @@ def get_props(self, *args, **kwargs): else: slices, blocksize = None, None + # remember last selection and slices (required in case explicit + # colors are provided since they must be selected accordingly) + self._last_qs = qs + self._last_slices = slices + self._current_data = dict( xorig=self._select_vals(self.xorig, qs, slices), yorig=self._select_vals(self.yorig, qs, slices), diff --git a/eomaps/_version.py b/eomaps/_version.py index 638b07121..6b5a69c78 100644 --- a/eomaps/_version.py +++ b/eomaps/_version.py @@ -1 +1 @@ -__version__ = "7.3.1" +__version__ = "7.3.2" diff --git a/eomaps/_webmap.py b/eomaps/_webmap.py index 8934b50da..3d7dccebe 100644 --- a/eomaps/_webmap.py +++ b/eomaps/_webmap.py @@ -584,12 +584,15 @@ def _do_add_layer(self, m, layer, **kwargs): class _WebServiceCollection: - def __init__(self, m, service_type="wmts", url=None): + def __init__(self, m, service_type="wmts", url=None, **kwargs): self._m = m self._service_type = service_type if url is not None: self._url = url + # additional kwargs that will be passed to owslib.WebMapService() + self._service_kwargs = kwargs.copy() + def __getitem__(self, key): return self.add_layer.__dict__[key] @@ -626,28 +629,28 @@ def findlayer(self, name): return [i for i in self.layers if name.lower() in i.lower()] @staticmethod - def _get_wmts(url): + def _get_wmts(url, **kwargs): # TODO expose useragent # lazy import used to avoid long import times from owslib.wmts import WebMapTileService - return WebMapTileService(url) + return WebMapTileService(url, **kwargs) @staticmethod - def _get_wms(url): + def _get_wms(url, **kwargs): # TODO expose useragent # lazy import used to avoid long import times from owslib.wms import WebMapService - return WebMapService(url) + return WebMapService(url, **kwargs) @property @lru_cache() def add_layer(self): if self._service_type == "wmts": - wmts = self._get_wmts(self._url) + wmts = self._get_wmts(self._url, **self._service_kwargs) layers = dict() for key in wmts.contents.keys(): layername = _sanitize(key) @@ -665,7 +668,7 @@ def add_layer(self): layers[layername] = wmtslayer elif self._service_type == "wms": - wms = self._get_wms(self._url) + wms = self._get_wms(self._url, **self._service_kwargs) layers = dict() for key in wms.contents.keys(): layername = _sanitize(key) @@ -821,14 +824,14 @@ def _fetch_layers(self): url = self._url if url is not None: if self._service_type == "wms": - wms = self._get_wms(url) + wms = self._get_wms(url, **self._service_kwargs) layer_names = list(wms.contents.keys()) for lname in layer_names: self._layers["layer_" + _sanitize(lname)] = _WMSLayer( self._m, wms, lname ) elif self._service_type == "wmts": - wmts = self._get_wmts(url) + wmts = self._get_wmts(url, **self._service_kwargs) layer_names = list(wmts.contents.keys()) for lname in layer_names: self._layers["layer_" + _sanitize(lname)] = _WMTSLayer( diff --git a/eomaps/eomaps.py b/eomaps/eomaps.py index a9359b3d9..6d43f01ab 100644 --- a/eomaps/eomaps.py +++ b/eomaps/eomaps.py @@ -86,21 +86,8 @@ def _handle_backends(): active_backend = plt.get_backend() - if active_backend in ["module://matplotlib_inline.backend_inline"]: - plt.ioff() - - if not Maps._backend_warning_shown and not BlitManager._snapshot_on_update: - _log.info( - "EOmaps disables matplotlib's interactive mode (e.g. 'plt.ioff()') " - f"for the backend {plt.get_backend()}.\n" - "Call `m.snapshot()` to print a static snapshot of the map " - "to a Jupyter Notebook cell (or an IPython console)!" - ) - - Maps._backend_warning_shown = True - # to avoid flickering in the layout editor in jupyter notebooks - elif active_backend in ["module://ipympl.backend_nbagg"]: + if active_backend in ["module://ipympl.backend_nbagg"]: plt.ioff() else: if Maps._use_interactive_mode is True: @@ -3390,19 +3377,18 @@ def show(self, clear=True): show_layer : Set the currently visible layer. """ - if not plt.isinteractive(): - try: - __IPYTHON__ - except NameError: - plt.show() + try: + __IPYTHON__ + except NameError: + plt.show() + else: + active_backend = plt.get_backend() + # print a snapshot to the active ipython cell in case the + # inline-backend is used + if active_backend in ["module://matplotlib_inline.backend_inline"]: + self.BM.update(clear_snapshot=clear) else: - active_backend = plt.get_backend() - # print a snapshot to the active ipython cell in case the - # inline-backend is used - if active_backend in ["module://matplotlib_inline.backend_inline"]: - self.BM.update(clear_snapshot=clear) - else: - plt.show() + plt.show() def snapshot(self, *layer, transparent=False, clear=False): """ @@ -3476,9 +3462,13 @@ def snapshot(self, *layer, transparent=False, clear=False): else: sn = self._get_snapshot() try: - from IPython.display import display + from IPython.display import display_png, clear_output + + if clear: + clear_output(wait=True) + # use display_png to avoid issues with transparent snapshots + display_png(Image.fromarray(sn, "RGBA"), raw=False) - display(Image.fromarray(sn, "RGBA"), display_id=True, clear=clear) except Exception: _log.exception( "Unable to display the snapshot... is the script " @@ -4090,6 +4080,8 @@ def _init_figure(self, ax=None, plot_crs=None, **kwargs): _handle_backends() self._f = plt.figure(**kwargs) + # to hide canvas header in jupyter notebooks (default figure label) + self._f.canvas.header_visible = False # override Figure.savefig with Maps.savefig but keep original # method accessible via Figure._mpl_orig_savefig @@ -4215,16 +4207,17 @@ def _init_figure(self, ax=None, plot_crs=None, **kwargs): if self.parent._layout_editor is None: self.parent._layout_editor = LayoutEditor(self.parent, modifier="alt+l") - if newfig: - # we only need to call show if a new figure has been created! - if ( - # plt.isinteractive() or - plt.get_backend() - == "module://ipympl.backend_nbagg" - ): - # make sure to call show only if we use an interactive backend... - # or within the ipympl backend (otherwise it will block subsequent code!) - plt.show() + active_backend = plt.get_backend() + # we only need to call show if a new figure has been created! + if newfig and active_backend == "module://ipympl.backend_nbagg": + # make sure to call show only if we use an interactive backend... + # or within the ipympl backend (otherwise it will block subsequent code!) + plt.show() + + if active_backend == "module://matplotlib_inline.backend_inline": + # close the figure to avoid duplicated (empty) plots created + # by the inline-backend manager in jupyter notebooks + plt.close(self.f) def _get_ax_label(self): return "map" @@ -4567,7 +4560,7 @@ def _classify_data( bins = [vmin, *bins] if vmax > max(bins): - bins[np.argmax(bins)] = vmax + bins = [*bins, vmax] cbcmap = cmap norm = mpl.colors.BoundaryNorm(bins, cmap.N) @@ -4826,7 +4819,9 @@ def _plot_map( def _sel_c_transp(self, c): return self._data_manager._select_vals( - c.T if self._data_manager._z_transposed else c + c.T if self._data_manager._z_transposed else c, + qs=self._data_manager._last_qs, + slices=self._data_manager._last_slices, ) def _handle_explicit_colors(self, color): @@ -5645,11 +5640,8 @@ def set_frame(self, rounded=0, **kwargs): >>> path_effects=[pe.withStroke(linewidth=7, foreground="m")]) """ - self.redraw("__SPINES__") for key in ("fc", "facecolor"): - self.redraw("__BG__") - if key in kwargs: self.ax.patch.set_facecolor(kwargs.pop(key)) @@ -5712,3 +5704,6 @@ def cb(*args, **kwargs): self.BM._before_fetch_bg_actions.append(cb) self.ax._EOmaps_rounded_spine_attached = True + + self.redraw("__SPINES__") + self.redraw("__BG__") diff --git a/eomaps/helpers.py b/eomaps/helpers.py index c4fc715ec..3d7dd4076 100644 --- a/eomaps/helpers.py +++ b/eomaps/helpers.py @@ -624,6 +624,13 @@ def modifier_pressed(self, val): self._modifier_pressed = val self.m.cb.execute_callbacks(not val) + if self._modifier_pressed: + self.m.BM._disable_draw = True + self.m.BM._disable_update = True + else: + self.m.BM._disable_draw = False + self.m.BM._disable_update = False + @property def ms(self): return [self.m.parent, *self.m.parent._children] @@ -1493,7 +1500,7 @@ def apply_layout(self, layout): # check if all relevant axes are specified in the layout valid_keys = set(self.get_layout()) if valid_keys != set(layout): - warnings.warn( + _log.warning( "EOmaps: The the layout does not match the expected structure! " "Layout might not be properly restored. " "Invalid or missing keys:\n" @@ -1549,6 +1556,9 @@ def __init__(self, m): List of the artists to manage """ + self._disable_draw = False + self._disable_update = False + self._m = m self._artists = dict() @@ -2109,12 +2119,11 @@ def _do_fetch_bg(self, layer, bbox=None): return if renderer: - if not self._m.parent._layout_editor._modifier_pressed: - for art in allartists: - if art not in self._hidden_artists: - art.draw(renderer) - art.stale = False - self._bg_layers[layer] = renderer.copy_from_bbox(bbox) + for art in allartists: + if art not in self._hidden_artists: + art.draw(renderer) + art.stale = False + self._bg_layers[layer] = renderer.copy_from_bbox(bbox) def fetch_bg(self, layer=None, bbox=None): """ @@ -2132,8 +2141,6 @@ def fetch_bg(self, layer=None, bbox=None): The default is None. """ - if self._m.parent._layout_editor._modifier_pressed: - return if layer is None: layer = self.bg_layer @@ -2161,28 +2168,31 @@ def _disconnect_draw(self): def on_draw(self, event): """Callback to register with 'draw_event'.""" + + if self._disable_draw: + return + cv = self.canvas - _log.log(5, "draw") + loglevel = _log.getEffectiveLevel() - try: - if ( - "RendererBase._draw_disabled" - in cv.get_renderer().draw_image.__qualname__ - ): - # TODO this fixes issues when saving figues with a "tight" bbox, e.g.: - # m.savefig(bbox_inches='tight', pad_inches=0.1) - - # This workaround is necessary but the implementation is suboptimal since - # it relies on the __qualname__ to identify if the - # `matplotlib.backend_bases.RendererBase._draw_disabled()` context is active - # The reason why the "_draw_disabled" context has to be handled explicitly - # is because otherwise empty backgrounds would be fetched (and cached) by - # the draw-event and the export would result in an empty figure. + if hasattr(cv, "get_renderer") and not cv.is_saving(): + + renderer = cv.get_renderer() + if renderer is None: + # don't run on_draw if no renderer is available return - except AttributeError: - # return on AttributeError to handle backends that don't expose the renderer + else: + # don't run on_draw if no renderer is available + # (this is true for svg export where mpl export routines + # are used to avoid issues) + if loglevel <= 5: + _log.log(5, " not drawing") + return + if loglevel <= 5: + _log.log(5, "draw") + if event is not None: if event.canvas != cv: raise RuntimeError @@ -2226,7 +2236,8 @@ def on_draw(self, event): except Exception: # we need to catch exceptions since QT does not like them... - pass + if loglevel <= 5: + _log.log(5, "There was an error during draw!", exc_info=True) def add_artist(self, art, layer=None): """ @@ -2657,7 +2668,7 @@ def update( If True, clear the active cell before plotting a snapshot of the figure. The default is True. """ - if self._m.parent._layout_editor._modifier_pressed: + if self._disable_update: # don't update during layout-editing return diff --git a/eomaps/ne_features.py b/eomaps/ne_features.py index 4cec87112..f6986d485 100644 --- a/eomaps/ne_features.py +++ b/eomaps/ne_features.py @@ -34,7 +34,7 @@ class NaturalEarth_presets: def __init__(self, m): self._m = m - def __call__(self, *args, scale=50, layer=None): + def __call__(self, *args, scale=50, layer=None, **kwargs): """ Add multiple preset-features in one go. @@ -42,7 +42,7 @@ def __call__(self, *args, scale=50, layer=None): Parameters ---------- - *args : str + \*args : str The names of the features to add. scale : int or str Set the scale of the feature preset (10, 50, 110 or "auto") @@ -54,7 +54,9 @@ def __call__(self, *args, scale=50, layer=None): - If None, the layer of the parent object is used. The default is None. - + \*\*kwargs: + Additional style kwargs passed to all features + (e.g. alpha, facecolor, edgecolor, linewidth, ...) """ wrong_names = set(args).difference(self._feature_names) assert len(wrong_names) == 0, ( @@ -63,7 +65,7 @@ def __call__(self, *args, scale=50, layer=None): ) for a in args: - getattr(self, a)(scale=scale, layer=layer) + getattr(self, a)(scale=scale, layer=layer, **kwargs) @property def _feature_names(self): diff --git a/eomaps/shapes.py b/eomaps/shapes.py index 044d53c7f..bfe5640df 100644 --- a/eomaps/shapes.py +++ b/eomaps/shapes.py @@ -326,7 +326,6 @@ def _get_colors_and_array(kwargs, mask): # identify colors and the array # special treatment of array input to properly mask values array = kwargs.pop("array", None) - if array is not None: if mask is not None: array = array[mask] @@ -336,17 +335,18 @@ def _get_colors_and_array(kwargs, mask): color_vals = dict() for c_key in ["fc", "facecolor", "color"]: color = kwargs.pop(c_key, None) + if color is not None: # explicit treatment for recarrays (to avoid performance issues) # with matplotlib.colors.to_rgba_array() # (recarrays are used to convert 3/4 arrays into an rgb(a) array # in m._handle_explicit_colors() ) if isinstance(color, np.recarray): - color_vals[c_key] = color[mask].view( + color_vals[c_key] = color[mask.reshape(color.shape)].view( (float, len(color.dtype.names)) ) # .ravel() elif isinstance(color, np.ndarray): - color_vals[c_key] = color[mask] + color_vals[c_key] = color[mask.reshape(color.shape)] else: color_vals[c_key] = color diff --git a/eomaps/webmap_containers.py b/eomaps/webmap_containers.py index 8162b2a5c..112970c4d 100644 --- a/eomaps/webmap_containers.py +++ b/eomaps/webmap_containers.py @@ -2422,7 +2422,9 @@ def Austria(self): WMS.__doc__ = type(self).Austria.__doc__ return WMS - def get_service(self, url, service_type="wms", rest_API=False, maxzoom=19): + def get_service( + self, url, service_type="wms", rest_API=False, maxzoom=19, **kwargs + ): """ Get a object that can be used to add WMS, WMTS or XYZ services based on a GetCapabilities-link or a link to a ArcGIS REST API @@ -2470,6 +2472,11 @@ def get_service(self, url, service_type="wms", rest_API=False, maxzoom=19): The maximum zoom-level available (to avoid http-request errors) for too high zoom levels. The default is 19. + kwargs : + Additional keyword arguments passed to `owslib.WebMapService()`. + (only relevant if type is "wms" or "wmts") + + For example: `version=1.3.0` Returns ------- @@ -2541,6 +2548,8 @@ def get_service(self, url, service_type="wms", rest_API=False, maxzoom=19): service_type=service_type, ) else: - service = _WebServiceCollection(self._m, service_type="wms", url=url) + service = _WebServiceCollection( + self._m, service_type="wms", url=url, **kwargs + ) return service