Bokeh is designed to make it possible to construct rich, deeply interactive browser-based visualizations from Python source code. It has a syntax more compact and natural than older libraries like Matplotlib, particularly when using the Charts API, but it still requires a good bit of code to do relatively common data-science tasks like complex multi-figure layouts, animations, and widgets for parameter space exploration.
To make it feasible to generate complex interactive visualizations "on the fly" in Jupyter notebooks while exploring data, we have created the new HoloViews library built on top of Bokeh.
HoloViews allows you to annotate your data with a small amount of metadata that makes it instantly visualizable, usually without writing any plotting code. HoloViews makes it practical to explore datasets and visualize them from every angle interactively, wrapping up Bokeh code for common tasks into a set of configurable and composable components. HoloViews installs separately from Bokeh, e.g. using conda install holoviews
, and also works with matplotlib.
import holoviews as hv
import numpy as np
hv.notebook_extension(bokeh=True)
First, let us define a mathematical function to explore, using the Numpy array library:
def sine(x, phase=0, freq=100):
return np.sin((freq * x + phase))
We will examine the effect of varying phase and frequency:
phases = np.linspace(0,2*np.pi,7) # Explored phases
freqs = np.linspace(50,150,5) # Explored frequencies
Over a specific spatial area, sampled on a grid:
dist = np.linspace(-0.5,0.5,81) # Linear spatial sampling
x,y = np.meshgrid(dist, dist)
grid = (x**2+y**2) # 2D spatial sampling
With HoloViews, we can immediately view our simple function as an image in a Bokeh plot in the Jupyter notebook, without any coding:
hv.__version__
Version((1, 5, 0),'/Users/jbednar/holoviews_git/holoviews/__init__.py','6943b97')
hv.Image(sine(grid, freq=20))
But we can just as easily use +
to combine Image
and Curve
objects, visualizing both the 2D array (with associated histogram) and a 1D cross-section:
grating = hv.Image(sine(grid, freq=20), label="Sine Grating")
((grating * hv.HLine(y=0)).hist() + grating.sample(y=0).relabel("Sine Wave"))
Here you can see that a HoloViews object isn't really a plot (though it generates a Bokeh Plot when requested for display by the Jupyter notebook); it is just a wrapper around your data, and the data can be processed directly (as when taking the cross-section using sample()
here). In fact, your raw data is always still available,allowing you to go back and forth between visualizations and numerical analysis easily and flexibly:
grating[0,0]
0.0
type(grating.data)
numpy.ndarray
Here the underlying data is the original Numpy array, but Python dictionaries as well as Pandas and other data formats can also be supplied.
The underlying objects and data can always be retrieved, even in complex multi-figure objects, if you look at the repr
of the object to find the indexes needed to address that data:
layout = ((grating * hv.HLine(y=0)) + grating.sample(y=0))
print(repr(layout))
layout.Overlay.Sine_Grating.Image.Sine_Grating[0,0]
:Layout .Overlay.Sine_Grating :Overlay .Image.Sine_Grating :Image [x,y] (z) .HLine.I :HLine [x,y] .Curve.I :Curve [x] (z)
0.0
Here layout
is the name of the full complex object, and Overlay.Sine_Grating
selects the first item (an HLine overlaid on a grating), and Image.Sine_Grating
selects the grating within the overlay. The grating itself is then indexed by 'x' and 'y' as shown in the repr, and the return value from such indexing is 'z' (nearly zero in this case, which you can also see by examining the curve plot above).
HoloViews is designed to explore complicated datasets, where there can often be much more data than can be shown on screen at once. If there are dimensions to your data that have not been laid out as adjacent plots or overlaid plots, then HoloViews will automatically generate sliders covering the remaining range of the data. For instance, if we add an additional dimension Y
indicating the location of the cross-section, we'll get a slider for Y
:
positions = np.linspace(-0.3, 0.3, 17)
hv.HoloMap({y: (grating * hv.HLine(y)) for y in positions}, kdims='Y') + \
hv.HoloMap({y: (grating.sample(y=y)) for y in positions}, kdims='Y')
Here instead of single visualizable objects as above, the "+" here is combining "HoloMaps", which are flexible dictionary-like structures that hold visualizable data for a variety of parameter values. Here both objects are indexed by a single continuous 'Y' value, but in general they can have any number of dimensions, each of which will result in its own slider widget if that dimension is not selected or sampled before viewing. E.g. here's an example of a 2-dimensional space, where we declare the dimensions of the parameter space to be explored (dimensions
) as well as the specific samples to take in this parameter space (keys
):
dims = dict(kdims = ['Phase','Frequency'], vdims=['Amplitude'])
keys = [(p,f) for p in phases for f in freqs]
hv.HoloMap([(k, hv.Image(sine(grid, *k))) for k in keys], **dims) + \
hv.HoloMap([(k, hv.Curve(zip(dist, sine(dist**2, *k)),vdims=dims['vdims'])) for k in keys], **dims)