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> | |