Search code examples
dc.jscrossfilter

crossfilter reduction is crashing


I am unable to render a dc.js stacked bar chart successfully and I receive a console error

unable to read property 'Total' of undefined

I am new to the library and suspect my group or reduce is not successfully specified.

How do I resolve this issue?

 $scope.riskStatusByMonth = function(){

    var data = [ 
                    {"Month":"Jan","High":12},{"Month":"Jan","Med":14},{"Month":"Jan","Low":2},{"Month":"Jan","Closed":8},
                    {"Month":"Feb","High":12},{"Month":"Feb","Med":14},{"Month":"Feb","Low":2},{"Month":"Feb","Closed":8},
                    {"Month":"Mar","High":12},{"Month":"Mar","Med":14},{"Month":"Mar","Low":2},{"Month":"Mar","Closed":8},
                    {"Month":"Apr","High":12},{"Month":"Apr","Med":14},{"Month":"Apr","Low":2},{"Month":"Apr","Closed":8},
                    {"Month":"May","High":12},{"Month":"May","Med":14},{"Month":"May","Low":2},{"Month":"May","Closed":8},
                    {"Month":"Jun","High":12},{"Month":"Jun","Med":14},{"Month":"Jun","Low":2},{"Month":"Jun","Closed":8},
                    {"Month":"Jul","High":12},{"Month":"Jul","Med":14},{"Month":"Jul","Low":2},{"Month":"Jul","Closed":8},
                    {"Month":"Aug","High":12},{"Month":"Aug","Med":14},{"Month":"Aug","Low":2},{"Month":"Aug","Closed":8},
                    {"Month":"Sep","High":12},{"Month":"Sep","Med":14},{"Month":"Sep","Low":2},{"Month":"Sep","Closed":8},
                    {"Month":"Oct","High":12},{"Month":"Oct","Med":14},{"Month":"Oct","Low":2},{"Month":"Oct","Closed":8},
                    {"Month":"Nov","High":12},{"Month":"Nov","Med":14},{"Month":"Nov","Low":2},{"Month":"Nov","Closed":8},
                    {"Month":"Dec","High":8},{"Month":"Dec","Med":6},{"Month":"Dec","Low":13},{"Month":"Dec","Closed":8},
               ]

    data.forEach(function(x) {
      x.Total = 0;
    });

    var ndx = crossfilter(data)

    var xdim = ndx.dimension(function (d) {return d.Month;});

    function root_function(dim,stack_name) {
        return dim.group().reduce(
      function(p, v) {
        p[v[stack_name]] = (p[v[stack_name]] || 0) + v.High;
        return p;},
        function(p, v) {
        p[v[stack_name]] = (p[v[stack_name]] || 0) + v.Med;
        return p;},
        function(p, v) {
        p[v[stack_name]] = (p[v[stack_name]] || 0) + v.Low;     <-------------------here is where error occurs
        return p;},
        function(p, v) {
        p[v[stack_name]] = (p[v[stack_name]] || 0) + v.Closed;
        return p;}, 
      function() {
        return {};
      });}

    var ydim = root_function(xdim,'Total')

    function sel_stack(i) {
    return function(d) {
      return d.value[i];
    };}

    $scope.monthlyRiskStatus = dc.barChart("#risk-status-by-month");

    $scope.monthlyRiskStatus
      .x(d3.scaleLinear().domain(xdim))
      .dimension(xdim)
      .group(ydim, '1', sel_stack("Jan"))
      .xUnits(dc.units.ordinal);


    month = [null,'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    for(var i = 2; i<=12; ++i)
      $scope.monthlyRiskStatus.stack(ydim, ''+i, sel_stack(month[i]));

    $scope.monthlyRiskStatus.render();
}

Solution

  • group.reduce() takes three arguments: add, remove, init.

    You are passing 5.

    Looks like it is trying to call the third one as the initializer, with no arguments, so therefore v is undefined.

    how to stack by level

    It looks like what you're really trying to do is group by month (X axis) and then stack by status or level. Here's one way to do that.

    First, you're on the right track with a function that takes a stack name, but we'll want it to take all of the stack names:

    function root_function(dim,stack_names) {
        return dim.group().reduce(
      function(p, v) {
        stack_names.forEach(stack_name => { // 1
          if(v[stack_name] !== undefined) // 2
              p[stack_name] = (p[v[stack_name]] || 0) + v[stack_name] // 3
        });
        return p;}, 
      function(p, v) {
        stack_names.forEach(stack_name => { // 1
          if(v[stack_name] !== undefined) // 2
              p[stack_name] = (p[v[stack_name]] || 0) + v[stack_name] // 3
        });
        return p;}, 
      function() {
        return {};
      });}
    
    1. In the add and reduce functions, we'll loop over all the stack names
    2. Stack names are fields which may or may not exist in each row. If the stack name exists in the current row...
    3. We'll add or subtract the row field stack_name from the field with the same name in the current bin.

    We'll define both levels and months arrays. levels will be used for stacking and months will be used for the ordinal X domain:

    var levels = ['High', 'Med', 'Low', 'Closed']
    var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    

    When we define the group, we'll pass levels to root_function():

    var ygroup = root_function(xdim,levels)
    

    I see you had some confusion between the English/math definition of "dimension" and the crossfilter dimension. Yes, in English "Y" would be a dimension, but in crossfilter and dc.js, "dimensions" are what you aggregate on, and groups are the aggregations that often go into Y. (Naming things is difficult.)

    We'll use an ordinal scale (you had half ordinal half linear, which won't work):

    $scope.monthlyRiskStatus
      .x(d3.scaleOrdinal().domain(months))
      .dimension(xdim)
      .group(ygroup, levels[0], sel_stack(levels[0]))
      .xUnits(dc.units.ordinal);
    

    Passing the months to the domain of the ordinal scale tells dc.js to draw the bars in that order. (Warning: it's a little more complicated for line charts because you also have to sort the input data.)

    Note we are stacking by level, not by month. Also here:

    for(var i = 1; i<levels.length; ++i)
      $scope.monthlyRiskStatus.stack(ygroup, levels[i], sel_stack(levels[i]));
    

    Let's also add a legend, too, so we know what we're looking at:

      .margins({left:75, top: 0, right: 0, bottom: 20})
      .legend(dc.legend())
    

    screenshot stacked by level

    Demo fiddle.