{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Custom Interactivity"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import param\n",
    "import numpy as np\n",
    "import holoviews as hv\n",
    "from holoviews import opts\n",
    "\n",
    "hv.extension('bokeh')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In previous notebooks we discovered how the ``DynamicMap`` class allows us to declare objects in a lazy way to enable exploratory analysis of large parameter spaces. In the [Responding to Events](./12-Responding_to_Events.ipynb) guide we learned how to interactively push updates to existing plots by declaring Streams on a DynamicMap. In this user guide we will extend the idea to so called *linked* Streams, which allows complex interactions to be declared by specifying which events should be exposed when a plot is interacted with. By passing information about live interactions to a simple Python based callback, you will be able to build richer, even more interactive visualizations that enable seamless data exploration.\n",
    "\n",
    "Some of the possibilities this opens up include:\n",
    "\n",
    "* Dynamically aggregating datasets of billions of datapoints depending on the plot axis ranges using the [datashader](./15-Large_Data.ipynb) library.\n",
    "* Responding to ``Tap`` and ``DoubleTap`` events to reveal more information in subplots.\n",
    "* Computing statistics in response to selections applied with box- and lasso-select tools.\n",
    "\n",
    "Currently only the bokeh backend for HoloViews supports the linked streams system but the principles used should extend to any backend that can define callbacks that fire when a user zooms or pans or interacts with a plot.\n",
    "\n",
    "<center><div class=\"alert alert-info\" role=\"alert\">To use and visualize <b>DynamicMap</b> with linked <b>Stream</b> objects you need to be running a live Jupyter server.<br>This user guide assumes that it will be run in a live notebook environment.<br>\n",
    "When viewed statically, DynamicMaps on this page will only show the first available Element.<br></div></center>\n",
    "\n",
    "## Available Linked Streams\n",
    "\n",
    "There are a huge number of ways one might want to interact with a plot. The HoloViews streams module aims to expose many of the most common interactions you might want want to employ, while also supporting extensibility via custom linked Streams. \n",
    "\n",
    "Here is the full list of linked Stream that are all descendants of the ``LinkedStream`` baseclass:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from holoviews import streams\n",
    "listing = ', '.join(sorted([str(s.name) for s in param.descendents(streams.LinkedStream)]))\n",
    "print('The linked stream classes supported by HoloViews are:\\n\\n{listing}'.format(listing=listing))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, most of these events are about specific interactions with a plot such as the current axis ranges (the ``RangeX``, ``RangeY`` and ``RangeXY`` streams), the mouse pointer position (the ``PointerX``, ``PointerY`` and ``PointerXY`` streams), click or tap positions (``Tap``, ``DoubleTap``). Additionally there a streams to access plotting selections made using box- and lasso-select tools (``Selection1D``), the plot size (``PlotSize``) and the ``Bounds`` of a selection. Finally there are a number of drawing/editing streams such as ``BoxEdit``, ``PointDraw``, ``FreehandDraw``, ``PolyDraw`` and ``PolyEdit``.\n",
    "\n",
    "Each of these linked Stream types has a corresponding backend specific ``Callback``, which defines which plot attributes or events to link the stream to and triggers events on the ``Stream`` in response to changes on the plot. Defining custom ``Stream`` and ``Callback`` types will be covered in future guides."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Linking streams to plots\n",
    "\n",
    "At the end of the [Responding to Events](./12-Responding_to_Events.ipynb) guide we discovered that streams have ``subscribers``, which allow defining user defined callbacks on events, but also allow HoloViews to install subscribers that let plots respond to Stream updates. Linked streams add another concept on top of ``subscribers``, namely the Stream ``source``.\n",
    "\n",
    "The source of a linked stream defines which plot element to receive events from. Any plot containing the ``source`` object will be attached to the corresponding linked stream and will send event values in response to the appropriate interactions.\n",
    "\n",
    "Let's start with a simple example. We will declare one of the linked Streams from above, the ``PointerXY`` stream. This stream sends the current mouse position in plot axes coordinates, which may be continuous or categorical. The first thing to note is that we haven't specified a ``source`` which means it uses the default value of ``None``."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer = streams.PointerXY()\n",
    "print(pointer.source)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before continuing, we can check the stream parameters that are made available to user callbacks from a given stream instance by looking at its contents:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('The %s stream has contents %r' % (pointer, pointer.contents))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Automatic linking"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A stream instance is automatically linked to the first ``DynamicMap`` we pass it to, which we can confirm by inspecting the stream's ``source`` attribute after supplying it to a ``DynamicMap``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_dmap = hv.DynamicMap(lambda x, y: hv.Points([(x, y)]), streams=[pointer])\n",
    "print(pointer.source is pointer_dmap)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The ``DynamicMap`` we defined above simply defines returns a ``Points`` object composed of a single point that marks the current ``x`` and ``y`` position supplied by our ``PointerXY`` stream. The stream is linked whenever this ``DynamicMap`` object is displayed as it is the stream source:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_dmap.opts(size=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you hover over the plot canvas above you can see that the point tracks the current mouse position. We can also inspect the last cursor position by examining the stream contents:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer.contents"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the [Responding to Events](12-Responding_to_Events.ipynb) user guide, we introduced an integration example that would work more intuitively with linked streams. Here it is again with the ``limit`` value controlled by the ``PointerX`` linked stream:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xs = np.linspace(-3, 3, 400)\n",
    "\n",
    "def function(xs, time):\n",
    "    \"Some time varying function\"\n",
    "    return np.exp(np.sin(xs+np.pi/time))\n",
    "\n",
    "def integral(limit, time):\n",
    "    limit = -3 if limit is None else np.clip(limit,-3,3)\n",
    "    curve = hv.Curve((xs, function(xs, time)))[limit:]\n",
    "    area  = hv.Area ((xs, function(xs, time)))[:limit]\n",
    "    summed = area.dimension_values('y').sum() * 0.015  # Numeric approximation\n",
    "    return (area * curve * hv.VLine(limit) * hv.Text(limit + 0.8, 2.0, '%.2f' % summed))\n",
    "\n",
    "integral_streams = [\n",
    "    streams.Stream.define('Time', time=1.0)(),\n",
    "    streams.PointerX().rename(x='limit')]\n",
    "\n",
    "integral_dmap = hv.DynamicMap(integral, streams=integral_streams)\n",
    "\n",
    "integral_dmap.opts(\n",
    "    opts.Area(color='#fff8dc', line_width=2),\n",
    "    opts.Curve(color='black'),\n",
    "    opts.VLine(color='red'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We only needed to import and use the ``PointerX`` stream and rename the ``x`` parameter that tracks the cursor position to 'limit' so that it maps to the corresponding argument. Otherwise, the example only required bokeh specific style options to match the matplotlib example as closely as possible."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Explicit linking\n",
    "\n",
    "In the example above, we took advantage of the fact that a ``DynamicMap`` automatically becomes the stream source if a source isn't explicitly specified. If we want to link the stream instance to a different object we can specify our source explicitly. Here we will create a 2D ``Image`` of sine gratings, and then declare that this image is the ``source`` of the ``PointerXY`` stream. This pointer stream is then used to generate a single point that tracks the cursor when hovering over the image:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xvals = np.linspace(0,4,202)\n",
    "ys,xs = np.meshgrid(xvals, -xvals[::-1])\n",
    "img = hv.Image(np.sin(((ys)**3)*xs))\n",
    "\n",
    "pointer = streams.PointerXY(x=0,y=0, source=img)\n",
    "pointer_dmap = hv.DynamicMap(lambda x, y: hv.Points([(x, y)]), streams=[pointer])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now if we display a ``Layout`` consisting of the ``Image`` acting as the source together with the ``DynamicMap``, the point shown on the right tracks the cursor position when hovering over the image on the left:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "img + pointer_dmap.opts(size=10, xlim=(-.5, .5), ylim=(-.5, .5))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This will even work across different cells. If we use this particular stream instance in another ``DynamicMap`` and display it, this new visualization will also be supplied with the cursor position when hovering over the image. \n",
    "\n",
    "To illustrate this, we will now use the pointer ``x`` and ``y`` position to generate cross-sections of the image at the cursor position on the ``Image``, making use of the ``Image.sample`` method. Note the use of ``np.clip`` to make sure the cross-section is well defined when the cusor goes out of bounds:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x_sample = hv.DynamicMap(lambda x, y: img.sample(x=np.clip(x,-.49,.49)), streams=[pointer])\n",
    "y_sample = hv.DynamicMap(lambda x, y: img.sample(y=np.clip(y,-.49,.49)), streams=[pointer])\n",
    "\n",
    "(x_sample + y_sample).opts(opts.Curve(framewise=True))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now when you hover over the ``Image`` above, you will see the cross-sections update while the point position to the right of the ``Image`` simultaneously updates."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Unlinking objects"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sometimes we just want to display an object designated as a source without linking it to the stream. If the object is not a ``DynamicMap``, like the ``Image`` we designated as a ``source`` above, we can make a copy of the object using the ``clone`` method. We can do the same with ``DynamicMap`` though we just need to supply ``link_inputs=False`` as an extra argument.\n",
    "\n",
    "Here we will create a ``DynamicMap`` that draws a cross-hair at the cursor position:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer = streams.PointerXY(x=0, y=0)\n",
    "cross_dmap = hv.DynamicMap(lambda x, y: (hv.VLine(x) * hv.HLine(y)), streams=[pointer])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we will add two copies of the ``cross_dmap`` into a Layout but the subplot on the right will not be linking the inputs. Try hovering over the two subplots and observe what happens:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cross_dmap + cross_dmap.clone(link=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice how hovering over the left plot updates the crosshair position on both subplots, while hovering over the right subplot has no effect."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Transient linked streams"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the basic  [Responding to Events](12-Responding_to_Events.ipynb) user guide we saw that stream parameters can be updated and those values are then passed to the callback. This model works well for many different types of streams that have well-defined values at all times.\n",
    "\n",
    "This approach is not suitable for certain events which only have a well defined value at a particular point in time. For instance, when you hover your mouse over a plot, the hover position always has a well-defined value but the click position is only defined when a click occurs (if it occurs).\n",
    "\n",
    "This latter case is an example of what are called 'transient' streams. These streams are supplied new values only when they occur and fall back to a default value at all other times. This default value is typically ``None`` to indicate that the event is not occurring and therefore has no data.\n",
    "\n",
    "\n",
    "Transient streams are particularly useful when you are subscribed to multiple streams, some of which are only occasionally triggered. A good example are the ``Tap`` and ``DoubleTap`` streams; while you sometimes just want to know the last tapped position, we can only tell the two events apart if their values are ``None`` when not active. \n",
    "\n",
    "We'll start by declaring a ``SingleTap`` and a ``DoubleTap`` stream as ``transient``. Since both streams supply 'x' and 'y' parameters, we will rename the ``DoubleTap`` parameters to 'x2' and 'y2'."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tap = streams.SingleTap(transient=True)\n",
    "double_tap = streams.DoubleTap(rename={'x': 'x2', 'y': 'y2'}, transient=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we define a list of taps we can append to, and a function that accumulates the tap and double tap coordinates along with the number of taps, returning a ``Points`` Element of the tap positions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "taps = []\n",
    "\n",
    "def record_taps(x, y, x2, y2):\n",
    "    if None not in [x,y]:\n",
    "        taps.append((x, y, 1))\n",
    "    elif None not in [x2, y2]:\n",
    "        taps.append((x2, y2, 2))\n",
    "    return hv.Points(taps, vdims='Taps')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally we can create a ``DynamicMap`` from our callback and attach the streams. We also apply some styling so the points are colored depending on the number of taps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "taps_dmap = hv.DynamicMap(record_taps, streams=[tap, double_tap])\n",
    "\n",
    "taps_dmap.opts(color='Taps', cmap={1: 'red', 2: 'gray'}, size=10, tools=['hover'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now try single- and double-tapping within the plot area, each time you tap a new point is appended to the list and displayed. Single taps show up in red and double taps show up in grey.  We can also inspect the list of taps directly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "taps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pop-up panes\n",
    "\n",
    "Sometimes, you might want to display additional info, next to the selection, as a floating pane.\n",
    "\n",
    "To do this, specify `popup`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "points = hv.Points(np.random.randn(1000, 2))\n",
    "\n",
    "hv.streams.BoundsXY(source=points, popup=\"Used Box Select\")\n",
    "hv.streams.Lasso(source=points, popup=\"Used Lasso Select\")\n",
    "hv.streams.Tap(source=points, popup=\"Used Tap\")\n",
    "\n",
    "points.opts(\n",
    "    tools=[\"box_select\", \"lasso_select\", \"tap\"],\n",
    "    active_tools=[\"lasso_select\"],\n",
    "    size=6,\n",
    "    color=\"black\",\n",
    "    fill_color=None,\n",
    "    width=500,\n",
    "    height=500\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "An applicable example is using the `popup` to show stats of the selected points."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def popup_stats(index):\n",
    "    if not index:\n",
    "        return\n",
    "    return points.iloc[index].dframe().describe()\n",
    "\n",
    "\n",
    "points = hv.Points(np.random.randn(1000, 2))\n",
    "\n",
    "hv.streams.Selection1D(\n",
    "    source=points,\n",
    "    popup=popup_stats\n",
    "\n",
    ")\n",
    "\n",
    "points.opts(\n",
    "    tools=[\"box_select\", \"lasso_select\", \"tap\"],\n",
    "    active_tools=[\"lasso_select\"],\n",
    "    size=6,\n",
    "    color=\"black\",\n",
    "    fill_color=None,\n",
    "    width=500,\n",
    "    height=500\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The contents of the `popup` can be another HoloViews object too, like the distribution of the selected points."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def popup_distribution(index):\n",
    "    x, y = points.iloc[index].data.T\n",
    "    return hv.Distribution((x, y)).opts(\n",
    "        width=100,\n",
    "        height=100,\n",
    "        toolbar=None,\n",
    "        yaxis=\"bare\",\n",
    "        xlabel=\"\",\n",
    "        xticks=[-1, 0, 1],\n",
    "        xlim=(-2, 2),\n",
    "    )\n",
    "\n",
    "\n",
    "points = hv.Points(np.random.randn(1000, 2))\n",
    "\n",
    "hv.streams.Selection1D(\n",
    "    source=points,\n",
    "    popup=popup_distribution,\n",
    ")\n",
    "\n",
    "points.opts(\n",
    "    tools=[\"box_select\", \"lasso_select\", \"tap\"],\n",
    "    active_tools=[\"lasso_select\"],\n",
    "    size=6,\n",
    "    color=\"black\",\n",
    "    fill_color=None,\n",
    "    width=500,\n",
    "    height=500\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It can also be a object or any component that can be rendered with Panel, which is an open-source Python library built on top of Bokeh, with a variety of easy-to-use [widgets and panes](https://panel.holoviz.org/reference/index.html#), such as [`Image`](https://panel.holoviz.org/reference/panes/Image.html), [`Button`](https://panel.holoviz.org/reference/widgets/Button.html), [`TextInput`](https://panel.holoviz.org/reference/widgets/TextInput.html), and much more!\n",
    "\n",
    "To control the visibility of the `popup`, update `visible` parameter of the provided component."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "pn.extension()\n",
    "\n",
    "def popup_form(index):\n",
    "    def hide_popup(_):\n",
    "        layout.visible = False\n",
    "\n",
    "    if not index:\n",
    "        return\n",
    "    df = points.iloc[index].dframe().describe()\n",
    "    button = pn.widgets.Button(name=\"Close\", sizing_mode=\"stretch_width\")\n",
    "    layout = pn.Column(button, df)\n",
    "    button.on_click(hide_popup)\n",
    "    return layout\n",
    "\n",
    "\n",
    "points = hv.Points(np.random.randn(1000, 2))\n",
    "hv.streams.Selection1D(source=points, popup=popup_form)\n",
    "\n",
    "points.opts(\n",
    "    tools=[\"box_select\", \"lasso_select\", \"tap\"],\n",
    "    active_tools=[\"lasso_select\"],\n",
    "    size=6,\n",
    "    color=\"black\",\n",
    "    fill_color=None,\n",
    "    width=500,\n",
    "    height=500\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
