dgate 2.1.0

DGate API Gateway - High-performance API gateway with JavaScript module support
Documentation
import * as util from '../../util';
import * as math from '../../math';
import * as is from '../../is';

/* eslint-disable no-unused-vars */
const defaults = {
  fit: true, // whether to fit the viewport to the graph
  directed: false, // whether the tree is directed downwards (or edges can point in any direction if false)
  padding: 30, // padding on fit
  circle: false, // put depths in concentric circles if true, put depths top down if false
  grid: false, // whether to create an even grid into which the DAG is placed (circle:false only)
  spacingFactor: 1.75, // positive spacing factor, larger => more space between nodes (N.B. n/a if causes overlap)
  boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
  avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
  nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
  roots: undefined, // the roots of the trees
  depthSort: undefined, // a sorting function to order nodes at equal depth. e.g. function(a, b){ return a.data('weight') - b.data('weight') }
  animate: false, // whether to transition the node positions
  animationDuration: 500, // duration of animation in ms if enabled
  animationEasing: undefined, // easing of animation if enabled,
  animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated.  All nodes animated by default on animate enabled.  Non-animated nodes are positioned immediately when the layout starts
  ready: undefined, // callback on layoutready
  stop: undefined, // callback on layoutstop
  transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};

const deprecatedOptionDefaults = {
  maximal: false, // whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only); setting acyclic to true sets maximal to true also
  acyclic: false, // whether the tree is acyclic and thus a node could be shifted (due to the maximal option) multiple times without causing an infinite loop; setting to true sets maximal to true also; if you are uncertain whether a tree is acyclic, set to false to avoid potential infinite loops
};

/* eslint-enable */

const getInfo = ele => ele.scratch('breadthfirst');
const setInfo = (ele, obj) => ele.scratch('breadthfirst', obj);

function BreadthFirstLayout( options ){
  this.options = util.extend( {}, defaults, deprecatedOptionDefaults, options );
}

BreadthFirstLayout.prototype.run = function(){
  let params = this.options;
  let options = params;

  let cy = params.cy;
  let eles = options.eles;
  let nodes = eles.nodes().filter( n => !n.isParent() );
  let graph = eles;
  let directed = options.directed;
  let maximal = options.acyclic || options.maximal || options.maximalAdjustments > 0; // maximalAdjustments for compat. w/ old code; also, setting acyclic to true sets maximal to true

  let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
    x1: 0, y1: 0, w: cy.width(), h: cy.height()
  } );

  let roots;
  if( is.elementOrCollection( options.roots ) ){
    roots = options.roots;
  } else if( is.array( options.roots ) ){
    let rootsArray = [];

    for( let i = 0; i < options.roots.length; i++ ){
      let id = options.roots[ i ];
      let ele = cy.getElementById( id );
      rootsArray.push( ele );
    }

    roots = cy.collection( rootsArray );
  } else if( is.string( options.roots ) ){
    roots = cy.$( options.roots );

  } else {
    if( directed ){
      roots = nodes.roots();
    } else {
      let components = eles.components();

      roots = cy.collection();
      for( let i = 0; i < components.length; i++ ){
        let comp = components[i];
        let maxDegree = comp.maxDegree( false );
        let compRoots = comp.filter( function( ele ){
          return ele.degree( false ) === maxDegree;
        } );

        roots = roots.add( compRoots );
      }

    }
  }

  let depths = [];
  let foundByBfs = {};

  let addToDepth = ( ele, d ) => {
    if( depths[d] == null ){
      depths[d] = [];
    }

    let i = depths[d].length;

    depths[d].push( ele );

    setInfo( ele, {
      index: i,
      depth: d
    } );
  };

  let changeDepth = ( ele, newDepth ) => {
    let { depth, index } = getInfo( ele );

    depths[ depth ][ index ] = null;

    addToDepth( ele, newDepth );
  };

  // find the depths of the nodes
  graph.bfs( {
    roots: roots,
    directed: options.directed,
    visit: function( node, edge, pNode, i, depth ){
      let ele = node[0];
      let id = ele.id();

      addToDepth( ele, depth );
      foundByBfs[ id ] = true;
    }
  } );

  // check for nodes not found by bfs
  let orphanNodes = [];
  for( let i = 0; i < nodes.length; i++ ){
    let ele = nodes[ i ];

    if( foundByBfs[ ele.id() ] ){
      continue;
    } else {
      orphanNodes.push( ele );
    }
  }

  // assign the nodes a depth and index

  let assignDepthsAt = function( i ){
    let eles = depths[ i ];

    for( let j = 0; j < eles.length; j++ ){
      let ele = eles[ j ];

      if( ele == null ){
        eles.splice( j, 1 );
        j--;
        continue;
      }

      setInfo(ele, {
        depth: i,
        index: j
      });
    }
  };

  let assignDepths = function(){
    for( let i = 0; i < depths.length; i++ ){
      assignDepthsAt( i );
    }
  };

  let adjustMaximally = function( ele, shifted ){
    let eInfo = getInfo( ele );
    let incomers = ele.incomers().filter( el => el.isNode() && eles.has(el) );
    let maxDepth = -1;
    let id = ele.id();

    for( let k = 0; k < incomers.length; k++ ){
      let incmr = incomers[k];
      let iInfo = getInfo( incmr );

      maxDepth = Math.max( maxDepth, iInfo.depth );
    }

    if( eInfo.depth <= maxDepth ){
      if( !options.acyclic && shifted[id] ){
        return null;
      }

      let newDepth = maxDepth + 1;
      changeDepth( ele, newDepth );
      shifted[id] = newDepth;

      return true;
    }

    return false;
  };

  // for the directed case, try to make the edges all go down (i.e. depth i => depth i + 1)
  if( directed && maximal ){
    let Q = [];
    let shifted = {};

    let enqueue = n => Q.push(n);
    let dequeue = () => Q.shift();

    nodes.forEach( n => Q.push(n) );

    while( Q.length > 0 ){
      let ele = dequeue();
      let didShift = adjustMaximally( ele, shifted );

      if( didShift ){
        ele.outgoers().filter( el => el.isNode() && eles.has(el) ).forEach( enqueue );
      } else if( didShift === null ){
        util.warn('Detected double maximal shift for node `' + ele.id() + '`.  Bailing maximal adjustment due to cycle.  Use `options.maximal: true` only on DAGs.');

        break; // exit on failure
      }
    }
  }

  assignDepths(); // clear holes

  // find min distance we need to leave between nodes
  let minDistance = 0;
  if( options.avoidOverlap ){
    for( let i = 0; i < nodes.length; i++ ){
      let n = nodes[ i ];
      let nbb = n.layoutDimensions( options );
      let w = nbb.w;
      let h = nbb.h;

      minDistance = Math.max( minDistance, w, h );
    }
  }

  // get the weighted percent for an element based on its connectivity to other levels
  let cachedWeightedPercent = {};
  let getWeightedPercent = function( ele ){
    if( cachedWeightedPercent[ ele.id() ] ){
      return cachedWeightedPercent[ ele.id() ];
    }

    let eleDepth = getInfo( ele ).depth;
    let neighbors = ele.neighborhood();
    let percent = 0;
    let samples = 0;

    for( let i = 0; i < neighbors.length; i++ ){
      let neighbor = neighbors[ i ];

      if( neighbor.isEdge() || neighbor.isParent() || !nodes.has( neighbor ) ){
        continue;
      }

      let bf = getInfo( neighbor );

      if (bf == null){ continue; }

      let index = bf.index;
      let depth = bf.depth;

      // unassigned neighbours shouldn't affect the ordering
      if( index == null || depth == null ){
        continue;
      }

      let nDepth = depths[ depth ].length;

      if( depth < eleDepth ){ // only get influenced by elements above
        percent += index / nDepth;
        samples++;
      }
    }

    samples = Math.max( 1, samples );
    percent = percent / samples;

    if( samples === 0 ){ // put lone nodes at the start
      percent = 0;
    }

    cachedWeightedPercent[ ele.id() ] = percent;
    return percent;
  };


  // rearrange the indices in each depth level based on connectivity

  let sortFn = function( a, b ){
    let apct = getWeightedPercent( a );
    let bpct = getWeightedPercent( b );

    let diff = apct - bpct;

    if( diff === 0 ){
      return util.sort.ascending( a.id(), b.id() ); // make sure sort doesn't have don't-care comparisons
    } else {
      return diff;
    }
  };

  if (options.depthSort !== undefined) {
    sortFn = options.depthSort;
  }

  // sort each level to make connected nodes closer
  for( let i = 0; i < depths.length; i++ ){
    depths[ i ].sort( sortFn );
    assignDepthsAt( i );
  }

  // assign orphan nodes to a new top-level depth
  let orphanDepth = [];
  for( let i = 0; i < orphanNodes.length; i++ ){
    orphanDepth.push( orphanNodes[i] );
  }
  depths.unshift( orphanDepth );

  assignDepths();

  let biggestDepthSize = 0;
  for( let i = 0; i < depths.length; i++ ){
    biggestDepthSize = Math.max( depths[ i ].length, biggestDepthSize );
  }

  let center = {
    x: bb.x1 + bb.w / 2,
    y: bb.x1 + bb.h / 2
  };

  let maxDepthSize = depths.reduce( (max, eles) => Math.max(max, eles.length), 0 );

  let getPosition = function( ele ){
    let { depth, index } = getInfo( ele );
    let depthSize = depths[ depth ].length;

    let distanceX = Math.max( bb.w / ( (options.grid ? maxDepthSize : depthSize) + 1 ), minDistance );
    let distanceY = Math.max( bb.h / (depths.length + 1), minDistance );
    let radiusStepSize = Math.min( bb.w / 2 / depths.length, bb.h / 2 / depths.length );
    radiusStepSize = Math.max( radiusStepSize, minDistance );

    if( !options.circle ){
      let epos = {
        x: center.x + (index + 1 - (depthSize + 1) / 2) * distanceX,
        y: (depth + 1) * distanceY
      };

      return epos;
    } else {
      let radius = radiusStepSize * depth + radiusStepSize - (depths.length > 0 && depths[0].length <= 3 ? radiusStepSize / 2 : 0);
      let theta = 2 * Math.PI / depths[ depth ].length * index;

      if( depth === 0 && depths[0].length === 1 ){
        radius = 1;
      }

      return {
        x: center.x + radius * Math.cos( theta ),
        y: center.y + radius * Math.sin( theta )
      };
    }

  };

  eles.nodes().layoutPositions( this, options, getPosition );

  return this; // chaining
};

export default BreadthFirstLayout;