Interact with a Python plot using keys instead of dragging - python

Using Matplotlib I made a 3D simulation of some moving objects. Currently, it is defaulted in a way such that if I drag my cursor across the screen I can move the plot around and see the objects in different perspectives. I was wondering if there is a way to change this such that instead of dragging, I can use the arrow keys on my keyboard to move the plot around in 3D?

You can use key events to detect a key press and react accordingly.
The viewpoint of the 3D plot is given by ax.elev and ax.azim so it's just a matter of modifying those properties.
That being said, you will have to be careful not to clash with existing keyboard shortcuts (or redefine those if they interfere with the keys you were thinking of using).
Here is an example that shows how to use the Shift-> and Shift<- keys to turn the plot around
from matplotlib import cbook
from matplotlib import cm
from matplotlib.colors import LightSource
import matplotlib.pyplot as plt
import numpy as np
dem = cbook.get_sample_data('jacksboro_fault_dem.npz', np_load=True)
z = dem['elevation']
nrows, ncols = z.shape
x = np.linspace(dem['xmin'], dem['xmax'], ncols)
y = np.linspace(dem['ymin'], dem['ymax'], nrows)
x, y = np.meshgrid(x, y)
region = np.s_[5:50, 5:50]
x, y, z = x[region], y[region], z[region]
fig, ax = plt.subplots(subplot_kw=dict(projection='3d'))
ls = LightSource(270, 45)
# To use a custom hillshading mode, override the built-in shading and pass
# in the rgb colors of the shaded surface calculated from "shade".
rgb = ls.shade(z, cmap=cm.gist_earth, vert_exag=0.1, blend_mode='soft')
surf = ax.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=rgb,
linewidth=0, antialiased=False, shade=False)
ax.set_xticks([])
ax.set_yticks([])
ax.set_zticks([])
def on_press(event):
print(event.key)
if event.key == 'shift+right':
ax.azim+=10
if event.key == 'shift+left':
ax.azim-=10
fig.canvas.draw_idle()
fig.canvas.mpl_connect('key_press_event', on_press)

Related

Is there a way from preventing Python Matplotlib 3D from cutting off the plot?

So I am currently plotting a 3d figure based on some X, Y ,Z list values from a data log read in. The shape and figure is coming out as it should except for one problem, the top half of it is cut off.
Here is an image to show what I mean:
Cut off figure from matplotlib example
It seems like there is an invisible border or margin that is cutting off the top part of the plot, and I am not really sure how to adjust that. I am assuming there must be some way to manually move that but I cannot find a way.
Any guides, links or direction would be helpful.
Here is my code:
import numpy as np
import pandas as pd
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot
fig = plt.figure(figsize=(8,15))
fig.tight_layout = True
fig.frameon = False
# syntax for 3-D projection
ax = plt.axes(projection='3d')
def length(arr):
if min(arr) < 0 or max(arr) < 0:
return abs(min(arr)) + abs(max(arr))
return abs(max(arr) - min(arr))
# plotting
x, y, z = df[mp['P150']].values, df[mp['P151']].values, df[mp['P152']].values
len_max = max(length(x), length(y), length(z))
xscalar = length(x) / len_max
yscalar = length(y) / len_max
zscalar = length(z) / len_max
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([xscalar, yscalar, zscalar, min(xscalar, yscalar, zscalar)])) # [x, y, z, figview]
ax.scatter(x, y, z, 'blue')
ax.tick_params(axis='x', labelsize=7)
ax.tick_params(axis='y', labelsize=7)
ax.set_xlabel(mp['P150'])
ax.set_ylabel(mp['P151'])
ax.set_zlabel(mp['P152'])
plt.savefig('sample.png')
plt.show()

Setting camera view on ipyvolume gives different viewing angle than matplotlib

I am trying to make an interactive notebook (with voila) where I use ipyvolume for plotting a surface. However, I do not manage to set the camera correctly with ipyvolume. It should be a top-down view onto the z-direction. It works fine in the matplotlib case, but setting the same angle in ipyvolume does give me some 45º view. How can I get it to show the top down view?
If there is another way to achieve that, that's also be ok (needs to work in voila and be able to dynamically update the X, Y, Z and color data).
make data
import pandas as pd
import numpy as np
import ipyvolume as ipv
g = np.linspace(-np.pi/2, np.pi/2, 10)
X, Y = np.meshgrid(g, g, indexing='ij')
Z = np.sin(X**2+Y**2)
the ipyvolume plot
fig1 = ipv.figure()
mesh = ipv.plot_surface(X, Z, Y)
ipv.show()
ipv.pylab.view(90,-90)
the matpotib pot
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot(projection='3d')
ax.view_init(90, -90)
ax.set_xlabel('x')
ax.set_ylabel('y')
surf = ax.plot_surface(X, Y, Z)
It looks like you swapped Y and Z by accident in mesh = ipv.plot_surface(X, Z, Y), that could explain why you don't get the view you want. Once you make that change, the default view for ipyvolume (which is ipv.pylab.view(0,0)) appears to be the view you want. See code below, tested on google colab:
fig1 = ipv.figure()
mesh = ipv.plot_surface(X, Y, Z)
ipv.show()
And the output gives:

Only plot part of a 3d figure using matplotlib

I got a problem when I was plotting a 3d figure using matplotlib of python. Using the following python function, I got this figure:
Here X, Y are meshed grids and Z and Z_ are functions of X and Y. C stands for surface color.
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
def plot(X, Y, Z, Z_, C):
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_surface(
X, Y, Z, rstride=1, cstride=1,
facecolors=cm.jet(C),
linewidth=0, antialiased=False, shade=False)
surf_ = ax.plot_surface(
X, Y, Z_, rstride=1, cstride=1,
facecolors=cm.jet(C),
linewidth=0, antialiased=False, shade=False)
ax.view_init(elev=7,azim=45)
plt.show()
But now I want to cut this figure horizontally and only the part whose z is between -1 and 2 remain.
What I want, plotted with gnuplot, is this:
I have tried ax.set_zlim3d and ax.set_zlim, but neither of them give me the desired figure. Does anybody know how to do it using python?
Nice conical intersections you have there:)
What you're trying to do should be achieved by setting the Z data you want to ignore to NaN. Using graphene's tight binding band structure as an example:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# generate dummy data (graphene tight binding band structure)
kvec = np.linspace(-np.pi,np.pi,101)
kx,ky = np.meshgrid(kvec,kvec)
E = np.sqrt(1+4*np.cos(3*kx/2)*np.cos(np.sqrt(3)/2*ky) + 4*np.cos(np.sqrt(3)/2*ky)**2)
# plot full dataset
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(kx,ky,E,cmap='viridis',vmin=-E.max(),vmax=E.max(),rstride=1,cstride=1)
ax.plot_surface(kx,ky,-E,cmap='viridis',vmin=-E.max(),vmax=E.max(),rstride=1,cstride=1)
# focus on Dirac cones
Elim = 1 #threshold
E[E>Elim] = np.nan
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
#ax.plot_surface(kx2,ky2,E2,cmap='viridis',vmin=-Elim,vmax=Elim)
#ax.plot_surface(kx2,ky2,-E2,cmap='viridis',vmin=-Elim,vmax=Elim)
ax.plot_surface(kx,ky,E,cmap='viridis',rstride=1,cstride=1,vmin=-Elim,vmax=Elim)
ax.plot_surface(kx,ky,-E,cmap='viridis',rstride=1,cstride=1,vmin=-Elim,vmax=Elim)
plt.show()
The results look like this:
Unfortunately, there are problems with the rendering of the second case: the apparent depth order of the data is messed up in the latter case: cones in the background are rendered in front of the front ones (this is much clearer in an interactive plot). The problem is that there are more holes than actual data, and the data is not connected, which confuses the renderer of plot_surface. Matplotlib has a 2d renderer, so 3d visualization is a bit of a hack. This means that for complex overlapping surfaces you'll more often than not get rendering artifacts (in particular, two simply connected surfaces are either fully behind or fully in front of one another).
We can get around the rendering bug by doing a bit more work: keeping the data in a single surface by not using nans, but instead colouring the the surface to be invisible where it doesn't interest us. Since the surface we're plotting now includes the entire original surface, we have to set the zlim manually in order to focus on our region of interest. For the above example:
from matplotlib.cm import get_cmap
# create a color mapping manually
Elim = 1 #threshold
cmap = get_cmap('viridis')
colors_top = cmap((E + Elim)/2/Elim) # listed colormap that maps E from [-Elim, Elim] to [0.0, 1.0] for color mapping
colors_bott = cmap((-E + Elim)/2/Elim) # same for -E branch
colors_top[E > Elim, -1] = 0 # set outlying faces to be invisible (100% transparent)
colors_bott[-E < -Elim, -1] = 0
# in nature you would instead have something like this:
#zmin,zmax = -1,1 # where to cut the _single_ input surface (x,y,z)
#cmap = get_cmap('viridis')
#colors = cmap((z - zmin)/(zmax - zmin))
#colors[(z < zmin) | (z > zmax), -1] = 0
# then plot_surface(x, y, z, facecolors=colors, ...)
# or for your specific case where you have X, Y, Z and C:
#colors = get_cmap('viridis')(C)
#colors[(z < zmin) | (z > zmax), -1] = 0
# then plot_surface(x, y, z, facecolors=colors, ...)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# pass the mapped colours as the facecolors keyword arg
s1 = ax.plot_surface(kx, ky, E, facecolors=colors_top, rstride=1, cstride=1)
s2 = ax.plot_surface(kx, ky, -E, facecolors=colors_bott, rstride=1, cstride=1)
# but now we need to manually hide the invisible part of the surface:
ax.set_zlim(-Elim, Elim)
plt.show()
Here's the output:
Note that it looks a bit different from the earlier figures because 3 years have passed in between and the current version of matplotlib (3.0.2) has very different (and much prettier) default styles. In particular, edges are now transparent in surface plots. But the main point is that the rendering bug is gone, which is evident if you start rotating the surface around in an interactive plot.

Shifting the triangles order to match colormap

This question is a sequel of a previous one but regarding this time the colormap and the order of the triangle. I want to interpolate experimental data over a surface so as to enable a continuous colormap with however the surface known only at its corner node. To interpolate, I put a canonical example which works quite well but fails on real data.
Indeed as shown in the example below, the initial triangulation results in two triangles with a huge gap between them, cf first picture. When the interpolation is done, it doesn't get any better and the colormap is also lost, cf. second picture. The best so far is by interverting z and y to get adjacent triangles from the beginning which results in a successful interpolation. However as you might notice in the third picture, the surface is tilted by 90° which is normal since I switch y for z and vice-versa.
However when I switch back y and z in the tri_surf function with ax.plot_trisurf(new.x, new_z, new.y, **kwargs), the colormap doesn't follow, cf. picture 4.
I thought of rotating the colormap in somehow or generate new triangles from the interpolated ones with triang = tri.Triangulation(new.x, new_z) but without any success. So any idea or hint about properly doing the initial triangulation with two adjacent triangles, as for the third picture, but with the surface oriented correclty and ultimately the colormap proportional to the Y-value.
import numpy
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
import matplotlib.tri as tri
x=numpy.array([0.00498316, 0.00498316, 0.00996632, 0.00996632])
y=numpy.array([-0.00037677, -0.00027191, -0.00078681, -0.00088475])
z=numpy.array([0., -0.0049926, 0., -0.00744763])
# Initial Triangle
fig = plt.figure()
ax = Axes3D(fig)
triang = tri.Triangulation(x, y)
norm = plt.Normalize(vmax=y.max(), vmin=y.min())
ax.plot_trisurf(x, y, z, triangles=triang.triangles)
# Interpolated Triangle
fig = plt.figure()
ax = Axes3D(fig)
triang = tri.Triangulation(x, y)
refiner = tri.UniformTriRefiner(triang)
interpolator = tri.LinearTriInterpolator(triang, z)
new, new_z = refiner.refine_field(z, interpolator, subdiv=4)
kwargs = dict(triangles=new.triangles, cmap=cm.jet, norm=norm, linewidth=0, antialiased=False)
ax.plot_trisurf(new.x, new.y, new_z, **kwargs)
# Best so far
fig = plt.figure()
ax = Axes3D(fig)
triang = tri.Triangulation(x, z)
refiner = tri.UniformTriRefiner(triang)
interpolator = tri.LinearTriInterpolator(triang, y)
new, new_z = refiner.refine_field(y, interpolator, subdiv=4)
kwargs = dict(triangles=new.triangles, cmap=cm.jet, norm=norm, linewidth=0, antialiased=False)
ax.plot_trisurf(new.x, new.y, new_z, **kwargs)
plt.show()
Apparently the automatic triangulation doesn't produce the right triangles for you, but you can specify how you want your triangles manually:
triang = tri.Triangulation(x, y, [[3,2,1],[1,2,0]])
# alternatively:
triang = tri.Triangulation(x, y, [[3,2,0],[1,3,0]])
These two ways give rather different results:
However, now the interpolation becomes awkward, because for some (x,y) there are multiple z-values.. One way of bypassing this issue is interpolating and plotting the 2 large triangles separately:
import numpy
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
import matplotlib.tri as tri
def plot_refined_tri(x, y, z, ax, subdiv=4, **kwargs):
triang = tri.Triangulation(x, y)
refiner = tri.UniformTriRefiner(triang)
interpolator = tri.LinearTriInterpolator(triang, z)
new, new_z = refiner.refine_field(z, interpolator, subdiv=subdiv)
ax.plot_trisurf(new.x, new.y, new_z, triangles=new.triangles, **kwargs)
x=numpy.array([0.00498316, 0.00498316, 0.00996632, 0.00996632])
y=numpy.array([-0.00037677, -0.00027191, -0.00078681, -0.00088475])
z=numpy.array([0., -0.0049926, 0., -0.00744763])
fig = plt.figure()
ax = Axes3D(fig)
# note: I normalized on z-values to "fix" the colormap
norm = plt.Normalize(vmax=z.max(), vmin=z.min())
kwargs = kwargs = dict(linewidth=0.2, cmap=cm.jet, norm=norm)
idx = [3,2,1]
plot_refined_tri(x[idx], y[idx], z[idx], ax, **kwargs)
idx = [1,2,0]
plot_refined_tri(x[idx], y[idx], z[idx], ax, **kwargs)
plt.show()
Result:

Modelling an asteroid with Matplotlib using surface and wireframe

I am trying to model an asteroid using plot_surface and plot_wireframe. I have x y and z values for the points on the surface of the asteroid. The wireframe is accurate to the shape of the asteroid but the surface plot does not fit the wireframe. How can I get the surface plot to fit the wireframe or how could i use the wireframe to get a 3d solid model? Here is my code for the model:
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
from matplotlib.mlab import griddata
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
data = np.genfromtxt('data.txt')
x = data[:,0]
y = data[:,1]
z = data[:,2]
ax.plot_wireframe(x, y, z, rstride=1, cstride=1, alpha=1)
xi = np.linspace(min(x), max(x))
yi = np.linspace(min(y), max(y))
X, Y = np.meshgrid(xi, yi)
Z = griddata(x, y, z, xi, yi)
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.coolwarm,
linewidth=0, antialiased=False)
ax.set_zlim(-1.01, 1.01)
plt.show()
The data is in this format, although there are a lot more lines in the original file:
-1.7738946051191869E-002 4.3461451610545973E-002 1.3393057231408241
-0.29733561550902488 0.32305812106837900 1.3393057231408241
-0.29733561550902488 0.16510132228266330 1.3548631099230350
-0.21872587865015569 2.4170900455101410E-002 1.3610011616437809
1.4452975249810950E-002 -0.20900795344486520 1.3610011616437809
1.5732454381265970E-002 -0.20900795344486520 1.3608751439485580
-0.34501536374240321 0.51320241386595655 1.3158820995876130
-0.40193014435941982 0.45628763324893978 1.3158820995876130
-0.42505849480150409 0.28183419537116011 1.3307863198123011
-0.18994178462386799 -0.19294290416565860 1.3424523041534830
1.4452975249810939E-002 -0.39733766403933751 1.3424523041534830
5.8021940902131752E-002 -0.57108837516584876 1.3210481842104100
9.3746267961881152E-002 -0.61017602710257668 1.3136798474111200
0.26609469681891229 -0.43782759824554562 1.3136798474111200
0.17938460413447810 0.39179924148155021 1.2357401964919650
8.9613011902522258E-002 0.42818009222325598 1.2584008460875080
0.33671539027096409 -0.47165177581327772 1.2965073126705291
0.53703772594296528 -0.47165177581327777 1.2357401964919561
-0.19242375014122229 0.71021685426700043 1.2584008460875080
-0.34501536374240321 0.66763766324752027 1.2904902860951690
Hope you can help
Could you provide an image of what is happening? I'm guessing that you are getting an image where the surface of the asteroid seems to be jumping all over the place. Is that correct? If so it could be caused by the plotter not knowing the order of the points.
If we have a set of points, which contains all the points of a unit circle, then we would expect a drawing of those points to create a unit circle. If, however, you decided to connect each point to two other points, then it wouldn't necessarily look like a circle. If (for some reason) you connected one point to another point on the other side of the circle and continued to do that until each point was connected to two other points, it may or may not look like a circle because each point isn't necessarily connected to the adjacent points.
The same is true for the your asteroid. You need to come up with some scheme so that the plotter knows how to connect the points, otherwise you will continue to have the same problem.
The following example of a circle should illustrate my point:
import math
import matplotlib.pylab as plt
import random
thetaList = range(360)
random.shuffle(thetaList)
degToRad = lambda x: float(x) * math.pi / float(180)
x = [math.cos(degToRad(theta)) for theta in thetaList]
y = [math.sin(degToRad(theta)) for theta in thetaList]
#plot the cirlce
plt.plot(x,y)
plt.show()

Categories

Resources