diff --git a/Gruntfile.js b/Gruntfile.js
index c1456d7..0a6deb0 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -8,7 +8,9 @@ module.exports = function(grunt) {
srcFiles: [
"src/init.js",
"src/layer.js",
- "src/chart.js"
+ "src/layer-extensions.js",
+ "src/chart.js",
+ "src/chart-extensions.js"
]
},
watch: {
@@ -27,7 +29,15 @@ module.exports = function(grunt) {
},
chart: {
options: {
- browser: true
+ browser: true,
+ globalstrict: true,
+ globals: {
+ hasOwnProp: true,
+ d3: true,
+ d3cAssert: true,
+ Layer: true,
+ Chart: true
+ }
},
files: {
src: "<%= meta.srcFiles %>"
@@ -70,7 +80,8 @@ module.exports = function(grunt) {
banner: "/*! <%= meta.pkg.name %> - v<%= meta.pkg.version %>\n" +
" * License: <%= meta.pkg.license %>\n" +
" * Date: <%= grunt.template.today('yyyy-mm-dd') %>\n" +
- " */\n"
+ " */\n(function(window) {\n",
+ footer: "})(this);"
},
release: {
files: {
@@ -81,7 +92,8 @@ module.exports = function(grunt) {
uglify: {
options: {
// Preserve banner
- preserveComments: "some"
+ preserveComments: "some",
+ sourceMap: "d3.chart.min.map"
},
release: {
files: {
diff --git a/changelog.md b/changelog.md
index 9130d8d..6bffe23 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,14 @@
# d3.chart change log
+### 0.2.0 (02.21.2014)
+
+- Introduce more intuitive inheritance behavior for `Chart#transform`
+- Limit Chart constructors to a single "options" parameter
+- Implement `Chart#attach` as a different signature for `Chart#mixin`
+- Implement `Chart#demux`
+- Remove `Chart#mixin`
+- Honor `transform` option specified at Chart initialization time
+
### 0.1.3 (10.07.2013)
- Fix bug in Chart#unlayer
diff --git a/d3.chart.js b/d3.chart.js
index 2e679fc..2a03c18 100644
--- a/d3.chart.js
+++ b/d3.chart.js
@@ -1,470 +1,730 @@
-/*! d3.chart - v0.1.3
+/*! d3.chart - v0.2.0
* License: MIT Expat
- * Date: 2013-10-07
+ * Date: 2014-02-21
*/
-(function(window, undefined) {
-
+(function(window) {
"use strict";
+/*jshint unused: false */
-var previousD3Chart = window.d3Chart;
-var d3Chart = window.d3Chart = {};
var d3 = window.d3;
+var hasOwnProp = Object.hasOwnProperty;
-d3Chart.noConflict = function() {
- window.d3Chart = previousD3Chart;
- return d3Chart;
-};
-
-d3Chart.assert = function(test, message) {
+var d3cAssert = function(test, message) {
if (test) {
return;
}
throw new Error("[d3.chart] " + message);
};
-d3Chart.assert(d3, "d3.js is required");
-d3Chart.assert(typeof d3.version === "string" && d3.version.match(/^3/),
+d3cAssert(d3, "d3.js is required");
+d3cAssert(typeof d3.version === "string" && d3.version.match(/^3/),
"d3.js version 3 is required");
-}(this));
+"use strict";
-(function(window, undefined) {
+var lifecycleRe = /^(enter|update|merge|exit)(:transition)?$/;
+
+/**
+ * Create a layer using the provided `base`. The layer instance is *not*
+ * exposed to d3.chart users. Instead, its instance methods are mixed in to the
+ * `base` selection it describes; users interact with the instance via these
+ * bound methods.
+ *
+ * @private
+ * @constructor
+ *
+ * @param {d3.selection} base The containing DOM node for the layer.
+ */
+var Layer = function(base) {
+ d3cAssert(base, "Layers must be initialized with a base.");
+ this._base = base;
+ this._handlers = {};
+};
- "use strict";
+/**
+ * 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}
+ */
+Layer.prototype.dataBind = function() {
+ d3cAssert(false, "Layers must specify a `dataBind` method.");
+};
- var d3Chart = window.d3Chart;
- var d3 = window.d3;
+/**
+ * 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.
+ */
+Layer.prototype.insert = function() {
+ d3cAssert(false, "Layers must specify an `insert` method.");
+};
- var Layer = function(base) {
- d3Chart.assert(base, "Layers must be initialized with a base.");
- this._base = base;
- this._handlers = {};
- };
+/**
+ * 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 {d3.selection} Reference to the layer's base.
+ */
+Layer.prototype.on = function(eventName, handler, options) {
+ options = options || {};
- // dataBind
- Layer.prototype.dataBind = function() {
- d3Chart.assert(false, "Layers must specify a `dataBind` method.");
- };
+ d3cAssert(
+ lifecycleRe.test(eventName),
+ "Unrecognized lifecycle event name specified to `Layer#on`: '" +
+ eventName + "'."
+ );
- // insert
- Layer.prototype.insert = function() {
- d3Chart.assert(false, "Layers must specify an `insert` method.");
- };
+ if (!(eventName in this._handlers)) {
+ this._handlers[eventName] = [];
+ }
+ this._handlers[eventName].push({
+ callback: handler,
+ chart: options.chart || null
+ });
+ return this._base;
+};
- // on
- // Attach the specified handler to the specified event type.
- Layer.prototype.on = function(eventName, handler, options) {
- options = options || {};
- if (!(eventName in this._handlers)) {
- this._handlers[eventName] = [];
- }
- this._handlers[eventName].push({
- callback: handler,
- chart: options.chart || null
- });
- };
+/**
+ * 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 {d3.selection} Reference to the layer's base.
+ */
+Layer.prototype.off = function(eventName, handler) {
- // off
- // Remove the specified handler. If no handler is supplied, remove *all*
- // handlers from the specified event type.
- Layer.prototype.off = function(eventName, handler) {
+ var handlers = this._handlers[eventName];
+ var idx;
- var handlers = this._handlers[eventName];
- var idx;
+ d3cAssert(
+ lifecycleRe.test(eventName),
+ "Unrecognized lifecycle event name specified to `Layer#off`: '" +
+ eventName + "'."
+ );
- if (!handlers) {
- return;
- }
+ if (!handlers) {
+ return this._base;
+ }
+
+ if (arguments.length === 1) {
+ handlers.length = 0;
+ return this._base;
+ }
- if (arguments.length === 1) {
- handlers.length = 0;
- return;
+ for (idx = handlers.length - 1; idx > -1; --idx) {
+ if (handlers[idx].callback === handler) {
+ handlers.splice(idx, 1);
}
+ }
+ return this._base;
+};
- for (idx = handlers.length - 1; idx > -1; --idx) {
- if (handlers[idx].callback === handler) {
- handlers.splice(idx, 1);
- }
+/**
+ * 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.
+ */
+Layer.prototype.draw = function(data) {
+ var bound, entering, events, selection, handlers, eventName, idx, len;
+
+ bound = this.dataBind.call(this._base, data);
+
+ // Although `bound instanceof d3.selection` is more explicit, it fails
+ // in IE8, so we use duck typing to maintain compatability.
+ d3cAssert(bound && bound.call === d3.selection.prototype.call,
+ "Invalid selection defined by `Layer#dataBind` method.");
+ d3cAssert(bound.enter, "Layer selection not properly bound.");
+
+ entering = bound.enter();
+ entering._chart = this._base._chart;
+
+ events = [
+ {
+ name: "update",
+ selection: bound
+ },
+ {
+ name: "enter",
+ // Defer invocation of the `insert` method so that the previous
+ // `update` selection does not contain the new nodes.
+ selection: this.insert.bind(entering)
+ },
+ {
+ name: "merge",
+ // This selection will be modified when the previous selection
+ // is made.
+ selection: bound
+ },
+ {
+ name: "exit",
+ selection: bound.exit.bind(bound)
}
- };
+ ];
- // draw
- // Bind the data to the layer, make lifecycle selections, and invoke all
- // relevant handlers.
- Layer.prototype.draw = function(data) {
- var bound, entering, events, selection, handlers, eventName, idx, len;
-
- bound = this.dataBind.call(this._base, data);
-
- // Although `bound instanceof d3.selection` is more explicit, it fails
- // in IE8, so we use duck typing to maintain compatability.
- d3Chart.assert(bound && bound.call === d3.selection.prototype.call,
- "Invalid selection defined by `Layer#dataBind` method.");
- d3Chart.assert(bound.enter, "Layer selection not properly bound.");
-
- entering = bound.enter();
- entering._chart = this._base._chart;
-
- events = [
- {
- name: "update",
- selection: bound
- },
- {
- name: "enter",
- // Defer invocation of the `insert` method so that the previous
- // `update` selection does not contain the new nodes.
- selection: this.insert.bind(entering)
- },
- {
- name: "merge",
- // This selection will be modified when the previous selection
- // is made.
- selection: bound
- },
- {
- name: "exit",
- selection: bound.exit.bind(bound)
- }
- ];
+ for (var i = 0, l = events.length; i < l; ++i) {
+ eventName = events[i].name;
+ selection = events[i].selection;
- for (var i = 0, l = events.length; i < l; ++i) {
- eventName = events[i].name;
- selection = events[i].selection;
+ // Some lifecycle selections are expressed as functions so that
+ // they may be delayed.
+ if (typeof selection === "function") {
+ selection = selection();
+ }
- // Some lifecycle selections are expressed as functions so that
- // they may be delayed.
- if (typeof selection === "function") {
- selection = selection();
- }
+ if (selection.empty()) {
+ continue;
+ }
- // Although `selection instanceof d3.selection` is more explicit,
- // it fails in IE8, so we use duck typing to maintain
- // compatability.
- d3Chart.assert(selection &&
- selection.call === d3.selection.prototype.call,
- "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);
- }
+ // Although `selection instanceof d3.selection` is more explicit,
+ // it fails in IE8, so we use duck typing to maintain
+ // compatability.
+ d3cAssert(selection &&
+ selection.call === d3.selection.prototype.call,
+ "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 = this._handlers[eventName + ":transition"];
+ handlers = this._handlers[eventName + ":transition"];
- if (handlers && handlers.length) {
- selection = selection.transition();
- for (idx = 0, len = handlers.length; idx < len; ++idx) {
- selection._chart = handlers[idx].chart || this._base._chart;
- selection.call(handlers[idx].callback);
- }
+ if (handlers && handlers.length) {
+ selection = selection.transition();
+ for (idx = 0, len = handlers.length; idx < len; ++idx) {
+ selection._chart = handlers[idx].chart || this._base._chart;
+ selection.call(handlers[idx].callback);
}
}
- };
+ }
+};
- d3.selection.prototype.layer = function(options) {
- var layer = new Layer(this);
- var eventName;
+"use strict";
+
+/**
+ * Create a new layer on the d3 selection from which it is called.
+ *
+ * @static
+ *
+ * @param {Object} [options] Options to be forwarded to {@link Layer|the Layer
+ * constructor}
+ * @returns {d3.selection}
+ */
+d3.selection.prototype.layer = function(options) {
+ var layer = new Layer(this);
+ var eventName;
+
+ // Set layer methods (required)
+ layer.dataBind = options.dataBind;
+ layer.insert = options.insert;
+
+ // Bind events (optional)
+ if ("events" in options) {
+ for (eventName in options.events) {
+ layer.on(eventName, options.events[eventName]);
+ }
+ }
- // Set layer methods (required)
- layer.dataBind = options.dataBind;
- layer.insert = options.insert;
+ // Mix the public methods into the D3.js selection (bound appropriately)
+ this.on = function() { return layer.on.apply(layer, arguments); };
+ this.off = function() { return layer.off.apply(layer, arguments); };
+ this.draw = function() { return layer.draw.apply(layer, arguments); };
+
+ return this;
+};
+
+"use strict";
- // Bind events (optional)
- if ("events" in options) {
- for (eventName in options.events) {
- layer.on(eventName, options.events[eventName]);
+// extend
+// Borrowed from Underscore.js
+function extend(object) {
+ var argsIndex, argsLength, iteratee, key;
+ if (!object) {
+ return object;
+ }
+ argsLength = arguments.length;
+ for (argsIndex = 1; argsIndex < argsLength; argsIndex++) {
+ iteratee = arguments[argsIndex];
+ if (iteratee) {
+ for (key in iteratee) {
+ object[key] = iteratee[key];
}
}
+ }
+ return object;
+}
+
+/**
+ * Call the {@Chart#initialize} method up the inheritance chain, starting with
+ * the base class and continuing "downward".
+ *
+ * @private
+ */
+var initCascade = function(instance, args) {
+ var ctor = this.constructor;
+ var sup = ctor.__super__;
+ if (sup) {
+ initCascade.call(sup, instance, args);
+ }
- // Mix the public methods into the D3.js selection (bound appropriately)
- this.on = function() { return layer.on.apply(layer, arguments); };
- this.off = function() { return layer.off.apply(layer, arguments); };
- this.draw = function() { return layer.draw.apply(layer, arguments); };
+ // Do not invoke the `initialize` method on classes further up the
+ // prototype chain (again).
+ if (hasOwnProp.call(ctor.prototype, "initialize")) {
+ this.initialize.apply(instance, args);
+ }
+};
- return this;
- };
+/**
+ * Call the `transform` method down the inheritance chain, starting with the
+ * instance and continuing "upward". The result of each transformation should
+ * be supplied as input to the next.
+ *
+ * @private
+ */
+var transformCascade = function(instance, data) {
+ var ctor = this.constructor;
+ var sup = ctor.__super__;
+
+ // Unlike `initialize`, the `transform` method has significance when
+ // attached directly to a chart instance. Ensure that this transform takes
+ // first but is not invoked on later recursions.
+ if (this === instance && hasOwnProp.call(this, "transform")) {
+ data = this.transform(data);
+ }
-}(this));
+ // Do not invoke the `transform` method on classes further up the prototype
+ // chain (yet).
+ if (hasOwnProp.call(ctor.prototype, "transform")) {
+ data = ctor.prototype.transform.call(instance, data);
+ }
-(function(window, undefined) {
+ if (sup) {
+ data = transformCascade.call(sup, instance, data);
+ }
- "use strict";
+ return data;
+};
- var d3Chart = window.d3Chart;
- var d3 = window.d3;
- var hasOwnProp = Object.hasOwnProperty;
+/**
+ * Create a d3.chart
+ *
+ * @param {d3.selection} selection The chart's "base" DOM node. This should
+ * contain any nodes that the chart generates.
+ * @param {mixed} chartOptions A value for controlling how the chart should be
+ * created. This value will be forwarded to {@link Chart#initialize}, so
+ * charts may define additional properties for consumers to modify their
+ * behavior during initialization.
+ *
+ * @constructor
+ */
+var Chart = function(selection, chartOptions) {
- var Surrogate = function(ctor) { this.constructor = ctor; };
- var variadicNew = function(Ctor, args) {
- var inst;
- Surrogate.prototype = Ctor.prototype;
- inst = new Surrogate(Ctor);
- Ctor.apply(inst, args);
- return inst;
- };
+ this.base = selection;
+ this._layers = {};
+ this._attached = {};
+ this._events = {};
- // extend
- // Borrowed from Underscore.js
- function extend(object) {
- var argsIndex, argsLength, iteratee, key;
- if (!object) {
- return object;
- }
- argsLength = arguments.length;
- for (argsIndex = 1; argsIndex < argsLength; argsIndex++) {
- iteratee = arguments[argsIndex];
- if (iteratee) {
- for (key in iteratee) {
- object[key] = iteratee[key];
- }
- }
- }
- return object;
+ if (chartOptions && chartOptions.transform) {
+ this.transform = chartOptions.transform;
}
- // initCascade
- // Call the initialize method up the inheritance chain, starting with the
- // base class and continuing "downward".
- var initCascade = function(instance, args) {
- var sup = this.constructor.__super__;
- if (sup) {
- initCascade.call(sup, instance, args);
- }
- // Do not invoke the `initialize` method on classes further up the
- // prototype chain.
- if (hasOwnProp.call(this.constructor.prototype, "initialize")) {
- this.initialize.apply(instance, args);
- }
- };
-
- var Chart = function(selection) {
+ initCascade.call(this, this, [chartOptions]);
+};
- this.base = selection;
- this._layers = {};
- this._mixins = [];
- this._events = {};
+/**
+ * Set up a chart instance. This method is intended to be overridden by Charts
+ * authored with this library. It will be invoked with a single argument: the
+ * `options` value supplied to the {@link Chart|constructor}.
+ *
+ * For charts that are defined as extensions of other charts using
+ * `Chart.extend`, each chart's `initilize` method will be invoked starting
+ * with the "oldest" ancestor (see the private {@link initCascade} function for
+ * more details).
+ */
+Chart.prototype.initialize = function() {};
+
+/**
+ * Remove a layer from the chart.
+ *
+ * @param {String} name The name of the layer to remove.
+ *
+ * @returns {Layer} The layer removed by this operation.
+ */
+Chart.prototype.unlayer = function(name) {
+ var layer = this.layer(name);
- initCascade.call(this, this, Array.prototype.slice.call(arguments, 1));
- };
+ delete this._layers[name];
+ delete layer._chart;
- Chart.prototype.unlayer = function(name) {
- var layer = this.layer(name);
+ return layer;
+};
- delete this._layers[name];
- delete layer._chart;
+/**
+ * Interact with the chart's {@link Layer|layers}.
+ *
+ * If only a `name` is provided, simply return the layer registered to that
+ * name (if any).
+ *
+ * If a `name` and `selection` are provided, treat the `selection` as a
+ * previously-created layer and attach it to the chart with the specified
+ * `name`.
+ *
+ * If all three arguments are specified, initialize a new {@link Layer} using
+ * the specified `selection` as a base passing along the specified `options`.
+ *
+ * The {@link Layer.draw} method of attached layers will be invoked
+ * whenever this chart's {@link Chart#draw} is invoked and will receive the
+ * data (optionally modified by the chart's {@link Chart#transform} method.
+ *
+ * @param {String} name Name of the layer to attach or retrieve.
+ * @param {d3.selection|Layer} [selection] The layer's base or a
+ * previously-created {@link Layer}.
+ * @param {Object} [options] Options to be forwarded to {@link Layer|the Layer
+ * constructor}
+ *
+ * @returns {Layer}
+ */
+Chart.prototype.layer = function(name, selection, options) {
+ var layer;
- return layer;
- };
+ if (arguments.length === 1) {
+ return this._layers[name];
+ }
- Chart.prototype.layer = function(name, selection, options) {
- var layer;
+ // we are reattaching a previous layer, which the
+ // selection argument is now set to.
+ if (arguments.length === 2) {
- if (arguments.length === 1) {
+ if (typeof selection.draw === "function") {
+ selection._chart = this;
+ this._layers[name] = selection;
return this._layers[name];
- }
- // we are reattaching a previous layer, which the
- // selection argument is now set to.
- if (arguments.length === 2) {
-
- if (typeof selection.draw === "function") {
- selection._chart = this;
- this._layers[name] = selection;
- return this._layers[name];
-
- } else {
- d3Chart.assert(false, "When reattaching a layer, the second argument "+
- "must be a d3.chart layer");
- }
+ } else {
+ d3cAssert(false, "When reattaching a layer, the second argument "+
+ "must be a d3.chart layer");
}
+ }
- layer = selection.layer(options);
-
- this._layers[name] = layer;
+ layer = selection.layer(options);
- selection._chart = this;
+ this._layers[name] = layer;
- return layer;
- };
+ selection._chart = this;
- Chart.prototype.initialize = function() {};
+ return layer;
+};
- Chart.prototype.transform = function(data) {
- return data;
- };
+/**
+ * Register or retrieve an "attachment" Chart. The "attachment" chart's `draw`
+ * method will be invoked whenever the containing chart's `draw` method is
+ * invoked.
+ *
+ * @param {String} attachmentName Name of the attachment
+ * @param {Chart} [chart] d3.chart to register as a mix in of this chart. When
+ * unspecified, this method will return the attachment previously
+ * registered with the specified `attachmentName` (if any).
+ *
+ * @returns {Chart} Reference to this chart (chainable).
+ */
+Chart.prototype.attach = function(attachmentName, chart) {
+ if (arguments.length === 1) {
+ return this._attached[attachmentName];
+ }
- Chart.prototype.mixin = function(chartName, selection) {
- var args = Array.prototype.slice.call(arguments, 2);
- args.unshift(selection);
- var ctor = Chart[chartName];
- var chart = variadicNew(ctor, args);
+ this._attached[attachmentName] = chart;
+ return chart;
+};
- this._mixins.push(chart);
- return chart;
- };
+/**
+ * Update the chart's representation in the DOM, drawing all of its layers and
+ * any "attachment" charts (as attached via {@link Chart#attach}).
+ *
+ * @param {Object} data Data to pass to the {@link Layer#draw|draw method} of
+ * this cart's {@link Layer|layers} (if any) and the {@link
+ * Chart#draw|draw method} of this chart's attachments (if any).
+ */
+Chart.prototype.draw = function(data) {
- Chart.prototype.draw = function(data) {
+ var layerName, attachmentName, attachmentData;
- var layerName, idx, len;
+ data = transformCascade.call(this, this, data);
- data = this.transform(data);
+ for (layerName in this._layers) {
+ this._layers[layerName].draw(data);
+ }
- for (layerName in this._layers) {
- this._layers[layerName].draw(data);
+ for (attachmentName in this._attached) {
+ if (this.demux) {
+ attachmentData = this.demux(attachmentName, data);
+ } else {
+ attachmentData = data;
}
+ this._attached[attachmentName].draw(attachmentData);
+ }
+};
- for (idx = 0, len = this._mixins.length; idx < len; idx++) {
- this._mixins[idx].draw(data);
- }
- };
+/**
+ * Function invoked with the context specified when the handler was bound (via
+ * {@link Chart#on} {@link Chart#once}).
+ *
+ * @callback ChartEventHandler
+ * @param {...*} arguments Invoked with the arguments passed to {@link
+ * Chart#trigger}
+ */
- Chart.prototype.on = function(name, callback, context) {
- var events = this._events[name] || (this._events[name] = []);
- events.push({
- callback: callback,
- context: context || this,
- _chart: this
- });
- return this;
- };
+/**
+ * Subscribe a callback function to an event triggered on the chart. See {@link
+ * Chart#once} to subscribe a callback function to an event for one occurence.
+ *
+ * @param {String} name Name of the event
+ * @param {ChartEventHandler} callback Function to be invoked when the event
+ * occurs
+ * @param {Object} [context] Value to set as `this` when invoking the
+ * `callback`. Defaults to the chart instance.
+ *
+ * @returns {Chart} A reference to this chart (chainable).
+ */
+Chart.prototype.on = function(name, callback, context) {
+ var events = this._events[name] || (this._events[name] = []);
+ events.push({
+ callback: callback,
+ context: context || this,
+ _chart: this
+ });
+ return this;
+};
- Chart.prototype.once = function(name, callback, context) {
- var self = this;
- var once = function() {
- self.off(name, once);
- callback.apply(this, arguments);
- };
- return this.on(name, once, context);
+/**
+ * Subscribe a callback function to an event triggered on the chart. This
+ * function will be invoked at the next occurance of the event and immediately
+ * unsubscribed. See {@link Chart#on} to subscribe a callback function to an
+ * event indefinitely.
+ *
+ * @param {String} name Name of the event
+ * @param {ChartEventHandler} callback Function to be invoked when the event
+ * occurs
+ * @param {Object} [context] Value to set as `this` when invoking the
+ * `callback`. Defaults to the chart instance
+ *
+ * @returns {Chart} A reference to this chart (chainable)
+ */
+Chart.prototype.once = function(name, callback, context) {
+ var self = this;
+ var once = function() {
+ self.off(name, once);
+ callback.apply(this, arguments);
};
+ return this.on(name, once, context);
+};
- Chart.prototype.off = function(name, callback, context) {
- var names, n, events, event, i, j;
+/**
+ * Unsubscribe one or more callback functions from an event triggered on the
+ * chart. When no arguments are specified, *all* handlers will be unsubscribed.
+ * When only a `name` is specified, all handlers subscribed to that event will
+ * be unsubscribed. When a `name` and `callback` are specified, only that
+ * function will be unsubscribed from that event. When a `name` and `context`
+ * are specified (but `callback` is omitted), all events bound to the given
+ * event with the given context will be unsubscribed.
+ *
+ * @param {String} [name] Name of the event to be unsubscribed
+ * @param {ChartEventHandler} [callback] Function to be unsubscribed
+ * @param {Object} [context] Contexts to be unsubscribe
+ *
+ * @returns {Chart} A reference to this chart (chainable).
+ */
+Chart.prototype.off = function(name, callback, context) {
+ var names, n, events, event, i, j;
- // remove all events
- if (arguments.length === 0) {
- for (name in this._events) {
- this._events[name].length = 0;
- }
- return this;
+ // remove all events
+ if (arguments.length === 0) {
+ for (name in this._events) {
+ this._events[name].length = 0;
}
+ return this;
+ }
- // remove all events for a specific name
- if (arguments.length === 1) {
- events = this._events[name];
- if (events) {
- events.length = 0;
- }
- return this;
+ // remove all events for a specific name
+ if (arguments.length === 1) {
+ events = this._events[name];
+ if (events) {
+ events.length = 0;
}
+ return this;
+ }
- // remove all events that match whatever combination of name, context
- // and callback.
- names = name ? [name] : Object.keys(this._events);
- for (i = 0; i < names.length; i++) {
- n = names[i];
- events = this._events[n];
- j = events.length;
- while (j--) {
- event = events[j];
- if ((callback && callback === event.callback) ||
- (context && context === event.context)) {
- events.splice(j, 1);
- }
+ // remove all events that match whatever combination of name, context
+ // and callback.
+ names = name ? [name] : Object.keys(this._events);
+ for (i = 0; i < names.length; i++) {
+ n = names[i];
+ events = this._events[n];
+ j = events.length;
+ while (j--) {
+ event = events[j];
+ if ((callback && callback === event.callback) ||
+ (context && context === event.context)) {
+ events.splice(j, 1);
}
}
+ }
- return this;
- };
-
- Chart.prototype.trigger = function(name) {
- var args = Array.prototype.slice.call(arguments, 1);
- var events = this._events[name];
- var i, ev;
+ return this;
+};
- if (events !== undefined) {
- for (i = 0; i < events.length; i++) {
- ev = events[i];
- ev.callback.apply(ev.context, args);
- }
+/**
+ * Publish an event on this chart with the given `name`.
+ *
+ * @param {String} name Name of the event to publish
+ * @param {...*} arguments Values with which to invoke the registered
+ * callbacks.
+ *
+ * @returns {Chart} A reference to this chart (chainable).
+ */
+Chart.prototype.trigger = function(name) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var events = this._events[name];
+ var i, ev;
+
+ if (events !== undefined) {
+ for (i = 0; i < events.length; i++) {
+ ev = events[i];
+ ev.callback.apply(ev.context, args);
}
+ }
- return this;
- };
-
- Chart.extend = function(name, protoProps, staticProps) {
- var parent = this;
- var child;
+ return this;
+};
- // The constructor function for the new subclass is either defined by
- // you (the "constructor" property in your `extend` definition), or
- // defaulted by us to simply call the parent's constructor.
- if (protoProps && hasOwnProp.call(protoProps, "constructor")) {
- child = protoProps.constructor;
- } else {
- child = function(){ return parent.apply(this, arguments); };
- }
+/**
+ * Create a new {@link Chart} constructor with the provided options acting as
+ * "overrides" for the default chart instance methods. Allows for basic
+ * inheritance so that new chart constructors may be defined in terms of
+ * existing chart constructors. Based on the `extend` function defined by
+ * {@link http://backbonejs.org/|Backbone.js}.
+ *
+ * @static
+ *
+ * @param {String} name Identifier for the new Chart constructor.
+ * @param {Object} protoProps Properties to set on the new chart's prototype.
+ * @param {Object} staticProps Properties to set on the chart constructor
+ * itself.
+ *
+ * @returns {Function} A new Chart constructor
+ */
+Chart.extend = function(name, protoProps, staticProps) {
+ var parent = this;
+ var child;
+
+ // The constructor function for the new subclass is either defined by
+ // you (the "constructor" property in your `extend` definition), or
+ // defaulted by us to simply call the parent's constructor.
+ if (protoProps && hasOwnProp.call(protoProps, "constructor")) {
+ child = protoProps.constructor;
+ } else {
+ child = function(){ return parent.apply(this, arguments); };
+ }
- // Add static properties to the constructor function, if supplied.
- extend(child, parent, staticProps);
+ // Add static properties to the constructor function, if supplied.
+ extend(child, parent, staticProps);
- // Set the prototype chain to inherit from `parent`, without calling
- // `parent`'s constructor function.
- var Surrogate = function(){ this.constructor = child; };
- Surrogate.prototype = parent.prototype;
- child.prototype = new Surrogate();
+ // Set the prototype chain to inherit from `parent`, without calling
+ // `parent`'s constructor function.
+ var Surrogate = function(){ this.constructor = child; };
+ Surrogate.prototype = parent.prototype;
+ child.prototype = new Surrogate();
- // Add prototype properties (instance properties) to the subclass, if
- // supplied.
- if (protoProps) { extend(child.prototype, protoProps); }
+ // Add prototype properties (instance properties) to the subclass, if
+ // supplied.
+ if (protoProps) { extend(child.prototype, protoProps); }
- // Set a convenience property in case the parent's prototype is needed
- // later.
- child.__super__ = parent.prototype;
+ // Set a convenience property in case the parent's prototype is needed
+ // later.
+ child.__super__ = parent.prototype;
- Chart[name] = child;
- return child;
- };
+ Chart[name] = child;
+ return child;
+};
- // d3.chart
- // A factory for creating chart constructors
- d3.chart = function(name) {
- if (arguments.length === 0) {
- return Chart;
- } else if (arguments.length === 1) {
- return Chart[name];
- }
+"use strict";
- return Chart.extend.apply(Chart, arguments);
- };
+/**
+ * Create a new chart constructor or return a previously-created chart
+ * constructor.
+ *
+ * @static
+ *
+ * @param {String} name If no other arguments are specified, return the
+ * previously-created chart with this name.
+ * @param {Object} protoProps If specified, this value will be forwarded to
+ * {@link Chart.extend} and used to create a new chart.
+ * @param {Object} staticProps If specified, this value will be forwarded to
+ * {@link Chart.extend} and used to create a new chart.
+ */
+d3.chart = function(name) {
+ if (arguments.length === 0) {
+ return Chart;
+ } else if (arguments.length === 1) {
+ return Chart[name];
+ }
- d3.selection.prototype.chart = function(chartName) {
- // Without an argument, attempt to resolve the current selection's
- // containing d3.chart.
- if (arguments.length === 0) {
- return this._chart;
- }
- var ChartCtor = Chart[chartName];
- var chartArgs;
- d3Chart.assert(ChartCtor, "No chart registered with name '" +
- chartName + "'");
-
- chartArgs = Array.prototype.slice.call(arguments, 1);
- chartArgs.unshift(this);
- return variadicNew(ChartCtor, chartArgs);
- };
+ return Chart.extend.apply(Chart, arguments);
+};
- d3.selection.enter.prototype.chart = function() {
+/**
+ * Instantiate a chart or return the chart that the current selection belongs
+ * to.
+ *
+ * @static
+ *
+ * @param {String} [chartName] The name of the chart to instantiate. If the
+ * name is unspecified, this method will return the chart that the
+ * current selection belongs to.
+ * @param {mixed} options The options to use when instantiated the new chart.
+ * See {@link Chart} for more information.
+ */
+d3.selection.prototype.chart = function(chartName, options) {
+ // Without an argument, attempt to resolve the current selection's
+ // containing d3.chart.
+ if (arguments.length === 0) {
return this._chart;
- };
+ }
+ var ChartCtor = Chart[chartName];
+ d3cAssert(ChartCtor, "No chart registered with name '" + chartName + "'");
- d3.transition.prototype.chart = d3.selection.enter.prototype.chart;
+ return new ChartCtor(this, options);
+};
-}(this));
+// Implement the zero-argument signature of `d3.selection.prototype.chart`
+// for all selection types.
+d3.selection.enter.prototype.chart = function() {
+ return this._chart;
+};
+d3.transition.prototype.chart = d3.selection.enter.prototype.chart;
+})(this);
\ No newline at end of file
diff --git a/d3.chart.min.js b/d3.chart.min.js
index ccef3de..8e35b98 100644
--- a/d3.chart.min.js
+++ b/d3.chart.min.js
@@ -1,5 +1,6 @@
-/*! d3.chart - v0.1.3
+/*! d3.chart - v0.2.0
* License: MIT Expat
- * Date: 2013-10-07
+ * Date: 2014-02-21
*/
-(function(t){"use strict";var e=t.d3Chart,r=t.d3Chart={},n=t.d3;r.noConflict=function(){return t.d3Chart=e,r},r.assert=function(t,e){if(!t)throw Error("[d3.chart] "+e)},r.assert(n,"d3.js is required"),r.assert("string"==typeof n.version&&n.version.match(/^3/),"d3.js version 3 is required")})(this),function(t){"use strict";var e=t.d3Chart,r=t.d3,n=function(t){e.assert(t,"Layers must be initialized with a base."),this._base=t,this._handlers={}};n.prototype.dataBind=function(){e.assert(!1,"Layers must specify a `dataBind` method.")},n.prototype.insert=function(){e.assert(!1,"Layers must specify an `insert` method.")},n.prototype.on=function(t,e,r){r=r||{},t in this._handlers||(this._handlers[t]=[]),this._handlers[t].push({callback:e,chart:r.chart||null})},n.prototype.off=function(t,e){var r,n=this._handlers[t];if(n){if(1===arguments.length)return n.length=0,undefined;for(r=n.length-1;r>-1;--r)n[r].callback===e&&n.splice(r,1)}},n.prototype.draw=function(t){var n,i,s,a,o,h,c,l;n=this.dataBind.call(this._base,t),e.assert(n&&n.call===r.selection.prototype.call,"Invalid selection defined by `Layer#dataBind` method."),e.assert(n.enter,"Layer selection not properly bound."),i=n.enter(),i._chart=this._base._chart,s=[{name:"update",selection:n},{name:"enter",selection:this.insert.bind(i)},{name:"merge",selection:n},{name:"exit",selection:n.exit.bind(n)}];for(var u=0,p=s.length;p>u;++u){if(h=s[u].name,a=s[u].selection,"function"==typeof a&&(a=a()),e.assert(a&&a.call===r.selection.prototype.call,"Invalid selection defined for '"+h+"' lifecycle event."),o=this._handlers[h])for(c=0,l=o.length;l>c;++c)a._chart=o[c].chart||this._base._chart,a.call(o[c].callback);if(o=this._handlers[h+":transition"],o&&o.length)for(a=a.transition(),c=0,l=o.length;l>c;++c)a._chart=o[c].chart||this._base._chart,a.call(o[c].callback)}},r.selection.prototype.layer=function(t){var e,r=new n(this);if(r.dataBind=t.dataBind,r.insert=t.insert,"events"in t)for(e in t.events)r.on(e,t.events[e]);return this.on=function(){return r.on.apply(r,arguments)},this.off=function(){return r.off.apply(r,arguments)},this.draw=function(){return r.draw.apply(r,arguments)},this}}(this),function(t,e){"use strict";function r(t){var e,r,n,i;if(!t)return t;for(r=arguments.length,e=1;r>e;e++)if(n=arguments[e])for(i in n)t[i]=n[i];return t}var n=t.d3Chart,i=t.d3,s=Object.hasOwnProperty,a=function(t){this.constructor=t},o=function(t,e){var r;return a.prototype=t.prototype,r=new a(t),t.apply(r,e),r},h=function(t,e){var r=this.constructor.__super__;r&&h.call(r,t,e),s.call(this.constructor.prototype,"initialize")&&this.initialize.apply(t,e)},c=function(t){this.base=t,this._layers={},this._mixins=[],this._events={},h.call(this,this,Array.prototype.slice.call(arguments,1))};c.prototype.unlayer=function(t){var e=this.layer(t);return delete this._layers[t],delete e._chart,e},c.prototype.layer=function(t,e,r){var i;if(1===arguments.length)return this._layers[t];if(2===arguments.length){if("function"==typeof e.draw)return e._chart=this,this._layers[t]=e,this._layers[t];n.assert(!1,"When reattaching a layer, the second argument must be a d3.chart layer")}return i=e.layer(r),this._layers[t]=i,e._chart=this,i},c.prototype.initialize=function(){},c.prototype.transform=function(t){return t},c.prototype.mixin=function(t,e){var r=Array.prototype.slice.call(arguments,2);r.unshift(e);var n=c[t],i=o(n,r);return this._mixins.push(i),i},c.prototype.draw=function(t){var e,r,n;t=this.transform(t);for(e in this._layers)this._layers[e].draw(t);for(r=0,n=this._mixins.length;n>r;r++)this._mixins[r].draw(t)},c.prototype.on=function(t,e,r){var n=this._events[t]||(this._events[t]=[]);return n.push({callback:e,context:r||this,_chart:this}),this},c.prototype.once=function(t,e,r){var n=this,i=function(){n.off(t,i),e.apply(this,arguments)};return this.on(t,i,r)},c.prototype.off=function(t,e,r){var n,i,s,a,o,h;if(0===arguments.length){for(t in this._events)this._events[t].length=0;return this}if(1===arguments.length)return s=this._events[t],s&&(s.length=0),this;for(n=t?[t]:Object.keys(this._events),o=0;n.length>o;o++)for(i=n[o],s=this._events[i],h=s.length;h--;)a=s[h],(e&&e===a.callback||r&&r===a.context)&&s.splice(h,1);return this},c.prototype.trigger=function(t){var r,n,i=Array.prototype.slice.call(arguments,1),s=this._events[t];if(s!==e)for(r=0;s.length>r;r++)n=s[r],n.callback.apply(n.context,i);return this},c.extend=function(t,e,n){var i,a=this;i=e&&s.call(e,"constructor")?e.constructor:function(){return a.apply(this,arguments)},r(i,a,n);var o=function(){this.constructor=i};return o.prototype=a.prototype,i.prototype=new o,e&&r(i.prototype,e),i.__super__=a.prototype,c[t]=i,i},i.chart=function(t){return 0===arguments.length?c:1===arguments.length?c[t]:c.extend.apply(c,arguments)},i.selection.prototype.chart=function(t){if(0===arguments.length)return this._chart;var e,r=c[t];return n.assert(r,"No chart registered with name '"+t+"'"),e=Array.prototype.slice.call(arguments,1),e.unshift(this),o(r,e)},i.selection.enter.prototype.chart=function(){return this._chart},i.transition.prototype.chart=i.selection.enter.prototype.chart}(this);
\ No newline at end of file
+(function(t){"use strict";function e(t){var e,r,n,i;if(!t)return t;for(r=arguments.length,e=1;r>e;e++)if(n=arguments[e])for(i in n)t[i]=n[i];return t}var r=t.d3,n=Object.hasOwnProperty,i=function(t,e){if(!t)throw Error("[d3.chart] "+e)};i(r,"d3.js is required"),i("string"==typeof r.version&&r.version.match(/^3/),"d3.js version 3 is required");var a=/^(enter|update|merge|exit)(:transition)?$/,s=function(t){i(t,"Layers must be initialized with a base."),this._base=t,this._handlers={}};s.prototype.dataBind=function(){i(!1,"Layers must specify a `dataBind` method.")},s.prototype.insert=function(){i(!1,"Layers must specify an `insert` method.")},s.prototype.on=function(t,e,r){return r=r||{},i(a.test(t),"Unrecognized lifecycle event name specified to `Layer#on`: '"+t+"'."),t in this._handlers||(this._handlers[t]=[]),this._handlers[t].push({callback:e,chart:r.chart||null}),this._base},s.prototype.off=function(t,e){var r,n=this._handlers[t];if(i(a.test(t),"Unrecognized lifecycle event name specified to `Layer#off`: '"+t+"'."),!n)return this._base;if(1===arguments.length)return n.length=0,this._base;for(r=n.length-1;r>-1;--r)n[r].callback===e&&n.splice(r,1);return this._base},s.prototype.draw=function(t){var e,n,a,s,o,h,c,l;e=this.dataBind.call(this._base,t),i(e&&e.call===r.selection.prototype.call,"Invalid selection defined by `Layer#dataBind` method."),i(e.enter,"Layer selection not properly bound."),n=e.enter(),n._chart=this._base._chart,a=[{name:"update",selection:e},{name:"enter",selection:this.insert.bind(n)},{name:"merge",selection:e},{name:"exit",selection:e.exit.bind(e)}];for(var u=0,p=a.length;p>u;++u)if(h=a[u].name,s=a[u].selection,"function"==typeof s&&(s=s()),!s.empty()){if(i(s&&s.call===r.selection.prototype.call,"Invalid selection defined for '"+h+"' lifecycle event."),o=this._handlers[h])for(c=0,l=o.length;l>c;++c)s._chart=o[c].chart||this._base._chart,s.call(o[c].callback);if(o=this._handlers[h+":transition"],o&&o.length)for(s=s.transition(),c=0,l=o.length;l>c;++c)s._chart=o[c].chart||this._base._chart,s.call(o[c].callback)}},r.selection.prototype.layer=function(t){var e,r=new s(this);if(r.dataBind=t.dataBind,r.insert=t.insert,"events"in t)for(e in t.events)r.on(e,t.events[e]);return this.on=function(){return r.on.apply(r,arguments)},this.off=function(){return r.off.apply(r,arguments)},this.draw=function(){return r.draw.apply(r,arguments)},this};var o=function(t,e){var r=this.constructor,i=r.__super__;i&&o.call(i,t,e),n.call(r.prototype,"initialize")&&this.initialize.apply(t,e)},h=function(t,e){var r=this.constructor,i=r.__super__;return this===t&&n.call(this,"transform")&&(e=this.transform(e)),n.call(r.prototype,"transform")&&(e=r.prototype.transform.call(t,e)),i&&(e=h.call(i,t,e)),e},c=function(t,e){this.base=t,this._layers={},this._attached={},this._events={},e&&e.transform&&(this.transform=e.transform),o.call(this,this,[e])};c.prototype.initialize=function(){},c.prototype.unlayer=function(t){var e=this.layer(t);return delete this._layers[t],delete e._chart,e},c.prototype.layer=function(t,e,r){var n;if(1===arguments.length)return this._layers[t];if(2===arguments.length){if("function"==typeof e.draw)return e._chart=this,this._layers[t]=e,this._layers[t];i(!1,"When reattaching a layer, the second argument must be a d3.chart layer")}return n=e.layer(r),this._layers[t]=n,e._chart=this,n},c.prototype.attach=function(t,e){return 1===arguments.length?this._attached[t]:(this._attached[t]=e,e)},c.prototype.draw=function(t){var e,r,n;t=h.call(this,this,t);for(e in this._layers)this._layers[e].draw(t);for(r in this._attached)n=this.demux?this.demux(r,t):t,this._attached[r].draw(n)},c.prototype.on=function(t,e,r){var n=this._events[t]||(this._events[t]=[]);return n.push({callback:e,context:r||this,_chart:this}),this},c.prototype.once=function(t,e,r){var n=this,i=function(){n.off(t,i),e.apply(this,arguments)};return this.on(t,i,r)},c.prototype.off=function(t,e,r){var n,i,a,s,o,h;if(0===arguments.length){for(t in this._events)this._events[t].length=0;return this}if(1===arguments.length)return a=this._events[t],a&&(a.length=0),this;for(n=t?[t]:Object.keys(this._events),o=0;n.length>o;o++)for(i=n[o],a=this._events[i],h=a.length;h--;)s=a[h],(e&&e===s.callback||r&&r===s.context)&&a.splice(h,1);return this},c.prototype.trigger=function(t){var e,r,n=Array.prototype.slice.call(arguments,1),i=this._events[t];if(void 0!==i)for(e=0;i.length>e;e++)r=i[e],r.callback.apply(r.context,n);return this},c.extend=function(t,r,i){var a,s=this;a=r&&n.call(r,"constructor")?r.constructor:function(){return s.apply(this,arguments)},e(a,s,i);var o=function(){this.constructor=a};return o.prototype=s.prototype,a.prototype=new o,r&&e(a.prototype,r),a.__super__=s.prototype,c[t]=a,a},r.chart=function(t){return 0===arguments.length?c:1===arguments.length?c[t]:c.extend.apply(c,arguments)},r.selection.prototype.chart=function(t,e){if(0===arguments.length)return this._chart;var r=c[t];return i(r,"No chart registered with name '"+t+"'"),new r(this,e)},r.selection.enter.prototype.chart=function(){return this._chart},r.transition.prototype.chart=r.selection.enter.prototype.chart})(this);
+//@ sourceMappingURL=d3.chart.min.map
\ No newline at end of file
diff --git a/d3.chart.min.map b/d3.chart.min.map
new file mode 100644
index 0000000..3a23bf2
--- /dev/null
+++ b/d3.chart.min.map
@@ -0,0 +1 @@
+{"version":3,"file":"d3.chart.min.js","sources":["d3.chart.js"],"names":["window","extend","object","argsIndex","argsLength","iteratee","key","arguments","length","d3","hasOwnProp","Object","hasOwnProperty","d3cAssert","test","message","Error","version","match","lifecycleRe","Layer","base","this","_base","_handlers","prototype","dataBind","insert","on","eventName","handler","options","push","callback","chart","off","idx","handlers","splice","draw","data","bound","entering","events","selection","len","call","enter","_chart","name","bind","exit","i","l","empty","transition","layer","apply","initCascade","instance","args","ctor","constructor","sup","__super__","initialize","transformCascade","transform","Chart","chartOptions","_layers","_attached","_events","unlayer","attach","attachmentName","layerName","attachmentData","demux","context","once","self","names","n","event","j","keys","trigger","ev","Array","slice","undefined","protoProps","staticProps","child","parent","Surrogate","chartName","ChartCtor"],"mappings":"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"}
\ No newline at end of file
diff --git a/examples/scripts/app.js b/examples/scripts/app.js
index 33bdc76..abda600 100644
--- a/examples/scripts/app.js
+++ b/examples/scripts/app.js
@@ -17,10 +17,10 @@
var dataSrc = new DataSrc();
var myBarChart = d3.select("body")
.append("svg").chart("BarChart");
- myBarChart.draw(dataSrc);
+ myBarChart.draw(dataSrc.data);
setInterval(function() {
dataSrc.fetch();
- myBarChart.draw(dataSrc);
+ myBarChart.draw(dataSrc.data);
}, 1500);
var dataSrc2 = new DataSrc();
@@ -33,19 +33,19 @@
};
myCustomBarChart.layer("bars").on("enter:transition", fadeOut);
myCustomBarChart.layer("bars").on("update:transition", fadeOut);
- myCustomBarChart.draw(dataSrc2);
+ myCustomBarChart.draw(dataSrc2.data);
setInterval(function() {
dataSrc2.fetch();
- myCustomBarChart.draw(dataSrc2);
+ myCustomBarChart.draw(dataSrc2.data);
}, 1500);
var dataSrc3 = new DataSrc();
var myFadingBarChart = d3.select("body")
.append("svg").chart("FadingBarChart");
- myFadingBarChart.draw(dataSrc3);
+ myFadingBarChart.draw(dataSrc3.data);
setInterval(function() {
dataSrc3.fetch();
- myFadingBarChart.draw(dataSrc3);
+ myFadingBarChart.draw(dataSrc3.data);
}, 1500);
var myChord = d3.select("body")
@@ -72,13 +72,13 @@
var hybrid = d3.select("body")
.append("svg").chart("Hybrid");
hybrid.draw({
- series1: dataSrc4,
+ series1: dataSrc4.data,
series2: matrix
});
setInterval(function() {
dataSrc4.fetch();
hybrid.draw({
- series1: dataSrc4,
+ series1: dataSrc4.data,
series2: matrix
});
}, 1500);
diff --git a/examples/scripts/bar-chart.js b/examples/scripts/bar-chart.js
index 1b41d37..52e8230 100644
--- a/examples/scripts/bar-chart.js
+++ b/examples/scripts/bar-chart.js
@@ -83,7 +83,6 @@ d3.chart("BarChart", {
},
transform: function(data) {
- data = data.data;
this.x.domain([0, data.length]);
return data;
}
diff --git a/examples/scripts/hybrid.js b/examples/scripts/hybrid.js
index b9ecfd3..05fdd6c 100644
--- a/examples/scripts/hybrid.js
+++ b/examples/scripts/hybrid.js
@@ -4,14 +4,10 @@ d3.chart("Hybrid", {
var barHeight = this.barHeight();
var barWidth = this.radius * 2;
- var chord = this.chord = this.mixin("ImprovedChord", this.base.append("g"));
- var bc = this.bc = this.mixin("FadingBarChart", this.base.append("g"));
- chord.transform = function(data) {
- return d3.chart("Chord").prototype.transform(data.series2);
- };
- bc.transform = function(data) {
- return d3.chart("BarChart").prototype.transform.call(bc, data.series1);
- };
+ var chord = this.chord = this.base.append("g").chart("ImprovedChord");
+ this.attach("chord", chord);
+ var bc = this.bc = this.base.append("g").chart("FadingBarChart");
+ this.attach("bc", bc);
this.base.attr("width", chord.base.attr("width"));
this.base.attr("height", chord.base.attr("height"));
@@ -33,6 +29,14 @@ d3.chart("Hybrid", {
bc.layer("bars").on("update:transition", this.transformBars, { chart : this });
},
+ demux: function(attachmentName, data) {
+ if (attachmentName === "chord") {
+ return data.series2;
+ } else {
+ return data.series1;
+ }
+ },
+
radius: 200,
barHeight: function() {
@@ -43,11 +47,11 @@ d3.chart("Hybrid", {
var length = 0;
var chart = this.chart();
- // Cannot use `this.chart()` here, because it returns the BarChart mixin,
- // not the "hybrid" chart. This behavior should not be overridden
- // (otherwise, using a chart as a mixin will break that chart), but there
- // needs to be a way to access the higher-level chart from an event handler
- // on the mixin.
+ // Cannot use `this.chart()` here, because it returns the BarChart
+ // attachment, not the "hybrid" chart. This behavior should not be
+ // overridden (otherwise, using a chart as a mixin will break that chart),
+ // but there needs to be a way to access the higher-level chart from an
+ // event handler on the mixin.
var barHeight = chart.barHeight();
this.attr("x", function() { length++; });
this.attr("x", null);
@@ -56,4 +60,4 @@ d3.chart("Hybrid", {
});
}
-});
\ No newline at end of file
+});
diff --git a/migrating.md b/migrating.md
new file mode 100644
index 0000000..64eb878
--- /dev/null
+++ b/migrating.md
@@ -0,0 +1,37 @@
+# d3.chart migration guide
+
+### From 0.1 to 0.2
+
+- Update chart definitions:
+ - Remove all but the first argument to the `initialize` method. (This may
+ requiring refactoring the first argument to support multiple values via a
+ generic "options" object.)
+- Modify usage of the `Chart#mixin` method:
+ - Change references to the `Chart#mixin` method to `Chart#attach`.
+ - Instead of invoking with a Chart constructor name, a d3 selection, and
+ options for the chart constructor, first instantiate a chart explicitly and
+ invoke with a unique instance name and the new chart instance.
+
+Example:
+
+```diff
+ d3.chart('ExampleChart', {
+- initialize: function(arg1, arg2) {
++ // If your Chart's `initialize` method has to be changed in this way, don't
++ // forget to also update the usage of the chart.
++ initialize: function(options) {
+
+- this.mixin('OtherChart', this.base.append('g'), {
+- exampleAttribute: 'value'
+- });
++ // Create the chart explicitly
++ var otherChart = this.base.append('g').chart('OtherChart', {
++ exampleAttribute: 'value'
++ });
++ // Use `Chart#attach` to activate the behavior previously controlled via
++ // `Chart#mixin`--the specified Chart's `draw` method will be
++ // automatically invoked when you call this chart's `draw` method.
++ this.attach('otherChart', otherChart);
+ }
+ });
+```
diff --git a/package.json b/package.json
index 0afeb09..75c2b05 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "d3.chart",
- "version": "0.1.3",
+ "version": "0.2.0",
"description": "A framework for creating reusable charts with D3.js",
"repository": {
"type": "git",
diff --git a/src/chart-extensions.js b/src/chart-extensions.js
new file mode 100644
index 0000000..fde51c2
--- /dev/null
+++ b/src/chart-extensions.js
@@ -0,0 +1,55 @@
+"use strict";
+
+/**
+ * Create a new chart constructor or return a previously-created chart
+ * constructor.
+ *
+ * @static
+ *
+ * @param {String} name If no other arguments are specified, return the
+ * previously-created chart with this name.
+ * @param {Object} protoProps If specified, this value will be forwarded to
+ * {@link Chart.extend} and used to create a new chart.
+ * @param {Object} staticProps If specified, this value will be forwarded to
+ * {@link Chart.extend} and used to create a new chart.
+ */
+d3.chart = function(name) {
+ if (arguments.length === 0) {
+ return Chart;
+ } else if (arguments.length === 1) {
+ return Chart[name];
+ }
+
+ return Chart.extend.apply(Chart, arguments);
+};
+
+/**
+ * Instantiate a chart or return the chart that the current selection belongs
+ * to.
+ *
+ * @static
+ *
+ * @param {String} [chartName] The name of the chart to instantiate. If the
+ * name is unspecified, this method will return the chart that the
+ * current selection belongs to.
+ * @param {mixed} options The options to use when instantiated the new chart.
+ * See {@link Chart} for more information.
+ */
+d3.selection.prototype.chart = function(chartName, options) {
+ // Without an argument, attempt to resolve the current selection's
+ // containing d3.chart.
+ if (arguments.length === 0) {
+ return this._chart;
+ }
+ var ChartCtor = Chart[chartName];
+ d3cAssert(ChartCtor, "No chart registered with name '" + chartName + "'");
+
+ return new ChartCtor(this, options);
+};
+
+// Implement the zero-argument signature of `d3.selection.prototype.chart`
+// for all selection types.
+d3.selection.enter.prototype.chart = function() {
+ return this._chart;
+};
+d3.transition.prototype.chart = d3.selection.enter.prototype.chart;
diff --git a/src/chart.js b/src/chart.js
index cf44c6d..57ade4b 100644
--- a/src/chart.js
+++ b/src/chart.js
@@ -1,273 +1,412 @@
-(function(window, undefined) {
+"use strict";
- "use strict";
-
- var d3Chart = window.d3Chart;
- var d3 = window.d3;
- var hasOwnProp = Object.hasOwnProperty;
-
- var Surrogate = function(ctor) { this.constructor = ctor; };
- var variadicNew = function(Ctor, args) {
- var inst;
- Surrogate.prototype = Ctor.prototype;
- inst = new Surrogate(Ctor);
- Ctor.apply(inst, args);
- return inst;
- };
-
- // extend
- // Borrowed from Underscore.js
- function extend(object) {
- var argsIndex, argsLength, iteratee, key;
- if (!object) {
- return object;
- }
- argsLength = arguments.length;
- for (argsIndex = 1; argsIndex < argsLength; argsIndex++) {
- iteratee = arguments[argsIndex];
- if (iteratee) {
- for (key in iteratee) {
- object[key] = iteratee[key];
- }
- }
- }
+// extend
+// Borrowed from Underscore.js
+function extend(object) {
+ var argsIndex, argsLength, iteratee, key;
+ if (!object) {
return object;
}
-
- // initCascade
- // Call the initialize method up the inheritance chain, starting with the
- // base class and continuing "downward".
- var initCascade = function(instance, args) {
- var sup = this.constructor.__super__;
- if (sup) {
- initCascade.call(sup, instance, args);
- }
- // Do not invoke the `initialize` method on classes further up the
- // prototype chain.
- if (hasOwnProp.call(this.constructor.prototype, "initialize")) {
- this.initialize.apply(instance, args);
+ argsLength = arguments.length;
+ for (argsIndex = 1; argsIndex < argsLength; argsIndex++) {
+ iteratee = arguments[argsIndex];
+ if (iteratee) {
+ for (key in iteratee) {
+ object[key] = iteratee[key];
+ }
}
- };
-
- var Chart = function(selection) {
+ }
+ return object;
+}
+
+/**
+ * Call the {@Chart#initialize} method up the inheritance chain, starting with
+ * the base class and continuing "downward".
+ *
+ * @private
+ */
+var initCascade = function(instance, args) {
+ var ctor = this.constructor;
+ var sup = ctor.__super__;
+ if (sup) {
+ initCascade.call(sup, instance, args);
+ }
- this.base = selection;
- this._layers = {};
- this._mixins = [];
- this._events = {};
+ // Do not invoke the `initialize` method on classes further up the
+ // prototype chain (again).
+ if (hasOwnProp.call(ctor.prototype, "initialize")) {
+ this.initialize.apply(instance, args);
+ }
+};
+
+/**
+ * Call the `transform` method down the inheritance chain, starting with the
+ * instance and continuing "upward". The result of each transformation should
+ * be supplied as input to the next.
+ *
+ * @private
+ */
+var transformCascade = function(instance, data) {
+ var ctor = this.constructor;
+ var sup = ctor.__super__;
+
+ // Unlike `initialize`, the `transform` method has significance when
+ // attached directly to a chart instance. Ensure that this transform takes
+ // first but is not invoked on later recursions.
+ if (this === instance && hasOwnProp.call(this, "transform")) {
+ data = this.transform(data);
+ }
- initCascade.call(this, this, Array.prototype.slice.call(arguments, 1));
- };
+ // Do not invoke the `transform` method on classes further up the prototype
+ // chain (yet).
+ if (hasOwnProp.call(ctor.prototype, "transform")) {
+ data = ctor.prototype.transform.call(instance, data);
+ }
- Chart.prototype.unlayer = function(name) {
- var layer = this.layer(name);
+ if (sup) {
+ data = transformCascade.call(sup, instance, data);
+ }
- delete this._layers[name];
- delete layer._chart;
+ return data;
+};
+
+/**
+ * Create a d3.chart
+ *
+ * @param {d3.selection} selection The chart's "base" DOM node. This should
+ * contain any nodes that the chart generates.
+ * @param {mixed} chartOptions A value for controlling how the chart should be
+ * created. This value will be forwarded to {@link Chart#initialize}, so
+ * charts may define additional properties for consumers to modify their
+ * behavior during initialization.
+ *
+ * @constructor
+ */
+var Chart = function(selection, chartOptions) {
+
+ this.base = selection;
+ this._layers = {};
+ this._attached = {};
+ this._events = {};
+
+ if (chartOptions && chartOptions.transform) {
+ this.transform = chartOptions.transform;
+ }
- return layer;
- };
+ initCascade.call(this, this, [chartOptions]);
+};
+
+/**
+ * Set up a chart instance. This method is intended to be overridden by Charts
+ * authored with this library. It will be invoked with a single argument: the
+ * `options` value supplied to the {@link Chart|constructor}.
+ *
+ * For charts that are defined as extensions of other charts using
+ * `Chart.extend`, each chart's `initilize` method will be invoked starting
+ * with the "oldest" ancestor (see the private {@link initCascade} function for
+ * more details).
+ */
+Chart.prototype.initialize = function() {};
+
+/**
+ * Remove a layer from the chart.
+ *
+ * @param {String} name The name of the layer to remove.
+ *
+ * @returns {Layer} The layer removed by this operation.
+ */
+Chart.prototype.unlayer = function(name) {
+ var layer = this.layer(name);
+
+ delete this._layers[name];
+ delete layer._chart;
+
+ return layer;
+};
+
+/**
+ * Interact with the chart's {@link Layer|layers}.
+ *
+ * If only a `name` is provided, simply return the layer registered to that
+ * name (if any).
+ *
+ * If a `name` and `selection` are provided, treat the `selection` as a
+ * previously-created layer and attach it to the chart with the specified
+ * `name`.
+ *
+ * If all three arguments are specified, initialize a new {@link Layer} using
+ * the specified `selection` as a base passing along the specified `options`.
+ *
+ * The {@link Layer.draw} method of attached layers will be invoked
+ * whenever this chart's {@link Chart#draw} is invoked and will receive the
+ * data (optionally modified by the chart's {@link Chart#transform} method.
+ *
+ * @param {String} name Name of the layer to attach or retrieve.
+ * @param {d3.selection|Layer} [selection] The layer's base or a
+ * previously-created {@link Layer}.
+ * @param {Object} [options] Options to be forwarded to {@link Layer|the Layer
+ * constructor}
+ *
+ * @returns {Layer}
+ */
+Chart.prototype.layer = function(name, selection, options) {
+ var layer;
+
+ if (arguments.length === 1) {
+ return this._layers[name];
+ }
- Chart.prototype.layer = function(name, selection, options) {
- var layer;
+ // we are reattaching a previous layer, which the
+ // selection argument is now set to.
+ if (arguments.length === 2) {
- if (arguments.length === 1) {
+ if (typeof selection.draw === "function") {
+ selection._chart = this;
+ this._layers[name] = selection;
return this._layers[name];
- }
- // we are reattaching a previous layer, which the
- // selection argument is now set to.
- if (arguments.length === 2) {
-
- if (typeof selection.draw === "function") {
- selection._chart = this;
- this._layers[name] = selection;
- return this._layers[name];
-
- } else {
- d3Chart.assert(false, "When reattaching a layer, the second argument "+
- "must be a d3.chart layer");
- }
+ } else {
+ d3cAssert(false, "When reattaching a layer, the second argument "+
+ "must be a d3.chart layer");
}
+ }
- layer = selection.layer(options);
-
- this._layers[name] = layer;
-
- selection._chart = this;
-
- return layer;
- };
-
- Chart.prototype.initialize = function() {};
-
- Chart.prototype.transform = function(data) {
- return data;
- };
-
- Chart.prototype.mixin = function(chartName) {
- var args = Array.prototype.slice.call(arguments, 1);
- var ctor = Chart[chartName];
- var chart = variadicNew(ctor, args);
+ layer = selection.layer(options);
+
+ this._layers[name] = layer;
+
+ selection._chart = this;
+
+ return layer;
+};
+
+/**
+ * Register or retrieve an "attachment" Chart. The "attachment" chart's `draw`
+ * method will be invoked whenever the containing chart's `draw` method is
+ * invoked.
+ *
+ * @param {String} attachmentName Name of the attachment
+ * @param {Chart} [chart] d3.chart to register as a mix in of this chart. When
+ * unspecified, this method will return the attachment previously
+ * registered with the specified `attachmentName` (if any).
+ *
+ * @returns {Chart} Reference to this chart (chainable).
+ */
+Chart.prototype.attach = function(attachmentName, chart) {
+ if (arguments.length === 1) {
+ return this._attached[attachmentName];
+ }
- this._mixins.push(chart);
- return chart;
- };
+ this._attached[attachmentName] = chart;
+ return chart;
+};
- Chart.prototype.draw = function(data) {
+/**
+ * Update the chart's representation in the DOM, drawing all of its layers and
+ * any "attachment" charts (as attached via {@link Chart#attach}).
+ *
+ * @param {Object} data Data to pass to the {@link Layer#draw|draw method} of
+ * this cart's {@link Layer|layers} (if any) and the {@link
+ * Chart#draw|draw method} of this chart's attachments (if any).
+ */
+Chart.prototype.draw = function(data) {
- var layerName, idx, len;
+ var layerName, attachmentName, attachmentData;
- data = this.transform(data);
+ data = transformCascade.call(this, this, data);
- for (layerName in this._layers) {
- this._layers[layerName].draw(data);
- }
+ for (layerName in this._layers) {
+ this._layers[layerName].draw(data);
+ }
- for (idx = 0, len = this._mixins.length; idx < len; idx++) {
- this._mixins[idx].draw(data);
+ for (attachmentName in this._attached) {
+ if (this.demux) {
+ attachmentData = this.demux(attachmentName, data);
+ } else {
+ attachmentData = data;
}
+ this._attached[attachmentName].draw(attachmentData);
+ }
+};
+
+/**
+ * Function invoked with the context specified when the handler was bound (via
+ * {@link Chart#on} {@link Chart#once}).
+ *
+ * @callback ChartEventHandler
+ * @param {...*} arguments Invoked with the arguments passed to {@link
+ * Chart#trigger}
+ */
+
+/**
+ * Subscribe a callback function to an event triggered on the chart. See {@link
+ * Chart#once} to subscribe a callback function to an event for one occurence.
+ *
+ * @param {String} name Name of the event
+ * @param {ChartEventHandler} callback Function to be invoked when the event
+ * occurs
+ * @param {Object} [context] Value to set as `this` when invoking the
+ * `callback`. Defaults to the chart instance.
+ *
+ * @returns {Chart} A reference to this chart (chainable).
+ */
+Chart.prototype.on = function(name, callback, context) {
+ var events = this._events[name] || (this._events[name] = []);
+ events.push({
+ callback: callback,
+ context: context || this,
+ _chart: this
+ });
+ return this;
+};
+
+/**
+ * Subscribe a callback function to an event triggered on the chart. This
+ * function will be invoked at the next occurance of the event and immediately
+ * unsubscribed. See {@link Chart#on} to subscribe a callback function to an
+ * event indefinitely.
+ *
+ * @param {String} name Name of the event
+ * @param {ChartEventHandler} callback Function to be invoked when the event
+ * occurs
+ * @param {Object} [context] Value to set as `this` when invoking the
+ * `callback`. Defaults to the chart instance
+ *
+ * @returns {Chart} A reference to this chart (chainable)
+ */
+Chart.prototype.once = function(name, callback, context) {
+ var self = this;
+ var once = function() {
+ self.off(name, once);
+ callback.apply(this, arguments);
};
-
- Chart.prototype.on = function(name, callback, context) {
- var events = this._events[name] || (this._events[name] = []);
- events.push({
- callback: callback,
- context: context || this,
- _chart: this
- });
- return this;
- };
-
- Chart.prototype.once = function(name, callback, context) {
- var self = this;
- var once = function() {
- self.off(name, once);
- callback.apply(this, arguments);
- };
- return this.on(name, once, context);
- };
-
- Chart.prototype.off = function(name, callback, context) {
- var names, n, events, event, i, j;
-
- // remove all events
- if (arguments.length === 0) {
- for (name in this._events) {
- this._events[name].length = 0;
- }
- return this;
- }
-
- // remove all events for a specific name
- if (arguments.length === 1) {
- events = this._events[name];
- if (events) {
- events.length = 0;
- }
- return this;
+ return this.on(name, once, context);
+};
+
+/**
+ * Unsubscribe one or more callback functions from an event triggered on the
+ * chart. When no arguments are specified, *all* handlers will be unsubscribed.
+ * When only a `name` is specified, all handlers subscribed to that event will
+ * be unsubscribed. When a `name` and `callback` are specified, only that
+ * function will be unsubscribed from that event. When a `name` and `context`
+ * are specified (but `callback` is omitted), all events bound to the given
+ * event with the given context will be unsubscribed.
+ *
+ * @param {String} [name] Name of the event to be unsubscribed
+ * @param {ChartEventHandler} [callback] Function to be unsubscribed
+ * @param {Object} [context] Contexts to be unsubscribe
+ *
+ * @returns {Chart} A reference to this chart (chainable).
+ */
+Chart.prototype.off = function(name, callback, context) {
+ var names, n, events, event, i, j;
+
+ // remove all events
+ if (arguments.length === 0) {
+ for (name in this._events) {
+ this._events[name].length = 0;
}
+ return this;
+ }
- // remove all events that match whatever combination of name, context
- // and callback.
- names = name ? [name] : Object.keys(this._events);
- for (i = 0; i < names.length; i++) {
- n = names[i];
- events = this._events[n];
- j = events.length;
- while (j--) {
- event = events[j];
- if ((callback && callback === event.callback) ||
- (context && context === event.context)) {
- events.splice(j, 1);
- }
- }
+ // remove all events for a specific name
+ if (arguments.length === 1) {
+ events = this._events[name];
+ if (events) {
+ events.length = 0;
}
-
return this;
- };
-
- Chart.prototype.trigger = function(name) {
- var args = Array.prototype.slice.call(arguments, 1);
- var events = this._events[name];
- var i, ev;
+ }
- if (events !== undefined) {
- for (i = 0; i < events.length; i++) {
- ev = events[i];
- ev.callback.apply(ev.context, args);
+ // remove all events that match whatever combination of name, context
+ // and callback.
+ names = name ? [name] : Object.keys(this._events);
+ for (i = 0; i < names.length; i++) {
+ n = names[i];
+ events = this._events[n];
+ j = events.length;
+ while (j--) {
+ event = events[j];
+ if ((callback && callback === event.callback) ||
+ (context && context === event.context)) {
+ events.splice(j, 1);
}
}
+ }
- return this;
- };
-
- Chart.extend = function(name, protoProps, staticProps) {
- var parent = this;
- var child;
-
- // The constructor function for the new subclass is either defined by
- // you (the "constructor" property in your `extend` definition), or
- // defaulted by us to simply call the parent's constructor.
- if (protoProps && hasOwnProp.call(protoProps, "constructor")) {
- child = protoProps.constructor;
- } else {
- child = function(){ return parent.apply(this, arguments); };
+ return this;
+};
+
+/**
+ * Publish an event on this chart with the given `name`.
+ *
+ * @param {String} name Name of the event to publish
+ * @param {...*} arguments Values with which to invoke the registered
+ * callbacks.
+ *
+ * @returns {Chart} A reference to this chart (chainable).
+ */
+Chart.prototype.trigger = function(name) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var events = this._events[name];
+ var i, ev;
+
+ if (events !== undefined) {
+ for (i = 0; i < events.length; i++) {
+ ev = events[i];
+ ev.callback.apply(ev.context, args);
}
+ }
- // Add static properties to the constructor function, if supplied.
- extend(child, parent, staticProps);
-
- // Set the prototype chain to inherit from `parent`, without calling
- // `parent`'s constructor function.
- var Surrogate = function(){ this.constructor = child; };
- Surrogate.prototype = parent.prototype;
- child.prototype = new Surrogate();
-
- // Add prototype properties (instance properties) to the subclass, if
- // supplied.
- if (protoProps) { extend(child.prototype, protoProps); }
-
- // Set a convenience property in case the parent's prototype is needed
- // later.
- child.__super__ = parent.prototype;
-
- Chart[name] = child;
- return child;
- };
+ return this;
+};
+
+/**
+ * Create a new {@link Chart} constructor with the provided options acting as
+ * "overrides" for the default chart instance methods. Allows for basic
+ * inheritance so that new chart constructors may be defined in terms of
+ * existing chart constructors. Based on the `extend` function defined by
+ * {@link http://backbonejs.org/|Backbone.js}.
+ *
+ * @static
+ *
+ * @param {String} name Identifier for the new Chart constructor.
+ * @param {Object} protoProps Properties to set on the new chart's prototype.
+ * @param {Object} staticProps Properties to set on the chart constructor
+ * itself.
+ *
+ * @returns {Function} A new Chart constructor
+ */
+Chart.extend = function(name, protoProps, staticProps) {
+ var parent = this;
+ var child;
+
+ // The constructor function for the new subclass is either defined by
+ // you (the "constructor" property in your `extend` definition), or
+ // defaulted by us to simply call the parent's constructor.
+ if (protoProps && hasOwnProp.call(protoProps, "constructor")) {
+ child = protoProps.constructor;
+ } else {
+ child = function(){ return parent.apply(this, arguments); };
+ }
- // d3.chart
- // A factory for creating chart constructors
- d3.chart = function(name) {
- if (arguments.length === 0) {
- return Chart;
- } else if (arguments.length === 1) {
- return Chart[name];
- }
+ // Add static properties to the constructor function, if supplied.
+ extend(child, parent, staticProps);
- return Chart.extend.apply(Chart, arguments);
- };
+ // Set the prototype chain to inherit from `parent`, without calling
+ // `parent`'s constructor function.
+ var Surrogate = function(){ this.constructor = child; };
+ Surrogate.prototype = parent.prototype;
+ child.prototype = new Surrogate();
- d3.selection.prototype.chart = function(chartName) {
- // Without an argument, attempt to resolve the current selection's
- // containing d3.chart.
- if (arguments.length === 0) {
- return this._chart;
- }
- var ChartCtor = Chart[chartName];
- var chartArgs;
- d3Chart.assert(ChartCtor, "No chart registered with name '" +
- chartName + "'");
-
- chartArgs = Array.prototype.slice.call(arguments, 1);
- chartArgs.unshift(this);
- return variadicNew(ChartCtor, chartArgs);
- };
-
- d3.selection.enter.prototype.chart = function() {
- return this._chart;
- };
+ // Add prototype properties (instance properties) to the subclass, if
+ // supplied.
+ if (protoProps) { extend(child.prototype, protoProps); }
- d3.transition.prototype.chart = d3.selection.enter.prototype.chart;
+ // Set a convenience property in case the parent's prototype is needed
+ // later.
+ child.__super__ = parent.prototype;
-}(this));
+ Chart[name] = child;
+ return child;
+};
diff --git a/src/init.js b/src/init.js
index 412031f..1e07a9e 100644
--- a/src/init.js
+++ b/src/init.js
@@ -1,25 +1,16 @@
-(function(window, undefined) {
-
"use strict";
+/*jshint unused: false */
-var previousD3Chart = window.d3Chart;
-var d3Chart = window.d3Chart = {};
var d3 = window.d3;
+var hasOwnProp = Object.hasOwnProperty;
-d3Chart.noConflict = function() {
- window.d3Chart = previousD3Chart;
- return d3Chart;
-};
-
-d3Chart.assert = function(test, message) {
+var d3cAssert = function(test, message) {
if (test) {
return;
}
throw new Error("[d3.chart] " + message);
};
-d3Chart.assert(d3, "d3.js is required");
-d3Chart.assert(typeof d3.version === "string" && d3.version.match(/^3/),
+d3cAssert(d3, "d3.js is required");
+d3cAssert(typeof d3.version === "string" && d3.version.match(/^3/),
"d3.js version 3 is required");
-
-}(this));
diff --git a/src/layer-extensions.js b/src/layer-extensions.js
new file mode 100644
index 0000000..2048f76
--- /dev/null
+++ b/src/layer-extensions.js
@@ -0,0 +1,33 @@
+"use strict";
+
+/**
+ * Create a new layer on the d3 selection from which it is called.
+ *
+ * @static
+ *
+ * @param {Object} [options] Options to be forwarded to {@link Layer|the Layer
+ * constructor}
+ * @returns {d3.selection}
+ */
+d3.selection.prototype.layer = function(options) {
+ var layer = new Layer(this);
+ var eventName;
+
+ // Set layer methods (required)
+ layer.dataBind = options.dataBind;
+ layer.insert = options.insert;
+
+ // Bind events (optional)
+ if ("events" in options) {
+ for (eventName in options.events) {
+ layer.on(eventName, options.events[eventName]);
+ }
+ }
+
+ // Mix the public methods into the D3.js selection (bound appropriately)
+ this.on = function() { return layer.on.apply(layer, arguments); };
+ this.off = function() { return layer.off.apply(layer, arguments); };
+ this.draw = function() { return layer.draw.apply(layer, arguments); };
+
+ return this;
+};
diff --git a/src/layer.js b/src/layer.js
index c26c491..7e6eced 100644
--- a/src/layer.js
+++ b/src/layer.js
@@ -1,167 +1,204 @@
-(function(window, undefined) {
-
- "use strict";
-
- var d3Chart = window.d3Chart;
- var d3 = window.d3;
-
- var Layer = function(base) {
- d3Chart.assert(base, "Layers must be initialized with a base.");
- this._base = base;
- this._handlers = {};
- };
-
- // dataBind
- Layer.prototype.dataBind = function() {
- d3Chart.assert(false, "Layers must specify a `dataBind` method.");
- };
-
- // insert
- Layer.prototype.insert = function() {
- d3Chart.assert(false, "Layers must specify an `insert` method.");
- };
-
- // on
- // Attach the specified handler to the specified event type.
- Layer.prototype.on = function(eventName, handler, options) {
- options = options || {};
- if (!(eventName in this._handlers)) {
- this._handlers[eventName] = [];
- }
- this._handlers[eventName].push({
- callback: handler,
- chart: options.chart || null
- });
+"use strict";
+
+var lifecycleRe = /^(enter|update|merge|exit)(:transition)?$/;
+
+/**
+ * Create a layer using the provided `base`. The layer instance is *not*
+ * exposed to d3.chart users. Instead, its instance methods are mixed in to the
+ * `base` selection it describes; users interact with the instance via these
+ * bound methods.
+ *
+ * @private
+ * @constructor
+ *
+ * @param {d3.selection} base The containing DOM node for the layer.
+ */
+var Layer = function(base) {
+ d3cAssert(base, "Layers must be initialized with a base.");
+ this._base = base;
+ this._handlers = {};
+};
+
+/**
+ * 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}
+ */
+Layer.prototype.dataBind = function() {
+ d3cAssert(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.
+ */
+Layer.prototype.insert = function() {
+ d3cAssert(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 {d3.selection} Reference to the layer's base.
+ */
+Layer.prototype.on = function(eventName, handler, options) {
+ options = options || {};
+
+ d3cAssert(
+ 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._base;
+};
+
+/**
+ * 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 {d3.selection} Reference to the layer's base.
+ */
+Layer.prototype.off = function(eventName, handler) {
+
+ var handlers = this._handlers[eventName];
+ var idx;
+
+ d3cAssert(
+ lifecycleRe.test(eventName),
+ "Unrecognized lifecycle event name specified to `Layer#off`: '" +
+ eventName + "'."
+ );
+
+ if (!handlers) {
return this._base;
- };
-
- // off
- // Remove the specified handler. If no handler is supplied, remove *all*
- // handlers from the specified event type.
- Layer.prototype.off = function(eventName, handler) {
+ }
- var handlers = this._handlers[eventName];
- var idx;
+ if (arguments.length === 1) {
+ handlers.length = 0;
+ return this._base;
+ }
- if (!handlers) {
- return this._base;
+ for (idx = handlers.length - 1; idx > -1; --idx) {
+ if (handlers[idx].callback === handler) {
+ handlers.splice(idx, 1);
}
-
- if (arguments.length === 1) {
- handlers.length = 0;
- return this._base;
+ }
+ return this._base;
+};
+
+/**
+ * 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.
+ */
+Layer.prototype.draw = function(data) {
+ var bound, entering, events, selection, handlers, eventName, idx, len;
+
+ bound = this.dataBind.call(this._base, data);
+
+ // Although `bound instanceof d3.selection` is more explicit, it fails
+ // in IE8, so we use duck typing to maintain compatability.
+ d3cAssert(bound && bound.call === d3.selection.prototype.call,
+ "Invalid selection defined by `Layer#dataBind` method.");
+ d3cAssert(bound.enter, "Layer selection not properly bound.");
+
+ entering = bound.enter();
+ entering._chart = this._base._chart;
+
+ events = [
+ {
+ name: "update",
+ selection: bound
+ },
+ {
+ name: "enter",
+ // Defer invocation of the `insert` method so that the previous
+ // `update` selection does not contain the new nodes.
+ selection: this.insert.bind(entering)
+ },
+ {
+ name: "merge",
+ // This selection will be modified when the previous selection
+ // is made.
+ selection: bound
+ },
+ {
+ name: "exit",
+ selection: bound.exit.bind(bound)
}
+ ];
- for (idx = handlers.length - 1; idx > -1; --idx) {
- if (handlers[idx].callback === handler) {
- handlers.splice(idx, 1);
- }
- }
- return this._base;
- };
-
- // draw
- // Bind the data to the layer, make lifecycle selections, and invoke all
- // relevant handlers.
- Layer.prototype.draw = function(data) {
- var bound, entering, events, selection, handlers, eventName, idx, len;
-
- bound = this.dataBind.call(this._base, data);
-
- // Although `bound instanceof d3.selection` is more explicit, it fails
- // in IE8, so we use duck typing to maintain compatability.
- d3Chart.assert(bound && bound.call === d3.selection.prototype.call,
- "Invalid selection defined by `Layer#dataBind` method.");
- d3Chart.assert(bound.enter, "Layer selection not properly bound.");
-
- entering = bound.enter();
- entering._chart = this._base._chart;
-
- events = [
- {
- name: "update",
- selection: bound
- },
- {
- name: "enter",
- // Defer invocation of the `insert` method so that the previous
- // `update` selection does not contain the new nodes.
- selection: this.insert.bind(entering)
- },
- {
- name: "merge",
- // This selection will be modified when the previous selection
- // is made.
- selection: bound
- },
- {
- name: "exit",
- selection: bound.exit.bind(bound)
- }
- ];
-
- for (var i = 0, l = events.length; i < l; ++i) {
- eventName = events[i].name;
- selection = events[i].selection;
+ for (var i = 0, l = events.length; i < l; ++i) {
+ eventName = events[i].name;
+ selection = events[i].selection;
- // Some lifecycle selections are expressed as functions so that
- // they may be delayed.
- if (typeof selection === "function") {
- selection = selection();
- }
-
- // Although `selection instanceof d3.selection` is more explicit,
- // it fails in IE8, so we use duck typing to maintain
- // compatability.
- d3Chart.assert(selection &&
- selection.call === d3.selection.prototype.call,
- "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);
- }
- }
+ // Some lifecycle selections are expressed as functions so that
+ // they may be delayed.
+ if (typeof selection === "function") {
+ selection = selection();
+ }
- handlers = this._handlers[eventName + ":transition"];
+ if (selection.empty()) {
+ continue;
+ }
- if (handlers && handlers.length) {
- selection = selection.transition();
- for (idx = 0, len = handlers.length; idx < len; ++idx) {
- selection._chart = handlers[idx].chart || this._base._chart;
- selection.call(handlers[idx].callback);
- }
+ // Although `selection instanceof d3.selection` is more explicit,
+ // it fails in IE8, so we use duck typing to maintain
+ // compatability.
+ d3cAssert(selection &&
+ selection.call === d3.selection.prototype.call,
+ "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);
}
}
- };
- d3.selection.prototype.layer = function(options) {
- var layer = new Layer(this);
- var eventName;
+ handlers = this._handlers[eventName + ":transition"];
- // Set layer methods (required)
- layer.dataBind = options.dataBind;
- layer.insert = options.insert;
-
- // Bind events (optional)
- if ("events" in options) {
- for (eventName in options.events) {
- layer.on(eventName, options.events[eventName]);
+ if (handlers && handlers.length) {
+ selection = selection.transition();
+ for (idx = 0, len = handlers.length; idx < len; ++idx) {
+ selection._chart = handlers[idx].chart || this._base._chart;
+ selection.call(handlers[idx].callback);
}
}
-
- // Mix the public methods into the D3.js selection (bound appropriately)
- this.on = function() { return layer.on.apply(layer, arguments); };
- this.off = function() { return layer.off.apply(layer, arguments); };
- this.draw = function() { return layer.draw.apply(layer, arguments); };
-
- return this;
- };
-
-}(this));
+ }
+};
diff --git a/test/index.html b/test/index.html
index 182b4a9..1333916 100644
--- a/test/index.html
+++ b/test/index.html
@@ -17,7 +17,9 @@
+
+
diff --git a/test/tests/chart.js b/test/tests/chart.js
index 729367e..2883b43 100644
--- a/test/tests/chart.js
+++ b/test/tests/chart.js
@@ -25,6 +25,17 @@ suite("d3.chart", function() {
assert.equal(myChart.base, selection);
});
+ test("sets the `transform` method as specified to the constructor", function() {
+ var transform = function() {};
+ var myChart;
+ d3.chart("test", {});
+
+ myChart = d3.select("#test").chart("test", {
+ transform: transform
+ });
+
+ assert.equal(myChart.transform, transform);
+ });
suite("`initialize` method invocation", function() {
setup(function() {
@@ -46,10 +57,13 @@ suite("d3.chart", function() {
assert.equal(instance, this.init1.thisValues[0]);
});
- test("immediately invoked with the specified arguments", function() {
- d3.select("#test").chart("test", 1, 2, 3);
+ test("immediately invoked with the specified options", function() {
+ var options = {};
+ d3.select("#test").chart("test", options);
- assert.deepEqual(this.init1.args[0], [1, 2, 3]);
+ assert.equal(this.init1.callCount, 1);
+ assert.equal(this.init1.args[0].length, 1);
+ assert.strictEqual(this.init1.args[0][0], options);
});
test("recursively invokes parent `initialize` methods (from the topmost, down)", function() {
d3.select("#test").chart("test3");
@@ -86,33 +100,76 @@ suite("d3.chart", function() {
});
});
- suite("#mixin", function() {
+ suite("Attachments", function() {
setup(function() {
d3.chart("test", {});
- d3.chart("test2", {
- initialize: sinon.spy()
- });
this.myChart = d3.select("#test").chart("test");
+ var attachmentChart = this.attachmentChart =
+ d3.select("body").chart("test");
+ sinon.spy(attachmentChart, "draw");
});
- test("instantiates the specified chart", function() {
- var mixin = this.myChart.mixin("test2", d3.select("body"), 1, 2, 45);
- assert(mixin instanceof d3.chart("test2"));
- });
- test("instantiates with the correct arguments", function() {
- var mixin = this.myChart.mixin("test2", d3.select("body"), 1, 2, 45);
- assert.deepEqual(mixin.initialize.args[0], [1, 2, 45]);
+ suite("#attach", function() {
+ test("returns the requested attachment", function() {
+ this.myChart.attach("myAttachment", this.attachmentChart);
+
+ assert.equal(
+ this.myChart.attach("myAttachment"),
+ this.attachmentChart
+ );
+ });
+ test("connects the specified chart", function() {
+ var data = [23, 45];
+ this.myChart.attach("myAttachment", this.attachmentChart);
+ this.myChart.draw(data);
+
+ assert.equal(this.attachmentChart.draw.callCount, 1);
+ assert.equal(this.attachmentChart.draw.args[0].length, 1);
+ assert.deepEqual(this.attachmentChart.draw.args[0][0], data);
+ });
});
- test("correctly sets the `base` attribute of the mixin", function() {
- var mixinBase = d3.select("body");
- var mixin = this.myChart.mixin("test2", mixinBase);
- assert.equal(mixin.base, mixinBase);
+
+ suite("#demux", function() {
+ var data = {
+ series1: [1, 2, 3],
+ series2: [4, 5, 6]
+ };
+ setup(function() {
+ this.attachmentChart2 = d3.select("body").chart("test");
+ sinon.spy(this.attachmentChart2, "draw");
+ this.myChart.attach("attachment1", this.attachmentChart);
+ this.myChart.attach("attachment2", this.attachmentChart2);
+ });
+ test("uses provided function to demultiplex data", function() {
+ this.myChart.demux = function(attachmentName, data) {
+ if (attachmentName === "attachment1") {
+ return data.series1;
+ }
+ return data;
+ };
+ this.myChart.draw(data);
+
+ assert.deepEqual(
+ this.attachmentChart.draw.args,
+ [[[1, 2, 3]]],
+ "Demuxes data passed to charts with registered function"
+ );
+ assert.deepEqual(
+ this.attachmentChart2.draw.args[0][0].series1,
+ data.series1,
+ "Unmodified data passes through to attachments directly"
+ );
+ assert.deepEqual(
+ this.attachmentChart2.draw.args[0][0].series2,
+ data.series2,
+ "Unmodified data passes through to attachments directly"
+ );
+ });
});
});
suite("#draw", function() {
setup(function() {
- var layer1, layer2, mixin1, mixin2, transform, transformedData,
- myChart;
+ var layer1, layer2, transform, transformedData, myChart;
this.transformedData = transformedData = {};
this.transform = transform = sinon.stub().returns(transformedData);
d3.chart("test", {});
@@ -130,13 +187,15 @@ suite("d3.chart", function() {
});
sinon.spy(layer2, "draw");
- this.mixin1 = mixin1 = myChart.mixin("test", d3.select("#test"));
- this.mixin2 = mixin2 = myChart.mixin("test", d3.select("#test"));
- sinon.stub(mixin1, "draw");
- sinon.stub(mixin2, "draw");
+ this.attachment1 = d3.select("#test").chart("test");
+ this.attachment2 = d3.select("#test").chart("test");
+ myChart.attach("test1", this.attachment1);
+ myChart.attach("test2", this.attachment2);
+ sinon.stub(this.attachment1, "draw");
+ sinon.stub(this.attachment2, "draw");
});
test("invokes the transform method once with the specified data", function() {
- var data = {};
+ var data = [1, 2, 3];
assert.equal(this.transform.callCount, 0);
this.myChart.draw(data);
@@ -144,43 +203,73 @@ suite("d3.chart", function() {
assert.equal(this.transform.callCount, 1);
assert.equal(this.transform.args[0][0], data);
});
+
+ test("transform cascading", function() {
+ var grandpaTransform = sinon.spy(function(d) { return d * 2; });
+ var paTransform = sinon.spy(function(d) { return d * 3; });
+ var instanceTransform = sinon.spy(function(d) { return d * 5; });
+
+ d3.chart("TestTransformGrandpa", {
+ transform: grandpaTransform
+ });
+ d3.chart("TestTransformGrandpa").extend("TestTransformPa", {
+ transform: paTransform
+ });
+
+ var chart = d3.select("#test").chart("TestTransformPa");
+ chart.transform = instanceTransform;
+
+ chart.draw(7);
+
+ sinon.assert.calledWith(instanceTransform, 7);
+ sinon.assert.calledWith(paTransform, 35);
+ sinon.assert.calledWith(grandpaTransform, 105);
+
+ });
+
test("invokes the `draw` method of each of its layers", function() {
assert.equal(this.layer1.draw.callCount, 0);
assert.equal(this.layer2.draw.callCount, 0);
- this.myChart.draw();
+ this.myChart.draw([]);
assert.equal(this.layer1.draw.callCount, 1);
assert.equal(this.layer2.draw.callCount, 1);
});
test("invokes the `draw` method of each of its layers with the transformed data", function() {
- this.myChart.draw({});
+ this.myChart.draw([]);
assert.equal(this.layer1.draw.args[0][0], this.transformedData);
assert.equal(this.layer2.draw.args[0][0], this.transformedData);
});
- test("invokes the `draw` method on each of its mixins", function() {
- assert.equal(this.mixin1.draw.callCount, 0);
- assert.equal(this.mixin2.draw.callCount, 0);
+ test("invokes the `draw` method on each of its attachments", function() {
+ assert.equal(this.attachment1.draw.callCount, 0);
+ assert.equal(this.attachment2.draw.callCount, 0);
this.myChart.draw();
- assert.equal(this.mixin1.draw.callCount, 1);
- assert.equal(this.mixin2.draw.callCount, 1);
+ assert.equal(this.attachment1.draw.callCount, 1);
+ assert.equal(this.attachment2.draw.callCount, 1);
});
- test("invokes the `draw` method of each of its mixins with the transformed data", function() {
+ test("invokes the `draw` method of each of its attachments with the transformed data", function() {
this.myChart.draw();
- assert.equal(this.mixin1.draw.args[0][0], this.transformedData);
- assert.equal(this.mixin2.draw.args[0][0], this.transformedData);
+ assert.equal(
+ this.attachment1.draw.args[0][0],
+ this.transformedData
+ );
+ assert.equal(
+ this.attachment2.draw.args[0][0],
+ this.transformedData
+ );
});
- test("invokes the `draw` method of its layers before invoking the `draw` method of its mixins", function() {
+ test("invokes the `draw` method of its layers before invoking the `draw` method of its attachments", function() {
this.myChart.draw();
- assert(this.layer1.draw.calledBefore(this.mixin1.draw));
- assert(this.layer1.draw.calledBefore(this.mixin2.draw));
- assert(this.layer2.draw.calledBefore(this.mixin1.draw));
- assert(this.layer2.draw.calledBefore(this.mixin2.draw));
+ assert(this.layer1.draw.calledBefore(this.attachment1.draw));
+ assert(this.layer1.draw.calledBefore(this.attachment2.draw));
+ assert(this.layer2.draw.calledBefore(this.attachment1.draw));
+ assert(this.layer2.draw.calledBefore(this.attachment2.draw));
});
});
diff --git a/test/tests/integration.js b/test/tests/integration.js
index 90c2f5b..e4b7c0a 100644
--- a/test/tests/integration.js
+++ b/test/tests/integration.js
@@ -22,8 +22,9 @@ suite("integration", function() {
"exit": sinon.spy(),
"exit:transition": sinon.spy()
};
- this.dataBind = sinon.spy(function() {
- return this.data([]);
+ this.dataBind = sinon.spy(function(data) {
+ return this.selectAll("g")
+ .data(data, function(d) { return d; });
});
this.insert = sinon.spy(function() {
return this.append("g");
@@ -36,7 +37,8 @@ suite("integration", function() {
});
sinon.spy(this.layer, "draw");
- this.myChart.draw();
+ this.myChart.draw([1, 2]);
+ this.myChart.draw([2, 3]);
});
test("`dataBind` selection's `.chart` method returns a reference to the parent chart", function() {
diff --git a/test/tests/layer.js b/test/tests/layer.js
index e50e592..f59ac19 100644
--- a/test/tests/layer.js
+++ b/test/tests/layer.js
@@ -27,7 +27,7 @@ suite("d3.layer", function() {
suite("#draw", function() {
setup(function() {
var dataBind = this.dataBind = sinon.spy(function(data) {
- var updating = this.data(data, function(d) { return d; });
+ var updating = this.selectAll("g").data(data, function(d) { return d; });
// Cache `exit` method so it can be invoked from its stub
// without provoking infinite recursion.
var originalExit = updating.exit;
@@ -49,7 +49,7 @@ suite("d3.layer", function() {
sinon.spy(entering, "transition");
return entering;
});
- var base = this.base = d3.select("#test");
+ var base = this.base = d3.select("#test").append("svg");
this.layer = base.layer({
dataBind: dataBind,
@@ -130,6 +130,28 @@ suite("d3.layer", function() {
layer.remove();
});
+ test("Does not invoke lifecycle events for empty selections", function() {
+ var layer = d3.select("#test").append("svg").layer({
+ dataBind: function(d) {
+ return this.selectAll("g").data(d);
+ },
+ insert: function() {
+ return this.append("g");
+ }
+ });
+ var enterSpy = sinon.spy();
+ var updateSpy = sinon.spy();
+ layer.draw([1]);
+
+ layer.on("enter", enterSpy);
+ layer.on("update", updateSpy);
+
+ layer.draw([1]);
+
+ sinon.assert.callCount(enterSpy, 0);
+ sinon.assert.callCount(updateSpy, 1);
+ });
+
suite("Layer#off", function() {
setup(function() {
this.onEnter1 = sinon.spy();
@@ -139,6 +161,7 @@ suite("d3.layer", function() {
insert: this.insert,
dataBind: this.dataBind
});
+ this.layer.draw([1]);
this.layer.on("enter", this.onEnter1);
this.layer.on("enter", this.onEnter2);
this.layer.on("update", this.onUpdate);
@@ -148,7 +171,7 @@ suite("d3.layer", function() {
});
test("unbinds only the specified handler", function() {
this.layer.off("enter", this.onEnter1);
- this.layer.draw([]);
+ this.layer.draw([1, 2]);
assert.equal(this.onEnter1.callCount, 0);
assert.equal(this.onEnter2.callCount, 1);
@@ -156,7 +179,7 @@ suite("d3.layer", function() {
});
test("unbinds only the handlers for the specified lifecycle selection", function() {
this.layer.off("enter");
- this.layer.draw([]);
+ this.layer.draw([1]);
assert.equal(this.onEnter1.callCount, 0);
assert.equal(this.onEnter2.callCount, 0);
@@ -175,12 +198,13 @@ suite("d3.layer", function() {
merge: sinon.spy(),
exit: sinon.spy()
};
+ layer.draw([1, 2]);
layer.on("update", spies.update);
layer.on("enter", spies.enter);
layer.on("merge", spies.merge);
layer.on("exit", spies.exit);
- layer.draw([]);
+ layer.draw([2, 3]);
assert.equal(spies.update.callCount, 1);
assert.equal(spies.update.thisValues[0].transition.callCount,
@@ -215,7 +239,11 @@ suite("d3.layer", function() {
});
});
test("invokes all event handlers exactly once", function() {
- this.layer.draw([]);
+ this.layer.draw([1, 2]);
+ Object.keys(this.spies).forEach(function(key) {
+ this.spies[key].reset();
+ }, this);
+ this.layer.draw([2, 3]);
assert.equal(this.spies.enter.callCount, 1);
assert.equal(this.spies.update.callCount, 1);
@@ -228,11 +256,12 @@ suite("d3.layer", function() {
});
test("invokes all event handlers in the context of the corresponding 'lifecycle selection'", function() {
var entering, updating, exiting;
- this.layer.draw([]);
+ this.layer.draw([1, 2]);
+ this.layer.draw([2, 3]);
// Alias lifecycle selections
entering = this.insert.returnValues[0];
- updating = this.dataBind.returnValues[0];
+ updating = this.dataBind.returnValues[1];
exiting = updating.exit.returnValues[0];
assert(this.spies.enter.calledOn(entering));
@@ -269,6 +298,8 @@ suite("d3.layer", function() {
});
test("invokes all event handlers exactly once", function() {
+ this.layer.draw([1, 2]);
+
this.layer.on("enter", this.onEnter1);
this.layer.on("update", this.onUpdate1);
this.layer.on("update", this.onUpdate2);
@@ -287,7 +318,7 @@ suite("d3.layer", function() {
this.layer.on("merge:transition", this.onMergeTrans3);
this.layer.on("exit:transition", this.onExitTrans1);
- this.layer.draw([]);
+ this.layer.draw([2, 3]);
assert.equal(this.onEnter1.callCount, 1);
assert.equal(this.onUpdate1.callCount, 1);
@@ -312,6 +343,7 @@ suite("d3.layer", function() {
this.layer.on("exit", this.onExit2);
this.layer.on("exit", this.onExit3);
+ this.layer.draw([1]);
this.layer.draw([]);
assert(this.onExit1.calledBefore(this.onExit2));
@@ -328,11 +360,12 @@ suite("d3.layer", function() {
this.layer.on("merge:transition", this.onMergeTrans1);
this.layer.on("exit:transition", this.onExitTrans1);
- this.layer.draw([]);
+ this.layer.draw([1, 2]);
+ this.layer.draw([2, 3]);
// Alias lifecycle selections
entering = this.insert.returnValues[0];
- updating = this.dataBind.returnValues[0];
+ updating = this.dataBind.returnValues[1];
exiting = updating.exit.returnValues[0];
assert(this.onEnter1.calledOn(entering));
@@ -355,7 +388,7 @@ suite("d3.layer", function() {
this.layer.on("enter", this.handler, {
chart: this.chartVal
});
- this.layer.draw([]);
+ this.layer.draw([1]);
assert.equal(this.handler.thisValues[0].chart(),
this.chartVal);
@@ -365,7 +398,7 @@ suite("d3.layer", function() {
this.layer.on("enter:transition", this.handler, {
chart: this.chartVal
});
- this.layer.draw([]);
+ this.layer.draw([1]);
assert.equal(this.handler.thisValues[0].chart(),
this.chartVal);
@@ -375,7 +408,8 @@ suite("d3.layer", function() {
this.layer.on("update", this.handler, {
chart: this.chartVal
});
- this.layer.draw([]);
+ this.layer.draw([1]);
+ this.layer.draw([1]);
assert.equal(this.handler.thisValues[0].chart(),
this.chartVal);
@@ -385,7 +419,8 @@ suite("d3.layer", function() {
this.layer.on("update:transition", this.handler, {
chart: this.chartVal
});
- this.layer.draw([]);
+ this.layer.draw([1]);
+ this.layer.draw([1]);
assert.equal(this.handler.thisValues[0].chart(),
this.chartVal);
@@ -395,7 +430,7 @@ suite("d3.layer", function() {
this.layer.on("merge", this.handler, {
chart: this.chartVal
});
- this.layer.draw([]);
+ this.layer.draw([1]);
assert.equal(this.handler.thisValues[0].chart(),
this.chartVal);
@@ -405,7 +440,7 @@ suite("d3.layer", function() {
this.layer.on("merge:transition", this.handler, {
chart: this.chartVal
});
- this.layer.draw([]);
+ this.layer.draw([1]);
assert.equal(this.handler.thisValues[0].chart(),
this.chartVal);
@@ -415,6 +450,7 @@ suite("d3.layer", function() {
this.layer.on("exit", this.handler, {
chart: this.chartVal
});
+ this.layer.draw([1]);
this.layer.draw([]);
assert.equal(this.handler.thisValues[0].chart(),
@@ -425,6 +461,7 @@ suite("d3.layer", function() {
this.layer.on("exit:transition", this.handler, {
chart: this.chartVal
});
+ this.layer.draw([1]);
this.layer.draw([]);
assert.equal(this.handler.thisValues[0].chart(),
@@ -442,12 +479,12 @@ suite("d3.layer", function() {
});
suite("#on", function () {
test("returns the layer instance (chains)", function() {
- assert.equal(this.layer.on("e1"), this.layer);
+ assert.equal(this.layer.on("enter"), this.layer);
});
});
suite("#off", function () {
test("returns the layer instance (chains)", function() {
- assert.equal(this.layer.off("e1"), this.layer);
+ assert.equal(this.layer.off("enter"), this.layer);
});
});
});