Plotting with Matplotlib#

The default plotting extension for HoloViews until a 2.0 release is Matplotlib when HoloViews will start defaulting to Bokeh (see the Plotting with Bokeh user guide).

While the 'bokeh' backend provides many useful interactive features, the 'matplotlib' plotting extension provides many additional features that are well suited to static exports for printed figures or static documents. To enable the 'matplotlib' backend, we can initialize the Holoviews notebook extension:

import numpy as np
import holoviews as hv
from holoviews import opts

hv.extension('matplotlib')

Working with matplotlib directly#

When HoloViews outputs matplotlib plots it creates and manipulates a matplotlib Figure, axes and artists in the background. If at any time you need access to the underlying matplotlib representation of an object you can use the hv.render function to convert it. For example let us convert a HoloViews Image to a matplotlib Figure, which will let us access and modify every aspect of the plot:

img = hv.Image(np.random.rand(10, 10))

fig = hv.render(img)

print('Figure: ', fig)
print('Axes:   ', fig.axes)
Figure:  Figure(400x400)
Axes:    [<Axes: xlabel='x', ylabel='y'>]

Setting Backend Opts#

HoloViews does not expose every single option from matplotlib.

Instead, HoloViews allow users to attach hooks to modify the plot object directly–but writing these hooks could be cumbersome, especially if it’s only used for a single line of update.

Fortunately, HoloViews allows backend_opts for the Matplotlib backend to configure options by declaring a dictionary with accessor specification for updating the plot components.

For example, here’s how to remove the frame on the legend.

plot = hv.Curve([1, 2, 3], label="a") * hv.Curve([1, 4, 9], label="b")
plot.opts(
    show_legend=True,
    backend_opts={
        "legend.frame_on": False,
    }
)

The following is the equivalent, as a hook.

def hook(plot, element):
    legend = plot.handles["legend"]
    legend.set_frame_on(False)

plot = hv.Curve([1, 2, 3], label="a") * hv.Curve([1, 4, 9], label="b")
plot.opts(
    show_legend=True,
    hooks=[hook]
)

Notice how much more concise it is with backend_opts, and it’s even possible to update items in a list.

For example you can set the first legend label’s fontsize!

plot = hv.Curve([1, 2, 3], label="a") * hv.Curve([1, 4, 9], label="b")
plot.opts(
    show_legend=True,
    backend_opts={"legend.get_texts()[0:2].fontsize": 18}
)

With knowledge of the methods in matplotlib, it’s possible to configure many other plot components besides legend. Some examples include colorbar, xaxis, yaxis, and much, much more.

If you’re unsure, simply input your best guess and it’ll try to provide a list of suggestions if there’s an issue.

Static file format#

Matplotlib supports a wide range of export formats suitable for both web and print publishing. During interactive exploration in the Notebook, your results are always visible within the notebook itself, and usually png plots are good enough. To switch the default file format you can use the hv.output utility and control set fig option, supported formats include:

['png', 'svg', 'pdf']

however pdf output is not supported in the notebook. To demonstrate let us switch output to SVG:

hv.output(fig='svg')

Now when we create a plot in the notebook the output will be rendered as SVGs:

from holoviews.operation import contours

x = y = np.arange(-3.0, 3.0, 0.1)
X, Y = np.meshgrid(x, y) 

def g(x,y,c):
    return 2*((x-y)**2/(x**2+y**2)) + np.exp(-(np.sqrt(x**2+y**2)-c)**2)

img = hv.Image(g(X,Y,2))
filled_contours = contours(img, filled=True)

filled_contours

The hv.save function allows exporting plots to all supported formats simply by changing the file extension. Certain formats support additional options, e.g. for png export we can also specify the dpi (dots per inch):

hv.save(filled_contours, 'contours.png', dpi=144)

To confirm the plot was exported correctly we can load it back in using IPython’s Image object:

from IPython.display import Image
Image('contours.png', width=400)

For a publication, you will usually want to select SVG format by changing the file extension, because this vector format preserves the full resolution of all text and drawing elements. SVG files can be be used in some document preparation programs directly (e.g. LibreOffice), and can easily be converted using e.g. Inkscape to PDF for use with PDFLaTeX or to EMF for use with Microsoft Word. They can also be edited using Inkscape or other vector drawing programs to move graphical elements around, add arbitrary text, etc., if you need to make final tweaks before using the figures in a document. You can also embed them within other SVG figures in such a drawing program, e.g. by creating a larger figure as a template that automatically incorporates multiple SVG files you have exported separately.

Animation support#

The 'matplotlib' backend supports animated outputs either as video (using mp4 or webm formats) or as animated GIFS. This is useful for output to web pages that users can view without needing to interact with. It can also be useful for creating descriptive pages for HoloViews constructs that require a live Python/Jupyter server rather than just a web page - see for example DynamicMap.

GIF#

In recent versions of matplotlib (>=2.2.0) GIF output can also be generated using pillow, which is what HoloViews uses by default. The pillow dependency can be installed using conda or pip using: conda install pillow or pip install pillow.

To display a plot The speed of the animation is controlled using the fps (frames per second):

holomap = hv.HoloMap([(t, hv.Image(g(X,Y, 4 * np.sin(np.pi*t)))) for t in np.linspace(0,1,21)]).opts(
    cmap='fire', colorbar=True, show_title=False, xaxis='bare', yaxis='bare')

contour_hmap = contours(holomap, filled=True)

hv.output(contour_hmap, holomap='gif', fps=5)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~/work/holoviews/holoviews/.pixi/envs/docs/lib/python3.11/site-packages/matplotlib/animation.py:224, in AbstractMovieWriter.saving(self, fig, outfile, dpi, *args, **kwargs)
    223 try:
--> 224     yield self
    225 finally:

File ~/work/holoviews/holoviews/.pixi/envs/docs/lib/python3.11/site-packages/matplotlib/animation.py:1126, in Animation.save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback)
   1125         frame_number += 1
-> 1126 writer.grab_frame(**savefig_kwargs)

File ~/work/holoviews/holoviews/.pixi/envs/docs/lib/python3.11/site-packages/matplotlib/animation.py:499, in PillowWriter.grab_frame(self, **savefig_kwargs)
    497 if im.getextrema()[3][0] < 255:
    498     # This frame has transparency, so we'll just add it as is.
--> 499     self._frame.append(im)
    500 else:
    501     # Without transparency, we switch to RGB mode, which converts to P mode a
    502     # little better if needed (specifically, this helps with GIF output.)

AttributeError: 'PillowWriter' object has no attribute '_frame'

During handling of the above exception, another exception occurred:

IndexError                                Traceback (most recent call last)
File ~/work/holoviews/holoviews/.pixi/envs/docs/lib/python3.11/site-packages/IPython/core/formatters.py:1036, in MimeBundleFormatter.__call__(self, obj, include, exclude)
   1033     method = get_real_method(obj, self.print_method)
   1035     if method is not None:
-> 1036         return method(include=include, exclude=exclude)
   1037     return None
   1038 else:

File ~/work/holoviews/holoviews/holoviews/core/dimension.py:1277, in Dimensioned._repr_mimebundle_(self, include, exclude)
   1270 def _repr_mimebundle_(self, include=None, exclude=None):
   1271     """
   1272     Resolves the class hierarchy for the class rendering the
   1273     object using any display hooks registered on Store.display
   1274     hooks.  The output of all registered display_hooks is then
   1275     combined and returned.
   1276     """
-> 1277     return Store.render(self)

File ~/work/holoviews/holoviews/holoviews/core/options.py:1423, in Store.render(cls, obj)
   1421 data, metadata = {}, {}
   1422 for hook in hooks:
-> 1423     ret = hook(obj)
   1424     if ret is None:
   1425         continue

File ~/work/holoviews/holoviews/holoviews/ipython/display_hooks.py:287, in pprint_display(obj)
    285 if not ip.display_formatter.formatters['text/plain'].pprint:
    286     return None
--> 287 return display(obj, raw_output=True)

File ~/work/holoviews/holoviews/holoviews/ipython/display_hooks.py:261, in display(obj, raw_output, **kwargs)
    259 elif isinstance(obj, (HoloMap, DynamicMap)):
    260     with option_state(obj):
--> 261         output = map_display(obj)
    262 elif isinstance(obj, Plot):
    263     output = render(obj)

File ~/work/holoviews/holoviews/holoviews/ipython/display_hooks.py:149, in display_hook.<locals>.wrapped(element)
    147 try:
    148     max_frames = OutputSettings.options['max_frames']
--> 149     mimebundle = fn(element, max_frames=max_frames)
    150     if mimebundle is None:
    151         return {}, {}

File ~/work/holoviews/holoviews/holoviews/ipython/display_hooks.py:209, in map_display(vmap, max_frames)
    206     max_frame_warning(max_frames)
    207     return None
--> 209 return render(vmap)

File ~/work/holoviews/holoviews/holoviews/ipython/display_hooks.py:76, in render(obj, **kwargs)
     73 if renderer.fig == 'pdf':
     74     renderer = renderer.instance(fig='png')
---> 76 return renderer.components(obj, **kwargs)

File ~/work/holoviews/holoviews/holoviews/plotting/renderer.py:380, in Renderer.components(self, obj, fmt, comm, **kwargs)
    377     plot, fmt = self._validate(obj, fmt)
    379 if not isinstance(plot, Viewable):
--> 380     html = self._figure_data(plot, fmt, as_script=True, **kwargs)
    381     return {'text/html': html}, {MIME_TYPES['jlab-hv-exec']: {}}
    383 registry = list(Stream.registry.items())

File ~/work/holoviews/holoviews/holoviews/plotting/mpl/renderer.py:145, in MPLRenderer._figure_data(self, plot, fmt, bbox_inches, as_script, **kwargs)
    143     with mpl.rc_context(rc=plot.fig_rcparams):
    144         anim = plot.anim(fps=self.fps)
--> 145     data = self._anim_data(anim, fmt)
    146 else:
    147     fig = plot.state

File ~/work/holoviews/holoviews/holoviews/plotting/mpl/renderer.py:198, in MPLRenderer._anim_data(self, anim, fmt)
    195 if not hasattr(anim, '_encoded_video'):
    196     # Windows will throw PermissionError with auto-delete
    197     with NamedTemporaryFile(suffix=f'.{fmt}', delete=False) as f:
--> 198         anim.save(f.name, writer=writer, **anim_kwargs)
    199         video = f.read()
    200     f.close()

File ~/work/holoviews/holoviews/.pixi/envs/docs/lib/python3.11/site-packages/matplotlib/animation.py:1098, in Animation.save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback)
   1093     return a * np.array([r, g, b]) + 1 - a
   1095 # canvas._is_saving = True makes the draw_event animation-starting
   1096 # callback a no-op; canvas.manager = None prevents resizing the GUI
   1097 # widget (both are likewise done in savefig()).
-> 1098 with (writer.saving(self._fig, filename, dpi),
   1099       cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None)):
   1100     if not writer._supports_transparency():
   1101         facecolor = savefig_kwargs.get('facecolor',
   1102                                        mpl.rcParams['savefig.facecolor'])

File ~/work/holoviews/holoviews/.pixi/envs/docs/lib/python3.11/contextlib.py:158, in _GeneratorContextManager.__exit__(self, typ, value, traceback)
    156     value = typ()
    157 try:
--> 158     self.gen.throw(typ, value, traceback)
    159 except StopIteration as exc:
    160     # Suppress StopIteration *unless* it's the same exception that
    161     # was passed to throw().  This prevents a StopIteration
    162     # raised inside the "with" statement from being suppressed.
    163     return exc is not value

File ~/work/holoviews/holoviews/.pixi/envs/docs/lib/python3.11/site-packages/matplotlib/animation.py:226, in AbstractMovieWriter.saving(self, fig, outfile, dpi, *args, **kwargs)
    224     yield self
    225 finally:
--> 226     self.finish()

File ~/work/holoviews/holoviews/.pixi/envs/docs/lib/python3.11/site-packages/matplotlib/animation.py:506, in PillowWriter.finish(self)
    505 def finish(self):
--> 506     self._frames[0].save(
    507         self.outfile, save_all=True, append_images=self._frames[1:],
    508         duration=int(1000 / self.fps), loop=0)

IndexError: list index out of range
:HoloMap   [Default]
   :Polygons   [x,y]   (z)

Animated output can also be exported using the save function by changing the file extension:

hv.save(contour_hmap, 'holomap.gif', fps=5)

Videos#

Video output in matplotlib depends on ffmpeg, which may be compiled from source, installed from conda using conda install ffmpeg, or installed on OSX using brew using brew install ffmpeg. To select video output use set the holomap format to 'mp4':

hv.output(contour_hmap, holomap='mp4', fps=5)

Plot size#

One of the major differences between the matplotlib extension and others is the way plot sizes work. In Plotly and Bokeh, plot sizes are inside out, i.e. each plot defines its height and can then be composed together as needed, while matplotlib defines the size of the figure and the size of each subplot is relative to that. This affords greater control over plot aspect but can also make things more difficult. In HoloViews the size of a plot can be controlled using a couple of main options

  • aspect: Determines the aspect ratio of a subplot

  • fig_bounds: A four-tuple declaring the (left, bottom, right, top) of the plot in figure coordinates with a range of 0-1.

  • fig_inches: The size of the plot in inches can be a single which will be scaled according to the plots aspect or a tuple specifying both width and height).

  • fig_size: A percentage scaling factor.

For example assuming a dpi (dots per inch) of 72, setting fig_inches=5, aspect=2 and fig_bounds=(0, 0, 1, 1) should produce a plot roughly 720 pixels wide and 360 pixels in height:

hv.output(fig='png', dpi=72)
filled_contours.opts(aspect=2, fig_inches=5, fig_bounds=(0, 0, 1, 1))

This turns out to be not quite correct because any empty space will be clipped, e.g. the plot above is actually 603 × 318 pixels, however it is a good approximation.

Plot layouts#

Another aspect that differs quite substantially between matplotlib and other extension is the layout system. Since plots do not have an absolute size relative to one another it depends on the aspect of each plot. The main options to control the layout include:

  • aspect_weight: Whether to weight the aspect of plots when laying out plots (default=False).

  • hspace: Horizontal spacing between subplots.

  • tight: Whether to automatically reduce space between subplots.

  • vspace: Vertical space between subplots.

First let us see what happens when we compose plots with different aspects, and use the tight option to reduce the vertical space between them (we could also manually reduce the vspace):

line_contours = contours(img).opts(aspect=3)
fill_contours = filled_contours.opts(aspect=2)

opts.defaults(opts.Layout(sublabel_format='', fig_size=150))

(line_contours + fill_contours).opts(tight=True)

We can see that the two subplots have very different heights, to equalize this we can enable the aspect_weight option, which will rescale the subplots:

(line_contours + fill_contours).opts(aspect_weight=True, tight=True)
This web page was generated from a Jupyter notebook and not all interactivity will work on this website. Right click to download and run locally for full Python-backed interactivity.