Forked from Canvas globe with cities
to illustrate how to simulate depth using the d3.geoDistance()
function, using an interactive globe (zoom & rotate).
Also demonstrates additive blending via context.globalCompositeOperation = 'lighter'
, where drawing pixels on top of each other adds the RGB values. Starting with any base color that has non-zero red, blue and green components will ultimately saturate to white, giving a glow effect. This is different than opacity-based blending where continually over-drawing using a partially transparent color eventually converges to the non-transparent version of that same color.
Here we use a blue => red color shift and simulate a see-through world, but we could equally hide points where distance > π/2. We need to dim colors based on the normal vector at the globe’s surface to avoid a “glow” effect around the edges where points tend to overlap more often.
Built with blockbuilder.org
<!DOCTYPE html>
<!-- from https://bl.ocks.org/larsvers/dab7c2d6ea5ab964d10df0ef1470b90e -->
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0;background: #000; }
</style>
</head>
<body>
<script>
/* Set up */
/* ====== */
var width = 900,
height = 500,
originalScale = 200,
scale = originalScale,
scaleChange,
rotation;
var sphere = {type: 'Sphere'};
var graticule = d3.geoGraticule();
// set up the main canvas and the projection
var canvas = d3.select('body').append('canvas')
.attr('width', width)
.attr('height', height);
var context = canvas.node().getContext('2d');
context.imageSmoothingEnabled = true;
// blend overlapping colors towards white (vs default source-over)
context.globalCompositeOperation = 'lighter';
context.translate(width / 2, height / 2);
var projection = d3.geoOrthographic()
.scale(scale)
.rotate([42, -23])
.translate([0, 0])
.clipAngle(90);
var path = d3.geoPath()
.projection(projection)
.context(context);
// interpolate color from front to back of globe,
// accounting for normal vector at globe surface
function globeColor(frontColor, backColor) {
// simple interpolation from front to back
var redShift = d3.scaleLinear()
.domain([1, -1])
.range([frontColor, backColor]);
// with additive blending, pixels tend to overlap more near globe edges
// so we dim based on the angle (in radians) from the point nearest viewer
// allow a secondary brightness to increase (>1) or decrease (<1) brightness
function angularDim(angle, brightness) {
brightness = brightness || 1;
const z = Math.cos(angle),
c = d3.rgb(redShift(z)),
f = Math.abs(z) * brightness;
return d3.rgb(c.r*f, c.g*f, c.b*f, c.opacity).toString();
}
return angularDim;
}
/* Data load */
/* ========= */
d3.queue()
.defer(d3.csv, 'cities_all.csv')
.await(load);
function load(error, cities) {
if (error) { console.log(error); }
cities.sort((a, b) => d3.ascending(+a.Population, +b.Population));
var grid = graticule();
var popScale = d3.scaleSqrt()
.domain([0, d3.median(cities, d => +d.Population)])
.clamp(true)
.range([0, 1]);
var cityColor = globeColor('#080880', '#100404');
// Draw the world
function drawWorld() {
context.clearRect(-width/2, -height/2, width, height);
// draw grid(s)
context.save();
context.lineWidth = 0.5;
context.strokeStyle = '#202020';
context.beginPath();
path(sphere);
context.stroke();
context.beginPath();
path(grid);
context.stroke();
// draw cities
// find the center point facing the viewer
const c = projection.invert([0, 0]);
cities.forEach(p => {
const point = [+p.Longitude, +p.Latitude];
// measure distance from the center, so d in [0, Math.PI]
d = d3.geoDistance(c, point);
context.fillStyle = cityColor(d, popScale(+p.Population));
xy = projection(point);
context.fillRect(xy[0], xy[1], 1, 1);
})
context.restore();
} // drawWorld()
// First draw
requestAnimationFrame(drawWorld);
var zoom = d3.zoom()
.scaleExtent([0.5, 4])
.on("zoom", zoomed)
canvas.call(zoom);
var previousScaleFactor = 1;
function zoomed() {
var dx = d3.event.sourceEvent.movementX;
var dy = d3.event.sourceEvent.movementY;
var event = d3.event.sourceEvent.type;
context.save();
context.clearRect(0, 0, width, height);
if (event === 'wheel') {
scaleFactor = d3.event.transform.k;
scaleChange = scaleFactor - previousScaleFactor;
scale = scale + scaleChange * originalScale;
projection.scale(scale);
previousScaleFactor = scaleFactor;
} else {
var r = projection.rotate();
rotation = [r[0] + dx * 0.4, r[1] - dy * 0.5, r[2]];
projection.rotate(rotation);
}
requestAnimationFrame(drawWorld);
context.restore();
} // zoomed()
} // load()
</script>
</body>