Skip to content

Instantly share code, notes, and snippets.

@comnik
Last active June 22, 2018 07:32
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 comnik/156ce1cc1207faa7fee8cdac962696c6 to your computer and use it in GitHub Desktop.
Save comnik/156ce1cc1207faa7fee8cdac962696c6 to your computer and use it in GitHub Desktop.
A hacky re-write of the d3/data join operation to work directly with output from Differential Dataflow.
//
// Usage
//
// where before you might've had:
// const selection = parent.selectAll('li').data(data, keyFn)
//
// you now have:
const selection = diff(parent.selectAll('li'), data, keyFn)
selection.attr('class', 'update')...
selection.enter().append('li')...
selection.exit().remove()...
//
// Implementation
//
const keyPrefix = '$'
// meat of the matter in here
const bindDiff = (parent, group, enter, update, exit, diff, keyFn) => {
// @TODO this needs to be maintained separately, eventually
const nodeByKeyValue = {}
for (let node of group) {
const key = keyPrefix + keyFn.call(node, node.__data__)
nodeByKeyValue[key] = node
}
const retractions = new Set()
// This assumes 'diff' to be directly derived from a Differential output,
// i.e. containing tuples of the form (data, +1 / -1). Higher-multiplicity
// changes should work analogously as well.
for (let [tuple, op] of diff) {
const key = keyPrefix + keyFn.call(parent, tuple)
const node = nodeByKeyValue[key]
if (op === -1) {
// This is necessary to distinguish between true retractions
// and retractions followed by additions => i.e. updates.
retractions.add(key)
} else if (op === 1) {
if (retractions.has(key) && node) {
console.log('Updating', key, tuple)
node.__data__ = tuple
retractions.delete(key)
update.push(node)
} else {
enter.push(new EnterNode(parent, tuple))
}
} else {
console.warn('Unknown op', tuple, op)
}
}
for (let key of retractions) {
exit.push(nodeByKeyValue[key])
}
}
const constantly = (x) => () => x;
const diff = (selection, value, key) => {
let parents = selection._parents,
groups = selection._groups
if (typeof value !== "function") {
value = constantly(value)
}
let m = groups.length,
update = new Array(m),
enter = new Array(m),
exit = new Array(m)
for (let j = 0; j < m; ++j) {
let parent = parents[j],
group = groups[j],
groupLength = group.length,
data = value.call(parent, parent && parent.__data__, j, parents),
dataLength = data.length,
enterGroup = enter[j] = new Array(dataLength),
updateGroup = update[j] = new Array(dataLength),
exitGroup = exit[j] = new Array(groupLength);
bindDiff(parent, group, enterGroup, updateGroup, exitGroup, data, key);
// Now connect the enter nodes to their following update node,
// such that appendChild can insert the materialized enter node
// before this node, rather than at the end of the parent node.
for (let i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
if (previous = enterGroup[i0]) {
if (i0 >= i1) {
i1 = i0 + 1
}
while (!(next = updateGroup[i1]) && ++i1 < dataLength) {
previous._next = next || null
}
}
}
}
const joined = d3.selection()
joined._groups = update
joined._parents = parents
joined._enter = enter
joined._exit = exit
return joined
}
//
// Requires d3's EnterNode, ripped out and reproduced
// below for convenience:
//
function EnterNode(parent, datum) {
this.ownerDocument = parent.ownerDocument;
this.namespaceURI = parent.namespaceURI;
this._next = null;
this._parent = parent;
this.__data__ = datum;
}
EnterNode.prototype = {
constructor: EnterNode,
appendChild: function(child) { return this._parent.insertBefore(child, this._next); },
insertBefore: function(child, next) { return this._parent.insertBefore(child, next); },
querySelector: function(selector) { return this._parent.querySelector(selector); },
querySelectorAll: function(selector) { return this._parent.querySelectorAll(selector); }
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment