I'm learning how to use mplot3d to produce nice plots of 3d data and I'm pretty happy so far. What I am trying to do at the moment is a little animation of a rotating surface. For that purpose, I need to set a camera position for the 3D projection. I guess this must be possible since a surface can be rotated using the mouse when using matplotlib interactively. But how can I do this from a script?
I found a lot of transforms in mpl_toolkits.mplot3d.proj3d but I could not find out how to use these for my purpose and I didn't find any example for what I'm trying to do.
By "camera position," it sounds like you want to adjust the elevation and the azimuth angle that you use to view the 3D plot. You can set this with ax.view_init. I've used the below script to first create the plot, then I determined a good elevation, or elev, from which to view my plot. I then adjusted the azimuth angle, or azim, to vary the full 360deg around my plot, saving the figure at each instance (and noting which azimuth angle as I saved the plot). For a more complicated camera pan, you can adjust both the elevation and angle to achieve the desired effect.
from mpl_toolkits.mplot3d import Axes3D
ax = Axes3D(fig)
ax.scatter(xx,yy,zz, marker='o', s=20, c="goldenrod", alpha=0.6)
for ii in xrange(0,360,1):
ax.view_init(elev=10., azim=ii)
savefig("movie%d.png" % ii)
What would be handy would be to apply the Camera position to a new plot.
So I plot, then move the plot around with the mouse changing the distance. Then try to replicate the view including the distance on another plot.
I find that axx.ax.get_axes() gets me an object with the old .azim and .elev.
IN PYTHON...
axx=ax1.get_axes()
azm=axx.azim
ele=axx.elev
dst=axx.dist # ALWAYS GIVES 10
#dst=ax1.axes.dist # ALWAYS GIVES 10
#dst=ax1.dist # ALWAYS GIVES 10
Later 3d graph...
ax2.view_init(elev=ele, azim=azm) #Works!
ax2.dist=dst # works but always 10 from axx
EDIT 1...
OK, Camera position is the wrong way of thinking concerning the .dist value. It rides on top of everything as a kind of hackey scalar multiplier for the whole graph.
This works for the magnification/zoom of the view:
xlm=ax1.get_xlim3d() #These are two tupples
ylm=ax1.get_ylim3d() #we use them in the next
zlm=ax1.get_zlim3d() #graph to reproduce the magnification from mousing
axx=ax1.get_axes()
azm=axx.azim
ele=axx.elev
Later Graph...
ax2.view_init(elev=ele, azim=azm) #Reproduce view
ax2.set_xlim3d(xlm[0],xlm[1]) #Reproduce magnification
ax2.set_ylim3d(ylm[0],ylm[1]) #...
ax2.set_zlim3d(zlm[0],zlm[1]) #...
Minimal example varying azim, dist and elev
To add some simple sample images to what was explained at: https://stackoverflow.com/a/12905458/895245
Here is my test program:
#!/usr/bin/env python3
import sys
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import numpy as np
fig = plt.figure()
ax = fig.gca(projection='3d')
if len(sys.argv) > 1:
azim = int(sys.argv[1])
else:
azim = None
if len(sys.argv) > 2:
dist = int(sys.argv[2])
else:
dist = None
if len(sys.argv) > 3:
elev = int(sys.argv[3])
else:
elev = None
# Make data.
X = np.arange(-5, 6, 1)
Y = np.arange(-5, 6, 1)
X, Y = np.meshgrid(X, Y)
Z = X**2
# Plot the surface.
surf = ax.plot_surface(X, Y, Z, linewidth=0, antialiased=False)
# Labels.
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
if azim is not None:
ax.azim = azim
if dist is not None:
ax.dist = dist
if elev is not None:
ax.elev = elev
print('ax.azim = {}'.format(ax.azim))
print('ax.dist = {}'.format(ax.dist))
print('ax.elev = {}'.format(ax.elev))
plt.savefig(
'main_{}_{}_{}.png'.format(ax.azim, ax.dist, ax.elev),
format='png',
bbox_inches='tight'
)
Running it without arguments gives the default values:
ax.azim = -60
ax.dist = 10
ax.elev = 30
main_-60_10_30.png
Vary azim
The azimuth is the rotation around the z axis e.g.:
0 means "looking from +x"
90 means "looking from +y"
main_-60_10_30.png
main_0_10_30.png
main_60_10_30.png
Vary dist
dist seems to be the distance from the center visible point in data coordinates.
main_-60_10_30.png
main_-60_5_30.png
main_-60_20_-30.png
Vary elev
From this we understand that elev is the angle between the eye and the xy plane.
main_-60_10_60.png
main_-60_10_30.png
main_-60_10_0.png
main_-60_10_-30.png
Tested on matpotlib==3.2.2.
Try the following code to find the optimal camera position
Move the viewing angle of the plot using the keyboard keys as mentioned in the if clause
Use print to get the camera positions
def move_view(event):
ax.autoscale(enable=False, axis='both')
koef = 8
zkoef = (ax.get_zbound()[0] - ax.get_zbound()[1]) / koef
xkoef = (ax.get_xbound()[0] - ax.get_xbound()[1]) / koef
ykoef = (ax.get_ybound()[0] - ax.get_ybound()[1]) / koef
## Map an motion to keyboard shortcuts
if event.key == "ctrl+down":
ax.set_ybound(ax.get_ybound()[0] + xkoef, ax.get_ybound()[1] + xkoef)
if event.key == "ctrl+up":
ax.set_ybound(ax.get_ybound()[0] - xkoef, ax.get_ybound()[1] - xkoef)
if event.key == "ctrl+right":
ax.set_xbound(ax.get_xbound()[0] + ykoef, ax.get_xbound()[1] + ykoef)
if event.key == "ctrl+left":
ax.set_xbound(ax.get_xbound()[0] - ykoef, ax.get_xbound()[1] - ykoef)
if event.key == "down":
ax.set_zbound(ax.get_zbound()[0] - zkoef, ax.get_zbound()[1] - zkoef)
if event.key == "up":
ax.set_zbound(ax.get_zbound()[0] + zkoef, ax.get_zbound()[1] + zkoef)
# zoom option
if event.key == "alt+up":
ax.set_xbound(ax.get_xbound()[0]*0.90, ax.get_xbound()[1]*0.90)
ax.set_ybound(ax.get_ybound()[0]*0.90, ax.get_ybound()[1]*0.90)
ax.set_zbound(ax.get_zbound()[0]*0.90, ax.get_zbound()[1]*0.90)
if event.key == "alt+down":
ax.set_xbound(ax.get_xbound()[0]*1.10, ax.get_xbound()[1]*1.10)
ax.set_ybound(ax.get_ybound()[0]*1.10, ax.get_ybound()[1]*1.10)
ax.set_zbound(ax.get_zbound()[0]*1.10, ax.get_zbound()[1]*1.10)
# Rotational movement
elev=ax.elev
azim=ax.azim
if event.key == "shift+up":
elev+=10
if event.key == "shift+down":
elev-=10
if event.key == "shift+right":
azim+=10
if event.key == "shift+left":
azim-=10
ax.view_init(elev= elev, azim = azim)
# print which ever variable you want
ax.figure.canvas.draw()
fig.canvas.mpl_connect("key_press_event", move_view)
plt.show()
Q: How can I set view in matplotlib?
For a 3d plot, how do you fixate the view?
A: By setting properties ax.azim and ax.level
ax.elev = 0
ax.azim = 270 # xz view
ax.elev = 0
ax.azim = 0 # yz view
ax.elev = 0
ax.azim = -90 # xy view
Related
I am trying to change the plot window extents on an annotated plot to "zoom" into a certain window of interest. My text annotations fall outside of the plot window. If I use clip_on = True then all the text is hidden, but I just want to trim the text outside of the x-axis.
import matplotlib.pyplot as plt
x = [0,2,4,6,8,10]
y = [3,2,5,9,6,7]
plt.plot(x,y)
for i in range(len(x)):
plt.text(x[i],11, '%d' %y[i])
plt.axis([0,5,0,10])
Full data:
Reduced window:
Desired output:
This isn't fancy at all, but it works for x_max 1 to 10:
import matplotlib.pyplot as plt
x = [0,2,4,6,8,10]
y = [3,2,5,9,6,7]
x_max = 5
plot_axis = [0,x_max,0,10]
if plot_axis[1] == x[-1:][0]:
range_set = range(len(x))
else:
try:
x_idx = x.index(plot_axis[1])
except:
x_idx = x.index(plot_axis[1]-1)
range_set = range(x_idx+1)
plt.plot(x,y)
for i in range_set:
plt.text(x[i],11, '%d' %y[i])
plt.axis(plot_axis)
plt.show()
Note: There isn't a sanity check for x_max = 0 or >10 implemented, but from your plt.axis([0,5,0,10]) it seems you had this in manually in check anyway.
I want to draw a 3D volume using Matplotlib, slice by slice.
Mouse scroll to change the index. My program is given below:
#Mouse scroll event.
def mouse_scroll(event):
fig = event.canvas.figure
ax = fig.axes[0]
if event.button == 'down':
next_slice(ax)
fig.canvas.draw()
#Next slice func.
def previous_slice(ax):
volume = ax.volume
ax.index = (ax.index - 1) % volume.shape[0]
#ax.imshow(volume[ax.index])
ax.images[0].set_array(volume[ax.index])
Figure is initialized in the main function. like:
fig, ax = plt.subplots()
ax.volume = volume # volume is a 3D data, a 3d np array.
ax.index = 1
ax.imshow(volume[ax.index])
fig.canvas.mpl_connect('scroll_event', mouse_scroll)
Everything worked pretty well even I don't understand what is the ax.images. However, problem occurred when I replace the ax.volume with a new volume data. It suddenly stop to render! Debug into the code, the ax.image[0] is correctly set at each event callback.
But, if change the image set_array method to ax.show(). Figure begins to render again. But axes imshow function is really slow comparing to the ax.images[0].set_array() method.
How can I fix this problem? really want to use set_array() method. Thank you very much.
A simple executable script is attached.
plot.py#googledrive
You need to work on the same image all the time. Best give this a name
img = ax.imshow(volume[ax.index])
You can then set the data for it using set_data.
import numpy as np
import matplotlib.pyplot as plt
#Mouse scroll event.
def mouse_scroll(event):
fig = event.canvas.figure
ax = fig.axes[0]
if event.button == 'down':
next_slice(ax)
fig.canvas.draw()
#Next slice func.
def next_slice(ax):
volume = ax.volume
ax.index = (ax.index - 1) % volume.shape[0]
img.set_array(volume[ax.index])
def mouse_click(event):
fig = event.canvas.figure
ax = fig.axes[0]
volume = np.random.rand(10, 10, 10)
ax.volume = volume
ax.index = (ax.index - 1) % volume.shape[0]
img.set_array(volume[ax.index])
fig.canvas.draw_idle()
if __name__ == '__main__':
fig, ax = plt.subplots()
volume = np.random.rand(40, 40, 40)
ax.volume = volume # volume is a 3D data, a 3d np array.
ax.index = 1
img = ax.imshow(volume[ax.index])
fig.canvas.mpl_connect('scroll_event', mouse_scroll)
fig.canvas.mpl_connect('button_press_event', mouse_click)
plt.show()
I have file containing points under the columns "x-cord", "y-cord", "value". These are irregularly spaced. I am trying to make a contour plot of "value" and overlay this over the original domain. I gave up trying to do this in both pgfplots and matlab and thought I would give python a go. An answer in any of these scripts would be fine. The python script is as follows
import numpy as np
from scipy.interpolate import griddata
import matplotlib.pyplot as plt
import numpy.ma as ma
from numpy.random import uniform, seed
from scipy.spatial import ConvexHull
#
# Loading data
filename = "strain.dat"
coordinates = []
x_c = []
y_c = []
z_c = []
xyz = open(filename)
title = xyz.readline()
for line in xyz:
x,y,z = line.split()
coordinates.append([float(x), float(y), float(z)])
x_c.append([float(x)])
y_c.append([float(y)])
z_c.append([float(z)])
xyz.close()
#
# Rehaping and translating data
x_c=np.ravel(np.array(x_c))
y_c=np.ravel(np.array(y_c))
z_c=np.ravel(np.array(z_c))
x_c = x_c-100.0
y_c = y_c-100.0
#
# Checking the convex hull
points=np.column_stack((x_c,y_c))
hull = ConvexHull(points);
plt.plot(points[hull.vertices,0], points[hull.vertices,1], 'r--', lw=2)
plt.scatter(x_c, y_c, marker='o', s=5, zorder=10)
#
# Mapping the irregular data onto a regular grid and plotting
xic = np.linspace(min(x_c), max(x_c), 1000)
yic = np.linspace(min(y_c), max(y_c), 1000)
zic = griddata((x_c, y_c), z_c, (xic[None,:], yic[:,None]))
CS = plt.contour(xic,yic,zic,15,linewidths=0.5,colors='k')
CS = plt.contourf(xic,yic,zic,15,cmap=plt.cm.summer)
plt.colorbar() # draw colorbar
#
#plt.scatter(x_c, y_c, marker='o', s=5, zorder=10)
plt.axis('equal')
plt.savefig('foo.pdf', bbox_inches='tight')
plt.show()
and the output looks like
The problem is that griddata uses a convex hull and this convex hull exceeds the edges of the irregular data. Is there any way to set the values of the griddata points which are outside the edges of the boundary of the original points to zero?
Edit
In the end I threw in the towel and reverted back to Matlab. I'll have to export the data to pgfplots to get a nice plot. The code I came up with was
x = strain.x;
y = strain.y;
z = strain.eps;
% Get the alpha shape (couldn't do this in python easily)
shp = alphaShape(x,y,.001);
% Get the boundary nodes
[bi, xy] = boundaryFacets(shp);
no_grid = 500;
xb=xy(:,1);
yb=xy(:,2);
[X,Y] = ndgrid(linspace(min(x),max(x),no_grid),linspace(min(y),max(y),no_grid));
Z = griddata(x,y,z,X,Y,'v4');
% Got through the regular grid and set the values which are outside the boundary of the original domain to Nans
for j = 1:no_grid
[in,on] = inpolygon(X(:,j),Y(:,j),xb,yb);
Z(~in,j) = NaN;
end
contourf(X,Y,Z,10),axis equal
colorbar
hold on
plot(xb,yb)
axis equal
hold off
Here is the resulting image.
If someone can do something similar in Python I'll happily accept the answer.
I had to plot interpolated data on a complex geometry (see the blue points on figure) P(x,z) (z is the horizontal coordinate). I used mask operations and it worked well. Without mask, the whole square (x=0..1 ; z=0..17.28) is covered by contourf.
## limiting values for geometry
xmax1=0.408
zmin1=6.
xmax2=0.064
zmin2=13.12
xmin=0.
xmax=1.
zmin=0.
zmax=17.28
# Grid for points
x1 = np.arange(xmin,xmax+dx,dx)
z1 = np.arange(zmin,zmax+dz,dz)
zi2,xi2 = np.meshgrid(z1,x1)
mask = (((zi2 > zmin2) & (xi2 > xmax2)) | ((zi2 > zmin1) & (zi2 <= zmin2) & (xi2 > xmax1)))
zim=np.ma.masked_array(zi2,mask)
xim=np.ma.masked_array(xi2,mask)
# Grid for P values
# npz=z coordinates of data, npx is the x coordinates and npp is P values
grid_p = scipy.interpolate.griddata((npz, npx), npp, (zim,xim),method='nearest')
pm=np.ma.masked_array(grid_p,mask)
# plot
plt.contour(zim, xim, pm, 25, linewidths=0.5, colors='k',corner_mask=False)
plt.contourf(zim, xim, pm, 25,vmax=grid_p.max(), vmin=grid_p.min(),corner_mask=False)
plt.colorbar()
# Scatter plot to check
plt.scatter(npz,npr, marker='x', s=2)
plt.show()
enter image description here
I want to do something with plt.hist2d and plt.colorbar and I'm having real trouble working out how to do it. To explain, I've written the following example:
import numpy as np
from matplotlib import pyplot as plt
x = np.random.random(1e6)
y = np.random.random(1e6)
plt.hist2d(x, y)
plt.colorbar()
plt.show()
This code generates a plot that looks something like the image below.
If I generate a histogram, ideally I would like the colour bar to extend beyond the maximum and minimum range of the data to the next step beyond the maximum and minimum. In the example in this question, this would set the colour bar extent from 9660 to 10260 in increments of 60.
How can I force either plt.hist2d or plt.colorbar to set the colour bar such that ticks are assigned to the start and end of the plotted colour bar?
I think this is what you're looking for:
h = plt.hist2d(x, y)
mn, mx = h[-1].get_clim()
mn = 60 * np.floor(mn / 60.)
mx = 60 * np.ceil(mx / 60.)
h[-1].set_clim(mn, mx)
cbar = plt.colorbar(h[-1], ticks=np.arange(mn, mx + 1, 60), )
This gives something like,
It's also often convenient to use tickers from the matplotlib.ticker, and use the tick_values method of tickers, but for this purpose I think the above is most convenient.
Good luck!
With huge thanks to farenorth, who got me thinking about this in the right way, I came up with a function, get_colour_bar_ticks:
def get_colour_bar_ticks(colourbar):
import numpy as np
# Get the limits and the extent of the colour bar.
limits = colourbar.get_clim()
extent = limits[1] - limits[0]
# Get the yticks of the colour bar as values (ax.get_yticks() returns them as fractions).
fractions = colourbar.ax.get_yticks()
yticks = (fractions * extent) + limits[0]
increment = yticks[1] - yticks[0]
# Generate the expanded ticks.
if (fractions[0] == 0) & (fractions[-1] == 1):
return yticks
else:
start = yticks[0] - increment
end = yticks[-1] + increment
if fractions[0] == 0:
newticks = np.concatenate((yticks, [end]))
elif fractions[1] == 1:
newticks = np.concatenate(([start], yticks))
else:
newticks = np.concatenate(([start], yticks, [end]))
return newticks
With this function I can then do this:
from matplotlib import pyplot as plt
x = np.random.random(1e6)
y = np.random.random(1e6)
h = plt.hist2d(x, y)
cbar = plt.colorbar()
ticks = get_colour_bar_ticks(cbar)
h[3].set_clim(ticks[0], ticks[-1])
cbar.set_clim(ticks[0], ticks[-1])
cbar.set_ticks(ticks)
plt.show()
Which results in this, which is what I really wanted:
So it's somewhat well known that in matplotlib zoom, pressing 'x' or 'y' when zooming will zoom on only the x or y axis. I would like to modify this slightly by subclassing the NavigationToolbar2 in backend_bases.py
Specifically, I would like to have the functionality that if the mouse is in the region on the canvas below a plot (or above, depending on where I have put my axes), or to the left or right of the plot, to choose to zoom with _zoom_mode = 'x' or 'y'. (In addition to keeping the default zoom to rect functionality when the mouse is inside the plot.)
Looking at backend_bases, it appears the method I need to modify is press_zoom
def press_zoom(self, event):
if event.button=1:
self._button_pressed=1
elif event.button == 3:
self._button_pressed=3
else:
self._button_pressed=None
return
x, y = event.x, event.y
# push the current view to define home if stack is empty
if self._views.empty(): self.push_current()
self._xypress=[]
for i, a in enumerate(self.canvas.figure.get_axes()):
if (x is not None and y is not None and a.in_axes(event) and
a.get_navigate() and a.can_zoom()) :
self._xypress.append(( x, y, a, i, a.viewLim.frozen(),
a.transData.frozen() ))
id1 = self.canvas.mpl_connect('motion_notify_event', self.drag_zoom)
id2 = self.canvas.mpl_connect('key_press_event',
self._switch_on_zoom_mode)
id3 = self.canvas.mpl_connect('key_release_event',
self._switch_off_zoom_mode)
self._ids_zoom = id1, id2, id3
self._zoom_mode = event.key
self.press(event)
and add an elif to the big if statement there and use it to set the zoom mode there, but what I cannot figure out (yet), is how to tell if the mouse is in the region appropriate for 'x' or 'y' zoom mode.
So, how can I tell when the mouse is just outside of a plot?
By converting the x and y coordinate into Axes coordinates. You can tell if they're just outside the axes, if they're less than 0 or larger than 1.
Here is a simple example of how it could work.
def is_just_outside(fig, event):
x,y = event.x, event.y
print x, y
for ax in fig.axes:
xAxes, yAxes = ax.transAxes.inverted().transform([x, y])
print xAxes, yAxes
if (-0.02 < xAxes < 0) | (1 < xAxes < 1.02):
print "just outside x-axis"
if (-0.02 < yAxes < 0) | (1 < yAxes < 1.02):
print "just outside y-axis"
x = np.linspace(-np.pi,np.pi,100)
y = np.sin(x)
fig = plt.figure()
plt.plot(x,y)
ax = fig.add_subplot(111)
fig.canvas.mpl_connect('button_press_event', lambda e: is_just_outside(fig, e))
plt.show()