Figure Friday 2025 - week 16

join the Figure Friday session on April 25, at noon Eastern Time, to showcase your creation and receive feedback from the community.

What percentage of Brazilians own a dog? Is it higher than Argentinians dog ownership?

Try to answer these and several other questions by using Plotly and Dash to visualize the pet ownership dataset.

This small dataset is a good opportunity to focus on building an infographic style dashboard:

Things to consider:

  • what can you improve in the app or sample figure below (bubble chart)?
  • would you like to tell a different data story using a different graph?
  • can you create a different Dash app?

Sample figure:

Code for sample figure:
from dash import Dash, dcc
import dash_ag_grid as dag
import plotly.express as px
import pandas as pd


df = pd.read_csv('https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-16/pet_ownership_data_updated.csv')
df = df.head(10)
fig = px.scatter(df, x='Country', y='Cat', size=df['Dog']**2, size_max=60, custom_data=['Dog'])
fig.update_traces(hovertemplate='<b>Country</b>: %{x}<br><b>Cat</b>: %{y}<br><b>Dog</b>: %{customdata[0]}')

grid = dag.AgGrid(
    rowData=df.to_dict("records"),
    columnDefs=[{"field": i, 'filter': True, 'sortable': True} for i in df.columns],
    dashGridOptions={"pagination": True},
    columnSize="sizeToFit"
)

app = Dash()
app.layout = [
    grid,
    dcc.Graph(figure=fig)
]


if __name__ == "__main__":
    app.run(debug=True)

Participation Instructions:

  • Create - use the weekly data set to build your own Plotly visualization or Dash app. Or, enhance the sample figure provided in this post, using Plotly or Dash.
  • Submit - post your creation to LinkedIn or Twitter with the hashtags #FigureFriday and #plotly by midnight Thursday, your time zone. Please also submit your visualization as a new post in this thread.
  • Celebrate - join the Figure Friday sessions to showcase your creation and receive feedback from the community.

:point_right: If you prefer to collaborate with others on Discord, join the Plotly Discord channel.

Data Source:

Thank you to MakeoverMonday and GfK for the data.

3 Likes

I used this FigureFriday to do as much as possible with AI (things I had done before myself but spent many hours on, images instead of dots and text) and I ended up with ChatGPT 4.1 on Openrouter.ai, somehow we work well together.
This version (grouped by continent) and the basic version (by country) took approx 6 hours including creating images, finding flags (country version) rethinking my idea etc. And I estimate $1.20 in terms of credits used.

  • flags downloaded from Download all country flags of the world for free | Flagpedia.net
  • icons used from the nounproject (I have a subscription for my other work)
  • unreadable but funny font used Google “Indie flowers”, I promissed someone to use Microsoft Comic once on a data assignment, Indie Flowers is unreadable too :slight_smile:

I moved from countries (too crowded to continents) but I actually like the country visual more in terms of information and questions it raises.

Questions like:

  • are dogs used for protection in North & South America
  • is there a relation between the dog number for Turkey and the behaviour towards stray dogs?

Continent version: PyCafe - Dash - Infographic percentage of households with a dog, cat, bird or fish by continent

Country version: PyCafe - Dash - Infographic percentage of households with a dog, cat, bird or fish by country

5 Likes

I just used GPT-4.1 to help, I really like it too.
I chose my 2 favorite animals, but I might add more by Friday:)

4 Likes

cool app @Ester . Interesting to see the inverse relationship in the correlation chart between dog and cat ownership.

I’m not sure the first card on the top left is needed. That gives us the total car owners, presumably adding the average ownership numbers for the top five countries. But because they represent average ownership, adding them up to 180 doesn’t really mean much.

I’m not sure what the middle card is telling us? 89 average ownership per country? I think the highest average ownership country for dogs is 66% so I’m not sure how we can get to 89.

2 Likes

Beautiful infographic, @marieanne . I like the second graph even more – with all the country flags and the animals representing the y axis. Nice job. It was surprising for me to see that Turkey has a 20% average ownership of birds. 1 in 5 people owns a bird pet in Turkey
 wow.

2 Likes

Thank you @adamschroeder, I 'll check it.

2 Likes

Beatiful app

3 Likes

It would be interesting to see how Italy, Spain, Portugal, Turkey & Greece compare based on South Europe and “I only have to open the door of my holiday appartment and I have a holiday cat”.
And it would be interesting to see some historical ownership data, say 10, 20 and today years ago and the price for food and petcare for the same years. I was surprised by the low percentages for the Netherlands, on the other hand, the price of catfood has almost doubled in 4 years and the costs of vetcare increase rapidly, investors buying out a lot of vets and centralizing everything beyond basics in expensive animal hospitals.

1 Like

Week 16 was simple but fun, I got to repurpose another app to build mosaics, pulling images on demand, used this for the animals instead of manually going to google images and saving each photo to my project directory. Although I only needed 4 images it was still nice to use an old app for new use case.

For the flags I used the link @marieanne provided, Thank you!

:dog_face: What it does:

  • Shows average global ownership rates for dogs, cats, fish, and birds
  • Lets you select a pet type to explore country-by-country ownership
  • Displays responsive flag cards with percentages for each country
  • Highlights the top country for each pet with an automatic summary
  • Includes a choropleth map to visualize regional differences
  • Summarizes overall pet popularity in a clean bar chart

import dash
from dash import html, dcc, Input, Output
import dash_bootstrap_components as dbc
import pandas as pd
import plotly.express as px
import base64
import os

# Load dataset
df = pd.read_csv("pet_ownership_data_updated.csv")

# Compute average ownership
avg_df = df[['Dog', 'Cat', 'Fish', 'Bird']].mean().reset_index()
avg_df.columns = ['Pet', 'AveragePercentage']
avg_df['AveragePercentage'] = avg_df['AveragePercentage'].round(1)

# Copy country-level data
country_df = df.copy()

# Image encoding function
def encode_image(file_path):
    ext = os.path.splitext(file_path)[1].lower()
    with open(file_path, 'rb') as f:
        data = f.read()
    mime = 'image/png' if ext == '.png' else 'image/jpeg'
    return f"data:{mime};base64,{base64.b64encode(data).decode()}"

# Load images from assets/
ASSETS_DIR = os.path.join(os.path.dirname(__file__), 'assets')
pet_icons = {pet: encode_image(os.path.join(ASSETS_DIR, f"{pet.lower()}.jpg")) for pet in avg_df['Pet']}

flag_map = {}
for country in df['Country']:
    fname = country.replace(' ', '_') + '.png'
    path = os.path.join(ASSETS_DIR, fname)
    flag_map[country] = encode_image(path) if os.path.exists(path) else None

# Initialize app
def create_app():
    app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX])
    app.title = "Pet Ownership Infographic"

    pet_options = [{'label': pet, 'value': pet} for pet in avg_df['Pet']]

    def create_avg_chart():
        fig = px.bar(
            avg_df,
            x='Pet',
            y='AveragePercentage',
            text='AveragePercentage',
            title='Average Pet Ownership by Type'
        )
        fig.update_traces(texttemplate='%{text}%', textposition='outside')
        fig.update_layout(
            yaxis_title='Average Ownership (%)',
            xaxis_title='Pet Type',
            height=450,
            margin=dict(t=80, b=40, l=60, r=20),
            title_x=0.5,
            yaxis=dict(range=[0, avg_df['AveragePercentage'].max() + 10])  # Add space above the tallest bar
            )
        return fig

    app.layout = dbc.Container([
        html.H1('Pet Ownership Infographic', className='my-4 text-center'),

        html.H3("Average Ownership by Pet", className='text-center my-3'),
        dbc.Row([
            dbc.Col(
                dbc.Card([
                    dbc.CardImg(src=pet_icons[pet], top=True, style={'height':'100px','objectFit':'contain'}),
                    dbc.CardBody([
                        html.H5(pet, className='card-title text-center'),
                        html.P(f"{pct}% avg ownership", className='text-center')
                    ])
                ], className='m-2', style={'width': '160px'}),
                xs=6, sm=4, md=3, lg=2
            )
            for pet, pct in zip(avg_df['Pet'], avg_df['AveragePercentage'])
        ], justify='center'),

        html.Hr(),

        html.H3("Compare Countries", className='text-center my-3'),
        dbc.Row([
            dbc.Col(dcc.Dropdown(id='pet-select', options=pet_options, value=avg_df['Pet'][0], clearable=False), width=6)
        ], justify='center'),

        html.Div(id='top-country-summary', className='text-center my-2'),

        html.Div(
            id='country-row-wrapper',
            children=dbc.Row(id='country-row', className="flex-nowrap"),
            style={"overflowX": "auto", "whiteSpace": "nowrap", "paddingBottom": "1rem"}
        ),

        html.Hr(),

        html.H3("Pet Ownership by Country", className='text-center my-3'),
        dbc.Row(dbc.Col(dcc.Graph(id='choropleth-map'), width=12)),

        html.Hr(),

        html.H3("Overall Pet Ownership Comparison", className='text-center my-3'),
        dbc.Row(dbc.Col(dcc.Graph(figure=create_avg_chart()), width=10), justify='center')

    ], fluid=True)

    # Callback for cards and summary
    @app.callback(
        Output('country-row', 'children'),
        Output('top-country-summary', 'children'),
        Input('pet-select', 'value')
    )
    def update_country_cards(selected_pet):
        cards = []
        for _, row in country_df.iterrows():
            country = row['Country']
            val = int(round(row[selected_pet]))
            img_src = flag_map.get(country) or pet_icons[selected_pet]

            cards.append(
                dbc.Col(
                    dbc.Card([
                        dbc.CardImg(src=img_src, top=True, style={'height':'80px','objectFit':'contain'}),
                        dbc.CardBody([
                            html.H6(country, className='card-title text-center',
                                    style={
                                        'fontSize': '0.85rem',
                                        'whiteSpace': 'normal',
                                        'wordWrap': 'break-word',
                                        'lineHeight': '1.2'
                                    }),
                            html.P(f"{val}% {selected_pet} ownership", className='text-center',
                                   style={
                                       'fontSize': '0.8rem',
                                       'marginBottom': '0',
                                       'whiteSpace': 'normal',
                                       'wordWrap': 'break-word'
                                   })
                        ])
                    ], className='m-2', style={'width': '170px', 'minHeight': '200px', 'padding': '5px'}),
                    xs="auto"
                )
            )

        top_country = country_df[['Country', selected_pet]].sort_values(by=selected_pet, ascending=False).iloc[0]
        summary_text = html.Div([
            "🏆 ",
            html.B(top_country['Country']),
            f" has the highest {selected_pet.lower()} ownership at ",
            html.B(f"{int(top_country[selected_pet])}%"),
            "."
        ])
        return cards, summary_text

    # Callback for map
    @app.callback(
        Output('choropleth-map', 'figure'),
        Input('pet-select', 'value')
    )
    def update_map(selected_pet):
        fig = px.choropleth(
            country_df,
            locations="Country",
            locationmode="country names",
            color=selected_pet,
            color_continuous_scale="Blues",
            title=f"{selected_pet} Ownership by Country (%)"
        )
        fig.update_layout(title_x=0.5)
        return fig

    return app

if __name__ == '__main__':
    app = create_app()
    app.run(debug=True)

6 Likes

For anybody interested in streamlining the download of each of these flags; Here is a script to pull all the flags from that link I used.

import requests
import os

# List of countries
countries = [
    "USA", "Argentina", "UK", "Australia", "Turkey", "Belgium", "Sweden", "Brazil", "Spain",
    "Canada", "South Korea", "Russia", "Czech Republic", "China", "Poland", "France",
    "Netherlands", "Germany", "Mexico", "Hong Kong", "Japan", "Italy"
]

# Mapping of country names to their ISO 3166-1 alpha-2 codes
country_codes = {
    "USA": "us",
    "Argentina": "ar",
    "UK": "gb",
    "Australia": "au",
    "Turkey": "tr",
    "Belgium": "be",
    "Sweden": "se",
    "Brazil": "br",
    "Spain": "es",
    "Canada": "ca",
    "South Korea": "kr",
    "Russia": "ru",
    "Czech Republic": "cz",
    "China": "cn",
    "Poland": "pl",
    "France": "fr",
    "Netherlands": "nl",
    "Germany": "de",
    "Mexico": "mx",
    "Hong Kong": "hk",
    "Japan": "jp",
    "Italy": "it"
}

# Output directory
output_dir = r"YOUR_ASSET_LOCATION" #UPDATE THIS ROW WITH YOUR PROJECT DIRECTLY
os.makedirs(output_dir, exist_ok=True)

# Base URL for Flagpedia CDN
base_url = "https://flagcdn.com/w2560"

# Download each flag
for country in countries:
    code = country_codes.get(country)
    if code:
        url = f"{base_url}/{code}.png"
        response = requests.get(url, stream=True)
        if response.status_code == 200:
            file_path = os.path.join(output_dir, f"{country.replace(' ', '_')}.png")
            with open(file_path, "wb") as f:
                for chunk in response.iter_content(1024):
                    f.write(chunk)
            print(f"Downloaded: {country}")
        else:
            print(f"Failed to download: {country} (HTTP {response.status_code})")
    else:
        print(f"No ISO code found for: {country}")
2 Likes

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)
6 Likes

@Avacsiglo21 , Very impressive work here! I like how you incorporate statistics even with a very limited dataset like this, truly enriching the data for group!

2 Likes

@avacsiglo21 Beautiful!

3 Likes

Wow, Thanks you Thomas,

2 Likes

Thank you Marianne :smiling_face_with_three_hearts:

2 Likes

nice app, @ThomasD21M . I find the top summary cards very helpful. Where did you get the country flags from?

Does the bar chart ever change, based on the dropdown value chosen or is it always the same?

2 Likes

Thanks @adamschroeder, that bottom bar chart is static. I pulled the country flags from the link Marie-Anne shared. had a script to download the specific flags off a dataframe list of the countries. I shared that script above.

2 Likes

@Avacsiglo21 good idea adding the diversity index control to the choropleth. Now I want to live in Turkey :slight_smile:
I also like the summary card on the right. I wish we had data on more countries, it’s really fun looking at the data using the orthographic choropleth view

3 Likes

Adam,

Would you like to live in Turkey because of their preference for birds as pets?

Yes, the orthographic view is actually my favorite as well. I was tempted to create synthetic data about it, but to create a greater difference in the diversity index.

2 Likes

“The Journal of pet demographics”

Ha!

That’s a beauty of an app. Do you have a background in ecology?

2 Likes