crossfilter reduction is crashing - dc.js

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.

Related

D3 Zoom - Scales

I am new to D3 and trying to use zoom functionality.
I have a 100 thousand plus points to be displayed and inorder to not do break my browser, I am trying to randomly select 5000 points and show the user, and on zooming in - query more relevant points from the database.
I do not want to do this on every zoom , my zoom scaleExtent is set to [1,5] and every time the zoomscale is 3 or 5 I will query data from database and refresh the plot, this works fine, just a problem every time there is a plot refresh the scales are set to [1,5] again and it goes in an endless loop. And zoom out completely breaks.
Anybody have any ideas how to approach doing this.
I tried to create to buttons for zoomin and zoomout and call the function as below onClick them
function initiateZoom(val)
{
var direction = 1
direction = (val === 'zoom_in') ? 1 : -1;
extent = zoom.scaleExtent();
incrZoom = parseInt(lastZoomScale) + parseInt(direction);
console.log("Target Zoom Scale"+ incrZoom);
if (incrZoom < extent[0] || incrZoom > extent[1])
{
if(incrZoom < extent[0])
{
incrZoom = parseInt(incrZoom) + direction;
}else {
incrZoom = parseInt(incrZoom) + direction;
}
alert("Min/Max Reached");
return false;
}
zoom.scaleTo(scatter_plt_svg, (incrZoom));
}
And on zoom call the function zoomed
function zoomed() {
var zoom_scale = (d3.event.transform.k);//.toFixed(2);
var new_xScale= null;
var new_yScale = null;
new_xScale = d3.event.transform.rescaleX(xScl);
new_yScale = d3.event.transform.rescaleY(yScl);
// update axes
gX.call(d3.axisBottom(new_xScale).ticks(10));
gY.call(d3.axisLeft(new_yScale).ticks(10));
circles.data(plotdata)
.attr('cx', function (d, i) {
return new_xScale(d[1])
})
.attr('cy', function (d, i) {
return new_yScale(d[2])
});
curr_xScale = new_xScale;
curr_yScale = new_yScale;
lastZoomScale = zoom_scale;
//If zoom scale is a value of scaleExtent, refresh plot.
if(zoom_scale == 2 || zoom_scale == 4 || zoom_scale == 6 || zoom_scale == 8 || zoom_scale == 10)
{
scatter_plt_svg.on('.zoom', null);
initialize_plots(false,"zoom"); //helps get data from server and refresh plots.
}
}
Regards
Tasneem

Creating a histogram chart for each category

I am trying to create histogram having ages in groups like this (0-10), (10-20), ...(90,100)
Dataset look like this:
0: {agebracket: "20", currentstatus: "Recovered", dateannounced: "30/01/2020"}
1: {agebracket: "45", currentstatus: "Confirmed", dateannounced: "02/03/2020"}
2: {agebracket: "24", currentstatus: "Recovered", dateannounced: "02/03/2020"}
.
.
.
99: {agebracket: "58", currentstatus: "Hospitalized", dateannounced: "20/03/2020"}
I was able to create histogram but that was on the whole dataset. I didn't take account of
"currentstatus" --> "Recovered", "Hospitalized", "Deceased"
On whole dataset:
I tried to create histogram by currentstatus but it look like this:
This is what I have tried:
var binwidth = 10;
var dim = cf.dimension(function(d) {
return parseInt(d.agebracket); });
var age_by_cases= dim.group().reduce(
// add
(p, v) => {
p[v.currentstatus] = (p[v.currentstatus] || 0) + 1;
return p;
},
// remove
(p, v) => {
p[v.currentstatus] -= 1;
return p;
},
// init
() => ({})
);
barChart
.height(300)
.width(500) //give it a width
.dimension(dim)
.group(age_by_cases, type)
.elasticY(true)
.valueAccessor(function(p) {
return p.value[type_c];
// return (binwidth * Math.floor(parseInt(p.value[type_c])/binwidth)) ;
})
.x(d3.scaleLinear().domain([1,101]))
.xUnits(dc.units.fp.precision(binwidth))
.elasticX(true);
Line no. 170-184 and Line no. 227-243
https://blockbuilder.org/ninjakx/8e2c0b407fdb1991c9cc5e81e447ebe2
I just got struck at this badly. I don't know how to solve it.
Usually you will define the binning in the dimension key function:
var dim = cf.dimension(function(d) {
return binwidth * Math.floor(parseInt(p.agebracket)/binwidth);
};
And then, use the bin width (or other binning spec) in xUnits, as you have it:
.xUnits(dc.units.fp.precision(binwidth))
This causes crossfilter to sort each row into the bin with the value rounded down by binwidth, and it tells dc.js to calculate the bar width also using the binwidth.

d3fc - Crosshair with snapping using latest version 14

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.

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.

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