Skip to content

Instantly share code, notes, and snippets.

@vsapsai
Last active May 14, 2017 22:35
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save vsapsai/77f215c0e7fe420ae78d84ed3e6e7e7b to your computer and use it in GitHub Desktop.
Income distribution in Canada per province for tax year 2014
license: gpl-3.0
height: 2200

Compare income distribution across different Canadian provinces and territories according to filed tax returns in 2014.

Data is taken from Canada Revenue Agency.

Important: data is nominal and not adjusted for inflation. Be careful comparing years, including comparison with your own income in current year.

Visualization is based on

<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment