Search code examples
d3.jsdc.jscrossfilter

How can I retrieve the original data row to a dc.js boxplot data-point?


Im using dc.js to create a Boxplot which is working as expected. To add some more interactivity to the chart I wanted to enable the user to click on outliers to retrieve additional information.

Therefore I created an event handler as it is explained in the D3 documentation. The result is working as expected and I get the event fired when a user clicks on the data-point. My expectation was that somehow I will then be able to access the original data to retrieve the sfc attribute to the clicked data-point but I failed and do currently not have any idea how to resolve this. Any help would be greatly appreciated.

const data = [{
    "duration": 248,
    "type": "M248",
    "sfc": "M248BJ0L2809783",
    "pass": 1
  },
  {
    "duration": 249,
    "type": "M248",
    "sfc": "M248BK0L2809676",
    "pass": 1
  },
  {
    "duration": 156,
    "type": "M248",
    "sfc": "M248BK0L2809676",
    "pass": 1
  },
  {
    "duration": 254,
    "type": "M248",
    "sfc": "M248BP0L2809798",
    "pass": 1
  },
  {
    "duration": 134,
    "type": "M248",
    "sfc": "M248BJ0L2809783",
    "pass": 1
  },
  {
    "duration": 128,
    "type": "M248",
    "sfc": "M248BP0L2809798",
    "pass": 0
  },
  {
    "duration": 228,
    "type": "M248",
    "sfc": "M248B90L2809800",
    "pass": 0
  },
  {
    "duration": 125,
    "type": "M248",
    "sfc": "M248B90L2809800",
    "pass": 0
  },
  {
    "duration": 242,
    "type": "M248",
    "sfc": "M248BJ0L2809792",
    "pass": 1
  },
  {
    "duration": 149,
    "type": "M248",
    "sfc": "M248BJ0L2809792",
    "pass": 1
  },
  {
    "duration": 237,
    "type": "M248",
    "sfc": "M248BJ0L2809819",
    "pass": 1
  },
  {
    "duration": 153,
    "type": "M248",
    "sfc": "M248BJ0L2809819",
    "pass": 1
  },
  {
    "duration": 232,
    "type": "M248",
    "sfc": "M248BK0L2809847",
    "pass": 1
  },
  {
    "duration": 482,
    "type": "M248",
    "sfc": "M248BK0L2809847",
    "pass": 1
  },
  {
    "duration": 238,
    "type": "M248",
    "sfc": "M248BK0L2809883",
    "pass": 1
  },
  {
    "duration": 143,
    "type": "M248",
    "sfc": "M248BK0L2809883",
    "pass": 1
  },
  {
    "duration": 213,
    "type": "M247",
    "sfc": "M247B50L2693004",
    "pass": 1
  },
  {
    "duration": 217,
    "type": "M247",
    "sfc": "M247B50L2693004",
    "pass": 0
  },
  {
    "duration": 229,
    "type": "M248",
    "sfc": "M248BC0L2809902",
    "pass": 1
  },
  {
    "duration": 151,
    "type": "M248",
    "sfc": "M248BC0L2809902",
    "pass": 0
  }
];

const cycletimeChart = dc.boxPlot('#cycletime-chart');

const ndx = crossfilter(data),
  typeDimension = ndx.dimension(function(d) {
    return d.type;
  }),
  cycletimeGroupByType = typeDimension.group().reduce(function(p, v) {
    // keep array sorted for efficiency
    p.splice(d3.bisectLeft(p, v.duration), 0, v.duration);
    return p;
  }, function(p, v) {
    p.splice(d3.bisectLeft(p, v.duration), 1);
    return p;
  }, function() {
    return [];
  });

cycletimeChart
  .dimension(typeDimension)
  .group(cycletimeGroupByType)
  .on('pretransition', function(chart) {
    chart.selectAll('circle.outlier').on('click.sfcClick', function(datum, index, nodes) {
      console.log(`Clicked on outlier with array index ${datum}, ${index}, ${nodes}.`);
      //Here I would like to retrieve the the sfc attribute from the original data object.
    });
  });
cycletimeChart.render();
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Boxplot test</title>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
  <script type="text/javascript" src="https://unpkg.com/dc@4/dist/dc.js"></script>
  <link rel="stylesheet" type="text/css" href="https://unpkg.com/dc@4/dist/style/dc.css">
</head>

<body>
  <div id="cycletime-chart"></div>

</body>

</html>


Solution

  • There are three problems here:

    1. Since crossfilter aggregates the data, only what you provide in the reduce function will be available.
    2. The boxplot binds the array index to the outliers, rather than the data.
    3. You'll run into trouble with filtering if those durations are not unique.

    1. Passing the entire row through crossfilter

    Since crossfilter is all about aggregation, the original data is not available by default. One workaround is to change the reduction to store v instead of v.duration:

      bisectLeft = d3.bisector(d => d.duration).left,
      cycletimeGroupByType = typeDimension.group().reduce(function(p, v) {
        // keep array sorted for efficiency
        p.splice(bisectLeft(p, v.duration), 0, v);
        return p;
      }, function(p, v) {
        p.splice(bisectLeft(p, v.duration), 1);
        return p;
      }, function() {
        return [];
      });
    

    Since dc.boxPlot expects an array of numbers, you'll also need to change the valueAccessor to extract the durations:

      .valueAccessor(d => d.value.map(r => r.duration))
    

    2. Getting the datum using the array index

    As described in Alignment and Colors of Data Point Outliers of Box Plot, the dc.js boxplot binds the array index to the outlier dots.

    Since the crossfilter key/value pair is bound to the parent <g> element, the original datum can be retrieved with the incantation

    d3.select(this.parentNode).datum().value[datum]
    

    3. Non-unique durations

    In step one we started saving the rows of data in the reduction, instead of just the durations. There is a problem if these durations are not unique: the wrong value might get removed from the array when the row is filtered out.

    To be more careful, we should scan for the exact value instead of removing the first entry with the same duration:

      function(p, v) {
        let i = d3.bisectLeft(p, v.duration);
        while(p[i] !== v) ++i;
        p.splice(i, 1);
        return p;
      }
    

    Caveat: this is untested, since there is no filtering in your example.

    const data = [{
        "duration": 248,
        "type": "M248",
        "sfc": "M248BJ0L2809783",
        "pass": 1
      },
      {
        "duration": 249,
        "type": "M248",
        "sfc": "M248BK0L2809676",
        "pass": 1
      },
      {
        "duration": 156,
        "type": "M248",
        "sfc": "M248BK0L2809676",
        "pass": 1
      },
      {
        "duration": 254,
        "type": "M248",
        "sfc": "M248BP0L2809798",
        "pass": 1
      },
      {
        "duration": 134,
        "type": "M248",
        "sfc": "M248BJ0L2809783",
        "pass": 1
      },
      {
        "duration": 128,
        "type": "M248",
        "sfc": "M248BP0L2809798",
        "pass": 0
      },
      {
        "duration": 228,
        "type": "M248",
        "sfc": "M248B90L2809800",
        "pass": 0
      },
      {
        "duration": 125,
        "type": "M248",
        "sfc": "M248B90L2809800",
        "pass": 0
      },
      {
        "duration": 242,
        "type": "M248",
        "sfc": "M248BJ0L2809792",
        "pass": 1
      },
      {
        "duration": 149,
        "type": "M248",
        "sfc": "M248BJ0L2809792",
        "pass": 1
      },
      {
        "duration": 237,
        "type": "M248",
        "sfc": "M248BJ0L2809819",
        "pass": 1
      },
      {
        "duration": 153,
        "type": "M248",
        "sfc": "M248BJ0L2809819",
        "pass": 1
      },
      {
        "duration": 232,
        "type": "M248",
        "sfc": "M248BK0L2809847",
        "pass": 1
      },
      {
        "duration": 482,
        "type": "M248",
        "sfc": "M248BK0L2809847",
        "pass": 1
      },
      {
        "duration": 238,
        "type": "M248",
        "sfc": "M248BK0L2809883",
        "pass": 1
      },
      {
        "duration": 143,
        "type": "M248",
        "sfc": "M248BK0L2809883",
        "pass": 1
      },
      {
        "duration": 213,
        "type": "M247",
        "sfc": "M247B50L2693004",
        "pass": 1
      },
      {
        "duration": 217,
        "type": "M247",
        "sfc": "M247B50L2693004",
        "pass": 0
      },
      {
        "duration": 229,
        "type": "M248",
        "sfc": "M248BC0L2809902",
        "pass": 1
      },
      {
        "duration": 151,
        "type": "M248",
        "sfc": "M248BC0L2809902",
        "pass": 0
      }
    ];
    
    const cycletimeChart = dc.boxPlot('#cycletime-chart');
    
    const ndx = crossfilter(data),
      typeDimension = ndx.dimension(function(d) {
        return d.type;
      }),
      bisectLeft = d3.bisector(d => d.duration).left,
      cycletimeGroupByType = typeDimension.group().reduce(function(p, v) {
        // keep array sorted for efficiency
        p.splice(bisectLeft(p, v.duration), 0, v);
        return p;
      }, function(p, v) {
        let i = bisectLeft(p, v.duration);
        while(p[i] !== v) ++i;
        p.splice(i, 1);
        return p;
      }, function() {
        return [];
      });
    
    cycletimeChart
      .dimension(typeDimension)
      .group(cycletimeGroupByType)
      .valueAccessor(d => d.value.map(r => r.duration))
      .on('pretransition', function(chart) {
        chart.selectAll('circle.outlier').on('click.sfcClick', function(datum, index, nodes) {
          console.log(`Clicked on outlier with datum, index, nodes ${datum}, ${index}, ${nodes}.`);
          console.log('parent array', d3.select(this.parentNode).datum().value);
          console.log(`Original datum`, d3.select(this.parentNode).datum().value[datum]);
          //Here I would like to retrieve the the sfc attribute from the original data object.
        });
      });
    cycletimeChart.render();
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <title>Boxplot test</title>
      <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"></script>
      <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
      <script type="text/javascript" src="https://unpkg.com/dc@4/dist/dc.js"></script>
      <link rel="stylesheet" type="text/css" href="https://unpkg.com/dc@4/dist/style/dc.css">
    </head>
    
    <body>
      <div id="cycletime-chart"></div>
    
    </body>
    
    </html>