World Map

Lesson 5

d3.geoNaturalEarth1() projection converts longitude/latitude to screen coordinates. d3.geoPath() generates SVG path data from GeoJSON features. Country fill color is mapped to a data value via d3.scaleSequential(d3.interpolateGreys). Country borders use stroke: #fff.

Source Code script.js
// ─── Tooltip reference (used in event handlers below) ───────────────────────
const tooltip = d3.select('#tooltip');

// ─── Dimensions ─────────────────────────────────────────────────────────────
const width = document.getElementById('map').clientWidth || 900;
const height = Math.round(width * 0.55);

// ─── ISO 3166-1 numeric → country name lookup ────────────────────────────────
const countryNames = {
  '004': 'Afghanistan', '008': 'Albania', '012': 'Algeria',
  '024': 'Angola', '032': 'Argentina', '036': 'Australia',
  '040': 'Austria', '050': 'Bangladesh', '056': 'Belgium',
  '064': 'Bhutan', '068': 'Bolivia', '072': 'Botswana',
  '076': 'Brazil', '100': 'Bulgaria', '104': 'Myanmar',
  '116': 'Cambodia', '120': 'Cameroon', '124': 'Canada',
  '140': 'Central African Republic', '144': 'Sri Lanka',
  '152': 'Chile', '156': 'China', '170': 'Colombia',
  '178': 'Republic of Congo', '180': 'DR Congo',
  '188': 'Costa Rica', '191': 'Croatia',
  '192': 'Cuba', '203': 'Czech Republic', '208': 'Denmark',
  '214': 'Dominican Republic', '218': 'Ecuador', '818': 'Egypt',
  '222': 'El Salvador', '231': 'Ethiopia', '246': 'Finland',
  '250': 'France', '266': 'Gabon', '276': 'Germany',
  '288': 'Ghana', '300': 'Greece', '320': 'Guatemala',
  '324': 'Guinea', '332': 'Haiti', '340': 'Honduras',
  '348': 'Hungary', '356': 'India', '360': 'Indonesia',
  '364': 'Iran', '368': 'Iraq', '372': 'Ireland',
  '376': 'Israel', '380': 'Italy', '388': 'Jamaica',
  '392': 'Japan', '400': 'Jordan', '398': 'Kazakhstan',
  '404': 'Kenya', '408': 'North Korea', '410': 'South Korea',
  '414': 'Kuwait', '418': 'Laos', '422': 'Lebanon',
  '426': 'Lesotho', '430': 'Liberia', '434': 'Libya',
  '450': 'Madagascar', '454': 'Malawi', '458': 'Malaysia',
  '466': 'Mali', '484': 'Mexico', '496': 'Mongolia',
  '504': 'Morocco', '508': 'Mozambique', '516': 'Namibia',
  '524': 'Nepal', '528': 'Netherlands', '554': 'New Zealand',
  '558': 'Nicaragua', '562': 'Niger', '566': 'Nigeria',
  '578': 'Norway', '586': 'Pakistan', '591': 'Panama',
  '598': 'Papua New Guinea', '600': 'Paraguay', '604': 'Peru',
  '608': 'Philippines', '616': 'Poland', '620': 'Portugal',
  '630': 'Puerto Rico', '642': 'Romania', '643': 'Russia',
  '646': 'Rwanda', '682': 'Saudi Arabia', '686': 'Senegal',
  '694': 'Sierra Leone', '706': 'Somalia', '710': 'South Africa',
  '724': 'Spain', '729': 'Sudan', '736': 'Sudan (old)',
  '748': 'Eswatini', '752': 'Sweden', '756': 'Switzerland',
  '760': 'Syria', '158': 'Taiwan', '762': 'Tajikistan',
  '764': 'Thailand', '768': 'Togo', '788': 'Tunisia',
  '792': 'Turkey', '800': 'Uganda', '804': 'Ukraine',
  '784': 'United Arab Emirates', '826': 'United Kingdom',
  '840': 'United States', '858': 'Uruguay', '860': 'Uzbekistan',
  '862': 'Venezuela', '704': 'Vietnam', '887': 'Yemen',
  '894': 'Zambia', '716': 'Zimbabwe',
};

// ─── Projection & path generator ────────────────────────────────────────────
const projection = d3.geoNaturalEarth1()
  .scale(width / 6.3)
  .translate([width / 2, height / 2]);

const path = d3.geoPath().projection(projection);
const graticule = d3.geoGraticule();

// ─── SVG ─────────────────────────────────────────────────────────────────────
const svg = d3.select('#map')
  .append('svg')
  .attr('width', width)
  .attr('height', height);

const g = svg.append('g');

// ─── Static layers: sphere (ocean) + graticule ───────────────────────────────
g.append('path')
  .datum({ type: 'Sphere' })
  .attr('class', 'sphere')
  .attr('d', path);

g.append('path')
  .datum(graticule())
  .attr('class', 'graticule')
  .attr('d', path);

// ─── Load TopoJSON and draw countries + borders ──────────────────────────────
d3.json('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json').then(world => {
  const countries = topojson.feature(world, world.objects.countries);

  g.selectAll('.country')
    .data(countries.features)
    .join('path')
    .attr('class', 'country')
    .attr('d', path)
    .on('mouseover', (event, d) => {
      const name = countryNames[String(d.id).padStart(3, '0')] || 'Unknown';
      tooltip.style('opacity', 1).text(name);
    })
    .on('mousemove', (event) => {
      tooltip
        .style('left', (event.clientX + 12) + 'px')
        .style('top', (event.clientY - 28) + 'px');
    })
    .on('mouseout', () => tooltip.style('opacity', 0));

  // Country border mesh (internal borders only — where a !== b)
  g.append('path')
    .datum(topojson.mesh(world, world.objects.countries, (a, b) => a !== b))
    .attr('class', 'border')
    .attr('d', path);
});

// ─── Zoom & pan ───────────────────────────────────────────────────────────────
const zoom = d3.zoom()
  .scaleExtent([1, 8])
  .on('zoom', (event) => {
    g.attr('transform', event.transform);
  });

svg.call(zoom);