Filling complements of areas with matplotlib - python

I'm currently implementing something with Python and matplotlib. I know how to draw polygons and also how to fill them, but how do I fill everything except the interior of a polygon? To be clearer, I'd like to modify the result below, obtained using axhspan's and axvspan's, by clipping the horizontal and vertical red lines so as to obtain a red rectangle (outside which everything is hatched as it is now):

This post asks (and answers) essentially this question. Look at 'Edit 2' in the accepted answer. It describes how to create a vector polygon the size of your plot bounds and then how to create a hole in it to match the shape you want to complement. It does this by assigning line codes that define whether or not the pen draws when it moves.
Here is the portion of the above-referenced post that is relevant to this question:
import numpy as np
import matplotlib.pyplot as plt
def main():
# Contour some regular (fake) data
grid = np.arange(100).reshape((10,10))
plt.contourf(grid)
# Verticies of the clipping polygon in counter-clockwise order
# (A triange, in this case)
poly_verts = [(2, 2), (5, 2.5), (6, 8), (2, 2)]
mask_outside_polygon(poly_verts)
plt.show()
def mask_outside_polygon(poly_verts, ax=None):
"""
Plots a mask on the specified axis ("ax", defaults to plt.gca()) such that
all areas outside of the polygon specified by "poly_verts" are masked.
"poly_verts" must be a list of tuples of the verticies in the polygon in
counter-clockwise order.
Returns the matplotlib.patches.PathPatch instance plotted on the figure.
"""
import matplotlib.patches as mpatches
import matplotlib.path as mpath
if ax is None:
ax = plt.gca()
# Get current plot limits
xlim = ax.get_xlim()
ylim = ax.get_ylim()
# Verticies of the plot boundaries in clockwise order
bound_verts = [(xlim[0], ylim[0]), (xlim[0], ylim[1]),
(xlim[1], ylim[1]), (xlim[1], ylim[0]),
(xlim[0], ylim[0])]
# A series of codes (1 and 2) to tell matplotlib whether to draw a line or
# move the "pen" (So that there's no connecting line)
bound_codes = [mpath.Path.MOVETO] + (len(bound_verts) - 1) * [mpath.Path.LINETO]
poly_codes = [mpath.Path.MOVETO] + (len(poly_verts) - 1) * [mpath.Path.LINETO]
# Plot the masking patch
path = mpath.Path(bound_verts + poly_verts, bound_codes + poly_codes)
patch = mpatches.PathPatch(path, facecolor='white', edgecolor='none')
patch = ax.add_patch(patch)
# Reset the plot limits to their original extents
ax.set_xlim(xlim)
ax.set_ylim(ylim)
return patch
if __name__ == '__main__':
main()

If you only need the complement of a rectangle, you could instead draw 4 rectangles around it (like the 4 rectangles that are visible in your example image). The coordinates of the plot edges can be obtained with xlim() and ylim().
I am not sure that Matplotlib offers a way of painting the outside of a polygon…

Related

Trouble using Insetposition for Inset Axes with Cartopy

I want to create an inset map on a map using Cartopy. I'd like to specify the x & y inset location positions as a function of the parent axes so that the child axes are always inside the parent. For example, 0,0 aligns the inset axes at the bottom left and 1,1 aligns the inset axes at the top right, but in both cases the inset plot is inside the parent. I've achieved this using the following:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
import cartopy.crs as ccrs
ax = plt.axes(projection=ccrs.PlateCarree(), label='1')
ax.coastlines()
iax = plt.axes([0, 0, 1, 1], projection=ccrs.PlateCarree(), label='2')
iax.coastlines()
size = .5
inset_x = 1
inset_y = 1
left = inset_x - inset_x*size
bottom = inset_y - inset_y*size
ip = InsetPosition(ax, [left, bottom, size, size]) #posx, posy, width, height
iax.set_axes_locator(ip)
working fine when extents not set
The problem is if I apply new extents to the inset map. Depending on the aspect ratio of the newly set extents, my x or y inset position translates to positions well inside the parent axes - a 0-y results in the inset being off the bottom and a 1-y results it being offset at the top. When this happens, the offset is symmetric and only occurs in one of the position axes, the other one behaves as desired. I've tried using get_position, but that doesn't seem intuitive when using Cartopy because the bbox returned does not reflect the aspect ratio of the plot extents. For example, adding this before applying the InsetPosition:
# extent=[-20,60,40,65] # this breaks y positioning
extent=[-20,60,0,65] # this breaks x positioning
iax.set_extent(extent, crs=ccrs.PlateCarree())
not working as expected
I can manually adjust them to where I want but the correction doesn't match differences in bbox height/width or any other value I've thought to check. Any suggestions?
If I change the left and bottom locations to:
left = inset_x - size/2
bottom = inset_y - size/2
That always works consistently, regardless of the extents set, but it puts the inset map overlapping the corner.
works consistently but not desired results
Additional note - the same behavior can be found if you use ordinary (non-GeoAxes) plots and change the aspect of the inset using set_aspect. I still haven't figured out the bbox size relationships (parent and inset) between the pre- and post-aspect change and how it impacts the specific inset placement.
#Hagbeard,I found the similar question the similar question, and I have further refined #gepcel's code so that it can better meet your needs.
Here is the code.
import matplotlib.pyplot as plt
#from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
import cartopy.crs as ccrs
ax = plt.axes(projection=ccrs.PlateCarree(), label='1')
ax.coastlines()
size = .2 # Figure Standardized coordinates (0~1)
#GeoAxes has a width/height ratio according to self's projection
#Therefore, Here only set a same width and height
iax = plt.axes([0, 0, size, size], projection=ccrs.PlateCarree(), label='2')
iax.coastlines()
extent=[-20,60,40,65]
#extent=[-20,60,0,65]
iax.set_extent(extent, crs=ccrs.PlateCarree())
def set_subplot2corner(ax,ax_sub,corner="bottomright"):
ax.get_figure().canvas.draw()
p1 = ax.get_position()
p2 = ax_sub.get_position()
if corner == "topright":
ax_sub.set_position([p1.x1-p2.width, p1.y1-p2.height, p2.width, p2.height])
if corner == "bottomright":
ax_sub.set_position([p1.x1-p2.width, p1.y0, p2.width, p2.height])
if corner == "bottomleft":
ax_sub.set_position([p1.x0, p1.y0, p2.width, p2.height])
if corner == "topleft":
ax_sub.set_position([p1.x0, p1.y1-p2.height, p2.width, p2.height])
set_subplot2corner(ax,iax,corner="topright")
# Do not support a interactive zoom in and out
# so plz save the plot as a static figure
plt.savefig("corner_subfig.png",bbox_inches="tight")
#plt.show()
And the final plot.

Can't draw circle with right proportions Matplotlib Python

I want to draw Circle on my plot. For this purpose I decided to use patch.Circle class from matplotlib. Cirlce object uses radius argument to set a radius of a circle, but if the axes ratio is not 1 (see my plot), how to draw circle with right proportions?
My code for drawing circle is:
rect = patches.Circle(xy=(9, yaxes),radius= 2, linewidth=3, edgecolor='r', facecolor='red',alpha=0.5)
ax.add_patch(rect)
yaxes is equal 206 in this example (because I wanted to draw it upper left coner).
Here is a picture I got using this code:
But I want something like this:
You could use ax.transData to transform 1,1 vs 0,0 and obtain the deformation in x vs y direction. That ratio can be used to know the horizontal versus the vertical size of the circle.
If you just need to place a circle using coordinates relative to the axes, plt.scatter with transform=ax.transAxes can be used. Note that the size is an "area" measure based on "points" (a "point" is 1/72th of an inch).
The following example code uses the data coordinates to position the "circle" (using an ellipse) and the x-coordinates for the radius. A red circle is placed using axes coordinates.
from matplotlib import pyplot as plt
from matplotlib.patches import Ellipse
import pandas as pd
import numpy as np
# plot some random data
np.random.seed(2021)
df = pd.DataFrame({'y': np.random.normal(10, 100, 50).cumsum() + 2000},
index=np.arange(101, 151))
ax = df.plot(figsize=(12, 5))
# find an "interesting" point
max_ind = df['y'].argmax()
max_x = df.index[max_ind]
max_y = df.iloc[max_ind]['y']
# calculate the aspect ratio
xscale, yscale = ax.transData.transform([1, 1]) - ax.transData.transform([0, 0])
# draw the ellipse to be displayed as circle
radius_x = 4
radius_y = radius_x * xscale / yscale
ax.add_patch(Ellipse((max_x, max_y), radius_x, radius_y, color='purple', alpha=0.4))
# use ax.scatter to draw a red dot at the top left
ax.scatter(0.05, 0.9, marker='o', s=2000, color='red', transform=ax.transAxes)
plt.show()
Some remarks about drawing the ellipse:
this will only work for linear coordinates, not e.g. for logscale or polar coordinates
the code supposes nor the axis limits nor the axis position will change afterwards, as these will distort the aspect ratio
The issue seems to be that your X (passed to xy=) is not always the same as your Y, thus the oval instead of a perfect circle.

What is the correct matplotlib transform for a "virtual third axis" in my waterfall plot?

While working on improving my answer to this question, I have stumbled into a dead end.
What I want to achieve, is create a "fake" 3D waterfall plot in matplotlib, where individual line plots (or potentially any other plot type) are offset in figure pixel coordinates and plotted behind each other. This part works fine already, and using my code example (see below) you should be able to plot ten equivalent lines which are offset by fig.dpi/10. in x- and y-direction, and plotted behind each other via zorder.
Note that I also added fill_between()'s to make the "depth-cue" zorder more visible.
Where I'm stuck is that I'd like to add a "third axis", i.e. a line (later on perhaps formatted with some ticks) which aligns correctly with the base (i.e. [0,0] in data units) of each line.
This problem is perhaps further complicated by the fact that this isn't a one-off thing (i.e. the solutions should not only work in static pixel coordinates), but has to behave correctly on rescale, especially when working interactively.
As you can see, setting e.g. the xlim's allows one to rescale the lines "as expected" (best if you try it interactively), yet the red line (future axis) that I tried to insert is not transposed in the same way as the bases of each line plot.
What I'm not looking for are solutions which rely on mpl_toolkits.mplot3d's Axes3D, as this would lead to many other issues regarding to zorder and zoom, which are exactly what I'm trying to avoid by coming up with my own "fake 3D plot".
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D,IdentityTransform
def offset(myFig,myAx,n=1,xOff=60,yOff=60):
"""
this function will apply a shift of n*dx, n*dy
where e.g. n=2, xOff=10 would yield a 20px offset in x-direction
"""
## scale by fig.dpi to have offset in pixels!
dx, dy = xOff/myFig.dpi , yOff/myFig.dpi
t_data = myAx.transData
t_off = mpl.transforms.ScaledTranslation( n*dx, n*dy, myFig.dpi_scale_trans)
return t_data + t_off
fig,axes=plt.subplots(nrows=1, ncols=3,figsize=(10,5))
ys=np.arange(0,5,0.5)
print(len(ys))
## just to have the lines colored in some uniform way
cmap = mpl.cm.get_cmap('viridis')
norm=mpl.colors.Normalize(vmin=ys.min(),vmax=ys.max())
## this defines the offset in pixels
xOff=10
yOff=10
for ax in axes:
## plot the lines
for yi,yv in enumerate(ys):
zo=(len(ys)-yi)
ax.plot([0,0.5,1],[0,1,0],color=cmap(norm(yv)),
zorder=zo, ## to order them "behind" each other
## here we apply the offset to each plot:
transform=offset(fig,ax,n=yi,xOff=xOff,yOff=yOff)
)
### optional: add a fill_between to make layering more obvious
ax.fill_between([0,0.5,1],[0,1,0],0,
facecolor=cmap(norm(yv)),edgecolor="None",alpha=0.1,
zorder=zo-1, ## to order them "behind" each other
## here we apply the offset to each plot:
transform=offset(fig,ax,n=yi,xOff=xOff,yOff=yOff)
)
##################################
####### this is the important bit:
ax.plot([0,2],[0,2],color='r',zorder=100,clip_on=False,
transform=ax.transData+mpl.transforms.ScaledTranslation(0.,0., fig.dpi_scale_trans)
)
## make sure to set them "manually", as autoscaling will fail due to transformations
for ax in axes:
ax.set_ylim(0,2)
axes[0].set_xlim(0,1)
axes[1].set_xlim(0,2)
axes[2].set_xlim(0,3)
### Note: the default fig.dpi is 100, hence an offset of of xOff=10px will become 30px when saving at 300dpi!
# plt.savefig("./test.png",dpi=300)
plt.show()
Update:
I've now included an animation below, which shows how the stacked lines behave on zooming/panning, and how their "baseline" (blue circles) moves with the plot, instead of the static OriginLineTrans solution (green line) or my transformed line (red, dashed).
The attachment points observe different transformations and can be inserted by:
ax.scatter([0],[0],edgecolors="b",zorder=200,facecolors="None",s=10**2,)
ax.scatter([0],[0],edgecolors="b",zorder=200,facecolors="None",s=10**2,transform=offset(fig,ax,n=len(ys)-1,xOff=xOff,yOff=yOff),label="attachment points")
The question boils down to the following:
How to produce a line that
starts from the origin (0,0) in axes coordinates and
evolves at an angle angle in physical coordinates (pixel space)
by using a matpotlib transform?
The problem is that the origin in axes coordinates may vary depending on the subplot position. So the only option I see is to create some custom transform that
transforms to pixel space
translates to the origin in pixel space
skews the coordinate system (say, in x direction) by the given angle
translates back to the origin of the axes
That could look like this
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms as mtrans
class OriginLineTrans(mtrans.Transform):
input_dims = 2
output_dims = 2
def __init__(self, origin, angle, axes):
self.axes = axes
self.origin = origin
self.angle = angle # in radiants
super().__init__()
def get_affine(self):
origin = ax.transAxes.transform_point(self.origin)
trans = ax.transAxes + \
mtrans.Affine2D().translate(*(-origin)) \
.skew(self.angle, 0).translate(*origin)
return trans.get_affine()
fig, ax = plt.subplots()
ax.plot([0,0], [0,1], transform=OriginLineTrans((0,0), np.arctan(1), ax))
plt.show()
Note that in the case of the original question, the angle would be np.arctan(dx/dy).

Polar grid on left hand side of rectangular plot

I am trying to reproduce a plot like this:
So the requirements are actually that the grid (that is to be present just on the left side) behaves just like a grid, that is, if we zoom in and out, it is always there present and not dependent on specific x-y limits for the actual data.
Unfortunately there is no diagonal version of axhline/axvline (open issue here) so I was thinking about using the grid from polar plots.
So for that I have two problems:
This answer shows how to overlay a polar axis on top of a rectangular one, but it does not match the origins and x-y values. How can I do that?
I also tried the suggestion from this answer for having polar plots using ax.set_thetamin/max but I get an AttributeError: 'AxesSubplot' object has no attribute 'set_thetamin' How can I use these functions?
This is the code I used to try to add a polar grid to an already existing rectangular plot on ax axis:
ax_polar = fig.add_axes(ax, polar=True, frameon=False)
ax_polar.set_thetamin(90)
ax_polar.set_thetamax(270)
ax_polar.grid(True)
I was hoping I could get some help from you guys. Thanks!
The mpl_toolkits.axisartist has the option to plot a plot similar to the desired one. The following is a slightly modified version of the example from the mpl_toolkits.axisartist tutorial:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
from mpl_toolkits.axisartist import SubplotHost, ParasiteAxesAuxTrans
from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear
import mpl_toolkits.axisartist.angle_helper as angle_helper
from matplotlib.projections import PolarAxes
from matplotlib.transforms import Affine2D
# PolarAxes.PolarTransform takes radian. However, we want our coordinate
# system in degree
tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
# polar projection, which involves cycle, and also has limits in
# its coordinates, needs a special method to find the extremes
# (min, max of the coordinate within the view).
# 20, 20 : number of sampling points along x, y direction
extreme_finder = angle_helper.ExtremeFinderCycle(20, 20,
lon_cycle=360,
lat_cycle=None,
lon_minmax=None,
lat_minmax=(0, np.inf),)
grid_locator1 = angle_helper.LocatorDMS(36)
tick_formatter1 = angle_helper.FormatterDMS()
grid_helper = GridHelperCurveLinear(tr,
extreme_finder=extreme_finder,
grid_locator1=grid_locator1,
tick_formatter1=tick_formatter1
)
fig = plt.figure(1, figsize=(7, 4))
fig.clf()
ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
# make ticklabels of right invisible, and top axis visible.
ax.axis["right"].major_ticklabels.set_visible(False)
ax.axis["right"].major_ticks.set_visible(False)
ax.axis["top"].major_ticklabels.set_visible(True)
# let left axis shows ticklabels for 1st coordinate (angle)
ax.axis["left"].get_helper().nth_coord_ticks = 0
# let bottom axis shows ticklabels for 2nd coordinate (radius)
ax.axis["bottom"].get_helper().nth_coord_ticks = 1
fig.add_subplot(ax)
## A parasite axes with given transform
## This is the axes to plot the data to.
ax2 = ParasiteAxesAuxTrans(ax, tr)
## note that ax2.transData == tr + ax1.transData
## Anything you draw in ax2 will match the ticks and grids of ax1.
ax.parasites.append(ax2)
intp = cbook.simple_linear_interpolation
ax2.plot(intp(np.array([150, 230]), 50),
intp(np.array([9., 3]), 50),
linewidth=2.0)
ax.set_aspect(1.)
ax.set_xlim(-12, 1)
ax.set_ylim(-5, 5)
ax.grid(True, zorder=0)
wp = plt.Rectangle((0,-5),width=1,height=10, facecolor="w", edgecolor="none")
ax.add_patch(wp)
ax.axvline(0, color="grey", lw=1)
plt.show()

Understanding matplotlib verts

I'm trying to create custom markers in matplotlib for a scatter plot, where the markers are rectangles with fix height and varying width. The width of each marker is a function of the y-value. I tried it like this using this code as a template and assuming that if verts is given a list of N 2-D tuples it plots rectangles with the width of the corresponing first value and the height of the second (maybe this is already wrong, but then how else do I accomplish that?).
I have a list of x and y values, each containing angles in degrees. Then, I compute the width and height of each marker by
field_size = 2.
symb_vec_x = [(field_size / np.cos(i * np.pi / 180.)) for i in y]
symb_vec_y = [field_size for i in range(len(y))]
and build the verts list and plot everything with
symb_vec = list(zip(symb_vec_x, symb_vec_y))
fig = plt.figure(1, figsize=(14.40, 9.00))
ax = fig.add_subplot(1,1,1)
sc = ax.scatter(ra_i, dec_i, marker='None', verts=symb_vec)
But the resulting plot is empty, no error message however. Can anyone tell me what I did wrong with defining the verts and how to do it right?
Thanks!
As mentioned 'marker='None' need to be removed then the appropriate way to specify a rectangle with verts is something like
verts = list(zip([-10.,10.,10.,-10],[-5.,-5.,5.,5]))
ax.scatter([0.5,1.0],[1.0,2.0], marker=(verts,0))
The vertices are defined as ([x1,x2,x3,x4],[y1,y2,y3,y4]) so attention must be paid to which get minus signs etc.
This (verts,0) is mentioned in the docs as
For backward compatibility, the form (verts, 0) is also accepted,
but it is equivalent to just verts for giving a raw set of vertices
that define the shape.
However I find using just verts does not give the correct shape.
To automate the process you need to do something like
v_val=1.0
h_val=2.0
verts = list(zip([-h_val,h_val,h_val,-h_val],[-v_val,-v_val,v_val,v_val]))
Basic example:
import pylab as py
ax = py.subplot(111)
v_val=1.0
h_val=2.0
verts = list(zip([-h_val,h_val,h_val,-h_val],[-v_val,-v_val,v_val,v_val]))
ax.scatter([0.5,1.0],[1.0,2.0], marker=(verts,0))
*
edit
Individual markers
So you need to manually create a vert for each case. This will obviously depend on how you want your rectangles to change point to point. Here is an example
import pylab as py
ax = py.subplot(111)
def verts_function(x,y,r):
# Define the vertex's multiplying the x value by a ratio
x = x*r
y = y
return [(-x,-y),(x,-y),(x,y),(-x,y)]
n=5
for i in range(1,4):
ax.scatter(i,i, marker=(verts_function(i,i,0.3),0))
py.show()
so in my simple case I plot the points i,i and draw rectangles around them. The way the vert markers are specified is non intuitive. In the documentation it's described as follows:
verts: A list of (x, y) pairs used for Path vertices. The center of
the marker is located at (0,0) and the size is normalized, such that
the created path is encapsulated inside the unit cell.
Hence, the following are equivalent:
vert = [(-300.0, -1000), (300.0, -1000), (300.0, 1000), (-300.0, 1000)]
vert = [(-0.3, -1), (0.3, -1), (0.3, 1), (-0.3, 1)]
e.g they will produce the same marker. As such I have used a ratio, this is where you need to do put in the work. The value of r (the ratio) will change which axis remains constant.
This is all getting very complicated, I'm sure there must be a better way to do this.
I got the solution from Ryan of the matplotlib users mailing list. It's quite elegant, so I will share his example here:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
n = 100
# Get your xy data points, which are the centers of the rectangles.
xy = np.random.rand(n,2)
# Set a fixed height
height = 0.02
# The variable widths of the rectangles
widths = np.random.rand(n)*0.1
# Get a color map and make some colors
cmap = plt.cm.hsv
colors = np.random.rand(n)*10.
# Make a normalized array of colors
colors_norm = colors/colors.max()
# Here's where you have to make a ScalarMappable with the colormap
mappable = plt.cm.ScalarMappable(cmap=cmap)
# Give it your non-normalized color data
mappable.set_array(colors)
rects = []
for p, w in zip(xy, widths):
xpos = p[0] - w/2 # The x position will be half the width from the center
ypos = p[1] - height/2 # same for the y position, but with height
rect = Rectangle( (xpos, ypos), w, height ) # Create a rectangle
rects.append(rect) # Add the rectangle patch to our list
# Create a collection from the rectangles
col = PatchCollection(rects)
# set the alpha for all rectangles
col.set_alpha(0.3)
# Set the colors using the colormap
col.set_facecolor( cmap(colors_norm) )
# No lines
col.set_linewidth( 0 )
#col.set_edgecolor( 'none' )
# Make a figure and add the collection to the axis.
fig = plt.figure()
ax = fig.add_subplot(111)
ax.add_collection(col)
# Add your ScalarMappable to a figure colorbar
fig.colorbar(mappable)
plt.show()
Thank you, Ryan, and everyone who contributed their ideas!

Categories