Blog

Coloring dc.js Stacked Bar Charts


About Solinea

Solinea services help enterprises build step-by-step modernization plans to evolve from legacy infrastructure and processes to modern cloud and open source infrastructure driven by DevOps and Agile processes.

Better processes and tools equals better customer (and employee) satisfaction, lower IT costs, and easier recruiting, with fewer legacy headaches.

 

The Blog:

I recently went through the exercise of providing custom colors to a dc.js stacked bar chart.  For all of the acceleration that dc.js provides, it is still a work in progress, and one of the missing capabilities seems to be using the .color() and .colorAccessor() functions to fill a stacked rectangle. That said, I tip my hat to @NickQiZhu for writing an extremely useful library.

8

Renderlet Approach

My initial approach ended up as a renderlet to process the rectangles in the bar and apply a color to each based on the key.  It worked, but it took time to apply the colors, and they were initially painted with some default colors.  It still seems like a useful approach should you need to repaint all the bars with a new color scheme or repaint one class of bars based on user interaction.  The renderlet uses the D3.js selection mechanism to find a set of elements, and then uses the style() function to change the fill based on the key of the data associated with the element.  This is a very powerful pattern in D3.js that let’s you do all sorts of customization to discrete elements of the chart.  If you haven’t checked out their example page, it’s definitely worth a visit: 

var colorRenderlet = function (_chart) {

 

function setStyle(selection, keyName) {

selection.style(“fill”, function (d) {

if (d[keyName] == “Apples”)

return “red”;

else if (d[keyName] == “Kiwis”)

return “green”;

else if (d[keyName] == “Lemons”)

return “yellow”;

});

}

// set the fill attribute for the bars

setStyle(_chart

.selectAll(“g.stack”)

.selectAll(“rect.bar”)

, “layer”

);

// set the fill attribute for the legend

setStyle(_chart

.selectAll(“g.dc-legend-item”)

.selectAll(“rect”)

, “name”

);

};

Then you can apply the renderlet to the chart as part of the function chain (highlighted).  Renderlets can be associated with callbacks like onClick or mouseOver, so they provide a very useful way to interact with the data. 

myChart

.width(panelWidth)

.height(panelHeight)

.margins(margin)

.dimension(dim)

.group(frequencies, “Apples”).valueAccessor(function (d) {

return d.value.apples;

})

.stack(frequencies, “Kiwis”, function (d) {

return d.value.kiwis;

})

.stack(frequencies, “Lemons”, function (d) {

return d.value.lemons;

})

.x(d3.time.scale().domain([minDate, maxDate]))

.xUnits(xUnitInterval(interval))

.renderHorizontalGridLines(true)

.centerBar(true)

.elasticY(true)

.brushOn(false)

.renderlet(colorRenderlet)

.legend(dc.legend().x(100).y(0).itemHeight(13).gap(5))

;

I applied some CSS to override the default fill for these elements, which helped with the delay while the renderlet filled the rectangles.  Here’s the CSS:

#fruit-chart rect.bar {

stroke: none;

fill: none;

}

#fruit-chart rect {

stroke: none;

fill: none;

}

That basically leaves the chart empty until the renderlet has finished.  The only unpleasant part is that the legend text is still there without visible rectangles until the renderlet is done.  You can use a spinner for most of the wait, but there’s still a little dead time.

 

CSS Approach

After I sorted that out, I still didn’t like the short wait while the rectangles are painted, so here is an alternative approach using only CSS:

#fruit-chart g.stack._0  > rect.bar {

stroke: none;

fill: red;

}

#fruit-chart g.stack._1 > rect.bar {

stroke: none;

fill: green;

}

#fruit-chart g.stack._2 > rect.bar {

stroke: none;

fill: yellow;

}

#fruit-chart g.dc-legend-item:nth-child(1) > rect:nth-child(1) {

stroke: none;

fill: red;

}

#fruit-chart g.dc-legend-item:nth-child(2) > rect:nth-child(1) {

stroke: none;

fill: green;

}

#fruit-chart g.dc-legend-item:nth-child(3) > rect:nth-child(1) {

stroke: none;

fill: yellow;

}

The CSS approach has the advantage of not having to process the renderlet, but It’s a one time affair, so you may want to keep the renderlet approach in your back pocket in case you need to change the color of elements on the fly.

I put all of this together in a JSFiddle that uses the CSS approach initially, and changes the bar colors using a renderlet if you click on one of them.  That’s about the best I can come up with for the moment.  It’s not perfect, but I hope it helps someone!

Edit in JSFiddle

  • JavaScript
  • Resources
  • HTML
  • CSS
  • Result

   var data = [

   {“date”: “2014/02/09”, “apples”: 10, “kiwis”: 10, “lemons”: 10},

 {“date”: “2014/02/10”, “apples”: 2, “kiwis”: 10, “lemons”: 10},

   {“date”: “2014/02/11”, “apples”: 10, “kiwis”: 2, “lemons”: 10},

   {“date”: “2014/02/12”, “apples”: 10, “kiwis”: 10, “lemons”: 2},

   {“date”: “2014/02/13”, “apples”: 2, “kiwis”: 2, “lemons”: 10},

   {“date”: “2014/02/14”, “apples”: 2, “kiwis”: 10, “lemons”: 2},

   {“date”: “2014/02/15”, “apples”: 10, “kiwis”: 2, “lemons”: 2}

   ];

 

   dateFormat = d3.time.format(“%Y/%m/%d”);

   data.forEach(function (e) {

   e.dd = dateFormat.parse(e.date);

   });

 

   var cssChart = dc.barChart(“#chart”);

   var xf = crossfilter(data);

 

   var dateDim = xf.dimension(function (d) {

   return d.dd;

   });

 

   var eventsByDate = dateDim.group().reduce(

function (p, v) {

   p.apples += v.apples;

   p.kiwis += v.kiwis;

   p.lemons += v.lemons;

   return p;

   },

   function (p, v) {

   p.apples -= v.apples;

 p.kiwis -= v.kiwis;

   p.lemons -= v.lemons;

   return p;

   },

   function () {

   return {

   apples: 0,

   kiwis: 0,

   lemons: 0

};

   }

   );

 

 

   var minDate = dateDim.bottom(1)[0].dd;

   var maxDate = dateDim.top(1)[0].dd;

 

   /* when any bar is clicked, recolor the chart */

   var colorRenderlet = function (_chart) {

   _chart.selectAll(“rect.bar”)

  .on(“click”, function (d) {

   function setAttr(selection, keyName) {

   selection.style(“fill”, function (d) {

   if (d[keyName] == “Apples”) return “#63D3FF”;

  else if (d[keyName] == “Kiwis”) return “#FF548F”;

   else if (d[keyName] == “Lemons”) return “#9061C2”;

   });

   };

   setAttr(_chart.selectAll(“g.stack”).selectAll(“rect.bar”), “layer”)

   setAttr(_chart.selectAll(“g.dc-legend-item”).selectAll(“rect”), “name”)

   });

   };

 

   cssChart

   .margins({top: 50, right: 20, left: 50, bottom: 50})

   .width(500)

   .height(200)

   .gap(50)

   .dimension(dateDim)

   .group(eventsByDate, “Apples”)

   .valueAccessor(function (d) {

   return d.value.apples;

   })

   .stack(eventsByDate, “Kiwis”, function (d) {

   return d.value.kiwis;

   })

   .stack(eventsByDate, “Lemons”, function (d) {

   return d.value.lemons;

   })

   .x(d3.time.scale().domain([minDate, maxDate]))

   .xUnits(d3.time.days)

   .centerBar(true)

   .elasticY(true)

   .brushOn(false)

   .renderlet(colorRenderlet)

   .legend(dc.legend().x(100).y(0).itemHeight(13).gap(5))

   .render();

 


Author: John Stanford