The Graphics Module

The graphics module’s goal is to solve the following limitations of the usual plotting functions (whose names start with plot_):

  1. Some functions perform too many tasks, making them difficult and confusing to use.

  2. The documentation is difficult to maintain because many keywords arguments are repeated on all plotting functions.

  3. The procedures to combine multiple plots together is not ideal. Namely, extend() or append() or adding multiple plots. All of them requires the plots to be shown on the screen or to be explicitely hidden by setting show=False in the function call.

The graphics module implements new functions into appropriate submodules. Each function solves a very specific task and is able to plot only one symbolic expression. Each function returns a list containing one or more data series, depending on the required visualization. Function names’ were chosen to allow TAB completion. For example, typing line and pressing TAB, a list of function names related to plotting lines will appear.

In order to render the data series on the screen, they must be passed into the graphics() function. We can think of it as the overall figure: thanks to its keyword arguments, we can set axis labels, axis limits, title, aspect ratio, etc. This function provides a clear separation between the resulting figure and the data series we are trying to plot.

A few examples will illustrate the differences between the usual plotting functions and graphics(). There are times were usual plotting functions are perfectly good, other times were graphics() is decisively better. Ultimately, this decision is left to the user.

In this example, plot() is good enough to visualize multiple expressions over the same range.

>>> from sympy import *
>>> from sympy.abc import x, y
>>> from spb import *
>>> plot(sin(x), cos(x), (x, -pi, pi))
Plot object containing:
[0]: cartesian line: sin(x) for x over (-3.141592653589793, 3.141592653589793)
[1]: cartesian line: cos(x) for x over (-3.141592653589793, 3.141592653589793)

(Source code, png)

../../_images/index-1.png

More typing is required to achieve the same results with graphics():

>>> graphics(
...     line(sin(x), (x, -pi, pi)),
...     line(cos(x), (x, -pi, pi)))
Plot object containing:
[0]: cartesian line: sin(x) for x over (-3.141592653589793, 3.141592653589793)
[1]: cartesian line: cos(x) for x over (-3.141592653589793, 3.141592653589793)

(Source code, png)

../../_images/index-2.png

Note that both approaches returned an instance of Plot, containing two data series.

Let’s visualize multiple expressions over different ranges, with custom rendering options:

>>> plot(
...     (sin(x), (x, 0, pi), "a"),
...     (cos(x), (x, -pi, pi), "b", {"linestyle": "--"}),
...     n=500)
Plot object containing:
[0]: cartesian line: sin(x) for x over (0.0, 3.141592653589793)
[1]: cartesian line: cos(x) for x over (-3.141592653589793, 3.141592653589793)

(Source code, png)

../../_images/index-3.png

Both expressions were evaluated over 500 points. The graphics module allows a much finer level of control. In the following code block, the first expression is evaluated over 20 points and will be rendered with a scatter, the second expression is evaluated over a 1000 points.

>>> graphics(
...     line(sin(x), (x, 0, pi), label="a", n=20, scatter=True),
...     line(cos(x), (x, -pi, pi), label="b", rendering_kw={"linestyle": "--"}))
Plot object containing:
[0]: cartesian line: sin(x) for x over (0.0, 3.141592653589793)
[1]: cartesian line: cos(x) for x over (-3.141592653589793, 3.141592653589793)

(Source code, png)

../../_images/index-4.png

Things gets even better for graphics() when we combine different kinds of visualization. The usual approach with plotting functions is kind of a mess:

from sympy import *
from spb import *
var("t u v theta phi")
r_sphere = 1
t = pi / 3 # half-cone angle
r_cone = r_sphere * sin(t)
p1 = plot3d_spherical(
    r_sphere, (theta, 0, pi), (phi, pi, 2*pi),
    "", rendering_kw={"opacity": 0},
    wireframe=True, wf_n1=13, wf_rendering_kw={"line_dash": "dot"},
    backend=PB, show=False, grid=False)
p2 = plot3d_spherical(
    r_sphere, (theta, pi - t, pi), (phi, pi, 2*pi),
    "sphere cap", wireframe=True, wf_n1=5,
    backend=PB, show=False)
p3 = plot3d_parametric_surface(
    u * cos(v), u * sin(v), -u / tan(t), (u, 0, r_cone), (v, pi , 2*pi),
    "cone", wireframe=True, wf_n1=7,
    backend=PB, show=False)
final = p1 + p2 + p3
# in real world, uncomment this line and remove the following two
# final.show()
fig = final.fig
fig

(Source code, png)

Note that show=False and backend=PB were set on all plots. Now, let’s achieve a similar result with the graphics module:

from sympy import *
from spb import *
var("t u v theta phi")
r_sphere = 1
sphere = surface_spherical(r_sphere, (theta, 0, pi), (phi, pi, 2*pi))[0]
t = pi / 3 # half-cone angle
r_cone = r_sphere * sin(t)
graphics(
    wireframe(sphere, n1=13, rendering_kw={"line_dash": "dot"}),
    surface_spherical(r_sphere, (theta, pi - t, pi), (phi, pi, 2*pi),
        label="sphere cap", wireframe=True, wf_n1=5),
    surface_parametric(u * cos(v), u * sin(v), -u / tan(t), (u, 0, r_cone), (v, pi , 2*pi),
        label="cone", wireframe=True, wf_n1=7),
    backend=PB, grid=False)

(Source code, png)

In the above code block:

  • backend=PB and grid=False were set only once as graphics() keyword arguments, which illustrates the separation between figure-level and data-level.

  • no surface of a half-sphere of unit radius was added to the plot, only wireframe lines. Previously, there were 3 surface, one of which was hidden by setting opacity=0. In the above code block there are only two surfaces. This explains the difference of the surface colors between the two approaches.

  • code is much cleaner and easy to read.

Without further ado, let’s explore the graphics module.