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
= pd.read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2025/2025-09-09/country_lists.csv')
country_lists
# 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
= json.loads(column_value)
data # Extract the inner array which contains the country objects
if data and isinstance(data, list) and len(data) > 0:
= data[0] if isinstance(data[0], list) else data
inner_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_required', 'visa_online', 'visa_on_arrival', 'visa_free_access', 'electronic_travel_authorisation']
visa_columns for col in visa_columns:
f'{col}_countries'] = country_lists[col].apply(parse_json_column)
country_lists[
# Get unique country names for dropdown
= sorted(country_lists['country'].unique())
countries
= ui.page_fluid(
app_ui # Modern header with gradient background
ui.div("🌍 Visa Travel Information Explorer", style="color: white; text-align: center; margin-bottom: 10px;"),
ui.h1(# ui.p("Discover visa requirements and travel information worldwide", style="color: #e0e0e0; text-align: center; font-size: 16px;"),
="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);"
style
),
# First Section - Origin/Destination Check
ui.div("✈️ Check Visa Requirements Between Countries", style="color: #2c3e50; margin-bottom: 20px;"),
ui.h3(
ui.div(
ui.row(6,
ui.column(
ui.div("🏠 Traveler's Nationality:", style="font-weight: bold; color: #34495e;"),
ui.tags.label("traveler_nationality", "",
ui.input_select(=countries,
choices=countries[0] if countries else None),
selected="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
style
)
),6,
ui.column(
ui.div("🎯 Destination Country:", style="font-weight: bold; color: #34495e;"),
ui.tags.label("destination_country", "",
ui.input_select(=countries,
choices=countries[1] if len(countries) > 1 else countries[0] if countries else None),
selected="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
style
)
)
),="margin-bottom: 20px;"
style
),
ui.div(
ui.div("visa_requirement"),
ui.output_ui(="color: #2c3e50; text-align: center; background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 10px;"
style
),# 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;"),
"detailed_info"),
ui.output_ui(="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 6px 12px rgba(0,0,0,0.1);"
style
),="margin-bottom: 40px;"
style
),
# Separator
ui.div(="border: none; height: 2px; background: linear-gradient(90deg, #667eea, #764ba2); margin: 40px 0;"),
ui.tags.hr(style="text-align: center;"
style
),
# Second Section - Country Explorer
ui.div("🔍 Explore All Visa Categories for a Country", style="color: #2c3e50; margin-bottom: 20px;"),
ui.h3(# 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(12,
ui.column(
ui.div("🌐 Select a Country:", style="font-weight: bold; color: #34495e;"),
ui.tags.label("selected_country", "",
ui.input_select(=countries,
choices=countries[0] if countries else None),
selected="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); margin-bottom: 20px;"
style
)
)
),
ui.div("country_info"),
ui.output_text(="font-size: 16px; color: #2c3e50; text-align: center; background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;"
style
),
ui.row(2,
ui.column(
ui.div("visa_required_list"),
ui.output_ui(="background: #ffeaa7; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
style
)
),2,
ui.column(
ui.div("visa_online_list"),
ui.output_ui(="background: #74b9ff; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
style
)
),2,
ui.column(
ui.div("visa_on_arrival_list"),
ui.output_ui(="background: #00b894; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
style
)
),3,
ui.column(
ui.div("visa_free_list"),
ui.output_ui(="background: #00cec9; padding: 15px; border-radius: 8px; height: 100%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"
style
)
),3,
ui.column(
ui.div("electronic_travel_auth_list"),
ui.output_ui(="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);"
style
),
# Footer
ui.div("bioinfo@manishdatt.com", style="text-align: center; color: #95a5a6; font-size: 12px; margin-top: 30px;"),
ui.p(="text-align: center; margin-top: 40px;"
style
),
=shinyswatch.theme.flatly()
theme
)
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
= ['#ffeaa7', '#74b9ff', '#00b894', '#00cec9', '#a29bfe']
colors
= plt.subplots(figsize=(8, 4))
fig, ax = ax.barh(categories, counts, color=colors, edgecolor='white', linewidth=2)
bars
# Add value labels on bars
for bar, count in zip(bars, counts):
+ 0.5, bar.get_y() + bar.get_height()/2,
ax.text(bar.get_width() f'{count}', ha='left', va='center', fontweight='bold', fontsize=12)
'Number of Countries', fontsize=12, fontweight='bold')
ax.set_xlabel(# ax.set_title('Visa Policy Distribution', fontsize=14, fontweight='bold', pad=20)
='x', alpha=0.3)
ax.grid(axis
ax.invert_yaxis()
# Remove top and right spines
'right', 'top', 'left']].set_visible(False)
ax.spines[[# remove y-axis tick lines
='y', length=0)
ax.tick_params(axis='x', length=0)
ax.tick_params(axis
plt.tight_layout()
# Convert plot to base64 string
= io.BytesIO()
buf format='png', dpi=100, bbox_inches='tight', facecolor='white')
fig.savefig(buf, 0)
buf.seek(= base64.b64encode(buf.read()).decode('utf-8')
image_base64
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)
= country_lists[country_lists['country'] == origin]
origin_data if origin_data.empty:
return "Origin country data not found"
= origin_data.iloc[0]
origin_data
# 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():
= input.traveler_nationality()
traveler = input.destination_country()
destination
= get_visa_requirement(traveler, destination)
requirement
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():
= input.traveler_nationality()
traveler = input.destination_country()
destination
if not traveler or not destination:
return ui.div()
if traveler == destination:
return ui.div(
"Travel Information:"),
ui.h4("Domestic travel - no visa requirements.")
ui.p(
)
# Get traveler's nationality country data (not destination)
= country_lists[country_lists['country'] == traveler].iloc[0]
traveler_data
= get_visa_requirement(traveler, destination)
requirement
# Create the bar plot
= create_visa_bar_plot(traveler_data)
plot_url
return ui.div(
# ui.h4("Travel Requirements:"),
# ui.p(f"From {traveler} to {destination}: {requirement}"),
# ui.br(),
f"{traveler}'s Visa Policy Distribution:"),
ui.h5(=plot_url, style="max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"),
ui.tags.img(src
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():
= input.selected_country()
selected if not selected:
return "Please select a country"
= country_lists[country_lists['country'] == selected].iloc[0]
country_data return f"Selected Country: {selected} ({country_data['code']})"
def create_visa_list_ui(title, countries_list):
if not countries_list:
return ui.div(
f"{title}:"),
ui.h5("None")
ui.p(
)return ui.div(
f"{title} ({len(countries_list)}):"),
ui.h5(for country in sorted(countries_list)])
ui.tags.ul([ui.tags.li(country)
)
@output
@render.ui
def visa_required_list():
= input.selected_country()
selected if not selected:
return ui.div()
= country_lists[country_lists['country'] == selected].iloc[0]
country_data return create_visa_list_ui("Visa Required", country_data['visa_required_countries'])
@output
@render.ui
def visa_online_list():
= input.selected_country()
selected if not selected:
return ui.div()
= country_lists[country_lists['country'] == selected].iloc[0]
country_data return create_visa_list_ui("Visa Online", country_data['visa_online_countries'])
@output
@render.ui
def visa_on_arrival_list():
= input.selected_country()
selected if not selected:
return ui.div()
= country_lists[country_lists['country'] == selected].iloc[0]
country_data return create_visa_list_ui("Visa on Arrival", country_data['visa_on_arrival_countries'])
@output
@render.ui
def visa_free_list():
= input.selected_country()
selected if not selected:
return ui.div()
= country_lists[country_lists['country'] == selected].iloc[0]
country_data return create_visa_list_ui("Visa-Free Access", country_data['visa_free_access_countries'])
@output
@render.ui
def electronic_travel_auth_list():
= input.selected_country()
selected if not selected:
return ui.div()
= country_lists[country_lists['country'] == selected].iloc[0]
country_data return create_visa_list_ui("Electronic Travel Authorisation", country_data['electronic_travel_authorisation_countries'])
= App(app_ui, server) app
TidyTuesday dataset of September 9, 2025
The shiny app is available here.