javascriptdc.js

dc.js Scatter Plot sum up X and Y values by key


My data set in general looks like this

var records = [
    {name : "A", duration : 1, quantity : 2},
    {name : "A", duration : 2, quantity : 1},
    {name : "B", duration : 1, quantity : 4},
    {name : "A", duration : 3, quantity : 1},
    {name : "B", duration : 1, quantity : 1},
];

Using dc.js I am trying to create a scatter plot which creates a sum by the name of both values (duration and quantiy). Thus in the above example I would like to see a scatter plot with only two dots, like this:

Scatter

There is something I am doing wrong either with the dimension or the group because none of my dots appear in the plot.

var cfilter = crossfilter(records),
    dim     = cfilter.dimension(d => d.name),
    grp     = dim.group().reduce(
        (p, v) => [ p[0] + v.duration, p[1] + v.quantity ],
        (p, v) => [ p[0] - v.duration, p[1] - v.quantity ],
        () => [0, 0]
    );
var chart = dc.scatterPlot("#scatter_plot");
chart
    .dimension(dim)
    .group(grp)
    .x(d3.scaleLinear());

I'm assuming that my reduce function is correct. Atleast when I look at the output of grp.all() the datasets seem to be summed up correctly.

Result

But as already mentioned my plot remains empty.


Solution

  • It takes half a dozen little tricks to make this work.

    First, we need the coordinates in the key, not the value. We can use a "fake group" to flip key and value on the fly:

    function flip_key_value(group) {
      return {
        all: () => group.all().map(({key, value}) => ({key: value, value: key}))
      };
    }
    
    chart
      .group(flip_key_value(grp))
    

    Next, we need to map colors based on the value (now the name):

    chart
      .colorAccessor(({value}) => value)
    

    We need to set up both scales, use elastic and padding (for convenience), and set the axis labels:

    chart
      .x(d3.scaleLinear())
      .y(d3.scaleLinear())
      .elasticX(true).elasticY(true)
      .xAxisPadding(0.25).yAxisPadding(0.25)
      .xAxisLabel('duration').yAxisLabel('quantity')
    

    Finally, we need to set a custom .legendables() method, because by default the legend displays different series for a series chart, but we want each dot to have its own color:

    chart.legendables = () => chart.group().all()
        .map(({key, value}) => ({chart, name: value, color: chart.colors()(value)}));
    

    Note we are setting the colors consistent with the colorAccessor specified above.

    And finally, set margins to fit the legend on the right, and declare the legend:

    chart
      .margins({left: 35, top: 0, right: 75, bottom: 30})
      .legend(dc.legend().x(425).y(50));
    

    Screenshot (I think you have Xs and Ys flipped in your desired output):

    scatterplot screenshot

    Fiddle demo.

    Implementing filtering

    In dc.js and crossfilter, filtering works by setting a filter on the dimension when the user makes a selection.

    Since we have changed keys, the dimension is now inconsistent with the group data being displayed, and selection will cause an invalid filtering operation, causing all data to be erased from other charts.

    In order to rectify this, we can search for any points falling within the rectangular selection, and filter by the original keys, which are now the values:

    chart.filterHandler((dimension, filters) => {
      if(filters.length === 0) {
        dimension.filter(null);
        return filters;
      }
      console.assert(filters.length && filters[0].filterType === 'RangedTwoDimensionalFilter');
      const filterFun = filters[0].isFiltered;
      const itemsInside = chart.group().all().filter(({key}) => filterFun(key));
      const origKeys = itemsInside.map(({value}) => value);
      if(origKeys.length === 1)
        dimension.filterExact(origKeys[0]);
      else
        dimension.filterFunction(k => origKeys.includes(k));
      return filters;
    });
    

    New fiddle version, with working selection.