How to add strings to the axes in Axes3D instead of numbers?
I just started using the matplotlib. I have used Axes3dD to plot similar to the example given on their website (http://matplotlib.sourceforge.net/examples/mplot3d/bars3d_demo.html). Note that one must use the last verson (matplotlib 0.99.1), otherwise the axis gets a bit freaky. The example use this code:
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = Axes3D(fig)
for c, z in zip(['r', 'g', 'b', 'y'], [30, 20, 10, 0]):
xs = np.arange(20)
ys = np.random.rand(20)
ax.bar(xs, ys, zs=z, zdir='y', color=c, alpha=0.8)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.show()
Now to my problem, I cant rename the axis to what I want. Instead of numbers i need to name the staples to string names. This I only want to do in two dimensions - the x dimension, and the z dimension (depth). I have tried to use this command:
ax.set_xticklabels('First staple',(0,0))
I get no error message but nevertheless no sting. If anyone can answer this question I would be most delighted!
Your actually on the right path there. but instead of a string you will want to pass a list or tuple to set_xticklabels(). You may also wish to adjust the center location for the label with set_xticks().
You may also find this function of use get_xmajorticklabels(). It will return the rendered tick labels. So you may also be able to adjust the value from its results.
Also retrieve the axis data, work on that and set it back to the renderer, turned out to be much simpler for me ;) other than that, what NerdyNick says
Related
I am trying to plot some data. It is recorded as different parameters against a common parameter. For example, say time on X Axis and Temperature, Wind Speed etc. on Y Axis.
I have been using origin to do my plotting. In origin, such manipulations are carried out by use of Layers. Each axis on different layers can share X or Y axis from the host. And it can be resized so that the plot is drawn on some percentage of actual area. Please see the attached sketch.
I am trying to achieve something similar in python to shift to open-source. After some reading, I found out twinx() and twiny() are two axes functions that can clone/share an axis. Spines can be moved as required through spine['pos'].set_position().
My problem is that, although X axis is shared, I can not individually resize the Y axis of individual parameters. On using set_position, whole figure changes. Request the community for a solution. I am including a snippet of representative code.
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (8,11))
host = fig.add_axes([0.1,0.1,0.7,0.8])
host.plot([1,2,3],[1,4,9])
parm1 = host.twinx()
#parm1.set_position([0.1,0.1,0.8,0.5]) #this doesn't work
parm1.spines['right'].set_position(('axes',1.1))
..
..
I do not think there is an existing function in matplotlib that can do what you want in one call. However, with a little bit of work, you can achieve something like that:
import matplotlib.pyplot as plt
import numpy as np
fig, (ax, bx) = plt.subplots(nrows=2, ncols=1, sharex=True)
bx.yaxis.set_label_position('right')
bx.yaxis.tick_right()
ax.spines['bottom'].set_visible(False)
bx.spines['top'].set_visible(False)
fig.subplots_adjust(hspace=0)
bx.set_xlabel('X Axis')
ax.set_ylabel('Cosine')
bx.set_ylabel('Sine')
x = np.linspace(-np.pi, np.pi, 100)
ax.plot(x, np.cos(x), color='xkcd:avocado', linestyle='dashdot', linewidth=3)
bx.plot(x, np.sin(x), color='xkcd:purple', linestyle='dotted', linewidth=3)
plt.show()
I'm trying to get the functionality of fill_betweenx() without having to use the function itself, because it doesn't accept the interpolate parameter. I need the interpolate functionality that is supported by fill_between(), but for the filling to happen relative to the x axis. It sounds like the interpolate parameter will be supported for fill_betweenx() in matplotlib 2.1, but it would be great to have access to the functionality via a workaround in the meantime.
This is the line of code in question:
ax4.fill_betweenx(x,300,p, where=p>=150, interpolate=True, facecolor='White', lw=1, zorder=2)
Unfortunately this gives me AttributeError: Unknown property interpolate.
One lazy way to do it is to use the fill_between() function with inverted coordinates on a figure that you don't show (i.e. close the figure before using plt.show()), and then re-use the vertices of the PolyCollection that fill_between() returns on your actual plot. It's not perfect, but it works as a quick fix. Here an example of what I'm talking about:
from matplotlib import pyplot as plt
from matplotlib.collections import PolyCollection
import numpy as np
fig, axes = plt.subplots(nrows = 2, ncols =2, figsize=(8,8))
#the data
x = np.linspace(0,np.pi/2,3)
y = np.sin(x)
#fill_between without interpolation
ax = axes[0,0]
ax.plot(x,y,'k')
ax.fill_between(x,0.5,y,where=y>0.25)
#fill_between with interpolation, keep the PolyCollection
ax = axes[0,1]
ax.plot(x,y,'k')
poly_col = ax.fill_between(x,0.5,y,where=y>0.25,interpolate=True)
#fill_betweenx -- no interpolation possible
ax = axes[1,0]
ax.plot(y,x,'k')
ax.fill_betweenx(x,0.5,y,where=y>0.25)
#faked fill_betweenx:
ax = axes[1,1]
ax.plot(y,x,'k')
#get the vertices from the saved PolyCollection, swap x- and y-values
v=poly_col.get_paths()[0].vertices
#convert to correct format
v2=list(zip(v[:,1],v[:,0]))
#and add to axes
ax.add_collection(PolyCollection([v2]))
#voila
plt.show()
The result of the code looks like this:
I have some test data:
import numpy as np
x_data = np.arange(10)
y = np.random.rand(len(x_data))
With different properties
ix1 = x_data < 5
ix2 = x_data >= 5
I want to investigate the differences visually, but am messing the plot up:
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_context('poster')
fig, ax = plt.subplots(figsize=(4, 4))
for i, x in enumerate(x_data):
if ix1[i]:
sns.set_palette('rainbow', sum(ix1))
if ix2[i]:
sns.set_palette('coolwarm', sum(ix2))
plt.plot(x, y[i], 'o', label='{}'.format(x))
plt.legend(loc='best', prop={'size': 6})
plt.show()
The result should be points 0-4 are rainbow (red-violet) and points 5-9 are coolwarm (blue-white-red), but instead:
So, two questions:
Is it ok to call sns.set_palette() after calling plt.subplots?
Is there a way to set the palette more than once?
No, because of the way matplotlib works, the color palette is a property of the Axes object and so whatever the currently set palette is at the time an Axes is created is what it's going to use. This is possible to get around if you want to hack on private attributes (see here), but I wouldn't really recommend that.
Here's what I could come up with in your case, using a somewhat different approach that might not be broadly applicable:
pal1 = sns.color_palette('rainbow', sum(ix1))
pal2 = sns.color_palette('coolwarm', sum(ix2))
fig, ax = plt.subplots(figsize=(4, 4))
ax.scatter(x_data[ix1], y[ix1], c=pal1, s=60, label="smaller")
ax.scatter(x_data[ix2], y[ix2], c=pal2, s=60, label="larger")
ax.legend(loc="lower right", scatterpoints=5)
FWIW, this visualization feels pretty complex and hard to process (and the two palettes you've chosen overlap a fair amount and aren't really appropriate for these data) so it might be worth starting with something simpler.
I have the following Python code which I am using to plot a filled contour plot:
def plot_polar_contour(values, azimuths, zeniths):
theta = np.radians(azimuths)
zeniths = np.array(zeniths)
values = np.array(values)
values = values.reshape(len(azimuths), len(zeniths))
r, theta = np.meshgrid(zeniths, np.radians(azimuths))
fig, ax = subplots(subplot_kw=dict(projection='polar'))
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
cax = ax.contourf(theta, r, values, 30)
autumn()
cb = fig.colorbar(cax)
cb.set_label("Pixel reflectance")
show()
This gives me a plot like:
However, when I add the line ax.plot(0, 30, 'p') just before show() I get the following:
It seems that just adding that one point (which is well within the original axis range) screws up the axis range on the radius axis.
Is this by design, or is this a bug? What would you suggest doing to fix it? Do I need to manually adjust the axis ranges, or is there a way to stop the extra plot command doing this?
If the axis auto-scaling mode isn't explicitly specified, plot will use "loose" autoscaling and contourf will use "tight" autoscaling.
The same things happens for non-polar axes. E.g.
import matplotlib.pyplot as plt
import numpy as np
plt.imshow(np.random.random((10,10)))
plt.plot([7], [7], 'ro')
plt.show()
You have a number of options.
Explicitly call ax.axis('image') or ax.axis('tight') at some point in the code.
Pass in scalex=False and scaley=False as keyword arguments to plot.
Manually set the axis limits.
The easiest and most readable is to just explicitly call ax.axis('tight'), i.m.o.
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))