{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import param\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This example demonstrates how to use the Param library to express nested hierarchies of classes whose parameters can be edited in a GUI, without tying those classes to Panel or any other GUI framework.\n",
    "\n",
    "For this purpose we create a hierarchy of three classes that draw Bokeh plots. At the top level there is a ``ShapeViewer`` that allows selecting between different ``Shape`` classes. The Shape classes include a subobject controlling the ``Style`` (i.e. the `color` and `line_width`) of the drawn shapes. \n",
    "\n",
    "In each case, `param.depends` is used to indicate which parameter that computation depends on, either a parameter of this object (as for  `radius` below) or a parameter of a subobject (i.e., a parameter of one of this object's parameters, as for `style.color` below)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bokeh.plotting import figure\n",
    "\n",
    "class Style(param.Parameterized):\n",
    "    \n",
    "    color = param.Color(default='#0f6f0f')\n",
    "    \n",
    "    line_width = param.Number(default=2, bounds=(0, 10))\n",
    "\n",
    "\n",
    "class Shape(param.Parameterized):\n",
    "\n",
    "    radius = param.Number(default=1, bounds=(0, 1))\n",
    "    \n",
    "    style = param.Parameter(precedence=3)\n",
    "\n",
    "    def __init__(self, **params):\n",
    "        if 'style' not in params:\n",
    "            params['style'] = Style(name='Style')\n",
    "        super(Shape, self).__init__(**params)\n",
    "        self.figure = figure(x_range=(-1, 1), y_range=(-1, 1), width=400, height=400)\n",
    "        self.renderer = self.figure.line(*self._get_coords())\n",
    "        self._update_style()\n",
    "\n",
    "    @param.depends('style.color', 'style.line_width', watch=True)\n",
    "    def _update_style(self):\n",
    "        self.renderer.glyph.line_color = self.style.color\n",
    "        self.renderer.glyph.line_width = self.style.line_width\n",
    "\n",
    "    def _get_coords(self):\n",
    "        return [], []\n",
    "\n",
    "    def view(self):\n",
    "        return self.figure\n",
    "\n",
    "\n",
    "class Circle(Shape):\n",
    "\n",
    "    n = param.Integer(default=100, precedence=-1)\n",
    "    \n",
    "    def _get_coords(self):\n",
    "        angles = np.linspace(0, 2*np.pi, self.n+1)\n",
    "        return (self.radius*np.sin(angles),\n",
    "                self.radius*np.cos(angles))\n",
    "        \n",
    "    @param.depends('radius', watch=True)\n",
    "    def update(self):\n",
    "        xs, ys = self._get_coords()\n",
    "        self.renderer.data_source.data.update({'x': xs, 'y': ys})\n",
    "    \n",
    "class NGon(Circle):\n",
    "\n",
    "    n = param.Integer(default=3, bounds=(3, 10), precedence=1)\n",
    "\n",
    "    @param.depends('radius', 'n', watch=True)\n",
    "    def update(self):\n",
    "        xs, ys = self._get_coords()\n",
    "        self.renderer.data_source.data.update({'x': xs, 'y': ys})\n",
    "        \n",
    "        \n",
    "shapes = [NGon(name='NGon'), Circle(name='Circle')]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Having defined our basic domain model (of shapes in this case), we can now make a generic viewer using Panel without requiring or encoding information about the underlying domain objects.  Here, we define a `view` method that will be called whenever any of the possible parameters that a shape might have changes, without necessarily knowing what those are (as for `shape.param` below). That way, if someone adds a `Line` shape that has no `n` parameter but has `orientation`, the viewer should continue to work and be responsive. We can also depend on specific parameters (as for `shape.radius`) if we wish. Either way, the panel should then reactively update to each of the shape's parameters as they are changed in the browser:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "\n",
    "pn.extension()\n",
    "\n",
    "class ShapeViewer(param.Parameterized):\n",
    "    \n",
    "    shape = param.ObjectSelector(default=shapes[0], objects=shapes)\n",
    "    \n",
    "    @param.depends('shape', 'shape.param')\n",
    "    def view(self):\n",
    "        return self.shape.view()\n",
    "\n",
    "    @param.depends('shape', 'shape.radius')\n",
    "    def title(self):\n",
    "        return '## %s (radius=%.1f)' % (type(self.shape).__name__, self.shape.radius)\n",
    "    \n",
    "    def panel(self):\n",
    "        return pn.Column(self.title, self.view)\n",
    "    \n",
    "    \n",
    "# Instantiate and display ShapeViewer\n",
    "viewer = ShapeViewer()\n",
    "subpanel = pn.Column()\n",
    "\n",
    "pn.Row(\n",
    "    pn.Column(pn.Param(viewer.param, expand_layout=subpanel), subpanel),\n",
    "    viewer.panel(),\n",
    ").servable()"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
