docs: function ngraphSvg(graph, settings) {
    settings = settings || {};
  
    var layout = require('./lib/defaultLayout.js')(graph, settings);
  
    var scrollSpeed = typeof settings.scrollSpeed === 'number' ?
        settings.scrollSpeed : 0.1;
    var container = settings.container || document.body;
  
    var isStable = false;
    var disposed = false;
    var sceneInitialized = false;
  
    var svgRoot = createSvgRoot(container);
    var sceneRoot = createSceneRoot(svgRoot);
    var sceneTransform = createSceneTransform(sceneRoot);
    var panSession = {};
    var panNode = 0;
  
    var linkLayer = addLayer('links', sceneRoot);
    var nodeLayer = addLayer('nodes', sceneRoot);
  
    var nodes = Object.create(null);
    var links = Object.create(null);
  
    var fromX = 0, fromY = 0;
    var screenPinchX, screenPinchY, lastScale;
    var cachedPos = { x: 0, y: 0 },
      cachedFromPos = { x: 0, y: 0 },
      cachedToPos = { x: 0, y: 0 };
  
    var defaultUI = require('./lib/defaultUI.js');
    var nodeBuilder = defaultUI.nodeBuilder,
      nodePositionCallback = defaultUI.nodePositionCallback,
      linkBuilder = defaultUI.linkBuilder,
      linkPositionCallback = defaultUI.linkPositionCallback;
  
    var currentTransform = {
      tx : 0,
      ty : 0,
      scale: 1
    };
  
    var api = {
      run: animationLoop,
      renderOneFrame: renderOneFrame,
      layout: layout,
  
      dispose: dispose,
  
      node: setNodeBuilder,
  
      link: setLinkBuilder,
  
      placeNode: placeNode,
  
      placeLink: placeLink,
  
      svgRoot: svgRoot,
  
      resetStable: resetStable
    };
  
    require('ngraph.events')(api);
  
    return api;
  
    function dispose() {
      layout.dispose();
      api.off();
      disposed = true;
      listenToGraphEvents(false);
      releaseDOMEvents();
    }
  
    function placeLink(newPlaceLinkCallback) {
      linkPositionCallback = newPlaceLinkCallback;
      return api;
    }
  
    function placeNode(newPlaceCallback) {
      nodePositionCallback = newPlaceCallback;
      return api;
    }
  
    function animationLoop() {
      if (disposed) return;
      requestAnimationFrame(animationLoop);
  
      if (!isStable) {
        isStable = layout.step();
        renderOneFrame();
      }
    }
  
    function renderOneFrame() {
      if (disposed) return;
      if (!sceneInitialized) initializeScene();
  
      for (var nodeId in nodes) {
        var nodeInfo = nodes[nodeId];
        cachedPos.x = nodeInfo.pos.x;
        cachedPos.y = nodeInfo.pos.y;
        nodePositionCallback(nodeInfo.ui, cachedPos, nodeInfo.model);
      }
  
      for (var linkId in links) {
        var linkInfo = links[linkId];
        cachedFromPos.x = linkInfo.pos.from.x;
        cachedFromPos.y = linkInfo.pos.from.y;
        cachedToPos.x = linkInfo.pos.to.x;
        cachedToPos.y = linkInfo.pos.to.y;
        linkPositionCallback(linkInfo.ui, cachedFromPos, cachedToPos, linkInfo.model);
      }
    }
  
    function initializeScene() {
      graph.forEachNode(addNode);
      graph.forEachLink(addLink);
  
      moveTo(container.clientWidth / 2, container.clientHeight / 2);
  
      listenToGraphEvents(true);
      sceneInitialized = true;
    }
  
    function addLink(link) {
      var linkUI = linkBuilder(link);
      if (!linkUI) throw new Error('Link builder is supposed to return SVG object');
  
      links[link.id] = {
        pos: layout.getLinkPosition(link.id),
        model: link,
        ui: linkUI
      };
  
      linkLayer.append(linkUI);
    }
  
    function addNode(node) {
      var ui = nodeBuilder(node);
      if (!ui) throw new Error('Node builder is supposed to return SVG object');
  
      nodeLayer.append(ui);
  
      var pos = layout.getNodePosition(node.id);
      var nodeDescriptor = {
        pos: pos,
        model: node,
        ui: ui
      };
  
      var recognizers = { recognizers: [
        [hammer.Pan, { threshold: 1 }]
      ] };
      nodeDescriptor.events = hammer(ui, recognizers).on(MOVE_EVENTS, onNodePan(pos, node));
      nodes[node.id] = nodeDescriptor;
    }
  
    function setLinkBuilder(builderCallback) {
      if (typeof builderCallback !== "function") throw new Error('link builder callback is supposed to be a function');
  
      linkBuilder = builderCallback; // todo: rebuild all nodes?
      return api;
    }
  
    function setNodeBuilder(builderCallback) {
      if (typeof builderCallback !== "function") throw new Error('node builder callback is supposed to be a function');
  
      nodeBuilder = builderCallback; // todo: rebuild all nodes?
      return api;
    }
  
    function listenToGraphEvents(isOn) {
      graph[isOn ? 'on' : 'off']('changed', onGraphChanged);
    }
  
    function onGraphChanged(changes) {
      resetStable();
  
      for (var i = 0; i < changes.length; ++i) {
        var change = changes[i];
        if (change.changeType === 'add') {
          if (change.node) {
            addNode(change.node);
          }
          if (change.link) {
            addLink(change.link);
          }
        } else if (change.changeType === 'remove') {
          if (change.node) {
            removeNode(change.node);
          }
          if (change.link) {
            removeLink(change.link);
          }
        }
      }
    }
  
    function resetStable() {
      isStable = false;
    }
  
  
    function createSvgRoot(element) {
      if (element instanceof SVGSVGElement) return element;
      var svgRoot = svg("svg");
      element.appendChild(svgRoot);
  
      return svgRoot;
    }
  
    function createSceneRoot(svgRoot) {
      var scene = svg('g').attr("buffered-rendering", "dynamic");
      svgRoot.append(scene);
  
      var sceneMoveRecognizer = { recognizers: [
        [hammer.Pan, { threshold: 1 }],
        [hammer.Pinch, { enable: true }]
      ] };
  
      // somehow ios does not fire events on svg. Use body instead:
      hammer(container, sceneMoveRecognizer)
        .on(MOVE_EVENTS, onScenePan)
        .on('pinchstart pinchin pinchout', onScreenPinch);
  
      var addWheelListener = require('wheel');
      addWheelListener(svgRoot, onWheel);
      return scene;
    }
  
  
    function onScenePan(e) {
      if (e.target !== svgRoot || panNode > 0) return;
      if (e.type === 'panmove') {
        currentTransform.tx = fromX + e.deltaX;
        currentTransform.ty = fromY + e.deltaY;
        updateTransformMatrix();
      } else if (e.type === 'panstart') {
        fromX = currentTransform.tx;
        fromY = currentTransform.ty;
      }
    }
  
    function onWheel(e) {
      var isZoomIn = e.deltaY < 0;
      var direction = isZoomIn ? 1 : -1;
      var factor = (1 + direction * scrollSpeed);
      var x = e.offsetX === undefined ? e.layerX : e.offsetX;
      var y = e.offsetY === undefined ? e.layerY : e.offsetY;
      zoomTo(x, y, factor);
      e.preventDefault();
    }
  
    function zoomTo(x, y, factor) {
      currentTransform.tx = x - factor * (x - currentTransform.tx);
      currentTransform.ty = y - factor * (y - currentTransform.ty);
      currentTransform.scale *= factor;
      updateTransformMatrix();
    }
  
  
    function addLayer(name, parent) {
      var layer = svg('g').attr('id', name);
      parent.append(layer);
      return layer;
    }
  
  
    function getModelPosition(pos) {
      return {
        x: (pos.x - currentTransform.tx)/currentTransform.scale,
        y: (pos.y - currentTransform.ty)/currentTransform.scale,
      };
    }
    function createSceneTransform(scene) {
      var transform = svgRoot.createSVGTransform();
      scene.transform.baseVal.appendItem(transform);
  
      return transform;
    }
  
    function moveTo(x, y) {
      currentTransform.tx = x;
      currentTransform.ty = y;
      updateTransformMatrix();
    }
  
    function updateTransformMatrix() {
      sceneTransform.matrix.e = currentTransform.tx;
      sceneTransform.matrix.f = currentTransform.ty;
      sceneTransform.matrix.a = sceneTransform.matrix.d = currentTransform.scale;
    }
  
    function onNodePan(pos, model) {
      return function onNodePan(e) {
        var clickPosition = getModelPosition(e.center);
        var status;
        resetStable();
  
        if (e.type === 'panmove') {
          status = panSession[model.id];
          layout.setNodePosition(model.id, clickPosition.x - status.dx , clickPosition.y - status.dy);
        } else if (e.type === 'panstart') {
          panSession[model.id] = {
            isPinned: layout.isNodePinned(model),
            dx: clickPosition.x - pos.x,
            dy: clickPosition.y - pos.y
          };
          layout.pinNode(model, true);
          panNode += 1;
        } else if (e.type === 'panend') {
          status = panSession[model.id];
          if (status) layout.pinNode(model, status.isPinned);
  
          panNode -= 1;
          if (panNode < 0) panNode = 0;
        }
      };
    }
  
    function releaseDOMEvents() {
      for (var key in nodes) {
        var descriptor = nodes[key];
        if (descriptor.events) descriptor.events.destroy();
      }
    }
  
    function removeNode(node) {
      var descriptor = nodes[node.id];
      if (!descriptor) return;
  
      descriptor.events.destroy();
  
      var parent = descriptor.ui.parentNode;
      if (parent) parent.removeChild(descriptor.ui);
  
      delete nodes[node.id];
    }
  
    function removeLink(link) {
      var descriptor = links[link.id];
      if (!descriptor) return;
  
      var parent = descriptor.ui.parentNode;
      if (parent) parent.removeChild(descriptor.ui);
  
      delete links[link.id];
    }
  
  
    function onScreenPinch(e) {
      if (e.target !== svgRoot) return;
  
      if (e.type === 'pinchstart') {
        screenPinchX = e.center.x;
        screenPinchY = e.center.y;
        lastScale = e.scale;
      } else {
        var direction = lastScale > e.scale ? -1 : 1;
        lastScale = e.scale;
        var factor = (1 + direction * 0.04);
        zoomTo(screenPinchX, screenPinchY, factor);
      }
    }
  }