So far we've seen how to wrap data in elements and compose those Elements into Overlays and Layout. In this guide will see how we can use containers to hold Elements and declare parameter spaces to explore multi-dimensional parameter spaces visually. These containers allow faceting your data by one or more variables and exploring the resulting parameter space with widgets, positioning plots on a grid or simply laying them out consecutively. Here we will introduce the HoloMap
, NdOverlay
, NdLayout
and GridSpace
, which make all this possible.
In [ ]:
import numpy as np
import holoviews as hv
from holoviews import opts
hv.notebook_extension('bokeh')
opts.defaults(opts.Curve(line_width=1))
Python users will be familiar with dictionaries as a way to collect data together in a conveniently accessible manner. Unlike NumPy arrays, dictionaries are sparse and do not have to be declared with a fixed size. The dimensioned types are therefore closely modeled on dictionaries but support n-dimensional keys.
Therefore they allow you to express a mapping between a multi-variable key and other viewable objects, letting you structure your multi-dimensional data to facilitate exploration. The key here can represent anything from an ID, a filename, a parameter controlling some custom processing to some category representing a subset of your data.
As a simple example we will use a function which varies with multiple parameters, in this case a function which generates a simple frequency modulation signal, with two main parameters a carrier frequency and a modulation frequency (see Wikipedia). All we have to know here is that the function generates a Curve
based on the parameters that we give it.
In [ ]:
def fm_modulation(f_carrier=220, f_mod=220, mod_index=1, length=0.1, sampleRate=2000):
sampleInc = 1.0/sampleRate
x = np.arange(0,length, sampleInc)
y = np.sin(2*np.pi*f_carrier*x + mod_index*np.sin(2*np.pi*f_mod*x))
return hv.Curve((x, y), 'Time', 'Amplitude')
Next we have to declare the parameter space we want to explore. Combinatorial parameter spaces can quickly get very large so here we will declare just 5 carrier frequencies and 5 modulation frequencies. If we have many more parameters or a larger set of values we could instead use a DynamicMap
, which does lazy evaluation and is introduced in the next section on Live Data.
However when working with relatively small datasets a HoloMap
is preferred as HoloMap
s can be exported to static HTML. To declare the data we use a dictionary comprehension to compute an FM modulation curve for each combination of carrier and modulation frequencies and then pass that dictionary to a newly declared HoloMap
, which declares our two parameters as key dimensions. If we want to customize the starting value of the widgets a default
value may be supplied on the Dimension
objects:
In [ ]:
f_carrier = np.linspace(20, 60, 3)
f_mod = np.linspace(20, 100, 5)
curve_dict = {(fc, fm): fm_modulation(fc, fm) for fc in f_carrier for fm in f_mod}
kdims = [hv.Dimension(('f_carrier', 'Carrier frequency'), default=40),
hv.Dimension(('f_mod', 'Modulation frequency'), default=60)]
holomap = hv.HoloMap(curve_dict, kdims=kdims)
holomap.opts(opts.Curve(width=600))
Any numeric key dimension values will automatically generate sliders while any non-numeric value will present you with a dropdown widget to select between the values. A HoloMap is therefore an easy way to quickly explore a parameter space. Since only one Curve
is on the screen at the same time it is difficult to compare the data, we can however easily facet the data in other ways.
Exploring a parameter space using widgets is one of the most flexible approaches but as we noted above it also makes it difficult to compare between different parameter values. HoloViews therefore supplies several container objects, which behave similarly to a HoloMap
but have a different visual representation:
Since all these classes share the same baseclass we can trivially cast between them, e.g. we can cast the HoloMap
to a GridSpace
.
In [ ]:
grid = hv.GridSpace(holomap)
grid.opts(
opts.GridSpace(plot_size=75),
opts.Curve(width=100))
Similarly we can select just a few values and lay the data out in an NdLayout
:
In [ ]:
ndlayout = hv.NdLayout(grid[20, 20:81])
ndlayout.opts(opts.Curve(width=500, height=200)).cols(2)
Casting between container types as we did above allows us to facet all dimensions but often it is more desirable to facet a specific dimension in some way. We may for example want to overlay the carrier frequencies, while still having a slider vary the modulation frequencies. For this purpose HoloMap
and DynamicMap
have .overlay
, .grid
and .layout
methods, which accept one or more dimensions as input, e.g. here we overlay the carrier frequencies:
In [ ]:
holomap.overlay('f_carrier').opts(width=600)
We can chain these faceting operations but we have to be careful about the order. We should always overlay
first and only then use .grid
or .layout
. To better understand why that is, look at the Building Composite objects guide, for now it suffices to say that objects are built to nest in a specific order. Here we will overlay
the carrier frequencies and grid
the modulation frequency:
In [ ]:
gridspace = holomap.overlay('f_carrier').grid('f_mod')
gridspace.opts(opts.Curve(width=200,height=200))
This ability to facet data in different ways becomes incredibly useful when working with tabular and gridded datasets, which vary by multiple dimensions. By faceting the data by one or more dimensions and utilizing the wide range of elements we can quickly explore huge and complex datasets particularly when working with using DynamicMap
, which allows you to define dynamic and lazy data processing pipelines, which visualize themselves and allow you to build complex interactive dashboards.
Just like Elements
dimensioned containers can be sliced and indexed by value using the regular indexing syntax and the select
method. As a simple example we can index the carrier frequency 20 and modulation frequency 40 using both the indexing and select syntax:
In [ ]:
layout = holomap[20, 40] + holomap.select(f_carrier=20, f_mod=40)
layout.opts(opts.Curve(width=400))
For more detail on indexing both Elements and containers see the Indexing and Selecting user guide.
If we inspect one of the containers we created in the examples above we can clearly see the structure and the dimensionality of each container and the underlying Curve
elements:
In [ ]:
print(grid)
Since this simply represents a multi-dimensional parameter space we can collapse this datastructure into a single table containing columns for each of the dimensions we have declared:
In [ ]:
ds = hv.Dataset(grid.table())
ds.data.head()
Something we might want to do quite frequently is to change the order of dimensions in a dimensioned container. Let us inspect the key dimensions on our HoloMap from above:
In [ ]:
holomap.kdims
Using the reindex
method we can easily change the order of dimensions:
In [ ]:
reindexed = holomap.reindex(['f_mod', 'f_carrier'])
reindexed.kdims
Another common operation is adding a new dimension to a HoloMap, which can be useful when you want to merge two HoloMaps which vary by yet another dimension. The add_dimension
method takes the new dimension name, the position in the list of key dimensions and the actual value as arguments:
In [ ]:
new_holomap = holomap.add_dimension('New dimension', dim_pos=0, dim_val=1)
new_holomap.kdims
As you can see the 'New dimension' was added at position zero.