Skip to content

Instantly share code, notes, and snippets.

@stevemandl
Last active February 13, 2016 02:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevemandl/02febfc129131db79adf to your computer and use it in GitHub Desktop.
Save stevemandl/02febfc129131db79adf to your computer and use it in GitHub Desktop.
UpdatingCrossfilter Demo
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dc/2.0.0-beta.20/dc.js"></script>
<script src="updatingCrossfilter.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dc/2.0.0-beta.20/dc.css" />
<div> DC Version: <span id="version"></span></div>
<div id="timechart"></div>
<div id="histogram"></div>
<script>
var getData = function (d) { return {ts: d, y: Math.random()* 2 -1 }; }, //get a data point
size = 400, // the sample size
binCount = 20, //number of bins in the histogram
bins = d3.range(-1,1,1/binCount),
step = 50, // milliseconds between data points
th = new Date().getTime(), // [tl,th] is the visible time window
tl = th - size * step,
data = d3.range(tl, th, step).map(getData), //fetch the data
trend = updatingCrossfilter(data), //use updatingCrossfilter as you would a regular crossfilter
dim = trend.dimension(function (d) { return d.ts; }),
//grp sums y values - even though there is only one per timestamp in this example
grp = dim.group().reduceSum(function (d) { return d.y; }),
//yDim is used for the histogram
yDim = trend.dimension(function(d){ return Math.floor(d.y*binCount)/binCount;}),
yGrp = yDim.group(), // sums the number of data points in this bin
colors = d3.scale.linear() //pretty colors
.domain([-1.0, -.2, 0.0, 0.2, 1.0])
.range(["red", "blue", "green", "blue", "red"])
.interpolate(d3.interpolateHcl);
yGrp._all = yGrp.all; //need to replace all() to ensure empty bins are always there
yGrp.all = function () {//add artificial empty bins
var result = d3.map();
bins.forEach( function(d){ result.set(d,0); });
yGrp._all().forEach(function(d) {
result.set(d.key,d.value);
});
return result.entries();
}
var chart = dc.barChart("#timechart")
.width(500)
.height(400)
.dimension(dim)
.group(grp)
.zoomOutRestrict(false)
.brushOn(false)
.transitionDuration(0)
.y(d3.scale.linear().domain([-1, 1]))
.x(d3.time.scale().domain([tl, th]))
.colors(colors)
.colorAccessor(dc.pluck("value")),
histogram = dc.rowChart("#histogram")
.width(300)
.height(400)
.gap(1)
.x(d3.scale.linear().domain([0, 100]).range([0,200]))
.transitionDuration(0)
.group(yGrp)
.dimension(yDim)
.elasticX(false)
.ordinalColors(bins.map(colors))
.ordering( function(d){return -d.key;} ),
// refresh is called periodically to add, delete, and update data from the crossfilter
refresh = function () {
trend.add([getData(th + step)]); //add a new data point
tl += step, th += step; //slide the window
dim.filter(function (d) { return d < tl; }); // filter the off-screen older data
trend.remove(); // and remove it from the crossfilter
dim.filter(); // clear the filter
// update half the data points with modified data
trend.update(dim.bottom(size/2).map(
function(d){
d.y = d.y * .99;
return d;
}
));
};
chart.xAxis().ticks(5);
// when the updatingCrossfilter gets updated, refresh and refocus the affected charts
trend.on("update", function(){
chart.focus([tl,th]);
histogram.redraw();
});
d3.select("#version").text(dc.version);
dc.renderAll();
setInterval(refresh, step);
</script>
updatingCrossfilter = function(cbk){
var c = crossfilter(); //the underlying crossfilter
//function to get a serial ID
var _getID = (function(){var id = 0; return function(){ return id++;}})(),
//allows the client to subscribe to update events
_listener = d3.dispatch("update", "startUpdate");
// registers an event handler
c.on = function (event, listener) {
_listener.on(event, listener);
return c;
};
//the unique ids of the data elements of the crossfilter
c._ids = d3.set();
//the dimension to track the ids
c._idDim = c.dimension(function(d) { return d._id; });
//backup add and remove for later
c._add = c.add;
c._remove = c.remove;
// add new data
c.add = function(newData){
newData.forEach(function(d){
d._id = _getID();
this._ids.add(d._id);}, this);
return c._add(newData);
};
//remove data matching the current filter(s)
c.remove = function(){
this._idDim
.top(Infinity)
.forEach(function(d){this._ids.remove(d._id);}, this);
return c._remove();
};
//update newData by replacing the elements with matching id's
c.update = function(newData){
_listener.startUpdate();
c.liftFilters();
newData.forEach(function(d){
c._idDim.filter(d._id);
c.remove();
}, this);
c._idDim.filterAll();
c.add(newData);
c.restoreFilters()
_listener.update();
return c;
};
//temporarily lift filters from all charts' dimensions, saving them to for restoreFilters() later
//TODO: listen for filter changes so this is not dependent on dc
c.liftFilters = function(){
dc.chartRegistry.list().forEach(function(d){
d._liftedFilters = d.filters();
d.filterAll();
});
return c;
};
//restore filters to charts' dimensions previously saved by liftFilters()
c.restoreFilters = function(){
dc.chartRegistry.list().forEach(function(d){
if (d._liftedFilters){
d._liftedFilters.map(d.filter);
delete d._liftedFilters;
}
});
return c;
};
//sanitize cbk as a function
if (cbk && typeof arguments[0] != "function"){
var o = cbk;
cbk = function(c){return c.add(o);};
}
cbk && cbk(c);
return c;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment