import dash
from dash import dcc, html, Input, Output
import dash_bootstrap_components as dbc
import plotly.graph_objects as go
import pandas as pd
import numpy as np
# --- Config ---
class Config:
CAT_GRADIENT = ["#f7b6d2", "#e07bb7", "#d05fa3", "#bc5090", "#7c2c5e"]
DOG_GRADIENT = ["#90caf9", "#42a5f5", "#1976d2", "#1565c0", "#0d47a1"]
PIE_HEIGHT = 420
PIE_WIDTH = 500
BAR_HEIGHT = 420
SCATTER_HEIGHT = 420 # <-- egységes magasság!
CAT_IMG = "https://png.pngtree.com/png-clipart/20230512/original/pngtree-isolated-cat-on-white-background-png-image_9158356.png"
DOG_IMG = "https://www.pngarts.com/files/3/Dog-Transparent-Images.png"
# --- Data ---
df = pd.read_csv(
"https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-16/pet_ownership_data_updated.csv"
)
df["Total"] = df["Cat"] + df["Dog"]
df_top5 = df.sort_values("Total", ascending=False).head(5).reset_index(drop=True)
# --- Stats ---
total_cat_owners = df_top5['Cat'].sum()
total_dog_owners = df_top5['Dog'].sum()
total_owners = total_cat_owners + total_dog_owners
cat_pct = total_cat_owners / total_owners * 100
dog_pct = total_dog_owners / total_owners * 100
# --- Chart Functions ---
def make_pie(animal, colors, pull_idx=None):
d = df_top5.sort_values(animal, ascending=False).reset_index(drop=True)
color_order = list(reversed(colors)) # legsötétebb az elsőhöz
pulls = [0.13 if i == pull_idx else 0 for i in range(len(d))]
insidetextfont = dict(color="white") if animal == "Dog" else None
fig = go.Figure(go.Pie(
labels=d["Country"],
values=d[animal],
hole=0.5,
marker=dict(colors=color_order),
pull=pulls,
textinfo="label+percent",
textposition="inside",
insidetextfont=insidetextfont,
hovertemplate="<b>%{label}</b><br>Owners: %{value:,}<extra></extra>",
))
img_url = Config.CAT_IMG if animal == "Cat" else Config.DOG_IMG
fig.add_layout_image(
dict(
source=img_url,
xref="paper", yref="paper",
x=0.5, y=0.5,
sizex=0.38, sizey=0.38,
xanchor="center", yanchor="middle",
layer="above", opacity=0.9
)
)
fig.update_layout(
title=f"{animal} Owners (Top 5 Countries)",
showlegend=False,
margin=dict(t=60, b=40, l=20, r=20),
height=Config.PIE_HEIGHT,
width=Config.PIE_WIDTH,
paper_bgcolor="#f7f7fa",
font=dict(family="Montserrat, Arial", color="#222")
)
return fig
def make_comparison_bar():
sorted_df = df_top5.sort_values('Total', ascending=True)
fig = go.Figure()
fig.add_trace(go.Bar(
y=sorted_df['Country'],
x=sorted_df['Cat'],
name='Cats',
orientation='h',
marker_color=Config.CAT_GRADIENT[3]
))
fig.add_trace(go.Bar(
y=sorted_df['Country'],
x=sorted_df['Dog'],
name='Dogs',
orientation='h',
marker_color=Config.DOG_GRADIENT[3]
))
fig.update_layout(
title='Cat vs Dog Ownership (Top 5 Countries)',
barmode='group',
height=Config.BAR_HEIGHT,
margin=dict(l=120, r=20, t=60, b=40), # egységes margó
xaxis_title='Number of Owners',
yaxis_title='Country',
showlegend=True,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
paper_bgcolor="#f7f7fa",
plot_bgcolor="#f7f7fa",
font=dict(family="Montserrat, Arial", color="#222")
)
return fig
def make_scatter_plot():
correlation = df_top5['Cat'].corr(df_top5['Dog'])
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df_top5['Cat'],
y=df_top5['Dog'],
mode='markers+text',
text=df_top5['Country'],
textposition='top center',
marker=dict(
size=16,
color=Config.DOG_GRADIENT[3],
line=dict(width=2, color=Config.CAT_GRADIENT[3])
),
name='Countries'
))
z = np.polyfit(df_top5['Cat'], df_top5['Dog'], 1)
p = np.poly1d(z)
x_range = np.linspace(df_top5['Cat'].min(), df_top5['Cat'].max(), 100)
fig.add_trace(go.Scatter(
x=x_range,
y=p(x_range),
mode='lines',
line=dict(color=Config.CAT_GRADIENT[3], dash='dash'),
name=f'Trend Line (r={correlation:.2f})'
))
fig.update_layout(
title='Cat vs Dog Ownership Correlation (Top 5)',
height=Config.SCATTER_HEIGHT,
margin=dict(l=120, r=20, t=60, b=40), # egységes margó
xaxis_title='Cat Owners',
yaxis_title='Dog Owners',
showlegend=True,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
paper_bgcolor="#f7f7fa",
plot_bgcolor="#f7f7fa",
font=dict(family="Montserrat, Arial", color="#222")
)
return fig
def create_stat_card(title, value, subtitle=None, gradient=None, text_color="#fff"):
return dbc.Card(
dbc.CardBody([
html.H4(title, className="mb-0", style={"fontSize": "1rem", "color": text_color}),
html.H2(value, className="mb-0", style={"color": text_color, "fontWeight": "bold"}),
html.P(subtitle if subtitle else "", style={"fontSize": "0.8rem", "color": text_color, "marginBottom": 0})
], style={"textAlign": "center", "background": gradient, "borderRadius": "1rem"}),
className="mb-3 shadow-sm",
style={"background": gradient, "border": "none", "borderRadius": "1rem"}
)
# --- App Layout ---
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.layout = dbc.Container(fluid=True, style={"maxWidth": "1200px", "background": "#f7f7fa"}, children=[
html.H2(
"Pet Ownership Dashboard – Top 5 Countries",
className="text-center my-4",
style={"fontFamily": "Montserrat, Arial", "color": "#003f5c"}
),
dbc.Row([
dbc.Col(
create_stat_card(
"Total Cat Owners (Top 5)",
f"{total_cat_owners:,.0f} ({cat_pct:.1f}%)",
"Sum of top 5",
gradient="linear-gradient(135deg, #bc5090 0%, #58508d 100%)",
text_color="#fff"
),
width=4
),
dbc.Col([], width=2, style={"minWidth": "10px"}),
dbc.Col(
create_stat_card(
"Total Dog Owners (Top 5)",
f"{total_dog_owners:,.0f} ({dog_pct:.1f}%)",
"Sum of top 5",
gradient="linear-gradient(135deg, #1976d2 0%, #0d47a1 100%)",
text_color="#fff"
),
width=4
),
], className="mb-4 justify-content-center align-items-center"),
dbc.Row([
dbc.Col([], width=1),
dbc.Col(dcc.Graph(id="cat-pie", config={"responsive": True}), width=5),
dbc.Col(dcc.Graph(id="dog-pie", config={"responsive": True}), width=5),
dbc.Col([], width=1),
], justify="center", className="mb-4"),
dbc.Row([
dbc.Col(dcc.Graph(figure=make_comparison_bar(), config={"responsive": True}), width=6),
dbc.Col(dcc.Graph(figure=make_scatter_plot(), config={"responsive": True}), width=6),
], justify="center", className="mb-4"),
html.P([
"Data source: ",
html.A("Figure Friday", href="https://github.com/plotly/Figure-Friday"),
], className="text-center small", style={"color": "#003f5c"})
])
# --- Callbacks for hover effect ---
@app.callback(Output("cat-pie", "figure"), Input("cat-pie", "hoverData"))
def animate_cat(hoverData):
idx = hoverData["points"][0]["pointNumber"] if hoverData and hoverData.get("points") else None
return make_pie("Cat", Config.CAT_GRADIENT, pull_idx=idx)
@app.callback(Output("dog-pie", "figure"), Input("dog-pie", "hoverData"))
def animate_dog(hoverData):
idx = hoverData["points"][0]["pointNumber"] if hoverData and hoverData.get("points") else None
return make_pie("Dog", Config.DOG_GRADIENT, pull_idx=idx)
if __name__ == "__main__":
app.run(debug=True)