Crossfiltering with heatmap dc.js - dc.js

Trying to build a simple dashboard with heatmap (build as a cartogram) and rowchart. Selecting region, i expect to see changes on rowchart and vice versa. But it doesn't work in this way.
var dim = ndx.dimension(d=>[d.x,d.y,d.region])
var group = dim.group().reduce(
(p,v)=>{
++p.count;
p.total+=v.salary;
p.avg_salary=p.total/p.count;
return p
},
(p,v)=>{
--p.count;
p.total-=v.salary;
p.avg_salary=p.total/v.count;
return p
},
()=>({avg_salary: 0,count: 0,total:0})
)
var graduationDim = ndx.dimension(d=>d.graduation)
var graduationGroup = graduationDim.group().reduce(
(p,v)=>{
++p.count;
p.total+=v.salary;
p.avg_salary=p.total/p.count;
return p
},
(p,v)=>{
--p.count;
p.total-=v.salary;
p.avg_salary=p.total/v.count;
return p
},
()=>({avg_salary: 0,count: 0,total:0}))
What am i doing wrong? Any help or advice is highly appreciated.
Thank you

It's a bug in your reduce-remove functions, v.count should be p.count:
(p,v)=>{
--p.count;
p.total-=v.salary;
p.avg_salary=p.total/p.count;
return p
},
Since this field doesn't exist in your original data, you are probably getting NaNs.
Working fork of your codepen.

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.

NVD3 chart is getting cut off near axes

I'm working on a new chart type for NVD3 called lineWithFocusPlusSecondary. It has two graphs on top of each other. It's working well except for one problem: if the x values are dates, when you zoom in, the graph gets cut off in an unpleasant manner. This doesn't happen with the default lineChart so I've definitely done something wrong.
I've put my code in this plnkr: https://plnkr.co/edit/9GzI0Jxi5qXZas3ljuBQ?p=preview
Would love some help :) It seems like the issue in the screenshot is that the x-axis domain goes until ~7:05pm but we don't have a data point until 7pm.
It could be something something to do with my onBrush function:
function onBrush(extent) {
var processedData = processData(container.datum()),
dataPrimary = processedData.dataPrimary,
dataSecondary = processedData.dataSecondary,
seriesPrimary = processedData.seriesPrimary,
seriesSecondary = processedData.seriesSecondary;
updateChartData(
getIntegerExtent(extent),
dataPrimary,
dataSecondary,
seriesPrimary,
seriesSecondary
);
}
function getIntegerExtent(extent) {
return [Math.ceil(extent[0]), Math.floor(extent[1])];
}
function updateAxes(extent) {
primaryXAxis.scale(primaryChart.xScale());
primaryXAxis.domain(extent);
g
.select('.nv-primary .nv-x.nv-axis')
.transition()
.duration(transitionDuration)
.call(primaryXAxis);
g
.select('.nv-secondary .nv-ySecondary.nv-axis')
.transition()
.duration(transitionDuration)
.call(yAxisSecondary);
g
.select('.nv-primary .nv-yPrimary.nv-axis')
.transition()
.duration(transitionDuration)
.call(yAxisPrimary);
}
function updateChartData(currentExtent, dataPrimary, dataSecondary) {
updateAxes(currentExtent);
var primaryDatasetsWithinBrushExtent = !dataPrimary.length
? [
{
values: []
}
]
: dataPrimary.map(function(d) {
var restrictedDataset = Object.assign({}, d);
restrictedDataset.values = d.values.filter(function(d, i) {
return (
primaryChart.x()(d, i) >= currentExtent[0] &&
primaryChart.x()(d, i) <= currentExtent[1]
);
});
return restrictedDataset;
});
var primaryChartWrap = g
.select('.nv-primary .nv-linesWrap')
.datum(primaryDatasetsWithinBrushExtent);
var secondaryDatasetsWithinExtent = !dataSecondary.length
? [
{
values: []
}
]
: dataSecondary.map(function(d) {
var restrictedDataset = Object.assign({}, d);
restrictedDataset.values = d.values.filter(function(d, i) {
return (
secondaryChart.x()(d, i) >= currentExtent[0] &&
secondaryChart.x()(d, i) <= currentExtent[1]
);
});
return restrictedDataset;
});
var focusSecondaryChartWrap = g
.select('.nv-secondary .nv-secondaryChartWrap')
.datum(secondaryDatasetsWithinExtent);
primaryChart.xDomain(currentExtent);
secondaryChart.xDomain(currentExtent);
primaryChartWrap
.transition()
.duration(transitionDuration)
.call(primaryChart);
focusSecondaryChartWrap
.transition()
.duration(transitionDuration)
.call(secondaryChart);
}
I discovered the issue was that I was trying to set the xDomain in multiple locations. This seems to mess up NVD3's logic. After I removed all of the .domain/.xDomain it worked perfectly :)
Debugging approach was to carefully read through the lineChart.js code and notice what it didn't have that I had.

How to wrap a group with a top method

I have a map display and would like to total all electoral votes of the states that have been selected and display it in a number display. I was given the advice in a previous thread to wrap my group with an object with a top method. I'm not sure how to do this, so far I have:
var stateDim4 = ndx.dimension(function(d) { return d.state; }),
group = stateDim4.group().reduceSum(function(d) {return d.elecVote }),
count = group.top(51);
count[0].key;
count[0].value;
Please could someone help me on how to do this?
Previous Thread: Summing a column and displaying percentage in a number chart
Building on your previous example, here is a fake group with a top method that sums up all the electoral votes for the selected states.
var fakeGroup = {
top: function() {
return [
grp2.top(Infinity).reduce(function(a,b) {
if(b.value > 0) {
a.value += elecVotesMap.get(b.key)
}
return a
}, {
key: "",
value: 0
})
]
}
}
percentElec
.group(fakeGroup)
.formatNumber(d3.format("d"))
Here is a working JSFiddle: https://jsfiddle.net/esjewett/u9dq33v2/2/
There are a bunch of other fake group examples in the dc.js FAQ, but I don't think any of them do exactly what you want here.

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

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.

custom linear scales in D3

I have a linear scale and I am using it for one of my axis in a chart.
I want to define a custom format only showing positive numbers and following this example I created a customFormat in this way:
function yFormat(formats) {
return function(value) {
var i = formats.length - 1, f = formats[i];
while (!f[1](value)) f = formats[--i];
return f[0](value);
};
}
var customFormat = yFormat([
[d3.format("?????"), function() { return true }],
[d3.format("d"), function(d) { return d >= 0 }]
]);
but I don't know how to define an empty format for Numbers (link to doc)
Does anybody have an idea? Thanks
EDIT
d3.format("") seems to work for time scales but not for linear scale. In a console:
> var emptyTime = d3.time.format("");
undefined
> emptyTime("something");
""
> var emptyInt = d3.format("");
undefined
> emptyInt(5);
"5"
ANSWER
I am just using tickValues on the axis itself. So, something like:
yAxis.tickValues(
d3.range(
0,
d3.max(y.domain()),
shift
)
);

Resources