Skip to content

Instantly share code, notes, and snippets.

@monfera
Last active August 8, 2016 21:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save monfera/f02c0a7ef9ded9ca2387 to your computer and use it in GitHub Desktop.
Save monfera/f02c0a7ef9ded9ca2387 to your computer and use it in GitHub Desktop.
D3.js with FRP (Functional Reactive Programming) and Transducers [UNLISTED]
license: gpl-3.0

D3.js with FRP (Functional Reactive Programming) and Transducers

A skeleton version of a d3.js scatterplot is getting updated asynchronously with data via Functional Reactive Programming and transducers. Uses kefir, transducers-js and lodash, but it would be similar with bacon, RxJS, transducers.js and underscore.

A Pen by Robert Monfera on CodePen.

License.

<script src="http://cdn.cognitect.com/transducers/transducers-0.4.158-min.js"></script>
<script src="https://pozadi.github.io/kefir/dist/kefir.min.js"></script>
/**
* D3 with FRP (kefir) and transducers
* Codepen gist by Robert Monfera
*/
/**
* A scanning transducer by Roman Pominov (https://github.com/pozadi/kefir/issues/80)
*/
function scan(handler, seed) {
var prevValue = seed;
return function(xf) {
return {
"@@transducer/init": function() {
return xf["@@transducer/init"]();
},
"@@transducer/result": function(result) {
return xf["@@transducer/result"](result);
},
"@@transducer/step": function(result, input) {
var newValue = handler(prevValue, input);
prevValue = newValue;
return xf["@@transducer/step"](result, newValue);
}
};
};
}
var width = 1920, height = 500;
var frp = Kefir;
var t = transducers;
var svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
var ns = 1000;
// Kick off datastreams with primary FRP input stream(s)
var clock = frp.interval(ns, null);
// Use transducers for the actual logic
var counter = scan(function(prev, next) {
return prev + 1;
}, 0);
var sample = t.map(function(point) {
return {
key: point,
x: width * Math.random(),
y: height * Math.random(),
size: 5 + Math.random() * Math.max(width, height),
color: 'rgb(' + [Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)].join(',') + ')'
};
})
var taggedWithPartialInvisibility = t.map(function(point) {
return _.extend(point, {partlyVisible: point.x - point.size < 0 || point.x + point.size > width || point.y - point.size < 0 || point.y + point.size > height});
});
var keepPartlyInvisible = t.filter(_.property('partlyVisible'));
var pointSet = scan(function(prev, next) {
return [next].concat(prev).slice(0, Math.ceil(Math.random() * (prev.length + 1)));
}, []);
var render = t.map(function(data) {
// Not really a map; executed for side effects - maybe invent new operator like t.each
// It seems possible to eventually alter d3.selection.data() bind to allow transducers
var scatterPoints = svg.selectAll('circle').data(data, _.property('key'));
scatterPoints.enter().append('circle')
.attr('cx', _.property('x'))
.attr('cy', _.property('y'))
.attr('r', _.property('size'))
.attr('fill', _.property('color'))
.attr('opacity', 0)
.transition().duration(ns / 2)
.attr('opacity', 0.5);
scatterPoints.exit()
.transition().duration(ns / 2)
.attr('opacity', 0)
.remove();
});
var keyedRandomSample = t.comp(counter, sample);
var keepPartiallyInvisibleSample = t.comp(taggedWithPartialInvisibility, keepPartlyInvisible);
var partiallyInvisibleSample = t.comp(keyedRandomSample, keepPartiallyInvisibleSample);
var partiallyInvisiblePointSet = t.comp(partiallyInvisibleSample, pointSet);
var renderedSet = t.comp(partiallyInvisiblePointSet, render);
// Conclude datastreams with FRP
clock.transduce(renderedSet).onValue(_.identity); // we prime the FRP data flow with the dummy onValue (alternative: convert render to a non-transducer function)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment