Search code examples
javascriptchartsdisposedc.jscrossfilter

dc.js: Problems with dispose() and deregisterChart()


I am creating an application where I can add and remove charts dynamically. Everything works fine but deleting a chart.

The problem I'm having is occurring when I have a line chart and a bar chart, one of them being a composite, and I delete one of them (dispose the dimension and de-register the chart). At this point the chart that I have not disposed of stops reacting to filters from other charts and stops reacting to its own events even thought the other charts get filtered by them. Also, as you can see in the fiddle, the line chart I removed from dc is still reacting to the other chart filters.

I found out that the problem is on the deregisterChart dc call but since my application can create and delete infinity charts I need a way of removing from dc the ones I don't need any more without breaking the ones left behind.

This is the code:

resetFilter = function() {
  lineData.dispose()
  dc.deregisterChart(linechart);
}


for (var j = 0; j < axis.length; j++) {

    var dimData = __cfArray[dataId].dimension(function(d) {
      return d[axis[j].xaxis];
    });
    var barGroup = getGroup(dimData, axis[j].xaxis, axis[j].yaxis, operation, isDate);

    barCharts.push(dc.barChart(composite)
      .valueAccessor(accesor)
      .dimension(dimData)
      .group(barGroup, xAxisTitle[j])
      .transitionDuration(1000)
      .gap(gap)
      .colors(barColors[j])
      .centerBar(function() {
        if (axis.length > 1) return false;
        else return true;
      })
      .title(function(d) {
        if (operation === count) {
          if (isDate) return format(d.key) + ": " + d.value.count;
          else return d.key + ": " + d.value.count;
        } else {
          if (isDate) return format(d.key) + ": " + d.value.total;
          else return d.key + ": " + d.value.total;
        }
      })
    );


    lineData = __cfArray[dataId].dimension(function(d) {
      return d[axis[j].xaxis];
    });
    var lineGroup = getGroupLine(lineData, axis[j].xaxis, axis[j].yaxis, true, operation, isDate);

    linechart = dc.lineChart(lineDom)
      .dimension(lineData)
      .group(lineGroup, xAxisTitle[j] + "/" + yAxisTitle)
      .useRightYAxis(true)
      .colors(lineColors[j])
      .title(function(d) {
        if (operation === count) {
          if (isDate) return format(d.key) + ": " + d.valueCount;
          else return d.key + ": " + d.valueCount;
        } else {
          if (isDate) return format(d.key) + ": " + d.valueTotal;
          else return d.key + ": " + d.valueTotal;
        }
      })
      .valueAccessor(function(p) {
        if (operation === count) {
          return p.valueCount;
        } else {
          return p.valueTotal;
        }
      });

  }

  var xMine;


  var dom = [];
  for (var i = 0; i < axis.length; i++) {
    if (dom.length === 0) {
      dom = __dataArray[dataId].map(function(d) {
        return d[axis[i].xaxis]
      });
    } else {
      dom = dom.concat(__dataArray[dataId].map(function(d) {
        return d[axis[i].xaxis]
      }));
    }
  }
  if (isNaN(dom[0])) {
    xMine = d3.scale.ordinal().domain(dom.sort());
  } else {
    xMine = d3.scale.ordinal().domain(dom.sort(function(a, b) {
      return a - b;
    }));
  }
  composite.xUnits(dc.units.ordinal)
  linechart.xUnits(dc.units.ordinal)


  linechart.width(width)
    .height(height)
    .margins(margin)
    .x(xMine)
    .elasticY(true)
    .legend(dc.legend().x(80).y(10).itemHeight(13).gap(5))
    ._rangeBandPadding(1)
    .brushOn(false);

  composite.width(width)
    .height(height)
    .margins(margin)
    .x(xMine)
    .rightYAxisLabel(yAxisRightTitle)
    .elasticY(true)
    .legend(dc.legend().x(80).y(10).itemHeight(13).gap(5))
    ._rangeBandPadding(1)
    .brushOn(false)
    .shareTitle(false)
    .mouseZoomable(true)
    .yAxisPadding('10%')
    .compose(barCharts)
    .renderHorizontalGridLines(true);

  composite.yAxis().tickFormat(d3.format('s'));
  composite.rightYAxis().tickFormat(d3.format('s'));

  composite.render();

  linechart.render();

This is the fiddle I created with my problem: https://jsfiddle.net/nofknndf/9/

Thanks!


Solution

  • This is a complex and sophisticated system you've got going, congratulations.

    I will admit I didn't get to the bottom of why you are seeing the current strange behavior, but I did find the problem and I think I fixed it.

    In getGroupLine, you're actually creating a group every single time the fake-group .all() method is called:

        function getGroupLine(dimension, xaxis, yaxis, isCum, operation, isDate){
            return {
                all:function () {
            // ...
                    var _group = dimension.group().reduce(reduceAdd, reduceRemove, reduceInitial).orderNatural();
    

    That is apt to get confusing pretty quick! I discovered this by stepping into dimension.dispose() in the debugger, to see if it was working - and each time it ran there were more and more groups.

    Groups are stored in the dimension, and can be disposed independently.

    You want to create the "base group" _group when the function is called, and the returned fake group will just fetch _group.all():

        function getGroupLine(dimension, xaxis, yaxis, isCum, operation, isDate){
            var reduceAdd = function(p, v) {
                if (!(isDate && v[xaxis] === 0 )) {
                    ++p.count;
                    p.total += v[yaxis];
                }
                return p;
            }
    
            var reduceRemove = function(p, v) {
                if (!(isDate && v[xaxis] === 0 )) {
                    --p.count;
                    p.total -= v[yaxis];
                }
                return p;
            }
    
            var reduceInitial = function() {
                return {
                    count: 0, 
                    total: 0,
                    elements: []
                };
            }
            var _group = dimension.group().reduce(reduceAdd, reduceRemove, reduceInitial).orderNatural();
            return {
                all:function () {
                    var totalCount = 0;
                    var total = 0;
                    var g = [];
    
    
                    _group.all().forEach(function(d,i) {
                        if(isCum){
                            totalCount += d.value.count;
                            total += d.value.total;
                        } else {
                            totalCount = d.value.count;
                            total = d.value.total;
                        }
                        g.push({key:d.key,valueTotal:total,valueCount:totalCount})
                    });
                    return g;
                }
            }; 
        }
    

    This seems to explain why the line chart was continuing to be filtered.

    The other problem is that the wrong chart gets deregistered. This is because each of your charts needs to have its own unique anchor (id) - in your example, they were both named "composite". dc.js relies on comparing the anchor names when it is deregistering, so if both charts have the same id, the wrong one will get removed. HTML requires ids to be unique, so you're likely to run into other problems with this.

    Renaming the id for the line chart to "line" fixes the deregistration.

    Updated fork of your fiddle here: https://jsfiddle.net/dnrxxjyo/4/