|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<link href="https://fonts.googleapis.com/css?family=Nunito:400,800" rel="stylesheet"> |
|
<style> |
|
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } |
|
|
|
text { |
|
font-family: 'Nunito', sans-serif; |
|
font-weight: 800; |
|
font-size: 16px; |
|
text-transform: capitalize; |
|
} |
|
|
|
.women { |
|
fill: black; |
|
} |
|
|
|
.women-uncertain, .women-uncertain-legend { |
|
fill: red; |
|
} |
|
|
|
.women-uncertain-text { |
|
fill: grey; |
|
} |
|
|
|
.men { |
|
fill: black; |
|
opacity: 0.3; |
|
} |
|
|
|
.title { |
|
font-size: 18px; |
|
} |
|
|
|
</style> |
|
</head> |
|
|
|
<body> |
|
<script> |
|
var margin = {top: 100, right: 0, bottom: 50, left: 50}; |
|
|
|
var width = 960 - margin.left - margin.right, |
|
height = 650 - margin.top - margin.bottom; |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
var dataURL = "https://raw.githubusercontent.com/tlfrd/pay-ratios/master/data/over140k.json"; |
|
|
|
var config = { |
|
radius: 4, |
|
padding: 5, |
|
top: 25, |
|
left: 0, |
|
iconWidth: 8, |
|
largeIconWidth: 64, |
|
legendOffset: 100, |
|
gridWidth: 7 |
|
}; |
|
|
|
var dataArray; |
|
|
|
d3.json(dataURL, function(error, payData) { |
|
if (error) throw error; |
|
|
|
payData = payData.number_over_140k.filter(d => d.women != "-") |
|
|
|
dataArray = []; |
|
payData.forEach(d => { |
|
t = d.name !== "City" ? "women" : "women-uncertain" |
|
dataArray.push({ |
|
name: d.name, |
|
pay: [{type: t, value: d.women}, {type: "men", value: d.over - d.women}] |
|
}) |
|
}); |
|
|
|
dataArray.sort((a, b) => a.pay[1].value > b.pay[1].value ? 1 : -1) |
|
|
|
var gridGroups = svg.append("g") |
|
.attr("class", "grids"); |
|
|
|
var payGrids = gridGroups.selectAll("g") |
|
.data(dataArray) |
|
.enter().append("g") |
|
.attr("class", "pay-grid") |
|
.attr("transform", (d, i) => |
|
"translate(" + |
|
[config.left + ((config.iconWidth * config.radius * 4) * (i % config.gridWidth)), |
|
config.top + findMaxHeightOfPreviousRow(dataArray, i, config.iconWidth) * (config.radius * 20)] + ")") |
|
|
|
payGrids.each(function(d) { |
|
var selection = d3.select(this); |
|
drawGridplot(selection, config, d.pay, config.iconWidth); |
|
}); |
|
|
|
payGrids.append("text") |
|
.attr("class", "label") |
|
.attr("x", -config.radius) |
|
.attr("y", -config.radius * 4) |
|
.text(d => d.name); |
|
|
|
var legend = svg.append("g") |
|
.attr("class", "legend") |
|
.attr("transform", "translate(" + [0, height - margin.bottom / 2] + ")"); |
|
|
|
var title = svg.append("text") |
|
.attr("class", "title") |
|
.attr("text-anchor", "start") |
|
.attr("y", -margin.top / 2) |
|
.attr("x", -config.radius) |
|
.text("The Number of Individuals Earning Over £140,000 at London's Universities"); |
|
|
|
var legendIcons = legend.selectAll("g") |
|
.data(dataArray[0].pay) |
|
.enter().append("g") |
|
.attr("class", d => d.type) |
|
.attr("transform", (d, i) => "translate(" + [(i) * config.legendOffset, 0] + ")"); |
|
|
|
legendIcons.append("circle") |
|
.attr("r", config.radius); |
|
|
|
legendIcons.append("text") |
|
.attr("x", config.radius * 3) |
|
.attr("y", config.radius) |
|
.text(d => d.type) |
|
|
|
legend.append("circle") |
|
.attr("class", "women-uncertain-legend") |
|
.attr("cy", 30) |
|
.attr("r", config.radius) |
|
|
|
legend.append("text") |
|
.attr("class", "women-uncertain-text") |
|
.attr("x", config.radius * 3) |
|
.attr("y", 30 + config.radius) |
|
.text("Range Provided Only"); |
|
|
|
var allLabel = svg.append("g") |
|
.attr("class", "all-label") |
|
.append("text") |
|
.attr("transform", "translate(" + [-config.radius, config.radius * 2] + ")") |
|
.text("All") |
|
.style("opacity", 0); |
|
}); |
|
|
|
d3.select("svg").on("click", animate); |
|
|
|
var separate = true; |
|
|
|
function animate() { |
|
var labels = svg.selectAll(".label"); |
|
var men = svg.selectAll(".men"); |
|
var women = svg.selectAll(".women"); |
|
var womenUn = svg.selectAll(".women-uncertain"); |
|
var payGrids = svg.selectAll(".pay-grid"); |
|
|
|
var womenUnOffset = women.size() - 1, |
|
menOffset = womenUnOffset + womenUn.size() - 1; |
|
|
|
if (separate) { |
|
|
|
labels.transition() |
|
.style("opacity", 0); |
|
|
|
payGrids.transition().duration(1000).attr("transform", "translate(" + [0, config.top] + ")"); |
|
|
|
women.transition() |
|
.duration(1000) |
|
.attr("cx", (d, i) => (i % config.largeIconWidth) * (config.radius * 2 + config.padding)) |
|
.attr("cy", (d, i) => Math.floor(i / config.largeIconWidth) * (config.radius * 2 + config.padding)); |
|
|
|
womenUn.transition() |
|
.duration(1000) |
|
.attr("cx", (d, i) => ((womenUnOffset + i) % config.largeIconWidth) * (config.radius * 2 + config.padding)) |
|
.attr("cy", (d, i) => Math.floor((womenUnOffset + i) / config.largeIconWidth) * (config.radius * 2 + config.padding)); |
|
|
|
men.transition() |
|
.duration(1000) |
|
.attr("cx", (d, i) => ((menOffset + i) % config.largeIconWidth) * (config.radius * 2 + config.padding)) |
|
.attr("cy", (d, i) => Math.floor((menOffset + i) / config.largeIconWidth) * (config.radius * 2 + config.padding)); |
|
|
|
svg.transition() |
|
.delay(500) |
|
.duration(500) |
|
.select(".all-label text") |
|
.style("opacity", 1); |
|
|
|
} else { |
|
payGrids.transition() |
|
.duration(1000) |
|
.attr("transform", (d, i) => |
|
"translate(" + |
|
[config.left + ((config.iconWidth * config.radius * 4) * (i % config.gridWidth)), |
|
config.top + findMaxHeightOfPreviousRow(dataArray, i, config.iconWidth) * (config.radius * 20)] + ")"); |
|
|
|
women.transition() |
|
.duration(1000) |
|
.attr("cx", d => d.x * (config.radius * 2 + config.padding)) |
|
.attr("cy", d => d.y * (config.radius * 2 + config.padding)); |
|
|
|
womenUn.transition() |
|
.duration(1000) |
|
.attr("cx", d => d.x * (config.radius * 2 + config.padding)) |
|
.attr("cy", d => d.y * (config.radius * 2 + config.padding)); |
|
|
|
men.transition() |
|
.duration(1000) |
|
.attr("cx", d => d.x * (config.radius * 2 + config.padding)) |
|
.attr("cy", d => d.y * (config.radius * 2 + config.padding)); |
|
|
|
labels.transition() |
|
.duration(1000) |
|
.style("opacity", 1); |
|
|
|
svg.transition() |
|
.select(".all-label text") |
|
.style("opacity", 0); |
|
|
|
} |
|
|
|
separate = !separate; |
|
|
|
} |
|
|
|
function findMaxHeightOfPreviousRow(array, currentIndex) { |
|
var row = Math.floor(currentIndex / config.gridWidth) - 1; |
|
var start = config.gridWidth * row, |
|
end = config.gridWidth * (row + 1); |
|
var highest = 0; |
|
if (row >= 0) { |
|
for (var i = start; i < end; i++) { |
|
var total = array[i].pay[0].value + array[i].pay[1].value; |
|
if (total > highest) { |
|
highest = total; |
|
} |
|
} |
|
} |
|
var heightOfHeighest = Math.ceil(highest / config.iconWidth); |
|
return heightOfHeighest; |
|
} |
|
|
|
|
|
function gridLayout(array, length) { |
|
array.forEach(function(d, i) { |
|
d.x = i % length; |
|
d.y = Math.floor(i / length); |
|
}) |
|
} |
|
|
|
// Will draw a gridplot given an array of classes and values |
|
function drawGridplot(elem, cfg, data, l) { |
|
var total = d3.sum(data, d => d.value), |
|
dataArray = new Array(total); |
|
|
|
data.forEach(function(d, i) { |
|
var previous = 0; |
|
if (i > 0) previous = data[i - 1].value; |
|
|
|
for (var w = previous; w < (d.value + previous); w++) { |
|
dataArray[w] = {type: d.type}; |
|
}; |
|
}); |
|
|
|
gridLayout(dataArray, l); |
|
|
|
var dotElems = elem.append("g") |
|
.selectAll("circle") |
|
.data(dataArray) |
|
.enter().append("circle") |
|
.attr("class", d => d.type) |
|
.attr("cx", d => d.x * (config.radius * 2 + config.padding)) |
|
.attr("cy", d => d.y * (config.radius * 2 + config.padding)) |
|
.attr("r", cfg.radius); |
|
} |
|
</script> |
|
</body> |