The PyPlotCreator
Contents
The PyPlotCreator
#
The PyPlotCreator
focusses on creating plots using matplotlib.pyplot
.
Like the BasePlotCreator
, it relies on the plots being defined in a so-called plot function, which can be retrieved from importable modules or even from some file path.
These plot functions are meant to provide a bridge between the selected and transformed data and their visualization.
The PyPlotCreator
aims to make this process as smooth as possible by implementing a number of automations that reduce boilerplate code:
The plot helper interface provides an interface to
matplotlib.pyplot
and allows configuration-based manipulation of the axes limits, scales, and many other structural elements of a plot.With style contexts, plot aesthetics can be controlled right from the plot configuration, making consistent plotting styles more accessible.
The integration of the
matplotlib.animation
framework allows to easily implement plot functions that generate animation output.
Hint
There are further specializations of the PyPlotCreator
that make plotting of data originating from parameter sweeps easier.
See Plots from Multidimensional Data or the creator overview.
Note
Prior to dantro 0.18, this plot creator used to be called ExternalPlotCreator
, highlighting its ability to load external modules.
The PlotHelper
#
The PyPlotCreator
allows to automate many of the matplotlib.pyplot
function calls that would usually have to be part of the plot function itself.
Instead, the PlotHelper
takes up this task and provides a config-accessible bridge to the matplotlib interface.
See here for more information on the plot helper framework.
Adjusting a Plot’s Style#
Using the style
keyword, matplotlib RC parameters can be configured fully via the plot configuration; no need to touch the code.
Basically, this allows setting the matplotlib.rcParams
and makes the matplotlib stylesheets (matplotlib.style
) available.
The following example illustrates the usage:
---
my_plot:
# ...
# Configure the plot style
style:
base_style: ~ # optional, name of a matplotlib style to use
rc_file: ~ # optional, path to YAML file to load params from
# ... all further parameters are interpreted directly as rcParams
In the following example, the ggplot
style is used and subsequently adjusted by setting the linewidth, marker size and label sizes.
---
my_ggplot:
# ...
style:
base_style: ggplot
lines.linewidth : 3
lines.markersize : 10
xtick.labelsize : 16
ytick.labelsize : 16
For the base_style
entry, choose the name of a matplotlib stylesheet.
For valid RC parameters, see the matplotlib customization documentation.
Hint
Even the axes property cycle, i.e. the axes.prop_cycle
RC parameter, can be adjusted in this way.
For example, to use a Tab20-based color cycle, specify:
my_plot:
# ...
style:
axes.prop_cycle: "cycler('color', ['1f77b4', 'aec7e8', 'ff7f0e', 'ffbb78', '2ca02c', '98df8a', 'd62728', 'ff9896', '9467bd', 'c5b0d5', '8c564b', 'c49c94', 'e377c2', 'f7b6d2', '7f7f7f', 'c7c7c7', 'bcbd22', 'dbdb8d', '17becf', '9edae5'])"
The full syntax is supported here, including +
and *
operators between cycler(..)
definitions.
Implementing Plot Functions#
This section details how to implement plot functions for the PyPlotCreator
, making use of its specializations.
Recommended plot function signature#
The recommend plot function signature for this creator is not that different from the general one: It also makes use of the data transformation framework (implemented by the parent class).
Additionally, however, it uses the plot helper framework which requires that the plot function can handle an additional argument, hlpr
.
This PlotHelper
is the bridge to matplotlib.pyplot
and thus also needs to be used to invoke any plot-related commands:
from dantro.plot import is_plot_func, PlotHelper
@is_plot_func(use_dag=True, required_dag_tags=("x", "y"))
def my_plot(*, data: dict, hlpr: PlotHelper, **plot_kwargs):
"""A creator-averse plot function using the data transformation
framework and the plot helper framework.
Args:
data: The selected and transformed data, containing specified tags.
hlpr: The associated plot helper.
**plot_kwargs: Passed on to matplotlib.pyplot.plot
"""
# Create a lineplot on the currently selected axis
hlpr.ax.plot(data["x"], data["y"], **plot_kwargs)
# Done! The plot helper saves the plot :tada:
Super simple, aye? :)
In the case of the PyPlotCreator
, such a plot function can be averse to any creator, because it is compatible not only with the PyPlotCreator
but also with derived creators.
This makes it very flexible in its usage, serving solely as the bridge between data and their visualization:
For that reason, the decorator does not specify a creator
argument, but the plot configuration does.
The corresponding plot configuration could then look like this:
my_plot:
creator: pyplot
# Select the plot function
# ...
# Select data
select:
x: data/MyModel/some/path/foo
y:
path: data/MyModel/some/path/bar
transform:
- .mean
- increment
# ... further arguments
For more detail on the syntax, see above.
Note
While the plot function signature can remain as it is regardless of the chosen specialization of the PyPlotCreator
, the plot configuration will differ for the specializations.
See here and here for more information.
Note
This is the recommended way to define a plot function because it outsources a lot of the typical tasks (data selection and plot aesthetics) to dantro, allowing you to focus on implementing the bridge from data to visualization of the data.
Using these features not only reduces the amount of code required in a plot function but also makes the plot function future-proof. We highly recommend to use this interface.
Other possible plot function signatures#
Without data transformation framework#
There is the option to not using the transformation framework for data selection while still profiting from the plot helper.
Simply use the plot function decorator without passing use_dag
:
from dantro import DataManager
from dantro.plot import is_plot_func, PyPlotCreator, PlotHelper
@is_plot_func(creator=PyPlotCreator)
def my_plot(
*, dm: DataManager, hlpr: PlotHelper, **additional_plot_kwargs
):
"""A simple plot function using the plot helper framework.
Args:
dm: The loaded data tree.
hlpr: The plot helper, taking care of setting up the figure and
saving the plot.
**additional_kwargs: Anything else from the plot config.
"""
# Select some data ...
data = dm["foo/bar"]
# Create the plot
hlpr.ax.plot(data)
# Done. The helper will save the plot after the plot function returns.
Note
The dm
argument is only provided when not using the DAG framework.
Hint
To omit the helper as well, pass use_helper=False
to the decorator.
In that case you will also have to take care of saving the plot to the out_path
provided as argument to the plot function.
Bare basics#
If you do not want to use the decorator either, the signature is the same as in the case of the base class.
Animations#
With the PlotHelper
framework it is really simple to let your plot function support animation.
Say you have defined the following plot function:
from dantro.plot import is_plot_func, PlotHelper
@is_plot_func(use_dag=True, required_dag_tags=('time_series',))
def plot_some_data(*, data: dict,
hlpr: PlotHelper,
at_time: int,
**plot_kwargs):
"""Plots the data ``time_series`` for the selected time ``at_time``."""
# Via plot helper, perform a line plot of the data at the specified time
hlpr.ax.plot(data['time_series'][at_time], **plot_kwargs)
# Dynamically provide some information to the plot helper
hlpr.provide_defaults('set_title',
title="My data at time {}".format(at_time))
hlpr.provide_defaults('set_labels', y=dict(label="My data"))
To now make this function support animation, you only need to extend it by some update function, register that function with the helper, and mark the plot function as supporting an animation:
from dantro.plot import is_plot_func, PlotHelper
@is_plot_func(use_dag=True, required_dag_tags=('time_series',),
supports_animation=True)
def plot_some_data(*, data: dict,
hlpr: PlotHelper,
at_time: int,
**plot_kwargs):
"""Plots the data ``time_series`` for the selected time ``at_time``."""
# Via plot helper, perform a line plot of the data at the specified time
hlpr.ax.plot(data['time_series'][at_time], **plot_kwargs)
# Dynamically provide some information to the plot helper
hlpr.provide_defaults('set_title',
title="My data at time {}".format(at_time))
hlpr.provide_defaults('set_labels', y=dict(label="My data"))
# End of regular plot function
# Define update function
def update():
"""The animation update function: a python generator"""
# Go over all available times
for t, y_data in enumerate(data['time_series']):
# Clear the plot and plot anew
hlpr.ax.clear()
hlpr.ax.plot(y_data, **plot_kwargs)
# Set the title with current time step
hlpr.invoke_helper('set_title',
title="My data at time {}".format(t))
# Set the y-label
hlpr.provide_defaults('set_labels', y=dict(label="My data"))
# Done with this frame. Yield control to the plot framework,
# which will take care of grabbing the frame.
yield
# Register the animation update with the helper
hlpr.register_animation_update(update)
Ok, so the following things happened:
The
update
function is definedThe
update
function is passed to helper viadantro.plot.plot_helper.PlotHelper.register_animation_update()
The plot function is marked
supports_animation
This is all that is needed to define an animation update for a plot.
There are a few things to look out for:
In order for the animation update actually being used, the feature needs to be enabled in the plot configuration. The behaviour of the animation is controlled via the
animation
key; in it, set theenabled
flag.The animation update function is expected to be a so-called Python Generator, thus using the yield keyword. For more information, have a look here.
The file extension is taken care of by the
PlotManager
, which is why it needs to be adjusted on the top level of the plot configuration, e.g. when storing the animation as a movie.While whatever happens before the registration of the animation function is also executed, the animation update function should be build such as to also include the initial frame of the animation. This is to allow the plot function itself to be more flexible and the animation update not requiring to distinguish between initial frame and other frames.
In the example above, the
set_labels
helper has to be invoked for each frame ashlpr.ax.clear
removes it. To avoid this, one could use theset_data
method of the Line2d object, which is returned by matplotlib.pyplot.plot, to update the data. Depending on the objects used in your plot functions, there might exist a similar solution.
Warning
If it is not possible or too complicated to let the animation update function set the data directly, one typically has to redraw the axis or the whole figure.
In such cases, two important steps need to be taken in order to ensure correct functioning of the PlotHelper()
:
Specifying the
invoke_helpers_before_grab
flag when callingregister_animation_update()
, such that the helpers are invoked before grabbing each frame.If using a new figure object and/or axes grid, that needs to be communicated to the
PlotHelper()
viaattach_figure_and_axes()
.
For example implementations of such cases, refer to the plot functions specified in the dantro.plot.funcs.generic
module.
An example for an animation configuration is the following:
my_plot:
# Regular plot configuration
# ...
# Specify file extension to use, with leading dot (handled by PlotManager)
file_ext: .png # change to .mp4 if using ffmpeg writer
# Animation configuration
animation:
enabled: true # false by default
writer: frames # which writer to use: frames, ffmpeg, ...
writer_kwargs: # additional configuration for each writer
frames: # passed to 'frames' writer
saving: # passed to Writer.saving method
dpi: 254
ffmpeg:
init: # passed to Writer.__init__ method
fps: 15
saving:
dpi: 92
grab_frame: {} # passed to Writer.grab_frame and from there to savefig
animation_update_kwargs: {} # passed to the animation update function
Dynamically entering/exiting animation mode#
In some situations, one might want to dynamically determine if an animation should be carried out or not. For instance, this could be dependent on whether the dimensionality of the data requires another representation mode (the animation) or not.
For that purpose, the PlotHelper
supplies two methods to enter or exit animation mode, enable_animation()
and disable_animation()
.
When these are invoked, the plot function is directly left, the PyPlotCreator
enables or disables the animation, and the plot function is invoked anew.
A few remarks:
The decision on entering or exiting animation mode should ideally occur as early as possible within a plot function.
Repeatedly switching between modes is not possible. You should implement the logic for entering or exiting animation mode in such a way, that flip-flopping between the two modes is not possible.
The
animation
parameters need to be given if entering into animation mode is desired. In such cases,animation.enabled
key should be set toFalse
.The
PlotHelper
instance of the first plot function invocation will be discarded and a new instance will be created for the second invocation.
A plot function could then look like this:
from dantro.plot import is_plot_func, PlotHelper
@is_plot_func(use_dag=True, required_dag_tags=('nd_data',),
supports_animation=True)
def plot_nd(*, data: dict, hlpr: PlotHelper,
x: str, y: str, frames: str=None):
"""Performs an (animated) heatmap plot of 2D or 3D data.
The ``x``, ``y``, and ``frames`` arguments specify which data dimension
to associate with which representation.
If the ``frames`` argument is not given, the data needs to be 2D.
"""
d = data['nd_data']
if frames and d.ndim == 3:
hlpr.enable_animation()
elif not frames and d.ndim == 2:
hlpr.disable_animation()
else:
raise ValueError("Need either 2D data without the ``frames`` "
"argument, or 3D data with the ``frames`` "
"argument specified!")
# Do the 2D plotting for x and y dimensions here
# ...
def update():
"""Update the heatmap using the ``frames`` argument"""
# ...
hlpr.register_animation_update(update)
Specializing PyPlotCreator
#
This is basically the same as in the base class with the additional ability to specialize the plot helper.
For specializing the PlotHelper
, see here and then set the PyPlotCreator.PLOT_HELPER_CLS
class variable accordingly.
Note
For an operational example in a more complex framework setting, see the specialization used in the utopya project.