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.

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:

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 the enabled 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 as hlpr.ax.clear removes it. To avoid this, one could use the set_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():

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 to False.

  • 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.