Skip to content

Instantly share code, notes, and snippets.

@tlfrd
Last active September 13, 2019 10:43
Show Gist options
  • Save tlfrd/c63f9e449ff9e98db97e711889054018 to your computer and use it in GitHub Desktop.
Save tlfrd/c63f9e449ff9e98db97e711889054018 to your computer and use it in GitHub Desktop.
Gridplot
license: mit
height: 650

An example of a Gridplot. Visualising what proportion of those who earn over 140k at London universities are women.

This is part of a series of visualisations called My Visual Vocabulary which aims to recreate every visualisation in the FT's Visual Vocabulary from scratch using D3.

Future ideas:

  • Transition to force layout
  • Transition from force layout to pie chart
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment