Skip to content

Commit fa22f58

Browse files
authored
Merge pull request #399 from gwmod/plot_improvements
Add cross-section plot utils
2 parents 8d7a221 + 7157717 commit fa22f58

5 files changed

Lines changed: 162 additions & 6 deletions

File tree

docs/examples/13_plot_methods.ipynb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@
171171
"id": "fba12db0",
172172
"metadata": {},
173173
"source": [
174-
"With the DatasetCrossSection in `nlmod` it is also possible to plot the layers according to the official colors of REGIS, to plot the layer names on the plot, or to plot the model grid in the cross-section. An example is shown in the plot below."
174+
"With the DatasetCrossSection in `nlmod` it is also possible to plot the layers according to the official colors of REGIS, to plot the layer names on the plot, or to plot the model grid in the cross-section. An example is shown in the plot below.\n",
175+
"\n",
176+
"The location of the cross-section and the cross-section labels can be added using `nlmod.plot.inset_map()` and `nlmod.plot.add_xsec_line_and_labels()`."
175177
]
176178
},
177179
{
@@ -185,7 +187,9 @@
185187
"dcs = DatasetCrossSection(ds, line=line, zmin=-200, zmax=10, ax=ax)\n",
186188
"colors = nlmod.read.regis.get_legend()\n",
187189
"dcs.plot_layers(colors=colors, min_label_area=1000)\n",
188-
"dcs.plot_grid(vertical=False, linewidth=0.5);"
190+
"dcs.plot_grid(vertical=False, linewidth=0.5)\n",
191+
"mapax = nlmod.plot.inset_map(ax, ds.extent)\n",
192+
"nlmod.plot.add_xsec_line_and_labels(line, ax, mapax)"
189193
]
190194
},
191195
{

nlmod/plot/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
)
1515
from .plotutil import (
1616
add_background_map,
17+
add_xsec_line_and_labels,
1718
colorbar_inside,
1819
get_figsize,
1920
get_map,
21+
inset_map,
2022
rd_ticks,
2123
rotate_yticklabels,
2224
title_inside,

nlmod/plot/dcs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -566,9 +566,9 @@ def animate(
566566
elif "units" in da.attrs:
567567
cbar.set_label(da.units)
568568

569-
if da.time.dtype.kind == 'M':
569+
if da.time.dtype.kind == "M":
570570
t = pd.Timestamp(da.time.values[iper]).strftime(date_fmt)
571-
elif da.time.dtype.kind == 'O':
571+
elif da.time.dtype.kind == "O":
572572
t = da.time.values[iper].strftime(date_fmt)
573573
else:
574574
t = f"{da.time.values[iper]} {da.time.time_units}"
@@ -584,9 +584,9 @@ def update(iper, pc, title):
584584
pc.set_array(array)
585585

586586
# update title
587-
if da.time.dtype.kind == 'M':
587+
if da.time.dtype.kind == "M":
588588
t = pd.Timestamp(da.time.values[iper]).strftime(date_fmt)
589-
elif da.time.dtype.kind == 'O':
589+
elif da.time.dtype.kind == "O":
590590
t = da.time.values[iper].strftime(date_fmt)
591591
else:
592592
t = f"{da.time.values[iper]} {da.time.time_units}"

nlmod/plot/plotutil.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from typing import Callable, Optional, Union
2+
13
import matplotlib.pyplot as plt
24
import numpy as np
5+
from matplotlib import patheffects
36
from matplotlib.patches import Polygon
47
from matplotlib.ticker import FuncFormatter, MultipleLocator
8+
from shapely import LineString
59

610
from ..dims.grid import get_affine_mod_to_world
711
from ..epsg28992 import EPSG_28992
@@ -269,3 +273,135 @@ def title_inside(
269273
bbox=bbox,
270274
**kwargs,
271275
)
276+
277+
278+
def inset_map(
279+
ax: plt.Axes,
280+
extent: Union[tuple[float], list[float]],
281+
axes_bounds: Union[tuple[float], list[float]] = (0.63, 0.025, 0.35, 0.35),
282+
anchor: str = "SE",
283+
provider: Optional[str] = "nlmaps.water",
284+
add_to_plot: Optional[list[Callable]] = None,
285+
):
286+
"""Add an inset map to an axes.
287+
288+
Parameters
289+
----------
290+
ax : matplotlib.Axes
291+
The axes to add the inset map to.
292+
extent : list of 4 floats
293+
The extent of the inset map.
294+
axes_bounds : list of 4 floats, optional
295+
The bounds (left, right, width height) of the inset axes, default
296+
is [0.63, 0.025, 0.35, 0.35]. This is rescaled according to the extent of
297+
the inset map.
298+
anchor : str, optional
299+
The anchor point of the inset map, default is 'SE'.
300+
provider : str, optional
301+
Add a backgroundmap if map provider is passed, default is 'nlmaps.water'. To
302+
turn off the backgroundmap set provider to None.
303+
add_to_plot : list of functions, optional
304+
List of functions to plot on the inset map, default is None. The functions
305+
must accept an ax argument. Hint: use `functools.partial` to set plot style,
306+
and pass the partial function to add_to_plot.
307+
308+
Returns
309+
-------
310+
mapax : matplotlib.Axes
311+
The inset map axes.
312+
"""
313+
mapax = ax.inset_axes(axes_bounds)
314+
mapax.axis(extent)
315+
mapax.set_aspect("equal", adjustable="box", anchor=anchor)
316+
mapax.set_xticks([])
317+
mapax.set_yticks([])
318+
mapax.set_xlabel("")
319+
mapax.set_ylabel("")
320+
321+
if provider:
322+
add_background_map(mapax, map_provider=provider, attribution=False)
323+
324+
if add_to_plot:
325+
for fplot in add_to_plot:
326+
fplot(ax=mapax)
327+
328+
return mapax
329+
330+
331+
def add_xsec_line_and_labels(
332+
line: Union[list, LineString],
333+
ax: plt.Axes,
334+
mapax: plt.Axes,
335+
x_offset: float = 0.0,
336+
y_offset: float = 0.0,
337+
label: str = "A",
338+
**kwargs,
339+
):
340+
"""Add a cross-section line to an overview map and label the start and end points.
341+
342+
Parameters
343+
----------
344+
line : list or shapely LineString
345+
The line to plot.
346+
ax : matplotlib.Axes
347+
The axes to plot the labels on.
348+
mapax : matplotlib.Axes
349+
The axes of the overview map to plot the line on.
350+
x_offset : float, optional
351+
The x offset of the labels, default is 0.0.
352+
y_offset : float, optional
353+
The y offset of the labels, default is 0.0.
354+
kwargs : dict
355+
Keyword arguments to pass to the line plot function.
356+
357+
Raises
358+
------
359+
ValueError
360+
If the line is not a list or a shapely LineString.
361+
"""
362+
if isinstance(line, list):
363+
x, y = np.array(line).T
364+
elif isinstance(line, LineString):
365+
x, y = line.xy
366+
else:
367+
raise ValueError("line should be a list or a shapely LineString")
368+
mapax.plot(x, y, **kwargs)
369+
stroke = [patheffects.withStroke(linewidth=2, foreground="w")]
370+
mapax.text(
371+
x[0] + x_offset,
372+
y[0] + y_offset,
373+
f"{label}",
374+
fontweight="bold",
375+
path_effects=stroke,
376+
fontsize=7,
377+
)
378+
mapax.text(
379+
x[-1] + x_offset,
380+
y[-1] + y_offset,
381+
f"{label}'",
382+
fontweight="bold",
383+
path_effects=stroke,
384+
fontsize=7,
385+
)
386+
ax.text(
387+
0.01,
388+
0.99,
389+
f"{label}",
390+
transform=ax.transAxes,
391+
path_effects=stroke,
392+
fontsize=14,
393+
ha="left",
394+
va="top",
395+
fontweight="bold",
396+
)
397+
ax.text(
398+
0.99,
399+
0.99,
400+
f"{label}'",
401+
transform=ax.transAxes,
402+
path_effects=stroke,
403+
fontsize=14,
404+
ha="right",
405+
va="top",
406+
fontweight="bold",
407+
)

tests/test_011_dcs.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import util
22

33
import nlmod
4+
import matplotlib.pyplot as plt
45

56

67
def test_dcs_structured():
@@ -21,3 +22,16 @@ def test_dcs_vertex():
2122
dcs.label_layers()
2223
dcs.plot_array(ds["kh"], alpha=0.5)
2324
dcs.plot_grid(vertical=False)
25+
26+
27+
def test_cross_section_utils():
28+
ds = util.get_ds_vertex()
29+
line = [(0, 0), (1000, 1000)]
30+
_, ax = plt.subplots(1, 1, figsize=(10, 4))
31+
dcs = nlmod.plot.DatasetCrossSection(ds, line, ax=ax)
32+
dcs.plot_layers()
33+
dcs.label_layers()
34+
dcs.plot_array(ds["kh"], alpha=0.5)
35+
dcs.plot_grid(vertical=False)
36+
mapax = nlmod.plot.inset_map(ax, ds.extent, provider=None)
37+
nlmod.plot.add_xsec_line_and_labels(line, ax, mapax)

0 commit comments

Comments
 (0)