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.
Related
How to plot this kind of thermal plot in Python? I tried to search for any sample plot like this but didn't find one.
This image I got from the internet. I want to plot something same like this:
FROM
TO
To represent this type of data the canonical solution is, of course, a heat map. Here it is the code to produce both the figures at the top of this post.
import numpy as np
import matplotlib.pyplot as plt
t = np.linspace(0, 5, 501)
x = np.linspace(0, 1, 201)[:, None]
T = 50 + (30-6*t)*(4*x*(1-x)) + 4*t
fig, ax = plt.subplots(layout='constrained')
hm = ax.imshow(T, cmap='plasma',
aspect='auto', origin='lower', extent=(0, 5, 0, 1))
fig.colorbar(hm)
def heat_lines(x, t, T, n):
from matplotlib.cm import ScalarMappable
from matplotlib.collections import LineCollection
lx, lt = T.shape
ones = np.ones(lx)
norm = plt.Normalize(np.min(T), np.max(T))
plasma = plt.cm.plasma
fig, ax = plt.subplots(figsize=(1+1.2*n, 9), layout='constrained')
ax.set_xlim((-0.6, n-0.4))
ax.set_ylim((x[0], x[-1]))
ax.set_xticks(range(n))
ax.tick_params(right=False,top=False, bottom=False)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.grid(axis='y')
fig.colorbar(ScalarMappable(cmap=plasma, norm=norm))
dt = round(lt/(n-1))
for pos, ix in enumerate(range(0, len(t)+dt//2, dt)):
points = np.array([ones*pos, x[:,0]]).T.reshape(-1,1,2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
lc = LineCollection(segments, linewidth=72, ec=None,
color=plasma(norm(T[:,ix])))
lc.set_array(T[:,ix])
ax.add_collection(lc)
heat_lines(x, t, T, 6)
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()
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:
I am trying to plot the path taken by some input function (whose vertices are recorded in a numpy array)
I want to add an arrow-head (any direction marker) for every vertex of the form "→→→→" to track the direction of path.
I know the FancyArrowPatch which adds only one arrow-head a the terminal vertex, of the form: "————>". That's NOT what I want. [for reasons that are outside the scope of this question]
Currently, my code looks like this: Note, we can't guess the direction.
def plot_track(self, verts: np.array) -> None:
'''Plot followed track: verts is 2D array: x, y'''
track = Path(verts)
patch = PathPatch(path=track, edgecolor="#FF0000",
fill=False)
self.axs.add_patch(patch)
self.fig.canvas.draw()
self.root.mainloop()
matplotlib.patches.Arrow can be used to draw arrows. A loop needs to visit each vertex and its successor. A similar approach can be used with FancyArrowPatch.
import matplotlib.pyplot as plt
from matplotlib.patches import Arrow
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch, Path, Arrow
import numpy as np
def plot_track(verts, ax, **kw_args):
'''Plot followed track: verts is 2D array: x, y'''
for xy0, xy1 in zip(verts[:-1], verts[1:]):
patch = Arrow(*xy0, *(xy1 - xy0), **kw_args)
ax.add_patch(patch)
ax.relim()
ax.autoscale_view()
fig, ax = plt.subplots()
t = np.arange(2, 11, 1)
x = t * np.sin(t)
y = t * np.cos(t)
verts = np.vstack([x, y]).T
plot_track(verts - np.array([7, 0]), ax, color='red', fill=True, width=1)
plot_track(verts + np.array([7, 0]), ax, color='red', fill=False, width=1)
plt.show()
I want to draw the graph like the picture below. Its x-axis is the order of the data points, e.g. from 1 to 7. The y-axis is the scale from 0 to 25. If I want to draw a triangle, for example, with its data (1,22,20), then '1' gives the order among all data points(different triangles), the triangle should be drew in most left; "22,20" gives the "bottom-tip" of the triangle along the y-axis.
Does anyone know how to draw such triangle with multiple number in a graph using matplotlib python package?
Read this post and this post about drawing polygons with matplotlib.
EDIT1: Just saw #Poolka's answer. This was also my way to go, but notice that in one of the above links, it is stated, that adding single polygons (p = pat.Polygon([[x1, y1], [x2, y2], [x3, y3]); ax.add_patch(p)) to the figure can become very slow, and therefore collections are preferred.
EDIT 2: Also see TheImportanceOfBeingErnest's answer for a more elaborated version of this concept.
Together with this snippet of code, it should get you going:
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as pat # Patches like pat.Polygon()
from matplotlib.collections import PolyCollection # Collections of patches
test = ((1, 22, 20),
(2, 21, 19.5),
(3, 18, 20)) # Test data
triangles = []
fig, ax = plt.subplots(1, 1)
for t in test:
xmid = t[0] # Middle x-coord
xleft = t[0] - 0.5
xright = t[0] + 0.5 # Use fixed width of 0.5
y1 = t[1] # y-coords
y2 = t[2]
coordinates = [[xleft, y1], [xright, y1], [xmid, y2]]
print(coordinates)
triangles.append(coordinates) # Append to collection
z = np.random.random(len(triangles))
collec = PolyCollection(triangles, array=z, cmap=matplotlib.cm.viridis)
ax.add_collection(collec) # Plot polygon collection
ax.autoscale_view()
plt.show()
Consider the following simple example:
import matplotlib.pyplot as plt
# data
data = [[1, 22, 20], [3, 20, 25]]
plt.figure()
for val in data:
# coordinates
dy = val[1] - val[2]
dx = abs(dy) / 2
x0 = val[0]
y0 = val[1]
# drawing
triangle = plt.Polygon([[x0, y0], [x0 - dx, y0 + dy], [x0 + dx, y0 + dy]])
plt.gca().add_patch(triangle)
# misc
plt.grid()
plt.axis('square')
# these 2 lines are needed because patches in matplotlib do not adjust axes limits automatically, another approach is to add some data to the figure with plot, scatter, etc.
plt.xlim([-20, 20])
plt.ylim([0, 40])
Result is:
Using a PolyCollection (as shown in #cripcate's answer) is advantageous in this case. A more condensed version using a single numpy array could look like this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import PolyCollection
def triangle_collection(d, ax=None, width=0.4, **kwargs):
ax = ax or plt.gca()
verts = np.c_[d[:,0]-width/2, d[:,1], d[:,0]+width/2,
d[:,1], d[:,0], d[:,2]].reshape(len(d),3,2)
c = PolyCollection(verts, **kwargs)
ax.add_collection(c)
ax.autoscale()
return c
data = np.array([(1,22,20), (2,21,19.5), (3,18,20),
(4,17,19), (5,15,17), (6,11,8.5), (7,14,12)])
fig, ax = plt.subplots()
fig.subplots_adjust(left=0.3, right=0.7)
triangle_collection(data, facecolors=plt.cm.tab10(np.arange(len(data))))
plt.show()