diff --git a/_includes/cytoscape.html b/_includes/cytoscape.html
index 74dac64..ea19d63 100644
--- a/_includes/cytoscape.html
+++ b/_includes/cytoscape.html
@@ -1,7 +1,10 @@
diff --git a/_includes/head.html b/_includes/head.html
index 6a841c3..09a277b 100644
--- a/_includes/head.html
+++ b/_includes/head.html
@@ -82,6 +82,7 @@
{% endif %}
{% if page.cytoscape %}
+
{% endif %}
diff --git a/public/js/cytoscape/cytoscape-automove.js b/public/js/cytoscape/cytoscape-automove.js
new file mode 100644
index 0000000..7cb6b89
--- /dev/null
+++ b/public/js/cytoscape/cytoscape-automove.js
@@ -0,0 +1,711 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["cytoscapeAutomove"] = factory();
+ else
+ root["cytoscapeAutomove"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 3);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
+
+var defaults = __webpack_require__(2);
+
+var typeofStr = _typeof('');
+var typeofObj = _typeof({});
+var typeofFn = _typeof(function () {});
+
+var isObject = function isObject(x) {
+ return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === typeofObj;
+};
+var isString = function isString(x) {
+ return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === typeofStr;
+};
+var isFunction = function isFunction(x) {
+ return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === typeofFn;
+};
+var isCollection = function isCollection(x) {
+ return isObject(x) && isFunction(x.collection);
+};
+
+// Object.assign() polyfill
+var assign = __webpack_require__(1);
+
+var eleExists = function eleExists(ele) {
+ return ele != null && !ele.removed();
+};
+
+var _elesHasEle = function elesHasEle(eles, ele) {
+ if (eles.has != undefined) {
+ // 3.x
+ _elesHasEle = function elesHasEle(eles, ele) {
+ return eles.has(ele);
+ };
+ } else {
+ // 2.x
+ _elesHasEle = function elesHasEle(eles, ele) {
+ return eles.intersection(ele).length > 0;
+ };
+ }
+
+ return _elesHasEle(eles, ele);
+};
+
+var getEleMatchesSpecFn = function getEleMatchesSpecFn(spec) {
+ if (isString(spec)) {
+ return function (ele) {
+ return ele.is(spec);
+ };
+ } else if (isFunction(spec)) {
+ return spec;
+ } else if (isCollection(spec)) {
+ return function (ele) {
+ return _elesHasEle(spec, ele);
+ };
+ } else {
+ throw new Error('Can not create match function for spec', spec);
+ }
+};
+
+var bindings = [];
+
+var bind = function bind(cy, events, selector, fn) {
+ var b = { cy: cy, events: events, selector: selector || 'node', fn: fn };
+
+ bindings.push(b);
+
+ cy.on(b.events, b.selector, b.fn);
+
+ return b;
+};
+
+var bindOnRule = function bindOnRule(rule, cy, events, selector, fn) {
+ var b = bind(cy, events, selector, fn);
+ var bindings = rule.bindings = rule.bindings || [];
+
+ bindings.push(b);
+};
+
+var unbindAllOnRule = function unbindAllOnRule(rule) {
+ var unbind = function unbind(b) {
+ b.cy.off(b.events, b.selector, b.fn);
+ };
+
+ rule.bindings.forEach(unbind);
+
+ rule.bindings = [];
+};
+
+var getRepositioner = function getRepositioner(rule, cy) {
+ var r = rule.reposition;
+
+ if (r === 'mean') {
+ return meanNeighborhoodPosition(getEleMatchesSpecFn(rule.meanIgnores));
+ } else if (r === 'viewport') {
+ return viewportPosition(cy);
+ } else if (r === 'drag') {
+ return dragAlong(rule);
+ } else if (isObject(r)) {
+ if (r.type == undefined || r.type == "inside") {
+ return boxPosition(r);
+ } else if (r.type == "outside") {
+ return outsideBoxPosition(r);
+ }
+ } else {
+ return r;
+ }
+};
+
+var dragAlong = function dragAlong(rule) {
+ return function (node) {
+ var pos = node.position();
+ var delta = rule.delta;
+
+ if (rule.delta != null && !node.same(rule.grabbedNode) && !node.grabbed()) {
+ return {
+ x: pos.x + delta.x,
+ y: pos.y + delta.y
+ };
+ }
+ };
+};
+
+var meanNeighborhoodPosition = function meanNeighborhoodPosition(ignore) {
+ return function (node) {
+ var nhood = node.neighborhood();
+ var avgPos = { x: 0, y: 0 };
+ var nhoodSize = 0;
+
+ for (var i = 0; i < nhood.length; i++) {
+ var nhoodEle = nhood[i];
+
+ if (nhoodEle.isNode() && !ignore(nhoodEle)) {
+ var pos = nhoodEle.position();
+
+ avgPos.x += pos.x;
+ avgPos.y += pos.y;
+
+ nhoodSize++;
+ }
+ }
+
+ // the position should remain unchanged if we would stack the nodes on top of each other
+ if (nhoodSize < 2) {
+ return undefined;
+ }
+
+ avgPos.x /= nhoodSize;
+ avgPos.y /= nhoodSize;
+
+ return avgPos;
+ };
+};
+
+var constrain = function constrain(val, min, max) {
+ return val < min ? min : val > max ? max : val;
+};
+
+var constrainInBox = function constrainInBox(node, bb) {
+ var pos = node.position();
+
+ return {
+ x: constrain(pos.x, bb.x1, bb.x2),
+ y: constrain(pos.y, bb.y1, bb.y2)
+ };
+};
+
+var boxPosition = function boxPosition(bb) {
+ return function (node) {
+ return constrainInBox(node, bb);
+ };
+};
+
+var constrainOutsideBox = function constrainOutsideBox(node, bb) {
+ var pos = node.position();
+ var x = pos.x,
+ y = pos.y;
+ var x1 = bb.x1,
+ y1 = bb.y1,
+ x2 = bb.x2,
+ y2 = bb.y2;
+
+ var inX = x1 <= x && x <= x2;
+ var inY = y1 <= y && y <= y2;
+ var abs = Math.abs;
+
+ if (inX && inY) {
+ // inside
+ var dx1 = abs(x1 - x);
+ var dx2 = abs(x2 - x);
+ var dy1 = abs(y1 - y);
+ var dy2 = abs(y2 - y);
+ var min = Math.min(dx1, dx2, dy1, dy2); // which side of box is closest?
+
+ // get position outside, by closest side of box
+ if (min === dx1) {
+ return { x: x1, y: y };
+ } else if (min === dx2) {
+ return { x: x2, y: y };
+ } else if (min === dy1) {
+ return { x: x, y: y1 };
+ } else {
+ // min === dy2
+ return { x: x, y: y2 };
+ }
+ } else {
+ // outside already
+ return { x: x, y: y };
+ }
+};
+
+var outsideBoxPosition = function outsideBoxPosition(bb) {
+ return function (node) {
+ return constrainOutsideBox(node, bb);
+ };
+};
+
+var viewportPosition = function viewportPosition(cy) {
+ return function (node) {
+ var extent = cy.extent();
+ var w = node.outerWidth();
+ var h = node.outerHeight();
+ var bb = {
+ x1: extent.x1 + w / 2,
+ x2: extent.x2 - w / 2,
+ y1: extent.y1 + h / 2,
+ y2: extent.y2 - h / 2
+ };
+
+ return constrainInBox(node, bb);
+ };
+};
+
+var meanListener = function meanListener(rule) {
+ return function (update, cy) {
+ var matches = function matches(ele) {
+ // must meet ele set and be connected to more than (1 edge + 1 node)
+ return rule.matches(ele) && ele.neighborhood().length > 2 && !ele.grabbed();
+ };
+
+ bindOnRule(rule, cy, 'position', 'node', function () {
+ var movedNode = this;
+
+ if (movedNode.neighborhood().some(matches) || rule.meanOnSelfPosition(movedNode) && matches(movedNode)) {
+ update(cy, [rule]);
+ }
+ });
+
+ bindOnRule(rule, cy, 'add remove', 'edge', function () {
+ var edge = this;
+ var src = cy.getElementById(edge.data('source'));
+ var tgt = cy.getElementById(edge.data('target'));
+
+ if ([src, tgt].some(matches)) {
+ update(cy, [rule]);
+ }
+ });
+ };
+};
+
+var dragListener = function dragListener(rule) {
+ return function (update, cy) {
+ bindOnRule(rule, cy, 'grab', 'node', function () {
+ var node = this;
+
+ if (rule.dragWithMatches(node)) {
+ var p = node.position();
+
+ rule.grabbedNode = node;
+ rule.p1 = { x: p.x, y: p.y };
+ rule.delta = { x: 0, y: 0 };
+ }
+ });
+
+ bindOnRule(rule, cy, 'drag', 'node', function () {
+ var node = this;
+
+ if (node.same(rule.grabbedNode)) {
+ var d = rule.delta;
+ var p1 = rule.p1;
+ var p = node.position();
+ var p2 = { x: p.x, y: p.y };
+
+ d.x = p2.x - p1.x;
+ d.y = p2.y - p1.y;
+
+ rule.p1 = p2;
+
+ update(cy, [rule]);
+ }
+ });
+
+ bindOnRule(rule, cy, 'free', 'node', function () {
+ rule.grabbedNode = null;
+ rule.delta = null;
+ rule.p1 = null;
+ });
+ };
+};
+
+var matchingNodesListener = function matchingNodesListener(rule) {
+ return function (update, cy) {
+ bindOnRule(rule, cy, 'position', 'node', function () {
+ var movedNode = this;
+
+ if (rule.matches(movedNode)) {
+ update(cy, [rule]);
+ }
+ });
+ };
+};
+
+var getListener = function getListener(cy, rule) {
+ if (rule.reposition === 'mean') {
+ return meanListener(rule);
+ } else if (rule.reposition === 'drag') {
+ return dragListener(rule);
+ } else if (isObject(rule.reposition) || rule.when === 'matching' || rule.reposition === 'viewport') {
+ return matchingNodesListener(rule);
+ } else {
+ return rule.when;
+ }
+};
+
+var addRule = function addRule(cy, scratch, options) {
+ var rule = assign({}, defaults, options);
+
+ rule.getNewPos = getRepositioner(rule, cy);
+ rule.listener = getListener(cy, rule);
+
+ var nodesAreCollection = isCollection(rule.nodesMatching);
+
+ if (nodesAreCollection) {
+ rule.nodes = rule.nodesMatching.slice();
+
+ rule.matches = function (ele) {
+ return eleExists(ele) && _elesHasEle(rule.nodes, ele);
+ };
+ } else {
+ var matches = getEleMatchesSpecFn(rule.nodesMatching);
+
+ rule.matches = function (ele) {
+ return eleExists(ele) && matches(ele);
+ };
+ }
+
+ if (rule.dragWith != null) {
+ rule.dragWithMatches = getEleMatchesSpecFn(rule.dragWith);
+ }
+
+ rule.listener(function () {
+ update(cy, [rule]);
+ }, cy);
+
+ rule.enabled = true;
+
+ scratch.rules.push(rule);
+
+ return rule;
+};
+
+var bindForNodeList = function bindForNodeList(cy, scratch) {
+ scratch.onAddNode = function (evt) {
+ var target = evt.target;
+
+ scratch.nodes.merge(target);
+ };
+
+ scratch.onRmNode = function (evt) {
+ var target = evt.target;
+
+ scratch.nodes.unmerge(target);
+ };
+
+ cy.on('add', 'node', scratch.onAddNode);
+ cy.on('remove', 'node', scratch.onRmNode);
+};
+
+var unbindForNodeList = function unbindForNodeList(cy, scratch) {
+ cy.removeListener('add', 'node', scratch.onAddNode);
+ cy.removeListener('remove', 'node', scratch.onRmNode);
+};
+
+var update = function update(cy, rules) {
+ var scratch = cy.scratch().automove;
+
+ rules = rules != null ? rules : scratch.rules;
+
+ cy.batch(function () {
+ // batch for performance
+ for (var i = 0; i < rules.length; i++) {
+ var rule = rules[i];
+
+ if (rule.destroyed || !rule.enabled) {
+ break;
+ } // ignore destroyed rules b/c user may use custom when()
+
+ var nodes = rule.nodes || scratch.nodes;
+
+ for (var j = nodes.length - 1; j >= 0; j--) {
+ var node = nodes[j];
+
+ if (node.removed()) {
+ // remove from list for perf
+ nodes.unmerge(node);
+ continue;
+ }
+
+ if (!rule.matches(node)) {
+ continue;
+ }
+
+ var pos = node.position();
+ var newPos = rule.getNewPos(node);
+ var newPosIsDiff = newPos != null && (pos.x !== newPos.x || pos.y !== newPos.y);
+
+ if (newPosIsDiff) {
+ // only update on diff for perf
+ node.position(newPos);
+
+ node.trigger('automove', [rule]);
+ }
+ }
+ }
+ });
+};
+
+var automove = function automove(options) {
+ var cy = this;
+
+ var scratch = cy.scratch().automove = cy.scratch().automove || {
+ rules: []
+ };
+
+ if (scratch.rules.length === 0) {
+ scratch.nodes = cy.nodes().slice();
+
+ bindForNodeList(cy, scratch);
+ }
+
+ if (options === 'destroy') {
+ scratch.rules.forEach(function (r) {
+ r.destroy();
+ });
+ scratch.rules.splice(0, scratch.rules.length);
+
+ unbindForNodeList(cy, scratch);
+
+ return;
+ }
+
+ var rule = addRule(cy, scratch, options);
+
+ update(cy, [rule]); // do an initial update to make sure the start state is correct
+
+ return {
+ apply: function apply() {
+ update(cy, [rule]);
+ },
+
+ disable: function disable() {
+ this.toggle(false);
+ },
+
+ enable: function enable() {
+ this.toggle(true);
+ },
+
+ enabled: function enabled() {
+ return rule.enabled;
+ },
+
+ toggle: function toggle(on) {
+ rule.enabled = on !== undefined ? on : !rule.enabled;
+
+ if (rule.enabled) {
+ update(cy, [rule]);
+ }
+ },
+
+ destroy: function destroy() {
+ var rules = scratch.rules;
+
+ unbindAllOnRule(rule);
+
+ rule.destroyed = true;
+
+ rules.splice(rules.indexOf(rule), 1);
+
+ if (rules.length === 0) {
+ unbindForNodeList(cy, scratch);
+ }
+
+ return this;
+ }
+ };
+};
+
+module.exports = automove;
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+// Simple, internal Object.assign() polyfill for options objects etc.
+
+module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) {
+ for (var _len = arguments.length, srcs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ srcs[_key - 1] = arguments[_key];
+ }
+
+ srcs.forEach(function (src) {
+ Object.keys(src).forEach(function (k) {
+ return tgt[k] = src[k];
+ });
+ });
+
+ return tgt;
+};
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* eslint-disable no-unused-vars */
+var defaults = {
+ // specify nodes that should be automoved with one of
+ // - a function that returns true for matching nodes
+ // - a selector that matches the nodes
+ // - a collection of nodes (very good for performance)
+ nodesMatching: function nodesMatching(node) {
+ return false;
+ },
+
+ // specify how a node's position should be updated with one of
+ // - function( node ){ return { x: 1, y: 2 }; } => put the node where the function returns
+ // - { x1, y1, x2, y2 } => constrain the node position within the bounding box (in model co-ordinates)
+ // - { x1, y1, x2, y2, type: 'inside' } => constrain the node position within the bounding box (in model co-ordinates)
+ // - { x1, y1, x2, y2, type: 'outside' } => constrain the node position outside the bounding box (in model co-ordinates)
+ // - 'mean' => put the node in the average position of its neighbourhood
+ // - 'viewport' => keeps the node body within the viewport
+ // - 'drag' => matching nodes are effectively dragged along
+ reposition: 'mean',
+
+ // specify when the repositioning should occur by specifying a function that
+ // calls update() when reposition updates should occur
+ // - function( update ){ /* ... */ update(); } => a manual function for updating
+ // - 'matching' => automatically update on position events for nodesMatching
+ // - set efficiently and automatically for
+ // - reposition: 'mean'
+ // - reposition: { x1, y1, x2, y2 }
+ // - reposition: 'viewport'
+ // - reposition: 'drag'
+ // - default/undefined => on a position event for any node (not as efficient...)
+ when: undefined,
+
+ //
+ // customisation options for non-function `reposition` values
+ //
+
+ // `reposition: 'mean'`
+
+ // specify nodes that should be ignored in the mean calculation
+ // - a function that returns true for nodes to be ignored
+ // - a selector that matches the nodes to be ignored
+ // - a collection of nodes to be ignored (very good for performance)
+ meanIgnores: function meanIgnores(node) {
+ return false;
+ },
+
+ // specify whether moving a particular `nodesMatching` node causes repositioning
+ // - true : the mid node can't be independently moved/dragged
+ // - false : the mid node can be independently moved/dragged (useful if you want the mid node to use `reposition: 'drag' in another rule with its neighbourhood`)
+ meanOnSelfPosition: function meanOnSelfPosition(node) {
+ return true;
+ },
+
+ // `reposition: 'drag'`
+
+ // specify nodes that when dragged cause the matched nodes to move along (i.e. the master nodes)
+ // - a function that returns true for nodes to be listened to for drag events
+ // - a selector that matches the nodes to be listened to for drag events
+ // - a collection of nodes to be listened to for drag events (very good for performance)
+ dragWith: function dragWith(node) {
+ return false;
+ }
+};
+
+/* eslint-enable */
+
+module.exports = defaults;
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var automove = __webpack_require__(0);
+
+// registers the extension on a cytoscape lib ref
+var register = function register(cytoscape) {
+ if (!cytoscape) {
+ return;
+ } // can't register if cytoscape unspecified
+
+ cytoscape('core', 'automove', automove); // register with cytoscape.js
+};
+
+if (typeof cytoscape !== 'undefined') {
+ // expose to global cytoscape (i.e. window.cytoscape)
+ register(cytoscape);
+}
+
+module.exports = register;
+
+/***/ })
+/******/ ]);
+});
\ No newline at end of file