Hey Everyone! Check out my Week 16 Figure Friday contributionâ a pet journal-style dashboard! Itâs like a cool, interactive website where you can see which pets are the big winners in different countries and regions worldwide. The main map uses colors to show the most popular pet in each place (think dogs, cats, fish, and birds). Weâve also got another map showing how mixed the pet choices are in each country, using something called a diversity score (inspired by how scientists measure variety in nature). The neat part is, if you click on a country, a little window pops up with all sorts of info: the most common pet, how many of each pet people have, and how that compares to nearby countries and the whole world. Plus, there are easy charts to see it all. The dashboard looks like an old journal with soft colors, making it nice and simple to explore. So, if youâre curious about pet trends around the world, this is your interactive map to find out!
Comments/Suggestion More than Welcome
Best Regards
Code
import pandas as pd
import numpy as np
import dash
from dash import dcc, html, Input, Output, State
import plotly.express as px
import plotly.graph_objects as go
import dash_bootstrap_components as dbc
# ------------------------------------------------------------------------
# CONFIGURATION
# ------------------------------------------------------------------------
# Color scheme and consistent styling - Modified to more journal-like colors
PET_COLORS = {
'Dog': '#1A5276', # Darker blue
'Cat': '#922B21', # Darker red
'Fish': '#196F3D', # Darker green
'Bird': '#B9770E' # Darker orange/yellow
}
PET_ICONS = {
'Dog': 'đ¶', # Dog Face
'Cat': 'đ±', # Cat Face
'Fish': 'đ', # Fish
'Bird': 'đŠ' # Bird
}
PET_LABELS = {
'Dog': 'Dogs',
'Cat': 'Cats',
'Fish': 'Fish',
'Bird': 'Birds'
}
# Define regions once
REGION_MAP = {
'Europe': ['Germany', 'France', 'United Kingdom', 'Italy', 'Spain', 'Russia', 'Turkey','Czech Republic', 'Belgium', 'Swedem','Poland','Netherlands'],
'Americas': ['United States', 'Canada', 'Brazil', 'Mexico', 'Argentina'],
'Asia-Pacific': ['China', 'Japan', 'India', 'Australia', 'South Korea'],
}
# ------------------------------------------------------------------------
# DATA PREPARATION
# ------------------------------------------------------------------------
# Load data
df = pd.read_csv("pet_ownership_data_updated.csv").iloc[:22,:]
df["Country"] = df.Country.replace("USA", "United States").replace("UK", "United Kingdom")
# Calculate the shannon diversity index correctly
def calculate_shannon_index(row):
# Get values and filter zeros
values = [row['Dog'], row['Cat'], row['Fish'], row['Bird']]
values = [val for val in values if val > 0]
if not values:
return 0
# Normalize to ensure sum equals 100
total = sum(values)
proportions = [val/total for val in values]
# Calculate Shannon index
shannon = -sum(p * np.log(p) for p in proportions)
# Normalize to 0-100 scale
max_shannon = np.log(len(proportions)) if len(proportions) > 1 else 1
normalized = (shannon / max_shannon) * 100
return round(normalized, 1)
# Process and enrich data all at once
def prepare_data(df):
# Add predominant pet
df['Predominant_Pet'] = df[['Dog', 'Cat', 'Fish', 'Bird']].idxmax(axis=1)
# Add diversity index
df['Diversity_Index'] = df.apply(calculate_shannon_index, axis=1)
# Add region
for region, countries in REGION_MAP.items():
df.loc[df['Country'].isin(countries), 'Region'] = region
return df
df = prepare_data(df)
# Pre-calculate insights
def generate_insights(df):
insights = {}
# Pet summary counts
insights['pet_counts'] = {
pet: df[df['Predominant_Pet'] == pet].shape[0]
for pet in ['Dog', 'Cat', 'Fish', 'Bird']
}
# Regional analysis
insights['regional_data'] = df.groupby('Region').agg({
'Dog': 'mean',
'Cat': 'mean',
'Fish': 'mean',
'Bird': 'mean',
'Diversity_Index': 'mean'
}).reset_index()
# Additional insights
insights['region_predominant'] = insights['regional_data'].apply(
lambda x: x[['Dog', 'Cat', 'Fish', 'Bird']].idxmax(), axis=1
)
# Add global averages
insights['global_avg'] = {
pet: df[pet].mean() for pet in ['Dog', 'Cat', 'Fish', 'Bird']
}
return insights
insights = generate_insights(df)
# ------------------------------------------------------------------------
# APP INITIALIZATION
# ------------------------------------------------------------------------
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.JOURNAL])
app.title = "Global Pet Ownership Analysis"
# ------------------------------------------------------------------------
# LAYOUT - REDESIGNED FOR JOURNAL INFOGRAPHIC STYLE WITH IMPROVED ALIGNMENT
# ------------------------------------------------------------------------
app.layout = dbc.Container([
# Header with journal-style title and subtitle
html.Div([
html.H1("The World of Pets",
className="text-center mt-4 mb-0",
),
html.H2("An Analysis of Global Pet Preference Patterns",
className="text-center mb-2"),
html.Hr(style={"width": "40%", "margin": "auto", "marginBottom": "2rem", "border": "1px solid #666"}),
# Brief introduction - journal style
html.H5("Cultural, social, and regional factors shape our choices in animal companions. How pet ownership differs globally.",
className="text-center mb-4"),
], className="mb-5", style={"backgroundColor": "#f9f9f9", "padding": "20px 0"}),
# Main content section - Removed fixed heights for responsive design
dbc.Row([
# Map section
dbc.Col([
html.H3("Global Pet Preference Map",
className="mb-3"),
# Map controls with journal-style formatting
html.Div([
dcc.RadioItems(
id="map-color-option",
options=[
{'label': 'Predominant pet', 'value': 'predominant'},
{'label': 'Diversity index', 'value': 'diversity'}
],
value='predominant',
inline=True,
className="mb-2",
inputStyle={"marginRight": "5px"},
labelStyle={"marginRight": "15px", "fontSize": "0.9rem"}
),
# BotĂłn de informaciĂłn que aparece condicionalmente
html.Div(
id="diversity-info-container",
children=[
html.Button(
"What is Diversity Score?",
id="diversity-info-button",
className="btn btn-sm btn-outline-secondary ms-2",
style={"fontSize": "0.8rem"}
),
],
style={"display": "none"} # Inicialmente oculto
),
dcc.RadioItems(
id="map-type",
options=[
{'label': 'Natural Earth', 'value': 'natural earth'},
{'label': 'Orthographic', 'value': 'orthographic'}
],
value='natural earth',
inline=True,
className="mb-2 ms-4",
inputStyle={"marginRight": "5px"},
labelStyle={"marginRight": "15px", "fontSize": "0.9rem"}
),
], className="d-flex justify-content-center mb-3 align-items-center"),
# Añadimos el modal de explicación
dbc.Modal([
dbc.ModalHeader("Understanding the Diversity Score"),
dbc.ModalBody([
html.P([
"The Diversity Score measures how varied or balanced pet preferences are in a country or region, on a scale from 0 to 100."
]),
html.Hr(),
html.H6("How to interpret the score:", className="mt-3"),
html.Ul([
html.Li([
html.Strong("Low Score (0-30): "),
"Strong preference for one pet type. Most pet owners choose the same type of pet."
]),
html.Li([
html.Strong("Medium Score (30-70): "),
"Some diversity in preferences. There's a more balanced distribution between different pet types."
]),
html.Li([
html.Strong("High Score (70-100): "),
"High diversity. Pet ownership is distributed evenly across multiple pet types."
])
]),
html.Hr(),
html.P([
"This score is calculated using the Shannon Diversity Index, a metric commonly used in ecology to measure biodiversity, adapted to measure the diversity of pet preferences."
], className="mt-3 fst-italic text-muted")
]),
dbc.ModalFooter(
dbc.Button("Close", id="close-diversity-modal", className="ms-auto")
),
],id="diversity-modal",centered=True,
size="lg"),
# Map with invisible borders
html.Div([
dcc.Graph(id="world-map", style={"height": "480px"}
)
], style={"padding": "10px", "backgroundColor": "#f9f9f9", "boxShadow": "0 0 10px rgba(0,0,0,0.05)"})
], width=7, className="pe-4"),
# Country Spotlight with journal styling - Responsive design
dbc.Col([
html.H3("Country Spotlight",
className="mb-3"),
html.P("Click on a country on the map to see detailed analysis",
className="text-center mb-3 fst-italic"),
# Selected country information with journal-style formatting and no visible borders
html.Div([
html.H4(id="selected-country",
className="text-center mb-3"),
html.Div(id="country-profile", className="mb-3"),
html.Div(id="country-comparison", className="mb-3"),
html.Div(id="country-chart")
], style={"backgroundColor": "#f9f9f9", "padding": "20px", "borderRadius": "0", "boxShadow": "0 0 10px rgba(0,0,0,0.05)", "minHeight": "480px", "overflowY": "auto"})
], width=5, className="ps-4")
], className="mb-5"),
# Journal-style separator with quote
html.Div([
html.Hr(style={"width": "30%", "margin": "auto", "marginBottom": "15px", "marginTop": "15px"}),
html.P("The greatness of a nation and its moral progress can be judged by the way its animals are treated. â Mahatma Gandhi",
className="text-center fst-italic",
style={"fontSize": "1rem", "color": "#666", "maxWidth": "600px", "margin": "auto"}),
html.Hr(style={"width": "30%", "margin": "auto", "marginTop": "15px", "marginBottom": "40px"})
]),
# Second row: Global Pet Landscape and Regional Insights with journal styling
dbc.Row([
# Global statistics - journal style
dbc.Col([
html.H3("Global Pet Landscape",
className="mb-4"),
# Pet distribution with journal-style formatting and no visible borders
html.Div([
html.H5("Predominant Pet by Country",
className="mb-3",
),
html.Div([
*[html.Div([
html.Span(PET_ICONS[pet], style={"fontSize": "28px"}),
html.Span(f" {count} countries",
style={"fontSize": "16px", "marginLeft": "10px",
"color": PET_COLORS[pet]})
], className="me-4 d-inline-block") for pet, count in insights['pet_counts'].items()]
], className="d-flex justify-content-center mb-4"),
# Global averages visualization with journal styling
html.Div([
dcc.Graph(
figure=px.bar(
x=list(insights['global_avg'].keys()),
y=list(insights['global_avg'].values()),
color=list(insights['global_avg'].keys()),
color_discrete_map=PET_COLORS,
labels={'x': 'Pet Type', 'y': 'Global Average %'},
text=[f"{val:.1f}%" for val in insights['global_avg'].values()],
).update_layout(
showlegend=False,
margin=dict(l=40, r=40, t=30, b=40),
height=350,
title="Global Average (%) Pet Ownership",
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
).update_xaxes(
showgrid=False
).update_yaxes(
visible=False,
gridcolor='#eee'
)
)
], className="mb-4"),
], style={"backgroundColor": "#f9f9f9", "padding": "20px", "boxShadow": "0 0 10px rgba(0,0,0,0.05)", "minHeight": "530px"})
], md=6, className="pe-4"),
# Regional analysis with journal styling
dbc.Col([
html.H3("Regional Insights",
className="mb-4", ),
html.Div([
# Regional comparison graph with journal styling
dcc.Graph(
figure=px.bar(
insights['regional_data'].melt(
id_vars='Region',
value_vars=['Dog', 'Cat', 'Fish', 'Bird'],
var_name='Pet', value_name='Percentage'
),
x='Region', y='Percentage', color='Pet',
color_discrete_map=PET_COLORS,
barmode='group',
text_auto='.1f',
labels={'Percentage': 'Average %', 'Pet': 'Pet Type', 'Region':''},
title="Pet Preference Distribution by Region (Average %)"
).update_layout(
margin=dict(l=40, r=40, t=40, b=40),
showlegend=False,
# legend_title_text='Pet Type',
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
height=280
).update_xaxes(
showgrid=False
).update_yaxes(
visible=False,
gridcolor='#eee'
),
),
# Regional diversity index with journal styling
html.H5("Pet Diversity by Region (Diversity Score)",
className="mt-4 mb-3 text-center",
style={"fontFamily": "Georgia, serif"}),
dcc.Graph(
figure=px.bar(
insights['regional_data'],
x='Region', y='Diversity_Index',
color='Diversity_Index',
color_continuous_scale=px.colors.sequential.Viridis,
labels={'Diversity_Index': 'Diversity Score'},
text=[f"{val:.1f}" for val in insights['regional_data']['Diversity_Index']]
).update_layout(
showlegend=False,
coloraxis_showscale=False,
margin=dict(l=40, r=40, t=20, b=40),
height=175,
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
).update_xaxes(
showgrid=False
).update_yaxes(
visible=False,
gridcolor='#eee'
)
),
html.P([
html.Strong("Key finding: "),
"Europe shows the highest diversity in pet preferences, while the Americas display more pronounced preference for dogs."
], className="mt-3 text-center fst-italic")
], style={"backgroundColor": "#f9f9f9", "padding": "20px", "boxShadow": "0 0 10px rgba(0,0,0,0.05)", "minHeight": "530px"})
], md=6, className="ps-4")
], className="mb-5"),
# Footer with journal style citation
html.Div([
html.Hr(style={"width": "50%", "margin": "auto", "marginBottom": "20px"}),
html.P([
"© 2025 Global Pet Analysis",
html.Br(),
"Thank you to MakeoverMonday and GfK for the data.",
html.Br(),
"Analysis and visualization by The Journal of Pet Demographics"
], className="text-center", style={"fontSize": "0.9rem", "color": "#666"}),
html.Hr(style={"width": "50%", "margin": "auto", "marginBottom": "20px"})
], style={"marginTop": "20px",})
], fluid=True, style={
"backgroundColor": "#F5F5EA",
"color": "#0d0d0d",
"lineHeight": "1.6"
})
# Map callback - update for better responsiveness
@app.callback(
Output("world-map", "figure"),
[Input("map-color-option", "value"),
Input("map-type", "value")]
)
def update_map(color_option, map_type):
if color_option == 'predominant':
# Map colored by predominant pet
fig = px.choropleth(
df,
locations='Country',
labels={'Predominant_Pet':''},
locationmode='country names',
color='Predominant_Pet',
color_discrete_map=PET_COLORS,
hover_name='Country',
hover_data={
'Country': False,
'Predominant_Pet': True,
'Dog': ':.1f',
'Cat': ':.1f',
'Fish': ':.1f',
'Bird': ':.1f',
'Diversity_Index': ':.1f'
}
)
else: # diversity
# Map colored by diversity index
fig = px.choropleth(
df,
locations='Country',
locationmode='country names',
color='Diversity_Index',
color_continuous_scale=px.colors.sequential.Viridis,
range_color=[0, 100],
hover_name='Country',
hover_data={
'Country': False,
'Diversity_Index': ':.1f',
'Dog': ':.1f',
'Cat': ':.1f',
'Fish': ':.1f',
'Bird': ':.1f',
'Predominant_Pet': True
}
)
fig.update_layout(
geo=dict(
showframe=False,
showcoastlines=True,
projection_type=map_type
),
margin={"r":0,"t":0,"l":0,"b":0},
# font=dict(family="Georgia, serif"),
autosize=True,
coloraxis_colorbar=dict(len=0.5, thickness=20,orientation="h", y=-0.15, x=0.5, xanchor='center', title="Diversity Index"),
paper_bgcolor='rgb(249, 249, 249)',
plot_bgcolor='rgb(249, 249, 249)'
)
fig.update_geos(
showocean=True,
oceancolor="#EBF5FB",
showland=True,
landcolor="#F0F0E8",
showlakes=True,
lakecolor="#EBF5FB",
showcountries=True,
countrycolor="#BBBBBB",
bgcolor='rgb(249, 249, 249)'
)
return fig
# Country details callback - journal style formatting
@app.callback(
[Output("selected-country", "children"),
Output("country-profile", "children"),
Output("country-comparison", "children"),
Output("country-chart", "children")],
[Input("world-map", "clickData")]
)
def update_country_details(click_data):
if click_data is None:
return "Select a country on the map", [], [], []
# Extract country name and data
country_name = click_data['points'][0]['location']
country_data = df[df['Country'] == country_name].iloc[0]
# Get key data points
predominant_pet = country_data['Predominant_Pet']
diversity_index = country_data['Diversity_Index']
region = country_data['Region']
# Interpret diversity index
if diversity_index < 25:
diversity_interpretation = "Low diversity (strong preference for one pet type)"
elif diversity_index < 75:
diversity_interpretation = "Medium diversity (some variation in preferences)"
else:
diversity_interpretation = "High diversity (balanced preferences across pet types)"
# Create profile content with journal styling
profile = [
html.H5(f"Pet Ownership Profile",
className="mb-3"),
html.P([
f"{country_name} shows a ",
html.Strong(f"strong preference for {PET_LABELS[predominant_pet].lower()}",
style={"color": PET_COLORS[predominant_pet]}),
f", with {country_data[predominant_pet]:.1f}% of pet ownership."
]),
html.P([
f"The country has ",
html.Strong(diversity_interpretation),
f" (index: {diversity_index:.1f}/100)."
]),
# Regional context - new comparative information
html.P([
f"Compared to other {region} countries, {country_name}'s pet preference profile is ",
html.Strong("typical" if abs(country_data[predominant_pet] -
df[df['Region'] == region][predominant_pet].mean()) < 10
else "distinctive"),
"."
])
]
# Create comparison with global and regional averages
comparison_data = pd.DataFrame({
'Category': ['Dogs', 'Cats', 'Fish', 'Birds'],
'Country': [country_data['Dog'], country_data['Cat'],
country_data['Fish'], country_data['Bird']],
'Region Avg': [df[df['Region'] == region]['Dog'].mean(),
df[df['Region'] == region]['Cat'].mean(),
df[df['Region'] == region]['Fish'].mean(),
df[df['Region'] == region]['Bird'].mean()],
'Global Avg': [df['Dog'].mean(), df['Cat'].mean(),
df['Fish'].mean(), df['Bird'].mean()]
})
# Find notable differences with journal styling
notable = []
for pet in ['Dog', 'Cat', 'Fish', 'Bird']:
country_val = country_data[pet]
global_val = df[pet].mean()
diff = country_val - global_val
if abs(diff) > 15: # Significant difference threshold
direction = "higher" if diff > 0 else "lower"
notable.append(
html.Li(f"{PET_LABELS[pet]}: {abs(diff):.1f}% {direction} than global average",
style={"marginBottom": "5px"})
)
# Create comparison section with journal styling
comparison_section = [
html.H5("Notable Differences",
className="mt-4 mb-3"),
html.Ul(notable, style={"paddingLeft": "20px"}) if notable else
html.P("No significant deviations from global averages."
)
]
# Create chart with journal styling
fig = px.bar(
comparison_data,
x='Category', y=['Country', 'Region Avg', 'Global Avg'],
barmode='group',
labels={'value': 'Percentage', 'variable': ''},
title=f"Pet Preferences: {country_name} vs. Averages(%)",
color_discrete_sequence=['#5D6D7E', '#85929E', '#AEB6BF'] # Journal-like muted colors
)
fig.update_layout(
legend_title_text='',
margin=dict(l=40, r=40, t=40, b=40),
height=225,
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
autosize=True
)
fig.update_xaxes(showgrid=False)
fig.update_yaxes(gridcolor='#eee')
country_chart = dcc.Graph(figure=fig)
return f"{country_name}", profile, comparison_section, country_chart
@app.callback(
Output("diversity-info-container", "style"),
[Input("map-color-option", "value")]
)
def toggle_diversity_info_button(color_option):
if color_option == 'diversity':
return {"display": "inline-block"}
else:
return {"display": "none"}
# 2. Callback para abrir el modal cuando se hace clic en el botĂłn
@app.callback(
Output("diversity-modal", "is_open"),
[Input("diversity-info-button", "n_clicks"), Input("close-diversity-modal", "n_clicks")],
[State("diversity-modal", "is_open")]
)
def toggle_modal(n1, n2, is_open):
if n1 or n2:
return not is_open
return is_open
if __name__ == '__main__':
app.run_server(debug=True)