year-vs-climatology / hvplot_docs /Linking_Plots.md
ahuang11's picture
Upload 52 files
b9a0f21 verified
```python
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:
```python
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:
```python
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:
```python
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.
```python
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](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/callbacks.html#userguide-interaction-jscallbacks). 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``.
```python
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](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/callbacks.html#userguide-interaction-jscallbacks).
For this example we want to respond to changes to the ``ColumnDataSource.selected`` property. We can declare this in the ``on_source_changes`` class attribute 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:
```python
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``.
```python
from holoviews.plotting.bokeh 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:
```python
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:
```python
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.