How to measure and modify length of path? - python

I am trying to create paths with mathplotlib.path, more precisely n-gons. Although I would like to add the constraint that all polygons have the same perimeter. In order to do that I would have to calculate the perimeter of the polygon, and the adjust the path length to a fixed variable.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.path as mpltPath
N = 10000
points = np.random.rand(N,2)
# regular polygon
sidepoly = 5
polygon = [[np.sin(x),np.cos(x)] for x in np.linspace(0, 2*np.pi, sidepoly)[:sidepoly]]
# Matplotlib mplPath
path = mpltPath.Path(polygon)
fig, ax = plt.subplots()
patch = patches.PathPatch(path, facecolor='none', lw=2)
ax.add_patch(patch)
ax.axis('equal')
ax.set_xlim(-1,1)
ax.set_ylim(-1,1)
plt.show()
Any recomendations?

The side length of a regular polygon can be calculated via twice the sine of half the angle (see e.g. here). The perimeter is just one side length multiplied by the number of sides. Using a radius that divides away the default perimeter and multiplies by the desired perimeter, creates a polygon with that perimeter.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.path as mpltPath
from matplotlib.colors import to_rgba
fig, axs = plt.subplots(ncols=5, nrows=2, figsize=(12,5))
for sidepoly, ax in zip(range(3, 3+axs.size), axs.flatten()):
# regular polygon
desired_perimeter = 5
default_perimeter = 2 * sidepoly * np.sin(np.pi / sidepoly)
theta = np.linspace(0, 2 * np.pi, sidepoly+1)
polygon = np.c_[np.sin(theta), np.cos(theta)] * desired_perimeter / default_perimeter
path = mpltPath.Path(polygon)
patch = patches.PathPatch(path, facecolor=to_rgba('dodgerblue', alpha=0.2), edgecolor='black', lw=2)
ax.add_patch(patch)
side_length = np.sqrt((polygon[1, 0] - polygon[0, 0]) ** 2 + (polygon[1, 1] - polygon[0, 1]) ** 2)
perimeter = side_length * sidepoly
ax.text(0, 0, f'{sidepoly}-gon\nside:{side_length:.2f}\nperim.:{perimeter:.2f}', ha='center', va='center')
ax.axis('equal')
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
plt.tight_layout()
plt.show()

Related

How to add axis lines to ellipse using python matplotlib.patches.ellipse?

I have this simple code that generates an ellipse
import matplotlib.patches as patches
import matplotlib.pyplot as plt
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
ellipse = patches.Ellipse((0, 0), 4, 2, angle=45, fill=False)
ax.add_artist(ellipse)
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
plt.show()
This is the current output:
ellipse
I need to add axis of ellipse so it would look like this:
ellipse_output
Is there a way to do that?
I need a generic way to use in more complex ellipses, thanks.
I tried to search for parameters in patches.Ellipse() to draw those axis lines, but didn't find anything.
You can add the major and minor axes of the ellipse.
In the code i show, i do the major axis, but you need to work on the angle part (based on the points of the elipse), whereas i just set it to 45 degrees to post a quick answer.
The result of this would give the complete solution.
So, I do something like this:
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
#################################
# you need to figure this bit out
#################################
ellipse = patches.Ellipse((0, 0), 4, 2, angle=45, fill=False)
ax.add_artist(ellipse)
ellipse.set_clip_box(ax.bbox)
ellipse.set_alpha(0.1)
ax.annotate("",
xy=(ellipse.center[0], ellipse.center[1] - ellipse.height / 2),
xytext=(ellipse.center[0], ellipse.center[1] + ellipse.height / 2),
arrowprops=dict(arrowstyle="<->", color="black"))
ax.annotate("",
xy=(ellipse.center[0] - ellipse.width / 2, ellipse.center[1]),
xytext=(ellipse.center[0] + ellipse.width / 2, ellipse.center[1]),
arrowprops=dict(arrowstyle="<->", color="black"))
ax.annotate("",
xy=(ellipse.center[0] - ellipse.width / 2 * np.cos(np.deg2rad(ellipse.angle)),
ellipse.center[1] - ellipse.height / 2 * np.sin(np.deg2rad(ellipse.angle))),
xytext=(ellipse.center[0] + ellipse.width / 2 * np.cos(np.deg2rad(ellipse.angle)),
ellipse.center[1] + ellipse.height / 2 * np.sin(np.deg2rad(ellipse.angle))),
arrowprops=dict(arrowstyle="<->", color="black"))
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
plt.show()
Which leaves you with a plot like this:
Basically, in summary, the anotate lines let you do the final bits that you require.
EDIT:
I was able to reduce to this:
import matplotlib.patches as patches
import matplotlib.pyplot as plt
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
# patches.Ellipse(center, width, height, angle)
ellipse = patches.Ellipse((0, 0), 4, 2, angle=45, fill=False)
ax.add_artist(ellipse)
ellipse.set_clip_box(ax.bbox)
ax.annotate("",
xy=(ellipse.center[0] - ellipse.width+2 ,
ellipse.center[1] - ellipse.height ),
xytext=(ellipse.center[0] + ellipse.width-1,
ellipse.center[1] + ellipse.height+1),
arrowprops=dict(arrowstyle="<->", color="red"))
ax.set_xlim(-2.2, 2.2;)
ax.set_ylim(-2.2, 2.2)
plt.show()
which looks like this:
from math import sin, cos, radians
import matplotlib.patches as patches
import matplotlib.pyplot as plt
fig, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
##############
ellipse_size = (4,2)
ellipse_rotation = 45
ellipse_position = (0,0)
ellipse = patches.Ellipse(ellipse_position, ellipse_size[0], ellipse_size[1], angle=ellipse_rotation, fill=False)
ax.add_artist(ellipse)
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
# math for the start and end axis point positions
ax1_points = [
(ellipse_position[0]+ellipse_size[0]/2*cos(radians(ellipse_rotation)),
ellipse_position[1]+ellipse_size[0]/2*sin(radians(ellipse_rotation))),
(ellipse_position[0]+ellipse_size[0]/2*cos(radians(ellipse_rotation + 180)),
ellipse_position[1]+ellipse_size[0]/2*sin(radians(ellipse_rotation + 180)))
]
ax2_points = [
(ellipse_position[0]+ellipse_size[1]/2*cos(radians(ellipse_rotation+90)),
ellipse_position[1]+ellipse_size[1]/2*sin(radians(ellipse_rotation+90))),
(ellipse_position[0]+ellipse_size[1]/2*cos(radians(ellipse_rotation + 270)),
ellipse_position[1]+ellipse_size[1]/2*sin(radians(ellipse_rotation + 270)))]
# ax1 and ax2 contains the start and the end point of the axis ([x,y] format)
# drawing the arrows
arrowprops=dict(arrowstyle="<->", color="red")
ax.annotate("", xy=ax1_points[0], xytext=ax1_points[1], arrowprops=arrowprops)
ax.annotate("", xy=ax2_points[0], xytext=ax2_points[1], arrowprops=arrowprops)
plt.show()
produces this:
Hope this helps.

How to create a multi-colored curve in 3d?

I'm trying to plot a 3d curve that has different colors depending on one of its parameters. I tried this method similar to this question, but it doesn't work. Can anyone point me in the right direction?
import matplotlib.pyplot as plt
from matplotlib import cm
T=100
N=5*T
x=np.linspace(0,T,num=N)
y=np.cos(np.linspace(0,T,num=N))
z=np.sin(np.linspace(0,T,num=N))
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot(x,y,z,cmap = cm.get_cmap("Spectral"),c=z)
plt.show()
To extend the approach in this tutorial to 3D, use x,y,z instead of x,y.
The desired shape for the segments is (number of segments, 2 points, 3 coordinates per point), so N-1,2,3. First the array of points is created with shape N, 3. Then start (xyz[:-1, :]) and end points (xyz[1:, :]) are stacked together.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Line3DCollection
T = 100
N = 5 * T
x = np.linspace(0, T, num=N)
y = np.cos(np.linspace(0, T, num=N))
z = np.sin(np.linspace(0, T, num=N))
xyz = np.array([x, y, z]).T
segments = np.stack([xyz[:-1, :], xyz[1:, :]], axis=1) # shape is 499,2,3
cmap = plt.cm.get_cmap("Spectral")
norm = plt.Normalize(z.min(), z.max())
lc = Line3DCollection(segments, linewidths=2, colors=cmap(norm(z[:-1])))
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.add_collection(lc)
ax.set_xlim(-10, 110)
ax.set_ylim(-1.1, 1.1)
ax.set_zlim(-1.1, 1.1)
plt.show()

How to properly draw hexagons with offset coordinates?

This is my code:
import matplotlib.pyplot as plt
from matplotlib.patches import RegularPolygon
import numpy as np
offCoord = [[-2,-2],[-1,-2],[0,-2],[1,-2],[2,-2]]
fig, ax = plt.subplots(1)
ax.set_aspect('equal')
for c in offCoord:
hex = RegularPolygon((c[0], c[1]), numVertices=6, radius=2./3., alpha=0.2, edgecolor='k')
ax.add_patch(hex)
plt.autoscale(enable = True)
plt.show()
Expected result vs actual result in the attached image
Please tell me why my hexagons are not lined up edge by edge but overlap each other?
What am I doing wrong?
Use law of cosines (for isosceles triangle with angle 120 degrees and sides r, r, and 1):
1 = r*r + r*r - 2*r*r*cos(2pi/3) = r*r + r*r + r*r = 3*r*r
r = sqrt(1/3)
This is the right code:
import matplotlib.pyplot as plt
from matplotlib.patches import RegularPolygon
import numpy as np
offCoord = [[-2,-2],[-1,-2],[0,-2],[1,-2],[2,-2]]
fig, ax = plt.subplots(1)
ax.set_aspect('equal')
for c in offCoord:
# fix radius here
hexagon = RegularPolygon((c[0], c[1]), numVertices=6, radius=np.sqrt(1/3), alpha=0.2, edgecolor='k')
ax.add_patch(hexagon)
plt.autoscale(enable = True)
plt.show()
Very simply, your geometry is wrong. You specified a radius of 2/3. Check your documentation for RegularPolygon; I think that you'll find the correct radius is 0.577 (sqrt(3) / 3) or something close to that.
Radius of regular hexagon equals its side. In that case, the proper offset should be:
offset = radius*3**0.5. If radius is 2/3, the offsets should be 1.1547k, where k=-2,-1...

Matplotlib: Set cmap in plot_surface to x and y-axes

How can I set the colormap in relation to the radius of the figure?
And how can I close the ends of the cylinder (on the element, not the top and bottom bases)?
My script:
import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from math import sin, cos, pi
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
h, w = 60,30
znew = np.random.randint(low=90, high=110, size=(60,30))
theta = np.linspace(0,2*pi, h)
Z = np.linspace(0,1,w)
Z,theta = np.meshgrid(Z, theta)
R = 1
X = (R*np.cos(theta))*znew
Y = (R*np.sin(theta))*znew
ax1 = ax.plot_surface(X,Y,Z,linewidth = 0, cmap="coolwarm",
vmin= 80,vmax=130, shade = True, alpha = 0.75)
fig.colorbar(ax1, shrink=0.9, aspect=5)
plt.show()
First you need to use the facecolors keyword argument of plot_surface to draw your surface with arbitrary (non-Z-based) colours. You have to pass an explicit RGBA colour four each point, which means we need to sample a colormap object with the keys given by the radius at every point. Finally, this will break the mappable property of the resulting surface, so we will have to construct the colorbar by manually telling it to use our radii for colours:
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.cm as cm
from matplotlib.colors import Normalize
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
h, w = 60,30
#znew = np.random.randint(low=90, high=110, size=(h,w))
theta = np.linspace(0,2*np.pi, h)
Z = np.linspace(0,1,w)
Z,theta = np.meshgrid(Z, theta)
znew = 100 + 10*np.cos(theta/2)*np.cos(2*Z*np.pi)
R = 1
X = (R*np.cos(theta))*znew
Y = (R*np.sin(theta))*znew
true_radius = np.sqrt(X**2 + Y**2)
norm = Normalize()
colors = norm(true_radius) # auto-adjust true radius into [0,1] for color mapping
cmap = cm.get_cmap("coolwarm")
ax.plot_surface(X, Y, Z, linewidth=0, facecolors=cmap(colors), shade=True, alpha=0.75)
# the surface is not mappable, we need to handle the colorbar manually
mappable = cm.ScalarMappable(cmap=cmap)
mappable.set_array(colors)
fig.colorbar(mappable, shrink=0.9, aspect=5)
plt.show()
Note that I changed the radii to something smooth for a less chaotic-looking result. The true_radius arary contains the actual radii in data units, which after normalization becomes colors (essentially colors = (true_radius - true_radius.min())/true_radius.ptp()).
The result:
Finally, note that I generated the radii such that the cylinder doesn't close seamlessly. This mimicks your random example input. There's nothing you can do about this as long as the radii are not 2π-periodic in theta. This has nothing to do with visualization, this is geometry.

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