Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get color mapping for variables with flag_values attribute #1012

Merged
merged 2 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,21 @@
To enforce the old behaviour, provide the `var_configs` keyword-argument
and set `recover_nan` to `True` for desired variables.

* The class `MaskSet()` of module `xcube.core.maskset` now correctly recognises
the variable attributes `flag_values`, `flag_masks`, `flag_meanings` when
their values are lists (ESA CCI LC data encodes them as JSON arrays). (#1002)

* The class `MaskSet()` now provides a method `get_cmap()` which creates
a suitable matplotlib color map for variables that define the
`flag_values` CF-attribute and optionally a `flag_colors` attribute. (#1011)


### Fixes

* When using the `xcube.webapi.viewer.Viewer` class in Jupyter notebooks
multi-level datasets opened from S3 or from deeper subdirectories into
the local filesystem are now fully supported. (#1007)

* The class `MaskSet()` of module `xcube.core.maskset` now correctly recognises
the variable attributes `flag_values`, `flag_masks`, `flag_meanings` when
their values are lists (ESA CCI LC data encodes them as JSON arrays). (#1002)

* Fixed an issue with xcube server `/timeseries` endpoint that returned
status 500 if a given dataset used a CRS other geographic and the
geometry was not a point. (#995)
Expand Down
24 changes: 24 additions & 0 deletions test/core/test_maskset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import unittest
from typing import Tuple

import matplotlib
import numpy as np
import xarray as xr
from numpy.testing import assert_array_almost_equal
Expand Down Expand Up @@ -237,6 +238,29 @@ def test_mask_set_with_flag_values_as_list(self):
mask_set = MaskSet(flag_var)
self.assertEqual(38, len(mask_set))

def test_mask_set_cmap_with_flag_values_and_flag_colors(self):
flag_var = create_cci_lccs_class_var(flag_values_as_list=True)
mask_set = MaskSet(flag_var)
self.assertEqual(38, len(mask_set))
cmap: matplotlib.colors.Colormap = mask_set.get_cmap()
self.assertIsInstance(cmap, matplotlib.colors.LinearSegmentedColormap)

def test_mask_set_cmap_with_flag_values_and_no_flag_colors(self):
flag_var = create_cci_lccs_class_var(flag_values_as_list=True)
del flag_var.attrs["flag_colors"]
mask_set = MaskSet(flag_var)
self.assertEqual(38, len(mask_set))
cmap: matplotlib.colors.Colormap = mask_set.get_cmap()
self.assertIsInstance(cmap, matplotlib.colors.LinearSegmentedColormap)

def test_mask_set_cmap_with_no_flag_values_and_no_flag_colors(self):
flag_var = create_c2rcc_flag_var()
mask_set = MaskSet(flag_var)
self.assertEqual(4, len(mask_set))
cmap: matplotlib.colors.Colormap = mask_set.get_cmap()
# Uses default "viridis"
self.assertIsInstance(cmap, matplotlib.colors.ListedColormap)

def test_mask_set_with_missing_values_and_masks_attrs(self):
flag_var = create_c2rcc_flag_var().chunk(dict(x=2, y=2))
flag_var.attrs.pop("flag_masks", None)
Expand Down
51 changes: 47 additions & 4 deletions xcube/core/maskset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
# Permissions are hereby granted under the terms of the MIT License:
# https://opensource.org/licenses/MIT.

from typing import Dict, Any
import random
from collections.abc import Iterable
from typing import Any

import dask.array as da
import matplotlib.colors
import numpy as np
import xarray as xr

Expand Down Expand Up @@ -52,6 +54,7 @@ def __init__(self, flag_var: xr.DataArray):
flag_meanings = flag_var.attrs.get("flag_meanings")
if not isinstance(flag_meanings, str):
raise TypeError("attribute 'flag_meanings' of flag_var " "must be a string")

flag_names = flag_meanings.split(" ")
if flag_masks is not None and len(flag_names) != len(flag_masks):
raise ValueError(
Expand All @@ -61,14 +64,25 @@ def __init__(self, flag_var: xr.DataArray):
raise ValueError(
"attributes 'flag_meanings' and 'flag_values' " "are not corresponding"
)

flag_colors = flag_var.attrs.get("flag_colors")
if isinstance(flag_colors, str):
flag_colors = flag_colors.split(" ")
elif not isinstance(flag_colors, (list, tuple)):
flag_colors = None

self._masks = {}
self._flag_var = flag_var
self._flag_names = flag_names
self._flag_masks = flag_masks
self._flag_values = flag_values
self._flag_colors = flag_colors

if flag_masks is None:
flag_masks = [None] * len(flag_names)
if flag_values is None:
flag_values = [None] * len(flag_names)
self._flag_var = flag_var
self._flag_names = flag_names
self._flags = dict(zip(flag_names, list(zip(flag_masks, flag_values))))
self._masks = {}

@classmethod
def is_flag_var(cls, var: xr.DataArray) -> bool:
Expand Down Expand Up @@ -177,6 +191,35 @@ def get_mask(self, flag_name: str):
self._masks[flag_name] = mask_var
return mask_var

def get_cmap(self, default: str = "viridis") -> matplotlib.colors.Colormap:
"""Get a suitable color mapping for use with matplotlib.

Args:
default: Default color map name in case a color mapping
cannot be created, e.g., ``flag_values`` are not defined.

Returns:
An suitable instance of ```matplotlib.colors.Colormap```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An suitable instance of ```matplotlib.colors.Colormap```
A suitable instance of ```matplotlib.colors.Colormap```

"""
if self._flag_values is not None:
flag_values = self._flag_values
num_values = len(flag_values)
# Note, here is room for improvement if we insert transparent
# (alpha=0) colors for gaps between the integer values.
# Currently, gap color is taken from the first value before the gap.
if self._flag_colors is not None and len(self._flag_colors) == num_values:
colors = [(v, c) for v, c in zip(flag_values, self._flag_colors)]
else:
# Use random colors so they are all different.
colors = [
(v, (random.random(), random.random(), random.random()))
for v in flag_values
]
return matplotlib.colors.LinearSegmentedColormap.from_list(
str(self._flag_var.name), colors
)
return matplotlib.colormaps.get_cmap(default)


_MASK_DTYPES = (
(2**8, np.uint8),
Expand Down
Loading