Is there a function in python to fill the area between two contourlines each given by different functions?

Question:

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)

Asked By: Novo

||

Answers:

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

coloring between contours

Answered By: JohanC

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

Plot

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.

Answered By: Jared
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.