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

Tutorial 08. Custom Interactivity

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In a few places earlier in the tutorials, you may have noticed examples of the [``DynamicMap``](http://holoviews.org/reference/containers/bokeh/DynamicMap.html) container, which can be used like a HoloMap but dynamically rendering each plot when requested instead of precomputing them statically. In those examples, the arguments to the callable returning elements were supplied by HoloViews sliders. In this section, we will generalize the ways in which you can generate values to update a ``DynamicMap``, to support an extensive range of custom interactivity to help you explore and reveal your data.\n", "\n", "
\n", "\n", "\n", "\n", "\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import holoviews as hv\n", "from holoviews import opts, dim\n", "hv.extension('bokeh', 'matplotlib')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "opts.defaults(\n", " opts.Box(xaxis=None, yaxis=None, color='red', line_width=2),\n", " opts.Ellipse(xaxis=None, yaxis=None, color='blue', line_width=2),\n", " opts.HLine(xaxis=None, yaxis=None))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A simple ``DynamicMap``\n", "\n", "Let us now create a simple [``DynamicMap``](http://holoviews.org/reference/containers/bokeh/DynamicMap.html) using three *annotation* elements, namely [``Box``](http://holoviews.org/reference/elements/bokeh/Ellipse.html), [``Text``](http://holoviews.org/reference/elements/bokeh/Ellipse.html), and [``Ellipse``](http://holoviews.org/reference/elements/bokeh/Ellipse.html):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def annotations(angle):\n", " radians = (angle / 180) * np.pi\n", " return (hv.Box(0,0,4, orientation=np.pi/4) \n", " * hv.Ellipse(0,0,(2,4), orientation=radians) \n", " * hv.Text(0,0,'{0}º'.format(float(angle))))\n", "\n", "hv.DynamicMap(annotations, kdims=['angle']).redim.range(angle=(0, 360)).redim.label(angle='angle (º)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This example uses concepts discussed in the [A1 Exploration with Containers](./A1_Exploration_with_Containers.ipynb) tutorial. Here the argument ``angle`` is supplied by the position of the 'angle' slider.\n", "\n", "## Introducing Streams\n", "\n", "HoloViews offers a way of supplying the ``angle`` value to our annotation function through means other than sliders, namely via the *streams* system which you can learn about in the [user guide](http://holoviews.org/user_guide/Responding_to_Events.html).\n", " \n", "All stream classes are found in the ``streams`` submodule and are subclasses of ``Stream``. You can use ``Stream`` directly to make custom stream classes via the ``define`` classmethod:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from holoviews import streams\n", "from holoviews.streams import Stream\n", "Angle = Stream.define('Angle', angle=0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here ``Angle`` is capitalized as it is a sub*class* of ``Stream`` with a numeric *angle* parameter, which has a default value of zero. You can verify this using ``hv.help``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hv.help(Angle)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can declare a ``DynamicMap`` where instead of specifying ``kdims``, we instantiate ``Angle`` with an ``angle`` of 45º and pass it to the ``streams`` parameter of the [``DynamicMap``](http://holoviews.org/reference/containers/bokeh/DynamicMap.html):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dmap=hv.DynamicMap(annotations, streams=[Angle(angle=45)])\n", "dmap.opts(opts.Box(color='green'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As expected, we see our ellipse with an angle of 45º as specified via the ``angle`` parameter of our ``Angle`` instance. In itself, this wouldn't be very useful but given that we have a handle on our [``DynamicMap``](http://holoviews.org/reference/containers/bokeh/DynamicMap.html) ``dmap``, we can now use the ``event`` method to update the ``angle`` parameter value and update the plot:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dmap.event(angle=90)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*When running this cell, the visualization above will jump to the 90º position!* If you have already run the cell, just change the value above and re-run, and you'll see the plot above update.\n", "\n", "This simple example shows how you can use the ``event`` method to update a visualization with any value you can generate in Python." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Regenerate the DynamicMap, initializing the angle to 15 degrees\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Use dmap.event to set the angle shown to 145 degrees.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Do not specify an initial angle so that the default value of 0 degrees is used.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Use the cell magic %%output backend='matplotlib' to try the above with matplotlib\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Declare a DynamicMap using annotations2 and AngleAndSize\n", "# Then use the event method to set the size to 1.5 and the angle to 30 degrees\n", "def annotations2(angle, size):\n", " radians = (angle / 180.) * np.pi\n", " return (hv.Box(0,0,4, orientation=np.pi/4) \n", " * hv.Ellipse(0,0,(size,size*2), orientation=radians) \n", " * hv.Text(0,0,'{0}º'.format(float(angle))))\n", "\n", "AngleAndSize = Stream.define('AngleAndSize', angle=0., size=1.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Periodic updates\n", "\n", "Using streams you can animate your visualizations by driving them with events from Python. Of course, you could use loops to call the ``event`` method, but this approach can queue up events much faster than they can be visualized. Instead of inserting sleeps into your loops to avoid that problem, it is recommended you use the ``periodic`` method, which lets you specify a time period between updates (in seconds):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dmap2=hv.DynamicMap(annotations, streams=[Angle(angle=0)])\n", "dmap2.opts(opts.Ellipse(color='orange'))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dmap2.periodic(0.01, count=180, timeout=8, param_fn=lambda i: {'angle':i})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you re-execute the above cell, you should see the preceding plot update continuously until the count value is reached." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Experiment with different period values. How fast can things update?\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Increase count so that the oval completes a full rotation.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Lower the timeout so the oval completes less than a quarter turn before stopping\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Linked streams\n", "\n", "Often, you will want to tie streams to specific user actions in the live JavaScript interface. There are no limitations on how you can generate updated stream parameter values in Python, and so you could manually support updating streams from JavaScript as long as it can communicate with Python to trigger an appropriate stream update. But as Python programmers, we would rather avoid writing JavaScript directly, so HoloViews supports the concept of *linked stream* classes where possible.\n", "\n", "Currently, linked streams are only supported by the Bokeh plotting extension, because only Bokeh executes JavaScript in the notebook and has a suitable event system necessary to enable linked streams (matplotlib displays output as static PNG or SVG in the browser). Here is a simple linked stream example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pointer = streams.PointerXY(x=0, y=0)\n", "\n", "def crosshair(x, y):\n", " return hv.Ellipse(0,0,1) * hv.HLine(y) * hv.VLine(x)\n", "\n", "hv.DynamicMap(crosshair, streams=[pointer])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When hovering in the plot above when backed by a live Python process, the crosshair will track the cursor. \n", "\n", "The way it works is very simple: the ``crosshair`` function puts a crosshair at whatever x,y location it is given, the ``pointer`` object supplies a stream of x,y values based on the mouse pointer location, and the ``DynamicMap`` object connects the pointer stream's x,y values to the ``crosshair`` function to generate the resulting plots." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Set the defaults so that the crosshair initializes at x=0.25, y=0.25\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercise: Copy the above example and adapt it to make a red point of size 10 follow your cursor (using hv.Points)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can view other similar examples of custom interactivity in our [reference gallery](http://holoviews.org/reference/index.html) and learn more about linked streams in the [user guide](http://holoviews.org/user_guide/Custom_Interactivity.html). Here is a quick summary of some of the more useful linked stream classes HoloViews currently offers and the parameters they supply:\n", "\n", "* ``PointerX/PointerY/PointerYX``: The x,y or (x,y) position of the cursor.\n", "* ``SingleTap/DoubleTap/Tap``: Position of single, double or all tap events.\n", "* ``BoundsX/BoundsY/BoundsXY``: The x,y or x and y extents selected with the Bokeh box select tool.\n", "* ``RangeX/RangeY/RangeXY``: The x,y or x and y range of the currently displayed axes\n", "* ``Selection1D``: The selected glyphs as a 1D selection.\n", "\n", "Any of these values can easily be tied to any visible element of your visualization." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A more advanced example\n", "\n", "Let's now build a more advanced example using the eclipse dataset we explored earlier, where the stream supplies values when a particular Bokeh tool (\"Box Select\") is active:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scatter_options = opts.Scatter(width=900, height=400, tools=['xbox_select'], cmap='RdBu', \n", " line_color='black', size=5, line_width=0.5, color=dim('latitude'),\n", " colorbar=True, colorbar_position='bottom', colorbar_opts={'title': 'Latitude'})\n", "\n", "eclipses = pd.read_csv('../data/eclipses_21C.csv', parse_dates=['date'])\n", "magnitudes = hv.Scatter(eclipses, kdims=['hour_local'], vdims=['magnitude','latitude'])\n", "\n", "def selection_example(index):\n", " text = '{0} eclipses'.format(len(index) if index else len(magnitudes)) \n", " return magnitudes.opts(scatter_options) * hv.Text(2,0.5, text)\n", "\n", "dmap3 = hv.DynamicMap(selection_example, streams=[streams.Selection1D()])\n", "dmap3.redim.label(magnitude='Eclipse Magnitude', hour_local='Hour (local time)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Try enabling the Box Select tool and then select a region of the plot.\n", "\n", "In a single code cell we have achieved quite a lot! We have:\n", "\n", "1. Loaded the data using pandas\n", "2. Turned this data into a Scatter of magnitude against local time\n", "3. Declared that an 'xbox_select' Bokeh tool should be used\n", "4. Used the ``Selection1D`` stream class to supply the points selected by the tool\n", "5. Declared a callback to show the number of selected eclipses as text\n", "6. Applied a colormap to the points encoding latitude and added a colorbar.\n", "7. Styled everything to our liking.\n", "\n", "# Onwards\n", "\n", "This visualization would be a good candidate for an online dashboard. We will see how you can deploy such visualizations using bokeh server in the [deploying bokeh apps](./11-deploying-bokeh-apps.ipynb) section, but first we will look at [Operations and Pipelines](./09_Operations_and_Pipelines.ipynb), which can be used to implement analyses or transformations while exploring a dataset." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 2 }