Globe

Lesson 6

d3.geoOrthographic() projects a sphere. d3.drag() updates the projection’s rotate property on drag — the projection is re-called to regenerate paths. requestAnimationFrame auto-rotates by incrementing the Y rotation each frame. d3.graticule() draws latitude/longitude grid lines.

Source Code script.js
const countryNames = {
  '004':'Afghanistan','008':'Albania','012':'Algeria','024':'Angola','032':'Argentina',
  '036':'Australia','040':'Austria','050':'Bangladesh','056':'Belgium','068':'Bolivia',
  '076':'Brazil','100':'Bulgaria','116':'Cambodia','120':'Cameroon','124':'Canada',
  '152':'Chile','156':'China','170':'Colombia','180':'DR Congo','191':'Croatia',
  '192':'Cuba','203':'Czech Republic','208':'Denmark','218':'Ecuador','818':'Egypt',
  '231':'Ethiopia','246':'Finland','250':'France','276':'Germany','288':'Ghana',
  '300':'Greece','320':'Guatemala','332':'Haiti','348':'Hungary','356':'India',
  '360':'Indonesia','364':'Iran','368':'Iraq','372':'Ireland','376':'Israel',
  '380':'Italy','392':'Japan','400':'Jordan','398':'Kazakhstan','404':'Kenya',
  '410':'South Korea','414':'Kuwait','458':'Malaysia','484':'Mexico','504':'Morocco',
  '524':'Nepal','528':'Netherlands','554':'New Zealand','566':'Nigeria','578':'Norway',
  '586':'Pakistan','604':'Peru','608':'Philippines','616':'Poland','620':'Portugal',
  '642':'Romania','643':'Russia','682':'Saudi Arabia','710':'South Africa',
  '724':'Spain','144':'Sri Lanka','752':'Sweden','756':'Switzerland','760':'Syria',
  '764':'Thailand','792':'Turkey','804':'Ukraine','784':'United Arab Emirates',
  '826':'United Kingdom','840':'United States','858':'Uruguay','862':'Venezuela',
  '704':'Vietnam','887':'Yemen','716':'Zimbabwe',
};

const size = Math.min(560, window.innerWidth - 40);

const projection = d3.geoOrthographic()
  .scale(size / 2 - 10)
  .translate([size / 2, size / 2])
  .clipAngle(90)
  .rotate([0, -25]);

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

const graticule = d3.geoGraticule()();

const svg = d3.select('#globe').append('svg').attr('width', size).attr('height', size);
const g = svg.append('g');

const spherePath    = g.append('path').attr('class', 'sphere');
const graticulePath = g.append('path').attr('class', 'graticule');
let countries;

const tooltip = d3.select('#tooltip');

function redraw() {
  spherePath.attr('d', path({ type: 'Sphere' }));
  graticulePath.attr('d', path(graticule));
  if (countries) countries.attr('d', path);
}

let isDragging = false;
let resumeTimeout;
const rotateSpeed = 0.2;

const timer = d3.timer(() => {
  if (!isDragging) {
    const r = projection.rotate();
    projection.rotate([r[0] + rotateSpeed, r[1]]);
    redraw();
  }
});

const drag = d3.drag()
  .on('start', () => {
    isDragging = true;
    clearTimeout(resumeTimeout);
  })
  .on('drag', (event) => {
    const r = projection.rotate();
    const sensitivity = 0.3;
    projection.rotate([
      r[0] + event.dx * sensitivity,
      r[1] - event.dy * sensitivity,
    ]);
    redraw();
  })
  .on('end', () => {
    resumeTimeout = setTimeout(() => { isDragging = false; }, 1500);
  });

svg.call(drag);

d3.json('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json').then(world => {
  const features = topojson.feature(world, world.objects.countries).features;

  countries = g.selectAll('.country')
    .data(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);
      d3.select(event.currentTarget).raise();
    })
    .on('mousemove', (event) => {
      tooltip
        .style('left', (event.clientX + 12) + 'px')
        .style('top',  (event.clientY - 28) + 'px');
    })
    .on('mouseout', () => tooltip.style('opacity', 0));

  redraw();
});

// Initial draw of sphere and graticule before data loads
redraw();