Putting Stack, Group and Dual y-axis Together in a D3 Graph

by

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.