---
title: "Me and the world"
format:
html:
code-tools: true
title-block-banner: "#dd643fd3"
description: "Last updated: Aug. 2025"
resources:
- files/
- frames/
- photos/
include-in-header:
- files/maplibre.html
jupyter: base
---
## My Travel History
Click country shapes to see more information.
<!-- <iframe src="./files/my_travel_history.html" width="100%" height="400px" style="border: none;" scrolling="no"></iframe> -->
```{python}
#| echo: false
import geopandas as gpd
import pandas as pd
import numpy as np
import folium
import altair as alt
country_boundaries = gpd.read_file('./files/world-administrative-boundaries.zip')
my_travel_df = pd.read_excel('./files/Travel History.xlsx')
iso3_unique = my_travel_df.iso3.unique()
country_visited = country_boundaries[country_boundaries.iso3.isin(iso3_unique)]
def find_short_name_subdivision(full_name: str, short_list:list):
for i in short_list:
if i in full_name:
return i
return None
province_shape_CN = gpd.read_file('./files/chn_adm_ocha_2020_shp.zip', layer='chn_admbnda_adm1_ocha_2020')
province_shape_CN = province_shape_CN[~province_shape_CN.ADM1_EN.str.contains('Taiwan')]
province_shape_CN = province_shape_CN[~province_shape_CN.ADM1_EN.str.contains('Macao')]
province_shape_CN = province_shape_CN[~province_shape_CN.ADM1_EN.str.contains('Hong Kong')]
my_travel_df_CN = my_travel_df[my_travel_df.iso3 == 'CHN']
province_shape_CN['short'] = province_shape_CN.ADM1_EN.apply(lambda x: find_short_name_subdivision(full_name=x, short_list= my_travel_df_CN.Subdivisions.unique()))
iso32popup = {}
gdf_CN = my_travel_df_CN.merge(province_shape_CN, left_on='Subdivisions',right_on='short', how='outer')
gdf_CN.rename(columns={'ADM1_EN': 'Subdivision'}, inplace=True)
gdf_CN = gpd.GeoDataFrame(gdf_CN, geometry=gdf_CN.geometry, crs=province_shape_CN.crs)
gdf_CN = gdf_CN[['Subdivision', 'First visit', 'geometry']].to_crs(epsg = 4326)
# Simplify geometries
tolerance = 0.05 # smaller values retain more detail
gdf_CN['geometry'] = gdf_CN['geometry'].simplify(tolerance, preserve_topology=True)
# gdf_CH.columns = ['Canton', 'First visit', 'geometry']
gdf_CN['color'] = np.where(gdf_CN['First visit'].notna(), 'pink', 'lightgray')
gdf = gdf_CN # Replace with the actual file path
# Ensure the GeoDataFrame has a column for canton names and geometries
gdf_json = gdf.to_json()
# Create an Altair Chart
chart = alt.Chart(alt.Data(values=gdf.__geo_interface__['features'])).mark_geoshape(
# fill='pink',
stroke='black',
strokeWidth=0.5
).encode(
color=alt.Color(
'properties.color:N', # Use the color column
scale=None, # No color scale since we're using fixed values
legend=None # Remove legend for simplicity
),
tooltip=[
alt.Tooltip('properties.Subdivision:N', title='Subdivision'),
alt.Tooltip('properties.First visit:N', title='First Visit')
]
).properties(
width=300,
height=170,
).project(
type='mercator'
).properties(title=f"China Mainland: {(~gdf_CN['First visit'].isna()).sum()} of {len(gdf_CN)} subdivisions visited")
# Display the chart
vega_lite = folium.VegaLite(
chart,
width="100%",
height="100%",
)
cn_popup = folium.Popup()
vega_lite.add_to(cn_popup)
# chart.show()
iso32popup['CHN'] = cn_popup
canton_shape_CH = gpd.read_file('./files/swissBOUNDARIES.zip').set_index('name')
my_travel_df_CH = my_travel_df[my_travel_df.Country == 'Switzerland']
gdf_CH = my_travel_df_CH.merge(canton_shape_CH, left_on='Subdivisions',right_index=True, how='outer')
gdf_CH = gpd.GeoDataFrame(gdf_CH, geometry=canton_shape_CH.loc[gdf_CH['Subdivisions'].values].geometry.values, crs=canton_shape_CH.crs)
gdf_CH = gdf_CH[['Subdivisions', 'First visit', 'geometry']].to_crs(epsg = 4326)
# Simplify geometries
tolerance = 0.005 # smaller values retain more detail
gdf_CH['geometry'] = gdf_CH['geometry'].simplify(tolerance, preserve_topology=True)
gdf_CH.columns = ['Canton', 'First visit', 'geometry']
gdf_CH['color'] = np.where(gdf_CH['First visit'].notna(), 'pink', 'lightgray')
gdf = gdf_CH # Replace with the actual file path
# Ensure the GeoDataFrame has a column for canton names and geometries
# Convert GeoDataFrame to GeoJSON format
# gdf = gdf.to_crs("EPSG:4326") # Ensure WGS84 CRS for web visualizations
gdf_json = gdf.to_json()
# Create an Altair Chart
chart = alt.Chart(alt.Data(values=gdf.__geo_interface__['features'])).mark_geoshape(
# fill='pink',
stroke='black',
strokeWidth=0.5
).encode(
color=alt.Color(
'properties.color:N', # Use the color column
scale=None, # No color scale since we're using fixed values
legend=None # Remove legend for simplicity
),
tooltip=[
alt.Tooltip('properties.Canton:N', title='Canton'),
alt.Tooltip('properties.First visit:N', title='First Visit')
]
).properties(
width=300,
height=150,
# title="Swiss Cantons Map"
).project(
type='mercator'
).properties(title=f"Switzerland: {(~gdf_CH['First visit'].isna()).sum()} of {len(gdf_CH)} cantons visited")
# Display the chart
vega_lite = folium.VegaLite(
chart,
width="100%",
height="100%",
)
ch_popup = folium.Popup()
vega_lite.add_to(ch_popup)
# chart.show()
iso32popup['CHE'] = ch_popup
m = folium.Map([40, 50], zoom_start=2, tiles="cartodbpositron", height=400)
for country_code in iso3_unique:
country_gdf = country_boundaries[country_boundaries.iso3 == country_code]
df_show = my_travel_df[my_travel_df.iso3 == country_code].drop(columns=['iso3'])
df_show.index = np.arange(len(df_show))+1
html = df_show.to_html(
classes="table table-striped table-hover table-condensed table-responsive"
)
folium.GeoJson(
country_gdf,
# name="lines",
style_function=lambda x: {
"fillColor": "red" if x['properties']['iso3'] in iso32popup.keys() else "orange",
"color": 'black',
"weight": 1.0,
"opacity": 0.8,
"fillOpacity": 0.5,
},
popup=folium.Popup(html=html,max_width="300") if country_code not in iso32popup.keys() else iso32popup[country_code],
highlight_function=lambda x: {"fillOpacity": 0.9},
# zoom_on_click=True,
).add_to(m)
m
```
## My Flight Map
```{=html}
<div id="map" style="width: 100%; height: 400px;"></div>
<script>
const map = new maplibregl.Map({
container: "map", // the id of the div element
style: `https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json`,
zoom: 1, // starting zoom
center: [50, 40] // starting location [longitude, latitude]
});
map.on('load', () => {
map.addSource('airports', {
type: 'geojson',
data: './files/my_airports_gdf.geojson'
});
map.addLayer({
id: 'airports',
type: 'circle',
source: 'airports',
paint: {
'circle-radius': ["+",["get", "No. visit"], 3],
'circle-color': 'orange',
'circle-opacity': 0.6,
'circle-stroke-color': 'black',
'circle-stroke-width': 1
}
});
map.on('click', 'airports', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const { Name, 'No. visit': noVisit } = e.features[0].properties;
new maplibregl.Popup()
.setLngLat(coordinates)
.setHTML(`<strong>${Name}</strong><br>Total visit: ${noVisit}`)
.addTo(map);
});
map.on("mouseenter", "airports", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "airports", () => {
map.getCanvas().style.cursor = "";
});
map.addSource('flights', {
type: 'geojson',
data: './files/my_flights_gdf.geojson'
});
map.addLayer({
id: 'flights',
type: 'line',
source: 'flights',
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': 'black',
'line-width': ["*", ["get", "No. flight"], 1.2]
}
});
map.on('click', 'flights', (e) => {
const coordinates = e.features[0].geometry.coordinates[0];
const { Route, 'No. flight': noFlight } = e.features[0].properties;
new maplibregl.Popup()
.setLngLat(coordinates)
.setHTML(`<strong>${Route}</strong><br>Total flight: ${noFlight}`)
.addTo(map);
});
map.on("mouseenter", "flights", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "flights", () => {
map.getCanvas().style.cursor = "";
});
});
</script>
```
<!-- <iframe src="./files/my_flight_log.html" width="100%" height="400px" style="border: none;" scrolling="no"></iframe> -->
<!-- <iframe src="./files/mpl_test_flight.html" width="100%" height="400px" style="border: none;" scrolling="no"></iframe> -->
### Records
```{python}
#| echo: false
import pandas as pd
import requests
import geopandas as gpd
from itables import show
my_flights_gdf = gpd.read_file('./files/my_flights_gdf.geojson')
airlines_json = requests.get('https://cdn.jsdelivr.net/gh/besrourms/airlines@latest/airlines.json').json()
airlines = pd.read_json('https://cdn.jsdelivr.net/gh/besrourms/airlines@latest/airlines.json')
code2fullname = dict(zip(airlines.code, airlines.name))
code2logo = dict(zip(airlines.code, airlines.logo))
my_flights_gdf['Airlines'] = my_flights_gdf['Flight Number'].str[:2].apply(lambda x: code2fullname[x])
my_flights_gdf['Airlines logo'] = my_flights_gdf['Flight Number'].str[:2].apply(lambda x: code2logo[x])
my_flights_gdf.rename(columns={'Aircraft Registration': 'Tail', 'Flight Number': 'Flight', 'Departure': 'DEP', 'Arrival': 'ARR'}, inplace=True)
my_flights_df = my_flights_gdf[['Date', 'Airlines', 'Flight', 'DEP', 'ARR', 'Aircraft', 'Tail', 'Distance']]
my_flights_df.index += 1#my_flights_df.index
# display(my_flights_df.to_html(escape=False))
show(my_flights_df, buttons=["pageLength" ,"copyHtml5", "csvHtml5", "excelHtml5"], searching = True, lengthMenu=[20, 50, 100], select=True)
```
### Top Airlines
```{python}
#| echo: false
my_flights_gdf['Airlines logo'] = "<img src=\"" + my_flights_gdf['Airlines logo']+ " Logo\" width=\"25\" height=\"25\">"
airline_counts = my_flights_gdf.groupby('Airlines',as_index=False).count().iloc[:,[0,1]]
airline_counts.columns = [' ','Flights']
airline_counts['Airline'] = my_flights_gdf.groupby('Airlines',as_index=False).apply(lambda x : x['Airlines logo']).unique()
airline_counts = airline_counts.sort_values(by='Flights',ascending=False).set_index('Airline')
airline_counts.index.rename(None,inplace=True)
# display(airline_counts.to_html(escape=False))
airline_counts.columns = ['Airline','Total flights']
show(airline_counts, buttons=["copyHtml5", "csvHtml5", "excelHtml5"], searching = False, paging=False, select=True)
```
### Top Aircrafts
```{python}
#| echo: false
aircraft_counts = my_flights_df.groupby('Aircraft',as_index=False).count().iloc[:,[0,1]]
aircraft_counts.columns = [' ','Flights']
com2con = {'Airbus':'eu', 'Boeing':'us', 'COMAC': 'cn'}
aircraft_counts['Aircraft'] = [com2con[i[0]] for i in aircraft_counts[' '].str.split()]
aircraft_counts['Aircraft'] = "<img src=\"https://flagicons.lipis.dev/flags/4x3/" + aircraft_counts['Aircraft'] + ".svg\" width=\"25\" height=\"25\">"
aircraft_counts = aircraft_counts.sort_values(by='Flights',ascending=False).set_index('Aircraft')
aircraft_counts.index.rename(None,inplace=True)
# display(aircraft_counts.to_html(escape=False))
aircraft_counts.columns = ['Aircraft','Total flights']
show(aircraft_counts , buttons=["copyHtml5", "csvHtml5", "excelHtml5"], searching = False, paging=False, select=True)
```
## My Grand Tour of Switzerland
```{=html}
<style>
.gallery-holder {
justify-content: center;
display: flex;
}
.gallery {
display: grid;
justify-content: center;
grid-template-columns: repeat(auto-fit, minmax(185px, 1fr));
gap: 30px;
max-width: 900px;
width: 100%;
}
.parent {
justify-content: center;
position: relative;
width: 100%;
aspect-ratio: 2 / 3;
overflow: visible;
}
.image-bottom,
.image-top {
position: absolute;
bottom: 0;
width: 100%;
object-fit: cover;
}
/* Ensure frame is always on top */
.image-bottom {
z-index: 1;
}
.image-top {
pointer-events: none; /* let clicks pass through frame */
z-index: 2;
}
</style>
<div class="gallery-holder">
<div class="gallery" id="gallery"></div>
</div>
<script>
// Example: list of image IDs
const ids = [
"03dfca1732a34e048bf9e982cfb1b047",
"0489f03a650548c8b1d530b726d5520c",
"053adb7f766546b2aca72fcdd3f5574e",
"059bb164caea44ab8acab92eeb6c41f5",
"0d5380288eb4425faeeedcabe1777a75",
"1fe26c24311a48bfb22e6bbe77d064fd",
"2c1fec19b3d74cdeb242c54871711c1a",
"310bd03a0c1b4295be48feca44679d8a",
"310e8b3aacff4e36a9059d6683fd78ce",
"37ed973cbfd945c08fcc727d88dcaab4",
"3f16e603cffe4ea89ac6d1597af0fc2b",
"3f217c0c47cb48f09016114759f50a25",
"6343af663e674f05be3e23b7a1f4b80f",
"642e5924174b4598aeed9c2a4f1e9606",
"6479e9157c1a425b8ccabc2f79209117",
"65ed913bb30c43eeb34128870081b254",
"6ef48e6bd2f64cbf8d8b8cfca3698808",
"74c6ef63346746cea2f59a22359a9c3d",
"781b9934cd8e4704b1022bcc70a84d18",
"7b0b1c3581e74a6ebea20fbeafd9a0bc",
"7d376d03b5254bf98a8d74d77fc10f31",
"82e8286205ad4fb3a722e28922222667",
"88d1ed0c2bc648f6b78e63cf8ebc88d1",
"89c6c4733a1c408eb6d58a62bee2e491",
"94ed931a90f84eafa6f91958b1437a67",
"9f438c5e776e4ef380e4dbbe557e7e40",
"a8961d340bf84f4589b56a5f74cbe579",
"b691bd3cd0f24a599ebda59a8d8de64d",
"ce20c923bb2d4c38a12a1a2083a3193a",
"d0802a7d70194ed7a173e09ea9d41c01",
"d220a804a14d4a79a7e26373ab9e09c2",
"e85f260bb9094bafa330635b55023b01",
"e896f538371843dc9ca9bf87b150cb8a",
"f246203efbfb4185a9ac7b70f47765f4",
"f6a6747825ce4c399409ac6819824d71",
"f703532e6d774b4198eea6ca2d88361a",
"030bf58a23034cb09b0302cbeae5816d",
"0b1da4f3bb554956ae320b14059bdcc8",
"13d623fc89e144deb317c6016f2b726d",
"19eb4d4084c848bc97c8e5f05a938f5b",
"1cf3274b35c34b01892675cd1c91ed97",
"1e13bb9b41ba4688813b1321f34e7441",
"21db9c9bd0ff4a599828e367000a311f",
"21fd86b67fdb4b279e12dc4d54d16b00",
"28d93d4a946546bfac952a25e39ca74e",
"37db038cd3a54fce9684f84f2bf38b5d",
"3f3f65b2892c4820afb7662b585eda9d",
"411ab3a98c5c443091847b259c69e6d5",
"5578cf9ed1cc4c8799fc53356ca90323",
"56fd2186cdf7435489b90d5897fe5326",
"584fe4fe251943ccae0e37b645a96dba",
"5f3802f34b8b4da6bfec76c0eef2f07e",
"60ea54929c3f45ff9d592883cfeaca08",
"708096b6a3e649e88f809142916d6710",
"86d0a7f0935744e3b24f216561af35cb",
"8ca3a384611a4b5bb546dae578675cf6",
"8e59a288d2eb4638a14fb15d9c03a97c",
"907715938bcd45ffafad5f797c735d7a",
"965762fed8f54cfe8197b1cb5592f394",
"96a9869e584f483b8fa7673c3d969c05",
"981afb89c78042949c8e3130955ad6de",
"9c821bff0135447b954481deb43c45bd",
"a7f80df0d20f46af80cf459f1678a681",
"b1241a41d1ee4235ab86098095c42429",
"b29a89aaf7614bdd8dc9109c33720d1d",
"bde80944ac79498ea2f299f94c8e640f",
"bebe3a8672d64d2387285a95eaaa794b",
"c1fa38410bb74bc6beedb8af5bce3fb0",
"d11553b4b564486fa20d80f5b9669623",
"d83fa98cad7f4703acd568ae68756c94",
"e04f924aca294e9db5fa0d5abb90249b",
"e73e13f4076b41b4a87114c0a7ce6060",
"077c78c45ab545bd8058dedac150bc90",
"0b3128e8bfd740c6b3ac97f90d823c9d",
"12e9dced25a74fb7b893c56cfe50f51f",
"12ff20e92eda4fd4ae6527b5fbdb3fbd",
"17790fd51d2148cab11b50fc294d8d33",
"7741a7e8f19141a0bc25da1eb3b3e2fb",
"820b38c82fe543b68040f37bcf7e3b7b",
"8bdcb4c1528949d88a9f93ac172fa219",
"944d54d1a0f34db78341849432672531",
"96c95ddafb574e30b9995cdf6b2bc816",
"a8607dd1f6944322bec3058b32329b27",
"ab4cf7dd49fc43eda92601f26b2b2525",
"c6144fefb1f8426ea5a557e1cb2fa218",
"e2285fd4e11c4a96a980988024aa3f70",
"e3bae9ed51c34c66856958e71de3df43",
"f2fb9ad058214e35ac1b8bce52191d85"
];
const gallery = document.getElementById("gallery");
ids.forEach(id => {
const parent = document.createElement("div");
parent.className = "parent";
const object = document.createElement("object");
object.className = "image-bottom";
object.type = "image/svg+xml";
object.data = `./photos/${id}.svg`;
const img = document.createElement("img");
img.className = "image-top";
img.src = `./frames/${id}.svg`;
// If the object fails to load, remove it
object.onerror = () => object.remove();
parent.appendChild(object);
parent.appendChild(img);
gallery.appendChild(parent);
});
</script>
```