Animate a World Map Between Projections
Build a D3 world map that morphs from Mercator to Equal Earth.
Introduction
30 Second Summary
Turning a globe into a rectangle always stretches some places or squeezes others. A familiar world map can make that trade-off feel invisible until the shapes begin to move.
In this project, you will build a one-file world map with D3 that starts in the Mercator projection. The finished page morphs into the Equal Earth projection under slider control from a public URL.
What You'll Build
You will watch the continents reshape under your slider as the same interactive map runs from a public URL on your phone.
By the end of this project, you'll have:
- A browser-based Mercator country map that draws the world from a simplified outline file.
- A smooth projection morph that reveals how Greenland changes size against Africa.
- A shareable public page with a slider that holds the map at any point between both projections.
- Secret Mission: An optional challenge to push your map animation skills further.
Are there any prerequisites?
You need a text editor plus a modern browser. Basic JavaScript knowledge helps you understand how the map changes over time.
Before We Start
Your finished map becomes a public web page that anyone can open. A clean local workspace makes each visual change easier to preview.
This setup prepares VS Code for the project. It also signs you in to GitHub so static hosting is ready later.
In this step, get ready to:
- Sign in to GitHub through a modern browser.
- Open a new world-map-projections folder in VS Code.
- Install Live Server for local previews.
Confirm browser access to GitHub
Loading GitHub checks that your browser can reach the internet. Your account also gives you somewhere to publish the finished map.
Your credentials stay on GitHub's official pages. This project never asks you to paste them into a file.
✔️ I have a GitHub account
- Open a new tab in the browser displaying this project.
- Visit the official GitHub sign-in page.
- Enter your GitHub account details.
- Complete any account verification GitHub requests.
You should see your profile avatar in the upper-right corner of GitHub.
ⓧ I need a GitHub account
- Open the official GitHub account creation page.
- Follow GitHub's prompts to create your personal account.
- Verify your email address using the message GitHub sends you.
- Visit the official GitHub sign-in page.
- Sign in with your new account.
You should see your profile avatar in the upper-right corner of GitHub.
Good work. Your profile avatar confirms that your browser connection works and your hosting account is ready.
Create and open the project folder
Your project needs one named folder to hold the map page. VS Code keeps that folder visible while you edit its files.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter.
✔️ VS Code opens
VS Code is ready. Continue below to create your project folder.
ⓧ VS Code is not installed
VS Code needs to be installed before you can prepare the workspace.
- Open the official VS Code download page.
- Select the download for your operating system.
Your browser starts downloading the installer. You should see the installer file in your downloads.
- Run the downloaded installer.
- Follow the installer prompts to complete the installation.
The installer confirms when VS Code is ready.
- Press Cmd+Space (macOS) or the Windows key (Windows) to reopen your search bar.
- Type VS Code and press Enter.
You should see the VS Code window open.
VS Code Still Won't Open?
Run the installer again if it closed before showing a completion message. Check your operating system's applications list after the installation finishes.
Ask for help with opening VS Code after installation.
- Click File in the VS Code menu bar.
- Click Open Folder.
You should see a file picker where you can choose the folder VS Code opens.
- Select your Desktop location.
The file picker should now show the folders on your Desktop.
- Click New Folder.
- Type world-map-projections and press Enter.
You should see the new world-map-projections folder on your Desktop.
- Select the world-map-projections folder.
- Click Open (macOS) or Select Folder (Windows).
VS Code may ask whether you trust the folder. The prompt refers to the empty folder you just created.
- Approve the folder trust prompt if it appears.
Your workspace now has a clear home. You should see world-map-projections in the Explorer sidebar.
Install Live Server
A preview extension gives your HTML page a local address while you build. It also keeps the feedback loop short when the map changes.
Why Live Server for This Project?
You could open the HTML file directly in a browser. Live Server gives this project a tighter feedback loop because it reloads the browser when saved files change.
- Click the Extensions icon in the VS Code Activity Bar.
- Type Live Server into the Extensions search box.
The Marketplace shows several results related to local previews.
Similar extension names make this easy to misread. Choose Live Server from the publisher Ritwick Dey.
- Select Live Server from Ritwick Dey.
VS Code may ask you to confirm that you trust this third-party publisher. The confirmation appears when you install an extension from a new publisher.
- Click Install on the extension details page.
- Confirm the publisher trust prompt if it appears.
Before you check, what should replace the Install button if the extension is ready?
- Check the action area on the Live Server extension details page.
You should see a Manage gear in place of the Install button. That is your preview loop ready for the map page.
Live Server Didn't Install?
Confirm that the selected extension is Live Server from Ritwick Dey. Recheck your internet connection if the Marketplace results fail to load.
Ask for help with installing Live Server in VS Code.
Your browser, GitHub account, project folder, and preview tool are ready. Next up, you will draw the first Mercator world map.
Draw the Mercator World Map
A projection comparison needs a clear starting point. This step creates that baseline with the Mercator projection used by many familiar web maps.
D3 turns geographic coordinates into SVG paths inside the browser. TopoJSON Client converts compact atlas data into GeoJSON country features.
In this step, get ready to:
- Create a 960 by 500 map canvas in index.html.
- Load the browser mapping libraries from remote script URLs.
- Render World Atlas country data with a fitted Mercator projection.
Create the map canvas
The page needs a drawing surface before it can display geographic data. A fixed SVG canvas gives every projection the same 960 by 500 frame.
- Create index.html inside the open world-map-projections folder with your editor's new-file control.
You should see index.html in the file sidebar for world-map-projections.
- Paste this page shell into index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Map Projections</title>
<style>
/* Keep the fixed map canvas visible and centered. */
body { margin: 0; background: #f4f1e8; }
svg { display: block; max-width: 100%; height: auto; margin: 40px auto; background: #cfe8f3; }
.country { fill: #d9c79e; stroke: #ffffff; stroke-width: 0.5; }
</style>
</head>
<body>
<!-- Hold the country paths inside a 960 by 500 canvas. -->
<svg width="960" height="500" viewBox="0 0 960 500"></svg>
<!-- Load the mapping libraries directly in the browser. -->
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
</body>
</html>
How Does the Page Shell Work?
- The svg element provides the canvas where the map paths appear.
- The D3 script makes its mapping tools available through d3.
- The TopoJSON Client script exposes its conversion tools through topojson.
- The .country rule gives each country a tan fill with a white border.
- Save index.html.
- Start the local preview for index.html with the preview method available in your editor.
You should see a wide pale-blue rectangle centered on the page. This confirms that the SVG canvas is rendering.
Canvas Missing?
Confirm that index.html is saved inside world-map-projections. Check that your local preview is serving this file.
Make sure the page contains the opening <svg> tag. A missing angle bracket can stop the canvas from rendering.
help me find why my SVG canvas is missing.
Fetch and draw the countries
World Atlas stores country boundaries as TopoJSON. The countries-110m.json file keeps the first render lightweight.
The browser converts that data into GeoJSON features. A Mercator projection then turns each longitude and latitude into a position on the SVG canvas.
- Return to index.html in your editor.
- Find the <script src="https://unpkg.com/topojson-client@3"></script> line near the bottom of the file.
- Paste this script directly below that line:
<script>
// Store the shared map dimensions and SVG selection.
const width = 960;
const height = 500;
const svg = d3.select("svg");
// Load simplified country boundaries for a fast first render.
d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
.then((world) => {
// Convert TopoJSON arcs into GeoJSON country features.
const countries = topojson.feature(
world,
world.objects.countries
);
// Fit the Mercator projection to the available SVG space.
const projection = d3.geoMercator()
.fitSize([width, height], countries);
const path = d3.geoPath(projection);
// Draw one SVG path for every country.
svg.selectAll("path")
.data(countries.features)
.join("path")
.attr("class", "country")
.attr("d", path);
});
</script>
What Does This Code Do?
- The d3.json() call fetches the simplified country boundary file.
- The topojson.feature() call converts world.objects.countries into drawable country features.
- The d3.geoMercator() call creates the projection. Its fitSize() method scales the countries to the canvas.
- The d3.geoPath() generator converts each projected country into SVG path data.
- The join("path") call creates one visible path for every country feature.
- Save index.html.
Before you refresh, do you expect one combined outline or a separate shape for every country?
- Refresh the local preview in your browser.
You'll see a tan Mercator world map on the pale-blue canvas. Thin white borders separate the country paths.
That first visible map is a real milestone. Your browser now turns geographic data into a complete world view.
Map Still Blank?
Confirm that your internet connection can load the library scripts and the World Atlas file. A blocked request leaves the canvas visible without country paths.
Check that the map script appears after the D3 script and the TopoJSON Client script. The browser needs those libraries before it reaches d3.json().
help me debug my blank D3 world map.
✔️ Awesome, I've got everything!
Your Mercator map is visible. Make sure index.html is saved before moving on.
ⓧ I'd like to double check the full code
Compare your complete index.html file with this version:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Map Projections</title>
<style>
/* Keep the fixed map canvas visible and centered. */
body { margin: 0; background: #f4f1e8; }
svg { display: block; max-width: 100%; height: auto; margin: 40px auto; background: #cfe8f3; }
.country { fill: #d9c79e; stroke: #ffffff; stroke-width: 0.5; }
</style>
</head>
<body>
<!-- Hold the country paths inside a 960 by 500 canvas. -->
<svg width="960" height="500" viewBox="0 0 960 500"></svg>
<!-- Load the mapping libraries directly in the browser. -->
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script>
// Store the shared map dimensions and SVG selection.
const width = 960;
const height = 500;
const svg = d3.select("svg");
// Load simplified country boundaries for a fast first render.
d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
.then((world) => {
// Convert TopoJSON arcs into GeoJSON country features.
const countries = topojson.feature(
world,
world.objects.countries
);
// Fit the Mercator projection to the available SVG space.
const projection = d3.geoMercator()
.fitSize([width, height], countries);
const path = d3.geoPath(projection);
// Draw one SVG path for every country.
svg.selectAll("path")
.data(countries.features)
.join("path")
.attr("class", "country")
.attr("d", path);
});
</script>
</body>
</html>
Your Mercator baseline is on screen. Next, you'll add Equal Earth so the continents can change shape with one control.
Add the Equal Earth Projection
Your Mercator projection now turns the world outline into visible country paths. One map can still hide how strongly its projection changes apparent size.
Now you will give the same country data a second interpretation with D3's Equal Earth projection. A toggle will redraw the paths with whichever projection is active.
In this step, get ready to:
- Create fitted Mercator and Equal Earth projections.
- Track the current projection with activeProjection.
- Switch projections with a button that redraws every country.
Add the second projection
Both projections need the same canvas dimensions and country features. Fitting each projection separately keeps the complete world inside the existing SVG.
- In index.html, locate the existing SVG element.
- Replace that element with this controls-and-map block:
<!-- Let viewers switch the active map projection -->
<button id="projection-toggle">Show Equal Earth</button>
<!-- Keep the map at the established canvas size -->
<svg width="960" height="500"></svg>
What does this markup do?
- The button gives the viewer one control for changing the active projection.
- The button text names the projection that appears after the next click.
- The SVG keeps the map at its established size of 960 by 500.
- Inside the callback that receives world, replace the current projection and path declarations with this code:
// Fit both projections to the same country data and canvas
const mercatorProjection = d3.geoMercator()
.fitSize([width, height], countries);
const equalEarthProjection = d3.geoEqualEarth()
.fitSize([width, height], countries);
// Start with Mercator and let the path generator follow the active choice
let activeProjection = mercatorProjection;
const path = d3.geoPath().projection(activeProjection);
How do the projections work?
- The mercatorProjection variable stores the fitted Mercator projection.
- The equalEarthProjection variable stores the fitted Equal Earth projection.
- The activeProjection variable points to the projection currently used by the path generator.
- The fitSize() calls calculate a suitable scale and position for each projection.
- Save index.html.
- Return to the local preview from the previous step.
- Refresh the page.
You should see the Mercator map with a Show Equal Earth button above it. This proves both the new control and the updated projection setup load without stopping the map.
Map missing after the edit?
Check that both fitSize() calls sit after the countries declaration. Each projection needs those country features before it can fit itself.
Check the browser console for a syntax error near the projection declarations. A missing parenthesis can stop the country paths from rendering.
help me debug the two fitted D3 projections in my index.html file
Make the map redrawable
The toggle needs access to the country paths that already exist. A redraw function can update their geometry whenever activeProjection changes.
- Replace the existing country path rendering chain with this reusable version:
// Render every country and keep the selection for later redraws
const countryPaths = svg.selectAll("path")
.data(countries.features)
.join("path")
.attr("d", path);
// Apply the active projection before rebuilding every path
function redrawMap() {
path.projection(activeProjection);
countryPaths.attr("d", path);
}
What does this code do?
- The countryPaths selection keeps a reference to every rendered country.
- The redrawMap() function gives the path generator the current projection.
- Updating the d attribute recalculates each country's visible outline.
- Save index.html.
- Refresh the local preview.
You should still see every country in the Mercator view. The visible map confirms that countryPaths renders correctly before the toggle calls redrawMap().
Country outlines disappeared?
Make sure countries.features remains the data passed into the country path selection. Passing the full TopoJSON object will not create the expected paths.
Check that countryPaths is declared before redrawMap(). The function needs that selection when it redraws the map.
help me restore the country paths after adding redrawMap
Connect the projection toggle
The final piece connects the button to activeProjection. Each click selects the other projection before asking the map to redraw.
- Add this click handler immediately after the redrawMap() function:
// Switch projections and label the button with the next available view
d3.select("#projection-toggle").on("click", function () {
const showEqualEarth = activeProjection === mercatorProjection;
activeProjection = showEqualEarth
? equalEarthProjection
: mercatorProjection;
d3.select(this).text(showEqualEarth
? "Show Mercator"
: "Show Equal Earth");
redrawMap();
});
How does the toggle work?
- The comparison checks whether Mercator is currently active.
- The conditional assignment stores the other projection in activeProjection.
- The button label always names the view available on the next click.
- The final redrawMap() call rebuilds the country outlines with the selected projection.
- Save index.html.
- Refresh the local preview.
Before you click the button, which parts of the world do you expect to change shape most clearly?
- Click Show Equal Earth.
You should see the world redraw into a rounded Equal Earth shape. The button should now read Show Mercator.
- Click Show Mercator.
You should see the rectangular Mercator view return. That is the key interaction working. The same country data now switches between two projections on demand.
Button does not change the map?
Check that the button ID matches #projection-toggle in the click handler. A different ID leaves the handler disconnected.
Make sure the click handler sits inside the data callback. It needs access to both fitted projections and the rendered country paths.
help me debug my D3 projection toggle
✔️ Awesome, I've got everything!
Great. Confirm that index.html is saved and that both projection buttons redraw the map.
ⓧ I'd like to double check the full code
Compare your complete index.html file with this version:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Map Projections</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
</head>
<body>
<!-- Let viewers switch the active map projection -->
<button id="projection-toggle">Show Equal Earth</button>
<!-- Keep the map at the established canvas size -->
<svg width="960" height="500"></svg>
<script>
// Keep the canvas dimensions available to both projections
const width = 960;
const height = 500;
const svg = d3.select("svg");
// Load the world topology and convert its countries to GeoJSON
d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json").then((world) => {
const countries = topojson.feature(world, world.objects.countries);
// Fit both projections to the same country data and canvas
const mercatorProjection = d3.geoMercator()
.fitSize([width, height], countries);
const equalEarthProjection = d3.geoEqualEarth()
.fitSize([width, height], countries);
// Start with Mercator and let the path generator follow the active choice
let activeProjection = mercatorProjection;
const path = d3.geoPath().projection(activeProjection);
// Render every country and keep the selection for later redraws
const countryPaths = svg.selectAll("path")
.data(countries.features)
.join("path")
.attr("d", path);
// Apply the active projection before rebuilding every path
function redrawMap() {
path.projection(activeProjection);
countryPaths.attr("d", path);
}
// Switch projections and label the button with the next available view
d3.select("#projection-toggle").on("click", function () {
const showEqualEarth = activeProjection === mercatorProjection;
activeProjection = showEqualEarth
? equalEarthProjection
: mercatorProjection;
d3.select(this).text(showEqualEarth
? "Show Mercator"
: "Show Equal Earth");
redrawMap();
});
});
</script>
</body>
</html>
You can now compare two projections with one click. Next, you will replace the instant redraw with a frame-by-frame morph.
Animate the Projection Morph
Your D3 map can already switch between two map projections. Each click still replaces one complete map with another.
The missing piece is motion between those endpoints. You will calculate the in-between coordinates for every country so the distortion becomes visible as it changes.
In this step, get ready to:
- Interpolate each country coordinate across the Mercator to Equal Earth transition.
- Redraw the country paths frame by frame with requestAnimationFrame.
- Connect the existing projection button to the animated morph.
Interpolate the projected coordinates
A morph needs a position for every frame. Transition progress t runs from 0 at Mercator to 1 at Equal Earth.
Each geographic point receives one coordinate from each projection. A linear interpolation places that point between the two coordinates.
- In index.html, find the map-rendering section that defines activeProjection.
- Select that map-rendering section through its initial renderMap() call.
- Replace the selected section with the following code:
function interpolatedProjection(t) {
// Blend each geographic point between both fitted projections
return d3.geoTransform({
point(longitude, latitude) {
const [mercatorX, mercatorY] = mercatorProjection([longitude, latitude])
const [equalEarthX, equalEarthY] = equalEarthProjection([longitude, latitude])
const x = mercatorX + (equalEarthX - mercatorX) * t
const y = mercatorY + (equalEarthY - mercatorY) * t
this.stream.point(x, y)
}
})
}
function renderMap(t) {
// Rebuild every country path at the current transition progress
const path = d3.geoPath(interpolatedProjection(t))
svg.selectAll("path").attr("d", path)
}
renderMap(0)
What does this code do?
- The interpolatedProjection(t) function creates a projection for one moment in the morph.
- Each longitude-latitude point passes through Mercator first. The same point also passes through Equal Earth.
- The coordinate formulas blend both projected positions according to t.
- The renderMap(t) function recalculates every country path at the selected progress.
- Save index.html.
- Refresh the local preview in your browser.
You should see the complete Mercator map. This confirms that t = 0 reproduces the first projection.
Map disappeared after the edit?
Check that the old path declaration was removed with the previous rendering section. A duplicate declaration can stop the script.
Confirm that mercatorProjection and equalEarthProjection match the projection names already in your file.
help me debug my interpolated D3 projection
Animate every frame
Your renderer can now draw any point between the two projections. The browser needs to advance t over time so those still frames become one continuous movement.
- In index.html, find the existing click handler attached to the projection toggle button.
- Replace that click handler with the following animation code:
let showingEqualEarth = false
function animateProjection() {
// Move from the current endpoint to the opposite projection
const startT = showingEqualEarth ? 1 : 0
const endT = showingEqualEarth ? 0 : 1
const duration = 1500
let startTime
function updateFrame(timestamp) {
// Convert elapsed time into transition progress from 0 to 1
if (startTime === undefined) startTime = timestamp
const elapsed = timestamp - startTime
const progress = Math.min(elapsed / duration, 1)
const t = startT + (endT - startT) * progress
renderMap(t)
if (progress < 1) {
requestAnimationFrame(updateFrame)
}
}
showingEqualEarth = !showingEqualEarth
requestAnimationFrame(updateFrame)
}
d3.select("#projection-toggle").on("click", animateProjection)
How does the animation work?
- The startT and endT values choose the direction of the morph.
- The updateFrame(timestamp) function converts elapsed time into progress from 0 to 1.
- Each call to renderMap(t) rebuilds the country outlines at that instant.
- The next requestAnimationFrame call is scheduled while the animation is still in progress.
- Save index.html.
Before you click the button, do you expect the countries to jump directly to their new outlines or pass through intermediate shapes?
- Return to the local preview in your browser.
- Click the projection toggle button once.
You should see every country move smoothly from Mercator to Equal Earth. Around halfway, the map matches neither endpoint.
- Click the projection toggle button again.
You should see the map follow the same path in reverse before settling back into Mercator. That smooth return confirms the button can animate in both directions.
Projection still jumps or stays still?
Check that the button handler passes animateProjection without calling it immediately. The handler should run only after a click.
Confirm that the old single-redraw click handler has been removed. Two click handlers can compete over the same map.
help me fix my projection animation
✔️ Awesome, I've got everything!
Great. Your saved index.html now animates the map between both fitted projections.
ⓧ I'd like to double check the full code
Compare your complete index.html file with this version:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Map Projection Morph</title>
<!-- Load the mapping and topology libraries in the browser -->
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<style>
/* Keep the map readable and centered on the page */
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 24px;
background: #f4f7fb;
color: #172033;
}
button {
margin-bottom: 16px;
padding: 10px 16px;
cursor: pointer;
}
svg {
display: block;
max-width: 100%;
height: auto;
margin: 0 auto;
background: #dceeff;
}
path {
fill: #d9c7a2;
stroke: #ffffff;
stroke-width: 0.5;
}
</style>
</head>
<body>
<h1>World Map Projection Morph</h1>
<button id="projection-toggle">Toggle Projection</button>
<svg id="map" width="960" height="500" viewBox="0 0 960 500"></svg>
<script>
// Match the projection fitting area to the SVG dimensions
const width = 960
const height = 500
const svg = d3.select("#map")
// Load the simplified world topology for a quick first render
d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
.then(world => {
// Convert the topology into GeoJSON country features
const countries = topojson.feature(world, world.objects.countries)
// Fit both projection endpoints to the same drawing area
const mercatorProjection = d3.geoMercator()
.fitSize([width, height], countries)
const equalEarthProjection = d3.geoEqualEarth()
.fitSize([width, height], countries)
// Create the country paths once so each frame only updates their shapes
svg.selectAll("path")
.data(countries.features)
.join("path")
function interpolatedProjection(t) {
// Blend each geographic point between both fitted projections
return d3.geoTransform({
point(longitude, latitude) {
const [mercatorX, mercatorY] = mercatorProjection([longitude, latitude])
const [equalEarthX, equalEarthY] = equalEarthProjection([longitude, latitude])
const x = mercatorX + (equalEarthX - mercatorX) * t
const y = mercatorY + (equalEarthY - mercatorY) * t
this.stream.point(x, y)
}
})
}
function renderMap(t) {
// Rebuild every country path at the current transition progress
const path = d3.geoPath(interpolatedProjection(t))
svg.selectAll("path").attr("d", path)
}
renderMap(0)
let showingEqualEarth = false
function animateProjection() {
// Move from the current endpoint to the opposite projection
const startT = showingEqualEarth ? 1 : 0
const endT = showingEqualEarth ? 0 : 1
const duration = 1500
let startTime
function updateFrame(timestamp) {
// Convert elapsed time into transition progress from 0 to 1
if (startTime === undefined) startTime = timestamp
const elapsed = timestamp - startTime
const progress = Math.min(elapsed / duration, 1)
const t = startT + (endT - startT) * progress
renderMap(t)
if (progress < 1) {
requestAnimationFrame(updateFrame)
}
}
showingEqualEarth = !showingEqualEarth
requestAnimationFrame(updateFrame)
}
d3.select("#projection-toggle").on("click", animateProjection)
})
</script>
</body>
</html>
Your map now reveals the distortion through movement instead of a sudden swap. Next up, you will add reference lines plus colour highlights so the changing scale of Greenland becomes easier to compare with Africa.
Highlight Distortion with a Graticule
Your projection morph now makes the continents move. The comparison still lacks a fixed visual reference.
A graticule adds a coordinate grid to your D3 map. Highlighting Greenland against Africa turns the animation into a size comparison you can follow frame by frame.
In this step, get ready to:
- Draw a graticule through the existing interpolated projection.
- Highlight Greenland against Africa with contrasting colours.
- Replay the morph to watch Greenland shrink relative to Africa.
Add the graticule layer
A graticule turns the map into a coordinate reference. renderMap(t) must pass that reference through the same interpolation as the countries.
SVG drawing order determines which marks sit on top. Placing the grid before the country paths keeps the borders visible.
- Update renderMap(t) in index.html with a graticule layer based on d3.geoGraticule. Place the layer before the country paths through interpolatedProjection.
- Save index.html.
- Refresh the browser preview from earlier.
Before you replay the morph, what do you expect the coordinate grid to do between the two projections?
- Click the existing projection toggle button.
You’ll see the coordinate grid morph with the country outlines. Each frame keeps the grid aligned with the changing projection.
Why Redraw the Grid?
Each projection bends the same coordinates differently. Redrawing the graticule through interpolatedProjection keeps the reference grid synchronized with the countries.
Is the Grid Staying Still?
Confirm the graticule path uses the same interpolatedProjection created inside renderMap(t).
Check that the graticule is redrawn every time renderMap(t) runs.
help me debug the graticule animation
Highlight Greenland against Africa
Colour gives your eye two fixed regions to compare. Greenland becomes the visual test case for the projection’s changing area distortion.
Africa provides the reference across the transition. Every other country stays neutral so the comparison remains clear.
How Do the Country Fills Work?
- Greenland receives the first highlight colour.
- African country features receive the contrasting highlight colour.
- Every remaining country keeps the neutral fill.
- Implement the three-way country fill logic on the country paths inside renderMap(t).
- Save index.html.
- Refresh the browser preview.
That gives your animation its comparison markers. You’ll see Greenland in the first highlight colour.
African countries use the contrasting colour. Every other country keeps its neutral fill.
✔️ Awesome, I've got everything!
Your map now has a moving reference grid. Its highlights make the distortion visible.
ⓧ I'd like to double check the full code
Use this checkpoint to compare the structure of index.html:
- Check that d3.geoGraticule is defined beside the existing fitted projections.
- Check that renderMap(t) redraws the graticule with d3.geoPath through interpolatedProjection.
- Check that the graticule is drawn before the country paths.
- Check that Greenland receives one highlight colour.
- Check that African country features receive the contrasting highlight colour.
- Check that every other country keeps the neutral fill.
- Leave animateProjection() connected to requestAnimationFrame.
- Leave the projection toggle button connected to the animated morph.
Before you replay the morph, which highlighted region do you expect to change most dramatically?
- Click the projection toggle button to replay the animation.
Across the animation, you’ll see Greenland visibly shrink relative to highlighted Africa. The coordinate grid warps at the same time.
What Does the Comparison Reveal?
Mercator enlarges high-latitude areas on this map. Greenland therefore starts much larger against Africa.
Equal Earth brings the area comparison closer to the landmasses’ relative sizes. The animation makes that correction visible as movement.
Are the Highlights Missing?
Confirm the fill logic is attached to the same country paths drawn by renderMap(t).
Check the Greenland case separately from the African country case. Keep the neutral colour as the fallback.
help me debug the country highlights
Your map now turns projection distortion into something visible. Next, you’ll add direct control over the transition.
Scrub the Map with a Slider
Your D3 map now animates between Mercator and Equal Earth. The highlighted landmasses make the changing distortion visible.
That automatic transition moves at its own pace. A slider puts t under your control so you can stop the map at any intermediate projection.
In this step, get ready to:
- Add a range slider covering the transition from 0 to 1.
- Connect the slider to renderMap so the map follows your input.
- Synchronize the slider with the animated projection transition.
Add the range control
A range input represents the complete transition as a line of selectable values. Small increments let you pause close to any point in the morph.
- Switch back to the index.html tab in your editor.
- Add one input element immediately before the existing projection toggle button with the range type, min value 0, max value 1, plus step value 0.01.
- Save index.html.
- Refresh the local preview from earlier.
You should see a slider beside the projection toggle button. Its thumb can travel across the full width of the control.
Don't see the slider?
Check that the new input sits beside the existing projection toggle button in index.html.
Confirm that you saved the file currently loaded by your local preview.
help me find why my range slider is missing
Connect the slider to the map
The slider reports its current value whenever an input event fires. Converting that value to a number gives renderMap the transition progress it expects.
- In index.html, attach an input event listener that converts the slider's current value to a number before passing it to renderMap.
- Save index.html.
- Refresh the local preview.
Before you move the slider, where do you expect the map to stop when you release it?
- Drag the slider slowly across several positions.
You should see the countries redraw continuously. The map should remain at the projection selected by the slider when you release it.
You now control the projection directly. The map stays at the exact transition point you choose.
Map not following the slider?
Confirm that the listener responds to the slider's input event. A different event can delay the redraw until you release the control.
Check that the slider value becomes a number before it reaches renderMap.
help me debug the slider redraw
Synchronize the animation
The animation already calculates t for every frame. Writing that progress into the slider keeps the control aligned with the moving map.
- In animateProjection, update the slider's current value from t inside the requestAnimationFrame frame logic.
- Save index.html.
- Refresh the local preview.
Before you start the animation, do you expect the slider thumb to stay still or travel with the map?
- Click the existing projection toggle button.
You should see the slider thumb travel from one end to the other while the continents morph. Both should reach the final projection together.
Before the final check, do you expect the middle position to match either endpoint exactly?
- Drag the slider to the middle of its track.
You should see a map shape that matches neither Mercator nor Equal Earth. Greenland should remain partway through its visible change against Africa.
Slider not moving during animation?
Check that the slider update sits inside the same frame logic that calculates t.
Confirm that animateProjection updates the same slider element that handles your manual input.
help me synchronize the slider animation
Your map now supports automatic playback plus precise manual scrubbing. Next up, you will deploy the page so anyone can test the projection shift from a public URL.
Publish Your Interactive Map
Your map now morphs between the Mercator projection and the Equal Earth projection. The slider can hold the world at any point between them.
A local preview stays on your computer. GitHub Pages gives the finished page a public address you can open on your phone.
In this step, get ready to:
- Publish index.html in a public GitHub repository named world-map-projections.
- Deploy the main branch from the repository root with GitHub Pages.
- Test the published animation from your phone.
Publish the project file to GitHub
A repository stores your finished page online. Keeping index.html at the top level of main puts it where GitHub Pages can publish it.
- On GitHub, select New repository from the menu in the upper-right corner.
- Choose your personal account from the Owner dropdown.
- Enter world-map-projections in the Repository name field.
- Choose Public from the Choose visibility dropdown.
- Set Add README to On.
- Click Create repository.
That is your project's online home created. Its main branch is ready for your map file.
- Open the Add file dropdown above the file list.
- Select Upload files.
- Click choose your files in the upload area.
- Select index.html from your local world-map-projections folder.
- Enter Publish interactive map in the Commit message field.
- Choose the option that adds the upload to the current main branch.
- Click Propose changes.
You should see index.html beside README.md in the file list. The branch selector should show main.
That's the repository ready. Your interactive map file now lives on GitHub.
Can't see index.html on main?
- Confirm the branch selector shows main.
- Confirm index.html appears at the top level beside README.md.
- Repeat the upload to the current branch if either check differs.
help me publish index.html at the top level of my main branch.
Enable GitHub Pages
GitHub Pages serves static files directly from a chosen branch and folder. Selecting main with / (root) points the deployment at your published index.html file.
- Click Settings under the repository name.
- In the Code, planning, and automation sidebar section, click Pages.
- Under Build and deployment, use the Source dropdown to select Deploy from a branch.
- Use the Branch dropdown to select main.
- Use the folder dropdown to select / (root).
- Click Save.
The first publication can take up to 10 minutes. That wait is GitHub building your public copy.
- Wait for GitHub to show a public site address on the Pages settings screen.
- Record the public address here as https://<github-username>.github.io/world-map-projections/.
Before you open the address, which page do you expect to see? Make your prediction now.
- Visit https://<github-username>.github.io/world-map-projections/ in your browser.
You'll see the world map framed by its graticule. Greenland and Africa use contrasting highlight colours.
That is the deployment done. Your local experiment now has a public address.
Public page not ready?
- Retry the public URL after the 10-minute publishing window.
- Confirm the Pages source uses main with / (root).
- Confirm index.html remains at the top level of main.
help me diagnose my GitHub Pages deployment.
Test the public map on your phone
A desktop load confirms the deployment. A phone check proves the public page works away from your local preview.
Before you test the public copy, do you expect every interactive control to behave the same way? Make your prediction now.
- On your phone, visit https://<github-username>.github.io/world-map-projections/ in a web browser.
- Tap the projection toggle button.
You'll see the continents morph smoothly between the two projections. The slider moves with the animation.
- Drag the transition slider to its midpoint.
You'll hold a frame that matches neither endpoint projection. Greenland appears smaller against Africa as the map moves toward Equal Earth.
There it is. Your public map now carries the projection comparison beyond your computer.
Secret mission
Create a Three-Frame Projection Study
Turn the live map into visual evidence by capturing Mercator, midpoint, and Equal Earth views. Then explain what Greenland's changing size reveals when Africa stays as your reference.
Clean Up Your Resources
Clean Up Your Resources
Your map runs as a static GitHub Pages site with no ongoing project costs. Decide whether to keep it live, pause its public deployment, or delete it entirely.
Resources you used:
- GitHub repository world-map-projections with index.html on its main branch.
- GitHub Pages deployment at https://<github-username>.github.io/world-map-projections/ from the main branch root.
Keep everything running
No action needed. Choose this if you want to keep editing the map or sharing its public URL.
- Keep the world-map-projections repository to preserve your source code.
- Leave GitHub Pages enabled to keep the interactive map available at its public URL.
Pause - I'll come back to this later
Pause public access while keeping the repository ready for future edits.
- Return to the world-map-projections repository on GitHub.
- Select Settings in the repository navigation.
- Select Pages in the settings sidebar.
- Open the Branch menu under Build and deployment.
- Select None as the publishing branch.
- Click Save.
After GitHub processes the change, you should see that the public URL no longer serves the map. Your source remains safe in world-map-projections.
Delete - I don't want to use this again
Deleting the repository is permanent, so this option is for a clean break. The same action removes the GitHub Pages deployment that serves your map.
- Return to the world-map-projections repository on GitHub.
- Select Settings in the repository navigation.
- Select General in the settings sidebar.
- Scroll to Danger Zone at the bottom of the page.
- Click Delete this repository.
- Enter world-map-projections in the confirmation field.
- Complete GitHub's final deletion confirmation.
- Visit https://<github-username>.github.io/world-map-projections/ to confirm the deployment is gone.
Once GitHub processes the deletion, you should see that the project page is unavailable. That is the clean break complete: both the repository and its public deployment are gone.
Nice Work!
Nice Work!
You did it. Your interactive D3 world map now morphs from Mercator to Equal Earth on a public page served by GitHub Pages.
You've learned how to:
- Render a world map from simplified TopoJSON country data using fitted projections.
- Animate a projection morph frame by frame with interpolated coordinates. Greenland visibly shrinks against Africa while the graticule redraws.
- Scrub transition progress with a slider. Publish the one-file page through GitHub Pages. Open the public URL from a phone.
- Secret Mission: Complete the optional challenge to extend your interactive map with an additional feature.
Ready to quiz yourself?