Responding to Events#
import numpy as np
import holoviews as hv
from holoviews import opts
hv.extension('bokeh')
In the Live Data guide we saw how DynamicMap
allows us to explore high dimensional data using the widgets in the same style as HoloMaps
. Although suitable for unbounded exploration of large parameter spaces, the DynamicMaps
described in that notebook support exactly the same mode of interaction as HoloMaps
. In particular, the key dimensions are used to specify a set of widgets that when manipulated apply the appropriate indexing to invoke the user-supplied callable.
In this user guide we will explore the HoloViews streams system that allows any sort of value to be supplied from anywhere. This system opens a huge set of new possible visualization types, including continuously updating plots that reflect live data as well as dynamic visualizations that can be interacted with directly, as described in the Custom Interactivity guide.
When viewing this user guide as part of the documentation DynamicMaps will be sampled with a limited number of states.
# Styles and plot options used in this user guide
opts.defaults(
opts.Area(fill_color='cornsilk', line_width=2,
line_color='black'),
opts.Ellipse(bgcolor='white', color='black'),
opts.HLine(color='red', line_width=2),
opts.Image(cmap='viridis'),
opts.Path(bgcolor='white', color='black', line_dash='dashdot',
show_grid=False),
opts.VLine(color='red', line_width=2))
A simple DynamicMap
#
Before introducing streams, let us declare a simple DynamicMap
of the sort discussed in the Live Data user guide. This example consists of a Curve
element showing a Lissajous curve with VLine
and HLine
annotations to form a crosshair:
lin = np.linspace(-np.pi,np.pi,300)
def lissajous(t, a=3, b=5, delta=np.pi/2.):
return (np.sin(a * t + delta), np.sin(b * t))
def lissajous_crosshair(t, a=3, b=5, delta=np.pi/2):
(x,y) = lissajous(t,a,b,delta)
return hv.VLine(x) * hv.HLine(y)
crosshair = hv.DynamicMap(lissajous_crosshair, kdims='t').redim.range(t=(-3.,3.))
path = hv.Path(lissajous(lin))
path * crosshair
As expected, the declared key dimension (kdims
) has turned into a slider widget that lets us move the crosshair along the curve. Now let’s see how to position the crosshair using streams.
Introducing streams#
The core concept behind a stream is simple: it defines one or more parameters that can change over time that automatically refreshes code depending on those parameter values.
Like all objects in HoloViews, these parameters are declared using param and streams are defined as a parameterized subclass of the holoviews.streams.Stream
. A more convenient way is to use the Stream.define
classmethod:
from holoviews.streams import Stream, param
Time = Stream.define('Time', t=0.0)
This results in a Time
class with a numeric t
parameter that defaults to zero. As this object is parameterized, we can use hv.help
to view its parameters:
hv.help(Time)
Parameters of 'Time'
====================
Parameters changed from their default values are marked in red.
Soft bound values are marked in cyan.
C/V= Constant/Variable, RO/RW = ReadOnly/ReadWrite, AN=Allow None
NameValue Type Mode
t 0.0 Number C RW
Parameter docstrings:
=====================
t: < No docstring available >
This parameter is a param.Number
as we supplied a float, if we had supplied an integer it would have been a param.Integer
. Notice that there is no docstring in the help output above but we can add one by explicitly defining the parameter as follows:
Time = Stream.define('Time', t=param.Number(default=0.0, doc='A time parameter'))
hv.help(Time)
Parameters of 'Time'
====================
Parameters changed from their default values are marked in red.
Soft bound values are marked in cyan.
C/V= Constant/Variable, RO/RW = ReadOnly/ReadWrite, AN=Allow None
NameValue Type Mode
t 0.0 Number V RW
Parameter docstrings:
=====================
t: A time parameter
Now we have defined this Time
stream class, we can make of an instance of it and look at its parameters:
time_dflt = Time()
print('This Time instance has parameter t={t}'.format(t=time_dflt.t))
This Time instance has parameter t=0.0
As with all parameterized classes, we can choose to instantiate our parameters with suitable values instead of relying on defaults.
time = Time(t=np.pi/4)
print('This Time instance has parameter t={t}'.format(t=time.t))
This Time instance has parameter t=0.7853981633974483
For more information on defining Stream
classes this way, use hv.help(Stream.define)
.
Simple streams example#
We can now supply this streams object to a DynamicMap
using the same lissajous_crosshair
callback from above by adding it to the streams
list:
dmap = hv.DynamicMap(lissajous_crosshair, streams=[time])
path * dmap + path * lissajous_crosshair(t=np.pi/4.)
Immediately we see that the crosshair position of the DynamicMap
reflects the t
parameter values we set on the Time
stream. This means that the t
parameter was supplied as the argument to the lissajous_curve
callback. As we now have no key dimensions, there is no longer a widget for the t
dimensions.
Although we have what looks like a static plot, it is in fact dynamic and can be updated in place at any time. To see this, we can call the event
method on our DynamicMap
:
dmap.event(t=0.2)
Running this cell will have updated the crosshair from its original position where $t=\frac{\pi}{4}$ to a new position where t=0.2
. Try running the cell above with different values of t
and watch the plot update!
This event
method is the recommended way of updating the stream parameters on a DynamicMap
but if you have a handle on the relevant stream instance, you can also call the event
method on that:
time.event(t=-0.2)
Running the cell above also moves the crosshair to a new position. As there are no key dimensions, there is only a single valid (empty) key that can be accessed with dmap[()]
or dmap.select()
making event
the only way to explore new parameters.
We will examine the event
method and the machinery that powers streams in more detail later in the user guide after we have looked at more examples of how streams are used in practice.
Working with multiple streams#
The previous example showed a curve parameterized by a single dimension t
. Often you will have multiple stream parameters you would like to declare as follows:
ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)
XY = Stream.define('XY',x=0.0,y=0.0)
def marker(x,y):
return hv.VLine(x) * hv.HLine(y)
image = hv.Image(np.sin(xx)*np.cos(yy))
dmap = hv.DynamicMap(marker, streams=[XY()])
image * dmap
You can update both x
and y
by passing multiple keywords to the event
method:
dmap.event(x=-0.2, y=0.1)
Note that the definition above behaves the same as the following definition where we define separate X
and Y
stream classes:
X = Stream.define('X',x=0.0)
Y = Stream.define('Y',y=0.0)
hv.DynamicMap(marker, streams=[X(), Y()])
The reason why you might want to list multiple streams instead of always defining a single stream containing all the required stream parameters will be made clear in the Custom Interactivity guide.
Using Parameterized classes as a stream#
Creating a custom Stream
class is one easy way to declare parameters. However, there’s no need to make a Stream if you have already expressed your domain knowledge on a Parameterized
class. For instance, let’s assume you have made a simple parameterized BankAccount
class:
class BankAccount(param.Parameterized):
balance = param.Number(default=0, doc="Bank balance in USD")
overdraft = param.Number(default=200, doc="Overdraft limit")
account = BankAccount(name='Jane', balance=300)
You can link parameter changes straight to DynamicMap callable parameters by passing a keyword:param dictionary to the streams
argument (for HoloViews version >= 1.14.2):
streams = dict(total=account.param.balance, overdraft=account.param.overdraft, owner=account.param.name)
def table(owner, total, overdraft):
return hv.Table([(owner, overdraft, total)], ['Owner', 'Overdraft ($)', 'Total ($)'])
bank_dmap = hv.DynamicMap(table, streams=streams)
bank_dmap.opts(height=100)
Now as you set the balance
parameter on the janes_account
instance, the DynamicMap above updates. Note that the dictionary specifies that the balance
parameter is mapped to the total
argument of the callable.
account.balance=65.4
account.overdraft=350
Use with panel
#
This dictionary format is particularly useful when used with the Panel library (a dependency of HoloViews that should always be available), because panel
widgets always reflect their values on the value
parameter. This means that if you declare two Panel widgets as follows:
import panel as pn
slider = pn.widgets.FloatSlider(start=0, end=500, name='Balance')
checkbox = pn.widgets.Select(options=['student','regular', 'savings'], name='Account Type')
pn.Row(slider, checkbox)
You can map both widget values into a DynamicMap
callback without having a name clash as follows:
overdraft_limits = {'student':300, 'regular':100, 'savings':0} # Overdraft limits for different account types
streams = dict(owner=account.param.name, total=slider.param.value, acc=checkbox.param.value)
def account_info(owner, total, acc):
return hv.Table([(owner, acc, overdraft_limits[acc], total)],
['Owner', 'Account Type', 'Overdraft ($)', 'Total ($)'])
widget_dmap = hv.DynamicMap(account_info, streams=streams)
widget_dmap.opts(height=100)
You can now update the plot above using the slider and dropdown widgets. Note that for all these examples, a Params
stream is created internally. This type of stream can wrap Parameterized objects or sets of Parameters but (since HoloViews 1.10.8) it is rare that an explicit stream object like that needs to be used directly at the user level. To see more examples of how to use Panel with HoloViews, see the Dashboards user guide.
Using .apply.opts
#
You can supplying Parameters in a similar manner to the .apply.opts
method. In the following example, a Style
class has Parameters that indicate the desired colorma and color levels for the image
instance defined earlier. We can link these together as follows:
class Style(param.Parameterized):
colormap = param.ObjectSelector(default='viridis', objects=['viridis', 'plasma', 'magma'])
color_levels = param.Integer(default=255, bounds=(1, 255))
style = Style()
image.apply.opts(colorbar=True, width=400, cmap=style.param.colormap, color_levels=style.param.color_levels)