This block is my attempt to vizualize data provided by Makeover monday, 2017, week 29. A second iteration is avaialble.
I use a beeswarm arrangement (cf. d3-beeswarm plugin) for several reasons. Firstly, because there is no overlapping, it allows the user to see each individual data, and hover a particular data to access to its details. Secondly, this kind of arrangement suits very well distributions. In this example, it highlights the differences between the two administrations, and shows that Trump's salaries seems to use a salary grid, as opposed to Obama's ones.
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>MakeOver Monday - White House Salaries</title>
<meta name="description" content="MakeOver Monday, Obama's administration VS Trump's administration">
<script src="//d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="https://raw.githack.com/kcnarf/d3-beeswarm/master/build/d3-beeswarm.js"></script>
<style>
svg {
background-color: rgb(250,250,250);
}
text {
fill: grey;
}
text.tiny {
font-size: 10pt;
}
text.light {
fill: lightgrey
}
.text-background {
fill: rgb(250,250,250);
stroke: rgb(250,250,250);
stroke-width: 3px;
}
line {
stroke: lightgrey;
}
#header #title {
letter-spacing: 15pt;
font-weight: 200;
}
#header .name{
font-size: 30pt;
}
#header .label{
fill: lightgrey;
font-size: 10pt;
}
#axis-container text {
font-size: 8pt;
}
.Obama {
stroke: blue;
}
.Trump {
stroke: red;
}
circle.Obama {
fill: lightBlue;
}
circle.Trump {
fill: pink;
}
.slope {
stroke: grey;
}
div.tooltip {
position: absolute;
text-align: center;
width: 60px;
height: 14px;
padding: 2px;
font: 12px sans-serif;
background: rgb(250,250,250);
color: grey;
border: 1px solid grey;
border-radius: 3px;
pointer-events: none;
}
</style>
</head>
<body>
<svg></svg>
<script>
const WITH_TRANSITION = true;
const WITHOUT_TRANSITION = false;
const duration = 250;
//begin: raw data global def
var obamaSalaries = [],
trumpSalaries = [];
var obamaStaffCount = 0,
trumpStaffCount = 0;
var obamaTotalSalaries = 0,
trumpTotalSalaries = 0;
var salaryExtent, obamaSalariesExtent, trumpSalariesExtent;
var obamaMeanSalary, trumpMeanSalary;
var obamaMedianSalary, trumpMedianSalary;
//end: raw data global def
//begin: data-related utils
function salaryAccessor (d){ return d.salary;};
//end: data-related utils
//begin: layout conf.
var svgWidth = 960,
svgHeight = 500,
margin = {top: 10, right: 10, bottom: 10, left: 10},
halfSpaceBetween = {header: 15, circle: 15, slope: 5},
titleY = 10,
nameY = 50,
staffY = nameY+30,
totalY = staffY+15,
tillHeader = 15,
headerHeight = totalY + tillHeader + 5,
footerHeight = 30,
width = svgWidth - margin.left - margin.right,
height = svgHeight - headerHeight - footerHeight - margin.top - margin.bottom,
halfWidth = width/2,
halfHeight = height/2,
quarterWidth = width/4,
quarterHeight = height/4;
//end: layout conf.
//begin: beeswarm conf.
var beeRadius = 3;
var salaryScale = d3.scaleLinear().domain([0,width]);
var salariesArrangement, obamaArrangement, trumpArrangement;
//end: beeswarm conf.
//begin: reusable d3Selection
var svg, drawingArea, header, footer, axisContainer, avgContainer, medianContainer, circleContainer, tooltip;
//end: reusable d3Selection
d3.csv("whiteHouseSalaries.csv", csvParser, function(error, data) {
if (error) throw error;
obamaSalariesExtent = d3.extent(obamaSalaries, salaryAccessor);
trumpSalariesExtent = d3.extent(trumpSalaries, salaryAccessor);
salariesExtent = [Math.min(obamaSalariesExtent[0], trumpSalariesExtent[0]),
Math.max(obamaSalariesExtent[1], trumpSalariesExtent[1])];
obamaMeanSalary = Math.round(obamaTotalSalaries/obamaSalaries.length);
trumpMeanSalary = Math.round(trumpTotalSalaries/trumpSalaries.length);
obamaMedianSalary = d3.median(obamaSalaries, salaryAccessor);
trumpMedianSalary = d3.median(trumpSalaries, salaryAccessor);
salaryScale.domain(salariesExtent).range([height,0]);
/*
console.info("Obama");
console.info(" count: "+obamaStaffCount);
console.info(" total: "+obamaTotalSalaries);
console.info(" mean : "+obamaMeanSalary);
console.info(" median : "+obamaMedianSalary);
console.info("Trump");
console.info(" count: "+trumpStaffCount);
console.info(" total: "+trumpTotalSalaries);
console.info(" mean : "+trumpMeanSalary);
console.info(" median : "+trumpMedianSalary);
*/
initLayout();
drawBeeswarm();
drawMeans();
drawMedians();
});
​
function csvParser(d) {
d.administration = d.administration;
d.name = d.name;
d.salary = +d.salary;
d.positionTitle = d.positionTitle;
if (d.administration === "Obama") {
obamaSalaries.push(d);
obamaTotalSalaries += d.salary;
obamaStaffCount++;
} else {
trumpSalaries.push(d);
trumpTotalSalaries += d.salary;
trumpStaffCount++;
}
return d;
};
function initLayout() {svg = d3.select("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
drawingArea = svg.append("g")
.classed("drawingArea", true)
.attr("transform", "translate("+[margin.left+halfWidth,margin.top]+")");
header = drawingArea.append("g").attr("id", "header");
drawHeader();
footer = drawingArea.append("g")
.attr("id", "footer")
.attr("transform", "translate("+[0,headerHeight+height+footerHeight]+")");
drawFooter();
var graphContainer = drawingArea.append("g")
.attr("transform", "translate("+[0,headerHeight]+")")
axisContainer = graphContainer.append("g").attr("id", "axis-container");
drawAxis()
avgContainer = graphContainer.append("g").attr("id","average-container");
medianContainer = graphContainer.append("g").attr("id","median-container");
circleContainer = graphContainer.append("g").attr("id","circle-container");
tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
}
function drawHeader() {
var labelMarginX = 5;
header.append("text")
.attr("id", "title")
.attr("transform", "translate("+[0, titleY]+")")
.attr("text-anchor", "middle")
.text("White House Salaries")
var names = header.append("g")
.attr("transform", "translate("+[0,nameY]+")")
var obama = names.append("text")
.classed("name", true)
.attr("transform", "translate("+[-halfSpaceBetween.header, 0]+")")
.attr("text-anchor", "end")
.text("Obama");
names.append("text")
.attr("text-anchor", "middle")
.text("vs");
var trump = names.append("text")
.classed("name", true)
.attr("transform", "translate("+[halfSpaceBetween.header, 0]+")")
.attr("text-anchor", "start")
.text("Trump");
names.append("text")
.classed("date tiny", true)
.attr("transform", "translate("+[-(halfSpaceBetween.header+obama.node().getBBox().width+labelMarginX), 0]+")")
.attr("text-anchor", "end")
.text("(2016)");
names.append("text")
.classed("date tiny", true)
.attr("transform", "translate("+[halfSpaceBetween.header + trump.node().getBBox().width+labelMarginX, 0]+")")
.attr("text-anchor", "start")
.text("(2017)");
var staffCounts = header.append("g")
.attr("transform", "translate("+[0,staffY]+")")
var oStaffCount = staffCounts.append("text")
.classed("staff-count", true)
.attr("transform", "translate("+[-halfSpaceBetween.header, 0]+")")
.attr("text-anchor", "end")
.text(obamaStaffCount);
staffCounts.append("text")
.attr("text-anchor", "middle")
.text(">");
var tStaffCount = staffCounts.append("text")
.classed("staff-count", true)
.attr("transform", "translate("+[halfSpaceBetween.header, 0]+")")
.attr("text-anchor", "start")
.text(trumpStaffCount);
staffCounts.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[-(halfSpaceBetween.header+oStaffCount.node().getBBox().width+labelMarginX), 0]+")")
.attr("text-anchor", "end")
.text("staff count ------");
staffCounts.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[halfSpaceBetween.header + tStaffCount.node().getBBox().width+labelMarginX, 0]+")")
.attr("text-anchor", "start")
.text("------ staff count");
var totals = header.append("g")
.attr("transform", "translate("+[0,totalY]+")")
var oTotal = totals.append("text")
.classed("total", true)
.attr("transform", "translate("+[-halfSpaceBetween.header, 0]+")")
.attr("text-anchor", "end")
.text(d3.format("$,")(obamaTotalSalaries));
totals.append("text")
.attr("text-anchor", "middle")
.text(">");
var tTotal = totals.append("text")
.classed("total", true)
.attr("transform", "translate("+[halfSpaceBetween.header, 0]+")")
.attr("text-anchor", "start")
.text(d3.format("$,")(trumpTotalSalaries));
totals.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[-(halfSpaceBetween.header+oTotal.node().getBBox().width+labelMarginX), 0]+")")
.attr("text-anchor", "end")
.text("total in 2016 ------");
totals.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[halfSpaceBetween.header + tTotal.node().getBBox().width+labelMarginX, 0]+")")
.attr("text-anchor", "start")
.text("------ total in 2017");
}
function drawAxis() {
var lineHalfWidth = halfWidth-halfWidth/4,
labelMargin = 5;
var ticks = axisContainer.selectAll(".tick")
.data(d3.range(salariesExtent[0], salariesExtent[1], 20000))
.enter()
.append("g")
.classed("tick", true)
.attr("transform", (d)=>{ return "translate("+[0, salaryScale(+d)]+")"; })
ticks.append("line")
.attr("x1", -lineHalfWidth)
.attr("y1", 0)
.attr("x2", lineHalfWidth)
.attr("y2", 0);
ticks.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[lineHalfWidth+labelMargin,3]+")")
.attr("text-anchor", "start")
.text((d)=>{ return (d===0)? 0 : d/1000+"k"; });
ticks.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[-(lineHalfWidth+labelMargin),3]+")")
.attr("text-anchor", "end")
.text((d)=>{ return (d===0)? 0 : d/1000+"k"; });
axisContainer.append("text")
.classed("unit light", true)
.attr("transform", "translate("+[-(lineHalfWidth+labelMargin), 0]+")")
.attr("text-anchor", "end")
.text("($ per year)")
axisContainer.append("text")
.classed("unit light", true)
.attr("transform", "translate("+[(lineHalfWidth+labelMargin), 0]+")")
.attr("text-anchor", "start")
.text("($ per year)")
}
function drawFooter() {
footer.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[-halfWidth, 0]+")")
.attr("text-anchor", "start")
.text("Makeover Monday, 2017, week 29")
footer.append("text")
.classed("tiny light", true)
.attr("text-anchor", "middle")
.text("available at bl.ocks.org/Kcnarf/9d92d095147667bae9beb6b7448820bd")
footer.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[halfWidth, 0]+")")
.attr("text-anchor", "end")
.text("twitter: _Kcnarf")
}
function drawBeeswarm() {
var beeswarm = d3.beeswarm()
.radius(beeRadius)
.orientation("vertical")
.distributeOn((d)=>{ return salaryScale(d.salary); });
obamaArrangement = beeswarm.data(d3.shuffle(obamaSalaries)).side("negative").arrange();
trumpArrangement = beeswarm.data(d3.shuffle(trumpSalaries)).side("positive").arrange();
salariesArrangement = obamaArrangement.concat(trumpArrangement);
drawCircles();
};
function drawCircles() {
circleContainer.selectAll("circle")
.data(salariesArrangement)
.enter()
.append("circle")
.attr("cx", (d)=>{
return (d.datum.administration==="Obama")? d.x-halfSpaceBetween.circle : d.x+halfSpaceBetween.circle; })
.attr("cy", (d)=>{ return d.y; })
.attr("r", beeRadius)
.attr("class", (d)=>{ return d.datum.administration; })
.on("mouseover", function(d) {
tooltip.transition()
.duration(0)
.style("opacity", .9);
tooltip.html(""+d3.format("$,")(d.datum.salary))
.style("left", (d3.event.pageX - 30) + "px")
.style("top", (d3.event.pageY - 24) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(duration)
.style("opacity", 0);
});;
}
function drawMeans() {
var tickWidth = 30,
lineHalfWidth = halfWidth-halfWidth/4+tickWidth,
labelMargin = 5;
var oMean = avgContainer.append("g")
.attr("id", "obama-average")
.classed("average", true)
.attr("transform", "translate("+[0, salaryScale(obamaMeanSalary)]+")")
oMean.append("line")
.classed("Obama", true)
.attr("x1", -lineHalfWidth)
.attr("y1", 0)
.attr("x2", -halfSpaceBetween.slope)
.attr("y2", 0);
oMean.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[-(lineHalfWidth+labelMargin),3]+")")
.attr("text-anchor", "end")
.text(d3.format("$,")(obamaMeanSalary));
oMean.append("text")
.classed("tiny text-background", true)
.attr("transform", "translate("+[-lineHalfWidth+tickWidth,3]+")")
.attr("text-anchor", "start")
.text("mean");
oMean.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[-lineHalfWidth+tickWidth,3]+")")
.attr("text-anchor", "start")
.text("mean");
avgContainer.append("line")
.classed("slope", true)
.attr("id", "mean-slope")
.attr("x1", -halfSpaceBetween.slope)
.attr("y1", salaryScale(obamaMeanSalary))
.attr("x2", halfSpaceBetween.slope)
.attr("y2", salaryScale(trumpMeanSalary));
var tMean = avgContainer.append("g")
.attr("id", "trump-average")
.classed("average", true)
.attr("transform", "translate("+[0, salaryScale(trumpMeanSalary)]+")")
tMean.append("line")
.classed("Trump", true)
.attr("x1", halfSpaceBetween.slope)
.attr("y1", 0)
.attr("x2", lineHalfWidth)
.attr("y2", 0);
tMean.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[lineHalfWidth+labelMargin,3]+")")
.attr("text-anchor", "start")
.text(d3.format("$,")(trumpMeanSalary));
tMean.append("text")
.classed("tiny text-background", true)
.attr("transform", "translate("+[lineHalfWidth-tickWidth,3]+")")
.attr("text-anchor", "end")
.text("mean");
tMean.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[lineHalfWidth-tickWidth,3]+")")
.attr("text-anchor", "end")
.text("mean");
}
function drawMedians() {
var tickWidth = 30,
lineHalfWidth = halfWidth-halfWidth/4+tickWidth,
labelMargin = 5;
var oMedian = medianContainer.append("g")
.attr("id", "obama-median")
.classed("median", true)
.attr("transform", "translate("+[0, salaryScale(obamaMedianSalary)]+")")
oMedian.append("line")
.classed("Obama", true)
.attr("x1", -lineHalfWidth)
.attr("y1", 0)
.attr("x2", -halfSpaceBetween.slope)
.attr("y2", 0);
oMedian.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[-(lineHalfWidth+labelMargin),3]+")")
.attr("text-anchor", "end")
.text(d3.format("$,")(obamaMedianSalary));
oMedian.append("text")
.classed("tiny text-background", true)
.attr("transform", "translate("+[-lineHalfWidth+tickWidth,3]+")")
.attr("text-anchor", "start")
.text("median");
oMedian.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[-lineHalfWidth+tickWidth,3]+")")
.attr("text-anchor", "start")
.text("median");
avgContainer.append("line")
.classed("slope", true)
.attr("id", "mean-slope")
.attr("x1", -halfSpaceBetween.slope)
.attr("y1", salaryScale(obamaMedianSalary))
.attr("x2", halfSpaceBetween.slope)
.attr("y2", salaryScale(trumpMedianSalary));
var tMedian = medianContainer.append("g")
.attr("id", "trump-median")
.classed("median", true)
.attr("transform", "translate("+[0, salaryScale(trumpMedianSalary)]+")")
tMedian.append("line")
.classed("Trump", true)
.attr("x1", halfSpaceBetween.slope)
.attr("y1", 0)
.attr("x2", lineHalfWidth)
.attr("y2", 0);
tMedian.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[lineHalfWidth+labelMargin,3]+")")
.attr("text-anchor", "start")
.text(d3.format("$,")(trumpMedianSalary));
tMedian.append("text")
.classed("tiny text-background", true)
.attr("transform", "translate("+[lineHalfWidth-tickWidth,3]+")")
.attr("text-anchor", "end")
.text("median");
tMedian.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[lineHalfWidth-tickWidth,3]+")")
.attr("text-anchor", "end")
.text("median");
}
</script>
</body>
</html>
Updated missing url https://raw.githack.com/Kcnarf/d3-beeswarm/master/build/d3-beeswarm.js to https://raw.githack.com/kcnarf/d3-beeswarm/master/build/d3-beeswarm.js
https://d3js.org/d3.v4.min.js
https://raw.githack.com/Kcnarf/d3-beeswarm/master/build/d3-beeswarm.js