mirror of https://gitee.com/bigwinds/arangodb
551 lines
14 KiB
JavaScript
551 lines
14 KiB
JavaScript
/*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */
|
|
/*global _, document, ForceLayouter, DomObserverFactory*/
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief Graph functionality
|
|
///
|
|
/// @file
|
|
///
|
|
/// DISCLAIMER
|
|
///
|
|
/// Copyright 2010-2012 triagens GmbH, Cologne, Germany
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
///
|
|
/// Copyright holder is triAGENS GmbH, Cologne, Germany
|
|
///
|
|
/// @author Michael Hackstein
|
|
/// @author Copyright 2011-2013, triAGENS GmbH, Cologne, Germany
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
function CommunityNode(parent, initial) {
|
|
"use strict";
|
|
|
|
if (_.isUndefined(parent)
|
|
|| !_.isFunction(parent.dissolveCommunity)
|
|
|| !_.isFunction(parent.checkNodeLimit)) {
|
|
throw "A parent element has to be given.";
|
|
}
|
|
|
|
initial = initial || [];
|
|
|
|
var
|
|
|
|
////////////////////////////////////
|
|
// Private variables //
|
|
////////////////////////////////////
|
|
self = this,
|
|
bBox,
|
|
bBoxBorder,
|
|
bBoxTitle,
|
|
nodes = {},
|
|
observer,
|
|
nodeArray = [],
|
|
intEdgeArray = [],
|
|
internal = {},
|
|
inbound = {},
|
|
outbound = {},
|
|
outReferences = {},
|
|
layouter,
|
|
////////////////////////////////////
|
|
// Private functions //
|
|
////////////////////////////////////
|
|
|
|
getDistance = function(def) {
|
|
if (self._expanded) {
|
|
return 2 * def * Math.sqrt(nodeArray.length);
|
|
}
|
|
return def;
|
|
},
|
|
|
|
getCharge = function(def) {
|
|
if (self._expanded) {
|
|
return 4 * def * Math.sqrt(nodeArray.length);
|
|
}
|
|
return def;
|
|
},
|
|
|
|
compPosi = function(p) {
|
|
var d = self.position,
|
|
x = p.x * d.z + d.x,
|
|
y = p.y * d.z + d.y,
|
|
z = p.z * d.z;
|
|
return {
|
|
x: x,
|
|
y: y,
|
|
z: z
|
|
};
|
|
},
|
|
|
|
getSourcePosition = function(e) {
|
|
if (self._expanded) {
|
|
return compPosi(e._source.position);
|
|
}
|
|
return self.position;
|
|
},
|
|
|
|
|
|
getTargetPosition = function(e) {
|
|
if (self._expanded) {
|
|
return compPosi(e._target.position);
|
|
}
|
|
return self.position;
|
|
},
|
|
|
|
updateBoundingBox = function() {
|
|
var boundingBox = document.getElementById(self._id).getBBox();
|
|
bBox.attr("transform", "translate(" + (boundingBox.x - 5) + "," + (boundingBox.y - 25) + ")");
|
|
bBoxBorder.attr("width", boundingBox.width + 10)
|
|
.attr("height", boundingBox.height + 30);
|
|
bBoxTitle.attr("width", boundingBox.width + 10);
|
|
},
|
|
|
|
getObserver = function() {
|
|
if (!observer) {
|
|
var factory = new DomObserverFactory();
|
|
observer = factory.createObserver(function(e){
|
|
if (_.any(e, function(obj) {
|
|
return obj.attributeName === "transform";
|
|
})) {
|
|
updateBoundingBox();
|
|
observer.disconnect();
|
|
}
|
|
});
|
|
}
|
|
return observer;
|
|
},
|
|
|
|
updateNodeArray = function() {
|
|
layouter.stop();
|
|
nodeArray.length = 0;
|
|
_.each(nodes, function(v) {
|
|
nodeArray.push(v);
|
|
});
|
|
layouter.start();
|
|
},
|
|
|
|
updateEdgeArray = function() {
|
|
layouter.stop();
|
|
intEdgeArray.length = 0;
|
|
_.each(internal, function(e) {
|
|
intEdgeArray.push(e);
|
|
});
|
|
layouter.start();
|
|
},
|
|
|
|
toArray = function(obj) {
|
|
var res = [];
|
|
_.each(obj, function(v) {
|
|
res.push(v);
|
|
});
|
|
return res;
|
|
},
|
|
|
|
hasNode = function(id) {
|
|
return !!nodes[id];
|
|
},
|
|
|
|
getNodes = function() {
|
|
return nodeArray;
|
|
},
|
|
|
|
getNode = function(id) {
|
|
return nodes[id];
|
|
},
|
|
|
|
insertNode = function(n) {
|
|
nodes[n._id] = n;
|
|
updateNodeArray();
|
|
self._size++;
|
|
},
|
|
|
|
insertInitialNodes = function(ns) {
|
|
_.each(ns, function(n) {
|
|
nodes[n._id] = n;
|
|
self._size++;
|
|
});
|
|
updateNodeArray();
|
|
},
|
|
|
|
removeNode = function(n) {
|
|
var id = n._id || n;
|
|
delete nodes[id];
|
|
updateNodeArray();
|
|
self._size--;
|
|
},
|
|
|
|
removeInboundEdge = function(e) {
|
|
var id;
|
|
if (!_.has(e, "_id")) {
|
|
id = e;
|
|
e = internal[id] || inbound[id];
|
|
} else {
|
|
id = e._id;
|
|
}
|
|
e.target = e._target;
|
|
delete e._target;
|
|
if (internal[id]) {
|
|
delete internal[id];
|
|
self._outboundCounter++;
|
|
outbound[id] = e;
|
|
updateEdgeArray();
|
|
return;
|
|
}
|
|
delete inbound[id];
|
|
self._inboundCounter--;
|
|
return;
|
|
},
|
|
|
|
removeOutboundEdge = function(e) {
|
|
var id;
|
|
if (!_.has(e, "_id")) {
|
|
id = e;
|
|
e = internal[id] || outbound[id];
|
|
} else {
|
|
id = e._id;
|
|
}
|
|
e.source = e._source;
|
|
delete e._source;
|
|
delete outReferences[e.source._id][id];
|
|
if (internal[id]) {
|
|
delete internal[id];
|
|
self._inboundCounter++;
|
|
inbound[id] = e;
|
|
updateEdgeArray();
|
|
return;
|
|
}
|
|
delete outbound[id];
|
|
self._outboundCounter--;
|
|
return;
|
|
},
|
|
|
|
removeOutboundEdgesFromNode = function(n) {
|
|
var id = n._id || n,
|
|
res = [];
|
|
_.each(outReferences[id], function(e) {
|
|
removeOutboundEdge(e);
|
|
res.push(e);
|
|
});
|
|
delete outReferences[id];
|
|
return res;
|
|
},
|
|
|
|
insertInboundEdge = function(e) {
|
|
e._target = e.target;
|
|
e.target = self;
|
|
if (outbound[e._id]) {
|
|
delete outbound[e._id];
|
|
self._outboundCounter--;
|
|
internal[e._id] = e;
|
|
updateEdgeArray();
|
|
return true;
|
|
}
|
|
inbound[e._id] = e;
|
|
self._inboundCounter++;
|
|
return false;
|
|
},
|
|
|
|
insertOutboundEdge = function(e) {
|
|
var sId = e.source._id;
|
|
e._source = e.source;
|
|
e.source = self;
|
|
outReferences[sId] = outReferences[sId] || {};
|
|
outReferences[sId][e._id] = e;
|
|
if (inbound[e._id]) {
|
|
delete inbound[e._id];
|
|
self._inboundCounter--;
|
|
internal[e._id] = e;
|
|
updateEdgeArray();
|
|
return true;
|
|
}
|
|
self._outboundCounter++;
|
|
outbound[e._id] = e;
|
|
return false;
|
|
},
|
|
|
|
getDissolveInfo = function() {
|
|
return {
|
|
nodes: nodeArray,
|
|
edges: {
|
|
both: intEdgeArray,
|
|
inbound: toArray(inbound),
|
|
outbound: toArray(outbound)
|
|
}
|
|
};
|
|
},
|
|
|
|
expand = function() {
|
|
this._expanded = true;
|
|
},
|
|
|
|
dissolve = function() {
|
|
parent.dissolveCommunity(self);
|
|
},
|
|
|
|
collapse = function() {
|
|
this._expanded = false;
|
|
},
|
|
|
|
addCollapsedLabel = function(g, colourMapper) {
|
|
var width = g.select("rect").attr("width"),
|
|
textN = g.append("text") // Append a label for the node
|
|
.attr("text-anchor", "middle") // Define text-anchor
|
|
.attr("fill", colourMapper.getForegroundCommunityColour())
|
|
.attr("stroke", "none"); // Make it readable
|
|
width *= 2;
|
|
width /= 3;
|
|
if (self._reason && self._reason.key) {
|
|
textN.append("tspan")
|
|
.attr("x", "0")
|
|
.attr("dy", "-4")
|
|
.text(self._reason.key + ":");
|
|
textN.append("tspan")
|
|
.attr("x", "0")
|
|
.attr("dy", "16")
|
|
.text(self._reason.value);
|
|
}
|
|
textN.append("tspan")
|
|
.attr("x", width)
|
|
.attr("y", "0")
|
|
.attr("fill", colourMapper.getCommunityColour())
|
|
.text(self._size);
|
|
},
|
|
|
|
addCollapsedShape = function(g, shapeFunc, start, colourMapper) {
|
|
var inner = g.append("g")
|
|
.attr("stroke", colourMapper.getForegroundCommunityColour())
|
|
.attr("fill", colourMapper.getCommunityColour());
|
|
shapeFunc(inner, 9);
|
|
shapeFunc(inner, 6);
|
|
shapeFunc(inner, 3);
|
|
shapeFunc(inner);
|
|
inner.on("click", function() {
|
|
self.expand();
|
|
parent.checkNodeLimit(self);
|
|
start();
|
|
});
|
|
addCollapsedLabel(inner, colourMapper);
|
|
},
|
|
|
|
addNodeShapes = function(g, shapeQue) {
|
|
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();
|
|
shapeQue(interior);
|
|
},
|
|
|
|
addBoundingBox = function(g, start) {
|
|
bBox = g.append("g");
|
|
bBoxBorder = bBox.append("rect")
|
|
.attr("rx", "8")
|
|
.attr("ry", "8")
|
|
.attr("fill", "none")
|
|
.attr("stroke", "black");
|
|
bBoxTitle = bBox.append("rect")
|
|
.attr("rx", "8")
|
|
.attr("ry", "8")
|
|
.attr("height", "20")
|
|
.attr("fill", "#686766")
|
|
.attr("stroke", "none");
|
|
var dissolveBtn = bBox.append("image")
|
|
.attr("id", self._id + "_dissolve")
|
|
.attr("xlink:href", "img/icon_delete.png")
|
|
.attr("width", "16")
|
|
.attr("height", "16")
|
|
.attr("x", "5")
|
|
.attr("y", "2")
|
|
.attr("style", "cursor:pointer")
|
|
.on("click", function() {
|
|
self.dissolve();
|
|
start();
|
|
}),
|
|
collapseBtn = bBox.append("image")
|
|
.attr("id", self._id + "_collapse")
|
|
.attr("xlink:href", "img/gv_collapse.png")
|
|
.attr("width", "16")
|
|
.attr("height", "16")
|
|
.attr("x", "25")
|
|
.attr("y", "2")
|
|
.attr("style", "cursor:pointer")
|
|
.on("click", function() {
|
|
self.collapse();
|
|
start();
|
|
}),
|
|
title = bBox.append("text")
|
|
.attr("x", "45")
|
|
.attr("y", "15")
|
|
.attr("fill", "white")
|
|
.attr("stroke", "none")
|
|
.attr("text-anchor", "left");
|
|
if (self._reason) {
|
|
title.text(self._reason.text);
|
|
}
|
|
getObserver().observe(document.getElementById(self._id), {
|
|
subtree:true,
|
|
attributes:true
|
|
});
|
|
},
|
|
|
|
addDistortion = function(distFunc) {
|
|
if (self._expanded) {
|
|
var oldFocus = distFunc.focus(),
|
|
newFocus = [
|
|
oldFocus[0] - self.position.x,
|
|
oldFocus[1] - self.position.y
|
|
];
|
|
distFunc.focus(newFocus);
|
|
_.each(nodeArray, function(n) {
|
|
n.position = distFunc(n);
|
|
n.position.x /= self.position.z;
|
|
n.position.y /= self.position.z;
|
|
n.position.z /= self.position.z;
|
|
});
|
|
distFunc.focus(oldFocus);
|
|
}
|
|
},
|
|
|
|
shapeAll = function(g, shapeFunc, shapeQue, start, colourMapper) {
|
|
// First unbind all click events that are proably still bound
|
|
g.on("click", null);
|
|
if (self._expanded) {
|
|
addBoundingBox(g, start);
|
|
addNodeShapes(g, shapeQue, start, colourMapper);
|
|
return;
|
|
}
|
|
addCollapsedShape(g, shapeFunc, start, colourMapper);
|
|
},
|
|
|
|
updateEdges = function(g, addPosition, addUpdate) {
|
|
if (self._expanded) {
|
|
var interior = g.selectAll(".link"),
|
|
line = interior.select("line");
|
|
addPosition(line, interior);
|
|
addUpdate(interior);
|
|
}
|
|
},
|
|
|
|
shapeEdges = function(g, addQue) {
|
|
var idFunction = function(d) {
|
|
return d._id;
|
|
},
|
|
line,
|
|
interior;
|
|
if (self._expanded) {
|
|
interior = g
|
|
.selectAll(".link")
|
|
.data(intEdgeArray, idFunction);
|
|
// Append the group and class to all new
|
|
interior.enter()
|
|
.append("g")
|
|
.attr("class", "link") // link is CSS class that might be edited
|
|
.attr("id", idFunction);
|
|
// Remove all old
|
|
interior.exit().remove();
|
|
// Remove all elements that are still included.
|
|
interior.selectAll("* > *").remove();
|
|
line = interior.append("line");
|
|
addQue(line, interior);
|
|
}
|
|
},
|
|
|
|
collapseNode = function(n) {
|
|
removeOutboundEdgesFromNode(n);
|
|
};
|
|
|
|
////////////////////////////////////
|
|
// Setup //
|
|
////////////////////////////////////
|
|
|
|
layouter = new ForceLayouter({
|
|
distance: 100,
|
|
gravity: 0.1,
|
|
charge: -500,
|
|
width: 1,
|
|
height: 1,
|
|
nodes: nodeArray,
|
|
links: intEdgeArray
|
|
});
|
|
|
|
////////////////////////////////////
|
|
// Values required for shaping //
|
|
////////////////////////////////////
|
|
this._id = "*community_" + Math.floor(Math.random()* 1000000);
|
|
if (initial.length > 0) {
|
|
this.x = initial[0].x;
|
|
this.y = initial[0].y;
|
|
} else {
|
|
this.x = 0;
|
|
this.y = 0;
|
|
}
|
|
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;
|
|
|
|
insertInitialNodes(initial);
|
|
|
|
////////////////////////////////////
|
|
// Public functions //
|
|
////////////////////////////////////
|
|
|
|
this.hasNode = hasNode;
|
|
this.getNodes = getNodes;
|
|
this.getNode = getNode;
|
|
this.getDistance = getDistance;
|
|
this.getCharge = getCharge;
|
|
|
|
|
|
this.insertNode = insertNode;
|
|
this.insertInboundEdge = insertInboundEdge;
|
|
this.insertOutboundEdge = insertOutboundEdge;
|
|
|
|
this.removeNode = removeNode;
|
|
this.removeInboundEdge = removeInboundEdge;
|
|
this.removeOutboundEdge = removeOutboundEdge;
|
|
this.removeOutboundEdgesFromNode = removeOutboundEdgesFromNode;
|
|
|
|
|
|
this.collapseNode = collapseNode;
|
|
|
|
this.dissolve = dissolve;
|
|
this.getDissolveInfo = getDissolveInfo;
|
|
|
|
this.collapse = collapse;
|
|
this.expand = expand;
|
|
|
|
this.shapeNodes = shapeAll;
|
|
this.shapeInnerEdges = shapeEdges;
|
|
this.updateInnerEdges = updateEdges;
|
|
|
|
|
|
this.addDistortion = addDistortion;
|
|
|
|
this.getSourcePosition = getSourcePosition;
|
|
|
|
this.getTargetPosition = getTargetPosition;
|
|
}
|