How Far Can You Get in X Hours?¶
Select countries on the globe, set your available time, and choose a travel mode to see how far you could travel from each country's capital.
In [1]:
Collapse Code
import plotly.graph_objects as go
import ipywidgets as widgets
import numpy as np
from IPython.display import display, HTML
print("Setup complete.")
Travel Mode Speeds & Country Infrastructure Adjustments¶
Speeds vary by country due to infrastructure quality, terrain, and regulations.
In [2]:
Collapse Code
MODE_SPEEDS = {
"Walking": 5.0,
"Cycling": 15.0,
"Driving": 80.0,
"Public Transit": 40.0,
}
INFRA_MULTIPLIERS = {
"Walking": {
"Japan": 1.1, "Netherlands": 1.1, "Singapore": 1.15,
"Switzerland": 1.05, "Germany": 1.05, "UK": 1.0,
"USA": 0.9, "India": 0.85, "Brazil": 0.85, "Nigeria": 0.8,
"Default": 1.0
},
"Cycling": {
"Netherlands": 1.25, "Denmark": 1.2, "Germany": 1.15,
"Belgium": 1.1, "Sweden": 1.1, "Norway": 1.1,
"Japan": 1.05, "UK": 1.0, "France": 1.0,
"USA": 0.85, "India": 0.7, "Brazil": 0.75,
"Default": 0.9
},
"Driving": {
"Germany": 1.3, "France": 1.15, "USA": 1.1,
"Japan": 1.05, "UK": 1.0, "South Korea": 1.0,
"China": 0.9, "Brazil": 0.8, "India": 0.7,
"Nigeria": 0.65, "Default": 0.9
},
"Public Transit": {
"Japan": 1.5, "Switzerland": 1.4, "South Korea": 1.35,
"Singapore": 1.3, "Germany": 1.25, "France": 1.2,
"UK": 1.15, "China": 1.1, "Netherlands": 1.1,
"USA": 0.75, "India": 0.6, "Brazil": 0.65,
"Nigeria": 0.55, "Default": 0.85
}
}
def get_speed(mode, country):
base = MODE_SPEEDS[mode]
mult = INFRA_MULTIPLIERS[mode].get(country, INFRA_MULTIPLIERS[mode]["Default"])
return base * mult
Country Data¶
In [3]:
Collapse Code
COUNTRIES = [
{"name": "Afghanistan", "lat": 33.93, "lon": 67.71, "iso": "AFG", "region": "Asia"},
{"name": "Albania", "lat": 41.15, "lon": 20.17, "iso": "ALB", "region": "Europe"},
{"name": "Algeria", "lat": 28.03, "lon": 1.66, "iso": "DZA", "region": "Africa"},
{"name": "Angola", "lat": -11.2,"lon": 17.87, "iso": "AGO", "region": "Africa"},
{"name": "Argentina", "lat": -38.4,"lon": -63.6, "iso": "ARG", "region": "South America"},
{"name": "Armenia", "lat": 40.07, "lon": 45.04, "iso": "ARM", "region": "Asia"},
{"name": "Australia", "lat": -25.27,"lon": 133.78, "iso": "AUS", "region": "Oceania"},
{"name": "Austria", "lat": 47.52, "lon": 14.55, "iso": "AUT", "region": "Europe"},
{"name": "Azerbaijan", "lat": 40.14, "lon": 47.58, "iso": "AZE", "region": "Asia"},
{"name": "Bangladesh", "lat": 23.68, "lon": 90.36, "iso": "BGD", "region": "Asia"},
{"name": "Belarus", "lat": 53.71, "lon": 27.95, "iso": "BLR", "region": "Europe"},
{"name": "Belgium", "lat": 50.5, "lon": 4.47, "iso": "BEL", "region": "Europe"},
{"name": "Bolivia", "lat": -16.29,"lon": -63.59, "iso": "BOL", "region": "South America"},
{"name": "Bosnia and Herz.", "lat": 43.92, "lon": 17.68, "iso": "BIH", "region": "Europe"},
{"name": "Botswana", "lat": -22.33,"lon": 24.68, "iso": "BWA", "region": "Africa"},
{"name": "Brazil", "lat": -14.24,"lon": -51.93, "iso": "BRA", "region": "South America"},
{"name": "Bulgaria", "lat": 42.73, "lon": 25.49, "iso": "BGR", "region": "Europe"},
{"name": "Burkina Faso", "lat": 12.37, "lon": -1.52, "iso": "BFA", "region": "Africa"},
{"name": "Burundi", "lat": -3.37, "lon": 29.92, "iso": "BDI", "region": "Africa"},
{"name": "Cambodia", "lat": 12.57, "lon": 104.99, "iso": "KHM", "region": "Asia"},
{"name": "Cameroon", "lat": 7.37, "lon": 12.35, "iso": "CMR", "region": "Africa"},
{"name": "Canada", "lat": 56.13, "lon": -106.35,"iso": "CAN", "region": "North America"},
{"name": "Central African Rep.","lat": 6.61, "lon": 20.94, "iso": "CAF", "region": "Africa"},
{"name": "Chad", "lat": 15.45, "lon": 18.73, "iso": "TCD", "region": "Africa"},
{"name": "Chile", "lat": -35.68,"lon": -71.54, "iso": "CHL", "region": "South America"},
{"name": "China", "lat": 35.86, "lon": 104.2, "iso": "CHN", "region": "Asia"},
{"name": "Colombia", "lat": 4.57, "lon": -74.3, "iso": "COL", "region": "South America"},
{"name": "Congo", "lat": -0.23, "lon": 15.83, "iso": "COG", "region": "Africa"},
{"name": "Costa Rica", "lat": 9.75, "lon": -83.75, "iso": "CRI", "region": "North America"},
{"name": "Croatia", "lat": 45.1, "lon": 15.2, "iso": "HRV", "region": "Europe"},
{"name": "Cuba", "lat": 21.52, "lon": -77.78, "iso": "CUB", "region": "North America"},
{"name": "Czech Rep.", "lat": 49.82, "lon": 15.47, "iso": "CZE", "region": "Europe"},
{"name": "Dem. Rep. Congo", "lat": -4.04, "lon": 21.76, "iso": "COD", "region": "Africa"},
{"name": "Denmark", "lat": 56.26, "lon": 9.5, "iso": "DNK", "region": "Europe"},
{"name": "Dominican Rep.", "lat": 18.74, "lon": -70.16, "iso": "DOM", "region": "North America"},
{"name": "Ecuador", "lat": -1.83, "lon": -78.18, "iso": "ECU", "region": "South America"},
{"name": "Egypt", "lat": 26.82, "lon": 30.8, "iso": "EGY", "region": "Africa"},
{"name": "El Salvador", "lat": 13.79, "lon": -88.9, "iso": "SLV", "region": "North America"},
{"name": "Ethiopia", "lat": 9.15, "lon": 40.49, "iso": "ETH", "region": "Africa"},
{"name": "Finland", "lat": 61.92, "lon": 25.75, "iso": "FIN", "region": "Europe"},
{"name": "France", "lat": 46.23, "lon": 2.21, "iso": "FRA", "region": "Europe"},
{"name": "Gabon", "lat": -0.8, "lon": 11.61, "iso": "GAB", "region": "Africa"},
{"name": "Gambia", "lat": 13.44, "lon": -15.31, "iso": "GMB", "region": "Africa"},
{"name": "Germany", "lat": 51.17, "lon": 10.45, "iso": "DEU", "region": "Europe"},
{"name": "Ghana", "lat": 7.95, "lon": -1.02, "iso": "GHA", "region": "Africa"},
{"name": "Greece", "lat": 39.07, "lon": 21.82, "iso": "GRC", "region": "Europe"},
{"name": "Guatemala", "lat": 15.78, "lon": -90.23, "iso": "GTM", "region": "North America"},
{"name": "Guinea", "lat": 9.95, "lon": -11.86, "iso": "GIN", "region": "Africa"},
{"name": "Haiti", "lat": 18.97, "lon": -72.29, "iso": "HTI", "region": "North America"},
{"name": "Honduras", "lat": 15.2, "lon": -86.24, "iso": "HND", "region": "North America"},
{"name": "Hungary", "lat": 47.16, "lon": 19.5, "iso": "HUN", "region": "Europe"},
{"name": "India", "lat": 20.59, "lon": 78.96, "iso": "IND", "region": "Asia"},
{"name": "Indonesia", "lat": -0.79, "lon": 113.92, "iso": "IDN", "region": "Asia"},
{"name": "Iran", "lat": 32.43, "lon": 53.69, "iso": "IRN", "region": "Asia"},
{"name": "Iraq", "lat": 33.22, "lon": 43.68, "iso": "IRQ", "region": "Asia"},
{"name": "Ireland", "lat": 53.14, "lon": -7.69, "iso": "IRL", "region": "Europe"},
{"name": "Israel", "lat": 31.05, "lon": 34.85, "iso": "ISR", "region": "Asia"},
{"name": "Italy", "lat": 41.87, "lon": 12.57, "iso": "ITA", "region": "Europe"},
{"name": "Ivory Coast", "lat": 7.54, "lon": -5.55, "iso": "CIV", "region": "Africa"},
{"name": "Jamaica", "lat": 18.11, "lon": -77.3, "iso": "JAM", "region": "North America"},
{"name": "Japan", "lat": 36.2, "lon": 138.25, "iso": "JPN", "region": "Asia"},
{"name": "Jordan", "lat": 30.59, "lon": 36.24, "iso": "JOR", "region": "Asia"},
{"name": "Kazakhstan", "lat": 48.02, "lon": 66.92, "iso": "KAZ", "region": "Asia"},
{"name": "Kenya", "lat": -0.02, "lon": 37.91, "iso": "KEN", "region": "Africa"},
{"name": "Kuwait", "lat": 29.31, "lon": 47.48, "iso": "KWT", "region": "Asia"},
{"name": "Kyrgyzstan", "lat": 41.2, "lon": 74.77, "iso": "KGZ", "region": "Asia"},
{"name": "Laos", "lat": 19.86, "lon": 102.5, "iso": "LAO", "region": "Asia"},
{"name": "Lebanon", "lat": 33.85, "lon": 35.86, "iso": "LBN", "region": "Asia"},
{"name": "Liberia", "lat": 6.43, "lon": -9.43, "iso": "LBR", "region": "Africa"},
{"name": "Libya", "lat": 26.34, "lon": 17.23, "iso": "LBY", "region": "Africa"},
{"name": "Madagascar", "lat": -18.77,"lon": 46.87, "iso": "MDG", "region": "Africa"},
{"name": "Malawi", "lat": -13.25,"lon": 34.3, "iso": "MWI", "region": "Africa"},
{"name": "Malaysia", "lat": 4.21, "lon": 101.98, "iso": "MYS", "region": "Asia"},
{"name": "Mali", "lat": 17.57, "lon": -3.99, "iso": "MLI", "region": "Africa"},
{"name": "Mexico", "lat": 23.63, "lon": -102.55,"iso": "MEX", "region": "North America"},
{"name": "Mongolia", "lat": 46.86, "lon": 103.85, "iso": "MNG", "region": "Asia"},
{"name": "Morocco", "lat": 31.79, "lon": -7.09, "iso": "MAR", "region": "Africa"},
{"name": "Mozambique", "lat": -18.67,"lon": 35.53, "iso": "MOZ", "region": "Africa"},
{"name": "Myanmar", "lat": 21.91, "lon": 95.96, "iso": "MMR", "region": "Asia"},
{"name": "Namibia", "lat": -22.96,"lon": 18.49, "iso": "NAM", "region": "Africa"},
{"name": "Nepal", "lat": 28.39, "lon": 84.12, "iso": "NPL", "region": "Asia"},
{"name": "Netherlands", "lat": 52.13, "lon": 5.29, "iso": "NLD", "region": "Europe"},
{"name": "New Zealand", "lat": -40.9,"lon": 174.89, "iso": "NZL", "region": "Oceania"},
{"name": "Nicaragua", "lat": 12.87, "lon": -85.21, "iso": "NIC", "region": "North America"},
{"name": "Niger", "lat": 17.61, "lon": 8.08, "iso": "NER", "region": "Africa"},
{"name": "Nigeria", "lat": 9.08, "lon": 8.68, "iso": "NGA", "region": "Africa"},
{"name": "North Korea", "lat": 40.34, "lon": 127.51, "iso": "PRK", "region": "Asia"},
{"name": "Norway", "lat": 60.47, "lon": 8.47, "iso": "NOR", "region": "Europe"},
{"name": "Oman", "lat": 21.47, "lon": 55.98, "iso": "OMN", "region": "Asia"},
{"name": "Pakistan", "lat": 30.38, "lon": 69.35, "iso": "PAK", "region": "Asia"},
{"name": "Panama", "lat": 8.54, "lon": -80.78, "iso": "PAN", "region": "North America"},
{"name": "Papua New Guinea", "lat": -6.31, "lon": 143.96, "iso": "PNG", "region": "Oceania"},
{"name": "Paraguay", "lat": -23.44,"lon": -58.44, "iso": "PRY", "region": "South America"},
{"name": "Peru", "lat": -9.19, "lon": -75.02, "iso": "PER", "region": "South America"},
{"name": "Philippines", "lat": 12.88, "lon": 121.77, "iso": "PHL", "region": "Asia"},
{"name": "Poland", "lat": 51.92, "lon": 19.15, "iso": "POL", "region": "Europe"},
{"name": "Portugal", "lat": 39.4, "lon": -8.22, "iso": "PRT", "region": "Europe"},
{"name": "Romania", "lat": 45.94, "lon": 24.97, "iso": "ROU", "region": "Europe"},
{"name": "Russia", "lat": 61.52, "lon": 105.32, "iso": "RUS", "region": "Europe"},
{"name": "Rwanda", "lat": -1.94, "lon": 29.87, "iso": "RWA", "region": "Africa"},
{"name": "Saudi Arabia", "lat": 23.89, "lon": 45.08, "iso": "SAU", "region": "Asia"},
{"name": "Senegal", "lat": 14.5, "lon": -14.45, "iso": "SEN", "region": "Africa"},
{"name": "Serbia", "lat": 44.02, "lon": 21.01, "iso": "SRB", "region": "Europe"},
{"name": "Sierra Leone", "lat": 8.46, "lon": -11.78, "iso": "SLE", "region": "Africa"},
{"name": "Singapore", "lat": 1.35, "lon": 103.82, "iso": "SGP", "region": "Asia"},
{"name": "Slovakia", "lat": 48.67, "lon": 19.7, "iso": "SVK", "region": "Europe"},
{"name": "Slovenia", "lat": 46.15, "lon": 14.99, "iso": "SVN", "region": "Europe"},
{"name": "Somalia", "lat": 5.15, "lon": 46.2, "iso": "SOM", "region": "Africa"},
{"name": "South Africa", "lat": -30.56,"lon": 22.94, "iso": "ZAF", "region": "Africa"},
{"name": "South Korea", "lat": 35.91, "lon": 127.77, "iso": "KOR", "region": "Asia"},
{"name": "Spain", "lat": 40.46, "lon": -3.75, "iso": "ESP", "region": "Europe"},
{"name": "Sri Lanka", "lat": 7.87, "lon": 80.77, "iso": "LKA", "region": "Asia"},
{"name": "Sudan", "lat": 12.86, "lon": 30.22, "iso": "SDN", "region": "Africa"},
{"name": "Sweden", "lat": 60.13, "lon": 18.64, "iso": "SWE", "region": "Europe"},
{"name": "Switzerland", "lat": 46.82, "lon": 8.23, "iso": "CHE", "region": "Europe"},
{"name": "Syria", "lat": 34.8, "lon": 38.99, "iso": "SYR", "region": "Asia"},
{"name": "Taiwan", "lat": 23.7, "lon": 120.96, "iso": "TWN", "region": "Asia"},
{"name": "Tanzania", "lat": -6.37, "lon": 34.89, "iso": "TZA", "region": "Africa"},
{"name": "Thailand", "lat": 15.87, "lon": 100.99, "iso": "THA", "region": "Asia"},
{"name": "Tunisia", "lat": 33.89, "lon": 9.54, "iso": "TUN", "region": "Africa"},
{"name": "Turkey", "lat": 38.96, "lon": 35.24, "iso": "TUR", "region": "Europe"},
{"name": "Uganda", "lat": 1.37, "lon": 32.29, "iso": "UGA", "region": "Africa"},
{"name": "Ukraine", "lat": 48.38, "lon": 31.17, "iso": "UKR", "region": "Europe"},
{"name": "United Arab Emirates","lat": 23.42,"lon": 53.85, "iso": "ARE", "region": "Asia"},
{"name": "United Kingdom", "lat": 55.38, "lon": -3.44, "iso": "GBR", "region": "Europe"},
{"name": "United States", "lat": 37.09, "lon": -95.71, "iso": "USA", "region": "North America"},
{"name": "Uruguay", "lat": -32.52,"lon": -55.77, "iso": "URY", "region": "South America"},
{"name": "Uzbekistan", "lat": 41.38, "lon": 64.59, "iso": "UZB", "region": "Asia"},
{"name": "Venezuela", "lat": 6.42, "lon": -66.59, "iso": "VEN", "region": "South America"},
{"name": "Vietnam", "lat": 14.06, "lon": 108.28, "iso": "VNM", "region": "Asia"},
{"name": "Yemen", "lat": 15.55, "lon": 48.52, "iso": "YEM", "region": "Asia"},
{"name": "Zambia", "lat": -13.13,"lon": 27.85, "iso": "ZMB", "region": "Africa"},
{"name": "Zimbabwe", "lat": -19.02,"lon": 29.15, "iso": "ZWE", "region": "Africa"},
]
COUNTRY_INDEX = {c["name"]: c for c in COUNTRIES}
print(f"Loaded {len(COUNTRIES)} countries.")
Interactive Globe¶
Select countries from the dropdown, set hours with the slider, and pick a travel mode.
In [4]:
Collapse Code
MODE_COLORS = {
"Walking": "rgba(46, 204, 113, 0.25)",
"Cycling": "rgba(52, 152, 219, 0.25)",
"Driving": "rgba(231, 76, 60, 0.25)",
"Public Transit": "rgba(155, 89, 182, 0.25)",
}
MODE_LINE_COLORS = {
"Walking": "rgba(46, 204, 113, 0.8)",
"Cycling": "rgba(52, 152, 219, 0.8)",
"Driving": "rgba(231, 76, 60, 0.8)",
"Public Transit": "rgba(155, 89, 182, 0.8)",
}
def make_circle_points(center_lat, center_lon, radius_km, n=120):
"""Generate polygon points for a circle on the globe."""
R = 6371.0
angular_radius = radius_km / R
center_lat_rad = np.radians(center_lat)
center_lon_rad = np.radians(center_lon)
angles = np.linspace(0, 2 * np.pi, n)
lats = []
lons = []
for a in angles:
lat_rad = np.arcsin(
np.sin(center_lat_rad) * np.cos(angular_radius)
+ np.cos(center_lat_rad) * np.sin(angular_radius) * np.cos(a)
)
lon_rad = center_lon_rad + np.arctan2(
np.sin(a) * np.sin(angular_radius) * np.cos(center_lat_rad),
np.cos(angular_radius) - np.sin(center_lat_rad) * np.sin(lat_rad)
)
lats.append(np.degrees(lat_rad))
lons.append(np.degrees(lon_rad))
lats.append(lats[0])
lons.append(lons[0])
return lats, lons
def build_figure(selected_countries, hours, mode):
fig = go.Figure()
for name in selected_countries:
c = COUNTRY_INDEX[name]
speed = get_speed(mode, name)
radius_km = speed * hours
lats, lons = make_circle_points(c["lat"], c["lon"], radius_km)
fig.add_trace(go.Scattergeo(
lat=lats, lon=lons,
mode="lines",
line=dict(width=1.5, color=MODE_LINE_COLORS[mode]),
fill="toself",
fillcolor=MODE_COLORS[mode],
name=f"{name} ({radius_km:.0f} km)",
hoverinfo="name",
showlegend=True,
))
fig.add_trace(go.Scattergeo(
lat=[c["lat"]], lon=[c["lon"]],
mode="markers+text",
marker=dict(size=8, color=MODE_LINE_COLORS[mode], symbol="star"),
text=[name],
textposition="top center",
textfont=dict(size=10, color="white"),
hoverinfo="text",
hovertext=f"{name}<br>{speed:.1f} km/h<br>{radius_km:.0f} km in {hours}h",
showlegend=False,
))
fig.update_geos(
projection_type="orthographic",
showcoastlines=True,
coastlinecolor="rgba(255,255,255,0.3)",
showland=True,
landcolor="rgb(40, 40, 55)",
showocean=True,
oceancolor="rgb(20, 25, 40)",
showlakes=True,
lakecolor="rgb(20, 25, 40)",
showcountries=True,
countrycolor="rgba(255,255,255,0.15)",
bgcolor="rgba(0,0,0,0)",
)
fig.update_layout(
title=dict(
text=f"How Far Can You Get in {hours} Hour{'s' if hours != 1 else ''} by {mode}?",
font=dict(size=18, color="white"),
x=0.5,
),
paper_bgcolor="rgb(18, 18, 28)",
plot_bgcolor="rgb(18, 18, 28)",
font=dict(color="white"),
legend=dict(
font=dict(size=11),
bgcolor="rgba(0,0,0,0.3)",
bordercolor="rgba(255,255,255,0.1)",
borderwidth=1,
x=0.01, y=0.99,
),
margin=dict(l=0, r=0, t=50, b=0),
height=620,
)
return fig
country_names = [c["name"] for c in sorted(COUNTRIES, key=lambda x: x["name"])]
selected_dd = widgets.SelectMultiple(
options=country_names,
value=["United States", "Japan", "Germany", "Brazil", "Nigeria", "Australia", "India"],
rows=8,
description="Countries",
layout=widgets.Layout(width="240px"),
)
hours_slider = widgets.IntSlider(
value=4, min=1, max=48, step=1,
description="Hours",
style={"description_width": "50px"},
layout=widgets.Layout(width="400px"),
continuous_update=False,
)
hours_label = widgets.HTML(value="<b style='font-size:16px'>4 hours</b>")
mode_buttons = widgets.ToggleButtons(
options=["Walking", "Cycling", "Driving", "Public Transit"],
value="Driving",
description="Mode",
style={"description_width": "40px"},
button_style="",
tooltips=[
"~5 km/h",
"~15 km/h",
"~80 km/h",
"~40 km/h",
],
)
info_html = widgets.HTML()
def update_hours_label(change):
h = change["new"]
hours_label.value = f"<b style='font-size:16px'>{h} hour{'s' if h != 1 else ''}</b>"
hours_slider.observe(update_hours_label, names="value")
output = widgets.Output()
def on_change(change=None):
with output:
output.clear_output(wait=True)
sel = list(selected_dd.value)
hours = hours_slider.value
mode = mode_buttons.value
fig = build_figure(sel, hours, mode)
fig.show()
lines = []
for name in sel:
c = COUNTRY_INDEX[name]
speed = get_speed(mode, name)
dist = speed * hours
lines.append(
f"<div style='padding:4px 8px;margin:2px 0;border-radius:6px;"
f"background:rgba(255,255,255,0.05);font-size:13px'>"
f"<b>{name}</b>: {speed:.1f} km/h → <b>{dist:.0f} km</b> in {hours}h</div>"
)
info_html.value = "<div style='max-height:200px;overflow-y:auto;padding:8px;'>" + "".join(lines) + "</div>"
selected_dd.observe(on_change, names="value")
hours_slider.observe(on_change, names="value")
mode_buttons.observe(on_change, names="value")
header = widgets.HTML(
value="<h2 style='color:white;margin:0'>How Far Can You Get?</h2>"
"<p style='color:rgba(255,255,255,0.5);margin:4px 0 0 0;font-size:13px'>"
"Select countries, set time, pick a mode</p>"
)
controls = widgets.VBox([
header,
widgets.HBox([
widgets.VBox([selected_dd], layout=widgets.Layout(align_items="flex-start")),
widgets.VBox([
mode_buttons,
hours_slider,
hours_label,
info_html,
], layout=widgets.Layout(padding="0 0 0 16px")),
]),
], layout=widgets.Layout(
padding="16px",
background_color="rgb(28, 28, 42)",
border_radius="12px",
margin="0 0 8px 0",
))
display(controls)
display(output)
on_change()