In the exploring with containers section, the DynamicMap
container was introduced. In that section, the arguments to the callable returning elements were supplied by HoloViews sliders. In this section, we will generalize the ways in which you can generate values to update a DynamicMap
.
In [ ]:
import numpy as np
import pandas as pd
import holoviews as hv
hv.extension('bokeh', 'matplotlib')
%opts Ellipse [xaxis=None yaxis=None] (color='red' line_width=2)
%opts Box [xaxis=None yaxis=None] (color='blue' line_width=2)
DynamicMap
Let us now create a simple DynamicMap
using three annotation elements, namely Box
, Text
, and Ellipse
:
In [ ]:
def annotations(angle):
radians = (angle / 180) * np.pi
return (hv.Box(0,0,4, orientation=np.pi/4)
* hv.Ellipse(0,0,(2,4), orientation=radians)
* hv.Text(0,0,'{0}º'.format(float(angle))))
hv.DynamicMap(annotations, kdims=['angle']).redim.range(angle=(0, 360)).redim.label(angle='angle (º)')
This example uses the concepts introduced in the exploring with containers section. As before, the argument angle
is supplied by the position of the 'angle' slider.
HoloViews offers a way of supplying the angle
value to our annotation function through means other than sliders, namely via the streams system which you can learn about in the user guide.
All stream classes are found in the streams
submodule and are subclasses of Stream
. You can use Stream
directly to make custom stream classes via the define
classmethod:
In [ ]:
from holoviews import streams
from holoviews.streams import Stream
Angle = Stream.define('Angle', angle=0)
Here Angle
is capitalized as it is a subclass of Stream
with a numeric angle parameter, which has a default value of zero. You can verify this using hv.help
:
In [ ]:
hv.help(Angle)
Now we can declare a DynamicMap
where instead of specifying kdims
, we instantiate Angle
with an angle
of 45º and pass it to the streams
parameter of the DynamicMap
:
In [ ]:
%%opts Box (color='green')
dmap=hv.DynamicMap(annotations, streams=[Angle(angle=45)])
dmap
As expected, we see our ellipse with an angle of 45º as specified via the angle
parameter of our Angle
instance. In itself, this wouldn't be very useful but given that we have a handle on our DynamicMap
dmap
, we can now use the event
method to update the angle
parameter value and update the plot:
In [ ]:
dmap.event(angle=90)
When running this cell, the visualization above will jump to the 90º position! If you have already run the cell, just change the value above and re-run, and you'll see the plot above update.
This simple example shows how you can use the event
method to update a visualization with any value you can generate in Python.
In [ ]:
# Exercise: Regenerate the DynamicMap, initializing the angle to 15 degrees
In [ ]:
# Exercise: Use dmap.event to set the angle shown to 145 degrees.
In [ ]:
# Exercise: Do not specify an initial angle so that the default value of 0 degrees is used.
In [ ]:
# Exercise: Use the cell magic %%output backend='matplotlib' to try the above with matplotlib
In [ ]:
# Exercise: Declare a DynamicMap using annotations2 and AngleAndSize
# Then use the event method to set the size to 1.5 and the angle to 30 degrees
def annotations2(angle, size):
radians = (angle / 180.) * np.pi
return (hv.Box(0,0,4, orientation=np.pi/4)
* hv.Ellipse(0,0,(size,size*2), orientation=radians)
* hv.Text(0,0,'{0}º'.format(float(angle))))
AngleAndSize = Stream.define('AngleAndSize', angle=0., size=1.)
Using streams you can animate your visualizations by driving them with events from Python. Of course, you could use loops to call the event
method, but this approach can queue up events much faster than they can be visualized. Instead of inserting sleeps into your loops to avoid that problem, it is recommended you use the periodic
method, which lets you specify a time period between updates (in seconds):
In [ ]:
%%opts Ellipse (color='orange')
dmap2=hv.DynamicMap(annotations, streams=[Angle(angle=0)])
dmap2
In [ ]:
dmap2.periodic(0.01, count=180, timeout=8, param_fn=lambda i: {'angle':i})
If you re-execute the above cell, you should see the preceding plot update continuously until the count value is reached.
In [ ]:
# Exercise: Experiment with different period values. How fast can things update?
In [ ]:
# Exercise: Increase count so that the oval completes a full rotation.
In [ ]:
# Exercise: Lower the timeout so the oval completes less than a quarter turn before stopping
Often, you will want to tie streams to specific user actions in the live JavaScript interface. There are no limitations on how you can generate updated stream parameters values in Python, and so you could manually support updating streams from JavaScript as long as it can communicate with Python to trigger an appropriate stream update. But as Python programmers, we would rather avoid writing JavaScript directly, so HoloViews supports the concept of linked stream classes where possible.
Currently, linked streams are only supported by the Bokeh plotting extension, because only Bokeh executes JavaScript in the notebook and has a suitable event system necessary to enable linked streams (matplotlib displays output as static PNG or SVG in the browser). Here is a simple linked stream example:
In [ ]:
%%opts HLine [xaxis=None yaxis=None]
pointer = streams.PointerXY(x=0, y=0)
def crosshair(x, y):
return hv.Ellipse(0,0,1) * hv.HLine(y) * hv.VLine(x)
hv.DynamicMap(crosshair, streams=[pointer])
When hovering in the plot above when backed by a live Python process, the crosshair will track the cursor.
The way it works is very simple: the crosshair
function puts a crosshair at whatever x,y location it is given, the pointer
object supplies a stream of x,y values based on the mouse pointer location, and the DynamicMap
object connects the pointer stream's x,y values to the crosshair
function to generate the resulting plots.
In [ ]:
# Exercise: Set the defaults so that the crosshair initializes at x=0.25, y=0.25
In [ ]:
# Exercise: Copy the above example and adapt it to make a red point of size 10 follow your cursor (using hv.Points)
You can view other similar examples of custom interactivity in our reference gallery and learn more about linked streams in the user guide. Here is a quick summary of some of the more useful linked stream classes HoloViews currently offers and the parameters they supply:
PointerX/PointerY/PointerYX
: The x,y or (x,y) position of the cursor.SingleTap/DoubleTap/Tap
: Position of single, double or all tap events.BoundsX/BoundsY/BoundsXY
: The x,y or x and y extents selected with the Bokeh box select tool.RangeX/RangeY/RangeXY
: The x,y or x and y range of the currently displayed axesSelection1D
: The selected glyphs as a 1D selection.Any of these values can easily be tied to any visible element of your visualization.
In [ ]:
%%opts Scatter[width=900 height=400 tools=['xbox_select'] ] (cmap='RdBu' line_color='black' size=5 line_width=0.5)
%%opts Scatter [color_index='latitude' colorbar=True colorbar_position='bottom' colorbar_opts={'title': 'Latitude'}]
eclipses = pd.read_csv('../data/eclipses_21C.csv', parse_dates=['date'])
magnitudes = hv.Scatter(eclipses, kdims=['hour_local'], vdims=['magnitude','latitude'])
def selection_example(index):
text = '{0} eclipses'.format(len(index)) if index else ''
return magnitudes * hv.Text(2,1, text)
dmap3 = hv.DynamicMap(selection_example, streams=[streams.Selection1D()])
dmap3.redim.label(magnitude='Eclipse Magnitude', hour_local='Hour (local time)')
Try enabling the Box Select tool and then select a region of the plot.
In a single code cell we have achieved quite a lot! We have:
Selection1D
stream class to supply the points selected by the toolThis visualization would be a good candidate for an online dashboard. We will see how you can deploy such visualizations using bokeh server in the deploying bokeh apps section, but first we will look at how we can handle truly large datasets.