|
/** |
|
* 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) |
|
|