import holoviews as hv
import hvplot.pandas
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,
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:
='mpg', y='hp'); df.plot.scatter(x
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:
='mpg', y='hp') df.hvplot.scatter(x
Other kinds of plots
Use tab completion to explore the available plot types.
<TAB> df.hvplot.
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
='mpg').hvplot(x='mpg', y='hp') df.sort_values(by
# histogram
'mpg', bins=100) df.hvplot.hist(
Graphics “grammar”
+
and *
- to have several plots (+
) or to plot overlays (*
)
'mpg', bins=10, width=300) + df.hvplot.scatter(x='mpg', y='hp', width=300) df.hvplot.hist(
Separate single country:
=='Asia'].hvplot.scatter(x='mpg', y='hp', by='origin') df[df.origin
Different plots with same “axes” on top of each other as overlay:
( =='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')
df[df.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
= "../files/data/L3/category_3_species_porijogi.gpkg"
species_fp = gpd.read_file(species_fp, layer='category_3_species_porijogi', driver='GPKG')
species_data 5)) display(species_data.head(
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()
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
'bokeh')
gv.extension(
= species_data.to_crs(4326)
species_data_wgs84
opts.defaults(opts.WMTS())
= gv.Points(species_data_wgs84, vdims=['LIIK', 'NIMI']).opts(tools=['hover']) data_view
* gvts.OSM).opts(width=600, height=400, projection=crs.Mercator()) (data_view
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
'tabulator', sizing_mode="stretch_width") pn.extension(
import hvplot.pandas
import holoviews as hv
'bokeh') hv.extension(
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[== 4) &
(df.cyl 'ford','chevrolet']))
(df.mfr.isin([
]'origin', 'cyl', 'mfr', 'yr'])['hp'].mean()
.groupby([
.to_frame()
.reset_index()='yr')
.sort_values(by1) ).head(
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.”
= df.interactive() idf
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.
= pn.widgets.IntSlider(name='Cylinders', start=4, end=8, step=2)
cylinders cylinders
= pn.widgets.ToggleGroup(
mfr ='MFR',
name=['ford', 'chevrolet', 'honda', 'toyota', 'audi'],
options=['ford', 'chevrolet', 'honda', 'toyota', 'audi'],
value='success')
button_type mfr
= pn.widgets.RadioButtonGroup(
yaxis ='Y axis',
name=['hp', 'weight'],
options='success'
button_type
) 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[== cylinders) &
(idf.cyl
(idf.mfr.isin(mfr))
]'origin', 'mpg'])[yaxis].mean()
.groupby([
.to_frame()
.reset_index()='mpg')
.sort_values(by=True)
.reset_index(drop
) ipipeline.head()
Pipe to Table
= ipipeline.pipe(pn.widgets.Tabulator, pagination='remote', page_size=10, theme="fast")
itable itable
Check out the Tabulator Reference Guide for more inspiration.
Pipe to hvplot
= ["#ff6f69", "#ffcc5c", "#88d8b0", ]
PALETTE
pn.Row(=50, styles={'background':PALETTE[0]}),
pn.layout.HSpacer(height=50, styles={'background':PALETTE[1]}),
pn.layout.HSpacer(height=50, styles={'background':PALETTE[2]}),
pn.layout.HSpacer(height )
= ipipeline.hvplot(x='mpg', y=yaxis, by='origin', color=PALETTE, line_width=6, height=400) ihvplot
Layout using Panel
= pn.Column(
layout
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
.
= pn.template.FastListTemplate(
template ='Interactive DataFrame Dashboards with hvplot .interactive',
title=[cylinders, 'Manufacturers', mfr, 'Y axis' , yaxis],
sidebar=[ihvplot.panel(), itable.panel()],
main="#88d8b0",
accent_base_color="#88d8b0",
header_background
)
; 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
= "../files/data/L6/population_square_km.shp"
grid_fp = "../files/data/L6/roads.shp"
roads_fp = "../files/data/L6/schools_tartu.shp"
schools_fp
# Read files
= gpd.read_file(grid_fp)
grid = gpd.read_file(roads_fp)
roads = gpd.read_file(schools_fp)
schools
# Re-project to WGS84
'geometry'] = grid['geometry'].to_crs(epsg=4326)
grid['geometry'] = roads['geometry'].to_crs(epsg=4326)
roads['geometry'] = schools['geometry'].to_crs(epsg=4326)
schools[
# Make a selection (only data above 0 and below 1000)
= grid.loc[(grid['Population'] > 0)]
grid
# Create a Geo-id which is needed by the Folium (it needs to have a unique identifier for each row)
'geoid'] = grid.index.astype(str)
grid['geoid'] = roads.index.astype(str)
roads['geoid'] = schools.index.astype(str)
schools[
# Select data
= grid[['geoid', 'Population', 'geometry']]
grid = roads[['geoid', 'TYYP', 'geometry']]
roads = schools[['geoid', 'name', 'geometry']]
schools
# convert the dataframe to geojson
= grid.to_json()
grid_jsontxt = roads.to_json()
roads_jsontxt = schools.to_json()
schools_jsontxt
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
= mc.NaturalBreaks.make(k=5)
classifier
'pop_km2'] = grid[['Population']].apply(classifier)
grid[
# basemap
= folium.Map(location=[58.37, 26.72],
m ='Stamen toner',
tiles=8,
zoom_start=True,
control_scale=True,
prefer_canvas=600,
width=450)
height
# coloured polygon layer
folium.Choropleth(=grid_jsontxt,
geo_data=grid,
data=['geoid', 'pop_km2'],
columns="feature.id",
key_on='RdBu',
fill_color=0.5,
fill_opacity=0.2,
line_opacity='white',
line_color=0,
line_weight='Population in Tartu',
legend_name='Population Grid',
name=False
highlight
).add_to(m)
= folium.FeatureGroup(name="circles layer")
circles_layer
# Calculate x and y coordinates of the line
'points_tuple'] = schools.apply(getPoints, geom='geometry', axis=1)
schools[
# the yellow school circles as reference
for idx, school in schools.iterrows():
=school['points_tuple'],
folium.CircleMarker(location=school['name'],
popup="yellow",
color=2.5,
radius=0.9).add_to(circles_layer)
opacity
circles_layer.add_to(m)
# the layer control switch
folium.LayerControl().add_to(m)
<folium.map.LayerControl at 0x7f29b139e350>
= pn.pane.plot.Folium(m, height=400) folium_pane
= pn.Column(
layout2
pn.Row("""# Schools in Tartumaa""")
pn.pane.Markdown(
),
pn.Row( folium_pane),=['geometry']))))
pn.Row( pn.widgets.Tabulator(pd.DataFrame(schools.drop(columns )
layout2.show()
= pn.template.FastListTemplate(
template2 ='Folium in Panel Dashboard',
title=[],
sidebar=[layout2],
main="#88d8b0",
accent_base_color="#88d8b0",
header_background )
template2.show()