diff --git a/.github/workflows/testMaps.yml b/.github/workflows/testMaps.yml index 807c4219f..af00f804d 100644 --- a/.github/workflows/testMaps.yml +++ b/.github/workflows/testMaps.yml @@ -20,9 +20,15 @@ jobs: # checkout the repository - uses: actions/checkout@v2 # install miniconda environment - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v2.1.1 with: environment-file: tests/test_env.yml + + # use mamba to speed up installation + mamba-version: "*" + channels: conda-forge + channel-priority: true + activate-environment: testMaps show-channel-urls: true diff --git a/.gitignore b/.gitignore index d3f6d2388..aa5f75a0a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,11 @@ docs/_build docs/debug.log docs/.vscode docs/generated +docs/_tables .spyproject +EOmaps.egg-info +.pytest_cache +logos +tests/Images +tests/.ipynb_checkpoints +docs/_static/example_gifs_very_old diff --git a/README.md b/README.md index 9892b5d09..e705a7505 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ### A library to create interactive maps of geographical datasets.
-

- 🌲🌳 Checkout the documentation for more details and examples 🌳🌲 -

--- @@ -46,48 +43,26 @@ > There are breaking API changes between `EOmaps v3.x` and `EOmaps v4.0` > To quickly update existing scripts, see: [⚙ Port script from EOmaps v3.x to v4.x](https://eomaps.readthedocs.io/en/latest/FAQ.html#port-script-from-eomaps-v3-x-to-v4-x) -
-[click to show] a quick summary of the API changes - -- the following properties and functions have been removed: - - ❌ `m.plot_specs.` - - ❌ `m.set_plot_specs()` - - Arguments are now directly passed to relevant functions: - - ```python - m = Maps - # m.set_plot_specs(cmap=..., vmin=..., vmax=..., cpos=..., cpos_radius=..., histbins=...) - # m.plot_specs.< > = ... - m.set_data(..., cpos=..., cpos_radius=...) - m.plot_map(cmap=..., vmin=..., vmax=...) - m.add_colorbar(histbins=...) - ``` - - -- 🔶 `m.set_shape.voroni_diagram` is renamed to `m.set_shape.voronoi_diagram` -- 🔷 custom callbacks are no longer bound to the Maps-object - - the call-signature of custom callbacks has changed to: - `def cb(self, *args, **kwargs) >> def cb(*args, **kwargs)` - - - - - -
- --- ## 🔨 Installation To install EOmaps (and all its dependencies) via the `conda` package-manager, simply use: - ```python conda install -c conda-forge eomaps ``` -For more information, have a look at the [installation instructions](https://eomaps.readthedocs.io/en/latest/general.html#installation) in the documentation! +... to get a huge speedup, use `mamba` to solve the dependencies! +```python +conda install -c conda-forge mamba +mamba install -c conda-forge eomaps +``` +For more information, have a look at the [installation instructions](https://eomaps.readthedocs.io/en/latest/general.html#installation) or checkout the quickstart guide [🚀 from 0 to EOmaps](https://eomaps.readthedocs.io/en/latest/FAQ.html#from-0-to-eomaps-a-quickstart-guide)!
+## 📖 Documentation + +Make sure to have a look at the 🌳 documentation 🌳 which provides a lot of examples on how to create awesome interactive maps (incl. source code)! ## ✔️ Citation Did EOmaps help in your research? @@ -103,15 +78,41 @@ Open an [issue](https://github.com/raphaelquast/EOmaps/issues) or start a [discu (I'm of course also happy about actual pull requests on features and bug-fixes!) --------------- + + + + + + + + + + + + + + + + + +
+ EOmaps example 6 + + EOmaps example 2 +
+ EOmaps example 7 + + EOmaps example 8 +
+ EOmaps example 9 + + EOmaps example 4 +
+ EOmaps inset-maps + + EOmaps example 3 +
-

-EOmaps example image 2 -EOmaps example image 1 -EOmaps example image 3 -EOmaps example image 1 -EOmaps example image 1 -EOmaps example image 1 -

## 🌳 Basic usage diff --git a/docs/_static/fig1.gif b/docs/_static/fig1.gif index 7502742c4..c5b60d3d8 100644 Binary files a/docs/_static/fig1.gif and b/docs/_static/fig1.gif differ diff --git a/docs/_static/fig1.png b/docs/_static/fig1.png deleted file mode 100644 index 8d8b279ee..000000000 Binary files a/docs/_static/fig1.png and /dev/null differ diff --git a/docs/_static/fig2.gif b/docs/_static/fig2.gif index b9aa530bd..55eb8168a 100644 Binary files a/docs/_static/fig2.gif and b/docs/_static/fig2.gif differ diff --git a/docs/_static/fig2.png b/docs/_static/fig2.png deleted file mode 100644 index 9b60feb3a..000000000 Binary files a/docs/_static/fig2.png and /dev/null differ diff --git a/docs/_static/fig3.png b/docs/_static/fig3.png index 829af94f5..85eb9287a 100644 Binary files a/docs/_static/fig3.png and b/docs/_static/fig3.png differ diff --git a/docs/_static/fig6.gif b/docs/_static/fig6.gif index 7cfafa37f..1ae7197fa 100644 Binary files a/docs/_static/fig6.gif and b/docs/_static/fig6.gif differ diff --git a/docs/_static/inset_maps.png b/docs/_static/inset_maps.png new file mode 100644 index 000000000..b654e6007 Binary files /dev/null and b/docs/_static/inset_maps.png differ diff --git a/docs/api.rst b/docs/api.rst index 253e307d8..8b7f5fb0e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -24,9 +24,12 @@ Possible ways for specifying the crs for plotting are: -- If you provide an integer, it is identified as an epsg-code (e.g. ``4326``, ``3035``, etc.). +- If you provide an integer, it is identified as an epsg-code (e.g. ``4326``, ``3035``, etc.) + + - 4326 hereby defaults to `PlateCarree` projection + - All other CRS usable for plotting are accessible via ``Maps.CRS``, - e.g.: ``crs=Maps.CRS.Orthographic()`` or ``crs=Maps.CRS.Equi7Grid_projection("EU")``. + e.g.: ``crs=Maps.CRS.Orthographic()``, ``crs=Maps.CRS.GOOGLE_MERCATOR`` or ``crs=Maps.CRS.Equi7Grid_projection("EU")``. (``Maps.CRS`` is just an accessor for ``cartopy.crs``) ▤ Layers @@ -1141,6 +1144,7 @@ To indicate rectangular areas in any given crs, simply use ``m.indicate_extent`` | Before adding a colorbar, you must plot the data using ``m.plot_map()``. | A colorbar with a colored histogram on top can then be added to the map via ``m.add_colorbar``. + .. note:: Colorbars are only visible if the layer at which the data was plotted is visible! diff --git a/eomaps/_cb_container.py b/eomaps/_cb_container.py index dba690877..cc12ddd05 100644 --- a/eomaps/_cb_container.py +++ b/eomaps/_cb_container.py @@ -69,10 +69,11 @@ def _clear_temporary_artists(self): def _sort_cbs(self, cbs): if not cbs: return set() - cbnames = set([i.rsplit("_", 1)[0] for i in cbs]) - + cbnames = set([i.rsplit("__", 1)[0].rsplit("_", 1)[0] for i in cbs]) sortp = self._cb_list + list(set(self._cb_list) ^ cbnames) - return sorted(list(cbs), key=lambda w: sortp.index(w.rsplit("_", 1)[0])) + return sorted( + list(cbs), key=lambda w: sortp.index(w.rsplit("__", 1)[0].rsplit("_", 1)[0]) + ) def __repr__(self): txt = "Attached callbacks:\n " + "\n ".join( diff --git a/eomaps/_containers.py b/eomaps/_containers.py index 3519bd1aa..f6c6cdb5a 100644 --- a/eomaps/_containers.py +++ b/eomaps/_containers.py @@ -138,13 +138,23 @@ def set_colorbar_position(self, pos=None, ratio=None, cb=None): """ if cb is None: - _, _, ax_cb, ax_cb_plot, orientation, _ = self._m._colorbar + ( + _, + _, + ax_cb, + ax_cb_plot, + ax_cb_extend, + extend_frac, + orientation, + _, + ) = self._m._colorbar else: - _, _, ax_cb, ax_cb_plot, orientation, _ = cb + _, _, ax_cb, ax_cb_plot, ax_cb_extend, extend_frac, orientation, _ = cb if orientation == "horizontal": pcb = ax_cb.get_position() pcbp = ax_cb_plot.get_position() + if pos is None: pos = [pcb.x0, pcb.y0, pcb.width, pcb.height + pcbp.height] if ratio is None: @@ -160,6 +170,16 @@ def set_colorbar_position(self, pos=None, ratio=None, cb=None): [pos[0], pos[1] + hcb, pos[2], hp], ) + # adjust colorbar extension arrows + if ax_cb_extend: + frac = ( + ax_cb.bbox.transformed(ax_cb.figure.transFigure.inverted()).width + * extend_frac + ) + ax_cb_extend.set_position( + [pos[0] - frac / 2, pos[1], pos[2] + frac, hcb], + ) + elif orientation == "vertical": pcb = ax_cb.get_position() pcbp = ax_cb_plot.get_position() @@ -177,6 +197,17 @@ def set_colorbar_position(self, pos=None, ratio=None, cb=None): ax_cb_plot.set_position( [pos[0], pos[1], wp, pos[3]], ) + + # adjust colorbar extension arrows + if ax_cb_extend: + frac = ( + ax_cb.bbox.transformed(ax_cb.figure.transFigure.inverted()).height + * extend_frac + ) + ax_cb_extend.set_position( + [pos[0] + wp, pos[1] - frac / 2, wcb, pos[3] + frac], + ) + else: raise TypeError(f"EOmaps: '{orientation}' is not a valid orientation") diff --git a/eomaps/_webmap.py b/eomaps/_webmap.py index ee2fac054..d4a31c343 100644 --- a/eomaps/_webmap.py +++ b/eomaps/_webmap.py @@ -271,7 +271,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) pass - def __call__(self, layer=None, **kwargs): + def __call__(self, layer=None, zorder=0, **kwargs): """ Add the WMTS layer to the map @@ -284,6 +284,9 @@ def __call__(self, layer=None, **kwargs): - If None, the layer of the parent object is used. The default is None. + zorder : float + The zorder of the artist (e.g. the stacking level of overlapping artists) + The default is 0 **kwargs : additional kwargs passed to the WebMap service request. (e.g. transparent=True, time='2020-02-05', etc.) @@ -291,6 +294,7 @@ def __call__(self, layer=None, **kwargs): from . import MapsGrid # do this here to avoid circular imports! for m in self._m if isinstance(self._m, MapsGrid) else [self._m]: + self._zorder = zorder self._kwargs = kwargs if layer is None: self._layer = m.layer @@ -311,7 +315,11 @@ def _do_add_layer(self, m, l): print(f"EOmaps: Adding wmts-layer: {self.name}") art = m.figure.ax.add_wmts( - self._wms, self.name, wmts_kwargs=self._kwargs, interpolation="spline36" + self._wms, + self.name, + wmts_kwargs=self._kwargs, + interpolation="spline36", + zorder=self._zorder, ) m.BM.add_bg_artist(art, l) @@ -322,7 +330,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) pass - def __call__(self, layer=None, **kwargs): + def __call__(self, layer=None, zorder=0, **kwargs): """ Add the WMS layer to the map @@ -335,6 +343,9 @@ def __call__(self, layer=None, **kwargs): - If None, the layer of the parent object is used. The default is None. + zorder : float + The zorder of the artist (e.g. the stacking level of overlapping artists) + The default is 0 **kwargs : additional kwargs passed to the WebMap service request. (e.g. transparent=True, time='2020-02-05', etc.) @@ -343,6 +354,8 @@ def __call__(self, layer=None, **kwargs): for m in self._m if isinstance(self._m, MapsGrid) else [self._m]: self._kwargs = kwargs + self._zorder = zorder + if layer is None: self._layer = m.layer else: @@ -364,7 +377,11 @@ def _do_add_layer(self, m, l, usem=None): # actually add the layer to the map. print(f"EOmaps: ... adding wms-layer {self.name}") art = m.figure.ax.add_wms( - self._wms, self.name, wms_kwargs=self._kwargs, interpolation="spline36" + self._wms, + self.name, + wms_kwargs=self._kwargs, + interpolation="spline36", + zorder=self._zorder, ) m.BM.add_bg_artist(art, l) @@ -877,6 +894,7 @@ def __call__( transparent=False, alpha=1, interpolation="spline36", + zorder=0, **kwargs, ): """ @@ -904,6 +922,9 @@ def __call__( required (e.g. if you don't use the native projection of the WMS) changing this value will slow down re-projection but it can provide a huge boost in image quality! The default is 750. + zorder : float + The zorder of the artist (e.g. the stacking level of overlapping artists) + The default is 0 **kwargs : Additional kwargs passed to the cartopy-wrapper for matplotlib's `imshow`. @@ -920,7 +941,7 @@ def __call__( if isinstance(self._m, MapsGrid): for m in self._m: self._reinit(m).__call__( - layer, transparent, alpha, interpolation, **kwargs + layer, transparent, alpha, interpolation, zorder, **kwargs ) else: @@ -928,6 +949,7 @@ def __call__( interpolation=interpolation, alpha=alpha, origin="lower" ) self._kwargs.update(kwargs) + self._zorder = zorder if self._layer == "all" or self._m.BM.bg_layer == self._layer: # add the layer immediately if the layer is already active @@ -959,7 +981,9 @@ def _do_add_layer(self, m, l): # (only SlippyImageArtist has been subclassed) self._raster_source.validate_projection(m.ax.projection) - img = SlippyImageArtist_NEW(m.ax, self._raster_source, **self._kwargs) + img = SlippyImageArtist_NEW( + m.ax, self._raster_source, zorder=self._zorder, **self._kwargs + ) with self._m.ax.hold_limits(): m.ax.add_image(img) self._artist = img diff --git a/eomaps/eomaps.py b/eomaps/eomaps.py index 8c7e9a267..518cfa798 100644 --- a/eomaps/eomaps.py +++ b/eomaps/eomaps.py @@ -51,6 +51,10 @@ from matplotlib.colors import LinearSegmentedColormap from matplotlib.gridspec import GridSpec, GridSpecFromSubplotSpec, SubplotSpec +import matplotlib.path as mpath +import matplotlib.patches as mpatches +from matplotlib.collections import PatchCollection + from cartopy import crs as ccrs @@ -1747,15 +1751,20 @@ def _add_colorbar( orientation=cb_orientation, ) # plot the histogram - hist_vals, hist_bins, init_hist = ax_cb_plot.hist( - z_data, - orientation=orientation, - bins=bins if (classified and histbins == "bins") else histbins, - color="k", - align="mid", - range=(vmin, vmax) if (vmin and vmax) else None, - density=density, - ) + # (only if the axis has a finite size) + if ax_cb_plot.bbox.width > 0 and ax_cb_plot.bbox.height > 0: + ax_cb_plot.set_axis_on() + _ = ax_cb_plot.hist( + z_data.clip(vmin, vmax) if (vmin or vmax) else z_data, + orientation=orientation, + bins=bins if (classified and histbins == "bins") else histbins, + color="k", + align="mid", + range=(vmin, vmax) if (vmin and vmax) else None, + density=density, + ) + else: + ax_cb_plot.set_axis_off() # identify position of color-splits in the colorbar if isinstance(n_cmap.cmap, LinearSegmentedColormap): @@ -1885,8 +1894,135 @@ def _add_colorbar( ot.set_horizontalalignment("center") ot.set_position((1, 0)) + # make sure axis limits are correct + if orientation == "vertical": + limsetfunc = ax_cb.set_xlim + else: + limsetfunc = ax_cb.set_ylim + + limsetfunc(vmin, vmax) + return cb + def _add_cb_extend_arrows(self, cb, orientation, extend_frac=0.025, which="auto"): + """ + Add a new axis that holds extension-triangles for the colorbar. + + Parameters + ---------- + cb : matplotilb.colorbar + the colorbar to use + orientation : str + "vertical" or "horizontal" + extend_frac : float, optional + the fraction of the axis to use for the extension-triangles. + (NOTE: triangles will be drawn OUTSIDE the axes!!) + The default is 0.03. + which : str + Indicator which triangles should be drawn + (one of "auto", "upper", "lower" or "both"). + The default is "auto" in which case only relevant arrows are drawn + (e.g. "upper" is only drawn if there are values > vmax). + """ + + if which == "auto": + a = cb.mappable.get_array() + vmin, vmax = cb.mappable.norm.vmin, cb.mappable.norm.vmax + + if (a > vmax).any(): + which = "upper" + if (a < vmin).any(): + if which == "upper": + which = "both" + else: + which = "lower" + + if which == "auto": + # (no extension arrows necessary) + return + + # get the bounding box of the current colorbar axis + bbox = self.figure.ax_cb.bbox.transformed(self.figure.f.transFigure.inverted()) + + if orientation == "horizontal": + if which == "both": + which = "left right" + elif which == "upper": + which = "right" + elif which == "lower": + which = "left" + + frac = extend_frac * (bbox.width) + axcb2 = self.figure.f.add_axes( + (bbox.x0 - frac / 2, bbox.y0, bbox.width + frac, bbox.height), + zorder=-99, + ) + axcb2.get_shared_y_axes().join(axcb2, self.figure.ax_cb) + # set the limits to (0, 1) so we can directly use the fractions + axcb2.set_xlim(0, 1) + + else: + if which == "both": + which = "top bottom" + elif which == "upper": + which = "top" + elif which == "lower": + which = "bottom" + frac = extend_frac * (bbox.height) + axcb2 = self.figure.f.add_axes( + (bbox.x0, bbox.y0 - frac / 2, bbox.width, bbox.height + frac), + zorder=-99, + ) + axcb2.get_shared_x_axes().join(axcb2, self.figure.ax_cb) + # set the limits to (0, 1) so we can directly use the fractions + axcb2.set_ylim(0, 1) + + axcb2.set_axis_off() + + # get the coordinates of the colorbar in units of axcb2 + + bbox_transf = self.figure.ax_cb.bbox.transformed(axcb2.transAxes.inverted()) + + x0, x1, y0, y1 = bbox_transf.x0, bbox_transf.x1, bbox_transf.y0, bbox_transf.y1 + + # make sure the arrow overlapps a little bit with the parent cb-axes + # to avoid gaps between the arrow and the colorbar + ovx, ovy = bbox_transf.width * 0.1, bbox_transf.height * 0.1 + + points = dict( + left=[[0, 0.5], [x0, 1], [x0 + ovx, 1], [x0 + ovx, 0], [x0, 0], [0, 0.5]], + right=[[1, 0.5], [x1, 1], [x1 - ovx, 1], [x1 - ovx, 0], [x1, 0], [1, 0.5]], + top=[[0.5, 1], [0, y1 - ovy], [1, y1 - ovy], [0.5, 1]], + bottom=[[0.5, 0], [0, y0 + ovy], [1, y0 + ovy], [0.5, 0]], + ) + + cmap = self.figure.coll.cmap + colors = dict( + left=cmap.get_under(), + right=cmap.get_over(), + top=cmap.get_over(), + bottom=cmap.get_under(), + ) + + patches = [] + use_colors = [] + for w in which.split(" "): + p = points[w] + Path = mpath.Path + path_data = [ + (Path.MOVETO, p[0]), + *[(Path.LINETO, pi) for pi in p[1:-1]], + (Path.CLOSEPOLY, p[-1]), + ] + codes, verts = zip(*path_data) + path = mpath.Path(verts, codes) + patches.append(mpatches.PathPatch(path)) + use_colors.append(colors[w]) + collection = PatchCollection(patches, fc=use_colors, ec="none", lw=0) + + axcb2.add_collection(collection) + return axcb2 + @property @wraps(NaturalEarth_features) def add_feature(self): @@ -2409,23 +2545,41 @@ def add_annotation( Examples -------- - >>> m.add_annotation(ID=1) - >>> m.add_annotation(xy=(45, 35), xy_crs=4326) + >>> m.add_annotation(ID=1) + >>> m.add_annotation(xy=(45, 35), xy_crs=4326) + + NOTE: You can provide lists to add multiple annotations in one go! + + >>> m.add_annotation(ID=[1, 5, 10, 20]) + >>> m.add_annotation(xy=([23.5, 45.8, 23.7], [5, 6, 7]), xy_crs=4326) + + The text can be customized by providing either a string - NOTE: You can provide lists to add multiple annotations in one go! + >>> m.add_annotation(ID=1, text="some text") - >>> m.add_annotation(ID=[1, 5, 10, 20]) - >>> m.add_annotation(xy=([23.5, 45.8, 23.7], [5, 6, 7]), xy_crs=4326) + or a callable that returns a string with the following signature: - The text can be customized by providing either a string + >>> def addtxt(m, ID, val, pos, ind): + >>> return f"The ID {ID} at position {pos} has a value of {val}" + >>> m.add_annotation(ID=1, text=addtxt) - >>> m.add_annotation(ID=1, text="some text") + **Customizing the appearance** - or a callable that returns a string with the following signature: + For the full set of possibilities, see: + https://matplotlib.org/stable/tutorials/text/annotations.html - >>> def addtxt(m, ID, val, pos, ind): - >>> return f"The ID {ID} at position {pos} has a value of {val}" - >>> m.add_annotation(ID=1, text=addtxt) + >>> m.add_annotation(xy=[7.10, 45.16], xy_crs=4326, + >>> text="blubb", xytext=(30,30), + >>> horizontalalignment="center", verticalalignment="center", + >>> arrowprops=dict(ec="g", + >>> arrowstyle='-[', + >>> connectionstyle="angle", + >>> ), + >>> bbox=dict(boxstyle='circle,pad=0.5', + >>> fc='yellow', + >>> alpha=0.3 + >>> ) + >>> ) """ @@ -2434,7 +2588,7 @@ def add_annotation( mask = np.isin(self._props["ids"], ID) xy = (self._props["xorig"][mask], self._props["yorig"][mask]) val = self._props["z_data"][mask] - ind = self._props["ids"][mask] + ind = np.where(mask)[0] ID = np.atleast_1d(ID) xy_crs = self.data_specs.crs else: @@ -2687,11 +2841,13 @@ def _shade_map( # (e.g. count, std, var ... ) use an automatic "linear" normalization if aggname in ["first", "last", "max", "min", "mean", "mode"]: kwargs.setdefault("norm", self.classify_specs._norm) + kwargs.setdefault("vmin", vmin) + kwargs.setdefault("vmax", vmax) # clip the data to properly account for vmin and vmax # (do this only if we don't intend to use the full dataset!) - if vmin or vmax: - props["z_data"] = props["z_data"].clip(vmin, vmax) + # if vmin or vmax: + # props["z_data"] = props["z_data"].clip(vmin, vmax) else: kwargs.setdefault("norm", "linear") kwargs.setdefault("vmin", vmin) @@ -3080,8 +3236,8 @@ def _plot_map( # clip the data to properly account for vmin and vmax # (do this only if we don't intend to use the full dataset!) - if vmin or vmax: - props["z_data"] = props["z_data"].clip(vmin, vmax) + # if vmin or vmax: + # props["z_data"] = props["z_data"].clip(vmin, vmax) # ---------------------- classify the data cbcmap, norm, bins, classified = self._classify_data( @@ -3224,7 +3380,9 @@ def plot_map( ----------------- vmin, vmax : float, optional Min- and max. values assigned to the colorbar. The default is None. - + zorder : float + The zorder of the artist (e.g. the stacking level of overlapping artists) + The default is 1 kwargs kwargs passed to the initialization of the matpltolib collection (dependent on the plot-shape) [linewidth, edgecolor, facecolor, ...] @@ -3246,6 +3404,10 @@ def plot_map( kwargs["alpha"], ) + # make sure zorder is set to 1 by default + # (by default shading would use 0 while ordinary collections use 1) + kwargs.setdefault("zorder", 1) + if useshape.name.startswith("shade"): self._shade_map( pick_distance=pick_distance, @@ -3389,10 +3551,13 @@ def add_colorbar( bottom=0.1, left=0.1, right=0.05, + histogram_size=9, layer=None, log=False, tick_formatter=None, dynamic_shade_indicator=False, + add_extend_arrows="auto", + extend_frac=0.025, ): """ Add a colorbar to an existing figure. @@ -3403,14 +3568,16 @@ def add_colorbar( By default, the colorbar will only be visible on the layer of the associated Maps-object (you can override this by providing an explicit "layer"-name). - To change the position of the colorbar, use: + To change the position of the colorbar after it has been created, use: - >>> m.add_colorbar() - >>> m.figure.set_colorbar_position(pos=[.1, .05, .8, .2], ratio=10) + >>> cb = m.add_colorbar() + >>> m.figure.set_colorbar_position(pos=[.1, .05, .8, .2], ratio=10, cb=cb) Parameters ---------- gs : float or matpltolib.gridspec.SubplotSpec + The relative size of the colorbar (or an explicit GridSpec definition) + - if float: The fraction of the the parent axes to use for the colorbar. (The colorbar will "steal" some space from the parent axes.) - if SubplotSpec : A SubplotSpec instance that will be used to initialize @@ -3442,6 +3609,16 @@ def add_colorbar( The padding between the colorbar and the parent axes (as fraction of the plot-height (if "horizontal") or plot-width (if "vertical") The default is (0.05, 0.1, 0.1, 0.05) + histogram_size : float + Set the relative size of the histogram compared to the colorbar, e.g.: + + ` = histogram_size * ` + + - 0 = NO histogram (e.g. a "plain" colorbar) + - 1 = histogram and colorbar have the same size + - 999 = NO colorbar (e.g. a "plain" histogram) + + The default is 9. layer : int, str or None, optional The layer to put the colorbar on. To make the colorbar visible on all layers, use `layer="all"` @@ -3473,6 +3650,20 @@ def add_colorbar( >>> return f"{x} m" The default is None. + add_extend_arrows : str + Set if extension-arrows should be drawn. (e.g. to indicate that there are + some data-values outside the colorbar-range) + + - Can be one of: ("auto", "upper", "lower", "both") + - If "auto": extension-arrows are only drawn if values outside the color + boundaries are encoundered. The default is "auto" + + Note: The range of the colors is set with `m.plot_map(vmin=..., vmax=...)` + extend_frac : float or None + The fraction of the colorbar to use for adding "extension-arrows" to + indicate out-of-bounds values. + If None, no extension arrows will be drawn. + The default is 0.015 See Also -------- @@ -3490,6 +3681,7 @@ def add_colorbar( >>> | | >>> | (top) | >>> | | + >>> | [ - HISTOGRAM - ] | >>> | (left) [ - COLORBAR - ] (right) | >>> | | >>> | (bottom) | @@ -3504,6 +3696,13 @@ def add_colorbar( ) return + assert hasattr( + self.classify_specs, "_bins" + ), "EOmaps: you need to call `m.plot_map()` before adding a colorbar!" + + if layer is None: + layer = self.layer + self._cb_kwargs = dict( orientation=orientation, label=label, @@ -3516,33 +3715,29 @@ def add_colorbar( tick_formatter=tick_formatter, ) - if hasattr(self, "_colorbar"): - print( - "EOmaps: A colorbar already exists for this Maps-object!\n" - + "...use a new layer if you want multiple colorbars or use " - + "`m._remove_colorbar() to remove the existing colorbar." - ) - return + parent_m_for_cb = None + if isinstance(gs, (int, float)): + if hasattr(self, "_colorbar"): + print( + "EOmaps: A colorbar already exists for this Maps-object!\n" + + "...use a new layer if you want multiple colorbars or use " + + "`m._remove_colorbar() to remove the existing colorbar." + ) + return - if layer is None: - layer = self.layer + # check if there is already an existing colorbar in another axis + # and if we find one, use its specs instead of creating a new one - assert hasattr( - self.classify_specs, "_bins" - ), "EOmaps: you need to call `m.plot_map()` before adding a colorbar!" - # check if there is already an existing colorbar in another axis - # and if we find one, use its specs instead of creating a new one - parent_m_for_cb = None - if hasattr(self, "_cb_gridspec"): - parent_m_for_cb = self - else: - # check if self is actually just another layer of an existing Maps object - # that already has a colorbar assigned - for m in [self.parent, *self.parent._children]: - if m is not self and m.ax is self.ax: - if hasattr(m, "_cb_gridspec"): - parent_m_for_cb = m - break + if hasattr(self, "_cb_gridspec"): + parent_m_for_cb = self + else: + # check if self is actually just another layer of an existing Maps object + # that already has a colorbar assigned + for m in [self.parent, *self.parent._children]: + if m is not self and m.ax is self.ax: + if hasattr(m, "_cb_gridspec"): + parent_m_for_cb = m + break if parent_m_for_cb: try: @@ -3607,7 +3802,7 @@ def add_colorbar( subplot_spec=gsspec, hspace=0, wspace=0, - height_ratios=[0.9, 0.1], + height_ratios=[histogram_size, 1], ) # "_add_colorbar" orientation is opposite to the colorbar-orientation! @@ -3621,7 +3816,7 @@ def add_colorbar( subplot_spec=gsspec, hspace=0, wspace=0, - width_ratios=[0.9, 0.1], + width_ratios=[histogram_size, 1], ) # "_add_colorbar" orientation is opposite to the colorbar-orientation! @@ -3705,8 +3900,8 @@ def add_colorbar( norm = coll.norm # make sure the norm clips with respect to vmin/vmax # (only clip if either vmin or vmax is not None) - if vmin or vmax: - z_data = z_data.clip(vmin, vmax) + # if vmin or vmax: + # z_data = z_data.clip(vmin, vmax) cmap = coll.get_cmap() else: norm = self.classify_specs._norm @@ -3796,10 +3991,31 @@ def redraw(*args, **kwargs): self.BM.add_bg_artist(self._ax_cb, layer) self.BM.add_bg_artist(self._ax_cb_plot, layer) + ax_cb_extend = self._add_cb_extend_arrows( + cb, orientation, extend_frac=extend_frac, which=add_extend_arrows + ) # remember colorbar for later (so that we can update its position etc.) - self._colorbar = [layer, cbgs, ax_cb, ax_cb_plot, orientation, cb] - - return [layer, cbgs, ax_cb, ax_cb_plot, orientation, cb] + self._colorbar = [ + layer, + cbgs, + ax_cb, + ax_cb_plot, + ax_cb_extend, + extend_frac, + orientation, + cb, + ] + + return [ + layer, + cbgs, + ax_cb, + ax_cb_plot, + ax_cb_extend, + extend_frac, + orientation, + cb, + ] def indicate_masked_points(self, radius=1.0, **kwargs): """ diff --git a/eomaps/helpers.py b/eomaps/helpers.py index 3400fa202..be490aed5 100644 --- a/eomaps/helpers.py +++ b/eomaps/helpers.py @@ -636,7 +636,7 @@ def cb_scroll(self, event): self._m_picked.figure.set_colorbar_position(b) self._color_axes() - self.m.BM.canvas.draw() + self.m.redraw() def cb_key_press(self, event): if (self.f.canvas.toolbar is not None) and self.f.canvas.toolbar.mode != "": @@ -690,9 +690,7 @@ def _undo_draggable(self): self._modifier_pressed = False self.m._ignore_cb_events = False - self.m.BM._refetch_bg = True - self.f.canvas.draw() - self.m.BM.update() + self.m.redraw() def _make_draggable(self): # all ordinary callbacks will not execute if" self._modifier_pressed" is True! @@ -704,8 +702,17 @@ def _make_draggable(self): self._ax_visible[ax] = ax.get_visible() # make all artists invisible (and remember their visibility state for later) - for l in self.m.BM._bg_artists.values(): - for a in l: + # for l in self.m.BM._bg_artists.values(): + # for a in l: + # self._artists_visible[a] = a.get_visible() + # a.set_visible(False) + dyn_artists = list(chain(*self.m.BM._artists.values())) + for a in set( + [*self.m.figure.f.artists, *chain(*self.m.BM._bg_artists.values())] + ): + if a in dyn_artists: + continue + if not isinstance(a, plt.Axes): self._artists_visible[a] = a.get_visible() a.set_visible(False) @@ -740,9 +747,8 @@ def _make_draggable(self): self.f.canvas.mpl_connect("key_press_event", self.cb_move_with_key) ) - self.m.BM.fetch_bg() self.set_annotations() - self.f.canvas.draw() + self.m.redraw() # taken from https://matplotlib.org/stable/tutorials/advanced/blitting.html#class-based-example @@ -837,15 +843,27 @@ def bg_layer(self, val): # hide all colorbars that are not no the visible layer for m in [self._m.parent, *self._m.parent._children]: if getattr(m, "_colorbar", None) is not None: - [layer, cbgs, ax_cb, ax_cb_plot, orientation, cb] = m._colorbar + [ + layer, + cbgs, + ax_cb, + ax_cb_plot, + ax_cb_extend, + extend_frac, + orientation, + cb, + ] = m._colorbar if layer != val: ax_cb.set_visible(False) ax_cb_plot.set_visible(False) + if ax_cb_extend: + ax_cb_extend.set_visible(False) else: ax_cb.set_visible(True) ax_cb_plot.set_visible(True) - + if ax_cb_extend: + ax_cb_extend.set_visible(True) # self.canvas.flush_events() self._clear_temp_artists("on_layer_change") # self.fetch_bg(self._bg_layer) diff --git a/setup.py b/setup.py index ec8aa345d..35f231266 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ except Exception: long_description = "A library to create interactive maps of geographical datasets." -version = "4.1" +version = "4.1.1" setup( name="EOmaps", diff --git a/tests/example3.py b/tests/example3.py index 763ce7e89..3a5507b75 100644 --- a/tests/example3.py +++ b/tests/example3.py @@ -44,14 +44,17 @@ ) # pass some additional arguments to the plotted collection # ------------------ add a colorbar and change it's appearance -m.add_colorbar(histbins="bins", label="some parameter", density=True) -_ = m.figure.ax_cb_plot.set_ylabel("The Y label") # add a y-label to the histogram +m.add_colorbar( + histbins="bins", label="some parameter", density=True, histogram_size=999 +) + +# add a y-label to the histogram +_ = m.figure.ax_cb_plot.set_ylabel("The Y label") -m.subplots_adjust( - bottom=0.1, top=0.95, left=0.075, right=0.95, hspace=0.2 -) # adjust the padding -m.figure.set_colorbar_position( - pos=[0.125, 0.1, 0.83, 0.15], ratio=999 -) # manually re-position the colorbar +# adjust the padding of the subplots +m.subplots_adjust(bottom=0.1, top=0.95, left=0.075, right=0.95, hspace=0.2) +# manually re-position the colorbar +m.figure.set_colorbar_position(pos=[0.125, 0.1, 0.83, 0.15]) +# add a logo to the plot m.add_logo(position="lr", pad=(-1.1, 0), size=0.1)