|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Income (not wealth) distribution in Canada</title> |
|
<style> |
|
svg { |
|
cursor: crosshair; |
|
} |
|
|
|
.axis.x-axis .domain { |
|
display: none; |
|
} |
|
|
|
#interactive line { |
|
fill: none; |
|
stroke: gray; |
|
stroke-width: 1px; |
|
stroke-dasharray: 4, 2; |
|
} |
|
|
|
#interactive text { |
|
font-size: 12px; |
|
font-family: sans-serif; |
|
} |
|
|
|
#interactive.inactive line, #interactive.inactive text { |
|
display: none; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<svg width="960" height="500"></svg> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
var svg = d3.select("svg"), |
|
margin = {top: 20, right: 40, bottom: 50, left: 40}, |
|
width = +svg.attr("width") - margin.left - margin.right, |
|
height = +svg.attr("height") - margin.top - margin.bottom, |
|
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
var incomeBuckets = [ |
|
"under-5_000", "5_000-10_000", "10_000-15_000", "15_000-20_000", |
|
"20_000-25_000", "25_000-30_000", "30_000-35_000", "35_000-40_000", |
|
"40_000-45_000", "45_000-50_000", "50_000-55_000", "55_000-60_000", |
|
"60_000-70_000", "70_000-80_000", "80_000-90_000", "90_000-100_000", |
|
"100_000-150_000", "150_000-250_000", "over-250_000", |
|
], |
|
// Use upper bound of the bucket because I expect that's the value people |
|
// will be looking for when checking their own income. |
|
incomeBucketDisplayedIncome = { |
|
"under-5_000": 5000, |
|
"5_000-10_000": 10000, |
|
"10_000-15_000": 15000, |
|
"15_000-20_000": 20000, |
|
"20_000-25_000": 25000, |
|
"25_000-30_000": 30000, |
|
"30_000-35_000": 35000, |
|
"35_000-40_000": 40000, |
|
"40_000-45_000": 45000, |
|
"45_000-50_000": 50000, |
|
"50_000-55_000": 55000, |
|
"55_000-60_000": 60000, |
|
"60_000-70_000": 70000, |
|
"70_000-80_000": 80000, |
|
"80_000-90_000": 90000, |
|
"90_000-100_000": 100000, |
|
"100_000-150_000": 150000, |
|
"150_000-250_000": 250000, |
|
"over-250_000": 500000, // fairly arbitrary number just to make the plot |
|
// look nice and not as depressing as the real life. |
|
}; |
|
|
|
var xScale = d3.scaleLinear() |
|
.range([0, width]) |
|
.domain([0, 1.0]); |
|
var yScale = d3.scaleLinear() |
|
.range([height, 0]) |
|
.domain([0, d3.max(Object.values(incomeBucketDisplayedIncome))]); |
|
|
|
d3.csv("/vsapsai/raw/155794a4b1d3b432be4e67accd247c23/cndtbl-summary.csv", function(error, data) { |
|
if (error) { |
|
throw error; |
|
} |
|
//console.log(data); |
|
|
|
var specificRow = null; |
|
var i, length = data.length; |
|
for (i = 0; i < length; i++) { |
|
var row = data[i]; |
|
if ((row.classification.trim() === "CANADA TOTAL") && (row.year === "2014")) { |
|
specificRow = row; |
|
break; |
|
} |
|
} |
|
//console.log(specificRow); |
|
|
|
// Prepare data for plotting. |
|
var totalPopulation = parseInt(specificRow.total); |
|
var bucketData = []; |
|
incomeBuckets.forEach(function(bucketName) { |
|
var bucketPopulation = parseInt(specificRow[bucketName]); |
|
bucketData.push({ |
|
name: bucketName, |
|
population: bucketPopulation, |
|
percentage: (bucketPopulation / totalPopulation), |
|
}); |
|
}); |
|
// Add percentage offset for all buckets. |
|
var percentageOffset = 0; |
|
bucketData.forEach(function(bucketDatum) { |
|
bucketDatum.percentageOffset = percentageOffset; |
|
percentageOffset += bucketDatum.percentage; |
|
}); |
|
//console.log(bucketData); |
|
|
|
// Plot data. |
|
g.append("g") |
|
.selectAll("rect") |
|
.data(bucketData) |
|
.enter().append("rect") |
|
.attr("x", function(d) { return xScale(d.percentageOffset); }) |
|
.attr("y", function(d) { return yScale(incomeBucketDisplayedIncome[d.name]); }) |
|
.attr("width", function(d) { return xScale(d.percentage); }) |
|
.attr("height", function(d) { return height - yScale(incomeBucketDisplayedIncome[d.name]); }) |
|
.attr("fill", d3.schemeCategory10[0]); |
|
|
|
g.append("g") |
|
.attr("class", "axis y-axis") |
|
.call(d3.axisLeft(yScale).ticks(null, "$s")); |
|
g.append("g") |
|
.attr("class", "axis x-axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(d3.axisBottom(xScale).tickFormat(d3.format(".0%"))); |
|
|
|
// Interactive part with mouse cursor tracking. |
|
var interactiveGroup = g.append("g") |
|
.attr("id", "interactive") |
|
.classed("inactive", true); |
|
var horizontalLine = interactiveGroup.append("line") |
|
.attr("x1", 0) |
|
.attr("y1", 0) |
|
.attr("x2", width) |
|
.attr("y2", 0); |
|
var verticalLine = interactiveGroup.append("line") |
|
.attr("x1", 0) |
|
.attr("y1", height) |
|
.attr("x2", 0) |
|
.attr("y2", 0); |
|
var yValueMarker = interactiveGroup.append("text") |
|
.attr("x", 2) |
|
.attr("y", 0) |
|
.attr("dy", "-0.3em") |
|
.attr("text-anchor", "start"); |
|
var yValueFormat = d3.format("$.5s"); |
|
var xValueMarker = interactiveGroup.append("text") |
|
.attr("x", 0) |
|
.attr("y", height) |
|
.attr("dx", "0.3em") |
|
.attr("dy", "-0.3em") |
|
.attr("text-anchor", "start"); |
|
var xValueFormat = d3.format(".1%"); |
|
|
|
svg.on("mouseout", function() { |
|
interactiveGroup.classed("inactive", true); |
|
}); |
|
svg.on("mousemove", updateMouseTracking); |
|
|
|
function updateMouseTracking() { |
|
var coordinates = d3.mouse(this); |
|
var x = coordinates[0] - margin.left, |
|
y = coordinates[1] - margin.top; |
|
horizontalLine |
|
.attr("y1", y) |
|
.attr("y2", y) |
|
.attr("x2", x); |
|
verticalLine |
|
.attr("x1", x) |
|
.attr("x2", x) |
|
.attr("y2", y); |
|
|
|
var isInBounds = ((0 <= x) && (x <= width) && |
|
(0 <= y) && (y <= height)); |
|
if (isInBounds) { |
|
yValueMarker |
|
.attr("y", y) |
|
.text(yValueFormat(yScale.invert(y))); |
|
xValueMarker |
|
.attr("x", x) |
|
.text(xValueFormat(xScale.invert(x))); |
|
} |
|
interactiveGroup.classed("inactive", !isInBounds); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |