how to use matplotlib PATH to draw polygon - python

I have a problem when using python's matplotlib PATH modules
I want to draw a close poly like this:
but I don't know exactly the sequence of the points to be connected and it turned out the result images can't meet my needs. How can I draw a polygon correctly without determining the sequence by myself but by the code?
here is my code:
import matplotlib
import matplotlib.pyplot as plt
import pandas
from matplotlib.path import Path
import matplotlib.patches as patches
#read data
info = pandas.read_csv('/Users/james/Desktop/nba.csv')
info.columns = ['number', 'team_id', 'player_id', 'x_loc', 'y_loc',
'radius', 'moment', 'game_clock', 'shot_clock', 'player_name',
'player_jersey']
#first_team_info
x_1 = info.x_loc[1:6]
y_1 = info.y_loc[1:6]
matrix= [x_1,y_1]
z_1 = list(zip(*matrix))
z_1.append(z_1[4])
n_1 = info.player_jersey[1:6]
verts = z_1
codes = [Path.MOVETO,
Path.LINETO,
Path.LINETO,
Path.LINETO,
Path.LINETO,
Path.CLOSEPOLY,
]
path = Path(verts, codes)
fig = plt.figure()
ax = fig.add_subplot(111)
patch = patches.PathPatch(path, facecolor='orange', lw=2)
ax.add_patch(patch)
ax.set_xlim(0, 100)
ax.set_ylim(0, 55)
plt.show()
and I got this:

Matplotlib plots the points of a path in order they are given to patch.
This can lead to undesired results, if there is no control over the order, like in the case from the question.
So the solution may be to
(A) use a hull. Scipy provides scipy.spatial.ConvexHull to calculate the circonference of the points, which is automatically in the correct order. This gives good results in many cases, see first row, but may fail in other cases, because points inside the hull are ignored.
(B) sort the points, e.g. counter clockwise around a certain point in the middle. In the example below I take the mean of all points for that. The sorting can be imagined like a radar scanner, points are sorted by their angle to the x axis. This solves e.g. the problem of the hull in the second row, but may of course also fail in more complicated shapes.
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial import ConvexHull
p = [(1,1), (2,1.6), (0.8,2.7), (1.7,3.2)]
p2 = [(0.7,1.3),(2,0.9),(1.4,1.5),(1.9,3.1),(0.6,2.5),(1.4,2.3)]
def convexhull(p):
p = np.array(p)
hull = ConvexHull(p)
return p[hull.vertices,:]
def ccw_sort(p):
p = np.array(p)
mean = np.mean(p,axis=0)
d = p-mean
s = np.arctan2(d[:,0], d[:,1])
return p[np.argsort(s),:]
fig, axes = plt.subplots(ncols=3, nrows=2, sharex=True, sharey=True)
axes[0,0].set_title("original")
poly = plt.Polygon(p, ec="k")
axes[0,0].add_patch(poly)
poly2 = plt.Polygon(p2, ec="k")
axes[1,0].add_patch(poly2)
axes[0,1].set_title("convex hull")
poly = plt.Polygon(convexhull(p), ec="k")
axes[0,1].add_patch(poly)
poly2 = plt.Polygon(convexhull(p2), ec="k")
axes[1,1].add_patch(poly2)
axes[0,2].set_title("ccw sort")
poly = plt.Polygon(ccw_sort(p), ec="k")
axes[0,2].add_patch(poly)
poly2 = plt.Polygon(ccw_sort(p2), ec="k")
axes[1,2].add_patch(poly2)
for ax in axes[0,:]:
x,y = zip(*p)
ax.scatter(x,y, color="k", alpha=0.6, zorder=3)
for ax in axes[1,:]:
x,y = zip(*p2)
ax.scatter(x,y, color="k", alpha=0.6, zorder=3)
axes[0,0].margins(0.1)
axes[0,0].relim()
axes[0,0].autoscale_view()
plt.show()

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:

Python: how to extend Voronoi cells to the boundary of the geometry?

I have two geopandas dataframes dfMd and centers. dfMd is made by polygons while centers by points. Here the link to the files.
f,ax=plt.subplots()
dfMd.plot(ax=ax, column='volume')
centers.plot(ax=ax, color='red')
I would like to generate the Voronoi tasselation of the points extended to the entire geometry. This is what I am doing:
from shapely.geometry import mapping
g = [i for i in centers.geometry]
coords = []
for i in range(0, len(g)):
coords.append(mapping(g[i])["coordinates"]) # for first feature/row
from libpysal.cg.voronoi import voronoi, voronoi_frames
regions, vertices = voronoi(coords)
region_df, point_df = voronoi_frames(coords)
fig, ax = plt.subplots()
region_df.plot(ax=ax, color='white',edgecolor='black', lw=3)
dfMd.plot(ax=ax, column='volume',alpha=0.1)
point_df.plot(ax=ax, color='red')
fig, ax = plt.subplots()
region_df.plot(ax=ax, color='white',edgecolor='black', lw=3)
dfMd.plot(ax=ax, column='volume',alpha=0.1)
point_df.plot(ax=ax, color='red')
How can I extend the Voronoi region to the external boundary of my dfMd?
You need an option clip=box() within voronoi_frames(). Relevant code follows.
from shapely.geometry import box
region_df, point_df = voronoi_frames(coords, clip=box(-4.2, 40.15, -3.0, 40.85))

Matplotlib Circle patch does not have smooth edges

I'm trying to display Matplotlib patches using the Circle function on a map plot using cartopy geographical projections. Apparently this is supposed to give a smooth, near scale-free circular patch, however the edges are very polygonal. Strangely, CirclePolygon, the polygonal approximation counterpart of Circle, produces a smoother circle, albeit still not as smooth as I would like.
This is pretty much all the code as it pertains to adding the plot and the patches:
fig = plt.figure(figsize=(8,6))
img_extent = [340, 348, -35.5, -31]
ax = fig.add_subplot(1, 1, 1, projection = ccrs.Mollweide(), extent = img_extent)
patch_coords = [[342.5833, -34.5639],[343.4042, -34.3353],[343.8500, -33.8728],
[344.4917, -33.7636],[344.9250, -33.3108],[345.1333, -32.6811],
[344.9233, -32.1583]]
for pair in patch_coords:
ax.add_patch(mpatches.Circle(xy = pair, radius = 0.5,
color = 'r', alpha = 0.3, rasterized = None,
transform = ccrs.Geodetic()))
ax.scatter(ra1, dec1, transform = ccrs.Geodetic(), rasterized = True, s = 1,
marker = ".", c = 'g', label = 'z < 0.025')
ax.scatter(ra2, dec2, transform = ccrs.Geodetic(), rasterized = True, s = 2,
marker = ".", c = 'b', label = '0.25 < z < 0.034')
ax.scatter(ra3, dec3, transform = ccrs.Geodetic(), rasterized = True, s = 0.75,
marker = ".", c = 'grey', label = '0.034 < z < 0.05')
Which produces this
I've tried looking through the available arguments but none seem to fix it. Is there a reason why it comes out like this and is there any way to make it smoother?
I believe plotting Tissot's Indicatrices is more appropriate in your case. An Indicatrix represents a ground circle on a map projection. In many cases, the Indicatrices are rendered as ellipses as map projections do not always preserve shapes. The following is the working code that plots all the ground circles of radius = 55 km on the map projection that you desire. Read the comments in the code for some useful information.
import matplotlib.pyplot as plt
# import matplotlib.patches as mpatches
import cartopy.crs as ccrs
import numpy as np
fig = plt.figure(figsize=(12,8))
img_extent = [340, 348, -35.5, -31]
ax = fig.add_subplot(1, 1, 1, projection = ccrs.Mollweide(), extent = img_extent)
patch_coords = [[342.5833, -34.5639],[343.4042, -34.3353],[343.8500, -33.8728],
[344.4917, -33.7636],[344.9250, -33.3108],[345.1333, -32.6811],
[344.9233, -32.1583]]
for ix,pair in enumerate(patch_coords):
# plot tissot indicatrix at each location
# n_samples = number of points forming indicatrix' perimeter
# rad_km = 55 km. is about the angular distance 0.5 degree
ax.tissot(rad_km=55, lons=np.array(patch_coords)[:,0][ix], \
lats=np.array(patch_coords)[:,1][ix], n_samples=36, \
facecolor='red', edgecolor='black', linewidth=0.15, alpha = 0.3)
gl = ax.gridlines(draw_labels=False, linewidth=1, color='blue', alpha=0.3, linestyle='--')
plt.show()
The resulting plot:
Edit
Since the first version of the code is not optimal.
Code update is offered as follows:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
fig = plt.figure(figsize=(12,8))
img_extent = [340, 348, -35.5, -31]
ax = fig.add_subplot(1, 1, 1, projection = ccrs.Mollweide(), extent = img_extent)
patch_coords = [[342.5833, -34.5639],[343.4042, -34.3353],[343.8500, -33.8728],
[344.4917, -33.7636],[344.9250, -33.3108],[345.1333, -32.6811],
[344.9233, -32.1583]]
for pair in patch_coords:
# plot tissot indicatrix at each location
# n_samples = number of points forming indicatrix' perimeter
# rad_km = 55 km. is about the angular distance 0.5 degree at equator
ax.tissot(rad_km=55, lons=pair[0], lats=pair[1], n_samples=36, \
facecolor='red', edgecolor='black', linewidth=0.15, alpha = 0.3)
gl = ax.gridlines(draw_labels=False, linewidth=1, color='blue', alpha=0.3, linestyle='--')
plt.show()
I believe that Cartopy does line projections with an arbitrary fixed accuracy, rather than a dynamic line-split calculation.
See e.g. :
https://github.com/SciTools/cartopy/issues/825
https://github.com/SciTools/cartopy/issues/363
I also think work is ongoing right now to address that.
In the meantime, to solve specific problems you can hack the CRS.threshold property,
as explained here : https://github.com/SciTools/cartopy/issues/8
That is, you can make it use finer steps by reprogramming the fixed value.
I think this would also fix this circle-drawing problem, though I'm not 100%

Setting a clip on a seaborn plot

I am having trouble clipping a seaborn plot (a kdeplot, specifically) as I thought would be fairly simple per this example in the matplotlib docs.
For example, the following code:
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
fig = plt.figure()
ax = fig.add_subplot(111, frameon=False, xticks=[], yticks=[])
random_points = np.array([p for p in np.random.random(size=(100, 2)) if 0 < p[0] < 1 and 0 < p[1] < 1])
kde = sns.kdeplot(random_points[:,0], random_points[:,1], ax=ax)
xmin, xmax = kde.get_xlim()
ymin, ymax = kde.get_ylim()
patch = mpl.patches.Circle(((xmin + xmax)/2, (ymin + ymax) / 2), radius=0.4)
ax.add_patch(patch)
kde.set_clip_path(patch)
Results in the following output:
I would like to clip this result so that the KDE contour lines do not appear outside of the circle. I haven't found a way to do it thus far...is this possible?
Serenity's answer works for simple shapes, but breaks down for reasons unknown when the shape contains more than three or so vertices (I had difficulty establishing the exact parameters, even). For sufficiently large shapes the fill flows into where the edge should be, as for example here.
It did get me thinking along the right path, however. While it doesn't seem to be possible to do so simply using matplotlib natives (perhaps there's an error in the code he provided anyway?), it's easy as pie when using the shapely library, which is meant for tasks like this one.
Generating the Shape
In this case you will need shapely's symmetric_difference method. A symmetric difference is the set theoretic name for this cut-out operation.
For this example I've loaded a Manhattan-shaped polygon as a shapely.geometry.Polygon object. I won't covert the initialization process here, it's easy to do, and everything you expect it to be.
We can draw a box around our manhattan using manhattan.envelope, and then apply the difference. This is the following:
unmanhattan = manhattan.envelope.symmetric_difference(manhattan)
Doing which gets us to:
Adding it to the Plot
Ok, but this is a shapely object not a matplotlib Patch, how do we add it to the plot? The descartes library handles this conversion.
unmanhattan_patch = descartes.PolygonPatch(unmanhattan)
This is all we need! Now we do:
unmanhattan_patch = descartes.PolygonPatch(unmanhattan)
ax.add_patch(unmanhattan_patch)
sns.kdeplot(x=points['x_coord'], y=points['y_coord'], ax=ax)
And get:
And with a little bit more work extending this to the rest of the polygons in the view (New York City), we can get the following final result:
I guess your example work only for 'imshow'.
To hide contours lines over the circle you have to plot 'inverse' polygon of desired color.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
import seaborn as sns
# Color plot except polygon
def mask_outside(poly_verts, facecolor = None, ax = None):
from matplotlib.patches import PathPatch
from matplotlib.path import Path
if ax is None: ax = plt.gca()
if facecolor is None: facecolor = plt.gcf().get_facecolor()
# Construct inverse polygon
xlim, ylim = ax.get_xlim(), ax.get_ylim()
bound_verts = [(xlim[0], ylim[0]), (xlim[0], ylim[1]),
(xlim[1], ylim[1]), (xlim[1], ylim[0]), (xlim[0], ylim[0])]
bound_codes = [Path.MOVETO] + (len(bound_verts) - 1) * [Path.LINETO]
poly_codes = [Path.MOVETO] + (len(poly_verts) - 1) * [Path.LINETO]
# Plot it
path = Path(bound_verts + poly_verts, bound_codes + poly_codes)
ax.add_patch(PathPatch(path, facecolor = facecolor, edgecolor = 'None', zorder = 1e+3))
# Your example
fig = plt.figure()
ax = fig.add_subplot(111, frameon=False, xticks=[], yticks=[])
random_points = np.array([p for p in np.random.random(size=(100, 2)) if 0 < p[0] < 1 and 0 < p[1] < 1])
kde = sns.kdeplot(random_points[:,0], random_points[:,1], ax=ax)
xmin, xmax = kde.get_xlim()
ymin, ymax = kde.get_ylim()
patch = mpl.patches.Circle(((xmin + xmax) / 2, (ymin + ymax) / 2), radius=0.4)
mask_outside([tuple(x) for x in patch.get_verts()]) # call before add_patch!
ax.add_patch(patch)
plt.show()

setting color range in matplotlib patchcollection

I am plotting a PatchCollection in matplotlib with coords and patch color values read in from a file.
The problem is that matplotlib seems to automatically scale the color range to the min/max of the data values. How can I manually set the color range? E.g. if my data range is 10-30, but I want to scale this to a color range of 5-50 (e.g. to compare to another plot), how can I do this?
My plotting commands look much the same as in the api example code: patch_collection.py
colors = 100 * pylab.rand(len(patches))
p = PatchCollection(patches, cmap=matplotlib.cm.jet, alpha=0.4)
p.set_array(pylab.array(colors))
ax.add_collection(p)
pylab.colorbar(p)
pylab.show()
Use p.set_clim([5, 50]) to set the color scaling minimums and maximums in the case of your example. Anything in matplotlib that has a colormap has the get_clim and set_clim methods.
As a full example:
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
from matplotlib.patches import Circle
import numpy as np
# (modified from one of the matplotlib gallery examples)
resolution = 50 # the number of vertices
N = 100
x = np.random.random(N)
y = np.random.random(N)
radii = 0.1*np.random.random(N)
patches = []
for x1, y1, r in zip(x, y, radii):
circle = Circle((x1, y1), r)
patches.append(circle)
fig = plt.figure()
ax = fig.add_subplot(111)
colors = 100*np.random.random(N)
p = PatchCollection(patches, cmap=matplotlib.cm.jet, alpha=0.4)
p.set_array(colors)
ax.add_collection(p)
fig.colorbar(p)
fig.show()
Now, if we just add p.set_clim([5, 50]) (where p is the patch collection) somewhere before we call fig.show(...), we get this:

Categories