I am trying to plot arrows pointing at a point on a curve in python using matplotlib.
On this line i need to point vertical arrows at specific points.
This is for indicating forces acting on a beam, so their direction is very important. Where the curve is the beam and the arrow is the force.
I know the coordinate of said point, exactly, but it is of cause changing with the input.
This input should also dictate whether the arrow points upwards or downwards from the line. (negative and positive forces applied).
I have tried endlessly with plt.arrow, but because the scale changes drastically and so does the quadrant in which the arrow has to be. So it might have to start at y < 0 and end in a point where y > 0.
The problem is that the arrowhead length then points the wrong way like this --<. instead of -->.
So before I go bald because of this, I would like to know if there is an easy way to apply a vertical arrow (could be infinite in the opposite direction for all i care) pointing to a point on a curve, of which I can control whether it point upwards to the curve, or downwards to the curve.
I'm not sure I completely follow you, but my approach would be to use annotate rather than arrow (just leave the text field blank). You can specify one end of the arrow in data coordinates and the other in offset pixels: but you do have to map your forces (indicating the length of the arrows) to number of pixels. For example:
import matplotlib.pyplot as plt
import numpy as np
# Trial function for adding vertical arrows to
def f(x):
return np.sin(2*x)
x = np.linspace(0,10,1000)
y = f(x)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x,y, 'k', lw=2)
ax.set_ylim(-3,3)
def add_force(F, x1):
"""Add a vertical force arrow F pixels long at x1 (in data coordinates)."""
ax.annotate('', xy=(x1, f(x1)), xytext=(0, F), textcoords='offset points',
arrowprops=dict(arrowstyle='<|-', color='r'))
add_force(60, 4.5)
add_force(-45, 6.5)
plt.show()
The inverted arrowhead is due to a negative sign of the head_length variable. Probably you are scaling it using a negative value. Using head_length= abs(value)*somethingelse should take care of your problem.
Related
I have the equation: z(x,y)=1+x^(2/3)y^(-3/4)
I would like to calculate values of z for x=[0,100] and y=[10^1,10^4]. I will do this for 100 points in each axis direction. My grid, then, will be 100x100 points. In the x-direction I want the points spaced linearly. In the y-direction I want the points space logarithmically.
Were I to need these values I could easily go through the following:
x=np.linspace(0,100,100)
y=np.logspace(1,4,100)
z=np.zeros( (len(x), len(y)) )
for i in range(len(x)):
for j in range(len(y)):
z[i,j]=1+x[i]**(2/3)*y[j]**(-3/4)
The problem for me comes with visualizing these results. I know that I would need to create a grid of points. I feel my options are to create a meshgrid with the values and then use pcolor.
My issue here is that the values at the center of the block do not coincide with the calculated values. In the x-direction I could fix this by shifting the x-vector by half of dx (the step between successive values). I'm not so sure how I would do this for the y-axis. Furthermore, If I wanted to compute values for each of the y-direction values, including the end points, they would not all show up.
In the final visualization I would like to have the y-axis as a log scale and the x axis as a linear scale. I would also like the tick marks to fall in the center of the cells, correlating with the correct value. Can someone point me to the correct plotting functions for this. I have to resolve the issue using pcolor or pcolormesh.
Should you require more details, please let me know.
In current matplotlib, you can use pcolormesh with shading='nearest', and it will center the blocks with the values:
import matplotlib.pyplot as plt
y_plot = np.log10(y)
z[5, 5] = 0 # to make it more evident
plt.pcolormesh(x, y_plot, z, shading="nearest")
plt.colorbar()
ax = plt.gca()
ax.set_xticks(x)
ax.set_yticks(y_plot)
plt.axvline(x[5])
plt.axhline(y_plot[5])
Output:
While working on improving my answer to this question, I have stumbled into a dead end.
What I want to achieve, is create a "fake" 3D waterfall plot in matplotlib, where individual line plots (or potentially any other plot type) are offset in figure pixel coordinates and plotted behind each other. This part works fine already, and using my code example (see below) you should be able to plot ten equivalent lines which are offset by fig.dpi/10. in x- and y-direction, and plotted behind each other via zorder.
Note that I also added fill_between()'s to make the "depth-cue" zorder more visible.
Where I'm stuck is that I'd like to add a "third axis", i.e. a line (later on perhaps formatted with some ticks) which aligns correctly with the base (i.e. [0,0] in data units) of each line.
This problem is perhaps further complicated by the fact that this isn't a one-off thing (i.e. the solutions should not only work in static pixel coordinates), but has to behave correctly on rescale, especially when working interactively.
As you can see, setting e.g. the xlim's allows one to rescale the lines "as expected" (best if you try it interactively), yet the red line (future axis) that I tried to insert is not transposed in the same way as the bases of each line plot.
What I'm not looking for are solutions which rely on mpl_toolkits.mplot3d's Axes3D, as this would lead to many other issues regarding to zorder and zoom, which are exactly what I'm trying to avoid by coming up with my own "fake 3D plot".
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D,IdentityTransform
def offset(myFig,myAx,n=1,xOff=60,yOff=60):
"""
this function will apply a shift of n*dx, n*dy
where e.g. n=2, xOff=10 would yield a 20px offset in x-direction
"""
## scale by fig.dpi to have offset in pixels!
dx, dy = xOff/myFig.dpi , yOff/myFig.dpi
t_data = myAx.transData
t_off = mpl.transforms.ScaledTranslation( n*dx, n*dy, myFig.dpi_scale_trans)
return t_data + t_off
fig,axes=plt.subplots(nrows=1, ncols=3,figsize=(10,5))
ys=np.arange(0,5,0.5)
print(len(ys))
## just to have the lines colored in some uniform way
cmap = mpl.cm.get_cmap('viridis')
norm=mpl.colors.Normalize(vmin=ys.min(),vmax=ys.max())
## this defines the offset in pixels
xOff=10
yOff=10
for ax in axes:
## plot the lines
for yi,yv in enumerate(ys):
zo=(len(ys)-yi)
ax.plot([0,0.5,1],[0,1,0],color=cmap(norm(yv)),
zorder=zo, ## to order them "behind" each other
## here we apply the offset to each plot:
transform=offset(fig,ax,n=yi,xOff=xOff,yOff=yOff)
)
### optional: add a fill_between to make layering more obvious
ax.fill_between([0,0.5,1],[0,1,0],0,
facecolor=cmap(norm(yv)),edgecolor="None",alpha=0.1,
zorder=zo-1, ## to order them "behind" each other
## here we apply the offset to each plot:
transform=offset(fig,ax,n=yi,xOff=xOff,yOff=yOff)
)
##################################
####### this is the important bit:
ax.plot([0,2],[0,2],color='r',zorder=100,clip_on=False,
transform=ax.transData+mpl.transforms.ScaledTranslation(0.,0., fig.dpi_scale_trans)
)
## make sure to set them "manually", as autoscaling will fail due to transformations
for ax in axes:
ax.set_ylim(0,2)
axes[0].set_xlim(0,1)
axes[1].set_xlim(0,2)
axes[2].set_xlim(0,3)
### Note: the default fig.dpi is 100, hence an offset of of xOff=10px will become 30px when saving at 300dpi!
# plt.savefig("./test.png",dpi=300)
plt.show()
Update:
I've now included an animation below, which shows how the stacked lines behave on zooming/panning, and how their "baseline" (blue circles) moves with the plot, instead of the static OriginLineTrans solution (green line) or my transformed line (red, dashed).
The attachment points observe different transformations and can be inserted by:
ax.scatter([0],[0],edgecolors="b",zorder=200,facecolors="None",s=10**2,)
ax.scatter([0],[0],edgecolors="b",zorder=200,facecolors="None",s=10**2,transform=offset(fig,ax,n=len(ys)-1,xOff=xOff,yOff=yOff),label="attachment points")
The question boils down to the following:
How to produce a line that
starts from the origin (0,0) in axes coordinates and
evolves at an angle angle in physical coordinates (pixel space)
by using a matpotlib transform?
The problem is that the origin in axes coordinates may vary depending on the subplot position. So the only option I see is to create some custom transform that
transforms to pixel space
translates to the origin in pixel space
skews the coordinate system (say, in x direction) by the given angle
translates back to the origin of the axes
That could look like this
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms as mtrans
class OriginLineTrans(mtrans.Transform):
input_dims = 2
output_dims = 2
def __init__(self, origin, angle, axes):
self.axes = axes
self.origin = origin
self.angle = angle # in radiants
super().__init__()
def get_affine(self):
origin = ax.transAxes.transform_point(self.origin)
trans = ax.transAxes + \
mtrans.Affine2D().translate(*(-origin)) \
.skew(self.angle, 0).translate(*origin)
return trans.get_affine()
fig, ax = plt.subplots()
ax.plot([0,0], [0,1], transform=OriginLineTrans((0,0), np.arctan(1), ax))
plt.show()
Note that in the case of the original question, the angle would be np.arctan(dx/dy).
I'm currently analyzing some data by creating a vector plot. All the vectors have length 1 unit. Most show up fine, but certain vectors such as:
fig = plt.figure()
plt.axes(xlim=(-24, 24), ylim=(0, 150))
plt.quiver([-19.1038], [96.5851], [-19.1001+19.1038], [97.5832-96.5851],angles='xy', scale_units='xy', scale=1, headwidth=1, headlength=10, minshaft=5)
plt.show()
show up as a point. (Please note that I am not drawing my vectors individually like this; I only drew this particular one to try to debug my code.) This appears to only be occurring for nearly vertical vectors. I've also noticed that this issue is resolved if I "zoom in" on the vector (i.e. change the axis scaling). However, I cannot do that as many other vectors in my plot will be outside of the domain/range. Is there another way to fix this?
The problem is demonstrated in the below figure:
There are two components to your problem, and both have to do with how you chose to represent your data.
The default behaviour of quiver is to auto-scale your vectors to a reasonable size for a pretty result. The documentation says as much:
The default settings auto-scales the length of the arrows to a reasonable size. To change this behavior see the scale and scale_units kwargs.
And then
scale_units : [ ‘width’ | ‘height’ | ‘dots’ | ‘inches’ | ‘x’ | ‘y’ | ‘xy’ ], None, optional
[...]
If scale_units is ‘x’ then the vector will be 0.5 x-axis units. To plot vectors in the x-y plane, with u and v having the same units as x and y, use angles='xy', scale_units='xy', scale=1.
So in your case, you're telling quiver to plot the arrow in xy data units. Since your arrow is of unit length, it is drawn as a 1-length arrow. Your data limits, on the other hand, are huge: 40 units wide, 150 units tall. On this scale a length-1 arrow is just too small, and matplotlib decides to truncate the arrow and plot a dot instead.
If you zoom in, as you said yourself, the arrow appears. If we remove the parameters that turn your arrow into a toothpick, it turns out that the arrow you plot is perfectly fine if you look close enough (not the axes):
Now, the question is why this behaviour depends on the orientation of your vectors. The reason for this behaviour is that the x and y limits are different in your plot, so a unit-length horizontal line and a unit-length vertical line contain a different number of pixels (since your data is scaled in xy units). This implies that while horizontal arrows are long enough to be represented accurately, vertical ones become so short that matplotlib decides to truncate them to dots, which shouldn't be too obvious with the default arrow format, but it is pretty bad with your custom arrows. Your use case is such that the rendering cut-off used by matplotlib happens to fall between the length of your horizontal vectors and the length of your vertical ones.
You have two straightforward choices. One is to increase the scaling for your arrows to the point where every orientation is represented accurately. This would probably be the solution to Y in a small XY problem here. What you should really do, is represent your data accurately. Since you're plotting your vector field in xy data units, you presumably want your x and y axes to have equal sizes, and you want your arrows to have visually unit length (i.e. a length that's independent from their orientation).
So I suggest that you force your plot to have equal units on both axes, at the cost of ending up with a rectangular figure:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.axis('scaled') # <-- key addition
ax.axis([-24, 24, 0, 150])
ax.quiver([-19.1038], [96.5851], [-19.1001+19.1038], [97.5832-96.5851],
angles='xy', scale_units='xy', scale=1, headwidth=1,
headlength=10, minshaft=5)
plt.show()
Trust me: there's a tiny arrow in there. The main point is that this way either all of your vectors will be dots (if you're zoomed out too much), or neither of them will. Then you have a sane situation, and can choose the overall scaling of your vectors accordingly.
I am trying to make a stack plot where the bins don't seem to be aligning correctly with the data. What I have plotted is the proportion of something in a sphere as you go radially outward from the center. The error became visible to me in the rightmost section of this plot. The lighter blue should be a vertical column of one width. Instead the dark blue seems to slant into the lighter blue section.
What I believe is the problem is that the data are not evenly spaced. For example: at a radius of 300 I might have a certain proportion value. Then at a radius of 330 I might have another, then the next at 400.
I had thought that stackplot would be able to take care of this but it appears not. Is there a way for me to straighten up these columns of data?
Source Code:
def phaseProp(rad,phase):
#phaseLabel = np.array(['coe','en','fs','olv','maj','perov','ppv','ring','wad','per','wust','st'])
#print phaseLabel
rad = rad/1000.
phase = phase/100.
print phase[:,:]
#print phase[:,0]
fig, ax = plt.subplots(figsize = (15,10))
ax.stackplot(rad[:],phase[:,0],phase[:,1],phase[:,2],phase[:,3],phase[:,4], \
phase[:,5],phase[:,6],phase[:,7],phase[:,8], \
phase[:,9] ,phase[:,10],phase[:,11],phase[:,12], \
colors = ['gainsboro','gold','lightsage','darkorange','tomato','indianred',\
'darksage','sage','palevioletred','darkgrey','dodgerblue' ,'mediumblue' ,'darkblue' ])
plt.legend([mpatches.Patch(color='gainsboro'),
mpatches.Patch(color='gold'),
mpatches.Patch(color='lightsage'),
mpatches.Patch(color='darkorange'),
mpatches.Patch(color='tomato'),
mpatches.Patch(color='indianred'),
mpatches.Patch(color='darksage'),
mpatches.Patch(color='sage'),
mpatches.Patch(color='palevioletred'),
mpatches.Patch(color='darkgrey'),
mpatches.Patch(color='dodgerblue'),
mpatches.Patch(color='mediumblue'),
mpatches.Patch(color='darkblue')],
['coe','opx','ol','gt','pv','ppv','rw','wad','fp','st','h2o','iceIh','iceVII'],\
loc='upper center', bbox_to_anchor=(0.5, 1.127),fancybox=True, shadow=True, ncol=5,fontsize='20')
plt.ylabel(r'Phase Proportion',fontsize = 34)
plt.xlabel(r'Radius (km)',fontsize = 34)
plt.tick_params(axis='both', which='both', labelsize=32)
plt.xlim(rad[noc+1],rad[nr])
plt.ylim(0,1.0)
#ax.stackplot(rad,phase)
#plt.gca().invert_xaxis()
plt.show()
I've had a look at your problem and I think the problem lies with the fact that the last two points for the H20 line are (7100,0) and (7150,1) therefore it simply slopes up as you are seeing.
However it is very simple to add an additional point to give a square edge:
rad_amended = np.hstack((rad,rad[-1])) #extend the array by 1
rad_amended[-2] = rad[-2] +1 #alter the penultimate value
phase_amended = np.vstack((phase,phase[-1])) #extend the Y values
nr+=1 #extend the range of the x-axis
phaseProp(rad_amended,phase_amended)
This principle could be extended for the full dataset and give square edges to every Area, but I assume you are happy with the rest of the graph?
I'm trying to fill the area under a curve with matplotlib. The script below works fine.
import matplotlib.pyplot as plt
from math import sqrt
x = range(100)
y = [sqrt(i) for i in x]
plt.plot(x,y,color='k',lw=2)
plt.fill_between(x,y,0,color='0.8')
plt.show()
However if I set the y-scale to logarithmic (see below). It sometimes fills the area above the curve ! Can anyone help me? I would like to fill the area between the curve and y = 0.
x = range(100)
y = [sqrt(i) for i in x]
plt.plot(x,y,color='k',lw=2)
plt.fill_between(x,y,0,color='0.8')
plt.yscale('log')
plt.show()
Thanks in advance!
With a logarithmic y-scale, fill_between(x, y, 0) tells matplotlib to fill the region between log(0) = -infinity and log(y). Naturally, it balks. You can avoid the problem by changing 0 to some small number like 1e-6.
As mentioned, 0 -> -inf in a log scale. Thus, any plotted value that was less than or equal to zero would be problematic (requiring an infinite ylim in log space). This problem exists independently of whether you are using fill_between() or not.
Fortunately, matplotlib provides a way to handle this nicely. In the default behavior, matplotlib masks the values of every value less than or equal to zero. In your example, this means that your entire y=0 line is masked and excluded from the polygon defining the filled-between area. The result is that the polygon is simply closed by drawing a line from (100,10) down and leftward to (0,0). Another option is to clip the values. In this case, they are set to 1e-300 and are not consulted when determining the ylim of the plot. So to get your desired result, do the following:
plt.yscale('log', nonposy='clip')