Customizing visual appearance

HoloViews elements like the Scatter points illustrated in the Introduction contain two types of information:

  • Your data, in as close to its original form as possible, so that it can be analyzed and accessed as you see fit.
  • Metadata specifying what your data is, which allows HoloViews to construct an appropriate visual representation for it.

What elements do not contain is:

  • The endless details that one might want to tweak about the visual representation, such as line widths, colors, fonts, and spacing.

HoloViews is designed to let you work naturally with the meaningful features of your data, while making it simple to adjust the display details separately using the Options system. Among many other benefits, this separation of content from presentation simplifies your data analysis workflow, and makes it independent of any particular plotting backend.

Visualizing neural spike trains

To illustrate how the options system works, we will use a dataset containing "spike" (neural firing) events extracted from the recorded electrical activity of a neuron. We will be visualizing the first trial of this publicly accessible neural recording. First, we import pandas and holoviews and load our data:


In [ ]:
import pandas as pd
import holoviews as hv
from holoviews import opts

spike_train = pd.read_csv('../assets/spike_train.csv.gz')
spike_train.head(n=3)

This dataset contains the spike times (in milliseconds) for each detected spike event in this five-second recording, along with a spiking frequency in Hertz (spikes per second), averaged over a rolling 200 millisecond window. We will now declare Curve and Spike elements using this data and combine them into a Layout:


In [ ]:
curve  = hv.Curve( spike_train, 'milliseconds', 'Hertz', label='Firing Rate')
spikes = hv.Spikes(spike_train, 'milliseconds', [],      label='Spike Train')

layout = curve + spikes
layout

Notice that the representation for this object is purely textual; so far we have not yet loaded any plotting system for HoloViews, and so all you can see is a description of the data stored in the elements.

To be able to see a visual representation and adjust its appearance, we'll need to load a plotting system, and here let's load two so they can be compared:


In [ ]:
hv.extension('bokeh', 'matplotlib')

Even though we can happily create, analyze, and manipulate HoloViews objects without using any plotting backend, this line is normally executed just after importing HoloViews so that objects can have a rich graphical representation rather than the very-limited textual representation shown above. Putting 'bokeh' first in this list makes visualizations default to using Bokeh, but including matplotlib as well means that backend can be selected for any particular plot as shown below.

Default appearance

With the extension loaded, let's look at the default appearance as rendered with Bokeh:


In [ ]:
layout

As you can see, we can immediately appreciate more about this dataset than we could from the textual representation. The curve plot, in particular, conveys clearly that the firing rate varies quite a bit over this 5-second interval. However, the spikes plot is much more difficult to interpret, because the plot is nearly solid black.

One thing we can do is click on one of the Bokeh plot's zoom tools to enable it, then zoom in until individual spikes are clearly visible. Even then, though, it's difficult to relate the spiking and firing-rate representations to each other. Maybe we can do better by adjusting the display options away from their default settings?

Customization

Let's see what we can achieve when we do decide to customize the appearance:


In [ ]:
layout.opts(
    opts.Curve( height=200, width=900, xaxis=None, line_width=1.50, color='red', tools=['hover']),
    opts.Spikes(height=150, width=900, yaxis=None, line_width=0.25, color='grey')).cols(1)

Much better! It's the same underlying data, but now we can clearly see both the individual spike events and how they affect the moving average. You can also see how the moving average trails the actual spiking, due to how the window function was defined.

A detailed breakdown of this exact customization is given in the User Guide, but we can use this example to understand a number of important concepts:

  • The options system is based around keyword settings supplied to the .opts() method.
  • Collections of keyword options can be built for a given element type using an "options builder" object, such as opts.Curve and opts.Spikes here, so that we can set options separately for each component of a composite object (as for height here)
  • Options builders also provide early validation of keywords (allowing errors to be detected even before the options are applied to an element) as well as tab-completion in IPython (try adding a comma to the opts.Curve or opts.Spikes keyword list to see what's available!).
  • The layout container has a cols method to specify the number of columns in the layout.

The corresponding User Guide entry explains the keywords used in detail, but a quick summary is that when you tab-complete using the opts.* builders, you are completing across two fundamental types of options: plot options (processed by HoloViews) and style options (processed by the underlying backend, either Bokeh or Matplotlib here). If you only use a single backend, you don't need to worry much about this distinction because HoloViews will ensure that the option setting is given to the appropriate backend when needed. Here, for instance, the color and line_width keywords are not used by HoloViews; they will just be passed on to the corresponding Bokeh glyphs. In this way you can control both HoloViews and the current backend, to customize almost any aspect of your plot.

Discovering options

In the above cell, the result of calling opts.Curve() is passed into the .opts method returning an Options object. opts.Curve() and the other option builders aren't always needed, but the are very helpful for validating options and offer tab completion to help you discover possible values:


In [ ]:
dotted_options = opts.Curve(color='purple', width=600, height=250, line_dash='dotted')
dotted_options

Try tab-completing the options for Curve above or specifying an invalid keyword. Now the dotted_options object can be passed to the .opts method call to customize a Curve:


In [ ]:
dotted = hv.Curve(spike_train, 'milliseconds', 'Hertz')
dotted.opts(dotted_options)

When working directly with a single element, you can omit the options builder entirely because it's clear what type the options apply to:


In [ ]:
dashed = hv.Curve( spike_train, 'milliseconds', 'Hertz')
dashed.opts(color='orange', width=600, height=250, line_dash='dashed')

The code is then a bit shorter and more readable with the same result, but it no longer tab completes, and so omitting the builder is probably only useful for a final, published set of code, not during exploration. When using the .opts method on compositions of elements (i.e., layouts or overlays) you still need to use the options builders to indicate which type of object the options should be applied to.

If you want to find out which options have been changed on a given object, you can use .opts.info():


In [ ]:
dashed.opts.info()

For more information on how to work with options, see the the User Guide.

Switching to matplotlib

Now let's customize our layout with options appropriate for the Matplotlib renderer, by supplying options associated with the matplotlib backend to the .opts method:


In [ ]:
layout = layout.opts(
    opts.Curve( aspect=6, xaxis=None,   color='blue', linewidth=2, show_grid=False, 
               linestyle='dashed', backend='matplotlib'),
    opts.Spikes(aspect=6, yaxis='bare', color='red',  linewidth=0.25, backend='matplotlib'),
    opts.Layout(sublabel_format='', vspace=0.1, fig_size=200, backend='matplotlib'))
layout

These options are now associated with matplotlib (due to backend='matplotlib') even though the plot is still rendered with bokeh as we haven't switched to the matplotlib backend just yet (although matplotlib support was was loaded by hv.extension at the start of this notebook). The above code sets the options appropriate to matplotlib without immediately making use of them and naturally, a few changes needed to be made:

  • Some of the options are different because of differences in how the plotting backends work. For instance, matplotlib uses aspect instead of setting width and height. In some cases, but not all, HoloViews can smooth over such differences in the plotting options to make it simpler to switch backends.
  • The Bokeh hover tool is not supported by the matplotlib backend, as you might expect, nor are there any other interactive controls, because the Matplotlib backend generates static PNG or SVG images.
  • Some options have different names; for instance, the Bokeh line_width option is called linewidth in matplotlib. These "style" options are directly inherited from the API of the plotting library backend, not defined by HoloViews.
  • Containers like Layouts also have some options to control the arrangement of its components. Here we adjust the gap betwen the plots using vspace.

Now we can use the hv.output utility to to show the same elements in layout as rendered with these different customizations, in a different output format (SVG), with a completely different plotting library:


In [ ]:
hv.output(layout, backend='matplotlib', fig='svg')

This approach allows you to associate options for multiple different backends with the same object. See the User Guide for more details, including information of how to use hv.output to affect global output settings.

Persistent styles

Let's switch back to the default (Bokeh) plotting extension for this notebook and apply the .select operation illustrated in the Introduction, to the spikes object we made earlier:


In [ ]:
hv.output(backend='bokeh')
spikes.select(milliseconds=(2000,4000))

Note how HoloViews remembered the Bokeh-specific styles we previously applied to the spikes object! This feature allows us to style objects once and then keep that styling as we work, without having to repeat the styles every time we work with that object. Note that even though this styling is associated with the element, it is not actually stored on it, which is mostly an implementation detail but does define a strict separation between what HoloViews considers parts of your data (the Element) and what is part of the "look" or the "view" of that data (the options associated with the object, but stored separately).

If we want to reset back to the original styling, we can call .opts.clear():


In [ ]:
spikes.select(milliseconds=(2000,4000)).opts.clear()

You can learn more about the output utility and how the options system handles persistent options in the User Guide.

Setting axis labels

If you look closely, the example above might worry you. First we defined our Spikes element with kdims=['milliseconds'], which we then used as a keyword argument in select above. This is also the string used as the axis label. Does this mean we are limited to Python identifiers for axis labels, if we want to use the corresponding dimension with select?

Luckily, there is no limitation involved. Dimensions specified as strings are often convenient, but behind the scenes, HoloViews always uses a much richer Dimensions object that you can pass to the kdims and vdims explicitly (see the User Guide for more information). One of the things each Dimension object supports is a long, descriptive label, which complements the short programmer-friendly name.

We can set the dimension labels on our existing spikes object as follows:


In [ ]:
spikes = spikes.redim.label(milliseconds='Time in milliseconds (10⁻³ seconds)')
curve  = curve.redim.label(Hertz='Frequency (Hz)')
(curve + spikes).select(milliseconds=(2000,4000)).cols(1)

As you can see, we can set long descriptive labels on our dimensions (including unicode) while still making use of the short dimension name in methods like select.

Now that you know how to set up and customize basic visualizations, the next Getting-Started sections show how to work with various common types of data in HoloViews.