|
<!DOCTYPE html> |
|
|
|
<html> |
|
<meta charset="utf-8"> |
|
<head> |
|
<script src="http://d3js.org/d3.v4.min.js"></script> |
|
<script src="d3-tip.js"></script> |
|
<script src="d3-grid.js"></script> |
|
|
|
<style type="text/css"> |
|
body { |
|
font: 11px sans-serif; |
|
margin: 10px; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.bar:hover { |
|
fill: #bcbcbc ; |
|
} |
|
|
|
.x.axis path { |
|
display: none; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
Sort by |
|
<a href="#" class="sort-btn" data-sort="Cons">Conservative</a> / |
|
<a href="#" class="sort-btn" data-sort="Lab">Labour</a> |
|
<!-- / <a href="#" class="sort-btn" data-sort="LD">Lib Dems</a> / |
|
<a href="#" class="sort-btn" data-sort="Gre">Greens</a> / |
|
<a href="#" class="sort-btn" data-sort="PC">Plaid Cymru</a> / |
|
<a href="#" class="sort-btn" data-sort="SNP">SNP</a> / |
|
<a href="#" class="sort-btn" data-sort="UKIP">UKIP</a> --> |
|
|
|
<div id="vis"></div> |
|
<script type="text/javascript"> |
|
|
|
var innerPadding = 30; |
|
|
|
var formatPercent = d3.format(".0%"); |
|
|
|
var dataUrl = "https://raw.githubusercontent.com/ft-interactive/ge2017-dataset/master/financialTimes_ge2017_dataset.csv"; |
|
|
|
var partyColours = { |
|
"Cons": "#0087dc", |
|
"Gre": "#008066", |
|
"Lab": "#d50000", |
|
"LD": "#FDBB30", |
|
"PC": "#3F8428", |
|
"SNP": "#FFF95D", |
|
"UKIP": "#B3009D" |
|
}; |
|
|
|
var rectGrid = d3.grid() |
|
.bands() |
|
.size([1000, 1000]); |
|
|
|
var x = d3.scaleBand() |
|
.range([0, rectGrid.nodeSize()[0]]) |
|
.round(0.1); |
|
|
|
// Scales. Note the inverted domain fo y-scale: bigger is up! |
|
var y = d3.scaleLinear() |
|
.range([rectGrid.nodeSize()[1] - innerPadding, 0]); |
|
|
|
var xAxis = d3.axisBottom() |
|
.scale(x); |
|
|
|
var yAxis = d3.axisLeft() |
|
.scale(y); |
|
|
|
var tip = d3.tip() |
|
.attr('class', 'd3-tip') |
|
.offset([-1, 0]) |
|
.html(d => Math.round(d.value.percent) + "%" ); |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", 1200) |
|
.attr("height", 1200) |
|
.append("g") |
|
.attr("transform", "translate(70, 70)") |
|
.attr("class", "charts"); |
|
|
|
var nestedGeData = []; |
|
|
|
d3.selectAll(".sort-btn") |
|
.on("click", function(d) { |
|
d3.event.preventDefault(); |
|
updatedData = sortByParty(nestedGeData, this.dataset.sort); |
|
update(updatedData); |
|
}); |
|
|
|
d3.csv(dataUrl, function(geData) { |
|
|
|
var parties = [ |
|
"Cons", |
|
"Gre", |
|
"Lab", |
|
"LD", |
|
"PC", |
|
"SNP", |
|
"UKIP" |
|
] |
|
|
|
geData.forEach(function(g) { |
|
var key = g.PCON15CD; |
|
var seat = g.seat; |
|
|
|
var values = []; |
|
if (g.con_PC_2017 !== "NA") { |
|
values.push({ |
|
"party": "Cons", |
|
"percent": +g.con_PC_2017 |
|
}); |
|
} |
|
if (g.green_PC_2017 !== "NA") { |
|
values.push({ |
|
"party": "Gre", |
|
"percent": +g.green_PC_2017 |
|
}); |
|
} |
|
if (g.lab_PC_2017 !== "NA") { |
|
values.push({ |
|
"party": "Lab", |
|
"percent": +g.lab_PC_2017 |
|
}); |
|
} |
|
if (g.ld_PC_2017 !== "NA") { |
|
values.push({ |
|
"party": "LD", |
|
"percent": +g.ld_PC_2017 |
|
}); |
|
} |
|
if (g.pc_PC_2017 !== "NA") { |
|
values.push({ |
|
"party": "PC", |
|
"percent": +g.pc_PC_2017 |
|
}); |
|
} |
|
if (g.snp_PC_2017 !== "NA") { |
|
values.push({ |
|
"party": "SNP", |
|
"percent": +g.snp_PC_2017 |
|
}); |
|
} |
|
if (g.ukip_PC_2017 !== "NA") { |
|
values.push({ |
|
"party": "UKIP", |
|
"percent": +g.ukip_PC_2017 |
|
}); |
|
} |
|
|
|
nestedGeData.push({ |
|
"key": key, |
|
"seat": seat, |
|
"values": values |
|
}) |
|
}); |
|
|
|
x.domain(parties); |
|
|
|
slicedData = nestedGeData.slice(0, 100); |
|
update(slicedData); |
|
|
|
svg.call(tip); |
|
|
|
}); |
|
|
|
function multipleEnter(constituency) { |
|
var barChart = d3.select(this); |
|
|
|
var x = d3.scaleBand(); |
|
|
|
var sortedValues = constituency.values.sort((a, b) => |
|
b.percent - a.percent |
|
); |
|
|
|
x.domain(sortedValues.map(d => d.party)); |
|
|
|
x.range([0, rectGrid.nodeSize()[0] - (innerPadding / 2)]) |
|
.round(0.1); |
|
|
|
var xAxis = d3.axisBottom() |
|
.scale(x) |
|
.tickFormat(d => d.slice(0, 2)); |
|
|
|
y = d3.scaleLinear() |
|
.range([rectGrid.nodeSize()[1] - innerPadding, 0]) |
|
.domain([0, 100]); |
|
|
|
var yAxis = d3.axisLeft() |
|
.scale(y) |
|
.tickFormat(formatPercent); |
|
|
|
barChart |
|
.append("g") |
|
.attr("class", "x axis") |
|
.call(xAxis) |
|
.transition().duration(500) |
|
.delay((d, i) => i * 20) |
|
.attr("transform", function(d) { |
|
return "translate(" + d.x + "," + (d.y + rectGrid.nodeSize()[1] - innerPadding) + ")"; |
|
}) |
|
|
|
barChart.selectAll(".bar") |
|
.data(d => d.values.map(function(a) { |
|
return { |
|
properties: { |
|
key: d.key, |
|
seat: d.seat, |
|
x: d.x, |
|
y: d.y |
|
}, |
|
value: a |
|
} |
|
})) |
|
.enter() |
|
.append("rect") |
|
.attr("class", "bar") |
|
.attr("x", d => x(d.value.party)) |
|
.attr("width", x.bandwidth()) |
|
.attr("y", d => y(d.value.percent)) |
|
.attr("height", d => rectGrid.nodeSize()[1] - y(d.value.percent) - innerPadding) |
|
.attr("fill", d => partyColours[d.value.party]) |
|
.on('mouseover', tip.show) |
|
.on('mouseout', tip.hide) |
|
.transition().duration(500) |
|
.delay((d, i) => i * 20) |
|
.attr("transform", function(d) { |
|
return "translate(" + d.properties.x + "," + d.properties.y + ")"; |
|
}) |
|
|
|
barChart |
|
.append("text") |
|
.attr("class", "label") |
|
.attr("dy", "-1em") |
|
.attr("text-anchor", "start") |
|
.attr("font-size", "1em") |
|
.text(d => { |
|
if (d.seat.length < 12) { |
|
return d.seat; |
|
} else { |
|
return d.seat.slice(0, 12) + "..."; |
|
} |
|
}) |
|
.transition().duration(500) |
|
.delay((d, i) => i * 20) |
|
.attr("transform", function(d) { |
|
return "translate(" + d.x + "," + (d.y + (innerPadding / 2)) + ")"; |
|
}) |
|
} |
|
|
|
function multipleUpdate(constituency) { |
|
// some duplication here that should be removed |
|
var barChart = d3.select(this); |
|
|
|
var x = d3.scaleBand(); |
|
|
|
var sortedValues = constituency.values.sort((a, b) => |
|
b.percent - a.percent |
|
); |
|
|
|
x.domain(sortedValues.map(d => d.party)); |
|
|
|
x.range([0, rectGrid.nodeSize()[0] - (innerPadding / 2)]) |
|
.round(0.1); |
|
|
|
var xAxis = d3.axisBottom() |
|
.scale(x) |
|
.tickFormat(d => d.slice(0, 2)); |
|
|
|
y = d3.scaleLinear() |
|
.range([rectGrid.nodeSize()[1] - innerPadding, 0]) |
|
.domain([0, 100]); |
|
|
|
var yAxis = d3.axisLeft() |
|
.scale(y) |
|
.tickFormat(formatPercent); |
|
|
|
barChart.select("g.x.axis") |
|
.transition().duration(500) |
|
// .delay((d, i) => i * 20) |
|
.attr("transform", function(d) { |
|
return "translate(" + d.x + "," + (d.y + rectGrid.nodeSize()[1] - innerPadding) + ")"; |
|
}) |
|
|
|
barChart.selectAll(".bar") |
|
.data(d => d.values.map(function(a) { |
|
return { |
|
properties: { |
|
key: d.key, |
|
seat: d.seat, |
|
x: d.x, |
|
y: d.y |
|
}, |
|
value: a |
|
} |
|
})) |
|
.transition().duration(500) |
|
.delay((d, i) => i * 20) |
|
.attr("transform", function(d) { |
|
return "translate(" + d.properties.x + "," + d.properties.y + ")"; |
|
}); |
|
|
|
barChart.select("text.label") |
|
.transition().duration(500) |
|
.delay((d, i) => i * 20) |
|
.attr("transform", function(d) { |
|
return "translate(" + d.x + "," + d.y + ")"; |
|
}); |
|
} |
|
|
|
function sortByParty(nestedGeData, partyName) { |
|
|
|
var sortedDataSet = nestedGeData |
|
.slice(0, 100); |
|
|
|
// sortedDataSet = sortedDataSet.filter((a) => { |
|
// for (var i in a.values) { |
|
// if (a.values[i].party === partyName) { |
|
// return true; |
|
// } |
|
// } |
|
// return false; |
|
// }); |
|
|
|
sortedDataSet = sortedDataSet.sort((a, b) => { |
|
var aPc; |
|
var bPc; |
|
for (var i in a.values) { |
|
if (a.values[i].party === partyName) { |
|
aPc = a.values[i].percent; |
|
} |
|
} |
|
for (var i in b.values) { |
|
if (b.values[i].party === partyName) { |
|
bPc = b.values[i].percent; |
|
} |
|
} |
|
return bPc - aPc; |
|
}); |
|
|
|
return sortedDataSet; |
|
|
|
} |
|
|
|
function update(data) { |
|
var update = svg.selectAll("g") |
|
.data(rectGrid(data), d => d.key); |
|
|
|
var enter = update.enter() |
|
.append("g") |
|
.attr("class", "chart"); |
|
|
|
var exit = update.exit(); |
|
|
|
// follow pattern https://bl.ocks.org/cmgiven/32d4c53f19aea6e528faf10bfe4f3da9 |
|
// console.log("Update: " + update.size()); |
|
update.each(multipleUpdate); |
|
|
|
// console.log("Enter: " + enter.size()); |
|
enter.each(multipleEnter); |
|
|
|
// console.log("Exit: " + exit.size()); |
|
// exit.remove(); |
|
|
|
update.merge(enter); |
|
|
|
|
|
} |
|
|
|
</script> |
|
</body> |
|
</html> |