pyplot drawing on an angle - python

I'm trying to draw a couple of vectors with pyplot.
My approach is to define first the plot size (or axis values), as this will change depending on the job run.
pretty much what i need to do is to draw a straight, upwards-facing vector.
So for the test i am using the same values for x-axis, and two different ones for Y (as that would give me an vertical vector)
problem I face is - whatever i am doing, i can't seem to make the vector straight. It always gets rotated.
Does anyone know what reason there might be for it ?
my current code
import matplotlib.pyplot as plt
import math
start = (162305,299400,162890,299400) #define coordinates for vector
#define plot axis values
top = 163440
bottom = 161910
left = 299595
right = 300510
fig, ax = plt.subplots()
ax.quiver(start[0],start[1],start[2],start[3])
plt.xlim(left,right)
plt.ylim(bottom,top)
plt.autoscale()
plt.show()
and the resulting plot

Hope this would help!
import matplotlib.pyplot as plt
import math
start = (162305,299400,162890,299400) #define coordinates for vector
#define plot axis values
top = 163440
bottom = 161910
left = 299595
right = 300510
fig, ax = plt.subplots()
ax.quiver(start[0],start[1],1,0)
#ax.quiver(x_pos, y_pos, x_dir, y_dir, color)
#give the x_pos, y_pos value
#x_dir=1, y_dir=1, meaning 45 degree angle
#x_dir=1, y_dir=0, meaning 0 degree angle
#x_dir=0, y_dir=1, meaning 90 degree angle
# you put x_dir=start[2] and y_dir=start[3], making an arbitary angle of atan
#(y_dir/x_dir) which makes the angled arrow
plt.xlim(left,right)
plt.ylim(bottom,top)
plt.autoscale()
plt.show()

Related

Can't draw circle with right proportions Matplotlib Python

I want to draw Circle on my plot. For this purpose I decided to use patch.Circle class from matplotlib. Cirlce object uses radius argument to set a radius of a circle, but if the axes ratio is not 1 (see my plot), how to draw circle with right proportions?
My code for drawing circle is:
rect = patches.Circle(xy=(9, yaxes),radius= 2, linewidth=3, edgecolor='r', facecolor='red',alpha=0.5)
ax.add_patch(rect)
yaxes is equal 206 in this example (because I wanted to draw it upper left coner).
Here is a picture I got using this code:
But I want something like this:
You could use ax.transData to transform 1,1 vs 0,0 and obtain the deformation in x vs y direction. That ratio can be used to know the horizontal versus the vertical size of the circle.
If you just need to place a circle using coordinates relative to the axes, plt.scatter with transform=ax.transAxes can be used. Note that the size is an "area" measure based on "points" (a "point" is 1/72th of an inch).
The following example code uses the data coordinates to position the "circle" (using an ellipse) and the x-coordinates for the radius. A red circle is placed using axes coordinates.
from matplotlib import pyplot as plt
from matplotlib.patches import Ellipse
import pandas as pd
import numpy as np
# plot some random data
np.random.seed(2021)
df = pd.DataFrame({'y': np.random.normal(10, 100, 50).cumsum() + 2000},
index=np.arange(101, 151))
ax = df.plot(figsize=(12, 5))
# find an "interesting" point
max_ind = df['y'].argmax()
max_x = df.index[max_ind]
max_y = df.iloc[max_ind]['y']
# calculate the aspect ratio
xscale, yscale = ax.transData.transform([1, 1]) - ax.transData.transform([0, 0])
# draw the ellipse to be displayed as circle
radius_x = 4
radius_y = radius_x * xscale / yscale
ax.add_patch(Ellipse((max_x, max_y), radius_x, radius_y, color='purple', alpha=0.4))
# use ax.scatter to draw a red dot at the top left
ax.scatter(0.05, 0.9, marker='o', s=2000, color='red', transform=ax.transAxes)
plt.show()
Some remarks about drawing the ellipse:
this will only work for linear coordinates, not e.g. for logscale or polar coordinates
the code supposes nor the axis limits nor the axis position will change afterwards, as these will distort the aspect ratio
The issue seems to be that your X (passed to xy=) is not always the same as your Y, thus the oval instead of a perfect circle.

How to rotate theta ticklabels in a matplotlib polar plot?

In a matplotlib polar plot, I would like to rotate each individual theta ticklabel by a different angle. However, I cannot find anything in the documentation to do that. Here's a simple plot to illustrate:
from matplotlib import pyplot as plt
import numpy as np
fig = plt.figure()
ax = plt.axes(polar=True)
ax.set_thetalim(0., np.pi/4.)
ax.set_rlim(0., 2.)
# set the size of theta ticklabels (works)
thetatick_locs = np.linspace(0.,45.,4)
thetatick_labels = [u'%i\u00b0'%np.round(x) for x in thetatick_locs]
ax.set_thetagrids(thetatick_locs, thetatick_labels, fontsize=16)
This adds labels at 0, 15, 30 and 45 degrees. What I'd like to do is rotate the 15 degree label by 15 degrees, the 30 degree label by 30 degrees, and so on, so that each label's text direction is radially outward. Since get_xticklabels on a PolarAxes instance seems to get the theta ticklabels, I tried:
for i,t in enumerate(ax.get_xticklabels()):
t.set_rotation(thetatick_locs[i])
However, that did nothing. Is there any other way of doing what I want? In general, I'm finding that the documentation for polar axes is not as thorough as for rectangular axes, probably because fewer people use it. So maybe there's already a way to do this.
Your current method works for cartesian coordinates but for polar coordinates, you can use the workaround solution presented earlier here. I have adapted that answer for you below. You can add the following code after setting the theta grids
fig.canvas.draw()
labels = []
for label, angle in zip(ax.get_xticklabels(), thetatick_locs):
x,y = label.get_position()
lab = ax.text(x,y, label.get_text(), transform=label.get_transform(),
ha=label.get_ha(), va=label.get_va())
lab.set_rotation(angle)
labels.append(lab)
ax.set_xticklabels([])
plt.show()

What is the correct matplotlib transform for a "virtual third axis" in my waterfall plot?

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).

Matplotlib plot has slanted lines

I'm trying to plot projections of coordinates onto a line, but for some reason, Matplotlib is plotting the projections in a slightly slanted manner. Ideally, I would like the (blue) projections to be perpendicular to the (green) line. Here's an image of how it looks with sample data:
As you can see, the angles between the blue lines and the green line are slightly obtuse instead of right. I tried playing around with the rotation parameter to the annotate function, but this did not help. The code for this plot is below, although the data might look a bit different since the random generator is not seeded:
import numpy as np
import matplotlib.pyplot as plt
prefs = {'color':'purple','edgecolors':'black'}
X = np.dot(np.random.rand(2,2), np.random.rand(2,50)).T
pts = np.linspace(-1,1)
v1_m = 0.8076549717643662
plt.scatter(X[:,0],X[:,1],**prefs)
plt.plot(pts, [v1_m*x for x in pts], color='lightgreen')
for x,y in X:
# slope of connecting line
# y = mx+b
m = -np.reciprocal(v1_m)
b = y-m*x
# find intersecting point
zx = b/(v1_m-m)
zy = v1_m*zx
# draw line
plt.annotate('',(zx,zy),(x,y),arrowprops=dict(linewidth=2,arrowstyle='-',color='lightblue'))
plt.show()
The problem lies in the unequal axes which makes it look like they are not at a right angle. Use plt.axis('equal') to have equal axis spans on x- and y-axis and a square figure with equal height and width. plt.axis('scaled') works the same way. As pointed out by #CedricZoppolo, you should set the equal aspect ratios before plt.show(). As per docs, setting the aspect ratio to "equal" means
same scaling from data to plot units for x and y
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8,8))
# Your code here
plt.axis('equal')
plt.show()
Choosing a square figure is not necessary as it works also with rectangular figures as
fig = plt.figure(figsize=(8,6))
# Your code here
plt.axis('equal')
plt.show()
The blue lines not being perpendicular is due to axis not being equal.
You just need to add below line before plt.show()
plt.gca().set_aspect('equal')
Below you can see the resulted graph:

defining a partial geostationary projection with matplotlib Basemap

i'm having trouble creating a custom geostationary projection with Basemap. what i'd need is not a full globe projection, but the upper third of a full disc (full width, one third from the top down ). i found an example here:
http://matplotlib.org/basemap/users/geos.html
explaining how to make a projection for the upper right quadrant of the disc using the llcrnrx, llcrnry, urcrnrx, and urcrnry keywords. following the example, i thought i would get my result like this:
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
lon_0 = 9.5
fig = plt.figure()
m1 = Basemap(projection='geos',lon_0=lon_0,resolution=None)
m = Basemap(projection='geos',lon_0=lon_0,resolution='l',
# lower left
llcrnrx=0., llcrnry=0.,
# calculate the upper right coordinates, full width
# two 3rds of the height
urcrnrx=m1.urcrnrx, urcrnry=m1.urcrnry-m1.urcrnry/3.)
m.drawcoastlines()
plt.show()
which gives me a weird plot. from the example i though that for the new projection the lower left corner is always (0,0), then you just calculate the width and height coordinates (upper right). this obviously is not the case. with just bare experimentation i have this now:
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
lon_0 = 9.5
fig = plt.figure()
m1 = Basemap(projection='geos',lon_0=lon_0,resolution=None)
m = Basemap(projection='geos',lon_0=lon_0,resolution='l',
llcrnrx=-m1.urcrnrx/2., llcrnry=m1.urcrnry/4.,
urcrnrx=m1.urcrnrx/2., urcrnry=m1.urcrnry/2.)
m.drawcoastlines()
plt.show()
which gives me a rough estimate on what i'm after, but i have no idea why this works and what the corner points really represent.
i hope you guys get the drift...

Categories