{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "import pandas as pd\n", "import altair as alt\n", "\n", "from bokeh.sampledata import stocks\n", "\n", "pn.extension('vega', template='fast-list')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This example is meant to make it easy to compare and contrast the different APIs Panel provides to declare apps and dashboards. Specifically, it compares four different implementations of the same app using 1) the quick and easy ``interact`` function, 2) more flexible reactive functions, 3) declarative Param-based code, and 4) explicit callbacks.\n", "\n", "Before comparing the different approaches, we will first declare some components of the app that will be shared, including the title of the app, a set of stock tickers, a function to return a dataframe given the stock ``ticker`` and the rolling mean ``window_size``, and another function to return a plot given those same inputs:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "title = '## Stock Explorer Altair'\n", "\n", "tickers = ['AAPL', 'FB', 'GOOG', 'IBM', 'MSFT']\n", "\n", "def get_df(ticker, window_size):\n", " df = pd.DataFrame(getattr(stocks, ticker))\n", " df['date'] = pd.to_datetime(df.date)\n", " return df.set_index('date').rolling(window=window_size).mean().reset_index()\n", "\n", "def get_plot(ticker, window_size):\n", " df = get_df(ticker, window_size)\n", " return alt.Chart(df).mark_line().encode(x='date', y='close').properties(width=\"container\", height=\"container\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Interact\n", "\n", "In the ``interact`` model the widgets are automatically generated from the arguments to the function or by providing additional hints to the ``interact`` call. This is a very convenient way to generate a simple app, particularly when first exploring some data. However, because widgets are created implicitly based on introspecting the code, it is difficult to see how to modify the behavior. Also, to compose the different components in a custom way it is necessary to unpack the layout returned by the ``interact`` call, as we do here:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "interact = pn.interact(get_plot, ticker=tickers, window_size=(1, 21, 5))\n", "\n", "pn.Row(\n", " pn.Column(title, interact[0]),\n", " interact[1]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Reactive\n", "\n", "The reactive programming model is similar to the ``interact`` function but relies on the user (a) explicitly instantiating widgets, (b) declaring how those widgets relate to the function arguments (using the ``bind`` function), and (c) laying out the widgets and other components explicitly. In principle we could reuse the ``get_plot`` function from above here but for clarity we will repeat it:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ticker = pn.widgets.Select(name='Ticker', options=tickers)\n", "window = pn.widgets.IntSlider(name='Window Size', value=6, start=1, end=21)\n", "\n", "def get_plot(ticker, window_size):\n", " df = get_df(ticker, window_size)\n", " return alt.Chart(df).mark_line().encode(x='date', y='close').properties(\n", " width=\"container\", height=\"container\"\n", " )\n", "\n", "pn.Row(\n", " pn.Column(title, ticker, window),\n", " pn.bind(get_plot, ticker, window)\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Parameterized class\n", "\n", "Another approach expresses the app entirely as a single ``Parameterized`` class with parameters to declare the inputs, rather than explicit widgets. The parameters are independent of any GUI code, which can be important for maintaining large codebases, with parameters and functionality defined separately from any GUI or panel code. Once again the ``depends`` decorator is used to express the dependencies, but in this case the dependencies are expressed as strings referencing class parameters, not parameters of widgets. The parameters and the ``plot`` method can then be laid out independently, with Panel used only for this very last step." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class StockExplorer(param.Parameterized):\n", " \n", " ticker = param.Selector(default='AAPL', objects=tickers)\n", " \n", " window_size = param.Integer(default=6, bounds=(1, 21))\n", " \n", " @param.depends('ticker', 'window_size')\n", " def plot(self):\n", " return get_plot(self.ticker, self.window_size).properties(width=\"container\", height=\"container\")\n", " \n", "explorer = StockExplorer()\n", "\n", "pn.Row(\n", " pn.Column(explorer.param),\n", " explorer.plot,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Callbacks\n", "\n", "The above approaches are all reactive in some way, triggering actions whenever manipulating a widget causes a parameter to change, without users writing code to trigger callbacks explicitly. Explicit callbacks allow complete low-level control of precisely how the different components of the app are updated, but they can quickly become unmaintainable because the complexity increases dramatically as more callbacks are added. The approach works by defining callbacks using the ``.param.watch`` API that either update or replace the already rendered components when a watched parameter changes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ticker = pn.widgets.Select(name='Ticker', options=['AAPL', 'FB', 'GOOG', 'IBM', 'MSFT'])\n", "window = pn.widgets.IntSlider(name='Window', value=6, start=1, end=21)\n", "\n", "row = pn.Row(\n", " pn.Column(title, ticker, window, sizing_mode=\"fixed\", width=300),\n", " get_plot(ticker.options[0], window.value)\n", ")\n", "\n", "def update(event):\n", " row[1].object = get_plot(ticker.value, window.value)\n", "\n", "ticker.param.watch(update, 'value')\n", "window.param.watch(update, 'value')\n", "\n", "row" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In practice, different projects will be suited to one or the other of these APIs, and most of Panel's functionality should be available from any API." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## App\n", "\n", "This notebook may also be served as a standalone application by running it with `panel serve stocks_altair.ipynb`. Above we enabled a custom `template`, in this section we will add components to the template with the `.servable` method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ticker.servable(area='sidebar')\n", "window.servable(area='sidebar')\n", "\n", "pn.panel(\"\"\"This example compares **four different implementations of the same app** using \n", "\n", "- the quick and easy ``interact`` function, \n", "- more flexible *reactive* functions,\n", "- declarative *Param-based* code, and \n", "- explicit *callbacks*.\"\"\").servable()\n", "\n", "pn.panel(pn.bind(get_plot, ticker, window)).servable(title='Altair Stock Explorer');" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }