Improved version: Zoom to Group of Countries II.
xxxxxxxxxx
<meta charset="utf-8">
<style>
* {
line-height: 1;
font-family: Arial, Helvetica, sans-serif;
color: #999;
margin: 0;
padding: 0;
text-align: center;
}
canvas {
border-bottom: 1px solid #dedede;
}
</style>
<body>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<script src="utils.js"></script>
<script>
var width = 960;
var height = 480;
var canvasScale = ((document.body.clientWidth > width) ? document.body.clientWidth : width) / width;
width *= canvasScale;
height *= canvasScale;
var maxScale = 5;
var scaleMargin = (40 * canvasScale / width);
var maxCountry = (48 * canvasScale / width);
var config = {
duration: 800,
interval: 1050,
fontSize: 10,
fontShift: 2,
font: 'Arial, Helvetica, sans-serif',
land: {
fillStyle: '#bbb',
shadowColor: '#000',
shadowOffsetY: 1,
shadowBlur: 1
},
border: {
lineWidth: 0.75,
strokeStyle: '#fff',
shadowColor: '#eee',
shadowOffsetY: -1
},
past: {
fillStyle: '#9c8f8f',
shadowColor: '#fff',
shadowOffsetY: -1,
shadowBlur: 0
},
names: {
shadowOffsetY: 0,
fillStyle: '#fff',
shadowColor: 'rgba(0,0,0,0.67)',
strokeStyle: '#888'
},
namesBlur: {
shadowBlur: 6,
lineWidth: 0.5
},
graticule: {
strokeStyle: '#aaa',
lineWidth: 0.25
}
};
var centroidsShifts = {
USA: [30, 10],
IRN: [0, 5],
FRA: [15, -12],
IND: [-2, 8],
MAR: [0, -6],
CAN: [-22, 7],
IRQ: [2, 2],
SYR: [0, -2]
};
var rusUsaShift = 12;
var valueInterpolation = d3.interpolate('#ff9f9f', '#f80000');
var canvas = d3.select('body').append('canvas').attr('width', width).attr('height', height);
var info = d3.select('body').append('div');
var ctx = canvas.node().getContext('2d');
var projection = d3.geo.equirectangular().translate([width / 2, height / 2]).scale(153 * canvasScale).rotate([-rusUsaShift, 0, 0]);
var path = d3.geo.path().projection(projection).context(ctx);
var graticule = d3.geo.graticule().step([10, 10]).extent([[-180, -90.001], [180, 90.001]])();
var currentScale = 1;
var currentTranslate = [0, 0];
var pastCountries = {};
var currentEventIndex = -1;
var loadedEvents;
var land, borders, countries = {};
d3.json('/darosh/raw/2d12a584a14910032ab8/countries.json', function (world) {
initWorld(world);
d3.json('events.json', function (events) {
loadedEvents = events;
update();
});
});
function update() {
var e = loadedEvents[currentEventIndex];
if (e) {
var d = new Date(e[0]);
var mm = getValuesMinMax(e[1]);
info.text('Month: ' + (d.getMonth() + 1) + '/' + d.getFullYear() +
', Record: ' + (currentEventIndex + 1) + ' of ' + loadedEvents.length +
', Countries: ' + Object.keys(e[1]).length +
', Past countries: ' + Object.keys(pastCountries).length +
', Min: ' + mm.min + ', Max: ' + mm.max + ', Sum: ' + mm.sum);
transition(e[1]);
} else {
transition({});
}
currentEventIndex++;
if (currentEventIndex < loadedEvents.length) {
setTimeout(update, config.interval);
} else {
transition({});
}
}
function initWorld(world) {
land = topojson.merge(world, world.objects.countries.geometries);
borders = topojson.mesh(world, world.objects.countries, function (a, b) {
return a !== b;
});
topojson.feature(world, world.objects.countries).features.forEach(function (v) {
countries[v.id] = v;
});
}
function transition(values) {
var selectedFeatures = Object.keys(values).map(function (v) {
return countries[v];
});
d3.transition()
.duration(config.duration)
.tween('tween', function getTween() {
var currentRotation = projection.rotate();
var targetRotation = !values['RUS'] && values['USA'] ? [+rusUsaShift, 0] : [-rusUsaShift, 0];
var rotationInterpolation = d3.interpolate(currentRotation, targetRotation, 'easeIn');
projection.rotate(targetRotation);
var bound = groupBounds(path, selectedFeatures, width, height, maxCountry * width, maxCountry * height);
var size = [bound[1][0] - bound[0][0], bound[1][1] - bound[0][1]];
var targetScale = getScale(size, width, height, scaleMargin, maxScale);
var scaleInterpolation = d3.interpolate(currentScale, targetScale, 'easeIn');
var targetCenter = [(bound[0][0] + bound[1][0]) / 2, (bound[0][1] + bound[1][1]) / 2];
var realCenter = [width / 2, height / 2];
var scaledBox = [
[targetCenter[0] - realCenter[0] / targetScale,
targetCenter[1] - realCenter[1] / targetScale],
[targetCenter[0] + realCenter[0] / targetScale,
targetCenter[1] + realCenter[1] / targetScale]];
scaledBox[0][1] = scaledBox[0][1] < 0 ? 0 : scaledBox[0][1];
var targetTranslate = [-scaledBox[0][0] * targetScale, -scaledBox[0][1] * targetScale];
var translateInterpolation = d3.interpolate(currentTranslate, targetTranslate, 'easeIn');
var minMax = getValuesMinMax(values);
return function drawTween(t) {
// Clear
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, width, height);
// Transform
projection.rotate(rotationInterpolation(t));
currentTranslate = translateInterpolation(t);
ctx.translate(currentTranslate[0], currentTranslate[1]);
currentScale = scaleInterpolation(t);
ctx.scale(currentScale, currentScale);
// Graticule
ctx.beginPath();
setContextSyle(ctx, config.graticule);
ctx.lineWidth = config.graticule.lineWidth / currentScale;
path(graticule);
ctx.stroke();
// Land
ctx.beginPath();
setContextSyle(ctx, config.land);
path(land);
ctx.fill();
// Past
ctx.beginPath();
setContextSyle(ctx, config.past);
Object.keys(pastCountries).forEach(function fillPast(id) {
if (!values[id]) {
path(countries[id]);
}
});
ctx.fill();
// Current
selectedFeatures.forEach(function fillNow(f) {
ctx.beginPath();
ctx.fillStyle = valueInterpolation(normalize(values[f.id], minMax));
path(f);
ctx.fill();
pastCountries[f.id] = true;
});
// Borders
ctx.beginPath();
setContextSyle(ctx, config.border);
ctx.lineWidth = config.border.lineWidth / currentScale;
path(borders);
ctx.stroke();
// Names
setContextSyle(ctx, config.names);
ctx.font = config.fontSize + 'px ' + config.font;
ctx.textAlign = 'center';
selectedFeatures.forEach(function addCountryName(f) {
var x = path.centroid(f);
var name = (f.properties.name).split(',')[0];
if (centroidsShifts[f.id]) {
x[0] += centroidsShifts[f.id][0];
x[1] += centroidsShifts[f.id][1];
}
ctx.shadowBlur = config.namesBlur.shadowBlur;
ctx.lineWidth = config.namesBlur.lineWidth;
ctx.strokeText(name, x[0], x[1] + config.fontShift);
ctx.shadowBlur = 0;
ctx.lineWidth = 0;
ctx.fillText(name, x[0], x[1] + config.fontShift);
});
};
})
.transition();
}
</script>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js
https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js