d3 =require("d3@7")topojson =require("topojson-client@3")// Your location data by yearlocationData = [ { year:1998,location:"Racine",country:"USA",lat:42.7261,lon:-87.7828 }, { year:1999,location:"Racine",country:"USA",lat:42.7261,lon:-87.7828 }, { year:2000,location:"Racine",country:"USA",lat:42.7261,lon:-87.7828 }, { year:2001,location:"Racine",country:"USA",lat:42.7261,lon:-87.7828 }, { year:2002,location:"Racine",country:"USA",lat:42.7261,lon:-87.7828 }, { year:2003,location:"Racine",country:"USA",lat:42.7261,lon:-87.7828 }, { year:2004,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2005,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2006,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2007,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2008,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2009,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2010,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2011,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2012,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2013,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2014,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2015,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2016,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2017,location:"Reutlingen",country:"Germany",lat:48.4921,lon:9.2095 }, { year:2018,location:"Stirling",country:"Scotland",lat:56.1165,lon:-3.9369 }, { year:2019,location:"Stirling",country:"Scotland",lat:56.1165,lon:-3.9369 }, { year:2020,location:"Stirling",country:"Scotland",lat:56.1165,lon:-3.9369 }, { year:2021,location:"Stirling",country:"Scotland",lat:56.1165,lon:-3.9369 }, { year:2022,location:"Royal Leamington Spa",country:"England",lat:52.2851,lon:-1.5364 }, { year:2023,location:"Bath",country:"England",lat:51.3751,lon:-2.3618 }, { year:2024,location:"Irvine",country:"USA",lat:33.6846,lon:-117.8265 }, { year:2025,location:"Irvine",country:"USA",lat:33.6846,lon:-117.8265 }]// Load world map dataworld = {const response =awaitfetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json");return topojson.feature(await response.json(),"countries");}// Function to get current theme colors for timeline mapgetTimelineThemeColors = () => {const isDark =document.body.classList.contains('quarto-dark');if (isDark) {return {landColor:"#374151",// gray-700countryStroke:"#6b7280",// gray-500futurePath:"#60a5fa",// blue-400traveledPath:"#f87171",// red-400currentMarker:"#f87171",// red-400otherMarkers:"#60a5fa",// blue-400markerStroke:"#e5e7eb",// gray-200labelText:"#f3f4f6",// gray-100labelBg:"rgba(17, 24, 39, 0.8)",// gray-900 with opacitytooltipBg:"rgba(17, 24, 39, 0.95)",// gray-900 with opacitytooltipText:"#f3f4f6",// gray-100tooltipAccent:"#fbbf24",// amber-400controlsBg:"rgba(17, 24, 39, 0.8)",// gray-900 with opacitycontrolsText:"#f3f4f6",// gray-100controlsButtonBg:"rgba(75, 85, 99, 0.8)",// gray-600 with opacitysliderBg:"rgba(17, 24, 39, 0.8)",// gray-900 with opacitysliderText:"#f3f4f6",// gray-100sliderButtonBg:"rgba(75, 85, 99, 0.8)"// gray-600 with opacity }; } else {return {landColor:"#e5e7eb",// gray-200countryStroke:"#9ca3af",// gray-400futurePath:"#3b82f6",// blue-500traveledPath:"#ef4444",// red-500currentMarker:"#ef4444",// red-500otherMarkers:"#3b82f6",// blue-500markerStroke:"#374151",// gray-700labelText:"#374151",// gray-700labelBg:"rgba(255, 255, 255, 0.9)",// white with opacitytooltipBg:"rgba(255, 255, 255, 0.95)",// white with opacitytooltipText:"#374151",// gray-700tooltipAccent:"#f59e0b",// amber-500controlsBg:"rgba(255, 255, 255, 0.9)",// white with opacitycontrolsText:"#374151",// gray-700controlsButtonBg:"rgba(243, 244, 246, 0.8)",// gray-100 with opacitysliderBg:"rgba(255, 255, 255, 0.9)",// white with opacitysliderText:"#374151",// gray-700sliderButtonBg:"rgba(243, 244, 246, 0.8)"// gray-100 with opacity }; }}// Theme-adaptive year selector with buttonsviewof selectedYear = {const MIN_YEAR =1998;const MAX_YEAR =2025;const DEFAULT_YEAR =2025;const colors =getTimelineThemeColors();const container =html`<div style="display: flex; flex-direction: column; width: 100%;"> <div style="display: flex; align-items: center; margin-bottom: 8px;"> <button id="prev-year" style=" background: ${colors.sliderButtonBg}; color: ${colors.sliderText}; border: 1px solid ${colors.countryStroke}; border-radius: 4px; padding: 4px 8px; margin-right: 10px; cursor: pointer; font-size: 16px; width: 36px; ">◀</button> <div style="flex-grow: 1; text-align: center; color: ${colors.sliderText}; font-size: 16px;"> Year: <span id="year-display">${DEFAULT_YEAR}</span> </div> <button id="next-year" style=" background: ${colors.sliderButtonBg}; color: ${colors.sliderText}; border: 1px solid ${colors.countryStroke}; border-radius: 4px; padding: 4px 8px; margin-left: 10px; cursor: pointer; font-size: 16px; width: 36px; ">▶</button> </div> <input type="range" id="year-slider" min="${MIN_YEAR}" max="${MAX_YEAR}" step="1" value="${DEFAULT_YEAR}" style="width: 100%; accent-color: ${colors.traveledPath};"> </div>`;const slider = container.querySelector("#year-slider");const display = container.querySelector("#year-display");const prevBtn = container.querySelector("#prev-year");const nextBtn = container.querySelector("#next-year");let value = DEFAULT_YEAR;// Function to update colors based on themeconst updateColors = () => {const currentColors =getTimelineThemeColors(); prevBtn.style.background= currentColors.sliderButtonBg; prevBtn.style.color= currentColors.sliderText; prevBtn.style.borderColor= currentColors.countryStroke; nextBtn.style.background= currentColors.sliderButtonBg; nextBtn.style.color= currentColors.sliderText; nextBtn.style.borderColor= currentColors.countryStroke; display.parentElement.style.color= currentColors.sliderText; slider.style.accentColor= currentColors.traveledPath; };// Function to update everythingconst updateYear = (year) => { year =Math.max(MIN_YEAR,Math.min(MAX_YEAR, year)); value = year; slider.value= year; display.textContent= year;// Update button states prevBtn.disabled= year <= MIN_YEAR; prevBtn.style.opacity= year <= MIN_YEAR ?"0.5":"1"; nextBtn.disabled= year >= MAX_YEAR; nextBtn.style.opacity= year >= MAX_YEAR ?"0.5":"1"; container.dispatchEvent(newCustomEvent("input")); };// Update when slider changes slider.oninput= () => {updateYear(parseInt(slider.value)); };// Previous year button prevBtn.onclick= () => {updateYear(value -1); };// Next year button nextBtn.onclick= () => {updateYear(value +1); };// Add keyboard navigation container.tabIndex=0; container.style.outline="none"; container.addEventListener("keydown", (event) => {if (event.key==="ArrowLeft") {event.preventDefault();updateYear(value -1); } elseif (event.key==="ArrowRight") {event.preventDefault();updateYear(value +1); } });// Listen for theme changesconst observer =newMutationObserver(() => {updateColors(); }); observer.observe(document.body, {attributes:true,attributeFilter: ['class'] });// InitializeupdateYear(DEFAULT_YEAR);updateColors();// Define getter for ObservableObject.defineProperty(container,"value", {get: () => value });return container;}// Current location based on selected yearcurrentLocation = locationData.find(d => d.year=== selectedYear)// Create a theme-adaptive tooltip elementtooltip = {const colors =getTimelineThemeColors();const div =html`<div style=" position: absolute; background: ${colors.tooltipBg}; border: 1px solid ${colors.countryStroke}; border-radius: 5px; padding: 10px; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 1000; font-family: sans-serif; font-size: 14px; max-width: 200px; color: ${colors.tooltipText}; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); "></div>`;document.body.appendChild(div);// Function to update tooltip colorsconst updateTooltipColors = () => {const currentColors =getTimelineThemeColors(); div.style.background= currentColors.tooltipBg; div.style.borderColor= currentColors.countryStroke; div.style.color= currentColors.tooltipText; };// Listen for theme changesconst observer =newMutationObserver(() => {updateTooltipColors(); }); observer.observe(document.body, {attributes:true,attributeFilter: ['class'] });return div;}// Theme-adaptive map visualizationmapVisualization = {const colors =getTimelineThemeColors();// Set up dimensions and projectionconst width =900;const height =500;// Create a projection focused on US and Europeconst projection = d3.geoMercator().center([-40,45]).scale(250).translate([width /2, height /2]);// Create path generatorconst path = d3.geoPath().projection(projection);// Create SVGconst svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0,0, width, height]).attr("style","max-width: 100%; height: auto;");// Add ocean background svg.append("rect").attr("width", width).attr("height", height).attr("fill","none");// Create a group for all map elementsconst g = svg.append("g");// Draw countriesconst countries = g.append("g").selectAll("path").data(world.features).join("path").attr("fill", colors.landColor).attr("stroke", colors.countryStroke).attr("stroke-width",0.5).attr("d", path);// Get unique locations to avoid drawing the same point multiple timesconst uniqueLocations =Array.from(newMap(locationData.map(d => [`${d.lat},${d.lon}`, d])).values() ).sort((a, b) => a.year- b.year);// Draw path lines connecting unique locationsconst lineGenerator = d3.line().x(d =>projection([d.lon, d.lat])[0]).y(d =>projection([d.lon, d.lat])[1]).curve(d3.curveCardinal.tension(0.5));// Draw the complete path const futurePath = g.append("path").attr("d",lineGenerator(uniqueLocations)).attr("fill","none").attr("stroke", colors.futurePath).attr("stroke-width",2).attr("stroke-dasharray","5,5").attr("opacity",0.7);// Highlight the path traveled up to the current yearconst currentYearIndex = uniqueLocations.findIndex(d => {const locationsAtThisPoint = locationData.filter(loc => loc.lat=== d.lat&& loc.lon=== d.lon );const years = locationsAtThisPoint.map(loc => loc.year);const minYear =Math.min(...years);return selectedYear < minYear; });let traveledPathIndex;if (currentYearIndex ===-1) { traveledPathIndex = uniqueLocations.length; } elseif (currentYearIndex ===0) { traveledPathIndex =0; } else { traveledPathIndex = currentYearIndex; }// Draw traveled pathlet traveledPath =null;if (traveledPathIndex >0) {const traveledPathData = uniqueLocations.slice(0, traveledPathIndex); traveledPath = g.append("path").attr("d",lineGenerator(traveledPathData)).attr("fill","none").attr("stroke", colors.traveledPath).attr("stroke-width",3).attr("opacity",0.9); }// Create a group for markers and labelsconst markersGroup = g.append("g");// Get unique locations for markersconst markerLocations =Array.from(newMap(locationData.map(d => [`${d.lat},${d.lon}`, d])).values() );// Draw all locations as markersconst markers = markersGroup.selectAll("circle").data(markerLocations).join("circle").attr("cx", d =>projection([d.lon, d.lat])[0]).attr("cy", d =>projection([d.lon, d.lat])[1]).attr("r", d => {const isCurrentLocation = locationData.some(loc => loc.year=== selectedYear && loc.lat=== d.lat&& loc.lon=== d.lon );return isCurrentLocation ?8:4; }).attr("fill", d => {const isCurrentLocation = locationData.some(loc => loc.year=== selectedYear && loc.lat=== d.lat&& loc.lon=== d.lon );return isCurrentLocation ? colors.currentMarker: colors.otherMarkers; }).attr("stroke", colors.markerStroke).attr("stroke-width", d => {const isCurrentLocation = locationData.some(loc => loc.year=== selectedYear && loc.lat=== d.lat&& loc.lon=== d.lon );return isCurrentLocation ?2:1; }).attr("opacity", d => {const isCurrentLocation = locationData.some(loc => loc.year=== selectedYear && loc.lat=== d.lat&& loc.lon=== d.lon );return isCurrentLocation ?1:0.7; }).on("mouseover",function(event, d) {const locYears = locationData.filter(loc => loc.lat=== d.lat&& loc.lon=== d.lon).map(loc => loc.year);const yearRanges = [];let start = locYears[0];let end = locYears[0];for (let i =1; i < locYears.length; i++) {if (locYears[i] === end +1) { end = locYears[i]; } else { yearRanges.push(start === end ?`${start}`:`${start}-${end}`); start = end = locYears[i]; } } yearRanges.push(start === end ?`${start}`:`${start}-${end}`); d3.select(this).attr("r",12).attr("stroke-width",3);const currentColors =getTimelineThemeColors(); tooltip.innerHTML=` <strong style="color: ${currentColors.tooltipAccent};">${d.location}, ${d.country}</strong><br/> Years: ${yearRanges.join(", ")}<br/> Coordinates: ${d.lat.toFixed(2)}, ${d.lon.toFixed(2)} `; tooltip.style.opacity="1"; tooltip.style.left= (event.pageX+15) +"px"; tooltip.style.top= (event.pageY-28) +"px"; }).on("mouseout",function(event, d) {const isCurrentLocation = locationData.some(loc => loc.year=== selectedYear && loc.lat=== d.lat&& loc.lon=== d.lon ); d3.select(this).attr("r", isCurrentLocation ?8:4).attr("stroke-width", isCurrentLocation ?2:1); tooltip.style.opacity="0"; }).on("mousemove",function(event) { tooltip.style.left= (event.pageX+15) +"px"; tooltip.style.top= (event.pageY-28) +"px"; });// Add label for current locationlet currentLocationLabel =null;let currentLocationBg =null;if (currentLocation) {const [x, y] =projection([currentLocation.lon, currentLocation.lat]); currentLocationBg = markersGroup.append("rect").attr("x", x +10).attr("y", y -15).attr("width",`${currentLocation.location.length*8+30}px`).attr("height","22px").attr("fill", colors.labelBg).attr("rx",3).attr("ry",3); currentLocationLabel = markersGroup.append("text").attr("x", x +12).attr("y", y).text(`${currentLocation.location} (${currentLocation.year})`).attr("font-family","sans-serif").attr("font-size","14px").attr("font-weight","bold").attr("fill", colors.labelText); }// Add zoom controlsconst zoomControls = svg.append("g").attr("transform",`translate(${width -70}, 20)`);// Zoom in buttonconst zoomInButton = zoomControls.append("rect").attr("x",0).attr("y",0).attr("width",30).attr("height",30).attr("fill", colors.controlsButtonBg).attr("stroke", colors.countryStroke).attr("rx",5).attr("ry",5).attr("cursor","pointer").on("click", () => zoom.scaleBy(svg.transition().duration(300),1.5));const zoomInText = zoomControls.append("text").attr("x",15).attr("y",20).attr("text-anchor","middle").attr("fill", colors.controlsText).attr("font-size","18px").attr("font-weight","bold").attr("pointer-events","none").text("+");// Zoom out buttonconst zoomOutButton = zoomControls.append("rect").attr("x",0).attr("y",40).attr("width",30).attr("height",30).attr("fill", colors.controlsButtonBg).attr("stroke", colors.countryStroke).attr("rx",5).attr("ry",5).attr("cursor","pointer").on("click", () => zoom.scaleBy(svg.transition().duration(300),0.75));const zoomOutText = zoomControls.append("text").attr("x",15).attr("y",60).attr("text-anchor","middle").attr("fill", colors.controlsText).attr("font-size","18px").attr("font-weight","bold").attr("pointer-events","none").text("−");// Reset buttonconst resetButton = zoomControls.append("rect").attr("x",0).attr("y",80).attr("width",30).attr("height",30).attr("fill", colors.controlsButtonBg).attr("stroke", colors.countryStroke).attr("rx",5).attr("ry",5).attr("cursor","pointer").on("click", resetZoom);const resetText = zoomControls.append("text").attr("x",15).attr("y",100).attr("text-anchor","middle").attr("fill", colors.controlsText).attr("font-size","14px").attr("font-weight","bold").attr("pointer-events","none").text("R");// Function to update all colors based on current themefunctionupdateMapColors() {const currentColors =getTimelineThemeColors();// Update countries countries.attr("fill", currentColors.landColor).attr("stroke", currentColors.countryStroke);// Update paths futurePath.attr("stroke", currentColors.futurePath);if (traveledPath) { traveledPath.attr("stroke", currentColors.traveledPath); }// Update markers markers.attr("fill", d => {const isCurrentLocation = locationData.some(loc => loc.year=== selectedYear && loc.lat=== d.lat&& loc.lon=== d.lon );return isCurrentLocation ? currentColors.currentMarker: currentColors.otherMarkers; }).attr("stroke", currentColors.markerStroke);// Update labelsif (currentLocationBg) { currentLocationBg.attr("fill", currentColors.labelBg); }if (currentLocationLabel) { currentLocationLabel.attr("fill", currentColors.labelText); }// Update controls zoomInButton.attr("fill", currentColors.controlsButtonBg).attr("stroke", currentColors.countryStroke); zoomOutButton.attr("fill", currentColors.controlsButtonBg).attr("stroke", currentColors.countryStroke); resetButton.attr("fill", currentColors.controlsButtonBg).attr("stroke", currentColors.countryStroke); zoomInText.attr("fill", currentColors.controlsText); zoomOutText.attr("fill", currentColors.controlsText); resetText.attr("fill", currentColors.controlsText); }// Define zoom behaviorconst zoom = d3.zoom().scaleExtent([0.5,8]).on("zoom", zoomed);functionzoomed(event) { g.attr("transform",event.transform);const strokeScale =1/Math.sqrt(event.transform.k); g.selectAll("path").attr("stroke-width",0.5* strokeScale); markersGroup.selectAll("circle").each(function(d) {const isCurrentLocation = locationData.some(loc => loc.year=== selectedYear && loc.lat=== d.lat&& loc.lon=== d.lon ); d3.select(this).attr("r", (isCurrentLocation ?8:4) * strokeScale).attr("stroke-width", (isCurrentLocation ?2:1) * strokeScale); }); }functionresetZoom() { svg.transition().duration(750).call( zoom.transform, d3.zoomIdentity ); }// Enable zoom and pan on the SVG svg.call(zoom);// Listen for theme changes and update colors accordinglyconst observer =newMutationObserver(() => {updateMapColors(); }); observer.observe(document.body, {attributes:true,attributeFilter: ['class'] });return svg.node();}// Display the mapmapVisualization;
Hi, I’m Luke, or more formally Lukas. I’m a German national with dual citizenship in the US. I was born in Wisconsin, USA but grew up in the greater Stuttgart area (Germany), which is known for its car industry and the Swabian dialect.
After a brief stint working in a closed psychiatric ward at my local university hospital in Tübingen, I decided to pursue undergraduate education at a small university in beautiful Scotland.
Despite my initial ambitions to pursue a career in clinical psychology, I discovered my analytical side when I was introduced to computer programming by a PhD student in the lab I was helping out in. I got very lucky in this regard as just prior to graduating, I was offered a summer internship at a London-based Data-Science consultancy. I enjoyed the work as it offered diverse problems to solve from different clients, but ultimately I felt that I should also seek formal education in data analytics.
The Behavioral and Data Science M.Sc. at the University of Warwick was very much the perfect fit for me, as it combined my interest in behavioral research with my newfound passion for select topics in Computer Science. It was at Warwick where I first came to appreciate the application of computational modelling to describe cognitive processes. This was when things “clicked” in my head, and I knew that I wanted to pursue a PhD in this area.
I spent a very fun year working in the Applied Digital Behavior Lab at the University of Bath while preparing my PhD applications and was fortunate to be accepted into the Cognitive Science PhD program at the University of California, Irvine, where I am currently working in Mark Steyvers’ lab.
Bath, England - Credit: Ian Woolcock — Shutterstock
I’ve always been pretty active, but when I was younger I somehow never really stuck with any particular sport for too long. To give you an idea, I’ve had a phase for: archery, badminton, Crossfit, golf, parkour, powerlifting, and skateboarding. I’m still interested in strength training, which I have been doing in some form for 10+ years, and badminton. I have limited means to engage with these in an organized format at the moment, so to fill the hole in my heart, I’ve been investing some time to figure out the perfect home gym setup that still fits inside my bedroom. I think I’ve been pretty successful with this, so maybe I’ll show it off in a post sometime.
2. Technology
I like GNU/Linux, and use it as my everyday operating system. At the moment, my laptop runs Debian with KDE. I’ve also played around with operating systems on mobile devices, currently using GrapheneOS. I enjoy coding, and wish I could do it more! My favorite languages at the moment are Julia, R, and Python. As you can see, I’ve also taken an interest in web development. While Quarto admittedly makes this website pretty easy to create, I also use web development in my research to create experiments that run in a browser. I’m super interested in picking up creative coding and I am hoping to eventually generate some art that I can post on here! :)
3. Languages
English used to be my first language as I only started learning German after relocating to the Länd at age six. Fast-forward to now, English has become my second language. Maybe because of this strange experience, I’ve had a bit of an interest in language-learning. I have a habit of doing language lessons for fun on Duolingo, where I have a streak of over 2000 days. I’m mostly learning French and Dutch at the moment. My Dutch is definitely better than my French, probably just by virtue of being so similar to German, but both are good enough to follow a YouTube video. I hope to eventually move away from Duolingo and bring my skills to the next level, but don’t feel I have the bandwidth for it just now. If you know a good free resource, be sure to let me know!
4. Gaming
I was quite the avid gamer as a teen, spending countless hours on the likes of Team Fortress 2, Minecraft and Guild Wars 2. I don’t actively play much these days, but I still like to keep up with the industry. Perhaps I’m just waiting for the next big thing to come along. I also really enjoy going to board game cafés and completing escape rooms with friends!
Places I’ve been
Code
d3Highlight =require("d3@7")topoHighlight =require("topojson-client@3")// Your list of visited countriescountriesVisited = ["Germany","France","USA","Spain","Italy","Austria","Switzerland","Netherlands","Belgium","San Marino","Vatican","Malta","Monaco","United Kingdom"]// Load world map data with a unique variable nameworldGeoData = {const response =awaitfetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json");return topoHighlight.feature(await response.json(),"countries");}// Function to get current theme colorsgetThemeColors = () => {const isDark =document.body.classList.contains('quarto-dark');if (isDark) {return {visitedColor:"#00a653",notVisitedColor:"#374151",// gray-700 equivalentstrokeColor:"#6b7280",// gray-500 equivalentstrokeHoverColor:"#ffffff",tooltipBg:"rgba(17, 24, 39, 0.95)",// gray-900 with opacitytooltipText:"#ffffff",legendBg:"rgba(17, 24, 39, 0.8)",legendText:"#ffffff",controlsBg:"rgba(17, 24, 39, 0.8)",controlsButtonBg:"rgba(75, 85, 99, 0.8)",// gray-600 with opacitycontrolsButtonStroke:"#9ca3af"// gray-400 }; } else {return {visitedColor:"#00a653",notVisitedColor:"#e5e7eb",// gray-200 equivalentstrokeColor:"#9ca3af",// gray-400 equivalentstrokeHoverColor:"#374151",tooltipBg:"rgba(255, 255, 255, 0.95)",tooltipText:"#374151",legendBg:"rgba(255, 255, 255, 0.9)",legendText:"#374151",controlsBg:"rgba(255, 255, 255, 0.9)",controlsButtonBg:"rgba(243, 244, 246, 0.8)",// gray-100 with opacitycontrolsButtonStroke:"#d1d5db"// gray-300 }; }}// Map visualization with theme-adaptive colorshighlightedCountriesMap = {const colors =getThemeColors();// Set up dimensions and projectionconst mapWidth =900;const mapHeight =500;// Create a projection focused on the worldconst mapProjection = d3Highlight.geoMercator().center([10,45]).scale(150).translate([mapWidth /2, mapHeight /2]);// Create path generatorconst mapPath = d3Highlight.geoPath().projection(mapProjection);// Create SVG with transparent backgroundconst mapSvg = d3Highlight.create("svg").attr("width", mapWidth).attr("height", mapHeight).attr("viewBox", [0,0, mapWidth, mapHeight]).attr("style","max-width: 100%; height: auto; background: none;");// Create a group for all map elementsconst mapGroup = mapSvg.append("g");// Create a tooltipconst mapTooltip = d3Highlight.select(document.createElement("div")).attr("class","highlight-map-tooltip").style("position","absolute").style("background", colors.tooltipBg).style("color", colors.tooltipText).style("border-radius","5px").style("padding","8px").style("pointer-events","none").style("opacity",0).style("z-index",1000).style("font-family","sans-serif").style("font-size","14px");document.body.appendChild(mapTooltip.node());// Country name mapping for special casesconst countryNameMap = {"United States of America":"USA","United States":"USA","Holy See":"Vatican","Vatican City":"Vatican","Vatican":"Vatican","San Marino":"San Marino" };// Function to update map colors based on current themefunctionupdateMapColors() {const currentColors =getThemeColors();// Update country colors mapGroup.selectAll("path").attr("fill", d => {const countryName = d.properties.name;const normalizedName = countryNameMap[countryName] || countryName;return countriesVisited.includes(normalizedName) ? currentColors.visitedColor: currentColors.notVisitedColor; }).attr("stroke", currentColors.strokeColor);// Update tooltip colors mapTooltip.style("background", currentColors.tooltipBg).style("color", currentColors.tooltipText);// Update legend colors mapLegend.select(".legend-bg").attr("fill", currentColors.legendBg); mapLegend.selectAll(".legend-text").style("fill", currentColors.legendText);// Update control colors mapZoomControls.select(".controls-bg").attr("fill", currentColors.controlsBg); mapZoomControls.selectAll(".control-button").attr("fill", currentColors.controlsButtonBg).attr("stroke", currentColors.controlsButtonStroke); }// Draw countriesconst countries = mapGroup.selectAll("path").data(worldGeoData.features).join("path").attr("fill", d => {const countryName = d.properties.name;const normalizedName = countryNameMap[countryName] || countryName;return countriesVisited.includes(normalizedName) ? colors.visitedColor: colors.notVisitedColor; }).attr("stroke", colors.strokeColor).attr("stroke-width",0.5).attr("d", mapPath).on("mouseover",function(event, d) {const currentColors =getThemeColors(); d3Highlight.select(this).attr("stroke-width",1.5).attr("stroke", currentColors.strokeHoverColor);const countryName = d.properties.name;const normalizedName = countryNameMap[countryName] || countryName;const isVisited = countriesVisited.includes(normalizedName); mapTooltip.style("opacity",1).html(`<strong>${countryName}</strong><br/>${isVisited ?"✓ Visited":"Not visited"}`).style("left", (event.pageX+10) +"px").style("top", (event.pageY-28) +"px"); }).on("mouseout",function() {const currentColors =getThemeColors(); d3Highlight.select(this).attr("stroke-width",0.5).attr("stroke", currentColors.strokeColor); mapTooltip.style("opacity",0); }).on("mousemove",function(event) { mapTooltip.style("left", (event.pageX+10) +"px").style("top", (event.pageY-28) +"px"); });// Add a legendconst mapLegend = mapSvg.append("g").attr("transform",`translate(20, ${mapHeight -60})`);// Background for legend mapLegend.append("rect").attr("class","legend-bg").attr("x",-5).attr("y",-15).attr("width",120).attr("height",70).attr("fill", colors.legendBg).attr("rx",5).attr("ry",5);// Visited countries legend mapLegend.append("rect").attr("width",20).attr("height",20).attr("fill", colors.visitedColor); mapLegend.append("text").attr("class","legend-text").attr("x",30).attr("y",15).text("Visited").style("font-family","sans-serif").style("font-size","14px").style("fill", colors.legendText);// Not visited countries legend mapLegend.append("rect").attr("width",20).attr("height",20).attr("y",30).attr("fill", colors.notVisitedColor); mapLegend.append("text").attr("class","legend-text").attr("x",30).attr("y",45).text("Not Visited").style("font-family","sans-serif").style("font-size","14px").style("fill", colors.legendText);// Add zoom controlsconst mapZoomControls = mapSvg.append("g").attr("transform",`translate(${mapWidth -70}, 20)`);// Background for zoom controls mapZoomControls.append("rect").attr("class","controls-bg").attr("x",-5).attr("y",-5).attr("width",40).attr("height",120).attr("fill", colors.controlsBg).attr("rx",5).attr("ry",5);// Zoom in button mapZoomControls.append("rect").attr("class","control-button").attr("x",0).attr("y",0).attr("width",30).attr("height",30).attr("fill", colors.controlsButtonBg).attr("stroke", colors.controlsButtonStroke).attr("rx",5).attr("ry",5).attr("cursor","pointer").on("click", () => mapZoomBehavior.scaleBy(mapSvg.transition().duration(300),1.5)); mapZoomControls.append("text").attr("x",15).attr("y",20).attr("text-anchor","middle").attr("fill", colors.legendText).attr("font-size","18px").attr("font-weight","bold").attr("pointer-events","none").text("+");// Zoom out button mapZoomControls.append("rect").attr("class","control-button").attr("x",0).attr("y",40).attr("width",30).attr("height",30).attr("fill", colors.controlsButtonBg).attr("stroke", colors.controlsButtonStroke).attr("rx",5).attr("ry",5).attr("cursor","pointer").on("click", () => mapZoomBehavior.scaleBy(mapSvg.transition().duration(300),0.75)); mapZoomControls.append("text").attr("x",15).attr("y",60).attr("text-anchor","middle").attr("fill", colors.legendText).attr("font-size","18px").attr("font-weight","bold").attr("pointer-events","none").text("−");// Reset button mapZoomControls.append("rect").attr("class","control-button").attr("x",0).attr("y",80).attr("width",30).attr("height",30).attr("fill", colors.controlsButtonBg).attr("stroke", colors.controlsButtonStroke).attr("rx",5).attr("ry",5).attr("cursor","pointer").on("click", resetMapZoom); mapZoomControls.append("text").attr("x",15).attr("y",100).attr("text-anchor","middle").attr("fill", colors.legendText).attr("font-size","14px").attr("font-weight","bold").attr("pointer-events","none").text("R");// Define zoom behaviorconst mapZoomBehavior = d3Highlight.zoom().scaleExtent([1,8]).on("zoom", mapZoomed);functionmapZoomed(event) { mapGroup.attr("transform",event.transform);const mapStrokeScale =1/Math.sqrt(event.transform.k); mapGroup.selectAll("path").attr("stroke-width",0.5* mapStrokeScale); }functionresetMapZoom() { mapSvg.transition().duration(750).call( mapZoomBehavior.transform, d3Highlight.zoomIdentity ); }// Enable zoom and pan on the SVG mapSvg.call(mapZoomBehavior);// Listen for theme changes and update colors accordinglyconst observer =newMutationObserver(() => {updateMapColors(); }); observer.observe(document.body, {attributes:true,attributeFilter: ['class'] });return mapSvg.node();}// Display the maphighlightedCountriesMap
I have also visited a few micro-states including: Malta, Monaco, San Marino, and the Vatican.