{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Panel supports using parameters and dependencies between parameters as expressed by ``param`` in a simple way to encapsulate dashboards as declarative, self-contained classes.\n", "\n", "Parameters are Python attributes extended using the [Param library](https://github.com/holoviz/param) to support types, ranges, and documentation, which turns out to be just the information you need to automatically create widgets for each parameter.\n", "\n", "Additionally Panel provides support for linking parameters to the URL query string to allow parameterizing an app very easily.\n", "\n", "# Parameters and widgets\n", "\n", "To use this approach, first declare some Parameterized classes with various Parameters:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "import pandas as pd\n", "import datetime as dt\n", "\n", "class BaseClass(param.Parameterized):\n", " x = param.Parameter(default=3.14, doc=\"X position\")\n", " y = param.Parameter(default=\"Not editable\", constant=True)\n", " string_value = param.String(default=\"str\", doc=\"A string\")\n", " num_int = param.Integer(50000, bounds=(-200, 100000))\n", " unbounded_int = param.Integer(23)\n", " float_with_hard_bounds = param.Number(8.2, bounds=(7.5, 10))\n", " float_with_soft_bounds = param.Number(0.5, bounds=(0, None), softbounds=(0,2))\n", " unbounded_float = param.Number(30.01, precedence=0)\n", " hidden_parameter = param.Number(2.718, precedence=-1)\n", " integer_range = param.Range(default=(3, 7), bounds=(0, 10))\n", " float_range = param.Range(default=(0, 1.57), bounds=(0, 3.145))\n", " dictionary = param.Dict(default={\"a\": 2, \"b\": 9})\n", "\n", "\n", "class Example(BaseClass):\n", " \"\"\"An example Parameterized class\"\"\"\n", " timestamps = []\n", "\n", " boolean = param.Boolean(True, doc=\"A sample Boolean parameter\")\n", " color = param.Color(default='#FFFFFF')\n", " date = param.Date(dt.datetime(2017, 1, 1),\n", " bounds=(dt.datetime(2017, 1, 1), dt.datetime(2017, 2, 1)))\n", " dataframe = param.DataFrame(pd._testing.makeDataFrame().iloc[:3])\n", " select_string = param.ObjectSelector(default=\"yellow\", objects=[\"red\", \"yellow\", \"green\"])\n", " select_fn = param.ObjectSelector(default=list,objects=[list, set, dict])\n", " int_list = param.ListSelector(default=[3, 5], objects=[1, 3, 5, 7, 9], precedence=0.5)\n", " single_file = param.FileSelector(path='../../*/*.py*', precedence=0.5)\n", " multiple_files = param.MultiFileSelector(path='../../*/*.py?', precedence=0.5)\n", " record_timestamp = param.Action(lambda x: x.timestamps.append(dt.datetime.utcnow()), \n", " doc=\"\"\"Record timestamp.\"\"\", precedence=0.7)\n", " \n", "Example.num_int" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, declaring Parameters depends only on the separate Param library. Parameters are a simple idea with some properties that are crucial for helping you create clean, usable code:\n", "\n", "- The Param library is pure Python with no dependencies, which makes it easy to include in any code without tying it to a particular GUI or widgets library, or even to the Jupyter notebook. \n", "- Parameter declarations focus on semantic information relevant to your domain, allowing you to avoid polluting your domain-specific code with anything that ties it to a particular way of displaying or interacting with it. \n", "- Parameters can be defined wherever they make sense in your inheritance hierarchy, allowing you to document, type, and range-limit them _once_, with all of those properties inherited by any base class. E.g. parameters work the same here whether they were declared in `BaseClass` or `Example`, which makes it easy to provide this metadata once, and avoiding duplicating it throughout the code wherever ranges or types need checking or documentation needs to be stored.\n", "\n", "If you then decide to use these Parameterized classes in a notebook or web-server environment, you can `import panel` and easily display and edit the parameter values as an optional additional step:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "pn.extension()\n", "\n", "base = BaseClass()\n", "pn.Row(Example.param, base.param)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, Panel does not need to be provided with any knowledge of your domain-specific application, not even the names of your parameters; it simply displays widgets for whatever Parameters may have been defined on that object. Using Param with Panel thus achieves a nearly complete separation between your domain-specific code and your display code, making it vastly easier to maintain both of them over time. Here even the `msg` button behavior was specified declaratively, as an action that can be invoked (printing \"Hello\") independently of whether it is used in a GUI or other context.\n", "\n", "Interacting with the widgets above is only supported in the notebook and on Bokeh server, but you can also export static renderings of the widgets to a file or web page. \n", "\n", "By default, editing values in this way requires you to run the notebook cell by cell -- when you get to the above cell, edit the values as you like, and then move on to execute subsequent cells, where any reference to those parameter values will use your interactively selected setting:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Example.unbounded_int" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Example.num_int" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The reverse is possible; editing a parameter from Python will automatically update any widgets that were generated from the parameter:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Example.int_list = [1, 7]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Example.timestamps records the times you pressed the \"record timestamp\" button." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Example.timestamps" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Passing the ``.param`` object renders the full set of widgets, while passing a single parameter will display just one widget. In this way we can easily declare exactly which parameters to display:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Row(Example.param.float_range, Example.param.num_int)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, you can access the parameter values at the class level from within the notebook to control behavior explicitly, e.g. to select what to show in subsequent cells. Moreover, any instances of the Parameterized classes in your own code will now use the new parameter values unless specifically overridden in that instance, so you can now import and use your domain-specific library however you like, knowing that it will use your interactive selections wherever those classes appear. None of the domain-specific code needs to know or care that you used Panel; it will simply see new values for whatever attributes were changed interactively. Panel thus allows you to provide notebook-specific, domain-specific interactive functionality without ever tying your domain-specific code to the notebook environment. This default behavior can also be modified if you wish, as outlined below." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Custom widgets\n", "\n", "In the previous section we saw how parameters can automatically be turned into widgets. This is possible because internally Panel maintains a mapping between parameter types and widget types. However, sometimes the default widget does not provide the most convenient UI and we want to provide an explicit hint to Panel to tell it how to render a parameter. This is possible using the ``widgets`` argument to the `Param` pane. Using the ``widgets`` keyword we can declare a mapping between the parameter name and the type of widget that is desired (as long as the widget type supports the types of values held by the parameter type).\n", "\n", "As an example, we can map a string and a number Selector to a RadioButtonGroup and DiscretePlayer respectively." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class CustomExample(param.Parameterized):\n", " \"\"\"An example Parameterized class\"\"\"\n", "\n", " select_string = param.Selector(objects=[\"red\", \"yellow\", \"green\"])\n", " autocomplete_string = param.Selector(default='', objects=[\"red\", \"yellow\", \"green\"], check_on_set=False)\n", " select_number = param.Selector(objects=[0, 1, 10, 100])\n", "\n", "\n", "pn.Param(CustomExample.param, widgets={\n", " 'select_string': pn.widgets.RadioButtonGroup,\n", " 'autocomplete_string': pn.widgets.AutocompleteInput,\n", " 'select_number': pn.widgets.DiscretePlayer}\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Also, it's possible to pass arguments to the widget in order to customize it. Instead of passing the widget, pass a dictionary with the desired options. Use the ``widget_type`` keyword to map the widget. \n", "\n", "Taking up the previous example." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Param(CustomExample.param, widgets={\n", " 'select_string': {'widget_type': pn.widgets.RadioButtonGroup, 'button_type': 'success'},\n", " 'autocomplete_string': {'widget_type': pn.widgets.AutocompleteInput, 'placeholder': 'Find a color...'},\n", " 'select_number': pn.widgets.DiscretePlayer}\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However it is also possible to explicitly construct a widget from a parameter using the `.from_param` method, which makes it easy to override widget settings using keyword arguments:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.widgets.IntSlider.from_param(Example.param.unbounded_int, start=0, end=100)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Custom name\n", "\n", "By default, a param Pane has a title that is derived from the class name of its `Parameterized` object. Using the ``name`` keyword we can set any title to the pane, e.g. to improve the user interface." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Param(CustomExample.param, name=\"Custom Name\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Sort\n", "\n", "You can sort the widgets alphabetically by setting `sort=True`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Param(CustomExample.param, sort=True, name=\"Sort by Label Example\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also specify a custom sort function that takes the (parameter name, Parameter instance) as input." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def sort_func(x):\n", " return len(x[1].label)\n", "pn.Param(CustomExample.param, sort=sort_func, name=\"Sort by Label Length Example\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameter dependencies\n", "\n", "Declaring parameters is usually only the beginning of a workflow. In most applications these parameters are then tied to some computation. To express the relationship between a computation and the parameters it depends on, the ``param.depends`` decorator may be used on Parameterized methods. This decorator provides a hint to Panel and other Param-based libraries (e.g. HoloViews) that the method should be re-evaluated when a parameter changes.\n", "\n", "As a straightforward example without any additional dependencies we will write a small class that returns an ASCII representation of a sine wave, which depends on `phase` and `frequency` parameters. If we supply the ``.view`` method to a panel, it will automatically recompute and update the view when one or more of the parameters changes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "\n", "class Sine(param.Parameterized):\n", "\n", " phase = param.Number(default=0, bounds=(0, np.pi))\n", "\n", " frequency = param.Number(default=1, bounds=(0.1, 2))\n", "\n", " @param.depends('phase', 'frequency')\n", " def view(self):\n", " y = np.sin(np.linspace(0, np.pi * 3, 40) * self.frequency + self.phase)\n", " y = ((y - y.min()) / y.ptp()) * 20\n", " array = np.array(\n", " [list((' ' * (int(round(d)) - 1) + '*').ljust(20)) for d in y])\n", " return pn.pane.Str('\\n'.join([''.join(r) for r in array.T]), height=380, width=500)\n", "\n", "\n", "sine = Sine(name='ASCII Sine Wave')\n", "pn.Row(sine.param, sine.view)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The parameterized and annotated ``view`` method could return any one of the types handled by the [Pane objects](./Components.ipynb#Panes) Panel provides, making it easy to link parameters and their associated widgets to a plot or other output. Parameterized classes can therefore be a very useful pattern for encapsulating a part of a computational workflow with an associated visualization, declaratively expressing the dependencies between the parameters and the computation.\n", "\n", "By default, a Param pane will show widgets for all parameters with a `precedence` value above the value `pn.Param.display_threshold`, so you can use `precedence` to automatically hide parameters that are not meant to have widgets. You can also explicitly choose which parameters should have widgets in a given pane, by passing a `parameters` argument. For example, this code gives a `phase` widget, while maintaining `sine.frequency` at the initial value of `1`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.Row(pn.panel(sine.param, parameters=['phase']), sine.view)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Another common pattern is linking the values of one parameter to another parameter, e.g. when dependencies between parameters exist. In the example below we will define two parameters, one for the continent and one for the country. Since we want the selection of valid countries to change when we change the continent, we define a method to do that for us. In order to link the two we express the dependency using the ``param.depends`` decorator and then ensure that we will run the method whenever the continent changes by setting ``watch=True``.\n", "\n", "We also define a ``view`` method that returns an HTML iframe displaying the country using Google Maps." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class GoogleMapViewer(param.Parameterized):\n", " \n", " continent = param.ObjectSelector(default='Asia', objects=['Africa', 'Asia', 'Europe'])\n", " \n", " country = param.ObjectSelector(default='China', objects=['China', 'Thailand', 'Japan'])\n", " \n", " _countries = {'Africa': ['Ghana', 'Togo', 'South Africa', 'Tanzania'],\n", " 'Asia' : ['China', 'Thailand', 'Japan'],\n", " 'Europe': ['Austria', 'Bulgaria', 'Greece', 'Portugal', 'Switzerland']}\n", " \n", " @param.depends('continent', watch=True)\n", " def _update_countries(self):\n", " countries = self._countries[self.continent]\n", " self.param['country'].objects = countries\n", " self.country = countries[0]\n", "\n", " @param.depends('country')\n", " def view(self):\n", " iframe = \"\"\"\n", " \n", " \"\"\".format(country=self.country)\n", " return pn.pane.HTML(iframe, height=400)\n", " \n", "viewer = GoogleMapViewer(name='Google Map Viewer')\n", "pn.Row(viewer.param, viewer.view)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Whenever the continent changes Param will now eagerly execute the ``_update_countries`` method to change the list of countries that is displayed, which in turn triggers an update in the view method updating the map. Note that there is no need to add ``watch=True`` to decorators of methods that are passed to a Panel layout (e.g. ``viewer.View`` being passed to ``pn.Row`` here), because Panel will already handle dependencies on those methods, executing the method automatically when the dependent parameters change. Indeed, if you specify ``watch=True`` for such a method, the method will get invoked _twice_ each time a dependency changes (once by Param internally and once by Panel), so you should reserve ``watch=True`` only for methods that aren't otherwise being monitored for dependencies." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameter subobjects\n", "\n", "``Parameterized`` objects often have parameter values which are themselves ``Parameterized`` objects, forming a tree-like structure. Panel allows you to edit not just the main object's parameters but also lets you drill down to the subobject. Let us first define some classes declaring a hierarchy of Shape classes which draw a Bokeh plot of the selected shape:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.plotting import figure\n", "\n", "\n", "class Shape(param.Parameterized):\n", "\n", " radius = param.Number(default=1, bounds=(0, 1))\n", "\n", " def __init__(self, **params):\n", " super(Shape, self).__init__(**params)\n", " self.figure = figure(x_range=(-1, 1), y_range=(-1, 1))\n", " self.renderer = self.figure.line(*self._get_coords())\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", "\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})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have multiple Shape classes we can make instances of them and declare a ``ShapeViewer`` to select between them. We can also declare two methods with parameter dependencies, updating the plot and the plot title. The important thing to note here is that the ``param.depends`` decorator can not only depend on parameters on the object itself but also on specific parameters on the subobject, e.g. ``shape.radius``, or on all parameters of the subobject, expressed as ``shape.param``." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "shapes = [NGon(), Circle()]\n", "\n", "class ShapeViewer(param.Parameterized):\n", " \n", " shape = param.ObjectSelector(default=shapes[0], objects=shapes)\n", " \n", " @param.depends('shape')\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)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have a class with subobjects we can display it as usual. Three main options control how the subobject is rendered:\n", "\n", "* ``expand``: Whether the subobject is expanded on initialization (``default=False``).\n", "* ``expand_button``: Whether there should be a button to toggle expansion; otherwise it is fixed to the initial ``expand`` value (``default=True``).\n", "* ``expand_layout``: A layout type or instance to expand the plot into (``default=Column``).\n", "\n", "Let us start with the default view, which provides a toggle button to expand the subobject as desired:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "viewer = ShapeViewer()\n", "\n", "pn.Row(viewer.param, viewer.panel())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alternatively we can provide a completely separate ``expand_layout`` instance to the Param pane and request that it always remains expanded using the ``expand`` and ``expand_button`` option. This allows us to lay out the main widgets and the subobject's widgets separately:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "viewer = ShapeViewer()\n", "\n", "expand_layout = pn.Column()\n", "\n", "pn.Row(\n", " pn.Column(\n", " pn.panel(viewer.param, expand_button=False, expand=True, expand_layout=expand_layout),\n", " \"#### Subobject parameters:\",\n", " expand_layout),\n", " viewer.panel())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Syncing query parameters\n", "\n", "By default the current [query parameters](https://en.wikipedia.org/wiki/Query_string) in the URL (specified as a URL suffix such as `?color=blue`) are made available on `pn.state.location.query_params`. To make working with query parameters straightforward the `Location` object also provides a `sync` method which allows syncing query parameters with the parameters on a `Parameterized` object.\n", "\n", "We will start by defining a `Parameterized` class:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class QueryExample(param.Parameterized):\n", " \n", " integer = param.Integer(default=None, bounds=(0, 10))\n", " \n", " string = param.String(default='A string')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we will use the `pn.state.location` object to sync it with the URL query string (note that in a notebook environment `pn.state.location` is not initialized until the first plot has been displayed). The sync method takes the Parameterized object or instance to sync with as the first argument and a list or dictionary of the parameters as the second argument. If a dictionary is provided it should map from the Parameterized object's parameters to the query parameter name in the URL:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.state.location.sync(QueryExample, {'integer': 'int', 'string': 'str'})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now the Parameterized object is bi-directionally linked to the URL query parameter, if we set a query parameter in Python it will update the URL bar and when we specify a URL with a query parameter that will be set on the Parameterized, e.g. let us set the 'integer' parameter and watch the URL in your browser update:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "QueryExample.integer = 5" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note to unsync the Parameterized object you can simply call pn.state.location.unsync(QueryExample)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this user guide we have seen how to leverage Param to declare parameters, which Panel can then turn into a GUI with little additionl effort or code. We have also seen how to link parameters to views and to other parameters using the ``param.depends`` operator. This approach allows building complex and reactive panels. In the [pipelines user guide](Pipelines.ipynb) we can discover how to link multiple such classes into pipelines, making it possible to encapsulate complex workflows in clean self-contained classes." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }