{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The below probably needs some more explanation, but it's a starting point."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Changing Element behaviour, or adding custom Elements, in Holoviews"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "File paths are always given starting from the `holoviews/` top level."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are (at least) three files one needs to change for a new Element:\n",
    "\n",
    "1. The `Element`s, e.g. `Points`, themselves are defined in the modules in `element/`. These are backend-independent.\n",
    "`ElementConversion`s can be put into `__init__.py`.\n",
    "\n",
    "1. `plotting/*`'s submodules hold the various plotting routines. These are backend-specific as they specify how the data are transformed and eventually handled by the backend. Here (or most likely in one of the parent classes) you'll the concrete statements with which holoviews draws e.g. into matplotlib axes. \n",
    "    \n",
    "1. `plotting/*/__init__.py` holds the `Store.register()` statement, such that Holoviews knows which `Element` belongs to which `*Plot` routine.\n",
    "\n",
    "For changing an existing Element, number 2. is sufficient."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`*Plot` can have a number of methods. Some of the most important that I've so far come across are:\n",
    "\n",
    "1. `get_data`\n",
    "1. `_init_glyph`\n",
    "1. `get_extents`\n",
    "\n",
    "These are frequently overwritten when subclassing an existing plotting class."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Parameters to the operation are defined in the class as my_parameter param.[type]; and can be accessed in any method as self.p.my_parameter."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`_style_groups` is a `dict` where keys are Bokeh glyphs and values are what the respective \"part\" of the Element is called in the return of the `get_data` method."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# In action:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To see all of this in action, you may have a look at how I implemented the Segments element in the bokeh backend:\n",
    "\n",
    "1. First, define `SegmentPlot` (this is bokeh-specific).\n",
    "2. Then, define `Segments`, which is backend-agnostic\n",
    "3. Associate the two and register them into the backend: `hv.Store.register({Segments: SegmentPlot}, 'bokeh')` "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import param\n",
    "import holoviews as hv\n",
    "\n",
    "from holoviews.plotting.bokeh.chart import line_properties, fill_properties\n",
    "from holoviews.plotting.bokeh.element import ColorbarPlot\n",
    "from holoviews.element.geom import Geometry\n",
    "from holoviews.core.util import max_range\n",
    "\n",
    "class SegmentPlot(ColorbarPlot):\n",
    "    \"\"\"\n",
    "    Segments are lines in 2D space where each two each dimensions specify a\n",
    "    (x, y) node of the line.\n",
    "    \"\"\"\n",
    "    style_opts = line_properties + ['cmap']\n",
    "\n",
    "    _nonvectorized_styles = ['cmap']\n",
    "\n",
    "    _plot_methods = dict(single='segment')\n",
    "\n",
    "    def get_data(self, element, ranges, style):\n",
    "        # Get [x0, y0, x1, y1]\n",
    "        x0idx, y0idx, x1idx, y1idx = (\n",
    "            (1, 0, 3, 2) if self.invert_axes else (0, 1, 2, 3)\n",
    "        )\n",
    "\n",
    "        # Compute segments\n",
    "        x0s, y0s, x1s, y1s = (\n",
    "            element.dimension_values(x0idx),\n",
    "            element.dimension_values(y0idx),\n",
    "            element.dimension_values(x1idx),\n",
    "            element.dimension_values(y1idx)\n",
    "        )\n",
    "\n",
    "        data = {'x0': x0s, 'x1': x1s, 'y0': y0s, 'y1': y1s}\n",
    "        mapping = dict(x0='x0', x1='x1', y0='y0', y1='y1')\n",
    "        return (data, mapping, style)\n",
    "\n",
    "    def get_extents(self, element, ranges, range_type='combined'):\n",
    "        \"\"\"\n",
    "        Use first two key dimensions to set names, and all four\n",
    "        to set the data range.\n",
    "        \"\"\"\n",
    "        kdims = element.kdims\n",
    "        # loop over start and end points of segments\n",
    "        # simultaneously in each dimension\n",
    "        for kdim0, kdim1 in zip([kdims[i].name for i in range(2)],\n",
    "                                [kdims[i].name for i in range(2,4)]):\n",
    "            new_range = {}\n",
    "            for kdim in [kdim0, kdim1]:\n",
    "                # for good measure, update ranges for both start and end kdim\n",
    "                for r in ranges[kdim]:\n",
    "                    # combine (x0, x1) and (y0, y1) in range calculation\n",
    "                    new_range[r] = max_range([ranges[kd][r]\n",
    "                                              for kd in [kdim0, kdim1]])\n",
    "            ranges[kdim0] = new_range\n",
    "            ranges[kdim1] = new_range\n",
    "        return super(SegmentPlot, self).get_extents(element, ranges, range_type)\n",
    "\n",
    "\n",
    "\n",
    "class Segments(Geometry):\n",
    "    \"\"\"\n",
    "    Segments represent a collection of lines in 2D space.\n",
    "    \"\"\"\n",
    "    group = param.String(default='Segments', constant=True)\n",
    "\n",
    "    kdims = param.List(default=[Dimension('x0'), Dimension('y0'),\n",
    "                                Dimension('x1'), Dimension('y1')],\n",
    "                       bounds=(4, 4), constant=True, doc=\"\"\"\n",
    "        Segments represent lines given by x- and y-\n",
    "        coordinates in 2D space.\"\"\")\n",
    "\n",
    "\n",
    "hv.Store.register({Segments: SegmentPlot}, 'bokeh')\n",
    "hv.Store.set_current_backend('bokeh')"
   ]
  }
 ],
 "metadata": {
  "kernel_info": {
   "name": "python3"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "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.6.7"
  },
  "nteract": {
   "version": "0.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}