Source code for hydromt_sfincs.plots

"""Plotting functions for SFINCS model data."""
from typing import Dict, List, Tuple

import numpy as np
import pandas as pd
import xarray as xr

from .utils import get_bounds_vector

__all__ = ["plot_forcing", "plot_basemap"]

geom_style = {
    "rivers": dict(linestyle="-", linewidth=1.0, color="darkblue"),
    "rivers_out": dict(linestyle="-", linewidth=1.0, color="darkgreen"),
    "msk2": dict(linestyle="-", linewidth=1.5, color="r"),
    "msk3": dict(linestyle="-", linewidth=1.5, color="m"),
    "thd": dict(linestyle="-", linewidth=1.0, color="k", annotate=False),
    "weir": dict(linestyle="--", linewidth=1.0, color="k", annotate=False),
    "bnd": dict(marker="^", markersize=75, c="w", edgecolor="k", annotate=True),
    "src": dict(marker=">", markersize=75, c="w", edgecolor="k", annotate=True),
    "obs": dict(marker="d", markersize=75, c="w", edgecolor="r", annotate=True),
    "region": dict(ls="--", linewidth=1, color="r"),
}


[docs]def plot_forcing(forcing: Dict, **kwargs): """Plot model timeseries forcing. For distributed forcing a spatial avarage is plotted. Parameters ---------- forcing : Dict of xr.DataArray Model forcing Returns ------- fig, axes Model fig and ax objects """ import matplotlib.dates as mdates import matplotlib.pyplot as plt n = len(forcing.keys()) kwargs0 = dict(sharex=True, figsize=(6, n * 3)) kwargs0.update(**kwargs) fig, axes = plt.subplots(n, 1, **kwargs0) axes = [axes] if n == 1 else axes for i, name in enumerate(forcing): da = forcing[name].transpose("time", ...) longname = da.attrs.get("standard_name", "") unit = da.attrs.get("unit", "") prefix = "" if da.ndim == 3: da = da.mean(dim=[da.raster.x_dim, da.raster.y_dim]) prefix = "mean " # convert to Single index dataframe (bar plots don't work with xarray) df = da.to_pandas() if isinstance(df.index, pd.MultiIndex): df = df.unstack(0) # convert dates a-priori as automatic conversion doesn't always work df.index = mdates.date2num(df.index) if name.startswith("precip"): axes[i].bar(df.index, df.values, facecolor="darkblue") else: df.plot.line(ax=axes[i]).legend( title="index", bbox_to_anchor=(1.05, 1), loc="upper left", ncol=df.columns.size // 5 + 1, prop={"size": 8}, ) axes[i].set_ylabel(f"{prefix}{longname}\n[{unit}]") axes[i].set_title(f"SFINCS {longname} forcing ({name})") # use a concise date formatter for format date axis ticks locator = mdates.AutoDateLocator() formatter = mdates.ConciseDateFormatter(locator) axes[-1].xaxis.set_major_locator(locator) axes[-1].xaxis.set_major_formatter(formatter) return fig, axes
[docs]def plot_basemap( ds: xr.Dataset, geoms: Dict, variable: str = "dep", shaded: bool = False, plot_bounds: bool = True, plot_region: bool = False, plot_geoms: bool = True, bmap: str = None, zoomlevel: int = 11, figsize: Tuple[int] = None, geom_names: List[str] = None, geom_kwargs: Dict = {}, legend_kwargs: Dict = {}, bmap_kwargs: Dict = {}, **kwargs, ): """Create basemap plot. Parameters ---------- ds : xr.Dataset Dataset with model maps geoms : Dict of geopandas.GeoDataFrame Model geometries variable : str, optional Map of variable in ds to plot, by default 'dep' shaded : bool, optional Add shade to variable (only for variable = 'dep'), by default False plot_bounds : bool, optional Add waterlevel (msk=2) and open (msk=3) boundary conditions to plot. plot_region : bool, optional If True, plot region outline. plot_geoms : bool, optional If True, plot available geoms. bmap : {'sat', 'osm'} background map, by default "sat" zoomlevel : int, optional zoomlevel, by default 11 figsize : Tuple[int], optional figure size, by default None geom_names : List[str], optional list of model geometries to plot, by default all model geometries geom_kwargs : Dict of Dict, optional Model geometry styling per geometry, passed to geopandas.GeoDataFrame.plot method. For instance: {'src': {'markersize': 30}}. legend_kwargs : Dict, optional Legend kwargs, passed to ax.legend method. Returns ------- fig, axes Model fig and ax objects """ import cartopy.crs as ccrs import cartopy.io.img_tiles as cimgt import matplotlib.pyplot as plt from matplotlib import colors, patheffects # read crs and utm zone > convert to cartopy wkt = ds.raster.crs.to_wkt() if "UTM zone " not in wkt: raise ValueError("Model CRS UTM zone not found.") utm_zone = ds.raster.crs.to_wkt().split("UTM zone ")[1][:3] utm = ccrs.UTM(int(utm_zone[:2]), "S" in utm_zone) extent = np.array(ds.raster.box.buffer(1e2).total_bounds)[[0, 2, 1, 3]] # create fig with geo-axis and set background if figsize is None: ratio = ds.raster.ycoords.size / (ds.raster.xcoords.size * 1.4) figsize = (8, 8 * ratio) fig = plt.figure(figsize=figsize) ax = plt.subplot(projection=utm) ax.set_extent(extent, crs=utm) if bmap == "sat": ax.add_image(cimgt.QuadtreeTiles(**bmap_kwargs), zoomlevel) elif bmap == "osm": ax.add_image(cimgt.OSM(**bmap_kwargs), zoomlevel) elif bmap is not None and hasattr(cimgt, bmap): ax.add_image(getattr(cimgt, bmap)(**bmap_kwargs), zoomlevel) # by default colorbar on lower right & legend upper right kwargs0 = {"cbar_kwargs": {"shrink": 0.5, "anchor": (0, 0)}} kwargs0.update(kwargs) # make nice cmap if "cmap" not in kwargs or "norm" not in kwargs: if variable == "dep" and "dep" in ds: vmin, vmax = ds["dep"].raster.mask_nodata().quantile([0.0, 0.98]).values vmin, vmax = int(kwargs.pop("vmin", vmin)), int(kwargs.pop("vmax", vmax)) c_dem = plt.cm.terrain(np.linspace(0.25, 1, vmax)) if vmin < 0: c_bat = plt.cm.terrain(np.linspace(0, 0.17, max(1, abs(vmin)))) c_dem = np.vstack((c_bat, c_dem)) cmap = colors.LinearSegmentedColormap.from_list("dem", c_dem) norm = colors.Normalize(vmin=vmin, vmax=vmax) cmap, norm = kwargs.pop("cmap", cmap), kwargs.pop("norm", norm) kwargs0.update(norm=norm, cmap=cmap) elif variable == "msk" and "msk" in ds: cmap = colors.LinearSegmentedColormap.from_list( "Set1", ["grey", "r", "m"], N=4 ) norm = colors.BoundaryNorm([0.5, 1.5, 2.5, 3.5], 3) kwargs0.update(norm=norm, cmap=cmap) kwargs0["cbar_kwargs"].update(ticks=[1, 2, 3]) if variable in ds: da = ds[variable].raster.mask_nodata() if np.any(ds["msk"] > 0): da = da.where(ds["msk"] > 0) if da.raster.rotation != 0 and "xc" in da.coords and "yc" in da.coords: da.plot(transform=utm, x="xc", y="yc", ax=ax, zorder=1, **kwargs0) else: da.plot.imshow(transform=utm, ax=ax, zorder=1, **kwargs0) if shaded and variable == "dep" and da.raster.rotation == 0: ls = colors.LightSource(azdeg=315, altdeg=45) dx, dy = da.raster.res _rgb = ls.shade( da.fillna(0).values, norm=kwargs["norm"], cmap=kwargs["cmap"], blend_mode="soft", dx=dx, dy=dy, vert_exag=2, ) rgb = xr.DataArray( dims=("y", "x", "rgb"), data=_rgb, coords=da.raster.coords ) rgb = xr.where(np.isnan(da), np.nan, rgb) rgb.plot.imshow(transform=utm, ax=ax, zorder=1) # geometry plotting and annotate kwargs for k, d in geom_kwargs.items(): geom_style[k].update(**d) ann_kwargs = dict( xytext=(3, 3), textcoords="offset points", zorder=4, path_effects=[ patheffects.Stroke(linewidth=3, foreground="w"), patheffects.Normal(), ], ) # plot mask boundaries if plot_bounds and (ds["msk"] >= 1).any(): gdf_msk = get_bounds_vector(ds["msk"]) gdf_msk2 = gdf_msk[gdf_msk["value"] == 2] gdf_msk3 = gdf_msk[gdf_msk["value"] == 3] if gdf_msk2.index.size > 0: gdf_msk2.plot(ax=ax, zorder=3, label="waterlevel bnd", **geom_style["msk2"]) if gdf_msk3.index.size > 0: gdf_msk3.plot(ax=ax, zorder=3, label="outflow bnd", **geom_style["msk3"]) # plot static geoms if plot_geoms: geom_names = geom_names if isinstance(geom_names, list) else list(geoms.keys()) for name in geom_names: gdf = geoms.get(name, None) if gdf is None or name in ["region", "bbox"]: continue # copy is important to keep annotate working if repeated kwargs = geom_style.get(name, {}).copy() annotate = kwargs.pop("annotate", False) gdf.plot(ax=ax, zorder=3, label=name, **kwargs) if annotate and np.all(gdf.geometry.type == "Point"): for label, row in gdf.iterrows(): x, y = row.geometry.x, row.geometry.y ax.annotate(label, xy=(x, y), **ann_kwargs) if "region" in geoms and plot_region: geoms["region"].boundary.plot( ax=ax, zorder=2, label="region", **geom_style["region"] ) # title, legend and labels ax.xaxis.set_visible(True) ax.yaxis.set_visible(True) ax.set_ylabel(f"y coordinate UTM zone {utm_zone} [m]") ax.set_xlabel(f"x coordinate UTM zone {utm_zone} [m]") variable = "base" if variable is None else variable ax.set_title(f"SFINCS {variable} map") # NOTE without defined loc it takes forever to find a 'best' location # by default outside plot if geom_names or plot_bounds: legend_kwargs0 = dict( bbox_to_anchor=(1.05, 1), title="Legend", loc="upper left", frameon=True, prop=dict(size=8), ) legend_kwargs0.update(**legend_kwargs) ax.legend(**legend_kwargs0) return fig, ax