# 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  ¶

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

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]:

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 0x2ac4df7e7280>

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.