Skip to content

Commit 05843ab

Browse files
authored
hatch functions (#123)
* hatch functions * update functions and add tests * changelog * better names * changelog: better wording * add example * add to README (docs)
1 parent 03497d2 commit 05843ab

File tree

8 files changed

+528
-0
lines changed

8 files changed

+528
-0
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717

1818
### Enhancements
1919

20+
- Added convinience functions to draw hatches and add stippling:
21+
22+
1. `mpu.hatch`: for regular axes
23+
2. `mpu.hatch_map`: for cartopy GeoAxes
24+
3. `mpu.hatch_map_global`: as 2. but also adds a cyclic point to the array
25+
26+
all three functions expect a 2D boolean `xr.DataArray` and a hatch pattern. Values that are `True` are hatched.
27+
2028
- Enable passing `AxesGrid` (from `mpl_toolkits.axes_grid1`) to `set_map_layout` ([#116](https://github.com/mathause/mplotutils/pull/116)).
2129
- Raise more informative error when a wrong type is passed to `set_map_layout` ([#121](https://github.com/mathause/mplotutils/pull/121)).
2230
- `set_map_layout` now raises an explicit error when the figure contains SubFigure ([#121](https://github.com/mathause/mplotutils/pull/121)).

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
> *helper functions for cartopy and matplotlib*
44
5+
## fix layout for cartopy axes and colorbars
6+
57
This package solves two main problems for plots with maps created with cartopy. Because these plots have a fixed aspect ratio (1) colorbars will extend beyond the visible axes and (2) the distance between individual subplots will seemingly be random.
68

9+
**subplots**
710

811
Without mplotutils | With mplotutils
912
:-------------------------:|:-------------------------:
@@ -22,6 +25,13 @@ Matplotlib's [axes_grid](https://matplotlib.org/stable/users/explain/toolkits/ax
2225

2326
The code to create the example can be found in [docs/example_axes_grid.py](docs/example_axes_grid.py).
2427

28+
## hatching
29+
30+
mplotutils (from version 0.6) includes helper functions to draw hatches and add stippling:
31+
32+
<img src="docs/example_hatch.png" alt="mplotutils hatching" width="350"/>
33+
34+
The code to create the example can be found in [docs/example_hatch.py](docs/example_hatch.py).
2535

2636
## Installation
2737

docs/example_hatch.png

175 KB
Loading

docs/example_hatch.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import cartopy.crs as ccrs
2+
import matplotlib.pyplot as plt
3+
import numpy as np
4+
import xarray as xr
5+
6+
import mplotutils as mpu
7+
8+
9+
def main():
10+
11+
# read example data
12+
air = xr.tutorial.open_dataset("air_temperature").air
13+
air = air.resample(time="d").mean()
14+
15+
mean, std = air.mean("time"), air.std("time")
16+
17+
anomaly = air.isel(time=3) - mean
18+
19+
# create plot
20+
f, ax = plt.subplots(subplot_kw={"projection": ccrs.PlateCarree()})
21+
22+
ax.coastlines()
23+
24+
h = anomaly.plot(ax=ax, transform=ccrs.PlateCarree(), add_colorbar=False)
25+
26+
# only for illustration purposes
27+
signif = np.abs(anomaly) >= std
28+
29+
mpu.hatch_map(signif, "///", ax=ax, color="0.2", label="significant", linewidth=0.5)
30+
31+
ax.legend()
32+
33+
# adjust the margins manually
34+
f.subplots_adjust(left=0.025, right=0.85, top=0.9, bottom=0.05)
35+
36+
# ensure the figure has the correct size
37+
mpu.set_map_layout(ax)
38+
39+
mpu.colorbar(h, ax)
40+
41+
ax.set_title("Temperature")
42+
43+
44+
if __name__ == "__main__":
45+
46+
main()
47+
48+
plt.savefig("example_hatch.png", dpi=200)

mplotutils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from . import _colorbar, cartopy_utils, colormaps
66
from ._colorbar import *
7+
from ._hatch import hatch, hatch_map, hatch_map_global
78
from ._savefig import autodraw
89
from .cartopy_utils import *
910
from .colormaps import *

mplotutils/_hatch.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import warnings
2+
3+
import cartopy.crs as ccrs
4+
import matplotlib as mpl
5+
import numpy as np
6+
import xarray as xr
7+
8+
import mplotutils as mpu
9+
10+
_HATCHES_PER_FIGURE = {}
11+
12+
13+
from mplotutils.mpl import _maybe_gca
14+
15+
16+
def hatch(da, hatch, *, ax=None, label=None, linewidth=None, color="0.1"):
17+
"""add hatch pattern to an axes
18+
19+
Parameters
20+
----------
21+
da : xr.DataArray
22+
DataArray with the hatch information, must be boolean 2D array. Data of value
23+
`True` is hatched.
24+
hatch : str
25+
Hatch pattern, one of: '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'.
26+
Hatching patterns can be repeated to increase the density.
27+
ax : matplotlib.axes, default: None
28+
Axes to draw the hatch on. If not given, uses the current axes or creates new
29+
axes.
30+
label : str
31+
label for a legend entry
32+
linewidth : float, default: 0.25
33+
Default thickness of the hatching. Note that only one linewidth per figure is
34+
supported (by matplotlib).
35+
color : matplotlib color, default: "0.1"
36+
Color of the hatch lines.
37+
38+
Returns
39+
-------
40+
`~.contour.QuadContourSet`
41+
42+
Notes
43+
-----
44+
Don't use this function to hatch levels of a contour plot - it's better to add
45+
hatches directly to `countourf`.
46+
"""
47+
48+
return _hatch(
49+
da,
50+
hatch,
51+
ax=ax,
52+
label=label,
53+
linewidth=linewidth,
54+
color=color,
55+
cyclic=False,
56+
transform=None,
57+
)
58+
59+
60+
def hatch_map(
61+
da, hatch, *, ax=None, label=None, linewidth=None, color="0.1", transform=None
62+
):
63+
"""add hatch pattern to a regional cartopy map
64+
65+
Parameters
66+
----------
67+
da : xr.DataArray
68+
DataArray with the hatch information, must be boolean 2D array. Data of value
69+
`True` is hatched.
70+
hatch : str
71+
Hatch pattern, one of: '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'.
72+
Hatching patterns can be repeated to increase the density.
73+
ax : matplotlib.axes, default: None
74+
Axes to draw the hatch on. If not given, uses the current axes or creates new
75+
axes.
76+
label : str
77+
label for a legend entry
78+
linewidth : float, default: 0.25
79+
Default thickness of the hatching. Note that only one linewidth per figure is
80+
supported (by matplotlib).
81+
color : matplotlib color, default: "0.1"
82+
Color of the hatch lines.
83+
transform : cartopy projection, optional
84+
Defines the transformation of the data. If None uses 'PlateCarree'.
85+
86+
Returns
87+
-------
88+
`~.contour.QuadContourSet`
89+
90+
Notes
91+
-----
92+
Don't use this function to hatch levels of a contour plot - it's better to add
93+
hatches directly to `countourf`.
94+
"""
95+
96+
if transform is None:
97+
transform = ccrs.PlateCarree()
98+
99+
return _hatch(
100+
da,
101+
hatch,
102+
ax=ax,
103+
label=label,
104+
linewidth=linewidth,
105+
color=color,
106+
cyclic=False,
107+
transform=transform,
108+
)
109+
110+
111+
def hatch_map_global(
112+
da, hatch, *, ax=None, label=None, linewidth=None, color="0.1", transform=None
113+
):
114+
"""add hatch pattern to a global cartopy map - adds a cyclic data point
115+
116+
Parameters
117+
----------
118+
da : xr.DataArray
119+
DataArray with the hatch information, must be boolean 2D array. Data of value
120+
`True` is hatched.
121+
hatch : str
122+
Hatch pattern, one of: '/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'.
123+
Hatching patterns can be repeated to increase the density.
124+
ax : matplotlib.axes, default: None
125+
Axes to draw the hatch on. If not given, uses the current axes or creates new
126+
axes.
127+
label : str
128+
label for a legend entry
129+
linewidth : float, default: 0.25
130+
Default thickness of the hatching. Note that only one linewidth per figure is
131+
supported (by matplotlib).
132+
color : matplotlib color, default: "0.1"
133+
Color of the hatch lines.
134+
transform : cartopy projection, optional
135+
Defines the transformation of the data. If None uses 'PlateCarree'.
136+
137+
Returns
138+
-------
139+
`~.contour.QuadContourSet`
140+
141+
Notes
142+
-----
143+
Don't use this function to hatch levels of a contour plot - it's better to add
144+
hatches directly to `countourf`.
145+
"""
146+
147+
if transform is None:
148+
transform = ccrs.PlateCarree()
149+
150+
return _hatch(
151+
da,
152+
hatch,
153+
ax=ax,
154+
label=label,
155+
linewidth=linewidth,
156+
color=color,
157+
cyclic=True,
158+
transform=transform,
159+
)
160+
161+
162+
def _hatch(
163+
da,
164+
hatch,
165+
*,
166+
ax=None,
167+
label=None,
168+
linewidth=None,
169+
color="0.1",
170+
cyclic=False,
171+
transform=None,
172+
):
173+
174+
if not isinstance(da, xr.DataArray):
175+
raise TypeError(f"Expected a xr.DataArray, got {type(da)}.")
176+
177+
if not np.issubdtype(da.dtype, bool):
178+
raise TypeError(f"Expected a boolean array, got {da.dtype}")
179+
180+
if da.ndim != 2:
181+
raise ValueError(f"Expected a 2D array, got {da.ndim=}")
182+
183+
if ax is None:
184+
ax = _maybe_gca()
185+
186+
fig = ax.figure
187+
188+
# only one linewidth is possible per figure (actually it is just read before
189+
# saving the figure, so the above is not 100 % correct)
190+
if linewidth is None:
191+
# only set linewidth if not yet set
192+
if not _HATCHES_PER_FIGURE.get(fig):
193+
mpl.rcParams["hatch.linewidth"] = 0.25
194+
else:
195+
if _HATCHES_PER_FIGURE.get(fig):
196+
warnings.warn(
197+
"Can only set one `linewidth` per figure. Overwriting previous value of"
198+
f" {_HATCHES_PER_FIGURE[fig]}."
199+
)
200+
201+
mpl.rcParams["hatch.linewidth"] = linewidth
202+
203+
_HATCHES_PER_FIGURE[fig] = linewidth
204+
205+
mpl.rcParams["hatch.color"] = color
206+
207+
if label is not None:
208+
# add an empty patch to generate a legend entry
209+
xy = np.full((0, 2), fill_value=np.nan)
210+
empty_legend_patch = mpl.patches.Polygon(
211+
xy,
212+
facecolor="none",
213+
ec="0.1",
214+
hatch=hatch,
215+
label=label,
216+
)
217+
218+
# NOTE: manually overwrites the private _hatch_color property - allows to have
219+
# different ec and hatch color (so the box of the legend is black)
220+
empty_legend_patch._hatch_color = mpl.colors.to_rgba(
221+
mpl.rcParams["hatch.color"]
222+
)
223+
ax.add_patch(empty_legend_patch)
224+
225+
if cyclic:
226+
_, lon_dim = da.dims
227+
da = mpu.cyclic_dataarray(da, lon_dim)
228+
229+
return da.plot.contourf(
230+
ax=ax,
231+
hatches=["", hatch],
232+
levels=[0, 0.5, 1],
233+
colors="none",
234+
extend="neither",
235+
transform=transform,
236+
add_colorbar=False,
237+
)

mplotutils/tests/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import contextlib
2+
import warnings
23

34
import matplotlib
45
import matplotlib.pyplot as plt
56
import pytest
67

78

9+
@contextlib.contextmanager
10+
def assert_no_warnings():
11+
12+
with warnings.catch_warnings(record=True) as record:
13+
yield
14+
assert len(record) == 0, "got unexpected warning(s)"
15+
16+
817
@contextlib.contextmanager
918
def figure_context(*args, **kwargs):
1019
fig = plt.figure(*args, **kwargs)

0 commit comments

Comments
 (0)