Search code examples
dc.jscrossfilter

Nested Grouping with Crossfilter?


New to cross filter and working on a project that needs to maintain summed totals and peak values of those summed totals over a date range.

Tried to utilize the complex reduction from the dc.js examples.

I was able to get the max of a specific make and model over the date range but not the summed total peak (summed by day) for say the Region dimension or color dimension.

The project requires the following charts.

  • A bar chart that shows the total daily peak inventory count by region(s)
  • A selectable menu that has the make & model and their respective daily peak for the data set
  • A selectable menu that has the color and its respective daily peak for the data set

Below is a sample of the data (With the exclusion of the date being converted to a date object.)

var carData = [
    {Date: "11/26/2020", 'Inventory Region': 'SW', 'Make and Model': 'buick enclave' , 'Inventory Count': 12710, Color: 'charcoal' ,'Color Inventory': 3665},
    {Date: "11/26/2020", 'Inventory Region': 'SW', 'Make and Model': 'chevrolet 1500' , 'Inventory Count': 8510, Color: 'brown', 'Color Inventory': 2520},
    {Date: "11/26/2020", 'Inventory Region': 'NE', 'Make and Model': 'chevrolet camaro', 'Inventory Count': 5250, Color: 'silver', 'Color Inventory': 750},
    {Date: "11/26/2020", 'Inventory Region': 'NW', 'Make and Model': 'chevrolet malibu', 'Inventory Count': 4300, Color: 'brown','Color Inventory': 2100},
    {Date: "11/26/2020", 'Inventory Region': 'NW', 'Make and Model': 'dodge coupe', 'Inventory Count': 15100, Color: 'silver', 'Color Inventory': 5200},
    {Date: "11/26/2020", 'Inventory Region': 'NE', 'Make and Model': 'jeep compass', 'Inventory Count': 7300, Color: 'blue', 'Color Inventory': 2300},
    {Date: "11/26/2020", 'Inventory Region': 'NE', 'Make and Model': 'kia forte', 'Inventory Count': 4250,Color: 'white', 'Color Inventory': 2200},
    {Date: "11/26/2020", 'Inventory Region': 'SW', 'Make and Model': 'kia sorento', 'Inventory Count': 9450,Color: 'red', 'Color Inventory': 6525},
    {Date: "11/27/2020", 'Inventory Region': 'SW', 'Make and Model': 'buick enclave' , 'Inventory Count': 11251, Color: 'charcoal' ,'Color Inventory': 2206},
    {Date: "11/27/2020", 'Inventory Region': 'SW', 'Make and Model': 'chevrolet 1500' , 'Inventory Count': 8246, Color: 'brown', 'Color Inventory': 2256},
    {Date: "11/27/2020", 'Inventory Region': 'NE', 'Make and Model': 'chevrolet camaro', 'Inventory Count': 5200, Color: 'silver', 'Color Inventory': 700},
    {Date: "11/27/2020", 'Inventory Region': 'NW', 'Make and Model': 'chevrolet malibu', 'Inventory Count': 4250, Color: 'brown','Color Inventory': 2050},
    {Date: "11/27/2020", 'Inventory Region': 'NW', 'Make and Model': 'dodge coupe', 'Inventory Count': 15000, Color: 'silver', 'Color Inventory': 5100},
    {Date: "11/27/2020", 'Inventory Region': 'NE', 'Make and Model': 'jeep compass', 'Inventory Count': 7200, Color: 'blue', 'Color Inventory': 2200},
    {Date: "11/27/2020", 'Inventory Region': 'NE', 'Make and Model': 'kia forte', 'Inventory Count': 4150,Color: 'white', 'Color Inventory': 2100},
    {Date: "11/27/2020", 'Inventory Region': 'SW', 'Make and Model': 'kia sorento', 'Inventory Count': 8953,Color: 'red', 'Color Inventory': 6058}
];

Solution

  • To put your question another way, you want to group by both color and day, and then find the max day for each color.

    I'm going to assume you are also using dc.js since you mention it in your question. What we'll do is keep a running count of days for each color. Then, in the value accessor, we will find the max.

    This is more efficient than calculating the maximum during group reduction.

    const cf = crossfilter(carData),
      colorDimension = cf.dimension(({Color}) => Color),
      colorDayGroup = colorDimension.group().reduce(
        (p, v) => { // add
          const day = d3.timeDay(v.Date).getTime(); // 2. round to day, convert to int
          p[day] = (p[day] || 0) + v['Color Inventory']; // 3
          return p;
        },
        (p, v) => { // remove
          const day = d3.timeDay(v.Date).getTime(); // round to day, convert to int
          p[day] = (p[day] || 0) - v['Color Inventory']; // 4
          return p;
        },
        () => ({}) // 1. init
      );
    
    1. Each bin is a map from integer days to color sums
    2. We use d3.timeDay to round date objects to days - not needed in this example, but this would matter if the dates have times.
    3. When adding a row to a bin, lookup the day. If it does not exist, default it to zero. Then add the color inventory field.
    4. Same thing for removing a row, but we subtract.

    Use it with a valueAccessor like this:

    chart.valueAccessor(({key, value}) => d3.max(Object.values(value)))
    

    This retrieves all the counts from the day->count object and finds the max count.

    I used this fiddle to test. I changed the value of

    {Date: "11/27/2020", 'Inventory Region': 'NE', 'Make and Model': 'kia forte', 'Inventory Count': 4150,Color: 'white', 'Color Inventory': 2700},
    

    because 11/26 was the max day for every color, and I wanted to exercise the code better.

    To test, I mapped each value using the value accessor:

    console.log(JSON.stringify(colorDayGroup.all().map(({key, value}) => ({key, value: d3.max(Object.values(value))})), null, 2))
    

    Result:

    [
      {
        "key": "blue",
        "value": 2300
      },
      {
        "key": "brown",
        "value": 4620
      },
      {
        "key": "charcoal",
        "value": 3665
      },
      {
        "key": "red",
        "value": 6525
      },
      {
        "key": "silver",
        "value": 5950
      },
      {
        "key": "white",
        "value": 2700
      }
    ]