{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", " \n", " \n", "

Bokeh Tutorial

\n", "
\n", "\n", "

06. Linking and Interactions

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.io import output_notebook, show\n", "from bokeh.plotting import figure\n", "output_notebook()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we know from the previous chapter how multiple plots can be placed together in a layout, we can start to look at how different plots can be linked together, or how plots can be linked to widgets. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Linked Interactions\n", "\n", "It is possible to link various interactions between different Bokeh plots. For instance, the ranges of two (or more) plots can be linked, so that when one of the plots is panned (or zoomed, or otherwise has its range changed) the other plots will update in unison. It is also possible to link selections between two plots, so that when items are selected on one plot, the corresponding items on the second plot also become selected. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Linked panning\n", "\n", "Linked panning (when multiple plots have ranges that stay in sync) is simple to spell with Bokeh. You simply share the appropriate range objects between two (or more) plots. The example below shows how to accomplish this by linking the ranges of three plots in various ways:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.layouts import gridplot\n", "\n", "x = list(range(11))\n", "y0, y1, y2 = x, [10-i for i in x], [abs(i-5) for i in x]\n", "\n", "plot_options = dict(width=250, height=250, tools='pan,wheel_zoom')\n", "\n", "# create a new plot\n", "s1 = figure(**plot_options)\n", "s1.circle(x, y0, size=10, color=\"navy\")\n", "\n", "# create a new plot and share both ranges\n", "s2 = figure(x_range=s1.x_range, y_range=s1.y_range, **plot_options)\n", "s2.triangle(x, y1, size=10, color=\"firebrick\")\n", "\n", "# create a new plot and share only one range\n", "s3 = figure(x_range=s1.x_range, **plot_options)\n", "s3.square(x, y2, size=10, color=\"olive\")\n", "\n", "p = gridplot([[s1, s2, s3]])\n", "\n", "# show the results\n", "show(p)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# EXERCISE: create two plots in a gridplot, and link their ranges\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Linked brushing\n", "\n", "Linking selections is accomplished in a similar way, by sharing data sources between plots. Note that normally with ``bokeh.plotting`` and ``bokeh.charts`` creating a default data source for simple plots is handled automatically. However to share a data source, we must create them by hand and pass them explicitly. This is illustrated in the example below:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import ColumnDataSource\n", "\n", "x = list(range(-20, 21))\n", "y0, y1 = [abs(xx) for xx in x], [xx**2 for xx in x]\n", "\n", "# create a column data source for the plots to share\n", "source = ColumnDataSource(data=dict(x=x, y0=y0, y1=y1))\n", "\n", "TOOLS = \"box_select,lasso_select,help\"\n", "\n", "# create a new plot and add a renderer\n", "left = figure(tools=TOOLS, width=300, height=300)\n", "left.circle('x', 'y0', source=source)\n", "\n", "# create another new plot and add a renderer\n", "right = figure(tools=TOOLS, width=300, height=300)\n", "right.circle('x', 'y1', source=source)\n", "\n", "p = gridplot([[left, right]])\n", "\n", "show(p)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# EXERCISE: create two plots in a gridplot, and link their data sources\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Hover Tools\n", "\n", "Bokeh has a Hover Tool that allows additional information to be displayed in a popup whenever the user hovers over a specific glyph. Basic hover tool configuration amounts to providing a list of ``(name, format)`` tuples. The full details can be found in the User's Guide [here](https://bokeh.pydata.org/en/latest/docs/user_guide/tools.html#hovertool).\n", "\n", "The example below shows some basic usage of the Hover tool with a circle glyph, using hover information defined in utils.py:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import HoverTool\n", "\n", "source = ColumnDataSource(\n", " data=dict(\n", " x=[1, 2, 3, 4, 5],\n", " y=[2, 5, 8, 2, 7],\n", " desc=['A', 'b', 'C', 'd', 'E'],\n", " )\n", " )\n", "\n", "hover = HoverTool(\n", " tooltips=[\n", " (\"index\", \"$index\"),\n", " (\"(x,y)\", \"($x, $y)\"),\n", " (\"desc\", \"@desc\"),\n", " ]\n", " )\n", "\n", "p = figure(width=300, height=300, tools=[hover], title=\"Mouse over the dots\")\n", "\n", "p.circle('x', 'y', size=20, source=source)\n", "\n", "show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Widgets" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Bokeh supports direct integration with a small basic widget set. These can be used in conjunction with a Bokeh Server, or with ``CustomJS`` models to add more interactive capability to your documents. You can see a complete list, with example code in the [Widgets and DOM elements](https://docs.bokeh.org/en/latest/docs/user_guide/interaction/widgets.html) section of the User's Guide. \n", "\n", "\n", "\n", "*NOTE: In this Tutorial chapter, we will focus on using widgets with JavaScript callbacks. The Tutorial chapter on Bokeh server applications covers using Bokeh widgets with real Python callbacks*\n", "\n", "\n", "\n", "\n", "To use the widgets, include them in a layout like you would a plot object:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import Slider\n", "\n", "\n", "slider = Slider(start=0, end=10, value=1, step=.1, title=\"foo\")\n", "\n", "show(slider)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# EXERCISE: create and show a Select widget \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# CustomJS Callbacks\n", "\n", "In order for a widget to be useful, it needs to be able to perform some action. Using the Bokeh server, it is possible to have widgets trigger real Python code. That possibility will be explored in the Bokeh server chapter of the tutorial. Here, we look at how widgets can be configured with `CustomJS` callbacks that execute snippets of JavaScript code. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import TapTool, CustomJS, ColumnDataSource\n", "\n", "callback = CustomJS(code=\"alert('you tapped a circle!')\")\n", "tap = TapTool(callback=callback)\n", "\n", "p = figure(width=600, height=300, tools=[tap])\n", "\n", "p.circle(x=[1, 2, 3, 4, 5], y=[2, 5, 8, 2, 7], size=20)\n", "\n", "show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## CustomJS for Property changes\n", "\n", "Bokeh objects that have values associated can have small JavaScript actions attached to them using the `js_on_change` method. These actions (also referred to as \"callbacks\") are executed whenever the widget's value is changed. In order to make it easier to refer to specific Bokeh models (e.g., a data source, or a glyph) from JavaScript, the ``CustomJS`` object also accepts a dictionary of \"args\" that map names to Python Bokeh models. The corresponding JavaScript models are made available automatically to the ``CustomJS`` code:\n", "\n", "```python\n", "CustomJS(args=dict(source=source, slider=slider), code=\"\"\"\n", " // easily refer to BokehJS source and slider objects in this JS code\n", " var data = source.data;\n", " var f = slider.value;\n", "\"\"\")\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Slider widget example\n", "\n", "The example below shows an action attached to a slider that updates a data source whenever the slider is moved. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.layouts import column\n", "from bokeh.models import CustomJS, ColumnDataSource, Slider\n", "\n", "x = [x*0.005 for x in range(0, 201)]\n", "\n", "source = ColumnDataSource(data=dict(x=x, y=x))\n", "\n", "plot = figure(width=400, height=400)\n", "plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)\n", "\n", "slider = Slider(start=0.1, end=6, value=1, step=.1, title=\"power\")\n", "\n", "update_curve = CustomJS(args=dict(source=source, slider=slider), code=\"\"\"\n", " const f = cb_obj.value\n", " const x = source.data.x\n", " const y = Array.from(x, (x) => Math.pow(x, f))\n", " source.data = { x, y }\n", "\"\"\")\n", "slider.js_on_change('value', update_curve)\n", "\n", "\n", "show(column(slider, plot))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Create a plot that updates based on a Select widget\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Data selection example\n", "\n", "It's also possible to make JavaScript actions that execute whenever a user selection (e.g., box, point, lasso) changes. This is done by attaching the same kind of CustomJS object to whatever data source the selection is made on.\n", "\n", "The example below is a bit more sophisticated, and demonstrates updating one glyph's data source in response to another glyph's selection: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from random import random\n", "\n", "from bokeh.models import ColumnDataSource, CustomJS\n", "from bokeh.plotting import figure, show\n", "\n", "x = [random() for x in range(500)]\n", "y = [random() for y in range(500)]\n", "color = [\"navy\"] * len(x)\n", "\n", "s = ColumnDataSource(data=dict(x=x, y=y, color=color))\n", "p = figure(width=400, height=400, tools=\"lasso_select\", title=\"Select Here\")\n", "p.circle('x', 'y', color='color', size=8, source=s, alpha=0.4,\n", " selection_color=\"firebrick\")\n", "\n", "s2 = ColumnDataSource(data=dict(x=[0, 1], ym=[0.5, 0.5]))\n", "p.line(x='x', y='ym', color=\"orange\", line_width=5, alpha=0.6, source=s2)\n", "\n", "s.selected.js_on_change('indices', CustomJS(args=dict(s=s, s2=s2), code=\"\"\"\n", " const inds = s.selected.indices\n", " if (inds.length > 0) {\n", " const ym = inds.reduce((a, b) => a + s.data.y[b], 0) / inds.length\n", " s2.data = { x: s2.data.x, ym: [ym, ym] }\n", " }\n", "\"\"\"))\n", "\n", "show(p)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Experiment with selection callbacks\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## CustomJS for UI Events\n", "\n", "Bokeh also has a general events system\n", "\n", "All of the available UI events, and their properties, are listed in the Reference Guide section for [bokeh.events](https://bokeh.pydata.org/en/latest/docs/reference/events.html)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.plotting import figure\n", "from bokeh import events\n", "from bokeh.models import CustomJS, Div, Button\n", "from bokeh.layouts import column, row\n", "\n", "import numpy as np\n", "x = np.random.random(size=2000) * 100\n", "y = np.random.random(size=2000) * 100\n", "\n", "p = figure(tools=\"box_select\")\n", "p.scatter(x, y, radius=1, fill_alpha=0.6, line_color=None)\n", "\n", "div = Div(width=400)\n", "button = Button(label=\"Button\", width=300)\n", "layout = column(button, row(p, div))\n", "\n", "# Events with no attributes\n", "button.js_on_event(events.ButtonClick, CustomJS(args=dict(div=div), code=\"\"\"\n", "div.text = \"Button!\";\n", "\"\"\")) \n", "\n", "p.js_on_event(events.SelectionGeometry, CustomJS(args=dict(div=div), code=\"\"\"\n", "div.text = \"Selection!

\" + JSON.stringify(cb_obj.geometry, undefined, 2);\n", "\"\"\"))\n", "\n", "show(layout)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Create a plot that responds to different events from bokeh.events\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Additional Information\n", "\n", "There are many kinds of interactions and events that can be connected to `CustomJS` callbacks. \n", "\n", "\n", "* Widgets - Button, Toggle, Dropdown, TextInput, AutocompleteInput, Select, Multiselect, Slider, (DateRangeSlider), DatePicker,\n", "* Tools - TapTool, BoxSelectTool, HoverTool,\n", "* Selection - ColumnDataSource, AjaxDataSource, BlazeDataSource, ServerDataSource\n", "* Ranges - Range1d, DataRange1d, FactorRange\n", "\n", "\n", "For more complete examples the User Guide section on [JavaScript Interactions](https://bokeh.pydata.org/en/latest/docs/user_guide/interaction.html)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Next Section" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Click on this link to go to the next notebook: [07 - Bar and Categorical Data Plots](07%20-%20Bar%20and%20Categorical%20Data%20Plots.ipynb).\n", "\n", "To go back to the overview, click [here](00%20-%20Introduction%20and%20Setup.ipynb)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.13" } }, "nbformat": 4, "nbformat_minor": 4 }