Henley Passport Index Data

A shiny app for visa information.
Shiny
PyDyTuesday
TidyTuesday
Author

Manish Datt

Published

September 9, 2025

TidyTuesday dataset of September 9, 2025

The shiny app is available here.

import pandas as pd
import json
from shiny import App, ui, render
import shinyswatch
import matplotlib.pyplot as plt
import io
import base64

# Load the data
country_lists = pd.read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2025/2025-09-09/country_lists.csv')

# Function to parse JSON strings in the dataframe
def parse_json_column(column_value):
    if pd.isna(column_value):
        return []
    try:
        # The data is already a valid JSON string, just parse it directly
        data = json.loads(column_value)
        # Extract the inner array which contains the country objects
        if data and isinstance(data, list) and len(data) > 0:
            inner_data = data[0] if isinstance(data[0], list) else data
            return [item['name'] for item in inner_data if isinstance(item, dict) and 'name' in item]
        return []
    except:
        return []

# Parse all visa-related columns
visa_columns = ['visa_required', 'visa_online', 'visa_on_arrival', 'visa_free_access', 'electronic_travel_authorisation']
for col in visa_columns:
    country_lists[f'{col}_countries'] = country_lists[col].apply(parse_json_column)

# Get unique country names for dropdown
countries = sorted(country_lists['country'].unique())

app_ui = ui.page_fluid(
    # Modern header with gradient background
    ui.div(
        ui.h1("🌍 Visa Travel Information Explorer", style="color: white; text-align: center; margin-bottom: 10px;"),
#        ui.p("Discover visa requirements and travel information worldwide", style="color: #e0e0e0; text-align: center; font-size: 16px;"),
        style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px 20px; border-radius: 15px; margin-bottom: 30px; box-shadow: 0 8px 32px rgba(0,0,0,0.1);"
    ),

    # First Section - Origin/Destination Check
    ui.div(
        ui.h3("✈️ Check Visa Requirements Between Countries", style="color: #2c3e50; margin-bottom: 20px;"),
        ui.div(
            ui.row(
                ui.column(6,
                    ui.div(
                        ui.tags.label("🏠 Traveler's Nationality:", style="font-weight: bold; color: #34495e;"),
                        ui.input_select("traveler_nationality", "",
                                      choices=countries,
                                      selected=countries[0] if countries else None),
                        style="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
                    )
                ),
                ui.column(6,
                    ui.div(
                        ui.tags.label("🎯 Destination Country:", style="font-weight: bold; color: #34495e;"),
                        ui.input_select("destination_country", "",
                                      choices=countries,
                                      selected=countries[1] if len(countries) > 1 else countries[0] if countries else None),
                        style="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
                    )
                )
            ),
            style="margin-bottom: 20px;"
        ),
        ui.div(
            ui.div(
                ui.output_ui("visa_requirement"),
                style="color: #2c3e50; text-align: center; background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 10px;"
            ),
#            ui.p("💡 This shows what your HOME country requires from you before traveling to your destination.", style="font-size: 14px; color: #7f8c8d; text-align: center;"),
            ui.output_ui("detailed_info"),
            style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 6px 12px rgba(0,0,0,0.1);"
        ),
        style="margin-bottom: 40px;"
    ),

    # Separator
    ui.div(
        ui.tags.hr(style="border: none; height: 2px; background: linear-gradient(90deg, #667eea, #764ba2); margin: 40px 0;"),
        style="text-align: center;"
    ),

    # Second Section - Country Explorer
    ui.div(
        ui.h3("🔍 Explore All Visa Categories for a Country", style="color: #2c3e50; margin-bottom: 20px;"),
#        ui.p("💡 This shows what countries the selected nation requires visas FROM (outbound travel requirements).", style="font-size: 14px; color: #7f8c8d; margin-bottom: 20px;"),
        ui.row(
            ui.column(12,
                ui.div(
                    ui.tags.label("🌐 Select a Country:", style="font-weight: bold; color: #34495e;"),
                    ui.input_select("selected_country", "",
                                  choices=countries,
                                  selected=countries[0] if countries else None),
                    style="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); margin-bottom: 20px;"
                )
            )
        ),
        ui.div(
            ui.output_text("country_info"),
            style="font-size: 16px; color: #2c3e50; text-align: center; background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;"
        ),
        ui.row(
            ui.column(2,
                ui.div(
                    ui.output_ui("visa_required_list"),
                    style="background: #ffeaa7; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
                )
            ),
            ui.column(2,
                ui.div(
                    ui.output_ui("visa_online_list"),
                    style="background: #74b9ff; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
                )
            ),
            ui.column(2,
                ui.div(
                    ui.output_ui("visa_on_arrival_list"),
                    style="background: #00b894; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
                )
            ),
            ui.column(3,
                ui.div(
                    ui.output_ui("visa_free_list"),
                    style="background: #00cec9; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
                )
            ),
            ui.column(3,
                ui.div(
                    ui.output_ui("electronic_travel_auth_list"),
                    style="background: #a29bfe; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
                )
            )
        ),
        style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 6px 12px rgba(0,0,0,0.1);"
    ),

    # Footer
    ui.div(
        ui.p("bioinfo@manishdatt.com", style="text-align: center; color: #95a5a6; font-size: 12px; margin-top: 30px;"),
        style="text-align: center; margin-top: 40px;"
    ),

    theme=shinyswatch.theme.flatly()
)

def server(input, output, session):
    def create_visa_bar_plot(traveler_data):
        """Create a horizontal bar plot showing visa policy counts"""
        categories = [
            'Visa Required',
            'Visa Online',
            'Visa on Arrival',
            'Visa-Free Access',
            'Electronic Travel Authorisation'
        ]

        counts = [
            len(traveler_data['visa_required_countries']),
            len(traveler_data['visa_online_countries']),
            len(traveler_data['visa_on_arrival_countries']),
            len(traveler_data['visa_free_access_countries']),
            len(traveler_data['electronic_travel_authorisation_countries'])
        ]

        # Colors matching the UI theme
        colors = ['#ffeaa7', '#74b9ff', '#00b894', '#00cec9', '#a29bfe']

        fig, ax = plt.subplots(figsize=(8, 4))
        bars = ax.barh(categories, counts, color=colors, edgecolor='white', linewidth=2)

        # Add value labels on bars
        for bar, count in zip(bars, counts):
            ax.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height()/2,
                   f'{count}', ha='left', va='center', fontweight='bold', fontsize=12)

        ax.set_xlabel('Number of Countries', fontsize=12, fontweight='bold')
#        ax.set_title('Visa Policy Distribution', fontsize=14, fontweight='bold', pad=20)
        ax.grid(axis='x', alpha=0.3)
        ax.invert_yaxis()  

        # Remove top and right spines
        ax.spines[['right', 'top', 'left']].set_visible(False)
        # remove y-axis tick lines
        ax.tick_params(axis='y', length=0)
        ax.tick_params(axis='x', length=0)
        plt.tight_layout()

        # Convert plot to base64 string
        buf = io.BytesIO()
        fig.savefig(buf, format='png', dpi=100, bbox_inches='tight', facecolor='white')
        buf.seek(0)
        image_base64 = base64.b64encode(buf.read()).decode('utf-8')
        buf.close()
        plt.close(fig)

        return f"data:image/png;base64,{image_base64}"

    def get_visa_requirement(origin, destination):
        if not origin or not destination:
            return "Please select both origin and destination countries"

        if origin == destination:
            return "No visa required (domestic travel)"

        # Get ORIGIN country data (traveler's nationality)
        origin_data = country_lists[country_lists['country'] == origin]
        if origin_data.empty:
            return "Origin country data not found"

        origin_data = origin_data.iloc[0]

        # Check what the ORIGIN country requires from DESTINATION country's citizens
        # This answers: "What does my home country require from citizens of my destination?"
        if destination in origin_data['visa_free_access_countries']:
            return "Visa-Free Access"
        elif destination in origin_data['visa_on_arrival_countries']:
            return "Visa on Arrival"
        elif destination in origin_data['visa_online_countries']:
            return "Visa Online"
        elif destination in origin_data['electronic_travel_authorisation_countries']:
            return "Electronic Travel Authorisation Required"
        else:
            return "Visa Required"

    @output
    @render.ui
    def visa_requirement():
        traveler = input.traveler_nationality()
        destination = input.destination_country()

        requirement = get_visa_requirement(traveler, destination)

        if traveler and destination:
            return ui.HTML(f'<span style="font-size: 18px;">Traveling from {traveler} to {destination}:</span> <span style="font-size: 22px; font-weight: bold;">{requirement}</span>')
        else:
            return ui.HTML('<span style="font-size: 18px;">Please select both traveler\'s nationality and destination countries</span>')

    @output
    @render.ui
    def detailed_info():
        traveler = input.traveler_nationality()
        destination = input.destination_country()

        if not traveler or not destination:
            return ui.div()

        if traveler == destination:
            return ui.div(
                ui.h4("Travel Information:"),
                ui.p("Domestic travel - no visa requirements.")
            )

        # Get traveler's nationality country data (not destination)
        traveler_data = country_lists[country_lists['country'] == traveler].iloc[0]

        requirement = get_visa_requirement(traveler, destination)

        # Create the bar plot
        plot_url = create_visa_bar_plot(traveler_data)

        return ui.div(
#            ui.h4("Travel Requirements:"),
#            ui.p(f"From {traveler} to {destination}: {requirement}"),
#            ui.br(),
            ui.h5(f"{traveler}'s Visa Policy Distribution:"),
            ui.tags.img(src=plot_url, style="max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"),
            ui.br(),
#            ui.p("📊 This chart shows how many countries fall into each visa category for citizens of your nationality.", style="font-size: 12px; color: #7f8c8d; text-align: center; margin-top: 10px;")
        )

    # Single country selection functions
    @output
    @render.text
    def country_info():
        selected = input.selected_country()
        if not selected:
            return "Please select a country"

        country_data = country_lists[country_lists['country'] == selected].iloc[0]
        return f"Selected Country: {selected} ({country_data['code']})"

    def create_visa_list_ui(title, countries_list):
        if not countries_list:
            return ui.div(
                ui.h5(f"{title}:"),
                ui.p("None")
            )
        return ui.div(
            ui.h5(f"{title} ({len(countries_list)}):"),
            ui.tags.ul([ui.tags.li(country) for country in sorted(countries_list)])
        )

    @output
    @render.ui
    def visa_required_list():
        selected = input.selected_country()
        if not selected:
            return ui.div()
        country_data = country_lists[country_lists['country'] == selected].iloc[0]
        return create_visa_list_ui("Visa Required", country_data['visa_required_countries'])

    @output
    @render.ui
    def visa_online_list():
        selected = input.selected_country()
        if not selected:
            return ui.div()
        country_data = country_lists[country_lists['country'] == selected].iloc[0]
        return create_visa_list_ui("Visa Online", country_data['visa_online_countries'])

    @output
    @render.ui
    def visa_on_arrival_list():
        selected = input.selected_country()
        if not selected:
            return ui.div()
        country_data = country_lists[country_lists['country'] == selected].iloc[0]
        return create_visa_list_ui("Visa on Arrival", country_data['visa_on_arrival_countries'])

    @output
    @render.ui
    def visa_free_list():
        selected = input.selected_country()
        if not selected:
            return ui.div()
        country_data = country_lists[country_lists['country'] == selected].iloc[0]
        return create_visa_list_ui("Visa-Free Access", country_data['visa_free_access_countries'])

    @output
    @render.ui
    def electronic_travel_auth_list():
        selected = input.selected_country()
        if not selected:
            return ui.div()
        country_data = country_lists[country_lists['country'] == selected].iloc[0]
        return create_visa_list_ui("Electronic Travel Authorisation", country_data['electronic_travel_authorisation_countries'])

app = App(app_ui, server)