'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
  }
};