Skip to content

Instantly share code, notes, and snippets.

@Petrando
Last active October 7, 2017 13:09
Show Gist options
  • Save Petrando/9e34686fa789e890bf35f53a08e379e7 to your computer and use it in GitHub Desktop.
Save Petrando/9e34686fa789e890bf35f53a08e379e7 to your computer and use it in GitHub Desktop.
sortable, stackable pyramid barchart
license: mit

Side by side age range comparison between genders. Sortable by total or by genders.

The complete version with option to choose between different population groups can be found at my site here

<!DOCTYPE html>
<head>
<meta charset="utf-8"></meta>
<title>
Age range of genders
</title>
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.y.axis path, .yMale.axis path, .yFemale.axis path{
display: none;
}
.peopleText{
font-size:12px;
font-weight:bold;
pointer-events:none;
}
</style>
<body>
<form>
<label><input class="formationRadio" type="radio" name="mode" value="all">All</label>
<label><input class="formationRadio" type="radio" name="mode" value="gender" checked>Gender</label>
<label><input id="sort" type="checkbox">Sort</input></label>
</form>
<div></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 20, right: 100, bottom: 100, left: 100},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
function formatPeople(amt){return d3.format(",")(amt);}
var ageRanges = [];//["0-4", "5-9", "10-14", "15-19", "20-24", "25-29", "30-34", "35-39", "40-44", "45-49", "50-54", "55-59", "60-64", "65-69", "70-74", "75-79", "80+"];
var yMale = d3.scale.ordinal()
//.domain(ageRanges)
.rangeRoundBands([height, 0], .1);
var yFemale = d3.scale.ordinal()
//.domain(ageRanges)
.rangeRoundBands([height, 0], .1);
var xMale = d3.scale.linear()
.rangeRound([0, width/2]);
var xFemale = d3.scale.linear()
.rangeRound([width/2, width]);
var color = d3.scale.ordinal()
.range(["#98abc5", "#8a89a6"]);
var ageRangeColor = d3.scale.ordinal()
.range(["#ccffe6", "#b3ffda", "#99ffce", "#80ffc1", "#66ffb5", "#4dffa9", "#33ff9c", "#1aff90", "#00ff84", "#00e677", "#00cc69",
"#00b35c", "#00994f", "#008042", "#006635", "#004d28", "#00331a"]);
var genderColor = d3.scale.ordinal()
.range(["#0099ff", "#ff6699"]);
var genders = [];
var xAxisMale = d3.svg.axis()
.scale(xMale)
.orient("bottom")
.tickFormat(d3.format(".2s"));
var xAxisFemale = d3.svg.axis()
.scale(xFemale)
.orient("bottom")
.tickFormat(d3.format(".2s"));
var yAxisMale = d3.svg.axis()
.scale(yMale)
.orient("left");
var yAxisFemale = d3.svg.axis()
.scale(yFemale)
.orient("right");
//var data;
var svg = d3.select("div").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var canvas = svg.append("g")
.attr("class", "canvas")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
canvas.append("g")
.attr("class", "yMale axis")
.call(yAxisMale);
canvas.append("g")
.attr("class", "xMale axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxisMale);
canvas.append("g")
.attr("class", "yFemale axis")
.attr("transform", "translate(" + width + ",0)")
.call(yAxisFemale);
canvas.append("g")
.attr("class", "xFemale axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxisFemale);
var isAnimating = false;
var x0;
var activeLink = "none";
function render(file){
d3.csv(file, function(error, data) {
if (error) throw error;
var totalPeople = 0, totalMale = 0, totalFemale = 0;
data.forEach(function(d){
d.Total = 0;
d["Male"]=+d["Male"];
totalMale += d["Male"];
d["Female"]=+d["Female"];
totalFemale += d["Female"];
d.Total += d["Male"];
d.Total += d["Female"];
totalPeople += d.Total;
});
if(genders.length === 0){
genders = d3.keys(data[0]).filter(function(key){return key!="Age";});
genderColor.domain(genders);
}
var layers = d3.layout.stack()(genders.map(function(c) {
return data.map(function(d) {
//console.log(d);
return {x: d["Age"], y: +d[c], total: d.Total};
});
}));
console.log(layers);
var totalMax = d3.max(layers[0], function(d){return d.total;});//since total is the same for male and female
//just call d3.max once for a single layer....
var maleMax = d3.max(layers[0], function(d){return d.y;});
var maleSum = d3.sum(layers[0], function(d){return d.y;});
var femaleMax = d3.max(layers[1], function(d){return d.y;});
var femaleSum = d3.sum(layers[1], function(d){return d.y;});
var totalPopulation = maleSum + femaleSum;
var maxPopulation = Math.max(maleMax, femaleMax);
if(ageRanges.length === 0){
ageRanges = data.map(function(d){return d.Age;});
ageRangeColor.domain(ageRanges);
}
d3.select("input#sort").property("checked", false);
setupYAxises();
function setupYAxises(){
yMale.domain(ageRanges);
yFemale.domain(ageRanges);
canvas.select(".yMale.axis")
.transition().duration(1000)
.call(yAxisMale);
canvas.select(".yFemale.axis")
.transition().duration(1000)
.call(yAxisFemale);
}
var mode = d3.selectAll("input.formationRadio[name='mode']:checked").node().value;
var maleBars = canvas.selectAll(".maleBars")
.data(layers[0]);
maleBars.enter().append("rect")
.attr("class", "maleBars")
.style("fill", "#0099ff")
.attr("width", 0)
.attr("x", width/2);
var femaleBars = canvas.selectAll(".femaleBars")
.data(layers[1]);
femaleBars.enter().append("rect")
.attr("class", "femaleBars")
.style("fill", "#ff6699")
.attr("width", 0)
.attr("x", width/2);
function renderGenders(){
xMale.domain([maxPopulation, 0]).range([0, width/2]);//need to re-adjust the .range also...
xFemale.domain([0, maxPopulation]);
canvas.select(".xMale.axis")
.transition().duration(1000)
.call(xAxisMale);
canvas.select(".xFemale.axis")
.transition().duration(1000)
.call(xAxisFemale)
.style("opacity", 1);
canvas.select(".yFemale.axis")
.transition().duration(1000)
.style("opacity", 1);
/*maleBars
.attr("width", 0)
.attr("x", width/2); */
maleBars
.on("mouseover", function(d){
//console.log(d);
d3.select(this).style("stroke", "black").style("stroke-width", 1.5);
canvas.select(".yMale.axis").selectAll("text")
.filter(function(dText){return dText === d.x;})
.attr("font-size", 14)
.style("font-weight", "bold");
var tipX = d3.mouse(this)[0];
canvas.append("text")
.attr("class", "peopleText")
.text(formatPeople(d.y) + " males.")
.attr("x", ((tipX - 90) < 10?10:tipX - 90))
.attr("y", yMale(d.x) +15);
})
.on("mousemove", function(d){
var tipX = d3.mouse(this)[0];
canvas.select(".peopleText")
.attr("x", ((tipX - 90) < 10?10:tipX - 90))
.attr("y", yMale(d.x) + 15);
})
.on("mouseout", function(d){
d3.select(this).style("stroke", "none").style("stroke-width", 0);
canvas.select(".yMale.axis").selectAll("text")
.filter(function(dText){return dText === d.x;})
.attr("font-size", 10)
.style("font-weight", "normal");
canvas.select(".peopleText").remove();
});
maleBars
.transition().duration(1000)
.attr("y", function(d){return yMale(d.x);})
.attr("height", yMale.rangeBand())
.attr("x", function(d){return xMale(d.y);})
.attr("width", function(d){return width/2 - xMale(d.y);})
//.transition().duration(250)
.style("fill", "#0099ff");
femaleBars
.on("mouseover", function(d){
d3.select(this).style("stroke", "black").style("stroke-width", 1.5);
canvas.select(".yFemale.axis").selectAll("text")
.filter(function(dText){return dText === d.x;})
.attr("font-size", 14)
.style("font-weight", "bold");
var tipX = d3.mouse(this)[0];
canvas.append("text")
.attr("class", "peopleText")
.text(formatPeople(d.y) + " females.")
.attr("x", ((tipX + 10) > width - 100?width - 100:tipX + 10))
.attr("y", yFemale(d.x) +15);
})
.on("mousemove", function(d){
var tipX = d3.mouse(this)[0];
canvas.select(".peopleText")
.attr("x", ((tipX + 10) > width - 100?width - 100:tipX + 10))
.attr("y", yFemale(d.x) + 15);
})
.on("mouseout", function(d){
d3.select(this).attr("stroke", "none").style("stroke-width", 0);
canvas.select(".yFemale.axis").selectAll("text")
.filter(function(dText){return dText === d.x;})
.attr("font-size", 10)
.style("font-weight", "normal");
canvas.select(".peopleText").remove();
});
femaleBars
.transition().duration(1000)
.attr("y", function(d){return yFemale(d.x);})
.attr("height", yFemale.rangeBand())
.attr("x", width/2)
.attr("width", function(d){return xFemale(d.y) - width/2;})
.style("fill", "#ff6699");
}
function renderTotal(){
canvas.select(".xFemale.axis")
.transition().duration(1000)
.style("opacity", 0);
canvas.select(".yFemale.axis")
.transition().duration(1000)
.style("opacity", 0);
xMale.domain([0, totalMax]).rangeRound([0, width]);
canvas.select(".xMale.axis")
.transition().duration(1000)
.call(xAxisMale);
maleBars
.transition().duration(1000).delay(isNew?0:1000)
.attr("y", function(d){return yMale(d.x);})
.attr("height", yMale.rangeBand())
.attr("x", function(d) { return xMale(d.y0); })
.attr("width", function(d){return xMale(d.y0 + d.y) - xMale(d.y0);})
.style("fill", function(d){return ageRangeColor(d.x);});
maleBars
.on("mouseover", function(d){
canvas.selectAll("rect")
.filter(function(dRect){return dRect.x === d.x;})
.style("fill", "#ffff33");
canvas.select(".yMale.axis").selectAll("text")
.filter(function(dText){return dText === d.x;})
.attr("font-size", 14)
.style("font-weight", "bold");
mouseOverTip(d, d3.mouse(this)[0]);
})
.on("mousemove", function(d){
mouseMoveTip(d3.mouse(this)[0]);
})
.on("mouseout", function(d){
canvas.selectAll("rect")
.filter(function(dRect){return dRect.x === d.x;})
.style("fill", function(dRect){return ageRangeColor(dRect.x);});
canvas.select(".yMale.axis").selectAll("text")
.filter(function(dText){return dText === d.x;})
.attr("font-size", 10)
.style("font-weight", "normal");
canvas.select(".peopleText").remove();
});
femaleBars
.transition().duration(1000).delay(isNew?0:1500)
.attr("y", function(d){return yMale(d.x);})
.attr("height", yMale.rangeBand())
.attr("x", function(d) { return xMale(d.y0); })
.attr("width", function(d){return xMale(d.y0 + d.y) - xMale(d.y0);})
//.transition().duration(250)
.style("fill", function(d){return ageRangeColor(d.x);});
femaleBars
.on("mouseover", function(d){
canvas.selectAll("rect")
.filter(function(dRect){return dRect.x === d.x;})
.style("fill", "#ffff33");
canvas.select(".yMale.axis").selectAll("text")
.filter(function(dText){return dText === d.x;})
.attr("font-size", 14)
.style("font-weight", "bold");
mouseOverTip(d, d3.mouse(this)[0]);
})
.on("mousemove", function(d){
mouseMoveTip(d3.mouse(this)[0]);
})
.on("mouseout", function(d){
canvas.selectAll("rect")
.filter(function(dRect){return dRect.x === d.x;})
.style("fill", function(dRect){return ageRangeColor(dRect.x);});
canvas.select(".yMale.axis").selectAll("text")
.filter(function(dText){return dText === d.x;})
.attr("font-size", 10)
.style("font-weight", "normal");
canvas.select(".peopleText").remove();
});
function mouseOverTip(d, tipX){
canvas.append("text")
.attr("class", "peopleText")
.text(formatPeople(d.total) + " people.")
.attr("x", tipX + 10)
.attr("y", yMale(d.x) +15);
}
function mouseMoveTip(tipX){
canvas.select(".peopleText")
.attr("x", tipX + 10);
}
}
maleBars.exit().remove();
femaleBars.exit().remove();
if(d3.selectAll("input.formationRadio[name='mode']:checked").node().value === "gender")renderGenders();
else renderTotal();
isNew = false;
d3.selectAll("input.formationRadio").on("change", changeFormation);
function changeFormation() {
d3.select("input#sort").property("checked", false);
setupYAxises();
if (this.value === "gender") renderGenders();
else renderTotal();
}
d3.select("input#sort").on("change", change);
function change(){
var transition = svg.transition().duration(750),
delay = function(d, i) { return i * 50; };
if(d3.selectAll("input.formationRadio[name='mode']:checked").node().value === "gender"){
var y0Male = yMale.domain(layers[0].sort(this.checked
? function(a, b) { return b.y - a.y; }
: function(a, b) { return ageRanges.indexOf(a.x) - ageRanges.indexOf(b.x); })
.map(function(d) { return d.x; }))
.copy();
canvas.selectAll(".maleBars")
.sort(function(a, b) { return y0Male(a.y) - y0Male(b.y); });
transition.selectAll(".maleBars")
.delay(delay)
.attr("y", function(d) { return y0Male(d.x); });
transition.select(".canvas").select(".yMale.axis")
.call(yAxisMale)
.selectAll("g")
.delay(delay);
var y0Female = yFemale.domain(layers[1].sort(this.checked
? function(a, b) { return b.y - a.y; }
: function(a, b) { console.log(a.x);return ageRanges.indexOf(a.x) - ageRanges.indexOf(b.x); })
.map(function(d) { return d.x; }))
.copy();
canvas.selectAll(".femaleBars")
.sort(function(a, b) { return y0Female(a.y) - y0Female(b.y); });
transition.selectAll(".femaleBars")
.delay(delay)
.attr("y", function(d) { return y0Female(d.x); });
transition.select(".canvas").select(".yFemale.axis")
.call(yAxisFemale)
.selectAll("g")
.delay(delay);
}
else{
var y0Male = yMale.domain(layers[0].sort(this.checked
? function(a, b) { return b.total - a.total; }
: function(a, b) { return ageRanges.indexOf(a.x) - ageRanges.indexOf(b.x); })
.map(function(d) { return d.x; }))
.copy();
canvas.selectAll(".maleBars")
.sort(function(a, b) { return y0Male(a.y) - y0Male(b.y); });
transition.selectAll(".maleBars")
.delay(delay)
.attr("y", function(d) { return y0Male(d.x); });
transition.select(".canvas").select(".yMale.axis")
.call(yAxisMale)
.selectAll("g")
.delay(delay);
canvas.selectAll(".femaleBars")
.sort(function(a, b) { return y0Male(a.y) - y0Male(b.y); });
transition.selectAll(".femaleBars")
.delay(delay)
.attr("y", function(d) { return y0Male(d.x); });
}
}
});
}
//..initial render condition...
render("people.csv");
</script>
</body>
</head>
Age Male Female
0-4 8985 8366
5-9 18039 16363
10-14 21601 19947
15-19 19275 17721
20-24 15912 14551
25-29 14061 14325
30-34 17073 18471
35-39 19745 20542
40-44 23302 22166
45-49 19566 17278
50-54 14881 12777
55-59 11397 9879
60-64 8652 7202
65-69 6061 5646
70-74 4834 4154
75-79 2211 2331
80+ 2358 2647
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment