I asked earlier on the matplotlib-user mailing list so apologies for the cross-post.
Say I have a marker with a known size in points and I want to draw an arrow to this point. How can I get the ends points for the arrow? As you can see in the below, it overlaps the markers. I want to go to the edge. I can use shrinkA and shrinkB to do what I want, but I don't see how they're related to the points size**.5. Or should I somehow do the transformation using the known angle between the two points and the point itself. I don't know how to translate a point in data coordinates and the offset it in a certain direction by size**.5 points. Can anyone help clear this up?
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
point1 = (138.21, 19.5)
x1, y1 = point1
point2 = (67.0, 30.19)
x2, y2 = point2
size = 700
fig, ax = plt.subplots()
ax.scatter(*zip(point1, point2), marker='o', s=size)
# if I need to get and use the angles
dx = x2 - x1
dy = y2 - y1
d = np.sqrt(dx**2 + dy**2)
arrows = FancyArrowPatch(posA=(x1, y1), posB=(x2, y2),
color = 'k',
arrowstyle="-|>",
mutation_scale=700**.5,
connectionstyle="arc3")
ax.add_patch(arrows)
Edit: I made a little more progress. If I read the Translations Tutorial correctly, then this should give me a point on the radius of the markers. However, as soon as you resize the Axes then the transformation will be off. I'm stumped on what else to use.
from matplotlib.transforms import ScaledTranslation
# shift size points over and size points down so you should be on radius
# a point is 1/72 inches
dpi = ax.figure.get_dpi()
node_size = size**.5 / 2. # this is the radius of the marker
offset = ScaledTranslation(node_size/dpi, -node_size/dpi, fig.dpi_scale_trans)
shadow_transform = ax.transData + offset
ax.plot([x2], [y2], 'o', transform=shadow_transform, color='r')
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
from matplotlib.transforms import ScaledTranslation
point1 = (138.21, 19.5)
x1, y1 = point1
point2 = (67.0, 30.19)
x2, y2 = point2
size = 700
fig, ax = plt.subplots()
ax.scatter(*zip(point1, point2), marker='o', s=size)
# if I need to get and use the angles
dx = x2 - x1
dy = y2 - y1
d = np.sqrt(dx**2 + dy**2)
arrows = FancyArrowPatch(posA=(x1, y1), posB=(x2, y2),
color = 'k',
arrowstyle="-|>",
mutation_scale=700**.5,
connectionstyle="arc3")
ax.add_patch(arrows)
# shift size points over and size points down so you should be on radius
# a point is 1/72 inches
def trans_callback(event):
dpi = fig.get_dpi()
node_size = size**.5 / 2. # this is the radius of the marker
offset = ScaledTranslation(node_size/dpi, -node_size/dpi, fig.dpi_scale_trans)
shadow_transform = ax.transData + offset
arrows.set_transform(shadow_transform)
cid = fig.canvas.mpl_connect('resize_event', trans_callback)
You also need to include something about the aspect ratio of the axes get points on the rim of the point (because the shape of the marker in data units in an ellipse unless the aspect ratio = 1)
Related
I have two lines:
y=3x-4
y=-x+5
They intersect and form 4 regions in space (see image below).
Mathematically, the regions can be determined with the following inequations:
Region1: y<3x-4 & y>-x+5
Region2: y>3x-4 & y>-x+5
Region3: y>3x-4 & y<-x+5
Region4: y<3x-4 & y<-x+5
I want to fill all of those regions independently. However when I try to use plt.fill_between I can only get regions 1 and/or 3. How can I fill regions 2 and 4 using matplotlib and fill_between?
The code that I tried for region 3 is the following (source):
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(0,6,0.1)
y = np.arange(0,6,0.1)
# The lines to plot
y1 = 3*x - 4
y2 = -x+5
# The upper edge of polygon (min of lines y1 & y2)
y3 = np.minimum(y1, y2)
# Set y-limit, making neg y-values not show in plot
plt.ylim(0, 6)
plt.xlim(0, 6)
# Plotting of lines
plt.plot(x, y1,
x, y2)
# Filling between (region 3)
plt.fill_between(x, y2, y3, color='grey', alpha=0.5)
plt.show()
It's because you have set the limits incorrectly. Setting them as 2 horizontal lines, 6 and 0, does the trick. Note that I have changed the step of arange because, to have a neat picture, the intersection point (2.25 in your case) needs to belong to x range
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(0,6,0.05)
# The lines to plot
y1 = 3*x - 4
y2 = -x+5
# Set y-limit, making neg y-values not show in plot
plt.ylim(0, 6)
plt.xlim(0, 6)
# Filling between (region 3)
plt.fill_between(x, np.maximum(y1, y2), [6] * len(x), color='grey', alpha=0.5)
plt.fill_between(x, np.minimum(y1, y2), [0] * len(x), color='cyan', alpha=0.5)
plt.show()
The trick here is to define a new polygon based on the intersections of the lines, and use $y(x) = 0$ as your bottom border.
The implementation of this requires the following code. First we externally calculate and hardcode the intersection points between the lines and the a-axis:
# calculate intersections between lines and x axis
intersect_x = 9/4
y1_0 = 4/3
y2_0 = 5
Once we have those stored, we go through every x value. If the x value is greater than our starting point (where the climbing curve crosses the x-axis) but less than the intersection point, we add the climbing curve point at that x value. If the x-value is greater than the intersection point but less than the end point (where the falling curve crosses the x-axis), we add the falling curve point at that x value. At each step, we add a y=0 point to a new list to be our floor for the fill-in operation.
# here we manually perform the operation you were trying to do
# with np.minimum; we also add an (x, 0) point for
# every point we add to our new polygon
new_x = []
new_y = []
bottom_y = []
for x_dx, xx in enumerate(x):
if xx > y1_0 and xx < intersect_x:
new_x.append(xx)
new_y.append(y1[x_dx])
bottom_y.append(0)
elif xx < y2_0 and xx > intersect_x:
new_x.append(xx)
new_y.append(y2[x_dx])
bottom_y.append(0)
Now we just replace our fill_between code with the new curves:
plt.fill_between(new_x, new_y, bottom_y, color='grey', alpha=0.5)
This is the full code:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(0,6,0.1)
y = np.arange(0,6,0.1)
# The lines to plot
y1 = 3*x - 4
y2 = -x+5
# calculate intersections between lines and x axis
intersect_x = 9/4
y1_0 = 4/3
y2_0 = 5
# here we manually perform the operation you were trying to do
# with np.minimum; we also add an (x, 0) point for
# every point we add to our new polygon
new_x = []
new_y = []
bottom_y = []
for x_dx, xx in enumerate(x):
if xx > y1_0 and xx < intersect_x:
new_x.append(xx)
new_y.append(y1[x_dx])
bottom_y.append(0)
elif xx < y2_0 and xx > intersect_x:
new_x.append(xx)
new_y.append(y2[x_dx])
bottom_y.append(0)
# The upper edge of polygon (min of lines y1 & y2)
y3 = np.minimum(y1, y2)
# Set y-limit, making neg y-values not show in plot
plt.ylim(0, 6)
plt.xlim(0, 6)
# Plotting of lines
plt.plot(x, y1,
x, y2)
# Filling between (region 3)
plt.fill_between(new_x, new_y, bottom_y, color='grey', alpha=0.5)
plt.show()
And this is the result:
Happy coding!
The normal vector is calculated with the cross product of two vectors on the plane, so it shoud be perpendicular to the plane. But as you can seein the plot the normal vector produced with quiver isn't perpendicular.
Is the calculation of the plane wrong, my normal vector or the way i plot the normal vector?
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
points = [[3.2342, 1.8487, -1.8186],
[2.9829, 1.6434, -1.8019],
[3.4247, 1.5550, -1.8093]]
p0, p1, p2 = points
x0, y0, z0 = p0
x1, y1, z1 = p1
x2, y2, z2 = p2
ux, uy, uz = u = [x1-x0, y1-y0, z1-z0] #first vector
vx, vy, vz = v = [x2-x0, y2-y0, z2-z0] #sec vector
u_cross_v = [uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx] #cross product
point = np.array(p1)
normal = np.array(u_cross_v)
d = -point.dot(normal)
print('plane equation:\n{:1.4f}x + {:1.4f}y + {:1.4f}z + {:1.4f} = 0'.format(normal[0], normal[1], normal[2], d))
xx, yy = np.meshgrid(range(10), range(10))
z = (-normal[0] * xx - normal[1] * yy - d) * 1. / normal[2]
# plot the surface
plt3d = plt.figure().gca(projection='3d')
plt3d.quiver(x0, y0, z0, normal[0], normal[1], normal[2], color="m")
plt3d.plot_surface(xx, yy, z)
plt3d.set_xlabel("X", color='red', size=18)
plt3d.set_ylabel("Y", color='green', size=18)
plt3d.set_zlabel("Z", color='b', size=18)
plt.show()
Actually, your plot is 100% correct. The scale of your Z axis does not correspond to the same scale on X & Y axis. If you use a function to set the scale correct, you can see that:
...
plt3d.set_zlabel("Z", color='b', size=18)
# insert these lines
ax = plt.gca()
set_axis_equal(ax)
plt.show()
and the corresponding function from this post:
def set_axes_radius(ax, origin, radius):
'''
From StackOverflow question:
https://stackoverflow.com/questions/13685386/
'''
ax.set_xlim3d([origin[0] - radius, origin[0] + radius])
ax.set_ylim3d([origin[1] - radius, origin[1] + radius])
ax.set_zlim3d([origin[2] - radius, origin[2] + radius])
def set_axes_equal(ax, zoom=1.):
'''
Make axes of 3D plot have equal scale so that spheres appear as spheres,
cubes as cubes, etc.. This is one possible solution to Matplotlib's
ax.set_aspect("equal") and ax.axis("equal") not working for 3D.
input:
ax: a matplotlib axis, e.g., as output from plt.gca().
'''
limits = np.array([
ax.get_xlim3d(),
ax.get_ylim3d(),
ax.get_zlim3d(),
])
origin = np.mean(limits, axis=1)
radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0])) / zoom
set_axes_radius(ax, origin, radius)
Given a center and two angles of a rotated ellipse of Arc from matplotlib.patches, I want to plot the two lines starting from the center of the Arc to the ends of the Arc.
Here is a piece of code that does that:
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
fig, ax = plt.subplots(1,1)
r = 2 #Radius
a = 0.2*r #width
b = 0.5*r #height
#center of the ellipse
x = 0.5
y = 0.5
ax.add_patch(Arc((x, y), a, b, angle = 20,
theta1=0, theta2=120,
edgecolor='b', lw=1.1))
#Now look for the ends of the Arc and manually set the limits
ax.plot([x,0.687],[y,0.567], color='r',lw=1.1)
ax.plot([x,0.248],[y,0.711], color='r',lw=1.1)
plt.show()
Which results in
.
Here the red lines were drawn looking carefully at the ends of the arc. However, as Arc does not allow to fill the arc for optimization, I wonder if there is a way to do it automatically for any center and angles.
According to Wikipedia an ellipse in its polar form looks like
Using this you may calculate the end points of the lines.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
fig, ax = plt.subplots(1,1)
r = 2 #Radius
a = 0.2*r #width
b = 0.5*r #height
#center of the ellipse
x = 0.5; y = 0.5
# angle
alpha = 20
ax.add_patch(Arc((x, y), a, b, angle = alpha,
theta1=0, theta2=120,
edgecolor='b', lw=1.1))
def ellipse(x0,y0,a,b,alpha,phi):
r = a*b/np.sqrt((b*np.cos(phi))**2 + (a*np.sin(phi))**2)
return [x0+r*np.cos(phi+alpha), y0+r*np.sin(phi+alpha)]
x1,y1 = ellipse(x, y, a/2., b/2., np.deg2rad(alpha), np.deg2rad(0))
ax.plot([x,x1],[y,y1], color='r',lw=1.1)
x2,y2 = ellipse(x, y, a/2., b/2., np.deg2rad(alpha), np.deg2rad(120))
ax.plot([x,x2],[y,y2], color='r',lw=1.1)
ax.set_aspect("equal")
plt.show()
I want to plot a circle with specified center position and radius and plot 100 random points between [0-500] which 80% of them in circle and 20% around with different colors.
To solve this problem I used Plot circle that contains 80% (x, y) points and customized based on my requirements but it's not working.
import numpy as np
import matplotlib.pyplot as plt
n = 100
low = 0
high = 500
x = np.random.random_integers(low, high, n)
y = np.random.random_integers(low, high, n)
x0 = y0 = 250
r = 200
#t = 80 # percent
#r0 = np.percentile(r, t)
plt.plot(x, y, '.')
circle = plt.Circle((x0, y0), r, color='black', fill=False, linestyle='--')
plt.plot(x0, y0, color='black', marker='^')
plt.gca().add_artist(circle)
plt.axis([0, 500, 0, 500])
plt.show()
I found the answer of my question.
we can use the equation of circle to check the point is in the circle or not.
(x-x0)**2+(y-y0)**2 < r**2
center:(x0, y0)
radius:r
Is there a way to get matplotlib to plot a perfect circle? They look more like ovals.
Just to expand on DSM's correct answer. By default, plots have more pixels along one axis over the other. When you add a circle, it's traditionally added in data units. If your axes have a symmetric range, that means one step along the x axis will involve a different number of pixels than one step along your y axis. So a symmetric circle in data units is asymmetric in your Pixel units (what you actually see).
As DSM correctly pointed out, you can force the x and y axes to have equal number of pixels per data unit. This is done using the plt.axis("equal") or ax.axis("equal") methods (where ax is an instance of an Axes).
You can also draw an Ellipse such that it is appropriately scaled to look like a circle on your plot. Here's an example of such a case:
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Circle
fig = plt.figure()
ax1 = fig.add_subplot(211)
# calculate asymmetry of x and y axes:
x0, y0 = ax1.transAxes.transform((0, 0)) # lower left in pixels
x1, y1 = ax1.transAxes.transform((1, 1)) # upper right in pixes
dx = x1 - x0
dy = y1 - y0
maxd = max(dx, dy)
width = .15 * maxd / dx
height = .15 * maxd / dy
# a circle you expect to be a circle, but it is not
ax1.add_artist(Circle((.5, .5), .15))
# an ellipse you expect to be an ellipse, but it's a circle
ax1.add_artist(Ellipse((.75, .75), width, height))
ax2 = fig.add_subplot(212)
ax2.axis('equal')
# a circle you expect to be a circle, and it is
ax2.add_artist(Circle((.5, .5), .15))
# an ellipse you expect to be an ellipse, and it is
ax2.add_artist(Ellipse((.75, .75), width, height))
fig.savefig('perfectCircle1.png')
resulting in this figure:
Alternatively, you can adjust your figure so that the Axes are square:
# calculate dimensions of axes 1 in figure units
x0, y0, dx, dy = ax1.get_position().bounds
maxd = max(dx, dy)
width = 6 * maxd / dx
height = 6 * maxd / dy
fig.set_size_inches((width, height))
fig.savefig('perfectCircle2.png')
resulting in:
Notice how the second axes, which has the axis("equal") option, now has the same range for the x and y axes. The figure has been scaled so that the date units of each are represented by the same number of pixels.
You can also adjust your axes to be square, even if the figure is not. Or you can change the default transform for the Circle to None, which means the units used are pixels. I'm having difficulty successfully doing this at the moment (the circle is a circle, but not where I want it to be).
I believe the simpler thing to do is adding the following:
ax.set_aspect('equal')
I've encounter the same problem today and I think I might have a more flexible solution. Two main problems remain with the previous answer (if you don't use the equal aspect function). First if you resize your whole graph, the proportion will not be the same since the number of pixels will change. Second point, this trick do not work if you don't have the same lim for the xaxis and the yaxis.
This solution tricks mpl using a custom object. Indeed, whenever you change one of your axis lim or your graph size, mpl will call an internal function which will take the width and height value of the ellipse multiplied by the transform function value. Since the width and height value is stored in the ellipse object, one way is to create a custom object with a value updated whenever the function is called, based on the current ax properties :
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
class GraphDist() :
def __init__(self, size, ax, x=True) :
self.size = size
self.ax = ax
self.x = x
#property
def dist_real(self) :
x0, y0 = self.ax.transAxes.transform((0, 0)) # lower left in pixels
x1, y1 = self.ax.transAxes.transform((1, 1)) # upper right in pixes
value = x1 - x0 if self.x else y1 - y0
return value
#property
def dist_abs(self) :
bounds = self.ax.get_xlim() if self.x else self.ax.get_ylim()
return bounds[0] - bounds[1]
#property
def value(self) :
return (self.size / self.dist_real) * self.dist_abs
def __mul__(self, obj) :
return self.value * obj
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_xlim((0,10))
ax.set_ylim((0,5))
width = GraphDist(10, ax, True)
height = GraphDist(10, ax, False)
ax.add_artist(Ellipse((1, 3), width, height))
plt.show()