Skip to content

Commit

Permalink
Merge pull request #239 from raphaelquast/dev
Browse files Browse the repository at this point in the history
merge for v8.2
  • Loading branch information
raphaelquast authored May 13, 2024
2 parents c83405b + 2aa99c9 commit b40a54b
Show file tree
Hide file tree
Showing 55 changed files with 3,075 additions and 2,832 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/testMaps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ jobs:
- name: Test Maps
shell: bash -l {0}
run: |
pip install -e .[all]
pip install -e .[test]
python -m pytest -v --cov=eomaps --cov-report=xml
- name: Upload Image Comparison Artefacts
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: code-coverage-report
path: img_comparison_results
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
Expand Down
3 changes: 3 additions & 0 deletions docs/api_data_visualization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ Shade Raster
:nosignatures:
Maps.set_shape.shade_raster
Maps.set_shade_dpi
.. list-table::
:header-rows: 1
Expand All @@ -640,6 +641,7 @@ Shade Raster
agg_hook=None, # datashader aggregation hook callback
)
.. _shp_shade_points:
Shade Points
Expand All @@ -649,6 +651,7 @@ Shade Points
:nosignatures:
Maps.set_shape.shade_raster
Maps.set_shade_dpi
.. list-table::
:header-rows: 1
Expand Down
22 changes: 18 additions & 4 deletions docs/contribute.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,26 +99,40 @@ The `environment.yml` file already contains the packages required to run the tes

- `pytest <https://docs.pytest.org>`__ to run the tests
- `pytest-cov <https://github.com/pytest-dev/pytest-cov>`__ and `coveralls <https://github.com/TheKevJames/coveralls-python>`__ to track lines covered by the tests

- `pytest-mpl <https://pytest-mpl.readthedocs.io/en/stable/>`__ to perform image comparisons

To run the primary test suite and generate coverage report, navigate to the parent `eomaps` directory and run:

.. code-block:: console
python -m pytest -v --cov eomaps
python -m pytest -v --cov eomaps --mpl
Some of the tests compare exported images with a set of baseline-images to ensure stable image exports and to catch
potential issues that are not detected by the code based tests.

If changes require an update of the baseline images, you have to invoke
`pytest-mpl <https://pytest-mpl.readthedocs.io/en/stable/>`__ with the `mpl-generate-path` option:

.. code-block:: console
python -m pytest -v --cov eomaps --mpl --mpl-generate-path=tests/baseline
This will update all images in the `tests/baseline` folder.

.. note::

During the tests, a lot of figures will be created and destroyed!

Before updating new baseline images, make sure to manually check that they look exactly as expected!


.. tip::

You can run only a subset of the tests by using the ``-k`` flag! (This will select only tests whose names contain the provided keyword)

.. code-block:: console
python -m pytest -k <KEYWORD>
python -m pytest -k <QUERY KEYWORD>
(see `pytest command-line-flags <https://docs.pytest.org/en/7.3.x/reference/reference.html#command-line-flags>`_ for more details)

Expand All @@ -129,7 +143,7 @@ To run the primary test suite and generate coverage report, navigate to the pare

.. code-block:: console
python -m pytest -n <NUMCORE> --cov eomaps
python -m pytest -n <NUMBER OF CORES>
Style Checking
~~~~~~~~~~~~~~
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/example_multiple_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

# initialize a grid of Maps objects
m = Maps(ax=131, crs=4326, figsize=(11, 5))
m2 = Maps(f=m.f, ax=132, crs=Maps.CRS.Stereographic())
m3 = Maps(f=m.f, ax=133, crs=3035)
m2 = m.new_map(ax=132, crs=Maps.CRS.Stereographic())
m3 = m.new_map(ax=133, crs=3035)

# --------- set specs for the first map
m.text(0.5, 1.1, "epsg=4326", transform=m.ax.transAxes)
Expand Down
7 changes: 4 additions & 3 deletions docs/examples/example_scalebars.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# EOmaps example: Adding scalebars - what about distances?

from eomaps import Maps

m = Maps(figsize=(9, 5))
Expand All @@ -14,7 +13,9 @@
n=10,
scale_props=dict(width=5, colors=("k", ".25", ".5", ".75", ".95")),
patch_props=dict(offsets=(1, 1.4, 1, 1), fc=(0.7, 0.8, 0.3, 1)),
label_props=dict(offset=0.5, scale=1.4, every=5, weight="bold", family="Calibri"),
label_props=dict(
offset=0.5, scale=1.4, every=5, weight="bold" # , family="Calibri"
),
)

s3 = m.add_scalebar(
Expand All @@ -25,7 +26,7 @@
scale_props=dict(width=3, colors=(*["w", "darkred"] * 2, *["w"] * 5, "darkred")),
patch_props=dict(fc=(0.25, 0.25, 0.25, 0.8), ec="k", lw=0.5, offsets=(1, 1, 1, 2)),
label_props=dict(
every=(1, 4, 10), color="w", rotation=45, weight="bold", family="Impact"
every=(1, 4, 10), color="w", rotation=45, weight="bold" # , family="Impact"
),
line_props=dict(color="w"),
)
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies:
- coveralls
- pytest
- pytest-cov
- pytest-mpl
# --------------for testing the docs
# (e.g. parsing .rst code-blocks and Jupyter Notebooks)
- docutils
Expand Down
24 changes: 15 additions & 9 deletions eomaps/_blit_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@ def _get_renderer(self):

def _get_all_map_axes(self):
maxes = {
m.ax for m in (self._m.parent, *self._m.parent._children) if m._new_axis_map
m.ax
for m in (self._m.parent, *self._m.parent._children)
if getattr(m, "_new_axis_map", False)
}
return maxes

Expand Down Expand Up @@ -331,9 +333,9 @@ def _do_on_layer_change(self, layer, new=False):
try:
f = self._on_layer_change[False].pop(0)
f(layer=layer)
except Exception:
except Exception as ex:
_log.error(
"EOmaps: Issue while executing a layer-change action",
f"EOmaps: Issue during layer-change action: {ex}",
exc_info=_log.getEffectiveLevel() <= logging.DEBUG,
)

Expand All @@ -352,9 +354,9 @@ def _do_on_layer_change(self, layer, new=False):
try:
f = single_shot_funcs.pop(0)
f(layer=l)
except Exception:
except Exception as ex:
_log.error(
"EOmaps: Issue while executing a layer-change action",
f"EOmaps: Issue during layer-change action: {ex}",
exc_info=_log.getEffectiveLevel() <= logging.DEBUG,
)

Expand Down Expand Up @@ -430,7 +432,7 @@ def bg_layer(self, val):
for m in [self._m.parent, *self._m.parent._children]:
layer_visible = self._layer_is_subset(val, m.layer)

for cb in m._colorbars:
for cb in getattr(m, "_colorbars", []):
cb._hide_singular_axes()

if layer_visible:
Expand Down Expand Up @@ -727,8 +729,9 @@ def _do_fetch_bg(self, layer, bbox=None):

# update axes spines and patches since they are used to clip artists!
for ax in self._get_all_map_axes():
ax.spines["geo"]._adjust_location()
ax.patch._adjust_location()
if "geo" in ax.spines:
ax.spines["geo"]._adjust_location()
ax.patch._adjust_location()

# use contextmanagers to make sure the background patches are not stored
# in the buffer regions!
Expand Down Expand Up @@ -978,8 +981,11 @@ def add_bg_artist(self, art, layer=None, draw=True):
and not layer.startswith("__inset_")
):
layer = "__inset_" + str(layer)

if layer in self._bg_artists and art in self._bg_artists[layer]:
_log.info(f"EOmaps: Background-artist '{art}' already added")
_log.info(
f"EOmaps: Background-artist '{art}' already added on layer '{layer}'"
)
return

art.set_animated(True)
Expand Down
149 changes: 148 additions & 1 deletion eomaps/_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,153 @@ def _get_current_datasize(self):
else:
return 99

def _sel_c_transp(self, c):
return self._select_vals(
c.T if self._z_transposed else c,
qs=self._last_qs,
slices=self._last_slices,
)

def _handle_explicit_colors(self, color):
if isinstance(color, (int, float, str, np.number)):
# if a scalar is provided, broadcast it
pass
elif isinstance(color, (list, tuple)) and len(color) in [3, 4]:
if all(map(lambda i: isinstance(i, (int, float, np.number)), color)):
# check if a tuple of numbers is provided, and if so broadcast
# it as a rgb or rgba tuple
pass
elif all(map(lambda i: isinstance(i, (list, np.ndarray)), color)):
# check if a tuple of lists or arrays is provided, and if so,
# broadcast them as RGB arrays
color = self._sel_c_transp(
np.rec.fromarrays(np.broadcast_arrays(*color))
)
elif isinstance(color, np.ndarray) and (color.shape[-1] in [3, 4]):
color = self._sel_c_transp(np.rec.fromarrays(color.T))
elif isinstance(color, np.ndarray) and (color.shape[-1] in [3, 4]):
color = self._sel_c_transp(np.rec.fromarrays(color.T))
else:
# still use np.asanyarray in here in case lists are provided
color = self._sel_c_transp(np.asanyarray(color).reshape(self.m._zshape))

return color

@staticmethod
def _convert_1d_to_2d(data, x, y, fill_value=np.nan):
"""A function to convert 1D vectors + data into 2D."""

if _log.getEffectiveLevel() <= logging.DEBUG:
_log.debug(
"EOmaps: Required conversion of 1D arrays to 2D for 'raster'"
"shape might be slow and consume a lot of memory!"
)

x, y, data = map(np.asanyarray, (x, y, data))
assert (
x.size == y.size == data.size
), "EOmaps: You cannot use 1D arrays with different sizes for x, y and data"

x_vals, x_idx = np.unique(x, return_inverse=True)
y_vals, y_idx = np.unique(y, return_inverse=True)
# Get output array shape
m, n = (x_vals.size, y_vals.size)

# Get linear indices to be used as IDs with bincount
lidx = np.ravel_multi_index(np.vstack((x_idx, y_idx)), (m, n))
idx2d = np.unravel_index(lidx, (m, n))

# Distribute data to 2D

if not np.issubdtype(data.dtype, np.integer):
# Integer-dtypes do not support None!
data2d = np.full((m, n), fill_value=fill_value, dtype=data.dtype)
data2d[idx2d] = data

else:
# use smallest possible value as fill-value
fill_value = np.iinfo(data.dtype).min
data2d = np.full((m, n), fill_value=fill_value, dtype=data.dtype)
data2d[idx2d] = data

mask2d = np.full((m, n), fill_value=True, dtype=bool)
mask2d[idx2d] = False

data2d = np.ma.masked_array(data2d, mask2d)

# Distribute coordinates to 2D
x_vals, y_vals = np.meshgrid(x_vals, y_vals, indexing="ij")

return data2d, x_vals, y_vals

def _get_coll(self, props, **kwargs):
# handle selection of explicitly provided facecolors
# (e.g. for rgb composites)

# allow only one of the synonyms "color", "fc" and "facecolor"
if (
np.count_nonzero(
[kwargs.get(i, None) is not None for i in ["color", "fc", "facecolor"]]
)
> 1
):
raise TypeError(
"EOmaps: only one of 'color', 'facecolor' or 'fc' " "can be specified!"
)

explicit_fc = False
for key in ("color", "facecolor", "fc"):
if kwargs.get(key, None) is not None:
explicit_fc = True
kwargs[key] = self._handle_explicit_colors(kwargs[key])

# don't pass the array if explicit facecolors are set
if explicit_fc and self.m.shape.name not in ["contour"]:
args = dict(array=None, cmap=None, norm=None, **kwargs)
else:
args = dict(
array=props["z_data"],
cmap=getattr(self.m, "_cbcmap", "Reds"),
norm=getattr(self.m, "_norm", None),
**kwargs,
)

if (
self.m.shape.name in ["contour"]
and len(self.m._xshape) == 2
and len(self.m._yshape) == 2
):
# if 2D data is provided for a contour plot, keep the data 2d!
coll = self.m.shape.get_coll(props["xorig"], props["yorig"], "in", **args)
elif self.m.shape.name in ["raster"]:
# if input-data is 1D, try to convert data to 2D (required for raster)
# TODO make an explicit data-conversion function for 2D-only shapes
if len(self.m._xshape) == 2 and len(self.m._yshape) == 2:
coll = self.m.shape.get_coll(
props["xorig"], props["yorig"], "in", **args
)
else:
data2d, x2d, y2d = self._convert_1d_to_2d(
data=props["z_data"].ravel(),
x=props["x0"].ravel(),
y=props["y0"].ravel(),
)

if args["array"] is not None:
args["array"] = data2d

coll = self.m.shape.get_coll(x2d, y2d, "out", **args)

else:
# convert to 1D for further processing
if args["array"] is not None:
args["array"] = args["array"].ravel()

coll = self.m.shape.get_coll(
props["x0"].ravel(), props["y0"].ravel(), "out", **args
)
return coll

def on_fetch_bg(self, layer=None, bbox=None, check_redraw=True):
# TODO support providing a bbox as extent?
if layer is None:
Expand Down Expand Up @@ -757,7 +904,7 @@ def on_fetch_bg(self, layer=None, bbox=None, check_redraw=True):
# remove previous collection from the map
self._remove_existing_coll()
# draw the new collection
coll = self.m._get_coll(props, **self.m._coll_kwargs)
coll = self._get_coll(props, **self.m._coll_kwargs)
coll.set_clim(self.m._vmin, self.m._vmax)

coll.set_label("Dataset " f"({self.m.shape.name} | {self.z_data.shape})")
Expand Down
Loading

0 comments on commit b40a54b

Please sign in to comment.