xxxxxxxxxx
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<title>Countries & Events</title>
<link rel="stylesheet" href="style.css">
<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="geo.js"></script>
<script src="data.js"></script>
<script src="labels.js"></script>
<script src="svg-chart.js"></script>
<script src="svg-player.js"></script>
<script src="svg-scale-legend.js"></script>
<script src="svg-bar-legend.js"></script>
<script src="svg-info-legend.js"></script>
<body></body>
<script>
var width = 960;
var height = 410;
var shift = (480 - height) / 2;
var canvasScale = ((document.body.clientWidth > width / 3) ? document.body.clientWidth : width) / width;
width *= canvasScale;
height *= canvasScale;
var maxScale = 3;
var scaleMargin = 0.02;
var maxCountry = 1;
var svgMargin = 10;
var labelMargin = 1;
var gridModule = 10;
var scaleHeight = 20;
var svgLine = 10;
var chartHeight = gridModule * 5;
var infoRealHeight;
var legendItems;
var lastSelectedFeatures;
var lastColors;
var lastEvent;
var valueInterpolation = d3.interpolate('#f8af00', '#f80000');
d3.select(self.frameElement).style('height', (height + chartHeight + scaleHeight + 2 * svgMargin) + 'px');
var config = {
durationMultiplier: 1.5,
durationMin: 750,
durationMax: 1500,
durationColor: 350,
tracePast: false,
fontSize: 16,
fontShift: 4,
font: 'Calibri, Arial, Helvetica, sans-serif',
land: {
fillStyle: '#fff'
},
border: {
lineWidth: 0.444,
strokeStyle: '#666'
},
past: {
fillStyle: '#ddd'
},
simpleArea: 8,
baseArea: 8 / canvasScale / canvasScale
};
var currentScale = 1;
var realCenter = [width / 2, height / 2];
var currentTranslate = realCenter;
var pastCountries = {};
var pastColors = {};
var events;
var index = -1;
var auto = true;
var pending = false;
var borders, countries;
var svgHeader, svgs, message, chart, header, infoLegend, barLegend, scaleLegendData, barLegendData, chartData, playerData;
var path, ctx, projection, trans;
var mouseWasDown;
var zoomBehavior = d3.behavior.zoom()
.scaleExtent([1, 8])
.on('zoom', zoomed);
initItems();
initCanvas();
initSvg();
loadData();
function clamp(t, a, b) {
return Math.max(a, Math.min(b, t));
}
function zoomed() {
if (!auto) {
mouseWasDown = false;
requestAnimationFrame(drawManual);
}
}
function initItems() {
var infoMaxHeight = height - scaleHeight - 9 * svgMargin;
legendItems = Math.floor((infoMaxHeight - svgLine) / 20);
infoRealHeight = legendItems * 20 + svgLine;
}
function loaded() {
if (countries && events) {
initKeys();
update();
setTimeout(function () {
message.style('display', 'none');
}, 500);
}
}
function loadData() {
d3.json('topo.json', function (t) {
initTopo(t);
loaded();
});
d3.json('/darosh/raw/baf7dd8d481d83b7f37e/events.json', function (e) {
var max = initData(e, valueInterpolation);
events = e;
chartData = makeChart(chart, events, width - svgMargin * 2, chartHeight, max, setIndex);
playerData.updatePlayer(events, chartData.band, setIndex);
barLegendData = makeBarLegend(barLegend, gridModule * 32, max);
loaded();
});
}
function initCanvas() {
var canvas = d3.select('body').append('canvas').attr('width', width).attr('height', height);
ctx = canvas.node().getContext('2d');
ctx.textAlign = 'center';
ctx.lineJoin = 'round';
message = d3.select('body').append('div').html('Loading…');
var simplify = d3.geo.transform({
point: function (x, y, z) {
if (z >= projection.area && (!projection.clip || (projection.clip[0] <= x && x <= projection.clip[2] &&
projection.clip[1] <= y && y <= projection.clip[3]))) {
this.stream.point(x * canvasScale, (y + 20) * canvasScale);
}
}
});
projection = {
stream: function (s) {
return simplify.stream(s);
},
clip: false,
area: config.simpleArea
};
path = d3.geo.path()
.context(ctx)
.projection(projection);
canvas.on('mousedown', mouseDown);
canvas.call(zoomBehavior);
canvas.on('mouseup', mouseUp);
}
function mouseDown() {
mouseWasDown = true;
}
function mouseUp() {
if (mouseWasDown) {
clicked();
}
}
function clicked() {
auto = !auto;
if (pending) {
trans();
}
if (auto && !pending) {
index++;
index = index % events.length;
update();
}
}
function initSvg() {
svgHeader = d3.select('body').append('svg')
.attr('width', width)
.attr('height', scaleHeight + 2 * svgMargin);
var scaleLegend = svgHeader.append('g')
.attr('class', 'scale-legend')
.attr('transform', 'translate(' + (width - gridModule * 32 - svgMargin) + ',' + svgMargin + ')');
scaleLegendData = makeScaleLegend(svgHeader, scaleLegend, gridModule * 32, valueInterpolation);
var svgBarScale = d3.select('body').append('svg')
.style('left', svgMargin + 'px')
.style('top', (height - 2 * svgMargin - gridModule * 32 - svgLine * .5) + 'px')
.attr('width', scaleHeight + 3 * svgMargin)
.attr('height', gridModule * 32 + svgMargin + svgMargin);
barLegend = svgBarScale.append('g')
.attr('class', 'scale-legend')
.attr('transform', 'translate(' + 0 + ',' + svgMargin + ')');
var svgInfo = d3.select('body').append('svg')
.style('left', (width - 12 * gridModule - svgMargin) + 'px')
.style('top', (height - infoRealHeight - svgLine * 0.5) + 'px')
.attr('width', (12 * gridModule))
.attr('height', infoRealHeight);
infoLegend = svgInfo.append('g');
var svgChart = d3.select('body').append('svg')
.style('top', (height + svgMargin) + 'px')
.attr('width', width)
.attr('height', chartHeight + scaleHeight + svgMargin);
chart = svgChart.append('g')
.attr('class', 'chart')
.attr('transform', 'translate(' + svgMargin + ',' + 0 + ')');
var player = svgChart.append('g')
.attr('class', 'player')
.attr('transform', 'translate(' + svgMargin + ',' + (chartHeight) + ')');
playerData = makePlayer(player, width - 2 * svgMargin);
header = svgHeader.append('text')
.attr('dy', svgLine + svgMargin)
.attr('dx', svgMargin);
svgs = d3.selectAll('svg');
svgHeader.call(zoomBehavior);
svgBarScale.call(zoomBehavior);
svgInfo.call(zoomBehavior);
svgHeader.on('mousedown', mouseDown);
svgHeader.on('mouseup', mouseUp);
svgBarScale.on('mousedown', mouseDown);
svgBarScale.on('mouseup', mouseUp);
svgInfo.on('mousedown', mouseDown);
svgInfo.on('mouseup', mouseUp);
}
function stop() {
auto = false;
}
function initKeys() {
if (self.frameElement) {
self.frameElement.focus();
}
d3.select('body').on('keydown', function () {
if (d3.event.keyCode === 32 || d3.event.keyCode === 13) {
clicked();
} else {
if (auto && (d3.event.keyCode === 37 || d3.event.keyCode === 39 )) {
stop();
}
if (d3.event.keyCode === 37) {
setIndex(index - 1);
} else if (d3.event.keyCode === 39) {
setIndex(index + 1);
}
}
});
}
function setIndex(i) {
stop();
if (pending) {
trans(true, function () {
done();
});
} else {
done();
}
function done() {
var n = (i + events.length) % events.length;
if (index !== n) {
index = n;
update();
}
}
}
function reset() {
pastCountries = {};
}
function update() {
var event = events[index];
if (index === 0) {
reset();
} else if (index === (events.length - 1)) {
stop();
}
d3.select('body').attr('class', null);
if (event) {
svgs.style('display', 'block');
scaleLegendData.update(event[5], config.durationMin);
barLegendData.update(event[4], config.durationMin);
updateInfoLegend(infoLegend, event, infoRealHeight, gridModule * 12, countries, legendItems);
chartData.updateCursor(index);
header.text((event[2].getMonth() + 1) + '/' + event[2].getFullYear() + ' — ' +
event[5].sum + ' events in ' + event[3].length + ' countries');
trans = transition(event);
} else if (index >= -1 && index <= events.length) {
svgs.style('display', 'none');
trans = transition();
}
}
function done() {
pending = false;
if (auto) {
index++;
if (index < events.length) {
update();
} else {
index--;
stop();
}
} else {
d3.select('body').attr('class', 'paused');
}
}
function initTopo(topo) {
topojson.presimplify(topo);
countries = {};
var tempArcs = null;
var tempObj = {};
topo.objects.countries.geometries.forEach(function (country) {
if (country.id === 'RUS') {
tempObj = country;
var merged = topojson.mergeArcs(topo, [country]);
tempArcs = country.arcs;
country.arcs = merged.arcs;
}
});
borders = topojson.mesh(topo, topo.objects.countries, function (a) {
return a.id !== 'ATA';
});
tempObj.arcs = tempArcs;
topojson.feature(topo, topo.objects.countries).features.forEach(function (v) {
countries[v.id] = v;
});
}
function transition(event) {
event = event || [{}, {}, {}, {}, []];
pending = true;
var colors = {};
var selectedFeatures = event[4].map(function (v) {
colors[v.id] = d3.interpolate(pastColors[v.id] || config.land.fillStyle, event[6][v.id].color);
return countries[v.id];
});
pastColors = {};
projection.clip = false;
projection.area = config.simpleArea;
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];
targetCenter[1] = (targetCenter[1] < realCenter[1] / targetScale) ? realCenter[1] / targetScale : targetCenter[1];
targetCenter[0] = (targetCenter[0] < realCenter[0] / targetScale) ? realCenter[0] / targetScale : targetCenter[0];
var zoomInterpolation = d3.interpolateZoom([currentTranslate[0], currentTranslate[1], width * currentScale],
[targetCenter[0], targetCenter[1], width * targetScale]);
var duration = clamp(zoomInterpolation.duration * config.durationMultiplier, config.durationMin, config.durationMax);
var stop = false;
var stopped = false;
var forced = false;
var cb;
var dScale = d3.scale.linear().domain([0, duration]).range([0, 1])(config.durationColor);
var cScale = d3.scale.linear().domain([0, dScale]).range([0, 1]).clamp(true);
var ease = d3.ease('cubic-out');
function tScale(x) {
return ease(cScale(x));
}
d3.transition()
.duration(duration)
.tween('tween', function getTween() {
return drawTween;
})
.each('end', done);
return function (force, done) {
cb = cb || done;
if (force) {
forced = true;
} else {
stop = true;
}
};
function drawTween(t) {
if (cb && (t === 1 || stopped)) {
cb();
}
if (stopped) {
return;
} else if (stop) {
stopped = true;
t = 1;
} else if (forced) {
stopped = true;
}
var tt;
if (forced) {
tt = 1;
} else {
tt = tScale(t);
}
var zoom = zoomInterpolation(t);
currentTranslate = [zoom[0], zoom[1]];
currentScale = zoom[2] / width;
var box = [currentTranslate[0] - realCenter[0] / currentScale, currentTranslate[1] - realCenter[1] / currentScale];
var translate = [-box[0] * currentScale, -box[1] * currentScale];
zoomBehavior.scale(currentScale);
zoomBehavior.translate(translate);
lastSelectedFeatures = selectedFeatures;
lastColors = colors;
lastEvent = event;
drawMap(selectedFeatures, event, translate, currentScale, colors, tt);
// Labels
if (t === 1 && !auto) {
var labels = getLabels(selectedFeatures, currentScale);
arrangeLabels(labels, 1.25 * config.fontSize / currentScale, selectedFeatures.length * 2);
drawLabels(labels, currentScale);
}
}
}
function invertTranslate(t, currentScale) {
var box = [-t[0] / currentScale, -t[1] / currentScale];
return [box[0] + realCenter[0] / currentScale, box[1] + realCenter[1] / currentScale]
}
function drawManual() {
var scale = zoomBehavior.scale();
var translate = zoomBehavior.translate();
var tt = 1;
currentScale = scale;
currentTranslate = invertTranslate(translate, currentScale);
drawMap(lastSelectedFeatures, lastEvent, translate, scale, lastColors, tt);
var labels = getLabels(lastSelectedFeatures, scale);
arrangeLabels(labels, 1.25 * config.fontSize / scale, lastSelectedFeatures.length * 2);
drawLabels(labels, scale);
}
function setContextStyle(ctx, opt) {
ctx.fillStyle = opt.fillStyle;
ctx.strokeStyle = opt.strokeStyle;
}
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
function drawLabels(labels, currentScale) {
labels.forEach(function (label) {
addMargin(label, -labelMargin / currentScale);
ctx.fillStyle = 'rgba(0,0,0,0.87)';
roundRect(ctx, label.left, label.top, label.width, label.height, label.radius / 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.fillText(label.text, label.cx, label.cy + label.shift);
}
);
}
function getLabels(selectedFeatures, currentScale) {
projection.area = config.simpleArea;
var labels = [];
ctx.font = config.fontSize / currentScale + 'px ' + config.font;
selectedFeatures.forEach(function (f) {
var x = f._centroid || path.centroid(f);
var n = (f.properties.name.length >= 20) ? f.id : f.properties.name;
var m = ctx.measureText(n);
var l = {
id: f.id,
text: n,
cx: x[0],
cy: x[1],
width: m.width + config.fontSize / currentScale,
height: 1.25 * config.fontSize / currentScale,
shift: 0.25 * config.fontSize / currentScale,
radius: config.fontSize / 2 / currentScale
};
l.left = l.cx - l.width / 2;
l.right = l.cx + l.width / 2;
l.top = l.cy - l.height / 2;
l.bottom = l.cy + l.height / 2;
addMargin(l, labelMargin / currentScale);
labels.push(l);
});
return labels;
}
function drawMap(selectedFeatures, event, translate, currentScale, colors, tt) {
// Clear
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, width, height);
// Transform
ctx.translate(translate[0], translate[1]);
ctx.scale(currentScale, currentScale);
projection.area = config.baseArea / currentScale / currentScale;
projection.clip = false;
if (config.tracePast) {
// Past
ctx.beginPath();
setContextStyle(ctx, config.past);
Object.keys(pastCountries).forEach(function fillPast(id) {
if (!event[1][id]) {
path(countries[id]);
pastColors[id] = config.past.fillStyle;
}
});
ctx.fill();
}
// Current
selectedFeatures.forEach(function fillNow(f) {
ctx.beginPath();
pastColors[f.id] = ctx.fillStyle = colors[f.id](tt);
path(f);
ctx.fill();
pastCountries[f.id] = true;
});
projection.clip = [-translate[0] / canvasScale / currentScale,
-translate[1] / canvasScale / currentScale - 20,
-translate[0] / canvasScale / currentScale + width / currentScale,
-translate[1] / canvasScale / currentScale + height / currentScale];
// Borders
ctx.beginPath();
setContextStyle(ctx, config.border);
ctx.lineWidth = config.border.lineWidth / currentScale;
path(borders);
ctx.stroke();
projection.clip = false;
}
</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