Reputation: 123
If I have two contourlines given by
plt.contourf(xx, yy, zzmax, levels=[1], colors='r', alpha=0.8)
plt.contourf(xx, yy, zzmin, levels=[1], colors='r', alpha=0.8)
how do I plot a domain which fills the area between them?
(Sorry if this is a noob question)
Upvotes: 2
Views: 1216
Reputation: 98
As far as I can tell, the currently accepted answer suffers from the shortcoming that it doesn't interpolate well between data points. This is significant if e.g. this filled area represents the error bars on a contour line, which I suspect is the intention from the formulation given in the OP: using the other answer, when the size of the filled area is comparable to the spacing of data points, it sometimes vanishes where it shouldn't and has jagged edges.
Luckily, the library contourpy
allows us to get the contours directly - and this is the library that the most recent versions of matplotlib
use under the hood (there are also options to use legacy algorithms). Because this is a dependency of matplotlib
, there is nothing new to install.
We can then use matplotlib.fill
to fill the polygon between these two contours.
Here's my implementation (borrowing heavily off of @JohanC's answer for the setup), where I have data zz and error on the data zz_err. I then want to plot the contour where zz = 0, and the error bars on it (which is the region where 0 falls between zz - zz_err and zz + zz_err):
from matplotlib import pyplot as plt
from matplotlib.colors import ListedColormap
import contourpy
import numpy as np
from scipy.ndimage import gaussian_filter
xx = np.linspace(0, 10, 100)
yy = np.linspace(0, 8, 80)
np.random.seed(3153822019)
zz = gaussian_filter(np.random.randn(len(yy), len(xx)) + (yy - 4)[:, np.newaxis] + 0.1 * (xx - 5) * (xx - 5), 8)
zz_err = 0.8 + gaussian_filter(np.random.randn(len(yy), len(xx)) * 5, 8)
zzmin = zz - zz_err
zzmax = zz + zz_err
fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(15, 4))
cnt1 = ax1.contourf(xx, yy, zz, cmap='RdYlGn')
plt.colorbar(cnt1, ax=ax1)
ax1.contour(xx, yy, zz, levels=[0], colors='skyblue', linewidths=3)
ax1.set_title('zz')
cnt2 = ax2.contourf(xx, yy, zz_err, cmap='Reds')
plt.colorbar(cnt2, ax=ax2)
ax2.set_title('zz_err')
cnt3 = ax3.contourf(xx, yy, zz, cmap='RdYlGn')
ax3.contour(xx, yy, zz, levels=[0], colors='skyblue', linewidths=3)
################################################################################
# ACTUAL SOLUTION STARTS HERE
# Create a "contour generator" using both ends of the confidence interval as the z-values.
contour_generator_minus = contourpy.contour_generator(xx, yy, zzmin)
contour_generator_plus = contourpy.contour_generator(xx, yy, zzmax)
# Get the contour lines for when either end of the confidence interval is zero.
# Each is an (Nx2) array containing N 2D points.
lines_minus = contour_generator_minus.lines(0)
lines_plus = contour_generator_plus.lines(0)
# Iterate over the lines - if we have multiple disjoint contours for the same value, this ensures the code still works.
for line_minus, line_plus in zip(lines_minus, lines_plus):
# Construct a polygon from the two contour lines
polygon = np.concatenate([line_minus, line_plus[::-1]], axis=0)
ax3.fill(polygon[:, 0], polygon[:, 1], fc="skyblue", alpha=0.5, ec=None)
# ACTUAL SOLUTION ENDS HERE
################################################################################
plt.colorbar(cnt3, ax=ax3)
ax3.set_title('zz = 0 with error bars')
plt.tight_layout()
plt.show()
If you have closed loops, this might be more complicated, though I think it should work -- you might need to append the first point in each line onto the end before you construct the polygon. This might also fail if you have very complicated data, and so the set of contours you're trying to fill between aren't topologically the same.
Upvotes: 1
Reputation: 80459
The following code first creates some test data. The blue lines indicate where zzmax
and zzmin
are equal to 1
. The subplot at the right shows in red the region where both zzmax
is smaller than 1 and zzmin
is larger than 1.
from matplotlib import pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np
from scipy.ndimage import gaussian_filter
xx = np.linspace(0, 10, 100)
yy = np.linspace(0, 8, 80)
np.random.seed(11235813)
zzmax = gaussian_filter(np.random.randn(len(yy), len(xx)) * 10 + 1, 8)
zzmin = gaussian_filter(np.random.randn(len(yy), len(xx)) * 10 + 0.9, 8)
fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, figsize=(15, 4))
cnt1 = ax1.contourf(xx, yy, zzmax, cmap='RdYlGn')
plt.colorbar(cnt1, ax=ax1)
ax1.contour(xx, yy, zzmax, levels=[1], colors='skyblue', linewidths=3)
ax1.set_title('zzmax')
cnt2 = ax2.contourf(xx, yy, zzmin, cmap='RdYlGn')
plt.colorbar(cnt2, ax=ax2)
ax2.contour(xx, yy, zzmin, levels=[1], colors='skyblue', linewidths=3)
ax2.set_title('zzmin')
ax3.contourf(xx, yy, (zzmax <= 1) & (zzmin >= 1), levels=[0.5, 2], cmap=ListedColormap(['red']), alpha=0.3)
ax3.set_title('zzmax ≤ 1 and zmin ≥ 1')
plt.tight_layout()
plt.show()
Upvotes: 2