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); }); }); });