Dimensioned Containers

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))

Declaring n-dimensional collections

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 HoloMaps 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.

Casting between n-dimensional containers

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:

  • NdOverlay - An n-dimensional container which overlays the elements
  • NdLayout - An n-dimensional container which displays the data in separate plot axes and adds titles for each value
  • GridSpace - A 1D or 2D container which lays out up to two dimensions on a grid.

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)

Faceting by dimension

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.

Methods

Selecting by value

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.

Collapsing an n-dimensional container

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()

Reindexing containers

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

Add dimensions to a container

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.