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:
- Every time the chart is rendered or redrawn, we'll annotate it before any transitions start
- Select all the dots, which are
path
elements with the symbol
class
- Set an appropriate cursor (pointer-hand may not be ideal but there aren't too many to choose from)
- Set up a click handler for each point - use the
exclude-dots
event namespace to make sure we're not interfering with anyone else.
- 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.
- 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.