Skip to content

Commit

Permalink
Merge pull request #1057 from xcube-dev/konstntokas-1046-custom_color…
Browse files Browse the repository at this point in the history
…_maps

labels added, custom cmaps added also if they are not in styles
  • Loading branch information
forman authored Aug 8, 2024
2 parents 08845b4 + a3cfac6 commit 055b234
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 65 deletions.
24 changes: 10 additions & 14 deletions docs/source/cli/xcube_serve.rst
Original file line number Diff line number Diff line change
Expand Up @@ -650,20 +650,12 @@ Both, reversed and alpha blending is possible as well and can be configured by n
ColorBar: plasma_r_alpha
ValueRange: [0., 24.]
Colormaps may be user-defined within the configuration file. One example is shown below.


.. code-block:: yaml
Styles:
- Identifier: default
ColorMappings:
conc_chl:
CustomColorBar: my_cmap
The colormap `my_cmap` can then be configured in section
`customcolormaps`_.

Colormaps may be user-defined within the configuration file, which can be configured
in section `customcolormaps`_. The colormap can be selected by setting
`ColorBar: my_cmap`, where `my_cmap` is the identifier of the custom defined color map.
If the `ValueRange` is given, it overwrites the value range defined in
`CustomColorMaps` if the color map type is continuous or stepwise, and it is ignored
if the color map type is categorical, where a warning is raised.

.. _customcolormaps:

Expand Down Expand Up @@ -739,6 +731,10 @@ For example *CustomColorMaps* can look like this:
- [ 1, orange, medium_risk]
- [ 2, [1, 0, 0], high_risk]
All colormaps defined in the `CustomColorMaps` section will be available in the xcube
Viewer, even if they haven't been selected in the Styles section for a specific data
variable.

.. _example:

Example
Expand Down
26 changes: 21 additions & 5 deletions examples/serve/demo/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,10 @@ Styles:
- Identifier: default
ColorMappings:
conc_chl:
CustomColorBar: my_cmap
ColorBar: my_cmap
ValueRange: [0., 20.] # this value range overwrites the range defined in CustomColorMaps
chl_category:
CustomColorBar: cmap_bloom_risk
ColorBar: cmap_bloom_risk
conc_tsm:
ColorFile: cc_tsm.cpd
kd489:
Expand Down Expand Up @@ -220,9 +221,24 @@ CustomColorMaps:
- Identifier: cmap_bloom_risk
Type: categorical
Colors:
- [ 0, [0, 1, 0., 0.5], low_risk]
- [ 1, orange, medium_risk]
- [ 2, [1, 0, 0], high_risk]
- [ 0, [0, 1, 0., 0.5]]
- [ 1, orange]
- [ 2, [1, 0, 0]]
- Identifier: s2_l2_scl
Type: categorical
Colors:
- [ 0, red, no data]
- [ 1, yellow, defective]
- [ 2, black, dark area pixels]
- [ 3, gray, cloud shadows]
- [ 4, green, vegetation]
- [ 5, tan, bare soils]
- [ 6, blue, water]
- [ 7, "#aaaabb", clouds low prob ]
- [ 8, "#bbbbcc", clouds medium prob]
- [ 9, "#ccccdd", clouds high prob]
- [10, "#ddddee", cirrus]
- [11, "#ffffff", snow or ice]

ServiceProvider:
ProviderName: Brockmann Consult GmbH
Expand Down
14 changes: 11 additions & 3 deletions test/util/test_cmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,11 @@ def test_create_colormap_from_config_color_entry_object(self):
{
"name": "my_cmap",
"type": "continuous",
"colors": [[0.0, "red"], [12.0, "#0000FF"], [24.0, [0, 1, 0, 0.3]]],
"colors": [
[0.0, "red", "low"],
[12.0, "#0000FF", "medium"],
[24.0, [0, 1, 0, 0.3], "high"],
],
},
config_parse,
)
Expand All @@ -413,7 +417,7 @@ def test_create_colormap_from_config_color_entry_tuple(self):
Type="categorical",
Colors=[
[0, "red", "low"],
[1, "#0000FF", "medium"],
[1, "#0000FF"],
[2, [0, 1, 0], "high"],
],
)
Expand All @@ -430,7 +434,11 @@ def test_create_colormap_from_config_color_entry_tuple(self):
{
"name": "my_cmap",
"type": "categorical",
"colors": [[0.0, "red"], [1.0, "#0000FF"], [2.0, [0, 1, 0]]],
"colors": [
[0.0, "red", "low"],
[1.0, "#0000FF"],
[2.0, [0, 1, 0], "high"],
],
},
config_parse,
)
19 changes: 14 additions & 5 deletions test/webapi/datasets/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,32 @@ def test_config_and_dataset_cache(self):
self.assertNotIn("demo2", ctx.dataset_cache)

def test_get_color_mappings(self):
ctx = get_datasets_ctx()
with self.assertLogs("xcube", level="WARNING") as cm:
ctx = get_datasets_ctx()
self.assertEqual(
cm.output,
[
"WARNING:xcube:Custom color map 'cmap_bloom_risk' is categorical. "
"ValueRange is ignored."
],
)
color_mapping = ctx.get_color_mappings("demo-1w")
self.assertEqual(
{
"conc_chl": {"ColorBar": "my_cmap", "ValueRange": (0.0, 24.0)},
"conc_tsm": {"ColorBar": "cmap_cat", "ValueRange": (0.0, 3.0)},
"conc_chl": {"ColorBar": "my_cmap", "ValueRange": [0.0, 20.0]},
"conc_tsm": {"ColorBar": "cmap_bloom_risk", "ValueRange": [0.0, 3.0]},
"kd489": {"ColorBar": "jet", "ValueRange": [0.0, 6.0]},
},
color_mapping,
)
self.assertIn("s2_l2_scl", ctx.colormap_registry.colormaps)

def test_get_color_mapping(self):
ctx = get_datasets_ctx()
cm = ctx.get_color_mapping("demo", "conc_chl")
self.assertEqual(("my_cmap", "lin", (0.0, 24.0)), cm)
self.assertEqual(("my_cmap", "lin", (0.0, 20.0)), cm)
cm = ctx.get_color_mapping("demo", "conc_tsm")
self.assertEqual(("cmap_cat", "lin", (0.0, 3.0)), cm)
self.assertEqual(("cmap_bloom_risk", "lin", (0.0, 3.0)), cm)
cm = ctx.get_color_mapping("demo", "kd489")
self.assertEqual(("jet", "lin", (0.0, 6.0)), cm)
with self.assertRaises(ApiError.NotFound):
Expand Down
24 changes: 21 additions & 3 deletions test/webapi/res/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ Styles:
- Identifier: default
ColorMappings:
conc_chl:
CustomColorBar: my_cmap
ColorBar: my_cmap
ValueRange: [0., 20.]
conc_tsm:
CustomColorBar: cmap_cat
ColorBar: cmap_bloom_risk
ValueRange: [0., 1.]
kd489:
ColorBar: jet
ValueRange: [0., 6.]
Expand All @@ -72,12 +74,28 @@ CustomColorMaps:
- Value: 24
Color: [0, 1, 0, 0.3]
Label: high
- Identifier: cmap_cat
- Identifier: cmap_bloom_risk
Type: categorical
Colors:
- [ 0, [0, 1, 0., 0.5]]
- [ 1, orange]
- [ 2, [1, 0, 0]]
- Identifier: s2_l2_scl
Type: categorical
Colors:
- [ 0, red, no data]
- [ 1, yellow, defective]
- [ 2, black, dark area pixels]
- [ 3, gray, cloud shadows]
- [ 4, green, vegetation]
- [ 5, tan, bare soils]
- [ 6, blue, water]
- [ 7, "#aaaabb", clouds low prob ]
- [ 8, "#bbbbcc", clouds medium prob]
- [ 9, "#ccccdd", clouds high prob]
- [10, "#ddddee", cirrus]
- [11, "#ffffff", snow or ice]
- [11, "#ffffff", snow or ice]

Viewer:
Configuration:
Expand Down
18 changes: 10 additions & 8 deletions xcube/util/cmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ def parse_cm_code(cm_code: str) -> tuple[str, Optional[Colormap]]:
user_color_map: dict[str, Any] = json.loads(cm_code)
cm_name = user_color_map["name"]
cm_items = user_color_map["colors"]
cm_items = [item[:2] for item in cm_items]
cm_type = user_color_map.get("type", "continuous")
cm_base_name, _, _ = parse_cm_name(cm_name)
n = len(cm_items)
Expand Down Expand Up @@ -635,23 +636,24 @@ def get_cmap_png_base64(cmap: matplotlib.colors.Colormap) -> str:
def create_colormap_from_config(cmap_config: dict) -> Colormap:
registry = ColormapRegistry()
colors = []
labels = []
for color in cmap_config["Colors"]:
if isinstance(color, list):
colors.append(color[:2])
if len(color) == 3:
labels.append(labels)
colors.append(color)
else:
colors.append([color["Value"], color["Color"]])
if "Label" in color:
labels.append(color["Label"])
colors.append([color["Value"], color["Color"], color["Label"]])
else:
colors.append([color["Value"], color["Color"]])

for i, [_, color] in enumerate(colors):
for i, item in enumerate(colors):
color = item[1]
if isinstance(color, list):
if any([val > 1 for val in color]):
colors[i][1] = list(np.array(color) / 255)
config_parse = dict(
name=cmap_config["Identifier"], type=cmap_config["Type"], colors=colors
name=cmap_config["Identifier"],
type=cmap_config["Type"],
colors=colors,
)
_, cmap = registry.get_cmap(json.dumps(config_parse))
return cmap, config_parse
Expand Down
12 changes: 1 addition & 11 deletions xcube/webapi/datasets/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@

COLOR_MAPPING_EXPLICIT_SCHEMA = JsonObjectSchema(
properties=dict(ColorBar=STRING_SCHEMA, ValueRange=VALUE_RANGE_SCHEMA),
required=[],
required=["ColorBar"],
additional_properties=False,
)

Expand All @@ -134,21 +134,11 @@
additional_properties=False,
)

COLOR_MAPPING_BY_CUSTOM_CONFIG = JsonObjectSchema(
properties=dict(
CustomColorBar=STRING_SCHEMA,
),
required=[
"CustomColorBar",
],
additional_properties=False,
)

COLOR_MAPPING_SCHEMA = JsonComplexSchema(
one_of=[
COLOR_MAPPING_EXPLICIT_SCHEMA,
COLOR_MAPPING_BY_PATH_SCHEMA,
COLOR_MAPPING_BY_CUSTOM_CONFIG,
]
)

Expand Down
47 changes: 31 additions & 16 deletions xcube/webapi/datasets/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,12 @@ def get_color_mapping(
def _get_cm_styles(self) -> tuple[dict[str, Any], ColormapRegistry]:
custom_colormaps = {}
cm_styles = {}

# go through CustomColorMaps
for ctx_cmap in self.config.get("CustomColorMaps", []):
custom_colormap, config_parse = create_colormap_from_config(ctx_cmap)
custom_colormaps[custom_colormap.cm_name] = custom_colormap
# go through Styles
for style in self.config.get("Styles", []):
style_id = style["Identifier"]
color_mappings = dict()
Expand All @@ -556,23 +562,32 @@ def _get_cm_styles(self) -> tuple[dict[str, Any], ColormapRegistry]:
custom_colormap.norm.vmax,
),
}
elif "CustomColorBar" in color_mapping:
ctx_cmaps = self.config["CustomColorMaps"]
for ctx_cmap in ctx_cmaps:
if ctx_cmap["Identifier"] == color_mapping["CustomColorBar"]:
break
custom_colormap, config_parse = create_colormap_from_config(
ctx_cmap
)
custom_colormaps[custom_colormap.cm_name] = custom_colormap
color_mappings[var_name] = {
"ColorBar": custom_colormap.cm_name,
"ValueRange": (
min(custom_colormap.values),
max(custom_colormap.values),
),
}
else:
if color_mapping.get("ColorBar") in custom_colormaps:
cmap = custom_colormaps[color_mapping["ColorBar"]]
if (
cmap.cm_type == "categorical"
and "ValueRange" in color_mapping
):
LOG.warning(
f"Custom color map {color_mapping['ColorBar']!r} is "
"categorical. ValueRange is ignored."
)
color_mapping = dict(
ColorBar=color_mapping["ColorBar"],
ValueRange=[
min(cmap.values),
max(cmap.values),
],
)
if "ValueRange" not in color_mapping:
color_mapping = dict(
ColorBar=color_mapping["ColorBar"],
ValueRange=[
min(cmap.values),
max(cmap.values),
],
)
color_mappings[var_name] = dict(color_mapping)

cm_styles[style_id] = color_mappings
Expand Down

0 comments on commit 055b234

Please sign in to comment.