Plotting - poliastro/poliastro GitHub Wiki

Goals

Orbit visualization or plotting is essential to any Astrodynamics analysis. poliastro should make it trivial.

  • Plotting a single orbit with all the necessary information should be one line of code, ideally not requiring an extra import
  • Plotting should work in 3D as well as 2D
  • Plotting in the notebook should be interactive and allow zoom, pan, show/hide of individual elements... While plot outside the notebook should remain straightforward
  • Plots containing several orbits should be informative and prevent common mistakes (see Plotting multiple orbits: use cases)
  • Plotting should be flexible enough so power users can customize the results as much as they can
  • Plotting should follow common patterns already found in other Python projects, for example pandas

Current status

from poliastro.examples import iss
from poliastro.plotting import plot

plot(iss)

2D vs 3D

As a result of projection, 2D plots are more complicated to produce than 3D plots, however they are an invaluable tool because of its simplicity. At this moment, there is an essential difference between 2D and 3D plots: in 3D we set the view, which is an orbit-agnostic property, and in 2D we set the frame, which is an orbit-related property.

When we set the frame in 2D plots, the (1, 0, 0) vector is used as X and (0, 1, 0) is used as Y. This is an implicit decision that cannot be overriden. The reason it is a good decision is because it is consistent with 3D plots, that use (1, 0, 0) as X, (0, 1, 0) as Y, and (0, 0, 1) as Z, with the addition of a default view.

About "backends"

TL;DR: Say "no" to backends.

The word "backend" seems to imply that there is a unified API, and we already know that it is not that easy or even desirable. From https://github.com/poliastro/poliastro/issues/338#issuecomment-449955855:

  • matplotlib is special-cased everywhere in Jupyter/IPython, and the logic that shows the figure is very difficult to replicate elsewhere.
  • Alternative matplotlib backends, such as ipympl, still have some issues to retain PNG output, because of how the ecosystem works (see https://github.com/matplotlib/jupyter-matplotlib/issues/16).
  • matplotlib is literally everywhere and there will always be users that prefer its simplicity and publication-quality output, despite its inability to do 3D right.
  • Focusing on Plotly for "the notebook experience" will allow us to leverage its awesome interactive features.

Proposal

  1. Keep the OrbitPlotter class as the only method for matplotlib-based, 2D static plot generation (at most, rename it to StaticOrbitPlotter, but it's a bit long)
  2. Provide a super easy way to plot a single orbit, in 2D or 3D
  3. Rework the OrbitPlotter2D and OrbitPlotter3D classes to accept custom figures and be more flexible

Use cases

Following this, we discuss interactive plotting workflows. All the static part stays the same.

Plotting a single orbit

from poliastro.examples import iss

iss.plot()

The default representation should be 3D, which gives more insight about the orbit.

Notice that plot returns a new Plotly FigureWidget so it gets shown in the notebook directly. This is the same behavior as pandas:

>>> df.plot(ax=ax) is ax
True

If it returned a Figure, then we would need more boilerplate:

from poliastro.example import iss
from plotly.offline import iplot
from plotly.offline import init_notebook_mode
init_notebook_mode()

iplot(iss.plot())

This has the disadvantage that the figure won't be saved as output by default, as discussed in https://github.com/jupyter-widgets/ipywidgets/issues/1632#issuecomment-407648729.

  • If possible, we should avoid circular dependencies between poliastro.plotting and poliastro.twobody.Orbit (or ugly imports will be needed)
  • To separate interactive vs batch workflows, should we do automatic backend selection? Add an extra parameter to Orbit.plot? Offer a totally different API instead? (No need to worry, as there are no backends)

Plotting two orbits in the same cell

iss.plot()
churi.plot()

There can only be one output per cell, and globally tracking the figure would be difficult (and probably confusing). Therefore, we don't do anything special and just output the last plot.

Plotting one orbit in a user-created figure

Both Figure and FigureWidget are initialized in the same way (they both inherit from plotly.basedatatypes.BaseFigure): data (optional, to create an empty figure), layout (optional) and frames (for animations).

For API simplicity and consistency with more complex use cases, we introduce the OrbitPlotter* objects to control the plotting:

from plotly.graph_objs import Figure, FigureWidget
from poliastro.examples import iss
from poliastro.plotting import OrbitPlotter3D

fig = FigureWidget()
plotter = OrbitPlotter3D(fig)
plotter.plot(iss)

This still returns the figure being plotted. The OrbitPlotter* objects create a figure if not specified:

from poliastro.examples import iss
from poliastro.plotting import OrbitPlotter3D

plotter = OrbitPlotter3D()  # A FigureWidget is created
plotter.plot(iss)

This is good because it's a continuation of the old API. The only difference is that we do not need the show() call (which just returns the FigureWidget and remains optional).

Alternatives

We could as well have iss.plot(plotter=plotter), but then:

  • There's not "one obvious way to do it" anymore
  • One can't do trajectory.plot(plotter=plotter), so it's better to be consistent and make the user always do plotter.plot(iss) and plotter.plot_trajectory(iss.sample()).

Another alternative would be to accept iss.plot(fig=fig). But this makes it impossible to track the attractor or the frame for multiple orbits in the same figure (see older versions of this wiki page).

Layouts

The layout of the Figure* would be overwritten after calling .plot(fig=fig). This is the same behavior as pandas:

pandas plotting

poliastro would need to update the layout to name the axis, add shapes and other things. Therefore, the user is expected to modify the layout after the plot, unless they know what they are doing.

Plotting two orbits in the same figure

This should be as easy as the previous case:

from poliastro.examples import iss, churi
from poliastro.plotting import OrbitPlotter3D

plotter = OrbitPlotter3D()  # A FigureWidget is created
plotter.plot(iss)
plotter.plot(churi)

The plotter keeps track of the attractor of the orbits, the view, and some more things.

Plotting one orbit in 2D

from poliastro.examples import iss

iss.plot(kind="2d")

The implicit decision here is to use the perifocal frame. This should still return a FigureWidget.

Alternatives

We could have something like iss.plot(kind="2d") (with a default of kind="3d"). However, as it is meant to be a simple function, with no ability to specify the plotter, it's better to offer only one.

More ideas: Customizing the output

Users can always retrieve figure.data or figure.layout and customize it as much as they want. However, Plotly is not so well known, so it would be nice to offer some shortcuts to customize common things. Current methods:

  • OrbitPlotter*.plot_trajectory
  • OrbitPlotter*.set_attractor
  • OrbitPlotter3D.set_view / OrbitPlotter2D.set_frame

At the moment we have a OrbitPlotter.orbits that stores some state: a list of (orbit, label, color) data. However, trajectories are not stored (this is a bug) and therefore we could improve it a bit more to do things like:

# Show and hide attractor
plotter.show_attractor(False)

# Iterate over all segments
for segment in plotter.segments:
    segment.show_osculating(False)  # What happens with trajectories?
    segment.set_color("#ffcc00")

# Underlying plotly data
plotter.segments[0].data.line.color = "#ffcc00"

# Can we index by orbit?
plotter.segments[iss].data.line.color = "#ffcc00"