4 - Creating custom plots
Sometimes, the functions exposed by Sympy’s plotting module are not enough to accomplish our visualization objectives. If that’s the case, we can either:
lambdify
the symbolic expressions and evaluate it numerically. However, this process is manually intensive.If the expressions can be plotted by the common plotting functions (
plot
,plot3d
,plot_parametric
, …), we can easily extract the numerical data by calling theget_data
method of the interested series. Remember, the plot object can be indexed in order to access the series.
Once we have the numerical data, we can use our preferred plotting library. If we are lucky enough, we can also:
use one of the plotting functions as a starting point;
extract the plot object associated to the plotting library;
use the appropriate command of the plotting library to add new data to the plot.
Example - Editing and Adding Data
The current backends are able to plot lines, gradient lines, contours, quivers, streamlines. However, they are not able to plot things like curve fills, bars, …
In this example we are going to illustrate a procedure that can be used to
further customize the plot. Since we are going to use backend-specific
commands, the procedure is backend-specific. In the following, we are going
to use PlotlyBackend
. For other backends, the procedure might need to be
adjusted.
Let’s say we would like to plot on the same figure:
a normal distribution filled to the horizontal axis.
a dampened oscillation.
bars following an exponential decay at integer locations.
from sympy import *
from spb import *
x, mu, sigma = symbols("x, mu, sigma")
expr1 = 1 / (sigma * sqrt(2 * pi)) * exp(-((x - mu) / sigma)**2 / 2)
expr2 = cos(x) * exp(-x / 6)
expr3 = exp(-x / 5)
We start by plotting the first two expressions, as the third one requires a different approach:
p = plot(
(expr1.subs({sigma: 0.8, mu: 5}), "normal"),
(expr2, "oscillation"),
(x, 0, 10), backend=PB)
Now, we’d like to fill the first curve. First, we extract the figure object; then we set the necessary attribute to get the job done. Obviously, the following procedure depends on the backend being used.
f = p.fig
f.data[0]["fill"]="tozerox"
f
At this point we have to convert expr3
to numerical data.
We can do it with the plot
function:
p2 = plot(expr3, (x, 0, 10), adaptive=False, only_integers=True, show=False)
# p2[0] is the data series representing our expression
xx, yy = p2[0].get_data()
print(xx)
print(yy)
[ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
[1. 0.81873075 0.67032005 0.54881164 0.44932896 0.36787944
0.30119421 0.24659696 0.20189652 0.16529889 0.13533528]
The advantage of this approach is that we can visualize the data
(if show=True
).
It is important to realize that the get_data()
method of each series may
returns different elements. Read its documentation to find out what it returns:
help(p2[0].get_data)
Help on method get_data in module spb.series:
get_data() method of spb.series.LineOver1DRangeSeries instance
Return coordinates for plotting the line.
Returns
=======
x: np.ndarray
x-coordinates
y: np.ndarray
y-coordinates
z: np.ndarray (optional)
z-coordinates in case of Parametric3DLineSeries,
Parametric3DLineInteractiveSeries
param : np.ndarray (optional)
The parameter in case of Parametric2DLineSeries,
Parametric3DLineSeries or AbsArgLineSeries (and their
corresponding interactive series).
Now that we have generated the numerical values at integer locations, we can add the bars with the appropriate command:
import plotly.graph_objects as go
import numpy as np
f.add_trace(go.Bar(x=xx, y=yy, width=np.ones_like(xx) / 2, name="bars"))
f
That’s it, job done.