Skip to content

Instantly share code, notes, and snippets.

@mpkocher
Last active June 26, 2019 17:32
Show Gist options
  • Save mpkocher/83ac8f99a7b6a4ed87f1f2014ef74c2b to your computer and use it in GitHub Desktop.
Save mpkocher/83ac8f99a7b6a4ed87f1f2014ef74c2b to your computer and use it in GitHub Desktop.
Pyviz Panel. Kicking the Tires
dependencies:
- panel>=0.6.0
- holoviews
- hvplot
- param
- matplotlib
- scipy
- conda-forge::altair
- conda-forge::plotly
- conda-forge::vtk=8.1.1
- conda-forge::jupyter
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Python Dashboards with Panel: Kicking the Tires\n",
"\n",
"This explore creating dashboard components using Pyviz's [Panel](http://panel.pyviz.org). The [release post](https://medium.com/@philipp.jfr/panel-announcement-2107c2b15f52) has a good overview of the features and overall design.\n",
"\n",
"This notebook provides an example of creating generic Panel components to create Panel driven dashboard. \n",
"\n",
"This dashboard will require a server, hence **the output cells of the notebook are not rendered** (this also keeps the `.ipynb` file very small).\n",
"\n",
"The raw notebook can be launched from [mybinder.org](https://mybinder.org/v2/gist/mpkocher/83ac8f99a7b6a4ed87f1f2014ef74c2b/master?filepath=panel-dashboard.ipynb). Note, this is complete notebook with the embedded dashboard, not the dashboard only \"view\" that is obtained via `panel serve /path/to/notebook.ipynb`).\n",
"\n",
"\n",
"## High-Level Summary of Panel\n",
"\n",
"- Tame the entropy in the Python visualization ecosystem (specifically in the dashboard space)\n",
"- Support several different libs (e.g., bokeh, matplotlib, holoviews, altair, etc...)\n",
"- Provide an iterative model to develop components directly within Jupyter notebook\n",
"- Deploy dashboard directly from a Jupyter (`.ipynb`) or python (`.py`) format using `panel serve /path/to/notebook.ipynb`\n",
"\n",
"## Outline/Goals\n",
"\n",
"- Creating components using bokeh figures\n",
"- Create component with Tabs\n",
"- Create an generic interactive XY scatter and Histogram components\n",
"- Create linked components to update multiple downstream figures\n",
"- Generate a component using holoview\n",
"- Construct an example dashboard using the components defined above\n",
"- Tips and Suggestions "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Imports and Setup\n",
"\n",
"import math\n",
"import datetime\n",
"\n",
"from typing import List, Optional, Any, Callable\n",
"\n",
"import matplotlib.pyplot as plt\n",
"plt.style.use('ggplot')\n",
"\n",
"#import seaborn as sns\n",
"#sns.set(rc={'figure.figsize': (16, 9.)})\n",
"\n",
"%config InlineBackend.figure_format = 'retina'\n",
"\n",
"import panel as pn\n",
"import panel.widgets as pnw\n",
"import param\n",
"\n",
"import bokeh\n",
"from bokeh.plotting import figure, show\n",
"\n",
"import holoviews as hv\n",
"import hvplot.pandas\n",
"\n",
"pn.extension('vega', 'katex', 'plotly', logo=False)\n",
"hv.extension('bokeh', logo=False)\n",
"\n",
"# test data to use in dashboard\n",
"from bokeh.sampledata.iris import flowers as iris\n",
"from bokeh.sampledata.autompg import autompg\n",
"\n",
"\n",
"def to_info():\n",
" # Util for package versions\n",
" components = (bokeh, pn, hv)\n",
" attrs = ('__name__', '__version__')\n",
" \n",
" def to_s(c):\n",
" return \" = \".join([getattr(c, a) for a in attrs])\n",
" \n",
" return \"\\n\".join(map(to_s, components))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Creating a Figure and wrapping it in Panel"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def to_example_figure(func):\n",
" p1 = figure(width=600, height=400)\n",
" xs = list(range(0, 100))\n",
" ys = [func(x) for x in xs]\n",
" p1.line(xs, ys)\n",
" return p1\n",
"\n",
"def to_pow(y):\n",
" def f(x):\n",
" return math.pow(x, y)\n",
" return f"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"f0 = to_example_figure(to_pow(1))\n",
"f1 = to_example_figure(to_pow(3))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Column(f0, f1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Define some util funcs to generate $x^n$ datasets"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def to_name(i):\n",
" return f\"x^{i}\"\n",
"\n",
"def to_f(i):\n",
" return to_name(i), to_pow(i)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"funcs = map(to_f, range(1, 9))\n",
"\n",
"t0 = pn.Tabs()\n",
"t0.extend([(name, to_example_figure(f)) for name, f in funcs])\n",
"\n",
"\n",
"c0 = pn.Column(pn.Row(pn.pane.Markdown(\"##Example Dashboard Component using Tabs\")), t0)\n",
"c0"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Create Interactive Plots\n",
"\n",
"There are [four different models/styles](https://panel.pyviz.org/user_guide/APIs.html) to create components.\n",
"\n",
"1. Interact functions: Auto-generates a full UI (including widgets) given a function\n",
"2. Reactive functions: Linking functions or methods to widgets using the pn.depends decorator, declaring that the function should be re-run when those widget values change\n",
"3. Parameterized class: Declare parameters and their ranges in Parameterized classes, then get GUIs (and value checking!) for free\n",
"4. Callbacks: Generate a UI by manually declaring callbacks that update panels or panes\n",
"\n",
"In the examples below use style #2 and #3. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Defining Components using Parameterized Class Style"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def to_interactive_xy_class_style(columns, func, default_color = '#058805', name=\"XY-Explorer\"):\n",
" \n",
" class XYExplorer(param.Parameterized):\n",
" x = param.Selector(default=columns[0], objects=columns)\n",
" y = param.Selector(default=columns[1], objects=columns)\n",
" color = param.Color(default=default_color)\n",
" \n",
" @param.depends('x', 'y', 'color') # optional in this case\n",
" def plot(self):\n",
" return func(self.x, self.y, self.color)\n",
" \n",
" def panel(self):\n",
" return pn.Row(self.param, self.plot)\n",
" \n",
" return XYExplorer(name=name)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Defining Components using Reactive Functions Style"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def to_interactive_xy_deco_style(columns, func, default_color = '#058805', name=\"XY-Explorer\"):\n",
" x = pnw.Select(name=\"X\", value=columns[0], options=columns)\n",
" y = pnw.Select(name=\"Y\", value=columns[1], options=columns)\n",
" color = pnw.ColorPicker(name=\"Color\", value=default_color)\n",
" \n",
" @pn.depends(x.param.value, y.param.value, color.param.value)\n",
" def reactive_xy(x, y, color):\n",
" return func(x, y, color)\n",
" \n",
" return pn.Row(pn.Column(pn.pane.Markdown(name), x, y, color), reactive_xy)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create a figure using AutoMPG dataset"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def autompg_plot(x='mpg', y='hp', color='#058805'):\n",
" return autompg.hvplot.scatter(x, y, c=color, padding=0.1)\n",
"\n",
"auto_mpg_columns = list(autompg.columns[:-2]) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"auto_mpg_exp = to_interactive_xy_class_style(auto_mpg_columns, autompg_plot)\n",
"auto_mpg_exp.panel()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"c_autompg = to_interactive_xy_deco_style(auto_mpg_columns, autompg_plot)\n",
"c_autompg"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def iris_xy_plot(x, y, color):\n",
" return iris.hvplot.scatter(x, y, c=color, padding=0.1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"iris_columns = list(iris.columns[:-1])\n",
"\n",
"c_iris_xy = to_interactive_xy_deco_style(iris_columns, iris_xy_plot, default_color=\"#0000FF\", name=\"# Iris XY Explorer\")\n",
"\n",
"c_iris_xy"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For dashboard components where the data source isn't configurable, it's trivial to wrap them in a **Panel**."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Row(iris.hvplot.hist(y='sepal_length'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Defining a Histogram using Reactive Style"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def to_hist_explorer(columns: List[str], runner_func:Callable[[str, str], Any], \n",
" default_y:Optional[str]=None, \n",
" default_color='#0000ff', name=\"Histogram Explorer\"):\n",
" \n",
" ys = columns[0] if default_y is None else default_y\n",
" y = pn.widgets.Select(name=\"Y\", value=ys, options=columns)\n",
" color = pn.widgets.ColorPicker(name=\"Color\", value=default_color)\n",
" \n",
" @pn.depends(y.param.value, color.param.value)\n",
" def to_hist(y, color):\n",
" return runner_func(y, color)\n",
" \n",
" return pn.Row(pn.pane.Markdown(name), pn.Row(pn.Column(y, color), to_hist))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def iris_hist_plot(y, color):\n",
" return iris.hvplot.hist(y=y, color=color)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"iris_hist_exp = to_hist_explorer(iris_columns, iris_hist_plot, default_y=iris_columns[0])\n",
"\n",
"iris_hist_exp"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Creating Linked Dashboard Components"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def to_linked_component(columns, xy_plotter_func, hist_plotter_func, \n",
" default_color='#0000ff', name=\"Linked XY/Histogram Component\"):\n",
" \n",
" x = pn.widgets.Select(name=\"X\", value=columns[0], options=columns)\n",
" y = pn.widgets.Select(name=\"Y\", value=columns[1], options=columns)\n",
" color = pn.widgets.ColorPicker(name=\"Color\", value=default_color)\n",
" \n",
" @pn.depends(x.param.value, y.param.value, color.param.value)\n",
" def reactive_xy(x, y, color):\n",
" return xy_plotter_func(x, y, color)\n",
" \n",
" @pn.depends(y.param.value, color.param.value)\n",
" def reactive_hist_y(y, color):\n",
" return hist_plotter_func(y, color)\n",
" \n",
" @pn.depends(x.param.value, color.param.value)\n",
" def reactive_hist_x(x, color):\n",
" return hist_plotter_func(x, color)\n",
" \n",
" input_params = pn.Column(y, x, color)\n",
" \n",
" return pn.Column(pn.pane.Markdown(name), pn.Row(input_params), reactive_xy, reactive_hist_y, reactive_hist_x) "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"c_iris_xy = to_linked_component(iris_columns, iris_xy_plot, iris_hist_plot, name=\"Iris Linked XY/Histogram Component\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"c_iris_xy"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"autompg.describe()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"autompg_ds = hv.Dataset(autompg[['mpg', 'cyl', 'hp', 'weight', 'accel', 'yr']])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"autompg_ds"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from holoviews import opts\n",
"from holoviews.operation import gridmatrix"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def to_grid(df):\n",
" density_grid = gridmatrix(df, diagonal_type=hv.Distribution)\n",
" point_grid = gridmatrix(df, chart_type=hv.Points)\n",
"\n",
" p = (density_grid * point_grid).opts(opts.Bivariate(bandwidth=0.5, cmap='Blues'), \n",
" opts.Points(size=2, tools=['box_select']))\n",
" return pn.Row(p)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"rg = to_grid(autompg_ds)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"c_autompg_joint = pn.Row(pn.pane.Markdown(\"## AutoMPG Plots\"), rg)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#c_autompg_joint"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Dashboard \n",
"\n",
"Any component can be exported/registered by calling the `.servable()` on the component that are defined above. It's easier to"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"header = \"\"\"# Example Dashboard\"\"\"\n",
"\n",
"desc = \"\"\"\n",
"Example dashboard using Pyviz's [Panel](https://panel.pyviz.org/) (version 0.6.0)\n",
"\"\"\"\n",
"\n",
"img_src = \"https://vignette.wikia.nocookie.net/simpsons/images/b/b3/Homerswebpage.jpg\"\n",
"\n",
"\n",
"dashboard = pn.Column(\n",
" pn.pane.Markdown(header), \n",
" pn.panel(img_src, width=200),\n",
" pn.pane.Markdown(desc),\n",
" c0,\n",
" pn.pane.Markdown(\"## IRIS Components\"),\n",
" c_iris_xy,\n",
" pn.pane.Markdown(\"# AutoMPG Components\"),\n",
" c_autompg,\n",
" pn.pane.Markdown(\"## AutoMPG Joint Plot\"),\n",
" rg,\n",
" pn.pane.Markdown(\"Generated at {} using Components: {}\".format(datetime.datetime.now(), to_info()))\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dashboard.servable()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Tips and Suggestions\n",
"\n",
"- Be pragmatic about mixing up layout and Components\n",
"- Prefer \"thin\" components that are largely purely wired up calls to your analysis functions (this will decouple the layers in the dashboard)\n",
"- Use small factory-ish functions to create generic reusable components. It's trivial to share widgets between components this way.\n",
"- Use `panel.widgets.*` instead of `param.*`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment