|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Income distribution in Canada per province for tax year 2014</title> |
|
<style> |
|
.axis .domain { |
|
display: none; |
|
} |
|
|
|
.province-title { |
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; |
|
} |
|
.explanation-text { |
|
font-family: Georgia, "Times New Roman", serif; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<svg width="1280" height="2200"></svg> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
var svg = d3.select("svg"), |
|
margin = {top: 20, right: 20, bottom: 20, left: 40, |
|
explanationText: 30, innerHorizontal: 30, provinceName: 10, bucketNames: 15}, |
|
width = +svg.attr("width") - margin.left - margin.right, |
|
height = +svg.attr("height") - margin.top - margin.bottom - margin.explanationText, |
|
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", |
|
], |
|
incomeBucketLabels = [ |
|
"< $5k", "$5k-$10k", |
|
"$10k-$15k", "$15k-$20k", |
|
"$20k-$25k", "$25k-$30k", |
|
"$30k-$35k", "$35k-$40k", |
|
"$40k-$45k", "$45k-$50k", |
|
"$50k-$55k", "$55k-$60k", |
|
"$60k-$70k", |
|
"$70k-$80k", |
|
"$80k-$90k", |
|
"$90k-$100k", |
|
"$100k-$150k", |
|
"$150k-$250k", |
|
"> $250k", |
|
], |
|
provinces = { |
|
"10": { |
|
province: "NEWFOUNDLAND AND LABRADOR", |
|
abbreviation: "NL", |
|
code: "10", |
|
}, |
|
"11": { |
|
province: "PRINCE EDWARD ISLAND", |
|
abbreviation: "PE", |
|
code: "11", |
|
}, |
|
"12": { |
|
province: "NOVA SCOTIA", |
|
abbreviation: "NS", |
|
code: "12", |
|
}, |
|
"13": { |
|
province: "NEW BRUNSWICK", |
|
abbreviation: "NB", |
|
code: "13", |
|
}, |
|
"24": { |
|
province: "QUÉBEC", |
|
abbreviation: "QC", |
|
code: "24", |
|
}, |
|
"35": { |
|
province: "ONTARIO", |
|
abbreviation: "ON", |
|
code: "35", |
|
}, |
|
"46": { |
|
province: "MANITOBA", |
|
abbreviation: "MB", |
|
code: "46", |
|
}, |
|
"47": { |
|
province: "SASKATCHEWAN", |
|
abbreviation: "SK", |
|
code: "47", |
|
}, |
|
"48": { |
|
province: "ALBERTA", |
|
abbreviation: "AB", |
|
code: "48", |
|
}, |
|
"59": { |
|
province: "BRITISH COLUMBIA", |
|
abbreviation: "BC", |
|
code: "59", |
|
}, |
|
"60": { |
|
province: "YUKON TERRITORY", |
|
abbreviation: "YT", |
|
code: "60", |
|
}, |
|
"61": { |
|
province: "NORTHWEST TERRITORIES", |
|
abbreviation: "NT", |
|
code: "61", |
|
}, |
|
"62": { |
|
province: "NUNAVUT", |
|
abbreviation: "NU", |
|
code: "62", |
|
}, |
|
}; |
|
|
|
d3.csv("/vsapsai/raw/155794a4b1d3b432be4e67accd247c23/cndtbl-summary.csv", function(error, data) { |
|
if (error) { |
|
throw error; |
|
} |
|
//console.log(data); |
|
|
|
var provinceData = data.filter(function(item) { |
|
return (item.classification.trim() === "TOTAL-PROV") && (item.year === "2014"); |
|
}); |
|
// Convert strings to numbers and calculate percentages. |
|
provinceData.forEach(function(item) { |
|
item.values = incomeBuckets.map(function(bucket) { |
|
return { |
|
bucket: bucket, |
|
value: parseInt(item[bucket]), |
|
}; |
|
}); |
|
var provinceTotalTaxReturns = parseInt(item.total); |
|
item.percentage = incomeBuckets.map(function(bucket) { |
|
return { |
|
bucket: bucket, |
|
value: parseInt(item[bucket]) / provinceTotalTaxReturns, |
|
}; |
|
}); |
|
}); |
|
//console.log(provinceData); |
|
|
|
var maxPopulation = d3.max(provinceData, function(item) { |
|
return d3.max(item.values, function(d) { return d.value; }); |
|
}), |
|
maxPercentage = d3.max(provinceData, function(item) { |
|
return d3.max(item.percentage, function(d) { return d.value; }); |
|
}); |
|
|
|
var x = d3.scaleBand() |
|
.domain(incomeBuckets) |
|
.rangeRound([0, (width - margin.innerHorizontal) / 2]) |
|
.paddingInner(0.1); |
|
|
|
var yProvinces = d3.scaleBand() |
|
.domain(provinceData.map(function(item) { return item.province; }).sort()) |
|
.rangeRound([height, 0]) |
|
.paddingInner(0.2); |
|
|
|
var provinceHeight = yProvinces.bandwidth(), |
|
provinceInnerHeight = provinceHeight - margin.provinceName - margin.bucketNames; |
|
var yPopulation = d3.scaleLinear() |
|
.domain([0, maxPopulation]) |
|
.rangeRound([provinceInnerHeight, 0]); |
|
|
|
var yPercentage = d3.scaleLinear() |
|
.domain([0, maxPercentage]) |
|
.rangeRound([provinceInnerHeight, 0]); |
|
|
|
// Plot data. |
|
g.append("g") |
|
.selectAll("g") |
|
.data(provinceData) |
|
.enter().append("g") |
|
.attr("transform", function(d) { |
|
var yOffset = height + margin.explanationText - provinceHeight - yProvinces(d.province); |
|
return "translate(0," + yOffset + ")"; |
|
}) |
|
.each(buildProvincePlots); |
|
|
|
// Add explanation text. |
|
g.append("text") |
|
.attr("x", 0) |
|
.attr("y", 0) |
|
.attr("class", "explanation-text") |
|
.text("Population in each province with income falling in specified range."); |
|
g.append("text") |
|
.attr("x", width - x.range()[1]) |
|
.attr("y", 0) |
|
.attr("class", "explanation-text") |
|
.text("Percentage of province population with income falling in specified range."); |
|
|
|
function buildProvincePlots(provinceD) { |
|
// Province name. |
|
d3.select(this) |
|
.append("text") |
|
.attr("x", width / 2) |
|
.attr("y", 0) |
|
.attr("text-anchor", "middle") |
|
.attr("class", "province-title") |
|
.text(provinces[provinceD.province].province); |
|
|
|
var plotsG = d3.select(this) |
|
.append("g") |
|
.attr("transform", "translate(0," + margin.provinceName + ")"); |
|
// Population plot. |
|
var populationPlotG = plotsG.append("g"); |
|
populationPlotG |
|
.selectAll("rect") |
|
.data(provinceD.values) |
|
.enter().append("rect") |
|
.attr("x", function(d) { return x(d.bucket); }) |
|
.attr("y", function(d) { return yPopulation(d.value); }) |
|
.attr("width", x.bandwidth()) |
|
.attr("height", function(d) { return provinceInnerHeight - yPopulation(d.value); }) |
|
.attr("fill", d3.schemeCategory10[0]); |
|
// y-axis |
|
populationPlotG.append("g") |
|
.attr("class", "axis") |
|
.call(d3.axisLeft(yPopulation).ticks(null, "s")); |
|
// x-axis |
|
appendXAxis(populationPlotG); |
|
|
|
// Percentage plot. |
|
var percentagePlotG = plotsG |
|
.append("g") |
|
.attr("transform", "translate(" + (x.range()[1] + margin.innerHorizontal) + ",0)"); |
|
percentagePlotG |
|
.selectAll("rect") |
|
.data(provinceD.percentage) |
|
.enter().append("rect") |
|
.attr("x", function(d) { return x(d.bucket); }) |
|
.attr("y", function(d) { return yPercentage(d.value); }) |
|
.attr("width", x.bandwidth()) |
|
.attr("height", function(d) { return provinceInnerHeight - yPercentage(d.value); }) |
|
.attr("fill", d3.schemeCategory10[2]); |
|
// y-axis |
|
percentagePlotG.append("g") |
|
.attr("class", "axis") |
|
.call(d3.axisLeft(yPercentage).tickFormat(d3.format(".0%"))); |
|
// x-axis |
|
appendXAxis(percentagePlotG); |
|
|
|
function appendXAxis(g) { |
|
g.append("g") |
|
.attr("class", "axis") |
|
.attr("transform", "translate(0," + provinceInnerHeight + ")") |
|
.call(customXAxis); |
|
} |
|
} |
|
|
|
// Draw text at different height because labels for consecutive bands overlap. |
|
function customXAxis(g) { |
|
// Based on Axis Styling https://bl.ocks.org/mbostock/3371592 |
|
var xAxis = d3.axisBottom(x.copy().domain(incomeBucketLabels)); |
|
g.call(xAxis); |
|
g.selectAll(".tick:nth-child(odd) text").attr("dy", 18); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |