Interactive visualization of European NUTS administrative regions. Hover over regions to see NUTS ID and name, and use the dropdown to switch between NUTS levels (1/2/3).
Setup¶
import geopandas as gpd
import plotly.graph_objects as go
import plotly.express as px
from pathlib import Path
import pandas as pd
import json
import numpy as np
import matplotlib.colors as mcolors
import ipywidgets as widgets
from IPython.display import displayLoad NUTS Data¶
# Path to NUTS shapefile
data_dir = Path('../data')
nuts_shp = data_dir / 'regions' / 'NUTS_RG_20M_2024_4326' / 'NUTS_RG_20M_2024_4326.shp'
# Read shapefile
nuts_gdf = gpd.read_file(nuts_shp)
# Display basic info
print(f"Loaded {len(nuts_gdf)} NUTS regions")
print(f"\nColumns: {nuts_gdf.columns.tolist()}")Loaded 1798 NUTS regions
Columns: ['NUTS_ID', 'LEVL_CODE', 'CNTR_CODE', 'NAME_LATN', 'NUTS_NAME', 'MOUNT_TYPE', 'URBN_TYPE', 'COAST_TYPE', 'geometry']
Prepare Data for Visualization¶
# Extract country code from NUTS_ID (first 2 characters)
nuts_gdf['country'] = nuts_gdf['NUTS_ID'].str[:2]
# Determine NUTS level
if 'LEVL_CODE' in nuts_gdf.columns:
nuts_gdf['NUTS_level'] = nuts_gdf['LEVL_CODE']
else:
nuts_gdf['NUTS_level'] = nuts_gdf['NUTS_ID'].str.len() - 1
print(f"NUTS Levels available: {sorted(nuts_gdf['NUTS_level'].unique())}")
print(f"\nRegions per level:")
print(nuts_gdf['NUTS_level'].value_counts().sort_index())NUTS Levels available: [np.int32(0), np.int32(1), np.int32(2), np.int32(3)]
Regions per level:
NUTS_level
0 39
1 115
2 299
3 1345
Name: count, dtype: int64
Create Interactive Map¶
def create_choropleth(nuts_level):
"""Create an interactive choropleth map for a specific NUTS level."""
# Filter data by NUTS level
nuts_filtered = nuts_gdf[nuts_gdf['NUTS_level'] == nuts_level].copy()
nuts_filtered = nuts_filtered.reset_index(drop=True)
# Add a unique identifier for each region
nuts_filtered['region_id'] = nuts_filtered.index
# Country code to full name mapping
country_names = {
'AT': 'Austria', 'BE': 'Belgium', 'BG': 'Bulgaria', 'HR': 'Croatia', 'CY': 'Cyprus',
'CZ': 'Czechia', 'DK': 'Denmark', 'EE': 'Estonia', 'FI': 'Finland', 'FR': 'France',
'DE': 'Germany', 'GR': 'Greece', 'HU': 'Hungary', 'IE': 'Ireland', 'IT': 'Italy',
'LV': 'Latvia', 'LT': 'Lithuania', 'LU': 'Luxembourg', 'MT': 'Malta', 'NL': 'Netherlands',
'PL': 'Poland', 'PT': 'Portugal', 'RO': 'Romania', 'SK': 'Slovakia', 'SI': 'Slovenia',
'ES': 'Spain', 'SE': 'Sweden', 'GB': 'United Kingdom', 'EL': 'Greece', 'UK': 'United Kingdom'
}
# Build hover text and country name columns
hover_text = []
full_country_names = []
for _, row in nuts_filtered.iterrows():
country_code = row['country']
country_name = country_names.get(country_code, country_code)
nuts_id = row['NUTS_ID']
nuts_name = row.get('NAME_LATN', '')
text = f"<b>Country:</b> {country_name}<br>"
text += f"<b>NUTS ID:</b> {nuts_id}<br>"
if pd.notna(nuts_name) and isinstance(nuts_name, str) and nuts_name.strip():
text += f"<b>NUTS Name:</b> {nuts_name}"
hover_text.append(text)
full_country_names.append(country_name)
nuts_filtered['hover_text'] = hover_text
nuts_filtered['country_full_name'] = full_country_names
# Create color map for countries using Set1 colors
countries = sorted(nuts_filtered['country'].unique())
set1_colors = px.colors.qualitative.Set1
# Create numeric color codes based on country
country_to_num = {country: i for i, country in enumerate(countries)}
nuts_filtered['color_code'] = nuts_filtered['country'].map(country_to_num)
# Create GeoJSON with proper feature properties
features = []
for idx, row in nuts_filtered.iterrows():
feature = {
"type": "Feature",
"properties": {
"id": idx,
"NUTS_ID": row['NUTS_ID'],
"NAME": row.get('NAME_LATN', ''),
"country": row['country']
},
"geometry": row['geometry'].__geo_interface__
}
features.append(feature)
geojson = {"type": "FeatureCollection", "features": features}
# Create color scale for countries
colorscale = [set1_colors[i % len(set1_colors)] for i in range(len(countries))]
# Create the figure using graph_objects for better control
fig = go.Figure(data=go.Choroplethmapbox(
geojson=geojson,
locations=nuts_filtered['region_id'],
z=nuts_filtered['color_code'],
hovertext=nuts_filtered['hover_text'],
hoverinfo='text',
marker_line_width=0.5,
marker_line_color='white',
marker_opacity=0.9,
showscale=False,
colorscale=colorscale,
featureidkey='properties.id'
))
# Update layout with better zoom
fig.update_layout(
title=f'NUTS {nuts_level} Regions',
height=800,
mapbox=dict(
style='open-street-map',
center=dict(lat=54, lon=10),
zoom=4.5
),
hovermode='closest',
showlegend=False,
margin=dict(l=0, r=0, t=60, b=0)
)
return fig
# Get available NUTS levels
available_levels = sorted(nuts_gdf['NUTS_level'].unique())
print(f"Available NUTS levels: {available_levels}")Available NUTS levels: [np.int32(0), np.int32(1), np.int32(2), np.int32(3)]
Interactive Map with Level Selection¶
# Create dropdown for NUTS level selection
level_dropdown = widgets.Dropdown(
options={f'NUTS {level}': level for level in available_levels},
value=available_levels[0] if len(available_levels) > 0 else 1,
description='Select Level:',
style={'description_width': 'initial'}
)
# Create output area for the map
output = widgets.Output()
# Function to update map
def update_map(change):
output.clear_output(wait=True)
with output:
fig = create_choropleth(change['new'])
fig.show()
# Connect dropdown to update function
level_dropdown.observe(update_map, names='value')
# Display widgets
display(level_dropdown)
display(output)
# Show initial map
update_map({'new': level_dropdown.value})Loading...
Loading...