Introduction to Interactive Dashboards with Panel

Panel is an open-source Python library that enables users to create custom interactive web apps and dashboards using their existing data-processing environments. Unlike many dashboarding tools, Panel is built to integrate seamlessly with the PyData ecosystem, meaning it works naturally with tools like pandas, Matplotlib, Holoviews, and even geospatial libraries. With a wide variety of built-in widgets, layouts, and themes, Panel offers a high degree of customization while maintaining Python-centric workflows. Its ability to produce both standalone apps and embeddable components for existing web applications makes it highly versatile for both data scientists and developers aiming for rich, interactive visualizations and analytics.

Why use Panel?

Panel offers a suite of unique advantages when it comes to crafting interactive dashboards:

  • Pythonic Integration: No need to juggle between languages. Design, script, and visualize all within your Python environment.

  • Holoviz Ecosystem: Being a part of the Holoviz family, Panel works effortlessly with other libraries like Holoviews, Datashader, and Param, ensuring a cohesive data visualization experience.

  • Versatility: Whether you’re aiming for a quick prototype or a production-ready application, Panel has you covered. It supports both notebook-based development for rapid iteration and standalone deployment for broader access.

  • Extensibility: Panel isn’t limited to its built-in widgets. If needed, it can be extended with custom JavaScript, ensuring that you’re never boxed in by limitations.

  • Rich Widget Collection: From sliders to color pickers, Panel’s comprehensive widget suite allows for intricate user interactivity without demanding extensive coding.

  • Seamless Deployment: Once your dashboard is ready, Panel supports various deployment options, including Jupyter, standalone servers, and even embedding in larger web applications.

Choosing Panel streamlines the dashboard development process, ensuring a smooth transition from data processing to interactive visualization, all within the Python landscape.

Comparison with other dashboarding tools

HvPlot Introduction

If you have tried to visualize a pandas.DataFrame before, then you have likely encountered the Pandas .plot() API. These plotting commands use Matplotlib to render static PNGs or SVGs in a Jupyter notebook using the inline backend, or interactive figures via %matplotlib widget, with a command that can be as simple as df.plot() for a DataFrame with one or two columns.

The Pandas .plot() API has emerged as a de-facto standard for high-level plotting APIs in Python, and is now supported by many different libraries that use various underlying plotting engines to provide additional power and flexibility. Learning this API allows you to access capabilities provided by a wide variety of underlying tools, with relatively little additional effort. The libraries currently supporting this API include:

  • Pandas – Matplotlib-based API included with Pandas. Static or interactive output in Jupyter notebooks.
  • hvPlot – HoloViews and Bokeh-based interactive plots for Pandas, GeoPandas, xarray, Dask, Intake, and Streamz data.
  • xarray – Matplotlib-based API included with xarray, based on pandas .plot API. Static or interactive output in Jupyter notebooks.
  • GeoViews – provides a set of coordinate-aware data types (geometries) and functions for visual integration with the HoloViews library, to explore and visualize geographical datasets,
import holoviews as hv
import hvplot.pandas
from bokeh.sampledata.autompg import autompg_clean as df
# you may also read in data from a local file, for example using
# df = pd.read_csv('data.csv')
df.describe()
mpg cyl displ hp weight accel yr
count 392.000000 392.000000 392.000000 392.000000 392.000000 392.000000 392.000000
mean 23.445918 5.471939 194.411990 104.469388 2977.584184 15.541327 75.979592
std 7.805007 1.705783 104.644004 38.491160 849.402560 2.758864 3.683737
min 9.000000 3.000000 68.000000 46.000000 1613.000000 8.000000 70.000000
25% 17.000000 4.000000 105.000000 75.000000 2225.250000 13.775000 73.000000
50% 22.750000 4.000000 151.000000 93.500000 2803.500000 15.500000 76.000000
75% 29.000000 8.000000 275.750000 126.000000 3614.750000 17.025000 79.000000
max 46.600000 8.000000 455.000000 230.000000 5140.000000 24.800000 82.000000

The first thing that we’d like to do with this data is visualize two features of the dataset: mpg and hp. So we would like to make a scatter or points plot where x is mpg and y is hp.

We can do that for the smaller dataframe using the pandas.plot API and Matplotlib:

df.plot.scatter(x='mpg', y='hp');

Using .hvplot

As you can see above, the Pandas API gives you a usable plot very easily. You can make a very similar plot with the same arguments using hvplot, after importing hvplot.pandas to install hvPlot support into Pandas:

df.hvplot.scatter(x='mpg', y='hp')

Other kinds of plots

Use tab completion to explore the available plot types.

df.hvplot.<TAB>

Plot types available include:

  • .area(): Plots a area chart similar to a line chart except for filling the area under the curve and optionally stacking

  • .bar(): Plots a bar chart that can be stacked or grouped

  • .bivariate(): Plots 2D density of a set of points

  • .box(): Plots a box-whisker chart comparing the distribution of one or more variables

  • .heatmap(): Plots a heatmap to visualizing a variable across two independent dimensions

  • .hexbins(): Plots hex bins

  • .hist(): Plots the distribution of one or histograms as a set of bins

  • .kde(): Plots the kernel density estimate of one or more variables.

  • .line(): Plots a line chart (such as for a time series)

  • .scatter(): Plots a scatter chart comparing two variables

  • .step(): Plots a step chart akin to a line plot

  • .table(): Generates a SlickGrid DataTable

  • .violin(): Plots a violin plot comparing the distribution of one or more variables using the kernel density estimate

# default is line plot
df.sort_values(by='mpg').hvplot(x='mpg', y='hp')
# histogram
df.hvplot.hist('mpg', bins=100)

Graphics “grammar”

+ and * - to have several plots (+) or to plot overlays (*)

df.hvplot.hist('mpg', bins=10, width=300) + df.hvplot.scatter(x='mpg', y='hp', width=300)

Separate single country:

df[df.origin=='Asia'].hvplot.scatter(x='mpg', y='hp', by='origin') 

Different plots with same “axes” on top of each other as overlay:

( 
    df[df.origin=='Asia'].hvplot.scatter(x='mpg', y='hp', by='origin') * 
    df[df.origin=='Europe'].hvplot.scatter(x='mpg', y='hp', by='origin') * 
    df[df.origin=='North America'].hvplot.scatter(x='mpg', y='hp', by='origin') 
)

Recap: Interactive plotting and data exploration with hvPlot and GeoViews

Since recent versions, GeoPandas support the .explore() function that allows interactive plotting of the data. This function is based on the hvPlot

Let’s recap GeoDataFrame.explore() with:

# lets use the data from the previous example
data.explore()
import geopandas as gpd

# protected species under class 3 monitoring sightings
species_fp = "../files/data/L3/category_3_species_porijogi.gpkg"
species_data = gpd.read_file(species_fp, layer='category_3_species_porijogi', driver='GPKG')
display(species_data.head(5))
OBJECTID LIIK NIMI EXT_SYST_I KKR_KOOD PRIV_TYYP STAATUS IMPORT LAADIMISKP geometry
0 148179 taimed III soo-neiuvaip 652542557 KLO9320309 Avalik kontrollitud 0 2018-10-29 POINT (646978.483 6444887.321)
1 148180 taimed III soo-neiuvaip 1989720139 KLO9320255 Avalik kontrollitud 0 2018-10-29 POINT (646730.472 6459776.774)
2 162026 loomad III valge-toonekurg -665748946 KLO9108356 Peidetud arhiveeritud 0 2019-09-26 POINT (653008.611 6467205.284)
3 144301 taimed võõrliik Sosnovski karuputk -297982508 VLL1000576 Peidetud arhiveeritud 0 2018-10-29 POINT (638031.354 6444230.237)
4 144305 taimed võõrliik Sosnovski karuputk 1137537662 VLL1000598 Peidetud arhiveeritud 0 2018-10-29 POINT (669298.005 6443792.562)
species_data.explore()
Make this Notebook Trusted to load map: File -> Trust Notebook

GeoViews, like matplotlib, has a large number of options to customize the plots. Let’s plot more interactively, this will also work exactly in the Panel dashboard.

import geoviews as gv
import geoviews.feature as gf
import cartopy.crs as crs
from geoviews import opts, tile_sources as gvts

from bokeh.plotting import figure, output_file, show
gv.extension('bokeh')

species_data_wgs84 = species_data.to_crs(4326)

opts.defaults(opts.WMTS())

data_view = gv.Points(species_data_wgs84, vdims=['LIIK', 'NIMI']).opts(tools=['hover'])
(data_view  * gvts.OSM).opts(width=600, height=400, projection=crs.Mercator())

Basic Panel Concepts

Panel components

  • Panes: Description of what panes are in Panel.
  • Widgets: Different widgets available in Panel and their uses.
  • Layouts: How to layout different components in Panel.

Running a Panel app

  • Steps to run a Panel app and different modes of running.

Difference between a static output and an interactive server

  • Understanding the static vs server modes in Panel.

the Easiest Way to Create an Interactive Dashboard in Python from any DataFrame. If you already know some Pandas, you can almost immediately use hvPlot .interactive and Panel to turn your DataFrame processing pipeline into a dashboard! It just takes a few lines of familiar code to make an interactive dashboard.

import panel as pn

pn.extension('tabulator', sizing_mode="stretch_width")
import hvplot.pandas
import holoviews as hv

hv.extension('bokeh')
from bokeh.sampledata.autompg import autompg_clean as df
df.head()
mpg cyl displ hp weight accel yr origin name mfr
0 18.0 8 307.0 130 3504 12.0 70 North America chevrolet chevelle malibu chevrolet
1 15.0 8 350.0 165 3693 11.5 70 North America buick skylark 320 buick
2 18.0 8 318.0 150 3436 11.0 70 North America plymouth satellite plymouth
3 16.0 8 304.0 150 3433 12.0 70 North America amc rebel sst amc
4 17.0 8 302.0 140 3449 10.5 70 North America ford torino ford
from bokeh.sampledata.autompg import autompg_clean as df
df.head()
mpg cyl displ hp weight accel yr origin name mfr
0 18.0 8 307.0 130 3504 12.0 70 North America chevrolet chevelle malibu chevrolet
1 15.0 8 350.0 165 3693 11.5 70 North America buick skylark 320 buick
2 18.0 8 318.0 150 3436 11.0 70 North America plymouth satellite plymouth
3 16.0 8 304.0 150 3433 12.0 70 North America amc rebel sst amc
4 17.0 8 302.0 140 3449 10.5 70 North America ford torino ford

Define DataFrame Pipeline

(
    df[
        (df.cyl == 4) &
        (df.mfr.isin(['ford','chevrolet']))
    ]
    .groupby(['origin', 'cyl', 'mfr', 'yr'])['hp'].mean()
    .to_frame()
    .reset_index()
    .sort_values(by='yr')
).head(1)
origin cyl mfr yr hp
0 North America 4 chevrolet 71 81.0

Make DataFrame Pipeline Interactive

What if we would like to turn the values of cyl, the values of mfr, and the variable hp into interactive widgets that we can change and control? Is it possible? Yes, it is, and with hvPlot it’s not even difficult. Here are the steps:

  • First, we need to wrap our dataframe with .interactive(): idf = df.interactive(), so that this dataframe becomes interactive and we can use Panel widgets on this dataframe.

“.interactive stores a copy of your pipeline (series of method calls or other expressions on your data) and dynamically replays the pipeline whenever that widget changes.”

idf = df.interactive()

Define Panel widgets

  • Second, we can define the panel widgets we would like to use. Here I defined a panel widget for cylinders, a widget for the manufacturer, and a widget to select the y axis.
cylinders = pn.widgets.IntSlider(name='Cylinders', start=4, end=8, step=2)
cylinders
mfr = pn.widgets.ToggleGroup(
    name='MFR',
    options=['ford', 'chevrolet', 'honda', 'toyota', 'audi'],
    value=['ford', 'chevrolet', 'honda', 'toyota', 'audi'],
    button_type='success')
mfr
yaxis = pn.widgets.RadioButtonGroup(
    name='Y axis',
    options=['hp', 'weight'],
    button_type='success'
)
yaxis
  • Finally, we can replace the values or variables from the original Pandas pipeline to these widgets we just defined. We define the output of the pipeline as ipipeline:
ipipeline = (
    idf[
        (idf.cyl == cylinders) &
        (idf.mfr.isin(mfr))
    ]
    .groupby(['origin', 'mpg'])[yaxis].mean()
    .to_frame()
    .reset_index()
    .sort_values(by='mpg')
    .reset_index(drop=True)
)
ipipeline.head()

Pipe to Table

itable = ipipeline.pipe(pn.widgets.Tabulator, pagination='remote', page_size=10, theme="fast")
itable

Check out the Tabulator Reference Guide for more inspiration.

Pipe to hvplot

PALETTE = ["#ff6f69", "#ffcc5c", "#88d8b0", ]
pn.Row(
    pn.layout.HSpacer(height=50, styles={'background':PALETTE[0]}),
    pn.layout.HSpacer(height=50, styles={'background':PALETTE[1]}),
    pn.layout.HSpacer(height=50, styles={'background':PALETTE[2]}),
)
ihvplot = ipipeline.hvplot(x='mpg', y=yaxis, by='origin', color=PALETTE, line_width=6, height=400)

Layout using Panel

layout = pn.Column(
    pn.Row(
        cylinders, mfr, yaxis
    ),
    ihvplot.panel(),
    itable.panel()
)

From within the notebook you could try like that:

layout.show()

Layout using Template

Here we use the FastListTemplate.

Please note that to get the Tabulator table styled nicely for dark mode you can set theme='fast' when you define the itable. It won’t work in the notebook though.

To serve the notebook run panel serve dashboard.ipynb.

template = pn.template.FastListTemplate(
    title='Interactive DataFrame Dashboards with hvplot .interactive',
    sidebar=[cylinders, 'Manufacturers', mfr, 'Y axis' , yaxis],
    main=[ihvplot.panel(), itable.panel()],
    accent_base_color="#88d8b0",
    header_background="#88d8b0",
)

template.servable();

Creating a Simple Panel App with Geospatial Data

import geopandas as gpd
from fiona.crs import from_epsg
from shapely.geometry import LineString, MultiLineString

# Filepaths
grid_fp = "../files/data/L6/population_square_km.shp"
roads_fp = "../files/data/L6/roads.shp"
schools_fp = "../files/data/L6/schools_tartu.shp"

# Read files
grid = gpd.read_file(grid_fp)
roads = gpd.read_file(roads_fp)
schools = gpd.read_file(schools_fp)

# Re-project to WGS84
grid['geometry'] = grid['geometry'].to_crs(epsg=4326)
roads['geometry'] = roads['geometry'].to_crs(epsg=4326)
schools['geometry'] = schools['geometry'].to_crs(epsg=4326)

# Make a selection (only data above 0 and below 1000)
grid = grid.loc[(grid['Population'] > 0)]

# Create a Geo-id which is needed by the Folium (it needs to have a unique identifier for each row)
grid['geoid'] = grid.index.astype(str)
roads['geoid'] = roads.index.astype(str)
schools['geoid'] = schools.index.astype(str)

# Select data
grid = grid[['geoid', 'Population', 'geometry']]
roads = roads[['geoid', 'TYYP', 'geometry']]
schools = schools[['geoid', 'name', 'geometry']]

# convert the dataframe to geojson
grid_jsontxt = grid.to_json()
roads_jsontxt = roads.to_json()
schools_jsontxt = schools.to_json()

from shapely.geometry import Point

def getPoints(row, geom):
    """Returns coordinate pair tuples for the point ('lat', 'lon') of a Point geometry"""
    if isinstance(row[geom], Point):
        # we need lat lon order for the folium map!!!
        return (row[geom].y, row[geom].x)
    else:
        return ()
import folium
import pandas as pd
import mapclassify as mc

# Initialize the classifier and apply it
classifier = mc.NaturalBreaks.make(k=5)

grid['pop_km2'] = grid[['Population']].apply(classifier)

# basemap
m = folium.Map(location=[58.37, 26.72],
               tiles='Stamen toner',
               zoom_start=8,
               control_scale=True,
               prefer_canvas=True,
               width=600,
               height=450)

# coloured polygon layer
folium.Choropleth(
    geo_data=grid_jsontxt,
    data=grid,
    columns=['geoid', 'pop_km2'],
    key_on="feature.id",
    fill_color='RdBu',
    fill_opacity=0.5,
    line_opacity=0.2,
    line_color='white',
    line_weight=0,
    legend_name='Population in Tartu',
    name='Population Grid',
    highlight=False
).add_to(m)


circles_layer = folium.FeatureGroup(name="circles layer")

# Calculate x and y coordinates of the line
schools['points_tuple'] = schools.apply(getPoints, geom='geometry', axis=1)

# the yellow school circles as reference
for idx, school in schools.iterrows():
    folium.CircleMarker(location=school['points_tuple'],
                        popup=school['name'],
                        color="yellow",
                        radius=2.5,
                        opacity=0.9).add_to(circles_layer)

circles_layer.add_to(m)

# the layer control switch
folium.LayerControl().add_to(m)
<folium.map.LayerControl at 0x7f29b139e350>
folium_pane = pn.pane.plot.Folium(m, height=400)
layout2 = pn.Column(
    pn.Row(
        pn.pane.Markdown("""# Schools in Tartumaa""")
    ),
    pn.Row( folium_pane),
    pn.Row( pn.widgets.Tabulator(pd.DataFrame(schools.drop(columns=['geometry'])))) 
)
layout2.show()
template2 = pn.template.FastListTemplate(
    title='Folium in Panel Dashboard',
    sidebar=[],
    main=[layout2],
    accent_base_color="#88d8b0",
    header_background="#88d8b0",
)
template2.show()