Linking Plots¶
import numpy as np
import holoviews as hv
from holoviews import opts
hv.extension('bokeh')
When working with the bokeh backend in HoloViews complex interactivity can be achieved using very little code, whether that is shared axes, which zoom and pan together or shared datasources, which allow for linked cross-filtering. Separately it is possible to create custom interactions by attaching LinkedStreams to a plot and thereby triggering events on interactions with the plot. The Streams based interactivity affords a lot of flexibility to declare custom interactivity on a plot, however it always requires a live Python kernel to be connected either via the notebook or bokeh server. The Link
classes described in this user guide however allow declaring interactions which do not require a live server, opening up the possibility of declaring complex interactions in a plot that can be exported to a static HTML file.
What is a Link
?¶
A Link
defines some connection between a source and target object in their visualization. It is quite similar to a Stream
as it allows defining callbacks in response to some change or event on the source object, however, unlike a Stream, it does not transfer data between the browser and a Python process. Instead a Link
directly causes some action to occur on the target
, for JS based backends this usually means that a corresponding JS callback will effect some change on the target in response to a change on the source.
One of the simplest examples of a Link
is the DataLink
which links the data from two sources as long as they match in length, e.g. below we create two elements with data of the same length. By declaring a DataLink
between the two we can ensure they are linked and can be selected together:
from holoviews.plotting.links import DataLink
scatter1 = hv.Scatter(np.arange(100))
scatter2 = hv.Scatter(np.arange(100)[::-1], 'x2', 'y2')
dlink = DataLink(scatter1, scatter2)
(scatter1 + scatter2).opts(
opts.Scatter(tools=['box_select', 'lasso_select']))
If we want to display the elements subsequently without linking them we can call the unlink
method:
dlink.unlink()
(scatter1 + scatter2)
Another example of a link is the RangeToolLink
which adds a RangeTool to the source
plot which is linked to the axis range on the target
plot. In this way the source plot can be used as an overview of the full data while the target plot provides a more detailed view of a subset of the data:
from holoviews.plotting.links import RangeToolLink
data = np.random.randn(1000).cumsum()
source = hv.Curve(data).opts(width=800, height=125, axiswise=True, default_tools=[])
target = hv.Curve(data).opts(width=800, labelled=['y'], toolbar=None)
rtlink = RangeToolLink(source, target)
(target + source).opts(merge_tools=False).cols(1)
Advanced: Writing a Link
¶
A Link
consists of two components the Link
itself and a LinkCallback
which provides the actual implementation behind the Link
. In order to demonstrate writing a Link
we'll start with a fairly straightforward example, linking an HLine
or VLine
to the mean value of a selection on a Scatter
element. To express this we declare a MeanLineLink
class subclassing from the Link
baseclass and declare ClassSelector
parameters for the source
and target
with the appropriate types to perform some basic validation. Additionally we declare a column
parameter to specify which column to compute the mean on.
import param
from holoviews.plotting.links import Link
class MeanLineLink(Link):
column = param.String(default='x', doc="""
The column to compute the mean on.""")
_requires_target = True
Now we have the Link
class we need to write the implementation in the form of a LinkCallback
, which in the case of bokeh will be translated into a CustomJS
callback. A LinkCallback
should declare the source_model
we want to listen to events on and a target_model
, declaring which model should be altered in response. To find out which models we can attach the Link
to we can create a Plot
instance and look at the plot.handles
, e.g. here we create a ScatterPlot
and can see it has a 'cds', which represents the ColumnDataSource
.
renderer = hv.renderer('bokeh')
plot = renderer.get_plot(hv.Scatter([]))
plot.handles.keys()
In this case we are interested in the 'cds' handle, but we still have to tell it which events should trigger the callback. Bokeh callbacks can be grouped into two types, model property changes and events. For more detail on these two types of callbacks see the Bokeh user guide.
For this example we want to respond to changes to the ColumnDataSource.selected
property. We can declare this in the on_source_changes
class atttribute on our callback. So now that we have declared which model we want to listen to events on and which events we want to listen to, we have to declare the model on the target we want to change in response.
We can once again look at the handles on the plot corresponding to the HLine
element:
plot = renderer.get_plot(hv.HLine(0))
plot.handles.keys()
We now want to change the glyph
, which defines the position of the HLine
, so we declare the target_model
as 'glyph'
. Having defined both the source and target model and the events we can finally start writing the JS callback that should be triggered. To declare it we simply define the source_code
class attribute. To understand how to write this code we need to understand how the source and target models, we have declared, can be referenced from within the callback.
The source_model
will be made available by prefixing it with source_
, while the target model is made available with the prefix target_
. This means that the ColumnDataSource
on the source
can be referenced as source_source
, while the glyph on the target can be referenced as target_glyph
.
Finally, any parameters other than the source
and target
on the Link
will also be made available inside the callback, which means we can reference the appropriate column
in the ColumnDataSource
to compute the mean value along a particular axis.
Once we know how to reference the bokeh models and Link
parameters we can access their properties to compute the mean value of the current selection on the source ColumnDataSource
and set the target_glyph.position
to that value.
A LinkCallback
may also define a validate method to validate that the Link parameters and plots are compatible, e.g. in this case we can validate that the column
is actually present in the source_plot ColumnDataSource
.
from holoviews.plotting.bokeh.callbacks import LinkCallback
class MeanLineCallback(LinkCallback):
source_model = 'selected'
source_handles = ['cds']
on_source_changes = ['indices']
target_model = 'glyph'
source_code = """
var inds = source_selected.indices
var d = source_cds.data
var vm = 0
if (inds.length == 0)
return
for (var i = 0; i < inds.length; i++)
vm += d[column][inds[i]]
vm /= inds.length
target_glyph.location = vm
"""
def validate(self):
assert self.link.column in self.source_plot.handles['cds'].data
Finally we need to register the MeanLineLinkCallback
with the MeanLineLink
using the register_callback
classmethod:
MeanLineLink.register_callback('bokeh', MeanLineCallback)
Now the newly declared Link is ready to use, we'll create a Scatter
element along with an HLine
and VLine
element and link each one:
options = opts.Scatter(
selection_fill_color='firebrick', alpha=0.4, line_color='black', size=8,
tools=['lasso_select', 'box_select'], width=500, height=500,
active_tools=['lasso_select']
)
scatter = hv.Scatter(np.random.randn(500, 2)).opts(options)
vline = hv.VLine(scatter['x'].mean()).opts(color='black')
hline = hv.HLine(scatter['y'].mean()).opts(color='black')
MeanLineLink(scatter, vline, column='x')
MeanLineLink(scatter, hline, column='y')
scatter * hline * vline
Using the 'box_select' and 'lasso_select' tools will now update the position of the HLine and VLine.