Interactive application

Note

In this notebook, we explore some interactive plotting of gospl outputs that might be useful for sharing the results within a jupyter environment.

We will create a Dashboard with two HoloViews objects:

  1. a panel pn.widgets.Select object that contains a list of Xarray variables, and

  2. a hvPlot object that takes the selected variable on input.

hvplot

Fig. 2 Python tools for data visualization PyViz.

See also

An in-depth description of the approach quickly presented here is well discussed in a recent paper by Signell & Pothina (2019)1.

Load the required Python libraries

First of all, load the necessary libraries. These are the ones we discussed previously:

  • numpy

  • matplotlib

  • cartopy

  • panel

  • xarray

  • holoviews

  • geoviews

import os
import pooch
import numpy as np
import xarray as xr

import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', cartopy.config.get('data_dir'))

import cmocean

import hvplot.xarray
import panel as pn

import holoviews as hv
from holoviews import opts, dim

import geoviews as gv
import geoviews.feature as gf
from cartopy import crs

import geoviews as gv
from geoviews import tile_sources as gvts
import geoviews.feature as gf
from cartopy import crs as ccrs
import pandas as pd

from script import readOutput as rout

gv.extension('bokeh')

import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning) 

We first define a folder where exported files will be stored:

out_path = 'export'
if not os.path.exists(out_path):
    os.makedirs(out_path)

Netcdf outputs

We start with the netcdf outputs as it is the most common one used in our field.

Building netcdf file

Netcdf exports are done by using the readOutput.py script presented in the previous notebook. Here we export all the time steps at once by looping through the number of outputs (5 in this case).

Then the buildLonLatMesh function is used to interpolate (using a kd-tree approach) the gospl variables on a regular mesh. It also provides a way to limit the created netcdf file by defining a bounding box:

uplift_forcing = False

# Specifying the grid resolution in degrees
reso = 0.1

# Total number of outputs
nbstep = 5

# Bounding box
bb = [-134,20,-46,80]

def build_ncgrid(ncout, nbstep, bbox, reso, fname, uplift_forcing):

    # Looping through the output time steps
    for k in range(nbstep+1):
        if k == 0:
            # Calling the initialisation function for our class 
            ncgrid = rout.readOutput(path='./', filename=fname, 
                                     step=k, nbstep=nbstep+1, 
                                     uplift=uplift_forcing)
        else:
            # Update the variables after the first time steps
            ncgrid.update(step=k, uplift=uplift_forcing)

        # Build the regular grid defining the bounding box
        ncgrid.buildLonLatMesh(res=reso, nghb=3, box=bbox)
    
    # Export as netcdf
    ncgrid.exportNetCDF(ncfile = ncout)
    
    return
    
ncout = os.path.join(out_path, "GoMresult.nc")
# We commented the next line and loaded the dataset from figshare (see below)
#build_ncgrid(ncout, nbstep, bb, reso, fname='input.yml', uplift_forcing=False)

Here we will download the file directly from figshare:

ncout = pooch.retrieve(
    url="https://ndownloader.figshare.com/files/27579284",
    known_hash="sha256:fdc4a4ac70bca66ed20c3a126fdbf8fd2b33a2b47170e45c53eaf96f20f42a32",
    downloader=pooch.HTTPDownloader(progressbar=True),
    fname="GoMresult.nc",
)

Open the netcdf file with xarray file:

ds = xr.open_dataset(ncout, decode_times=False)
ds
<xarray.Dataset>
Dimensions:        (latitude: 681, longitude: 881, time: 6)
Coordinates:
  * time           (time) float64 0.0 1e+04 2e+04 3e+04 4e+04 5e+04
  * latitude       (latitude) float64 12.0 12.1 12.2 12.3 ... 79.8 79.9 80.0
  * longitude      (longitude) float64 -134.0 -133.9 -133.8 ... -46.1 -46.0
Data variables:
    elevation      (time, latitude, longitude) float64 ...
    erodep         (time, latitude, longitude) float64 ...
    precipitation  (time, latitude, longitude) float64 ...
    drainageArea   (time, latitude, longitude) float64 ...
    basinID        (time, latitude, longitude) int32 ...
Attributes:
    description:  gospl outputs
    history:      Created Thu Apr  8 17:39:07 2021

Plotting a specific variable

We will plot the basin index for a specific region.

Let’s start by clipping the area to reduce the Dataset size. We will clip the spatial extent based on longitudinal and latitudinal values.

This is done using the sel function with the slice method.

min_lon = -130     # lower left longitude
min_lat = 21     # lower left latitude
max_lon = -55     # upper right longitude
max_lat = 70     # upper right latitude

# Defining the boundaries
lon_bnds = [min_lon, max_lon]
lat_bnds = [min_lat, max_lat]

# Time interval
tsteps = [1.e04, 5.e04]

# Performing the reduction 
ds_clip = ds.sel(latitude=slice(*lat_bnds), longitude=slice(*lon_bnds), time=slice(*tsteps))
ds_clip
<xarray.Dataset>
Dimensions:        (latitude: 491, longitude: 751, time: 5)
Coordinates:
  * time           (time) float64 1e+04 2e+04 3e+04 4e+04 5e+04
  * latitude       (latitude) float64 21.0 21.1 21.2 21.3 ... 69.8 69.9 70.0
  * longitude      (longitude) float64 -130.0 -129.9 -129.8 ... -55.1 -55.0
Data variables:
    elevation      (time, latitude, longitude) float64 ...
    erodep         (time, latitude, longitude) float64 ...
    precipitation  (time, latitude, longitude) float64 ...
    drainageArea   (time, latitude, longitude) float64 ...
    basinID        (time, latitude, longitude) int32 ...
Attributes:
    description:  gospl outputs
    history:      Created Thu Apr  8 17:39:07 2021

Using GeoViews we visualise the basin index over time:

# Specify the dataset, its coordinates and requested variable 
dataset = gv.Dataset(ds_clip, ['longitude', 'latitude', 'time'], 
                     'basinID', crs=crs.PlateCarree())

images = dataset.to(gv.Image,dynamic=True)

# Loading coastlines from Cartopy
coastline = gf.coastline(line_width=1,line_color='k').opts(projection=ccrs.PlateCarree(),
                                                           scale='10m')
# Slider location
hv.output(widget_location='bottom')

# Create stack of images grouped by time
images.opts(active_tools=['wheel_zoom', 'pan'], cmap='jet',
            colorbar=True, width=800, height=500, 
            ) * coastline
/usr/share/miniconda/envs/stellar/lib/python3.8/site-packages/cartopy/io/__init__.py:260: DownloadWarning: Downloading: https://naciscdn.org/naturalearth/110m/physical/ne_110m_coastline.zip
  warnings.warn('Downloading: {}'.format(url), DownloadWarning)
/usr/share/miniconda/envs/stellar/lib/python3.8/site-packages/cartopy/io/__init__.py:260: DownloadWarning: Downloading: https://naciscdn.org/naturalearth/10m/physical/ne_10m_coastline.zip
  warnings.warn('Downloading: {}'.format(url), DownloadWarning)

A better dashboard

We will use the quadmesh function to quickly rasterize the output to the requested width and height and to create a simple dashboard for interactive, dynamic visualization of gospl data.

Note

Using the controls on the right, the user can select the pan and wheel_zoom, which enables dynamic exploration of the desired variable.

Tip

By selecting the hover control it allows data values to be displayed along with their coordinates.

var = 'erodep'
base_map = gvts.EsriImagery  # ESRI satellite image as background

# Get title from dataset variables attributes
label = f'{var}: {ds_clip[var].units}'

# Build the quadmesh
mesh = ds_clip[var][:,:].hvplot.quadmesh(x='longitude',y='latitude',
                                        crs=ccrs.PlateCarree(), cmap='bwr',
                                        rasterize=True, groupby=list(ds_clip[var].dims[:2:3]),
                                        title=label, 
                                        width=500,height=500)

overlay = (base_map*mesh.opts(alpha=0.75)).opts(active_tools=['wheel_zoom', 'pan'])

# Define the slider as panel widgets
widgets = {dim: pn.widgets.Select for dim in ds_clip[var].dims[:2:3]}

# Combine everything in a dashboard
dashboard = pn.pane.HoloViews(overlay, widgets=widgets).layout

dashboard

Adding Dashboard functionalities

At this point we have built interactive apps and dashboards with Panel, to quickly build visualizations with hvPlot, and add custom interactivity by using HoloViews.

We will now work on putting all of this together to build a more complex, and efficient data processing pipelines, controlled by Panel widgets.

Defining panel widgets

# Define the existing variables in the xArray Dataset
rho_vars = []
for var in ds_clip.data_vars:
    if len(ds_clip[var].dims) > 0:
        rho_vars.append(var)
        

# Define the panel widget for the Xarray variables
var_select = pn.widgets.Select(name='Select variables:', options=rho_vars, 
                               value='elevation')

# Define the panel widget for the background maps
base_map_select = pn.widgets.Select(name='Choose underlying map:', 
                                    options=gvts.tile_sources, 
                                    value=gvts.EsriImagery)

# Define the panel widget for the different colormap
color_select = pn.widgets.Select(name='Pick a colormap', options= sorted([
    'cet_bgy', 'cet_bkr', 'cet_bgyw', 'cet_bky', 'cet_kbc', 'cet_coolwarm', 
    'cet_blues', 'cet_gwv', 'cet_bmw', 'cet_bjy', 'cet_bmy', 'cet_bwy', 'cet_kgy', 
    'cet_cwr', 'cet_gray', 'cet_dimgray', 'cet_fire', 'kb', 'cet_kg', 'cet_kr',
    'cet_colorwheel', 'cet_isolium', 'cet_rainbow', 'cet_bgy_r', 'cet_bkr_r', 
    'cet_bgyw_r', 'cet_bky_r', 'cet_kbc_r', 'cet_coolwarm_r', 'cet_blues_r', 
    'cet_gwv_r', 'cet_bmw_r', 'cet_bjy_r', 'cet_bmy_r', 'cet_bwy_r', 'cet_kgy_r', 
    'cet_cwr_r', 'cet_gray_r', 'cet_dimgray_r', 'cet_fire_r', 'kb_r', 'cet_kg_r', 
    'cet_kr_r', 'cet_colorwheel_r', 'cet_isolium_r', 'cet_rainbow_r', 'jet'], 
    key=str.casefold), value='jet') 

Defining the plotting functions

This function is the same as the one we defined for the simple dashboard above but it allows for the different variables defined in the panel widgets to be interactively chosen…

def plot(var=None, base_map=None, cmap='jet'):
    
    base_map = base_map or base_map_select.value
    var = var or var_select.value
    
    label = f'{ds_clip[var].name}: {ds_clip[var].units}'
    
    mesh = ds_clip[var].hvplot.quadmesh(x='longitude', y='latitude', rasterize=True, title=label,
                                    width=500, height=500, crs=ccrs.PlateCarree(),
                                    #groupby=list(ds[var].dims[:-2]), 
                                    cmap=cmap)
    
    mesh = mesh.redim.default(**{d: ds_clip[d].values.max() for d in ds_clip[var].dims[:-2]})
    overlay = (base_map * mesh.opts(alpha=0.5)).opts(active_tools=['wheel_zoom', 'pan'])
    widgets = {dim: pn.widgets.Select for dim in ds_clip[var].dims[:-2]}
    
    return pn.pane.HoloViews(overlay).layout #, widgets=widgets).layout

Widgets value selection functions:

def on_var_select(event):
    var = event.obj.value
    dashboard[-1] = plot(var=var)
    
def on_base_map_select(event):
    base_map = event.obj.value
    dashboard[-1] = plot(base_map=base_map)
    
def on_color_select(event):
    cmap = event.obj.value
    dashboard[-1] = plot(cmap=cmap)
    
var_select.param.watch(on_var_select, parameter_names=['value']);
base_map_select.param.watch(on_base_map_select, parameter_names=['value']);
color_select.param.watch(on_color_select, parameter_names=['value']);

Advanced dashboard

widget = pn.widgets.StaticText(name='', value='Interactive visualisation of gospl outputs', 
                               style={'font-size': "14px", 'font-style': "bold"})

selection_widget = pn.Row(var_select, color_select, base_map_select)

dashboard = pn.Column(widget, selection_widget, plot(var_select.value))
box = pn.WidgetBox('# gospl App', dashboard)
box.servable()

Box stream

We will use the BoxDraw stream to draw region of interests (e.g. ROIs) over the erosion/deposition data, and use them to compute and display timeseries of the cumulative erosion/deposition in the regions of interests.

Note

First, we will change the latitude and longitude to integer as the Box stream does not seem to accept floating values (or at least I didn’t find the trick yet).

# Slider location
var = 'erodep'

coords={'time': np.array([1514764800000000000+86400000000000*i 
                          for i in range(ds_clip[var].shape[0])]).astype('datetime64[ns]'),
        'y': np.arange(ds_clip[var].shape[1]), 
        'x': np.arange(ds_clip[var].shape[2]),
        }

viewdata = xr.DataArray(ds_clip[var].values, coords=coords, dims=['time', 'y', 'x'], name='erodep')

The following is from the HoloViews example and define the ROIs functionality:

titleName = 'Cumulative erosion/deposition (m)'
hv_ds = hv.Dataset(viewdata)

# Create stack of images grouped by time
im = hv_ds.to(hv.Image, ['x','y'], dynamic=True).opts(active_tools=['wheel_zoom', 'pan'], 
                                                      cmap=cmocean.cm.balance,
                     colorbar=True, width=450, height=400, clim=(-500,500))

polys = hv.Polygons([])

box_stream = hv.streams.BoxEdit(source=polys)

# Declare an empty DataFrame to declare the types
empty = pd.DataFrame({'time': np.array([], dtype='datetime64[ns]'), 'erodep': []})

def roi_curves(data):
    if not data or not any(len(d) for d in data.values()):
        return hv.NdOverlay({0: hv.Curve(empty, 'time', 'erodep')})

    curves = {}
    data = zip(data['x0'], data['x1'], data['y0'], data['y1'])
    for i, (x0, x1, y0, y1) in enumerate(data):
        selection = hv_ds.select(x=(x0, x1), y=(y0, y1))
        curves[i] = hv.Curve(selection.aggregate('time', np.mean))
    return hv.NdOverlay(curves)

# Generate VLines by getting time value from the image frames
def vline(frame):
    return hv.VLine(frame.data.time.values)
vlines = im.apply(vline)

dmap = hv.DynamicMap(roi_curves, streams=[box_stream])

Tip

To define an ROI, select the Box edit tool and double click to start defining the ROI and double click to finish placing the ROI:

(im * polys  + dmap * vlines ).opts(
    opts.Curve(width=400, framewise=True), 
    opts.Polygons(fill_alpha=0.2, line_color='white'), 
    opts.VLine(color='black'))

1

Signell & Pothina: Analysis and Visualization of Coastal Ocean Model Data in the Cloud, 2019.