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)TidyTuesday dataset of September 9, 2025
The shiny app is available here.