Reputation: 163
I have a data similar to
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
Reputation: 20150
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
|| 0
p.byMaterial[v.Material]
will always be defined so -=
is safeNow 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
}
}
]
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)
.
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.
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; });
Upvotes: 1