src/layer.js
import kotoAssert from './assert.js';
import * as d3 from 'd3';
/**
* Create a layer using the provided `base` selection.
*
* @class
*
* @param {d3.selection} base The containing DOM node for the layer.
* @param {Object} options Overrides for databind, insert and event methods.
* @param {Function} options.databind databind override
* @param {Function} options.insert insert override
* @param {Function} [options.events] life-cycle event handler overrides.
* Possible values are [enter, update, merge, exit]
* with or without the 'transition postfix'.
*/
class Layer {
constructor(base, options) {
this._base = base;
this._handlers = {};
this._lifecycleRe = /^(enter|update|merge|exit)(:transition)?$/;
if (options) {
// Set layer methods (required)
this.dataBind = options.dataBind;
this.insert = options.insert;
// Bind events (optional)
if ('events' in options) {
for (var eventName in options.events) {
this.on(eventName, options.events[eventName]);
}
}
}
}
/**
* Invoked by {@link Layer#draw} to join data with this layer's DOM nodes. This
* implementation is "virtual"--it *must* be overridden by Layer instances.
*
* @param {Array} data Value passed to {@link Layer#draw}
* @param {Object} [context] the instance of this layers
*/
dataBind() {
kotoAssert(false, 'Layers must specify a dataBind method.');
}
/**
* Invoked by {@link Layer#draw} in order to insert new DOM nodes into this
* layer's `base`. This implementation is "virtual"--it *must* be overridden by
* Layer instances.
*/
insert() {
kotoAssert(false, 'Layers must specify an `insert` method.');
}
/**
* Subscribe a handler to a lifecycle event. These events (and only these
* events) are triggered when {@link Layer#draw} is invoked--see that method
* for more details on lifecycle events.
*
* @param {String} eventName Identifier for the lifecycle event for which to
* subscribe.
* @param {Function} handler Callback function
*
* @returns {Chart} Reference to the layer instance (for chaining).
*/
on(eventName, handler, options) {
options = options || {};
kotoAssert(this._lifecycleRe.test(eventName),
`Unrecognized lifecycle event name specified to 'Layer#on': '${eventName}'.`);
if (!(eventName in this._handlers)) {
this._handlers[eventName] = [];
}
this._handlers[eventName].push({
callback: handler,
chart: options.chart || null
});
return this;
}
/**
* Unsubscribe the specified handler from the specified event. If no handler is
* supplied, remove *all* handlers from the event.
*
* @param {String} eventName Identifier for event from which to remove
* unsubscribe
* @param {Function} handler Callback to remove from the specified event
*
* @returns {Chart} Reference to the layer instance (for chaining).
*/
off(eventName, handler) {
var handlers = this._handlers[eventName];
var idx;
kotoAssert(this._lifecycleRe.test(eventName),
`Unrecognized lifecycle event name specified to 'Layer#on': '${eventName}'.`);
if (!handlers) {
return this;
}
if (arguments.length === 1) {
handlers.length = 0;
return this;
}
for (idx = handlers.length - 1; idx > -1; --idx) {
if (handlers[idx].callback === handler) {
handlers.splice(idx, 1);
}
}
return this;
}
/**
* Render the layer according to the input data. Bind the data to the layer
* (according to {@link Layer#dataBind}, insert new elements (according to
* {@link Layer#insert}, make lifecycle selections, and invoke all relevant
* handlers (as attached via {@link Layer#on}) with the lifecycle selections.
*
* - update
* - update:transition
* - enter
* - enter:transition
* - exit
* - exit:transition
*
* @param {Array} data Data to drive the rendering.
*/
draw(data) {
var bound,
entering,
events,
selection,
method,
handlers,
eventName,
idx,
len,
tidx,
tlen,
promises = [];
function endAll(transition, callback) {
let n = 0;
transition
.each(function inc () { ++n; })
.on('end', function done (...args) {
if (!--n) {
callback.apply(this, args);
}
});
}
function promiseCallback (resolve) {
selection.call(endAll, function allDone () { return resolve(true); });
}
bound = this.dataBind.call(this._base, data, this);
kotoAssert(bound instanceof d3.selection,
'Invalid selection defined by `Layer#dataBind` method.');
kotoAssert(bound.enter, 'Layer selection not properly bound.');
entering = bound.enter();
entering._chart = this._base._chart;
events = [
{
name: 'update',
selection: bound
},
{
name: 'enter',
selection: entering,
method: this.insert
},
{
name: 'merge',
// Although the `merge` lifecycle event shares its selection object
// with the `update` lifecycle event, the object's contents will be
// modified when koto invokes the user-supplied `insert` method
// when triggering the `enter` event.
selection: bound
},
{
name: 'exit',
// Although the `exit` lifecycle event shares its selection object
// with the `update` and `merge` lifecycle events, the object's
// contents will be modified when koto invokes
// `d3.selection.exit`.
selection: bound,
method: bound.exit
}
];
for (var i = 0, l = events.length; i < l; ++i) {
eventName = events[i].name;
selection = events[i].selection;
method = events[i].method;
// Some lifecycle selections modify shared state, so they must be
// deferred until just prior to handler invocation.
if (typeof method === 'function') {
selection = method.call(selection, selection);
}
if (selection.empty()) {
continue;
}
kotoAssert(selection && selection instanceof d3.selection,
`Invalid selection defined for ${eventName} lifecycle event.`);
handlers = this._handlers[eventName];
if (handlers) {
for (idx = 0, len = handlers.length; idx < len; ++idx) {
// Attach a reference to the parent chart so the selection's
// `chart` method will function correctly.
selection._chart = handlers[idx].chart || this._base._chart;
// selection.call(handlers[idx].callback);
handlers[idx].callback.call(selection, selection);
}
}
handlers = this._handlers[eventName + ':transition'];
if (handlers && handlers.length) {
selection = selection.transition();
for (tlen = handlers.length, tidx = 0; tidx < tlen; ++tidx) {
selection._chart = handlers[tidx].chart || this._base._chart;
// selection.call(handlers[tidx].callback);
handlers[tidx].callback.call(selection, selection);
promises.push(new Promise(promiseCallback));
}
}
this.promise = Promise.all(promises);
}
}
}
export default Layer;