DataViz.manishdatt.com

Henley Passport Index Data

A shiny app for visa information.

By Manish Datt

TidyTuesday dataset of 2025-09-09

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)