mrtc
mrtc

Reputation: 163

Stacking percentages out of total using dc.js

I have a data similar to

enter image description here

 Month      Material     Sales
   2            A         500
   2            A         300
   5            A         700
   1            B         400
   2            B         300
   4            C        1200
   2            C         500

I would like to display percentage of each material sales over total sales under Month dimension with dc.rowChart.

On month 2 for material A percentage will be %50 . Because on month 2 total sales are 1600 and A's sales are 800 . For material B percentage will be %18,75 because B's sales are 300 on month 2 . and so on.

So far i did below logic . But it doesn't display any data

var monthDim=ndx.dimension(function (d) {return +d.Month;});                                       

   var totalGroup = monthDim.group().reduce(
    /* callback for when data is added to the current filter results */
    (p, v) => {
        ++p.count;       
        p.Sales += v.Sales;          
        return p;
    },
    /* callback for when data is removed from the current filter results */
    (p, v) => {
        --p.count;       
        p.Sales -= v.Sales;           
        return p;
    },
    /* initialize p */
    () => ({
        count: 0,
        Sales: 0,      
    })
);

then find total sales :

var salesTotal= ndx.dimension(function (d) { return d.Sales; });
var salesTotalGroup = salesTotal.groupAll().reduceSum(function (d) { return d.Sales; });

Now I want to combine these to variable on bar chart. I know they don't seem to work together. But that is what I came up with.

var chart= dc.rowChart('#salespercentagechart')
                 .width(400)
                 .height(350)
                 .elasticX(true)
                 .dimension(monthDim)
                 .group(totalGroup )      
                 .valueAccessor(function(p) { return p.value.Sales / salesTotalGroup;} )                
                 .ordering(function (d) { return -d.key; })

Any idea is perfect for me. Thanks.

Upvotes: 1

Views: 195

Answers (1)

Gordon
Gordon

Reputation: 20150

Counting sum of each category and total

You can use the crossfilter group custom reduction to calculate the total for each material at the same time as the overall total:

var totalGroup = monthDim.group().reduce(
    /* callback for when data is added to the current filter results */
    (p, v) => {
        p.byMaterial[v.Material] = (p.byMaterial[v.Material] || 0) + v.Sales;  
        p.total += v.Sales;
        return p;
    },
    /* callback for when data is removed from the current filter results */
    (p, v) => {
        p.byMaterial[v.Material] -= v.Sales;  
        p.total -= v.Sales;
        return p;
    },
    /* initialize p */
    () => ({
        byMaterial: {},
        total: 0,      
    })
);

This is sort of the canonical way to aggregate multiple stacks at once1

  • keep an object keyed on the stack names
  • when adding, treat undefined as 0 by using || 0
  • when removing, p.byMaterial[v.Material] will always be defined so -= is safe

Now totalGroup.all() will produce

[
  {
    "key": 1,
    "value": {
      "byMaterial": {
        "B": 400
      },
      "total": 400
    }
  },
  {
    "key": 2,
    "value": {
      "byMaterial": {
        "A": 800,
        "B": 300,
        "C": 500
      },
      "total": 1600
    }
  },
  {
    "key": 4,
    "value": {
      "byMaterial": {
        "C": 1200
      },
      "total": 1200
    }
  },
  {
    "key": 5,
    "value": {
      "byMaterial": {
        "A": 700
      },
      "total": 700
    }
  }
]

Initializing stacks in a loop

It's convenient to define the chart stacks in a loop:

var materials = d3.set(data, d => d.Material);
materials.values().sort().forEach((material, i) => {
  const accessor = d => (d.value.byMaterial[material] || 0) / d.value.total * 100;
  if(i === 0)
    chart.group(totalGroup, material, accessor);
  else
    chart.stack(totalGroup, material, accessor);  
});

We use d3.set to find all the unique values of d.Material, then loop over them. dc.js has an annoying design bug that you have to call .group() the first time even though it has the same parameters as .stack(), thus the if(i === 0).

The accessor computes percentage

const accessor = d => (d.value.byMaterial[material] || 0) / d.value.total * 100;

It reads byMaterial, again defaulting undefined to 0 if that material did not exist in the month, then divides by the total and multiplies by 100 to get the percentage.

Rest of chart definition

var chart= dc.lineChart('#salespercentagechart')
    .width(400)
    .height(350)
    .renderArea(true)
    .elasticX(true)
    .dimension(monthDim)
    .x(d3.scaleLinear()).elasticX(true)
    .legend(dc.legend().x(300).y(50))
    //.ordering(function (d) { return -d.key; });

screenshot of stacked area/line chart

Fiddle demo.

Upvotes: 1

Related Questions