-->

Can I filter data based on an intersection (and) i

2019-02-25 04:59发布

问题:

I have the following example on jsfiddle: https://jsfiddle.net/woolycew65/bp5eavxy/ and am curious if I can select pie slices TP-0 and TP-2 to mean find the patients that have both timepoint 0 and timepoint 2, not 0 or 2.

A sample of the dataset is shown below:

    let data = [
        {patientId: 101, site: "AAA", timepoints: [0, 2, 4, 6, 8, 12, 18, 24]},
        {patientId: 102, site: "AAA", timepoints: [0, 2, 4, 6]},
        {patientId: 103, site: "AAA", timepoints: [8, 12, 18, 24]},
        {patientId: 104, site: "AAA", timepoints: [0, 4, 8, 18]},
        {patientId: 105, site: "AAA", timepoints: [2, 6, 12, 24]},
        {patientId: 501, site: "BBB", timepoints: [0]},
        {patientId: 502, site: "BBB", timepoints: [2]},
        {patientId: 503, site: "BBB", timepoints: [4]},
        {patientId: 504, site: "BBB", timepoints: [6]},
        {patientId: 505, site: "BBB", timepoints: [8]},
        {patientId: 506, site: "BBB", timepoints: [12]},
        {patientId: 507, site: "BBB", timepoints: [18]},
        {patientId: 508, site: "BBB", timepoints: [24]}
    ];

I would like the resulting intersection filter to produce "AAA" (2), "BBB" (0) since there are two patients at site AAA (101 and 102) that have timepoints 0 AND 2 and no patients at site BBB that meet the intersection filter criteria.

Obviously the resulting dataset returns "AAA" (4), "BBB" (2) since there are four patients at site AAA (101, 102, 104, and 105) that have timepoints 0 OR 2 and two patients at site BB that meet the union filter criteria.

If it is not possible via crossfilter then I assume I need to capture some event on the tpPie chart and perform my own filtering and then re-populate crossfilter with the new data.

Any help would be appreciated.

回答1:

Thank you @EthanJewett for the comments and crossfilter example in the above comments. I have been search for this off and on for many months and finally decided to concentrate exclusive to find the answer. @Gordon you might also be interested in this example.

I have been able to put together a final dc.js and crossfilter example where you can choose to filter the timepoints based on an intersection (and) or a union (or).

If you select TP-6 and TP-8 and switch between or / and you can see it work. The and option is saying that I am looking for patients that have timepoints at 6 and 8.

I have added an additional field "Gender" and another site "CCC" to the dataset.

Based on Ethan's guidance the key part that I was missing was defining an additional dimension that will be filtered when the timepoint pie chart is begin filtered as an intersection (and).

The timepoint pie chart dimension is defined below. The true argument lets the dimension know that the data is an array:

let tpDimension = ndx.dimension(function (d) {return d.timepoints;}, true);

The extra dimension is also defined as above except we do not break out the array, thus the true argument is not passed. This dimension is only used in the timepoint pie chart filterHandler.

let tp2Dimension = ndx.dimension(function (d) {return d.timepoints;});

The timepoint pie chart filter handler is overridden to use the extra dimension if the user has selected the and option.

tpPie.filterHandler(function (dimension, filters) {
    if (filters.length === 0) {
       // the empty case (no filtering)
       dimension.filter(null);
    } else if (filters.length === 1 && !filters[0].isFiltered) {
       // single value and not a function-based filter
       dimension.filterExact(filters[0]);
    } else if (filters.length === 1 && filters[0].filterType === 'RangedFilter') {
       // single range-based filter
       dimension.filterRange(filters[0]);
    } else {
       // an array of values, or an array of filter objects
       dimension.filterFunction(function (d) {
           for (var i = 0; i < filters.length; i++) {
                var filter = filters[i];
                if (filter.isFiltered && filter.isFiltered(d)) {
                    return true;
                 } else if (filter <= d && filter >= d) {
                    return true;
                 }
            }
            return false;
        });
     };

   // -------------------------------------------------------------
   // Custom code to handle intersection filtering (AND)
   if (glbTpFilterOption === 'tpAND') {
       tpIntersectFilter(filters);
   }

   return filters;
});

The actual code to perform the timepoint intersect filter is:

    function tpIntersectFilter(filters) {
        if (filters.length === 1) {
            // Do not perform the intersect test when only 1 filter selected
            tp2Dimension.filterAll();
        } else if (filters.length === 0) {
            // Since nothing is filtered we need to perform an intersect
            // based on all keys.
            tp2Dimension.filter(function (d) {
                uniqueObjs = tpGroup.all();
                for (var i = 0; i < uniqueObjs.length; i++) {
                    if (d.indexOf(uniqueObjs[i].key) == -1) return false;
                }
                return true;
            });
        } else {
            tp2Dimension.filter(function (d) {
                // Since we are looking for the intersection, test all 
                // filters, but once there isn't a match get out.
                for (var i = 0; i < filters.length; i++) {
                    if (d.indexOf(filters[i]) == -1) return false;
                }
                return true;
            });
        }
    }

Finally I need to handle when the user is switching between the options:

$('#tpFilterOptions a').on('click', function () {
    var sel = $(this).data('title');
    var tog = $(this).data('toggle');
    $('#' + tog).prop('value', sel);

    $('a[data-toggle="' + tog + '"]').not('[data-title="' + sel + '"]').removeClass('active').addClass('notActive');
    $('a[data-toggle="' + tog + '"][data-title="' + sel + '"]').removeClass('notActive').addClass('active');

    glbTpFilterOption = sel;
    if (sel === "tpOR") {
       tp2Dimension.filterAll();
    } else {
       tpIntersectFilter(tpPie.filters());
    }
    dc.redrawAll();

})