Globe
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.
d3.geoOrthographic()이 구체를 투영합니다. d3.drag()가 드래그 시 투영의 rotate 속성을 업데이트하며 투영을 재호출해 경로를 재생성합니다. requestAnimationFrame이 매 프레임 Y 회전을 증가시켜 자동 회전합니다. d3.graticule()이 위도/경도 격자선을 그립니다.
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();