8  Dashboards

Author

Gabriele Filomena

Published

April 20, 2026

The Lecture slides can be found here.

This lab’s notebook can be downloaded from here.

## Run only if necessary
!pip install panel
!pip install hvplot
Requirement already satisfied: hvplot in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (0.12.2)
Requirement already satisfied: bokeh>=3.1 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from hvplot) (3.8.2)
Requirement already satisfied: colorcet>=2 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from hvplot) (3.1.0)
Requirement already satisfied: holoviews>=1.19.0 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from hvplot) (1.22.1)
Requirement already satisfied: numpy>=1.21 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from hvplot) (2.3.5)
Requirement already satisfied: packaging in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from hvplot) (26.0)
Requirement already satisfied: pandas>=1.3 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from hvplot) (3.0.0)
Requirement already satisfied: panel>=1.0 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from hvplot) (1.8.7)
Requirement already satisfied: param<3.0,>=1.12.0 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from hvplot) (2.3.1)
Requirement already satisfied: Jinja2>=2.9 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from bokeh>=3.1->hvplot) (3.1.6)
Requirement already satisfied: contourpy>=1.2 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from bokeh>=3.1->hvplot) (1.3.3)
Requirement already satisfied: narwhals>=1.13 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from bokeh>=3.1->hvplot) (2.16.0)
Requirement already satisfied: pillow>=7.1.0 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from bokeh>=3.1->hvplot) (12.1.0)
Requirement already satisfied: PyYAML>=3.10 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from bokeh>=3.1->hvplot) (6.0.3)
Requirement already satisfied: tornado>=6.2 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from bokeh>=3.1->hvplot) (6.5.4)
Requirement already satisfied: xyzservices>=2021.09.1 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from bokeh>=3.1->hvplot) (2025.11.0)
Requirement already satisfied: python-dateutil>=2.8.2 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from holoviews>=1.19.0->hvplot) (2.9.0.post0)
Requirement already satisfied: pyviz-comms>=2.1 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from holoviews>=1.19.0->hvplot) (3.0.6)
Requirement already satisfied: MarkupSafe>=2.0 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from Jinja2>=2.9->bokeh>=3.1->hvplot) (3.0.3)
Requirement already satisfied: tzdata in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from pandas>=1.3->hvplot) (2025.3)
Requirement already satisfied: bleach in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from panel>=1.0->hvplot) (6.3.0)
Requirement already satisfied: linkify-it-py in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from panel>=1.0->hvplot) (2.0.3)
Requirement already satisfied: markdown in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from panel>=1.0->hvplot) (3.10.1)
Requirement already satisfied: markdown-it-py in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from panel>=1.0->hvplot) (4.0.0)
Requirement already satisfied: mdit-py-plugins in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from panel>=1.0->hvplot) (0.5.0)
Requirement already satisfied: requests in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from panel>=1.0->hvplot) (2.32.5)
Requirement already satisfied: tqdm in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from panel>=1.0->hvplot) (4.67.2)
Requirement already satisfied: typing-extensions in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from panel>=1.0->hvplot) (4.15.0)
Requirement already satisfied: six>=1.5 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from python-dateutil>=2.8.2->holoviews>=1.19.0->hvplot) (1.17.0)
Requirement already satisfied: webencodings in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from bleach->panel>=1.0->hvplot) (0.5.1)
Requirement already satisfied: uc-micro-py in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from linkify-it-py->panel>=1.0->hvplot) (1.0.3)
Requirement already satisfied: mdurl~=0.1 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from markdown-it-py->panel>=1.0->hvplot) (0.1.2)
Requirement already satisfied: charset_normalizer<4,>=2 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from requests->panel>=1.0->hvplot) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from requests->panel>=1.0->hvplot) (3.11)
Requirement already satisfied: urllib3<3,>=1.21.1 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from requests->panel>=1.0->hvplot) (2.6.3)
Requirement already satisfied: certifi>=2017.4.17 in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from requests->panel>=1.0->hvplot) (2026.1.4)
Requirement already satisfied: colorama in C:\Users\gfilo\AppData\Local\miniconda3\envs\geovis\Lib\site-packages (from tqdm->panel>=1.0->hvplot) (0.4.6)
# Standard library imports
import datetime as dt

# Third-party imports
import geopandas as gpd
import hvplot.pandas
import numpy as np
import pandas as pd
import panel as pn
import plotly.express as px

# Initialize Panel with extensions
pn.extension('plotly', design='material')

Importing the Data

df = pd.read_csv('../data/GTD_2022.csv', low_memory=False)
## visualise main columns
df[['gname', 'year', 'date', 'country_txt', 'nkill', 'nwound','weaptype1_txt']].head()
gname year date country_txt nkill nwound weaptype1_txt
0 Unknown 2005 28/05/2005 Pakistan 1.0 0.0 Firearms
1 Ansar al-Sunna 2005 29/05/2005 Iraq 1.0 0.0 Firearms
2 Al-Qaida in Iraq 2007 08/06/2007 Iraq 15.0 0.0 Firearms
3 Taliban 2010 15/06/2010 Afghanistan 1.0 0.0 Firearms
4 Communist Party of India - Maoist (CPI-Maoist) 2011 06/01/2011 India 1.0 0.0 Firearms

8.1 Creating a Basic Dashboard: Map + Date Slider

We are going to use Panel a Python library for creating interactive and dynamic web-based dashboards and applications. It allows data scientists, analysts, and so forth, to turn data sets into interactive dashboards using a wide array of widgets, plots, and layouts without requiring deep web development skills. Panel supports various plotting libraries like Matplotlib, Bokeh, and Plotly, as well as our friend Folium, and allows working with Pandas, NumPy, and others. It’s flexible enough to serve either as a standalone app or embedded in existing web applications, and it can be deployed easily. Have a look at this article for some insights on how to exploit Panel for nice data visualisations.

df['date']= pd.to_datetime(df['date'], dayfirst=True)
date_slider = pn.widgets.DateSlider(
    name='Date',
    start=df['date'].min(),
    end=df['date'].max(),
    value=df['date'].min()  # Single date value
)

df['date']= pd.to_datetime(df['date'], dayfirst=True).dt.date # this is not very handy but otherwise the slider would not communicate with the df

And here’s the slider

date_slider

Now, we create a function to update the map, on the basis of the slider.

def update_map_date(selected_date):
    # Filter the DataFrame based on the selected date
    filtered_df = df[df['date'] == selected_date].copy()
    # Create the scatter geo plot
    fig = px.scatter_geo(filtered_df, lat='latitude', lon='longitude')
    return fig
# Bind the update_map function to the date_slider, vertical layout
interactive_panel = pn.Column(date_slider, pn.bind(update_map_date, date_slider))

# Serve the Panel dashboard
interactive_panel.servable()

dashboard.servable() makes the dashboard “servable,” meaning it can be run as a standalone app or served via a notebook, depending on how you’re using Panel. When you call .servable() on a Panel object, you’re essentially telling Panel that this is the object you want to display when the dashboard is run. If you’re working in a Jupyter notebook, this allows the dashboard to be rendered inline.

8.1.1 The bind function

In Panel, pn.bind is a function that creates a dynamic link between widget values and a function, enabling interactive and reactive applications without the need for explicit callbacks or event handlers. When you use pn.bind, you’re essentially telling Panel to watch certain parameters (like the value of a widget) and call a specified function whenever those parameters change, automatically passing the new values to the function.

Here’s a breakdown of how pn.bind works:

  • pn.bind links a function to one or more parameters (often widget values). This function will be called whenever the linked parameters change.
  • When the linked parameters change, pn.bind automatically calls the specified function with the new values as arguments. This eliminates the need for manual event handling or value extraction within the function.
  • This binding creates a reactive link, meaning the output of the function will automatically update in the UI when the input parameters change. This is fundamental for creating interactive and dynamic dashboards.

Usage with Layouts: When you use pn.bind within a Panel layout (like pn.Column or pn.Row), the return value of the bound function is dynamically inserted into the layout. If the function returns a plot, a table, or any other visual component, that component will update in the UI whenever the function is triggered by a change in the bound parameters. Here’s a simple example to illustrate:

# Define a slider
slider = pn.widgets.IntSlider(name='Slider', start=0, end=10, value=5)

# Define a function that takes a parameter and returns a value based on that parameter
def multiply_by_two(x):
    return x * 2

# Bind the function to the slider's value
bound_function = pn.bind(multiply_by_two, slider.param.value)

# Create a layout that displays the slider and the output of the bound function
layout = pn.Column(slider, bound_function)
layout.servable()

pn.Column(...) creates a vertical layout (column) containing the row with certain elements/widgets/panes. pn.Column is used when you want to stack components vertically. Contrarily, pn.Row(): creates a horizontal layout (row). pn.Row is used when you want to place components side by side horizontally. In both cases you can add one or more elements.

8.2 Some More Widgets

Panel offers a range of widgets for interactive user inputs. These widgets can be used to receive input from users and update the dashboards/panels accordingly. Have a look here for a list of other widgets and usage examples.

# Select widget for choosing a city
group_selector = pn.widgets.Select(name='Group', options=list(df.gname.unique()))

# RangeSlider for selecting a numeric range
year_slider = pn.widgets.IntSlider(name='Year', start=df.year.min(), end=df.year.max(), step=1)

# RangeSlider for selecting a numeric range
range_slider = pn.widgets.RangeSlider(name='Nr of People Killed', start=df.nkill.min(), end=df.nkill.max(), step=1)

# CheckBox for a boolean choice
check_box = pn.widgets.Checkbox(name='Check Me')

# TextInput for freeform input
text_input = pn.widgets.TextInput(name='Enter Text')

# RadioButtons for exclusive selection
radio_button = pn.widgets.RadioButtonGroup(name='Options', options=['Option 1', 'Option 2', 'Option 3'])
# Layout these widgets in a column
widgets_column = pn.Column(group_selector, year_slider, check_box, text_input, radio_button)
widgets_column

8.2.1 Subsetting the Dataframe on the basis of categorical variables

# Create a dataset just for Iraq (feel free to change it)
iraq_df = df[df.country_txt == 'Iraq'].copy()
# Calculate the centroid of the filtered points for centering the map
center_lat = iraq_df['latitude'].mean()
center_lon = iraq_df['longitude'].mean()
# Create a Select widget for the 'group' column
group_selector = pn.widgets.Select(name='Group', options=iraq_df['gname'].unique().tolist())

# Define a function to update the map based on the selected group
@pn.depends(group_selector.param.value)
def update_map_iraq(selected_group):
    # Filter the DataFrame based on the selected group
    filtered_df = iraq_df[iraq_df['gname'] == selected_group].copy()
    
    # Generate the map
    fig = px.scatter_geo(filtered_df, lat='latitude', lon='longitude', title=f"Attacks carried out by: {selected_group}",
                        center={"lat": center_lat, "lon": center_lon})

    # Adjusting the map's view to a 'closer' zoom
    fig.update_geos(projection_type="natural earth", lataxis_range=[center_lat-10, center_lat+10], lonaxis_range=[center_lon-20, center_lon+20])
    # Return the figure
    return fig

# Create a Panel layout to display the widget and the map
dashboard = pn.Column(group_selector, update_map_iraq)

# Display the dashboard
dashboard.servable()

Plotly Express is here used to generate a geographical scatter plot.

px.scatter_geo: This creates a scatter plot on a geographic map. The arguments lat='latitude' and lon='longitude' specify the DataFrame columns that contain the latitude and longitude coordinates for the points to be plotted. The title argument sets the title of the map, and center specifies the central point of the map view, ensuring the map is centered around the points of interest. fig.update_geos updates the geographic layout of the figure. It’s used here to adjust the map’s projection and zoom level.

  • projection_type="natural earth": Sets the map’s projection type to “natural earth,” which is a visually appealing and commonly used projection for world maps.
  • lataxis_range=[center_lat-10, center_lat+10]: Defines the range of latitude to be displayed on the map. This setting zooms in on the region by limiting the latitude range to 10 degrees above and below the center latitude.
  • lonaxis_range=[center_lon-20, center_lon+20]: Defines the range of longitude to be displayed on the map. Similar to lataxis_range, this limits the longitude range to 20 degrees on either side of the center longitude, effectively zooming in on the area of interest.

We will be switching to folium in a bit, also for the sake of continuity and because it’s more powerful, so don’t worry to much about it.

columns = ['gname', 'year', 'date', 'country_txt', 'nkill', 'nwound','weaptype1_txt']

df = df.dropna(subset=['latitude', 'longitude'])
# Widget to select a country
countries = sorted(df['country_txt'].unique().tolist())
country_selector = pn.widgets.Select(name='Country', options=countries)

8.3 Complex Layouts

Layouts in Panel are used to organize widgets and plots in a structured manner. We are mainly working with pn.Row() and pn.Column() but familiarise yourself with other possible layouts (see here).

# still using the country selector here

new_country_selector = pn.widgets.Select(name='Country', options=countries)

We can add a scatter plot to the dashboard

# Plot
def update_scatter(data, width, height, title):
        return data.hvplot.scatter(
        x='year', 
        y=['nkill', 'nwound'],  # Ensure these column names match your DataFrame
        title=title,
        width=width,
        height=height
    )

And a function that updates several panels based on the country selector. See what happens when you select another country from the list.

@pn.depends(new_country_selector.param.value)
def update_dashboard(country):
    data = df[df['country_txt'] == country].copy()
    
    # Creating a plot for number of attacks over time
    plot = update_scatter(data, 800,300, 
                          title = f'Number of People Killed and Wounded Over Time in {country}').opts(legend_position='top_left')
    
    # Creating a summary table
    table = pn.widgets.DataFrame(data[columns], show_index=False, width=600)
    
    return pn.Column(plot, table)

# Layout the dashboard
dashboard = pn.Column(
    pn.Row(country_selector),
    update_dashboard
)

dashboard.servable()

8.3.1 Using Folium Maps

As mentioned, Panel allows us to incoporate folium maps.

# let's reset variables to avoid interactions between different functions/dashboards
%reset -f -s

# remporting again
import geopandas as gpd
import hvplot.pandas
import numpy as np
import pandas as pd
import panel as pn
import plotly.express as px

# Initialize Panel with extensions
pn.extension('plotly', design='material')
# reload the df
df = pd.read_csv('../data/GTD_2022.csv', low_memory=False)
df = df.dropna(subset=['latitude', 'longitude'])
countries = sorted(df['country_txt'].unique().tolist())
country_selector = pn.widgets.Select(name='Country', options=countries)
# Function to create a map centered on the selected country
import folium
from folium.plugins import MarkerCluster

def create_foliumMap(data):

    # Calculate the mean latitude and longitude to center the map
    center_lat = data['latitude'].mean()
    center_lon = data['longitude'].mean()

    # Create a Folium map centered on the average location
    folium_map = folium.Map(location=[center_lat, center_lon], zoom_start=6)

    # Use a MarkerCluster to add markers for each event
    marker_cluster = MarkerCluster().add_to(folium_map)

    # Add a marker for each event
    for idx, row in data.iterrows():
        folium.Marker(
            location=[row['latitude'], row['longitude']],
            popup=f"Date: {row['date']}<br>Deaths: {row['nkill']}",
        ).add_to(marker_cluster)

    # Return the Folium map object
    return folium_map

This function communicates with the one above for updating the attributes used to create the Folium map.

# Panel doesn't directly render Folium maps, so we need to render it as HTML
def update_map_country(df, country, width, height):
    # Filter the DataFrame for the selected country
    data = df[df['country_txt'] == country].copy()
    folium_map = create_foliumMap(data)
    # Panel doesn't directly render Folium maps, so we need to render it as HTML
    return pn.pane.HTML(folium_map._repr_html_(), width=width, height=height)

Then we bind the function to the widget and pass the DataFrame, along with the size attributes.

width = 700
height = 500
map_pane = pn.bind(update_map_country, df,  country_selector.param.value, width, height)

# Layout the dashboard
dashboard = pn.Column(
    pn.Row(country_selector),
    map_pane
)

dashboard.servable()