Skip to content

Instantly share code, notes, and snippets.

@Rdilorenzo73
Last active February 10, 2018 21:41
Show Gist options
  • Save Rdilorenzo73/d3ef804fca7ed0ddaf67a0fb74f76682 to your computer and use it in GitHub Desktop.
Save Rdilorenzo73/d3ef804fca7ed0ddaf67a0fb74f76682 to your computer and use it in GitHub Desktop.
D3 Grouped Bar Chart
license: gpl-3.0
height: 500
scrolling: no
border: yes

Data visualization using D3.js

Grouped bar chart, with negative values

<!DOCTYPE html>
<meta charset="utf-8">
<style>
/*hides tick marks on bottom xaxis */
.axis line{
visibility:hidden;
}
/* hides bottom xaxis line*/
.axis .domain {
display: none;
}
.axis {
font: 13px sans-serif;
}
.yUnits {
font: 14px sans-serif;
}
.caption {
font: 12px sans-serif;
}
.chartDisplayTitle{
fill:#354B5F;
font-weight: bold;
font: 20px sans-serif;
}
</style>
<svg class="chart" width="960" height="590" aria-labelledby="graph-title" aria-describedby="graph-desc">
<title>GDP Growth Remains Broad Based</title>
<desc id="graph-desc">GDP Growth Remains Broad Based, with values for 2017 quarters 1-3.</desc>
<text transform="translate(10, 20)" class="chartDisplayTitle">Chart1</text>
<text id="graph-title" transform="translate(10, 45)" class="chartDisplayTitle">GDP Growth Remains Broad Based</text>
<text transform="translate(10, 70)" class="yUnits">Percentage points*</text>
<text transform="translate(10, 570)" class="caption">*Contribution to total gross domestic product (GDP) growth; seasonally adjusted annualized rate.</text>
<text transform="translate(10, 585)" class="caption">SOURCE: Bureau of Economic Analysis.</text>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
/*
Derived from:
Grouped Bar Chart
https://bl.ocks.org/mbostock/3887051
Bar Chart with Negative Values
https://bl.ocks.org/mbostock/2368837
*/
var econ2 = [
{
"Category": "GDP",
"2017 Q1": 1.2,
"2017 Q2": 3.1,
"2017 Q3 First Estimate": 3.0
},
{
"Category": "Consumption",
"2017 Q1": 1.3,
"2017 Q2": 2.2,
"2017 Q3 First Estimate": 1.6
},
{
"Category": "Nonresidential investment",
"2017 Q1": 0.9,
"2017 Q2": 0.8,
"2017 Q3 First Estimate": 0.5
},
{
"Category": "Residential investment",
"2017 Q1": 0.4,
"2017 Q2": -0.3,
"2017 Q3 First Estimate": -0.2
},
{
"Category": "Inventories",
"2017 Q1": -1.5,
"2017 Q2": 0.1,
"2017 Q3 First Estimate": 0.7
},
{
"Category": "Net exports",
"2017 Q1": 0.2,
"2017 Q2": 0.2,
"2017 Q3 First Estimate": 0.4
},
{
"Category": "Government",
"2017 Q1": -0.1,
"2017 Q2": 0.0,
"2017 Q3 First Estimate": 0.0
}
]
//chart setup
var svg = d3.select("svg"),
margin = {top: 80, right: 10, bottom: 80, left: 25},
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 + ")");
//y position calculation function
var y = d3.scaleLinear()
.domain([-2, 4])
.range([height, 0]);
var x0 = d3.scaleBand() // domain defined below
.rangeRound([0, width])
.paddingInner(0.1)
.paddingOuter(0.1);
var x1 = d3.scaleBand() // domain and range defined below
.paddingOuter(0.25)
.paddingInner(0.15);
//blue, tan, red colors
var z = d3.scaleOrdinal()
.range(["#BC151E", "#D3B178", "#354B5F"]);
//reference to the y axis
//axisLeft put labels on left side
//ticks(n) refers to # of increment marks on the axis
const yAxis = d3.axisLeft(y).ticks(7);
//examine first object, retrieve the keys, and discard the first key
//return resulting array of keys
// [ "2017 Q1", "2017 Q2", "2017 Q3 First Estimate"]
var subCategories = Object.keys(econ2[0]).slice(1);
//use new array from just the Category values for the bottom x-axis
x0.domain(econ2.map( d => d.Category ));
//array of quarterly value names, fitted in the available bottom categories (x0.bandwidth())
x1.domain(subCategories).rangeRound([0, x0.bandwidth()])
// Add bar chart
var selection = g.selectAll("g")
.data(econ2)
.enter().append("g")
.attr("transform", d => "translate(" + x0(d.Category) + ",0)" )
selection.selectAll("rect")
//Use map function with the subCategories array and the Econ2 array
.data(function(d) { return subCategories.map(function(key) { return {key: key, value: d[key]}; }); })
.enter().append("rect")
.attr("x", d => x1(d.key) )
//If the value is negative, put the top left corner of the rect bar on the zero line
.attr("y", d => (d.value<0 ? y(0) : y(d.value)) )
.attr("width", x1.bandwidth())
.attr("height", d => Math.abs(y(d.value) - y(0)) )
.attr("fill", d => z(d.key) )
//can not nest the text element inside the rect element !
selection.selectAll("text")
.data(function(d) { return subCategories.map(function(key) { return {key: key, value: d[key]}; }); })
.enter().append("text")
.attr("x", d => x1(d.key) )
//offset the position of the y value (positive / negative) to have the text over/under the rect bar
.attr("y", d => d.value<=0 ? y(0) - (y(4) - (Math.abs(y(d.value) - y(0)) + 20)) : y(d.value) - 10)
.style('fill', d => z(d.key))
.style('font-size', '1.25em')
//make sure one just decimal place is displayed
.text(d => Number.parseFloat(d.value).toFixed(1))
//add the x-axis
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0))
.selectAll(".tick text")
//use wrap function to wrap long lines in labels
.call(wrap, x0.bandwidth());
//add the y-axis - notice it does not have css class 'axis'
g.append('g')
.call(yAxis)
//idenitfy zero line on the y axis.
g.append("line")
.attr("y1", y(0))
.attr("y2", y(0))
.attr("x1", 0)
.attr("x2", width)
.attr("stroke", "black");
var legend = g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 13)
.attr("text-anchor", "end")
.selectAll("g")
.data(subCategories)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 24 + ")"; });
legend.append("rect")
.attr("x", width - 142)
.attr("width", 8)
.attr("height", 8)
.attr("fill", z);
legend.append("text")
.attr("x", d => d.length > 7 ? (width + 5) : (width - 80))
.attr("y", 5.5)
.attr("dy", "0.22em")
.text(d => (d));
//https://bl.ocks.org/mbostock/7555321 - wrap long labels
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment