From f10c8c63bf19c7f88739bad20d6d0d713afb46ee Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Fri, 18 Dec 2015 19:53:21 -0500 Subject: [PATCH 1/7] Refactor internals Define internals using the AMD module pattern in order to improve the readability of the codebase while preserving the ability to test source files directly. --- Gruntfile.js | 17 ++--------- build/tasks/concat.js | 32 -------------------- build/tasks/jshint.js | 2 +- build/tasks/uglify.js | 18 ------------ build/tasks/webpack.js | 31 ++++++++++++++++++++ build/webpack.config.js | 30 +++++++++++++++++++ package.json | 5 +++- src/.jshintrc | 7 ++--- src/assert.js | 10 +++++++ src/chart-extensions.js | 13 ++++++++- src/chart.js | 8 ++++- src/has-own-prop.js | 4 +++ src/init.js | 15 ---------- src/layer-extensions.js | 5 ++++ src/layer.js | 23 ++++++++++----- src/wrapper/end.frag | 1 - src/wrapper/start.frag | 13 --------- test/index.html | 65 +++++++++++++++++++++++++++++------------ 18 files changed, 170 insertions(+), 129 deletions(-) delete mode 100644 build/tasks/concat.js delete mode 100644 build/tasks/uglify.js create mode 100644 build/tasks/webpack.js create mode 100644 build/webpack.config.js create mode 100644 src/assert.js create mode 100644 src/has-own-prop.js delete mode 100644 src/init.js delete mode 100644 src/wrapper/end.frag delete mode 100644 src/wrapper/start.frag diff --git a/Gruntfile.js b/Gruntfile.js index 546c0f7..6724747 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,27 +2,14 @@ module.exports = function(grunt) { "use strict"; - grunt.initConfig({ - meta: { - pkg: grunt.file.readJSON("package.json"), - srcFiles: [ - "src/init.js", - "src/layer.js", - "src/layer-extensions.js", - "src/chart.js", - "src/chart-extensions.js" - ] - } - }); - grunt.loadTasks("build/tasks"); - grunt.registerTask("build", ["concat:release", "uglify"]); + grunt.registerTask("build", ["webpack:dist", "webpack:dist-min"]); grunt.registerTask("test-unit", ["mocha:unit"]); grunt.registerTask( "test-build", - ["concat:test", "mocha:exportsAmd", "mocha:exportsGlobal"] + ["webpack:test", "mocha:exportsAmd", "mocha:exportsGlobal"] ); grunt.registerTask("test", ["jshint", "jscs", "test-unit", "test-build"]); diff --git a/build/tasks/concat.js b/build/tasks/concat.js deleted file mode 100644 index cb8306c..0000000 --- a/build/tasks/concat.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = function(grunt) { - "use strict"; - - grunt.config.set("concat", { - options: { - banner: "/*! <%= meta.pkg.name %> - v<%= meta.pkg.version %>\n" + - " * License: <%= meta.pkg.license %>\n" + - " * Date: <%= grunt.template.today('yyyy-mm-dd') %>\n" + - " */\n" - }, - test: { - files: { - "test/d3.chart.test-build.js": [ - "src/wrapper/start.frag", - "<%= meta.srcFiles %>", - "src/wrapper/end.frag" - ] - } - }, - release: { - files: { - "d3.chart.js": [ - "src/wrapper/start.frag", - "<%= meta.srcFiles %>", - "src/wrapper/end.frag" - ] - } - } - }); - - grunt.loadNpmTasks("grunt-contrib-concat"); -}; diff --git a/build/tasks/jshint.js b/build/tasks/jshint.js index f79f80a..656e1ef 100644 --- a/build/tasks/jshint.js +++ b/build/tasks/jshint.js @@ -6,7 +6,7 @@ module.exports = function(grunt) { options: { jshintrc: "src/.jshintrc" }, - src: ["<%= meta.srcFiles %>"] + src: ["src/*.js"] }, examples: { options: { diff --git a/build/tasks/uglify.js b/build/tasks/uglify.js deleted file mode 100644 index 0890d2d..0000000 --- a/build/tasks/uglify.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = function(grunt) { - "use strict"; - - grunt.config.set("uglify", { - options: { - // Preserve banner - preserveComments: "some", - sourceMap: "d3.chart.min.map" - }, - release: { - files: { - "d3.chart.min.js": "d3.chart.js" - } - } - }); - - grunt.loadNpmTasks("grunt-contrib-uglify"); -}; diff --git a/build/tasks/webpack.js b/build/tasks/webpack.js new file mode 100644 index 0000000..7a7236e --- /dev/null +++ b/build/tasks/webpack.js @@ -0,0 +1,31 @@ +var webpack = require("webpack"); + +module.exports = function(grunt) { + "use strict"; + + grunt.config.set("webpack", { + options: require("../webpack.config.js"), + dist: { + output: { + filename: "d3.chart.js" + } + }, + "dist-min": { + output: { + filename: "d3.chart.min.js", + sourceMapFilename: "d3.chart.min.map", + }, + devtool: "source-map", + plugins: [ + new webpack.optimize.UglifyJsPlugin() + ] + }, + test: { + output: { + filename: "test/d3.chart.test-build.js" + } + } + }); + + grunt.loadNpmTasks("grunt-webpack"); +}; diff --git a/build/webpack.config.js b/build/webpack.config.js new file mode 100644 index 0000000..25d606c --- /dev/null +++ b/build/webpack.config.js @@ -0,0 +1,30 @@ +"use strict"; +var webpack = require("webpack"); + +var pkg = require("../package.json"); +var now = new Date(); + +function pad(num) { + return (num < 10 ? "0" : "") + num; +} + +var banner = [ + pkg.name + " - v" + pkg.version, + "License: " + pkg.license, + "Date: " + now.getFullYear() + "-" + pad(now.getMonth()) + "-" + + pad(now.getDate()) +].join("\n"); + +module.exports = { + context: "src", + entry: "./chart-extensions", + output: { + libraryTarget: "umd" + }, + plugins: [ + new webpack.BannerPlugin(banner) + ], + externals: { + d3: true + } +}; diff --git a/package.json b/package.json index 3eb56d2..d27fa3d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,9 @@ "grunt-contrib-uglify": "~0.1.1", "grunt-contrib-watch": "~0.3.1", "grunt-jscs": "^1.8.0", - "grunt-mocha": "~0.4.6" + "grunt-mocha": "~0.4.6", + "grunt-webpack": "^1.0.11", + "webpack": "^1.12.2", + "webpack-dev-server": "^1.12.0" } } diff --git a/src/.jshintrc b/src/.jshintrc index f893eef..6256ea3 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -3,10 +3,7 @@ "browser": true, "globalstrict": true, "globals": { - "hasOwnProp": true, - "d3": true, - "d3cAssert": true, - "Layer": true, - "Chart": true + "require": false, + "define": false } } diff --git a/src/assert.js b/src/assert.js new file mode 100644 index 0000000..c27b000 --- /dev/null +++ b/src/assert.js @@ -0,0 +1,10 @@ +define(function(require, exports, module) { +"use strict"; + +module.exports = function(test, message) { + if (test) { + return; + } + throw new Error("[d3.chart] " + message); +}; +}); diff --git a/src/chart-extensions.js b/src/chart-extensions.js index 64c1f85..2701baa 100644 --- a/src/chart-extensions.js +++ b/src/chart-extensions.js @@ -1,4 +1,13 @@ +define(function(require) { "use strict"; +var d3 = require("d3"); + +var Chart = require("./chart"); +var assert = require("./assert"); + +assert(/^3\./.test(d3.version), "d3.js version 3 is required"); + +require("./layer-extensions"); /** * A namespace defined by [the D3.js library](http://d3js.org/). The d3.chart @@ -56,7 +65,7 @@ d3.selection.prototype.chart = function(chartName, options) { return this._chart; } var ChartCtor = Chart[chartName]; - d3cAssert(ChartCtor, "No chart registered with name '" + chartName + "'"); + assert(ChartCtor, "No chart registered with name '" + chartName + "'"); return new ChartCtor(this, options); }; @@ -67,3 +76,5 @@ 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 6dafb13..2cf795b 100644 --- a/src/chart.js +++ b/src/chart.js @@ -1,4 +1,7 @@ +define(function(require, exports, module) { "use strict"; +var assert = require("./assert"); +var hasOwnProp = require("./has-own-prop"); // extend // Borrowed from Underscore.js @@ -182,7 +185,7 @@ Chart.prototype.layer = function(name, selection, options) { return this._layers[name]; } else { - d3cAssert(false, "When reattaching a layer, the second argument "+ + assert(false, "When reattaching a layer, the second argument " + "must be a d3.chart layer"); } } @@ -459,3 +462,6 @@ Chart.extend = function(name, protoProps, staticProps) { Chart[name] = child; return child; }; + +module.exports = Chart; +}); diff --git a/src/has-own-prop.js b/src/has-own-prop.js new file mode 100644 index 0000000..74834f9 --- /dev/null +++ b/src/has-own-prop.js @@ -0,0 +1,4 @@ +define(function(require, exports, module) { + "use strict"; + module.exports = Object.hasOwnProperty; +}); diff --git a/src/init.js b/src/init.js deleted file mode 100644 index ec90c14..0000000 --- a/src/init.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; -/*jshint unused: false */ - -var hasOwnProp = Object.hasOwnProperty; - -var d3cAssert = function(test, message) { - if (test) { - return; - } - throw new Error("[d3.chart] " + message); -}; - -d3cAssert(d3, "d3.js is required"); -d3cAssert(typeof d3.version === "string" && d3.version.match(/^3/), - "d3.js version 3 is required"); diff --git a/src/layer-extensions.js b/src/layer-extensions.js index 2048f76..b312e42 100644 --- a/src/layer-extensions.js +++ b/src/layer-extensions.js @@ -1,4 +1,8 @@ +define(function(require) { "use strict"; +var d3 = require("d3"); + +var Layer = require("./layer"); /** * Create a new layer on the d3 selection from which it is called. @@ -31,3 +35,4 @@ d3.selection.prototype.layer = function(options) { return this; }; +}); diff --git a/src/layer.js b/src/layer.js index cfaa900..8a7a794 100644 --- a/src/layer.js +++ b/src/layer.js @@ -1,4 +1,8 @@ +define(function(require, exports, module) { "use strict"; +var d3 = require("d3"); + +var assert = require("./assert"); var lifecycleRe = /^(enter|update|merge|exit)(:transition)?$/; @@ -15,7 +19,7 @@ var lifecycleRe = /^(enter|update|merge|exit)(:transition)?$/; * @param {d3.selection} base The containing DOM node for the layer. */ var Layer = function(base) { - d3cAssert(base, "Layers must be initialized with a base."); + assert(base, "Layers must be initialized with a base."); this._base = base; this._handlers = {}; }; @@ -27,7 +31,7 @@ var Layer = function(base) { * @param {Array} data Value passed to {@link Layer#draw} */ Layer.prototype.dataBind = function() { - d3cAssert(false, "Layers must specify a `dataBind` method."); + assert(false, "Layers must specify a `dataBind` method."); }; /** @@ -36,7 +40,7 @@ Layer.prototype.dataBind = function() { * Layer instances. */ Layer.prototype.insert = function() { - d3cAssert(false, "Layers must specify an `insert` method."); + assert(false, "Layers must specify an `insert` method."); }; /** @@ -55,7 +59,7 @@ Layer.prototype.insert = function() { Layer.prototype.on = function(eventName, handler, options) { options = options || {}; - d3cAssert( + assert( lifecycleRe.test(eventName), "Unrecognized lifecycle event name specified to `Layer#on`: '" + eventName + "'." @@ -88,7 +92,7 @@ Layer.prototype.off = function(eventName, handler) { var handlers = this._handlers[eventName]; var idx; - d3cAssert( + assert( lifecycleRe.test(eventName), "Unrecognized lifecycle event name specified to `Layer#off`: '" + eventName + "'." @@ -136,9 +140,9 @@ Layer.prototype.draw = function(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, + assert(bound && bound.call === d3.selection.prototype.call, "Invalid selection defined by `Layer#dataBind` method."); - d3cAssert(bound.enter, "Layer selection not properly bound."); + assert(bound.enter, "Layer selection not properly bound."); entering = bound.enter(); entering._chart = this._base._chart; @@ -190,7 +194,7 @@ Layer.prototype.draw = function(data) { // Although `selection instanceof d3.selection` is more explicit, // it fails in IE8, so we use duck typing to maintain // compatability. - d3cAssert(selection && + assert(selection && selection.call === d3.selection.prototype.call, "Invalid selection defined for '" + eventName + "' lifecycle event."); @@ -217,3 +221,6 @@ Layer.prototype.draw = function(data) { } } }; + +module.exports = Layer; +}); diff --git a/src/wrapper/end.frag b/src/wrapper/end.frag deleted file mode 100644 index 27b8217..0000000 --- a/src/wrapper/end.frag +++ /dev/null @@ -1 +0,0 @@ -}); diff --git a/src/wrapper/start.frag b/src/wrapper/start.frag deleted file mode 100644 index e2032d6..0000000 --- a/src/wrapper/start.frag +++ /dev/null @@ -1,13 +0,0 @@ -(function(global, factory) { - "use strict"; - - if (typeof global.define === "function" && global.define.amd) { - define(["d3"], function(d3) { - factory(global, d3); - }); - } else { - factory(global, global.d3); - } - -})(this, function(window, d3) { -"use strict"; diff --git a/test/index.html b/test/index.html index 15a319c..6cc0d3d 100644 --- a/test/index.html +++ b/test/index.html @@ -14,14 +14,15 @@ d3.chart test suite - - -

Test Sources

-

Direct | AMD* | Global*

+

+ Direct | + AMD* | + CommonJS* | + Global*

(* requires built file)

@@ -66,16 +67,23 @@

Test Sources

getSourceHeadful : getSourceHeadless; var sources = { direct: { - prereqs: [ - "../bower_components/d3/d3.js", - "../src/init.js", - "../src/layer.js", - "../src/layer-extensions.js", - "../src/chart.js", - "../src/chart-extensions.js" - ], + prereqs: ["lib/require.js"], setup: function(done) { - done(); + require({ + baseUrl: "../src", + paths: { + d3: "../bower_components/d3/d3" + }, + shim: { + d3: { + exports: "d3" + } + } + }); + + require(["chart-extensions"], function() { + done(null, window.d3.chart); + }); } }, amd: { @@ -94,7 +102,7 @@

Test Sources

}); require(["test/d3.chart.test-build"], function() { - done(); + done(null, window.d3.chart); }); } }, @@ -104,19 +112,40 @@

Test Sources

"d3.chart.test-build.js" ], setup: function(done) { - done(); + done(null, window.d3.chart); } } }; var source = getSource(sources, location.search, navigator.userAgent); if (!source) { - throw new Error("d3.chart test source not recognized!"); + test("d3.chart test source defined correctly", function() { + throw new Error("d3.chart test source not recognized!"); + }); + mocha.run(); + return; } loadScripts(source.prereqs, function() { - source.setup(function() { - mocha.run(); + source.setup(function(err, d3chart) { + + test("d3.chart loaded successfully", function() { + if (err) { + throw err; + } + + if (window.d3.chart !== d3chart) { + throw new Error("Failed to load d3.chart"); + } + }); + + loadScripts([ + "../test/tests/chart.js", + "../test/tests/layer.js", + "../test/tests/integration.js" + ], function() { + mocha.run(); + }); }); }); }()); From c062e5d99f13d4b01e9e34832bdb798b84142500 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Fri, 18 Dec 2015 20:03:37 -0500 Subject: [PATCH 2/7] Formalize exported value and test CommonJS support In enhancing the project's internals, the previous commit also introduced support for loading d3.chart from CommonJS environments. Extend the test infrastructure to verify that this new module system is properly supported, and formalize the module's exported value in AMD and CommonJS environments. --- .gitignore | 1 + Gruntfile.js | 5 ++++- build/tasks/browserify.js | 20 ++++++++++++++++++++ build/tasks/mocha.js | 10 ++++++++++ package.json | 5 +++++ src/chart-extensions.js | 3 ++- test/index.html | 33 +++++++++++++++++++++++++++++---- test/sample-cjs-app/main.js | 17 +++++++++++++++++ 8 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 build/tasks/browserify.js create mode 100644 test/sample-cjs-app/main.js diff --git a/.gitignore b/.gitignore index 5e733d7..83fca36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ test/d3.chart.test-build.js +test/sample-cjs-app/packaged.js bower_components/ diff --git a/Gruntfile.js b/Gruntfile.js index 6724747..656959f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,7 +9,10 @@ module.exports = function(grunt) { grunt.registerTask("test-unit", ["mocha:unit"]); grunt.registerTask( "test-build", - ["webpack:test", "mocha:exportsAmd", "mocha:exportsGlobal"] + [ + "webpack:test", "browserify", "mocha:exportsAmd", + "mocha:exportsCommonjs", "mocha:exportsGlobal" + ] ); grunt.registerTask("test", ["jshint", "jscs", "test-unit", "test-build"]); diff --git a/build/tasks/browserify.js b/build/tasks/browserify.js new file mode 100644 index 0000000..5180838 --- /dev/null +++ b/build/tasks/browserify.js @@ -0,0 +1,20 @@ +module.exports = function(grunt) { + "use strict"; + + grunt.config.set("browserify", { + "test-app": { + files: { + "test/sample-cjs-app/packaged.js": [ + "test/sample-cjs-app/main.js" + ] + }, + options: { + alias: { + "d3.chart": "./test/d3.chart.test-build.js" + } + } + } + }); + + grunt.loadNpmTasks("grunt-browserify"); +}; diff --git a/build/tasks/mocha.js b/build/tasks/mocha.js index 52c2a3a..6a5ebd6 100644 --- a/build/tasks/mocha.js +++ b/build/tasks/mocha.js @@ -37,6 +37,16 @@ module.exports = function(grunt) { } } }, + exportsCommonjs: { + src: ["test/index.html"], + options: { + page: { + settings: { + userAgent: "PhantomJS:testSource(commonjs)" + } + } + } + }, exportsGlobal: { src: ["test/index.html"], options: { diff --git a/package.json b/package.json index d27fa3d..97e7838 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ }, "license": "MIT", "devDependencies": { + "d3": "^3.5.12", "grunt": "~0.4.0", + "grunt-browserify": "^4.0.1", "grunt-cli": "~0.1.11", "grunt-contrib-concat": "~0.1.3", "grunt-contrib-jshint": "^0.9.2", @@ -32,5 +34,8 @@ "grunt-webpack": "^1.0.11", "webpack": "^1.12.2", "webpack-dev-server": "^1.12.0" + }, + "peerDependencies": { + "d3": "3.x.x" } } diff --git a/src/chart-extensions.js b/src/chart-extensions.js index 2701baa..e5a8cca 100644 --- a/src/chart-extensions.js +++ b/src/chart-extensions.js @@ -1,4 +1,4 @@ -define(function(require) { +define(function(require, exports, module) { "use strict"; var d3 = require("d3"); @@ -77,4 +77,5 @@ d3.selection.enter.prototype.chart = function() { }; d3.transition.prototype.chart = d3.selection.enter.prototype.chart; +module.exports = d3.chart; }); diff --git a/test/index.html b/test/index.html index 6cc0d3d..7e5b222 100644 --- a/test/index.html +++ b/test/index.html @@ -81,8 +81,8 @@

Test Sources

} }); - require(["chart-extensions"], function() { - done(null, window.d3.chart); + require(["chart-extensions"], function(d3chart) { + done(null, d3chart); }); } }, @@ -101,11 +101,36 @@

Test Sources

} }); - require(["test/d3.chart.test-build"], function() { - done(null, window.d3.chart); + require(["test/d3.chart.test-build"], function(d3chart) { + if (typeof d3chart !== "function") { + done(new Error("Application failed to load d3.chart.")); + return; + } + + if (d3chart !== window.d3.chart) { + done(new Error("d3chart not attached to global `d3` object.")); + return; + } + + done(null, d3chart); }); } }, + commonjs: { + prereqs: [ + "sample-cjs-app/packaged.js" + ], + setup: function(done) { + if (!window.app) { + done(new Error("Application global not found.")); + return; + } + + window.d3 = window.app.d3; + + done(null, window.app.d3chart); + } + }, global: { prereqs: [ "../bower_components/d3/d3.js", diff --git a/test/sample-cjs-app/main.js b/test/sample-cjs-app/main.js new file mode 100644 index 0000000..32f5bd1 --- /dev/null +++ b/test/sample-cjs-app/main.js @@ -0,0 +1,17 @@ +/** + * This file is used to prove CommonJS support. It is built for the browser + * prior to running tests, and the exported "app" global is used to derive the + * test environment's reference to d3.chart. + */ +"use strict"; + +var d3 = require("d3"); + +if (Object.prototype.hasOwnProperty.call(d3, "chart")) { + throw new Error("d3.chart incorrectly loaded prior to import."); +} + +window.app = { + d3: d3, + d3chart: require("d3.chart") +}; From 9b394dfacd54738de4a7c90c105a3b5017dc5e55 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Fri, 8 Jan 2016 20:07:45 -0500 Subject: [PATCH 3/7] fixup! Formalize exported value and test CommonJS support --- test/index.html | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/test/index.html b/test/index.html index 7e5b222..80105f0 100644 --- a/test/index.html +++ b/test/index.html @@ -81,8 +81,8 @@

Test Sources

} }); - require(["chart-extensions"], function(d3chart) { - done(null, d3chart); + require(["d3", "chart-extensions"], function(d3, d3chart) { + done(null, d3, d3chart); }); } }, @@ -101,18 +101,13 @@

Test Sources

} }); - require(["test/d3.chart.test-build"], function(d3chart) { + require(["d3", "test/d3.chart.test-build"], function(d3, d3chart) { if (typeof d3chart !== "function") { done(new Error("Application failed to load d3.chart.")); return; } - if (d3chart !== window.d3.chart) { - done(new Error("d3chart not attached to global `d3` object.")); - return; - } - - done(null, d3chart); + done(null, d3, d3chart); }); } }, @@ -126,9 +121,7 @@

Test Sources

return; } - window.d3 = window.app.d3; - - done(null, window.app.d3chart); + done(null, window.app.d3, window.app.d3chart); } }, global: { @@ -137,7 +130,7 @@

Test Sources

"d3.chart.test-build.js" ], setup: function(done) { - done(null, window.d3.chart); + done(null, window.d3, window.d3.chart); } } }; @@ -152,16 +145,19 @@

Test Sources

} loadScripts(source.prereqs, function() { - source.setup(function(err, d3chart) { + source.setup(function(err, d3, d3chart) { test("d3.chart loaded successfully", function() { if (err) { throw err; } - if (window.d3.chart !== d3chart) { - throw new Error("Failed to load d3.chart"); + if (d3chart !== d3.chart) { + throw new Error("d3chart not attached to `d3` object."); + return; } + + window.d3 = d3; }); loadScripts([ From 35cc230cd85d6a651f324eb4e7caf828c1103096 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Fri, 8 Jan 2016 20:11:49 -0500 Subject: [PATCH 4/7] fixup! Formalize exported value and test CommonJS support --- build/webpack.config.js | 10 +++++++++- test/index.html | 7 ++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/build/webpack.config.js b/build/webpack.config.js index 25d606c..9aa2561 100644 --- a/build/webpack.config.js +++ b/build/webpack.config.js @@ -19,7 +19,15 @@ module.exports = { context: "src", entry: "./chart-extensions", output: { - libraryTarget: "umd" + /** + * Ensure the name of the exported AMD module is "d3.chart". This makes + * the browser global somewhat awkward to use (`window["d3.chart"]`), + * but consumers in those contexts are most likely referencing the + * function through the `d3` global. + */ + library: "d3.chart", + libraryTarget: "umd", + umdNamedDefine: true }, plugins: [ new webpack.BannerPlugin(banner) diff --git a/test/index.html b/test/index.html index 80105f0..d6e0a86 100644 --- a/test/index.html +++ b/test/index.html @@ -92,7 +92,8 @@

Test Sources

require({ baseUrl: "../", paths: { - d3: "bower_components/d3/d3" + d3: "bower_components/d3/d3", + "d3.chart": "test/d3.chart.test-build" }, shim: { d3: { @@ -101,7 +102,7 @@

Test Sources

} }); - require(["d3", "test/d3.chart.test-build"], function(d3, d3chart) { + require(["d3", "d3.chart"], function(d3, d3chart) { if (typeof d3chart !== "function") { done(new Error("Application failed to load d3.chart.")); return; @@ -130,7 +131,7 @@

Test Sources

"d3.chart.test-build.js" ], setup: function(done) { - done(null, window.d3, window.d3.chart); + done(null, window.d3, window["d3.chart"]); } } }; From 252357a2dd737f6fdcc92cf061a3f83e09f959ec Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Thu, 21 Jan 2016 19:52:01 -0500 Subject: [PATCH 5/7] fixup! Refactor internals --- src/assert.js | 14 +- src/chart-extensions.js | 136 +++---- src/chart.js | 862 ++++++++++++++++++++-------------------- src/layer-extensions.js | 58 +-- src/layer.js | 420 ++++++++++---------- 5 files changed, 745 insertions(+), 745 deletions(-) diff --git a/src/assert.js b/src/assert.js index c27b000..5cddd0f 100644 --- a/src/assert.js +++ b/src/assert.js @@ -1,10 +1,10 @@ define(function(require, exports, module) { -"use strict"; + "use strict"; -module.exports = function(test, message) { - if (test) { - return; - } - throw new Error("[d3.chart] " + message); -}; + module.exports = function(test, message) { + if (test) { + return; + } + throw new Error("[d3.chart] " + message); + }; }); diff --git a/src/chart-extensions.js b/src/chart-extensions.js index e5a8cca..5f0e147 100644 --- a/src/chart-extensions.js +++ b/src/chart-extensions.js @@ -1,81 +1,81 @@ define(function(require, exports, module) { -"use strict"; -var d3 = require("d3"); + "use strict"; + var d3 = require("d3"); -var Chart = require("./chart"); -var assert = require("./assert"); + var Chart = require("./chart"); + var assert = require("./assert"); -assert(/^3\./.test(d3.version), "d3.js version 3 is required"); + assert(/^3\./.test(d3.version), "d3.js version 3 is required"); -require("./layer-extensions"); + require("./layer-extensions"); -/** - * A namespace defined by [the D3.js library](http://d3js.org/). The d3.chart - * API is defined within this namespace. - * @namespace d3 - */ + /** + * A namespace defined by [the D3.js library](http://d3js.org/). The d3.chart + * API is defined within this namespace. + * @namespace d3 + */ -/** - * A constructor function defined by [the D3.js library](http://d3js.org/). - * @constructor d3.selection - * @memberof d3 - */ + /** + * A constructor function defined by [the D3.js library](http://d3js.org/). + * @constructor d3.selection + * @memberof d3 + */ -/** - * Create a new chart constructor or return a previously-created chart - * constructor. - * - * @static - * @memberof d3 - * @externalExample {runnable} chart - * - * @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]; - } + /** + * Create a new chart constructor or return a previously-created chart + * constructor. + * + * @static + * @memberof d3 + * @externalExample {runnable} chart + * + * @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); -}; + return Chart.extend.apply(Chart, arguments); + }; -/** - * Instantiate a chart or return the chart that the current selection belongs - * to. - * - * @externalExample {runnable} selection-chart - * - * @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]; - assert(ChartCtor, "No chart registered with name '" + chartName + "'"); + /** + * Instantiate a chart or return the chart that the current selection belongs + * to. + * + * @externalExample {runnable} selection-chart + * + * @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]; + assert(ChartCtor, "No chart registered with name '" + chartName + "'"); - return new ChartCtor(this, options); -}; + 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; + // 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; -module.exports = d3.chart; + module.exports = d3.chart; }); diff --git a/src/chart.js b/src/chart.js index 2cf795b..eee3957 100644 --- a/src/chart.js +++ b/src/chart.js @@ -1,467 +1,467 @@ define(function(require, exports, module) { -"use strict"; -var assert = require("./assert"); -var hasOwnProp = require("./has-own-prop"); - -// 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]; + "use strict"; + var assert = require("./assert"); + var hasOwnProp = require("./has-own-prop"); + + // 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); + return object; } - // 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); - } + /** + * 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 `transform` method on classes further up the prototype - // chain (yet). - if (hasOwnProp.call(ctor.prototype, "transform")) { - data = ctor.prototype.transform.call(instance, data); - } + // 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); + } + }; - if (sup) { - data = transformCascade.call(sup, instance, data); - } + /** + * 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); + } - 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; - } + // 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); + } - 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]; - } + if (sup) { + data = transformCascade.call(sup, instance, data); + } + + return data; + }; - // we are reattaching a previous layer, which the - // selection argument is now set to. - if (arguments.length === 2) { + /** + * 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; + } - if (typeof selection.draw === "function") { - selection._chart = this; - this._layers[name] = selection; + 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]; + } - } else { - assert(false, "When reattaching a layer, the second argument " + - "must be a d3.chart layer"); + // 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]; - } + layer = selection.layer(options); - 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); - } + this._layers[name] = layer; - for (attachmentName in this._attached) { - if (this.demux) { - attachmentData = this.demux(attachmentName, data); - } else { - attachmentData = data; + 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].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); + + this._attached[attachmentName] = chart; + return chart; }; - 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; + + /** + * 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); } - return this; - } - // remove all events for a specific name - if (arguments.length === 1) { - events = this._events[name]; - if (events) { - events.length = 0; + 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); + }; - // 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); + /** + * 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; } - } - 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); + // remove all events for a specific name + if (arguments.length === 1) { + events = this._events[name]; + if (events) { + events.length = 0; + } + return this; } - } - 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); }; - } + // 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); + } + } - // Add static properties to the constructor function, if supplied. - extend(child, parent, staticProps); + return this; + }; - // 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(); + /** + * 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); - // Add prototype properties (instance properties) to the subclass, if - // supplied. - if (protoProps) { extend(child.prototype, protoProps); } + // 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 a convenience property in case the parent's prototype is needed - // later. - child.__super__ = parent.prototype; + // Add prototype properties (instance properties) to the subclass, if + // supplied. + if (protoProps) { extend(child.prototype, protoProps); } - Chart[name] = child; - return child; -}; + // 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; + module.exports = Chart; }); diff --git a/src/layer-extensions.js b/src/layer-extensions.js index b312e42..eeb1e40 100644 --- a/src/layer-extensions.js +++ b/src/layer-extensions.js @@ -1,38 +1,38 @@ define(function(require) { -"use strict"; -var d3 = require("d3"); + "use strict"; + var d3 = require("d3"); -var Layer = require("./layer"); + var Layer = require("./layer"); -/** - * 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; + /** + * 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; + // 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]); + // 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); }; + // 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; -}; + return this; + }; }); diff --git a/src/layer.js b/src/layer.js index 8a7a794..eeb3a17 100644 --- a/src/layer.js +++ b/src/layer.js @@ -1,226 +1,226 @@ define(function(require, exports, module) { -"use strict"; -var d3 = require("d3"); - -var assert = require("./assert"); - -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 - * @externalExample {runnable} layer - * - * @param {d3.selection} base The containing DOM node for the layer. - */ -var Layer = function(base) { - assert(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() { - assert(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() { - assert(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. - * - * @externalExample {runnable} layer-on - * - * @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 || {}; - - assert( - 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. - * - * @externalExample {runnable} layer-off - * - * @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; - - assert( - lifecycleRe.test(eventName), - "Unrecognized lifecycle event name specified to `Layer#off`: '" + - eventName + "'." - ); - - if (!handlers) { - return this._base; - } - - if (arguments.length === 1) { - handlers.length = 0; + "use strict"; + var d3 = require("d3"); + + var assert = require("./assert"); + + 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 + * @externalExample {runnable} layer + * + * @param {d3.selection} base The containing DOM node for the layer. + */ + var Layer = function(base) { + assert(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() { + assert(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() { + assert(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. + * + * @externalExample {runnable} layer-on + * + * @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 || {}; + + assert( + 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. + * + * @externalExample {runnable} layer-off + * + * @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; + + assert( + lifecycleRe.test(eventName), + "Unrecognized lifecycle event name specified to `Layer#off`: '" + + eventName + "'." + ); + + 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 - * - * @externalExample {runnable} layer-draw - * - * @param {Array} data Data to drive the rendering. - */ -Layer.prototype.draw = function(data) { - var bound, entering, events, selection, method, 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. - assert(bound && bound.call === d3.selection.prototype.call, - "Invalid selection defined by `Layer#dataBind` method."); - assert(bound.enter, "Layer selection not properly bound."); - - entering = bound.enter(); - entering._chart = this._base._chart; - - events = [ - { - name: "update", - selection: bound - }, - { - name: "enter", - selection: entering, - method: this.insert - }, - { - name: "merge", - // Although the `merge` lifecycle event shares its selection object - // with the `update` lifecycle event, the object's contents will be - // modified when d3.chart invokes the user-supplied `insert` method - // when triggering the `enter` event. - selection: bound - }, - { - name: "exit", - // Although the `exit` lifecycle event shares its selection object - // with the `update` and `merge` lifecycle events, the object's - // contents will be modified when d3.chart invokes - // `d3.selection.exit`. - selection: bound, - method: bound.exit + + for (idx = handlers.length - 1; idx > -1; --idx) { + if (handlers[idx].callback === handler) { + handlers.splice(idx, 1); + } } - ]; + 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 + * + * @externalExample {runnable} layer-draw + * + * @param {Array} data Data to drive the rendering. + */ + Layer.prototype.draw = function(data) { + var bound, entering, events, selection, method, 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. + assert(bound && bound.call === d3.selection.prototype.call, + "Invalid selection defined by `Layer#dataBind` method."); + assert(bound.enter, "Layer selection not properly bound."); + + entering = bound.enter(); + entering._chart = this._base._chart; + + events = [ + { + name: "update", + selection: bound + }, + { + name: "enter", + selection: entering, + method: this.insert + }, + { + name: "merge", + // Although the `merge` lifecycle event shares its selection object + // with the `update` lifecycle event, the object's contents will be + // modified when d3.chart invokes the user-supplied `insert` method + // when triggering the `enter` event. + selection: bound + }, + { + name: "exit", + // Although the `exit` lifecycle event shares its selection object + // with the `update` and `merge` lifecycle events, the object's + // contents will be modified when d3.chart invokes + // `d3.selection.exit`. + selection: bound, + method: bound.exit + } + ]; - for (var i = 0, l = events.length; i < l; ++i) { - eventName = events[i].name; - selection = events[i].selection; - method = events[i].method; + for (var i = 0, l = events.length; i < l; ++i) { + eventName = events[i].name; + selection = events[i].selection; + method = events[i].method; - // Some lifecycle selections modify shared state, so they must be - // deferred until just prior to handler invocation. - if (typeof method === "function") { - selection = method.call(selection); - } + // Some lifecycle selections modify shared state, so they must be + // deferred until just prior to handler invocation. + if (typeof method === "function") { + selection = method.call(selection); + } - if (selection.empty()) { - continue; - } + if (selection.empty()) { + continue; + } - // Although `selection instanceof d3.selection` is more explicit, - // it fails in IE8, so we use duck typing to maintain - // compatability. - 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. + 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); + } } - } - 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); + } } } - } -}; + }; -module.exports = Layer; + module.exports = Layer; }); From 827fe6c5a08c1b58a1354ce3bcff0319993a71b5 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Thu, 21 Jan 2016 19:53:01 -0500 Subject: [PATCH 6/7] Remove internal `hasOwnProperty` module This module is just a wrapper around the built-in `Object.hasOwnProperty` method, making it somewhat superfluous. Remove it in order to enhance readability. --- src/chart.js | 2 +- src/has-own-prop.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 src/has-own-prop.js diff --git a/src/chart.js b/src/chart.js index eee3957..e8cc9cf 100644 --- a/src/chart.js +++ b/src/chart.js @@ -1,7 +1,7 @@ define(function(require, exports, module) { "use strict"; var assert = require("./assert"); - var hasOwnProp = require("./has-own-prop"); + var hasOwnProp = Object.hasOwnProperty; // extend // Borrowed from Underscore.js diff --git a/src/has-own-prop.js b/src/has-own-prop.js deleted file mode 100644 index 74834f9..0000000 --- a/src/has-own-prop.js +++ /dev/null @@ -1,4 +0,0 @@ -define(function(require, exports, module) { - "use strict"; - module.exports = Object.hasOwnProperty; -}); From 2010e6997b6dc6cd5ccccaea2e73f608432f0103 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Thu, 21 Jan 2016 20:09:30 -0500 Subject: [PATCH 7/7] Improve "usage" documentation --- readme.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 46efbbf..aa47c0b 100644 --- a/readme.md +++ b/readme.md @@ -21,15 +21,24 @@ You can also install it via [npm](http://npmjs.org) by running: $ npm install d3.chart -Otherwise, you can download it directly and embed it using a script tag. +Otherwise, you can download it directly from this repository. -`d3.chart` depends on D3.js, so include it only *after* D3.js has been -defined in the document, e.g. +## Using -```html - - -``` +d3.chart implements "UMD", making it convenient to consume from a number of +environments: + +- The library may be loaded in a web browser directly via HTML ` + + ``` +- From [AMD](https://github.com/amdjs/amdjs-api)-enabled environments, simply + add an entry for "d3.chart" in your `paths` configuration. +- Consuming using [CommonJS modules](http://wiki.commonjs.org/wiki/Modules/1.1) + (e.g. via tools like [Browserify](http://browserify.org/)) should "just work" ## Build Instructions