Jovan Damjanovic
Jovan Damjanovic

Reputation: 43

How do I filter a stacked line chart by stack in dc.js?

I am making a stacked line chart for a dashboard:

var json = [...]
var timeFormat = d3.time.format.iso;
json = json.map(function(c){
    c.date = timeFormat.parse(c.date);
  return c;
});
var data = crossfilter(json);
var days = data.dimension(function (d) {
  return d.date;
});
var minDate = days.bottom(1)[0].date;
var maxDate = days.top(1)[0].date;

var lineValues = days.group().reduce(function (acc, cur) {
  acc[cur.line] = (acc[cur.line] || 0) + 1
  return acc;
}, function (acc, cur) {
  acc[cur.line] = (acc[cur.line] || 0) - 1
  return acc;
}, function () {
  return {};
});

var personChart = dc.lineChart("#graph");
personChart
  .turnOnControls(true)
  .width(600).height(350)
  .dimension(days)
  .group(lineValues, "completed")
        .valueAccessor(function (d) {
            return d.value.completed || 0;
        })
        .stack(lineValues, "assigned", function (d) {
            return d.value.assigned || 0;
        })
        .stack(lineValues, "inactive", function (d) {
            return d.value.inactive || 0;
        })
        .stack(lineValues, "active", function (d) {
            return d.value.active || 0;
        })
        .stack(lineValues, "new", function (d) {
            return d.value.new || 0;
        })
        .stack(lineValues, "temp", function (d) {
            return d.value.temp || 0;
        })
        .elasticY(true)
  .renderArea(true)
  .x(d3.time.scale().domain([minDate, maxDate]))
  .ordinalColors(colorScale)
  .legend(dc.legend().x(50).y(10).itemHeight(13).gap(5).horizontal(true));
dc.renderAll();

Fiddle here

It is working fine so far, but I reached an obstacle. I need to implement an option to filter the chart by individual stacks. Is this possible in dc.js? I can modify and rewrite the entire code if necessary as well as ask my client to remodel the data differently, if needed. There are other fields in the data that I filter on for other charts so preserving that functionality is important.

Upvotes: 1

Views: 610

Answers (1)

Gordon
Gordon

Reputation: 20150

By design, dc.js has a lot of "leaky abstractions", so there is usually a way to get at the data you want, and customize the behavior by dropping down to d3, even if it's functionality that wasn't anticipated by the library.

Your workaround of using a pie chart is pretty reasonable, but I agree that clicking on the legend would be better.

Here's one way to do that:

var categories = data.dimension(function (d) {
  return d.line;
});
personChart
  .on('renderlet', function(chart) {
    chart.selectAll('.dc-legend-item')
    .on('click', function(d) {
      categories.filter(d.name);
      dc.redrawAll();
    })
  });

Basically, once the chart is done drawing, we select the legend items and replace the click behavior which our own, which filters on another dimension we've created for the purpose.

This does rely on the text of the legend matching the value you want to filter on. You might have to customize the undocumented interface .legendables() between the legend and its chart, if this doesn't match your actual use case, but it works here.

This fork of your fiddle demonstrates the functionality: https://jsfiddle.net/gordonwoodhull/gqj00v27/8/

I've also added a pie chart just to illustrate what is going on. You can have the legend filter via the pie chart by doing

  catPie.filter(d.name);

instead of

  categories.filter(d.name);

This way you can see the resulting filter in the slices of the pie. You also can get the toggle behavior of being able to click a second time to go back to the null selection, and clicking on multiple categories. Leave a comment if the toggle behavior is desired and I try to come up with a way to add that without using the pie chart.

Sometimes it seems like the legend should be its own independent chart type...

Upvotes: 3

Related Questions