Interactive Charts (by dc.js) not updating each other correctly
I am trying to make an interactive data visualization using dc.js.
My data is basically two series of values, distinguished by an id
attribute specified for each row. Here's the example how I generate it:
var data = [];
var n = 10000.;
for (var i = 0; i < n; i++) {
data.push({id: 0, "i": i, x: Math.random()});
data.push({id: 1, "i": i, x: (Math.random()+i/n)});
}
Then I do put this dataset into a crossfilter object and create two dimensions and two groups. One to display per id
the sum of binned values along the series, and one to display the sum of total selected values per id
:
var cf = crossfilter(data),
series = cf.dimension(function(d) {return [d.id, d.i];}),
series_grouped = series
.group(function(d){return [d[0], Math.floor(d[1]/100.)*100.];})
.reduceSum(function(d) { return d.x; }),
id = cf.dimension(function(d) {return d.id;}),
id_grouped = id.group().reduceSum(function(d){return d.x;});
I managed to create the charts I wanted, using below code. What I wasn't able to get right was the interactive behavior:
cf.filterAll()
here, because I didn't think that might solve my root problem.) How can I get the bar chart on the left, be updated when I select a range in the series chart on the right? Am I doing something wrong?
I'm using Firefox 45.0.2, these are my library versions:
This is the full document:
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<script src="crossfilter.js"></script>
<script src="d3.js"></script>
<script src="dc.js"></script>
<link rel="stylesheet" type="text/css" href="dc.css" />
<style>
body { width: 960px; }
.chart.left { width: 25%; }
.chart.right { width: 75%; float: right; }
</style>
</head>
<body>
<div id="charts">
<div id="chart_a" class="chart right"></div>
<div id="chart_b" class="chart left"></div>
</div>
<script>
// generate data
var data = [];
var n = 10000.;
for (var i = 0; i < n; i++) {
data.push({id: 0, "i": i, x: Math.random()});
data.push({id: 1, "i": i, x: (Math.random()+i/n)});
}
// do some crossfilter stuff
var cf = crossfilter(data),
series = cf.dimension(function(d) {return [d.id, d.i];}),
series_grouped = series
.group(function(d){return [d[0], Math.floor(d[1]/100.)*100.];})
.reduceSum(function(d) { return d.x; }),
id = cf.dimension(function(d) {return d.id;}),
id_grouped = id.group().reduceSum(function(d){return d.x;});
// generate charts
var chart_width = 960, chart_height = 200;
dc.seriesChart("#chart_a").height(chart_height).width(.74*chart_width)
.chart(function(c) { return dc.lineChart(c).renderArea(true); })
.x(d3.scale.linear().domain([0,n]))
.dimension(series)
.group(series_grouped)
.seriesAccessor(function(d) {return d.key[0];})
.keyAccessor(function(d) {return d.key[1];})
.valueAccessor(function(d) {return d.value;})
.xAxis();
dc.barChart("#chart_b").height(chart_height).width(.24*chart_width)
.dimension(id)
.group(id_grouped)
.x(d3.scale.ordinal().domain([0,1]))
.xUnits(dc.units.ordinal)
.xAxis();
dc.renderAll();
</script>
</body>
At the least you are going to need to override the filter handler on the series chart. Right now, say you are selecting from i = 20 to 400 in the series chart. On the Crossfilter it is saying series.filter([20,400])
. But your series dimension values look like [0,150]
, so what does it mean to evaluate 20 <= [0,150] && [0,150] <= 400
? Hard to say, and almost certainly not what you mean to do. With Crossfilter's automated type conversions it's probably evaluating "20" < "0,150" && "0,150" < "400"
. Instead, you probably want it to evaluate 20 <= [0,150][1] && [0,150][1] <= 400
, which you can force it to do in a custom filter handler.
Here's a version that "works" using a custom filter handler:
dc.seriesChart("#chart_a").height(chart_height).width(.74 * chart_width)
.chart(function(c) {
return dc.lineChart(c).renderArea(true)
.filterHandler(function(dimension, filter) {
if (filter[0]) {
dimension.filterFunction(function(d) {
return d[1] > filter[0][0] && d[1] < filter[0][1];
});
} else {
dimension.filterAll();
}
setTimeout(dc.redrawAll, 0);
return filter;
});
})
.x(d3.scale.linear().domain([0, n]))
.dimension(series)
.group(series_grouped)
.seriesAccessor(function(d) {
return d.key[0];
})
.keyAccessor(function(d) {
return d.key[1];
})
.valueAccessor(function(d) {
return d.value;
});
However, as you can probably see, using an array as your dimension key in Crossfilter is a really bad idea (unless you're using the array type in Crossfilter2, which is not what you want here). Dimensions must be naturally ordered, and arrays act in surprising ways when it comes to ordering as I hope the explanation above demonstrates.
So what do you do? I'd recommend transforming your data as the best option:
var data = [];
var n = 10000.;
for (var i = 0; i < n; i++) {
data.push({"i": i, x0: Math.random(), x1:(Math.random()+i/n)});
}
With this data, generate groups that sum both x0 and x1, then use the stack mix-in (included in the standard dc.lineChart) to display lines for each series.
An alternative but more painful method is to handle serialization and deserialization of your dimension keys to and from Strings yourself. Just be very careful to think about ordering, meaning you'll probably need to zero-pad your values, and it's probably a good idea to explicitly round floating point values before serializing. Ordering-wise, you should make your dimension values ordered by i
rather than id
if that's what you want to filter by.