import dash
from dash import dcc, html, Input, Output, dash_table, State
import dash_bootstrap_components as dbc
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics.pairwise import cosine_similarity
# Initialize Dash application with improved Bootstrap theme
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.FLATLY])
# Configure application title
app.title = "European Energy Dashboard"
# Load data from CSV and parse 'Date' column
df = pd.read_csv("euro_electricity_2016_2025.csv", parse_dates=['Date'])
# Get unique values for filters
countries = sorted(df['Area'].unique())
# Variables and units are now handled dynamically in callbacks
years = sorted(df['Date'].dt.year.unique())
units = sorted(df['Unit'].unique())
# --- ENHANCED DESIGN ---
app.layout = dbc.Container([
# Enhanced header
dbc.Row([
dbc.Col([
html.Div([
html.H1([
html.I(className="fas fa-bolt me-3"),
"European Energy Dashboard"
], className="text-center text-primary mb-3"),
html.P("Interactive analysis of electricity generations in Europe. 2016 - 2025",
className="text-center text-muted lead mb-4"),
html.Hr(className="border-secondary")
])
])
]),
# Enhanced control panel
dbc.Card([
dbc.CardHeader([
html.H5([
html.I(className="fas fa-cogs me-2"),
"Control Panel"
], className="mb-0 text-primary")
]),
dbc.CardBody([
dbc.Row([
dbc.Col([
html.Label([
html.I(className="fas fa-flag me-2"),
"Countries to Compare"
], className="fw-bold mb-2"),
dcc.Dropdown(
id='countries-radar',
options=[{'label': f"π³οΈ {c}", 'value': c} for c in countries],
value=countries[:3] if len(countries) >= 3 else countries,
multi=True,
placeholder="Select countries...",
className="mb-2"
),
html.Small(f"Available: {len(countries)} countries", className="text-muted")
], md=3),
dbc.Col([
html.Label([
html.I(className="fas fa-balance-scale me-2"), # Icon for unit
"Unit of Measure"
], className="fw-bold mb-2"),
dcc.Dropdown(
id='unit-selector', # New ID for the unit selector
options=[{'label': u, 'value': u} for u in units],
value='TWh', # Default value
clearable=False,
className="mb-2"
),
html.Small(f"Units: {', '.join(units)}", className="text-muted")
], md=1), # New column for the unit
dbc.Col([
html.Label([
html.I(className="fas fa-plug me-2"),
"Energy Variables"
], className="fw-bold mb-2"),
dcc.Dropdown(
id='variables-radar',
# Options and value will be set by a callback
options=[],
value=[],
multi=True,
placeholder="Select variables...",
className="mb-2"
),
html.Small(id='variables-count-info', className="text-muted") # Dynamic count
], md=3),
dbc.Col([
html.Label([
html.I(className="fas fa-calendar me-2"),
"Analysis Year"
], className="fw-bold mb-2"),
dcc.Dropdown(
id='year-radar',
options=[{'label': str(y), 'value': y} for y in years],
value=years[-2] if years else None,
placeholder="Select year...",
className="mb-2"
),
html.Small(f"Range: {min(years)}-{max(years)}", className="text-muted")
], md=1),
dbc.Col([
html.Label([
html.I(className="fas fa-chart-line me-2"),
"Normalization"
], className="fw-bold mb-2"),
dcc.Dropdown(
id='normalize',
options=[
{'label': 'π Scale 0-100', 'value': 'minmax'},
{'label': 'π Original Values', 'value': 'none'},
{'label': 'π Z-Score', 'value': 'zscore'}
],
value='minmax',
className="mb-2"
)
], md=3),
dbc.Col([
html.Label([
html.I(className="fas fa-info-circle me-2"),
"Help"
], className="fw-bold mb-2"),
dbc.Button([
html.I(className="fas fa-question-circle me-2"),
"Info"
], id="info-button", color="info", size="sm", className="w-100")
], md=1)
])
])
], className="mb-4 shadow-sm"),
# Summary metrics
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
html.Div(id="summary-metrics")
])
], className="border-0 bg-light")
])
], className="mb-4"),
# Main charts
dbc.Row([
# Enhanced Radar chart
dbc.Col([
dbc.Card([
dbc.CardHeader([
html.H5([
html.I(className="fas fa-radar-dish me-2"),
"Energy Profiles - Radar"
], className="mb-0")
]),
dbc.CardBody([
dcc.Loading([
dcc.Graph(id='radar-chart', style={'height': '650px'})
], type="circle", color="success")
])
], className="shadow-sm")
], lg=8),
# Enhanced side panel
dbc.Col([
# Rankings
dbc.Card([
dbc.CardHeader([
html.H6([
html.I(className="fas fa-trophy me-2"),
"Country Ranking"
], className="mb-0")
]),
dbc.CardBody([
html.Div(id='rankings-table')
])
], className="mb-3 shadow-sm"),
# Detailed statistics
dbc.Card([
dbc.CardHeader([
html.H6([
html.I(className="fas fa-chart-bar me-2"),
"Key Statistics"
], className="mb-0")
]),
dbc.CardBody([
html.Div(id='stats-info')
])
], className="shadow-sm")
], lg=4)
], className="mb-4"),
# Enhanced secondary charts
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader([
html.H5([
html.I(className="fas fa-chart-line me-2"),
"Time Evolution"
], className="mb-0")
]),
dbc.CardBody([
dcc.Loading([
dcc.Graph(id='timeseries-chart', style={'height': '450px'})
], type="circle", color="secondary")
])
], className="shadow-sm")
], lg=6),
dbc.Col([
dbc.Card([
dbc.CardHeader([
html.H5([
html.I(className="fas fa-project-diagram me-2"),
"Similarity Matrix"
], className="mb-0")
]),
dbc.CardBody([
dcc.Loading([
dcc.Graph(id='similarity-chart', style={'height': '450px'})
], type="circle", color="secondary")
])
], className="shadow-sm")
], lg=6)
], className="mb-4"),
# Footer
dbc.Row([
dbc.Col([
html.Hr(),
html.P([
"Dashboard developed using Plotly | Dash ",
html.I(className="fas fa-heart text-danger"),
" Data source EMBER"
], className="text-center text-muted small")
])
]),
# Information Modal
dbc.Modal([
dbc.ModalHeader(dbc.ModalTitle([
html.I(className="fas fa-info-circle me-2"),
"Dashboard Information Guide"
])),
dbc.ModalBody([
html.H5([
html.I(className="fas fa-chart-line me-2 text-primary"),
"Normalization Methods"
], className="mb-3"),
dbc.Card([
dbc.CardBody([
html.H6("π Scale 0-100 (MinMax)", className="text-success mb-2"),
html.P([
html.Strong("What it does: "),
"Converts all values to a 0-100 scale for fair comparison."
], className="mb-2 small"),
html.P([
html.Strong("Best for: "),
"Comparing energy profiles between countries in radar charts."
], className="mb-2 small"),
html.P([
html.Strong("Example: "),
"Germany's 200 TWh solar and Malta's 2 TWh both show proportionally in their contexts."
], className="mb-0 small text-muted")
])
], className="mb-3 border-start border-success border-3"),
dbc.Card([
dbc.CardBody([
html.H6("π Original Values", className="text-info mb-2"),
html.P([
html.Strong("What it does: "),
"Shows real energy generation or emission values in the selected unit."
], className="mb-2 small"),
html.P([
html.Strong("Best for: "),
"Business decisions, capacity planning, and executive reports."
], className="mb-2 small"),
html.P([
html.Strong("Example: "),
"See exactly how many TWh each country produces or how many MtCO2e it emits."
], className="mb-0 small text-muted")
])
], className="mb-3 border-start border-info border-3"),
dbc.Card([
dbc.CardBody([
html.H6("π Z-Score (Standardization)", className="text-warning mb-2"),
html.P([
html.Strong("What it does: "),
"Shows how many standard deviations each value is from the average."
], className="mb-2 small"),
html.P([
html.Strong("Best for: "),
"Detecting exceptional countries (outliers) and statistical analysis."
], className="mb-2 small"),
html.P([
html.Strong("Example: "),
"A country with +2 Z-score is 2 standard deviations above European average."
], className="mb-0 small text-muted")
])
], className="mb-4 border-start border-warning border-3"),
html.Hr(),
html.H5([
html.I(className="fas fa-project-diagram me-2 text-primary"),
"Similarity Matrix (Cosine Similarity)"
], className="mb-3"),
dbc.Card([
dbc.CardBody([
html.P([
html.Strong("What it measures: "),
"How similar the energy or emission profiles are between countries (0 = completely different, 1 = identical)."
], className="mb-2 small"),
html.P([
html.Strong("How it works: "),
"Compares the 'shape' of energy/emission portfolios, not just the size."
], className="mb-2 small"),
html.P([
html.Strong("Practical use: "),
"Find countries with similar renewable energy or emission strategies for partnerships or benchmarking."
], className="mb-2 small"),
html.P([
html.Strong("Example: "),
"Two countries with 0.95 similarity have very similar energy/emission mix patterns, regardless of total generation."
], className="mb-0 small text-muted")
])
], className="border-start border-secondary border-3")
]),
dbc.ModalFooter([
dbc.Button("Close", id="close-info", className="ms-auto", color="secondary", size="sm")
])
], id="info-modal", size="lg", is_open=False)
], fluid=True, className="px-4", style={
'background': 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 50%, #dee2e6 100%)',
'min-height': '100vh'}
)
# Define consistent color palette for countries
def get_country_color_map(countries):
"""Create consistent color mapping for countries across all charts."""
colors = px.colors.qualitative.Set1 + px.colors.qualitative.Set2 + px.colors.qualitative.Set3
return {country: colors[i % len(colors)] for i, country in enumerate(sorted(countries))}
# Define symbols for energy variables
def get_variable_symbol_map(variables):
"""Create symbol mapping for energy variables."""
symbols = ['circle', 'square', 'diamond', 'cross', 'x', 'triangle-up', 'triangle-down', 'star', 'hexagon', 'pentagon']
return {var: symbols[i % len(symbols)] for i, var in enumerate(sorted(variables))}
# --- ENHANCED CALLBACKS ---
@app.callback(
Output("info-modal", "is_open"),
[Input("info-button", "n_clicks"), Input("close-info", "n_clicks")],
[State("info-modal", "is_open")],
)
def toggle_modal(n1, n2, is_open):
"""Toggles the visibility of the information modal."""
if n1 or n2:
return not is_open
return is_open
# NEW CALLBACK: Update 'variables-radar' options based on 'unit-selector'
@app.callback(
[Output('variables-radar', 'options'),
Output('variables-radar', 'value'),
Output('variables-count-info', 'children')],
[Input('unit-selector', 'value')]
)
def set_variables_options(selected_unit):
"""
Updates the options and selected values for the 'Energy Variables' dropdown
based on the selected 'Unit of Measure'.
"""
if not selected_unit:
return [], [], "Available: 0 variables"
# Filter variables based on the selected unit
# This is the key line to ensure only relevant variables are shown
unit_specific_variables = sorted(df[df['Unit'] == selected_unit]['Variable'].unique())
# Create options with unit appended to the variable name for clarity
options = [{'label': f"β‘ {v} ({selected_unit})", 'value': v} for v in unit_specific_variables]
# Set default selected values for the variables dropdown
default_value = []
if selected_unit == 'TWh':
# Select some common energy generation variables for TWh
relevant_vars = [v for v in unit_specific_variables if 'Generation' in v or 'Hydro' in v or 'Nuclear' in v or 'Demand' in v]
default_value = relevant_vars[:3] if len(relevant_vars) >= 3 else relevant_vars
elif selected_unit == 'MtCO2e':
# Select some common emission variables for MtCO2e
relevant_vars = [v for v in unit_specific_variables if 'Emissions' in v or 'Intensity' in v]
default_value = relevant_vars[:3] if len(relevant_vars) >= 3 else relevant_vars
# Fallback if specific relevant_vars are not found or fewer than 3
if not default_value and unit_specific_variables:
default_value = unit_specific_variables[:3] if len(unit_specific_variables) >= 3 else unit_specific_variables
return options, default_value, f"Available: {len(unit_specific_variables)} variables"
@app.callback(
[Output('radar-chart', 'figure'),
Output('rankings-table', 'children'),
Output('stats-info', 'children'),
Output('timeseries-chart', 'figure'),
Output('similarity-chart', 'figure'),
Output('summary-metrics', 'children')],
[Input('countries-radar', 'value'),
Input('variables-radar', 'value'),
Input('year-radar', 'value'),
Input('normalize', 'value'),
Input('unit-selector', 'value')]
)
def update_dashboard(countries_selected, variables_selected, year_selected, normalize_option, unit_selected):
"""
Updates all charts and metrics in the dashboard based on user selections.
"""
# Default empty figures for when no data or incomplete selection
empty_fig = go.Figure().add_annotation(
text="β οΈ Select countries, variables, year, and unit to visualize the data",
xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False,
font=dict(size=16, color="gray")
)
empty_fig.update_layout(
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
xaxis={'visible': False},
yaxis={'visible': False}
)
# Input validation: if selections are missing, show warning messages
if not countries_selected or not variables_selected or not year_selected or not unit_selected:
return empty_fig, "β οΈ Select the necessary parameters", "π No data", empty_fig, empty_fig, "β οΈ No metrics available"
# Filter data based on user selections, including the unit
filtered_df = df[
(df['Variable'].isin(variables_selected)) &
(df['Date'].dt.year == year_selected) &
(df['Area'].isin(countries_selected)) &
(df['Unit'] == unit_selected) # Filter by the selected unit!
]
# If the filtered DataFrame is empty, show no data messages
if filtered_df.empty:
return empty_fig, "β No data for current selection", "π No statistics", empty_fig, empty_fig, "β No metrics"
# Create a pivot table for the radar chart and similarity matrix
# Fill missing values with 0 and ensure all selected variables are present
radar_data = filtered_df.pivot_table(
index='Area',
columns='Variable',
values='Value',
fill_value=0
).reindex(columns=variables_selected, fill_value=0)
# Apply the selected normalization
radar_data_scaled = radar_data.copy()
if normalize_option == 'minmax':
scaler = MinMaxScaler(feature_range=(0, 100))
# Scale only columns with variance > 0 to avoid errors
cols_to_scale = radar_data.columns[radar_data.var() > 0]
if not cols_to_scale.empty:
radar_data_scaled[cols_to_scale] = scaler.fit_transform(radar_data[cols_to_scale])
# Assign a default value for columns with 0 variance (all values are the same)
for col in radar_data.columns:
if col not in cols_to_scale:
# If the original value is 0, it remains 0; otherwise, it's set to 50 (midpoint)
radar_data_scaled[col] = 50 if radar_data[col].iloc[0] != 0 else 0
elif normalize_option == 'zscore':
# Calculate the Z-score
radar_data_scaled = ((radar_data - radar_data.mean()) / radar_data.std()).fillna(0)
# Rescale Z-scores to a 0-100 range for better visualization on the radar
# Avoid division by zero if max == min
for col in radar_data_scaled.columns:
col_min = radar_data_scaled[col].min()
col_max = radar_data_scaled[col].max()
if col_max - col_min > 0:
radar_data_scaled[col] = (radar_data_scaled[col] - col_min) / (col_max - col_min) * 100
else:
radar_data_scaled[col] = 50 if radar_data[col].iloc[0] != 0 else 0 # If all values are equal, assign 50 or 0
# Ensure no NaNs and that values are non-negative
radar_data_scaled = radar_data_scaled.fillna(0).clip(lower=0)
# Create consistent color and symbol mappings
country_colors = get_country_color_map(countries_selected)
variable_symbols = get_variable_symbol_map(variables_selected)
# 1. ENHANCED RADAR CHART
radar_fig = go.Figure()
for country in countries_selected:
if country in radar_data_scaled.index:
values = radar_data_scaled.loc[country].values.tolist()
values += [values[0]] # Close the radar shape
# Determine the unit suffix for the hover
unit_suffix = '%' if normalize_option in ['minmax', 'zscore'] else f' {unit_selected}'
radar_fig.add_trace(go.Scatterpolar(
r=values,
theta=variables_selected + [variables_selected[0]],
fill='toself',
fillcolor=country_colors[country],
line=dict(color=country_colors[country], width=3),
opacity=0.6,
name=f"π³οΈ {country}",
hovertemplate=f"<b>{country}</b><br>%{{theta}}: %{{r:.1f}}{unit_suffix}<br><extra></extra>"
))
# Enhanced radar configuration
# Max range depends on normalization or original values
max_range = 100 if normalize_option in ['minmax', 'zscore'] else radar_data_scaled.values.max() * 1.1 if not radar_data_scaled.empty else 100
if max_range == 0: # Avoid [0,0] range if all values are 0
max_range = 100
radar_fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, max_range],
# Unit suffix on the radial axis
ticksuffix="%" if normalize_option in ['minmax', 'zscore'] else f" {unit_selected}",
gridcolor="rgba(128,128,128,0.3)",
linecolor="rgba(128,128,128,0.5)",
tickfont=dict(size=10)
),
angularaxis=dict(
# Use original variable names for angular axis, as they are the 'categories'
tickvals=[v for v in variables_selected], # Ensure tickvals are just the variable names
ticktext=[f"{v} ({unit_selected})" for v in variables_selected], # Add unit to display text
tickfont=dict(size=11, color="darkblue"),
rotation=90,
direction="clockwise",
linecolor="rgba(128,128,128,0.5)"
),
bgcolor="rgba(248,249,250,0.8)"
),
showlegend=True,
title=dict(
text=f"{unit_selected} Profiles in Europe - {year_selected}",
x=0.5,
font=dict(size=16, color="darkblue")
),
font=dict(size=11),
legend=dict(
orientation="h",
yanchor="bottom",
y=-0.25,
xanchor="center",
x=0.5,
bgcolor="rgba(255,255,255,0.8)"
),
margin=dict(t=80, b=80, l=50, r=50)
)
# 2. ENHANCED RANKINGS TABLE
# Ranking is based on normalized values for a fair comparison of profiles
country_scores = radar_data_scaled.mean(axis=1).sort_values(ascending=False)
rankings = []
medals = ["οΏ½", "π₯", "π₯"]
for rank, (country, score) in enumerate(country_scores.items(), 1):
rankings.append({
'rank': rank,
'medal': medals[rank-1] if rank <= 3 else f"#{rank}",
'country': country,
'score': f"{score:.1f}",
'performance': "Excellent" if score >= 80 else "Good" if score >= 60 else "Regular"
})
rankings_table = dbc.Table([
html.Thead([
html.Tr([
html.Th("", style={'width': '15%'}),
html.Th("Country", style={'width': '40%'}),
html.Th("Score", style={'width': '25%'}),
html.Th("Level", style={'width': '20%'})
])
]),
html.Tbody([
html.Tr([
html.Td(r['medal'], style={'text-align': 'center', 'font-size': '1.2em'}),
html.Td(r['country'], style={'font-weight': 'bold'}),
html.Td(r['score'], style={'text-align': 'center'}),
html.Td(
dbc.Badge(r['performance'],
color="success" if r['performance'] == "Excellent"
else "warning" if r['performance'] == "Good"
else "secondary",
className="w-100")
)
], style={'background-color': '#f8f9fa' if r['rank'] <= 3 else 'transparent'})
for r in rankings
])
], striped=True, hover=True, size="sm")
# 3. ENHANCED KEY STATISTICS
stats_cards = []
# Limit to the first 3 selected variables to show statistics
for var in variables_selected[:3]:
# Filter by the current variable AND the selected unit
var_data = filtered_df[(filtered_df['Variable'] == var) & (filtered_df['Unit'] == unit_selected)]
if not var_data.empty:
top_country = var_data.loc[var_data['Value'].idxmax()]['Area']
top_value = var_data['Value'].max()
avg_value = var_data['Value'].mean()
stats_cards.append(
dbc.Card([
dbc.CardBody([
html.H6([
html.I(className="fas fa-bolt me-2 text-warning"),
f"{var} ({unit_selected})" # Show variable with unit
], className="card-title text-primary mb-2"),
html.P([
html.Strong("Leader: "), f"{top_country}"
], className="mb-1 small"),
html.P([
html.Strong("Maximum: "), f"{top_value:.1f} {unit_selected}" # Show the correct unit
], className="mb-1 small"),
html.P([
html.Strong("Average: "), f"{avg_value:.1f} {unit_selected}" # Show the correct unit
], className="mb-0 small text-muted")
])
], className="mb-2 border-start border-secondary border-3")
)
# 4. ENHANCED TIME SERIES
# Filter by countries, variables AND the selected unit for the time series
timeseries_data = df[
(df['Area'].isin(countries_selected)) &
(df['Variable'].isin(variables_selected)) &
(df['Unit'] == unit_selected) # Filter by the selected unit!
].copy()
timeseries_data['Year'] = timeseries_data['Date'].dt.year
# Group by country, variable, and year, calculating the average of the value
timeseries_agg = timeseries_data.groupby(['Area', 'Variable', 'Year'])['Value'].mean().reset_index()
timeseries_fig = go.Figure()
# Keep track of variables already added to the legend to avoid duplicates
variables_in_legend = set()
for country in countries_selected:
country_data = timeseries_agg[timeseries_agg['Area'] == country]
for var in variables_selected:
var_data = country_data[country_data['Variable'] == var]
if not var_data.empty:
# Show in legend only the first occurrence of each variable
show_legend = var not in variables_in_legend
if show_legend:
variables_in_legend.add(var)
timeseries_fig.add_trace(go.Scatter(
x=var_data['Year'],
y=var_data['Value'],
mode='lines+markers',
name=f"{var} ({unit_selected})", # Append unit to variable name in legend
legendgroup=var, # Group by variable
showlegend=show_legend, # Show only once per variable
line=dict(
color=country_colors[country],
width=1.5
),
marker=dict(
size=10,
symbol=variable_symbols[var],
color=country_colors[country],
line=dict(color='black', width=1) # Black border for markers
),
hovertemplate=f"<b>{country} - {var} ({unit_selected})</b><br>Year: %{{x}}<br>Value: %{{y:.1f}} {unit_selected}<extra></extra>" # Show the correct unit
))
# Add invisible traces only for legend symbols
for var in variables_selected:
timeseries_fig.add_trace(go.Scatter(
x=[None], y=[None],
mode='markers',
marker=dict(
size=10,
symbol=variable_symbols[var],
color='lightgray', # Neutral color for legend
line=dict(color='black', width=1)
),
name=f"{var} ({unit_selected})", # Append unit to variable name in legend
showlegend=True,
legendgroup=f"legend_{var}",
hoverinfo='skip'
))
# Hide the first set of traces from legend (the ones with country color)
for trace in timeseries_fig.data[:-len(variables_selected)]:
trace.showlegend = False
# Enhanced temporal chart configuration
timeseries_fig.update_layout(
title=dict(
text=f"Historical {unit_selected} Trends (2016-2025)",
x=0.5,
font=dict(size=14)
),
xaxis=dict(
title="Year",
gridcolor="rgba(128,128,128,0.2)",
showgrid=True
),
yaxis=dict(
title=f"{unit_selected}", # Y-axis title with the correct unit
gridcolor="rgba(128,128,128,0.2)",
showgrid=True
),
hovermode='x unified',
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="center",
x=0.5,
bgcolor="rgba(255,255,255,0.8)",
bordercolor="rgba(128,128,128,0.5)",
borderwidth=1
),
plot_bgcolor='rgba(248,249,250,0.8)',
margin=dict(t=100, b=50, l=60, r=50) # More top margin for legend
)
# 5. ENHANCED SIMILARITY MATRIX
if radar_data_scaled.shape[0] < 2:
similarity_fig = go.Figure().add_annotation(
text="At least 2 countries needed to calculate similarities",
xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False,
font=dict(size=14, color="gray")
)
else:
# Calculate cosine similarity between scaled data profiles
similarity_matrix = cosine_similarity(radar_data_scaled)
# Create annotations to show values on the heatmap
annotations = []
for i, country1 in enumerate(radar_data_scaled.index):
for j, country2 in enumerate(radar_data_scaled.index):
annotations.append(
dict(
x=country2, y=country1,
text=f"{similarity_matrix[i,j]:.2f}",
showarrow=False,
font=dict(color="white" if similarity_matrix[i,j] < 0.5 else "black", size=10)
)
)
similarity_fig = go.Figure(data=go.Heatmap(
z=similarity_matrix,
x=list(radar_data_scaled.index),
y=list(radar_data_scaled.index),
colorscale='RdYlGn', # Red-Yellow-Green color scale
zmin=0, zmax=1, # Range from 0 to 1 for cosine similarity
hoverongaps=False,
hovertemplate='<b>%{y}</b> vs <b>%{x}</b><br>Similarity: %{z:.3f}<extra></extra>',
colorbar=dict(
title="Similarity",
tickmode="linear",
tick0=0,
dtick=0.2
)
))
similarity_fig.update_layout(
title=dict(
text=f"{unit_selected} Profile Similarity Analysis - {year_selected}", # Title with the correct unit
x=0.5,
font=dict(size=14)
),
xaxis=dict(title="Countries", side='bottom'),
yaxis=dict(title="Countries", autorange='reversed'),
annotations=annotations,
margin=dict(t=60, b=50, l=80, r=80)
)
# 6. SUMMARY METRICS
total_countries = len(countries_selected)
total_variables = len(variables_selected)
# Calculate the average generation/emission for the selected unit
avg_value_summary = filtered_df['Value'].mean()
summary_metrics = dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H4(str(total_countries), className="text-primary mb-0"),
html.P("Countries", className="text-muted mb-0 small")
])
], className="text-center border-0 bg-secondary bg-opacity-10")
], md=3),
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H4(str(total_variables), className="text-success mb-0"),
html.P("Variables", className="text-muted mb-0 small")
])
], className="text-center border-0 bg-success bg-opacity-10")
], md=3),
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H4(f"{avg_value_summary:.1f}", className="text-warning mb-0"),
html.P(f"{unit_selected} Average", className="text-muted mb-0 small") # Show the correct unit
])
], className="text-center border-0 bg-warning bg-opacity-10")
], md=3),
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H4(str(year_selected), className="text-info mb-0"),
html.P("Year Analyzed", className="text-muted mb-0 small")
])
], className="text-center border-0 bg-info bg-opacity-10")
], md=3)
])
return radar_fig, rankings_table, stats_cards, timeseries_fig, similarity_fig, summary_metrics