This page was generated from ridges.ipynb. Interactive online version: Binder badge

Ridge Map#

[1]:
from __future__ import annotations

import matplotlib.pyplot as plt
import numpy as np
import xarray as xr
from matplotlib import colors
from matplotlib.collections import LineCollection

import py3dep
from pynhd import NLDI

In this tutorial, we use elevation contours to create a Ridge Map. A Ridge Map is a 2D representation of a 3D surface, where the ridges are shown as lines. The Ridge Map is created by finding the steepest ascent path from each point on the surface and plotting it as a line.

We modify a function from Ridge Map to accept a xarray.DataArray as input and plot the Ridge Map using Matplotlib.

[14]:
from typing import TYPE_CHECKING, Literal, cast

if TYPE_CHECKING:
    from matplotlib.axes import Axes
    from matplotlib.colors import Colormap
    from matplotlib.figure import Figure


def plot_ridges(
    elevation: xr.DataArray,
    label: str | None = None,
    label_x: float = 0.62,
    label_y: float = 0.15,
    label_verticalalignment: str = "bottom",
    label_size: int = 60,
    line_color: str | Colormap = "black",
    kind: Literal["gradient", "elevation"] = "gradient",
    linewidth: int = 2,
    background_color: tuple[float, float, float] = (0.9255, 0.9098, 0.9255),
    size_scale: int = 20,
) -> tuple[Figure, Axes]:
    """Plot the map.

    Notes
    -----
    This function is a modified version of the `plot_map` function from the
    `ridge_map <https://github.com/ColCarroll/ridge_map>`_ package.

    Parameters
    ----------
    elevation : xarray.DataArray
        Array of elevations to plot. Defaults to the elevations at the provided
        bounding box.
    label : str
        Label to place on the map. Use an empty string for no label.
    label_x : float in [0, 1]
        Where to position the label horizontally
    label_y : float in [0, 1]
        Where to position the label vertically
    label_verticalalignment: "top" or "bottom"
        Whether the label_x and label_y refer to the top or bottom left corner
        of the label text box
    label_size : int
        fontsize of the label
    line_color : str or callable
        colors for the map. A callable will be fed the scaled index in [0, 1]
    kind : {"gradient" | "elevation"}
        If you provide a colormap to ``line_color``, "gradient" colors by the line index, and
        "elevation" colors by the actual elevation along the line.
    linewidth : float
        Width of each line in the map
    background_color : color
        For the background of the map and figure
    scale_size : float
        If you are printing this, make this number bigger.

    Returns
    -------
    matplotlib.Axes
    """
    if kind not in {"gradient", "elevation"}:
        raise TypeError("Argument `kind` must be one of 'gradient' or 'elevation'")

    if not isinstance(elevation, xr.DataArray):
        raise TypeError("Argument `elevation` must be an xarray.DataArray")

    xmin, ymin, xmax, ymax = elevation.rio.bounds()
    ratio = (ymax - ymin) / (xmax - xmin)
    fig, ax = plt.subplots(figsize=(size_scale, size_scale * ratio))
    ax = cast("Axes", ax)

    values = elevation.to_numpy()
    x = np.arange(values.shape[1])
    norm = colors.Normalize(np.nanmin(values), np.nanmax(values))
    for idx, row in enumerate(values):
        y_base = -6 * idx * np.ones_like(row)
        y = row + y_base
        if callable(line_color) and kind == "elevation":
            points = np.array([x, y]).T.reshape((-1, 1, 2))
            segments = np.concatenate([points[:-1], points[1:]], axis=1).tolist()
            lines = LineCollection(segments, cmap=line_color, zorder=idx + 1, norm=norm)
            lines.set_array(row)
            lines.set_linewidth(linewidth)
            ax.add_collection(lines)
        else:
            if callable(line_color) and kind == "gradient":
                color = line_color(idx / values.shape[0])
            else:
                color = line_color

            ax.plot(x, y, "-", color=color, zorder=idx, lw=linewidth)
        ax.fill_between(x, y_base, y, color=background_color, zorder=idx)

    if label:
        ax.text(
            label_x,
            label_y,
            label,
            transform=ax.transAxes,
            size=label_size,
            verticalalignment=label_verticalalignment,
            bbox={"facecolor": background_color, "alpha": 1, "linewidth": 0},
            zorder=len(values) + 10,
        )

    ax.xaxis.set_visible(False)
    ax.yaxis.set_visible(False)
    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.set_facecolor(background_color)
    return fig, ax

We get the DEM for station 01031500 by first getting its basin geometry from the NLDI web service from pynhd and then use py3dep to get the DEM at 90-m resolution.

[15]:
geometry = NLDI().get_basins("01031500").geometry.iloc[0]
dem = py3dep.get_dem(geometry, 90)
[16]:
fig, _ = plot_ridges(dem, line_color=plt.get_cmap("terrain"), kind="elevation")
fig.savefig("_static/ridge_map.png", bbox_inches="tight", dpi=300)
../../_images/examples_notebooks_ridges_6_0.png