Using the Plotter class

Description

The Plotter class aims to provide a framework, which can be used to handle default values and collect common codeblocks needed for different plotting frameworks. The Plotter class is a base class that should be subclassed for different Plotting backends. See MatplotlibPlotter or BokehPlotter for examples. The subclass provides a dictionary of all the keys that should be handled by the plotter class. The Plotter class provides a hierarchy of overwriting these parameters (Higher numbers take precedence).

  1. Function defaults set with Plotter.set_defaults() with default_type='function'

  2. Global defaults set with Plotter.set_defaults()

  3. Parameters set with Plotter.set_parameters()

The subclasses should then also provide the plotting backend specific useful code snippets. For example showing colorbars, legends, and so on …

For a list of these functions you can look at the respective documentation (MatplotlibPlotter or BokehPlotter)

Writing a plotting function

In the following we will go through a few examples of how to write a simple plotting function using the Plotter class. We will be focusing on the MatplotlibPlotter, but all of this is very similar for other plotting backends.

Local instance

Even though the Plotter class is meant to be used globally or on the module level, it can also be useful locally for simplifying simple plotting scripts. Here we have a example of a function producing a single plot with the given data for the x and y coordinates.

def plot_with_defaults(x,y,**kwargs):
   from masci_tools.vis.matplotlib_plotter import MatplotlibPlotter

   #First we instantiate the MatplotlibPlotter class
   plot_params = MatplotlibPlotter()

   #Now we process the given arguments
   plot_params.set_parameters(**kwargs)

   #Set up the axis, on which to plot the data
   ax = plot_params.prepare_plot(xlabel='X', ylabel='Y', title='Single Scatterplot')

   #The plot_kwargs provides a way to get the keyword arguments for the
   #actual plotting call to `plot` in this case.
   plot_kwargs = plot_params.plot_kwargs()

   ax.plot(x, y, **plot_kwargs)

   #The MatplotlibPlotter has a lot of small helper functions
   #In this case we just want to set the limits and scale of the
   #axis if they were given
   plot_params.set_scale(ax)
   plot_params.set_limits(ax)
   plot_params.save_plot('figure')

   return ax

import numpy as np

x = np.linspace(-1, 1, 10)
y = x**2

#Some examples
plot_with_defaults(x, y)
plot_with_defaults(x, y, limits={'x': (0,1)})
plot_with_defaults(x, y, marker='s', markersize=20)
Hide code cell output
../_images/3753168a6613fc98d8e2ecd502906505c37553836f37a2f7cd5af0293527642f.png ../_images/eed3d5b89f82fe2cb306e3316920a13e8409edd8c0a1749bb403b6c160fa0311.png ../_images/0d786a49ac771712d95290eede75f82d231768952f8b67e199cc068969f2045b.png
<Axes: title={'center': 'Single Scatterplot'}, xlabel='X', ylabel='Y'>

Global/Module level instance

The local instance already gives us reusable code snippets to avoid common pitfalls when doing matplotlib/bokeh plots. But when instantiating the Plotter class locally we have no way of letting the user modify the global defaults.

However, when handling global state we need to be careful to not leave the instance of the Plotter class in an inconsistent state. If an error is thrown inside the plotting routine the parameters would stay set and may lead to very unexpected results. For this reason every plotting function using a global or module level instance of these plotters should be decorated with the ensure_plotter_consistency() decorator. This does two things:

  1. If an error occurs in the decorated function the parameters will be reset before the error is raised

  2. It makes sure that nothing inside the plotting routine changed the user defined defaults

Let us take the previous example and convert it to use a global instance

from masci_tools.vis.matplotlib_plotter import MatplotlibPlotter
from masci_tools.vis.parameters import ensure_plotter_consistency

#First we instantiate the MatplotlibPlotter class
plot_params = MatplotlibPlotter()

#The decorator needs to get the plotter object
#that is used inside the function
@ensure_plotter_consistency(plot_params)
def plot_with_defaults(x,y,**kwargs):

   #Now we process the given arguments
   plot_params.set_parameters(**kwargs)

   #Set up the axis, on which to plot the data
   ax = plot_params.prepare_plot(xlabel='X', ylabel='Y', title='Single Scatterplot')

   #The plot_kwargs provides a way to get the keyword arguments for the
   #actual plotting call to `plot` in this case.
   plot_kwargs = plot_params.plot_kwargs()

   ax.plot(x, y, **plot_kwargs)

   #The MatplotlibPlotter has a lot of small helper functions
   #In this case we just want to set the limits and scale of the
   #axis if they were given
   plot_params.set_scale(ax)
   plot_params.set_limits(ax)
   plot_params.save_plot('figure')

   return ax

import numpy as np

x = np.linspace(-1, 1, 10)
y = x**2

#Some examples
plot_with_defaults(x, y)
plot_params.set_defaults(marker='s', markersize=20)
plot_with_defaults(x, y, limits={'x': (0,1)})
plot_with_defaults(x, y)
Hide code cell output
../_images/3753168a6613fc98d8e2ecd502906505c37553836f37a2f7cd5af0293527642f.png ../_images/e4ff35132e16ae211a02b6af287ea3a1423ad9fb11b3cdfa0442c298b04fe685.png ../_images/0d786a49ac771712d95290eede75f82d231768952f8b67e199cc068969f2045b.png
<Axes: title={'center': 'Single Scatterplot'}, xlabel='X', ylabel='Y'>

The Plotter.set_defaults() method is exposed in the two main modules for plotting masci_tools.vis.plot_methods masci_tools.vis.bokeh_plots as the functions masci_tools.vis.plot_methods.set_mpl_plot_defaults() and masci_tools.vis.bokeh_plots.set_bokeh_plot_defaults() specific to the plotter instance that is used in these modules.

Function defaults

Some functions may want to set function specific defaults, that make sense inside the function, but may not be useful globally. The following example sets the default linewidth for our function to 6.

Note

Function defaults are also reset by the ensure_plotter_consistency() decorator, when the plotting function terminates successfully or in an error

from masci_tools.vis.matplotlib_plotter import MatplotlibPlotter
from masci_tools.vis.parameters import ensure_plotter_consistency

#First we instantiate the MatplotlibPlotter class
plot_params = MatplotlibPlotter()

#The decorator needs to get the plotter object
#that is used inside the function
@ensure_plotter_consistency(plot_params)
def plot_with_defaults(x,y,**kwargs):

   #Set the function defaults
   plot_params.set_defaults(default_type='function', linewidth=6)

   #Now we process the given arguments
   plot_params.set_parameters(**kwargs)

   #Set up the axis, on which to plot the data
   ax = plot_params.prepare_plot(xlabel='X', ylabel='Y', title='Single Scatterplot')

   #The plot_kwargs provides a way to get the keyword arguments for the
   #actual plotting call to `plot` in this case.
   plot_kwargs = plot_params.plot_kwargs()

   ax.plot(x, y, **plot_kwargs)

   #The MatplotlibPlotter has a lot of small helper functions
   #In this case we just want to set the limits and scale of the
   #axis if they were given
   plot_params.set_scale(ax)
   plot_params.set_limits(ax)
   plot_params.save_plot('figure')

   return ax

import numpy as np

x = np.linspace(-1, 1, 10)
y = x**2

#Some examples
plot_with_defaults(x, y)
plot_params.set_defaults(marker='s', markersize=20)
plot_with_defaults(x, y, limits={'x': (0,1)})
plot_with_defaults(x, y)
Hide code cell output
../_images/66efba187c64d87c617335a2dc017964147378b902a07c9818f2b4838ccc0304.png ../_images/a98e470936612e2cbb7d4617dea2673e512e787b2caf002dfdda8f468b46804f.png ../_images/82a2fec90637842f6c89cc780f210bdd50b0f4cb044b384d0e900b12b5b5bff7.png
<Axes: title={'center': 'Single Scatterplot'}, xlabel='X', ylabel='Y'>

Passing keyword arguments directly to plot calls

The plotter classes have a restricted set of keys that they recognize as valid parameters. This set is of course not complete, since there is a vast number of parameters you can set for all plotting backends. In our previous examples unknown keys will immediately lead to an error in the call to Plotter.set_parameters(). To enable this functionality we can provide the continue_on_error=True as an argument to this method.

Then the unknown keys are ignored and are returned in a dictionary. Additionally you can explicitly bypass the plotter object if you provide arguments in a dictionary with the name extra_kwargs it will be ignored, unpacked and returned along with the unknown keys

Warning

Be careful with the this feature and especially the extra_kwargs, since there is no check for name clashes with this argument. You might also run into situations, where arguments of different names collide with arguments provided by the Plotter

from masci_tools.vis.matplotlib_plotter import MatplotlibPlotter
from masci_tools.vis.parameters import ensure_plotter_consistency

#First we instantiate the MatplotlibPlotter class
plot_params = MatplotlibPlotter()

#The decorator needs to get the plotter object
#that is used inside the function
@ensure_plotter_consistency(plot_params)
def plot_with_defaults(x,y,**kwargs):

   #Set the function defaults
   plot_params.set_defaults(default_type='function', linewidth=6)

   #Now we process the given arguments (unknown ones are returned)
   kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs)

   #Set up the axis, on which to plot the data
   ax = plot_params.prepare_plot(xlabel='X', ylabel='Y', title='Single Scatterplot')

   #The plot_kwargs provides a way to get the keyword arguments for the
   #actual plotting call to `plot` in this case.
   plot_kwargs = plot_params.plot_kwargs()

   ax.plot(x, y, **plot_kwargs, **kwargs)

   #The MatplotlibPlotter has a lot of small helper functions
   #In this case we just want to set the limits and scale of the
   #axis if they were given
   plot_params.set_scale(ax)
   plot_params.set_limits(ax)
   plot_params.save_plot('figure')

   return ax

import numpy as np

x = np.linspace(-1, 1, 10)
y = x**2

#The key markerfacecolor is not known to the MatplotlibPlotter
plot_with_defaults(x, y, markerfacecolor='red', markersize=20)
Hide code cell output
../_images/8b306fea5a0c2c34701b17ce969c6447e08e829dde9f05d6f4203220b7aef53e.png
<Axes: title={'center': 'Single Scatterplot'}, xlabel='X', ylabel='Y'>

Multiple plotting calls

The plotter classes also provide support for multiple plotting calls with different data sets in a single plotting function. To enable this feature we need to set two properties on the Plotter; single_plot to False and num_plots to the number of plot calls made in this function. The plot specific parameters can then be specified in two ways. Shown behind the two ways is the way to set the color of the second data set to red.

  1. List of values (None for unspecified values) [None,'red']

  2. Dict with integer indices for the specified values {1: 'red'}

Unspecified values are replaced with the previously set defaults.

Note

The num_plots and single_plot properties are also reset by the ensure_plotter_consistency()

from masci_tools.vis.matplotlib_plotter import MatplotlibPlotter
from masci_tools.vis.parameters import ensure_plotter_consistency

#First we instantiate the MatplotlibPlotter class
plot_params = MatplotlibPlotter()

#The decorator needs to get the plotter object
#that is used inside the function
@ensure_plotter_consistency(plot_params)
def plot_2lines_with_defaults(x,y,**kwargs):

   plot_params.single_plot = False
   plot_params.num_plots = 2

   #Set the function defaults
   plot_params.set_defaults(default_type='function', linewidth=6)

   #Now we process the given arguments (unknown ones are returned)
   kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs)

   #Set up the axis, on which to plot the data
   ax = plot_params.prepare_plot(xlabel='X', ylabel='Y', title='Single Scatterplot')

   #The plot_kwargs provides a way to get the keyword arguments for the
   #actual plotting call to `plot` in this case.
   #For multiple plots this will be a list of dicts
   #of length `num_plots`
   plot_kwargs = plot_params.plot_kwargs()

   ax.plot(x[0], y[0], **plot_kwargs[0], **kwargs)
   ax.plot(x[1], y[1], **plot_kwargs[1], **kwargs)

   #The MatplotlibPlotter has a lot of small helper functions
   #In this case we just want to set the limits and scale of the
   #axis if they were given
   plot_params.set_scale(ax)
   plot_params.set_limits(ax)
   plot_params.save_plot('figure')

   return ax

import numpy as np

x = np.linspace(-1, 1, 10)
y = x**2
y2 = x**3

#The key markerfacecolor is not known to the MatplotlibPlotter
plot_2lines_with_defaults([x,x], [y,y2])
plot_2lines_with_defaults([x,x], [y,y2],
                          color={1:'red'}, linestyle=['--',None])
Hide code cell output
../_images/0ab8b55de29893803cd4598c47b949512dc637640abc87030d12654497d843bd.png ../_images/90e234789f4621d9d0172ba3f833bc57d8fffe469e0495004107213eeda075a9.png
<Axes: title={'center': 'Single Scatterplot'}, xlabel='X', ylabel='Y'>

Custom function specific parameters

You might have situations, where you want to have some function specific parameters, that should pull from the previously set defaults or even a custom default value.

The Plotter.add_parameter() method is implemented exactly for this purpose. It creates a new key to be handled by the plotter class and with the arguments default_from or default_value we can specify what the defaults should be. default_value sets a specific value, default_from specifies a key from the plotter class from which to take the default value.

The plot_kwargs() method then can take keyword arguments to replace the arguments to take with your custom parameters

Note

These added parameters live on the function defaults and parameters level, meaning they will be removed by the ensure_plotter_consistency() decorator after the function finishes

from masci_tools.vis.matplotlib_plotter import MatplotlibPlotter
from masci_tools.vis.parameters import ensure_plotter_consistency

#First we instantiate the MatplotlibPlotter class
plot_params = MatplotlibPlotter()

#The decorator needs to get the plotter object
#that is used inside the function
@ensure_plotter_consistency(plot_params)
def plot_shifted_with_defaults(x,y,**kwargs):

   #Set the function defaults
   plot_params.set_defaults(default_type='function', linewidth=6)

   plot_params.add_parameter('linestyle_shifted',
                             default_from='linestyle')

   #Now we process the given arguments (unknown ones are returned)
   kwargs = plot_params.set_parameters(continue_on_error=True, **kwargs)

   #Set up the axis, on which to plot the data
   ax = plot_params.prepare_plot(xlabel='X', ylabel='Y', title='Single Scatterplot')

   #The plot_kwargs provides a way to get the keyword arguments for the
   #actual plotting call to `plot` in this case.
   plot_kwargs = plot_params.plot_kwargs()
   ax.plot(x, y, **plot_kwargs, **kwargs)

   #This call replaces the parameter linestyle with our custom
   #parameter linestyle_shifted
   plot_kwargs = plot_params.plot_kwargs(linestyle='linestyle_shifted')
   ax.plot(x, y+2, **plot_kwargs, **kwargs)

   #The MatplotlibPlotter has a lot of small helper functions
   #In this case we just want to set the limits and scale of the
   #axis if they were given
   plot_params.set_scale(ax)
   plot_params.set_limits(ax)
   plot_params.save_plot('figure')

   return ax

import numpy as np

x = np.linspace(-1, 1, 10)
y = x**2

plot_shifted_with_defaults(x, y)
plot_shifted_with_defaults(x, y, linestyle_shifted='--')
Hide code cell output
../_images/59df643de0180c81c5f42fd3b077a98fa1960f3aa3deff8cb6017c59ddcd3538.png ../_images/4264f3ecc41be75a58026348b2fd31df83208a0164feae81199596fb8e67b921.png
<Axes: title={'center': 'Single Scatterplot'}, xlabel='X', ylabel='Y'>

Nested plotting functions

More complex plotting routines might want to call other plotting routines to simplify their structure. However, this has a side-effect when working with the Plotter class and the ensure_plotter_consistency() decorator. Since the decorator resets the parameters and function defaults after a plotting function has been called you lose everything that you might have modified in the enclosing plotting function.

If you do need access to these parameters after calling a nested plotting function the NestedPlotParameters() contextmanager is implemented. It defines a local scope, in which a plotting function can change the parameters and function defaults. After exiting the local scope the parameters and function defaults are always in the same state as when the with block was entered (Even if an error is raised). The nested plotting function will also start with the state that was set before.

Usage is shown here:

from masci_tools.vis.matplotlib_plotter import MatplotlibPlotter
from masci_tools.vis.parameters import ensure_plotter_consistency
from masci_tools.vis.parameters import NestedPlotParameters


#First we instantiate the MatplotlibPlotter class
plot_params = MatplotlibPlotter()

@ensure_plotter_consistency(plot_params)
def nested_plot_function(x, y, **kwargs):

   plot_params.set_defaults(default_type='function',
                            linewidth=10, linestyle='--')

   #The contextmanager also needs a reference to the plotter object
   #to manage
   with NestedPlotParameters(plot_params):
      ax = plot_with_defaults(x,y,**kwargs)

   #Will plot with the above set defaults
   plot_kwargs = plot_params.plot_kwargs()
   ax.plot(x, y+2, **plot_kwargs)
   plot_params.save_plot('figure')
   
   return ax

@ensure_plotter_consistency(plot_params)
def plot_with_defaults(x,y,**kwargs):

   #Set the function defaults
   plot_params.set_defaults(default_type='function', linewidth=6)

   #Now we process the given arguments
   plot_params.set_parameters(**kwargs)

   #Set up the axis, on which to plot the data
   ax = plot_params.prepare_plot(xlabel='X', ylabel='Y', title='Single Scatterplot')

   #The plot_kwargs provides a way to get the keyword arguments for the
   #actual plotting call to `plot` in this case.
   plot_kwargs = plot_params.plot_kwargs()

   ax.plot(x, y, **plot_kwargs)

   #The MatplotlibPlotter has a lot of small helper functions
   #In this case we just want to set the limits and scale of the
   #axis if they were given
   plot_params.set_scale(ax)
   plot_params.set_limits(ax)

   return ax

import numpy as np

x = np.linspace(-1, 1, 10)
y = x**2

nested_plot_function(x, y)
nested_plot_function(x, y, linewidth=1)
Hide code cell output
../_images/231e1df7a57511f3dfcf2416aba0ed234917abee6fcdbcc346120c5aa8cac14a.png ../_images/333e9390340fa4715209ff93e4996a12d0eb3cd46ee74beddfef0f621165d045.png
<Axes: title={'center': 'Single Scatterplot'}, xlabel='X', ylabel='Y'>