UNPKG

d3.chart

Version:

A framework for creating reusable charts with D3.js

492 lines (437 loc) 16.1 kB
suite("d3.layer", function() { "use strict"; suite("constructor", function() { test("returns a reference to the selection", function() { var base = d3.select("#test"); var inst = base.layer({}); assert.equal(inst, base); }); test("extends the selection with a `draw` method", function() { var base = d3.select("#test"); var inst = base.layer({}); assert.equal(typeof inst.draw, "function"); }); test("extends the selection with an `on` method", function() { var base = d3.select("#test"); var inst = base.layer({}); assert.equal(typeof inst.on, "function"); }); test("extends the selection with an `off` method", function() { var base = d3.select("#test"); var inst = base.layer({}); assert.equal(typeof inst.off, "function"); }); }); suite("#draw", function() { setup(function() { var dataBind = this.dataBind = sinon.spy(function(data) { 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; sinon.spy(updating, "enter"); sinon.spy(updating, "transition"); // Stub the `exit` method so that we can spy on the generated // selections `transition` method. sinon.stub(updating, "exit", function() { var exiting = originalExit.apply(this, arguments); sinon.spy(exiting, "transition"); return exiting; }); return updating; }); var insert = this.insert = sinon.spy(function() { var entering = this.insert("g"); sinon.spy(entering, "transition"); return entering; }); var base = this.base = d3.select("#test").append("svg"); this.layer = base.layer({ dataBind: dataBind, insert: insert }); }); test("invokes the provided `dataBind` method exactly once", function() { assert.equal(this.dataBind.callCount, 0); this.layer.draw([]); assert.equal(this.dataBind.callCount, 1); }); test("invokes the provided `dataBind` method in the context of the layer's selection", function() { assert.equal(this.dataBind.callCount, 0); this.layer.draw([]); assert(this.dataBind.calledOn(this.base)); }); test("invokes the provided `dataBind` method with the specified data", function() { var data = []; assert.equal(this.dataBind.callCount, 0); this.layer.draw(data); assert.equal(this.dataBind.args[0][0], data); }); test("invokes the provided `insert` method exactly once", function() { assert.equal(this.insert.callCount, 0); this.layer.draw([]); assert.equal(this.insert.callCount, 1); }); test("invokes the provided `insert` method in the context of the layer's bound 'enter' selection", function() { this.layer.draw([]); assert(this.insert.calledOn(this.dataBind.returnValues[0].enter.returnValues[0])); }); suite("event triggering", function() { test("invokes event handlers with the correct selection", function() { var layer = this.base.append("g").layer({ insert: function() { return this.append("g"); }, dataBind: function(data) { return this.selectAll("g").data(data, function(d) { return d; }); } }); // Wrap each assertion in a spy so we can ensure that each actually // runs. var updateSpy = sinon.spy(function() { assert.deepEqual(this.data(), [3, 4], "update handler"); }); var enterSpy = sinon.spy(function() { // Build the expected sparse array to pass lint and avoid // failures due to browser implementation differences var expected = []; expected[2] = 5; expected[3] = 6; assert.deepEqual(this.data(), expected, "enter handler"); }); var mergeSpy = sinon.spy(function() { assert.deepEqual(this.data(), [3, 4, 5, 6], "merge handler"); }); var exitSpy = sinon.spy(function() { assert.deepEqual(this.data(), [1, 2], "exit handler"); }); layer.draw([1, 2, 3, 4]); // Bind the spies layer.on("update", updateSpy); layer.on("enter", enterSpy); layer.on("merge", mergeSpy); layer.on("exit", exitSpy); layer.draw([3, 4, 5, 6]); assert.equal(updateSpy.callCount, 1); assert.equal(enterSpy.callCount, 1); assert.equal(mergeSpy.callCount, 1); assert.equal(exitSpy.callCount, 1); 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(); this.onEnter2 = sinon.spy(); this.onUpdate = sinon.spy(); this.layer = this.base.append("g").layer({ 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); }); teardown(function() { this.layer.remove(); }); test("unbinds only the specified handler", function() { this.layer.off("enter", this.onEnter1); this.layer.draw([1, 2]); assert.equal(this.onEnter1.callCount, 0); assert.equal(this.onEnter2.callCount, 1); assert.equal(this.onUpdate.callCount, 1); }); test("unbinds only the handlers for the specified lifecycle selection", function() { this.layer.off("enter"); this.layer.draw([1]); assert.equal(this.onEnter1.callCount, 0); assert.equal(this.onEnter2.callCount, 0); assert.equal(this.onUpdate.callCount, 1); }); }); test("does not make transition selections if related handlers are not bound", function() { var layer = this.base.append("g").layer({ insert: this.insert, dataBind: this.dataBind }); var spies = { update: sinon.spy(), enter: sinon.spy(), 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([2, 3]); assert.equal(spies.update.callCount, 1); assert.equal(spies.update.thisValues[0].transition.callCount, 0); assert.equal(spies.enter.callCount, 1); assert.equal(spies.enter.thisValues[0].transition.callCount, 0); assert.equal(spies.merge.callCount, 1); assert.equal(spies.merge.thisValues[0].transition.callCount, 0); assert.equal(spies.exit.callCount, 1); assert.equal(spies.exit.thisValues[0].transition.callCount, 0); }); suite("bound in constructor", function() { setup(function() { this.spies = { "enter": sinon.spy(), "update": sinon.spy(), "merge": sinon.spy(), "exit": sinon.spy(), "enter:transition": sinon.spy(), "update:transition": sinon.spy(), "merge:transition": sinon.spy(), "exit:transition": sinon.spy() }; this.layer = this.base.layer({ dataBind: this.dataBind, insert: this.insert, events: this.spies }); }); test("invokes all event handlers exactly once", function() { 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); assert.equal(this.spies.merge.callCount, 1); assert.equal(this.spies.exit.callCount, 1); assert.equal(this.spies["enter:transition"].callCount, 1); assert.equal(this.spies["update:transition"].callCount, 1); assert.equal(this.spies["merge:transition"].callCount, 1); assert.equal(this.spies["exit:transition"].callCount, 1); }); test("invokes all event handlers in the context of the corresponding 'lifecycle selection'", function() { var entering, updating, exiting; this.layer.draw([1, 2]); this.layer.draw([2, 3]); // Alias lifecycle selections entering = this.insert.returnValues[0]; updating = this.dataBind.returnValues[1]; exiting = updating.exit.returnValues[0]; assert(this.spies.enter.calledOn(entering)); assert(this.spies.update.calledOn(updating)); assert(this.spies.merge.calledOn(updating)); assert(this.spies.exit.calledOn(exiting)); assert(this.spies["enter:transition"].calledOn(entering.transition.returnValues[0])); assert(this.spies["update:transition"].calledOn(updating.transition.returnValues[0])); assert(this.spies["merge:transition"].calledOn(updating.transition.returnValues[1])); assert(this.spies["exit:transition"].calledOn(exiting.transition.returnValues[0])); }); }); suite("bound with `on`", function() { setup(function() { this.onEnter1 = sinon.spy(); this.onUpdate1 = sinon.spy(); this.onMerge1 = sinon.spy(); this.onExit1 = sinon.spy(); this.onEnterTrans1 = sinon.spy(); this.onUpdateTrans1 = sinon.spy(); this.onMergeTrans1 = sinon.spy(); this.onExitTrans1 = sinon.spy(); this.onUpdate2 = sinon.spy(); this.onMerge2 = sinon.spy(); this.onExit2 = sinon.spy(); this.onExit3 = sinon.spy(); this.onEnterTrans2 = sinon.spy(); this.onEnterTrans3 = sinon.spy(); this.onUpdateTrans2 = sinon.spy(); this.onMergeTrans2 = sinon.spy(); this.onMergeTrans3 = sinon.spy(); }); 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); this.layer.on("merge", this.onMerge1); this.layer.on("merge", this.onMerge2); this.layer.on("exit", this.onExit1); this.layer.on("exit", this.onExit2); this.layer.on("exit", this.onExit3); this.layer.on("enter:transition", this.onEnterTrans1); this.layer.on("enter:transition", this.onEnterTrans2); this.layer.on("enter:transition", this.onEnterTrans3); this.layer.on("update:transition", this.onUpdateTrans1); this.layer.on("update:transition", this.onUpdateTrans2); this.layer.on("merge:transition", this.onMergeTrans1); this.layer.on("merge:transition", this.onMergeTrans2); this.layer.on("merge:transition", this.onMergeTrans3); this.layer.on("exit:transition", this.onExitTrans1); this.layer.draw([2, 3]); assert.equal(this.onEnter1.callCount, 1); assert.equal(this.onUpdate1.callCount, 1); assert.equal(this.onUpdate2.callCount, 1); assert.equal(this.onMerge1.callCount, 1); assert.equal(this.onMerge2.callCount, 1); assert.equal(this.onExit1.callCount, 1); assert.equal(this.onExit2.callCount, 1); assert.equal(this.onExit3.callCount, 1); assert.equal(this.onEnterTrans1.callCount, 1); assert.equal(this.onEnterTrans2.callCount, 1); assert.equal(this.onEnterTrans3.callCount, 1); assert.equal(this.onUpdateTrans1.callCount, 1); assert.equal(this.onUpdateTrans2.callCount, 1); assert.equal(this.onMergeTrans1.callCount, 1); assert.equal(this.onMergeTrans2.callCount, 1); assert.equal(this.onMergeTrans3.callCount, 1); assert.equal(this.onExitTrans1.callCount, 1); }); test("invokes event handlers in the order they were bound", function() { this.layer.on("exit", this.onExit1); 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)); assert(this.onExit2.calledBefore(this.onExit3)); }); test("invokes all event handlers in the context of the corresponding 'lifecycle selection'", function() { var entering, updating, exiting; this.layer.on("enter", this.onEnter1); this.layer.on("update", this.onUpdate1); this.layer.on("merge", this.onMerge1); this.layer.on("exit", this.onExit1); this.layer.on("enter:transition", this.onEnterTrans1); this.layer.on("update:transition", this.onUpdateTrans1); this.layer.on("merge:transition", this.onMergeTrans1); this.layer.on("exit:transition", this.onExitTrans1); this.layer.draw([1, 2]); this.layer.draw([2, 3]); // Alias lifecycle selections entering = this.insert.returnValues[0]; updating = this.dataBind.returnValues[1]; exiting = updating.exit.returnValues[0]; assert(this.onEnter1.calledOn(entering)); assert(this.onUpdate1.calledOn(updating)); assert(this.onMerge1.calledOn(updating)); assert(this.onExit1.calledOn(exiting)); assert(this.onEnterTrans1.calledOn(entering.transition.returnValues[0])); assert(this.onUpdateTrans1.calledOn(updating.transition.returnValues[0])); assert(this.onMergeTrans1.calledOn(updating.transition.returnValues[1])); assert(this.onExitTrans1.calledOn(exiting.transition.returnValues[0])); }); suite("`chart' context specified in the `options` argument", function() { setup(function() { this.chartVal = {}; this.handler = sinon.spy(); }); test("`enter`", function() { this.layer.on("enter", this.handler, { chart: this.chartVal }); this.layer.draw([1]); assert.equal(this.handler.thisValues[0].chart(), this.chartVal); }); test("`enter:transition`", function() { this.layer.on("enter:transition", this.handler, { chart: this.chartVal }); this.layer.draw([1]); assert.equal(this.handler.thisValues[0].chart(), this.chartVal); }); test("`update`", function() { this.layer.on("update", this.handler, { chart: this.chartVal }); this.layer.draw([1]); this.layer.draw([1]); assert.equal(this.handler.thisValues[0].chart(), this.chartVal); }); test("`update:transition`", function() { this.layer.on("update:transition", this.handler, { chart: this.chartVal }); this.layer.draw([1]); this.layer.draw([1]); assert.equal(this.handler.thisValues[0].chart(), this.chartVal); }); test("`merge`", function() { this.layer.on("merge", this.handler, { chart: this.chartVal }); this.layer.draw([1]); assert.equal(this.handler.thisValues[0].chart(), this.chartVal); }); test("`merge:transition`", function() { this.layer.on("merge:transition", this.handler, { chart: this.chartVal }); this.layer.draw([1]); assert.equal(this.handler.thisValues[0].chart(), this.chartVal); }); test("`exit`", function() { this.layer.on("exit", this.handler, { chart: this.chartVal }); this.layer.draw([1]); this.layer.draw([]); assert.equal(this.handler.thisValues[0].chart(), this.chartVal); }); test("`exit:transition`", 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(), this.chartVal); }); }); }); }); }); suite("events", function () { setup(function () { var base = this.base = d3.select("#test"); this.layer = new base.layer({}); }); suite("#on", function () { test("returns the layer instance (chains)", function() { assert.equal(this.layer.on("enter"), this.layer); }); }); suite("#off", function () { test("returns the layer instance (chains)", function() { assert.equal(this.layer.off("enter"), this.layer); }); }); }); });