dc.js/crossfilter -> How to sort data in dc.js chart (like row) - Ascending x Descending - sorting

How to sort data in dc.js chart (like row) - Ascending x Descending
I want to reorder the chart (row/column) by specified attribute (like 'avg' -> ascending)
I'm trying to use ".top()"... but unsuccessfully
Thanks
draft below
jsfiddle -> ewr5Z/2/

You can use the ordering method:
chart.ordering(function(d){ return -d.value })
If you write a custom reduce, you can have more flexibility:
priceDimension = ndx.dimension(function(d) {return d.part_number; });
priceGroup = priceDimension.group().reduce(
function (p, v) {
++p.count;
p.sumPrice += v.price;
p.avgPrice = p.sumPrice/p.count;
return p;
},
function (p, v) {
--p.count;
p.sumPrice -= v.price;
p.avgPrice = p.sumPrice/p.count;
return p;
},
function () {
return { count:0, sumPrice:0, avgPrice};
});
....
chart.ordering(function(d){ return d.value.avgPrice});
If you want the user to be able to change the sort order, you'd need to build a sort button that changes the ordering and rerenders the chart.

Related

crossfilter reduction is crashing

I am unable to render a dc.js stacked bar chart successfully and I receive a console error
unable to read property 'Total' of undefined
I am new to the library and suspect my group or reduce is not successfully specified.
How do I resolve this issue?
$scope.riskStatusByMonth = function(){
var data = [
{"Month":"Jan","High":12},{"Month":"Jan","Med":14},{"Month":"Jan","Low":2},{"Month":"Jan","Closed":8},
{"Month":"Feb","High":12},{"Month":"Feb","Med":14},{"Month":"Feb","Low":2},{"Month":"Feb","Closed":8},
{"Month":"Mar","High":12},{"Month":"Mar","Med":14},{"Month":"Mar","Low":2},{"Month":"Mar","Closed":8},
{"Month":"Apr","High":12},{"Month":"Apr","Med":14},{"Month":"Apr","Low":2},{"Month":"Apr","Closed":8},
{"Month":"May","High":12},{"Month":"May","Med":14},{"Month":"May","Low":2},{"Month":"May","Closed":8},
{"Month":"Jun","High":12},{"Month":"Jun","Med":14},{"Month":"Jun","Low":2},{"Month":"Jun","Closed":8},
{"Month":"Jul","High":12},{"Month":"Jul","Med":14},{"Month":"Jul","Low":2},{"Month":"Jul","Closed":8},
{"Month":"Aug","High":12},{"Month":"Aug","Med":14},{"Month":"Aug","Low":2},{"Month":"Aug","Closed":8},
{"Month":"Sep","High":12},{"Month":"Sep","Med":14},{"Month":"Sep","Low":2},{"Month":"Sep","Closed":8},
{"Month":"Oct","High":12},{"Month":"Oct","Med":14},{"Month":"Oct","Low":2},{"Month":"Oct","Closed":8},
{"Month":"Nov","High":12},{"Month":"Nov","Med":14},{"Month":"Nov","Low":2},{"Month":"Nov","Closed":8},
{"Month":"Dec","High":8},{"Month":"Dec","Med":6},{"Month":"Dec","Low":13},{"Month":"Dec","Closed":8},
]
data.forEach(function(x) {
x.Total = 0;
});
var ndx = crossfilter(data)
var xdim = ndx.dimension(function (d) {return d.Month;});
function root_function(dim,stack_name) {
return dim.group().reduce(
function(p, v) {
p[v[stack_name]] = (p[v[stack_name]] || 0) + v.High;
return p;},
function(p, v) {
p[v[stack_name]] = (p[v[stack_name]] || 0) + v.Med;
return p;},
function(p, v) {
p[v[stack_name]] = (p[v[stack_name]] || 0) + v.Low; <-------------------here is where error occurs
return p;},
function(p, v) {
p[v[stack_name]] = (p[v[stack_name]] || 0) + v.Closed;
return p;},
function() {
return {};
});}
var ydim = root_function(xdim,'Total')
function sel_stack(i) {
return function(d) {
return d.value[i];
};}
$scope.monthlyRiskStatus = dc.barChart("#risk-status-by-month");
$scope.monthlyRiskStatus
.x(d3.scaleLinear().domain(xdim))
.dimension(xdim)
.group(ydim, '1', sel_stack("Jan"))
.xUnits(dc.units.ordinal);
month = [null,'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
for(var i = 2; i<=12; ++i)
$scope.monthlyRiskStatus.stack(ydim, ''+i, sel_stack(month[i]));
$scope.monthlyRiskStatus.render();
}
group.reduce() takes three arguments: add, remove, init.
You are passing 5.
Looks like it is trying to call the third one as the initializer, with no arguments, so therefore v is undefined.
how to stack by level
It looks like what you're really trying to do is group by month (X axis) and then stack by status or level. Here's one way to do that.
First, you're on the right track with a function that takes a stack name, but we'll want it to take all of the stack names:
function root_function(dim,stack_names) {
return dim.group().reduce(
function(p, v) {
stack_names.forEach(stack_name => { // 1
if(v[stack_name] !== undefined) // 2
p[stack_name] = (p[v[stack_name]] || 0) + v[stack_name] // 3
});
return p;},
function(p, v) {
stack_names.forEach(stack_name => { // 1
if(v[stack_name] !== undefined) // 2
p[stack_name] = (p[v[stack_name]] || 0) + v[stack_name] // 3
});
return p;},
function() {
return {};
});}
In the add and reduce functions, we'll loop over all the stack names
Stack names are fields which may or may not exist in each row. If the stack name exists in the current row...
We'll add or subtract the row field stack_name from the field with the same name in the current bin.
We'll define both levels and months arrays. levels will be used for stacking and months will be used for the ordinal X domain:
var levels = ['High', 'Med', 'Low', 'Closed']
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
When we define the group, we'll pass levels to root_function():
var ygroup = root_function(xdim,levels)
I see you had some confusion between the English/math definition of "dimension" and the crossfilter dimension. Yes, in English "Y" would be a dimension, but in crossfilter and dc.js, "dimensions" are what you aggregate on, and groups are the aggregations that often go into Y. (Naming things is difficult.)
We'll use an ordinal scale (you had half ordinal half linear, which won't work):
$scope.monthlyRiskStatus
.x(d3.scaleOrdinal().domain(months))
.dimension(xdim)
.group(ygroup, levels[0], sel_stack(levels[0]))
.xUnits(dc.units.ordinal);
Passing the months to the domain of the ordinal scale tells dc.js to draw the bars in that order. (Warning: it's a little more complicated for line charts because you also have to sort the input data.)
Note we are stacking by level, not by month. Also here:
for(var i = 1; i<levels.length; ++i)
$scope.monthlyRiskStatus.stack(ygroup, levels[i], sel_stack(levels[i]));
Let's also add a legend, too, so we know what we're looking at:
.margins({left:75, top: 0, right: 0, bottom: 20})
.legend(dc.legend())
Demo fiddle.

dc.js Grouping for Bubble Chart Removing from wrong groups

I'm trying to create a bubble chart with dc.js that will have a bubble for each data row and will be filtered by other charts on the same page. The initial bubble chart is created correctly, but when items are filtered from another chart and added or removed from the group it looks like they are being applied to the wrong group. I'm not sure what I'm messing up on the grouping or dimensions. I've created an example fiddle here
There's simple pie chart to filter on filterColumn, a bubble chart that uses identifer1, a unique field, as the dimension and xVal, yVal, and rVal to display the data, and a dataTable to display the current records.
I've tried other custom groups functions, but switched to the example from the FAQ and still had problems.
var
filterPieChart=dc.pieChart("#filterPieChart"),
bubbleChart = dc.bubbleChart('#bubbleChart'),
dataTable = dc.dataTable('#data-table');
var
bubbleChartDim=ndx.dimension(dc.pluck("identifier1")),
filterPieChartDim=ndx.dimension(dc.pluck("filterColumn")),
allDim = ndx.dimension(function(d) {return d;});
var filterPieChartGroup=filterPieChartDim.group().reduceCount();
function reduceFieldsAdd(fields) {
return function(p, v) {
fields.forEach(function(f) {
p[f] += 1*v[f];
});
return p;
};
}
function reduceFieldsRemove(fields) {
return function(p, v) {
fields.forEach(function(f) {
p[f] -= 1*v[f];
});
return p;
};
}
function reduceFieldsInitial(fields) {
return function() {
var ret = {};
fields.forEach(function(f) {
ret[f] = 0;
});
return ret;
};
}
var fieldsToReduce=['xVal', 'yVal', 'rVal'];
var bubbleChartGroup = bubbleChartDim.group().reduce(
reduceFieldsAdd(fieldsToReduce),
reduceFieldsRemove(fieldsToReduce),
reduceFieldsInitial(fieldsToReduce)
);
filterPieChart
.dimension(filterPieChartDim)
.group(filterPieChartGroup)
...
;
bubbleChart
.dimension(bubbleChartDim)
.group(bubbleChartGroup)
.keyAccessor(function (p) { return p.value.xVal; })
.valueAccessor(function (p) { return p.value.yVal; })
.radiusValueAccessor(function (p) { return p.value.rVal; })
...
;
This was a frustrating one to debug. Your groups and reductions are fine, and that's the best way to plot one bubble for each row, using a unique identifier like that.
[It's annoying that you have to specify a complicated reduction, when the values will be either the original value or 0, but the alternatives aren't much better.]
The reductions are going crazy. Definitely not just original values and zero, some are going to other values, bigger or negative, and sometimes clicking a pie slice twice does not even return to the original state.
I put breakpoints in the reduce functions and noticed, as you did, that the values were being removed from the wrong groups. How could this be? Finally, by logging bubbleChartGroup.all() in a filtered handler for the pie chart, I noticed that the groups were out of order after the first rendering!
Your code is fine. But you've unearthed a new bug in dc.js, which I filed here.
In order to implement the sortBubbleSize feature, we sort the bubbles. Unfortunately we are also sorting crossfilter's internal array of groups, which it trusted us with. (group.all() returns an internal data structure which must never be modified.)
The fix will be easy; we just need to copy the array before sorting it. You can test it out in your code by commenting out sortBubbleSize and instead supplying the data function, which is what it does internally:
bubbleChart.data(function (group) {
var data = group.all().slice(0);
if (true) { // (_sortBubbleSize) {
// sort descending so smaller bubbles are on top
var radiusAccessor = bubbleChart.radiusValueAccessor();
data.sort(function (a, b) { return d3.descending(radiusAccessor(a), radiusAccessor(b)); });
}
return data;
});
Notice the .slice(0) at the top.
Hope to fix this in the next release, but this workaround is pretty solid in case it takes longer.
Here is a fiddle demonstrating the workaround.

Using Custom reduce function with Fake Groups

I have line chart where I need to show frequency of order executions over the course of a day. These orders are grouped by time interval, for example every hour, using custom reduce functions. There could be an hour interval when there were no order executions, but I need to show that as a zero point on the line. I create a 'fake group' containing all the bins with a zero count...and the initial load of the page is correct.
However the line chart is one of 11 charts on the page, and needs to be updated when filters are applied to other charts. When I filter on another chart, the effects on this particular frequency line chart are incorrect. The dimension and the 'fake group' are used for the dc.chart.
I put console.log messages in the reduceRemove function and can see that there is something wrong...but not sure why.
Any thoughts on where I could be going wrong.
FrequencyVsTimeDimension = crossfilterData.dimension(function (d) { return d.execution_datetime; });
FrequencyVsTimeGroup = FrequencyVsTimeDimension.group(n_seconds_interval(interval));
FrequencyVsTimeGroup.reduce(
function (p, d) { //reduceAdd
if (d.execution_datetime in p.order_list) {
p.order_list[d.execution_datetime] += 1;
}
else {
p.order_list[d.execution_datetime] = 1;
if (d.execution_type !== FILL) p.order_count++;
}
return p;
},
function (p, d) { //reduceRemove
if (d.execution_type !== FILL) p.order_count--;
p.order_list[d.execution_datetime]--;
if (p.order_list[d.execution_datetime] === 0) {
delete p.order_list[d.execution_datetime];
}
return p;
},
function () { //reduceInitial
return { order_list: {}, order_count: 0 };
}
);
var FrequencyVsTimeFakeGroup = ensure_group_bins(FrequencyVsTimeGroup, interval); // function that returns bins for all the intervals, even those without data.

Group totals in d3/dc.js data table

I have a data table made with dc.js that groups data on month and shows daily data from various suppliers. I would like to include a monthly total within the table, in the row of the month group.
Is this possible? Here's the definition of my table:
aTable
.dimension(dailyDimension)
.group(function (d) {
return formatDateMonth(parseDate(d.date));
})
.size(Infinity)
.columns([{
label : 'Date',
format : function(d) {
return formatDateDay(parseDate(d.date))
}
}, {
label : 'Supplier',
format : function (d) {
return d.Supplier;
}
}, {
label : 'Volume',
format : function (d) {
return +d.total;
}
}])
And the fiddle: https://jsfiddle.net/urz6rjf8/
It's not something that's built into dc.js, but you can add it with a postprocessing step.
Here's one way:
var colspan = null;
aTable
.on('pretransition', function (table) {
var grouprow = table.selectAll('tr.dc-table-group').classed('info', true);
// fetch previous colspan only once
if(colspan === null) {
colspan = +grouprow.select('td').attr('colspan') - 1;
}
// reduce colspan of group label by 1
grouprow.selectAll('td.dc-table-label')
.attr('colspan', colspan);
// for each row, join td's to a single-element array
// containing the sum. this is a trick to only create a child element once
var sum = grouprow.selectAll('td.sum').data(function(d) {
return [d.values.map(function(d) {
return d.total;
}).reduce(function(a, b) {
return a+b;
})];
});
sum.enter().append('td').attr('class', 'sum');
sum.text(function(d) { return d; });
});
First we select the existing group label rows. We need to reduce the colspan of the existing tds (and any that get created later) by one. (But we should only calculate the colspan once, or they would keep getting smaller.)
Then we can add another td for the sum. This uses a trick where you selectAll an element that doesn't exist, and join to an array of size one. The result is that the element will get created if it doesn't exist, but won't get added if it's already there.
The sum is just map-to-value and then reduce with +.
Finally we set the value of the td.sum, whether or not it is new.
Fork of your fiddle: https://jsfiddle.net/gordonwoodhull/at95n2zg/11/

Filtering crossfilter dimension using dropdown

I am trying to filter my row chart by selecting values in a dropdown.
I do this by creating a dimension and applying a filter to that dimension based on selected values in the dropdown.
The row chart has to display average values, to do this I created ReduceAdd/Remove/Initial functions.
The average values are working as intended.
However, when I filter my dimension used for the dropdown, it seems to add duplicate values to my row chart.
Example code: http://jsfiddle.net/sy9xA/4/
var dropDownFilterDimension;
function ReduceAdd(p, v) {
var value = v.value;
++p.countAvg;
p.totalAvg += value;
return p;
}
function ReduceRemove(p, v) {
var value = v.value
--p.countAvg;
p.totalAvg -= value;
return p;
}
function ReduceInitial() {
return {countAvg: 0, totalAvg: 0};
}
function filterDropdown(dropDownID){
dropDown = document.getElementById(dropDownID);
dropDownFilterDimension.filterAll();
values = $(dropDown).val();
if( values != null ){
dropDownFilterDimension.filter(function(d) { if (values.indexOf(d) > -1) {return d;} });
}
dc.redrawAll();
}
Here for example, when "banana" is selected for filtering. ReduceRemove is called for all non selected values, but in addition ReduceAdd is called for banana (even though it is already in it).
So now when after selecting banana, apple is selected. Banana still has some value.
Can someone explain why this is happening? Or how i can avoid this from happening?
Thanks in advance!
Robert
I don't have fully understand what you are doing also because the jsfddle doesn't show nothing more than the js.
I can tell how I do:
you have a dimension associated to a dc chart (ex chartDim)
you have a group that calculate the average associated to a dc chart (ex averageGroup)
you have a dropdown with a set of values to be use as filter (#myDropDown)
d3.select('#myDropDown')
.on('change', function(){
chartDim.filter(this.value)
dc.redrawAll();
})
the call of dc.redrawAll() will update not only the chart but also the averageGroup and everything should go fine. If the dropDown has is own dimension just filter this one instead of the chartDim

Resources