Skip to content

Interactive plots with Plotly

Every plot that plasma-plots draws with Matplotlib can also be an interactive Plotly figure: pass backend="plotly". You get hover values, zoom and pan, legend entries you can click to hide or show a trace, and for animations a slider with Play and Pause buttons. The figure works in a notebook, or saved as a standalone HTML page.

Terminal window
pip install "plasma-plots[plotly]" # plotly, and kaleido for PNG/SVG/PDF
n.plasma.plot.slice(
coords="physical", plane="XY", t=-1, eta3=0, levels=[0.2], backend="plotly"
)

Loading interactive chart…

The Plotly version is the Matplotlib plot converted, not a second implementation. The plot is drawn with Matplotlib as usual, off screen, and the finished figure is translated into Plotly: the same data, color limits, contour lines, overlays, fits, reference curves, labels and layout. So both backends always show the same thing. On a mapped grid like this annulus, the colors are an image of the Matplotlib mesh, and hovering shows the value of the cell under the cursor.

Nothing else changes: the selection by keyword, the options, fit_results and data all stay the same.

result = energy.plasma.plot.timeseries(fit=(0.0, 2.0), backend="plotly")
result.fit_results[0].rate # the same fit as with Matplotlib
result.fig # a plotly.graph_objects.Figure
result.fig.update_layout(height=600)
result.save("energy.html") # a standalone page
result.save("energy.png") # through kaleido; also .svg, .pdf
result.save("energy.json") # the figure JSON, e.g. for a website

Under MPI, other ranks than 0 get a SkippedPlot, whose save writes nothing, so a script that saves its figures runs unchanged on several ranks. A figure you built yourself with plotly.graph_objects saves the same way, with the same defaults and only on rank 0: PlotResult(figure).save("page.html") (from plasma_plots.plotting import PlotResult).

To write the page, the image and the figure JSON at once, as a website needs them, use save_figure. It takes a plot’s result or a figure of your own, and saves nothing on other ranks:

from plasma_plots import save_figure
# energy.html, energy.png, energy.plotly.json
save_figure(result, "energy", show=True)
save_figure(movie, "phase-space", frame=-1) # the image shows the last frame
# a plotly.graph_objects.Figure, only the page
save_figure(my_figure, "orbits", formats=("html",))

A space-time map of a (t, z) field is a slice with time as one of its axes: e_x.plasma.plot.slice(x="z", y="t", symmetric=True, cmap="RdBu_r", backend="plotly").

Loading interactive chart…

import plasma_plots
# every plot from now on, e.g. in a notebook
plasma_plots.set_backend("plotly")
phi.plasma.plot.dispersion(dim="eta1")
out.plot.energies()
phi.plasma.plot.slice(t=-1, eta3=0, backend="matplotlib") # one exception

An animation becomes a figure with a slider over the sweep and Play/Pause buttons. The frames are drawn by the Matplotlib animation itself, so they match it frame by frame. plot.viewer(...) becomes a figure with a slider and no buttons. A Matplotlib viewer has one slider per remaining dimension; a Plotly figure has only one, so select all but one of them first.

n.plasma.plot.animation(
coords="physical",
plane="XY",
eta3=0,
levels=[0.2],
step=4,
backend="plotly",
)

Loading interactive chart…

Every frame stores only what changes: parts that are the same in every frame (static contour lines, the domain’s boundary, a fixed background, a mesh’s grid) are stored once. A contour level the field reaches only in some frames is hidden in the others. On a mapped grid, each frame is an image of the mesh; plasma_plots.plotly_backend.IMAGE_PIXELS = 600 (default 1200 along the longer side) makes those images, and the page, smaller.

Still, a long, finely resolved sweep makes a large page. Use step to keep every n-th frame, or max_frames for at most that many, evenly spaced (the first and the last included). If the first frame is still featureless, the image of an animation can show a later one; the page and the JSON keep the whole animation:

movie = f.plasma.plot.animation(
x="eta1", y="v1", max_frames=150, backend="plotly"
)
movie.save("phase-space.html")
movie.save(
"phase-space.png",
frame=len(movie.fig.frames) // 2,
width=1100,
height=650,
scale=2,
)

The option works on every Matplotlib plot: time series, profiles, slices, panels, vectors, spectra, dispersion relations, mode diagnostics, marker scatters, orbits, and out.plot.*.

phi.plasma.plot.power_spectrum(
peaks=2,
band=band,
frequencies={"TAE gap-centre estimate": omega},
backend="plotly",
)

Loading interactive chart…

The guides show many of the plots with Plotly, folded under each Matplotlib figure.

  • The PyVista 3-D views (isosurface, slices_3d, glyphs, streamlines, volume, movie, orbits_3d) are already interactive; see 3-D views.
  • frames and save_frames write PNG files for a movie.
  • ax=, which draws into a Matplotlib axes you made, cannot be combined with backend="plotly" (that raises TypeError). To compose several plots into one Plotly figure, use plasma_plots.figure(..., backend="plotly") (see Several plots in one figure).
  • out.plot.profile.* has its own backend="plotly", from scope-profiler (see Profiling).

The plotting functions (plasma_plots.plotting.plot_slice, …) return Matplotlib results, which convert with .to_plotly():

from plasma_plots.plotting import plot_slice
plot_slice(phi.isel(t=-1, eta3=0)).to_plotly().save("phi.html")

plasma_plots.plotly_backend.to_plotly(fig) and animation_to_plotly(animation) convert any figure or animation of these functions. If you draw something they don’t know (e.g. a histogram you added to a plot’s axes), it is left out with a ConversionWarning.