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.
In previous version of d3fc my code was using fc.util.seriesPointSnapXOnly for snapping the crosshair.
This appears to be gone in the latest version of d3fc (or maybe I'm missing it in one of the standalone packages?).
I'm using the canvas implementation (annotationCanvasCrosshair) and it seems to also be missing the "snap" function where it was previously used like so:
fc.tool.crosshair()
.snap(fc.util.seriesPointSnapXOnly(line, series))
Additionally, "on" is also not available, so I can't attach events like trackingstart, trackingend, etc.
How can I implement a snapping crosshair now? The canvas version of the components are badly lacking examples. Does anyone have an example showing a snapping crosshair in the latest version of d3fc via canvas rendering?
Here's what I have so far https://codepen.io/parliament718/pen/xxbQGgp
I understand you've raised the issue with d3fc github, therefore I'll assume you are aware that util/snap.js is been deprecated.
Since this functionality unsupported now, it seems that the only feasible way to work around it will be to implement your own.
I took your pen and original snap.js code as starting point and applied the method outlined in Simple Crosshair example from the documentation.
I ended up having to add missing functions and their dependencies verbatim (surely you can refactor and package it up into a separate module):
function defined() {
var outerArguments = arguments;
return function(d, i) {
for (var c = 0, j = outerArguments.length; c < j; c++) {
if (outerArguments[c](d, i) == null) {
return false;
}
}
return true;
};
}
function minimum(data, accessor) {
return data.map(function(dataPoint, index) {
return [accessor(dataPoint, index), dataPoint, index];
}).reduce(function(accumulator, dataPoint) {
return accumulator[0] > dataPoint[0] ? dataPoint : accumulator;
}, [Number.MAX_VALUE, null, -1]);
}
function pointSnap(xScale, yScale, xValue, yValue, data, objectiveFunction) {
// a default function that computes the distance between two points
objectiveFunction = objectiveFunction || function(x, y, cx, cy) {
var dx = x - cx,
dy = y - cy;
return dx * dx + dy * dy;
};
return function(point) {
var filtered = data.filter(function(d, i) {
return defined(xValue, yValue)(d, i);
});
var nearest = minimum(filtered, function(d) {
return objectiveFunction(point.x, point.y, xScale(xValue(d)), yScale(yValue(d)));
})[1];
return [{
datum: nearest,
x: nearest ? xScale(xValue(nearest)) : point.x,
y: nearest ? yScale(yValue(nearest)) : point.y
}];
};
}
function seriesPointSnap(series, data, objectiveFunction) {
return function(point) {
var xScale = series.xScale(),
yScale = series.yScale(),
xValue = series.crossValue(),
yValue = (series.openValue).call(series);
return pointSnap(xScale, yScale, xValue, yValue, data, objectiveFunction)(point);
};
};
function seriesPointSnapXOnly(series, data) {
function objectiveFunction(x, y, cx, cy) {
var dx = x - cx;
return Math.abs(dx);
}
return seriesPointSnap(series, data, objectiveFunction);
}
The working end result can be seen here: https://codepen.io/timur_kh/pen/YzXXOOG. I basically defined two series and used a pointer component to update that second series data and trigger a re-render:
const data = {
series: stream.take(50), // your candle stick chart
crosshair: [] // second series to hold the crosshair position
};
.............
const crosshair = fc.annotationCanvasCrosshair() // define your crosshair
const multichart = fc.seriesCanvasMulti()
.series([candlesticks, crosshair]) // we've got two series now
.mapping((data, index, series) => {
switch(series[index]) {
case candlesticks:
return data.series;
case crosshair:
return data.crosshair;
}
});
.............
function render() {
d3.select('#zoom-chart')
.datum(data)
.call(chart);
// add the pointer component to the plot-area, re-rendering each time the event fires.
var pointer = fc.pointer()
.on('point', (event) => {
data.crosshair = seriesPointSnapXOnly(candlesticks, data.series)(event[0]);// and when we update the crosshair position - we snap it to the other series using the old library code.
render();
});
d3.select('#zoom-chart .plot-area')
.call(pointer);
}
UPD:
the functionality can be simplified like so, i also updated the pen:
function minimum(data, accessor) {
return data.map(function(dataPoint, index) {
return [accessor(dataPoint, index), dataPoint, index];
}).reduce(function(accumulator, dataPoint) {
return accumulator[0] > dataPoint[0] ? dataPoint : accumulator;
}, [Number.MAX_VALUE, null, -1]);
}
function seriesPointSnapXOnly(series, data, point) {
if (point == undefined) return []; // short circuit if data point was empty
var xScale = series.xScale(),
xValue = series.crossValue();
var filtered = data.filter((d) => (xValue(d) != null));
var nearest = minimum(filtered, (d) => Math.abs(point.x - xScale(xValue(d))))[1];
return [{
x: xScale(xValue(nearest)),
y: point.y
}];
};
This is far from polished, but I'm hoping it conveys the general idea.
I am trying to place a string on the x axis of a line graph, initially the x axis is a date variable, but now I must add a string that differentiates me if it is morning 'AM', afternoon 'PM' or night 'ZM' .
I am using nvd3 v1.8.1 with angularjs. The initial code is as follows
tchart.xAxis
// Chart x-axis settings
.axisLabel('Muestras')
.tickFormat( function(d){
return d3.time.format('%y-%m-%d %p')
(Utils.utcDateToLocalDate(d))
});
chart.xScale(d3.time.scale.utc());
tchart.yAxis // Chart y-axis settings
.axisLabel('Temperatura').tickFormat(
d3.format('.02f'));
var tdata = getData('temperatura');
d3.select('#temperatureChart svg').datum(tdata).call(tchart);
var getData = function(_prop) {
var _data = [];
$scope.registros.forEach(function(_item, _index) {
var _cfecha = _item.fecha.split('-');
var _fecha = new Date(Date.UTC(
parseInt(_cfecha[0]),
parseInt(_cfecha[1]),
parseInt(_cfecha[2]),
(_item.horario === 'AM') ? 1: 13,0, 0));
_data.push({
x : _fecha,
y : _item[_prop]
});
});
return [ {
values : _data,
key : _prop,
color : '#ff7f0e'
} ];
};
With which I obtain the result of graph follow.
Result1
Then I increased a variable of 'label' in the data to use them in thickformat, but not all the values are shown in the graph2
//fill data array
_data.push({
x : _fecha,
y : _item[_prop],
label : _lfecha
});
//tchar.xAxis
.tickFormat(function(d,i) {
return tdata[0].values[i].label;
}
graph2
I also used an ordinal scale with the values of the data matrix, but only shows the start day and the end day, the other values are omitted (graph3)
var x = d3.extent(tdata[0].values, function(d) { console.log(d); return d.label; });
tchart.xScale(d3.scale.ordinal().domain(x));
grahp3
The result I expect is the same as the first graphic but the axis labels allow me to concatenate the date with the abbreviations AM-PM-ZM
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 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
)
);