|  | <!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> |