For a time-series data visualization report, I came up with a bar graph design and the library choice was obviously D3. I love D3 whenever I need to put data driven graphic charts and diagrams.
The graph got a bit complex due to nature of data. Hence to make the interpretation easy, I choose to put a combination of stacked bar with group, and dual y-axis. I thought to share the code sample here which might be useful for beginners who are starting with the usage of D3 JavaScript library for graphs.
Graph Preview
JSON Data Preparation
I have transformed my time-series data to this format which we will use to feed the graph. Most commonly you have “name” and “value” for your data. I have added three more keys: yscale, yoffset and total.
- yscale – 0 to scale with left y-axis, 1 to scale with right y-axis
- yoffset – Used for stacked rectangles within a bar to set the vertical offset. Its the accumulated sum of previous values for each stack.
- total – total height for the bar, its the sum of value of all rectangles in the bar.
In the sample data set, we have count of Android and iPhone users in a small geography and web traffic observed from them over three quarters.
var AllData = [ { date:"Q3-2018", values:[ {name:"Android User Count", value:8589, yoffset:8589, yscale:0, total:13851}, {name:"iPhone User Count", value:5262, yoffset:13851, yscale:0, total:13851}, {name:"Traffic from Android Users", value:51534, yoffset:51534, yscale:1, total:72582}, {name:"Traffic from iPhone Users", value:21048, yoffset:72582, yscale:1, total:72582}, ] }, { date:"Q4-2018", values:[ {name:"Android User Count", value:12552, yoffset:12552, yscale:0, total:20802}, {name:"iPhone User Count", value:8250, yoffset:20802, yscale:0, total:20802}, {name:"Traffic from Android Users", value:62762, yoffset:62762, yscale:1, total:95762}, {name:"Traffic from iPhone Users", value:33000, yoffset:95762, yscale:1, total:95762}, ] }, { date:"Q1-2019", values:[ {name:"Android User Count", value:15456, yoffset:15456, yscale:0, total:27441}, {name:"iPhone User Count", value:11985, yoffset:27441, yscale:0, total:27441}, {name:"Traffic from Android Users", value:61824, yoffset:61824, yscale:1, total:86992}, {name:"Traffic from iPhone Users", value:25168, yoffset:86992, yscale:1, total:86992}, ] } ];
Initialize variables
// Main variables var element = document.getElementById("graph_container"), height = 300; element.innerHTML = ""; // Define main variables var d3Container = d3.select(element), margin = {top: 5, right: 40, bottom: 40, left: 40}, width = d3Container.node().getBoundingClientRect().width - margin.left - margin.right, height = height - margin.top - margin.bottom - 5; var container = d3Container.append("svg"); // Add SVG group var svg = container .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var color = d3.scale.ordinal().range(["#36C265", "#397EFF", "#68F095", "#88B1FE"]);
Tooltip Setup
var round = function(n){return Math.round(n * 100) / 100}; var tip = d3.tip() .attr("class", "d3-tip") .offset([-5, 0]) .html(function(d) { p = d.total>0 ? d.value/d.total : 0; p = round(p*100)+" %"; return d.name + ": "+p+"
" + round(d.value) + " out of "+ round(d.total); }); svg.call(tip);
Defining X-Y Axis
var x0 = d3.scale.ordinal().rangeRoundBands([0, width], .2); var x1 = d3.scale.ordinal(); var y0 = d3.scale.linear().range([height, 0]); var y1 = d3.scale.linear().range([height, 0]); var xAxis = d3.svg.axis().scale(x0).orient("bottom").ticks(5); var yAxisLeft = d3.svg.axis().scale(y0).orient("left").tickFormat(function(d) { return parseInt(d) }); var yAxisRight = d3.svg.axis().scale(y1).orient("right").tickFormat(function(d) { return parseInt(d) }); x0.domain(AllData.map(function(d) { return d.date; })); x1.domain(["Users","Traffic"]).rangeRoundBands([0, x0.rangeBand()], 0.5); y0.domain([0, d3.max(AllData, function(d) { return d.values[0].value+d.values[1].value; })]); y1.domain([0, d3.max(AllData, function(d) { return Math.max(d.values[2].value+d.values[3].value)})]);
Append Ticks and Data
svg.append("g").attr("class", "x axis").attr("transform", "translate(0," + height + ")").call(xAxis); svg.append("g").attr("class", "y0 axis").call(yAxisLeft).append("text").attr("transform", "rotate(-90)").attr("y", 6).attr("dy", ".71em").style("text-anchor", "end").style("fill", "#98abc5").text("Users"); svg.select(".y0.axis").selectAll(".tick").style("fill","#98abc5"); svg.append("g").attr("class", "y1 axis").attr("transform", "translate(" + width + ",0)") .call(yAxisRight).append("text") .attr("transform", "rotate(-90)") .attr("y", -16) .attr("dy", ".71em") .style("text-anchor", "end") .style("fill", "#98abc5") .text("Traffic"); svg.select(".y1.axis") .selectAll(".tick") .style("fill","#98abc5"); //End ticks var graph = svg.selectAll(".date") .data(AllData) .enter() .append("g") .attr("class", "g") .attr("transform", function(d) { return "translate(" + x0(d.date) + ",0)"; }); graph.selectAll("rect") .data(function(d) { return d.values; }) .enter() .append("rect") .attr("width", x1.rangeBand()) .attr("x", function(d) { return x1(stack_key_mapping[d.name]); }) .attr("y", function(d) { return d.yscale==0 ? y0(d.yoffset) : y1(d.yoffset); }) .attr("height", function(d) { return height - (d.yscale==0 ? y0(d.value) : y1(d.value)); }) .style("fill", function(d) { return color(d.name); }) .on("mouseover", tip.show) .on("mouseout", tip.hide);
Adding Legend
var legend = svg.selectAll(".d3-legend") .data(Object.keys(stack_key_mapping)) .enter() .append("g") .attr("class", "d3-legend") .attr("transform", function(d, i) { return "translate(" + ((i * 150 ) + (width/2-370)) + ", "+(height+20)+")"; }); legend.append("rect") .attr("width", 12) .attr("height", 12) .attr("rx", 8) .style("fill", color); legend.append("text") .attr("x", 18) .attr("y", 2) .attr("dy", ".85em") .style("text-anchor", "start") .style("font-size", 10) .text(function(d) { return d; });
The Complete code
Get the complete code and workign example on CodePen. I have also added a few lines of CSS there. Hope the solution has helped.