World Map
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.
d3.geoNaturalEarth1() 투영이 경도/위도를 화면 좌표로 변환합니다. d3.geoPath()가 GeoJSON 피처에서 SVG 경로 데이터를 생성합니다. 국가 색상은 d3.scaleSequential(d3.interpolateGreys)를 통해 데이터 값에 매핑됩니다. 국가 경계선은 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);