3D Plots in Python
A tutorial on how to make beautiful 3d plots with plotly and Python or IPython.
Section 8: 3D Plots ¶
Section 8 is divided as follows:
Quickstart (a 3d scatter plot of 3 points):
>>> import plotly.plotly as py
>>> from plotly.graph_objs import *
>>> # auto sign-in with credentials or use py.sign_in()
>>> trace1 = Scatter3d(
x=[1,2,3],
y=[3,4,5],
z=[1,3,4]
)
>>> data = Data([trace1])
>>> py.plot(data)
Check which version is installed on your machine and please upgrade if needed.
# (*) Import plotly package
import plotly
# Check plolty version (if not latest, please upgrade)
plotly.__version__
See the User Guide's homepage for more info on installation and upgrading.
In this section, we introduce Plotly's 3D plot engine. 3D plots are a new feature to Plotly and this section (in its currently form) only scratches the surface on its possibilities.
We first import a few modules and sign in to Plotly using our credential file:
# (*) To communicate with Plotly's server, sign in with credentials file
import plotly.plotly as py
# (*) Useful Python/Plotly tools
import plotly.tools as tls
# (*) Graph objects to piece together plots
from plotly.graph_objs import *
import numpy as np # (*) numpy for math functions and arrays
If you are not familiar with credentials files, refer to the User Guide's homepage .
8.1 A simple surface plot ¶
# (*) Import the math functions needed in this cell
from numpy import pi, cos, exp
# Define the function to be plotted
def fxy(x, y):
A = 1 # choose a maximum amplitude
return A*(cos(pi*x*y))**2 * exp(-(x**2+y**2)/2.)
# Choose length of square domain, make row and column vectors
L = 4
x = y = np.arange(-L/2., L/2., 0.1) # use a mesh spacing of 0.1
yt = y[:, np.newaxis] # (!) make column vector
# Get surface coordinates!
z = fxy(x, yt)
We will generate the surface using (you guessed it) the Surface graph object. As always, a good way to start using a new Plotly graph object is to call help:
help(Surface) # call help()!
In a lot of ways, Surface works offers the same functionaliy as the Heatmap and Contour offers. So consider,
trace1 = Surface(
z=z, # link the fxy 2d numpy array
x=x, # link 1d numpy array of x coords
y=y # link 1d numpy array of y coords
)
# Package the trace dictionary into a data object
data = Data([trace1])
Axes in 3D plots ¶
Axes in 3D Plotly plots work in little differently than in 2D: axes are bound to a Scene .
help(Scene) # call help()!
A scene is the wrapping element for the x, y and z axes of a 3D plot. A scene can be rotated, translated and zoomed into (more on how to do so at the end of this subsection). The
Scene
object links an
XAxis
, a
YAxis
and a
ZAxis
objects. Moreover, users can programmatically set the camera position using the
'cameraposition'
key.
Note also that Plotly allows users to make multi-scene 3d plots, in a similar way than for multi-axis plots. Examples of this feature will be added to this section shortly.
For now, consider the layout object:
# Dictionary of style options for all axes
axis = dict(
showbackground=True, # (!) show axis background
backgroundcolor="rgb(204, 204, 204)", # set background color to grey
gridcolor="rgb(255, 255, 255)", # set grid line color
zerolinecolor="rgb(255, 255, 255)", # set zero grid line color
)
# Make a layout object
layout = Layout(
title='$f(x,y) = A \cos(\pi x y) e^{-(x^2+y^2)/2}$', # set plot title
scene=Scene( # (!) axes are part of a 'scene' in 3d plots
xaxis=XAxis(axis), # set x-axis style
yaxis=YAxis(axis), # set y-axis style
zaxis=ZAxis(axis) # set z-axis style
)
)
The
'showbackgroundcolor'
and
'backgroundcolor'
axis keys are new to the axis objects. With them users can set the color of the axes' wall. They have only an effects in 3D plots.
Packaging layout and data into a figure object and a call to Plotly gets us our first 3D plot:
# Make a figure object
fig = Figure(data=data, layout=layout)
# (@) Send to Plotly and show in notebook
py.iplot(fig, filename='s8_surface')
A 3D interacting surface plot
Plotly 3D plots have three modes of interaction (togglable from the top right corner of the plot's frame):
- Rotation (second from left button)
- Zoom (third from left button)
- Pan (rightmost button)
You are invited to play around with each of them, Plotly 3D plots are alive like no other.
In case you are having a hard time familiarizing yourself with the rotation drag interactions, you can return to the original camera position by clicking on the home button which is the leftmost button on the top right corner of the plot's frame.
Moreover, this surface plot can be viewed in full screen at the following unique URL:
8.2 A helix curve in 3D ¶
Our next plot will be a 3-dimensional helix curve.
A helix curve is described mathematically in cartesian coordinates by:
$$\begin{align} x(t) &= \cos(t) \,, \\ y(t) &= \sin(t) \,, \\ z(t) &= t \end{align}$$
where $t$ is some conitnuous variable. So consider,
# (*) Import the math functions needed in this cell
from numpy import cos, sin
# Define a function generating the helix coordinates
def helix(t):
x = cos(t)
y = sin(t)
z = t
return x, y, z
Next, get the coordinates of the helix:
from numpy import pi # import pi for this cell
# Make a linear space from 0 to 4pi (i.e. 2 revolutions), get coords
t = np.linspace(0, 4*pi, 200)
x, y, z = helix(t)
For this figure, we will use the
Scatted3d
object which shares the same functionality as its 2D version. Its available keys are:
help(Scatter3d)
trace1 = Scatter3d(
x=x, # x coords
y=y, # y coords
z=z, # z coords
mode='lines', # (!) draw lines between coords (as in Scatter)
line=Line(
color='black', # black line segments
width=3 # set line segment width
)
)
# Package the trace dictionary into a data object
data = Data([trace1])
Add a title to a layout object:
# Make a layout object
layout = Layout(
title='Fig 8.2: Helix curve'
)
And finally,
# Make a figure object
fig = Figure(data=data, layout=layout)
# (@) Send to Plotly and show in notebook
py.iplot(fig, filename='s8_helix')
The full screen verison is available at the following URL:
8.3 Lorenz Attrator streaming plot ¶
Plotly's Streaming API is compatible with the 3D plot engine. As an example, we simulate a chaotic solution to the Lorenz system, also known as the Lorenz attractor or butterfly .
The Lorenz system is described mathematically as:
$$\begin{align} \frac{\text{d}x}{\text{d}t} &= \sigma (y-x) \,, \\ \frac{\text{d}y}{\text{d}t} &= x(\rho-z) - y\,, \\ \frac{\text{d}z}{\text{d}t} &= xy - \beta z \end{align}$$
where $(x,y,z)$ are the spatial coordinates, $t$ is time, $\sigma$, $\beta$ and $\rho$ are constants. With $\sigma = 10$, $\beta = 8/3$ and $\rho = 28$ the system exhibits chaotic behavior.
For our following graph, we integrate the Lorenz system using the
scipy.integrate
module. The following computations were adapted from this blog
post
from Jake Vanderplas. Big thanks!
So, first define a function returning the time dervative (i.e. the right-hand side) of the Lorenz system with the choatic parameters mentioned above:
# Time derivatives of the Lorenz system
def lorenz_deriv((x, y, z), t0, sigma=10., beta=8./3, rho=28.0):
return [sigma * (y - x), x * (rho - z) - y, x * y - beta * z]
The Plotly Streaming API makes use of stream tokens (more info in section 7.0 ):
stream_ids = tls.get_credentials_file()['stream_ids']
Our graph will feature two traces: one line trace tracking the trajectory and one marker trace of one point leading the way (similar to the double pendulum stream of section 7.2 ).
Those two traces require two unique stream tokens packaged in
Stream
graph objects. So, we initialize two
Scatter3d
objects as such:
# Choose random starting points, uniformly distributed from -15 to 15
np.random.seed(1)
x0 = -15 + 30 * np.random.random(3)
# Line trace following the trajectory
trace1 = Scatter3d(
x=x0[0],
y=x0[1],
z=x0[2],
mode='lines',
stream=Stream(
token=stream_ids[0], # (!) link stream id
maxpoints=3000 # (!) show a max. of 3000 pts on plot
)
)
# Marker trace leading the way
trace2 = Scatter3d(
x=x0[0],
y=x0[1],
z=x0[2],
mode='markers',
marker=Marker(
color="#1f77b4", # a darker blue
size=12,
symbol='circle'
),
stream=Stream(token=stream_ids[1]) # (!) link other stream id
)
# Package the trace dictionary into a data object
data = Data([trace1, trace2])
Add a title and fix the range of each of the axes as well as the margins in the layout object:
# Make a layout object
layout = Layout(
title='Lorenz Attractor',
scene=Scene(
xaxis=dict(
autorange=False,
range=[-25, 25] # set axis range
),
yaxis=dict(
autorange=False,
range=[-35, 35]
),
zaxis=dict(
autorange=False,
range=[0, 55]
)
),
margin=Margin(
l=0,
r=0,
t=80, # remove margin except top margin
b=0
),
showlegend=False
)
Initialize the graph by making a first call to Plotly:
# Make a figure object
fig = Figure(data=data, layout=layout)
# (@) Send to Plotly and show in different browser tab
py.plot(fig, filename='s8_lorenz-system')
Now, make two stream link objects and open their connections:
# (@) Make 1st instance of the stream link object,
# with same stream id as the 1st stream id object (in trace1)
s1 = py.Stream(stream_ids[0])
# (@) Make 2nd instance of the stream link object,
# with same stream id as the 2nd stream id object (in trace2)
s2 = py.Stream(stream_ids[1])
# (@) Open both streams
s1.open()
s2.open()
And finally, integrate and stream the data to Plotly:
# (*) Import module to integrate ODE and keep track of time
import scipy.integrate as integrate
import time
N = 10 # number of time integrate.odeint() integrations
i = 1 # init. counter
# Delay start of stream by 5 sec (time to switch tabs)
time.sleep(5)
# Solve the system N times
while i < N:
# Generate a time vector (1000 pts from 0 to 4)
# and integrate from initial position (x0)
t = np.linspace(0, 4, 1000)
X_t = integrate.odeint(lorenz_deriv, x0, t)
# Loop through each time step
for x_t in X_t:
s_data1 = Scatter3d(
x=x_t[0], # (!) write scalars to append the line trace
y=x_t[1],
z=x_t[2]
)
s_data2 = Scatter3d(
x=[x_t[0]], # (!) write list to overwrite the leading marker pt
y=[x_t[1]],
z=[x_t[2]]
)
s1.write(s_data1) # (@) stream data to Plotly!
s2.write(s_data2)
time.sleep(0.05) # (!) halt for 50 ms, for smoother plotting
x0 = X_t[-1, :] # overwrite initial coordiniates
i += 1 # add to counter
# (@) Close both streams when done plotting
s1.close()
s2.close()
In the above, the integration loop is finite, but there is no reason to not let the streams go on indefinitely. Simply replace the while-loop condition with:
>>> while True:
and enjoy.
Got Questions or Feedback?
About Plotly
- email: feedback@plot.ly
- tweet: @plotlygraphs
Notebook styling ideas
Big thanks to