Skip to content

Instantly share code, notes, and snippets.

@darosh
Last active October 17, 2015 23:42
Show Gist options
  • Save darosh/f5204d3d85bdaa1fd6ea to your computer and use it in GitHub Desktop.
Save darosh/f5204d3d85bdaa1fd6ea to your computer and use it in GitHub Desktop.
Zoom to Group of Countries II
<!DOCTYPE html>
<meta charset="utf-8">
<style>
* {
line-height: 20px;
font-family: Calibri, Arial, Helvetica, sans-serif;
color: #999;
margin: 0;
padding: 0;
text-align: center;
text-rendering: optimizelegibility;
}
canvas {
display: block;
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 = {
durationMin: 800,
durationMax: 1200,
interval: 1000,
fontSize: 16,
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: 2,
fillStyle: '#fff',
shadowColor: 'rgba(0,0,0,0.87)',
strokeStyle: '#888',
shadowBlur: 4,
lineWidth: 0
},
graticule: {
strokeStyle: '#aaa',
lineWidth: 0.25,
shadowOffsetY: 0,
shadowBlur: 0
}
};
var centroidsShifts = {
USA: [30, 10],
IRN: [0, 5],
FRA: [20, -20],
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('/darosh/raw/baf7dd8d481d83b7f37e/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];
});
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 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 minMax = getValuesMinMax(values);
var zoomInterpolation = d3.interpolateZoom([currentTranslate[0], currentTranslate[1], width * currentScale],
[targetTranslate[0], targetTranslate[1], width * targetScale]);
d3.transition()
.duration(Math.min(config.durationMax, Math.max(config.durationMin, 2.5 * zoomInterpolation.duration)))
.tween('tween', function getTween() {
return function drawTween(t) {
// Clear
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, width, height);
// Transform
var zoom = zoomInterpolation(t);
projection.rotate(rotationInterpolation(t));
currentTranslate = [zoom[0], zoom[1]];
ctx.translate(currentTranslate[0], currentTranslate[1]);
currentScale = zoom[2] / width;
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 * canvasScale / currentScale + '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] * canvasScale / currentScale;
x[1] += centroidsShifts[f.id][1] * canvasScale / currentScale;
}
ctx.shadowBlur = config.names.shadowBlur * canvasScale / currentScale;
ctx.lineWidth = config.names.lineWidth * canvasScale / currentScale;
ctx.shadowOffsetY = config.names.shadowOffsetY * canvasScale / currentScale;
ctx.fillText(name, x[0], x[1] + config.fontShift * canvasScale / currentScale);
});
};
})
.transition();
}
</script>
function limitBounds(b, maxWidth, maxHeight) {
var w = b[1][0] - b[0][0];
if (w > maxWidth) {
var c = (b[1][0] + b[0][0]) / 2;
maxWidth /= 2;
b[0][0] = c - maxWidth;
b[1][0] = c + maxWidth;
}
var h = b[1][1] - b[0][1];
if (h > maxHeight) {
var ch = (b[1][1] + b[0][1]) / 2;
maxHeight /= 2;
b[0][1] = ch - maxHeight;
b[1][1] = ch + maxHeight;
}
}
function groupBounds(path, features, width, height, maxWidth, maxHeight) {
var r = features.length ? [[[], []], [[], []]] : [[[0], [0]], [[width], [height]]];
features.forEach(function (feature) {
var b = path.bounds(feature);
limitBounds(b, maxWidth, maxHeight);
r[0][0].push(b[0][0]);
r[0][1].push(b[0][1]);
r[1][0].push(b[1][0]);
r[1][1].push(b[1][1]);
});
r[0][0] = Math.min.apply(this, r[0][0]);
r[0][1] = Math.min.apply(this, r[0][1]);
r[1][0] = Math.max.apply(this, r[1][0]);
r[1][1] = Math.max.apply(this, r[1][1]);
return r;
}
function normalize(v, minMax) {
return minMax.diff ? (v - minMax.min) / minMax.diff : 0.5;
}
function getValuesMinMax(values) {
var v = [];
var s = 0;
for (var k in values) {
v.push(values[k]);
s += values[k];
}
var r = {
min: Math.min.apply(this, v),
max: Math.max.apply(this, v)
};
r.diff = r.max - r.min;
r.sum = s;
return r;
}
function getScale(size, width, height, scaleMargin, maxScale) {
var marginSize = [(size[0] + width * scaleMargin), (size[1] + width * scaleMargin)];
var sizeRatio = marginSize[0] / marginSize[1];
var boxRatio = width / height;
var r;
if (sizeRatio >= boxRatio) {
r = width / marginSize[0];
} else {
r = height / marginSize[1];
}
if (r < 1) {
r = 1;
} else if (r > maxScale) {
r = maxScale;
}
return r;
}
function setContextSyle(ctx, opt) {
ctx.fillStyle = opt.fillStyle;
ctx.strokeStyle = opt.strokeStyle;
ctx.shadowColor= opt.shadowColor;
ctx.shadowOffsetY = opt.shadowOffsetY;
ctx.shadowBlur = opt.shadowBlur;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment