- Space/Enter keys: Pause/Play
- Left/Right keys: Previous/Next
- Pan & Zoom: when paused
Last active
February 3, 2018 12:54
-
-
Save darosh/383197ed997156ac9fd5 to your computer and use it in GitHub Desktop.
Countries & Events
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function initData(data, colors) { | |
var max = 0; | |
data.forEach(function (v) { | |
v[2] = new Date(v[0]); | |
v[3] = d3.keys(v[1]); | |
v[3].sort(function (a, b) { | |
return v[1][b] - v[1][a]; | |
}); | |
var y0 = 0; | |
var i = 0; | |
var minMax = getValuesMinMax(v[1]); | |
v[6] = {}; | |
v[4] = v[3].map(function (id) { | |
var color = colors(normalize(v[1][id], minMax)); | |
var r = { | |
id: id, | |
i: i++, | |
v: v[1][id], | |
y0: y0, | |
y1: y0 += v[1][id], | |
color: color | |
}; | |
v[6][id] = r; | |
return r; | |
}); | |
v[5] = minMax; | |
max = Math.max(max, y0); | |
}); | |
return max; | |
} | |
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 normalize(v, minMax) { | |
return minMax.diff ? (v - minMax.min) / minMax.diff : 0.5; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 visualBounds(path, feature) { | |
if (feature._visualBounds) { | |
return feature._visualBounds; | |
} | |
var bb = feature._bounds || (feature._bounds = path.bounds(feature)); | |
var c = feature._centroid || (feature._centroid = path.centroid(feature)); | |
var d = 0.366; | |
var b = [[bb[0][0], bb[0][1]], [bb[1][0], bb[1][1]]]; | |
var sx = d3.scale.linear().domain([b[0][0], b[1][0]]).range([0, 1]); | |
var sy = d3.scale.linear().domain([b[0][1], b[1][1]]).range([0, 1]); | |
if (Math.abs(0.5 - sx(c[0])) > d || Math.abs(0.5 - sy(c[1])) > d) { | |
var h = Math.min(c[0] - b[0][0], b[1][0] - c[0], c[1] - b[0][1], b[1][1] - c[1]); | |
b[0][0] = c[0] - h; | |
b[1][0] = c[0] + h; | |
b[0][1] = c[1] - h; | |
b[1][1] = c[1] + h; | |
} | |
if (feature.id === 'AUS') { | |
b[1][1] = d3.interpolate(b[0][1], b[1][1])(0.75); | |
} else if (feature.id === 'USA') { | |
b[1][0] = d3.interpolate(b[0][0], b[1][0])(0.28); | |
} | |
feature._visualBounds = b; | |
return b; | |
} | |
function groupBounds(path, features, width, height, maxWidth, maxHeight) { | |
var r = features.length ? [[[], []], [[], []]] : [[[0], [0]], [[width], [height]]]; | |
features.forEach(function (feature) { | |
var b = visualBounds(path, 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 getScale(size, width, height, scaleMargin, maxScale) { | |
var marginSize = [(size[0] + width * scaleMargin * 2), (size[1] + width * scaleMargin * 2)]; | |
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function updateLabel(label, tx, ty) { | |
label.left += tx; | |
label.cx += tx; | |
label.right += tx; | |
label.top += ty; | |
label.cy += ty; | |
label.bottom += ty; | |
} | |
function colision(a, b) { | |
return a.left < b.right && | |
a.right > b.left && | |
a.top < b.bottom && | |
a.bottom > b.top; | |
} | |
// based on http://bl.ocks.org/larskotthoff/11406992 | |
function arrangeLabels(labels, module, loops) { | |
var move = true; | |
module /= labels.length; | |
while (move && loops--) { | |
move = false; | |
labels.forEach(function (a) { | |
labels.forEach(function (b) { | |
if (b !== a) { | |
if (colision(a, b)) { | |
move = true; | |
updateLabel(b, -module * Math.sign(a.cx - b.cx), -module * Math.sign(a.cy - b.cy)); | |
updateLabel(a, module * Math.sign(a.cx - b.cx), module * Math.sign(a.cy - b.cy)); | |
} | |
} | |
}); | |
}); | |
} | |
} | |
function addMargin(label, margin) { | |
label.left -= margin; | |
label.right += margin; | |
label.top -= margin; | |
label.bottom += margin; | |
label.width += 2 * margin; | |
label.height += 2 * margin; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* { | |
line-height: 20px; | |
font-family: Calibri, Arial, Helvetica, sans-serif; | |
color: #999; | |
margin: 0; | |
padding: 0; | |
text-align: center; | |
text-rendering: optimizelegibility; | |
-webkit-touch-callout: none; | |
-webkit-user-select: none; | |
-khtml-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
} | |
canvas { | |
display: block; | |
border-bottom: 1px solid #DEDEDE; | |
cursor: pointer; | |
} | |
svg { | |
position: absolute; | |
left: 0; | |
top: 0; | |
display: none; | |
cursor: pointer; | |
} | |
.paused canvas, | |
.paused svg { | |
cursor: move; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
stroke-opacity: 0.25; | |
stroke-width: 1; | |
shape-rendering: crispEdges; | |
} | |
.chart line { | |
stroke-opacity: 1; | |
stroke: #ddd; | |
} | |
.axis text { | |
font: 16px Calibri, Arial, Helvetica, sans-serif; | |
fill: #000; | |
fill-opacity: 0.87; | |
} | |
.click, .chart g { | |
cursor: crosshair; | |
} | |
.chart rect { | |
stroke-width: 0; | |
} | |
.chart g rect { | |
fill-opacity: 0.22; | |
} | |
.chart:hover g rect { | |
fill-opacity: 0.38; | |
} | |
.chart g.active rect { | |
fill-opacity: 1; | |
} | |
.player circle { | |
fill-opacity: 0.87; | |
fill: #000; | |
} | |
.player text { | |
cursor: pointer; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function makeBarLegend(root, height, max) { | |
var band = 6; | |
var scale = d3.scale.linear() | |
.domain([0, max]) | |
.range([height, 0]); | |
var axis = d3.svg.axis() | |
.scale(scale) | |
.ticks(8) | |
.tickSize(6) | |
.orient('right'); | |
var group = root.append('g'); | |
root.append('g') | |
.attr('transform', 'translate(6,0)') | |
.attr('class', 'axis') | |
.call(axis); | |
function update(event, duration) { | |
var r = group.selectAll('rect') | |
.data(event); | |
r.enter().append('rect') | |
.attr('width', band) | |
.attr('height', 0) | |
.attr('y', height) | |
.style('fill', function (d) { | |
return d.color; | |
}); | |
r.exit().remove(); | |
group.selectAll('rect').transition().duration(duration) | |
.attr('y', function (d) { | |
return scale(d.y1); | |
}) | |
.attr('height', function (d) { | |
return scale(d.y0) - scale(d.y1); | |
}) | |
.style('fill', function (d) { | |
return d.color; | |
}); | |
} | |
return { | |
update: update | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function makeChart(root, data, width, height, max, clicked) { | |
var band = (width + 1.5) / data.length - 1.5; | |
band = Math.min(band, 6); | |
var bandHalf = band / 2; | |
var x = d3.time.scale() | |
.range([0, width - band]) | |
.domain([data[0][2], data[data.length - 1][2]]); | |
var y = d3.scale.linear() | |
.range([height, 0]) | |
.domain([0, max]); | |
var line = root.append('line') | |
.style('stroke-width', band) | |
.attr('x1', bandHalf) | |
.attr('x2', bandHalf) | |
.attr('y2', height); | |
var group = root.selectAll('.group') | |
.data(data) | |
.enter().append('g') | |
.attr('class', 'group') | |
.attr('transform', function (d) { | |
return 'translate(' + x(d[2]) + ',0)'; | |
}); | |
group.selectAll('rect') | |
.data(function (d) { | |
return d[4]; | |
}) | |
.enter().append('rect') | |
.attr('width', band) | |
.attr('y', function (d) { | |
return y(d.y1); | |
}) | |
.attr('height', function (d) { | |
return y(d.y0) - y(d.y1); | |
}) | |
.style('fill', function (d) { | |
return d.color; | |
}); | |
root.append('rect') | |
.attr('class', 'click') | |
.attr('width', width) | |
.attr('height', height) | |
.style('fill', 'rgba(255,255,255,0)') | |
.on('click', function () { | |
d3.event.stopPropagation(); | |
var i = Math.round((d3.mouse(this)[0] / width) * (data.length - 1)); | |
clicked(i); | |
}); | |
function updateCursor(index) { | |
group | |
.attr('class', function (d) { | |
return data[index] === d ? 'active' : ''; | |
}); | |
var p = x(data[index][2]) + bandHalf; | |
line.transition() | |
.attr('x1', p) | |
.attr('x2', p); | |
} | |
return { | |
updateCursor: updateCursor, | |
band: band | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function updateInfoLegend(list, event, height, width, countries, items) { | |
var radius = 5; | |
var charsLimit = 13; | |
items--; | |
var display = event[3].slice(0, Math.min(items, event[3].length)); | |
var other = { | |
color: 'transparent', | |
id: 'other' | |
}; | |
if (event[3].length === (display.length + 1)) { | |
display.push(event[3].length - 1); | |
} else if (event[3].length > display.length) { | |
other.v = 0; | |
for (var i = display.length; i < event[3].length; i++) { | |
other.v += event[1][event[3][i]]; | |
} | |
other.title = (event[3].length - display.length) + ' other'; | |
other.i = display.length; | |
display.push(other.id); | |
} | |
var z = list.selectAll('g').data(display, function (d) { | |
return d; | |
}); | |
z.exit().remove(); | |
z.transition().attr('opacity', 1).attr('transform', function (d, i) { | |
return 'translate(0,' + (height - i * 20 - 10) + ')' | |
}); | |
var g = z.enter().append('g'); | |
g.append('circle'); | |
g.append('text') | |
.attr('class', 'name') | |
.attr('transform', 'translate(' + radius * 3 + ',0)'); | |
g.append('text') | |
.attr('class', 'value') | |
.attr('dx', width) | |
.attr('text-anchor', 'end'); | |
g.attr('opacity', 0) | |
.attr('transform', function (d, i) { | |
return 'translate(' + (width / 2) + ',' + (height - i * 20 - 10) + ')' | |
}) | |
.transition().duration(200).delay(200) | |
.attr('opacity', 1).attr('transform', function (d, i) { | |
return 'translate(0,' + (height - i * 20 - 10) + ')' | |
}); | |
list.selectAll('text.name') | |
.text(function (d) { | |
return !countries[d] ? other.title : ((countries[d].properties.name.length > charsLimit) ? d : countries[d].properties.name); | |
}); | |
list.selectAll('text.value') | |
.text(function (d) { | |
return !event[1][d] ? other.v : event[1][d]; | |
}); | |
list.selectAll('circle') | |
.attr('cy', -radius) | |
.attr('cx', radius) | |
.attr('r', radius) | |
.style('fill', function (d) { | |
var i = event[3].indexOf(d); | |
return i === -1 ? other.color : event[4][i].color; | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function makePlayer(root, width) { | |
var scale = d3.time.scale().range([0, width]); | |
var axis = d3.svg.axis() | |
.scale(scale) | |
.ticks(width / 80) | |
.tickSize(6) | |
.orient('bottom'); | |
var timeLine = root.append('g').attr('class', 'axis'); | |
function updatePlayer(events, band, clicked) { | |
scale.domain([new Date(events[0][0]), new Date(events[events.length - 1][0])]); | |
scale.range([0, width - band]); | |
scale.domain([new Date(events[0][0]), scale.invert(width)]); | |
scale.range([0, width]); | |
timeLine.call(axis) | |
.selectAll('text') | |
.style('text-anchor', 'start'); | |
timeLine.selectAll('text').on('click', function (d) { | |
for (var i = 0; i < events.length; i++) { | |
if (events[i][2] >= d) { | |
d3.event.stopPropagation(); | |
clicked(i); | |
break; | |
} | |
} | |
}); | |
} | |
return { | |
updatePlayer: updatePlayer | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function makeScaleLegend(svg, root, width, colors) { | |
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', colors(0)) | |
.attr('stop-opacity', 1); | |
gradient.append('svg:stop') | |
.attr('offset', '100%') | |
.attr('stop-color', colors(1)) | |
.attr('stop-opacity', 1); | |
root.append('svg:rect') | |
.attr('width', width) | |
.attr('height', 6) | |
.style('stroke-width', 0) | |
.style('fill', 'url(#gradient)'); | |
var scale = d3.scale.linear() | |
.domain([1, 2]) | |
.range([0, width]); | |
var axis = d3.svg.axis() | |
.scale(scale) | |
.ticks(1) | |
.tickSize(6) | |
.orient('bottom'); | |
var legend = root.append('g') | |
.attr('transform', 'translate(0,6)') | |
.attr('class', 'axis') | |
.call(axis); | |
function update(minMax, duration) { | |
scale.domain(!(minMax.max - minMax.min) ? [minMax.min - 0.5, minMax.max + 0.5] : [minMax.min, minMax.max]); | |
var ticks = Math.min(8, (minMax.max - minMax.min) || 1); | |
axis.ticks(ticks); | |
legend.transition().duration(duration).call(axis); | |
} | |
return { | |
update: update | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment