http://git-wip-us.apache.org/repos/asf/giraph/blob/8675c84a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/editor.core.js ---------------------------------------------------------------------- diff --git a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/editor.core.js b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/editor.core.js new file mode 100644 index 0000000..2536e3b --- /dev/null +++ b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/editor.core.js @@ -0,0 +1,634 @@ +/* + * Graph Editor is based on Directed Graph Editor by rkirsling http://bl.ocks.org/rkirsling/5001347. + */ + +/* + * Editor is a class that encapsulates the graph editing window + * @param {container, [undirected]} options - Initialize editor with these options. + * {options.container} - HTML element that contains the editor svg. + * {options.undirected} - Indicate whether the graph is directed/undirected. + * @constructor + */ +function Editor(options) { + this.container = 'body'; + this.undirected = false; + // Readonly editor does not let users + // add new nodes/links. + this.readonly = false; + // Data for the graph nodes and edges. + this.defaultColor = '#FFFDDB' + // Useful options. Not required by the editor class itself. + this.errorColor = '#FF9494'; + // Maximum number of nodes for which the graph view would be constructed and maintained. + this.graphViewNodeLimit = 2000; + // Graph members + this.nodes = []; + this.links = []; + this.messages = []; + this.currentZoom = { translate : [0,0], scale : 1 }; + // Table members + // Current scenario (adjList) object as received from the server. + this.currentScenario = {}; + // aggregators is a collecton of key-value pairs displayed in the top-right corner. + this.aggregators = {}; + // set graph as the default view + this.view = Editor.ViewEnum.GRAPH; + // linkDistance controls the distance between two nodes in the graph. + this.linkDistance = 150; + if (options) { + this.container = options['container'] ? options['container'] : this.container; + this.undirected = options['undirected'] === true; + if (options.onOpenNode) { + this.onOpenNode = options.onOpenNode; + } + if (options.onOpenEdge) { + this.onOpenEdge = options.onOpenEdge; + } + } + this.setSize(); + this.lastKeyDown = -1; + this.init(); + this.buildSample(); + return this; +} + +/* + * Represents the two views of the editor - tabular and graph + */ +Editor.ViewEnum = { + TABLET : 'tablet', + GRAPH : 'graph' +} + +Editor.prototype.onToggleView = function(toggleViewHandler) { + this.onToggleView.done = toggleViewHandler; +} + +/* + * Build a sample graph with three nodes and two edges. + */ +Editor.prototype.buildSample = function() { + this.empty(); + // Start with a sample graph. + for(var i = 0; i < 3; i++) { + this.addNode(); + } + this.addEdge('1', '2'); + this.addEdge('2', '3'); + this.restartGraph(); +} + +/* + * Empties the graph by deleting all nodes and links. + */ +Editor.prototype.empty = function() { + // NOTE : Don't use this.nodes = [] to empty the array + // This creates a new reference and messes up this.force.nodes + this.nodes.length = 0; + this.links.length = 0; + this.messages.length = 0; + this.numNodes = 0; + this.restartGraph(); +} + +/* + * Initializes the SVG elements, force layout and event bindings. + */ +Editor.prototype.init = function() { + // Initializes the SVG elements. + this.initElements(); + // Binds events and initializes variables used to track selected nodes/links. + this.initEvents(); + // Line displayed when dragging an edge off a node + this.drag_line = this.svg.append('svg:path') + .attr('class', 'link dragline hidden') + .attr('d', 'M0,0L0,0'); + // Handles to link and node element groups. + var pathContainer = this.svg.append('svg:g') + this.path = pathContainer.selectAll('path'); + this.pathLabels = pathContainer.selectAll('text'); + this.circle = this.svg.append('svg:g').selectAll('g'); + // Initializes the force layout. + this.initForce(); + this.restartGraph(); +} + +/* + * Wrapper for restarting both graph and table. Automatically switches to table + * view if the number of nodes is too large. + */ +Editor.prototype.restart = function() { + // If numNodes > graphViewLimit, empty the graph and switch + // to table view. + if (this.numNodes > this.graphViewNodeLimit) { + this.empty(); + if (this.view != Editor.ViewEnum.TABLET) { + this.toggleView(); + } + } + this.restartGraph(); + this.restartTable(); +} + +/* + * Updates the graph. Called internally on various events. + * May be called from the client after updating graph properties. + */ +Editor.prototype.restartGraph = function() { + this.setSize(); + this.restartNodes(); + this.restartLinks(); + this.resizeForce(); + this.restartAggregators(); + + // Set the background to light gray if editor is readonly. + d3.select('.editor').style('background-color', this.readonly ? '#f9f9f9' : 'white'); + this.svgRect.attr('fill', this.readonly ? '#f9f9f9' : 'white') + .attr('width', this.width) + .attr('height', this.height); + + // Set the graph in motion + this.force.start(); +} + +/* + * Handles mousedown event. + * Insert a new node if Shift key is not pressed. Otherwise, drag the graph. + */ +Editor.prototype.mousedown = function() { + if (this.readonly === true) { + return; + } + this.svg.classed('active', true); + if (d3.event.shiftKey || this.mousedown_node || this.mousedown_link) { + return; + } + // Insert new node at point. + var point = d3.mouse(d3.event.target), + node = this.addNode(); + node.x = point[0]; + node.y = point[1]; + this.restartGraph(); +} + +/* + * Returns all the messages sent by node with the given id. + * Output format: {receiverId: message} + * @param {string} id + */ +Editor.prototype.getMessagesSentByNode = function(id) { + var messagesSent = {}; + for (var i = 0; i < this.messages.length; i++) { + var messageObj = this.messages[i]; + if (messageObj.outgoing === true && messageObj.sender.id === id) { + messagesSent[messageObj.receiver.id] = messageObj.message; + } + } + return messagesSent; +} + +/* + * Returns all the edge values for this node's neighbor in a JSON object. + * Note that if an edge value is not present, still returns that neighborId with null/undefined value. + * Output format: {neighborId: edgeValue} + * @param {string} id + */ +Editor.prototype.getEdgeValuesForNode = function(id) { + var edgeValues = {}; + var outgoingEdges = this.getEdgesWithSourceId(id); + $.each(outgoingEdges, function(i, edge) { + edgeValues[edge.target.id] = edge; + }); + return edgeValues; +} + +/* + * Returns all the messages received by node with the given id. + * Output format: {senderId: message} + * @param {string} id + */ +Editor.prototype.getMessagesReceivedByNode = function(id) { + var messagesReceived = {}; + + for (var i = 0; i < this.messages.length; i++) { + var messageObj = this.messages[i]; + if (messageObj.incoming === true && messageObj.receiver.id === id) { + // Note: This is required because incoming messages do not have a sender as of now. + var senderId = '<i data-id="' + i + '"></i>'; + messagesReceived[senderId] = messageObj.message; + } + } + return messagesReceived; +} + +/* + * Returns the edge list. + * Edge list is the representation of the graph as a list of edges. + * An edge is represented as a vertex pair (u,v). + */ +Editor.prototype.getEdgeList = function() { + edgeList = ''; + + for (var i = 0; i < this.links.length; i++) { + var sourceId = this.links[i].source.id; + var targetId = this.links[i].target.id; + + // Right links are source->target. + // Left links are target->source. + if (this.links[i].right) { + edgeList += sourceId + '\t' + targetId + '\n'; + } else { + edgeList += targetId + '\t' + sourceId + '\n'; + } + } + return edgeList; +} + +/* + * Returns the adjacency list. + * Adj list is the representation of the graph as a list of nodes adjacent to + * each node. + */ +Editor.prototype.getAdjList = function() { + adjList = {} + $.each(this.nodes, (function(i, node) { + var id = node.id; + var edges = this.getEdgesWithSourceId(id); + adjList[id] = {adj : edges, vertexValue : node.attrs} + }).bind(this)); + return adjList; +} + +/* + * Returns the list of nodes along with their attributes. + */ +Editor.prototype.getNodeList = function() { + nodeList = ''; + for (var i = 0; i < this.nodes.length; i++){ + nodeList += this.nodes[i].id + '\t' + this.nodes[i].attrs; + nodeList += (i != this.nodes.length - 1 ? '\n' : ''); + } + return nodeList; +} + +/* + * Handle the mousemove event. + * Updates the drag line if mouse is pressed at present. + * Ignores otherwise. + */ +Editor.prototype.mousemove = function() { + if (this.readonly) { + return; + } + // This indicates if the mouse is pressed at present. + if (!this.mousedown_node) { + return; + } + // Update drag line. + this.drag_line.attr('d', 'M' + this.mousedown_node.x + ',' + + this.mousedown_node.y + 'L' + d3.mouse(this.svg[0][0])[0] + ',' + + d3.mouse(this.svg[0][0])[1] + ); + this.restartGraph(); +} + +/* + * Handles the mouseup event. + */ +Editor.prototype.mouseup = function() { + if (this.mousedown_node) { + // hide drag line + this.drag_line + .classed('hidden', true) + .style('marker-end', ''); + } + this.svg.classed('active', false); + // Clear mouse event vars + this.resetMouseVars(); +} + +/* + * Handles keydown event. + * If Key is Shift, drags the graph using the force layout. + * If Key is 'L' or 'R' and link is selected, orients the link likewise. + * If Key is 'R' and node is selected, marks the node as reflexive. + * If Key is 'Delete', deletes the selected node or edge. + */ +Editor.prototype.keydown = function() { + if (this.lastKeyDown !== -1) { + return; + } + this.lastKeyDown = d3.event.keyCode; + + // Shift key was pressed + if (d3.event.shiftKey) { + this.circle.call(this.force.drag); + this.svg.classed('ctrl', true); + } + + if (!this.selected_node && !this.selected_link || this.readonly) { + return; + } + + switch (d3.event.keyCode) { + case 46: // delete + if (this.selected_node) { + this.nodes.splice(this.nodes.indexOf(this.selected_node), 1); + this.spliceLinksForNode(this.selected_node); + } else if (this.selected_link) { + this.links.splice(this.links.indexOf(this.selected_link), 1); + } + this.selected_link = null; + this.selected_node = null; + this.restartGraph(); + break; + case 66: // B + if (this.selected_link) { + // set link direction to both left and right + this.selected_link.left = true; + this.selected_link.right = true; + } + + this.restartGraph(); + break; + case 76: // L + if (this.selected_link) { + // set link direction to left only + this.selected_link.left = true; + this.selected_link.right = false; + } + + this.restartGraph(); + break; + case 82: // R + if (this.selected_node) { + // toggle node reflexivity + this.selected_node.reflexive = !this.selected_node.reflexive; + } else if (this.selected_link) { + // set link direction to right only + this.selected_link.left = false; + this.selected_link.right = true; + } + + this.restartGraph(); + break; + } +} + +/* + * Handles the keyup event. + * Resets lastKeyDown to -1. + * Also resets the drag event binding to null if the key released was Shift. + */ +Editor.prototype.keyup = function() { + this.lastKeyDown = -1; + + // Shift + if (d3.event.keyCode === 16) { + this.circle + .on('mousedown.drag', null) + .on('touchstart.drag', null); + this.svg.classed('ctrl', false); + } +} + +/* + * Builds the graph from adj list by constructing the nodes and links arrays. + * @param {object} adjList - Adjacency list of the graph. attrs and msgs are optional. + * Format: + * { + * nodeId: { + * neighbors : [{ + * neighborId: "neighborId1", + * edgeValue: "edgeValue1" + * }, + * { + * neighborId: "neighborId2", + * edgeValue: "edgeValue2" + * }], + * vertexValue : attrs, + * outgoingMessages : { + * receiverId1: "message1", + * receiverId2: "message2", + * ... + * }, + * incomingMessages : [ "message1", "message2" ] + * enabled : true/false + * } + * } + */ +Editor.prototype.buildGraphFromAdjList = function(adjList) { + this.empty(); + + // Scan every node in adj list to build the nodes array. + for (var nodeId in adjList) { + var node = this.getNodeWithId(nodeId); + if (!node) { + node = this.addNode(nodeId); + } + var adj = adjList[nodeId]['neighbors']; + // For every node in the adj list of this node, + // add the node to this.nodes and add the edge to this.links + for (var i = 0; adj && i < adj.length; i++) { + var adjId = adj[i]['neighborId'].toString(); + var edgeValue = adj[i]['edgeValue']; + var adjNode = this.getNodeWithId(adjId); + if (!adjNode) { + adjNode = this.addNode(adjId); + } + // Add the edge. + this.addEdge(nodeId, adjId, edgeValue); + } + } + this.updateGraphData(adjList); +} + +/* + * Updates scenario properties - node attributes and messages from adj list. + * @param {object} scenario - scenario has the same format as adjList above, + * but with 'adj' ignored. + * **NOTE**: This method assumes the same scenario structure, + * only updates the node attributes and messages exchanged. + */ +Editor.prototype.updateGraphData = function(scenario) { + // Cache the scenario object. Used by tabular view. + this.currentScenario = scenario; + // Clear the messages array. Unlike other fields, messages is cleared and reloaded for every scenario. + this.messages.length = 0; + + // Scan every node in adj list to build the nodes array. + for (var nodeId in scenario) { + var node = this.getNodeWithId(nodeId); + if (scenario[nodeId]['vertexValue']) { + node.attrs = scenario[nodeId]['vertexValue']; + } + if (scenario[nodeId].enabled != undefined) { + node.enabled = scenario[nodeId].enabled; + } + + var outgoingMessages = scenario[nodeId]['outgoingMessages']; + var incomingMessages = scenario[nodeId]['incomingMessages']; + + // Build this.messages + if (outgoingMessages) { + for(var receiverId in outgoingMessages) { + this.messages.push({ + sender: node, + receiver: this.getNodeWithId(receiverId), + message: outgoingMessages[receiverId], + outgoing : true + }); + } + } + + if (incomingMessages) { + for (var i = 0; i < incomingMessages.length; i++) { + var incomingMessage = incomingMessages[i]; + this.messages.push({ + // TODO: sender is not supplied by the server as of now. + sender : null, + receiver: node, + message: incomingMessage, + incoming : true + }); + } + } + + // Update aggregators + // NOTE: Later vertices ovewrite value for a given key + var aggregators = scenario[nodeId]['aggregators']; + for (var key in aggregators) { + this.aggregators[key] = aggregators[key]; + } + } + // Restart the graph and table to show new values. + this.restart(); +} + +/* + * Adds new nodes and links to the graph without changing the existing structure. + * @param {object} - scenario has the same format as above. + * **NOTE** - This method will add news nodes and links without modifying + * the existing structure. For instance, if the passed graph object does + * not have a link, but it already exists in the graph, it will stay. + */ +Editor.prototype.addToGraph = function(scenario) { + for (var nodeId in scenario) { + // If this node is not present in the graph. Add it. + this.addNode(nodeId); + var neighbors = scenario[nodeId]['neighbors']; + // For each neighbor, add the edge. + for (var i = 0 ; i < neighbors.length; i++) { + var neighborId = neighbors[i]['neighborId']; + var edgeValue = neighbors[i]['edgeValue']; + // Add neighbor node if it doesn't exist. + this.addNode(neighborId); + // Addes edge, or ignores if already exists. + this.addEdge(nodeId, neighborId, edgeValue); + } + } +} + +/* + * Shows the preloader and hides all other elements. + */ +Editor.prototype.showPreloader = function() { + this.svg.selectAll('g').transition().style('opacity', 0); + this.preloader.transition().style('opacity', 1); +} + +/* + * Hides the preloader and shows all other elements. + */ +Editor.prototype.hidePreloader = function() { + this.svg.selectAll('g').transition().style('opacity', 1); + this.preloader.transition().style('opacity', 0); + this.restartGraph(); +} + +/* + * Enables the given node. Enabled nodes are shown as opaque. + */ +Editor.prototype.enableNode = function(nodeId) { + this.getNodeWithId(nodeId).enabled = true; +} + +/* + * Disables the given node. + * Disabled nodes are shown as slightly transparent with outgoing messages removed. + */ +Editor.prototype.disableNode = function(nodeId) { + this.getNodeWithId(nodeId).enabled = false; + // Remove the outgoing Messages for this node. + var toSplice = this.messages.filter(function(message) { + return (message.outgoing === true && message.sender.id === nodeId); + }); + + toSplice.map((function(message) { + this.messages.splice(this.messages.indexOf(message), 1); + }).bind(this)); +} + +/* + * Colors the given node ids with the given color. Use this method to uncolor + * all the nodes (reset to default color) by calling colorNodes([], 'random', true); + * @param {array} nodeIds - List of node ids. + * @param {color} color - Color of these nodes. + * @param {bool} [uncolorRest] - Optional parameter to reset the color of other nodes to default. + */ +Editor.prototype.colorNodes = function(nodeIds, color, uncolorRest) { + // Set the color property of each node in this array. restart will reflect changes. + for(var i = 0; i < nodeIds.length; i++) { + var node = this.getNodeWithId(nodeIds[i]); + if (node) { + node.color = color; + } + } + // If uncolorRest is specified + if (uncolorRest) { + for (var i = 0; i < this.nodes.length; i++) { + // Not in nodeIds, uncolor it. + if ($.inArray(this.nodes[i].id, nodeIds) === -1) { + this.nodes[i].color = this.defaultColor; + } + } + } + this.restartGraph(); +} + +/* + * Toggles the two views of the editor by sliding up/down the tablet. + */ +Editor.prototype.toggleView = function() { + if (this.view === Editor.ViewEnum.GRAPH) { + this.view = Editor.ViewEnum.TABLET; + $(this.tablet[0]).slideDown('slow'); + } else { + this.view = Editor.ViewEnum.GRAPH; + $(this.tablet[0]).slideUp('slow'); + } + // Call the handlers registered for toggleView + this.onToggleView.done(this.view); +} + +/* + * Creates graph from a simple adj list of the format given below. + * @param {object} simpleAdjList : A simple adjacency list. + * Format: + * { + * "vertexId1" : [ "neighborId1", "neighborId2" ...], + * "vertexId2" : [ "neighborId1", "neighborId2" ...], + * ... + * } + */ +Editor.prototype.buildGraphFromSimpleAdjList = function(simpleAdjList) { + var scenario = {}; + $.each(simpleAdjList, function(vertexId, neighbors) { + scenario[vertexId] = {} + scenario[vertexId].neighbors = []; + $.each(neighbors, function(index, neighborId) { + scenario[vertexId].neighbors.push({ neighborId : neighborId }); + }); + }); + this.buildGraphFromAdjList(scenario); +}
http://git-wip-us.apache.org/repos/asf/giraph/blob/8675c84a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/editor.utils.js ---------------------------------------------------------------------- diff --git a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/editor.utils.js b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/editor.utils.js new file mode 100644 index 0000000..863a8d9 --- /dev/null +++ b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/editor.utils.js @@ -0,0 +1,791 @@ +/* + * Sets the size of the graph editing window. + * The graph is always centered in the container according to these dimensions. + */ +Editor.prototype.setSize = function() { + this.width = $(this.container).width(); + this.height = $(this.container).height(); +} + +/* + * Resize the force layout. The D3 force layout controls the movement of the + * svg elements within the container. + */ +Editor.prototype.resizeForce = function() { + this.force.size([this.width, this.height]) + .linkDistance(this.linkDistance) + .charge(-500 - (this.linkDistance - 150)*2); +} + +/* + * Returns the detailed view of each row in the table view. + */ +Editor.prototype.getRowDetailsHtml = function(row) { + var outerContainer = $('<div />'); + var navContainer = $('<ul />') + .attr('class', 'nav nav-tabs') + .attr('id', 'tablet-nav') + .appendTo(outerContainer); + navContainer.append($('<li class="active"><a data-toggle="tab" data-name="outgoingMessages">Outgoing Messages</a></li>')); + navContainer.append($('<li><a data-toggle="tab" data-name="incomingMessages">Incoming Messages</a></li>')); + navContainer.append($('<li><a data-toggle="tab" data-name="neighbors">Neighbors</a></li>')); + + var dataContainer = $('<div />') + .attr('class', 'tablet-data-container') + .appendTo(outerContainer); + + return { + 'outerContainer' : outerContainer, + 'dataContainer' : dataContainer, + 'navContainer' : navContainer + }; +} + +Editor.prototype.initTable = function() { + var jqueryTableContainer = $(this.tablet[0]); + var jqueryTable = $('<table id="editor-tablet-table" class="editor-tablet-table table display">' + + '<thead><tr><th></th><th>Vertex ID</th><th>Vertex Value</th><th>Outgoing Msgs</th>' + + '<th>Incoming Msgs</th><th>Neighbors</th></tr></thead></table>'); + jqueryTableContainer.append(jqueryTable); + // Define the table schema and initialize DataTable object. + this.dataTable = $("#editor-tablet-table").DataTable({ + 'columns' : [ + { + 'class' : 'tablet-details-control', + 'orderable' : false, + 'data' : null, + 'defaultContent' : '' + }, + { 'data' : 'vertexId' }, + { 'data' : 'vertexValue' }, + { 'data' : 'outgoingMessages.numOutgoingMessages' }, + { 'data' : 'incomingMessages.numIncomingMessages' }, + { 'data' : 'neighbors.numNeighbors'} + ] + }); +} + +/* + * Zooms the svg element with the given translate and scale factors. + * Use translate = [0,0] and scale = 1 for original zoom level (unzoomed). + */ +Editor.prototype.zoomSvg = function(translate, scale) { + this.currentZoom.translate = translate; + this.currentZoom.scale = scale; + this.svg.attr("transform", "translate(" + translate + ")" + + " scale(" + scale + ")"); +} + +Editor.prototype.redraw = function() { + this.zoomSvg(d3.event.translate, d3.event.scale); +} + +/* + * Initializes the SVG element, along with marker and defs. + */ +Editor.prototype.initElements = function() { + // Create the tabular view and hide it for now. + this.tablet = d3.select(this.container) + .insert('div') + .attr('class', 'editor-tablet') + .style('display', 'none'); + + this.initTable(); + // Creates the main SVG element and appends it to the container as the first child. + // Set the SVG class to 'editor'. + this.svgRoot = d3.select(this.container) + .insert('svg') + this.zoomHolder = this.svgRoot + .attr('class','editor') + .attr('pointer-events', 'all') + .append('svg:g'); + + this.svg = this.zoomHolder.append('svg:g'); + this.svgRect = this.svg.append('svg:rect') + + // Defines end arrow marker for graph links. + this.svg.append('svg:defs') + .append('svg:marker') + .attr('id', 'end-arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 6) + .attr('markerWidth', 3) + .attr('markerHeight', 3) + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('fill', '#000'); + + // Defines start arrow marker for graph links. + this.svgRoot.append('svg:defs') + .append('svg:marker') + .attr('id', 'start-arrow') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 4) + .attr('markerWidth', 3) + .attr('markerHeight', 3) + .attr('orient', 'auto') + .append('svg:path') + .attr('d', 'M10,-5L0,0L10,5') + .attr('fill', '#000'); + // Append the preloader + // Dimensions of the image are 128x128 + var preloaderX = this.width / 2 - 64; + var preloaderY = this.height / 2 - 64; + this.preloader = this.svgRoot.append('svg:g') + .attr('transform', 'translate(' + preloaderX + ',' + preloaderY + ')') + .attr('opacity', 0); + + this.preloader.append('svg:image') + .attr('xlink:href', 'img/preloader.gif') + .attr('width', '128') + .attr('height', '128'); + this.preloader.append('svg:text') + .text('Loading') + .attr('x', '40') + .attr('y', '128'); + // Aggregators + this.aggregatorsContainer = this.svg.append('svg:g'); + this.aggregatorsContainer.append('text') + .attr('class', 'editor-aggregators-heading') + .text('Aggregators'); + // d3 selector for global key-value pairs + this.globs = this.aggregatorsContainer.append('text').selectAll('tspan'); +} + +/* + * Binds the mouse and key events to the appropriate methods. + */ +Editor.prototype.initEvents = function() { + // Mouse event vars - These variables are set (and reset) when the corresponding event occurs. + this.selected_node = null; + this.selected_link = null; + this.mousedown_link = null; + this.mousedown_node = null; + this.mouseup_node = null; + + // Binds mouse down/up/move events on main SVG to appropriate methods. + // Used to create new nodes, create edges and dragging the graph. + this.svg.on('mousedown', this.mousedown.bind(this)) + .on('mousemove', this.mousemove.bind(this)) + .on('mouseup', this.mouseup.bind(this)); + + // Binds Key down/up events on the window to appropriate methods. + d3.select(window) + .on('keydown', this.keydown.bind(this)) + .on('keyup', this.keyup.bind(this)); +} + +/* + * Initializes D3 force layout to update node/link location and orientation. + */ +Editor.prototype.initForce = function() { + this.force = d3.layout.force() + .nodes(this.nodes) + .links(this.links) + .size([this.width, this.height]) + .linkDistance(this.linkDistance) + .charge(-500 ) + .on('tick', this.tick.bind(this)) +} + +/* + * Reset the mouse event variables to null. + */ +Editor.prototype.resetMouseVars = function() { + this.mousedown_node = null; + this.mouseup_node = null; + this.mousedown_link = null; +} + +/* + * Called at a fixed time interval to update the nodes and edge positions. + * Gives the fluid appearance to the editor. + */ +Editor.prototype.tick = function() { + // draw directed edges with proper padding from node centers + this.path.attr('d', function(d) { + var sourcePadding = getPadding(d.source); + var targetPadding = getPadding(d.target); + + var deltaX = d.target.x - d.source.x, + deltaY = d.target.y - d.source.y, + dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY), + normX = deltaX / dist, + normY = deltaY / dist, + sourcePadding = d.left ? sourcePadding[0] : sourcePadding[1], + targetPadding = d.right ? targetPadding[0] : targetPadding[1], + sourceX = d.source.x + (sourcePadding * normX), + sourceY = d.source.y + (sourcePadding * normY), + targetX = d.target.x - (targetPadding * normX), + targetY = d.target.y - (targetPadding * normY); + return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY; + }); + + this.circle.attr('transform', function(d) { + return 'translate(' + d.x + ',' + d.y + ')'; + }); +} + +/* + * Returns the radius of the node. + * Radius is not fixed since nodes with longer identifiers need a bigger circle. + * @param {int} node - Node object whose radius is required. + */ +function getRadius(node) { + // Radius is detemined by multiplyiing the max of length of node ID + // and node value (first attribute) by a factor and adding a constant. + // If node value is not present, only node id length is used. + return 14 + Math.max(node.id.length, getAttrForDisplay(node.attrs).length) * 3; +} + +/* + * Truncates the attribute value so that it fits propertly on the editor node + * without exploding the circle. + */ +function getAttrForDisplay(attr) { + if (attr && attr.length > 11) { + return attr.slice(0, 4) + "..." + attr.slice(attr.length - 4); + } + return attr ? attr : ''; +} + +/* + * Returns the padding of the node. + * Padding is used by edges as an offset from the node center. + * Padding is not fixed since nodes with longer identifiers need bigger circle. + * @param {int} node - Node object whose padding is required. + */ +function getPadding(node) { + // Offset is detemined by multiplyiing the max of length of node ID + // and node value (first attribute) by a factor and adding a constant. + // If node value is not present, only node id length is used. + var nodeOffset = Math.max(node.id.length, getAttrForDisplay(node.attrs).length) * 3; + return [19 + nodeOffset, 12 + nodeOffset]; +} + +/* + * Returns a new node object. + * @param {string} id - Identifier of the node. + */ +Editor.prototype.getNewNode = function(id) { + return {id : id, reflexive : false, attrs : null, x: Math.random(), y: Math.random(), enabled: true, color: this.defaultColor}; +} + +/* + * Returns a new edge object. + * @param {object} source - Object for the source node. + * @param {object) target - Object for the target node. + * @param {object} edgeVal - Any edge value object. + */ +Editor.prototype.getNewEdge = function(source, target, edgeValue) { + return {source: source, target: target, edgeValue: edgeValue}; +} + +/* + * Returns a new link (edge) object from the node IDs of the logical edge. + * @param {string} sourceNodeId - The ID of the source node in the logical edge. + * @param {string} targetNodeId - The ID of the target node in the logical edge. + * @param {string} [edgeValue] - Value associated with the edge. Optional parameter. + * @desc - Logical edge means, "Edge from node with ID x to node with ID y". + * It implicitly captures the direction. However, the link objects have + * the 'left' and 'right' properties to denote direction. Also, source strictly < target. + * Therefore, the source and target may not match that of the logical edge, but the + * direction will compensate for the mismatch. + */ +Editor.prototype.getNewLink = function(sourceNodeId, targetNodeId, edgeValue) { + var source, target, direction, leftValue = null, rightValue = null; + if (sourceNodeId < targetNodeId) { + source = sourceNodeId; + target = targetNodeId; + direction = 'right'; + rightValue = edgeValue; + } else { + source = targetNodeId; + target = sourceNodeId; + direction = 'left'; + leftValue = edgeValue; + } + // Every link has an ID - Added to the SVG element to show edge value as textPath + if (!this.maxLinkId) { + this.maxLinkId = 0; + } + link = {source : this.getNodeWithId(source), target : this.getNodeWithId(target), + id : this.maxLinkId++, leftValue : leftValue, rightValue : rightValue, left : false, right : false}; + link[direction] = true; + return link; +} + +/* + * Returns the logical edge(s) from a link object. + * @param {object} link - Link object. + * This method is required because a single link object may encode + * two edges using the left/right attributes. + */ +Editor.prototype.getEdges = function(link) { + var edges = []; + if (link.left || this.undirected) { + edges.push(this.getNewEdge(link.target, link.source, link.leftValue)); + } + if (link.right || this.undirected) { + edges.push(this.getNewEdge(link.source, link.target, link.rightValue)); + } + return edges; +} + +/* + * Adds a new link object to the links array or updates an existing link. + * @param {string} sourceNodeId - Id of the source node in the logical edge. + * @param {string} targetNodeid - Id of the target node in the logical edge. + * @param {string} [edgeValue] - Value associated with the edge. Optional parameter. + */ +Editor.prototype.addEdge = function(sourceNodeId, targetNodeId, edgeValue) { + // console.log('Adding edge: ' + sourceNodeId + ' -> ' + targetNodeId); + // Get the new link object. + var newLink = this.getNewLink(sourceNodeId, targetNodeId, edgeValue); + // Check if a link with these source and target Ids already exists. + var existingLink = this.links.filter(function(l) { + return (l.source === newLink.source && l.target === newLink.target); + })[0]; + + // Add link to graph (update if exists). + if (existingLink) { + // Set the existingLink directions to true if either + // newLink or existingLink denote the edge. + existingLink.left = existingLink.left || newLink.left; + existingLink.right = existingLink.right || newLink.right; + if (edgeValue != undefined) { + if (sourceNodeId < targetNodeId) { + existingLink.rightValue = edgeValue; + } else { + existingLink.leftValue = edgeValue; + } + } + return existingLink; + } else { + this.links.push(newLink); + return newLink; + } +} + +/* + * Adds node with nodeId to the graph (or ignores if already exists). + * Returns the added (or already existing) node. + * @param [{string}] nodeId - ID of the node to add. If not provided, adds + * a new node with a new nodeId. + * TODO(vikesh): Incremental nodeIds are buggy. May cause conflict. Use unique identifiers. + */ +Editor.prototype.addNode = function(nodeId) { + if (!nodeId) { + nodeId = (this.numNodes + 1).toString(); + } + var newNode = this.getNodeWithId(nodeId); + if (!newNode) { + newNode = this.getNewNode(nodeId); + this.nodes.push(newNode); + this.numNodes++; + } + return newNode; +} + +/* + * Updates existing links and adds new links. + */ +Editor.prototype.restartLinks = function() { + // path (link) group + this.path = this.path.data(this.links); + this.pathLabels = this.pathLabels.data(this.links); + + // Update existing links + this.path.classed('selected', (function(d) { + return d === this.selected_link; + }).bind(this)) + .style('marker-start', (function(d) { + return d.left && !this.undirected ? 'url(#start-arrow)' : ''; + }).bind(this)) + .style('marker-end', (function(d) { + return d.right && !this.undirected ? 'url(#end-arrow)' : ''; + }).bind(this)); + + // Add new links. + // For each link in the bound data but not in elements group, enter() + // selection calls everything that follows once. + // Note that links are stored as source, target where source < target. + // If the link is from source -> target, it's a 'right' link. + // If the link is from target -> source, it's a 'left' link. + // A right link has end marker at the target side. + // A left link has a start marker at the source side. + this.path.enter() + .append('svg:path') + .attr('class', 'link') + .attr('id', function(d) { return d.id }) + .classed('selected', (function(d) { + return d === this.selected_link; + }).bind(this)) + .style('marker-start', (function(d) { + if(d.left && !this.undirected) { + return 'url(#start-arrow)'; + } + return ''; + }).bind(this)) + .style('marker-end', (function(d) { + if(d.right && !this.undirected) { + return 'url(#end-arrow)'; + } + return ''; + }).bind(this)) + .on('mousedown', (function(d) { + // Select link + this.mousedown_link = d; + // If edge was selected with shift key, call the openEdge handler and return. + if (d3.event.shiftKey) { + this.onOpenEdge({ event: d3.event, link: d, editor: this }); + return; + } + if (this.mousedown_link === this.selected_link) { + this.selected_link = null; + } else { + this.selected_link = this.mousedown_link; + } + this.selected_node = null; + this.restartGraph(); + }).bind(this)); + // Add edge value labels for the new edges. + // Note that two tspans are required for + // left and right links (represented by the same 'link' object) + var textPaths = this.pathLabels.enter() + .append('svg:text') + .append('svg:textPath') + .attr('xlink:href', function(d) { return '#' + d.id }) + .attr('startOffset', '35%'); + + textPaths.append('tspan') + .attr('dy', -6) + .attr('data-orientation', 'right') + textPaths.append('tspan') + .attr('dy', 20) + .attr('x', 5) + .attr('data-orientation', 'left') + + // Update the tspans with the edge value + this.pathLabels.selectAll('tspan').text(function(d) { + return $(this).attr('data-orientation') === 'right' + ? ( d.right ? d.rightValue : null ) + : ( d.left ? d.leftValue : null ); + }); + // Remove old links. + this.path.exit().remove(); + this.pathLabels.exit().remove(); +} + +/* + * Adds new nodes to the graph and binds mouse events. + * Assumes that the data for this.circle is already set by the caller. + * Creates 'circle' elements for each new node in this.nodes + */ +Editor.prototype.addNodes = function() { + // Adds new nodes. + // The enter() call appends a 'g' element for each node in this.nodes. + // that is not present in this.circle already. + var g = this.circle.enter().append('svg:g'); + + g.attr('class', 'node-container') + .append('svg:circle') + .attr('class', 'node') + .attr('r', (function(d) { + return getRadius(d); + }).bind(this)) + .style('fill', this.defaultColor) + .style('stroke', '#000000') + .classed('reflexive', function(d) { return d.reflexive; }) + .on('mouseover', (function(d) { + if (!this.mousedown_node || d === this.mousedown_node) { + return; + } + // Enlarge target node. + d3.select(d3.event.target).attr('transform', 'scale(1.1)'); + }).bind(this)) + .on('mouseout', (function(d) { + if (!this.mousedown_node || d === this.mousedown_node) { + return; + } + // Unenlarge target node. + d3.select(d3.event.target).attr('transform', ''); + }).bind(this)) + .on('mousedown', (function(d) { + if (d3.event.shiftKey || this.readonly) { + return; + } + // Select node. + this.mousedown_node = d; + if (this.mousedown_node === this.selected_node) { + this.selected_node = null; + } else { + this.selected_node = this.mousedown_node; + } + this.selected_link = null; + // Reposition drag line. + this.drag_line + .style('marker-end', 'url(#end-arrow)') + .classed('hidden', false) + .attr('d', 'M' + this.mousedown_node.x + ',' + this.mousedown_node.y + 'L' + this.mousedown_node.x + ',' + this.mousedown_node.y); + this.restartGraph(); + }).bind(this)) + .on('mouseup', (function(d) { + if (!this.mousedown_node) { + return; + } + this.drag_line + .classed('hidden', true) + .style('marker-end', ''); + + // Check for drag-to-self. + this.mouseup_node = d; + if (this.mouseup_node === this.mousedown_node) { + this.resetMouseVars(); + return; + } + + // Unenlarge target node to default size. + d3.select(d3.event.target).attr('transform', ''); + + // Add link to graph (update if exists). + var newLink = this.addEdge(this.mousedown_node.id, this.mouseup_node.id); + this.selected_link = newLink; + this.restartGraph(); + }).bind(this)) + .on('dblclick', (function(d) { + if (this.onOpenNode) { + this.onOpenNode({ event: d3.event, node: d , editor: this }); + this.restartGraph(); + } + }).bind(this)); + + // Show node IDs + g.append('svg:text') + .attr('x', 0) + .attr('y', 4) + .attr('class', 'tid') +} + +/* + * Updates existing nodes and adds new nodes. + */ +Editor.prototype.restartNodes = function() { + // Set the circle group's data to this.nodes. + // Note that nodes are identified by id, not their index in the array. + this.circle = this.circle.data(this.nodes, function(d) { return d.id; }); + // NOTE: addNodes must only be called after .data is set to the latest + // this.nodes. This is done at the beginning of this method. + this.addNodes(); + // Update existing nodes (reflexive & selected visual states) + this.circle.selectAll('circle') + .style('fill', function(d) { return d.color; }) + .classed('reflexive', function(d) { return d.reflexive; }) + .classed('selected', (function(d) { return d === this.selected_node }).bind(this)) + .attr('r', function(d) { return getRadius(d); }); + // If node is not enabled, set its opacity to 0.2 + this.circle.transition().style('opacity', function(d) { return d.enabled === true ? 1 : 0.2; }); + // Update node IDs + var el = this.circle.selectAll('text').text(''); + el.append('tspan') + .text(function(d) { + return d.id; + }) + .attr('x', 0) + .attr('dy', function(d) { + return d.attrs != null && d.attrs.trim() != '' ? '-8' : '0 '; + }) + .attr('class', 'id'); + // Node value (if present) is added/updated here + el.append('tspan') + .text(function(d) { + return getAttrForDisplay(d.attrs); + }) + .attr('x', 0) + .attr('dy', function(d) { + return d.attrs != null && d.attrs.trim() != '' ? '18' : '0'; + }) + .attr('class', 'vval'); + // remove old nodes + this.circle.exit().remove(); +} + +/* + * Restarts (refreshes, just using 'restart' for consistency) the aggregators. + */ +Editor.prototype.restartAggregators = function() { + this.aggregatorsContainer.attr('transform', 'translate(' + (this.width - 250) + ', 25)') + this.aggregatorsContainer.transition().style('opacity', Utils.count(this.aggregators) > 0 ? 1 : 0); + // Remove all values + this.globs = this.globs.data([]); + this.globs.exit().remove(); + // Convert JSON to array of 2-length arrays for d3 + var data = $.map(this.aggregators, function(value, key) { return [[key, value]]; }); + // Set new values + this.globs = this.globs.data(data); + this.globs.enter().append('tspan').classed('editor-aggregators-value', true) + .attr('dy', '2.0em') + .attr('x', 0) + .text(function(d) { return "{0} -> {1}".format(d[0], d[1]); }); +} + +/* + * Restarts the table with the latest currentScenario. + */ +Editor.prototype.restartTable = function() { + // Remove all rows of the table and add again. + this.dataTable.rows().remove(); + + // Modify the scenario object to suit dataTables format + for (var nodeId in this.currentScenario) { + var dataRow = {}; + var scenario = this.currentScenario[nodeId]; + dataRow.vertexId = nodeId; + dataRow.vertexValue = scenario.vertexValue ? scenario.vertexValue : '-', + dataRow.outgoingMessages = { + numOutgoingMessages : Utils.count(scenario.outgoingMessages), + data : scenario.outgoingMessages + }, + dataRow.incomingMessages = { + numIncomingMessages : Utils.count(scenario.incomingMessages), + data : scenario.incomingMessages + }, + dataRow.neighbors = { + numNeighbors : Utils.count(scenario.neighbors), + data : scenario.neighbors + } + this.dataTable.row.add(dataRow).draw(); + } + + // Bind click event for rows. + $('#editor-tablet-table td.tablet-details-control').click((function(event) { + var tr = $(event.target).parents('tr'); + var row = this.dataTable.row(tr); + if ( row.child.isShown()) { + // This row is already open - close it. + row.child.hide(); + tr.removeClass('shown'); + } else { + // Open this row. + var rowData = row.data(); + var rowHtml = this.getRowDetailsHtml(rowData); + var dataContainer = rowHtml.dataContainer; + row.child(rowHtml.outerContainer).show(); + // Now attach events to the tabs + // NOTE: MUST attach events after the row.child call. + $(rowHtml.navContainer).on('click', 'li a', (function(event) { + // Check which tab was clicked and populate data accordingly. + var dataContainer = rowHtml.dataContainer; + var tabName = $(event.target).data('name'); + // Clear the data container + $(dataContainer).empty(); + if (tabName === 'outgoingMessages') { + var mainTable = $('<table><thead><th>Receiver ID</th><th>Outgoing Message</th></thead></table>') + .attr('class', 'table') + .appendTo(dataContainer) + var outgoingMessages = rowData.outgoingMessages.data; + for (var receiverId in outgoingMessages) { + $(mainTable).append("<tr><td>{0}</td><td>{1}</td></tr>".format(receiverId, outgoingMessages[receiverId])); + } + $(mainTable).DataTable(); + } else if (tabName === 'incomingMessages') { + var mainTable = $('<table><thead><th>Incoming Message</th></thead></table>') + .attr('class', 'table') + .appendTo(dataContainer); + var incomingMessages = rowData.incomingMessages.data; + for (var i = 0; i < incomingMessages.length; i++) { + $(mainTable).append("<tr><td>{0}</td></tr>".format(incomingMessages[i])); + } + } else if (tabName === 'neighbors') { + var mainTable = $('<table><thead><th>Neighbor ID</th><th>Edge Value</th></thead></table>') + .attr('class', 'table') + .appendTo(dataContainer) + var neighbors = rowData.neighbors.data; + console.log(neighbors); + for (var i = 0 ; i < neighbors.length; i++) { + $(mainTable).append("<tr><td>{0}</td><td>{1}</td></tr>" + .format(neighbors[i].neighborId, neighbors[i].edgeValue ? neighbors[i].edgeValue : '-')); + } + $(mainTable).DataTable(); + } + }).bind(this)); + // Click the first tab of the navContainer - ul>li>a + $(rowHtml.navContainer).children(':first').children(':first').click(); + tr.addClass('shown'); + } + }).bind(this)); +} + +/* + * Returns the index of the node with the given id in the nodes array. + * @param {string} id - The identifier of the node. + */ +Editor.prototype.getNodeIndex = function(id) { + return this.nodes.map(function(e) { return e.id }).indexOf(id); +} + +/* + * Returns the node object with the given id, null if node is not present. + * @param {string} id - The identifier of the node. + */ +Editor.prototype.getNodeWithId = function(id) { + var index = this.getNodeIndex(id); + return index >= 0 ? this.nodes[index] : null; +} + +/* + * Returns the link objeccts with the given id as the source. + * Note that source here implies that all these links are outgoing from this node. + * @param {string} sourceId - The identifier of the source node. + */ +Editor.prototype.getEdgesWithSourceId = function(sourceId) { + var edges = []; + $.each(this.links, (function(i, link) { + $.each(this.getEdges(link), function(index, edge) { + if (edge.source.id === sourceId) { + edges.push(edge); + } + }); + }).bind(this)); + return edges; +} + +/* + * Returns true if the node with the given ID is present in the graph. + * @param {string} id - The identifier of the node. + */ +Editor.prototype.containsNode = function(id) { + return this.getNodeIndex(id) >= 0; +} + +/* + * Removes the links associated with a given node. + * Used when a node is deleted. + */ +Editor.prototype.spliceLinksForNode = function(node) { + var toSplice = this.links.filter(function(l) { + return (l.source === node || l.target === node); + }); + + toSplice.map((function(l) { + this.links.splice(this.links.indexOf(l), 1); + }).bind(this)); +} + +/* + * Puts the graph in readonly state. + */ +Editor.prototype.setReadonly = function(_readonly) { + this.readonly = _readonly; + if (this.readonly) { + // Support zooming in readonly mode. + this.zoomHolder.call(d3.behavior.zoom().on('zoom', this.redraw.bind(this))) + // Remove double click zoom since we display node attrs on double click. + this.zoomHolder.on('dblclick.zoom', null); + } else { + // Remove zooming in edit mode. + this.zoomHolder.on('.zoom', null); + this.zoomSvg([0,0], 1); + } +} http://git-wip-us.apache.org/repos/asf/giraph/blob/8675c84a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/slider/bootstrap-slider.js ---------------------------------------------------------------------- diff --git a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/slider/bootstrap-slider.js b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/slider/bootstrap-slider.js new file mode 100644 index 0000000..97e1738 --- /dev/null +++ b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/slider/bootstrap-slider.js @@ -0,0 +1,394 @@ +/* ========================================================= + * bootstrap-slider.js v2.0.0 + * http://www.eyecon.ro/bootstrap-slider + * ========================================================= + * Copyright 2012 Stefan Petre + * + * 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. + * ========================================================= + * + * NOTE: We are using this code to show a slider for changing link distance + * between nodes, among other things. + * USING A LOCAL COPY OF THIS LIBRARY. NO OFFICIAL CDN AVAILABLE. + * + */ + +!function( $ ) { + + var Slider = function(element, options) { + this.element = $(element); + this.picker = $('<div class="slider">'+ + '<div class="slider-track">'+ + '<div class="slider-selection"></div>'+ + '<div class="slider-handle"></div>'+ + '<div class="slider-handle"></div>'+ + '</div>'+ + '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+ + '</div>') + .insertBefore(this.element) + .append(this.element); + this.id = this.element.data('slider-id')||options.id; + if (this.id) { + this.picker[0].id = this.id; + } + + if (typeof Modernizr !== 'undefined' && Modernizr.touch) { + this.touchCapable = true; + } + + var tooltip = this.element.data('slider-tooltip')||options.tooltip; + + this.tooltip = this.picker.find('.tooltip'); + this.tooltipInner = this.tooltip.find('div.tooltip-inner'); + + this.orientation = this.element.data('slider-orientation')||options.orientation; + switch(this.orientation) { + case 'vertical': + this.picker.addClass('slider-vertical'); + this.stylePos = 'top'; + this.mousePos = 'pageY'; + this.sizePos = 'offsetHeight'; + this.tooltip.addClass('right')[0].style.left = '100%'; + break; + default: + this.picker + .addClass('slider-horizontal') + .css('width', this.element.outerWidth()); + this.orientation = 'horizontal'; + this.stylePos = 'left'; + this.mousePos = 'pageX'; + this.sizePos = 'offsetWidth'; + this.tooltip.addClass('top')[0].style.top = -this.tooltip.outerHeight() - 14 + 'px'; + break; + } + + this.min = this.element.data('slider-min')||options.min; + this.max = this.element.data('slider-max')||options.max; + this.step = this.element.data('slider-step')||options.step; + this.value = this.element.data('slider-value')||options.value; + if (this.value[1]) { + this.range = true; + } + + this.selection = this.element.data('slider-selection')||options.selection; + this.selectionEl = this.picker.find('.slider-selection'); + if (this.selection === 'none') { + this.selectionEl.addClass('hide'); + } + this.selectionElStyle = this.selectionEl[0].style; + + + this.handle1 = this.picker.find('.slider-handle:first'); + this.handle1Stype = this.handle1[0].style; + this.handle2 = this.picker.find('.slider-handle:last'); + this.handle2Stype = this.handle2[0].style; + + var handle = this.element.data('slider-handle')||options.handle; + switch(handle) { + case 'round': + this.handle1.addClass('round'); + this.handle2.addClass('round'); + break + case 'triangle': + this.handle1.addClass('triangle'); + this.handle2.addClass('triangle'); + break + } + + if (this.range) { + this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0])); + this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1])); + } else { + this.value = [ Math.max(this.min, Math.min(this.max, this.value))]; + this.handle2.addClass('hide'); + if (this.selection == 'after') { + this.value[1] = this.max; + } else { + this.value[1] = this.min; + } + } + this.diff = this.max - this.min; + this.percentage = [ + (this.value[0]-this.min)*100/this.diff, + (this.value[1]-this.min)*100/this.diff, + this.step*100/this.diff + ]; + + this.offset = this.picker.offset(); + this.size = this.picker[0][this.sizePos]; + + this.formater = options.formater; + + this.layout(); + + if (this.touchCapable) { + // Touch: Bind touch events: + this.picker.on({ + touchstart: $.proxy(this.mousedown, this) + }); + } else { + this.picker.on({ + mousedown: $.proxy(this.mousedown, this) + }); + } + + if (tooltip === 'show') { + this.picker.on({ + mouseenter: $.proxy(this.showTooltip, this), + mouseleave: $.proxy(this.hideTooltip, this) + }); + } else { + this.tooltip.addClass('hide'); + } + }; + + Slider.prototype = { + constructor: Slider, + + over: false, + inDrag: false, + + showTooltip: function(){ + this.tooltip.addClass('in'); + //var left = Math.round(this.percent*this.width); + //this.tooltip.css('left', left - this.tooltip.outerWidth()/2); + this.over = true; + }, + + hideTooltip: function(){ + if (this.inDrag === false) { + this.tooltip.removeClass('in'); + } + this.over = false; + }, + + layout: function(){ + this.handle1Stype[this.stylePos] = this.percentage[0]+'%'; + this.handle2Stype[this.stylePos] = this.percentage[1]+'%'; + if (this.orientation == 'vertical') { + this.selectionElStyle.top = Math.min(this.percentage[0], this.percentage[1]) +'%'; + this.selectionElStyle.height = Math.abs(this.percentage[0] - this.percentage[1]) +'%'; + } else { + this.selectionElStyle.left = Math.min(this.percentage[0], this.percentage[1]) +'%'; + this.selectionElStyle.width = Math.abs(this.percentage[0] - this.percentage[1]) +'%'; + } + if (this.range) { + this.tooltipInner.text( + this.formater(this.value[0]) + + ' : ' + + this.formater(this.value[1]) + ); + this.tooltip[0].style[this.stylePos] = this.size * (this.percentage[0] + (this.percentage[1] - this.percentage[0])/2)/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px'; + } else { + this.tooltipInner.text( + this.formater(this.value[0]) + ); + this.tooltip[0].style[this.stylePos] = this.size * this.percentage[0]/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px'; + } + }, + + mousedown: function(ev) { + + // Touch: Get the original event: + if (this.touchCapable && ev.type === 'touchstart') { + ev = ev.originalEvent; + } + + this.offset = this.picker.offset(); + this.size = this.picker[0][this.sizePos]; + + var percentage = this.getPercentage(ev); + + if (this.range) { + var diff1 = Math.abs(this.percentage[0] - percentage); + var diff2 = Math.abs(this.percentage[1] - percentage); + this.dragged = (diff1 < diff2) ? 0 : 1; + } else { + this.dragged = 0; + } + + this.percentage[this.dragged] = percentage; + this.layout(); + + if (this.touchCapable) { + // Touch: Bind touch events: + $(document).on({ + touchmove: $.proxy(this.mousemove, this), + touchend: $.proxy(this.mouseup, this) + }); + } else { + $(document).on({ + mousemove: $.proxy(this.mousemove, this), + mouseup: $.proxy(this.mouseup, this) + }); + } + + this.inDrag = true; + var val = this.calculateValue(); + this.element.trigger({ + type: 'slideStart', + value: val + }).trigger({ + type: 'slide', + value: val + }); + return false; + }, + + mousemove: function(ev) { + + // Touch: Get the original event: + if (this.touchCapable && ev.type === 'touchmove') { + ev = ev.originalEvent; + } + + var percentage = this.getPercentage(ev); + if (this.range) { + if (this.dragged === 0 && this.percentage[1] < percentage) { + this.percentage[0] = this.percentage[1]; + this.dragged = 1; + } else if (this.dragged === 1 && this.percentage[0] > percentage) { + this.percentage[1] = this.percentage[0]; + this.dragged = 0; + } + } + this.percentage[this.dragged] = percentage; + this.layout(); + var val = this.calculateValue(); + this.element + .trigger({ + type: 'slide', + value: val + }) + .data('value', val) + .prop('value', val); + return false; + }, + + mouseup: function(ev) { + if (this.touchCapable) { + // Touch: Bind touch events: + $(document).off({ + touchmove: this.mousemove, + touchend: this.mouseup + }); + } else { + $(document).off({ + mousemove: this.mousemove, + mouseup: this.mouseup + }); + } + + this.inDrag = false; + if (this.over == false) { + this.hideTooltip(); + } + this.element; + var val = this.calculateValue(); + this.element + .trigger({ + type: 'slideStop', + value: val + }) + .data('value', val) + .prop('value', val); + return false; + }, + + calculateValue: function() { + var val; + if (this.range) { + val = [ + (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step), + (this.min + Math.round((this.diff * this.percentage[1]/100)/this.step)*this.step) + ]; + this.value = val; + } else { + val = (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step); + this.value = [val, this.value[1]]; + } + return val; + }, + + getPercentage: function(ev) { + if (this.touchCapable) { + ev = ev.touches[0]; + } + var percentage = (ev[this.mousePos] - this.offset[this.stylePos])*100/this.size; + percentage = Math.round(percentage/this.percentage[2])*this.percentage[2]; + return Math.max(0, Math.min(100, percentage)); + }, + + getValue: function() { + if (this.range) { + return this.value; + } + return this.value[0]; + }, + + setValue: function(val) { + this.value = val; + + if (this.range) { + this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0])); + this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1])); + } else { + this.value = [ Math.max(this.min, Math.min(this.max, this.value))]; + this.handle2.addClass('hide'); + if (this.selection == 'after') { + this.value[1] = this.max; + } else { + this.value[1] = this.min; + } + } + this.diff = this.max - this.min; + this.percentage = [ + (this.value[0]-this.min)*100/this.diff, + (this.value[1]-this.min)*100/this.diff, + this.step*100/this.diff + ]; + this.layout(); + } + }; + + $.fn.slider = function ( option, val ) { + return this.each(function () { + var $this = $(this), + data = $this.data('slider'), + options = typeof option === 'object' && option; + if (!data) { + $this.data('slider', (data = new Slider(this, $.extend({}, $.fn.slider.defaults,options)))); + } + if (typeof option == 'string') { + data[option](val); + } + }) + }; + + $.fn.slider.defaults = { + min: 0, + max: 10, + step: 1, + orientation: 'horizontal', + value: 5, + selection: 'before', + tooltip: 'show', + handle: 'round', + formater: function(value) { + return value; + } + }; + + $.fn.slider.Constructor = Slider; + +}( window.jQuery ); http://git-wip-us.apache.org/repos/asf/giraph/blob/8675c84a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/utils.js ---------------------------------------------------------------------- diff --git a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/utils.js b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/utils.js new file mode 100644 index 0000000..a0b9167 --- /dev/null +++ b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/utils.js @@ -0,0 +1,256 @@ +/* + * Utility functions used in other JS files. + * Parts of this file are borrowed from others. A comment is placed on top in such cases. + */ +function Utils() {} + +/* + * Counts the number of keys of a JSON object. + */ +Utils.count = function(obj) { + var count=0; + for(var prop in obj) { + if (obj.hasOwnProperty(prop)) { + ++count; + } + } + return count; +} + +/* + * Format feature for JS strings. + * Example - "Hello {0}, {1}".format("World", "Graph") + * = Hello World, Graph + */ +if (!String.prototype.format) { + String.prototype.format = function() { + var args = arguments; + return this.replace(/{(\d+)}/g, function(match, number) { + return typeof args[number] != 'undefined' + ? args[number] + : match + ; + }); + }; +} + +/*! + *jQuery Ajax Retry - v0.2.4 - 2013-08-16 + * https://github.com/johnkpaul/jquery-ajax-retry + * Copyright (c) 2013 John Paul; Licensed MIT + * + * NOTE: We are using this code to retry AJAX calls to the debugger server. + */ +(function($) { + // enhance all ajax requests with our retry API + $.ajaxPrefilter(function(options, originalOptions, jqXHR){ + jqXHR.retry = function(opts){ + if(opts.timeout){ + this.timeout = opts.timeout; + } + if (opts.statusCodes) { + this.statusCodes = opts.statusCodes; + } + if (opts.retryCallback) { + this.retryCallback = opts.retryCallback; + } + return this.pipe(null, pipeFailRetry(this, opts)); + }; + }); + + // Generates a fail pipe function that will retry `jqXHR` `times` more times. + function pipeFailRetry(jqXHR, opts){ + var times = opts.times; + var timeout = opts.timeout; + var retryCallback = opts.retryCallback; + + // takes failure data as input, returns a new deferred + return function(input, status, msg){ + var ajaxOptions = this; + var output = new $.Deferred(); + + // whenever we do make this request, pipe its output to our deferred + function nextRequest() { + $.ajax(ajaxOptions) + .retry({times : times-1, timeout : timeout, retryCallback : retryCallback}) + .pipe(output.resolve, output.reject); + } + + if (times > 1 && (!jqXHR.statusCodes || $.inArray(input.status, jqXHR.statusCodes) > -1)) { + if (retryCallback) { + retryCallback(times - 1); + } + // time to make that next request... + if(jqXHR.timeout !== undefined){ + setTimeout(nextRequest, jqXHR.timeout); + } else { + nextRequest(); + } + } else { + // no times left, reject our deferred with the current arguments + output.rejectWith(this, arguments); + } + return output; + }; + } +}(jQuery)); + +/* + * Select all text in a given container. + * @param elementId : ID of the element with the text. + */ +function selectText(elementId) { + var doc = document + , text = doc.getElementById(elementId) + , range, selection + ; + if (doc.body.createTextRange) { //ms + range = doc.body.createTextRange(); + range.moveToElementText(text); + range.select(); + } else if (window.getSelection) { //all others + selection = window.getSelection(); + range = doc.createRange(); + range.selectNodeContents(text); + selection.removeAllRanges(); + selection.addRange(range); + } +} + +/* + * Hook to force a file download given the contents and the file name. + */ +Utils.downloadFile = function(contents, fileName) { + var pom = document.createElement('a'); + pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(contents)); + pom.setAttribute('download', fileName); + pom.click(); +} + +/* + * Utility method to fetch vertex sceario from server. + */ +Utils.fetchVertexTest = function(debuggerServerRoot, jobId, superstepId, vertexId, traceType) { + var url = debuggerServerRoot + '/test/vertex'; + var params = { + jobId : jobId, + superstepId : superstepId, + vertexId : vertexId, + traceType : traceType + }; + return $.ajax({ + url : url, + data : params, + dataFilter : function(data) { + return { + code: data, + url : "{0}?{1}".format(url, $.param(params)) + }; + } + }); +} + +/* + * Utility method to fetch master sceario from server. + */ +Utils.fetchMasterTest = function(debuggerServerRoot, jobId, superstepId) { + var url = debuggerServerRoot + '/test/master'; + var params = { + jobId: jobId, + superstepId : superstepId + }; + return $.ajax({ + url : url, + data : params, + dataFilter : function(data) { + return { + code: data, + url : "{0}?{1}".format(url, $.param(params)) + }; + } + }); +} + +/* + * Utility method to fetch the test graph for an adjacency list. + */ +Utils.fetchTestGraph = function(debuggerServerRoot, adjList) { + var url = debuggerServerRoot + '/test/graph'; + var params = { adjList : adjList }; + return $.ajax({ + url : debuggerServerRoot + '/test/graph', + data : params, + dataFilter : function(data) { + return { + code: data, + url : "{0}?{1}".format(url, $.param(params)) + }; + } + }); +} + +/* + * Converts the adjList object returned by editor to a string representation + * + */ +Utils.getAdjListStr = function(editorAdjList) { + adjList = ''; + $.each(editorAdjList, function(vertexId, obj) { + adjList += vertexId + '\t'; + $.each(obj.adj, function(i, edge) { + adjList += edge.target.id + '\t'; + }); + // Remove the last tab + adjList = adjList.slice(0, -1); + adjList += '\n'; + }); + // Remove the last newline + return adjList.slice(0, -1); +} + +/* + * Converts the adjList object returned by editor to a string representation + * used by the Test Graph debugger server. + */ +Utils.getAdjListStrForTestGraph = function(editorAdjList) { + adjList = ''; + $.each(editorAdjList, function(vertexId, obj) { + adjList += "{0}{1} ".format(vertexId, obj.vertexValue ? ":" + obj.vertexValue : ""); + $.each(obj.adj, function(i, edge) { + var edgeValue = edge.edgeValue; + adjList += "{0}{1} ".format(edge.target.id, edgeValue ? ":" + edgeValue : ""); + }); + // Remove the last whitespace + adjList = adjList.slice(0, -1); + adjList += '\n'; + }); + // Remove the last newline + return adjList.slice(0, -1); +} + +/* + * Creates and returns a submit button with an OK icon. + */ +Utils.getBtnSubmitSm = function() { + return $('<button />') + .attr('type', 'button') + .addClass('btn btn-primary btn-sm editable-submit') + .html('<i class="glyphicon glyphicon-ok"></i>') +} + +/* + * Creates and returns a cancel button with REMOVE icon. + */ +Utils.getBtnCancelSm = function() { + return $('<button />') + .attr('type', 'button') + .addClass('btn btn-default btn-sm editable-cancel') + .html('<i class="glyphicon glyphicon-remove"></i>') +} + +/* + * Returns the jQuery selector for element ID. + */ +Utils.getSelectorForId = function(id) { + return '#' + id; +} http://git-wip-us.apache.org/repos/asf/giraph/blob/8675c84a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/utils.sampleGraphs.js ---------------------------------------------------------------------- diff --git a/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/utils.sampleGraphs.js b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/utils.sampleGraphs.js new file mode 100644 index 0000000..9f4f87d --- /dev/null +++ b/giraph-debugger/src/main/resources/org/apache/giraph/debugger/gui/js/utils.sampleGraphs.js @@ -0,0 +1,25 @@ +/* + * Map of sample graphs and their corresponding init handlers. + * Format is "<human readable graph type>" : function() { } + * Attach the handler of the same name to Utils.SampleGraphs + */ +Utils.sampleGraphs = { + "Line Graph" : function(numNodes) { + var simpleAdjList = { 0 : {} }; + for (var i = 0; i < numNodes - 1; i++) { + simpleAdjList[i] = [i+1]; + } + return simpleAdjList; + }, + "Cycle" : function(numNodes) { + var simpleAdjList = Utils.sampleGraphs["Line Graph"](numNodes); + if (numNodes > 1) { + simpleAdjList[numNodes-1] = [0]; + } + return simpleAdjList; + }, + "Vertex Clique" : function(numNodes) {}, + "Tailed Cycle Graph" : function(numNodes) {}, + "Star Graph" : function(numNodes) {}, + "Disconnected Graphs" : function(numNodes) {} +}
