0

What is a good approach to have a scatter plot in which the data can be edited in the plot itself with a click action?

The idea is to spot outliers in the data in the plot and filter the values in the plot itself, rather than having to change the source data.

Even better would be to remove the data from the crossfilter, but a solution that just filters is acceptable.

Gordon
  • 19,811
  • 4
  • 36
  • 74
  • I'm guessing this was downvoted because it looks more like a feature request than a question. You might [file an enhancement request](https://github.com/dc-js/dc.js/issues/new) on dc.js. Actually it's going to be a lot easier to filter those points out than to delete them from crossfilter; does that work for you? – Gordon Nov 13 '16 at 23:39
  • created enhancement request https://github.com/dc-js/dc.js/issues/1223 – Ramesh Rajagopalan Nov 14 '16 at 15:47
  • I've edited the question - hopefully it should now be clear why this is an appropriate SO question. – Gordon Nov 20 '16 at 18:35

1 Answers1

1

I've come up with a solution using the current dc.js (beta 32).

It does not support the brush (needs to have .brushOn(false)) - I'll explain in the enhancement request why this would need some changes to dc.js.

But it does support clicking on points to toggle them, and the reset link. (Clicking on the background to reset is also possible but not implemented here.)

What we'll do is define our own ExcludePointsFilter with the standard dc.js filter signature:

function compare_point(p1, p2) {
  return p1[0] === p2[0] && p1[1] === p2[1];
}
function has_point(points, point) {
  return points.some(function(p) {
    return compare_point(point, p);
  });
}
function ExcludePointsFilter(points) {
  var points2 = points.slice(0);
  points2.filterType = 'ExcludePointsFilter';
  points2.isFiltered = function(k) {
    return !has_point(points2, k);
  };
  return points2;
}

We'll calculate a new set of points each time one is clicked, and replace the filter:

scatterPlot.on('pretransition.exclude-dots', function() { #1
  // toggle exclusion on click
  scatterPlot.selectAll('path.symbol') #2
      .style('cursor', 'pointer') // #3
      .on('click.exclude-dots', function(d) { // #4
    var p = [d.key[0],d.key[1]];
    // rebuild the filter #5
    var points = scatterPlot.filter() || [];
    if(has_point(points, p))
      points = points.filter(function(p2) {
        return !compare_point(p2, p);
      });
    else
      points.push(p);
    // bypass scatterPlot.filter, which will try to change
    // it into a RangedTwoDimensionalFilter #6
    scatterPlot.__filter(null)
      .__filter(ExcludePointsFilter(points));
    scatterPlot.redrawGroup();
  });
});

Explanation:

  1. Every time the chart is rendered or redrawn, we'll annotate it before any transitions start
  2. Select all the dots, which are path elements with the symbol class
  3. Set an appropriate cursor (pointer-hand may not be ideal but there aren't too many to choose from)
  4. Set up a click handler for each point - use the exclude-dots event namespace to make sure we're not interfering with anyone else.
  5. Get the current filter or start a new one. Look to see if the current point being clicked on (passed as d) is in that array, and either add it or remove it depending.
  6. Replace the current filters for the scatterplot. Since the scatter plot is deeply wed to the RangedTwoDimensionalFilter, we need to bypass its filter override (and also the coordinateGridMixin override!) and go all the way to baseMixin.filter(). Yes this is weird.

For good measure, we'll also replace the filter printer, which normally doesn't know how to deal with an array of points:

scatterPlot.filterPrinter(function(filters) {
  // filters will contain 1 or 0 elements (top map/join is just for safety)
  return filters.map(function(filter) {
    // filter is itself an array of points
    return filter.map(function(p) {
      return '[' + p.map(dc.utils.printSingleValue).join(',') + ']';
    }).join(',');
  }).join(',');
});

Here is a working example in a fiddle: http://jsfiddle.net/gordonwoodhull/3y72o0g8/16/

Note, if you then want to do something with the excluded points, you can read them from scatterPlot.filter() - the filter is the array of points with some annotation. You may even be able to reverse the filter and then call crossfilter.remove() but I'll leave that as an exercise.

Gordon
  • 19,811
  • 4
  • 36
  • 74
  • Thanks a lot for this solution Gordon. The x and y values from the clicks should be enough for me to delete from my base data list or allow user to edit and re-render. – Ramesh Rajagopalan Nov 20 '16 at 23:04