06. Custom Interactivity

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)

A simple 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.

Introducing Streams

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

Periodic updates

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

Linked streams

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 axes
  • Selection1D: The selected glyphs as a 1D selection.

Any of these values can easily be tied to any visible element of your visualization.

A more advanced example

Let's now build a more advanced example using the eclipse dataset we explored earlier, where the stream supplies values when a particular Bokeh tool ("Box Select") is active:


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:

  1. Loaded the data using pandas
  2. Turned this data into a Scatter of magnitude against local time
  3. Declared that an 'xbox_select' Bokeh tool should be used
  4. Used the Selection1D stream class to supply the points selected by the tool
  5. Declared a callback to show the number of selected eclipses as text
  6. Applied a colormap to the points encoding latitude and added a colorbar.
  7. Styled everything to our liking.

Onwards

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