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.
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.
I rendered a stacked bar and then I'm trying to re-draw the chart with new filter on clicking a button. Now, as I click the button the previous axes remains but the chart disappears and doesn't re-draw. I don't know what I'm missing here..
Here is the JSFiddle for this stackedBar.
function myFunc(){
var dimForHour = cf.dimension(function(d) { return d.date; });
dimForHour.filter([hour, today]);
var dimByChannel = cf.dimension(function(d) { return d.channelUUID; });
var groupAvgChan = dimByChannel.group().reduce(
function reduceAdd(p, v) {
p.bytesTxd = p.bytesTxd + v.bytesTxd;
p.count = p.count + 1;
p.avg = p.bytesTxd / p.count;
p.avgTot = p.avgTot + p.avg;
p.avgPrcnt = (p.avg / p.avgTot) * 100;
if(p.max < v.bytesTxd) { p.max = v.bytesTxd; }
p.maxTot = p.maxTot + p.max;
p.maxPrcnt = (p.max / p.maxTot) * 100;
return p;
},
function reduceRemove(p, v) {
p.bytesTxd = p.bytesTxd - v.bytesTxd;
p.count = p.count - 1;
p.avg = p.bytesTxd / p.count;
p.avgTot = p.avgTot - v.avgTot;
p.avgPrcnt = (p.avg / p.avgTot) * 100;
if(p.max > v.bytesTxd) { p.max = v.bytesTxd; }
p.maxTot = p.maxTot - p.max;
p.maxPrcnt = (p.max / p.maxTot) * 100;
return p;
},
function reduceInitial() {
return {
bytesTxd:0, count:0, avg:0, avgTot:0,
avgPrcnt:0, max:0, maxTot:0,maxPrcnt:0};
});
/*chanUtil = dc.barChart("#chanUtil")
chanUtil
.dimension(dimByChannel)
.group(groupAvgChan,"Avg Utilization %").valueAccessor(function(d) { return d.value.avgPrcnt; })
.stack(groupAvgChan,"Max Utilization %", function(d) { return d.value.maxPrcnt; })
.x(d3.scale.ordinal().domain(data.map(function (d) { return d.channelUUID })))
.xUnits(dc.units.ordinal);*/
dc.redrawAll();
}
I'm not sure what you're trying to calculate, but the problem seems to arise from this line:
p.avgTot = p.avgTot - v.avgTot;
which does not match the corresponding
p.avgTot = p.avgTot + p.avg;
v does not have a member avgTot, so you get NaN, and everything breaks from there. The best way to debug these kinds of things is
put a breakpoint on the redraw to see what values are in the group
put breakpoints inside the reduce functions to see what went wrong in the calculation.
Changing the reduceRemove function to use
p.avgTot = p.avgTot - p.avg;
seems to fix the problem (at least, there are still bars after the button is clicked).
Fork of your fiddle: http://jsfiddle.net/gordonwoodhull/0vvh1xex/6/
I also added elasticY(true) to be able to see the change better. And yes, the upward transition on bars is particularly wrong here; if you are able to upgrade to dc.js 2.0 (which is on beta 6), that is fixed.
Note: you have some dead code inside myFunc: it looks like you are recreating dimByChannel and groupAvgChan exactly the same, and then not using them for anything.
I'm using the clean axis function courtesy of #JohnKiernander. This works fine with static charts. But when I have a chart that updates (in this example when a button in clicked), the clean axis function does not work as expected. The function also erases others numbers of the axis. Is there a way to make this function work with dynamic charts? or do I have to take another approach?
See fiddle: http://jsfiddle.net/jdash99/oba54L1a/ for a better explanation.
// Clean Axis Function for reference
// Pass in an axis object and an interval.
var cleanAxis = function (axis, oneInEvery) {
// This should have been called after draw, otherwise do nothing
if (axis.shapes.length > 0) {
// Leave the first label
var del = 0;
// If there is an interval set
if (oneInEvery > 1) {
// Operate on all the axis text
axis.shapes.selectAll("text").each(function (d) {
// Remove all but the nth label
if (del % oneInEvery !== 0) {
this.remove();
// Find the corresponding tick line and remove
axis.shapes.selectAll("line").each(function (d2) {
if (d === d2) {
this.remove();
}
});
}
del += 1;
});
}
}
};
I suggest switching to a method with sets opacity rather than removing the label completely. I've modified your fiddle in 2 ways. Firstly the clean axis method becomes:
var cleanAxis = function (axis, oneInEvery) {
// This should have been called after draw, otherwise do nothing
if (axis.shapes.length > 0) {
// Leave the first label
var del = 0;
// If there is an interval set
if (oneInEvery > 1) {
// Operate on all the axis text
axis.shapes.selectAll("text").each(function (d) {
d3.select(this).attr("opacity", 1);
// Remove all but the nth label
if (del % oneInEvery !== 0) {
d3.select(this).attr("opacity", 0);
}
del += 1;
});
}
}
};
also because you are animating the draws you can't draw cleanAxis straight after, you need to assign it to the afterDraw property of the series instead:
s.afterDraw = function () { cleanAxis(myAxis, 10); };
This avoids a race condition on the label creation/hiding.
Here's the updated fiddle: http://jsfiddle.net/oba54L1a/2/
How do I create my own scale() function in d3?
I am trying to replace the nice linear scale in d3 d3.scale.linear() with a different function that I would like to create myself. My new scale would be based on a cumulative distribution function, so that the median value would appear in the center of the x axis, and a value that was two standard deviations from the median would appear twice as far from the center of the x axis as something that was one standard deviation from the mean.
Here is a link to my jsfiddle page: http://jsfiddle.net/tbcholla/kR2PS/3/ (I would appreciate any other comments you might have about my code as well!)
right now I have:
var x = d3.scale.linear()
.range([0, width])
.domain(d3.extent([0, data.length]));
I've seen scale.pow() and scale.log(). Now I'd like to create a new function!
Thanks!
EDIT: I found the function scale.quantile(), which might hold the solution for me. My related question: Plotting a line graph with scale.quantile()
This is an example how we can add new functionality in d3.scale.liner(). For null values my function returns null (d3.scale.liner() returns 0 in this case). The primary approach is to wrap the original scale and all his methods.
I didn't test this function for all cases. But for basic functionality it's working. Unfortunately I didn't found easier way to do it :(
/**
* d3.scale.linear() retrun 0 for null value
* I need to get null in this case
* This is a wrapper for d3.scale.linear()
*/
_getLinearScaleWithNull: function() {
var alternativeScale = function(origLineScale) {
var origScale = origLineScale ? origLineScale : d3.scale.linear();
function scale(x) {
if (x === null) return null; //this is the implementation of new behaviour
return origScale(x);
}
scale.domain = function(x) {
if (!arguments.length) return origScale.domain();
origScale.domain(x);
return scale;
}
scale.range = function(x) {
if (!arguments.length) return origScale.range();
origScale.range(x);
return scale;
}
scale.copy = function() {
return alternativeScale(origScale.copy());
}
scale.invert = function(x) {
return origScale.invert(x);
}
scale.nice = function(m) {
origScale = origScale.nice(m);
return scale;
}
scale.ticks = function(m) {
return origScale.ticks(m);
};
scale.tickFormat = function(m, Format) {
return origScale.tickFormat(m, Format);
}
return scale;
}
return alternativeScale();
},