From fdbbf42fbdc9854e9a91b9739d8cc397e28be10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Tue, 24 Dec 2024 14:43:16 +0100 Subject: [PATCH] Add cross-section plot utils - add inset_map - add xsec_line_and_labels --- nlmod/plot/__init__.py | 2 + nlmod/plot/dcs.py | 8 +-- nlmod/plot/plotutil.py | 136 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 4 deletions(-) diff --git a/nlmod/plot/__init__.py b/nlmod/plot/__init__.py index 1563924f..01fd1bed 100644 --- a/nlmod/plot/__init__.py +++ b/nlmod/plot/__init__.py @@ -14,9 +14,11 @@ ) from .plotutil import ( add_background_map, + add_xsec_line_and_labels, colorbar_inside, get_figsize, get_map, + inset_map, rd_ticks, rotate_yticklabels, title_inside, diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index cb901376..c41f8959 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -566,9 +566,9 @@ def animate( elif "units" in da.attrs: cbar.set_label(da.units) - if da.time.dtype.kind == 'M': + if da.time.dtype.kind == "M": t = pd.Timestamp(da.time.values[iper]).strftime(date_fmt) - elif da.time.dtype.kind == 'O': + elif da.time.dtype.kind == "O": t = da.time.values[iper].strftime(date_fmt) else: t = f"{da.time.values[iper]} {da.time.time_units}" @@ -584,9 +584,9 @@ def update(iper, pc, title): pc.set_array(array) # update title - if da.time.dtype.kind == 'M': + if da.time.dtype.kind == "M": t = pd.Timestamp(da.time.values[iper]).strftime(date_fmt) - elif da.time.dtype.kind == 'O': + elif da.time.dtype.kind == "O": t = da.time.values[iper].strftime(date_fmt) else: t = f"{da.time.values[iper]} {da.time.time_units}" diff --git a/nlmod/plot/plotutil.py b/nlmod/plot/plotutil.py index c1d23479..92b9895a 100644 --- a/nlmod/plot/plotutil.py +++ b/nlmod/plot/plotutil.py @@ -1,7 +1,11 @@ +from typing import Callable, Optional, Union + import matplotlib.pyplot as plt import numpy as np +from matplotlib import patheffects from matplotlib.patches import Polygon from matplotlib.ticker import FuncFormatter, MultipleLocator +from shapely import LineString from ..dims.grid import get_affine_mod_to_world from ..epsg28992 import EPSG_28992 @@ -260,3 +264,135 @@ def title_inside( bbox=bbox, **kwargs, ) + + +def inset_map( + ax: plt.Axes, + extent: Union[tuple[float], list[float]], + axes_bounds: Union[tuple[float], list[float]] = (0.63, 0.025, 0.35, 0.35), + anchor: str = "SE", + provider: Optional[str] = "nlmaps.water", + add_to_plot: Optional[list[Callable]] = None, +): + """Add an inset map to an axes. + + Parameters + ---------- + ax : matplotlib.Axes + The axes to add the inset map to. + extent : list of 4 floats + The extent of the inset map. + axes_bounds : list of 4 floats, optional + The bounds (left, right, width height) of the inset axes, default + is [0.63, 0.025, 0.35, 0.35]. This is rescaled according to the extent of + the inset map. + anchor : str, optional + The anchor point of the inset map, default is 'SE'. + provider : str, optional + Add a backgroundmap if map provider is passed, default is 'nlmaps.water'. To + turn off the backgroundmap set provider to None. + add_to_plot : list of functions, optional + List of functions to plot on the inset map, default is None. The functions + must accept an ax argument. Hint: use `functools.partial` to set plot style, + and pass the partial function to add_to_plot. + + Returns + ------- + mapax : matplotlib.Axes + The inset map axes. + """ + mapax = ax.inset_axes(axes_bounds) + mapax.axis(extent) + mapax.set_aspect("equal", adjustable="box", anchor=anchor) + mapax.set_xticks([]) + mapax.set_yticks([]) + mapax.set_xlabel("") + mapax.set_ylabel("") + + if provider: + add_background_map(mapax, map_provider=provider, attribution=False) + + if add_to_plot: + for fplot in add_to_plot: + fplot(ax=mapax) + + return mapax + + +def add_xsec_line_and_labels( + line: Union[list, LineString], + ax: plt.Axes, + mapax: plt.Axes, + x_offset: float = 0.0, + y_offset: float = 0.0, + label: str = "A", + **kwargs, +): + """Add a cross-section line to an overview map and label the start and end points. + + Parameters + ---------- + line : list or shapely LineString + The line to plot. + ax : matplotlib.Axes + The axes to plot the labels on. + mapax : matplotlib.Axes + The axes of the overview map to plot the line on. + x_offset : float, optional + The x offset of the labels, default is 0.0. + y_offset : float, optional + The y offset of the labels, default is 0.0. + kwargs : dict + Keyword arguments to pass to the line plot function. + + Raises + ------ + ValueError + If the line is not a list or a shapely LineString. + """ + if isinstance(line, list): + x, y = np.array(line).T + elif isinstance(line, LineString): + x, y = line.xy + else: + raise ValueError("line should be a list or a shapely LineString") + mapax.plot(x, y, **kwargs) + stroke = [patheffects.withStroke(linewidth=2, foreground="w")] + mapax.text( + x[0] + x_offset, + y[0] + y_offset, + f"{label}", + fontweight="bold", + path_effects=stroke, + fontsize=7, + ) + mapax.text( + x[-1] + x_offset, + y[-1] + y_offset, + f"{label}'", + fontweight="bold", + path_effects=stroke, + fontsize=7, + ) + ax.text( + 0.01, + 0.99, + f"{label}", + transform=ax.transAxes, + path_effects=stroke, + fontsize=14, + ha="left", + va="top", + fontweight="bold", + ) + ax.text( + 0.99, + 0.99, + f"{label}'", + transform=ax.transAxes, + path_effects=stroke, + fontsize=14, + ha="right", + va="top", + fontweight="bold", + )