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:
import pandas as pd
import holoviews as hv
from holoviews import opts
spike_train = pd.read_csv('../assets/spike_train.csv.gz')
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
Spike elements using this data and combine them into a
curve = hv.Curve( spike_train, 'milliseconds', 'Hertz', label='Firing Rate')
spikes = hv.Spikes(spike_train, 'milliseconds', , label='Spike Train')
layout = curve + spikes
.Curve.Firing_Rate :Curve [milliseconds] (Hertz)
.Spikes.Spike_Train :Spikes [milliseconds]
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:
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.
With the extension loaded, let’s look at the default appearance as rendered with Bokeh:
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?
Let’s see what we can achieve when we do decide to customize the appearance:
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
Collections of keyword options can be built for a given element type using an “options builder” object, such as
opts.Spikeshere, 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.Spikeskeyword list to see what’s available!).
The layout container has a
colsmethod 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
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.
In the above cell, the result of calling
opts.Curve() is passed into the
.opts method returning an
opts.Curve() and the other option builders aren’t always needed, but they are very helpful for validating options and offer tab completion to help you discover possible values:
dotted_options = opts.Curve(color='purple', width=600, height=250, line_dash='dotted')
Options('Curve', color='purple', height=250, line_dash='dotted', width=600)
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
dotted = hv.Curve(spike_train, 'milliseconds', 'Hertz')
When working directly with a single element, you can omit the options builder entirely because it’s clear what type the options apply to:
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
:Curve [milliseconds] (Hertz)
| Options(color='orange', height=250, line_dash='dashed', width=600)
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
layout = layout.opts(
opts.Curve( aspect=6, xaxis=None, color='blue', linewidth=2, show_grid=False,
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'))
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
aspectinstead of setting
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_widthoption is called
linewidthin matplotlib. These “style” options are directly inherited from the API of the plotting library backend, not defined by HoloViews.
Layouts also have some options to control the arrangement of its components. Here we adjust the gap between the plots using
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:
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.
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:
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
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
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
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:
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
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.