Linking objects in Panel#

In the Param user guide, we have seen how Parameterized classes can be used to automatically generate a graphical user interface for Python code without any additional effort. If you need full control over how your GUI is set up, you can instead manually define widgets linking directly to other objects using either Python or JavaScript (JS) callbacks. Python callbacks are simple for Python users to write and can directly access Python data structures, while JS callbacks can directly manipulate the displayed HTML document and allow setting up dynamic behavior even for exported HTML files (with no Python process running).

Here we will show how to link parameters of Panel objects, typically from widgets to other objects. To do this, we will introduce three API calls:

  • obj.link: convenient high-level API to link objects via Python calls

  • obj.param.watch: powerful but lower-level Python API provided by param

  • obj.jslink: high-level API to link objects via JS code

  • obj.jscallback: a lower-level API to define arbitrary Javascript callbacks

At the end of this notebook, we will then show how to use these APIs to set up links between widgets and plots that work even in exported HTML files where the Python process is no longer available.

import numpy as np
import panel as pn
pn.extension()


Linking objects in JS#

Linking objects in Python is often very convenient, because it allows writing code entirely in Python. However, it also requires a live Python kernel. If instead we want a static example (e.g. on a simple website or in an email) to have custom interactivity, or we simply want to avoid the overhead of having to call back into Python, we can define links in JavaScript.

Linking Panel components#

To begin with let us see how we can express simple links between standard Panel components before digging into some more specific and complex examples.

Linking model properties#

Let us start by rehashing the example from above, linking the value of the TextInput widget to the object property of the Markdown pane:

markdown = pn.pane.Markdown('Markdown display')
text_input = pn.widgets.TextInput(value=markdown.object)

link = text_input.jslink(markdown, value='object')

pn.Row(text_input, markdown)

As you can see the signature is identical to the link example above, but here Panel translates the specification into a JS code snippet which syncs the properties on the underlying Bokeh properties. But now if you edit the widget and press Return, the Markdown display will automatically update even in a static HTML web page.

Linking bi-directionally#

When you want the source and target to be linked bi-directionally, i.e. a change in one will automatically trigger a change in the other you can simply set the bidirectional argument:

t1 = pn.widgets.TextInput()
t2 = pn.widgets.TextInput()

t1.jslink(t2, value='value', bidirectional=True)

pn.Row(t1, t2)

Linking using custom JS code#

Since everything happens in JS for a jslink, we can’t provide a Python callback. Instead, we can define a JS code snippet, which is executed when a property changes. E.g. we can define a little code snippet which adds HTML bold tags (<b>) around the text before setting it on the target. The code argument should map from the parameter/property on the source object to the JS code snippet to execute:

markdown = pn.pane.Markdown("<b>Markdown display</b>", width=400)
text_input = pn.widgets.TextInput(value="Markdown display")

code = '''
    target.text = '<b>' + source.value + '</b>'
'''
link = text_input.jslink(markdown, code={'value': code})

pn.Row(text_input, markdown)

Here source and target are made available in the JavaScript namespace, allowing us to arbitrarily modify the models in response to property change events. Note however that the underlying Bokeh model property names may differ slightly from the naming of the parameters on Panel objects, e.g. the ‘object’ parameter on the Markdown pane translates to the ‘text’ property on the Bokeh model used to render the Markdown.

Of course, you can still update the value from Python, and it will automatically update the linked markdown:

text_input.value="Markdown display"

Open URL#

As an example of using jslink, here we open a URL from the TextInput widget value. A new browser tab will open with the provided URL:

button = pn.widgets.Button(name='Open URL', button_type = 'primary')
url = pn.widgets.TextInput(name='URL', value = 'https://pyviz.org/')
button.js_on_click(args={'target': url}, code='window.open(target.value)')
pn.Row(url, button)

Defining Javascript callbacks#

Sometimes defining a simple link between to objects is not sufficient, e.g. when there are a number of objects involved. In these cases it is helpful to be able to define arbitrary Javascript callbacks. A very simple example is a very basic calculator which allows multiplying or adding two values, in this case we have two widgets to input numbers, a selector to pick the operation, a display for the result and a button.

To implement this we define a jscallback, which is triggered when the Button.clicks property changes and provide a number of args allowing us to access the values of the various widgets:

value1 =   pn.widgets.Spinner(value=0, width=75)
operator = pn.widgets.Select(value='*', options=['*', '+'], width=50, align='center')
value2 =   pn.widgets.Spinner(value=0, width=75)
button =   pn.widgets.Button(name='=', width=50)
result =   pn.widgets.StaticText(value='0', width=50, align='center')

button.jscallback(clicks="""
if (op.value == '*')
  result.text = (v1.value * v2.value).toString()
else
  result.text = (v1.value + v2.value).toString()
""", args={'op': operator, 'result': result, 'v1': value1, 'v2': value2})

pn.Row(value1, operator, value2, button, result)

Linking Plots#

The above examples link widgets to simple static panes, but links are probably most useful when combined with dynamic objects like plots.

Bokeh#

The jslink API trivially allows us to link a parameter on a Panel widget to a Bokeh plot property. Here we create a Bokeh Figure with a simple sine curve. The jslink method allows us to pass any Bokeh model held by the Figure as the target, then link the widget value to some property on it. E.g. here we link a FloatSlider value to the line_width of the Line glyph:

from bokeh.plotting import figure

p = figure(width=300, height=300)
xs = np.linspace(0, 10)
r = p.line(xs, np.sin(xs))

width_slider = pn.widgets.FloatSlider(name='Line Width', start=0.1, end=10)
width_slider.jslink(r.glyph, value='line_width')

pn.Column(width_slider, p)

HoloViews#

Bokeh models allow us to directly access the underlying models and properties, but this access is more indirect when working with HoloViews objects. HoloViews makes various models available directly in the namespace so that they can be accessed for linking:

  • cds: The bokeh ColumnDataSource model which holds the data used to render the plot

  • glyph: The bokeh Glyph defining the style of the element

  • glyph_renderer: The Bokeh GlyphRenderer responsible for rendering the element

  • plot: The bokeh Figure

  • xaxis/yaxis: The Axis models of the plot

  • x_range/y_range: The x/y-axis Range1d models defining the axis ranges

All these are made available in the JS code’s namespace if we decide to provide a JS code snippet, but can also be referenced in the property mapping. We can map the widget value to a property on the glyph by providing a specification separated by periods. E.g. in this case we can map the value to the glyph.size:

import holoviews as hv
import holoviews.plotting.bokeh

colors = ["black", "red", "blue", "green", "gray"]

size_widget = pn.widgets.FloatSlider(value=8, start=3, end=20, name='Size')
color_widget = pn.widgets.Select(name='Color', options=colors, value='black')

points = hv.Points(np.random.rand(10, 2)).options(padding=0.1, line_color='black')

size_widget.jslink(points, value='glyph.size')
color_widget.jslink(points, value='glyph.fill_color')

pn.Row(points, pn.Column(size_widget, color_widget))

Of course, if you need to transform between the displayed widget value and the value to be used on the underlying Bokeh property, you can add custom JS code as shown in the previous section. Together these linking options should allow you to express whatever interactions you wish between your Panel objects.