UNPKG

d3.chart

Version:

A framework for creating reusable charts with D3.js

468 lines (416 loc) 14.6 kB
define(function(require, exports, module) { "use strict"; var assert = require("./assert"); var hasOwnProp = Object.hasOwnProperty; // 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); } // 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); } // 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); } if (sup) { data = transformCascade.call(sup, instance, data); } return data; }; /** * Create a d3.chart * * @constructor * @externalExample {runnable} 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. The following attributes will be * copied onto the chart instance (if present): * @param {Function} [chartOptions.transform] - A data transformation function * unique to the Chart instance being created. If specified, this * function will be invoked after all inherited implementations as part * of the `Chart#draw` operation. * @param {Function} [chartOptions.demux] - A data filtering function for * attachment charts. If specified, this function will be invoked with * every {@link Chart#draw|draw} operation and provided with two * arguments: the attachment name (see {@link Chart#attach}) and the * data. * * @constructor */ var Chart = function(selection, chartOptions) { this.base = selection; this._layers = {}; this._attached = {}; this._events = {}; if (chartOptions && chartOptions.transform) { this.transform = chartOptions.transform; } 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. * * @externalExample chart-unlayer * * @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. * * @externalExample chart-layer * * @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]; } // 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 { assert(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; }; /** * Register or retrieve an "attachment" Chart. The "attachment" chart's `draw` * method will be invoked whenever the containing chart's `draw` method is * invoked. * * @externalExample chart-attach * * @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._attached[attachmentName] = chart; return chart; }; /** * A "hook" method that you may define to modify input data before it is used * to draw the chart's layers and attachments. This method will be used by all * sub-classes (see {@link transformCascade} for details). * * Note you will most likely never call this method directly, but rather * include it as part of a chart definition, and then rely on d3.chart to * invoke it when you draw the chart with {@link Chart#draw}. * * @externalExample {runnable} chart-transform * * @param {Array} data Input data provided to @link Chart#draw}. * * @returns {mixed} Data to be used in drawing the chart's layers and * attachments. */ Chart.prototype.transform = function(data) { return data; }; /** * Update the chart's representation in the DOM, drawing all of its layers and * any "attachment" charts (as attached via {@link Chart#attach}). * * @externalExample chart-draw * * @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, attachmentName, attachmentData; data = transformCascade.call(this, this, 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); } }; /** * 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. * * @externalExample {runnable} chart-on * * @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. * * @externalExample {runnable} chart-once * * @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); }; /** * 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. * * @externalExample {runnable} chart-off * * @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 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); } } } return this; }; /** * Publish an event on this chart with the given `name`. * * @externalExample {runnable} chart-trigger * * @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; }; /** * 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 * [Backbone.js](http://backbonejs.org/). * * @static * @externalExample {runnable} chart-extend * * @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); // 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; }; module.exports = Chart; });