-
-
Notifications
You must be signed in to change notification settings - Fork 283
Plotting
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
from poliastro.examples import iss
from poliastro.plotting import plot
plot(iss)
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.
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.
- Keep the
OrbitPlotter
class as the only method for matplotlib-based, 2D static plot generation (at most, rename it toStaticOrbitPlotter
, but it's a bit long) - Provide a super easy way to plot a single orbit, in 2D or 3D
- Rework the
OrbitPlotter2D
andOrbitPlotter3D
classes to accept custom figures and be more flexible
Following this, we discuss interactive plotting workflows. All the static part stays the same.
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
andpoliastro.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(No need to worry, as there are no backends)Orbit.plot
? Offer a totally different API instead?
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.
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).
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 doplotter.plot(iss)
andplotter.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).
The layout of the Figure*
would be overwritten after calling .plot(fig=fig)
. This is the same behavior as pandas:
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.
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.
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
.
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.
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"
Be part of the poliastro community by joining our social media channels and stay up to date with current developments!