Fill OUTSIDE of polygon | Mask array where indicies are beyond a circular boundary?

Viewed 10349

I use plot(x,y,'r') to plot a red circle. x and y are arrays such that when paired as (x,y) and plotted, all points form a circle-line.

fill(x,y,'r') plots a red circle that is filled in (or colored in) red.

How can I keep the circle white on the inside, but fill outside the circle out to the axis boundaries?

I looked into using fill_between(x_array, y1_array, y2_array, where) but after a little playing with it I don’t think that will work for my x,y arrays. I thought to fill_between() outside the circle, and inside a square that is defined by the axis boundaries, but I don’t think fill_between() is capable… I’m sure I could make it into an integral type of problem with delta x and delta y going to zero, but I’m not willing to.

If anyone can see that I’m missing something with fill_between() please let me know.

All I am really needing to do is mask out numbers in a 2d array that are located beyond this boundary of the circle created with x and y, such that when the 2D array is viewed as a color plot, or contour, inside the circle will be the image, and outside will be white-ed out.

Can this be accomplished by a masking technique of the 2D array instead? Like by using masked_where() ? I haven’t looked into it yet, but will.

Any ideas? Thanks

Edit 1: Here is what i have permission to show that I think will explain my problem.

from pylab import *
from matplotlib.path import Path
from matplotlib.patches import PathPatch

f=Figure()
a=f.add_subplot(111)

# x,y,z are 2d arrays

# sometimes i plot a color plot
# im = a.pcolor(x,y,z)
a.pcolor(x,y,z)

# sometimes i plot a contour
a.contour(x,y,z)

# sometimes i plot both using a.hold(True)

# here is the masking part.
# sometimes i just want to see the boundary drawn without masking
# sometimes i want to see the boundary drawn with masking inside of the boundary
# sometimes i want to see the boundary drawn with masking outside of the boundary

# depending on the vectors that define x_bound and y_bound, sometimes the boundary
# is a circle, sometimes it is not.

path=Path(vpath)
patch=PathPatch(path,facecolor='none')
a.add_patch(patch) # just plots boundary if anything has been previously plotted on a
if ('I want to mask inside'):
    patch.set_facecolor('white') # masks(whitens) inside if pcolor is currently on a,
    # but if contour is on a, the contour part is not whitened out. 
else: # i want to mask outside 
    im.set_clip_path(patch) # masks outside only when im = a.pcolor(x,y,z)
    # the following commands don't update any masking but they don't produce errors?
    # patch.set_clip_on(True)
    # a.set_clip_on(True)
    # a.set_clip_path(patch)

a.show()
3 Answers

Since this is the first result that comes up when googling matplotlib fill outside, I'll answer what has been proposed in the title.

Background

From what I understand matplotlib does not provide a function to fill the area outside a polygon, but only inside it with Axes.fill. If we create a bigger "outside" polygon that contains the smaller one, and join the two in a matter that does not create any intersections, it's possible to "fool" matplotlib into thinking the inside polygon is a crevice of the outside polygon. If the outside polygon is kept outside the view limits, then this will have the effect of filling the entire space outside the inner polygon.

polygon setup

One thing to keep in mind is the orientation of the outside polygon, because the channel connecting to the outside should not intersect itself. For this the orientation of the outer poly should be the opposite of the inner poly.

self intersecting path

Solution

The following function finds the points of the inner poly closest to the lower left corner and inserts there the path for the outer poly, taking care of the orientation using the signed area of the parallelogram created by the vectors for the splice point and the next one.

import numpy as np

def concat(*arrs) -> np.ndarray:
    return np.concatenate(tuple(map(np.asarray, arrs)))

def insert_at(outer_arr, arr, n) -> np.ndarray:
    outer_arr = np.asarray(outer_arr)
    prev, post = np.split(outer_arr, (n,))
    return concat(prev, arr, post)

def cross2d(x1, y1, x2, y2):
    return x1*y2-x2*y1

def is_clockwise(x1, y1, x2, y2):
    cp = cross2d(x1, y1, x2, y2)
    return cp < 0 if cp != 0 else None

def fill_outside(x, y, ll, ur, counter_clockwise=None):
    """
    Creates a polygon where x and y form a crevice of an outer
    rectangle with lower left and upper right corners `ll` and `ur`
    respectively. If `counter_clockwise` is `None` then the orientation
    of the outer polygon will be guessed to be the opposite of the
    inner connecting points.
    """
    x = np.asarray(x)
    y = np.asarray(y)
    xmin, ymin = ll
    xmax, ymax = ur
    xmin, ymin = min(xmin, min(x)), min(ymin, min(y))
    xmax, ymax = max(xmax, max(x)), max(ymax, max(y))
    corners = np.array([
        [xmin, ymin],
        [xmin, ymax],
        [xmax, ymax],
        [xmax, ymin],
        [xmin, ymin],
    ])
    lower_left = corners[0]
    # Get closest point to splicing corner
    x_off, y_off = x-lower_left[0], y-lower_left[1]
    closest_n = (x_off**2+y_off**2).argmin()
    # Guess orientation
    p = [x_off[closest_n], y_off[closest_n]]
    try:
        pn = [x_off[closest_n+1], y_off[closest_n+1]]
    except IndexError:
        # wrap around if we're at the end of the array
        pn = [x_off[0], y_off[0]]
    if counter_clockwise is None:
        counter_clockwise = not is_clockwise(*p, *pn)
    corners = corners[::-1] if counter_clockwise else corners
    # Join the arrays
    corners = concat(np.array([[x[closest_n], y[closest_n]]]), corners)
    xs, ys = np.transpose(corners)
    return insert_at(x, xs, closest_n), insert_at(y, ys, closest_n)

Examples

Filling outside a simple triangle

import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(1, 2)
fig.set_figwidth(10)

x = [0, 1, 2]
y = [0, 1, 0]
ll, ur = (-.5, -.25), (2.5, 1.25)
x, y = fill_outside(x, y, ll, ur)
ax1.fill(x, y)
ax1.plot(x, y, c="C1")
ax2.fill(x, y)
ax2.set_xlim((ll[0], ur[0]))
ax2.set_ylim((ll[1], ur[1]))

Produces:

outside of triangle filled

Filling outside an arbitrary shape

import numpy as np

def concat(*arrs) -> np.ndarray:
    return np.concatenate(tuple(map(np.asarray, arrs)))

def z_eq_damping(damping, n=100):
    theta = np.arccos(damping)
    u = np.cos(theta)-np.sin(theta)*1j
    x = np.linspace(0, np.pi/u.imag, num=n)
    contour = np.exp(u*x)
    re, im = contour.real, contour.imag
    return concat(re, np.flip(re)), concat(im, np.flip(-im))

fig, (ax1, ax2) = plt.subplots(1, 2)
fig.set_figwidth(10)

x, y = z_eq_damping(.7)
ll, ur = (-1, -1), (1, 1)
x, y = fill_outside(x, y, ll, ur)
ax1.fill(x, y)
ax1.plot(x, y, c="C1")
ax2.fill(x, y)
ax2.set_xlim((ll[0], ur[0]))
ax2.set_ylim((ll[1], ur[1]))

Produces:

filled outside of shape

Related