Skip to content

Commit

Permalink
Merge pull request #1085 from xcube-dev/forman-x-dashi
Browse files Browse the repository at this point in the history
Server-side panels
  • Loading branch information
forman authored Nov 13, 2024
2 parents 4ed9c72 + ed59876 commit 765d0aa
Show file tree
Hide file tree
Showing 121 changed files with 4,998 additions and 3,782 deletions.
13 changes: 13 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@
* The explaination of the parameter `xy_scale` in the method
`xcube.core.gridmapping.GridMapping.scale` has been corrected. (#1086)

### Other changes

* Added experimental feature that allows for extending the xcube Viewer
user interface with _server-side panels_. For this to work, users can now
configure xcube Server to load one or more Python modules that provide
`xcube.webapi.viewer.contrib.Panel` UI-contributions.
Panel instances provide two decorators `layout()` and `callback()`
which are used to implement the UI and the interaction behaviour,
respectively. The functionality is provided by the
`https://github.com/bcdev/chartlets` Python library.
A working example can be found in `examples/serve/panels-demo`.


## Changes in 1.7.1

### Enhancements
Expand Down
5 changes: 5 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ dependencies:
- urllib3 >=1.26
- xarray >=2022.6
- zarr >=2.11
# Chartlets
- altair
- pip
- pip:
- git+https://github.com/bcdev/chartlets.git#subdirectory=chartlets.py
# Testing
- flake8 >=3.7
- kerchunk
Expand Down
64 changes: 64 additions & 0 deletions examples/serve/panels-demo/compute_indexes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import xarray as xr


def compute_indexes(dataset: xr.Dataset) -> xr.Dataset:
b04 = dataset.B04
b05 = dataset.B05
b06 = dataset.B06
b08 = dataset.B08
b11 = dataset.B11
scl = dataset.SCL

# See https://gisgeography.com/sentinel-2-bands-combinations/

# moisture_index
#
moisture_index = (b08 - b11) / (b08 + b11)
moisture_index.attrs.update(
color_value_min=0.0,
color_value_max=0.5,
color_bar_name="Blues",
units="-",
description="Simple moisture index: (B08-B11) / (B08+B11)",
)
# where valid and not cloud
moisture_index = moisture_index.where((scl >= 2) & (scl <= 7))

# vegetation_index
#
vegetation_index = (b08 - b04) / (b08 + b04)
vegetation_index.attrs.update(
color_value_min=0.0,
color_value_max=0.25,
color_bar_name="Greens",
units="-",
description="Simple vegetation index or NDVI: (B08-B04) / (B08+B04)",
)
# where water
vegetation_index = vegetation_index.where(scl == 6)

# chlorophyll_index
#
b_from, b_peek, b_to = b04, b05, b06
wlen_from = b04.attrs["wavelength"]
wlen_peek = b05.attrs["wavelength"]
wlen_to = b06.attrs["wavelength"]
f = (wlen_peek - wlen_from) / (wlen_to - wlen_from)
chlorophyll_index = (b_peek - b_from) - f * (b_to - b_from)
chlorophyll_index.attrs.update(
color_value_min=0.0,
color_value_max=0.025,
color_bar_name="viridis",
units="-",
description="Maximum chlorophyll index: (B05-B04) - f * (B06-B04)",
)
# where water
chlorophyll_index = chlorophyll_index.where(scl == 6)

return xr.Dataset(
dict(
moisture_index=moisture_index,
vegetation_index=vegetation_index,
chlorophyll_index=chlorophyll_index,
)
)
110 changes: 110 additions & 0 deletions examples/serve/panels-demo/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
Viewer:
Augmentation:
Path: ""
Extensions:
- my_viewer_ext.ext

DatasetChunkCacheSize: 100M

DataStores:
- Identifier: opensr-test
StoreId: s3
StoreParams:
root: opensr-test
Datasets:
- Path: openSR_musselbeds_use_case_waddensea_S2L2A_2018-2022.levels
Identifier: waddensea
Title: Wadden Sea
Style: waddensea
Augmentation:
Path: compute_indexes.py
Function: compute_indexes
- Path: S2_louisville_time.levels
Identifier: louisville
Title: Lousiville S2
Style: louisville

Styles:
- Identifier: waddensea
ColorMappings:
B01:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B02:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B03:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B04:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B05:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B06:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B07:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B08:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B8A:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B09:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B10:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B11:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
B12:
ColorBar: "Greys_r"
ValueRange: [0., 0.25]
SCL:
ColorBar: "Accent"
ValueRange: [ 0, 255 ]
rgb:
Red:
Variable: B04
ValueRange: [0., 0.25]
Green:
Variable: B03
ValueRange: [0., 0.25]
Blue:
Variable: B02
ValueRange: [0., 0.25]

- Identifier: louisville
ColorMappings:
band_1:
ColorBar: "gray"
ValueRange: [0, 5000]
band_2:
ColorBar: "gray"
ValueRange: [0, 5000]
band_3:
ColorBar: "gray"
ValueRange: [0, 5000]
band_4:
ColorBar: "bone"
ValueRange: [0, 10000]
rgb:
Red:
Variable: band_1
ValueRange: [0., 5000]
Green:
Variable: band_2
ValueRange: [0., 5000]
Blue:
Variable: band_3
ValueRange: [0., 5000]

ServiceProvider:
ProviderName: "Brockmann Consult GmbH"
ProviderSite: "https://www.brockmann-consult.de"
5 changes: 5 additions & 0 deletions examples/serve/panels-demo/my_viewer_ext/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from chartlets import Extension
from .my_panel_1 import panel as my_panel_1

ext = Extension(__name__)
ext.add(my_panel_1)
83 changes: 83 additions & 0 deletions examples/serve/panels-demo/my_viewer_ext/my_panel_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from chartlets import Component, Input, State, Output
from chartlets.components import Box, Dropdown, Checkbox, Typography

from xcube.webapi.viewer.contrib import Panel
from xcube.webapi.viewer.contrib import get_datasets_ctx
from xcube.server.api import Context


panel = Panel(__name__, title="Panel B")


COLORS = [("red", 0), ("green", 1), ("blue", 2), ("yellow", 3)]


@panel.layout(
Input(source="app", property="controlState.selectedDatasetId"),
)
def render_panel(
ctx: Context,
dataset_id: str = "",
) -> Component:

opaque = False
color = 0

opaque_checkbox = Checkbox(
id="opaque",
value=opaque,
label="Opaque",
)

color_dropdown = Dropdown(
id="color",
value=color,
label="Color",
options=COLORS,
style={"flexGrow": 0, "minWidth": 80},
)

info_text = Typography(
id="info_text", text=update_info_text(ctx, dataset_id, opaque, color)
)

return Box(
style={
"display": "flex",
"flexDirection": "column",
"width": "100%",
"height": "100%",
"gap": "6px",
},
children=[opaque_checkbox, color_dropdown, info_text],
)


# noinspection PyUnusedLocal
@panel.callback(
Input(source="app", property="controlState.selectedDatasetId"),
Input("opaque"),
Input("color"),
State("info_text", "text"),
Output("info_text", "text"),
)
def update_info_text(
ctx: Context,
dataset_id: str = "",
opaque: bool = False,
color: int = 0,
info_text: str = "",
) -> str:
ds_ctx = get_datasets_ctx(ctx)
ds_configs = ds_ctx.get_dataset_configs()

opaque = opaque or False
color = color if color is not None else 0
return (
f"The dataset is {dataset_id},"
f" the color is {COLORS[color][0]} and"
f" it {'is' if opaque else 'is not'} opaque."
f" The length of the last info text"
f" was {len(info_text or "")}."
f" The number of datasets is {len(ds_configs)}."
)
33 changes: 33 additions & 0 deletions test/webapi/res/config-panels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Viewer:
Augmentation:
Path: ""
Extensions:
- viewer_panels.ext

DataStores:
- Identifier: test
StoreId: file
StoreParams:
root: examples/serve/demo
Datasets:
- Path: "cube-1-250-250.zarr"
TimeSeriesDataset: "cube-5-100-200.zarr"

ServiceProvider:
ProviderName: "Brockmann Consult GmbH"
ProviderSite: "https://www.brockmann-consult.de"
ServiceContact:
IndividualName: "Norman Fomferra"
PositionName: "Senior Software Engineer"
ContactInfo:
Phone:
Voice: "+49 4152 889 303"
Facsimile: "+49 4152 889 330"
Address:
DeliveryPoint: "HZG / GITZ"
City: "Geesthacht"
AdministrativeArea: "Herzogtum Lauenburg"
PostalCode: "21502"
Country: "Germany"
ElectronicMailAddress: "[email protected]"

7 changes: 7 additions & 0 deletions test/webapi/res/viewer_panels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from chartlets import Extension
from .my_panel_a import panel as my_panel_a
from .my_panel_b import panel as my_panel_b

ext = Extension(__name__)
ext.add(my_panel_a)
ext.add(my_panel_b)
Loading

0 comments on commit 765d0aa

Please sign in to comment.