I walked up to Joe Weisenthal in a bar (The Churchill!) having a Brexit party and asked him to give me an assignment. Map based on Let’s Make A Map, which was a good head start!
Results based on the excellent Guardian Brexit results page.
<!DOCTYPE html>
<meta charset="utf-8">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1"> -->
<meta name="viewport" content="width=960">
<style>
html, body {
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
}
.subunit.SCT { fill: rgba(255,0,0,.05); stroke: #aaa; }
.subunit.WLS { fill: rgba(0,255,0,.05); stroke: #aaa; }
.subunit.NIR { fill: rgba(255,0,255,.05); stroke: #aaa; }
.subunit.ENG { fill: rgba(0,255,255,.05); stroke: #aaa; }
.subunit.IRL,
.subunit-label.IRL {
display: none;
}
.subunit-boundary {
fill: none;
stroke: #777;
stroke-dasharray: 2,2;
stroke-linejoin: round;
}
.subunit-boundary.IRL {
stroke: #aaa;
}
.subunit-label {
fill: #777;
fill-opacity: .5;
font-size: 20px;
font-weight: 300;
text-anchor: middle;
}
.place,
.place-label {
display: none;
fill: #444;
}
text {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 10px;
pointer-events: none;
}
h1 {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
text-align: center;
position: fixed;
width: 100%;
padding: 0 .25em;
font-size: 50px;
pointer-events: none;
}
h1.prompt {
top: .25em;
}
h1.results {
bottom: .25em;
}
circle {
stroke-width: 2;
stroke: black;
}
circle.guess {
fill: rgba(0,0,0,.6);
}
circle.answer {
fill: rgba(0,255,0,.6);
}
button {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%,-50%) rotate(20deg);
border-radius: 50%;
padding: 1em;
background: rgba(255,255,255,.5);
border: 2px solid black;
font-size: 70px;
cursor: pointer;
}
div.finish {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,.95);
}
div.finish h1 {
position: absolute;
top: 50%;
transform: translate(0,-50%);
margin: 0;
}
</style>
<body>
<h1 class="prompt">
<span class="name"></span> voted to
<span class="result"></span>,
<span class="percentage"></span>%. Where do you think it is?
</h1>
<h1 class="results"></h1>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="results.js"></script>
<script>
// COORDINATES are LONG, LAT
var guessDistances = [];
var width = 960,
height = 1160;
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([50, 60])
.scale(1200 * 5)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection)
.pointRadius(2);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("uk.json", function(error, uk) {
var subunits = topojson.feature(uk, uk.objects.subunits),
places = topojson.feature(uk, uk.objects.places);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id; })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a !== b && a.id !== "IRL"; }))
.attr("d", path)
.attr("class", "subunit-boundary");
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a === b && a.id === "IRL"; }))
.attr("d", path)
.attr("class", "subunit-boundary IRL");
svg.selectAll(".subunit-label")
.data(subunits.features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });
svg.append("path")
.datum(places)
.attr("d", path)
.attr("class", "place");
svg.selectAll(".place-label")
.data(places.features)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.geometry.coordinates) + ")"; })
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start" : "end"; })
.text(function(d) { return d.properties.name; });
promptPlace(results.pop());
function promptPlace(place) {
if(place===undefined) {
finish();
return;
}
// reset
d3.selectAll('circle').remove();
d3.select('button').remove();
d3.select('.results').text('');
// fill in prompt
d3.select('.name').text(place.name);
d3.select('.result').text(place.vote > 50 ? 'remain' : 'leave');
d3.select('.percentage').text(place.vote > 50 ? place.vote : 100-place.vote);
d3.select('svg').on('click', function() {
var performance = d3.scale.threshold()
.domain([20,80,150,300])
.range(['Amazing!!!', 'Very good!', 'OK!', 'Mediocre.', 'You have no clue.'])
var guess = projection.invert(d3.mouse(this));
var dist = distance(place.coordinates, guess);
guessDistances.push(dist);
d3.select('.results').text('You were ' + Math.round(dist) + ' “kilometres” away. ' + performance(dist));
svg.append('circle')
.classed('guess', true)
.attr('cx', d3.mouse(this)[0])
.attr('cy', d3.mouse(this)[1])
.attr('r', 1e-6)
.transition()
.delay(0)
.duration(250)
.attr('r', 30);
svg.append('circle')
.classed('answer', true)
.attr('cx', projection(place.coordinates)[0])
.attr('cy', projection(place.coordinates)[1])
.attr('r', 1e-6)
.transition()
.delay(250)
.duration(250)
.attr('r', 30);
// next
d3.select('svg').on('click', null);
setTimeout(function() {
d3.select('body').append('button').text('Next!')
.on('click', function() {
promptPlace(results.pop());
})
}, 1000);
})
}
function finish() {
var performance = d3.scale.threshold()
.domain([20,80,150,300])
.range([
'You are practically Executive Director Dr. John Ludden of the British Geological Survey!',
'You must be British!',
'You are a decent human being!',
'You are not good at this but we support your existence anyway!',
'You are very very bad at this unimportant game.'
]);
d3.select('body').append('div')
.classed('finish', true)
.append('h1')
.text('THE END! On average you were off by ' + Math.round(d3.mean(guessDistances)) + ' km, with a st.dev. of ' + Math.round(d3.deviation(guessDistances)) + ' km. ' + performance(d3.mean(guessDistances)));
}
});
// from https://www.geodatasource.com/developers/javascript
function distance(from, to, unit) {
if(unit===undefined) unit = 'K';
var lat1 = from[1];
var lon1 = from[0];
var lat2 = to[1];
var lon2 = to[0];
var radlat1 = Math.PI * lat1/180
var radlat2 = Math.PI * lat2/180
var theta = lon1-lon2
var radtheta = Math.PI * theta/180
var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
dist = Math.acos(dist)
dist = dist * 180/Math.PI
dist = dist * 60 * 1.1515
if (unit=="K") { dist = dist * 1.609344 }
if (unit=="N") { dist = dist * 0.8684 }
return dist
}
</script>
var results = [
{
'name': 'Clackmannanshire',
'coordinates': [-3.75, 56.166667],
'vote': 57.78
},
{
'name': 'Sunderland',
'coordinates': [-1.385, 54.91],
'vote': 38.66
},
{
'name': 'Isles of Scilly',
'coordinates': [-6.322778, 49.936111],
'vote': 56.39
},
{
'name': 'Broxbourne',
'coordinates': [-0.0216, 51.7495],
'vote': 33.74
},
{
'name': 'Swindon',
'coordinates': [-1.78, 51.56],
'vote': 45.34
},
{
'name': 'Kettering',
'coordinates': [-0.72292, 52.39312],
'vote': 39.01
},
{
'name': 'Shetland Islands',
'coordinates': [-1.216667, 60.35],
'vote': 56.51
},
{
'name': 'South Tyneside',
'coordinates': [-1.438, 54.959],
'vote': 37.95
},
{
'name': 'West Dunbartonshire',
'coordinates': [-4.515, 55.99],
'vote': 61.99
},
{
'name': 'Dundee',
'coordinates': [-2.97, 56.464],
'vote': 59.78
},
{
'name': 'Cannock Chase',
'coordinates': [-2.001, 52.746],
'vote': 31.14
},
{
'name': 'Coventry',
'coordinates': [-1.510556, 52.408056],
'vote': 44.4
},
{
'name': 'Rochdale',
'coordinates': [-2.161, 53.6136],
'vote': 39.93
},
{
'name': 'Erewash',
'coordinates': [-1.316667, 52.916667],
'vote': 38.77
},
{
'name': 'South Ribble',
'coordinates': [-2.69, 53.697],
'vote': 41.44
},
{
'name': 'South Somerset',
'coordinates': [-2.9893344, 50.9844058],
'vote': 42.75
},
{
'name': 'Haringey',
'coordinates': [-0.112915, 51.601632],
'vote': 75.57
},
{
'name': 'Oadby & Wigston',
'coordinates': [-1.095, 52.592],
'vote': 45.42
},
{
'name': 'Carlisle',
'coordinates': [-2.937, 54.879],
'vote': 39.86
},
{
'name': 'Peterborough',
'coordinates': [-0.25, 52.583333],
'vote': 39.11
},
{
'name': 'South Lakeland',
'coordinates': [-2.88, 54.312],
'vote': 52.86
},
{
'name': 'Reigate & Banstead',
'coordinates': [-0.16, 51.249],
'vote': 49.51
},
{
'name': 'Hastings',
'coordinates': [0.572875, 50.856302],
'vote': 45.12
},
{
'name': 'Powys',
'coordinates': [-3.416667, 52.3],
'vote': 46.26
},
{
'name': 'Ipswich',
'coordinates': [1.155556, 52.059444],
'vote': 41.74
},
{
'name': 'Chelmsford',
'coordinates': [0.4798, 51.7361],
'vote': 47.17
},
{
'name': 'Doncaster',
'coordinates': [-1.133, 53.515],
'vote': 31.04
},
{
'name': 'Vale of White Horse',
'coordinates': [-1.5, 51.6],
'vote': 56.7
},
{
'name': 'Reading',
'coordinates': [-0.973056, 51.454167],
'vote': 58.03
},
{
'name': 'Ryedale',
'coordinates': [-0.79, 54.139],
'vote': 44.74
},
];
d3.shuffle(results);
results = results.slice(0,10);
/*
{
'name': '',
'coordinates': [0,0],
'vote': 50 //remain
},
*/