export default function gup() {
The aim of d3-gup is to codify the General Update Pattern as described by Selection#data. In doing so, we create a callable D3 plugin that is intended to extend D3 rather than wrap or conceal its features. This plugin is designed to be composable with other D3 plugins and itself.
Firstly, a quick recap of the General Update Pattern (GUP). GUP describes the order of operations that conventional follows a data-join. Once a selection is made against the DOM the first phase of updates is conducted upon the selected elements that are pre-existing and will remain in the DOM. The second phase of updates are applied to exit selection, i.e. those elements that are pre-existing in the DOM but will no longer be bound to data. The third phase creates and binds elements for the enter selection, i.e. those elements that don’t yet exist but will be bound to data. The fourth and final phase provides an opportunity to update the final selection that is bound to the data, i.e. the merged selections of enter and pre-existing elements.
The plugin exports a factory that encapsulates these four update phases, we also recognise an initial phase (phase zero) that allows a plugin user to modify the selection before the data-join occurs. This initial phase is important since a GUP factory is independent of a particular data array but often dependent on the shape of the data, e.g. flat vs. heirarchical. Being albe to hook in before the data-join allow GUP authors to dictate the shape of the selection.
gup
creates a factory of GUP instances that, when bound to a data array,
can be called on a selection.
export default function gup() {
Internally, null
represents an no-op for any phase.
let pre = null
, exit = null
, enter = null
, post = null
, select = null
;
The factory accepts the same arguments as Selection#data but the data-join is deferred until it is called against a selection.
function gup(data, ...more) {
A GUP instance, bound to data
, executes the five phases when called on
a selection. In other words, the returned function from a GUP factory is
designed to be passed to the call
function of a
selection.
function _gup(...args) {
The first argument is expected to be a Selection or a
Transition instance. The remaining arguments are the
optional arguments passed to the call
call, which
are passed through to each of the five phase functions.
let context = args.shift();
We have made a conscious to not depend directly on d3-selection
or
d3-transition
for a variety of reasons. Instead, we consider any
object that has a selection
method to be a Transition
instance. The only requirements on this transition-like instance is
that this selection method returns a Selection
instance
and the object is accepted by the transition
method of a Selection
instance.
let shouldTransition = !!context.selection;
selection
will not be a Transition
instance, instead
it will be a real Selection
instance. This is necessary
because we cannot call data
on a transition.
Beyond this point, context
is assumed to be a Transition
instance.
let selection = shouldTransition ? context.selection() : context;
Phase 0: selection
can potentially be transformed by the select
function.
if (select && select != identity) {
selection = select.call(this, selection, ...args);
if (selection.selection) {
shouldTransition = true;
context = selection;
selection = selection.selection();
}
}
The data-join! Here we now pass the data and an optional key function that was passed to the GUP factory (that made this GUP instance).
selection = selection.data(data, more[0]);
Phase 1: the pre
function is applied to the pre-existing elements
with bound data.
if (pre && pre != empty) {
let $pre = selection
If this GUP instance was applied to a transition then we forward the
transition to the pre
function.
if (shouldTransition && selection.transition) {
$pre = selection.transition(context);
}
$pre.call(pre, ...args);
}
if (exit && exit != empty) {
let $exit = selection.exit();
Again, the same transition is forwarded to the exit
function.
if (shouldTransition && $exit.transition) {
$exit = $exit.transition(context);
}
$exit.call(exit, ...args);
}
Phase 3: unique among the update phases, like the select
function,
enter
has the opportunity to transform the selection. The return
value of the enter
function is immediately merged
with the data-join selection which is subsequently returned by this
GUP. Furthermore, the transition is not forwarded to the enter
function because its work is typically to create the elements, meaning
that there is no prior state to transition to (the transition should
occur during the next phase).
let $enter = selection.enter();
if (enter && enter != identity) {
$enter = enter.call(this, $enter, ...args);
if ($enter.selection) {
shouldTransition = true;
context = $enter;
$enter = $enter.selection();
}
}
Phase 4: the post
function is applied to the final set of elements
bound to the data.
let $post = $enter.merge(selection)
The third and final time that the transition is forwarded.
if (shouldTransition && $post.transition) {
$post = $post.transition(context);
}
if (post && post != empty) {
$post.call(post, ...args);
}
The returned selection or potentially a transition is (as mentioned
above) the update selection from the data-join merged into the return
value of the enter
function, which, by default, will be the newly
created elements. Therefore this selection/transition should be all
the elements bound to the data.
return $post;
}
data
sets the data array and optionally the key function and returns
the GUP instance. If no arguments are specified, returns the current
data array and key function in an array.
_gup.data = function(..._) {
return arguments.length ? ([data, ...more] = _, this) : [data, ...more];
}
return _gup;
}
select
sets the initial phase to the specified function, passing null
resets the phase to its default (identity function), and returns the GUP
factory. If no arguments are specified, returns the current select
function, which is guaranteed to be a function.
gup.select = function(_) {
return arguments.length ? (select = _, this) : (select || identity);
}
pre
sets the first phase to the specified function, passing null
resets
the phase to its default (empty function), and returns the GUP factory. If
no arguments are specified, returns the current pre
function, which is
guaranteed to be a function.
gup.pre = function(_) {
return arguments.length ? (pre = _, this) : (pre || empty);
}
exit
sets the second phase to the specified function, passing null
resets the phase to its default (empty function), and returns the GUP
factory. If no arguments are specified, returns the current exit
function, which is guaranteed to be a function.
gup.exit = function(_) {
return arguments.length ? (exit = _, this) : (exit || empty);
}
enter
sets the third phase to the specified function, passing null
resets the phase to its default (identity function), and returns the GUP
factory. If no arguments are specified, returns the current enter
function, which is guaranteed to be a function.
gup.enter = function(_) {
return arguments.length ? (enter = _, this) : (enter || identity);
}
post
sets the fourth phase to the specified function, passing null
resets the phase to its default (empty function), and returns the GUP
factory. If no arguments are specified, returns the current post
function, which is guaranteed to be a function.
gup.post = function(_) {
return arguments.length ? (post = _, this) : (post || empty);
}
update
is a shorthand for setting the four update phase functions, pre
,
exit
, enter
and post
in that sequence. If no functions are specified
then the four functions are returned in an array in the aforementioned
order.
gup.update = function(...args) {
switch (arguments.length) {
case 0: return [
pre || empty,
exit || empty,
enter || identity,
post || empty
];
case 1:
[pre] = args;
break;
case 2:
[pre, exit] = args;
break;
case 3:
[pre, exit, enter] = args;
break;
default:
[pre, exit, enter, post] = args;
}
return this;
}
return gup;
}
empty
is checked and considered an no-op for the pre
, exit
and post
functions.
export function empty() {}
identity
is checked and considered an no-op for the select
and enter
functions.
export function identity($) { return $; }