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

See the Pen
D3 Bar with Stack, Group and Dual y-axis
by San (@sank3)
on CodePen.

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.