Better FPS version of Zoom to Group of Countries III.
xxxxxxxxxx
<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;
}
svg {
position: absolute;
left: 0;
top: 0;
display: none;
}
.axis .domain {
display: none;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
stroke-opacity: 0.87;
stroke-width: 0.444;
}
.axis text {
font: 16px Calibri, Arial, Helvetica, sans-serif;
fill: #000;
fill-opacity: 0.87;
}
</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 valueInterpolation = d3.interpolate('#f89f00', '#f80000');
var maxScale = 6;
var scaleMargin = 0.014;
var maxCountry = 1;
var config = {
durationMin: 800,
durationMax: 1200,
interval: 1000,
land: {
fillStyle: '#fff',
shadowColor: '#000',
shadowOffsetY: 0,
shadowBlur: 0
},
border: {
lineWidth: 0.444,
strokeStyle: '#666',
shadowColor: '#888',
shadowOffsetY: 0,
shadowBlur: 0
},
past: {
fillStyle: '#ddd',
shadowColor: '#fff',
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 = 20;
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');
ctx.lineJoin = 'round';
var projection = d3.geo.equirectangular().translate([width / 2, height / 2]).scale(153 * canvasScale).rotate([-rusUsaShift, 0, 0]);
projection.precision(0);
var path = d3.geo.path().context(ctx);
var area = 1;
var simplify = d3.geo.transform({
point: function (x, y, z) {
if (z >= area) {
this.stream.point(x, y);
}
}
});
path.projection({
stream: function (s) {
return simplify.stream(projection.stream(s));
}
});
var currentScale = 1;
var currentTranslate = [width / 2, height / 2];
var pastCountries = {};
var pastColors = {};
var currentEventIndex = -1;
var loadedEvents;
var land, borders, countries = {};
var scale = d3.scale.linear()
.domain([1, 2])
.range([0, 380]);
var svg = d3.select('body').append('svg')
.style('top', height - 35 + 'px')
.style('left', width - 400 + 'px')
.attr('width', 400)
.attr('height', 35);
var gradient = svg.append('svg:defs')
.append('svg:linearGradient')
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '0%')
.attr('spreadMethod', 'pad');
gradient.append('svg:stop')
.attr('offset', '0%')
.attr('stop-color', valueInterpolation(0))
.attr('stop-opacity', 1);
gradient.append('svg:stop')
.attr('offset', '100%')
.attr('stop-color', valueInterpolation(1))
.attr('stop-opacity', 1);
svg.append('svg:rect')
.attr('width', 380)
.attr('transform', 'translate(10,7)')
.attr('height', 8)
.style('stroke-width', 0)
.style('fill', 'url(#gradient)');
var axis = d3.svg.axis()
.scale(scale)
.ticks(1)
.tickSize(8)
.orient("bottom");
var legend = svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(10,7)")
.call(axis);
d3.json('world.json', function (world) {
initWorld(world);
d3.json('/darosh/raw/0444bdd92c1f968c75ef/events.json', function (events) {
loadedEvents = events;
update();
});
});
function update() {
var e = loadedEvents[currentEventIndex];
if (e) {
svg.style('display', 'block');
var d = new Date(e[0]);
var mm = getValuesMinMax(e[1]);
info.text('Week: ' + (d.getMonth() + 1) + '/' + d.getDate() + '/' + 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);
scale.domain(!(mm.max - mm.min) ? [mm.min - 0.5, mm.max + 0.5] : [mm.min, mm.max]);
axis.ticks((mm.max - mm.min) || 1);
legend.transition().duration(config.durationMin).call(axis);
transition(e[1]);
} else {
transition({});
}
currentEventIndex++;
if (currentEventIndex < loadedEvents.length) {
setTimeout(update, config.interval);
} else {
setTimeout(function () {
config.durationMax *= 2;
info.text('');
svg.style('display', 'none');
transition({});
}, config.interval);
}
}
function initWorld(world) {
topojson.presimplify(world);
borders = topojson.mesh(world, world.objects.countries);
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);
projection.rotate(targetRotation);
area = 1.5;
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 minMax = getValuesMinMax(values);
targetCenter[1] = (targetCenter[1] < realCenter[1] / targetScale) ? realCenter[1] / targetScale : targetCenter[1];
var zoomInterpolation = d3.interpolateZoom([currentTranslate[0], currentTranslate[1], width * currentScale],
[targetCenter[0], targetCenter[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.globalAlpha = 1;
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]];
currentScale = zoom[2] / width;
var box = [currentTranslate[0] - realCenter[0] / currentScale,
currentTranslate[1] - realCenter[1] / currentScale];
box[1] = box[1] < 0 ? 0 : box[1];
var translate = [-box[0] * currentScale, -box[1] * currentScale];
ctx.translate(translate[0], translate[1]);
ctx.scale(currentScale, currentScale);
area = 1 / canvasScale / canvasScale / currentScale / currentScale;
// Past
ctx.beginPath();
setContextSyle(ctx, config.past);
Object.keys(pastCountries).forEach(function fillPast(id) {
if (!values[id]) {
path(countries[id]);
pastColors[id] = config.past.fillStyle;
}
});
ctx.fill();
// Current
selectedFeatures.forEach(function fillNow(f) {
ctx.beginPath();
ctx.fillStyle = d3.interpolate(pastColors[f.id] || config.land.fillStyle, valueInterpolation(normalize(values[f.id], minMax)), 'easeIn')(t);
pastColors[f.id] = ctx.fillStyle;
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();
};
})
.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