Plot methods in nlmod

This notebook shows different methods of plotting data with nlmod.

There are many ways to plot data and it depends on the type of data and plot which of these method is the most convenient:

  • using nlmod.plot utilities

  • using flopy plot methods

  • using xarray plot methods

The default plot methods in nlmod use a model Dataset as input (this is an xarray Dataset with some required variables and attributes). These plotting methods are accessible through nlmod.plot.

Flopy contains its own plotting utilities and nlmod contains some wrapper functions that use flopy’s plotting utilities under the hood. These require a flopy modelgrid or model object. These plotting methods are accessible through nlmod.plot.flopy.

Finally, xarray also allows plotting of data with .plot(). This is used in a few cases in this notebook but for more detailed information, refer to the xarray documentation.

import os
import flopy
import pandas as pd
import geopandas as gpd
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
from shapely.geometry import LineString

import nlmod
from nlmod.plot import DatasetCrossSection
nlmod.util.get_color_logger("INFO")
nlmod.show_versions()
Python version     : 3.11.14
NumPy version      : 2.4.4
Xarray version     : 2026.4.0
Matplotlib version : 3.10.9
Flopy version      : 3.10.0

nlmod version      : 0.11.3dev

First we read a fully run model, from the notebook 09_schoonhoven.ipynb. Please run that notebook first.

model_name = "Schoonhoven"
model_ws = os.path.join("..", "examples", "09_schoonhoven")
fname_nc = os.path.join(model_ws, f"{model_name}.nc")
if not os.path.isfile(fname_nc):
    raise (
        Exception(
            f"{fname_nc} not found. Please run notebook 09_schoonhoven.ipynb in the 'examples' directory first"
        )
    )
ds = xr.open_dataset(fname_nc)
ds.attrs["model_ws"] = model_ws
ds["head"] = nlmod.gwf.output.get_heads_da(ds)

For the flopy plot-methods we need a modelgrid object. We generate this from the model Dataset using the method. nlmod.grid.modelgrid_from_ds().

modelgrid = nlmod.grid.modelgrid_from_ds(ds)
modelgrid
xll:0.0; yll:0.0; rotation:0.0; units:undefined; lenuni:0

Maps

We can plot variables on a map using nlmod.plot.data_array(). We can also use the PlotMapView-class from flopy, and plot an array using the plot_array method.

f, ax = nlmod.plot.get_map(ds.extent, ncols=2)

# plot using nlmod
pc = nlmod.plot.data_array(ds["top"], ds=ds, ax=ax[0])

# plot using flopy
pmv = flopy.plot.PlotMapView(modelgrid=modelgrid, ax=ax[1])
pmv.plot_array(ds["top"])
<matplotlib.collections.PathCollection at 0x791935fdefd0>
../_images/e1ac4e786b50207396d736bea746c9db9c4015c3557669703c8a68ae38d715ef.png

Cross-sections

We can also plot cross-sections, either with DatasetCrossSection in nlmod, or using the PlotCrossSection class of flopy.

y = (ds.extent[2] + ds.extent[3]) / 2 + 0.1
line = [(ds.extent[0], y), (ds.extent[1], y)]
zmin = -100.0
zmax = 10.0
f, ax = plt.subplots(figsize=(10, 5), nrows=2)

# plot using nlmod
dcs = DatasetCrossSection(ds, line=line, zmin=zmin, zmax=zmax, ax=ax[0])
dcs.plot_array(ds["kh"])

# plot using flopy
pcs = flopy.plot.PlotCrossSection(modelgrid=modelgrid, line={"line": line}, ax=ax[1])
pcs.plot_array(ds["kh"])
pcs.ax.set_ylim((zmin, zmax));
../_images/0e526628540ac327c74b32f917567e75c05615ca4e2640c0087c2c1775f7dfff.png

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.

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().

f, ax = plt.subplots(figsize=(10, 5))
dcs = DatasetCrossSection(ds, line=line, zmin=-200, zmax=10, ax=ax)
colors = nlmod.read.regis.get_legend()
dcs.plot_layers(colors=colors, min_label_area=1000)
dcs.plot_grid(vertical=False, linewidth=0.5)
mapax = nlmod.plot.inset_map(ax, ds.extent)
nlmod.plot.add_xsec_line_and_labels(line, ax, mapax)
../_images/dc96cb92ab5b0d7b4dac4f4afecdefd0a81dc3ab5aa076f1fe500038daf06389.png

Another feature of the DatasetCrossSection is the plot_wells method to plot the screen depths in a cross section.

# create data frame with well data (random)
nwells = 11
max_dist = 1000
df = pd.DataFrame(
    {
        "screen_top": np.random.randint(-80, -40, nwells),
        "screen_bottom": np.random.randint(-125, -81, nwells),
        "x": np.random.randint(ds.extent[0], ds.extent[1], nwells),
        "y": np.random.randint(ds.extent[2], ds.extent[3], nwells),
    },
    index=[f"well{i}" for i in range(nwells)],
)
cmap = plt.get_cmap("tab20")
df["filtercolor_face"] = [
    cmap(i)
    for i in np.linspace(0, int((len(df) - 1) * cmap.N / len(df)), len(df), dtype=int)
]
df['tubecolor'] = 'white'
gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.x, df.y))

# plot cross section
f, ax = plt.subplots(figsize=(10, 5))
dcs = DatasetCrossSection(ds, line=line, zmin=-200, zmax=10, ax=ax)
colors = nlmod.read.regis.get_legend()
dcs.plot_layers(colors=colors)

# plot wells in cross section
dcs.plot_wells(
    df,
    legend=True,
    max_dist=max_dist,
    filter_patch_kwargs={'alpha':0.5, 'zorder':10},
    filter_rect_kwargs={'alpha':0.5, 'zorder':10},
    tubeline_kwargs={'alpha':0.5},
    legend_kwds={"loc": (0, 1), "frameon": False, "ncol": 7},
)

# inset map
mapax = nlmod.plot.inset_map(ax, ds.extent)

# plot all wells in grey, wells within x meter of cross section in color
gdf.plot(ax=mapax, color="grey", edgecolor="grey", markersize=20)
gdf_plotted = gdf[gdf.geometry.distance(LineString(line)) <= max_dist]
gdf_plotted.plot(
    ax=mapax, color=gdf_plotted["filtercolor_face"], edgecolor="k", markersize=20
)

nlmod.plot.add_xsec_line_and_labels(line, ax, mapax)
INFO:nlmod.plot.dcs.plot_wells:plotting 7 of 11 wells within 1000m of cross section line
../_images/74b5424f3935b0299913b42f48de9f21e0fc2b9326997f056e2ee0684a9e18a2.png

Animation

There is also an option to make an animation of a cross section with variations in heads

# make animation of the heads over time
import matplotlib as mpl
import numpy as np

f, ax = plt.subplots(figsize=(15, 5))
dcs = DatasetCrossSection(ds, line=line, zmin=-3.0, zmax=0.0, ax=ax)

dcs.plot_grid(vertical=False, linewidth=0.5)

vmin = np.nanmin(ds["head"])
vmax = np.nanmax(ds["head"])
norm = mpl.colors.Normalize(vmin, vmax)

mapax = nlmod.plot.inset_map(ax, ds.extent)
nlmod.plot.add_xsec_line_and_labels(line, ax, mapax)

plt.close(f)
from IPython.display import HTML

HTML(dcs.animate(ds["head"], head=ds["head"], norm=norm).to_jshtml())
INFO:matplotlib.animation.save:Animation.save using <class 'matplotlib.animation.HTMLWriter'>

Time series

For time series we use the functionality of xarray, as we have read the heads in a xarray DataArray.

x = 118228
y = 439870
head_point = nlmod.gwf.get_head_at_point(ds["head"], x=x, y=y, ds=ds)
head_point.plot.line(hue="layer", size=10);
../_images/0a0ca7dcddc735b46ff4d48cc73d516f9ab0a60cd37d3de964493e88a7f3c60c.png

We can also use pandas to plot the heads. First transform the data to a Pandas DataFrame.

df = head_point.to_pandas()
df
layer HLc KRz2 KRz3 URz1 URz3 URz4 URz5 STz1 STz2 WAk1 ... MSk1 MSz2 MSk2 MSz3 MSc MSz4 OOk1 OOz2 OOc BRk1
time
2020-01-01 -0.957947 -1.006008 -1.006603 -1.007084 -1.007134 -1.007176 -1.007268 -1.007666 -1.009145 -1.120537 ... -1.301889 -1.316371 -1.328745 -1.341205 -1.345656 -1.347565 -1.348113 -1.348692 -1.348732 -1.348842
2020-02-01 -0.914923 -0.981083 -0.981899 -0.982524 -0.982590 -0.982645 -0.982766 -0.983298 -0.985218 -1.106103 ... -1.302429 -1.317737 -1.330846 -1.343955 -1.348611 -1.350580 -1.350940 -1.351332 -1.351234 -1.349408
2020-03-01 -0.659914 -0.832363 -0.834467 -0.835933 -0.836092 -0.836222 -0.836515 -0.837826 -0.842272 -1.003414 ... -1.260430 -1.282422 -1.304581 -1.326548 -1.336608 -1.342988 -1.345886 -1.348680 -1.349039 -1.349457
2020-04-01 -0.805188 -0.929385 -0.930902 -0.931979 -0.932095 -0.932190 -0.932404 -0.933352 -0.936605 -1.071525 ... -1.283991 -1.300225 -1.314677 -1.329284 -1.335261 -1.338557 -1.340212 -1.341861 -1.342353 -1.347934
2020-05-01 -1.161582 -1.161145 -1.161152 -1.161250 -1.161259 -1.161266 -1.161281 -1.161328 -1.161697 -1.268931 ... -1.439606 -1.447577 -1.448729 -1.450768 -1.447017 -1.441591 -1.435256 -1.429745 -1.425309 -1.364815
2020-06-01 -1.503472 -1.414804 -1.413745 -1.413158 -1.413093 -1.413039 -1.412915 -1.412337 -1.410762 -1.512638 ... -1.674545 -1.677103 -1.667519 -1.659432 -1.646973 -1.633108 -1.618476 -1.605367 -1.594154 -1.416112
2020-07-01 -1.345397 -1.279946 -1.279170 -1.278781 -1.278737 -1.278700 -1.278617 -1.278219 -1.277240 -1.399996 ... -1.614650 -1.632484 -1.645581 -1.658485 -1.660325 -1.658084 -1.652132 -1.646394 -1.638251 -1.464797
2020-08-01 -1.434398 -1.352806 -1.351835 -1.351320 -1.351262 -1.351214 -1.351105 -1.350591 -1.349249 -1.469032 ... -1.675478 -1.690319 -1.698692 -1.707483 -1.706752 -1.702520 -1.695655 -1.689107 -1.681152 -1.513528
2020-09-01 -1.515144 -1.420623 -1.419496 -1.418882 -1.418814 -1.418757 -1.418629 -1.418026 -1.416423 -1.540322 ... -1.752592 -1.766465 -1.772344 -1.778708 -1.775826 -1.769509 -1.760824 -1.752634 -1.743749 -1.565402
2020-10-01 -1.371747 -1.307378 -1.306617 -1.306247 -1.306205 -1.306170 -1.306091 -1.305711 -1.304824 -1.443567 ... -1.692621 -1.717324 -1.739642 -1.761739 -1.769517 -1.772258 -1.769981 -1.767709 -1.761261 -1.608667
2020-11-01 -0.977837 -1.015943 -1.016422 -1.016849 -1.016894 -1.016931 -1.017013 -1.017364 -1.018768 -1.164169 ... -1.431944 -1.468057 -1.510755 -1.551875 -1.575159 -1.592199 -1.603739 -1.613868 -1.617194 -1.611243
2020-12-01 -0.911076 -0.980114 -0.980966 -0.981622 -0.981692 -0.981750 -0.981878 -0.982440 -0.984482 -1.119525 ... -1.363743 -1.396732 -1.436432 -1.475783 -1.498516 -1.516178 -1.528365 -1.539948 -1.544617 -1.597339
2021-01-01 -0.768088 -0.897691 -0.899276 -0.900403 -0.900525 -0.900625 -0.900849 -0.901848 -0.905284 -1.053087 ... -1.301803 -1.329165 -1.360352 -1.391044 -1.408559 -1.422020 -1.432837 -1.442916 -1.450063 -1.564655
2021-02-01 -0.701461 -0.864818 -0.866810 -0.868201 -0.868351 -0.868474 -0.868752 -0.869991 -0.874197 -1.028305 ... -1.279398 -1.304578 -1.332294 -1.359937 -1.374798 -1.385938 -1.394200 -1.402157 -1.408433 -1.529761
2021-03-01 -0.834969 -0.947792 -0.949171 -0.950158 -0.950264 -0.950351 -0.950546 -0.951412 -0.954399 -1.087093 ... -1.302417 -1.321884 -1.341475 -1.361198 -1.371129 -1.378234 -1.383832 -1.389409 -1.394420 -1.501698
2021-04-01 -0.974677 -1.026270 -1.026908 -1.027413 -1.027467 -1.027510 -1.027608 -1.028028 -1.029579 -1.143256 ... -1.330733 -1.346440 -1.360505 -1.374713 -1.380721 -1.384287 -1.386987 -1.389748 -1.393274 -1.477182
2021-05-01 -1.081399 -1.085412 -1.085474 -1.085607 -1.085620 -1.085630 -1.085652 -1.085729 -1.086177 -1.187514 ... -1.358051 -1.371703 -1.382614 -1.393666 -1.397310 -1.398690 -1.399493 -1.400443 -1.402573 -1.460620
2021-06-01 -1.023329 -1.041391 -1.041624 -1.041872 -1.041897 -1.041918 -1.041963 -1.042146 -1.042946 -1.151660 ... -1.338917 -1.356604 -1.373514 -1.390322 -1.397446 -1.401585 -1.403649 -1.405729 -1.407428 -1.448520
2021-07-01 -1.160977 -1.134512 -1.134205 -1.134102 -1.134089 -1.134078 -1.134052 -1.133910 -1.133665 -1.229005 ... -1.390169 -1.401806 -1.409410 -1.417231 -1.418454 -1.417577 -1.416656 -1.415949 -1.416579 -1.441311
2021-08-01 -1.223998 -1.175228 -1.174651 -1.174378 -1.174346 -1.174320 -1.174259 -1.173961 -1.173228 -1.269160 ... -1.433073 -1.444486 -1.451038 -1.457818 -1.457707 -1.455337 -1.452302 -1.449555 -1.448420 -1.442697
2021-09-01 -1.240530 -1.185914 -1.185268 -1.184951 -1.184915 -1.184885 -1.184815 -1.184478 -1.183626 -1.280822 ... -1.446823 -1.458737 -1.465815 -1.472965 -1.473216 -1.471057 -1.468389 -1.465874 -1.464592 -1.447458
2021-10-01 -1.295006 -1.229308 -1.228528 -1.228127 -1.228082 -1.228044 -1.227958 -1.227544 -1.226466 -1.328161 ... -1.502195 -1.513761 -1.519465 -1.525488 -1.524184 -1.520393 -1.515550 -1.511085 -1.507948 -1.460576
2021-11-01 -0.893436 -0.957834 -0.958629 -0.959251 -0.959317 -0.959371 -0.959492 -0.960022 -0.961960 -1.092640 ... -1.316208 -1.340516 -1.366831 -1.392124 -1.405368 -1.414423 -1.420751 -1.426312 -1.429162 -1.453674
2021-12-01 -0.804747 -0.917973 -0.919359 -0.920359 -0.920467 -0.920555 -0.920753 -0.921633 -0.924688 -1.067005 ... -1.304513 -1.328506 -1.354312 -1.380096 -1.393187 -1.402455 -1.408001 -1.413453 -1.415484 -1.445569
2022-01-01 -0.830589 -0.938704 -0.940027 -0.940982 -0.941084 -0.941168 -0.941357 -0.942194 -0.945097 -1.080482 ... -1.304119 -1.326116 -1.349249 -1.372454 -1.384086 -1.392260 -1.397267 -1.402225 -1.404228 -1.436494
2022-02-01 -0.837215 -0.942144 -0.943429 -0.944357 -0.944457 -0.944539 -0.944722 -0.945535 -0.948360 -1.082475 ... -1.304157 -1.325715 -1.348198 -1.370793 -1.381971 -1.389751 -1.394403 -1.399088 -1.400757 -1.428629
2022-03-01 -0.660133 -0.836946 -0.839101 -0.840599 -0.840761 -0.840894 -0.841193 -0.842532 -0.847060 -1.006587 ... -1.262663 -1.286691 -1.312348 -1.337831 -1.350913 -1.360212 -1.366247 -1.372059 -1.374843 -1.417625
2022-04-01 -0.975566 -1.035897 -1.036641 -1.037214 -1.037275 -1.037324 -1.037435 -1.037918 -1.039667 -1.155751 ... -1.341062 -1.353507 -1.362307 -1.371433 -1.373571 -1.373676 -1.373616 -1.373666 -1.374900 -1.407957
2022-05-01 -1.149343 -1.137589 -1.137460 -1.137471 -1.137471 -1.137470 -1.137467 -1.137432 -1.137530 -1.239628 ... -1.406748 -1.416784 -1.421420 -1.426453 -1.425317 -1.422215 -1.418738 -1.415659 -1.414376 -1.409209
2022-06-01 -1.169768 -1.140152 -1.139808 -1.139684 -1.139669 -1.139656 -1.139626 -1.139466 -1.139173 -1.240534 ... -1.410934 -1.422607 -1.429511 -1.436393 -1.436459 -1.434151 -1.431336 -1.428616 -1.427468 -1.413156
2022-07-01 -1.154427 -1.125774 -1.125441 -1.125329 -1.125315 -1.125303 -1.125274 -1.125124 -1.124868 -1.228774 ... -1.407296 -1.421236 -1.431515 -1.441694 -1.443797 -1.443147 -1.441197 -1.439300 -1.438091 -1.418543
2022-08-01 -1.444842 -1.353508 -1.352417 -1.351812 -1.351744 -1.351688 -1.351560 -1.350961 -1.349298 -1.444453 ... -1.601975 -1.608001 -1.604752 -1.602848 -1.595399 -1.586224 -1.576049 -1.566974 -1.559655 -1.450091
2022-09-01 -1.630578 -1.507630 -1.506157 -1.505312 -1.505218 -1.505141 -1.504965 -1.504151 -1.501857 -1.608462 ... -1.784978 -1.790426 -1.783888 -1.778556 -1.767335 -1.753997 -1.739244 -1.725811 -1.713775 -1.509146
2022-10-01 -1.081028 -1.077812 -1.077791 -1.077898 -1.077908 -1.077916 -1.077933 -1.077988 -1.078450 -1.216718 ... -1.474432 -1.509090 -1.548900 -1.587128 -1.607536 -1.621634 -1.629376 -1.636270 -1.634665 -1.537075
2022-11-01 -1.093442 -1.092550 -1.092555 -1.092666 -1.092677 -1.092685 -1.092702 -1.092759 -1.093186 -1.213292 ... -1.432403 -1.459682 -1.490211 -1.520444 -1.536628 -1.548294 -1.555678 -1.562480 -1.563695 -1.543609
2022-12-01 -0.873736 -0.952601 -0.953571 -0.954305 -0.954383 -0.954447 -0.954591 -0.955224 -0.957491 -1.092664 ... -1.328597 -1.357099 -1.389946 -1.422163 -1.440304 -1.453932 -1.463779 -1.472870 -1.477420 -1.529612
2023-01-01 -0.716005 -0.867841 -0.869694 -0.870997 -0.871138 -0.871254 -0.871513 -0.872673 -0.876630 -1.030746 ... -1.285622 -1.312422 -1.342546 -1.372437 -1.388942 -1.401417 -1.410521 -1.419244 -1.424304 -1.506350

37 rows × 26 columns

And then plot this DataFrame.

df.plot(figsize=(10, 10));
../_images/2263604e96663929047018c81c23cccca98425cd874c085ccb0baea3ad84df70.png