A connected dot plot showing the pay gap between the lowest, median, and highest paid employees at universities. Hover over dots to reveal the data. Click the screen to change between landscape and portrait versions.
forked from tlfrd's block: Connected Dot Plot
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body {
font-family: sans-serif;
font-size: 12px;
margin: 0;
}
.checkbox {
position: absolute;
top: 15px;
left: 15px;
}
.dropdown {
position: absolute;
top: 15px;
right: 15px;
}
text {
font-size: 12px;
}
.title {
font-weight: bold;
}
.grid-line {
stroke: black;
opacity: 0.2;
}
.rect-label {
stroke: black;
}
.selection-line {
stroke: black;
stroke-dasharray: 5,5;
}
.hover-label {
background-color: blue;
}
.lollipop-line {
stroke: black;
stroke-width: 1px;
}
circle {
stroke-width: 1px;
stroke: black;
}
circle.lollipop-start {
fill: #00c100;
}
.lollipop-end {
fill: #d700d7;
}
.lollipop-median {
fill: white;
}
div.tooltip {
position: absolute;
text-align: center;
padding: 5px;
font: 12px sans-serif;
background: white;
border: 1px black solid;
border-radius: 4px;
pointer-events: none;
}
</style>
</head>
<body>
<div class="checkbox">
Show Line On Hover
<input type="checkbox" class="hover-line">
</div>
<div class="dropdown">
Sort By
<select class="sort-by">
<option value="name">Name</option>
<option value="max">Highest Salary</option>
<option value="median">Median Salary</option>
<option value="min">Lowest Salary</option>
</select>
</div>
<script>
var hoverLine = false;
var landscape = true;
d3.select(".hover-line")
.on("click", function() {
hoverLine = d3.select(this).property("checked");
});
d3.select(".sort-by")
.on("change", function() {
var attribute = d3.select(this).property("value");
sortLollipops(attribute, 1);
});
var margin = {top: 120, right: 100, bottom: 50, left: 150};
var width = 960 - margin.left - margin.right,
height = 500 - 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 + ")");
function sortBy(data, attribute, order) {
data.sort(function(a, b) {
if(a[attribute] < b[attribute]) return -1 * order;
if(a[attribute] > b[attribute]) return 1 * order;
return 0;
});
}
// need to rewrite so start, min, lowest are the same
var classToPos = {
"lollipop-start": "min",
"lollipop-median": "median",
"lollipop-end": "max"
}
var legendLabels = [
{label: "Lowest", class: "lollipop-start"},
{label: "Median", class: "lollipop-median"},
{label: "Highest", class: "lollipop-end"}
];
var legendX = 300,
legendY = -50,
spaceBetween = 70,
titleOffset = -120;
// code for positioning legend
var legend = svg.append("g")
.attr("transform", "translate(" + [legendX, legendY] + ")");
legend.append("g")
.attr("class", "title")
.append("text")
.attr("x", titleOffset)
.text("Yearly Salary (£)")
// Try using Susie Lu's d3-legend
// add circles
legend.selectAll("circle")
.data(legendLabels)
.enter().append("circle")
.attr("cx", function(d, i) {
return spaceBetween * i;
})
.attr("cy", -4)
.attr("r", 5)
.attr("class", function(d) { return d.class });
// add labels
legend.append("g")
.selectAll("text")
.data(legendLabels)
.enter().append("text")
.attr("x", function(d, i) {
return spaceBetween * i + 10;
})
.text(function(d) { return d.label });
// Append div
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var posToColour = {
min: "#00c100",
median: "white",
max: "#d700d7"
};
var y = d3.scaleBand()
.range([0, height])
.paddingInner(0.5)
.paddingOuter(0.7);
var x = d3.scaleLinear()
.range([0, width]);
var lineGenerator = d3.line();
var axisLinePath = function(d) {
return lineGenerator([[x(d) + 0.5, 0], [x(d) + 0.5, height]]);
};
var lollipopLinePath = function(d) {
return lineGenerator([[x(d.min), 0], [x(d.max), 0]]);
};
var selectionLine,
lollipops,
yAxis,
yAxisGroup,
xAxis,
xAxisGroup,
axisLines,
lollipopsGroup;
var startCircles,
medianCircles,
endCircles;
// Make global for now
var payRatios;
var url = "https://raw.githubusercontent.com/tlfrd/pay-ratios/master/data/payratio.json";
d3.json(url, function(error, data) {
if (error) throw error;
payRatios = data.pay_ratios_2015_16;
// use -1 to flip ordering
sortBy(payRatios, "name", 1);
y.domain(payRatios.map(function(d) { return d.name }));
x.domain([0, d3.max(payRatios, function(d) { return d.max })]);
x.nice();
yAxis = d3.axisLeft().scale(y)
.tickSize(0);
xAxis = d3.axisTop().scale(x)
.tickFormat(function(d,i) {
if (i == 0) {
return "£0"
} else {
return d3.format(".2s")(d);
}
});
yAxisGroup = svg.append("g")
.attr("class", "y-axis-group");
yAxisGroup.append("g")
.attr("class", "y-axis")
.attr("transform", "translate(-20, 0)")
.call(yAxis)
.select(".domain")
.attr("opacity", 0);
xAxisGroup = svg.append("g")
.attr("class", "x-axis-group");
xAxisGroup.append("g")
.attr("class", "x-axis")
.call(xAxis);
axisLines = svg.append("g")
.attr("class", "grid-lines");
axisLines.selectAll("path")
.data(x.ticks())
.enter().append("path")
.attr("class", "grid-line")
.attr("d", axisLinePath);
selectionLine = xAxisGroup.append("g")
.attr("class", "interactive")
.append("path")
.attr("class", "selection-line")
.attr("d", function() {
return lineGenerator([[width / 2, 0],[width / 2, height]])
})
.attr("opacity", 0);
lollipopsGroup = svg.append("g").attr("class", "lollipops");
lollipops = lollipopsGroup.selectAll("g")
.data(payRatios)
.enter().append("g")
.attr("class", "lollipop")
.attr("transform", function(d) {
return "translate(0," + (y(d.name) + (y.bandwidth() / 2)) + ")";
})
lollipops.append("path")
.attr("class", "lollipop-line")
.attr("d", lollipopLinePath);
startCircles = lollipops.append("circle")
.attr("class", "lollipop-start")
.attr("r", 5)
.attr("cx", function(d) {
return x(d.min);
})
.on("mouseover", showLabel)
.on("mousemove", moveLabel)
.on("mouseout", hideLabel);
medianCircles = lollipops.append("circle")
.attr("class", "lollipop-median")
.attr("r", function(d) {
if (d.median === "N/A") {
return 0;
} else {
return 5;
}
})
.attr("cx", function(d) {
if (d.median === "N/A") {
return 0;
} else {
return x(d.median);
}
})
.on("mouseover", showLabel)
.on("mousemove", moveLabel)
.on("mouseout", hideLabel);
endCircles = lollipops.append("circle")
.attr("class", "lollipop-end")
.attr("r", 5)
.attr("cx", function(d) {
return x(d.max);
})
.on("mouseover", showLabel)
.on("mousemove", moveLabel)
.on("mouseout", hideLabel);
});
function showLabel() {
var selection = d3.select(this);
var pos = classToPos[selection.attr("class")];
var data = d3.select(this.parentNode).datum();
div.transition()
.duration(100)
.style("opacity", 0.9)
div.html("£" + (selection.datum()[pos]))
.style("left", (d3.event.pageX - 30) + "px")
.style("top", (d3.event.pageY - 40) + "px")
.style("background-color", posToColour[pos])
if (hoverLine) {
selectionLine
.attr("d", function() {
if (landscape) {
return lineGenerator([[x(data[pos]), 0], [x(data[pos]), height]]);
} else {
return lineGenerator([[0, x(data[pos])], [width, x(data[pos])]]);
}
})
.attr("opacity", 0.75);
}
}
function moveLabel() {
div.style("left", (d3.event.pageX - 30) + "px")
.style("top", (d3.event.pageY - 40) + "px")
}
function hideLabel() {
div.transition()
.duration(200)
.style("opacity", 0);
selectionLine
.attr("opacity", 0);
}
function sortLollipops(attribute, ordering) {
sortBy(payRatios, attribute, 1);
y.domain(payRatios.map(function(d) { return d.name })).copy();
if (landscape) {
lollipops
.transition()
.attr("transform", function(d) {
return "translate(" + [0, (y(d.name) + y.bandwidth() / 2)] + ")";
});
yAxisGroup.select(".y-axis")
.transition()
.call(yAxis);
} else {
lollipops
.transition()
.attr("transform", function(d) {
return "translate(" + [(y(d.name) + y.bandwidth() / 2), 0] + ")";
});
xAxisGroup.select(".x-axis")
.transition()
.call(yAxis);
}
}
d3.select("svg").on("click", switchBetweenPortraitAndLandscape);
function recalculateMargin(newMargin) {
width = 960 - newMargin.left - newMargin.right;
height = 500 - newMargin.top - newMargin.bottom;
d3.select("svg").select("g")
.attr("transform", "translate(" + newMargin.left + "," + newMargin.top + ")")
}
function switchBetweenPortraitAndLandscape() {
if (landscape) {
landscape = false;
recalculateMargin({top: 100, right: 100, bottom: 100, left: 100})
// Redraw xAxis
x.range([height, 0]);
xAxis = d3.axisLeft().scale(x)
.tickFormat(function(d, i) {
if (i == 0) {
return "£0"
} else {
return d3.format(".2s")(d);
}
});
yAxisGroup.select(".y-axis")
.call(xAxis)
.attr("transform", "translate(0, 0)")
.select(".domain")
.attr("opacity", 1);
// Redraw yAxis
y.range([0, width]);
yAxis = d3.axisBottom().scale(y)
.tickSize(0);
xAxisGroup.select(".x-axis")
.call(yAxis)
.attr("transform", "translate(0," + height + ")")
.select(".domain")
.attr("opacity", 0);
xAxisGroup.select(".x-axis").selectAll("text")
.attr("y", 9)
.attr("x", 9)
.attr("transform", "rotate(45)")
.style("text-anchor", "start");
// Redraw lollipops
lollipops.attr("transform", function(d) {
return "translate(" + [(y(d.name) + (y.bandwidth() / 2)), 0] + ")";
});
lollipops.select(".lollipop-line")
.attr("d", function(d) {
return lineGenerator([[0, x(d.min)], [0, x(d.max)]]);
})
lollipops.select(".lollipop-start")
.attr("cy", function(d) {
return x(d.min);
})
.attr("cx", 0);
lollipops.select(".lollipop-median")
.attr("cy", function(d) {
if (d.median === "N/A") {
return 0;
} else {
return x(d.median);
}
})
.attr("cx", 0);
lollipops.select(".lollipop-end")
.attr("cy", function(d) {
return x(d.max);
})
.attr("cx", 0);
// Redraw grid lines
axisLines.selectAll(".grid-line")
.attr("d", function(d) {
return lineGenerator([[0, x(d) + 0.5], [width, x(d) + 0.5]]);
});
// Move legend
legend.attr("transform", "translate(" + [legendX + 40, legendY] + ")");
} else {
landscape = true;
recalculateMargin(margin);
// Redraw xAxis
x.range([0, width]);
xAxis = d3.axisTop().scale(x)
.tickFormat(function(d, i) {
if (i == 0) {
return "£0"
} else {
return d3.format(".2s")(d);
}
});
xAxisGroup.select(".x-axis")
.call(xAxis)
.attr("transform", "translate(0,0)")
.select(".domain")
.attr("opacity", 1);
// Redraw yAxis
y.range([0, height]);
yAxis = d3.axisLeft().scale(y)
.tickSize(0);
yAxisGroup.select(".y-axis")
.call(yAxis)
.attr("transform", "translate(-20, 0)")
.select(".domain")
.attr("opacity", 0);
// Redraw lollipops
lollipops.attr("transform", function(d) {
return "translate(0," + (y(d.name) + (y.bandwidth() / 2)) + ")";
});
lollipops.select(".lollipop-line")
.attr("d", function(d) {
return lineGenerator([[x(d.min), 0], [x(d.max), 0]]);
})
lollipops.select(".lollipop-start")
.attr("cx", function(d) {
return x(d.min);
})
.attr("cy", 0);
lollipops.select(".lollipop-median")
.attr("cx", function(d) {
if (d.median === "N/A") {
return 0;
} else {
return x(d.median);
}
})
.attr("cy", 0);
lollipops.select(".lollipop-end")
.attr("cx", function(d) {
return x(d.max);
})
.attr("cy", 0);
// Redraw grid lines
axisLines.selectAll(".grid-line")
.attr("d", axisLinePath);
// Reposition legend
legend.attr("transform", "translate(" + [legendX, legendY] + ")");
}
}
</script>
</body>
https://d3js.org/d3.v4.min.js