From e1fd48c4242d0e8d95b8365806efd82c1f2adb83 Mon Sep 17 00:00:00 2001 From: Michael Hackstein Date: Fri, 19 Jul 2013 09:16:58 +0200 Subject: [PATCH] GraphViewer: The CommunityNodes are now able to expand and collapse themselfes. --- .../js/graphViewer/graph/communityNode.js | 119 +++++---- .../specCommunityNode/communityNodeSpec.js | 235 +++++++++++++++--- 2 files changed, 268 insertions(+), 86 deletions(-) diff --git a/html/admin/js/graphViewer/graph/communityNode.js b/html/admin/js/graphViewer/graph/communityNode.js index ac9656a937..711a768d69 100644 --- a/html/admin/js/graphViewer/graph/communityNode.js +++ b/html/admin/js/graphViewer/graph/communityNode.js @@ -66,6 +66,7 @@ function CommunityNode(parent, initial) { self = this, boundingBox, nodes = {}, + observer, nodeArray = [], internal = {}, inbound = {}, @@ -75,6 +76,20 @@ function CommunityNode(parent, initial) { // Private functions // //////////////////////////////////// + getObserver = function() { + if (!observer) { + observer = new WebKitMutationObserver(function(e){ + if (_.any(e, function(obj) { + return obj.attributeName === "transform"; + })) { + updateBoundingBox(); + observer.disconnect(); + } + }); + } + return observer; + }, + toArray = function(obj) { var res = []; _.each(obj, function(v) { @@ -205,14 +220,31 @@ function CommunityNode(parent, initial) { }, expand = function() { + this._expanded = true; // TODO: Just piped through for old Adapter Interface - dissolve(); + //dissolve(); }, dissolve = function() { parent.dissolveCommunity(self); }, + collapse = function() { + this._expanded = false; + // TODO + }, + + addDistortion = function() { + // Fake Layouting TODO + _.each(nodeArray, function(n) { + n.position = { + x: n.x, + y: n.y, + z: 1 + }; + }); + }, + addShape = function (g, shapeFunc, colourMapper) { g.attr("stroke", colourMapper.getForegroundCommunityColour()); shapeFunc(g); @@ -244,6 +276,35 @@ function CommunityNode(parent, initial) { textN.text(self._size); } }, + + addNodeShapes = function(g, shapeFunc, colourMapper) { + var interior = g.selectAll(".node") + .data(nodeArray, function(d) { + return d._id; + }); + interior.enter() + .append("g") + .attr("class", "node") + .attr("id", function(d) { + return d._id; + }); + // Remove all old + interior.exit().remove(); + interior.selectAll("* > *").remove(); + addShape(interior, shapeFunc, colourMapper); + }, + + addBoundingBox = function(g) { + boundingBox = g.append("rect") + .attr("rx", "8") + .attr("ry", "8") + .attr("fill", "none") + .attr("stroke", "black"); + getObserver().observe(document.getElementById(self._id), { + subtree:true, + attributes:true + }); + }, updateBoundingBox = function() { var bbox = document.getElementById(self._id).getBBox(); @@ -254,53 +315,14 @@ function CommunityNode(parent, initial) { }, shapeAll = function(g, shapeFunc, colourMapper) { - /* - boundingBox = g.append("rect") - .attr("rx", "8") - .attr("ry", "8") - .attr("fill", "none") - .attr("stroke", "black"); - */ + if (self._expanded) { + addBoundingBox(g); + addDistortion(); + addNodeShapes(g, shapeFunc, colourMapper); + return; + } addCollapsedShape(g, shapeFunc, colourMapper); addCollapsedLabel(g, colourMapper); - /* - _.each(nodeArray, function(n) { - n.position = { - x: n.x, - y: n.y, - z: 1 - }; - }); - - var interior = g.selectAll(".node") - .data(nodeArray, function(d) { - return d._id; - }); - interior.enter() - .append("g") - .attr("class", function(d) { - return "node"; - }) // node is CSS class that might be edited - .attr("id", function(d) { - return d._id; - }); - // Remove all old - interior.exit().remove(); - interior.selectAll("* > *").remove(); - var observer = new WebKitMutationObserver(function(e){ - if (_.any(e, function(obj) { - return obj.attributeName === "transform"; - })) { - updateBoundingBox(); - observer.disconnect(); - } - }); - observer.observe(document.getElementById(self._id), { - subtree:true, - attributes:true - }); - addShape(interior, shapeFunc, colourMapper); - */ }; //////////////////////////////////// @@ -321,7 +343,7 @@ function CommunityNode(parent, initial) { this._size = 0; this._inboundCounter = 0; this._outboundCounter = 0; - + this._expanded = false; // Easy check for the other classes, // no need for a regex on the _id any more. this._isCommunity = true; @@ -345,13 +367,12 @@ function CommunityNode(parent, initial) { this.removeNode = removeNode; this.removeInboundEdge = removeInboundEdge; this.removeOutboundEdge = removeOutboundEdge; - this.removeOutboundEdgesFromNode = removeOutboundEdgesFromNode; this.dissolve = dissolve; - this.getDissolveInfo = getDissolveInfo; + this.collapse = collapse; this.expand = expand; this.shape = shapeAll; diff --git a/html/admin/js/graphViewer/jasmine_test/specCommunityNode/communityNodeSpec.js b/html/admin/js/graphViewer/jasmine_test/specCommunityNode/communityNodeSpec.js index b01f3a878e..f2960dd1f8 100644 --- a/html/admin/js/graphViewer/jasmine_test/specCommunityNode/communityNodeSpec.js +++ b/html/admin/js/graphViewer/jasmine_test/specCommunityNode/communityNodeSpec.js @@ -1,8 +1,8 @@ /*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */ -/*global beforeEach, afterEach */ +/*global beforeEach, afterEach, jasmine*/ /*global describe, it, expect, spyOn */ /*global helper*/ -/*global document*/ +/*global document, window*/ /*global CommunityNode*/ //////////////////////////////////////////////////////////////////////////////// @@ -136,11 +136,7 @@ it('should offer a function to remove and return all outgoing edges of a node', function() { expect(testee).toHaveFunction("removeOutboundEdgesFromNode", 1); }); - - it('should offer a function to dissolve the community', function() { - expect(testee).toHaveFunction("dissolve", 0); - }); - + it('should offer a function to get the dissolve info', function() { expect(testee).toHaveFunction("getDissolveInfo", 0); }); @@ -157,7 +153,9 @@ expect(testee).toHaveFunction("collapse", 0); }); - + it('should offer a function to dissolve the community', function() { + expect(testee).toHaveFunction("dissolve", 0); + }); }); describe('node functionality', function() { @@ -295,7 +293,8 @@ describe('shaping functionality', function() { var tSpan1, tSpan2, text, g, shaper, colourMapper, box, boxRect, - parent; + parent, c; + beforeEach(function() { parent = { dissolveCommunity: function() {} @@ -373,6 +372,8 @@ } }; }); + + c = new CommunityNode(parent, nodes.slice(0, 5)); }); it('should initially contain the required attributes for shaping', function() { @@ -391,24 +392,23 @@ z: 1 } }, - initNodes = [n].concat(nodes.slice(3, 13)), - c = new CommunityNode(parent, initNodes); - expect(c.x).toBeDefined(); - expect(c.x).toEqual(x); - expect(c.y).toBeDefined(); - expect(c.y).toEqual(y); - expect(c._size).toEqual(11); - expect(c._isCommunity).toBeTruthy(); + otherC = new CommunityNode(parent, [n].concat(nodes.slice(5,21))); + expect(otherC.x).toBeDefined(); + expect(otherC.x).toEqual(x); + expect(otherC.y).toBeDefined(); + expect(otherC.y).toEqual(y); + expect(otherC._size).toEqual(17); + expect(otherC._isCommunity).toBeTruthy(); }); it('should shape the collapsed community with given functions', function() { - var c = new CommunityNode(parent, nodes.slice(0, 2)); spyOn(g, "attr").andCallThrough(); spyOn(g, "append").andCallThrough(); spyOn(shaper, "shapeFunc").andCallThrough(); spyOn(colourMapper, "getForegroundCommunityColour").andCallThrough(); c.shape(g, shaper.shapeFunc, colourMapper); + expect(colourMapper.getForegroundCommunityColour).wasCalled(); expect(g.attr).wasCalledWith("stroke", "black"); expect(shaper.shapeFunc).wasCalledWith(g, 9); @@ -418,7 +418,6 @@ }); it('should add a label containing the size of a community', function() { - var c = new CommunityNode(parent, nodes.slice(0, 2)); spyOn(g, "append").andCallThrough(); spyOn(text, "attr").andCallThrough(); spyOn(text, "text").andCallThrough(); @@ -430,12 +429,11 @@ expect(text.attr).wasCalledWith("text-anchor", "middle"); expect(text.attr).wasCalledWith("fill", "black"); expect(text.attr).wasCalledWith("stroke", "none"); - expect(text.text).wasCalledWith(2); + expect(text.text).wasCalledWith(5); expect(text.append).wasNotCalled(); }); it('should add a label if a reason is given', function() { - var c = new CommunityNode(parent, nodes.slice(0, 2)); c._reason = { key: "key", value: "label" @@ -467,25 +465,170 @@ expect(tSpan2.attr).wasCalledWith("dy", "16"); expect(tSpan2.text).wasCalledWith("label"); }); - /* - it('should print the bounding box correctly', function() { - var c = new CommunityNode(nodes.slice(0, 2)); - spyOn(g, "append").andCallThrough(); - spyOn(boxRect, "attr").andCallThrough(); + + describe('if the community is expanded', function() { - c.shape(g, shaper.shapeFunc, colourMapper); + var nodeSelector, interior, iEnter, iExit, iAll, iG, + observer, observerCB; + + beforeEach(function() { + g.selectAll = function(selector) { + return nodeSelector; + }; + nodeSelector = { + data: function() { + return interior; + } + }; + interior = { + enter: function() { + return iEnter; + }, + exit: function() { + return iExit; + }, + selectAll: function() { + return iAll; + }, + attr: function() { + return this; + } + }; + iEnter = { + append: function() { + return iG; + } + }; + iG = { + attr: function() { + return this; + } + }; + iExit = { + remove: function() {} + }; + iAll = { + remove: function() {} + }; + observer = { + observe: function() {}, + disconnect: function() {} + }; + spyOn(window, "WebKitMutationObserver").andCallFake(function(cb) { + observerCB = cb; + return observer; + }); + + c.expand(); + }); + + + it('should print the bounding box correctly', function() { + spyOn(g, "append").andCallThrough(); + spyOn(boxRect, "attr").andCallThrough(); + + c.shape(g, shaper.shapeFunc, colourMapper); + + expect(g.append).wasCalledWith("rect"); + expect(boxRect.attr).wasCalledWith("rx", "8"); + expect(boxRect.attr).wasCalledWith("ry", "8"); + expect(boxRect.attr).wasCalledWith("fill", "none"); + expect(boxRect.attr).wasCalledWith("stroke", "black"); + + }); + + it('should update the box as soon as the dom is ready', function() { + spyOn(boxRect, "attr").andCallThrough(); + spyOn(observer, "observe").andCallThrough(); + spyOn(observer, "disconnect").andCallThrough(); + + c.shape(g, shaper.shapeFunc, colourMapper); + + expect(document.getElementById).wasCalledWith(c._id); + expect(observer.observe).wasCalledWith( + jasmine.any(Object), + { + subtree:true, + attributes:true + } + ); + + + observerCB([{attributeName: "transform"}]); + + expect(boxRect.attr).wasCalledWith("width", box.width + 10); + expect(boxRect.attr).wasCalledWith("height", box.height + 10); + expect(boxRect.attr).wasCalledWith("x", box.x - 5); + expect(boxRect.attr).wasCalledWith("y", box.y - 5); + expect(observer.disconnect).wasCalled(); + }); + + it('should shape the expanded community with given functions', function() { + + spyOn(g, "selectAll").andCallThrough(); + spyOn(nodeSelector, "data").andCallFake(function(a, func) { + expect(func(nodes[0])).toEqual(nodes[0]._id); + expect(func(nodes[6])).toEqual(nodes[6]._id); + return interior; + }); + spyOn(interior, "enter").andCallThrough(); + spyOn(interior, "exit").andCallThrough(); + spyOn(interior, "selectAll").andCallThrough(); + spyOn(iEnter, "append").andCallThrough(); + spyOn(iG, "attr").andCallThrough(); + spyOn(iExit, "remove").andCallThrough(); + spyOn(iAll, "remove").andCallThrough(); + + + c.shape(g, shaper.shapeFunc, colourMapper); + + expect(g.selectAll).wasCalledWith(".node"); + expect(nodeSelector.data).wasCalledWith(c.getNodes(), jasmine.any(Function)); + expect(interior.enter).wasCalled(); + expect(iEnter.append).wasCalledWith("g"); + expect(iG.attr).wasCalledWith("class", "node"); + expect(iG.attr).wasCalledWith("id", jasmine.any(Function)); + expect(interior.exit).wasCalled(); + expect(iExit.remove).wasCalled(); + expect(interior.selectAll).wasCalledWith("* > *"); + expect(iAll.remove).wasCalled(); + + /* + var observer = new WebKitMutationObserver(function(e){ + if (_.any(e, function(obj) { + return obj.attributeName === "transform"; + })) { + updateBoundingBox(); + observer.disconnect(); + } + }); + observer.observe(document.getElementById(self._id), { + subtree:true, + attributes:true + }); + addShape(interior, shapeFunc, colourMapper); + */ + }); + + it('should apply distortion on the interior nodes', function() { + // Fake Layouting to test correctness + /* + nodes[0].x = -20; + nodes[0].y = 20; + nodes[1].x = -10; + nodes[1].y = 10; + nodes[2].x = 0; + nodes[2].y = 0; + nodes[3].x = 10; + nodes[3].y = -10; + nodes[4].x = 20; + nodes[4].y = -20; + */ + }); - expect(g.append).wasCalledWith("rect"); - expect(boxRect.attr).wasCalledWith("width", box.width + 10); - expect(boxRect.attr).wasCalledWith("height", box.height + 10); - expect(boxRect.attr).wasCalledWith("x", box.x - 5); - expect(boxRect.attr).wasCalledWith("y", box.y - 5); - expect(boxRect.attr).wasCalledWith("rx", "8"); - expect(boxRect.attr).wasCalledWith("ry", "8"); - expect(boxRect.attr).wasCalledWith("fill", "none"); - expect(boxRect.attr).wasCalledWith("stroke", "black"); }); - */ + + }); describe('edge functionality', function() { @@ -807,9 +950,27 @@ }; }); + it('should be possible to dissolve the community', function() { + var c = new CommunityNode(parent); + spyOn(parent, "dissolveCommunity"); + c.dissolve(); + expect(parent.dissolveCommunity).wasCalledWith(c); + }); + it('should be possible to expand the community', function() { var c = new CommunityNode(parent); + expect(c._expanded).toBeFalsy(); c.expand(); + expect(c._expanded).toBeTruthy(); + }); + + it('should be possible to collapse the community', function() { + var c = new CommunityNode(parent); + expect(c._expanded).toBeFalsy(); + c.expand(); + expect(c._expanded).toBeTruthy(); + c.collapse(); + expect(c._expanded).toBeFalsy(); }); });