Skip to content

Plotting

Bathymetry

bathy.plot_bathy(data, contours=None, cmap=None, mask_land=True, **kwargs)

Plot bathymetry elevation.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
contours int or list[float]

Number of contour levels or specific levels (in metres)

None
cmap str or Colormap

Colormap. Defaults to cmocean 'deep_r'.

None
mask_land bool

If True, mask positive elevations (land).

True
**kwargs

Additional arguments passed to xarray plot

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_bathy(
    data: xr.DataArray,
    contours: int | list[float] | None = None,
    cmap=None,
    mask_land: bool = True,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot bathymetry elevation.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    contours : int or list[float], optional
        Number of contour levels or specific levels (in metres)
    cmap : str or Colormap, optional
        Colormap. Defaults to cmocean 'deep_r'.
    mask_land : bool, default True
        If True, mask positive elevations (land).
    **kwargs
        Additional arguments passed to xarray plot

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    if cmap is None:
        cmap = cmo.deep_r

    fig, ax = plt.subplots(figsize=(10, 8))

    data_masked = data.where(data < 0) if mask_land else data

    if "cbar_kwargs" not in kwargs:
        kwargs["cbar_kwargs"] = {"label": "Elevation (m)"}

    data_masked.plot(ax=ax, cmap=cmap, **kwargs)

    if contours is not None:
        _add_contours(data, ax, contours)

    x_label, y_label = axis_labels(data)
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    return fig, ax

bathy.plot_hillshade(data, azimuth=315, altitude=45, contours=None, **kwargs)

Create hillshade visualisation.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
azimuth float

Light source azimuth in degrees

315
altitude float

Light source altitude in degrees

45
contours int or list[float]

Contour levels

None
**kwargs

Additional arguments passed to imshow

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_hillshade(
    data: xr.DataArray,
    azimuth: float = 315,
    altitude: float = 45,
    contours: int | list[float] | None = None,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Create hillshade visualisation.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    azimuth : float, default 315
        Light source azimuth in degrees
    altitude : float, default 45
        Light source altitude in degrees
    contours : int or list[float], optional
        Contour levels
    **kwargs
        Additional arguments passed to imshow

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    shaded = _hillshade(data, azimuth=azimuth, altitude=altitude)
    extent = get_extent(data)

    fig, ax = plt.subplots(figsize=(10, 8))
    ax.imshow(
        shaded, cmap="gray", origin="lower", extent=extent, aspect="auto", **kwargs
    )

    if contours is not None:
        _add_contours(data, ax, contours)

    x_label, y_label = axis_labels(data)
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    return fig, ax

bathy.plot_slope(data, contours=None, vmax=None, **kwargs)

Plot seafloor slope.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
contours int or list[float]

Contour levels

None
vmax float

Maximum slope value for colour scale.

None
**kwargs

Additional arguments passed to imshow

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_slope(
    data: xr.DataArray,
    contours: int | list[float] | None = None,
    vmax: float | None = None,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot seafloor slope.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    contours : int or list[float], optional
        Contour levels
    vmax : float, optional
        Maximum slope value for colour scale.
    **kwargs
        Additional arguments passed to imshow

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    slope_data = slope(data)
    if vmax is None:
        vmax = float(np.nanpercentile(slope_data.values, 99))
    return _plot_grid(
        slope_data.values,
        data,
        "Greys",
        "Slope (°)",
        contours=contours,
        vmin=0,
        vmax=vmax,
        **kwargs,
    )

bathy.plot_curvature(data, contours=None, **kwargs)

Plot seafloor curvature.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
contours int or list[float]

Contour levels

None
**kwargs

Additional arguments passed to imshow

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_curvature(
    data: xr.DataArray,
    contours: int | list[float] | None = None,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot seafloor curvature.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    contours : int or list[float], optional
        Contour levels
    **kwargs
        Additional arguments passed to imshow

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    curvature_data = curvature(data)
    vmax = np.nanmax(np.abs(curvature_data.values))
    return _plot_grid(
        curvature_data.values,
        data,
        cmo.balance,
        "Curvature",
        contours=contours,
        vmin=-vmax,
        vmax=vmax,
        **kwargs,
    )

bathy.plot_aspect(data, contours=None, **kwargs)

Plot seafloor aspect.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
contours int or list[float]

Contour levels

None
**kwargs

Additional arguments passed to imshow

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_aspect(
    data: xr.DataArray,
    contours: int | list[float] | None = None,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot seafloor aspect.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    contours : int or list[float], optional
        Contour levels
    **kwargs
        Additional arguments passed to imshow

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    asp_data = aspect(data)
    extent = get_extent(data)

    fig, ax = plt.subplots(figsize=(10, 8))
    im = ax.imshow(
        asp_data.values,
        cmap=cmo.phase,
        origin="lower",
        extent=extent,
        aspect="auto",
        vmin=0,
        vmax=360,
        **kwargs,
    )
    cbar = plt.colorbar(im, ax=ax, label="Aspect")
    cbar.set_ticks([0, 90, 180, 270, 360])
    cbar.set_ticklabels(["0° N", "90° E", "180° S", "270° W", "360° N"])

    if contours is not None:
        _add_contours(data, ax, contours)

    x_label, y_label = axis_labels(data)
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    return fig, ax

bathy.plot_bpi(data, radius_km=1.0, contours=None, **kwargs)

Plot Bathymetric Position Index (BPI).

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
radius_km float

Neighbourhood radius in kilometres

1.0
contours int or list[float]

Contour levels

None
**kwargs

Additional arguments passed to imshow

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_bpi(
    data: xr.DataArray,
    radius_km: float = 1.0,
    contours: int | list[float] | None = None,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot Bathymetric Position Index (BPI).

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    radius_km : float, default 1.0
        Neighbourhood radius in kilometres
    contours : int or list[float], optional
        Contour levels
    **kwargs
        Additional arguments passed to imshow

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    bpi_data = bpi(data, radius_km)
    vmax = np.nanmax(np.abs(bpi_data.values))
    return _plot_grid(
        bpi_data.values,
        data,
        cmo.balance,
        f"BPI (r={radius_km} km)",
        contours=contours,
        vmin=-vmax,
        vmax=vmax,
        **kwargs,
    )

bathy.plot_rugosity(data, radius_km=1.0, contours=None, vmax=None, **kwargs)

Plot Vector Ruggedness Measure (VRM).

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
radius_km float

Neighbourhood radius in kilometres

1.0
contours int or list[float]

Contour levels

None
vmax float

Maximum VRM value for colour scale.

None
**kwargs

Additional arguments passed to imshow

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_rugosity(
    data: xr.DataArray,
    radius_km: float = 1.0,
    contours: int | list[float] | None = None,
    vmax: float | None = None,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot Vector Ruggedness Measure (VRM).

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    radius_km : float, default 1.0
        Neighbourhood radius in kilometres
    contours : int or list[float], optional
        Contour levels
    vmax : float, optional
        Maximum VRM value for colour scale.
    **kwargs
        Additional arguments passed to imshow

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    rug_data = rugosity(data, radius_km)
    if vmax is None:
        vmax = float(np.nanpercentile(rug_data.values, 99))
    return _plot_grid(
        rug_data.values,
        data,
        cmo.amp,
        f"Rugosity VRM (r={radius_km} km)",
        contours=contours,
        vmin=0,
        vmax=vmax,
        **kwargs,
    )

bathy.plot_geomorphons(data, lookup_km=2.0, flatness_threshold=1.0, **kwargs)

Plot seafloor morphology using geomorphons.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
lookup_km float

Lookup distance in kilometres.

2.0
flatness_threshold float

Flatness angle threshold in degrees.

1.0
**kwargs

Additional arguments passed to imshow.

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Examples:

>>> plot_geomorphons(data, lookup_km=2.0)
Source code in src/bathy/plot.py
def plot_geomorphons(
    data: xr.DataArray,
    lookup_km: float = 2.0,
    flatness_threshold: float = 1.0,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot seafloor morphology using geomorphons.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    lookup_km : float, default 2.0
        Lookup distance in kilometres.
    flatness_threshold : float, default 1.0
        Flatness angle threshold in degrees.
    **kwargs
        Additional arguments passed to imshow.

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.

    Examples
    --------
    >>> plot_geomorphons(data, lookup_km=2.0)
    """
    geom_data = geomorphons(data, lookup_km, flatness_threshold)
    extent = get_extent(data)

    cmap = ListedColormap(_GEOMORPHON_COLORS)
    norm = BoundaryNorm(np.arange(0.5, 11.5), len(_GEOMORPHON_COLORS))

    fig, ax = plt.subplots(figsize=(10, 8))
    im = ax.imshow(
        geom_data.values,
        cmap=cmap,
        norm=norm,
        origin="lower",
        extent=extent,
        aspect="auto",
        **kwargs,
    )

    cbar = plt.colorbar(im, ax=ax)
    cbar.set_ticks(range(1, 11))
    cbar.set_ticklabels(_GEOMORPHON_LABELS)

    x_label, y_label = axis_labels(data)
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    return fig, ax

bathy.plot_overview(data, bpi_radius_km=1.0, rugosity_radius_km=1.0, geomorphons_lookup_km=2.0, label_prefix=None)

Plot key bathymetric metrics as subplot overview.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
bpi_radius_km float

Neighbourhood radius for BPI in kilometres.

1.0
rugosity_radius_km float

Neighbourhood radius for rugosity in kilometres.

1.0
geomorphons_lookup_km float

Lookup distance for geomorphons in kilometres.

2.0
label_prefix list[str] | None

Optional list of 8 prefix strings for panel titles.

None

Examples:

Returns

tuple[Figure, np.ndarray] Matplotlib figure and array of axes for further customisation.

Examples:

>>> plot_overview(data)
Source code in src/bathy/plot.py
def plot_overview(
    data: xr.DataArray,
    bpi_radius_km: float = 1.0,
    rugosity_radius_km: float = 1.0,
    geomorphons_lookup_km: float = 2.0,
    label_prefix: list[str] | None = None,
) -> tuple[Figure, np.ndarray]:
    """
    Plot key bathymetric metrics as subplot overview.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    bpi_radius_km : float, default 1.0
        Neighbourhood radius for BPI in kilometres.
    rugosity_radius_km : float, default 1.0
        Neighbourhood radius for rugosity in kilometres.
    geomorphons_lookup_km : float, default 2.0
        Lookup distance for geomorphons in kilometres.
    label_prefix : list[str] | None, default None
        Optional list of 8 prefix strings for panel titles.

    Examples
    --------
    Returns
    -------
    tuple[Figure, np.ndarray]
        Matplotlib figure and array of axes for further customisation.

    Examples
    --------
    >>> plot_overview(data)
    """
    n_panels = 8
    prefixes = label_prefix or [""] * n_panels
    if len(prefixes) != n_panels:
        raise ValueError(
            f"label_prefix must have {n_panels} entries, got {len(prefixes)}"
        )

    def title(i: int, name: str) -> str:
        p = prefixes[i]
        return f"{p} {name}" if p else name

    extent = get_extent(data)
    imkw = dict(origin="lower", extent=extent, aspect="auto")

    hs = _hillshade(data)
    sl = slope(data)
    asp = aspect(data)
    cu = curvature(data)
    bp = bpi(data, bpi_radius_km)
    vr = rugosity(data, rugosity_radius_km)
    gm = geomorphons(data, geomorphons_lookup_km)

    fig, axes = plt.subplots(
        4,
        2,
        figsize=(12, 20),
        constrained_layout=True,
        sharex=True,
        sharey=True,
    )

    im = axes[0, 0].imshow(data.where(data < 0).values, cmap=cmo.deep_r, **imkw)
    plt.colorbar(im, ax=axes[0, 0], label="m")
    axes[0, 0].set_title(title(0, "Bathymetry"))

    axes[0, 1].imshow(hs, cmap="gray", **imkw)
    axes[0, 1].set_title(title(1, "Hillshade"))

    vmax = float(np.nanpercentile(sl.values, 99))
    im = axes[1, 0].imshow(sl.values, cmap="Greys", vmin=0, vmax=vmax, **imkw)
    plt.colorbar(im, ax=axes[1, 0], label="°")
    axes[1, 0].set_title(title(2, "Slope"))

    im = axes[1, 1].imshow(asp.values, cmap=cmo.phase, vmin=0, vmax=360, **imkw)
    cbar = plt.colorbar(im, ax=axes[1, 1])
    cbar.set_ticks([0, 90, 180, 270, 360])
    cbar.set_ticklabels(["N", "E", "S", "W", "N"])
    axes[1, 1].set_title(title(3, "Aspect"))

    vmax = float(np.nanpercentile(np.abs(cu.values), 99))
    im = axes[2, 0].imshow(cu.values, cmap=cmo.balance, vmin=-vmax, vmax=vmax, **imkw)
    plt.colorbar(im, ax=axes[2, 0], label="m⁻¹")
    axes[2, 0].set_title(title(4, "Curvature"))

    vmax = float(np.nanpercentile(np.abs(bp.values), 99))
    im = axes[2, 1].imshow(bp.values, cmap=cmo.balance, vmin=-vmax, vmax=vmax, **imkw)
    plt.colorbar(im, ax=axes[2, 1], label="m")
    axes[2, 1].set_title(title(5, f"BPI (r={bpi_radius_km} km)"))

    vmax = float(np.nanpercentile(vr.values, 99))
    im = axes[3, 0].imshow(vr.values, cmap=cmo.amp, vmin=0, vmax=vmax, **imkw)
    plt.colorbar(im, ax=axes[3, 0], label="VRM")
    axes[3, 0].set_title(title(6, f"Rugosity (r={rugosity_radius_km} km)"))

    im = axes[3, 1].imshow(
        gm.values,
        cmap=ListedColormap(_GEOMORPHON_COLORS),
        norm=BoundaryNorm(np.arange(0.5, 11.5), 10),
        **imkw,
    )
    cbar = plt.colorbar(im, ax=axes[3, 1])
    cbar.set_ticks(range(1, 11))
    cbar.set_ticklabels(_GEOMORPHON_LABELS, fontsize=7)
    axes[3, 1].set_title(title(7, f"Geomorphons ({geomorphons_lookup_km} km)"))

    x_label, y_label = axis_labels(data)
    for ax in axes[3, :]:
        ax.set_xlabel(x_label)
    for ax in axes[:, 0]:
        ax.set_ylabel(y_label)
    for ax in axes.ravel():
        ax.label_outer()

    return fig, axes

Distribution

bathy.plot_depth_zones(data, zones=None, labels=None, contours=None, **kwargs)

Plot bathymetry color-coded by depth zones.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
zones list[float]

Depth boundaries (default: [0, -200, -1000, -4000])

None
labels list[str]

Zone labels (default: ['Shelf', 'Slope', 'Abyss', 'Deep'])

None
contours int or list[float]

Contour levels

None
**kwargs

Additional arguments passed to imshow

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_depth_zones(
    data: xr.DataArray,
    zones: list[float] | None = None,
    labels: list[str] | None = None,
    contours: int | list[float] | None = None,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot bathymetry color-coded by depth zones.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    zones : list[float], optional
        Depth boundaries (default: [0, -200, -1000, -4000])
    labels : list[str], optional
        Zone labels (default: ['Shelf', 'Slope', 'Abyss', 'Deep'])
    contours : int or list[float], optional
        Contour levels
    **kwargs
        Additional arguments passed to imshow

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    if zones is None:
        zones = [0, -200, -1000, -4000]
    if labels is None:
        labels = ["Shelf", "Slope", "Abyss", "Deep"]

    sorted_zones = sorted(zones)
    n_zones = len(sorted_zones)

    boundaries = np.array([data.min().values] + sorted_zones)
    reversed_labels = labels[::-1]

    deep_colors = cmo.deep(np.linspace(1, 0, n_zones))
    colors = ListedColormap(deep_colors)
    norm = BoundaryNorm(boundaries, n_zones)

    extent = get_extent(data)

    fig, ax = plt.subplots(figsize=(10, 8))
    im = ax.imshow(
        data.values,
        cmap=colors,
        norm=norm,
        origin="lower",
        extent=extent,
        aspect="auto",
        **kwargs,
    )

    if contours is not None:
        _add_contours(data, ax, contours)

    cbar = plt.colorbar(im, ax=ax, label="Depth zone")

    tick_positions = [(boundaries[i] + boundaries[i + 1]) / 2 for i in range(n_zones)]
    tick_labels = [
        f"{reversed_labels[i]}\n({int(boundaries[i + 1])} to {int(boundaries[i])} m)"
        for i in range(n_zones)
    ]
    cbar.set_ticks(tick_positions)
    cbar.set_ticklabels(tick_labels)

    x_label, y_label = axis_labels(data)
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    return fig, ax

bathy.plot_histogram(data, bins=50, **kwargs)

Plot histogram of elevation values.

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_histogram(data: xr.DataArray, bins: int = 50, **kwargs) -> tuple[Figure, Axes]:
    """
    Plot histogram of elevation values.

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    fig, ax = plt.subplots(figsize=(10, 6))

    values = _clean_values(data)

    ax.hist(values, bins=bins, edgecolor="black", **kwargs)
    ax.axvline(0, color="blue", linestyle="--", linewidth=2, label="Sea level")
    ax.set_xlabel("Elevation (m)")
    ax.set_ylabel("Frequency")
    ax.legend()
    ax.grid(True, alpha=0.3)
    return fig, ax

bathy.plot_hypsometric_curve(data, bins=100, **kwargs)

Plot the hypsometric curve.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
bins int

Number of elevation bins

100
**kwargs

Additional arguments passed to plt.plot

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Examples:

>>> plot_hypsometric_curve(data)
Source code in src/bathy/plot.py
def plot_hypsometric_curve(
    data: xr.DataArray, bins: int = 100, **kwargs
) -> tuple[Figure, Axes]:
    """
    Plot the hypsometric curve.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    bins : int, default 100
        Number of elevation bins
    **kwargs
        Additional arguments passed to plt.plot

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.

    Examples
    --------
    >>> plot_hypsometric_curve(data)
    """
    df = hypsometric_curve(data, bins)

    fig, ax = plt.subplots(figsize=(8, 8))
    ax.plot(df["relative_area"], df["relative_elevation"], linewidth=2, **kwargs)
    ax.plot([0, 1], [1, 0], "k--", alpha=0.3, label="Equidimensional")
    ax.legend()
    ax.set_xlabel("Relative Area (a/A)")
    ax.set_ylabel("Relative Elevation (h/H)")
    ax.set_xlim(0, 1)
    ax.set_ylim(0, 1)
    ax.set_aspect("equal")
    ax.grid(True, alpha=0.3)
    return fig, ax

Interactive

bathy.plot_interactive(data, overlays=None, cmap=None, opacity=0.7, mask_land=True, tiles='CartoDB positron')

Display bathymetry on an interactive Leaflet map.

Uses folium to render an interactive map overlay. Additional analysis layers (slope, aspect, etc.) can be added via overlays and toggled with the layer control.

Parameters:

Name Type Description Default
data DataArray

Elevation data.

required
overlays dict[str, DataArray]

Extra layers to display, e.g. {"Slope": bathy.slope(data)}. Each is rendered as a toggleable overlay with an auto-detected colormap and legend.

None
cmap str or Colormap

Colormap for the bathymetry layer. Defaults to cmocean 'deep_r'.

None
opacity float

Overlay opacity for valid (ocean) pixels.

0.7
mask_land bool

If True, mask positive elevations as transparent.

True
tiles str

Base tile layer name.

'CartoDB positron'

Returns:

Type Description
Map

Interactive map. Renders in Jupyter; call .save("map.html") to export.

Examples:

>>> m = bathy.plot_interactive(data)
>>> m = bathy.plot_interactive(data, overlays={"Slope": bathy.slope(data)})
>>> m.save("my_map.html")
Source code in src/bathy/plot.py
def plot_interactive(
    data: xr.DataArray,
    overlays: dict[str, xr.DataArray] | None = None,
    cmap=None,
    opacity: float = 0.7,
    mask_land: bool = True,
    tiles: str = "CartoDB positron",
):
    """
    Display bathymetry on an interactive Leaflet map.

    Uses ``folium`` to render an interactive map overlay. Additional
    analysis layers (slope, aspect, etc.) can be added via *overlays*
    and toggled with the layer control.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data.
    overlays : dict[str, xr.DataArray], optional
        Extra layers to display, e.g. ``{"Slope": bathy.slope(data)}``.
        Each is rendered as a toggleable overlay with an auto-detected
        colormap and legend.
    cmap : str or Colormap, optional
        Colormap for the bathymetry layer. Defaults to cmocean 'deep_r'.
    opacity : float, default 0.7
        Overlay opacity for valid (ocean) pixels.
    mask_land : bool, default True
        If True, mask positive elevations as transparent.
    tiles : str, default 'CartoDB positron'
        Base tile layer name.

    Returns
    -------
    folium.Map
        Interactive map. Renders in Jupyter; call ``.save("map.html")``
        to export.

    Examples
    --------
    >>> m = bathy.plot_interactive(data)
    >>> m = bathy.plot_interactive(data, overlays={"Slope": bathy.slope(data)})
    >>> m.save("my_map.html")
    """
    import folium  # noqa: PLC0415

    if cmap is None:
        cmap = cmo.deep_r
    elif isinstance(cmap, str):
        cmap = plt.get_cmap(cmap)

    x_dim, y_dim = get_dim_names(data)

    values = data.values.astype(float)
    if mask_land:
        values = np.where(values >= 0, np.nan, values)

    bounds = [
        [float(data[y_dim].min()), float(data[x_dim].min())],
        [float(data[y_dim].max()), float(data[x_dim].max())],
    ]

    m = folium.Map(
        location=[(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2],
        tiles=tiles,
    )

    vmin = float(np.nanmin(values))
    vmax = float(np.nanmax(values))
    _render_overlay(
        values, bounds, "Bathymetry", cmap, "Elevation (m)", vmin, vmax, opacity, m
    )

    if overlays:
        for name, layer in overlays.items():
            layer_values = layer.values.astype(float)
            layer_cmap, label, lv_min, lv_max = _overlay_defaults(name, layer_values)
            _render_overlay(
                layer_values,
                bounds,
                name,
                layer_cmap,
                label,
                lv_min,
                lv_max,
                opacity,
                m,
            )

    folium.LayerControl().add_to(m)
    m.fit_bounds(bounds)

    return m

3D

bathy.plot_surface3d(data, stride=10, vertical_exaggeration=50.0, smooth=None, elev=30, azim=-60, **kwargs)

Create static 3D surface plot.

Parameters:

Name Type Description Default
data DataArray

Elevation data

required
stride int

Stride for downsampling (every Nth point)

10
vertical_exaggeration float

Factor to exaggerate the vertical scale.

50.0
smooth int

Uniform filter kernel size for smoothing.

None
elev float

Elevation viewing angle in degrees.

30
azim float

Azimuth viewing angle in degrees.

-60
**kwargs

Additional arguments passed to plot_surface

{}

Returns:

Type Description
tuple[Figure, Axes]

Matplotlib figure and axes for further customisation.

Source code in src/bathy/plot.py
def plot_surface3d(
    data: xr.DataArray,
    stride: int = 10,
    vertical_exaggeration: float = 50.0,
    smooth: int | None = None,
    elev: float = 30,
    azim: float = -60,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Create static 3D surface plot.

    Parameters
    ----------
    data : xr.DataArray
        Elevation data
    stride : int, default 10
        Stride for downsampling (every Nth point)
    vertical_exaggeration : float, default 50.0
        Factor to exaggerate the vertical scale.
    smooth : int, optional
        Uniform filter kernel size for smoothing.
    elev : float, default 30
        Elevation viewing angle in degrees.
    azim : float, default -60
        Azimuth viewing angle in degrees.
    **kwargs
        Additional arguments passed to plot_surface

    Returns
    -------
    tuple[Figure, Axes]
        Matplotlib figure and axes for further customisation.
    """
    x_dim, y_dim = get_dim_names(data)

    fig = plt.figure(figsize=(14, 8))
    ax = fig.add_subplot(111, projection="3d")

    x_vals = data[x_dim].values[::stride]
    y_vals = data[y_dim].values[::stride]
    z = data.values[::stride, ::stride]

    if smooth is not None:
        from scipy.ndimage import uniform_filter  # noqa: PLC0415

        z = uniform_filter(z, size=smooth, mode="nearest")

    x_grid, y_grid = np.meshgrid(x_vals, y_vals)

    surf = ax.plot_surface(
        x_grid,
        y_grid,
        z,
        cmap=cmo.deep_r,
        linewidth=0,
        antialiased=True,
        **kwargs,
    )
    fig.colorbar(surf, ax=ax, label="Elevation (m)", shrink=0.5, pad=0.1)

    x_range = x_vals.max() - x_vals.min()
    y_range = y_vals.max() - y_vals.min()
    if not is_projected(data):
        # Correct for meridian convergence in geographic CRS
        lat_centre = float(data[y_dim].mean())
        x_range *= np.cos(np.radians(lat_centre))

    ax.set_box_aspect(
        [
            x_range,
            y_range,
            (z.max() - z.min()) * vertical_exaggeration / 1000,
        ]
    )

    ax.view_init(elev=elev, azim=azim)
    x_label, y_label = axis_labels(data)
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    ax.set_zlabel("Elevation (m)")
    plt.tight_layout()
    return fig, ax

Profile

bathy.plot_profile(profile, show_map=False, smooth=None, normalize=False, ensure_descending=False, cmap=cmo.deep_r, bathymetry_data=None, y_pad=0.05, **kwargs)

Plot the bathymetric profile.

Parameters:

Name Type Description Default
profile Profile
required
show_map bool

If True, show map with profile line. Requires bathymetry_data.

False
smooth float

Gaussian smoothing sigma.

None
normalize bool

If True, normalize elevation and distance to 0-1.

False
ensure_descending bool

If True, orient profile to descend from higher to lower elevation.

False
bathymetry_data DataArray

Background data for map view (required when show_map=True).

None
y_pad float

Fractional padding added above and below the elevation range. Defaults to 0.05 (5%).

0.05
**kwargs

Additional arguments passed to matplotlib plot()

{}

Returns:

Type Description
tuple[Figure, list[Axes]]

Figure and list of axes (one element without map, two with map).

Source code in src/bathy/profile_plot.py
def plot_profile(
    profile: Profile,
    show_map: bool = False,
    smooth: float | None = None,
    normalize: bool = False,
    ensure_descending: bool = False,
    cmap=cmo.deep_r,
    bathymetry_data: xr.DataArray | None = None,
    y_pad: float = 0.05,
    **kwargs,
) -> tuple[Figure, list[Axes]]:
    """
    Plot the bathymetric profile.

    Parameters
    ----------
    profile : Profile
    show_map : bool
        If True, show map with profile line. Requires bathymetry_data.
    smooth : float, optional
        Gaussian smoothing sigma.
    normalize : bool
        If True, normalize elevation and distance to 0-1.
    ensure_descending : bool
        If True, orient profile to descend from higher to lower elevation.
    bathymetry_data : xr.DataArray, optional
        Background data for map view (required when show_map=True).
    y_pad : float
        Fractional padding added above and below the elevation range.
        Defaults to 0.05 (5%).
    **kwargs
        Additional arguments passed to matplotlib plot()

    Returns
    -------
    tuple[Figure, list[Axes]]
        Figure and list of axes (one element without map, two with map).
    """
    elevations = (
        gaussian_filter1d(profile.elevations, sigma=smooth)
        if smooth
        else profile.elevations
    )
    distances = profile.distances / 1000

    if ensure_descending:
        distances, elevations = _ensure_descending(distances, elevations)

    if normalize:
        distances, elevations = _normalise_profile(distances, elevations)

    elev_range = float(elevations.max() - elevations.min())
    pad = elev_range * y_pad
    ylim = (float(elevations.min()) - pad, float(elevations.max()) + pad)
    xlim = (float(distances.min()), float(distances.max()))

    if show_map:
        fig, (ax_map, ax_profile) = plt.subplots(1, 2, figsize=(16, 6))

        if bathymetry_data is not None:
            extent = get_extent(bathymetry_data)
            ax_map.imshow(
                bathymetry_data.values,
                cmap=cmap,
                origin="lower",
                extent=extent,
                aspect="auto",
            )
        path_xs = profile.metadata.get("path_xs", [profile.start_x, profile.end_x])
        path_ys = profile.metadata.get("path_ys", [profile.start_y, profile.end_y])
        ax_map.plot(path_xs, path_ys, "r-", linewidth=2, label="Profile line")
        ax_map.plot(path_xs[0], path_ys[0], "go", markersize=10, label="Start")
        ax_map.plot(path_xs[-1], path_ys[-1], "ro", markersize=10, label="End")
        if bathymetry_data is not None:
            x_label, y_label = axis_labels(bathymetry_data)
        else:
            x_label, y_label = crs_axis_labels(profile.crs)
        ax_map.set_xlabel(x_label)
        ax_map.set_ylabel(y_label)
        ax_map.legend()
    else:
        fig, ax_profile = plt.subplots(figsize=(12, 5))

    ax_profile.plot(distances, elevations, **kwargs)
    ax_profile.fill_between(distances, elevations, ylim[0], alpha=0.3)

    ax_profile.set_xlabel("Normalized distance" if normalize else "Distance (km)")
    ax_profile.set_ylabel("Normalized elevation" if normalize else "Elevation (m)")
    ax_profile.set_xlim(xlim)
    ax_profile.set_ylim(ylim)
    ax_profile.grid(True, alpha=0.3)

    if show_map:
        return fig, [ax_map, ax_profile]
    return fig, [ax_profile]

bathy.plot_profiles(profiles, show_map=False, normalize=False, ensure_descending=False, bathymetry_data=None, cmap=cmo.deep_r, **kwargs)

Plot multiple profiles on the same axes.

Parameters:

Name Type Description Default
profiles Profile or list[Profile]
required
show_map bool

If True, show map with profile lines alongside the profile plot. Requires bathymetry_data.

False
normalize bool

If True, normalize each profile's elevation and distance to 0-1.

False
ensure_descending bool

If True, orient profiles to descend from higher to lower elevation.

False
bathymetry_data DataArray

Background data for map view.

None
**kwargs

Additional arguments passed to matplotlib plot()

{}

Returns:

Type Description
tuple[Figure, list[Axes]]

Figure and list of axes (one element without map, two with map).

Examples:

>>> from bathy.profile_plot import plot_profiles
>>> prof1 = extract_profile(data, start=(-8, 52), end=(-2, 58), name="Profile 1")
>>> prof2 = extract_profile(data, start=(-8, 53), end=(-2, 59), name="Profile 2")
>>> plot_profiles([prof1, prof2])
Source code in src/bathy/profile_plot.py
def plot_profiles(
    profiles: Profile | list[Profile],
    show_map: bool = False,
    normalize: bool = False,
    ensure_descending: bool = False,
    bathymetry_data: xr.DataArray | None = None,
    cmap=cmo.deep_r,
    **kwargs,
) -> tuple[Figure, list[Axes]]:
    """
    Plot multiple profiles on the same axes.

    Parameters
    ----------
    profiles : Profile or list[Profile]
    show_map : bool
        If True, show map with profile lines alongside the profile plot.
        Requires bathymetry_data.
    normalize : bool
        If True, normalize each profile's elevation and distance to 0-1.
    ensure_descending : bool
        If True, orient profiles to descend from higher to lower elevation.
    bathymetry_data : xr.DataArray, optional
        Background data for map view.
    **kwargs
        Additional arguments passed to matplotlib plot()

    Returns
    -------
    tuple[Figure, list[Axes]]
        Figure and list of axes (one element without map, two with map).

    Examples
    --------
    >>> from bathy.profile_plot import plot_profiles
    >>> prof1 = extract_profile(data, start=(-8, 52), end=(-2, 58), name="Profile 1")
    >>> prof2 = extract_profile(data, start=(-8, 53), end=(-2, 59), name="Profile 2")
    >>> plot_profiles([prof1, prof2])
    """
    if isinstance(profiles, Profile):
        profiles = [profiles]

    if not profiles:
        raise ValueError("Need at least one profile to plot")

    if show_map:
        fig, (ax_map, ax_profile) = plt.subplots(1, 2, figsize=(16, 6))

        if bathymetry_data is not None:
            extent = get_extent(bathymetry_data)
            ax_map.imshow(
                bathymetry_data.values,
                cmap=cmap,
                origin="lower",
                extent=extent,
                aspect="auto",
                alpha=0.6,
            )

        for i, prof in enumerate(profiles, start=1):
            label = prof.name if prof.name else f"Profile {i}"
            path_xs = prof.metadata.get("path_xs", [prof.start_x, prof.end_x])
            path_ys = prof.metadata.get("path_ys", [prof.start_y, prof.end_y])
            ax_map.plot(path_xs, path_ys, "-", linewidth=2, label=label)
            ax_map.plot(path_xs[0], path_ys[0], "o", markersize=6)
            ax_map.plot(path_xs[-1], path_ys[-1], "s", markersize=6)

        if bathymetry_data is not None:
            x_label, y_label = axis_labels(bathymetry_data)
        else:
            x_label, y_label = crs_axis_labels(profiles[0].crs)
        ax_map.set_xlabel(x_label)
        ax_map.set_ylabel(y_label)
        ax_map.legend()
    else:
        fig, ax_profile = plt.subplots(figsize=(12, 6))

    for i, prof in enumerate(profiles, start=1):
        distances = prof.distances / 1000
        elevations = prof.elevations.copy()

        if ensure_descending:
            distances, elevations = _ensure_descending(distances, elevations)

        if normalize:
            distances, elevations = _normalise_profile(distances, elevations)

        label = prof.name if prof.name else f"Profile {i}"
        ax_profile.plot(distances, elevations, label=label, **kwargs)

    ax_profile.set_xlabel("Normalized distance" if normalize else "Distance (km)")
    ax_profile.set_ylabel("Normalized elevation" if normalize else "Elevation (m)")
    ax_profile.grid(True, alpha=0.3)
    ax_profile.legend()

    if show_map:
        return fig, [ax_map, ax_profile]
    return fig, [ax_profile]

bathy.plot_profiles_map(profiles, bathymetry_data=None, main_profile=None, cmap=cmo.deep_r, **kwargs)

Plot profile locations on a map.

Parameters:

Name Type Description Default
profiles Profile or list[Profile]
required
bathymetry_data DataArray

Background bathymetry data.

None
main_profile Profile

Main profile to highlight.

None
**kwargs

Additional arguments passed to matplotlib plot()

{}

Returns:

Type Description
(Figure, Axes)

Examples:

>>> from bathy.profile_plot import plot_profiles_map
>>> plot_profiles_map([prof1, prof2], bathymetry_data=data)
Source code in src/bathy/profile_plot.py
def plot_profiles_map(
    profiles: Profile | list[Profile],
    bathymetry_data: xr.DataArray | None = None,
    main_profile: Profile | None = None,
    cmap=cmo.deep_r,
    **kwargs,
) -> tuple[Figure, Axes]:
    """
    Plot profile locations on a map.

    Parameters
    ----------
    profiles : Profile or list[Profile]
    bathymetry_data : xr.DataArray, optional
        Background bathymetry data.
    main_profile : Profile, optional
        Main profile to highlight.
    **kwargs
        Additional arguments passed to matplotlib plot()

    Returns
    -------
    Figure, Axes

    Examples
    --------
    >>> from bathy.profile_plot import plot_profiles_map
    >>> plot_profiles_map([prof1, prof2], bathymetry_data=data)
    """
    if isinstance(profiles, Profile):
        profiles = [profiles]

    if not profiles:
        raise ValueError("Need at least one profile to plot")

    fig, ax = plt.subplots(figsize=(10, 8))

    if bathymetry_data is not None:
        extent = get_extent(bathymetry_data)
        ax.imshow(
            bathymetry_data.values,
            cmap=cmap,
            origin="lower",
            extent=extent,
            aspect="auto",
            alpha=0.6,
        )

    for i, prof in enumerate(profiles, start=1):
        label = prof.name if prof.name else f"Profile {i}"

        if "path_xs" in prof.metadata and "path_ys" in prof.metadata:
            xs = prof.metadata["path_xs"]
            ys = prof.metadata["path_ys"]
            ax.plot(xs, ys, "-", linewidth=2, label=label, **kwargs)
            ax.plot(xs[0], ys[0], "o", markersize=8)
            ax.plot(xs[-1], ys[-1], "s", markersize=8)
        else:
            ax.plot(
                [prof.start_x, prof.end_x],
                [prof.start_y, prof.end_y],
                "-",
                linewidth=2,
                label=label,
                **kwargs,
            )
            ax.plot(prof.start_x, prof.start_y, "o", markersize=8)
            ax.plot(prof.end_x, prof.end_y, "s", markersize=8)

    if main_profile is not None:
        main_label = main_profile.name if main_profile.name else "Main Profile"
        ax.plot(
            [main_profile.start_x, main_profile.end_x],
            [main_profile.start_y, main_profile.end_y],
            "r-",
            linewidth=3,
            label=main_label,
            zorder=10,
        )
        ax.plot(
            main_profile.start_x,
            main_profile.start_y,
            "go",
            markersize=10,
            zorder=11,
            label="Start",
        )
        ax.plot(
            main_profile.end_x,
            main_profile.end_y,
            "rs",
            markersize=10,
            zorder=11,
            label="End",
        )

    if bathymetry_data is not None:
        x_label, y_label = axis_labels(bathymetry_data)
    else:
        x_label, y_label = crs_axis_labels(profiles[0].crs)
    ax.set_xlabel(x_label)
    ax.set_ylabel(y_label)
    if any(p.name for p in profiles) or main_profile is not None:
        ax.legend()

    return fig, ax

bathy.plot_profiles_grid(profiles, cols=2, figsize=None, main_profile=None, smooth=None, normalize=False, ensure_descending=False, y_pad=0.05, **kwargs)

Plot multiple profiles in a grid of subplots.

Parameters:

Name Type Description Default
profiles Profile or list[Profile]
required
cols int

Number of columns in the grid (default: 2)

2
figsize tuple[float, float]

Figure size. Calculated if None.

None
main_profile Profile

Main profile; marks intersection with each cross-section.

None
smooth float

Gaussian smoothing sigma.

None
normalize bool

If True, normalize each profile's elevation and distance to 0-1.

False
ensure_descending bool

If True, orient profiles to descend from higher to lower elevation.

False
y_pad float

Fractional padding added above and below the elevation range. Defaults to 0.05 (5%).

0.05
**kwargs

Additional arguments passed to matplotlib plot()

{}

Returns:

Type Description
(Figure, ndarray)

Examples:

>>> from bathy.profile_plot import plot_profiles_grid
>>> profiles = profiles_from_file(data, "canyons.shp")
>>> plot_profiles_grid(profiles[:10])
Source code in src/bathy/profile_plot.py
def plot_profiles_grid(
    profiles: Profile | list[Profile],
    cols: int = 2,
    figsize: tuple[float, float] | None = None,
    main_profile: Profile | None = None,
    smooth: float | None = None,
    normalize: bool = False,
    ensure_descending: bool = False,
    y_pad: float = 0.05,
    **kwargs,
) -> tuple[Figure, np.ndarray]:
    """
    Plot multiple profiles in a grid of subplots.

    Parameters
    ----------
    profiles : Profile or list[Profile]
    cols : int
        Number of columns in the grid (default: 2)
    figsize : tuple[float, float], optional
        Figure size. Calculated if None.
    main_profile : Profile, optional
        Main profile; marks intersection with each cross-section.
    smooth : float, optional
        Gaussian smoothing sigma.
    normalize : bool
        If True, normalize each profile's elevation and distance to 0-1.
    ensure_descending : bool
        If True, orient profiles to descend from higher to lower elevation.
    y_pad : float
        Fractional padding added above and below the elevation range.
        Defaults to 0.05 (5%).
    **kwargs
        Additional arguments passed to matplotlib plot()

    Returns
    -------
    Figure, np.ndarray

    Examples
    --------
    >>> from bathy.profile_plot import plot_profiles_grid
    >>> profiles = profiles_from_file(data, "canyons.shp")
    >>> plot_profiles_grid(profiles[:10])
    """
    if isinstance(profiles, Profile):
        profiles = [profiles]

    if not profiles:
        raise ValueError("Need at least one profile to plot")

    n_profiles = len(profiles)
    rows = (n_profiles + cols - 1) // cols

    if figsize is None:
        figsize = (7 * cols, 3.5 * rows)

    fig, axes = plt.subplots(rows, cols, figsize=figsize)
    axes = np.atleast_1d(axes).flatten()

    for i, prof in enumerate(profiles):
        ax = axes[i]

        elevations = (
            gaussian_filter1d(prof.elevations, sigma=smooth)
            if smooth
            else prof.elevations.copy()
        )
        distances = prof.distances / 1000

        if ensure_descending:
            distances, elevations = _ensure_descending(distances, elevations)

        if normalize:
            distances, elevations = _normalise_profile(distances, elevations)

        elev_range = float(elevations.max() - elevations.min())
        pad = elev_range * y_pad
        ylim = (float(elevations.min()) - pad, float(elevations.max()) + pad)
        xlim = (float(distances.min()), float(distances.max()))

        ax.plot(distances, elevations, **kwargs)
        ax.fill_between(distances, elevations, ylim[0], alpha=0.3)

        if main_profile is not None:
            mid_distance = distances[len(distances) // 2]
            ax.axvline(
                mid_distance,
                color="black",
                linestyle="-",
                linewidth=1.5,
                alpha=0.7,
                zorder=10,
            )

        ax.set_xlabel("Normalized distance" if normalize else "Distance (km)")
        ax.set_ylabel("Normalized elevation" if normalize else "Elevation (m)")
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
        title = prof.name if prof.name else f"Profile {i + 1}"
        ax.set_title(f"{title} ({prof.distances[-1] / 1000:.1f} km)")
        ax.grid(True, alpha=0.3)

    for i in range(n_profiles, len(axes)):
        axes[i].set_visible(False)

    plt.tight_layout()
    return fig, axes

bathy.plot_gradient(profile, **kwargs)

Plot the gradient (derivative) along the profile.

Parameters:

Name Type Description Default
profile Profile
required
**kwargs

Additional arguments passed to plot

{}

Returns:

Type Description
tuple[Figure, list[Axes]]

Figure and list containing the single gradient axes.

Source code in src/bathy/profile_plot.py
def plot_gradient(profile: Profile, **kwargs) -> tuple[Figure, list[Axes]]:
    """
    Plot the gradient (derivative) along the profile.

    Parameters
    ----------
    profile : Profile
    **kwargs
        Additional arguments passed to plot

    Returns
    -------
    tuple[Figure, list[Axes]]
        Figure and list containing the single gradient axes.
    """
    grad = gradient(profile)

    fig, ax = plt.subplots(figsize=(12, 5))
    ax.plot(profile.distances / 1000, grad, **kwargs)
    ax.axhline(0, color="gray", linestyle="--", alpha=0.5, linewidth=1)
    ax.set_xlabel("Distance (km)")
    ax.set_ylabel("Slope (°)")
    ax.grid(True, alpha=0.3)

    return fig, [ax]

bathy.plot_knickpoints(profile, knickpoints_df=None, threshold=None, smooth=None, **kwargs)

Plot profile with knickpoints marked.

Parameters:

Name Type Description Default
profile Profile
required
knickpoints_df DataFrame

Knickpoint data from knickpoints(). Detected if None.

None
threshold float

Minimum slope break for detection (ignored if knickpoints_df provided).

None
smooth float

Smoothing sigma (ignored if knickpoints_df provided).

None
**kwargs

Additional arguments passed to plot_profile

{}

Returns:

Type Description
tuple[Figure, list[Axes]]

Figure and list of axes.

Source code in src/bathy/profile_plot.py
def plot_knickpoints(
    profile: Profile,
    knickpoints_df: pl.DataFrame | None = None,
    threshold: float | None = None,
    smooth: float | None = None,
    **kwargs,
) -> tuple[Figure, list[Axes]]:
    """
    Plot profile with knickpoints marked.

    Parameters
    ----------
    profile : Profile
    knickpoints_df : pl.DataFrame, optional
        Knickpoint data from knickpoints(). Detected if None.
    threshold : float, optional
        Minimum slope break for detection (ignored if knickpoints_df provided).
    smooth : float, optional
        Smoothing sigma (ignored if knickpoints_df provided).
    **kwargs
        Additional arguments passed to plot_profile

    Returns
    -------
    tuple[Figure, list[Axes]]
        Figure and list of axes.
    """
    if knickpoints_df is None:
        knickpoints_df = knickpoints(profile, threshold=threshold, smooth=smooth)

    fig, axes = plot_profile(profile, **kwargs)

    if len(knickpoints_df) == 0:
        logger.info("No knickpoints detected. Try adjusting threshold or smoothing.")
        return fig, axes

    axes[-1].scatter(
        knickpoints_df["distance_m"] / 1000,
        knickpoints_df["depth_m"],
        c="red",
        s=50,
        zorder=5,
        label="Knickpoints",
    )
    axes[-1].legend()
    return fig, axes

bathy.plot_canyons(profile, canyons, **kwargs)

Plot profile with canyons marked.

Parameters:

Name Type Description Default
profile Profile
required
canyons DataFrame

Canyon data from get_canyons().

required
**kwargs

Additional arguments passed to plot_profile()

{}

Returns:

Type Description
tuple[Figure, list[Axes]]

Figure and list of axes.

Source code in src/bathy/profile_plot.py
def plot_canyons(
    profile: Profile,
    canyons: pl.DataFrame,
    **kwargs,
) -> tuple[Figure, list[Axes]]:
    """
    Plot profile with canyons marked.

    Parameters
    ----------
    profile : Profile
    canyons : pl.DataFrame
        Canyon data from ``get_canyons()``.
    **kwargs
        Additional arguments passed to plot_profile()

    Returns
    -------
    tuple[Figure, list[Axes]]
        Figure and list of axes.
    """

    if len(canyons) == 0:
        logger.info("No canyons detected. Try adjusting prominence or smoothing.")
        return plot_profile(profile, **kwargs)

    fig, axes = plot_profile(profile, **kwargs)
    ax = axes[-1]

    for row in canyons.iter_rows(named=True):
        floor_km = row["floor_distance"] / 1000
        floor_elev = row["floor_elevation"]
        shoulder_elev = row["shoulder_elevation"]
        ws_km, we_km = row["width_start"] / 1000, row["width_end"] / 1000

        ax.plot(floor_km, floor_elev, "ro", markersize=8, zorder=10)
        ax.plot(
            [ws_km, we_km],
            [shoulder_elev] * 2,
            "k--",
            linewidth=1.5,
            alpha=0.7,
            zorder=5,
        )
        ax.plot(
            [floor_km] * 2,
            [floor_elev, shoulder_elev],
            "k--",
            linewidth=1.5,
            alpha=0.7,
            zorder=5,
        )

    return fig, axes