## 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 importsimport datetime as dt# Third-party importsimport geopandas as gpdimport hvplot.pandasimport numpy as npimport pandas as pdimport panel as pnimport plotly.express as px# Initialize Panel with extensionspn.extension('plotly', design='material')
## visualise main columnsdf[['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 layoutinteractive_panel = pn.Column(date_slider, pn.bind(update_map_date, date_slider))# Serve the Panel dashboardinteractive_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 sliderslider = 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 parameterdef multiply_by_two(x):return x *2# Bind the function to the slider's valuebound_function = pn.bind(multiply_by_two, slider.param.value)# Create a layout that displays the slider and the output of the bound functionlayout = 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 citygroup_selector = pn.widgets.Select(name='Group', options=list(df.gname.unique()))# RangeSlider for selecting a numeric rangeyear_slider = pn.widgets.IntSlider(name='Year', start=df.year.min(), end=df.year.max(), step=1)# RangeSlider for selecting a numeric rangerange_slider = pn.widgets.RangeSlider(name='Nr of People Killed', start=df.nkill.min(), end=df.nkill.max(), step=1)# CheckBox for a boolean choicecheck_box = pn.widgets.Checkbox(name='Check Me')# TextInput for freeform inputtext_input = pn.widgets.TextInput(name='Enter Text')# RadioButtons for exclusive selectionradio_button = pn.widgets.RadioButtonGroup(name='Options', options=['Option 1', 'Option 2', 'Option 3'])
# Layout these widgets in a columnwidgets_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 mapcenter_lat = iraq_df['latitude'].mean()center_lon = iraq_df['longitude'].mean()
# Create a Select widget for the 'group' columngroup_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 figurereturn fig# Create a Panel layout to display the widget and the mapdashboard = pn.Column(group_selector, update_map_iraq)# Display the dashboarddashboard.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 countrycountries =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 herenew_country_selector = pn.widgets.Select(name='Country', options=countries)
We can add a scatter plot to the dashboard
# Plotdef 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 dashboarddashboard = 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 againimport geopandas as gpdimport hvplot.pandasimport numpy as npimport pandas as pdimport panel as pnimport plotly.express as px# Initialize Panel with extensionspn.extension('plotly', design='material')
# Function to create a map centered on the selected countryimport foliumfrom folium.plugins import MarkerClusterdef 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 eventfor 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 objectreturn 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 HTMLdef 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 HTMLreturn 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.