Unable to filter individual stacks using dc.js with multiple X keys - dc.js

Stacked Bar chart not able to filter on click of any Stack
I need to filter all the charts when clicking on any stack, which is not happening and struggling for a few days.
I've created a fiddle with link
http://jsfiddle.net/praveenNbd/09t5fd7v/13/
I feel am messing up with keys creation as suggested by gordonwoodhull.
function stack_second(group) {
return {
all: function () {
var all = group.all(),
m = {};
// build matrix from multikey/value pairs
all.forEach(function (kv) {
var ks = kv.key;
m[ks] = kv.value;
});
// then produce multivalue key/value pairs
return Object.keys(m).map(function (k) {
return {
key: k,
value: m[k]
};
});
}
};
}
I tried to follow this example https://dc-js.github.io/dc.js/examples/filter-stacks.html
Not able to figure out how below code works:
barChart.on('pretransition', function (chart) {
chart.selectAll('rect.bar')
.classed('stack-deselected', function (d) {
// display stack faded if the chart has filters AND
// the current stack is not one of them
var key = multikey(d.x, d.layer);
//var key = [d.x, d.layer];
return chart.filter() && chart.filters().indexOf(key) === -1;
})
.on('click', function (d) {
chart.filter(multikey(d.x, d.layer));
dc.redrawAll();
});
});
Can someone please point me out in the right direction.
Thanks for stopping by.

You usually don't want to use multiple keys for the X axis unless you have a really, really good reason. It is just going to make things difficult
Here, the filter-stacks example is already using multiple keys, and your data also has multiple keys. If you want to use your data with this example, I would suggest crunching together the two keys, since it looks like you are really using the two together as an ordinal key. We'll see one way to do that below.
You were also trying to combine two different techniques for stacking the bars, stack_second() and your own custom reducer. I don't think your custom reducer will be compatible with filtering by stacks, so I will drop it in this answer.
You'll have to use the multikey() function, and crunch together your two X keys:
dim = ndx.dimension(function (d) {
return multikey(d[0] + ',' + d[1], d[2]);
});
Messy, as this will create keys that look like 0,0xRejected... not so human-readable, but the filter-stacks hack relies on being able to split the key into two parts and this will let it do that.
I didn't see any good reason to use a custom reduction for the row chart, so I just used reduceCount:
var barGrp = barDim.group();
I found a couple of new problems when working on this.
First, your data doesn't have every stack for every X value. So I added a parameter to stack_second() include all the "needed" stacks:
function stack_second(group, needed) {
return {
all: function() {
var all = group.all(),
m = {};
// build matrix from multikey/value pairs
all.forEach(function(kv) {
var ks = splitkey(kv.key);
m[ks[0]] = m[ks[0]] || Object.fromEntries(needed.map(n => [n,0]));
m[ks[0]][ks[1]] = kv.value;
});
// then produce multivalue key/value pairs
return Object.entries(m).map(([key,value]) => ({key,value}));
}
};
}
Probably the example should incorporate this change, although the data it uses doesn't need it.
Second, I found that the ordinal X scale was interfering, because there is no way to disable the selection greying behavior for bar charts with ordinal scales. (Maybe .brushOn(false) is completely ignored? I'm not sure.)
I fixed it in the pretransition handler by explicitly removing the built-in deselected class, so that our custom click handler and stack-deselected class can do their work:
chart.selectAll('rect.bar')
.classed('deselected', false)
All in all, I think this is way too complicated and I would advise not to use multiple keys for the X axis. But, as always, there is a way to make it work.
Here is a working fork of your fiddle.

Related

dc.js - avoid data points animation when adding data to scatter plot

I'm trying to implement a live data visualization (i.e. with new data arriving periodically) using dc.js. The problem I'm having is the following - when new data is added to the plot, already existing points often start to "dance around", even though they were not changed. Can this be avoided?
The following fiddle illustrates this.
My guess is that crossfilter sorts data internally, which results in points moving on the chart for data items that changed their position (index) in the internal storage. Data is added in the following way:
var data = [];
var ndx = crossfilter(data)
setInterval(function() {
var value = ndx.size() + 1;
if (value > 50) {
return;
}
var newElement = {
x: myRandom(),
y: myRandom()
};
ndx.add([newElement]);
dc.redrawAll();
}, 1000);
Any ideas?
I stand by my comments above. dc.js should be fixed by binding the data using a key function, and probably the best way to deal with the problem is just to disable transitions on the scatterplot using .transitionDuration(0)
However, I was curious if it was possible to work around the current problems by keeping the group in a set order using a fake group. And it is indeed, at least for this example where there is no aggregation and we just want to display the original data points.
First, we add a third field, index, to the data. This has to order the data in the same order in which it comes in. As noted in the discussion above, the scatter plot is currently binding data by its index, so we need to keep the points in a set order; nothing should be inserted.
var newElement = {
index: value,
x: myRandom(),
y: myRandom()
};
Next, we have to preserve this index through the binning and aggregation. We could keep it either in the key or in the value, but keeping it in the key seems more fitting:
xyiDimension = ndx.dimension(function(d) {
return [+d.x, +d.y, d.index];
}),
xyiGroup = xyiDimension.group();
The original reduction didn't make sense to me, so I dropped it. We'll just use the default behavior, which counts the number of rows which fall into each bin. The counts should be 1 if included, or 0 if filtered out. Including the index in the key also ensures uniqueness, which the original keys were not guaranteed to have.
Now we can create a fake group that keeps everything sorted by index:
var xyiGroupSorted = {
all: function() {
var ret = xyiGroup.all().slice().sort((a,b) => a.key[2] - b.key[2]);
return ret;
}
}
This will fetch the original data whenever it's requested by the chart, create a copy of the array (because the original is owned by crossfilter), and sort it to return it to the correct order.
And voila, we have a scatter plot that behaves the way it should, even though the data has gone through crossfilter.
Fork of your fiddle: https://jsfiddle.net/gordonwoodhull/mj81m42v/13/
[After all this, maybe we shouldn't have given the data to crossfilter in the first place! We could have just created a fake group which exposes the original data. But maybe there's some use to this technique. At least it proves that there's almost always a way to work around any problems in dc.js & crossfilter.]

Using Grouped Bars with DC.js

I'm using the grouped bar PR of dc.js and the corresponding grouped bar chart example as a baseline.
For some reason, I have to use numbers in my data as opposed to strings. (Convert "male" and "female" to 1/0). I'm guessing it has to do with the reduce functions I'm using. This also effects my x-axis labels, of course. I'd rather they show the text variations.
ndx = crossfilter(eData),
groupDim = ndx.dimension(function(d) {return d.service;}),
qtySumGroup = groupDim.group().reduce(
function(p,v) { p[v.component] = (p[v.component] || 0) + v.qty; return p; },
function(p,v) { p[v.component] = (p[v.component] || 0) - v.qty; return p; },
function() { return{}; });
I'm also noticing that it doesn't seem to crossfilter the data. When I click one of the bars in a group, it doesn't filter my other charts on the page. What am I missing?
Here's the first part of the answer. In order to use string components/genders for grouping, you'll need to adjust the way data is selected for "stacking" (actually grouping when this version of dc.js is used).
So, you can grab the component names by first walking the data and grabbing the components:
var components = Object.keys(etsData.reduce(function(p, v) {
p[v.component] = 1;
return p;
}, {}));
This builds an object where the keys are the component names, and then pulls just the keys as an array.
Then we use components to select the categories like so:
grpChart
.group(qtySumGroup, components[0], sel_stack(components[0]));
for(var i=1; i<components.length; ++i)
grpChart.stack(qtySumGroup, components[i], sel_stack(components[i]));
This is just the same as the original
grpChart
.group(qtySumGroup, "1", sel_stack('1'));
for(var i=2; i<6; ++i)
grpChart.stack(qtySumGroup, ''+i, sel_stack(i));
except that it is indexing by string instead of integer.
I realize this is not the important part of your question, but unfortunately filtering by stack segments is not currently supported in dc.js. I'll try to return to that part later today if I have time - it should be possible to hack it in using a dimension with compound keys (or using two dimensions) and a custom click event, but I haven't seen anyone try this yet.
It would no doubt be a helpful feature to add to dc.js, even if just as an external customization.
EDIT: I've added an example of filtering the segments of a stack, which should apply equally well for grouped bars (although I haven't tried it with your code). The technique is explained in the relevant dc.js issue.

D3.js Stacked Bar Chart Selects

I am trying to come to grips with some D3 concepts but feel as though there are some fundamentals gaps in my knowledge. It seems to do with how the D3 stack() function works.
I am trying to understand why the following two code snippets are not equivalent. The first works and populates data, the second does not.
First (Working Code, Simplified):
var mainStack = d3.stack().keys(keys);
var seriesData = mainStack(dataset[0]);
gBars = g.selectAll("g")
.data(seriesData, function (d, i) { return (i); })
.enter().append("g").. more work here
Second (Not Working, Simplifed):
var mainStack = d3.stack().keys(keys);
var seriesData = mainStack(dataset[0]);
gBars = g.selectAll("g")
.data(seriesData, function (d, i) { return (i); });
gBars.enter().append("g").. more work here
Basically, I have just tried to break up the code to make it simpler (for me) to read, and also to allow me to implement an exit() function. However, when i do the above, the graphs fail to display.
I thought that the gBar variables should maintain their previous selects?
Any assistance would be appreciated, I have successfully used this pattern for simple charts, hence my suspicion that this is related to something I am missing when the d3.stacked() function is involved which nests the data?
With some friendly help, I found that the difference is in the way v4 handles selects. The answer was to utilise merges to combine the various selects and then perform any combined updates on the merged nodes.
Eg:
var mainStack = d3.stack().keys(keys);
var seriesData = mainStack(dataset[0]);
var gBarsUpdate = g.selectAll("g")
.data(seriesData, function (d, i) { return (i); });
var gBarsEnter = gBarsUpdate.enter().append("g")
var gBars = gBarsEnter.merge(gBarsUpdate)
//now update combined with common attributes as required
gBars.attr("fill", "#ff0000"); //etc
Hope this helps anyone else a bit confused by this. Took me a bit of time to understand what was going on, but thanks to some smart people, they put me on the right track :-)
ps. My problem ended up having nothing to do with the stack() function.

editable scatter chart using dc.js

What is a good approach to have a scatter plot in which the data can be edited in the plot itself with a click action?
The idea is to spot outliers in the data in the plot and filter the values in the plot itself, rather than having to change the source data.
Even better would be to remove the data from the crossfilter, but a solution that just filters is acceptable.
I've come up with a solution using the current dc.js (beta 32).
It does not support the brush (needs to have .brushOn(false)) - I'll explain in the enhancement request why this would need some changes to dc.js.
But it does support clicking on points to toggle them, and the reset link. (Clicking on the background to reset is also possible but not implemented here.)
What we'll do is define our own ExcludePointsFilter with the standard dc.js filter signature:
function compare_point(p1, p2) {
return p1[0] === p2[0] && p1[1] === p2[1];
}
function has_point(points, point) {
return points.some(function(p) {
return compare_point(point, p);
});
}
function ExcludePointsFilter(points) {
var points2 = points.slice(0);
points2.filterType = 'ExcludePointsFilter';
points2.isFiltered = function(k) {
return !has_point(points2, k);
};
return points2;
}
We'll calculate a new set of points each time one is clicked, and replace the filter:
scatterPlot.on('pretransition.exclude-dots', function() { #1
// toggle exclusion on click
scatterPlot.selectAll('path.symbol') #2
.style('cursor', 'pointer') // #3
.on('click.exclude-dots', function(d) { // #4
var p = [d.key[0],d.key[1]];
// rebuild the filter #5
var points = scatterPlot.filter() || [];
if(has_point(points, p))
points = points.filter(function(p2) {
return !compare_point(p2, p);
});
else
points.push(p);
// bypass scatterPlot.filter, which will try to change
// it into a RangedTwoDimensionalFilter #6
scatterPlot.__filter(null)
.__filter(ExcludePointsFilter(points));
scatterPlot.redrawGroup();
});
});
Explanation:
Every time the chart is rendered or redrawn, we'll annotate it before any transitions start
Select all the dots, which are path elements with the symbol class
Set an appropriate cursor (pointer-hand may not be ideal but there aren't too many to choose from)
Set up a click handler for each point - use the exclude-dots event namespace to make sure we're not interfering with anyone else.
Get the current filter or start a new one. Look to see if the current point being clicked on (passed as d) is in that array, and either add it or remove it depending.
Replace the current filters for the scatterplot. Since the scatter plot is deeply wed to the RangedTwoDimensionalFilter, we need to bypass its filter override (and also the coordinateGridMixin override!) and go all the way to baseMixin.filter(). Yes this is weird.
For good measure, we'll also replace the filter printer, which normally doesn't know how to deal with an array of points:
scatterPlot.filterPrinter(function(filters) {
// filters will contain 1 or 0 elements (top map/join is just for safety)
return filters.map(function(filter) {
// filter is itself an array of points
return filter.map(function(p) {
return '[' + p.map(dc.utils.printSingleValue).join(',') + ']';
}).join(',');
}).join(',');
});
Here is a working example in a fiddle: http://jsfiddle.net/gordonwoodhull/3y72o0g8/16/
Note, if you then want to do something with the excluded points, you can read them from scatterPlot.filter() - the filter is the array of points with some annotation. You may even be able to reverse the filter and then call crossfilter.remove() but I'll leave that as an exercise.

How to show "missing" rows in a rowChart using crossfilter and dc.js?

I'm using code similar to that in the dc.js annotated example:
var ndx = crossfilter(data);
...
var dayName=["0.Sun","1.Mon","2.Tue","3.Wed","4.Thu","5.Fri","6.Sat"];
var dayOfWeek = ndx.dimension(function (d) {
var day = d.dd.getDay();
return dayName[day];
});
var dayOfWeekGroup = dayOfWeek.group();
var dayOfWeekChart = dc.rowChart("#day-of-week-chart");
dayOfWeekChart.width(180)
.height(180)
.group(dayOfWeekGroup)
.label(function(d){return d.key.substr(2);})
.dimension(dayOfWeek);
The issue I've got is that only days of the week present in the data are displayed in my rowChart, and there's no guarantee every day will be represented in all of my data sets.
This is desirable behaviour for many types of categories, but it's a bit disconcerting to omit them for short and well-known lists like day and month names and I'd rather an empty row was included instead.
For a barChart, I can use .xUnits(dc.units.ordinal) and something like .x(d3.scale.ordinal.domain(dayName)).
Is there some way to do the same thing for a rowChart so that all days of the week are displayed, whether present in data or not?
From my understanding of the crossfilter library, I need to do this at the chart level, and the dimension is OK as is. I've been digging around in the dc.js 1.6.0 api reference, and the d3 scales documentation but haven't had any luck finding what I'm looking for.
Solution
Based on #Gordon's answer, I've added the following function:
function ordinal_groups(keys, group) {
return {
all: function () {
var values = {};
group.all().forEach(function(d, i) {
values[d.key] = d.value;
});
var g = [];
keys.forEach(function(key) {
g.push({key: key,
value: values[key] || 0});
});
return g;
}
};
}
Calling this as follows will fill in any missing rows with 0s:
.group(ordinal_groups(dayNames, dayOfWeekGroup))
Actually, I think you are better off making sure that the groups exist before passing them off to dc.js.
One way to do this is the "fake group" pattern described here:
https://github.com/dc-js/dc.js/wiki/FAQ#filter-the-data-before-its-charted
This way you can make sure the extra entries are created every time the data changes.
Are you saying that you tried adding the extra entries to the ordinal domain and they still weren't represented in the row chart, whereas this did work for bar charts? That sounds like a bug to me. Specifically, it looks like support for ordinal domains needs to be added to the row chart.

Resources