{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import panel as pn\n", "\n", "pn.extension()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One of the main design goals for Panel was that it should make it possible to seamlessly transition back and forth between interactively prototyping a dashboard in the notebook or on the commandline to deploying it as a standalone server app. This section shows how to display panels interactively, embed static output, save a snapshot, and deploy as a separate web-server app. For more information about deploying Panel apps to various cloud providers see the [Server Deployment](Server_Deployment.ipynb) documentation.\n", "\n", "## Configuring output\n", "\n", "As you may have noticed, almost all the Panel documentation is written using notebooks. Panel objects display themselves automatically in a notebook and take advantage of Jupyter Comms to support communication between the rendered app and the Jupyter kernel that backs it on the Python end. To display a Panel object in the notebook is as simple as putting it on the end of a cell. Note, however, that the ``panel.extension`` first has to be loaded to initialize the required JavaScript in the notebook context. In recent versions of JupyterLab this works out of the box but for older versions (`<3.0`) the PyViz labextension has to be installed with:\n", "\n", " jupyter labextension install @pyviz/jupyterlab_pyviz\n", "\n", "### Optional dependencies\n", "\n", "Also remember that in order to use certain components such as Vega, LaTeX, and Plotly plots in a notebook, the models must be loaded using the extension. If you forget to load the extension, you should get a warning reminding you to do it. To load certain JS components, simply list them as part of the call to ``pn.extension``:\n", "\n", " pn.extension('vega', 'katex')\n", "\n", "Here we've ensured that the Vega and LaTeX JS dependencies will be loaded.\n", "\n", "### Initializing JS and CSS \n", "\n", "Additionally, any external ``css_files``, ``js_files`` and ``raw_css`` needed should be declared in the extension. The ``js_files`` should be declared as a dictionary mapping from the exported JS module name to the URL containing the JS components, while the ``css_files`` can be defined as a list:\n", "\n", " pn.extension(js_files={'deck': https://unpkg.com/deck.gl@~5.2.0/deckgl.min.js},\n", " css_files=['https://api.tiles.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.css'])\n", "\n", "The ``raw_css`` argument allows defining a list of strings containing CSS to publish as part of the notebook and app.\n", "\n", "Providing keyword arguments via the ``extension`` is the same as setting them on ``pn.config``, which is the preferred approach outside the notebook. ``js_files`` and ``css_files`` may be set to your chosen values as follows:\n", "\n", " pn.config.js_files = {'deck': 'https://unpkg.com/deck.gl@~5.2.0/deckgl.min.js'}\n", " pn.config.css_files = ['https://api.tiles.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.css']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Display in the notebook\n", "\n", "#### The repr\n", " \n", "Once the extension is loaded, Panel objects will display themselves if placed at the end of cell in the notebook:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pane = pn.panel('Here is some custom HTML')\n", "\n", "pane" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To instead see a textual representation of the component, you can use the ``pprint`` method on any Panel object:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pane.pprint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### The ``display`` function\n", "\n", "To avoid having to put a Panel on the last line of a notebook cell, e.g. to display it from inside a function call, you can use the IPython built-in ``display`` function:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def display_marquee(text):\n", " display(pn.panel('{text}'.format(text=text)))\n", " \n", "display_marquee('This Panel was displayed from within a function')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Inline apps\n", "\n", "Lastly it is also possible to display a Panel object as a Bokeh server app inside the notebook. To do so call the ``.app`` method on the Panel object and provide the URL of your notebook server:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pane.app('localhost:8888')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The app will now run on a Bokeh server instance separate from the Jupyter notebook kernel, allowing you to quickly test that all the functionality of your app works both in a notebook and in a server context." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ipywidgets\n", "\n", "If the `jupyter_bokeh` package is installed it is also possible to render Panel objects as an ipywidget rather than using Bokeh's internal communication mechanisms. You can enable ipywidgets support globally using:\n", "\n", "```python\n", "pn.extension(comms='ipywidgets')\n", "# or\n", "pn.config.comms = 'ipywidgets'\n", "```\n", "\n", "This global setting can be useful when trying to serve an entire notebook using [VoilĂ ](https://github.com/voila-dashboards/voila). Alternatively, we can convert individual objects to an ipywidget one at a time using the `pn.ipywidget()` function:\n", "\n", "```python\n", "ipywidget = pn.ipywidget(pane)\n", "ipywidget\n", "```\n", "\n", "This approach also allows combining a Panel object with any other Jupyter-widget--based model:\n", "\n", "```python\n", "from ipywidgets import Accordion\n", "Accordion(children=[pn.ipywidget(pane)])\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To use Panel's ipywidgets support in JupyterLab, the following extensions have to be installed:\n", " \n", "```\n", "jupyter labextension install @jupyter-widgets/jupyterlab-manager\n", "jupyter labextension install @bokeh/jupyter_bokeh\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Additionally the `jupyter_bokeh` package should be installed using either pip:\n", "\n", "```\n", "pip install jupyter_bokeh\n", "```\n", "\n", "or using conda:\n", "\n", "```\n", "conda install -c bokeh jupyter_bokeh\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Launching a server dynamically\n", "\n", "The CLI `panel serve` command described below is usually the best approach for deploying applications. However when working on the REPL or embedding a Panel/Bokeh server in another application it is sometimes useful to dynamically launch a server, either using the `.show` method or using the `pn.serve` function.\n", "\n", "### Previewing an application\n", "\n", "Working from the command line will not automatically display rich representations inline as in a notebook, but you can still interact with your Panel components if you start a Bokeh server instance and open a separate browser window using the ``show`` method. The method has the following arguments:\n", "\n", " port: int (optional)\n", " Allows specifying a specific port (default=0 chooses an arbitrary open port)\n", " websocket_origin: str or list(str) (optional)\n", " A list of hosts that can connect to the websocket.\n", " This is typically required when embedding a server app in\n", " an external-facing web site.\n", " If None, \"localhost\" is used.\n", " threaded: boolean (optional, default=False)\n", " Whether to launch the Server on a separate thread, allowing\n", " interactive use.\n", " title : str\n", " A string title to give the Document (if served as an app)\n", " **kwargs : dict\n", " Additional keyword arguments passed to the bokeh.server.server.Server instance.\n", "\n", "To work with an app completely interactively you can set ``threaded=True`` which will launch the server on a separate thread and let you interactively play with the app.\n", "\n", "\n", "\n", "The ``.show`` call will return either a Bokeh server instance (if ``threaded=False``) or a ``StoppableThread`` instance (if ``threaded=True``) which both provide a ``stop`` method to stop the server instance." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Serving multiple apps\n", "\n", "If you want to serve more than one app on a single server you can use the ``pn.serve`` function. By supplying a dictionary where the keys represent the URL slugs and the values must be either Panel objects or functions returning Panel objects you can easily launch a server with a number of apps, e.g.:\n", "\n", "```python\n", "pn.serve({\n", " 'markdown': '# This is a Panel app',\n", " 'json': pn.pane.JSON({'abc': 123})\n", "})\n", "```\n", "\n", "Note that when you serve an object directly all sessions will share the same state, i.e. the parameters of all components will be synced across sessions such that the change in a widget by one user will affect all other users. Therefore you will usually want to wrap your app in a function, ensuring that each user gets a new instance of the application:\n", "\n", "```python\n", "\n", "def markdown_app():\n", " return '# This is a Panel app'\n", "\n", "def json_app():\n", " return pn.pane.JSON({'abc': 123})\n", "\n", "pn.serve({\n", " 'markdown': markdown_app,\n", " 'json': json_app\n", "})\n", "```\n", "\n", "You can customize the HTML title of each application by supplying a dictionary where the keys represent the URL slugs and the values represent the titles, e.g.:\n", "\n", "```python\n", "pn.serve({\n", " 'markdown': '# This is a Panel app',\n", " 'json': pn.pane.JSON({'abc': 123})\n", "}, title={'markdown': 'A Markdown App', 'json': 'A JSON App'}\n", ")\n", "```\n", "\n", "The ``pn.serve`` accepts a number of arguments:\n", "\n", " panel: Viewable, function or {str: Viewable or function}\n", " A Panel object, a function returning a Panel object or a\n", " dictionary mapping from the URL slug to either.\n", " port: int (optional, default=0)\n", " Allows specifying a specific port\n", " address: str\n", " The address the server should listen on for HTTP requests.\n", " websocket_origin: str or list(str) (optional)\n", " A list of hosts that can connect to the websocket.\n", "\n", " This is typically required when embedding a server app in\n", " an external web site.\n", "\n", " If None, \"localhost\" is used.\n", " loop: tornado.ioloop.IOLoop (optional, default=IOLoop.current())\n", " The tornado IOLoop to run the Server on\n", " show: boolean (optional, default=False)\n", " Whether to open the server in a new browser tab on start\n", " start: boolean(optional, default=False)\n", " Whether to start the Server\n", " title: str or {str: str} (optional, default=None)\n", " An HTML title for the application or a dictionary mapping\n", " from the URL slug to a customized title\n", " verbose: boolean (optional, default=True)\n", " Whether to print the address and port\n", " location: boolean or panel.io.location.Location\n", " Whether to create a Location component to observe and\n", " set the URL location.\n", " kwargs: dict\n", " Additional keyword arguments to pass to Server instance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Launching a server on the commandline\n", "\n", "Once the app is ready for deployment it can be served using the Bokeh server. For a detailed breakdown of the design and functionality of Bokeh server, see the [Bokeh documentation](https://bokeh.pydata.org/en/latest/docs/user_guide/server.html). The most important thing to know is that Panel (and Bokeh) provide a CLI command to serve a Python script, app directory, or Jupyter notebook containing a Bokeh or Panel app. To launch a server using the CLI, simply run:\n", "\n", " panel serve app.ipynb\n", " \n", "Alternatively you can also list multiple apps:\n", "\n", " panel serve app1.py app2.ipynb\n", " \n", "or even serve a number of apps at once:\n", "\n", " panel serve apps/*.py\n", " \n", "For development it can be particularly helpful to use the ``--autoreload`` option to `panel serve` as that will automatically reload the page whenever the application code or any of its imports change.\n", " \n", "The ``panel serve`` command has the following options:\n", "\n", " positional arguments:\n", " DIRECTORY-OR-SCRIPT The app directories or scripts or notebooks to serve \n", " (serve empty document if not specified)\n", "\n", " optional arguments:\n", " -h, --help show this help message and exit\n", " --port PORT Port to listen on\n", " --address ADDRESS Address to listen on\n", " --log-level LOG-LEVEL\n", " One of: trace, debug, info, warning, error or critical\n", " --log-format LOG-FORMAT\n", " A standard Python logging format string (default:\n", " '%(asctime)s %(message)s')\n", " --log-file LOG-FILE A filename to write logs to, or None to write to the\n", " standard stream (default: None)\n", " --args ... Any command line arguments remaining are passed on to\n", " the application handler\n", " --show Open server app(s) in a browser\n", " --allow-websocket-origin HOST[:PORT]\n", " Public hostnames which may connect to the Bokeh\n", " websocket\n", " --prefix PREFIX URL prefix for Bokeh server URLs\n", " --keep-alive MILLISECONDS\n", " How often to send a keep-alive ping to clients, 0 to\n", " disable.\n", " --check-unused-sessions MILLISECONDS\n", " How often to check for unused sessions\n", " --unused-session-lifetime MILLISECONDS\n", " How long unused sessions last\n", " --stats-log-frequency MILLISECONDS\n", " How often to log stats\n", " --mem-log-frequency MILLISECONDS\n", " How often to log memory usage information\n", " --use-xheaders Prefer X-headers for IP/protocol information\n", " --auth-module AUTH_MODULE\n", " Absolute path to a Python module that implements auth hooks\n", " --enable-xsrf-cookies\n", " Whether to enable Tornado support for XSRF cookies.\n", " All PUT, POST, or DELETE handlers must be properly\n", " instrumented when this setting is enabled.\n", " --exclude-headers EXCLUDE_HEADERS [EXCLUDE_HEADERS ...]\n", " A list of request headers to exclude from the session\n", " context (by default all headers are included).\n", " --exclude-cookies EXCLUDE_COOKIES [EXCLUDE_COOKIES ...]\n", " A list of request cookies to exclude from the session\n", " context (by default all cookies are included).\n", " --include-headers INCLUDE_HEADERS [INCLUDE_HEADERS ...]\n", " A list of request headers to make available in the\n", " session context (by default all headers are included).\n", " --include-cookies INCLUDE_COOKIES [INCLUDE_COOKIES ...]\n", " A list of request cookies to make available in the\n", " session context (by default all cookies are included).\n", " --session-ids MODE One of: unsigned, signed, or external-signed\n", " --index INDEX Path to a template to use for the site index or\n", " an app to serve at the root.\n", " --disable-index Do not use the default index on the root path\n", " --disable-index-redirect\n", " Do not redirect to running app from root path\n", " --num-procs N Number of worker processes for an app. Using 0 will\n", " autodetect number of cores (defaults to 1)\n", " --num-threads N Number of threads to launch in a ThreadPoolExecutor which\n", " Panel will dispatch events to for concurrent execution on\n", " separate cores (defaults to None).\n", " --warm Whether to execute scripts on startup to warm up the server.\n", " --autoreload\n", " Whether to automatically reload user sessions when the application or any of its imports change.\n", " --static-dirs KEY=VALUE [KEY=VALUE ...] \n", " Static directories to serve specified as key=value\n", " pairs mapping from URL route to static file directory.\n", "\n", " --dev [FILES-TO-WATCH [FILES-TO-WATCH ...]]\n", " Enable live reloading during app development.By\n", " default it watches all *.py *.html *.css *.yaml\n", " filesin the app directory tree. Additional files can\n", " be passedas arguments. NOTE: This setting only works\n", " with a single app.It also restricts the number of\n", " processes to 1.\n", " --session-token-expiration N\n", " Duration in seconds that a new session token is valid\n", " for session creation. After the expiry time has elapsed,\n", " the token will not be able create a new session\n", " (defaults to seconds).\n", " --websocket-max-message-size BYTES\n", " Set the Tornado websocket_max_message_size value\n", " (defaults to 20MB) NOTE: This setting has effect ONLY\n", " for Tornado>=4.5\n", " --websocket-compression-level LEVEL\n", " Set the Tornado WebSocket compression_level\n", " --websocket-compression-mem-level LEVEL\n", " Set the Tornado WebSocket compression mem_level\n", " --oauth-provider OAUTH_PROVIDER\n", " The OAuth2 provider to use.\n", " --oauth-key OAUTH_KEY\n", " The OAuth2 key to use\n", " --oauth-secret OAUTH_SECRET\n", " The OAuth2 secret to use\n", " --oauth-redirect-uri OAUTH_REDIRECT_URI\n", " The OAuth2 redirect URI\n", " --oauth-extra-params OAUTH_EXTRA_PARAMS\n", " Additional parameters to use.\n", " --oauth-jwt-user OAUTH_JWT_USER\n", " The key in the ID JWT token to consider the user.\n", " --oauth-encryption-key OAUTH_ENCRYPTION_KEY\n", " A random string used to encode the user information.\n", " --rest-provider REST_PROVIDER\n", " The interface to use to serve REST API\n", " --rest-endpoint REST_ENDPOINT\n", " Endpoint to store REST API on.\n", " --rest-session-info \n", " Whether to serve session info on the REST API\n", " --session-history SESSION_HISTORY\n", " The length of the session history to record.\n", " --setup\n", " Path to a setup script to run before server starts, e.g. to cache data or set up scheduled tasks.\n", "\n", "To turn a notebook into a deployable app simply append ``.servable()`` to one or more Panel objects, which will add the app to Bokeh's ``curdoc``, ensuring it can be discovered by Bokeh server on deployment. In this way it is trivial to build dashboards that can be used interactively in a notebook and then seamlessly deployed on Bokeh server.\n", "\n", "When called on a notebook, `panel serve` first converts it to a python script using [`nbconvert.PythonExporter()`](https://nbconvert.readthedocs.io/en/stable/api/exporters.html), albeit with [IPython magics](https://ipython.readthedocs.io/en/stable/interactive/magics.html) stripped out. This means that non-code cells, such as raw cells, are entirely handled by `nbconvert` and [may modify the served app](https://nbsphinx.readthedocs.io/en/latest/raw-cells.html)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Static file hosting\n", "\n", "Whether you're launching your application using `panel serve` from the commandline or using `pn.serve` in a script you can also serve static files. When using `panel serve` you can use the `--static-dirs` argument to specify a list of static directories to serve along with their routes, e.g.:\n", "\n", " panel serve some_script.py --static-dirs assets=./assets\n", " \n", "This will serve the `./assets` directory on the servers `/assets` route. Note however that the `/static` route is reserved internally by Panel.\n", "\n", "Similarly when using `pn.serve` or `panel_obj.show` the static routes may be defined as a dictionary, e.g. the equivalent to the example would be:\n", "\n", " pn.serve(panel_obj, static_dirs={'assets': './assets'})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Accessing session state\n", "\n", "Whenever a Panel app is being served the ``panel.state`` object exposes some of the internal Bokeh server components to a user.\n", "\n", "#### Document\n", "\n", "The current Bokeh ``Document`` can be accessed using ``panel.state.curdoc``.\n", "\n", "#### Request arguments\n", "\n", "When a browser makes a request to a Bokeh server a session is created for the Panel application. The request arguments are made available to be accessed on ``pn.state.session_args``. For example if your application is hosted at ``localhost:8001/app``, appending ``?phase=0.5`` to the URL will allow you to access the phase variable using the following code:\n", "\n", "```python\n", "try:\n", " phase = int(pn.state.session_args.get('phase')[0])\n", "except Exception:\n", " phase = 1\n", "```\n", "\n", "This mechanism may be used to modify the behavior of an app dependending on parameters provided in the URL. \n", "\n", "#### Cookies\n", "\n", "The `panel.state.cookies` will allow accessing the cookies stored in the browser and on the bokeh server.\n", "\n", "#### Headers\n", "\n", "The `panel.state.headers` will allow accessing the HTTP headers stored in the browser and on the bokeh server.\n", "\n", "#### Location\n", "\n", "When starting a server session Panel will attach a `Location` component which can be accessed using `pn.state.location`. The `Location` component servers a number of functions:\n", "\n", "- Navigation between pages via ``pathname``\n", "- Sharing (parts of) the page state in the url as ``search`` parameters for bookmarking and sharing.\n", "- Navigating to subsections of the page via the ``hash_`` parameter.\n", "\n", "##### Core\n", "\n", "* **``pathname``** (string): pathname part of the url, e.g. '/user_guide/Interact.html'.\n", "* **``search``** (string): search part of the url e.g. '?color=blue'.\n", "* **``hash_``** (string): hash part of the url e.g. '#interact'.\n", "* **``reload``** (bool): Whether or not to reload the page when the url is updated.\n", " - For independent apps this should be set to True. \n", " - For integrated or single page apps this should be set to False.\n", "\n", "##### Readonly\n", "\n", "* **``href``** (string): The full url, e.g. 'https://panel.holoviz.org/user_guide/Interact.html:80?color=blue#interact'.\n", "* **``protocol``** (string): protocol part of the url, e.g. 'http:' or 'https:'\n", "* **``port``** (string): port number, e.g. '80'\n", "\n", "#### pn.state.busy\n", "\n", "Often an application will have longer running callbacks which are being processed on the server, to give users some indication that the server is busy you may therefore have some way of indicating that busy state. The `pn.state.busy` parameter indicates whether a callback is being actively processed and may be linked to some visual indicator.\n", "\n", "Below we will create a little application to demonstrate this, we will create a button which executes some longer running task on click and then create an indicator function that displays `'I'm busy'` when the `pn.state.busy` parameter is `True` and `'I'm idle'` when it is not:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "def processing(event):\n", " # Some longer running task\n", " time.sleep(1)\n", " \n", "button = pn.widgets.Button(name='Click me!')\n", "button.on_click(processing)\n", "\n", "@pn.depends(pn.state.param.busy)\n", "def indicator(busy):\n", " return \"I'm busy\" if busy else \"I'm idle\"\n", "\n", "pn.Row(button, indicator)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This way we can create a global indicator for the busy state instead of modifying all our callbacks." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Scheduling task with `pn.state.schedule_task`\n", "\n", "The `pn.state.schedule_task` functionality allows scheduling global tasks at certain times or on a specific schedule. This is distinct from periodic callbacks, which are scheduled per user session. Global tasks are useful for performing periodic actions like updating cached data, performing cleanup actions or other housekeeping tasks, while periodic callbacks should be reserved for making periodic updates to an application.\n", "\n", "The different contexts in which global tasks and periodic callbacks run also has implications on how they should be scheduled. Scheduled task **must not** be declared in the application code itself, i.e. if you are serving `panel serve app.py` the callback you are scheduling must not be declared in the `app.py`. It must be defined in an external module or in a separate script declared as part of the `panel serve` invocation using the `--setup` commandline argument.\n", "\n", "Scheduling using `pn.state.schedule_task` is idempotent, i.e. if a callback has already been scheduled under the same name subsequent calls will have no effect. By default the starting time is immediate but may be overridden with the `at` keyword argument. The period may be declared using the `period` argument or a cron expression (which requires the `croniter` library). Note that the `at` time should be in local time but if a callable is provided it must return a UTC time. If `croniter` is installed a `cron` expression can be provided using the `cron` argument.\n", "\n", "As a simple example of a task scheduled at a fixed interval:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import datetime as dt\n", "import asyncio\n", "\n", "async def task():\n", " print(f'Task executed at: {dt.datetime.now()}')\n", "\n", "pn.state.schedule_task('task', task, period='1s')\n", "await asyncio.sleep(3)\n", "\n", "pn.state.cancel_task('task')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that while both `async` and regular callbacks are supported, asynchronous callbacks are preferred if you are performing any I/O operations to avoid interfering with any running applications.\n", "\n", "If you have the `croniter` library installed you may also provide a cron expression, e.g. the following will schedule a task to be repeated at 4:02 am every Monday and Friday:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.state.schedule_task('task', task, cron='2 4 * * mon,fri')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "See [crontab.guru](https://crontab.guru/) and the [`croniter` README](https://github.com/kiorky/croniter#introduction) to learn about cron expressions genrally and special syntax supported by `croniter`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### pn.state.onload\n", "\n", "Another useful callback to define the onload callback, in a server context this will execute when a session is first initialized. Let us for example define a minimal example inside a function which we will pass to `pn.serve`. This emulates what happens when we call `panel serve` on the commandline. We will create a widget without populating its options, then we will add an `onload` callback, which will set the options once the initial page is loaded. Imagine for example that we have to fetch the options from some database which might take a little while, by deferring the loading of the options to the callback we can get something on the screen as quickly as possible and only run the expensive callback when we have already rendered something for the user to look at." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "def app():\n", " widget = pn.widgets.Select()\n", "\n", " def on_load():\n", " time.sleep(1) # Emulate some long running process\n", " widget.options = ['A', 'B', 'C']\n", "\n", " pn.state.onload(on_load)\n", "\n", " return widget\n", "\n", "# pn.serve(app) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### pn.state.on_session_destroyed\n", "\n", "In many cases it is useful to define on_session_destroyed callbacks to perform any custom cleanup that is required, e.g, dispose a database engine, or when a user is logged out. These callbacks can be registered with `pn.state.on_session_destroyed(callback)`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Scheduling callbacks\n", "\n", "When you build an app you frequently want to schedule a callback to be run periodically to refresh the data and update visual components. Additionally if you want to update Bokeh components directly you may need to schedule a callback to get around Bokeh's document lock to avoid errors like this:\n", "\n", "> RuntimeError: _pending_writes should be non-None when we have a document lock, and we should have the lock when the document changes\n", "\n", "In this section we will discover how we can leverage Bokeh's Document and `pn.state.add_periodic_callback` to set this up.\n", "\n", "### Server callbacks\n", "\n", "The Bokeh server that Panel builds on is designed to be thread safe which requires a set of locks to avoid multiple threads modifying the Bokeh models simultaneously. Therefore if we want to work with Bokeh models directly we should ensure that any changes to a Bokeh model are executed on the correct thread by adding a callback, which the event loop will then execute safely.\n", "\n", "In the example below we will launch an application on a thread using `pn.serve` and make the Bokeh plot (in practice you may provide handles to this object on a class). Finally we will wait 1 second until the server is launched and schedule a callback which updates the `y_range` by accessing the `Document` and calling `add_next_tick_callback` on it. This pattern will ensure that the update to the Bokeh model is executed on the correct thread:\n", "\n", "```python\n", "import time\n", "import panel as pn\n", "\n", "from bokeh.plotting import figure\n", "\n", "global p\n", "p = None\n", "\n", "def app():\n", " global p\n", " doc = pn.state.curdoc\n", " p = figure()\n", " p.line([1, 2, 3], [1, 2, 3])\n", " return p\n", "\n", "pn.serve(app, threaded=True)\n", "\n", "time.sleep(1)\n", "\n", "p.document.add_next_tick_callback(lambda: p.y_range.update(start=0, end=4))\n", "```\n", "\n", "### Periodic callbacks\n", "\n", "As we discussed above periodic callbacks allow periodically updating your application with new data. Below we will create a simple Bokeh plot and display it with Panel:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import ColumnDataSource\n", "from bokeh.plotting import figure\n", "\n", "source = ColumnDataSource({\"x\": range(10), \"y\": range(10)})\n", "p = figure()\n", "p.line(x=\"x\", y=\"y\", source=source)\n", "\n", "bokeh_pane = pn.pane.Bokeh(p)\n", "bokeh_pane.servable()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we will define a callback that updates the data on the `ColumnDataSource` and use the `pn.state.add_periodic_callback` method to schedule updates every 200 ms. We will also set a timeout of 5 seconds after which the callback will automatically stop." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def update():\n", " data = np.random.randint(0, 2 ** 31, 10)\n", " source.data.update({\"y\": data})\n", " bokeh_pane.param.trigger('object') # Only needed in notebook\n", "\n", "cb = pn.state.add_periodic_callback(update, 200, timeout=5000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In a notebook or bokeh server context we should now see the plot update periodically. The other nice thing about this is that `pn.state.add_periodic_callback` returns `PeriodicCallback` we can call `.stop()` and `.start()` on if we want to stop or pause the periodic execution. Additionally we can also dynamically adjust the period by setting the `timeout` parameter to speed up or slow down the callback.\n", "\n", "Other nice features on a periodic callback are the ability to check the number of executions using the `cb.counter` property and the ability to toggle the callback on and off simply by setting the running parameter. This makes it possible to link a widget to the running state:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "toggle = pn.widgets.Toggle(name='Toggle callback')\n", "\n", "toggle.link(cb, bidirectional=True, value='running')\n", "toggle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that when starting a server dynamically with `pn.serve` you cannot start a periodic callback before the application is actually being served. Therefore you should create the application and start the callback in a wrapping function:\n", "\n", "```python\n", "from functools import partial\n", "\n", "import numpy as np\n", "import panel as pn\n", "\n", "from bokeh.models import ColumnDataSource\n", "from bokeh.plotting import figure\n", "\n", "def update(source):\n", " data = np.random.randint(0, 2 ** 31, 10)\n", " source.data.update({\"y\": data})\n", "\n", "def panel_app():\n", " source = ColumnDataSource({\"x\": range(10), \"y\": range(10)})\n", " p = figure()\n", " p.line(x=\"x\", y=\"y\", source=source)\n", " cb = pn.state.add_periodic_callback(partial(update, source), 200, timeout=5000)\n", " return pn.pane.Bokeh(p)\n", "\n", "pn.serve(panel_app)\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Accessing the Bokeh model\n", "\n", "Since Panel is built on top of Bokeh, all Panel objects can easily be converted to a Bokeh model. The ``get_root`` method returns a model representing the contents of a Panel:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model = pn.Column('# Some markdown').get_root()\n", "model" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "By default this model will be associated with Bokeh's ``curdoc()``, so if you want to associate the model with some other ``Document`` ensure you supply it explictly as the first argument. Once you have access to the underlying bokeh model you can use all the usual bokeh utilities such as ``components``, ``file_html``, or ``show``" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.embed import components, file_html\n", "from bokeh.io import show\n", "\n", "script, html = components(model)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Embedding\n", "\n", "Panel generally relies on either the Jupyter kernel or a Bokeh Server to be running in the background to provide interactive behavior. However for simple apps with a limited amount of state it is also possible to `embed` all the widget state, allowing the app to be used entirely from within Javascript. To demonstrate this we will create a simple app which simply takes a slider value, multiplies it by 5 and then display the result." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "slider = pn.widgets.IntSlider(start=0, end=10)\n", "\n", "@pn.depends(slider.param.value)\n", "def callback(value):\n", " return '%d * 5 = %d' % (value, value*5)\n", "\n", "row = pn.Row(slider, callback)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we displayed this the normal way it would call back into Python every time the value changed. However, the `.embed()` method will record the state of the app for the different widget configurations." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "row.embed()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you try the widget above you will note that it only has 3 different states, 0, 5 and 10. This is because by default embed will try to limit the number of options of non-discrete or semi-discrete widgets to at most three values. This can be controlled using the `max_opts` argument to the embed method or you can provide an explicit list of `states` to embed for each widget:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "row.embed(states={slider: list(range(0, 12, 2))})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " The full set of options for the embed method include:\n", "\n", "- **`max_states`**: The maximum number of states to embed\n", "\n", "- **`max_opts`**: The maximum number of states for a single widget\n", "\n", "- **`states`** (default={}): A dictionary specifying the widget values to embed for each widget\n", "\n", "- **`json`** (default=True): Whether to export the data to json files\n", "\n", "- **`save_path`** (default='./'): The path to save json files to\n", "\n", "- **`load_path`** (default=None): The path or URL the json files will be loaded from (same as ``save_path`` if not specified)\n", "\n", "* **`progress`** (default=False): Whether to report progress\n", "\n", "\n", "As you might imagine if there are multiple widgets there can quickly be a combinatorial explosion of states so by default the output is limited to about 1000 states. For larger apps the states can also be exported to json files, e.g. if you want to serve the app on a website specify the ``save_path`` to declare where it will be stored and the ``load_path`` to declare where the JS code running on the website will look for the files." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Saving \n", "\n", "In case you don't need an actual server or simply want to export a static snapshot of a panel app, you can use the ``save`` method, which allows exporting the app to a standalone HTML or PNG file.\n", "\n", "By default, the HTML file generated will depend on loading JavaScript code for BokehJS from the online ``CDN`` repository, to reduce the file size. If you need to work in an airgapped or no-network environment, you can declare that ``INLINE`` resources should be used instead of ``CDN``:\n", "\n", "```python\n", "from bokeh.resources import INLINE\n", "panel.save('test.html', resources=INLINE)\n", "```\n", "\n", "Additionally the save method also allows enabling the `embed` option, which, as explained above, will embed the apps state in the app or save the state to json files which you can ship alongside the exported HTML.\n", "\n", "Finally, if a 'png' file extension is specified, the exported plot will be rendered as a PNG, which currently requires Selenium and PhantomJS to be installed:\n", "\n", "```python\n", "pane.save('test.png')\n", "\n", "```" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }