UNPKG

jsnetworkx

Version:

A graph processing and visualization library for JavaScript (port of NetworkX for Python).

1,304 lines (1,144 loc) 39 kB
'use strict'; var _slicedToArray = require('babel-runtime/helpers/sliced-to-array')['default']; var _getIterator = require('babel-runtime/core-js/get-iterator')['default']; var _Array$from = require('babel-runtime/core-js/array/from')['default']; var _regeneratorRuntime = require('babel-runtime/regenerator')['default']; Object.defineProperty(exports, '__esModule', { value: true }); exports.draw = draw; /** * @fileoverview * * D3(http://mbostock.github.com/d3/) is a powerful library to associate data * with elements and provides various helpful methods to visualize the data, * such as color generators, layouts and DOM manipulation methods. * * Note: D3 must be included before running these functions */ var _internals = require('../_internals'); var nullFunction = function nullFunction() {}; function angleFor(x1, y1, x2, y2) { return Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI; } /** * Safely converts an iterator to an array. Because we often use tuples when * using generators internally, we have to be careful when converting the * generator to an array. Every element has to be converted explicitly. */ function toArray(iterator) { // shortcut. If the value is actually an array, we can just return it if (Array.isArray(iterator)) { return iterator; } var result = []; var i = 0; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = _getIterator(iterator), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var value = _step.value; result[i++] = Array.isArray(value) ? _Array$from(value) : value; } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator['return']) { _iterator['return'](); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return result; } /** * Holds a reference to the last container element for convenience. * * @type {?(string|Element)} * @private */ var LAST_ELEMENT = null; /** * Holds a reference to the last configuration for convenience. * * @type {Object} * @private */ var LAST_CONFIGURATION = null; /** * A list of graph mutator methods. * * @type {Array.<string>} * @const * @private */ var MUTATOR_METHODS = ['addNode', 'addNodesFrom', 'addEdge', 'addEdgesFrom', 'removeNode', 'removeNodesFrom', 'removeEdge', 'removeEdgesFrom', 'clear']; /** * The name of the attribute the D3 data is assigned to in the node and * edge data. * * @type {string} */ var D3_DATA_NAME = '__d3datum__'; /** * Keep a reference to d3. */ var d3 = global.d3; /** * Draw graph G with D3. * * This method draws `G` with the provided `options`. If `optBind` is set to * `true`, changes to the graph structure will automatically update the * visualization. * * Returns the force layout used to compute the position of the nodes. * * The following options are available: * * - element (Element|String): This option is **required**. Specifies the * container of the visualization. A string is interpreted as CSS selector. * - d3 (d3): Use to explicitly pass a reference to D3. If not present, the * global variable d3 will be used instead. * - width (number): The width of the canvas in pixels. Defaults to the width * of the container. * - height (number): The height of the canvas in pixels. Defaults to the * height of the container. * - layoutAttr (Object): Layout options. The default layout is "force", so * the options size, linkDistance, linkStrength, friction, charge, theta * and gravity can be set. For example, setting `{linkDistance: 10}` will call * `force.linkDistance(10)`. * - nodelist (Iterable): An iterable of nodes. If present, only nodes in that * list will be drawn. * - nodeShape (string): The tag name of the SVG element to be used as nodes. * Defaults to "circle". * - nodeAttr (Object): The attributes to set on the node SVG element. This * object is passed along to D3's `.attr()` method. * - nodeStyle (Object): The style properties to set on the node SVG element. * This object is passed along to D3's `.style()` method. * - edgeAttr (Object): The attributes to set on an edge SVG element. Edges are * represented by SVG path elements. * - edgeStyle (Object): The style properties to set on the edge SVG element. * Note: Even though the edge element is a SVG path element, you cannot set * `stroke-width` to set the stroke width. Instead, the value of * `stroke-width` is used as maximum value for the edge width. * - withLabels (boolean): Whether or not to draw node labels. SVG text elements * are used for labels. * - labels (string|Object|function): The node labels to use. * If `withLabels` is `true`, but `labels` is not present, defaults to the * node itself. * If a string is passed, the value of the property of the node data with the * same name will be used. * If an object is passed, the label is looked up in the object using the node * as property name. * If a function is passed, it gets called and passed the corresponding D3 * data object. * - labelAttr (Object): Like `nodeAttr` but for the label nodes. Labels are * represented by SVG text nodes. * - labelStyle (Object): Like `nodeStyle` but for the label nodes. Labels are * represented by SVG text nodes. * - withEdgeLabels (boolean): See `withLabels`, but for edges. * - edgeLabels (string|Object|function): See `labels`. * - edgeLabelAttr (Object): Like `labelAttr`. * - edgeLabelStyle (Object): Like `labelStyle`. * - weighted (boolean): Whether the edge width depends on the weight of the * edge. The max and min weight are automatically computed. This is a * convenience option so that you don't have to compute the edge weights * yourself. * - weights (string|function): Specifies the weight for each edge. * If `weighted` is `true` but `weights` is not present, defaults to * `"weight"`. * If a string is passed, the value of the property of the edge data with the * same name is used as weight. * If a function is passed, it gets called and passed the corresponding D3 * data object. * - edgeOffset (number|function): The distance in pixels between the edge start * and the node. If not set and `nodeShape` is a `"circle"`, the offset will * be automatically computed based on the radius. * If a different shape for nodes is used it might be necessary to set the * offset manually. * - edgeLabelOffset (number|function): By default edge labels are drawing in * in the center of the edge. Can be used to adjust the position. * - panZoom (Object): * - enabled (boolean): Enables panning and zooming of the canvas. * - scale (boolean): Whether nodes and labels should keep their size * when zooming or not. * * @param {jsnx.classes.Graph} G The graph to draw * @param {?Object=} config A dictionary of configuration parameters. * @param {?boolean=} optBind Set to true to automatically update * the output upon graph manipulation. Only works for adding nodes or edges * for now. * @return {d3.layout.force} */ function draw(G, config, optBind) { if (typeof config === 'boolean') { optBind = config; config = null; } config = config || LAST_CONFIGURATION || {}; LAST_CONFIGURATION = config; if (config.d3) { d3 = config.d3; } config = (0, _internals.deepmerge)({}, DEFAULT_CONFIG, config); if (!d3) { throw new Error('D3 requried for draw()'); } if (config.element == null && LAST_ELEMENT == null) { throw new Error('Output element required for draw()'); } // initialize LAST_ELEMENT = config.element || LAST_ELEMENT; // remove any possible previous graph d3.select(LAST_ELEMENT).select('svg.jsnx').remove(); // set up base elements var container = d3.select(LAST_ELEMENT); var d3nodes = []; var d3links = []; var canvas = container.append('svg').classed('jsnx', true).attr('pointer-events', 'all'); var parentContainer = canvas.append('g'); var edgeSelection = parentContainer.append('g').classed('edges', true).selectAll('g.edge'); var nodeSelection = parentContainer.append('g').classed('nodes', true).selectAll('g.node'); var force = d3.layout.force(); var width = config.width || parseInt(container.style('width'), 10); var height = config.height || parseInt(container.style('height'), 10); var layoutAttr = config.layoutAttr; var nodelist = config.nodelist || null; var labelFunc; var edgeLabelFunc; var weightFunc; var directed = G.isDirected(); var weighted = config.weighted; var selections = { nodeSelection: nodeSelection, edgeSelection: edgeSelection }; // determine node label function if (config.withLabels) { var labels = config.labels; switch (typeof labels) { case 'object': labelFunc = function (d) { return (0, _internals.getDefault)(labels[d.node], ''); }; break; case 'function': labelFunc = labels; break; case 'string': labelFunc = function (d) { return d.data[labels]; }; break; default: labelFunc = function (d) { return d.node; }; } } config.labels = labelFunc; // if the graph should be weighted, we need a weight function // these will be used as edge labels if no others are provided if (weighted) { var weights = config.weights; switch (typeof weigths) { case 'object': weightFunc = function (d) { return (0, _internals.getDefault)(weights[d.node], 1); }; break; case 'function': weightFunc = weights; break; case 'string': weightFunc = function (d) { return (0, _internals.getDefault)(d.data[weights], 1); }; break; default: weightFunc = function (d) { return 1; }; } } // determine edge labels if (config.withEdgeLabels) { var elabels = config.edgeLabels; if (weighted && elabels == null) { edgeLabelFunc = weightFunc; } else { switch (typeof elabels) { case 'object': edgeLabelFunc = function (d) { return (0, _internals.getDefault)(labels[d.node], ''); }; break; case 'function': edgeLabelFunc = elabels; break; case 'string': edgeLabelFunc = function (d) { return d.data[elabels]; }; break; default: edgeLabelFunc = function (d) { return d.edge; }; } } config.edgeLabels = edgeLabelFunc; } // scale the width of the edge according to the weight if (weighted && config.weightedStroke) { var maxWeight = 1; var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = _getIterator(G.edgesIter(null, true)), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var _step2$value = _step2.value; var u = _step2$value.u; var v = _step2$value.v; var data = _step2$value.data; var weight = weightFunc({ data: data }); if (weight > maxWeight) { maxWeight = weight; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2['return']) { _iterator2['return'](); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } var scale = d3.scale.linear().range([2, config.edgeStyle['stroke-width']]).domain([0, maxWeight]); config.edgeStyle['stroke-width'] = function (d) { return scale(weightFunc.call(this, d)); }; } // remove any possible previous graph canvas.select('svg.jsnx').remove(); // set size and hide the wild movement of nodes at the beginning canvas.attr('width', width + 'px').attr('height', height + 'px').style('opacity', 1e-6).transition().duration(1000).style('opacity', 1); // initialize layout // don't let the user set these: var exclude = { size: true, nodes: true, links: true, start: true }; for (var attr in layoutAttr) { if (exclude[attr] !== true) { force[attr](layoutAttr[attr]); } } force.nodes(d3nodes).links(d3links).size([width, height]); // set up zoom and pan behaviour var zoom = 1; var invScale = 1; // used to scale nodes and text accordingly if (config.panZoom.enabled) { (function () { var scaled = config.panZoom.scale; var zooming = false; var zoomStartScale = 1; var zoomStart = zoom; canvas.call(d3.behavior.zoom().on('zoom', function () { if (!d3.event.sourceEvent) { return; } var shiftKey = d3.event.sourceEvent.shiftKey, zoomed = scaled && shiftKey || !(scaled || shiftKey); // if the graph is zoomed, we have to keep track of the // ration it was zoomed by if (zoomed && !zooming) { zoomStartScale = d3.event.scale; zoomStart = zoom; zooming = true; } else if (!zoomed && zooming) { zooming = false; } zoom = zoomed ? zoomStart * (d3.event.scale / zoomStartScale) : zoom; invScale = !zoomed ? zoom / d3.event.scale : invScale; var tr = d3.event.translate; parentContainer.attr('transform', 'translate(' + tr[0] + ',' + tr[1] + ')scale(' + d3.event.scale + ')'); redraw(); })); })(); } var updateEdgePosition = nullFunction; var offset = config.edgeOffset; var nodeRadius = config.nodeAttr.r; var nodeStrokeWidth = config.nodeStyle['stroke-width']; if (config.nodeShape === 'circle') { if (typeof nodeRadius !== 'function') { nodeRadius = function () { return config.nodeAttr.r; }; } if (typeof nodeStrokeWidth !== 'function') { nodeStrokeWidth = function () { return config.nodeStyle['stroke-width']; }; } offset = function (d) { return [nodeRadius(d.source) + nodeStrokeWidth(d.source), nodeRadius(d.target) + nodeStrokeWidth(d.target)]; }; } else { if (Array.isArray(offset)) { offset = function () { return config.edgeOffset; }; } else if (typeof offset === 'number') { offset = function () { return [config.edgeOffset, config.edgeOffset]; }; } } var strw = config.edgeStyle['stroke-width']; if (typeof strw !== 'function') { strw = function () { return config.edgeStyle['stroke-width']; }; } var labelOffset = config.edgeLabelOffset; if (directed) { // don't rotate labels and draw curvy lines updateEdgePosition = function () { selections.edgeSelection.each(function (d) { if (d.source !== d.target) { var $this = d3.select(this); var x1 = d.source.x; var y1 = d.source.y; var x2 = d.target.x; var y2 = d.target.y; var angle = angleFor(x1, y1, x2, y2); var dx = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); var computedOffset = offset(d); computedOffset = [computedOffset[0] * invScale, computedOffset[1] * invScale]; $this.attr('transform', ['translate(', x1, ',', y1, ')', 'rotate(', angle, ')'].join('')); var shift = strw(d) * invScale; var arrowStartPoint = dx - computedOffset[1] - 2 * shift; var halfShift = shift / 2; $this.select('.line').attr('d', ['M', computedOffset[0], 0, 'L', computedOffset[0], -halfShift, 'L', arrowStartPoint, -halfShift, 'L', arrowStartPoint, -shift, 'L', dx - computedOffset[1], 0, 'z'].join(' ')); var edgeScale = 1 / invScale; $this.select('text').attr('x', labelOffset.x * edgeScale + computedOffset[0] + (dx * edgeScale - computedOffset[0] - computedOffset[1]) / 2).attr('y', -strw(d) / 2 + -labelOffset.y * edgeScale).attr('transform', 'scale(' + invScale + ')'); } }); }; } else { updateEdgePosition = function () { selections.edgeSelection.each(function (d) { if (d.source !== d.target) { var $this = d3.select(this); var x1 = d.source.x; var y1 = d.source.y; var x2 = d.target.x; var y2 = d.target.y; var angle = angleFor(x1, y1, x2, y2); var dx = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); var center = dx / 2; var computedOffset = offset(d); computedOffset = [computedOffset[0] * invScale, computedOffset[1] * invScale]; var edgeScale = 1 / invScale; var shift = strw(d) * invScale; var flip = angle > 90 && angle < 279; $this.attr('transform', ['translate(', x1, ',', y1, ')', 'rotate(', angle, ')'].join('')); $this.select('.line').attr('d', ['M', computedOffset[0], shift / 4, 'L', computedOffset[0], -shift / 4, 'L', dx - computedOffset[1], -shift / 4, 'L', dx - computedOffset[1], shift / 4, 'z'].join(' ')); if (config.withEdgeLabels) { $this.select('text').attr('x', (flip ? 1 : -1) * labelOffset.x * edgeScale + computedOffset[0] + (dx * edgeScale - computedOffset[0] - computedOffset[1]) / 2).attr('y', -strw(d) / 4 + -labelOffset.y * edgeScale).attr('transform', 'scale(' + invScale + ')' + (flip ? 'rotate(180,' + center * (1 / invScale) + ',0)' : '')); } } }); }; } var redraw = function redraw() { // update node position selections.nodeSelection.attr('transform', function (d) { return ['translate(', d.x, ',', d.y, ')', 'scale(', invScale, ')'].join(''); }); updateEdgePosition(); }; force.on('tick', redraw); var nodes = G.nodesIter(); var edges = G.edgesIter(); if (nodelist) { // limit drawn nodes, disable binding optBind = false; nodes = G.nbunch_iter(nodelist); edges = G.edges_iter(nodelist); } // update d3 node and link data selections.nodeSelection = addNodes(G, nodes, force, nodeSelection, config); selections.edgeSelection = addEdges(G, edges, force, edgeSelection, edgeLabelFunc); // apply attributes and styles updateNodeAttr(selections.nodeSelection, config); updateEdgeAttr(selections.edgeSelection, config, null, directed); if (optBind) { bind(G, force, config, selections); } else { if (isBound(G)) { unbind(G); } else { clean(G); } } force.start(); return force; } /** * Helper function to create new node objects for the force layout and * create the necessary SVG elements. * * @param {Graph} G * @param {Iterable} nodes The nodes to include from the Graph * default are all nodes * @param {d3.layout.force} force The layout * @param {d3.selection} selection D3 DOM node selection of nodes * @param {Object} Drawing configuration * * @return {!d3.selection} The new selection of SVG elements. */ function addNodes(G, nodes, force, selection, config) { // Get current data var layoutNodes = force.nodes(); // add new data var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = _getIterator(nodes), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var node = _step3.value; var data = G.node.get(node); var nobj = { node: node, data: data, G: G }; layoutNodes.push(nobj); data[D3_DATA_NAME] = nobj; } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3['return']) { _iterator3['return'](); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } // update data join selection = selection.data(layoutNodes, nodeKeyFunction); // create new elements var drag = force.drag().on('dragstart', function (d) { // Prevent pan if node is dragged d3.event.sourceEvent.stopPropagation(); if (config.stickyDrag) { d.fixed = true; d3.select(this).classed('fixed', true); } }); var nsel = selection.enter().append('g').classed('node', true).call(drag); nsel.append(config.nodeShape).classed('node-shape', true); if (config.labels) { nsel.append('text').text(config.labels); } return selection; } /** * Helper function to create new edge objects for the force layout. * * @param {Graph} G * @param {Iterable} edges The nodes to include from the Graph * default are all nodes * @param {d3.layout.force} force * @param {d3.selection} selection D3 DOM node selection of nodes * @param {Function=} opt_label_func Function to extract text for labels * * @return {!d3.selection} */ function addEdges(G, edges, force, selection, optLabelFunc) { // Get current data var layoutLinks = force.links(); // add new data var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = _getIterator(edges), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var _step4$value = _slicedToArray(_step4.value, 3); var u = _step4$value[0]; var v = _step4$value[1]; var data = _step4$value[2]; data = data || G.getEdgeData(u, v); var eobj = { edge: [u, v], source: G.node.get(u)[D3_DATA_NAME], target: G.node.get(v)[D3_DATA_NAME], data: data, G: G }; layoutLinks.push(eobj); data[D3_DATA_NAME] = eobj; } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4['return']) { _iterator4['return'](); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } // update data join selection = selection.data(layoutLinks, edgeKeyFunction); // create new elements var esel = selection.enter().append('g').classed('edge', true); esel.append('path').classed('line', true); if (optLabelFunc) { esel.append('text').text(optLabelFunc); } return selection; } /** * Updates attributes of nodes. * * @param {d3.selection} selection * @param {Object} config * @param {Iterable=} opt_nodes a container of nodes. If set, * only update these nodes. */ function updateNodeAttr(selection, config, optNodes) { if (optNodes != null) { var newNodes = new _internals.Set(); var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { for (var _iterator5 = _getIterator(optNodes), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { var node = _step5.value; newNodes.add((0, _internals.isArrayLike)(node) ? node[0] : node); } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5['return']) { _iterator5['return'](); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } selection = selection.filter(function (d) { return newNodes.has(d.node); }); } selection.selectAll('.node-shape').attr(config.nodeAttr).style(config.nodeStyle); if (config.withLabels) { selection.selectAll('text').attr(config.labelAttr).style(config.labelStyle); } } /** * Updates attributes of edges. * * @param {d3.selection} selection * @param {Object} config * @param {?=} optEdges If set, only updates the styles of the provided * edges * @param {boolean=} optDirected */ function updateEdgeAttr(selection, config, optEdges, optDirected) { if (optEdges != null) { var newEdges = new _internals.Map(); var _iteratorNormalCompletion6 = true; var _didIteratorError6 = false; var _iteratorError6 = undefined; try { for (var _iterator6 = _getIterator(optEdges), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { var _step6$value = _slicedToArray(_step6.value, 2); var u = _step6$value[0]; var v = _step6$value[1]; newEdges.set(u, v); } } catch (err) { _didIteratorError6 = true; _iteratorError6 = err; } finally { try { if (!_iteratorNormalCompletion6 && _iterator6['return']) { _iterator6['return'](); } } finally { if (_didIteratorError6) { throw _iteratorError6; } } } selection = selection.filter(function (_ref) { var edge = _ref.edge; return newEdges.get(edge[0]) === edge[1] || optDirected || newEdges.get(edge[1]) === edge[0]; }); } selection.selectAll('.line').attr(config.edgeAttr).style(config.edgeStyle).style('stroke-width', 0); if (config.withEdgeLabels) { selection.selectAll('text').attr(config.edgeLabelAttr).style(config.edgeLabelStyle); } } /** * Key function to extract the join value for the SVG nodes and the data. * * @param {Object} d The current datum * @return {Node} */ function nodeKeyFunction(d) { return d.node; } /** * Key function to extract the join value for the SVG nodes and the data. * * @param {Object} d The current datum * @return {Array} */ function edgeKeyFunction(d) { return d.edge; } /** * Helper function to remove node objects for the force layout. * * @param {Graph} G * @param {Iterable} nodes to remove from the graph * @param {d3.layout.force} force The force the nodes are bound to * @param {d3.selection} selection Selection of node elements * * @return {d3.selection} Updated selection */ function removeNodes(G, nodes, force, selection) { // get current data set var data = force.nodes(); // remove items from data var _iteratorNormalCompletion7 = true; var _didIteratorError7 = false; var _iteratorError7 = undefined; try { for (var _iterator7 = _getIterator(G.nbunchIter(nodes)), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { var node = _step7.value; var index = data.indexOf(G.node.get(node)[D3_DATA_NAME]); if (index > -1) { data.splice(index, 1); } } } catch (err) { _didIteratorError7 = true; _iteratorError7 = err; } finally { try { if (!_iteratorNormalCompletion7 && _iterator7['return']) { _iterator7['return'](); } } finally { if (_didIteratorError7) { throw _iteratorError7; } } } // rebind data selection = selection.data(data, nodeKeyFunction); // remove SVG elements selection.exit().remove(); return selection; } /** * Helper function to remove edge objects for the force layout. * * @param {jsnx.classes.Graph} G * @param {?} edges Edges to remove * @param {d3.layout.force} force The force the edges are bound to * @param {d3.selection} selection Selection of edge elements * * @return {!d3.selection} Updated selection */ function removeEdges(G, edges, force, selection) { // get current data set var data = force.links(); // remove items from data var _iteratorNormalCompletion8 = true; var _didIteratorError8 = false; var _iteratorError8 = undefined; try { for (var _iterator8 = _getIterator(edges), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { var _step8$value = _slicedToArray(_step8.value, 2); var u = _step8$value[0]; var v = _step8$value[1]; var index = data.indexOf(G.getEdgeData(u, v, {})[D3_DATA_NAME]); if (index > -1) { data.splice(index, 1); } } } catch (err) { _didIteratorError8 = true; _iteratorError8 = err; } finally { try { if (!_iteratorNormalCompletion8 && _iterator8['return']) { _iterator8['return'](); } } finally { if (_didIteratorError8) { throw _iteratorError8; } } } // rebind data selection = selection.data(data, edgeKeyFunction); // remove SVG elements selection.exit().remove(); return selection; } /** * Binds the output to the graph. This overrides mutator methods. To "free" * the graph, you can call jsnx.unbind (which is public) * * @param {Graph} G A Graph * @param {d3.layout.force} force Force layout * @param {Object} config The configuration for the output * @param {{nodeSelection:d3.selection, edgeSelection:d3.selection }} selections * Various D3 selections */ function bind(G, force, config, selections) { unbind(G, false); var proto = G.constructor.prototype; var edgeLabelFunc = config.edgeLabels; var directed = G.isDirected(); G.addNode = function (n, optAttr) { var newNode = !this.hasNode(n); proto.addNode.call(this, n, optAttr); if (newNode) { selections.nodeSelection = addNodes(this, [n], force, selections.nodeSelection, config); } // update node attributes updateNodeAttr(selections.nodeSelection, config, [n]); force.start(); }; G.addNodesFrom = function (nbunch, optAttr) { // istanbul ignore next var _this = this; nbunch = toArray(nbunch); var newNodes = nbunch.filter(function (node) { return !_this.hasNode((0, _internals.isArrayLike)(node) ? node[0] : node); }); proto.addNodesFrom.call(this, nbunch, optAttr); if (newNodes.length > 0) { // add new nodes and update selections.nodeSelection = addNodes(this, newNodes, force, selections.nodeSelection, config); } updateNodeAttr(selections.nodeSelection, config, nbunch); force.start(); }; G.addEdge = function (u, v, optAttr) { // istanbul ignore next var _this2 = this; var newEdge = !this.hasEdge(u, v); var edges = [[u, v]]; var newNodes = newEdge ? (u === v ? [u] : edges[0]).filter(function (node) { return !_this2.hasNode(node); }) : []; proto.addEdge.call(G, u, v, optAttr); if (newNodes.length > 0) { selections.nodeSelection = addNodes(this, newNodes, force, selections.nodeSelection, config); updateNodeAttr(selections.nodeSelection, config, newNodes); } if (newEdge) { selections.edgeSelection = addEdges(this, edges, force, selections.edgeSelection, edgeLabelFunc); } updateEdgeAttr(selections.edgeSelection, config, edges, directed); force.start(); }; G.addEdgesFrom = function (ebunch, optAttr) { var newEdges = []; var newNodes = []; var seenEdges = new _internals.Map(); var seenNodes = new _internals.Set(); ebunch = toArray(ebunch); var _iteratorNormalCompletion9 = true; var _didIteratorError9 = false; var _iteratorError9 = undefined; try { for (var _iterator9 = _getIterator(ebunch), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { var _step9$value = _slicedToArray(_step9.value, 2); var u = _step9$value[0]; var v = _step9$value[1]; if (!this.hasEdge(u, v) && seenEdges.get(u) !== v && (directed || seenEdges.get(v) === u)) { newEdges.push([u, v]); seenEdges.set(u, v); if (!this.hasNode(u) && !seenNodes.has(u)) { newNodes.push(u); seenNodes.add(u); } if (!this.hasNode(v) && !seenNodes.has(v)) { newNodes.push(v); seenNodes.add(v); } } } } catch (err) { _didIteratorError9 = true; _iteratorError9 = err; } finally { try { if (!_iteratorNormalCompletion9 && _iterator9['return']) { _iterator9['return'](); } } finally { if (_didIteratorError9) { throw _iteratorError9; } } } proto.addEdgesFrom.call(G, ebunch, optAttr); if (newNodes.length > 0) { selections.nodeSelection = addNodes(this, newNodes, force, selections.nodeSelection, config); updateNodeAttr(selections.nodeSelection, config, newNodes); } if (newEdges.length > 0) { selections.edgeSelection = addEdges(this, newEdges, force, selections.edgeSelection, edgeLabelFunc); } updateEdgeAttr(selections.edgeSelection, config, newEdges, directed); force.start(); }; G.removeNode = function (n) { if (this.hasNode(n)) { selections.nodeSelection = removeNodes(this, [n], force, selections.nodeSelection); var edges = this.edgesIter([n]); if (this.isDirected()) { edges = _regeneratorRuntime.mark(function callee$2$0(self, outEdges) { return _regeneratorRuntime.wrap(function callee$2$0$(context$3$0) { while (1) switch (context$3$0.prev = context$3$0.next) { case 0: return context$3$0.delegateYield(outEdges, 't0', 1); case 1: return context$3$0.delegateYield(self.inEdgesIter([n]), 't1', 2); case 2: case 'end': return context$3$0.stop(); } }, callee$2$0, this); })(this, edges); } selections.edgeSelection = removeEdges(this, edges, force, selections.edgeSelection); force.resume(); } proto.removeNode.call(this, n); }; G.removeNodesFrom = function (nbunch) { nbunch = toArray(nbunch); selections.nodeSelection = removeNodes(this, nbunch, force, selections.nodeSelection); var edges = this.edgesIter(nbunch); if (this.isDirected()) { edges = _regeneratorRuntime.mark(function callee$2$0(self, outEdges) { return _regeneratorRuntime.wrap(function callee$2$0$(context$3$0) { while (1) switch (context$3$0.prev = context$3$0.next) { case 0: return context$3$0.delegateYield(outEdges, 't0', 1); case 1: return context$3$0.delegateYield(self.inEdgesIter(nbunch), 't1', 2); case 2: case 'end': return context$3$0.stop(); } }, callee$2$0, this); })(this, edges); } selections.edgeSelection = removeEdges(this, edges, force, selections.edgeSelection); force.resume(); proto.removeNodesFrom.call(this, nbunch); }; G.removeEdge = function (u, v) { selections.edgeSelection = removeEdges(this, [[u, v]], force, selections.edgeSelection); force.resume(); proto.removeEdge.call(this, u, v); }; G.removeEdgesFrom = function (ebunch) { ebunch = toArray(ebunch); selections.edgeSelection = removeEdges(this, ebunch, force, selections.edgeSelection); force.resume(); proto.removeEdgesFrom.call(G, ebunch); }; G.clear = function () { selections.nodeSelection = selections.nodeSelection.data([], nodeKeyFunction); selections.nodeSelection.exit().remove(); selections.edgeSelection = selections.edgeSelection.data([], edgeKeyFunction); selections.edgeSelection.exit().remove(); force.nodes([]).links([]).resume(); proto.clear.call(this); }; /** * @type boolean */ G.bound = true; } /** * Returns True if the graph is bound to an output. * * @param {Graph} G A Graph * @return {boolean} */ function isBound(G) { return G.bound; } /** * Resets mutator methods to the originals * * @param {} G graph * @param {boolean=} opt_clean (default=True) * If true, all D3 data is removed from the graph */ function unbind(G) { var optClean = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1]; if (isBound(G)) { var proto = G.constructor.prototype; MUTATOR_METHODS.forEach(function (m) { return G[m] = proto[m]; }); delete G.bound; if (optClean) { clean(G); } } } /** * Removes any D3 data from the Graph. * * @param {Graph} G A Graph */ function clean(G) { /*eslint no-unused-vars:0*/ var _iteratorNormalCompletion10 = true; var _didIteratorError10 = false; var _iteratorError10 = undefined; try { for (var _iterator10 = _getIterator(G.nodesIter(true)), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { var _step10$value = _slicedToArray(_step10.value, 2); var _ = _step10$value[0]; var data = _step10$value[1]; delete data[D3_DATA_NAME]; } } catch (err) { _didIteratorError10 = true; _iteratorError10 = err; } finally { try { if (!_iteratorNormalCompletion10 && _iterator10['return']) { _iterator10['return'](); } } finally { if (_didIteratorError10) { throw _iteratorError10; } } } var _iteratorNormalCompletion11 = true; var _didIteratorError11 = false; var _iteratorError11 = undefined; try { for (var _iterator11 = _getIterator(G.edgesIter(null, true)), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) { var _step11$value = _slicedToArray(_step11.value, 3); var u = _step11$value[0]; var v = _step11$value[1]; var data = _step11$value[2]; delete data[D3_DATA_NAME]; } } catch (err) { _didIteratorError11 = true; _iteratorError11 = err; } finally { try { if (!_iteratorNormalCompletion11 && _iterator11['return']) { _iterator11['return'](); } } finally { if (_didIteratorError11) { throw _iteratorError11; } } } } /** * Default D3 configuration. * * @type Object * @private */ var DEFAULT_CONFIG = { layoutAttr: { charge: -120, linkDistance: 60 }, nodeShape: 'circle', nodeAttr: { r: 10 // radius of 10 }, nodeStyle: { 'stroke-width': 2, stroke: '#333', fill: '#999', cursor: 'pointer' }, edgeAttr: {}, edgeStyle: { fill: '#000', 'stroke-width': 3 }, labelAttr: {}, labelStyle: { 'text-anchor': 'middle', 'dominant-baseline': 'central', cursor: 'pointer', '-webkit-user-select': 'none', fill: '#000' }, edgeLabelAttr: {}, edgeLabelStyle: { 'font-size': '0.8em', 'text-anchor': 'middle', '-webkit-user-select': 'none' }, edgeLabelOffset: { x: 0, y: 0.5 }, withLabels: false, withEdgeLabels: false, edgeOffset: 10, weighted: false, weights: 'weight', weightedStroke: true, panZoom: { enabled: true, scale: true } };