Skip to content

Instantly share code, notes, and snippets.

@KatiRG
Last active March 14, 2022 11:12
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KatiRG/f7d064cd9c3efbc3c360 to your computer and use it in GitHub Desktop.
Save KatiRG/f7d064cd9c3efbc3c360 to your computer and use it in GitHub Desktop.
d3js clickable, sortable stacked bar chart
Released under the GNU General Public License, version 3.
(or, optionlly, any higher version)

Stacked bar chart with a clickable legend to display one class of bars at a time, with the option of sorting them afterwards.

Builds on my clickable stacked bar chart, which was based on Mike Bostock's stacked bar chart and yuuniverse4444’s block #8325617, and integrates Mike Bostock's sortable bar chart.

Note: my version fixes a small inaccuracy in Mike Bostock's original code in which the heights and y-position of the bars are not calculated directly from the data value, but on the difference between the cumulative values in the stacked bar. This inaccuracy, which is usually only around 1 pixel, is not noticeable until a single category is sorted and you can see all the bars side by side in decreasing order.

My fix is a crude hack but it works!

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.bar {
fill: steelblue;
}
.x.axis path {
display: none;
}
.tooltip{
text-anchor: middle;
font-family: sans-serif;
font-size: 10px;
font-weight: bold;
fill:black;
}
label {
position: absolute;
top: 222px;
right: 35px;
color: #D8D8D8;
}
</style>
<body>
<label><input type="checkbox"> Sort values</label>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.rangeRound([height, 0]);
var color = d3.scale.ordinal()
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(".2s"));
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 + ")");
var active_link = "0"; //to control legend selections and hover
var legendClicked; //to control legend selections
var legendClassArray = []; //store legend classes to select bars in plotSingle()
var legendClassArray_orig = []; //orig (with spaces)
var sortDescending; //if true, bars are sorted by height in descending order
var restoreXFlag = false; //restore order of bars back to original
//disable sort checkbox
d3.select("label")
.select("input")
.property("disabled", true)
.property("checked", false);
d3.csv("state_data.csv", function(error, data) {
if (error) throw error;
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "State"; }));
data.forEach(function(d) {
var mystate = d.State; //add to stock code
var y0 = 0;
//d.ages = color.domain().map(function(name) { return {name: name, y0: y0, y1: y0 += +d[name]}; });
d.ages = color.domain().map(function(name) {
//return { mystate:mystate, name: name, y0: y0, y1: y0 += +d[name]}; });
return {
mystate:mystate,
name: name,
y0: y0,
y1: y0 += +d[name],
value: d[name],
y_corrected: 0
};
});
d.total = d.ages[d.ages.length - 1].y1;
});
//Sort totals in descending order
data.sort(function(a, b) { return b.total - a.total; });
x.domain(data.map(function(d) { return d.State; }));
y.domain([0, d3.max(data, function(d) { return d.total; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end");
//.text("Population");
var state = svg.selectAll(".state")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + "0" + ",0)"; });
//.attr("transform", function(d) { return "translate(" + x(d.State) + ",0)"; })
height_diff = 0; //height discrepancy when calculating h based on data vs y(d.y0) - y(d.y1)
state.selectAll("rect")
.data(function(d) {
return d.ages;
})
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("y", function(d) {
height_diff = height_diff + y(d.y0) - y(d.y1) - (y(0) - y(d.value));
y_corrected = y(d.y1) + height_diff;
d.y_corrected = y_corrected //store in d for later use in restorePlot()
if (d.name === "65 Years and Over") height_diff = 0; //reset for next d.mystate
return y_corrected;
// return y(d.y1); //orig, but not accurate
})
.attr("x",function(d) { //add to stock code
return x(d.mystate)
})
.attr("height", function(d) {
//return y(d.y0) - y(d.y1); //heights calculated based on stacked values (inaccurate)
return y(0) - y(d.value); //calculate height directly from value in csv file
})
.attr("class", function(d) {
classLabel = d.name.replace(/\s/g, ''); //remove spaces
return "bars class" + classLabel;
})
.style("fill", function(d) { return color(d.name); });
state.selectAll("rect")
.on("mouseover", function(d){
var delta = d.y1 - d.y0;
var xPos = parseFloat(d3.select(this).attr("x"));
var yPos = parseFloat(d3.select(this).attr("y"));
var height = parseFloat(d3.select(this).attr("height"))
d3.select(this).attr("stroke","blue").attr("stroke-width",0.8);
svg.append("text")
.attr("x",xPos)
.attr("y",yPos +height/2)
.attr("class","tooltip")
.text(d.name +": "+ delta);
})
.on("mouseout",function(){
svg.select(".tooltip").remove();
d3.select(this).attr("stroke","pink").attr("stroke-width",0.2);
})
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("class", function (d) {
legendClassArray.push(d.replace(/\s/g, '')); //remove spaces
legendClassArray_orig.push(d); //remove spaces
return "legend";
})
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
//reverse order to match order in which bars are stacked
legendClassArray = legendClassArray.reverse();
legendClassArray_orig = legendClassArray_orig.reverse();
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color)
.attr("id", function (d, i) {
return "id" + d.replace(/\s/g, '');
})
.on("mouseover",function(){
if (active_link === "0") d3.select(this).style("cursor", "pointer");
else {
if (active_link.split("class").pop() === this.id.split("id").pop()) {
d3.select(this).style("cursor", "pointer");
} else d3.select(this).style("cursor", "auto");
}
})
.on("click",function(d){
if (active_link === "0") { //nothing selected, turn on this selection
d3.select(this)
.style("stroke", "black")
.style("stroke-width", 2);
active_link = this.id.split("id").pop();
plotSingle(this);
//gray out the others
for (i = 0; i < legendClassArray.length; i++) {
if (legendClassArray[i] != active_link) {
d3.select("#id" + legendClassArray[i])
.style("opacity", 0.5);
} else sortBy = i; //save index for sorting in change()
}
//enable sort checkbox
d3.select("label").select("input").property("disabled", false)
d3.select("label").style("color", "black")
//sort the bars if checkbox is clicked
d3.select("input").on("change", change);
} else { //deactivate
if (active_link === this.id.split("id").pop()) {//active square selected; turn it OFF
d3.select(this)
.style("stroke", "none");
//restore remaining boxes to normal opacity
for (i = 0; i < legendClassArray.length; i++) {
d3.select("#id" + legendClassArray[i])
.style("opacity", 1);
}
if (d3.select("label").select("input").property("checked")) {
restoreXFlag = true;
}
//disable sort checkbox
d3.select("label")
.style("color", "#D8D8D8")
.select("input")
.property("disabled", true)
.property("checked", false);
//sort bars back to original positions if necessary
change();
//y translate selected category bars back to original y posn
restorePlot(d);
active_link = "0"; //reset
}
} //end active_link check
});
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
// restore graph after a single selection
function restorePlot(d) {
//restore graph after a single selection
d3.selectAll(".bars:not(.class" + class_keep + ")")
.transition()
.duration(1000)
.delay(function() {
if (restoreXFlag) return 3000;
else return 750;
})
.attr("width", x.rangeBand()) //restore bar width
.style("opacity", 1);
//translate bars back up to original y-posn
d3.selectAll(".class" + class_keep)
.attr("x", function(d) { return x(d.mystate); })
.transition()
.duration(1000)
.delay(function () {
if (restoreXFlag) return 2000; //bars have to be restored to orig posn
else return 0;
})
.attr("y", function(d) {
//return y(d.y1); //not exactly correct since not based on raw data value
return d.y_corrected;
});
//reset
restoreXFlag = false;
}
// plot only a single legend selection
function plotSingle(d) {
class_keep = d.id.split("id").pop();
idx = legendClassArray.indexOf(class_keep);
//erase all but selected bars by setting opacity to 0
d3.selectAll(".bars:not(.class" + class_keep + ")")
.transition()
.duration(1000)
.attr("width", 0) // use because svg has no zindex to hide bars so can't select visible bar underneath
.style("opacity", 0);
//lower the bars to start on x-axis
state.selectAll("rect").forEach(function (d, i) {
//get height and y posn of base bar and selected bar
h_keep = d3.select(d[idx]).attr("height");
y_keep = d3.select(d[idx]).attr("y");
h_base = d3.select(d[0]).attr("height");
y_base = d3.select(d[0]).attr("y");
h_shift = h_keep - h_base;
y_new = y_base - h_shift;
//reposition selected bars
d3.select(d[idx])
.transition()
.ease("bounce")
.duration(1000)
.delay(750)
.attr("y", y_new);
})
}
//adapted change() fn in http://bl.ocks.org/mbostock/3885705
function change() {
if (this.checked) sortDescending = true;
else sortDescending = false;
colName = legendClassArray_orig[sortBy];
var x0 = x.domain(data.sort(sortDescending
? function(a, b) { return b[colName] - a[colName]; }
: function(a, b) { return b.total - a.total; })
.map(function(d,i) { return d.State; }))
.copy();
state.selectAll(".class" + active_link)
.sort(function(a, b) {
return x0(a.mystate) - x0(b.mystate);
});
var transition = svg.transition().duration(750),
delay = function(d, i) { return i * 20; };
//sort bars
transition.selectAll(".class" + active_link)
.delay(delay)
.attr("x", function(d) {
return x0(d.mystate);
});
//sort x-labels accordingly
transition.select(".x.axis")
.call(xAxis)
.selectAll("g")
.delay(delay);
transition.select(".x.axis")
.call(xAxis)
.selectAll("g")
.delay(delay);
}
});
</script>
State Under 5 Years 5 to 13 Years 14 to 17 Years 18 to 24 Years 25 to 44 Years 45 to 64 Years 65 Years and Over
AL 310504 552339 259034 450818 1231572 1215966 641667
AK 52083 85640 42153 74257 198724 183159 50277
AZ 515910 828669 362642 601943 1804762 1523681 862573
AR 202070 343207 157204 264160 754420 727124 407205
CA 2704659 4499890 2159981 3853788 10604510 8819342 4114496
CO 358280 587154 261701 466194 1464939 1290094 511094
CT 211637 403658 196918 325110 916955 968967 478007
DE 59319 99496 47414 84464 230183 230528 121688
DC 36352 50439 25225 75569 193557 140043 70648
FL 1140516 1938695 925060 1607297 4782119 4746856 3187797
GA 740521 1250460 557860 919876 2846985 2389018 981024
HI 87207 134025 64011 124834 356237 331817 190067
ID 121746 201192 89702 147606 406247 375173 182150
IL 894368 1558919 725973 1311479 3596343 3239173 1575308
IN 443089 780199 361393 605863 1724528 1647881 813839
IA 201321 345409 165883 306398 750505 788485 444554
KS 202529 342134 155822 293114 728166 713663 366706
KY 284601 493536 229927 381394 1179637 1134283 565867
LA 310716 542341 254916 471275 1162463 1128771 540314
ME 71459 133656 69752 112682 331809 397911 199187
MD 371787 651923 316873 543470 1556225 1513754 679565
MA 383568 701752 341713 665879 1782449 1751508 871098
MI 625526 1179503 585169 974480 2628322 2706100 1304322
MN 358471 606802 289371 507289 1416063 1391878 650519
MS 220813 371502 174405 305964 764203 730133 371598
MO 399450 690476 331543 560463 1569626 1554812 805235
MT 61114 106088 53156 95232 236297 278241 137312
NE 132092 215265 99638 186657 457177 451756 240847
NV 199175 325650 142976 212379 769913 653357 296717
NH 75297 144235 73826 119114 345109 388250 169978
NJ 557421 1011656 478505 769321 2379649 2335168 1150941
NM 148323 241326 112801 203097 517154 501604 260051
NY 1208495 2141490 1058031 1999120 5355235 5120254 2607672
NC 652823 1097890 492964 883397 2575603 2380685 1139052
ND 41896 67358 33794 82629 154913 166615 94276
OH 743750 1340492 646135 1081734 3019147 3083815 1570837
OK 266547 438926 200562 369916 957085 918688 490637
OR 243483 424167 199925 338162 1044056 1036269 503998
PA 737462 1345341 679201 1203944 3157759 3414001 1910571
RI 60934 111408 56198 114502 277779 282321 147646
SC 303024 517803 245400 438147 1193112 1186019 596295
SD 58566 94438 45305 82869 196738 210178 116100
TN 416334 725948 336312 550612 1719433 1646623 819626
TX 2027307 3277946 1420518 2454721 7017731 5656528 2472223
UT 268916 413034 167685 329585 772024 538978 246202
VT 32635 62538 33757 61679 155419 188593 86649
VA 522672 887525 413004 768475 2203286 2033550 940577
WA 433119 750274 357782 610378 1850983 1762811 783877
WV 105435 189649 91074 157989 470749 514505 285067
WI 362277 640286 311849 553914 1487457 1522038 750146
WY 38253 60890 29314 53980 137338 147279 65614
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment