Last active
June 22, 2018 07:32
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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