{ "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), sizing_mode=\"stretch_width\", 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, sizing_mode=\"stretch_width\")\n", " \n", " \n", "# Instantiate and display ShapeViewer\n", "viewer = ShapeViewer()\n", "subpanel = pn.Column()\n", "\n", "component = pn.Row(\n", " pn.Column(pn.Param(viewer.param, expand_layout=subpanel, name=\"Shape Settings\"), subpanel),\n", " viewer.panel(),\n", ")\n", "component" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## App\n", "\n", "Lets wrap it into nice template that can be served via `panel serve param_subobjects.ipynb`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.template.FastListTemplate(site=\"Panel\", title=\"Param Sub Objects\", main=[ \n", " pn.pane.Markdown(\"This example demonstrates how to use the `Param` library to express **nested hierarchies of classes** whose parameters can be edited in Panel or any other GUI.\", sizing_mode=\"stretch_width\"), \n", " component,\n", "]).servable();" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }