Intersection of a Shapely polygon with a Matplotlib wedge - python

In this scenario I am plotting matplotlib.patches.Wedge objects and also buffered shapely.geometry.LineString objects. I need to compute the overlapping areas of these two objects. However, the Wedge is a matplotlib.wedges object and cannot be used with Shapely's .intersection() method.
How can I do this?
Here is some code:
from shapely.geometry import LineString
from matplotlib.patches import Wedge
from matplotlib import pyplot as plt
from descartes.patch import PolygonPatch
width = 5
radius = 1
rich = 1
circle_patch = Wedge((0, 0), radius+3,
0, 360, 3)
fig, ax = plt.subplots()
ax.add_patch(circle_patch)
ax.plot(0, 0, 'xr')
plt.autoscale()
coords = [
[0, 0],
[0, 1],
[0, 2],
[1, 2],
[2, 2]
]
stick = LineString(coords)
stick_patch = PolygonPatch(stick.buffer(0.5))
ax.add_patch(stick_patch)
x, y = stick.xy
ax.plot(x, y, 'r-', zorder=1)
plt.show()
area = stick.buffer(0.5).intersection(circle_patch).area
P.S. It has to be a ring shape, not a circle

Figured it out. There is a ._path.vertices member of the matplotlib.patches class which gives you the array of coordinates of the wedge object which you can then use with Shapely's LinearRing class to create a Shapely object like so:
from shapely.geometry import LineString, LinearRing
from matplotlib.patches import Wedge
width = 5
radius = 1
rich = 1
circle_patch = Wedge((0, 0), radius,
0, 360,)
ring_coords = circle_patch._path.vertices
ring_coords = ring_coords[(ring_coords[:, 0] != 0) & (ring_coords[:, 1] != 0)]
ring = LinearRing(ring_coords)
It does however need manipulation of the coordinate array which I don't think is the most robust method but it will do for me. Also the ring is not entirely smooth but I am sure one could do some smoothing of the coordinate array with some or other Numpy or Scipy function.
EDIT: To create the single wedge line one must remove the width member of the wedge. This can however be re-incorporated later using Shapely's buffer() function.

The simplest solution would be not to work with Matplotlib patches and construct the wedge-polygon with Shapely in the first place:
import matplotlib.pyplot as plt
from descartes.patch import PolygonPatch
from shapely.geometry import LineString, Point
outer_circle = Point(0, 0).buffer(4)
inner_circle = Point(0, 0).buffer(1)
wedge = outer_circle.difference(inner_circle)
stick = LineString([(0, 0), (0, 2), (2, 2)])
buffered_stick = stick.buffer(0.5)
intersection = buffered_stick.intersection(wedge)
wedge_patch = PolygonPatch(wedge)
stick_patch = PolygonPatch(buffered_stick, alpha=0.5, hatch='/')
intersection_patch = PolygonPatch(intersection, alpha=0.5, hatch='.')
fig, ax = plt.subplots()
ax.add_patch(wedge_patch)
ax.add_patch(stick_patch)
ax.add_patch(intersection_patch)
plt.autoscale()
If, for some reason, this is not possible, and you have to work with the Matplotlib's Wedge, then I can think of two ways to get its intersection area with Shapely's polygon. In both of them, I convert the patches to Shapely polygons first. You probably can't get intersection area using only Matplotlib.
1) Using .get_path() method on the Matplotlib's patch from which you can extract vertices as a NumPy array and convert it to a Shapely polygon using asPolygon:
import matplotlib.pyplot as plt
from descartes.patch import PolygonPatch
from matplotlib.patches import Wedge
from shapely.geometry import asPolygon, LineString
wedge_patch = Wedge(center=(0, 0),
r=4,
theta1=0,
theta2=360,
width=3)
stick = LineString([(0, 0), (0, 2), (2, 2)])
buffered_stick = stick.buffer(0.5)
wedge_path = wedge_patch.get_path()
wedge_polygon = asPolygon(wedge_path.vertices).buffer(0)
intersection = buffered_stick.intersection(wedge_polygon)
stick_patch = PolygonPatch(buffered_stick, alpha=0.5, hatch='/')
intersection_patch = PolygonPatch(intersection, alpha=0.5, hatch='.')
fig, ax = plt.subplots()
ax.add_patch(wedge_patch)
ax.add_patch(stick_patch)
ax.add_patch(intersection_patch)
plt.autoscale()
Note the buffer(0) which I apply to the wedge polygon. This is a common trick in Shapely to make a valid polygon out of an invalid. In your answer you do something similar when removing zeros from ring_coords.
2) By accessing Wedge attributes: center, r and width, and using them to recreate a polygon:
import matplotlib.pyplot as plt
from descartes.patch import PolygonPatch
from matplotlib.patches import Wedge
from shapely.geometry import LineString, Point
wedge_patch = Wedge(center=(0, 0),
r=4,
theta1=0,
theta2=360,
width=3)
stick = LineString([(0, 0), (0, 2), (2, 2)])
buffered_stick = stick.buffer(0.5)
outer_circle = Point(wedge_patch.center).buffer(wedge_patch.r)
inner_circle = Point(wedge_patch.center).buffer(wedge_patch.r - wedge_patch.width)
wedge_polygon = outer_circle.difference(inner_circle)
intersection = buffered_stick.intersection(wedge_polygon)
stick_patch = PolygonPatch(buffered_stick, alpha=0.5, hatch='/')
intersection_patch = PolygonPatch(intersection, alpha=0.5, hatch='.')
fig, ax = plt.subplots()
ax.add_patch(wedge_patch)
ax.add_patch(stick_patch)
ax.add_patch(intersection_patch)
plt.autoscale()
All solutions give the same visual output.
And all methods give roughly the same area:
>>> intersection.area
3.3774012986988513 # 1st case
3.3823210603713694 # 2nd case and the original without Matplotlib

Related

Using fig.transFigure to draw a patch on ylabel

I created a sequence of points that I would like to convert into a Patch.
The goal is then to draw the patch on the left side of the y-label (see in Red in the figure), or draw it in any other part of the figure.
Although it can be accomplished with Gridspec, I would like to do it with a Patch.
import matplotlib.pyplot as plt
import numpy as np
plt.figure()
npoints = 100
td = np.linspace(np.pi*3/4, np.pi*5/4, npoints)
xd = np.cos(td)
yd = np.sin(td)
plt.plot(xd,yd)
EDIT1:
I am now able to make a Patch (just need to move it outside the axis):
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.path as mpath
import matplotlib.patches as mpatches
npoints = 100
td = np.linspace(np.pi*3/4, np.pi*5/4, npoints)
xd = np.cos(td)
yd = np.sin(td)
fig, ax = plt.subplots()
ax.axis([-2, 0, -1, 1])
verts=np.c_[xd,yd]
codes = np.ones(len(xd))*2 # Path.LINETO for all points except the first
codes[0] = 1 #Path.MOVETO only for the first point
path1 = mpath.Path(verts, codes)
patch = mpatches.PathPatch(path1, facecolor='none')
ax.add_patch(patch)
The result:
Now, I only need to move it outside the axis, maybe using a translation or scale.
I'm sure the key to do it is somewhere in this Matplotlib Transforms tutorial, more specifically, I am pretty sure the solution is using fig.transFigure.
EDIT 2: Almost there!
In order to use Figure coordinates (that are between [0,1]) I normalized the points that define the path. And instead of using ax.add_patch() that adds a patch to the axis, I use fig.add_artist() that adds the patch to the figure, over the axis.
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.path as mpath
import matplotlib.patches as mpatches
#Normalized Data
def normalize(x):
return (x - min(x)) / (max(x) - min(x))
#plt.figure()
npoints = 100
td = np.linspace(np.pi*3/4, np.pi*5/4, npoints)
xd = np.cos(td)
yd = np.sin(td)
#plt.plot(xd,yd)
xd = normalize(xd)
yd = normalize(yd)
fig, ax = plt.subplots()
ax.axis([-2, 2, -1, 1])
verts=np.c_[xd,yd]
codes = np.ones(len(xd))*2 # Path.LINETO for all points except the first
codes[0] = 1 #Path.MOVETO only for the first point
path1 = mpath.Path(verts, codes)
patch1 = mpatches.PathPatch(path1, facecolor='none')
ax.add_patch(patch1)
patch2 = mpatches.PathPatch(path1, fc='none', ec='red', transform=fig.transFigure)
fig.add_artist(patch2)
And the result so far:
Doing this, I just need to scale and translate the patch, maybe using Affine2D.
EDIT 3: Done!
Finally I was able to do it! I used Try and Error in the scale() and translate() parameters as I did not get what coordinate system they were using. However, it would be great to get the exact y center (0.5 in Figure coordinates).
Here is the complete code:
import numpy as np
import matplotlib.path as mpath
import matplotlib.patches as mpatches
#Normalized Data
def normalize(x):
return (x - min(x)) / (max(x) - min(x))
npoints = 100
td = np.linspace(np.pi*3/4, np.pi*5/4, npoints)
xd = np.cos(td)
yd = np.sin(td)
xd = normalize(xd)
yd = normalize(yd)
fig, ax = plt.subplots()
ax.axis([-2, 2, -1, 1])
verts=np.c_[xd,yd]
codes = np.ones(len(xd))*2 # Path.LINETO for all points except the first
codes[0] = 1 #Path.MOVETO only for the first point
path1 = mpath.Path(verts, codes)
patch1 = mpatches.PathPatch(path1, fc='none', ec='green')
ax.add_patch(patch1) #draw inside axis
patch2 = mpatches.PathPatch(path1, fc='none', ec='C0', transform=fig.transFigure)
fig.add_artist(patch2) #this works! Draw on figure
import matplotlib.transforms as mtransforms
tt = fig.transFigure + mtransforms.Affine2D().scale(0.02, 0.8).translate(10,45)
patch3 = mpatches.PathPatch(path1, fc='none', ec='red', transform=tt)
fig.add_artist(patch3)
And the resulting figure:
As #Pedro pointed out, most of this can be found in the tutorial that he linked. However, here is a short answer.
Basically, it's almost as if you're creating a line plot. Just specify the points you want to pass through, add them to a list and that's it.
In this example I want to pass through some points on the plot, then "lift the pen off of the paper" and continue from another point. So we create two lists - one containing the points I want to use and the second list which describes what I want to do with those points. Path.MOVETO will move your "pen" to the given point without drawing a line, so we use this to set our initial startpoint. Path.LINETO creates a straight line starting from your current pen position towards the next line in the list.
import matplotlib.pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches
# Points you want to "pass through"
pts = [
(0, 0),
(0.2, 0.2),
(0.4, 0.2),
(0.4, 0.4),
(0.4, 0.6),
(0.6, 0.6),
(0.8, 0.8)
]
# What you want to "do" with each point
codes = [
Path.MOVETO, # inital point
Path.LINETO,
Path.LINETO,
Path.LINETO,
Path.MOVETO, # pick up the pen
Path.LINETO,
Path.LINETO
]
# Create path object
# https://matplotlib.org/stable/tutorials/advanced/path_tutorial.html
path = Path(pts, codes)
patch = patches.PathPatch(path, lw='2', color='r', fill=False)
# patch the path to the figure
fig, ax = plt.subplots()
ax.add_patch(patch)
plt.show()
Result of code execution:

How do I make a custom path using python and matplotlib?

For my study project, I need to write an algorithm that tries to find the best solution to fold a protein. In order to make interpretation easy I wat to make a visualisation using MatPlotLib, exactly like this:
I followed some examples on the internet and managed to come up with the following code:`
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
protein = "PHPPHPHH"
coordinates = [(0, 0), (0, 1), (-1, 1), (-1, 2), (0, 2), (1, 2), (1, 1), (1, 0)]
fig, ax = plt.subplots()
Path = mpath.Path
path_data = []
for i in range(len(coordinates)):
if i == 0:
path_data.append((Path.MOVETO, (coordinates[i])))
else:
path_data.append((Path.LINETO, (coordinates[i])))
print(path_data)
codes, verts = zip(*path_data)
path = mpath.Path(verts, codes)
# plot control points and connecting lines
x, y = zip(*path.vertices)
line, = ax.plot(x, y, 'go-', color='grey')
ax.grid()
ax.axis('equal')
plt.title('Visualisation')
plt.legend()
plt.savefig("Visualisation.png")`
Resulting in this visualisation:
My question is:
-How can I change the color of the dots corresponding to the amino acids(H or P) and add a corresponding legend?
How do I draw the dotted lines in the case that two H amino acids lay next to eachother and can bond?
Thank you in advance!

set_clip_path on a matplotlib clabel not clipping properly

I'm making a contour plot that is clipped to a polygon path:
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import numpy as np
fig = plt.figure()
axes = plt.subplot()
x,y = np.meshgrid( np.linspace(-10,10,51), np.linspace(-10,10,51) )
z = np.sin(np.sqrt(x**2+y**2))
CS = axes.contour(x, y, z, np.linspace(-1,1,11) )
axes.set_aspect('equal')
# clip contours by polygon
radius = 8
t = np.linspace(0,2*np.pi,101)
x_bound,y_bound = radius*np.sin(t),radius*(np.cos(t)+0.1*(np.cos(7*t)))
clip_map = Polygon(list(zip(x_bound,y_bound)),fc='#EEEEEE',ec='none')
axes.add_patch(clip_map)
for collection in CS.collections:
collection.set_clip_path(clip_map)
# label the contours
CLB = axes.clabel(CS, colors='black')
for text_object in CLB:
text_object.set_clip_path(clip_map) # Doesn't do anything!
plt.show()
To my surprise, the labels aren't clipped despite the Text objects having a set_clip_path method that doesn't return an error:
How can I clip the labels outside of the gray polygon area? Do I need to resort to manually finding the X and Y positions, calculating point in polygon, and set_visible = False for each Text item? Why doesn't this code work as-is? I'm using matplotlib version 1.5.1 and python 3.5.1.
Just in case someone comes across the same issue someday, here's a solution that resorts to having to use the shapely package to test for point in polygon to set the visibility state of the Text object. It gets the job done, but it would be nice if it was possible to use set_clip_path to work directly on the Text object.
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import numpy as np
from shapely.geometry import Polygon as ShapelyPolygon
from shapely.geometry import Point as ShapelyPoint
fig = plt.figure()
axes = plt.subplot()
x,y = np.meshgrid( np.linspace(-10,10,51), np.linspace(-10,10,51) )
z = np.sin(np.sqrt(x**2+y**2))
CS = axes.contour(x, y, z, np.linspace(-1,1,11) )
axes.set_aspect('equal')
# clip contours by polygon
radius = 8
t = np.linspace(0,2*np.pi,101)
x_bound,y_bound = radius*np.sin(t),radius*(np.cos(t)+0.1*(np.cos(7*t)))
clip_map = Polygon(list(zip(x_bound,y_bound)),fc='#EEEEEE',ec='none')
axes.add_patch(clip_map)
for collection in CS.collections:
collection.set_clip_path(clip_map)
# label the contours
CLB = axes.clabel(CS, colors='black')
clip_map_shapely = ShapelyPolygon(clip_map.get_xy())
for text_object in CLB:
if not clip_map_shapely.contains(ShapelyPoint(text_object.get_position())):
text_object.set_visible(False)
plt.show()

get_path() return of a Circle from matplotlib.patches

Does anyone know what the get_path() of a Circle from matplotlib.patches returns? The get_path() of a circle is returning something different from the original circle, which can be seen from the result of the below code. As can be seen from the attached picture, the original orange circle is totally different with the blue circle from get_path() of the original circle.
import numpy as np
import matplotlib
from matplotlib.patches import Circle, Wedge, Polygon, Ellipse
from matplotlib.collections import PatchCollection
import matplotlib.pyplot as plt
import matplotlib.patches as matpatches
fig, ax = plt.subplots(figsize=(8, 8))
patches = []
circle = Circle((2, 2), 2)
patches.append(circle)
print patches[0].get_path()
print patches[0].get_verts()
polygon = matpatches.PathPatch(patches[0].get_path())
patches.append(polygon)
colors = 2*np.random.rand(len(patches))
p = PatchCollection(patches, cmap=matplotlib.cm.jet, alpha=0.4)
p.set_array(np.array(colors))
ax.add_collection(p)
plt.axis([-10, 10, -10, 10])
plt.show()
fig.savefig('test.png')
contain2 = patches[0].get_path().contains_points([[0.5, 0.5], [1.0, 1.0]])
print contain2
contain3 = patches[0].contains_point([0.5, 0.5])
print contain3
contain4 = patches[0].contains_point([1.0, 1.0])
print contain4
The path of the circle is the unit circle, and the way that matplotlib displays it as a circle with the center and radius that you specify is via a 2D affine transform. If you want the transformed path, you will need to get both the path and the transform and apply the transform to the path.
# Create the initial circle
circle = Circle([2,2], 2);
# Get the path and the affine transformation
path = circle.get_path()
transform = circle.get_transform()
# Now apply the transform to the path
newpath = transform.transform_path(path)
# Now you can use this
polygon = matpatches.PathPatch(newpath)
patches.append(polygon)

How to draw a filled arc in matplotlib

In matplotlib, I would like draw an filled arc which looks like this:
The following code results in an unfilled line arc:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
fg, ax = plt.subplots(1, 1)
pac = mpatches.Arc([0, -2.5], 5, 5, angle=0, theta1=45, theta2=135)
ax.add_patch(pac)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.canvas.draw()
The documentation says that filled arcs are not possible.
What would be the best way to draw one?
#jeanrjc's solution almost gets you there, but it adds a completely unnecessary white triangle, which will hide other objects as well (see figure below, version 1).
This is a simpler approach, which only adds a polygon of the arc:
Basically we create a series of points (points) along the edge of the circle (from theta1 to theta2). This is already enough, as we can set the close flag in the Polygon constructor which will add the line from the last to the first point (creating a closed arc).
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
def arc_patch(center, radius, theta1, theta2, ax=None, resolution=50, **kwargs):
# make sure ax is not empty
if ax is None:
ax = plt.gca()
# generate the points
theta = np.linspace(np.radians(theta1), np.radians(theta2), resolution)
points = np.vstack((radius*np.cos(theta) + center[0],
radius*np.sin(theta) + center[1]))
# build the polygon and add it to the axes
poly = mpatches.Polygon(points.T, closed=True, **kwargs)
ax.add_patch(poly)
return poly
And then we apply it:
fig, ax = plt.subplots(1,2)
# #jeanrjc solution, which might hide other objects in your plot
ax[0].plot([-1,1],[1,-1], 'r', zorder = -10)
filled_arc((0.,0.3), 1, 90, 180, ax[0], 'blue')
ax[0].set_title('version 1')
# simpler approach, which really is just the arc
ax[1].plot([-1,1],[1,-1], 'r', zorder = -10)
arc_patch((0.,0.3), 1, 90, 180, ax=ax[1], fill=True, color='blue')
ax[1].set_title('version 2')
# axis settings
for a in ax:
a.set_aspect('equal')
a.set_xlim(-1.5, 1.5)
a.set_ylim(-1.5, 1.5)
plt.show()
Result (version 2):
You can use fill_between to achieve this
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
fg, ax = plt.subplots(1, 1)
r=2.
yoff=-1
x=np.arange(-1.,1.05,0.05)
y=np.sqrt(r-x**2)+yoff
ax.fill_between(x,y,0)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.canvas.draw()
Play around with r and yoff to move the arc
EDIT:
OK, so you want to be able to plot arbitrary angles? You just need to find the equation of the chord, rather than using a flat line like above. Here's a function to do just that:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
fg, ax = plt.subplots(1, 1)
col='rgbkmcyk'
def filled_arc(center,r,theta1,theta2):
# Range of angles
phi=np.linspace(theta1,theta2,100)
# x values
x=center[0]+r*np.sin(np.radians(phi))
# y values. need to correct for negative values in range theta=90--270
yy = np.sqrt(r-x**2)
yy = [-yy[i] if phi[i] > 90 and phi[i] < 270 else yy[i] for i in range(len(yy))]
y = center[1] + np.array(yy)
# Equation of the chord
m=(y[-1]-y[0])/(x[-1]-x[0])
c=y[0]-m*x[0]
y2=m*x+c
# Plot the filled arc
ax.fill_between(x,y,y2,color=col[theta1/45])
# Lets plot a whole range of arcs
for i in [0,45,90,135,180,225,270,315]:
filled_arc([0,0],1,i,i+45)
ax.axis([-2, 2, -2, 2])
ax.set_aspect("equal")
fg.savefig('filled_arc.png')
And here's the output:
Here's a simpler workaround. Use the hatch argument in your mpatches.Arc command. If you repeat symbols with the hatch argument it increases the density of the patterning. I find that if you use 6 dashes, '-', or 6 dots, '.' (others probably also work), then it solidly fills in the arc as desired. When I run this
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
plt.axes()
pac = mpatches.Arc([0, -2.5], 5, 5, 45, theta1=45, theta2=135, hatch = '......')
plt.gca().add_patch(pac)
pac.set_color('cyan')
plt.axis('equal')
plt.show()
I get this:
Arc filled with dense dot hatch and rotated 45 degrees just for show
You can draw a wedge, and then hide part of it with a triangle:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
def filled_arc(center, radius, theta1, theta2, ax, color):
circ = mpatches.Wedge(center, radius, theta1, theta2, fill=True, color=color)
pt1 = (radius * (np.cos(theta1*np.pi/180.)) + center[0],
radius * (np.sin(theta1*np.pi/180.)) + center[1])
pt2 = (radius * (np.cos(theta2*np.pi/180.)) + center[0],
radius * (np.sin(theta2*np.pi/180.)) + center[1])
pt3 = center
pol = mpatches.Polygon([pt1, pt2, pt3], color=ax.get_axis_bgcolor(),
ec=ax.get_axis_bgcolor(), lw=2 )
ax.add_patch(circ)
ax.add_patch(pol)
and then you can call it:
fig, ax = plt.subplots(1,2)
filled_arc((0,0), 1, 45, 135, ax[0], "blue")
filled_arc((0,0), 1, 0, 40, ax[1], "blue")
and you get:
or:
fig, ax = plt.subplots(1, 1)
for i in range(0,360,45):
filled_arc((0,0), 1, i, i+45, ax, plt.cm.jet(i))
and you get:
HTH
The command ax.get_axis_bgcolor() needs to be replaced by ax.get_fc() for newer matplotlib.

Categories