Dynamic Map

The Containers Tutorial introduced the HoloMap , a core HoloViews data structure that allows easy exploration of parameter spaces. The essence of a HoloMap is that it contains a collection of Elements (e.g. Images and Curves) that you can easily select and visualize.

HoloMaps hold fully constructed Elements at specifically sampled points in a multidimensional space. Although HoloMaps are useful for exploring high-dimensional parameter spaces, they can very quickly consume huge amounts of memory to store all these Elements. For instance, a hundred samples along four orthogonal dimensions would need a HoloMap containing a hundred million Elements, each of which could be a substantial object that takes time to create and costs memory to store. Thus HoloMaps have some clear limitations:

  • HoloMaps may require the generation of millions of Elements before the first element can be viewed.
  • HoloMaps can easily exhaust all the memory available to Python.
  • HoloMaps can even more easily exhaust all the memory in the browser when displayed.
  • Static export of a notebook containing HoloMaps can result in impractically large HTML files.

The DynamicMap addresses these issues by computing and displaying elements dynamically, allowing exploration of much larger datasets:

  • DynamicMaps generate elements on the fly, allowing the process of exploration to begin immediately.
  • DynamicMaps do not require fixed sampling, allowing exploration of parameters with arbitrary resolution.
  • DynamicMaps are lazy in the sense they only compute only as much data as the user wishes to explore.

Of course, these advantages come with some limitations:

  • DynamicMaps require a live notebook server and cannot be fully exported to static HTML.
  • DynamicMaps store only a portion of the underlying data, in the form of an Element cache, which reduces the utility of pickling a DynamicMap.
  • DynamicMaps (and particularly their element caches) are typically stateful (with values that depend on patterns of user interaction), which can make them more difficult to reason about.

In order to handle the various situations in which one might want to use a DynamicMap , DynamicMaps can be defined in various "modes" that will be each described separately below:

  • Bounded mode: All dimension ranges specified, allowing exploration within the ranges (i.e., within a bounded region of the multidimensional parameter space).
  • Sampled mode: Some dimension ranges left unspecified, making the DynamicMap not viewable directly, but useful in combination with other objects or for later specifying samples or ranges.
  • Open mode: Dimension ranges not specified, with new elements created on request using a generator.
  • Counter mode: Dimension ranges not specified, with new elements created based on a shared counter state, e.g. from a simulation.

All this will make much more sense once we've tried out some DynamicMaps and showed how they work, so let's create one!

DynamicMap

Let's start by importing HoloViews and loading the extension:

In [1]:
import holoviews as hv
import numpy as np
hv.notebook_extension()
HoloViewsJS successfully loaded in this cell.

We will now create the DynamicMap equivalent of the HoloMap introduced in the Containers Tutorial . The HoloMap in that tutorial consisted of Image elements containing sine ring arrays as defined by the sine_array function:

In [2]:
x,y = np.mgrid[-50:51, -50:51] * 0.1

def sine_array(phase, freq):
    return np.sin(phase + (freq*x**2+freq*y**2))

This function returns NumPy arrays when called:

In [3]:
sine_array(0,1).shape
Out[3]:
(101, 101)

For a DynamicMap we will need a function that returns HoloViews elements. It is easy to modify the sine_array function so that it returns Image elements instead of raw arrays:

In [4]:
def sine_image(phase, freq):
    return hv.Image(np.sin(phase + (freq*x**2+freq*y**2)))

sine_image(0,1) + sine_image(0.5,2)
Out[4]:

Now we can demonstrate the first type of exploration enabled by a DynamicMap , called 'bounded' mode.

Bounded mode

A 'bounded' mode DynamicMap is simply one where all the key dimensions have finite bounds. Bounded mode has the following properties:

  • The limits of the space and/or the allowable values must be declared for all the key dimensions (unless sampled mode is also enabled).
  • You can explore within the declared bounds at any resolution.
  • The data for the DynamicMap is defined using a callable that must be a function of its arguments (i.e., the output is strictly determined by the input arguments).

We can now create a DynamicMap by simply declaring the ranges of the two dimensions and passing the sine_image function as the .data :

In [5]:
dmap = hv.DynamicMap(sine_image, kdims=[hv.Dimension('phase',range=(0, np.pi)),
                                 hv.Dimension('frequency', range=(0.01,np.pi))])

This object is created instantly, because it doesn't generate any hv.Image objects initially. We can now look at the printed representation of this object:

In [6]:
print(dmap)
:DynamicMap   [phase,frequency]

All DynamicMaps will look similar, only differing in the listed dimensions. Now let's see how this dynamic map visualizes itself:

In [7]:
dmap
Out[7]:

Here each hv.Image object visualizing a particular sine ring pattern with the given parameters is created dynamically, whenever the slider is set to that value. Any value in the allowable range can be requested, with a rough step size supported by sliding, and more precise values available by using the left and right arrow keys to change the value in small steps. In each case the new image is dynamically generated based on whatever the slider's values are.

As for any HoloViews Element, you can combine DynamicMaps to create a Layout using the + operator:

In [8]:
dmap + hv.DynamicMap(sine_image, kdims=[hv.Dimension('phase',range=(0, np.pi)),
                                 hv.Dimension('frequency', range=(0.01,np.pi))])
Out[8]:

As both elements are DynamicMaps with the same dimension ranges, the continuous sliders are retained. If one or more HoloMaps is used with a DynamicMap , the sliders will snap to the samples available in any HoloMap in the layout. For bounded DynamicMaps that do not require ranges to be declared, see sampled mode below.

If you are running this tutorial in a live notebook, the above cell should look like the HoloMap in the Containers Tutorial . DynamicMap is in fact a subclass of HoloMap with some crucial differences:

  • You can now pick any value of phase or frequency up to the precision allowed by the slider.
  • What you see in the cell above will not be exported in any HTML snapshot of the notebook.

Using your own callable

You can use any callable to define a DynamicMap in closed mode. A valid DynamicMap is defined by the following criteria:

  • There must be as many positional arguments in the callable signature as key dimensions.
  • The argument order in the callable signature must match the order of the declared key dimensions.
  • All key dimensions are defined with a bounded range or values parameter (for categorical dimensions).

Here is another example of a bounded DynamicMap :

In [9]:
def shapes(N, radius=0.5): # Positional keyword arguments are fine
    paths = [hv.Path([[(radius*np.sin(a), radius*np.cos(a)) 
                        for a in np.linspace(-np.pi, np.pi, n+2)]], 
                     extents=(-1,-1,1,1)) 
             for n in range(N,N+3)]
    return hv.Overlay(paths)
In [10]:
%%opts Path (linewidth=1.5)
dmap = hv.DynamicMap(shapes, kdims=[hv.Dimension('N', range=(2,20)), hv.Dimension('radius', range=(0.5,1))])
dmap
Out[10]:

As you can see, you can return Overlays from DynamicMaps , and DynamicMaps can be styled in exactly the same way as HoloMaps . Note that currently, Overlay objects should be returned from the callable itself; the * operator is not yet supported at the DynamicMap level (i.e., between a DynamicMap and other Elements).

In [11]:
%opts Path (linewidth=1.5)

The DynamicMap cache

Above we mentioned that DynamicMap is an instance of HoloMap . Does this mean it has a .data attribute?

In [12]:
dmap.data
Out[12]:
OrderedDict([((2, 0.5), :Overlay
                 .Path.I   :Path   [x,y]
                 .Path.II  :Path   [x,y]
                 .Path.III :Path   [x,y])])

This is exactly the same sort of .data as the equivalent HoloMap , except that this value will vary according to how much you explored the parameter space of dmap using the sliders above. In a HoloMap , .data contains a defined sampling along the different dimensions, whereas in a DynamicMap , the .data is simply the cache .

The cache serves two purposes:

  • Avoids recomputation of an element should we revisit a particular point in the parameter space. This works well for categorical or integer dimensions, but doesn't help much when using continuous sliders for real-valued dimensions.
  • Records the space that has been explored with the DynamicMap for any later conversion to a HoloMap .
  • Ensures that we store only a finite history of generator output when using open mode together with infinite generators.

We can always convert any DynamicMap directly to a HoloMap as follows:

In [13]:
hv.HoloMap(dmap)
Out[13]:

This is in fact equivalent to declaring a HoloMap with the same parameters (dimensions, etc.) using dmap.data as input, but is more convenient.

Although creating a HoloMap this way is easy, the result is poorly controlled, as the keys in the HoloMap are defined by how you moved the sliders around. For instance, if you run this tutorial straight through using Run All , the above cell won't have any sliders at all, because only a single element will be in the dmap's cache until the DynamicMap s sliders are dragged.

If you instead want to specify a specific set of samples, you can easily do so by using the same key-selection semantics as for a HoloMap to define exactly which elements are initially sampled in the cache:

In [14]:
dmap[{(2,0.5), (2,1.0), (3,0.5), (3,1.0)}] # Returns a *new* DynamicMap with the specified keys in its cache
Out[14]:

This object behaves the same way as before it was sampled, but now this DynamicMap can now be exported to static HTML with the allowed slider positions as specified in the cache, without even having to cast to a HoloMap . Of course, if the intent is primarily to have something statically exportable, then it's still a good idea to explicitly cast it to a HoloMap so that it will clearly contain only a finite set of Elements.

The key selection above happens to define a Cartesian product, which is one of the most common way to sample across dimensions. Because the list of such dimension values can quickly get very large when enumerated as above, we provide a way to specify a Cartesian product directly, which also works with HoloMaps . Here is an equivalent way of defining the same set of four points in that two-dimensional space:

In [15]:
dmap[{2,3},{0.5,1.0}]
Out[15]:
In [16]:
dmap.data
Out[16]:
OrderedDict([((2, 0.5), :Overlay
                 .Path.I   :Path   [x,y]
                 .Path.II  :Path   [x,y]
                 .Path.III :Path   [x,y])])

Note that you can index a DynamicMap with a literal key in exactly the same way as a HoloMap . If the key exists in the cache, it is returned directly, otherwise a suitable element will be generated. Here is an example of how you can access the last key in the cache, creating such an element if it didn't already exist:

In [17]:
dmap[dmap.keys()[-1]]
Out[17]:

The default cache size of 500 Elements is relatively high so that interactive exploration will work smoothly, but you can reduce it using the cache_size parameter if you find you are running into issues with memory consumption. A bounded DynamicMap with cache_size=1 requires the least memory, but will recompute a new Element every time the sliders are moved, making it less responsive.

Slicing bounded DynamicMaps

The declared dimension ranges define the absolute limits allowed for exploration in a bounded DynamicMap . That said, you can use the soft_range parameter to view subregions within that range. Setting the soft_range parameter on dimensions can be done conveniently using slicing on bounded DynamicMaps :

In [18]:
sliced = dmap[4:8, :]
sliced
Out[18]:

Notice that N is now restricted to the range 4:8. Open slices are used to release any soft_range values, which resets the limits back to those defined by the full range:

In [19]:
sliced[:, 0.8:1.0]
Out[19]:

The [:] slice leaves the soft_range values alone and can be used as a convenient way to clone a DynamicMap . Note that mixing slices with any other object type is not supported. In other words, once you use a single slice, you can only use slices in that indexing operation.

Sampling DynamicMaps

We have now seen one way of sampling a DynamicMap , which is to populate the cache with a set of keys. This approach is designed to make conversion of a DynamicMap into a HoloMap easy. One disadvantage of this type of sampling is that populating the cache consumes memory, resulting in many of the same limitations as HoloMap . To avoid this, there are two other ways of sampling a bounded DynamicMap :

Dimension values

If you want a fixed sampling instead of continuous sliders, yet still wish to retain the online generation of elements as the sliders are moved, you can simply declare the dimension to have a fixed list of values. The result appears to the user just like a HoloMap , and will show the same data as the pre-cached version above, but now generates the data dynamically to reduce memory requirements and speed up the initial display:

In [20]:
hv.DynamicMap(shapes, kdims=[hv.Dimension('N', values=[2,3,4,5]), 
                             hv.Dimension('radius', values=[0.7,0.8,0.9,1])])
Out[20]:

Sampled mode

A bounded DynamicMap in sampled mode is the least restricted type of DynamicMap , as it can be declared without any information about the allowable dimension ranges or values:

In [21]:
dmap = hv.DynamicMap(shapes, kdims=['N', 'radius'], sampled=True)
dmap
Out[21]:
:DynamicMap   [N,radius]

As you can see, this type of DynamicMap cannot be visualized in isolation, because there are no values or ranges specified for the dimensions. You could view it by explicitly specifying such values to get cached samples (and can then cast it to a HoloMap if desired):

In [22]:
dmap[{2,3},{0.5,1.0}]
Out[22]:

Still, on its own, a sampled-mode DynamicMap may not seem very useful. When they become very convenient is when combined with one or more HoloMaps in a Layout . Because a sampled DynamicMap doesn't have explicitly declared dimension ranges, it can always adopt the set of sample values from HoloMaps in the layout.

In [23]:
dmap + hv.HoloMap({(N,r):shapes(N, r) for N in [3,4,5] for r in [0.5,0.75]},  kdims=['N', 'radius'])
Out[23]:

In this way, you only need to worry about choosing specific values or ranges for your dimensions for a single ( HoloMap ) object, with the others left open-ended as sampled-mode DynamicMaps . This convenience is subject to three particular restrictions:

  • Sampled DynamicMaps do not visualize themselves in isolation (as we have already seen).
  • You cannot build a layout consisting of sampled-mode DynamicMaps only, because at least one HoloMap is needed to define the samples.
  • There currently cannot be more dimensions declared in the sampled DynamicMap than across the rest of the layout. We hope to relax this restriction in future.

Using groupby to discretize a DynamicMap

A DynamicMap also makes it easy to partially or completely discretize a function to evaluate in a complex plot. By grouping over specific dimensions that define a fixed sampling via the Dimension values parameter, the DynamicMap can be viewed as a GridSpace , NdLayout , or NdOverlay . If a dimension specifies only a continuous range it can't be grouped over, but it may still be explored using the widgets. This means we can plot partial or completely discretized views of a parameter space easily.

Partially discretize

The implementation for all the groupby operations uses the .groupby method internally, but we also provide three higher-level convenience methods to group dimensions into an NdOverlay ( .overlay ), GridSpace ( .grid ), or NdLayout ( .layout ).

Here we will evaluate a simple sine function with three dimensions, the phase, frequency, and amplitude. We assign the frequency and amplitude discrete samples, while defining a continuous range for the phase:

In [24]:
xs = np.linspace(0, 2*np.pi)

def sin(ph, f, amp):
    return hv.Curve((xs, np.sin(xs*f+ph)*amp))

kdims=[hv.Dimension('phase', range=(0, np.pi)),
       hv.Dimension('frequency', values=[0.1, 1, 2, 5, 10]),
       hv.Dimension('amplitude', values=[0.5, 5, 10])]

sine_dmap = hv.DynamicMap(sin, kdims=kdims)

Next we define the amplitude dimension to be overlaid and the frequency dimension to be gridded:

In [25]:
%%opts GridSpace [show_legend=True fig_size=200]
sine_dmap.overlay('amplitude').grid('frequency')
Out[25]:

As you can see, instead of having three sliders (one per dimension), we've now laid out the frequency dimension as a discrete set of values in a grid, and the amplitude dimension as a discrete set of values in an overlay, leaving one slider for the remaining dimension (phase). This approach can help you visualize a large, multi-dimensional space efficiently, with full control over how each dimension is made visible.

Fully discretize

Given a continuous function defined over a space, we could sample it manually, but here we'll look at an example of evaluating it using the groupby method. Let's look at a spiral function with a frequency and first- and second-order phase terms. Then we define the dimension values for all the parameters and declare the DynamicMap:

In [26]:
%opts Path (linewidth=1 color=Palette('Blues'))

def spiral_equation(f, ph, ph2):
    r = np.arange(0, 1, 0.005)
    xs, ys = (r * fn(f*np.pi*np.sin(r+ph)+ph2) for fn in (np.cos, np.sin))
    return hv.Path((xs, ys))

kdims=[hv.Dimension('f', values=list(np.linspace(1, 10, 10))),
       hv.Dimension('ph', values=list(np.linspace(0, np.pi, 10))),
       hv.Dimension('ph2', values=list(np.linspace(0, np.pi, 4)))]

spiral_dmap = hv.DynamicMap(spiral_equation, kdims=kdims)

Now we can make use of the .groupby method to group over the frequency and phase dimensions, which we will display as part of a GridSpace by setting the container_type . This leaves the second phase variable, which we assign to an NdOverlay by setting the group_type :

In [27]:
%%opts GridSpace [xaxis=None yaxis=None] Path [bgcolor='w' xaxis=None yaxis=None]
spiral_dmap.groupby(['f', 'ph'], group_type=hv.NdOverlay, container_type=hv.GridSpace)
Out[27]:

This grid shows a range of frequencies f on the x axis, a range of the first phase variable ph on the y axis, and a range of different ph2 phases as overlays within each location in the grid. As you can see, these techniques can help you visualize multidimensional parameter spaces compactly and conveniently.

Open mode

DynamicMap also allows unconstrained exploration over unbounded dimensions in 'open' mode. There are two key differences between open mode and bounded mode:

  • Instead of a callable, the input to an open DynamicMap is a generator. Once created, the generator is only used via next() .
  • At least one of the declared key dimensions must have an unbounded range (i.e., with an upper or lower bound not specified).
  • An open mode DynamicMap can run forever, or until a StopIteration exception is raised.
  • Open mode DynamicMaps can be stateful, with an irreversible direction of time.

Infinite generators

Our first example will be using an infinite generator which plots the histogram for a given number of random samples drawn from a Gaussian distribution:

In [28]:
def gaussian_histogram(samples, scale):
    frequencies, edges = np.histogram([np.random.normal(scale=scale) 
                                       for i in range(samples)], 20)
    return hv.Histogram(frequencies, edges).relabel('Gaussian distribution')

gaussian_histogram(100,1) + gaussian_histogram(150,1.5) 
Out[28]:

Lets now use this in the following generator:

In [29]:
def gaussian_sampler(samples=10, delta=10, scale=1.0):
    np.random.seed(1)
    while True:
        yield gaussian_histogram(samples, scale)
        samples+=delta
        
gaussian_sampler()
Out[29]:
<generator object gaussian_sampler at 0x2ba254447410>

Which allows us to define the following infinite DynamicMap :

In [30]:
dmap = hv.DynamicMap(gaussian_sampler(), kdims=['step'])
dmap
Out[30]:


Once Loop Reflect

Note that step is shown as an integer. This is the default behavior and corresponds to the call count (i.e the number of times next() has been called on the generator. If we want to show the actual number of samples properly, we need our generator to return a (key, element) pair:

In [31]:
def gaussian_sampler_kv(samples=10, delta=10, scale=1.0):
    np.random.seed(1)
    while True:
        yield (samples, gaussian_histogram(samples, scale))
        samples+=delta
        
hv.DynamicMap(gaussian_sampler_kv(), kdims=['samples'])
Out[31]:


Once Loop Reflect

Note that if you pause the DynamicMap , you can scrub back to previous frames in the cache. In other words, you can view a limited history of elements already output by the generator, which does not re-execute the generator in any way (as it is indeed impossible to rewind generator state). If you have a stateful generator that, say, depends on the current wind speed in Scotland, this history may be misleading, in which case you can simply set the cache_size parameter to 1.

Multi-dimensional generators

In open mode, elements are naturally serialized by a linear sequence of next() calls, yet multiple key dimensions can still be defined:

In [32]:
def gaussian_sampler_2D(samples=10, scale=1.0, delta=10):
    np.random.seed(1)
    while True:
        yield ((samples, scale), gaussian_histogram(samples, scale))
        samples=(samples + delta) if scale==2 else samples
        scale = 2 if scale == 1 else 1
        
dmap = hv.DynamicMap(gaussian_sampler_2D(), kdims=['samples', 'scale'])
dmap
Out[32]:


Once Loop Reflect

Here we bin the histogram for two different scale values. Above we can visualize this linear sequence of next() calls, but by casting this open map to a HoloMap , we can obtain a multi-dimensional parameter space that we can freely explore:

In [33]:
hv.HoloMap(dmap)
Out[33]:

Note that if you ran this notebook using Run All , only a single frame will be available in the above cell, with no sliders, but if you ran it interactively and viewed a range of values in the previous cell, you'll have multiple sliders in this cell allowing you to explore whatever range of frames is in the cache from the previous cell.

Finite generators

Open mode DynamicMaps are finite and terminate if StopIteration is raised. This example terminates when the means of two sets of gaussian samples fall within a certain distance of each other:

In [34]:
def sample_distributions(samples=10, delta=50, tol=0.04):
    np.random.seed(42)
    while True:
        gauss1 = np.random.normal(size=samples)
        gauss2 = np.random.normal(size=samples)
        data = (['A']*samples + ['B']*samples, np.hstack([gauss1, gauss2]))
        diff = abs(gauss1.mean() - gauss2.mean())
        if abs(gauss1.mean() - gauss2.mean()) > tol:
            yield ((samples, diff), hv.BoxWhisker(data, kdims=['Group'], vdims=['Value']))
        else:
            raise StopIteration
        samples+=delta
In [35]:
dmap = hv.DynamicMap(sample_distributions(), kdims=['samples', '$\delta$'])
dmap
Out[35]:


Once Loop Reflect

Now if you are familiar with generators in Python, you might be wondering what happens when a finite generator is exhausted. First we should mention that casting a DynamicMap to a list is always finite, because __iter__ returns the cache instead of a potentially infinite generator:

In [36]:
list(dmap) # The cache
Out[36]:
[:BoxWhisker   [Group]   (Value)]

As we know this DynamicMap is finite, we can make sure it is exhausted as follows:

In [37]:
while True:
    try:
        next(dmap) # Returns Image elements
    except StopIteration:
        print("The dynamic map is exhausted.")
        break
The dynamic map is exhausted.

Now let's have a look at the dynamic map:

In [38]:
dmap
Rendering process skipped: DynamicMap generator exhausted.
Out[38]:
:DynamicMap   [samples,$\delta$]
   :BoxWhisker   [Group]   (Value)

Here, we are given only the text-based representation, to indicate that the generator is exhausted. However, as the process of iteration has populated the cache, we can still view the output as a HoloMap using hv.HoloMap(dmap) as before.

Counter mode and temporal state

Open mode is intended to use live data streams or ongoing simulations with HoloViews. The DynamicMap will generate live visualizations for as long as new data is requested. Although this works for simple cases, Python generators have problematic limitations that can be resolved using 'counter' mode.

In this example, let's say we have a simulation or data recording where time increases in integer steps:

In [39]:
def time_gen(time=1):
    while True:
        yield time
        time += 1
        
time = time_gen()

Now let's create two generators that return Images that are a function of the simulation time. Here, they have identical output except one of the outputs includes additive noise:

In [40]:
ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)

def cells():
    while True:
        t = next(time)
        arr = np.sin(xx+t)*np.cos(yy+t)
        yield hv.Image(arr)

def cells_noisy():
    while True:
        t = next(time)
        arr = np.sin(xx+t)*np.cos(yy+t)
        yield hv.Image(arr + 0.2*np.random.rand(200,200))

Now let's create a Layout using these two generators:

In [41]:
hv.DynamicMap(cells(), kdims=['time']) + hv.DynamicMap(cells_noisy(), kdims=['time'])
Out[41]:


Once Loop Reflect

If you pause the animation, you'll see that these two outputs are not in phase, despite the fact that the generators are defined identically (modulo the additive noise)!

The issue is that generators are used via the next() interface, and so when either generator is called, the simulation time is increased. In other words, the noisy version in subfigure B actually corresponds to a later time than in subfigure A .

This is a fundamental issue, as the next method does not take arguments. What we want is for all the DynamicMaps presented in a Layout to share a common simulation time, which is only incremented by interaction with the scrubber widget. This is exactly the sort of situation where you want to use counter mode.

Handling time-dependent state

To define a DynamicMap in counter mode:

  • Leave one or more dimensions unbounded (as in open mode)
  • Supply a callable (as in bounded mode) that accepts one argument

This callable should act in the same way as the generators of open mode, except the output is controlled by the single counter argument.

In [42]:
ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)

def cells_counter(t):
    arr = np.sin(xx+t)*np.cos(yy+t)
    return hv.Image(arr)

def cells_noisy_counter(t):
    arr = np.sin(xx+t)*np.cos(yy+t)
    return hv.Image(arr + 0.2*np.random.rand(200,200))

Now if we supply these functions instead of generators, A and B will correctly be in phase:

In [43]:
hv.DynamicMap(cells_counter, kdims=['time']) + hv.DynamicMap(cells_noisy_counter, kdims=['time'])
Out[43]:


Once Loop Reflect

Unfortunately, an integer counter is often too simple to describe simulation time, which may be a float with real-world units. To address this, we can simply return the actual key values we want along the time dimension, just as was demonstrated in open mode using generators:

In [44]:
ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)

# Example of a global simulation time
# typical in many applications
t = 0 
        
def cells_counter_kv(c):
    global t
    t = 0.1 * c
    arr = np.sin(xx+t)*np.cos(yy+t)
    return (t, hv.Image(arr))

def cells_noisy_counter_kv(c):
    global t
    t = 0.1 * c
    arr = np.sin(xx+t)*np.cos(yy+t)
    return (t, hv.Image(arr + 0.2*np.random.rand(200,200)))
    
hv.DynamicMap(cells_counter_kv, kdims=['time']) + hv.DynamicMap(cells_noisy_counter_kv, kdims=['time'])
Out[44]:


Once Loop Reflect
In [45]:
print("The global simulation time is now t=%f" % t)
The global simulation time is now t=0.000000

Ensuring that the HoloViews counter maps to a suitable simulation time is the responsibility of the user. However, once a consistent scheme is configured, the callable in each DynamicMap can specify the desired simulation time. If the requested simulation time is the same as the current simulation time, nothing needs to happen. Otherwise, the simulator can be run forward by the requested amount. In this way, HoloViews can provide a rich graphical interface for controlling and visualizing an external simulator, with very little code required.

Slicing in open and counter mode

Slicing open and counter mode DynamicMaps has the exact same semantics as normal HoloMap slicing, except now the .data attribute corresponds to the cache. For instance:

In [46]:
def sine_kv_gen(phase=0, freq=0.5):
    while True:
        yield (phase, hv.Image(np.sin(phase + (freq*x**2+freq*y**2))))
        phase+=0.2
        
dmap = hv.DynamicMap(sine_kv_gen(), kdims=['phase'])

Let's fill the cache with some elements:

In [47]:
for i in range(21):
    dmap.next()
    
print("Min key value in cache:%s\nMax key value in cache:%s" % (min(dmap.keys()), max(dmap.keys())))
Min key value in cache:0
Max key value in cache:4.0
In [48]:
sliced = dmap[1:3.1]
print("Min key value in cache:%s\nMax key value in cache:%s" % (min(sliced.keys()), max(sliced.keys())))
Min key value in cache:1.0
Max key value in cache:3.0

DynamicMaps and normalization

By default, a HoloMap normalizes the display of elements according the minimum and maximum values found across the HoloMap . This automatic behavior is not possible in a DynamicMap , where arbitrary new elements are being generated on the fly. Consider the following examples where the arrays contained within the returned Image objects are scaled with time:

In [49]:
%%opts Image {+axiswise}
ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)

def cells(vrange=False):
    "The range is set on the value dimension when vrange is True "
    time = time_gen()
    while True:
        t = next(time)
        arr = t*np.sin(xx+t)*np.cos(yy+t)
        vdims=[hv.Dimension('Intensity', range=(0,10))] if vrange else ['Intensity']
        yield hv.Image(arr, vdims=vdims)

hv.DynamicMap(cells(vrange=False), kdims=['time']) + hv.DynamicMap(cells(vrange=True), kdims=['time'])
Out[49]:


Once Loop Reflect

Here we use +axiswise to see the behavior of the two cases independently. We see in A that when vrange=False and no range is set on the value dimension, no automatic normalization occurs (unlike a HoloMap ). In B we see that normalization is applied, but only when the value dimension range has been specified.

In other words, DynamicMaps cannot support automatic normalization across their elements, but do support the same explicit normalization behavior as HoloMaps . Values that are generated outside this range are simply clipped according the usual semantics of explicit value dimension ranges.

Note that we can always have the option of casting a DynamicMap to a HoloMap in order to automatically normalize across the cached values, without needing explicit value dimension ranges.

Using DynamicMaps in your code

As you can see, DynamicMaps let you use HoloViews with a very wide range of dynamic data formats and sources, making it simple to visualize ongoing processes or very large data spaces.


Download this notebook from GitHub (right-click to download).