import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
Setting the aspect ratio works for 2d plots:
ax = plt.axes()
ax.plot([0,1],[0,10])
ax.set_aspect('equal','box')
But does not for 3d:
ax = plt.axes(projection='3d')
ax.plot([0,1],[0,1],[0,10])
ax.set_aspect('equal','box')
Is there a different syntax for the 3d case, or it's not implemented?
As of matplotlib 3.3.0, Axes3D.set_box_aspect seems to be the recommended approach.
import numpy as np
import matplotlib.pyplot as plt
xs, ys, zs = ...
ax = plt.axes(projection='3d')
ax.set_box_aspect((np.ptp(xs), np.ptp(ys), np.ptp(zs))) # aspect ratio is 1:1:1 in data space
ax.plot(xs, ys, zs)
I didn't try all of these answers, but this kludge did it for me:
def axisEqual3D(ax):
extents = np.array([getattr(ax, 'get_{}lim'.format(dim))() for dim in 'xyz'])
sz = extents[:,1] - extents[:,0]
centers = np.mean(extents, axis=1)
maxsize = max(abs(sz))
r = maxsize/2
for ctr, dim in zip(centers, 'xyz'):
getattr(ax, 'set_{}lim'.format(dim))(ctr - r, ctr + r)
Looks like this feature has since been added so thought I'd add an answer for people who come by this thread in the future like I did:
fig = plt.figure(figsize=plt.figaspect(0.5)*1.5) #Adjusts the aspect ratio and enlarges the figure (text does not enlarge)
ax = fig.add_subplot(projection='3d')
figaspect(0.5) makes the figure twice as wide as it is tall. Then the *1.5 increases the size of the figure. The labels etc won't increase so this is a way to make the graph look less cluttered by the labels.
I think setting the correct "box aspect" is a good solution:
ax.set_box_aspect(aspect = (1,1,1))
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_box_aspect(aspect = (1,1,1))
ax.plot(dataX,dataY,dataZ)
https://matplotlib.org/stable/api/_as_gen/mpl_toolkits.mplot3d.axes3d.Axes3D.html?highlight=3d%20set_box_aspect#mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
If you know the bounds, eg. +-3 centered around (0,0,0), you can add invisible points like this:
import numpy as np
import pylab as pl
from mpl_toolkits.mplot3d import Axes3D
fig = pl.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')
MAX = 3
for direction in (-1, 1):
for point in np.diag(direction * MAX * np.array([1,1,1])):
ax.plot([point[0]], [point[1]], [point[2]], 'w')
If you know the bounds you can also set the aspect ratio this way:
ax.auto_scale_xyz([minbound, maxbound], [minbound, maxbound], [minbound, maxbound])
Another helpful (hopefully) solution when, for example, it is necessary to update an already existing figure:
world_limits = ax.get_w_lims()
ax.set_box_aspect((world_limits[1]-world_limits[0],world_limits[3]-world_limits[2],world_limits[5]-world_limits[4]))
get_w_lims()
set_box_aspect()
My understanding is basically that this isn't implemented yet (see this bug in GitHub). I'm also hoping that it is implemented soon. See This link for a possible solution (I haven't tested it myself).
A follow-up to Matt Panzer's answer. (This was originally a comment on said answer.)
limits = np.array([getattr(ax, f'get_{axis}lim')() for axis in 'xyz'])
ax.set_box_aspect(np.ptp(limits, axis=1))
Now that this pull request has been merged, when the next release of Matplotlib drops, you should be able to just use ax.set_aspect('equal'). I will try to remember and update this answer when that happens.
Update: Matplotlib 3.6 has been released; ax.set_aspect('equal') will now work as expected.
As of matplotlib 3.6.0, this feature has been added with the shortcut
ax.set_aspect('equal'). Other options are 'equalxy', 'equalxz', and 'equalyz', to set only two directions to equal aspect ratios. This changes the data limits, example below.
In the upcoming 3.7.0, you will be able to change the plot box aspect ratios rather than the data limits via the command ax.set_aspect('equal', adjustable='box'). (Thanks to #tfpf on another answer here for implementing that!) To get the original behavior, use adjustable='datalim'.
Matt Panzer's answer worked for me, but it took me a while to figure out an issue I had.
If you're plotting multiple datasets into the same graph, you have to calculate the peak-to-peak values for the entire range of datapoints.
I used the following code to solve it for my case:
x1, y1, z1 = ..., ..., ...
x2, y2, z2 = ..., ..., ...
ax.set_box_aspect((
max(np.ptp(x1), np.ptp(x2)),
max(np.ptp(y1), np.ptp(y2)),
max(np.ptp(z1), np.ptp(y2))
))
ax.plot(x1, y1, z1)
ax.scatter(x2, y2, z2)
Note that this solution is not perfect. It will not work if x1 contains the most negative number and x2 contains the most positive one. Only if either x1 or x2 contains the greatest peak-to-peak range.
If you know numpy better than I do, feel free to edit this answer so it works in a more general case.
I tried several methods, such as ax.set_box_aspect(aspect = (1,1,1)) and it does not work. I want a sphere to show up as a sphere -- not ellipsoid. I wrote this function and tried it on a variety of data. It is a hack and it is not perfect, but pretty close.
def set_aspect_equal(ax):
"""
Fix the 3D graph to have similar scale on all the axes.
Call this after you do all the plot3D, but before show
"""
X = ax.get_xlim3d()
Y = ax.get_ylim3d()
Z = ax.get_zlim3d()
a = [X[1]-X[0],Y[1]-Y[0],Z[1]-Z[0]]
b = np.amax(a)
ax.set_xlim3d(X[0]-(b-a[0])/2,X[1]+(b-a[0])/2)
ax.set_ylim3d(Y[0]-(b-a[1])/2,Y[1]+(b-a[1])/2)
ax.set_zlim3d(Z[0]-(b-a[2])/2,Z[1]+(b-a[2])/2)
ax.set_box_aspect(aspect = (1,1,1))
Related
I have a function with an histogram, plotted like this :
import matplotlib.pyplot as plt
import numpy as np
lin = np.linspace(min(foo), max(foo), len(foo))
plt.plot(lin, bar)
plt.hist(bar, density=True, bins=100, histtype='stepfilled', alpha=0.2)
plt.show()
Where foo and bar are simple arrays.
However, I would want to have the whole thing in a vertical way... I could add orientation='horizontal' to the histogram, but it would not change the function (and from what I have seen, there is nothing similar for a plot -> obviously it wouldn't be a function then, but a curve). Otherwise, I could add plt.gca().invert_yaxis() somewhere, but the same problem resides : plot is used for functions, so the swap of it does... well, that :
So, the only way I have now is to manually turn the whole original picture by 90 degrees, but then the axis are turned too and will no longer be on the left and bottom (obviously).
So, have you another idea ? Maybe I should try something else than plt.plot ?
EDIT : In the end, I would want something like the image below, but with axes made right.
If you have a plot of y vs x, you can swap axes by swapping arrays:
plt.plot(bar, lin)
There's no special feature because it's supported out of the box. As you've discovered, plotting a transposed histogram can be accomplished by passing in
orientation='horizontal'
I couldn't find any matplotlib method dealing with the issue. You can rotate the curve in a purely mathematical way, i.e. do it through the rotation matrix. In this simple case it is sufficient to just exchange variables x and y but in general it looks like this (let's take a parabola for a clear example):
rotation = lambda angle: np.array([[ np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
x = np.linspace(-10,10,1000)
y = -x**2
matrix = np.vstack([x,y]).T
rotated_matrix = matrix # rotation(np.deg2rad(90))
fig, ax = plt.subplots(1,2)
ax[0].plot(rotated_matrix[:,0], rotated_matrix[:,1])
ax[1].plot(x,y)
rotated_matrix = matrix # rotation(np.deg2rad(-45))
fig, ax = plt.subplots(1,2)
ax[0].plot(rotated_matrix[:,0], rotated_matrix[:,1])
ax[1].plot(x,y)
I am working with matplotlib to plot a heat map with some information and I want to move the xticks and the yticks to the center. I have searched in stackoverflow for previous questions but I couldn't reach one suitable for the problem. I attach my code and the image that I get:
import matplotlib.pyplot as plt
from matplotlib import colors
import numpy as np
def plot():
intensity= np.random.rand(10,10)
matrix_intensity=np.matrix(intensity)
max_intensity=matrix_intensity.max()
min_intensity = matrix_intensity.min()
for e in range(len(intensity)):
for i in range(len(intensity[e])):
intensity[e][i]=float(intensity[e][i])/float(max_intensity)
np.random.seed(101)
cmap = colors.ListedColormap(['white','khaki', 'goldenrod','yellowgreen','mediumseagreen','darkcyan','tomato','indianred' ,'sienna','maroon'])
bounds = np.linspace(min_intensity/max_intensity,1,11).tolist()
norm = colors.BoundaryNorm(bounds, cmap.N)
img = plt.imshow(intensity, interpolation='none', origin='lower',extent=[0,len(intensity),0,len(intensity)],
cmap=cmap, norm=norm)
cb=plt.colorbar(img, fraction=0.1,cmap=cmap, norm=norm, boundaries=bounds,format='%.2f') #'%.2f')
cb.set_label(label='Ratio',fontsize=12,labelpad=10)
plt.ylabel('Origin',fontsize=11)
plt.xlabel('Destination',fontsize=11)
plt.title('Best route:',fontsize=10)
plt.suptitle('Best Solution:',fontsize=10)
plt.xticks(range(1,len(intensity)+1))
plt.yticks(range(1,len(intensity)+1))
plt.savefig('images/hello.png')
plt.show()
The fact is that I would like the x and the y ticks to point out the center of every square because otherwise, it doesn't make sense to plot the squares. Does somebody know how to fix this? Maybe this question is obvious but the matplotlib documentation for all the statements sometimes is difficult to understand.
The obvious solution would probably to use a different extent, namely to let the image live in the range between 0.5 and len(intensity)+0.5.
extent=[.5, len(intensity)+.5, .5, len(intensity)+.5]
img = plt.imshow(intensity, interpolation='none', origin='lower',extent=extent,
cmap=cmap, norm=norm)
You need to change the way you set your xticks and yticks loc and labels to below:
plt.xticks([x-0.5 for x in list(range(1,len(intensity)+1))], range(1,len(intensity)+1))
plt.yticks([x-0.5 for x in list(range(1,len(intensity)+1))], range(1,len(intensity)+1))
Output:
The other answers are both good, however I would like to provide a more general implementation that also doesn't alter default ticks, as I have a function that can be used to calculate the axis limits and set them as in #ImportanceOfBeingErnest answer.
import numpy as np
def span_from_pixels(p,n=None):
"""From positions of pixel centers p returns a range from side to side. Useful to adjust plot extent in imshow.
In alternative, p can be provided as range and number of pixels.
Note that np.linspace has flag retsteps to return step size."""
if n is None:
n=len(p)
dx=(np.max(p)-np.min(p))/(n-1)
return (np.min(p)-dx/2,np.max(p)+dx/2)
def test_span_from_pixels():
print (span_from_pixels([0,3],4)) #[-0.5,3.5]
print (span_from_pixels([0,2],3)) #[-0.5,2.5]
print (span_from_pixels([0,1,2])) #[-0.5,2.5]
print (span_from_pixels([0,0.5,1,1.5,2])) #[-0.25,2.25]
Please let me know if something doesn't work, these are tested in my code, but I made some change to remove dependencies. I assume I didn't break anything, but I cannot test it now.
This question explains how to change the "camera position" of a 3D plot in matplotlib by specifying the elevation and azimuth angles. ax.view_init(elev=10,azim=20), for example.
Is there a similar way to specify the zoom of the figure numerically -- i.e. without using the mouse?
The only relevant question I could find is this one, but the accepted answer to that involves installing another library, which then also requires using the mouse to zoom.
EDIT:
Just to be clear, I'm not talking about changing the figure size (using fig.set_size_inches() or similar). The figure size is fine; the problem is that the plotted stuff only takes up a small part of the figure:
The closest solution to view_init is setting ax.dist directly. According to the docs for get_proj "dist is the distance of the eye viewing point from the object point". The initial value is currently hardcoded with dist = 10. Lower values (above 0!) will result in a zoomed in plot.
Note: This behavior is not really documented and may change. Changing the limits of the axes to plot only the relevant parts is probably a better solution in most cases. You could use ax.autoscale(tight=True) to do this conveniently.
Working IPython/Jupyter example:
%matplotlib inline
from IPython.display import display
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# Grab some test data.
X, Y, Z = axes3d.get_test_data(0.05)
# Plot a basic wireframe.
ax.view_init(90, 0)
ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
plt.close()
from ipywidgets import interact
#interact(dist=(1, 20, 1))
def update(dist=10):
ax.dist = dist
display(fig)
Output
dist = 10
dist = 5
I'm trying to create a 3D wireframe in Python using matplotlib.
When I get to the actual graph plotting, however, the wireframe joins the wrong way, as shown in the images below.
How can I force matplotlib to join the wireframe along a certain axis?
My code is below:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
def rossler(x_n, y_n, z_n, h, a, b, c):
#defining the rossler function
x_n1=x_n+h*(-y_n-z_n)
y_n1=y_n+h*(x_n+a*y_n)
z_n1=z_n+h*(b+z_n*(x_n-c))
return x_n1,y_n1,z_n1
#defining a, b, and c
a = 1.0/5.0
b = 1.0/5.0
c = 5
#defining time limits and steps
t_0 = 0
t_f = 32*np.pi
h = 0.01
steps = int((t_f-t_0)/h)
#3dify
c_list = np.linspace(5,10,6)
c_size = len(c_list)
c_array = np.zeros((c_size,steps))
for i in range (0, c_size):
for j in range (0, steps):
c_array[i][j] = c_list[i]
#create plotting values
t = np.zeros((c_size,steps))
for i in range (0, c_size):
t[i] = np.linspace(t_0,t_f,steps)
x = np.zeros((c_size,steps))
y = np.zeros((c_size,steps))
z = np.zeros((c_size,steps))
binvar, array_size = x.shape
#initial conditions
x[0] = 0
y[0] = 0
z[0] = 0
for j in range(0, c_size-1):
for i in range(array_size-1):
c = c_list[j]
#re-evaluate the values of the x-arrays depending on the initial conditions
[x[j][i+1],y[j][i+1],z[j][i+1]]=rossler(x[j][i],y[j][i],z[j][i],t[j][i+1]-t[j][i],a,b,c)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_wireframe(t,x,c_array, rstride=10, cstride=10)
plt.show()
I am getting this as an output:
The same output from another angle:
Whereas I'd like the wireframe to join along the wave-peaks. Sorry, I can't give you an image I'd like to see, that's my problem, but I guess it'd be more like the tutorial image.
If I understood, you want to link the 6 traces with polygons. You can do that by triangulating the traces 2 by 2, then plotting the surface with no edges or antialising. Maybe choosing a good colormap will also help.
Just keep in mind that this will be a very heavy plot. The exported SVG weight 10mb :)
import matplotlib.tri as mtri
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
for LineIndex in range(c_size-1):
# If plotting all at once, you get a MemoryError. I'll plot each 6 points
for Sample in range(0, array_size-1, 3):
# I switched x and c_array, because the surface and the triangles
# will look better by default
X = np.concatenate([t[LineIndex,Sample:Sample+3], t[LineIndex+1,Sample:Sample+3]])
Y = np.concatenate([c_array[LineIndex,Sample:Sample+3], c_array[LineIndex+1,Sample:Sample+3]])
Z = np.concatenate([x[LineIndex,Sample:Sample+3], x[LineIndex+1,Sample:Sample+3]])
T = mtri.Triangulation(X, Y)
ax.plot_trisurf(X, Y, Z, triangles=T.triangles, edgecolor='none', antialiased=False)
ax.set_xlabel('t')
ax.set_zlabel('x')
plt.savefig('Test.png', format='png', dpi=600)
plt.show()
Here is the resulting image:
I'm quite unsure about what you're exactly trying to achieve, but I don't think it will work.
Here's what your data looks like when plotted layer by layer (without and with filling):
You're trying to plot this as a wireframe plot. Here's how a wireframe plot looks like as per the manual:
Note the huge differene: a wireframe plot is essentially a proper surface plot, the only difference is that the faces of the surface are fully transparent. This also implies that you can only plot
single-valued functions of the form z(x,y), which are furthermore
specified on a rectangular mesh (at least topologically)
Your data is neither: your points are given along lines, and they are stacked on top of each other, so there's no chance that this is a single surface that can be plotted.
If you just want to visualize your functions above each other, here's how I plotted the above figures:
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
for zind in range(t.shape[0]):
tnow,xnow,cnow = t[zind,:],x[zind,:],c_array[zind,:]
hplot = ax.plot(tnow,xnow,cnow)
# alternatively fill:
stride = 10
tnow,xnow,cnow = tnow[::stride],xnow[::stride],cnow[::stride]
slice_from = slice(None,-1)
slice_to = slice(1,None)
xpoly = np.array([tnow[slice_from],
tnow[slice_to],
tnow[slice_to],
tnow[slice_from]]
).T
ypoly = np.array([xnow[slice_from],
xnow[slice_to],
np.zeros_like(xnow[slice_to]),
np.zeros_like(xnow[slice_from])]
).T
zpoly = np.array([cnow[slice_from],
cnow[slice_to],
cnow[slice_to],
cnow[slice_from]]
).T
tmppoly = [tuple(zip(xrow,yrow,zrow)) for xrow,yrow,zrow in zip(xpoly,ypoly,zpoly)]
poly3dcoll = Poly3DCollection(tmppoly,linewidth=0.0)
poly3dcoll.set_edgecolor(hplot[0].get_color())
poly3dcoll.set_facecolor(hplot[0].get_color())
ax.add_collection3d(poly3dcoll)
plt.xlabel('t')
plt.ylabel('x')
plt.show()
There is one other option: switching your coordinate axes, such that the (x,t) pair corresponds to a vertical plane rather than a horizontal one. In this case your functions for various c values are drawn on parallel planes. This allows a wireframe plot to be used properly, but since your functions have extrema in different time steps, the result is as confusing as your original plot. You can try using very few plots along the t axis, and hoping that the extrema are close. This approach needs so much guesswork that I didn't try to do this myself. You can plot each function as a filled surface instead, though:
from matplotlib.collections import PolyCollection
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
for zind in range(t.shape[0]):
tnow,xnow,cnow = t[zind,:],x[zind,:],c_array[zind,:]
hplot = ax.plot(tnow,cnow,xnow)
# alternative to fill:
stride = 10
tnow,xnow,cnow = tnow[::stride],xnow[::stride],cnow[::stride]
slice_from = slice(None,-1)
slice_to = slice(1,None)
xpoly = np.array([tnow[slice_from],
tnow[slice_to],
tnow[slice_to],
tnow[slice_from]]
).T
ypoly = np.array([xnow[slice_from],
xnow[slice_to],
np.zeros_like(xnow[slice_to]),
np.zeros_like(xnow[slice_from])]
).T
tmppoly = [tuple(zip(xrow,yrow)) for xrow,yrow in zip(xpoly,ypoly)]
polycoll = PolyCollection(tmppoly,linewidth=0.5)
polycoll.set_edgecolor(hplot[0].get_color())
polycoll.set_facecolor(hplot[0].get_color())
ax.add_collection3d(polycoll,zdir='y',zs=cnow[0])
hplot[0].set_color('none')
ax.set_xlabel('t')
ax.set_zlabel('x')
plt.show()
This results in something like this:
There are a few things to note, however.
3d scatter and wire plots are very hard to comprehend, due to the lacking depth information. You might be approaching your visualization problem in a fundamentally wrong way: maybe there are other options with which you can visualize your data.
Even if you do something like the plots I showed, you should be aware that matplotlib has historically been failing to plot complicated 3d objects properly. Now by "properly" I mean "with physically reasonable apparent depth", see also the mplot3d FAQ note describing exactly this. The core of the problem is that matplotlib projects every 3d object to 2d, and draws these pancakes on the sreen one after the other. Sometimes the asserted drawing order of the pancakes doesn't correspond to their actual relative depth, which leads to artifacts that are both very obvious to humans and uncanny to look at. If you take a closer look at the first filled plot in this post, you'll see that the gold flat plot is behind the magenta one, even though it should be on top of it. Similar things often happen with 3d bar plots and convoluted surfaces.
When you're saying "Sorry, I can't give you an image I'd like to see, that's my problem", you're very wrong. It's not just your problem. It might be crystal clear in your head what you're trying to achieve, but unless you very clearly describe what you see in your head, the outside world will have to resort to guesswork. You can make the work of others and yourself alike easier by trying to be as informative as possible.
I'm trying to create a 1D-Dotplot with python, similar to this:
https://owncloud.tu-berlin.de/public.php?service=files&t=9ead31dfc988757321c7ac391920c48a
I tried using the plot.scatter method from matplotlib, but it nees data for the x-axis. I tried setting all x-values to '1', but it turns out as kind of a 2d-diagram, anyway:
https://owncloud.tu-berlin.de/public.php?service=files&t=ab9f0f521f57526e871259f3a520d94a
How can I draw a real 1d-dotplot? I found nothing in the matplotlib-docs...
I would like to use matplotlib but am also open to other suggestions.
Thanks in advance!
Cheers, Jakob
As far as I can see, also the 1D-Dotplot you're showing is two dimensional but only strongly limited in x direction.
I don't know whether there already exists something like that but the following code is doing what you ask for.
import numpy as np
import matplotlib.pyplot as mpl
# your data
data = 3. + 0.7 * np.random.randn(N)
# a small spreading of the data in x direction
x = 0.2 * np.random.randn(data.size)
# the plotting
fig,ax = mpl.subplots(1,figsize=(0.5,5))
ax.set_axis_bgcolor('#FFD7B1')
ax.scatter(x,data,alpha=0.2,c='k')
ax.plot([-1,1],[np.mean(data),np.mean(data)],'r',linewidth=2)
ax.set_xlim((-1,1))
ax.set_ylim((1,6))
ax.set_xticks([])
ax.grid(True,axis='y')
ax.set_ylabel('Note')
Your own solution is close! Just play with the aspect ratio to "squash" down the size along the x-axis:
import numpy as np
import matplotlib.pyplot as plt
x = np.random.random(100)
y = np.random.randn(100)
fh, ax = plt.subplots(1,1)
ax.scatter(x,y)
ax.set_xlim(-.5, 1.5)
ax.axes.get_xaxis().set_visible(False) # remove the x-axis and its ticks
ax.set_aspect(5, adjustable='box') # adjustable='box' is important here
plt.show()