{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Linking objects in Panel" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the [Param user guide](Param.ipynb), 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).\n", "\n", "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:\n", "\n", "* ``obj.link``: convenient high-level API to link objects via Python calls\n", "* ``obj.param.watch``: powerful but lower-level Python API provided by [param](https://param.pyviz.org) \n", "* ``obj.jslink``: high-level API to link objects via JS code\n", "* ``obj.jscallback``: a lower-level API to define arbitrary Javascript callbacks\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import panel as pn\n", "pn.extension()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Python links using the ``link`` method\n", "\n", "To start, let's see how a ``TextInput`` widget and a ``Markdown`` pane normally behave:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Row(pn.widgets.TextInput(value=\"Editable text\"), pn.pane.Markdown('Some markdown'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These two Panel objects are entirely independent; text can be entered into the input area and separately the Markdown pane will display its argument. \n", "\n", "What if we wanted connected input and output displays? One option when you expect to have a live Python process available is to use the ``link`` method on Widgets to link its parameters to some other Panel object. In the simplest case we simply provide the ``target`` object and define the mapping between the source and target parameters as the keywords. In this case, we map the ``value`` parameter on the ``TextInput`` widget to the ``object`` parameter on the ``Markdown`` pane:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "markdown = pn.pane.Markdown(\"Some text\")\n", "text_input = pn.widgets.TextInput(value=markdown.object)\n", "\n", "text_input.link(markdown, value='object')\n", "\n", "pn.Row(text_input, markdown)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, if Python is running and you type something in the box above and press Return, the corresponding Markdown pane should update to match. And if you use Python to directly manipulate the value of the text input (e.g. by editing the following cell to have new text in it and then executing it), then the Markdown should also update to match:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "text_input.value = 'Some text'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For more complex mappings between the widget value and the target parameter, we can define an arbitrary transformation as a Python callback:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "m = pn.pane.Markdown(\"\")\n", "t = pn.widgets.TextInput()\n", "\n", "def callback(target, event):\n", " target.object = event.new.upper() + '!!!'\n", "\n", "t.link(m, callbacks={'value': callback})\n", "t.value=\"Some text\"\n", "\n", "pn.Row(t, m)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that here we explicitly set `t.value` before displaying the panel to trigger the linked Markdown pane to update to match the text widget; callbacks are not otherwise triggered when the links are first set up.\n", "\n", "\n", "## Python links using the ``watch`` method\n", "\n", "The ``link`` method used above provides a high-level API to link to parameters, which is adequate in most cases. If we need more control, we can fall back to the underlying ``param.watch`` method, which is what Panel uses internally to make all reactive features work. The main differences are that ``watch`` 1) does not assume you are linking two objects, providing more control over what you are watching, 2) allows batched callbacks when multiple parameters change at once, and 3) allows you to specify that an event should be triggered every time the parameter is set (instead of the default of only when the parameter value actually changes). If you do not need this level of control, just skip to the [Linking objects in JS](#Linking-objects-in-JS) section below.\n", "\n", "To demonstrate ``param.watch``, let us set up three different models: 1) a `Markdown` pane to display the possible options, 2) a ``Markdown`` pane to display the _selected_ options, and 3) a ``ToggleGroup`` widget that allows us to toggle between a number of options:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "selections = pn.pane.Markdown(object='')\n", "selected = pn.pane.Markdown(object='')\n", "toggle = pn.widgets.ToggleGroup(options=['A', 'B'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Defining a callback\n", "\n", "Next we define a callback that can handle multiple parameter changes at once and uses the ``Event``'s ``name`` to figure out how to process the event. In this case it updates either the ``selections`` or the ``selected`` pane depending on whether ToggleGroup ``options`` or ``value`` changed:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def callback(*events):\n", " print(events)\n", " for event in events:\n", " if event.name == 'options':\n", " selections.object = 'Possible options: %s' % ', '.join(event.new)\n", " elif event.name == 'value':\n", " selected.object = 'Selected: %s' % ','.join(event.new)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Event objects\n", "\n", "Before going any further let us discover what these ``Event`` objects are. An ``Event`` is used to signal the change in a parameter value. Event objects provide a number of useful attributes that provides additional information about the event:\n", "\n", "* **``name``**: The name of the parameter that has changed\n", "* **``new``**: The new value of the parameter\n", "* **``old``**: The old value of the parameter before the event was triggered\n", "* **``type``**: The type of event ('triggered', 'changed', or 'set')\n", "* **``what``**: Describes what about the parameter changed (usually the value but other parameter attributes can also change)\n", "* **``obj``**: The Parameterized instance that holds the parameter\n", "* **``cls``**: The Parameterized class that holds the parameter" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Registering a watcher\n", "\n", "Now that we know how to define a callback and make use of ``Event`` attributes, it is time to register the callback. The ``obj.param.watch`` method lets us supply the callback along with the parameters we want to watch. Additionally we can declare whether the events should only be triggered when the parameter value changes, or every time the parameter is set:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "watcher = toggle.param.watch(callback, ['options', 'value'], onlychanged=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let us display the widget alongside the ``Markdown`` panes that reflect the current state of the widget:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Row(pn.Column(toggle, width=200, height=50), selections, pn.Spacer(width=50, height=50), selected)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To initialize the `selections` and `selected` we can explicitly ``trigger`` options and value events:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "toggle.param.trigger('options', 'value')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also override the initial parameters using the ``set_param`` method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "options = ['A','B','C','D']\n", "toggle.param.set_param(options=dict(zip(options,options)), value=['D'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using `set_param` allows us to batch two separate changes (the options and the value) together, which you can see from the ``print`` output resulted into a single invocation of the callback. You could instead have set them separately using the usual parameter-setting syntax `toggle.value=['D']; toggle.options=dict(zip(options,options))`, but batching them can be much more efficient for a non-trivial callback like a database query or a complex plot that needs updating.\n", "\n", "Now that the widgets are visible, you can toggle the option values and see the selected pane update in response via the callback (if Python is running).\n", "\n", "#### Unlinking\n", "\n", "If for whatever reason we want to stop watching parameter changes we can unsubscribe by passing our ``watcher`` (returned in the ``watch`` call above) to the ``unwatch`` method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#toggle.param.unwatch(watcher)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Linking objects in JS\n", "\n", "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.\n", "\n", "### Linking Panel components\n", "\n", "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.\n", "\n", "#### Linking model properties\n", "\n", "Let us start by rehashing the example from above, linking the ``value`` of the ``TextInput`` widget to the ``object`` property of the ``Markdown`` pane:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "markdown = pn.pane.Markdown('Markdown display')\n", "text_input = pn.widgets.TextInput(value=markdown.object)\n", "\n", "link = text_input.jslink(markdown, value='object')\n", "\n", "pn.Row(text_input, markdown)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "#### Linking bi-directionally\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "t1 = pn.widgets.TextInput()\n", "t2 = pn.widgets.TextInput()\n", "\n", "t1.jslink(t2, value='value', bidirectional=True)\n", "\n", "pn.Row(t1, t2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Linking using custom JS code\n", "\n", "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 (````) 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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "markdown = pn.pane.Markdown(\"Markdown display\", width=400)\n", "text_input = pn.widgets.TextInput(value=\"Markdown display\")\n", "\n", "code = '''\n", " target.text = '' + source.value + ''\n", "'''\n", "link = text_input.jslink(markdown, code={'value': code})\n", "\n", "pn.Row(text_input, markdown)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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``.\n", "\n", "Of course, you can still update the value from Python, and it will automatically update the linked markdown:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "text_input.value=\"Markdown display\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Open URL\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "button = pn.widgets.Button(name='Open URL', button_type = 'primary')\n", "url = pn.widgets.TextInput(name='URL', value = 'https://pyviz.org/')\n", "button.js_on_click(args={'target': url}, code='window.open(target.value)')\n", "pn.Row(url, button)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Defining Javascript callbacks\n", "\n", "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.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "value1 = pn.widgets.Spinner(value=0, width=75)\n", "operator = pn.widgets.Select(value='*', options=['*', '+'], width=50, align='center')\n", "value2 = pn.widgets.Spinner(value=0, width=75)\n", "button = pn.widgets.Button(name='=', width=50)\n", "result = pn.widgets.StaticText(value='0', width=50, align='center')\n", "\n", "button.jscallback(clicks=\"\"\"\n", "if (op.value == '*') \n", " result.text = (v1.value * v2.value).toString()\n", "else\n", " result.text = (v1.value + v2.value).toString()\n", "\"\"\", args={'op': operator, 'result': result, 'v1': value1, 'v2': value2})\n", "\n", "pn.Row(value1, operator, value2, button, result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Linking Plots\n", "\n", "The above examples link widgets to simple static panes, but links are probably most useful when combined with dynamic objects like plots.\n", "\n", "#### Bokeh\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.plotting import figure\n", "\n", "p = figure(width=300, height=300)\n", "xs = np.linspace(0, 10)\n", "r = p.line(xs, np.sin(xs))\n", "\n", "width_slider = pn.widgets.FloatSlider(name='Line Width', start=0.1, end=10)\n", "width_slider.jslink(r.glyph, value='line_width')\n", "\n", "pn.Column(width_slider, p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### HoloViews\n", "\n", "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:\n", "\n", "* **``cds``**: The bokeh ``ColumnDataSource`` model which holds the data used to render the plot\n", "* **``glyph``**: The bokeh ``Glyph`` defining the style of the element\n", "* **``glyph_renderer``**: The Bokeh ``GlyphRenderer`` responsible for rendering the element\n", "* **``plot``**: The bokeh ``Figure``\n", "* **``xaxis``/``yaxis``**: The Axis models of the plot\n", "* **``x_range``/``y_range``**: The x/y-axis ``Range1d`` models defining the axis ranges\n", "\n", "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``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import holoviews as hv\n", "import holoviews.plotting.bokeh\n", "\n", "colors = [\"black\", \"red\", \"blue\", \"green\", \"gray\"]\n", "\n", "size_widget = pn.widgets.FloatSlider(value=8, start=3, end=20, name='Size')\n", "color_widget = pn.widgets.Select(name='Color', options=colors, value='black')\n", "\n", "points = hv.Points(np.random.rand(10, 2)).options(padding=0.1, line_color='black')\n", "\n", "size_widget.jslink(points, value='glyph.size')\n", "color_widget.jslink(points, value='glyph.fill_color')\n", "\n", "pn.Row(points, pn.Column(size_widget, color_widget))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](#Linking-using-custom-JS-code). Together these linking options should allow you to express whatever interactions you wish between your Panel objects." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }