Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Explore NUTS Regions

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 display

Load 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...