Data visualization using D3.js
Grouped bar chart, with negative values
license: gpl-3.0 | |
height: 500 | |
scrolling: no | |
border: yes |
<!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> | |