
web-data.js.graph-drawing.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of signal-collect_2.11 Show documentation
Show all versions of signal-collect_2.11 Show documentation
A framework for parallel and distributed graph processing.
The newest version!
/**
* @author Carol Alexandru
*
* Copyright 2013 University of Zurich
*
* Licensed below 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 below 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 below the License.
*/
/**
* The GraphD3 class is responsible for drawing and modifying the graph on the
* graph canvas. It provides functionality to add and remove vertices.
* @constructor
* @param {object} graphModule the graph module instance using this GraphD3
*/
scc.lib.graph.GraphD3 = function (graphModule) {
// Object-scope variables
var graphD3 = this;
var svg, force;
var color = d3.scale.category20();
var colorCategories = d3.scale.ordinal()
.domain(["n", "v"])
.range(["#cc0000", "#00cc00"]);
var scale = d3.scale.linear()
.range([5, 25])
.clamp(true);
var vertices = [];
var edges = [];
var vertexRefs = {};
var edgeRefs = {};
var svgVertices;
var svgEdges;
var gradientDomain = [null,null];
var zoomLevel = 1;
var hoveringOverVertex = undefined;
var fadeTooltipTimeout;
var hideBackgroundTimeout;
var vicinityAutoLoadDelay;
var vertexSequence = 0;
var vertexSequenceEnd = 0;
var GSTR = scc.STR["Graph"];
var signalThreshold = 0.01;
var collectThreshold = 0.0;
this.exposedVertexId = undefined;
/**
* The VertexStorageAgent provides methods for setting, getting and adding to the
* local storage which contains an array of vertexIds as strings. It represents
* the vertices which the user has loaded into the canvas.
* @constructor
*/
var VertexStorageAgent = function () {
// Initialize the localStorage if necessary
if (localStorage["vertexIds"] === undefined || localStorage["vertexIds"] === "") {
localStorage["vertexIds"] = "[]";
}
/**
* Adds the given vertices to the local storage.
* @param {array} vertexIds - The vertices to be added to the storage
*/
this.push = function (vertexIds) {
var stored = this.get()
$.each(stored, function (key, value) {
if (vertexIds.indexOf(value) !== -1) { vertexIds.splice(key, 1); }
});
stored.push.apply(stored, vertexIds)
localStorage["vertexIds"] = JSON.stringify(stored);
};
/**
* Replaces the existing list stored with the one passed to the function
* @param {array} vertexIds - The vertices to be stored
*/
this.save = function () {
localStorage["vertexIds"] = JSON.stringify(
$.map(vertices, function (vertex, i) { return vertex.id; })
);
};
/**
* Loads the list of vertices from the local storage
*/
this.get = function () {
return JSON.parse(localStorage["vertexIds"]);
};
};
// Instantiate an agent for us to use
this.vertexStorage = new VertexStorageAgent();
/**
* Returns a d3 scale that that maps the domain passed to the function
* to a blue-to-red color scale.
* @param {array} domain - A three-element array containing the
* lowest, median and highest values of the input domain
* @return {object} - The d3 color scale for this input domain
*/
var colorGradient = function (domain) {
var scale = d3.scale.linear()
.domain(domain)
.range(["blue", "green", "red"]);
return scale;
};
/**
* Returns a d3 scale that that maps the domain passed to the function
* to a green-to-red color scale.
* @param {array} domain - A three-element array containing the
* lowest, median and highest values of the input domain
* @return {object} - The d3 color scale for this input domain
*/
var sizeGradient = function (domain) {
var scale = d3.scale.linear()
.domain(domain)
.range([4, 10, 35]);
return scale;
};
/**
* An object containing the necessary transformation functions that can be
* used to style the graph.
*/
var vertexDesign = {
// functions returning a color
"gd_vertexColor": {
"Vertex type": function(d) {
return color(d.t); },
"Vertex state": function(d) {
return colorGradient(gradientDomain)(d.r); },
"Vertex id": function(d) {
return color(d.id); },
"Latest query": function(d) {
return d.seq > vertexSequenceEnd?"#00ff00":"#ff0000"; },
"All equal": function(d) {
return "#17becf"; },
"Outgoing degree": function(d) {
return colorGradient([1, 5, 50])(d.es); },
"Signal threshold": function(d) {
return d.ss > signalThreshold?"#00ff00":"#ff0000"; },
"Collect threshold": function(d) {
return d.cs > collectThreshold?"#00ff00":"#ff0000"; }
},
"gd_vertexBorder": {
"Vertex type": function(d) {
return color(d.t); },
"Vertex state": function(d) {
return colorGradient(gradientDomain)(d.r); },
"Vertex id": function(d) {
return color(d.id); },
"Outgoing degree": function(d) {
return colorGradient([1, 5, 50])(d.es); },
"Signal threshold": function(d) {
return d.ss > signalThreshold?"#00ff00":"#ff0000"; },
"Collect threshold": function(d) {
return d.cs > collectThreshold?"#00ff00":"#ff0000"; },
"All equal": function(d) {
return "#9edae5"; },
"Latest query": function(d) {
return d.seq > vertexSequenceEnd?"#00ff00":"#ff0000"; }
},
// functions returning a radius
"gd_vertexSize": {
"Vertex state": function(d) {
return sizeGradient(gradientDomain)(d.r); },
"All equal": function(d) {
return 5; }
}
};
/**
* Set the color of vertices in the graph. Providing a string, all vertices will
* have the same color. If a function is provided, it will be run for each
* vertex individually, resulting in different colors for different vertices.
* @param {string|function} s - The color or a function returning a color
*/
this.setVertexColor = function (s) {
vertexColor = s;
svgVertices.transition().style("fill", s);
};
/**
* The default vertex color
*/
var vertexColor = vertexDesign["gd_vertexColor"]["Vertex state"];
/**
* Set the color of vertex borders (outlines) in the graph.
* @see setVertexColor
* @param {string|function} s - The color or a function returning a color
*/
this.setVertexBorder = function (s) {
vertexBorder = s;
svgVertices.transition().style("stroke", s);
};
/**
* The default vertex border color
*/
var vertexBorder = vertexDesign["gd_vertexBorder"]["Vertex id"];
/**
* Set the radius of vertices in the graph.
* @see setVertexColor
* @param {int|function} s - The radius or a function returning a radius
*/
this.setVertexSize = function (s) {
vertexSize = s;
svgVertices.transition().attr("r", s);
};
/**
* The default vertex radius
*/
var vertexSize = vertexDesign["gd_vertexSize"]["Vertex state"];
/**
* Re-apply the current styling options to all vertices. This function is
* used when vertices have been styled abnormally, for example because they
* have been selected using the mouse.
*/
this.resetVertexStyle = function () {
svgVertices.style("fill", vertexColor)
.style("stroke", vertexBorder)
.attr("r", vertexSize);
};
/**
* Choose under what circumstances edges are drawn.
*/
this.setEdgeDrawing = function (setting) {
switch (setting) {
case "Always":
case "When graph is still":
svgEdges.attr("class", "edge"); break;
case "Only on hover":
svgEdges.attr("class", "edge hiddenOpacity"); break;
}
};
/**
* Function that is called by the Graph module when a new WebSocket connection
* is established. Create the SVG element and prepare the graph drawing
* functionality.
* @param {Event} e - The event that triggered the call
*/
this.onopen = function(e) {
$("#graph_background").text("Loading...");
// Add an SVG element to the canvas and enable the d3 zoom functionality
svg = d3.select("#graph_canvas").append("svg")
.attr("width", "100%")
.attr("height", "100%")
.call(d3.behavior.zoom().on("zoom", function () {
if (zoomLevel === d3.event.scale) {
svg.attr("transform",
"translate(" + d3.event.translate + ")" +
" scale(" + zoomLevel + ")")
}
else {
zoomLevel = d3.event.scale;
svg.transition().duration(200).attr("transform",
"translate(" + d3.event.translate + ")" +
" scale(" + zoomLevel + ")")
}
})).append("svg:g");
// Disable double-click zooming, because we need double clicks to expand
// the vicinity of a vertex. Zooming remains possible using the mouse wheel.
d3.select("#graph_canvas > svg").on("dblclick.zoom", null);
// When double clicking a vertex, load the vicinity of the vertex
d3.select("#graph").on("dblclick", function (e) {
if (d3.event.target.tagName === "circle") {
var target = d3.event.target;
var data = target.__data__;
graphModule.order({"provider": "graph",
"query": "vertexIds",
"vertexIds": [data.id],
"vicinityIncoming": ($("#gp_vicinityIncoming").val() === "Yes"),
"vicinityRadius": parseInt($("#gp_vicinityRadius").val())
});
}
});
/**
* Fill the tooltip with information from a data object
* @param {object} data - The data to use.
*/
var fillTooltip = function (data) {
$("#vertex_id").text(data.id);
$("#vertex_type").text(data.t);
$("#vertex_state").text(data.state);
$("#vertex_ss").text(data.ss);
$("#vertex_cs").text(data.cs);
};
/**
* Handler for when the user clicks on a node to load its exposed
* information.
* @param {Event} e - The event that triggered the call
*/
d3.select("#graph").on("click.exposeVertex", function (e) {
if ($("#exposition").is(":visible")) {
var target = d3.event.target;
var data = target.__data__;
if (target.tagName === "circle") {
var exposedVertexId = data.id;
localStorage["exposedVertexId"] = exposedVertexId;
$("#exposition_background").text("");
graphModule.expose(data);
}
}
});
/**
* Handler for when the user hovers over the graph. Shows a tooltip when
* hovering over a vertex.
* @param {Event} e - The event that triggered the call
*/
d3.select("#graph").on("mousemove", function (e) {
var coords = d3.mouse(this);
var target = d3.event.target;
var data = target.__data__;
var vertex = $(target);
var drawEdges = scc.settings.get().graph.options["gp_drawEdges"];
var tooltip = $("#graph_tooltip");
// When over a vertex, show the tooltip, highlight its edges and hide all
// other edges in the graph
if (target.tagName === "circle") {
$("#graph_tooltip").css({"left": coords[0]+5 + "px", "top": coords[1]+5 + "px"});
hoveringOverVertex = data.id;
fillTooltip(data);
clearTimeout(fadeTooltipTimeout);
tooltip.fadeIn(200);
svgEdges.attr("class", function(o) {
if (o.source.id === data.id) { return "edge outgoing"; }
if (o.target.id === data.id) { return "edge"; }
return "edge hiddenOpacity";
});
if (scc.settings.get().graph.options["gs_autoAddVicinities"] === "Yes") {
clearTimeout(vicinityAutoLoadDelay);
var target = d3.event.target;
var data = target.__data__;
vicinityAutoLoadDelay = setTimeout(function () {
graphModule.order({"provider": "graph",
"query": "vertexIds",
"vertexIds": [data.id],
"vicinityIncoming": ($("#gp_vicinityIncoming").val() === "Yes"),
"vicinityRadius": parseInt($("#gp_vicinityRadius").val())
});
}, 150);
}
}
// Otherwise, clear the tooltip and set a timeout to hide it soon
else {
hoveringOverVertex = undefined;
tooltip.css({"left": coords[0]+5 + "px", "top": coords[1]+5 + "px"});
fillTooltip({"id": "-", "state": "-", "ss": "-", "cs": "-", "t": "-"});
clearTimeout(fadeTooltipTimeout);
clearTimeout(vicinityAutoLoadDelay);
fadeTooltipTimeout = setTimeout(function() {
tooltip.fadeOut(200);
}, 500);
if (drawEdges === "Only on hover") {
svgEdges.attr("class", "edge hiddenOpacity");
}
else {
svgEdges.attr("class", "edge");
}
}
});
// Enable d3's forced directed graph layout
force = d3.layout.force()
.size([$("#graph_canvas").width(), $("#graph_canvas").height()])
.nodes(vertices)
.links(edges)
.friction(0.4)
.linkDistance(30)
.charge(function (d) {
var weight = 2;
if (d.weight > weight) { weight = d.weight; }
return Math.max(vertexSize(d)*-40*weight, -10000)
})
svgEdges = svg.append('svg:g').selectAll(".edge");
svgVertices = svg.append('svg:g').selectAll(".vertex");
// apply graph design options from the settings
$.each(scc.settings.get().graph.options, function (key, value) {
graphD3.setGraphDesign(key, value);
});
/**
* Handler on d3's force layout. This handler may be called several times
* per second and as such causes the fluid animation to occur. On each
* 'tick', the vertex positions need to be updated.
* @param {Event} e - The event that triggered the call
*/
force.on("tick", function(e) {
// The user may choose if the graph edges should be drawn always, never,
// or only when the graph is moving only very little or not at all. The
// amount of movement is expressed by d3 through the .alpha() property.
// Update the vertex and edge positions
svgVertices
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
svgEdges
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
// Add classes to edges depending on options and user interaction
var drawEdges = scc.settings.get().graph.options["gp_drawEdges"];
svgEdges.attr("class", function(o) {
// If the user is hovering over a vertex, only draw edges of that vertex
if (hoveringOverVertex) {
if (o.source.id === hoveringOverVertex) { return "edge outgoing"; }
if (o.target.id === hoveringOverVertex) { return "edge"; }
return "edge hiddenOpacity";
}
// Else draw vertices depending on the drawEdges setting
else {
if (drawEdges === "Always" ||
(drawEdges === "When graph is still" &&
force.alpha() < 0.05)) { return "edge"; }
else { return "edge hiddenOpacity"; }
}
});
});
};
/**
* Function that is called by the Graph module when a message is received
* from the WebSocket. Reads the graph data received and updates the graph
* accordingly.
* @param {object} j - The message object received from the server
*/
this.onmessage = function(j) {
if (j.provider === "configuration") {
if (j.executionConfiguration !== "unknown") {
signalThreshold = j.executionConfiguration.signalThreshold
collectThreshold = j.executionConfiguration.collectThreshold
}
return;
}
// Keep references to the forced layout data
var newVertices = false;
// If the server sent an empty graph, do nothing
if (j.vertices === undefined) {
$("#graph_background").text("There are no vertices matching your request");
hideBackgroundTimeout = setTimeout(function () {
if (vertices.length === 0) {
$("#graph_background").text(GSTR["canvasEmpty"]);
}
}, 2000);
return;
}
// The server sends us two maps, one for vertices and one for edges. In both,
// the vertices are identifies using strings. For example, given the following
// vertex map...
//
// {"1111":{ },
// "2222":{ },
// "3333":{ }}
//
// and the following edge map...
//
// {"1111":["2222","3333"],
// "2222":["3333"]}
//
// we'd had received a graph that looks like a triangle.
//
// d3 needs a different representation for the graph. It keeps a list of
// vertices, not a map, and as such every vertex is only identifiable by its
// index in the list. This is not sufficient for our purpose, since we
// want to be able to update existing vertices without having to redraw the
// whole graph but we have no way of mapping the actual vertex id to the
// index used by d3. For this reason, we keep a lookup table, 'vertexRefs',
// which keeps the (vertexId -> index) mapping. The same thing happens for
// the edges. For example given the graph above, the resulting maps could be:
//
// vertexRefs["1111"] = 0
// vertexRefs["2222"] = 1
// vertexRefs["3333"] = 2
// edgeRefs["1111-2222"] = 0
// edgeRefs["1111-3333"] = 1
// edgeRefs["2222-3333"] = 3
//
// This way, we can later update vertex states, for example by re-assigning
// to vertices[vertexRefs["1111"]], thereby updating the correct vertex in d3's
// vertex array.
// If there's a vertex limit, then check if we need to remove some vertices
// before there's enough space for the new ones
var maxVertexCount = parseInt(scc.settings.get().graph.options["gp_maxVertexCount"]);
vertexSequenceEnd = vertexSequence;
var newVertexCount = 0;
var tooManyVertices = 0;
$.each(j.vertices, function(id, data) {
newVertexCount += 1;
// If there are more vertices in the data than we're allowed to draw,
// then remove the remaining elements from the vertexRefs if necessary
if (newVertexCount > maxVertexCount) {
vertexRefs[id] = undefined;
tooManyVertices += 1;
return;
}
vertexSequence += 1;
var radius = data.s.replace(/[^0-9.,]/g, '')
if (radius === "NaN" || radius === "") { radius = 1; }
if (isNaN(radius)) { radius = 1; }
if (vertexRefs[id] === undefined) {
// The vertex hasn't existed yet. Update d3's vertex array
vertices.push({"id": id, "state": data.s, "seq": vertexSequence,
"es": data.es, "ss": data.ss, "cs": data.cs,
"info": data.info, "r": radius, "t": data.t});
// Store the index of the vertex in the lookup table
vertexRefs[id] = vertices.length - 1;
newVertices = true;
}
else {
// Look up the vertex with this id in d3's vertex array and update it
vertices[vertexRefs[id]].state = data.s;
vertices[vertexRefs[id]].seq = vertexSequence;
vertices[vertexRefs[id]].ss = data.ss;
vertices[vertexRefs[id]].cs = data.cs;
vertices[vertexRefs[id]].r = radius;
vertices[vertexRefs[id]].t = data.t;
vertices[vertexRefs[id]].info = data.info;
}
});
var verticesToRemove = getOverflowingVertices()
graphD3.vertexStorage.save();
// Determine maximum and minimum state to determine color gradient
var median = d3.median(vertices, function (d) { return d.r });
var lowest = parseFloat(j.lowestState)
var highest = parseFloat(j.highestState)
if (lowest > median) { lowest = median; }
if (highest < median) { highest = median; }
gradientDomain = [lowest, median, highest]
if (j.edges) {
$.each(j.edges, function (source, targets) {
for (var t = 0; t < targets.length; t++) {
var edgeId = source + "-" + targets[t];
if (edgeRefs[edgeId] === undefined) {
// The edge hasn't existed yet. Update d3's edge array
if (vertexRefs[source] !== undefined &&
vertexRefs[targets[t]] !== undefined) {
edges.push({"id": edgeId,
"source": vertices[vertexRefs[source]],
"target": vertices[vertexRefs[targets[t]]]});
// Store the index of the edge in the lookup table
edgeRefs[edgeId] = edges.length - 1;
}
}
else {
// One could update d3's edge array here, like with the vertices
}
}
});
}
if (verticesToRemove.length !== 0) {
graphD3.removeVerticesFromCanvas(verticesToRemove);
}
else {
restart(newVertices);
}
if (tooManyVertices > 0) {
$("#graph_background").stop().text(
"The query yields " + (tooManyVertices + vertices.length) + " vertices. Only the first " +
maxVertexCount + " are being displayed.");
hideBackgroundTimeout = setTimeout(function () {
$("#graph_background").text("Showing " + vertices.length + " vertices");
}, 4000);
}
// Update exposed vertex
if (typeof graphD3.exposedVertexId === "string") {
var exposedVertex = svgVertices.filter(function (d, i) {
return d.id === graphD3.exposedVertexId;
})[0][0];
if (exposedVertex !== undefined) {
var data = exposedVertex.__data__;
$("#exposition_background").text("");
graphModule.expose(data);
}
}
// Order new graph if autorefresh is enabled
if (scc.consumers.Graph.autoRefresh) {
if ($("#gp_refreshRate").val() !== "Never") {
scc.consumers.Graph.update(parseInt($("#gp_refreshRate").val())*1000);
}
}
};
/**
* Function that is called by the Graph module when a new WebSocket connection
* breaks down.
* @param {Event} e - The event that triggered the call
*/
this.onclose = function(e) {
this.destroy();
$("#graph_background").text("Connection lost");
};
/**
* Clear graph canvas and then re-initialize graph (calling onopen).
*/
this.reset = function() {
this.destroy();
scc.consumers.Graph.onopen();
};
/**
* Clear graph canvas and cancel all pending orders. Clear data variables.
*/
this.destroy = function() {
scc.resetOrders("graph");
$("#graph_canvas").empty();
vertices = [];
edges = [];
vertexRefs = {};
edgeRefs = {};
};
/**
* Calculate which are the 'oldest' vertices in the sequence that do not
* fit onto the canvas given current vertex count limitations.
* @return {array
© 2015 - 2025 Weber Informatics LLC | Privacy Policy