+165
@@ -0,0 +1,165 @@
|
||||
import Heap from '../../heap.mjs';
|
||||
import Set from '../../set.mjs';
|
||||
import { defaults } from '../../util/index.mjs';
|
||||
|
||||
const aStarDefaults = defaults({
|
||||
root: null,
|
||||
goal: null,
|
||||
weight: edge => 1,
|
||||
heuristic: edge => 0,
|
||||
directed: false
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
|
||||
// Implemented from pseudocode from wikipedia
|
||||
aStar: function( options ){
|
||||
let cy = this.cy();
|
||||
let { root, goal, heuristic, directed, weight } = aStarDefaults(options);
|
||||
|
||||
root = cy.collection(root)[0];
|
||||
goal = cy.collection(goal)[0];
|
||||
|
||||
let sid = root.id();
|
||||
let tid = goal.id();
|
||||
|
||||
let gScore = {};
|
||||
let fScore = {};
|
||||
let closedSetIds = {};
|
||||
let openSet = new Heap( (a, b) => fScore[a.id()] - fScore[b.id()] );
|
||||
let openSetIds = new Set();
|
||||
let cameFrom = {};
|
||||
let cameFromEdge = {};
|
||||
|
||||
let addToOpenSet = (ele, id) => {
|
||||
openSet.push(ele);
|
||||
openSetIds.add(id);
|
||||
};
|
||||
|
||||
let cMin, cMinId;
|
||||
|
||||
let popFromOpenSet = () => {
|
||||
cMin = openSet.pop();
|
||||
cMinId = cMin.id();
|
||||
openSetIds.delete(cMinId);
|
||||
};
|
||||
|
||||
let isInOpenSet = id => openSetIds.has(id);
|
||||
|
||||
addToOpenSet(root, sid);
|
||||
|
||||
gScore[ sid ] = 0;
|
||||
fScore[ sid ] = heuristic( root );
|
||||
|
||||
// Counter
|
||||
let steps = 0;
|
||||
|
||||
// Main loop
|
||||
while( openSet.size() > 0 ){
|
||||
popFromOpenSet();
|
||||
steps++;
|
||||
|
||||
// If we've found our goal, then we are done
|
||||
if( cMinId === tid ){
|
||||
let path = [];
|
||||
let pathNode = goal;
|
||||
let pathNodeId = tid;
|
||||
let pathEdge = cameFromEdge[pathNodeId];
|
||||
|
||||
for( ;; ){
|
||||
path.unshift(pathNode);
|
||||
|
||||
if( pathEdge != null ){
|
||||
path.unshift(pathEdge);
|
||||
}
|
||||
|
||||
pathNode = cameFrom[pathNodeId];
|
||||
|
||||
if( pathNode == null ){ break; }
|
||||
|
||||
pathNodeId = pathNode.id();
|
||||
pathEdge = cameFromEdge[pathNodeId];
|
||||
}
|
||||
|
||||
return {
|
||||
found: true,
|
||||
distance: gScore[ cMinId ],
|
||||
path: this.spawn( path ),
|
||||
steps
|
||||
};
|
||||
}
|
||||
|
||||
// Add cMin to processed nodes
|
||||
closedSetIds[ cMinId ] = true;
|
||||
|
||||
// Update scores for neighbors of cMin
|
||||
// Take into account if graph is directed or not
|
||||
let vwEdges = cMin._private.edges;
|
||||
|
||||
for( let i = 0; i < vwEdges.length; i++ ){
|
||||
let e = vwEdges[ i ];
|
||||
|
||||
// edge must be in set of calling eles
|
||||
if( !this.hasElementWithId( e.id() ) ){ continue; }
|
||||
|
||||
// cMin must be the source of edge if directed
|
||||
if( directed && e.data('source') !== cMinId ){ continue; }
|
||||
|
||||
let wSrc = e.source();
|
||||
let wTgt = e.target();
|
||||
|
||||
let w = wSrc.id() !== cMinId ? wSrc : wTgt;
|
||||
let wid = w.id();
|
||||
|
||||
// node must be in set of calling eles
|
||||
if( !this.hasElementWithId( wid ) ){ continue; }
|
||||
|
||||
// if node is in closedSet, ignore it
|
||||
if( closedSetIds[ wid ] ){
|
||||
continue;
|
||||
}
|
||||
|
||||
// New tentative score for node w
|
||||
let tempScore = gScore[ cMinId ] + weight( e );
|
||||
|
||||
// Update gScore for node w if:
|
||||
// w not present in openSet
|
||||
// OR
|
||||
// tentative gScore is less than previous value
|
||||
|
||||
// w not in openSet
|
||||
if( !isInOpenSet(wid) ){
|
||||
gScore[ wid ] = tempScore;
|
||||
fScore[ wid ] = tempScore + heuristic( w );
|
||||
addToOpenSet( w, wid );
|
||||
cameFrom[ wid ] = cMin;
|
||||
cameFromEdge[ wid ] = e;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// w already in openSet, but with greater gScore
|
||||
if( tempScore < gScore[ wid ] ){
|
||||
gScore[ wid ] = tempScore;
|
||||
fScore[ wid ] = tempScore + heuristic( w );
|
||||
cameFrom[ wid ] = cMin;
|
||||
cameFromEdge[ wid ] = e;
|
||||
}
|
||||
|
||||
} // End of neighbors update
|
||||
|
||||
} // End of main loop
|
||||
|
||||
// If we've reached here, then we've not reached our goal
|
||||
return {
|
||||
found: false,
|
||||
distance: undefined,
|
||||
path: undefined,
|
||||
steps: steps
|
||||
};
|
||||
}
|
||||
|
||||
}); // elesfn
|
||||
|
||||
|
||||
export default elesfn;
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
// Implemented by Zoe Xi @zoexi for GSOC 2016
|
||||
// https://github.com/cytoscape/cytoscape.js-affinity-propagation
|
||||
|
||||
// Implemented from the reference library: https://github.com/juhis/affinity-propagation
|
||||
// Additional reference: http://www.psi.toronto.edu/affinitypropagation/faq.html
|
||||
|
||||
import * as util from '../../util/index.mjs';
|
||||
import * as math from '../../math.mjs';
|
||||
import * as is from '../../is.mjs';
|
||||
import clusteringDistance from './clustering-distances.mjs';
|
||||
|
||||
let defaults = util.defaults({
|
||||
distance: 'euclidean', // distance metric to compare attributes between two nodes
|
||||
preference: 'median', // suitability of a data point to serve as an exemplar
|
||||
damping: 0.8, // damping factor between [0.5, 1)
|
||||
maxIterations: 1000, // max number of iterations to run
|
||||
minIterations: 100, // min number of iterations to run in order for clustering to stop
|
||||
attributes: [ // functions to quantify the similarity between any two points
|
||||
// e.g. node => node.data('weight')
|
||||
]
|
||||
});
|
||||
|
||||
let setOptions = function( options ) {
|
||||
let dmp = options.damping;
|
||||
let pref = options.preference;
|
||||
|
||||
if( !(0.5 <= dmp && dmp < 1) ){
|
||||
util.error(`Damping must range on [0.5, 1). Got: ${dmp}`);
|
||||
}
|
||||
|
||||
let validPrefs = ['median', 'mean', 'min', 'max'];
|
||||
if( !( validPrefs.some(v => v === pref) || is.number(pref) ) ){
|
||||
util.error(`Preference must be one of [${validPrefs.map( p => `'${p}'` ).join(', ')}] or a number. Got: ${pref}`);
|
||||
}
|
||||
|
||||
return defaults( options );
|
||||
};
|
||||
|
||||
if( process.env.NODE_ENV !== 'production' ){ /* eslint-disable no-console, no-unused-vars */
|
||||
var printMatrix = function( M ) { // used for debugging purposes only
|
||||
let str = '';
|
||||
let log = s => str = str + s + '\n';
|
||||
let n = Math.sqrt(M.length);
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
let row = '';
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
row += M[i*n+j] + ' ';
|
||||
}
|
||||
log(row);
|
||||
}
|
||||
|
||||
console.log(str);
|
||||
};
|
||||
} /* eslint-enable */
|
||||
|
||||
let getSimilarity = function( type, n1, n2, attributes ) {
|
||||
let attr = (n, i) => attributes[i](n);
|
||||
|
||||
// nb negative because similarity should have an inverse relationship to distance
|
||||
return -clusteringDistance( type, attributes.length, i => attr(n1, i), i => attr(n2, i), n1, n2 );
|
||||
};
|
||||
|
||||
let getPreference = function( S, preference ) { // larger preference = greater # of clusters
|
||||
let p = null;
|
||||
|
||||
if( preference === 'median' ){
|
||||
p = math.median( S );
|
||||
} else if( preference === 'mean' ){
|
||||
p = math.mean( S );
|
||||
} else if ( preference === 'min' ){
|
||||
p = math.min( S );
|
||||
} else if ( preference === 'max' ){
|
||||
p = math.max( S );
|
||||
} else { // Custom preference number, as set by user
|
||||
p = preference;
|
||||
}
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
let findExemplars = function( n, R, A ) {
|
||||
let indices = [];
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
if ( R[i * n + i] + A[i * n + i] > 0 ) {
|
||||
indices.push(i);
|
||||
}
|
||||
}
|
||||
return indices;
|
||||
};
|
||||
|
||||
let assignClusters = function( n, S, exemplars ) {
|
||||
let clusters = [];
|
||||
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
let index = -1;
|
||||
let max = -Infinity;
|
||||
|
||||
for ( let ei = 0; ei < exemplars.length; ei++ ) {
|
||||
let e = exemplars[ei];
|
||||
if ( S[i * n + e] > max ) {
|
||||
index = e;
|
||||
max = S[i * n + e];
|
||||
}
|
||||
}
|
||||
|
||||
if( index > 0 ){
|
||||
clusters.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
for (let ei = 0; ei < exemplars.length; ei++) {
|
||||
clusters[ exemplars[ei] ] = exemplars[ei];
|
||||
}
|
||||
|
||||
return clusters;
|
||||
};
|
||||
|
||||
let assign = function( n, S, exemplars ) {
|
||||
|
||||
let clusters = assignClusters( n, S, exemplars );
|
||||
|
||||
for ( let ei = 0; ei < exemplars.length; ei++ ) {
|
||||
|
||||
let ii = [];
|
||||
for ( let c = 0; c < clusters.length; c++ ) {
|
||||
if (clusters[c] === exemplars[ei]) {
|
||||
ii.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
let maxI = -1;
|
||||
let maxSum = -Infinity;
|
||||
for ( let i = 0; i < ii.length; i++ ) {
|
||||
let sum = 0;
|
||||
for ( let j = 0; j < ii.length; j++ ) {
|
||||
sum += S[ii[j] * n + ii[i]];
|
||||
}
|
||||
if ( sum > maxSum ) {
|
||||
maxI = i;
|
||||
maxSum = sum;
|
||||
}
|
||||
}
|
||||
|
||||
exemplars[ei] = ii[maxI];
|
||||
}
|
||||
|
||||
clusters = assignClusters( n, S, exemplars );
|
||||
|
||||
return clusters;
|
||||
};
|
||||
|
||||
let affinityPropagation = function( options ) {
|
||||
let cy = this.cy();
|
||||
let nodes = this.nodes();
|
||||
let opts = setOptions( options );
|
||||
|
||||
// Map each node to its position in node array
|
||||
let id2position = {};
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
id2position[ nodes[i].id() ] = i;
|
||||
}
|
||||
|
||||
// Begin affinity propagation algorithm
|
||||
|
||||
let n; // number of data points
|
||||
let n2; // size of matrices
|
||||
let S; // similarity matrix (1D array)
|
||||
let p; // preference/suitability of a data point to serve as an exemplar
|
||||
let R; // responsibility matrix (1D array)
|
||||
let A; // availability matrix (1D array)
|
||||
|
||||
n = nodes.length;
|
||||
n2 = n * n;
|
||||
|
||||
// Initialize and build S similarity matrix
|
||||
S = new Array(n2);
|
||||
for ( let i = 0; i < n2; i++ ) {
|
||||
S[i] = -Infinity; // for cases where two data points shouldn't be linked together
|
||||
}
|
||||
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
if ( i !== j ) {
|
||||
S[i * n + j] = getSimilarity( opts.distance, nodes[i], nodes[j], opts.attributes );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Place preferences on the diagonal of S
|
||||
p = getPreference( S, opts.preference );
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
S[i * n + i] = p;
|
||||
}
|
||||
|
||||
// Initialize R responsibility matrix
|
||||
R = new Array(n2);
|
||||
for ( let i = 0; i < n2; i++ ) {
|
||||
R[i] = 0.0;
|
||||
}
|
||||
|
||||
// Initialize A availability matrix
|
||||
A = new Array(n2);
|
||||
for ( let i = 0; i < n2; i++ ) {
|
||||
A[i] = 0.0;
|
||||
}
|
||||
|
||||
let old = new Array(n);
|
||||
let Rp = new Array(n);
|
||||
let se = new Array(n);
|
||||
|
||||
for ( let i = 0; i < n; i ++ ) {
|
||||
old[i] = 0.0;
|
||||
Rp[i] = 0.0;
|
||||
se[i] = 0;
|
||||
}
|
||||
|
||||
let e = new Array(n * opts.minIterations);
|
||||
for ( let i = 0; i < e.length; i++ ) {
|
||||
e[i] = 0;
|
||||
}
|
||||
|
||||
let iter;
|
||||
for ( iter = 0; iter < opts.maxIterations; iter++ ) { // main algorithmic loop
|
||||
|
||||
// Update R responsibility matrix
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
|
||||
let max = -Infinity,
|
||||
max2 = -Infinity,
|
||||
maxI = -1,
|
||||
AS = 0.0;
|
||||
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
|
||||
old[j] = R[i * n + j];
|
||||
|
||||
AS = A[i * n + j] + S[i * n + j];
|
||||
if ( AS >= max ) {
|
||||
max2 = max;
|
||||
max = AS;
|
||||
maxI = j;
|
||||
}
|
||||
else if ( AS > max2 ) {
|
||||
max2 = AS;
|
||||
}
|
||||
}
|
||||
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
R[i * n + j] = (1 - opts.damping) * (S[i * n + j] - max) + opts.damping * old[j];
|
||||
}
|
||||
|
||||
R[i * n + maxI] = (1 - opts.damping) * (S[i * n + maxI] - max2) + opts.damping * old[maxI];
|
||||
}
|
||||
|
||||
// Update A availability matrix
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
let sum = 0;
|
||||
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
old[j] = A[j * n + i];
|
||||
Rp[j] = Math.max(0, R[j * n + i]);
|
||||
sum += Rp[j];
|
||||
}
|
||||
|
||||
sum -= Rp[i];
|
||||
Rp[i] = R[i * n + i];
|
||||
sum += Rp[i];
|
||||
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
A[j * n + i] = (1 - opts.damping) * Math.min(0, sum - Rp[j]) + opts.damping * old[j];
|
||||
}
|
||||
A[i * n + i] = (1 - opts.damping) * (sum - Rp[i]) + opts.damping * old[i];
|
||||
}
|
||||
|
||||
// Check for convergence
|
||||
let K = 0;
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
let E = A[i * n + i] + R[i * n + i] > 0 ? 1 : 0;
|
||||
e[(iter % opts.minIterations) * n + i] = E;
|
||||
K += E;
|
||||
}
|
||||
|
||||
if ( K > 0 && (iter >= opts.minIterations - 1 || iter == opts.maxIterations - 1) ) {
|
||||
|
||||
let sum = 0;
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
se[i] = 0;
|
||||
for ( let j = 0; j < opts.minIterations; j++ ) {
|
||||
se[i] += e[j * n + i];
|
||||
}
|
||||
if ( se[i] === 0 || se[i] === opts.minIterations ) {
|
||||
sum++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( sum === n ) { // then we have convergence
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Identify exemplars (cluster centers)
|
||||
let exemplarsIndices = findExemplars( n, R, A );
|
||||
|
||||
// Assign nodes to clusters
|
||||
let clusterIndices = assign( n, S, exemplarsIndices, nodes, id2position );
|
||||
|
||||
let clusters = {};
|
||||
for ( let c = 0; c < exemplarsIndices.length; c++ ) {
|
||||
clusters[ exemplarsIndices[c] ] = [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let pos = id2position[ nodes[i].id() ];
|
||||
let clusterIndex = clusterIndices[pos];
|
||||
|
||||
if( clusterIndex != null ){ // the node may have not been assigned a cluster if no valid attributes were specified
|
||||
clusters[ clusterIndex ].push( nodes[i] );
|
||||
}
|
||||
}
|
||||
let retClusters = new Array(exemplarsIndices.length);
|
||||
for ( let c = 0; c < exemplarsIndices.length; c++ ) {
|
||||
retClusters[c] = cy.collection( clusters[ exemplarsIndices[c] ] );
|
||||
}
|
||||
|
||||
return retClusters;
|
||||
};
|
||||
|
||||
export default { affinityPropagation, ap: affinityPropagation };
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
import * as is from '../../is.mjs';
|
||||
import { warn, defaults } from '../../util/index.mjs';
|
||||
import Map from '../../map.mjs';
|
||||
|
||||
const bellmanFordDefaults = defaults({
|
||||
weight: edge => 1,
|
||||
directed: false,
|
||||
root: null
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
|
||||
// Implemented from pseudocode from wikipedia
|
||||
bellmanFord: function( options ){
|
||||
let { weight, directed, root } = bellmanFordDefaults(options);
|
||||
let weightFn = weight;
|
||||
let eles = this;
|
||||
let cy = this.cy();
|
||||
let { edges, nodes } = this.byGroup();
|
||||
let numNodes = nodes.length;
|
||||
let infoMap = new Map();
|
||||
let hasNegativeWeightCycle = false;
|
||||
let negativeWeightCycles = [];
|
||||
|
||||
root = cy.collection(root)[0]; // in case selector passed
|
||||
|
||||
edges.unmergeBy( edge => edge.isLoop() );
|
||||
|
||||
let numEdges = edges.length;
|
||||
|
||||
let getInfo = node => {
|
||||
let obj = infoMap.get( node.id() );
|
||||
|
||||
if( !obj ){
|
||||
obj = {};
|
||||
|
||||
infoMap.set( node.id(), obj );
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
let getNodeFromTo = to => (is.string(to) ? cy.$(to) : to)[0];
|
||||
|
||||
let distanceTo = to => getInfo( getNodeFromTo(to) ).dist;
|
||||
|
||||
let pathTo = (to, thisStart = root) => {
|
||||
let end = getNodeFromTo(to);
|
||||
let path = [];
|
||||
let node = end;
|
||||
|
||||
for( ;; ){
|
||||
if( node == null ){ return this.spawn(); }
|
||||
|
||||
let { edge, pred } = getInfo( node );
|
||||
|
||||
path.unshift( node[0] );
|
||||
|
||||
if( node.same(thisStart) && path.length > 0 ){ break; }
|
||||
|
||||
if( edge != null ){
|
||||
path.unshift( edge );
|
||||
}
|
||||
|
||||
node = pred;
|
||||
}
|
||||
|
||||
return eles.spawn( path );
|
||||
};
|
||||
|
||||
// Initializations { dist, pred, edge }
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
let node = nodes[i];
|
||||
let info = getInfo( node );
|
||||
|
||||
if( node.same(root) ){
|
||||
info.dist = 0;
|
||||
} else {
|
||||
info.dist = Infinity;
|
||||
}
|
||||
|
||||
info.pred = null;
|
||||
info.edge = null;
|
||||
}
|
||||
|
||||
// Edges relaxation
|
||||
let replacedEdge = false;
|
||||
|
||||
let checkForEdgeReplacement = (node1, node2, edge, info1, info2, weight) => {
|
||||
let dist = info1.dist + weight;
|
||||
|
||||
if( dist < info2.dist && !edge.same(info1.edge) ){
|
||||
info2.dist = dist;
|
||||
info2.pred = node1;
|
||||
info2.edge = edge;
|
||||
replacedEdge = true;
|
||||
}
|
||||
};
|
||||
|
||||
for( let i = 1; i < numNodes; i++ ){
|
||||
replacedEdge = false;
|
||||
|
||||
for( let e = 0; e < numEdges; e++ ){
|
||||
let edge = edges[e];
|
||||
let src = edge.source();
|
||||
let tgt = edge.target();
|
||||
let weight = weightFn(edge);
|
||||
let srcInfo = getInfo(src);
|
||||
let tgtInfo = getInfo(tgt);
|
||||
|
||||
checkForEdgeReplacement(src, tgt, edge, srcInfo, tgtInfo, weight);
|
||||
|
||||
// If undirected graph, we need to take into account the 'reverse' edge
|
||||
if( !directed ){
|
||||
checkForEdgeReplacement(tgt, src, edge, tgtInfo, srcInfo, weight);
|
||||
}
|
||||
}
|
||||
|
||||
if( !replacedEdge ){ break; }
|
||||
}
|
||||
|
||||
if( replacedEdge ){
|
||||
// Check for negative weight cycles
|
||||
const negativeWeightCycleIds = [];
|
||||
for( let e = 0; e < numEdges; e++ ){
|
||||
let edge = edges[e];
|
||||
let src = edge.source();
|
||||
let tgt = edge.target();
|
||||
let weight = weightFn(edge);
|
||||
let srcDist = getInfo(src).dist;
|
||||
let tgtDist = getInfo(tgt).dist;
|
||||
|
||||
if( srcDist + weight < tgtDist || (!directed && tgtDist + weight < srcDist) ){
|
||||
if( !hasNegativeWeightCycle ){
|
||||
warn('Graph contains a negative weight cycle for Bellman-Ford');
|
||||
|
||||
hasNegativeWeightCycle = true;
|
||||
}
|
||||
|
||||
if( options.findNegativeWeightCycles !== false ){
|
||||
const negativeNodes = [];
|
||||
|
||||
if( srcDist + weight < tgtDist ){
|
||||
negativeNodes.push(src);
|
||||
}
|
||||
|
||||
if( !directed && tgtDist + weight < srcDist ) {
|
||||
negativeNodes.push(tgt);
|
||||
}
|
||||
|
||||
const numNegativeNodes = negativeNodes.length;
|
||||
for( let n = 0; n < numNegativeNodes; n++ ){
|
||||
const start = negativeNodes[n];
|
||||
let cycle = [start];
|
||||
|
||||
cycle.push(getInfo(start).edge);
|
||||
|
||||
let node = getInfo(start).pred;
|
||||
while( cycle.indexOf(node) === -1 ){
|
||||
cycle.push(node);
|
||||
cycle.push(getInfo(node).edge);
|
||||
node = getInfo(node).pred;
|
||||
}
|
||||
cycle = cycle.slice(cycle.indexOf(node));
|
||||
|
||||
let smallestId = cycle[0].id();
|
||||
let smallestIndex = 0;
|
||||
for( let c = 2; c < cycle.length; c+=2 ){
|
||||
if( cycle[c].id() < smallestId ){
|
||||
smallestId = cycle[c].id();
|
||||
smallestIndex = c;
|
||||
}
|
||||
}
|
||||
cycle = cycle.slice(smallestIndex)
|
||||
.concat(cycle.slice(0, smallestIndex));
|
||||
cycle.push(cycle[0]);
|
||||
|
||||
const cycleId = cycle.map(el => el.id()).join(",");
|
||||
if( negativeWeightCycleIds.indexOf(cycleId) === -1 ){
|
||||
negativeWeightCycles.push(eles.spawn(cycle));
|
||||
negativeWeightCycleIds.push(cycleId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
distanceTo,
|
||||
pathTo,
|
||||
hasNegativeWeightCycle,
|
||||
negativeWeightCycles
|
||||
};
|
||||
|
||||
} // bellmanFord
|
||||
|
||||
}); // elesfn
|
||||
|
||||
export default elesfn;
|
||||
Generated
Vendored
+174
@@ -0,0 +1,174 @@
|
||||
import Heap from '../../heap.mjs';
|
||||
import * as util from '../../util/index.mjs';
|
||||
|
||||
const defaults = util.defaults({
|
||||
weight: null,
|
||||
directed: false
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
|
||||
// Implemented from the algorithm in the paper "On Variants of Shortest-Path Betweenness Centrality and their Generic Computation" by Ulrik Brandes
|
||||
betweennessCentrality: function( options ){
|
||||
let { directed, weight } = defaults(options);
|
||||
let weighted = weight != null;
|
||||
let cy = this.cy();
|
||||
|
||||
// starting
|
||||
let V = this.nodes();
|
||||
let A = {};
|
||||
let _C = {};
|
||||
let max = 0;
|
||||
let C = {
|
||||
set: function( key, val ){
|
||||
_C[ key ] = val;
|
||||
|
||||
if( val > max ){ max = val; }
|
||||
},
|
||||
|
||||
get: function( key ){ return _C[ key ]; }
|
||||
};
|
||||
|
||||
// A contains the neighborhoods of every node
|
||||
for( let i = 0; i < V.length; i++ ){
|
||||
let v = V[ i ];
|
||||
let vid = v.id();
|
||||
|
||||
if( directed ){
|
||||
A[ vid ] = v.outgoers().nodes(); // get outgoers of every node
|
||||
} else {
|
||||
A[ vid ] = v.openNeighborhood().nodes(); // get neighbors of every node
|
||||
}
|
||||
|
||||
C.set( vid, 0 );
|
||||
}
|
||||
|
||||
for( let s = 0; s < V.length; s++ ){
|
||||
let sid = V[s].id();
|
||||
let S = []; // stack
|
||||
let P = {};
|
||||
let g = {};
|
||||
let d = {};
|
||||
let Q = new Heap(function( a, b ){
|
||||
return d[a] - d[b];
|
||||
}); // queue
|
||||
|
||||
// init dictionaries
|
||||
for( let i = 0; i < V.length; i++ ){
|
||||
let vid = V[ i ].id();
|
||||
|
||||
P[ vid ] = [];
|
||||
g[ vid ] = 0;
|
||||
d[ vid ] = Infinity;
|
||||
}
|
||||
|
||||
g[ sid ] = 1; // sigma
|
||||
d[ sid ] = 0; // distance to s
|
||||
|
||||
Q.push( sid );
|
||||
|
||||
while( !Q.empty() ){
|
||||
let v = Q.pop();
|
||||
|
||||
S.push( v );
|
||||
|
||||
if( weighted ){
|
||||
for( let j = 0; j < A[v].length; j++ ){
|
||||
let w = A[v][j];
|
||||
let vEle = cy.getElementById( v );
|
||||
|
||||
let edge;
|
||||
if( vEle.edgesTo( w ).length > 0 ){
|
||||
edge = vEle.edgesTo( w )[0];
|
||||
} else {
|
||||
edge = w.edgesTo( vEle )[0];
|
||||
}
|
||||
|
||||
let edgeWeight = weight( edge );
|
||||
|
||||
w = w.id();
|
||||
|
||||
if( d[w] > d[v] + edgeWeight ){
|
||||
d[w] = d[v] + edgeWeight;
|
||||
|
||||
if( Q.nodes.indexOf( w ) < 0 ){ //if w is not in Q
|
||||
Q.push( w );
|
||||
} else { // update position if w is in Q
|
||||
Q.updateItem( w );
|
||||
}
|
||||
|
||||
g[w] = 0;
|
||||
P[w] = [];
|
||||
}
|
||||
|
||||
if( d[w] == d[v] + edgeWeight ){
|
||||
g[w] = g[w] + g[v];
|
||||
P[w].push( v );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for( let j = 0; j < A[v].length; j++ ){
|
||||
let w = A[v][j].id();
|
||||
|
||||
if( d[w] == Infinity ){
|
||||
Q.push( w );
|
||||
|
||||
d[w] = d[v] + 1;
|
||||
}
|
||||
|
||||
if( d[w] == d[v] + 1 ){
|
||||
g[w] = g[w] + g[v];
|
||||
P[w].push( v );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let e = {};
|
||||
for( let i = 0; i < V.length; i++ ){
|
||||
e[ V[ i ].id() ] = 0;
|
||||
}
|
||||
|
||||
while( S.length > 0 ){
|
||||
let w = S.pop();
|
||||
|
||||
for( let j = 0; j < P[w].length; j++ ){
|
||||
let v = P[w][j];
|
||||
|
||||
e[v] = e[v] + (g[v] / g[w]) * (1 + e[w]);
|
||||
}
|
||||
|
||||
if( w != V[s].id() ){
|
||||
C.set( w, C.get( w ) + e[w] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ret = {
|
||||
betweenness: function( node ){
|
||||
let id = cy.collection(node).id();
|
||||
|
||||
return C.get( id );
|
||||
},
|
||||
|
||||
betweennessNormalized: function( node ){
|
||||
if ( max == 0 ){ return 0; }
|
||||
|
||||
let id = cy.collection(node).id();
|
||||
|
||||
return C.get( id ) / max;
|
||||
}
|
||||
};
|
||||
|
||||
// alias
|
||||
ret.betweennessNormalised = ret.betweennessNormalized;
|
||||
|
||||
return ret;
|
||||
} // betweennessCentrality
|
||||
|
||||
}); // elesfn
|
||||
|
||||
// nice, short mathematical alias
|
||||
elesfn.bc = elesfn.betweennessCentrality;
|
||||
|
||||
export default elesfn;
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import * as is from '../../is.mjs';
|
||||
|
||||
let defineSearch = function( params ){
|
||||
params = {
|
||||
bfs: params.bfs || !params.dfs,
|
||||
dfs: params.dfs || !params.bfs
|
||||
};
|
||||
|
||||
// from pseudocode on wikipedia
|
||||
return function searchFn( roots, fn, directed ){
|
||||
let options;
|
||||
if( is.plainObject( roots ) && !is.elementOrCollection( roots ) ){
|
||||
options = roots;
|
||||
roots = options.roots || options.root;
|
||||
fn = options.visit;
|
||||
directed = options.directed;
|
||||
}
|
||||
|
||||
directed = arguments.length === 2 && !is.fn( fn ) ? fn : directed;
|
||||
fn = is.fn( fn ) ? fn : function(){};
|
||||
|
||||
let cy = this._private.cy;
|
||||
let v = roots = is.string( roots ) ? this.filter( roots ) : roots;
|
||||
let Q = [];
|
||||
let connectedNodes = [];
|
||||
let connectedBy = {};
|
||||
let id2depth = {};
|
||||
let V = {};
|
||||
let j = 0;
|
||||
let found;
|
||||
let { nodes, edges } = this.byGroup();
|
||||
|
||||
// enqueue v
|
||||
for( let i = 0; i < v.length; i++ ){
|
||||
let vi = v[i];
|
||||
let viId = vi.id();
|
||||
|
||||
if( vi.isNode() ){
|
||||
Q.unshift( vi );
|
||||
|
||||
if( params.bfs ){
|
||||
V[ viId ] = true;
|
||||
|
||||
connectedNodes.push( vi );
|
||||
}
|
||||
|
||||
id2depth[ viId ] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
while( Q.length !== 0 ){
|
||||
let v = params.bfs ? Q.shift() : Q.pop();
|
||||
let vId = v.id();
|
||||
|
||||
if( params.dfs ){
|
||||
if( V[ vId ] ){ continue; }
|
||||
|
||||
V[ vId ] = true;
|
||||
|
||||
connectedNodes.push( v );
|
||||
}
|
||||
|
||||
let depth = id2depth[ vId ];
|
||||
let prevEdge = connectedBy[ vId ];
|
||||
let src = prevEdge != null ? prevEdge.source() : null;
|
||||
let tgt = prevEdge != null ? prevEdge.target() : null;
|
||||
let prevNode = prevEdge == null ? undefined : ( v.same(src) ? tgt[0] : src[0] );
|
||||
let ret;
|
||||
|
||||
ret = fn( v, prevEdge, prevNode, j++, depth );
|
||||
|
||||
if( ret === true ){
|
||||
found = v;
|
||||
break;
|
||||
}
|
||||
|
||||
if( ret === false ){
|
||||
break;
|
||||
}
|
||||
|
||||
let vwEdges = v.connectedEdges().filter(e => (!directed || e.source().same(v)) && edges.has(e));
|
||||
for( let i = 0; i < vwEdges.length; i++ ){
|
||||
let e = vwEdges[ i ];
|
||||
let w = e.connectedNodes().filter(n => !n.same(v) && nodes.has(n));
|
||||
let wId = w.id();
|
||||
|
||||
if( w.length !== 0 && !V[ wId ] ){
|
||||
w = w[0];
|
||||
|
||||
Q.push( w );
|
||||
|
||||
if( params.bfs ){
|
||||
V[ wId ] = true;
|
||||
|
||||
connectedNodes.push( w );
|
||||
}
|
||||
|
||||
connectedBy[ wId ] = e;
|
||||
|
||||
id2depth[ wId ] = id2depth[ vId ] + 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let connectedEles = cy.collection();
|
||||
|
||||
for( let i = 0; i < connectedNodes.length; i++ ){
|
||||
let node = connectedNodes[ i ];
|
||||
let edge = connectedBy[ node.id() ];
|
||||
|
||||
if( edge != null ){
|
||||
connectedEles.push( edge );
|
||||
}
|
||||
|
||||
connectedEles.push( node );
|
||||
}
|
||||
|
||||
return {
|
||||
path: cy.collection( connectedEles ),
|
||||
found: cy.collection( found )
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// search, spanning trees, etc
|
||||
let elesfn = ({
|
||||
breadthFirstSearch: defineSearch( { bfs: true } ),
|
||||
depthFirstSearch: defineSearch( { dfs: true } )
|
||||
});
|
||||
|
||||
// nice, short mathematical alias
|
||||
elesfn.bfs = elesfn.breadthFirstSearch;
|
||||
elesfn.dfs = elesfn.depthFirstSearch;
|
||||
|
||||
export default elesfn;
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import * as is from '../../is.mjs';
|
||||
import * as util from '../../util/index.mjs';
|
||||
|
||||
const defaults = util.defaults({
|
||||
harmonic: true,
|
||||
weight: () => 1,
|
||||
directed: false,
|
||||
root: null
|
||||
});
|
||||
|
||||
const elesfn = ({
|
||||
|
||||
closenessCentralityNormalized: function( options ){
|
||||
let { harmonic, weight, directed } = defaults(options);
|
||||
|
||||
let cy = this.cy();
|
||||
let closenesses = {};
|
||||
let maxCloseness = 0;
|
||||
let nodes = this.nodes();
|
||||
let fw = this.floydWarshall({ weight, directed });
|
||||
|
||||
// Compute closeness for every node and find the maximum closeness
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
let currCloseness = 0;
|
||||
let node_i = nodes[i];
|
||||
|
||||
for( let j = 0; j < nodes.length; j++ ){
|
||||
if( i !== j ){
|
||||
let d = fw.distance( node_i, nodes[j] );
|
||||
|
||||
if( harmonic ){
|
||||
currCloseness += 1 / d;
|
||||
} else {
|
||||
currCloseness += d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( !harmonic ){
|
||||
currCloseness = 1 / currCloseness;
|
||||
}
|
||||
|
||||
if( maxCloseness < currCloseness ){
|
||||
maxCloseness = currCloseness;
|
||||
}
|
||||
|
||||
closenesses[ node_i.id() ] = currCloseness;
|
||||
}
|
||||
|
||||
return {
|
||||
closeness: function( node ){
|
||||
if( maxCloseness == 0 ){ return 0; }
|
||||
|
||||
if( is.string( node ) ){
|
||||
// from is a selector string
|
||||
node = (cy.filter( node )[0]).id();
|
||||
} else {
|
||||
// from is a node
|
||||
node = node.id();
|
||||
}
|
||||
|
||||
return closenesses[ node ] / maxCloseness;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// Implemented from pseudocode from wikipedia
|
||||
closenessCentrality: function( options ){
|
||||
let { root, weight, directed, harmonic } = defaults(options);
|
||||
|
||||
root = this.filter(root)[0];
|
||||
|
||||
// we need distance from this node to every other node
|
||||
let dijkstra = this.dijkstra({ root, weight, directed });
|
||||
let totalDistance = 0;
|
||||
let nodes = this.nodes();
|
||||
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
let n = nodes[i];
|
||||
|
||||
if( !n.same(root) ){
|
||||
let d = dijkstra.distanceTo(n);
|
||||
|
||||
if( harmonic ){
|
||||
totalDistance += 1 / d;
|
||||
} else {
|
||||
totalDistance += d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return harmonic ? totalDistance : 1 / totalDistance;
|
||||
} // closenessCentrality
|
||||
|
||||
}); // elesfn
|
||||
|
||||
// nice, short mathematical alias
|
||||
elesfn.cc = elesfn.closenessCentrality;
|
||||
elesfn.ccn = elesfn.closenessCentralityNormalised = elesfn.closenessCentralityNormalized;
|
||||
|
||||
export default elesfn;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Common distance metrics for clustering algorithms
|
||||
// https://en.wikipedia.org/wiki/Hierarchical_clustering#Metric
|
||||
|
||||
import * as is from '../../is.mjs';
|
||||
|
||||
let identity = x => x;
|
||||
let absDiff = ( p, q ) => Math.abs( q - p );
|
||||
let addAbsDiff = ( total, p, q ) => total + absDiff(p, q);
|
||||
let addSquaredDiff = ( total, p, q ) => total + Math.pow( q - p, 2 );
|
||||
let sqrt = x => Math.sqrt(x);
|
||||
let maxAbsDiff = ( currentMax, p, q ) => Math.max( currentMax, absDiff(p, q) );
|
||||
|
||||
let getDistance = function( length, getP, getQ, init, visit, post = identity ){
|
||||
let ret = init;
|
||||
let p, q;
|
||||
|
||||
for ( let dim = 0; dim < length; dim++ ) {
|
||||
p = getP(dim);
|
||||
q = getQ(dim);
|
||||
|
||||
ret = visit( ret, p, q );
|
||||
}
|
||||
|
||||
return post( ret );
|
||||
};
|
||||
|
||||
let distances = {
|
||||
euclidean: function ( length, getP, getQ ) {
|
||||
if( length >= 2 ){
|
||||
return getDistance( length, getP, getQ, 0, addSquaredDiff, sqrt );
|
||||
} else { // for single attr case, more efficient to avoid sqrt
|
||||
return getDistance( length, getP, getQ, 0, addAbsDiff );
|
||||
}
|
||||
},
|
||||
squaredEuclidean: function ( length, getP, getQ ) {
|
||||
return getDistance( length, getP, getQ, 0, addSquaredDiff );
|
||||
},
|
||||
manhattan: function ( length, getP, getQ ) {
|
||||
return getDistance( length, getP, getQ, 0, addAbsDiff );
|
||||
},
|
||||
max: function ( length, getP, getQ ) {
|
||||
return getDistance( length, getP, getQ, -Infinity, maxAbsDiff );
|
||||
}
|
||||
};
|
||||
|
||||
// in case the user accidentally doesn't use camel case
|
||||
distances['squared-euclidean'] = distances['squaredEuclidean'];
|
||||
distances['squaredeuclidean'] = distances['squaredEuclidean'];
|
||||
|
||||
export default function( method, length, getP, getQ, nodeP, nodeQ ){
|
||||
let impl;
|
||||
|
||||
if( is.fn( method ) ){
|
||||
impl = method;
|
||||
} else {
|
||||
impl = distances[ method ] || distances.euclidean;
|
||||
}
|
||||
|
||||
if( length === 0 && is.fn( method ) ){
|
||||
return impl( nodeP, nodeQ );
|
||||
} else {
|
||||
return impl( length, getP, getQ, nodeP, nodeQ );
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import * as is from '../../is.mjs';
|
||||
import * as util from '../../util/index.mjs';
|
||||
|
||||
const defaults = util.defaults({
|
||||
root: null,
|
||||
weight: edge => 1,
|
||||
directed: false,
|
||||
alpha: 0
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
degreeCentralityNormalized: function( options ){
|
||||
options = defaults( options );
|
||||
|
||||
let cy = this.cy();
|
||||
let nodes = this.nodes();
|
||||
let numNodes = nodes.length;
|
||||
|
||||
if( !options.directed ){
|
||||
let degrees = {};
|
||||
let maxDegree = 0;
|
||||
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
let node = nodes[ i ];
|
||||
|
||||
// add current node to the current options object and call degreeCentrality
|
||||
options.root = node;
|
||||
|
||||
let currDegree = this.degreeCentrality( options );
|
||||
|
||||
if( maxDegree < currDegree.degree ){
|
||||
maxDegree = currDegree.degree;
|
||||
}
|
||||
|
||||
degrees[ node.id() ] = currDegree.degree;
|
||||
}
|
||||
|
||||
return {
|
||||
degree: function( node ){
|
||||
if( maxDegree === 0 ){ return 0; }
|
||||
|
||||
if( is.string( node ) ){
|
||||
// from is a selector string
|
||||
node = cy.filter( node );
|
||||
}
|
||||
|
||||
return degrees[ node.id() ] / maxDegree;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let indegrees = {};
|
||||
let outdegrees = {};
|
||||
let maxIndegree = 0;
|
||||
let maxOutdegree = 0;
|
||||
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
let node = nodes[ i ];
|
||||
let id = node.id();
|
||||
|
||||
// add current node to the current options object and call degreeCentrality
|
||||
options.root = node;
|
||||
|
||||
let currDegree = this.degreeCentrality( options );
|
||||
|
||||
if( maxIndegree < currDegree.indegree )
|
||||
maxIndegree = currDegree.indegree;
|
||||
|
||||
if( maxOutdegree < currDegree.outdegree )
|
||||
maxOutdegree = currDegree.outdegree;
|
||||
|
||||
indegrees[ id ] = currDegree.indegree;
|
||||
outdegrees[ id ] = currDegree.outdegree;
|
||||
}
|
||||
|
||||
return {
|
||||
indegree: function( node ){
|
||||
if ( maxIndegree == 0 ){ return 0; }
|
||||
|
||||
if( is.string( node ) ){
|
||||
// from is a selector string
|
||||
node = cy.filter( node );
|
||||
}
|
||||
|
||||
return indegrees[ node.id() ] / maxIndegree;
|
||||
},
|
||||
outdegree: function( node ){
|
||||
if ( maxOutdegree === 0 ){ return 0; }
|
||||
|
||||
if( is.string( node ) ){
|
||||
// from is a selector string
|
||||
node = cy.filter( node );
|
||||
}
|
||||
|
||||
return outdegrees[ node.id() ] / maxOutdegree;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}, // degreeCentralityNormalized
|
||||
|
||||
// Implemented from the algorithm in Opsahl's paper
|
||||
// "Node centrality in weighted networks: Generalizing degree and shortest paths"
|
||||
// check the heading 2 "Degree"
|
||||
degreeCentrality: function( options ){
|
||||
options = defaults( options );
|
||||
|
||||
let cy = this.cy();
|
||||
let callingEles = this;
|
||||
let { root, weight, directed, alpha } = options;
|
||||
|
||||
root = cy.collection(root)[0];
|
||||
|
||||
if( !directed ){
|
||||
let connEdges = root.connectedEdges().intersection( callingEles );
|
||||
let k = connEdges.length;
|
||||
let s = 0;
|
||||
|
||||
// Now, sum edge weights
|
||||
for( let i = 0; i < connEdges.length; i++ ){
|
||||
s += weight( connEdges[i] );
|
||||
}
|
||||
|
||||
return {
|
||||
degree: Math.pow( k, 1 - alpha ) * Math.pow( s, alpha )
|
||||
};
|
||||
} else {
|
||||
let edges = root.connectedEdges();
|
||||
let incoming = edges.filter( edge => edge.target().same(root) && callingEles.has(edge) );
|
||||
let outgoing = edges.filter( edge => edge.source().same(root) && callingEles.has(edge) );
|
||||
let k_in = incoming.length;
|
||||
let k_out = outgoing.length;
|
||||
let s_in = 0;
|
||||
let s_out = 0;
|
||||
|
||||
// Now, sum incoming edge weights
|
||||
for( let i = 0; i < incoming.length; i++ ){
|
||||
s_in += weight( incoming[i] );
|
||||
}
|
||||
|
||||
// Now, sum outgoing edge weights
|
||||
for( let i = 0; i < outgoing.length; i++ ){
|
||||
s_out += weight( outgoing[i] );
|
||||
}
|
||||
|
||||
return {
|
||||
indegree: Math.pow( k_in, 1 - alpha ) * Math.pow( s_in, alpha ),
|
||||
outdegree: Math.pow( k_out, 1 - alpha ) * Math.pow( s_out, alpha )
|
||||
};
|
||||
}
|
||||
} // degreeCentrality
|
||||
|
||||
}); // elesfn
|
||||
|
||||
// nice, short mathematical alias
|
||||
elesfn.dc = elesfn.degreeCentrality;
|
||||
elesfn.dcn = elesfn.degreeCentralityNormalised = elesfn.degreeCentralityNormalized;
|
||||
|
||||
export default elesfn;
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import * as is from '../../is.mjs';
|
||||
import Heap from '../../heap.mjs';
|
||||
import { defaults } from '../../util/index.mjs';
|
||||
|
||||
const dijkstraDefaults = defaults({
|
||||
root: null,
|
||||
weight: edge => 1,
|
||||
directed: false
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
|
||||
dijkstra: function( options ){
|
||||
if( !is.plainObject(options) ){
|
||||
let args = arguments;
|
||||
|
||||
options = { root: args[0], weight: args[1], directed: args[2] };
|
||||
}
|
||||
|
||||
let { root, weight, directed } = dijkstraDefaults(options);
|
||||
|
||||
let eles = this;
|
||||
let weightFn = weight;
|
||||
let source = is.string( root ) ? this.filter( root )[0] : root[0];
|
||||
let dist = {};
|
||||
let prev = {};
|
||||
let knownDist = {};
|
||||
|
||||
let { nodes, edges } = this.byGroup();
|
||||
edges.unmergeBy( ele => ele.isLoop() );
|
||||
|
||||
let getDist = node => dist[ node.id() ];
|
||||
|
||||
let setDist = ( node, d ) => {
|
||||
dist[ node.id() ] = d;
|
||||
|
||||
Q.updateItem( node );
|
||||
};
|
||||
|
||||
let Q = new Heap( (a, b) => getDist(a) - getDist(b) );
|
||||
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
let node = nodes[ i ];
|
||||
|
||||
dist[ node.id() ] = node.same( source ) ? 0 : Infinity;
|
||||
Q.push( node );
|
||||
}
|
||||
|
||||
let distBetween = ( u, v ) => {
|
||||
let uvs = ( directed ? u.edgesTo(v) : u.edgesWith(v) ).intersect( edges );
|
||||
let smallestDistance = Infinity;
|
||||
let smallestEdge;
|
||||
|
||||
for( let i = 0; i < uvs.length; i++ ){
|
||||
let edge = uvs[ i ];
|
||||
let weight = weightFn( edge );
|
||||
|
||||
if( weight < smallestDistance || !smallestEdge ){
|
||||
smallestDistance = weight;
|
||||
smallestEdge = edge;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
edge: smallestEdge,
|
||||
dist: smallestDistance
|
||||
};
|
||||
};
|
||||
|
||||
while( Q.size() > 0 ){
|
||||
let u = Q.pop();
|
||||
let smalletsDist = getDist( u );
|
||||
let uid = u.id();
|
||||
|
||||
knownDist[ uid ] = smalletsDist;
|
||||
|
||||
if( smalletsDist === Infinity ){
|
||||
continue;
|
||||
}
|
||||
|
||||
let neighbors = u.neighborhood().intersect( nodes );
|
||||
for( let i = 0; i < neighbors.length; i++ ){
|
||||
let v = neighbors[ i ];
|
||||
let vid = v.id();
|
||||
let vDist = distBetween( u, v );
|
||||
|
||||
let alt = smalletsDist + vDist.dist;
|
||||
|
||||
if( alt < getDist( v ) ){
|
||||
setDist( v, alt );
|
||||
|
||||
prev[ vid ] = {
|
||||
node: u,
|
||||
edge: vDist.edge
|
||||
};
|
||||
}
|
||||
} // for
|
||||
} // while
|
||||
|
||||
return {
|
||||
distanceTo: function( node ){
|
||||
let target = is.string( node ) ? nodes.filter( node )[0] : node[0];
|
||||
|
||||
return knownDist[ target.id() ];
|
||||
},
|
||||
|
||||
pathTo: function( node ){
|
||||
let target = is.string( node ) ? nodes.filter( node )[0] : node[0];
|
||||
let S = [];
|
||||
let u = target;
|
||||
let uid = u.id();
|
||||
|
||||
if( target.length > 0 ){
|
||||
S.unshift( target );
|
||||
|
||||
while( prev[ uid ] ){
|
||||
let p = prev[ uid ];
|
||||
|
||||
S.unshift( p.edge );
|
||||
S.unshift( p.node );
|
||||
|
||||
u = p.node;
|
||||
uid = u.id();
|
||||
}
|
||||
}
|
||||
|
||||
return eles.spawn( S );
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export default elesfn;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import * as is from '../../is.mjs';
|
||||
import { defaults } from '../../util/index.mjs';
|
||||
|
||||
const floydWarshallDefaults = defaults({
|
||||
weight: edge => 1,
|
||||
directed: false
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
|
||||
// Implemented from pseudocode from wikipedia
|
||||
floydWarshall: function( options ){
|
||||
let cy = this.cy();
|
||||
|
||||
let { weight, directed } = floydWarshallDefaults(options);
|
||||
let weightFn = weight;
|
||||
|
||||
let { nodes, edges } = this.byGroup();
|
||||
|
||||
let N = nodes.length;
|
||||
let Nsq = N * N;
|
||||
|
||||
let indexOf = node => nodes.indexOf(node);
|
||||
let atIndex = i => nodes[i];
|
||||
|
||||
// Initialize distance matrix
|
||||
let dist = new Array(Nsq);
|
||||
for( let n = 0; n < Nsq; n++ ){
|
||||
let j = n % N;
|
||||
let i = (n - j) / N;
|
||||
|
||||
if( i === j ){
|
||||
dist[n] = 0;
|
||||
} else {
|
||||
dist[n] = Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize matrix used for path reconstruction
|
||||
// Initialize distance matrix
|
||||
let next = new Array(Nsq);
|
||||
let edgeNext = new Array(Nsq);
|
||||
|
||||
// Process edges
|
||||
for( let i = 0; i < edges.length; i++ ){
|
||||
let edge = edges[i];
|
||||
let src = edge.source()[0];
|
||||
let tgt = edge.target()[0];
|
||||
|
||||
if( src === tgt ){ continue; } // exclude loops
|
||||
|
||||
let s = indexOf( src );
|
||||
let t = indexOf( tgt );
|
||||
let st = s * N + t; // source to target index
|
||||
let weight = weightFn( edge );
|
||||
|
||||
// Check if already process another edge between same 2 nodes
|
||||
if( dist[st] > weight ){
|
||||
dist[st] = weight;
|
||||
next[st] = t;
|
||||
edgeNext[st] = edge;
|
||||
}
|
||||
|
||||
// If undirected graph, process 'reversed' edge
|
||||
if( !directed ){
|
||||
let ts = t * N + s; // target to source index
|
||||
|
||||
if( !directed && dist[ts] > weight ){
|
||||
dist[ts] = weight;
|
||||
next[ts] = s;
|
||||
edgeNext[ts] = edge;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main loop
|
||||
for( let k = 0; k < N; k++ ){
|
||||
|
||||
for( let i = 0; i < N; i++ ){
|
||||
let ik = i * N + k;
|
||||
|
||||
for( let j = 0; j < N; j++ ){
|
||||
let ij = i * N + j;
|
||||
let kj = k * N + j;
|
||||
|
||||
if( dist[ik] + dist[kj] < dist[ij] ){
|
||||
dist[ij] = dist[ik] + dist[kj];
|
||||
next[ij] = next[ik];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let getArgEle = ele => ( is.string(ele) ? cy.filter(ele) : ele )[0];
|
||||
let indexOfArgEle = ele => indexOf(getArgEle(ele));
|
||||
|
||||
let res = {
|
||||
distance: function( from, to ){
|
||||
let i = indexOfArgEle(from);
|
||||
let j = indexOfArgEle(to);
|
||||
|
||||
return dist[ i * N + j ];
|
||||
},
|
||||
|
||||
path: function( from, to ){
|
||||
let i = indexOfArgEle(from);
|
||||
let j = indexOfArgEle(to);
|
||||
|
||||
let fromNode = atIndex(i);
|
||||
|
||||
if( i === j ){ return fromNode.collection(); }
|
||||
|
||||
if( next[i * N + j] == null ){ return cy.collection(); }
|
||||
|
||||
let path = cy.collection();
|
||||
let prev = i;
|
||||
let edge;
|
||||
|
||||
path.merge( fromNode );
|
||||
|
||||
while( i !== j ){
|
||||
prev = i;
|
||||
i = next[i * N + j];
|
||||
edge = edgeNext[prev * N + i];
|
||||
|
||||
path.merge( edge );
|
||||
path.merge( atIndex(i) );
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
return res;
|
||||
|
||||
} // floydWarshall
|
||||
|
||||
}); // elesfn
|
||||
|
||||
export default elesfn;
|
||||
Generated
Vendored
+316
@@ -0,0 +1,316 @@
|
||||
// Implemented by Zoe Xi @zoexi for GSOC 2016
|
||||
// https://github.com/cytoscape/cytoscape.js-hierarchical
|
||||
|
||||
// Implemented from the reference library: https://harthur.github.io/clusterfck/
|
||||
|
||||
import * as util from '../../util/index.mjs';
|
||||
import clusteringDistance from './clustering-distances.mjs';
|
||||
|
||||
const defaults = util.defaults({
|
||||
distance: 'euclidean', // distance metric to compare nodes
|
||||
linkage: 'min', // linkage criterion : how to determine the distance between clusters of nodes
|
||||
mode: 'threshold',
|
||||
// mode:'threshold' => clusters must be threshold distance apart
|
||||
threshold: Infinity, // the distance threshold
|
||||
// mode:'dendrogram' => the nodes are organised as leaves in a tree (siblings are close), merging makes clusters
|
||||
addDendrogram: false, // whether to add the dendrogram to the graph for viz
|
||||
dendrogramDepth: 0, // depth at which dendrogram branches are merged into the returned clusters
|
||||
attributes: [] // array of attr functions
|
||||
});
|
||||
|
||||
const linkageAliases = {
|
||||
'single': 'min',
|
||||
'complete': 'max'
|
||||
};
|
||||
|
||||
let setOptions = ( options ) => {
|
||||
let opts = defaults( options );
|
||||
|
||||
let preferredAlias = linkageAliases[ opts.linkage ];
|
||||
|
||||
if( preferredAlias != null ){
|
||||
opts.linkage = preferredAlias;
|
||||
}
|
||||
|
||||
return opts;
|
||||
};
|
||||
|
||||
let mergeClosest = function( clusters, index, dists, mins, opts ) {
|
||||
// Find two closest clusters from cached mins
|
||||
let minKey = 0;
|
||||
let min = Infinity;
|
||||
let dist;
|
||||
let attrs = opts.attributes;
|
||||
|
||||
let getDist = (n1, n2) => clusteringDistance( opts.distance, attrs.length, i => attrs[i](n1), i => attrs[i](n2), n1, n2 );
|
||||
|
||||
for ( let i = 0; i < clusters.length; i++ ) {
|
||||
let key = clusters[i].key;
|
||||
let dist = dists[key][mins[key]];
|
||||
if ( dist < min ) {
|
||||
minKey = key;
|
||||
min = dist;
|
||||
}
|
||||
}
|
||||
if ( (opts.mode === 'threshold' && min >= opts.threshold) ||
|
||||
(opts.mode === 'dendrogram' && clusters.length === 1) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let c1 = index[minKey];
|
||||
let c2 = index[mins[minKey]];
|
||||
let merged;
|
||||
|
||||
// Merge two closest clusters
|
||||
if ( opts.mode === 'dendrogram' ) {
|
||||
merged = {
|
||||
left: c1,
|
||||
right: c2,
|
||||
key: c1.key
|
||||
};
|
||||
}
|
||||
else {
|
||||
merged = {
|
||||
value: c1.value.concat(c2.value),
|
||||
key: c1.key
|
||||
};
|
||||
}
|
||||
|
||||
clusters[c1.index] = merged;
|
||||
clusters.splice(c2.index, 1);
|
||||
|
||||
index[c1.key] = merged;
|
||||
|
||||
// Update distances with new merged cluster
|
||||
for ( let i = 0; i < clusters.length; i++ ) {
|
||||
let cur = clusters[i];
|
||||
|
||||
if ( c1.key === cur.key ) {
|
||||
dist = Infinity;
|
||||
}
|
||||
else if ( opts.linkage === 'min' ) {
|
||||
dist = dists[c1.key][cur.key];
|
||||
if ( dists[c1.key][cur.key] > dists[c2.key][cur.key] ) {
|
||||
dist = dists[c2.key][cur.key];
|
||||
}
|
||||
}
|
||||
else if ( opts.linkage === 'max' ) {
|
||||
dist = dists[c1.key][cur.key];
|
||||
if ( dists[c1.key][cur.key] < dists[c2.key][cur.key] ) {
|
||||
dist = dists[c2.key][cur.key];
|
||||
}
|
||||
}
|
||||
else if ( opts.linkage === 'mean' ) {
|
||||
dist = (dists[c1.key][cur.key] * c1.size + dists[c2.key][cur.key] * c2.size) / (c1.size + c2.size);
|
||||
}
|
||||
else {
|
||||
if ( opts.mode === 'dendrogram' )
|
||||
dist = getDist( cur.value, c1.value );
|
||||
else
|
||||
dist = getDist( cur.value[0], c1.value[0] );
|
||||
}
|
||||
|
||||
dists[c1.key][cur.key] = dists[cur.key][c1.key] = dist; // distance matrix is symmetric
|
||||
}
|
||||
|
||||
// Update cached mins
|
||||
for ( let i = 0; i < clusters.length; i++ ) {
|
||||
let key1 = clusters[i].key;
|
||||
if ( mins[key1] === c1.key || mins[key1] === c2.key ) {
|
||||
let min = key1;
|
||||
for ( let j = 0; j < clusters.length; j++ ) {
|
||||
let key2 = clusters[j].key;
|
||||
if ( dists[key1][key2] < dists[key1][min] ) {
|
||||
min = key2;
|
||||
}
|
||||
}
|
||||
mins[key1] = min;
|
||||
}
|
||||
clusters[i].index = i;
|
||||
}
|
||||
|
||||
// Clean up meta data used for clustering
|
||||
c1.key = c2.key = c1.index = c2.index = null;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
let getAllChildren = function( root, arr, cy ) {
|
||||
|
||||
if ( !root )
|
||||
return;
|
||||
|
||||
if ( root.value ) {
|
||||
arr.push( root.value );
|
||||
}
|
||||
else {
|
||||
if ( root.left )
|
||||
getAllChildren( root.left, arr, cy );
|
||||
if ( root.right )
|
||||
getAllChildren( root.right, arr, cy );
|
||||
}
|
||||
};
|
||||
|
||||
let buildDendrogram = function ( root, cy ) {
|
||||
|
||||
if ( !root )
|
||||
return '';
|
||||
|
||||
if ( root.left && root.right ) {
|
||||
|
||||
let leftStr = buildDendrogram( root.left, cy );
|
||||
let rightStr = buildDendrogram( root.right, cy );
|
||||
|
||||
let node = cy.add({group:'nodes', data: {id: leftStr + ',' + rightStr}});
|
||||
|
||||
cy.add({group:'edges', data: { source: leftStr, target: node.id() }});
|
||||
cy.add({group:'edges', data: { source: rightStr, target: node.id() }});
|
||||
|
||||
return node.id();
|
||||
}
|
||||
else if ( root.value ) {
|
||||
return root.value.id();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let buildClustersFromTree = function( root, k, cy ) {
|
||||
|
||||
if ( !root )
|
||||
return [];
|
||||
|
||||
let left = [], right = [], leaves = [];
|
||||
|
||||
if ( k === 0 ) { // don't cut tree, simply return all nodes as 1 single cluster
|
||||
if ( root.left )
|
||||
getAllChildren( root.left, left, cy );
|
||||
if ( root.right )
|
||||
getAllChildren( root.right, right, cy );
|
||||
|
||||
leaves = left.concat(right);
|
||||
return [ cy.collection(leaves) ];
|
||||
}
|
||||
else if ( k === 1 ) { // cut at root
|
||||
|
||||
if ( root.value ) { // leaf node
|
||||
return [ cy.collection( root.value ) ];
|
||||
}
|
||||
else {
|
||||
if ( root.left )
|
||||
getAllChildren( root.left, left, cy );
|
||||
if ( root.right )
|
||||
getAllChildren( root.right, right, cy );
|
||||
|
||||
return [ cy.collection(left), cy.collection(right) ];
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ( root.value ) {
|
||||
return [ cy.collection(root.value) ];
|
||||
}
|
||||
else {
|
||||
if ( root.left )
|
||||
left = buildClustersFromTree( root.left, k - 1, cy );
|
||||
if ( root.right )
|
||||
right = buildClustersFromTree( root.right, k - 1, cy );
|
||||
|
||||
return left.concat(right);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if( process.env.NODE_ENV !== 'production' ){ /* eslint-disable no-console, no-unused-vars */
|
||||
let printMatrix = function( M ) { // used for debugging purposes only
|
||||
let n = M.length;
|
||||
for(let i = 0; i < n; i++ ) {
|
||||
let row = '';
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
row += Math.round(M[i][j]*100)/100 + ' ';
|
||||
}
|
||||
console.log(row);
|
||||
}
|
||||
console.log('');
|
||||
};
|
||||
} /* eslint-enable */
|
||||
|
||||
let hierarchicalClustering = function( options ){
|
||||
let cy = this.cy();
|
||||
let nodes = this.nodes();
|
||||
|
||||
// Set parameters of algorithm: linkage type, distance metric, etc.
|
||||
let opts = setOptions( options );
|
||||
|
||||
let attrs = opts.attributes;
|
||||
let getDist = (n1, n2) => clusteringDistance( opts.distance, attrs.length, i => attrs[i](n1), i => attrs[i](n2), n1, n2 );
|
||||
|
||||
// Begin hierarchical algorithm
|
||||
let clusters = [];
|
||||
let dists = []; // distances between each pair of clusters
|
||||
let mins = []; // closest cluster for each cluster
|
||||
let index = []; // hash of all clusters by key
|
||||
|
||||
// In agglomerative (bottom-up) clustering, each node starts as its own cluster
|
||||
for ( let n = 0; n < nodes.length; n++ ) {
|
||||
let cluster = {
|
||||
value: (opts.mode === 'dendrogram') ? nodes[n] : [ nodes[n] ],
|
||||
key: n,
|
||||
index: n
|
||||
};
|
||||
clusters[n] = cluster;
|
||||
index[n] = cluster;
|
||||
dists[n] = [];
|
||||
mins[n] = 0;
|
||||
}
|
||||
|
||||
// Calculate the distance between each pair of clusters
|
||||
for ( let i = 0; i < clusters.length; i++ ) {
|
||||
for ( let j = 0; j <= i; j++ ) {
|
||||
let dist;
|
||||
|
||||
if ( opts.mode === 'dendrogram' ){ // modes store cluster values differently
|
||||
dist = (i === j) ? Infinity : getDist( clusters[i].value, clusters[j].value );
|
||||
} else {
|
||||
dist = (i === j) ? Infinity : getDist( clusters[i].value[0], clusters[j].value[0] );
|
||||
}
|
||||
|
||||
dists[i][j] = dist;
|
||||
dists[j][i] = dist;
|
||||
|
||||
if ( dist < dists[i][mins[i]] ) {
|
||||
mins[i] = j; // Cache mins: closest cluster to cluster i is cluster j
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the closest pair of clusters and merge them into a single cluster.
|
||||
// Update distances between new cluster and each of the old clusters, and loop until threshold reached.
|
||||
let merged = mergeClosest( clusters, index, dists, mins, opts );
|
||||
while ( merged ) {
|
||||
merged = mergeClosest( clusters, index, dists, mins, opts );
|
||||
}
|
||||
|
||||
let retClusters;
|
||||
|
||||
// Dendrogram mode builds the hierarchy and adds intermediary nodes + edges
|
||||
// in addition to returning the clusters.
|
||||
if ( opts.mode === 'dendrogram') {
|
||||
retClusters = buildClustersFromTree( clusters[0], opts.dendrogramDepth, cy );
|
||||
|
||||
if ( opts.addDendrogram )
|
||||
buildDendrogram( clusters[0], cy );
|
||||
}
|
||||
else { // Regular mode simply returns the clusters
|
||||
|
||||
retClusters = new Array(clusters.length);
|
||||
clusters.forEach( function( cluster, i ) {
|
||||
// Clean up meta data used for clustering
|
||||
cluster.key = cluster.index = null;
|
||||
|
||||
retClusters[i] = cy.collection( cluster.value );
|
||||
});
|
||||
}
|
||||
|
||||
return retClusters;
|
||||
};
|
||||
|
||||
export default { hierarchicalClustering, hca: hierarchicalClustering };
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import * as is from '../../is.mjs';
|
||||
import { defaults } from '../../util/index.mjs';
|
||||
|
||||
const hierholzerDefaults = defaults({
|
||||
root: undefined,
|
||||
directed: false
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
hierholzer: function( options ){
|
||||
if (!is.plainObject(options)) {
|
||||
let args = arguments;
|
||||
options = { root: args[0], directed: args[1] };
|
||||
}
|
||||
let { root, directed } = hierholzerDefaults(options);
|
||||
let eles = this;
|
||||
let dflag = false;
|
||||
let oddIn;
|
||||
let oddOut;
|
||||
let startVertex;
|
||||
if (root) startVertex = is.string(root) ? this.filter(root)[0].id() : root[0].id();
|
||||
let nodes = {};
|
||||
let edges = {};
|
||||
|
||||
if (directed) {
|
||||
eles.forEach(function(ele){
|
||||
let id = ele.id();
|
||||
if(ele.isNode()) {
|
||||
let ind = ele.indegree(true);
|
||||
let outd = ele.outdegree(true);
|
||||
let d1 = ind - outd;
|
||||
let d2 = outd - ind;
|
||||
if (d1 == 1) {
|
||||
if (oddIn) dflag = true;
|
||||
else oddIn = id;
|
||||
} else if (d2 == 1) {
|
||||
if (oddOut) dflag = true;
|
||||
else oddOut = id;
|
||||
} else if ((d2 > 1) || (d1 > 1)) {
|
||||
dflag = true;
|
||||
}
|
||||
nodes[id] = [];
|
||||
ele.outgoers().forEach(e => {
|
||||
if (e.isEdge()) nodes[id].push(e.id());
|
||||
});
|
||||
} else {
|
||||
edges[id] = [undefined, ele.target().id()];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
eles.forEach(function(ele){
|
||||
let id = ele.id();
|
||||
if(ele.isNode()) {
|
||||
let d = ele.degree(true);
|
||||
if (d%2) {
|
||||
if (!oddIn) oddIn = id;
|
||||
else if (!oddOut) oddOut = id;
|
||||
else dflag = true;
|
||||
}
|
||||
nodes[id] = [];
|
||||
ele.connectedEdges().forEach(e => nodes[id].push(e.id()));
|
||||
} else {
|
||||
edges[id] = [ele.source().id(), ele.target().id()];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let result = {
|
||||
found: false,
|
||||
trail: undefined
|
||||
};
|
||||
|
||||
if (dflag) return result;
|
||||
else if (oddOut && oddIn) {
|
||||
if (directed) {
|
||||
if (startVertex && (oddOut != startVertex)) {
|
||||
return result;
|
||||
}
|
||||
startVertex = oddOut;
|
||||
} else {
|
||||
if (startVertex && (oddOut != startVertex) && (oddIn != startVertex)) {
|
||||
return result;
|
||||
} else if (!startVertex) {
|
||||
startVertex = oddOut;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!startVertex) startVertex = eles[0].id();
|
||||
}
|
||||
|
||||
const walk = (v) => {
|
||||
let currentNode = v;
|
||||
let subtour = [v];
|
||||
let adj, adjTail, adjHead;
|
||||
while (nodes[currentNode].length) {
|
||||
adj = nodes[currentNode].shift();
|
||||
adjTail = edges[adj][0];
|
||||
adjHead = edges[adj][1];
|
||||
if (currentNode != adjHead) {
|
||||
nodes[adjHead] = nodes[adjHead].filter(e => e != adj);
|
||||
currentNode = adjHead;
|
||||
} else if (!directed && (currentNode != adjTail)) {
|
||||
nodes[adjTail] = nodes[adjTail].filter(e => e != adj);
|
||||
currentNode = adjTail;
|
||||
}
|
||||
subtour.unshift(adj);
|
||||
subtour.unshift(currentNode);
|
||||
}
|
||||
return subtour;
|
||||
};
|
||||
|
||||
let trail = [];
|
||||
let subtour = [];
|
||||
subtour = walk(startVertex);
|
||||
while (subtour.length != 1) {
|
||||
if (nodes[subtour[0]].length == 0) {
|
||||
trail.unshift(eles.getElementById(subtour.shift()));
|
||||
trail.unshift(eles.getElementById(subtour.shift()));
|
||||
} else {
|
||||
subtour = walk(subtour.shift()).concat(subtour);
|
||||
}
|
||||
}
|
||||
trail.unshift(eles.getElementById(subtour.shift())); // final node
|
||||
|
||||
for (let d in nodes) {
|
||||
if (nodes[d].length) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
result.found = true;
|
||||
result.trail = this.spawn( trail, true );
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export default elesfn;
|
||||
Generated
Vendored
+118
@@ -0,0 +1,118 @@
|
||||
let hopcroftTarjanBiconnected = function() {
|
||||
let eles = this;
|
||||
let nodes = {};
|
||||
let id = 0;
|
||||
let edgeCount = 0;
|
||||
let components = [];
|
||||
let stack = [];
|
||||
let visitedEdges = {};
|
||||
|
||||
const buildComponent = (x, y) => {
|
||||
let i = stack.length-1;
|
||||
let cutset = [];
|
||||
let component = eles.spawn();
|
||||
|
||||
while (stack[i].x != x || stack[i].y != y) {
|
||||
cutset.push(stack.pop().edge);
|
||||
i--;
|
||||
}
|
||||
cutset.push(stack.pop().edge);
|
||||
|
||||
cutset.forEach(edge => {
|
||||
let connectedNodes = edge.connectedNodes()
|
||||
.intersection(eles);
|
||||
component.merge(edge);
|
||||
connectedNodes.forEach(node => {
|
||||
const nodeId = node.id();
|
||||
const connectedEdges = node.connectedEdges()
|
||||
.intersection(eles);
|
||||
component.merge(node);
|
||||
if (!nodes[nodeId].cutVertex) {
|
||||
component.merge(connectedEdges);
|
||||
} else {
|
||||
component.merge(connectedEdges.filter(edge => edge.isLoop()));
|
||||
}
|
||||
});
|
||||
});
|
||||
components.push(component);
|
||||
};
|
||||
|
||||
const biconnectedSearch = (root, currentNode, parent) => {
|
||||
if (root === parent) edgeCount += 1;
|
||||
nodes[currentNode] = {
|
||||
id : id,
|
||||
low : id++,
|
||||
cutVertex : false
|
||||
};
|
||||
let edges = eles.getElementById(currentNode)
|
||||
.connectedEdges()
|
||||
.intersection(eles);
|
||||
|
||||
if (edges.size() === 0) {
|
||||
components.push(eles.spawn(eles.getElementById(currentNode)));
|
||||
} else {
|
||||
let sourceId, targetId, otherNodeId, edgeId;
|
||||
|
||||
edges.forEach(edge => {
|
||||
sourceId = edge.source().id();
|
||||
targetId = edge.target().id();
|
||||
otherNodeId = (sourceId === currentNode) ? targetId : sourceId;
|
||||
|
||||
if (otherNodeId !== parent) {
|
||||
edgeId = edge.id();
|
||||
|
||||
if (!visitedEdges[edgeId]) {
|
||||
visitedEdges[edgeId] = true;
|
||||
stack.push({
|
||||
x : currentNode,
|
||||
y : otherNodeId,
|
||||
edge
|
||||
});
|
||||
}
|
||||
|
||||
if (!(otherNodeId in nodes)) {
|
||||
biconnectedSearch(root, otherNodeId, currentNode);
|
||||
nodes[currentNode].low = Math.min(nodes[currentNode].low,
|
||||
nodes[otherNodeId].low);
|
||||
|
||||
if (nodes[currentNode].id <= nodes[otherNodeId].low) {
|
||||
nodes[currentNode].cutVertex = true;
|
||||
buildComponent(currentNode, otherNodeId);
|
||||
}
|
||||
} else {
|
||||
nodes[currentNode].low = Math.min(nodes[currentNode].low,
|
||||
nodes[otherNodeId].id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
eles.forEach(ele => {
|
||||
if (ele.isNode()) {
|
||||
let nodeId = ele.id();
|
||||
|
||||
if (!(nodeId in nodes)) {
|
||||
edgeCount = 0;
|
||||
biconnectedSearch(nodeId, nodeId);
|
||||
nodes[nodeId].cutVertex = (edgeCount > 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let cutVertices = Object.keys(nodes)
|
||||
.filter(id => nodes[id].cutVertex)
|
||||
.map(id => eles.getElementById(id));
|
||||
|
||||
return {
|
||||
cut: eles.spawn(cutVertices),
|
||||
components
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
hopcroftTarjanBiconnected,
|
||||
htbc: hopcroftTarjanBiconnected,
|
||||
htb: hopcroftTarjanBiconnected,
|
||||
hopcroftTarjanBiconnectedComponents: hopcroftTarjanBiconnected
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import * as util from '../../util/index.mjs';
|
||||
import bfsDfs from './bfs-dfs.mjs';
|
||||
import dijkstra from './dijkstra.mjs';
|
||||
import kruskal from './kruskal.mjs';
|
||||
import aStar from './a-star.mjs';
|
||||
import floydWarshall from './floyd-warshall.mjs';
|
||||
import bellmanFord from './bellman-ford.mjs';
|
||||
import kargerStein from './karger-stein.mjs';
|
||||
import pageRank from './page-rank.mjs';
|
||||
import degreeCentrality from './degree-centrality.mjs';
|
||||
import closenessCentrality from './closeness-centrality.mjs';
|
||||
import betweennessCentrality from './betweenness-centrality.mjs';
|
||||
import markovClustering from './markov-clustering.mjs';
|
||||
import kClustering from './k-clustering.mjs';
|
||||
import hierarchicalClustering from './hierarchical-clustering.mjs';
|
||||
import affinityPropagation from './affinity-propagation.mjs';
|
||||
import hierholzer from './hierholzer.mjs';
|
||||
import hopcroftTarjanBiconnected from './hopcroft-tarjan-biconnected.mjs';
|
||||
import tarjanStronglyConnected from './tarjan-strongly-connected.mjs';
|
||||
|
||||
var elesfn = {};
|
||||
|
||||
[
|
||||
bfsDfs,
|
||||
dijkstra,
|
||||
kruskal,
|
||||
aStar,
|
||||
floydWarshall,
|
||||
bellmanFord,
|
||||
kargerStein,
|
||||
pageRank,
|
||||
degreeCentrality,
|
||||
closenessCentrality,
|
||||
betweennessCentrality,
|
||||
markovClustering,
|
||||
kClustering,
|
||||
hierarchicalClustering,
|
||||
affinityPropagation,
|
||||
hierholzer,
|
||||
hopcroftTarjanBiconnected,
|
||||
tarjanStronglyConnected
|
||||
].forEach(function(props) {
|
||||
util.extend(elesfn, props);
|
||||
});
|
||||
|
||||
export default elesfn;
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
// Implemented by Zoe Xi @zoexi for GSOC 2016
|
||||
// https://github.com/cytoscape/cytoscape.js-k-means
|
||||
|
||||
// References for k-means: https://harthur.github.io/clusterfck/
|
||||
// References for k-medoids: http://www.math.le.ac.uk/people/ag153/homepage/KmeansKmedoids/Kmeans_Kmedoids.html
|
||||
// References for fuzzy c-means: Ross, Fuzzy Logic w/Engineering Applications (2010), pages 352-353
|
||||
// http://yaikhom.com/2013/03/16/implementing-the-fuzzy-c-means-algorithm.html
|
||||
|
||||
import clusteringDistance from './clustering-distances.mjs';
|
||||
import * as util from '../../util/index.mjs';
|
||||
|
||||
let defaults = util.defaults({
|
||||
k: 2,
|
||||
m: 2,
|
||||
sensitivityThreshold: 0.0001,
|
||||
distance: 'euclidean',
|
||||
maxIterations: 10,
|
||||
attributes: [],
|
||||
testMode: false,
|
||||
testCentroids: null
|
||||
});
|
||||
|
||||
var setOptions = ( options ) => defaults( options );
|
||||
|
||||
if( process.env.NODE_ENV !== 'production' ){ /* eslint-disable no-console, no-unused-vars */
|
||||
var printMatrix = function( M ) { // used for debugging purposes only
|
||||
|
||||
for ( let i = 0; i < M.length; i++ ) {
|
||||
let row = '';
|
||||
for ( let j = 0; j < M[0].length; j++ ) {
|
||||
row += Number(M[i][j]).toFixed(3) + ' ';
|
||||
}
|
||||
console.log(row);
|
||||
}
|
||||
console.log('');
|
||||
};
|
||||
} /* eslint-enable */
|
||||
|
||||
let getDist = function(type, node, centroid, attributes, mode){
|
||||
let noNodeP = mode !== 'kMedoids';
|
||||
let getP = noNodeP ? ( i => centroid[i] ) : ( i => attributes[i](centroid) );
|
||||
let getQ = i => attributes[i](node);
|
||||
let nodeP = centroid;
|
||||
let nodeQ = node;
|
||||
|
||||
return clusteringDistance( type, attributes.length, getP, getQ, nodeP, nodeQ );
|
||||
};
|
||||
|
||||
let randomCentroids = function( nodes, k, attributes ) {
|
||||
let ndim = attributes.length;
|
||||
let min = new Array(ndim);
|
||||
let max = new Array(ndim);
|
||||
let centroids = new Array(k);
|
||||
let centroid = null;
|
||||
|
||||
// Find min, max values for each attribute dimension
|
||||
for ( let i = 0; i < ndim; i++ ) {
|
||||
min[i] = nodes.min( attributes[i] ).value;
|
||||
max[i] = nodes.max( attributes[i] ).value;
|
||||
}
|
||||
|
||||
// Build k centroids, each represented as an n-dim feature vector
|
||||
for ( let c = 0; c < k; c++ ) {
|
||||
centroid = [];
|
||||
for ( let i = 0; i < ndim; i++ ) {
|
||||
centroid[i] = Math.random() * (max[i] - min[i]) + min[i]; // random initial value
|
||||
}
|
||||
centroids[c] = centroid;
|
||||
}
|
||||
|
||||
return centroids;
|
||||
};
|
||||
|
||||
let classify = function( node, centroids, distance, attributes, type ) {
|
||||
let min = Infinity;
|
||||
let index = 0;
|
||||
|
||||
for ( let i = 0; i < centroids.length; i++ ) {
|
||||
let dist = getDist( distance, node, centroids[i], attributes, type );
|
||||
if (dist < min) {
|
||||
min = dist;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
};
|
||||
|
||||
let buildCluster = function( centroid, nodes, assignment ) {
|
||||
let cluster = [];
|
||||
let node = null;
|
||||
|
||||
for ( let n = 0; n < nodes.length; n++ ) {
|
||||
node = nodes[n];
|
||||
if ( assignment[ node.id() ] === centroid ) {
|
||||
//console.log("Node " + node.id() + " is associated with medoid #: " + m);
|
||||
cluster.push( node );
|
||||
}
|
||||
}
|
||||
return cluster;
|
||||
};
|
||||
|
||||
let haveValuesConverged = function( v1, v2, sensitivityThreshold ){
|
||||
return Math.abs( v2 - v1 ) <= sensitivityThreshold;
|
||||
};
|
||||
|
||||
let haveMatricesConverged = function( v1, v2, sensitivityThreshold ) {
|
||||
for ( let i = 0; i < v1.length; i++ ) {
|
||||
for (let j = 0; j < v1[i].length; j++ ) {
|
||||
let diff = Math.abs( v1[i][j] - v2[i][j] );
|
||||
|
||||
if( diff > sensitivityThreshold ){ return false; }
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
let seenBefore = function ( node, medoids, n ) {
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
if ( node === medoids[i] )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let randomMedoids = function( nodes, k ) {
|
||||
let medoids = new Array(k);
|
||||
|
||||
// For small data sets, the probability of medoid conflict is greater,
|
||||
// so we need to check to see if we've already seen or chose this node before.
|
||||
if (nodes.length < 50) {
|
||||
|
||||
// Randomly select k medoids from the n nodes
|
||||
for (let i = 0; i < k; i++) {
|
||||
let node = nodes[ Math.floor( Math.random() * nodes.length ) ];
|
||||
|
||||
// If we've already chosen this node to be a medoid, don't choose it again (for small data sets).
|
||||
// Instead choose a different random node.
|
||||
while ( seenBefore( node, medoids, i ) ) {
|
||||
node = nodes[ Math.floor( Math.random() * nodes.length ) ];
|
||||
}
|
||||
medoids[i] = node;
|
||||
}
|
||||
}
|
||||
else { // Relatively large data set, so pretty safe to not check and just select random nodes
|
||||
for (let i = 0; i < k; i++) {
|
||||
medoids[i] = nodes[ Math.floor( Math.random() * nodes.length ) ];
|
||||
}
|
||||
}
|
||||
return medoids;
|
||||
};
|
||||
|
||||
let findCost = function( potentialNewMedoid, cluster, attributes ) {
|
||||
let cost = 0;
|
||||
for ( let n = 0; n < cluster.length; n++ ) {
|
||||
cost += getDist( 'manhattan', cluster[n], potentialNewMedoid, attributes, 'kMedoids' );
|
||||
}
|
||||
return cost;
|
||||
};
|
||||
|
||||
let kMeans = function( options ){
|
||||
let cy = this.cy();
|
||||
let nodes = this.nodes();
|
||||
let node = null;
|
||||
|
||||
// Set parameters of algorithm: # of clusters, distance metric, etc.
|
||||
let opts = setOptions( options );
|
||||
|
||||
// Begin k-means algorithm
|
||||
let clusters = new Array(opts.k);
|
||||
let assignment = {};
|
||||
let centroids;
|
||||
|
||||
// Step 1: Initialize centroid positions
|
||||
if ( opts.testMode ) {
|
||||
if( typeof opts.testCentroids === 'number') {
|
||||
// TODO: implement a seeded random number generator.
|
||||
let seed = opts.testCentroids;
|
||||
centroids = randomCentroids( nodes, opts.k, opts.attributes, seed );
|
||||
}
|
||||
else if ( typeof opts.testCentroids === 'object') {
|
||||
centroids = opts.testCentroids;
|
||||
}
|
||||
else {
|
||||
centroids = randomCentroids( nodes, opts.k, opts.attributes );
|
||||
}
|
||||
}
|
||||
else {
|
||||
centroids = randomCentroids( nodes, opts.k, opts.attributes );
|
||||
}
|
||||
|
||||
let isStillMoving = true;
|
||||
let iterations = 0;
|
||||
|
||||
while ( isStillMoving && iterations < opts.maxIterations ) {
|
||||
|
||||
// Step 2: Assign nodes to the nearest centroid
|
||||
for ( let n = 0; n < nodes.length; n++ ) {
|
||||
node = nodes[n];
|
||||
// Determine which cluster this node belongs to: node id => cluster #
|
||||
assignment[ node.id() ] = classify( node, centroids, opts.distance, opts.attributes, 'kMeans' );
|
||||
}
|
||||
|
||||
// Step 3: For each of the k clusters, update its centroid
|
||||
isStillMoving = false;
|
||||
for ( let c = 0; c < opts.k; c++ ) {
|
||||
|
||||
// Get all nodes that belong to this cluster
|
||||
let cluster = buildCluster( c, nodes, assignment );
|
||||
|
||||
if ( cluster.length === 0 ) { // If cluster is empty, break out early & move to next cluster
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update centroids by calculating avg of all nodes within the cluster.
|
||||
let ndim = opts.attributes.length;
|
||||
let centroid = centroids[c]; // [ dim_1, dim_2, dim_3, ... , dim_n ]
|
||||
let newCentroid = new Array(ndim);
|
||||
let sum = new Array(ndim);
|
||||
|
||||
for ( let d = 0; d < ndim; d++ ) {
|
||||
sum[d] = 0.0;
|
||||
for ( let i = 0; i < cluster.length; i++ ) {
|
||||
node = cluster[i];
|
||||
sum[d] += opts.attributes[d](node);
|
||||
}
|
||||
newCentroid[d] = sum[d] / cluster.length;
|
||||
|
||||
// Check to see if algorithm has converged, i.e. when centroids no longer change
|
||||
if ( !haveValuesConverged(newCentroid[d], centroid[d], opts.sensitivityThreshold) ) {
|
||||
isStillMoving = true;
|
||||
}
|
||||
}
|
||||
|
||||
centroids[c] = newCentroid;
|
||||
clusters[c] = cy.collection( cluster );
|
||||
}
|
||||
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return clusters;
|
||||
};
|
||||
|
||||
let kMedoids = function( options ) {
|
||||
let cy = this.cy();
|
||||
let nodes = this.nodes();
|
||||
let node = null;
|
||||
let opts = setOptions( options );
|
||||
|
||||
// Begin k-medoids algorithm
|
||||
let clusters = new Array(opts.k);
|
||||
let medoids;
|
||||
let assignment = {};
|
||||
let curCost;
|
||||
let minCosts = new Array(opts.k); // minimum cost configuration for each cluster
|
||||
|
||||
// Step 1: Initialize k medoids
|
||||
if ( opts.testMode ) {
|
||||
if( typeof opts.testCentroids === 'number') {
|
||||
// TODO: implement random generator so user can just input seed number
|
||||
}
|
||||
else if ( typeof opts.testCentroids === 'object') {
|
||||
medoids = opts.testCentroids;
|
||||
}
|
||||
else {
|
||||
medoids = randomMedoids(nodes, opts.k);
|
||||
}
|
||||
}
|
||||
else {
|
||||
medoids = randomMedoids(nodes, opts.k);
|
||||
}
|
||||
|
||||
let isStillMoving = true;
|
||||
let iterations = 0;
|
||||
|
||||
while ( isStillMoving && iterations < opts.maxIterations ) {
|
||||
|
||||
// Step 2: Assign nodes to the nearest medoid
|
||||
for ( let n = 0; n < nodes.length; n++ ) {
|
||||
node = nodes[n];
|
||||
// Determine which cluster this node belongs to: node id => cluster #
|
||||
assignment[ node.id() ] = classify( node, medoids, opts.distance, opts.attributes, 'kMedoids' );
|
||||
}
|
||||
|
||||
isStillMoving = false;
|
||||
// Step 3: For each medoid m, and for each node associated with mediod m,
|
||||
// select the node with the lowest configuration cost as new medoid.
|
||||
for ( let m = 0; m < medoids.length; m++ ) {
|
||||
|
||||
// Get all nodes that belong to this medoid
|
||||
let cluster = buildCluster( m, nodes, assignment );
|
||||
|
||||
if ( cluster.length === 0 ) { // If cluster is empty, break out early & move to next cluster
|
||||
continue;
|
||||
}
|
||||
|
||||
minCosts[m] = findCost( medoids[m], cluster, opts.attributes ); // original cost
|
||||
|
||||
// Select different medoid if its configuration has the lowest cost
|
||||
for ( let n = 0; n < cluster.length; n++ ) {
|
||||
curCost = findCost( cluster[n], cluster, opts.attributes );
|
||||
if ( curCost < minCosts[m] ) {
|
||||
minCosts[m] = curCost;
|
||||
medoids[m] = cluster[n];
|
||||
isStillMoving = true;
|
||||
}
|
||||
}
|
||||
|
||||
clusters[m] = cy.collection( cluster );
|
||||
}
|
||||
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return clusters;
|
||||
};
|
||||
|
||||
let updateCentroids = function( centroids, nodes, U, weight, opts ) {
|
||||
let numerator, denominator;
|
||||
|
||||
for ( let n = 0; n < nodes.length; n++ ) {
|
||||
for ( let c = 0; c < centroids.length; c++ ) {
|
||||
weight[n][c] = Math.pow( U[n][c], opts.m );
|
||||
}
|
||||
}
|
||||
|
||||
for ( let c = 0; c < centroids.length; c++ ) {
|
||||
for ( let dim = 0; dim < opts.attributes.length; dim++ ) {
|
||||
numerator = 0;
|
||||
denominator = 0;
|
||||
for ( let n = 0; n < nodes.length; n++ ) {
|
||||
numerator += weight[n][c] * opts.attributes[dim](nodes[n]);
|
||||
denominator += weight[n][c];
|
||||
}
|
||||
centroids[c][dim] = numerator / denominator;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let updateMembership = function( U, _U, centroids, nodes, opts ) {
|
||||
// Save previous step
|
||||
for (let i = 0; i < U.length; i++) {
|
||||
_U[i] = U[i].slice();
|
||||
}
|
||||
|
||||
let sum, numerator, denominator;
|
||||
let pow = 2 / (opts.m - 1);
|
||||
|
||||
for ( let c = 0; c < centroids.length; c++ ) {
|
||||
for ( let n = 0; n < nodes.length; n++ ) {
|
||||
|
||||
sum = 0;
|
||||
for ( let k = 0; k < centroids.length; k++ ) { // against all other centroids
|
||||
numerator = getDist( opts.distance, nodes[n], centroids[c], opts.attributes, 'cmeans' );
|
||||
denominator = getDist( opts.distance, nodes[n], centroids[k], opts.attributes, 'cmeans' );
|
||||
sum += Math.pow( numerator / denominator, pow );
|
||||
}
|
||||
U[n][c] = 1 / sum;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let assign = function( nodes, U, opts, cy ) {
|
||||
let clusters = new Array(opts.k);
|
||||
for ( let c = 0; c < clusters.length; c++ ) {
|
||||
clusters[c] = [];
|
||||
}
|
||||
|
||||
let max;
|
||||
let index;
|
||||
for ( let n = 0; n < U.length; n++ ) { // for each node (U is N x C matrix)
|
||||
max = -Infinity;
|
||||
index = -1;
|
||||
// Determine which cluster the node is most likely to belong in
|
||||
for ( let c = 0; c < U[0].length; c++ ) {
|
||||
if ( U[n][c] > max ) {
|
||||
max = U[n][c];
|
||||
index = c;
|
||||
}
|
||||
}
|
||||
clusters[index].push( nodes[n] );
|
||||
}
|
||||
|
||||
// Turn every array into a collection of nodes
|
||||
for ( let c = 0; c < clusters.length; c++ ) {
|
||||
clusters[c] = cy.collection( clusters[c] );
|
||||
}
|
||||
return clusters;
|
||||
};
|
||||
|
||||
let fuzzyCMeans = function( options ) {
|
||||
let cy = this.cy();
|
||||
let nodes = this.nodes();
|
||||
let opts = setOptions( options );
|
||||
|
||||
// Begin fuzzy c-means algorithm
|
||||
let clusters;
|
||||
let centroids;
|
||||
let U;
|
||||
let _U;
|
||||
let weight;
|
||||
|
||||
// Step 1: Initialize letiables.
|
||||
_U = new Array(nodes.length);
|
||||
for ( let i = 0; i < nodes.length; i++ ) { // N x C matrix
|
||||
_U[i] = new Array(opts.k);
|
||||
}
|
||||
|
||||
U = new Array(nodes.length);
|
||||
for ( let i = 0; i < nodes.length; i++ ) { // N x C matrix
|
||||
U[i] = new Array(opts.k);
|
||||
}
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let total = 0;
|
||||
for (let j = 0; j < opts.k; j++) {
|
||||
U[i][j] = Math.random();
|
||||
total += U[i][j];
|
||||
}
|
||||
for (let j = 0; j < opts.k; j++) {
|
||||
U[i][j] = U[i][j] / total;
|
||||
}
|
||||
}
|
||||
|
||||
centroids = new Array(opts.k);
|
||||
for ( let i = 0; i < opts.k; i++ ) {
|
||||
centroids[i] = new Array(opts.attributes.length);
|
||||
}
|
||||
|
||||
weight = new Array(nodes.length);
|
||||
for ( let i = 0; i < nodes.length; i++ ) { // N x C matrix
|
||||
weight[i] = new Array(opts.k);
|
||||
}
|
||||
// end init FCM
|
||||
|
||||
let isStillMoving = true;
|
||||
let iterations = 0;
|
||||
|
||||
while ( isStillMoving && iterations < opts.maxIterations ) {
|
||||
isStillMoving = false;
|
||||
|
||||
// Step 2: Calculate the centroids for each step.
|
||||
updateCentroids( centroids, nodes, U, weight, opts );
|
||||
|
||||
// Step 3: Update the partition matrix U.
|
||||
updateMembership( U, _U, centroids, nodes, opts );
|
||||
|
||||
// Step 4: Check for convergence.
|
||||
if ( !haveMatricesConverged( U, _U, opts.sensitivityThreshold ) ) {
|
||||
isStillMoving = true;
|
||||
}
|
||||
|
||||
iterations++;
|
||||
}
|
||||
|
||||
// Assign nodes to clusters with highest probability.
|
||||
clusters = assign( nodes, U, opts, cy );
|
||||
|
||||
return {
|
||||
clusters: clusters,
|
||||
degreeOfMembership: U
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
kMeans, kMedoids, fuzzyCMeans, fcm: fuzzyCMeans
|
||||
};
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import { error } from '../../util/index.mjs';
|
||||
|
||||
const sqrt2 = Math.sqrt(2);
|
||||
|
||||
// Function which colapses 2 (meta) nodes into one
|
||||
// Updates the remaining edge lists
|
||||
// Receives as a paramater the edge which causes the collapse
|
||||
const collapse = function( edgeIndex, nodeMap, remainingEdges ){
|
||||
if( remainingEdges.length === 0 ){
|
||||
error(`Karger-Stein must be run on a connected (sub)graph`);
|
||||
}
|
||||
|
||||
let edgeInfo = remainingEdges[ edgeIndex ];
|
||||
let sourceIn = edgeInfo[1];
|
||||
let targetIn = edgeInfo[2];
|
||||
let partition1 = nodeMap[ sourceIn ];
|
||||
let partition2 = nodeMap[ targetIn ];
|
||||
let newEdges = remainingEdges; // re-use array
|
||||
|
||||
// Delete all edges between partition1 and partition2
|
||||
for( let i = newEdges.length - 1; i >=0; i-- ){
|
||||
let edge = newEdges[i];
|
||||
let src = edge[1];
|
||||
let tgt = edge[2];
|
||||
|
||||
if(
|
||||
( nodeMap[ src ] === partition1 && nodeMap[ tgt ] === partition2 ) ||
|
||||
( nodeMap[ src ] === partition2 && nodeMap[ tgt ] === partition1 )
|
||||
){
|
||||
newEdges.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// All edges pointing to partition2 should now point to partition1
|
||||
for( let i = 0; i < newEdges.length; i++ ){
|
||||
let edge = newEdges[i];
|
||||
|
||||
if( edge[1] === partition2 ){ // Check source
|
||||
newEdges[i] = edge.slice(); // copy
|
||||
newEdges[i][1] = partition1;
|
||||
} else if( edge[2] === partition2 ){ // Check target
|
||||
newEdges[i] = edge.slice(); // copy
|
||||
newEdges[i][2] = partition1;
|
||||
}
|
||||
}
|
||||
|
||||
// Move all nodes from partition2 to partition1
|
||||
for( let i = 0; i < nodeMap.length; i++ ){
|
||||
if( nodeMap[i] === partition2 ){
|
||||
nodeMap[i] = partition1;
|
||||
}
|
||||
}
|
||||
|
||||
return newEdges;
|
||||
};
|
||||
|
||||
// Contracts a graph until we reach a certain number of meta nodes
|
||||
const contractUntil = function( metaNodeMap, remainingEdges, size, sizeLimit ){
|
||||
while( size > sizeLimit ){
|
||||
// Choose an edge randomly
|
||||
let edgeIndex = Math.floor( (Math.random() * remainingEdges.length) );
|
||||
|
||||
// Collapse graph based on edge
|
||||
remainingEdges = collapse( edgeIndex, metaNodeMap, remainingEdges );
|
||||
|
||||
size--;
|
||||
}
|
||||
|
||||
return remainingEdges;
|
||||
};
|
||||
|
||||
const elesfn = ({
|
||||
|
||||
// Computes the minimum cut of an undirected graph
|
||||
// Returns the correct answer with high probability
|
||||
kargerStein: function(){
|
||||
let { nodes, edges } = this.byGroup();
|
||||
edges.unmergeBy(edge => edge.isLoop());
|
||||
|
||||
let numNodes = nodes.length;
|
||||
let numEdges = edges.length;
|
||||
let numIter = Math.ceil( Math.pow( Math.log( numNodes ) / Math.LN2, 2 ) );
|
||||
let stopSize = Math.floor( numNodes / sqrt2 );
|
||||
|
||||
if( numNodes < 2 ){
|
||||
error( 'At least 2 nodes are required for Karger-Stein algorithm' );
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Now store edge destination as indexes
|
||||
// Format for each edge (edge index, source node index, target node index)
|
||||
let edgeIndexes = [];
|
||||
for( let i = 0; i < numEdges; i++ ){
|
||||
let e = edges[ i ];
|
||||
edgeIndexes.push([ i, nodes.indexOf(e.source()), nodes.indexOf(e.target()) ]);
|
||||
}
|
||||
|
||||
// We will store the best cut found here
|
||||
let minCutSize = Infinity;
|
||||
let minCutEdgeIndexes = [];
|
||||
let minCutNodeMap = new Array(numNodes);
|
||||
|
||||
// Initial meta node partition
|
||||
let metaNodeMap = new Array(numNodes);
|
||||
let metaNodeMap2 = new Array(numNodes);
|
||||
|
||||
let copyNodesMap = (from, to) => {
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
to[i] = from[i];
|
||||
}
|
||||
};
|
||||
|
||||
// Main loop
|
||||
for( let iter = 0; iter <= numIter; iter++ ){
|
||||
// Reset meta node partition
|
||||
for( let i = 0; i < numNodes; i++ ){ metaNodeMap[i] = i; }
|
||||
|
||||
// Contract until stop point (stopSize nodes)
|
||||
let edgesState = contractUntil( metaNodeMap, edgeIndexes.slice(), numNodes, stopSize );
|
||||
let edgesState2 = edgesState.slice(); // copy
|
||||
|
||||
// Create a copy of the colapsed nodes state
|
||||
copyNodesMap(metaNodeMap, metaNodeMap2);
|
||||
|
||||
// Run 2 iterations starting in the stop state
|
||||
let res1 = contractUntil( metaNodeMap, edgesState, stopSize, 2 );
|
||||
let res2 = contractUntil( metaNodeMap2, edgesState2, stopSize, 2 );
|
||||
|
||||
// Is any of the 2 results the best cut so far?
|
||||
if( res1.length <= res2.length && res1.length < minCutSize ){
|
||||
minCutSize = res1.length;
|
||||
minCutEdgeIndexes = res1;
|
||||
copyNodesMap(metaNodeMap, minCutNodeMap);
|
||||
} else if( res2.length <= res1.length && res2.length < minCutSize ){
|
||||
minCutSize = res2.length;
|
||||
minCutEdgeIndexes = res2;
|
||||
copyNodesMap(metaNodeMap2, minCutNodeMap);
|
||||
}
|
||||
} // end of main loop
|
||||
|
||||
|
||||
// Construct result
|
||||
let cut = this.spawn( minCutEdgeIndexes.map(e => edges[e[0]]) );
|
||||
let partition1 = this.spawn();
|
||||
let partition2 = this.spawn();
|
||||
|
||||
// traverse metaNodeMap for best cut
|
||||
let witnessNodePartition = minCutNodeMap[0];
|
||||
for( let i = 0; i < minCutNodeMap.length; i++ ){
|
||||
let partitionId = minCutNodeMap[i];
|
||||
let node = nodes[i];
|
||||
|
||||
if( partitionId === witnessNodePartition ){
|
||||
partition1.merge( node );
|
||||
} else {
|
||||
partition2.merge( node );
|
||||
}
|
||||
}
|
||||
|
||||
// construct components corresponding to each disjoint subset of nodes
|
||||
const constructComponent = (subset) => {
|
||||
const component = this.spawn();
|
||||
|
||||
subset.forEach(node => {
|
||||
component.merge(node);
|
||||
|
||||
node.connectedEdges().forEach(edge => {
|
||||
// ensure edge is within calling collection and edge is not in cut
|
||||
if (this.contains(edge) && !cut.contains(edge)) {
|
||||
component.merge(edge);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
const components = [
|
||||
constructComponent(partition1),
|
||||
constructComponent(partition2)
|
||||
];
|
||||
|
||||
let ret = {
|
||||
cut,
|
||||
components,
|
||||
|
||||
// n.b. partitions are included to be compatible with the old api spec
|
||||
// (could be removed in a future major version)
|
||||
partition1,
|
||||
partition2
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
}); // elesfn
|
||||
|
||||
|
||||
export default elesfn;
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
let elesfn = ({
|
||||
|
||||
// kruskal's algorithm (finds min spanning tree, assuming undirected graph)
|
||||
// implemented from pseudocode from wikipedia
|
||||
kruskal: function( weightFn ){
|
||||
weightFn = weightFn || ( edge => 1 );
|
||||
|
||||
let { nodes, edges } = this.byGroup();
|
||||
let numNodes = nodes.length;
|
||||
let forest = new Array(numNodes);
|
||||
let A = nodes; // assumes byGroup() creates new collections that can be safely mutated
|
||||
|
||||
let findSetIndex = ele => {
|
||||
for( let i = 0; i < forest.length; i++ ){
|
||||
let eles = forest[i];
|
||||
|
||||
if( eles.has(ele) ){ return i; }
|
||||
}
|
||||
};
|
||||
|
||||
// start with one forest per node
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
forest[i] = this.spawn( nodes[i] );
|
||||
}
|
||||
|
||||
let S = edges.sort( (a, b) => weightFn(a) - weightFn(b) );
|
||||
|
||||
for( let i = 0; i < S.length; i++ ){
|
||||
let edge = S[i];
|
||||
let u = edge.source()[0];
|
||||
let v = edge.target()[0];
|
||||
let setUIndex = findSetIndex(u);
|
||||
let setVIndex = findSetIndex(v);
|
||||
let setU = forest[ setUIndex ];
|
||||
let setV = forest[ setVIndex ];
|
||||
|
||||
if( setUIndex !== setVIndex ){
|
||||
A.merge( edge );
|
||||
|
||||
// combine forests for u and v
|
||||
setU.merge( setV );
|
||||
forest.splice( setVIndex, 1 );
|
||||
}
|
||||
}
|
||||
|
||||
return A;
|
||||
}
|
||||
});
|
||||
|
||||
export default elesfn;
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
// Implemented by Zoe Xi @zoexi for GSOC 2016
|
||||
// https://github.com/cytoscape/cytoscape.js-markov-cluster
|
||||
|
||||
// Implemented from Stijn van Dongen's (author of MCL algorithm) documentation: http://micans.org/mcl/
|
||||
// and lecture notes: https://www.cs.ucsb.edu/~xyan/classes/CS595D-2009winter/MCL_Presentation2.pdf
|
||||
|
||||
import * as util from '../../util/index.mjs';
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
let defaults = util.defaults({
|
||||
expandFactor: 2, // affects time of computation and cluster granularity to some extent: M * M
|
||||
inflateFactor: 2, // affects cluster granularity (the greater the value, the more clusters): M(i,j) / E(j)
|
||||
multFactor: 1, // optional self loops for each node. Use a neutral value to improve cluster computations.
|
||||
maxIterations: 20, // maximum number of iterations of the MCL algorithm in a single run
|
||||
attributes: [ // attributes/features used to group nodes, ie. similarity values between nodes
|
||||
function(edge) {
|
||||
return 1;
|
||||
}
|
||||
]
|
||||
});
|
||||
/* eslint-enable */
|
||||
|
||||
let setOptions = ( options ) => defaults( options );
|
||||
|
||||
/* eslint-disable no-unused-vars, no-console */
|
||||
if( process.env.NODE_ENV !== 'production' ){
|
||||
var printMatrix = function( M ) { // used for debugging purposes only
|
||||
let n = Math.sqrt(M.length);
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
let row = '';
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
row += Number(M[i*n+j]).toFixed(3) + ' ';
|
||||
}
|
||||
console.log(row);
|
||||
}
|
||||
console.log('');
|
||||
};
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
let getSimilarity = function( edge, attributes ) {
|
||||
let total = 0;
|
||||
for ( let i = 0; i < attributes.length; i++ ) {
|
||||
total += attributes[i]( edge );
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
let addLoops = function( M, n, val ) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
M[i * n + i] = val;
|
||||
}
|
||||
};
|
||||
|
||||
let normalize = function( M, n ) {
|
||||
let sum;
|
||||
for ( let col = 0; col < n; col++ ) {
|
||||
sum = 0;
|
||||
for ( let row = 0; row < n; row++ ) {
|
||||
sum += M[row * n + col];
|
||||
}
|
||||
for ( let row = 0; row < n; row++ ) {
|
||||
M[row * n + col] = M[row * n + col] / sum;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: blocked matrix multiplication?
|
||||
let mmult = function( A, B, n ) {
|
||||
let C = new Array( n * n );
|
||||
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
C[i * n + j] = 0;
|
||||
}
|
||||
|
||||
for ( let k = 0; k < n; k++ ) {
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
C[i * n + j] += A[i * n + k] * B[k * n + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
};
|
||||
|
||||
let expand = function( M, n, expandFactor /** power **/ ) {
|
||||
let _M = M.slice(0);
|
||||
|
||||
for ( let p = 1; p < expandFactor; p++ ) {
|
||||
M = mmult( M, _M, n );
|
||||
}
|
||||
return M;
|
||||
};
|
||||
|
||||
let inflate = function( M, n, inflateFactor /** r **/ ) {
|
||||
let _M = new Array( n * n );
|
||||
|
||||
// M(i,j) ^ inflatePower
|
||||
for ( let i = 0; i < n * n; i++ ) {
|
||||
_M[i] = Math.pow( M[i], inflateFactor );
|
||||
}
|
||||
|
||||
normalize( _M, n );
|
||||
|
||||
return _M;
|
||||
};
|
||||
|
||||
let hasConverged = function( M, _M, n2, roundFactor ) {
|
||||
// Check that both matrices have the same elements (i,j)
|
||||
for ( let i = 0; i < n2; i++ ) {
|
||||
let v1 = Math.round( M[i] * Math.pow(10, roundFactor) ) / Math.pow(10, roundFactor); // truncate to 'roundFactor' decimal places
|
||||
let v2 = Math.round( _M[i] * Math.pow(10, roundFactor) ) / Math.pow(10, roundFactor);
|
||||
|
||||
if ( v1 !== v2 ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
let assign = function( M, n, nodes, cy ) {
|
||||
let clusters = [];
|
||||
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
let cluster = [];
|
||||
for ( let j = 0; j < n; j++ ) {
|
||||
// Row-wise attractors and elements that they attract belong in same cluster
|
||||
if ( Math.round( M[i * n + j] * 1000 ) / 1000 > 0 ) {
|
||||
cluster.push( nodes[j] );
|
||||
}
|
||||
}
|
||||
if ( cluster.length !== 0 ) {
|
||||
clusters.push( cy.collection(cluster) );
|
||||
}
|
||||
}
|
||||
return clusters;
|
||||
};
|
||||
|
||||
let isDuplicate = function( c1, c2 ) {
|
||||
for (let i = 0; i < c1.length; i++) {
|
||||
if (!c2[i] || c1[i].id() !== c2[i].id()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
let removeDuplicates = function( clusters ) {
|
||||
|
||||
for (let i = 0; i < clusters.length; i++) {
|
||||
for (let j = 0; j < clusters.length; j++) {
|
||||
if (i != j && isDuplicate(clusters[i], clusters[j])) {
|
||||
clusters.splice(j, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return clusters;
|
||||
};
|
||||
|
||||
let markovClustering = function( options ) {
|
||||
let nodes = this.nodes();
|
||||
let edges = this.edges();
|
||||
let cy = this.cy();
|
||||
|
||||
// Set parameters of algorithm:
|
||||
let opts = setOptions( options );
|
||||
|
||||
// Map each node to its position in node array
|
||||
let id2position = {};
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
id2position[ nodes[i].id() ] = i;
|
||||
}
|
||||
|
||||
// Generate stochastic matrix M from input graph G (should be symmetric/undirected)
|
||||
let n = nodes.length, n2 = n * n;
|
||||
let M = new Array( n2 ), _M;
|
||||
for ( let i = 0; i < n2; i++ ) {
|
||||
M[i] = 0;
|
||||
}
|
||||
|
||||
for ( let e = 0; e < edges.length; e++ ) {
|
||||
let edge = edges[e];
|
||||
let i = id2position[ edge.source().id() ];
|
||||
let j = id2position[ edge.target().id() ];
|
||||
|
||||
let sim = getSimilarity( edge, opts.attributes );
|
||||
|
||||
M[i * n + j] += sim; // G should be symmetric and undirected
|
||||
M[j * n + i] += sim;
|
||||
}
|
||||
|
||||
// Begin Markov cluster algorithm
|
||||
|
||||
// Step 1: Add self loops to each node, ie. add multFactor to matrix diagonal
|
||||
addLoops( M, n, opts.multFactor );
|
||||
|
||||
// Step 2: M = normalize( M );
|
||||
normalize( M, n );
|
||||
|
||||
let isStillMoving = true;
|
||||
let iterations = 0;
|
||||
while ( isStillMoving && iterations < opts.maxIterations ) {
|
||||
|
||||
isStillMoving = false;
|
||||
|
||||
// Step 3:
|
||||
_M = expand( M, n, opts.expandFactor );
|
||||
|
||||
// Step 4:
|
||||
M = inflate( _M, n, opts.inflateFactor );
|
||||
|
||||
// Step 5: check to see if ~steady state has been reached
|
||||
if ( ! hasConverged( M, _M, n2, 4 ) ) {
|
||||
isStillMoving = true;
|
||||
}
|
||||
|
||||
iterations++;
|
||||
}
|
||||
|
||||
// Build clusters from matrix
|
||||
let clusters = assign( M, n, nodes, cy );
|
||||
|
||||
// Remove duplicate clusters due to symmetry of graph and M matrix
|
||||
clusters = removeDuplicates( clusters );
|
||||
|
||||
return clusters;
|
||||
};
|
||||
|
||||
export default {
|
||||
markovClustering,
|
||||
mcl: markovClustering
|
||||
};
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { defaults } from '../../util/index.mjs';
|
||||
import { inPlaceSumNormalize } from '../../math.mjs';
|
||||
|
||||
const pageRankDefaults = defaults({
|
||||
dampingFactor: 0.8,
|
||||
precision: 0.000001,
|
||||
iterations: 200,
|
||||
weight: edge => 1
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
|
||||
pageRank: function( options ){
|
||||
let { dampingFactor, precision, iterations, weight } = pageRankDefaults(options);
|
||||
let cy = this._private.cy;
|
||||
let { nodes, edges } = this.byGroup();
|
||||
let numNodes = nodes.length;
|
||||
let numNodesSqd = numNodes * numNodes;
|
||||
let numEdges = edges.length;
|
||||
|
||||
// Construct transposed adjacency matrix
|
||||
// First lets have a zeroed matrix of the right size
|
||||
// We'll also keep track of the sum of each column
|
||||
let matrix = new Array(numNodesSqd);
|
||||
let columnSum = new Array(numNodes);
|
||||
let additionalProb = (1 - dampingFactor) / numNodes;
|
||||
|
||||
// Create null matrix
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
for( let j = 0; j < numNodes; j++ ){
|
||||
let n = i * numNodes + j;
|
||||
|
||||
matrix[n] = 0;
|
||||
}
|
||||
|
||||
columnSum[i] = 0;
|
||||
}
|
||||
|
||||
// Now, process edges
|
||||
for( let i = 0; i < numEdges; i++ ){
|
||||
let edge = edges[ i ];
|
||||
let srcId = edge.data('source');
|
||||
let tgtId = edge.data('target');
|
||||
|
||||
// Don't include loops in the matrix
|
||||
if( srcId === tgtId ){ continue; }
|
||||
|
||||
let s = nodes.indexOfId( srcId );
|
||||
let t = nodes.indexOfId( tgtId );
|
||||
let w = weight( edge );
|
||||
let n = t * numNodes + s;
|
||||
|
||||
// Update matrix
|
||||
matrix[n] += w;
|
||||
|
||||
// Update column sum
|
||||
columnSum[s] += w;
|
||||
}
|
||||
|
||||
// Add additional probability based on damping factor
|
||||
// Also, take into account columns that have sum = 0
|
||||
let p = 1.0 / numNodes + additionalProb; // Shorthand
|
||||
|
||||
// Traverse matrix, column by column
|
||||
for( let j = 0; j < numNodes; j++ ){
|
||||
if( columnSum[j] === 0 ){
|
||||
// No 'links' out from node jth, assume equal probability for each possible node
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
let n = i * numNodes + j;
|
||||
matrix[n] = p;
|
||||
}
|
||||
} else {
|
||||
// Node jth has outgoing link, compute normalized probabilities
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
let n = i * numNodes + j;
|
||||
|
||||
matrix[n] = matrix[n] / columnSum[j] + additionalProb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute dominant eigenvector using power method
|
||||
let eigenvector = new Array(numNodes);
|
||||
let temp = new Array(numNodes);
|
||||
let previous;
|
||||
|
||||
// Start with a vector of all 1's
|
||||
// Also, initialize a null vector which will be used as shorthand
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
eigenvector[i] = 1;
|
||||
}
|
||||
|
||||
for( let iter = 0; iter < iterations; iter++ ){
|
||||
// Temp array with all 0's
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
temp[i] = 0;
|
||||
}
|
||||
|
||||
// Multiply matrix with previous result
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
for( let j = 0; j < numNodes; j++ ){
|
||||
let n = i * numNodes + j;
|
||||
|
||||
temp[i] += matrix[n] * eigenvector[j];
|
||||
}
|
||||
}
|
||||
|
||||
inPlaceSumNormalize( temp );
|
||||
previous = eigenvector;
|
||||
eigenvector = temp;
|
||||
temp = previous;
|
||||
|
||||
let diff = 0;
|
||||
// Compute difference (squared module) of both vectors
|
||||
for( let i = 0; i < numNodes; i++ ){
|
||||
let delta = previous[i] - eigenvector[i];
|
||||
|
||||
diff += delta * delta;
|
||||
}
|
||||
|
||||
// If difference is less than the desired threshold, stop iterating
|
||||
if( diff < precision ){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct result
|
||||
let res = {
|
||||
rank: function( node ){
|
||||
node = cy.collection(node)[0];
|
||||
|
||||
return eigenvector[ nodes.indexOf(node) ];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return res;
|
||||
} // pageRank
|
||||
|
||||
}); // elesfn
|
||||
|
||||
export default elesfn;
|
||||
Generated
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
let tarjanStronglyConnected = function() {
|
||||
|
||||
let eles = this;
|
||||
let nodes = {};
|
||||
let index = 0;
|
||||
let components = [];
|
||||
let stack = [];
|
||||
let cut = eles.spawn(eles);
|
||||
|
||||
const stronglyConnectedSearch = sourceNodeId => {
|
||||
stack.push(sourceNodeId);
|
||||
nodes[sourceNodeId] = {
|
||||
index : index,
|
||||
low : index++,
|
||||
explored : false
|
||||
};
|
||||
|
||||
let connectedEdges = eles.getElementById(sourceNodeId)
|
||||
.connectedEdges()
|
||||
.intersection(eles);
|
||||
|
||||
connectedEdges.forEach(edge => {
|
||||
let targetNodeId = edge.target().id();
|
||||
if (targetNodeId !== sourceNodeId) {
|
||||
if (!(targetNodeId in nodes)) {
|
||||
stronglyConnectedSearch(targetNodeId);
|
||||
}
|
||||
if (!(nodes[targetNodeId].explored)) {
|
||||
nodes[sourceNodeId].low = Math.min(nodes[sourceNodeId].low,
|
||||
nodes[targetNodeId].low);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (nodes[sourceNodeId].index === nodes[sourceNodeId].low) {
|
||||
let componentNodes = eles.spawn();
|
||||
for (;;) {
|
||||
const nodeId = stack.pop();
|
||||
componentNodes.merge(eles.getElementById(nodeId));
|
||||
nodes[nodeId].low = nodes[sourceNodeId].index;
|
||||
nodes[nodeId].explored = true;
|
||||
if (nodeId === sourceNodeId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let componentEdges = componentNodes.edgesWith(componentNodes);
|
||||
let component = componentNodes.merge(componentEdges);
|
||||
components.push(component);
|
||||
cut = cut.difference(component);
|
||||
}
|
||||
};
|
||||
|
||||
eles.forEach(ele => {
|
||||
if (ele.isNode()) {
|
||||
let nodeId = ele.id();
|
||||
if (!(nodeId in nodes)) {
|
||||
stronglyConnectedSearch(nodeId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
cut,
|
||||
components
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
export default {
|
||||
tarjanStronglyConnected,
|
||||
tsc: tarjanStronglyConnected,
|
||||
tscc: tarjanStronglyConnected,
|
||||
tarjanStronglyConnectedComponents: tarjanStronglyConnected
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import define from '../define/index.mjs';
|
||||
|
||||
let elesfn = ({
|
||||
animate: define.animate(),
|
||||
animation: define.animation(),
|
||||
animated: define.animated(),
|
||||
clearQueue: define.clearQueue(),
|
||||
delay: define.delay(),
|
||||
delayAnimation: define.delayAnimation(),
|
||||
stop: define.stop()
|
||||
});
|
||||
|
||||
export default elesfn;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import * as is from '../is.mjs';
|
||||
import * as util from '../util/index.mjs';
|
||||
|
||||
let cache = function( fn, name ){
|
||||
return function traversalCache( arg1, arg2, arg3, arg4 ){
|
||||
let selectorOrEles = arg1;
|
||||
let eles = this;
|
||||
let key;
|
||||
|
||||
if( selectorOrEles == null ){
|
||||
key = '';
|
||||
} else if( is.elementOrCollection( selectorOrEles ) && selectorOrEles.length === 1 ){
|
||||
key = selectorOrEles.id();
|
||||
}
|
||||
|
||||
if( eles.length === 1 && key ){
|
||||
let _p = eles[0]._private;
|
||||
let tch = _p.traversalCache = _p.traversalCache || {};
|
||||
let ch = tch[ name ] = tch[ name ] || [];
|
||||
let hash = util.hashString( key );
|
||||
let cacheHit = ch[ hash ];
|
||||
|
||||
if( cacheHit ){
|
||||
return cacheHit;
|
||||
} else {
|
||||
return ( ch[ hash ] = fn.call( eles, arg1, arg2, arg3, arg4 ) );
|
||||
}
|
||||
} else {
|
||||
return fn.call( eles, arg1, arg2, arg3, arg4 );
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default cache;
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import Set from '../set.mjs';
|
||||
import * as is from '../is.mjs';
|
||||
|
||||
let elesfn = ({
|
||||
classes: function( classes ){
|
||||
let self = this;
|
||||
|
||||
if( classes === undefined ){
|
||||
let ret = [];
|
||||
|
||||
self[0]._private.classes.forEach(cls => ret.push(cls));
|
||||
|
||||
return ret;
|
||||
} else if( !is.array( classes ) ){
|
||||
// extract classes from string
|
||||
classes = ( classes || '' ).match( /\S+/g ) || [];
|
||||
}
|
||||
|
||||
let changed = [];
|
||||
let classesSet = new Set( classes );
|
||||
|
||||
// check and update each ele
|
||||
for( let j = 0; j < self.length; j++ ){
|
||||
let ele = self[ j ];
|
||||
let _p = ele._private;
|
||||
let eleClasses = _p.classes;
|
||||
let changedEle = false;
|
||||
|
||||
// check if ele has all of the passed classes
|
||||
for( let i = 0; i < classes.length; i++ ){
|
||||
let cls = classes[i];
|
||||
let eleHasClass = eleClasses.has(cls);
|
||||
|
||||
if( !eleHasClass ){
|
||||
changedEle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// check if ele has classes outside of those passed
|
||||
if( !changedEle ){
|
||||
changedEle = eleClasses.size !== classes.length;
|
||||
}
|
||||
|
||||
if( changedEle ){
|
||||
_p.classes = classesSet;
|
||||
|
||||
changed.push( ele );
|
||||
}
|
||||
}
|
||||
|
||||
// trigger update style on those eles that had class changes
|
||||
if( changed.length > 0 ){
|
||||
this.spawn( changed )
|
||||
.updateStyle()
|
||||
.emit( 'class' )
|
||||
;
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
addClass: function( classes ){
|
||||
return this.toggleClass( classes, true );
|
||||
},
|
||||
|
||||
hasClass: function( className ){
|
||||
let ele = this[0];
|
||||
return ( ele != null && ele._private.classes.has(className) );
|
||||
},
|
||||
|
||||
toggleClass: function( classes, toggle ){
|
||||
if( !is.array( classes ) ){
|
||||
// extract classes from string
|
||||
classes = classes.match( /\S+/g ) || [];
|
||||
}
|
||||
let self = this;
|
||||
let toggleUndefd = toggle === undefined;
|
||||
let changed = []; // eles who had classes changed
|
||||
|
||||
for( let i = 0, il = self.length; i < il; i++ ){
|
||||
let ele = self[ i ];
|
||||
let eleClasses = ele._private.classes;
|
||||
let changedEle = false;
|
||||
|
||||
for( let j = 0; j < classes.length; j++ ){
|
||||
let cls = classes[ j ];
|
||||
let hasClass = eleClasses.has(cls);
|
||||
let changedNow = false;
|
||||
|
||||
if( toggle || (toggleUndefd && !hasClass) ){
|
||||
eleClasses.add(cls);
|
||||
changedNow = true;
|
||||
} else if( !toggle || (toggleUndefd && hasClass) ){
|
||||
eleClasses.delete(cls);
|
||||
changedNow = true;
|
||||
}
|
||||
|
||||
if( !changedEle && changedNow ){
|
||||
changed.push( ele );
|
||||
changedEle = true;
|
||||
}
|
||||
|
||||
} // for j classes
|
||||
} // for i eles
|
||||
|
||||
// trigger update style on those eles that had class changes
|
||||
if( changed.length > 0 ){
|
||||
this.spawn( changed )
|
||||
.updateStyle()
|
||||
.emit( 'class' )
|
||||
;
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
removeClass: function( classes ){
|
||||
return this.toggleClass( classes, false );
|
||||
},
|
||||
|
||||
flashClass: function( classes, duration ){
|
||||
let self = this;
|
||||
|
||||
if( duration == null ){
|
||||
duration = 250;
|
||||
} else if( duration === 0 ){
|
||||
return self; // nothing to do really
|
||||
}
|
||||
|
||||
self.addClass( classes );
|
||||
setTimeout( function(){
|
||||
self.removeClass( classes );
|
||||
}, duration );
|
||||
|
||||
return self;
|
||||
}
|
||||
});
|
||||
|
||||
elesfn.className = elesfn.classNames = elesfn.classes;
|
||||
|
||||
export default elesfn;
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import Selector from '../selector/index.mjs';
|
||||
|
||||
let elesfn = ({
|
||||
allAre: function( selector ){
|
||||
let selObj = new Selector( selector );
|
||||
|
||||
return this.every(function( ele ){
|
||||
return selObj.matches( ele );
|
||||
});
|
||||
},
|
||||
|
||||
is: function( selector ){
|
||||
let selObj = new Selector( selector );
|
||||
|
||||
return this.some(function( ele ){
|
||||
return selObj.matches( ele );
|
||||
});
|
||||
},
|
||||
|
||||
some: function( fn, thisArg ){
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ret = !thisArg ? fn( this[ i ], i, this ) : fn.apply( thisArg, [ this[ i ], i, this ] );
|
||||
|
||||
if( ret ){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
every: function( fn, thisArg ){
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ret = !thisArg ? fn( this[ i ], i, this ) : fn.apply( thisArg, [ this[ i ], i, this ] );
|
||||
|
||||
if( !ret ){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
same: function( collection ){
|
||||
// cheap collection ref check
|
||||
if( this === collection ){ return true; }
|
||||
|
||||
collection = this.cy().collection( collection );
|
||||
|
||||
let thisLength = this.length;
|
||||
let collectionLength = collection.length;
|
||||
|
||||
// cheap length check
|
||||
if( thisLength !== collectionLength ){ return false; }
|
||||
|
||||
// cheap element ref check
|
||||
if( thisLength === 1 ){ return this[0] === collection[0]; }
|
||||
|
||||
return this.every(function( ele ){
|
||||
return collection.hasElementWithId( ele.id() );
|
||||
});
|
||||
},
|
||||
|
||||
anySame: function( collection ){
|
||||
collection = this.cy().collection( collection );
|
||||
|
||||
return this.some(function( ele ){
|
||||
return collection.hasElementWithId( ele.id() );
|
||||
});
|
||||
},
|
||||
|
||||
allAreNeighbors: function( collection ){
|
||||
collection = this.cy().collection( collection );
|
||||
|
||||
let nhood = this.neighborhood();
|
||||
|
||||
return collection.every(function( ele ){
|
||||
return nhood.hasElementWithId( ele.id() );
|
||||
});
|
||||
},
|
||||
|
||||
contains: function( collection ){
|
||||
collection = this.cy().collection( collection );
|
||||
|
||||
let self = this;
|
||||
|
||||
return collection.every(function( ele ){
|
||||
return self.hasElementWithId( ele.id() );
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
elesfn.allAreNeighbours = elesfn.allAreNeighbors;
|
||||
elesfn.has = elesfn.contains;
|
||||
elesfn.equal = elesfn.equals = elesfn.same;
|
||||
|
||||
export default elesfn;
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
import Set from '../set.mjs';
|
||||
import cache from './cache-traversal-call.mjs';
|
||||
|
||||
let elesfn = ({
|
||||
parent: function( selector ){
|
||||
let parents = [];
|
||||
|
||||
// optimisation for single ele call
|
||||
if( this.length === 1 ){
|
||||
let parent = this[0]._private.parent;
|
||||
|
||||
if( parent ){ return parent; }
|
||||
}
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let parent = ele._private.parent;
|
||||
|
||||
if( parent ){
|
||||
parents.push( parent );
|
||||
}
|
||||
}
|
||||
|
||||
return this.spawn( parents, true ).filter( selector );
|
||||
},
|
||||
|
||||
parents: function( selector ){
|
||||
let parents = [];
|
||||
|
||||
let eles = this.parent();
|
||||
while( eles.nonempty() ){
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
parents.push( ele );
|
||||
}
|
||||
|
||||
eles = eles.parent();
|
||||
}
|
||||
|
||||
return this.spawn( parents, true ).filter( selector );
|
||||
},
|
||||
|
||||
commonAncestors: function( selector ){
|
||||
let ancestors;
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let parents = ele.parents();
|
||||
|
||||
ancestors = ancestors || parents;
|
||||
|
||||
ancestors = ancestors.intersect( parents ); // current list must be common with current ele parents set
|
||||
}
|
||||
|
||||
return ancestors.filter( selector );
|
||||
},
|
||||
|
||||
orphans: function( selector ){
|
||||
return this.stdFilter( function( ele ){
|
||||
return ele.isOrphan();
|
||||
} ).filter( selector );
|
||||
},
|
||||
|
||||
nonorphans: function( selector ){
|
||||
return this.stdFilter( function( ele ){
|
||||
return ele.isChild();
|
||||
} ).filter( selector );
|
||||
},
|
||||
|
||||
children: cache( function( selector ){
|
||||
let children = [];
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let eleChildren = ele._private.children;
|
||||
|
||||
for( let j = 0; j < eleChildren.length; j++ ){
|
||||
children.push( eleChildren[j] );
|
||||
}
|
||||
}
|
||||
|
||||
return this.spawn( children, true ).filter( selector );
|
||||
}, 'children' ),
|
||||
|
||||
siblings: function( selector ){
|
||||
return this.parent().children().not( this ).filter( selector );
|
||||
},
|
||||
|
||||
isParent: function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return ele.isNode() && ele._private.children.length !== 0;
|
||||
}
|
||||
},
|
||||
|
||||
isChildless: function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return ele.isNode() && ele._private.children.length === 0;
|
||||
}
|
||||
},
|
||||
|
||||
isChild: function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return ele.isNode() && ele._private.parent != null;
|
||||
}
|
||||
},
|
||||
|
||||
isOrphan: function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return ele.isNode() && ele._private.parent == null;
|
||||
}
|
||||
},
|
||||
|
||||
descendants: function( selector ){
|
||||
let elements = [];
|
||||
|
||||
function add( eles ){
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
|
||||
elements.push( ele );
|
||||
|
||||
if( ele.children().nonempty() ){
|
||||
add( ele.children() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add( this.children() );
|
||||
|
||||
return this.spawn( elements, true ).filter( selector );
|
||||
}
|
||||
});
|
||||
|
||||
function forEachCompound( eles, fn, includeSelf, recursiveStep ){
|
||||
let q = [];
|
||||
let did = new Set();
|
||||
let cy = eles.cy();
|
||||
let hasCompounds = cy.hasCompoundNodes();
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[i];
|
||||
|
||||
if( includeSelf ){
|
||||
q.push( ele );
|
||||
} else if( hasCompounds ){
|
||||
recursiveStep( q, did, ele );
|
||||
}
|
||||
}
|
||||
|
||||
while( q.length > 0 ){
|
||||
let ele = q.shift();
|
||||
|
||||
fn( ele );
|
||||
|
||||
did.add( ele.id() );
|
||||
|
||||
if( hasCompounds ){
|
||||
recursiveStep( q, did, ele );
|
||||
}
|
||||
}
|
||||
|
||||
return eles;
|
||||
}
|
||||
|
||||
function addChildren( q, did, ele ){
|
||||
if( ele.isParent() ){
|
||||
let children = ele._private.children;
|
||||
|
||||
for( let i = 0; i < children.length; i++ ){
|
||||
let child = children[i];
|
||||
|
||||
if( !did.has( child.id() ) ){
|
||||
q.push( child );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// very efficient version of eles.add( eles.descendants() ).forEach()
|
||||
// for internal use
|
||||
elesfn.forEachDown = function( fn, includeSelf = true ){
|
||||
return forEachCompound( this, fn, includeSelf, addChildren );
|
||||
};
|
||||
|
||||
function addParent( q, did, ele ){
|
||||
if( ele.isChild() ){
|
||||
let parent = ele._private.parent;
|
||||
|
||||
if( !did.has( parent.id() ) ){
|
||||
q.push( parent );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elesfn.forEachUp = function( fn, includeSelf = true ){
|
||||
return forEachCompound( this, fn, includeSelf, addParent );
|
||||
};
|
||||
|
||||
function addParentAndChildren( q, did, ele ){
|
||||
addParent( q, did, ele );
|
||||
addChildren( q, did, ele );
|
||||
}
|
||||
|
||||
elesfn.forEachUpAndDown = function( fn, includeSelf = true ){
|
||||
return forEachCompound( this, fn, includeSelf, addParentAndChildren );
|
||||
};
|
||||
|
||||
// aliases
|
||||
elesfn.ancestors = elesfn.parents;
|
||||
|
||||
export default elesfn;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import define from '../define/index.mjs';
|
||||
|
||||
let fn, elesfn;
|
||||
|
||||
fn = elesfn = ({
|
||||
|
||||
data: define.data( {
|
||||
field: 'data',
|
||||
bindingEvent: 'data',
|
||||
allowBinding: true,
|
||||
allowSetting: true,
|
||||
settingEvent: 'data',
|
||||
settingTriggersEvent: true,
|
||||
triggerFnName: 'trigger',
|
||||
allowGetting: true,
|
||||
immutableKeys: {
|
||||
'id': true,
|
||||
'source': true,
|
||||
'target': true,
|
||||
'parent': true
|
||||
},
|
||||
updateStyle: true
|
||||
} ),
|
||||
|
||||
removeData: define.removeData( {
|
||||
field: 'data',
|
||||
event: 'data',
|
||||
triggerFnName: 'trigger',
|
||||
triggerEvent: true,
|
||||
immutableKeys: {
|
||||
'id': true,
|
||||
'source': true,
|
||||
'target': true,
|
||||
'parent': true
|
||||
},
|
||||
updateStyle: true
|
||||
} ),
|
||||
|
||||
scratch: define.data( {
|
||||
field: 'scratch',
|
||||
bindingEvent: 'scratch',
|
||||
allowBinding: true,
|
||||
allowSetting: true,
|
||||
settingEvent: 'scratch',
|
||||
settingTriggersEvent: true,
|
||||
triggerFnName: 'trigger',
|
||||
allowGetting: true,
|
||||
updateStyle: true
|
||||
} ),
|
||||
|
||||
removeScratch: define.removeData( {
|
||||
field: 'scratch',
|
||||
event: 'scratch',
|
||||
triggerFnName: 'trigger',
|
||||
triggerEvent: true,
|
||||
updateStyle: true
|
||||
} ),
|
||||
|
||||
rscratch: define.data( {
|
||||
field: 'rscratch',
|
||||
allowBinding: false,
|
||||
allowSetting: true,
|
||||
settingTriggersEvent: false,
|
||||
allowGetting: true
|
||||
} ),
|
||||
|
||||
removeRscratch: define.removeData( {
|
||||
field: 'rscratch',
|
||||
triggerEvent: false
|
||||
} ),
|
||||
|
||||
id: function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return ele._private.data.id;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// aliases
|
||||
fn.attr = fn.data;
|
||||
fn.removeAttr = fn.removeData;
|
||||
|
||||
export default elesfn;
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import * as util from '../util/index.mjs';
|
||||
|
||||
let elesfn = {};
|
||||
|
||||
function defineDegreeFunction( callback ){
|
||||
return function( includeLoops ){
|
||||
let self = this;
|
||||
|
||||
if( includeLoops === undefined ){
|
||||
includeLoops = true;
|
||||
}
|
||||
|
||||
if( self.length === 0 ){ return; }
|
||||
|
||||
if( self.isNode() && !self.removed() ){
|
||||
let degree = 0;
|
||||
let node = self[0];
|
||||
let connectedEdges = node._private.edges;
|
||||
|
||||
for( let i = 0; i < connectedEdges.length; i++ ){
|
||||
let edge = connectedEdges[ i ];
|
||||
|
||||
if( !includeLoops && edge.isLoop() ){
|
||||
continue;
|
||||
}
|
||||
|
||||
degree += callback( node, edge );
|
||||
}
|
||||
|
||||
return degree;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
util.extend( elesfn, {
|
||||
degree: defineDegreeFunction( function( node, edge ){
|
||||
if( edge.source().same( edge.target() ) ){
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} ),
|
||||
|
||||
indegree: defineDegreeFunction( function( node, edge ){
|
||||
if( edge.target().same( node ) ){
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} ),
|
||||
|
||||
outdegree: defineDegreeFunction( function( node, edge ){
|
||||
if( edge.source().same( node ) ){
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} )
|
||||
} );
|
||||
|
||||
function defineDegreeBoundsFunction( degreeFn, callback ){
|
||||
return function( includeLoops ){
|
||||
let ret;
|
||||
let nodes = this.nodes();
|
||||
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
let ele = nodes[ i ];
|
||||
let degree = ele[ degreeFn ]( includeLoops );
|
||||
if( degree !== undefined && (ret === undefined || callback( degree, ret )) ){
|
||||
ret = degree;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
|
||||
util.extend( elesfn, {
|
||||
minDegree: defineDegreeBoundsFunction( 'degree', function( degree, min ){
|
||||
return degree < min;
|
||||
} ),
|
||||
|
||||
maxDegree: defineDegreeBoundsFunction( 'degree', function( degree, max ){
|
||||
return degree > max;
|
||||
} ),
|
||||
|
||||
minIndegree: defineDegreeBoundsFunction( 'indegree', function( degree, min ){
|
||||
return degree < min;
|
||||
} ),
|
||||
|
||||
maxIndegree: defineDegreeBoundsFunction( 'indegree', function( degree, max ){
|
||||
return degree > max;
|
||||
} ),
|
||||
|
||||
minOutdegree: defineDegreeBoundsFunction( 'outdegree', function( degree, min ){
|
||||
return degree < min;
|
||||
} ),
|
||||
|
||||
maxOutdegree: defineDegreeBoundsFunction( 'outdegree', function( degree, max ){
|
||||
return degree > max;
|
||||
} )
|
||||
} );
|
||||
|
||||
util.extend( elesfn, {
|
||||
totalDegree: function( includeLoops ){
|
||||
let total = 0;
|
||||
let nodes = this.nodes();
|
||||
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
total += nodes[ i ].degree( includeLoops );
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
} );
|
||||
|
||||
export default elesfn;
|
||||
+1045
File diff suppressed because it is too large
Load Diff
+56
@@ -0,0 +1,56 @@
|
||||
import * as math from '../../math.mjs';
|
||||
|
||||
const ifEdge = (ele, getValue) => {
|
||||
if( ele.isEdge() && ele.takesUpSpace() ){
|
||||
return getValue( ele );
|
||||
}
|
||||
};
|
||||
|
||||
const ifEdgeRenderedPosition = (ele, getPoint) => {
|
||||
if( ele.isEdge() && ele.takesUpSpace() ){
|
||||
let cy = ele.cy();
|
||||
|
||||
return math.modelToRenderedPosition( getPoint( ele ), cy.zoom(), cy.pan() );
|
||||
}
|
||||
};
|
||||
|
||||
const ifEdgeRenderedPositions = (ele, getPoints) => {
|
||||
if( ele.isEdge() && ele.takesUpSpace() ){
|
||||
let cy = ele.cy();
|
||||
let pan = cy.pan();
|
||||
let zoom = cy.zoom();
|
||||
|
||||
return getPoints( ele ).map( p => math.modelToRenderedPosition( p, zoom, pan ) );
|
||||
}
|
||||
};
|
||||
|
||||
const controlPoints = ele => ele.renderer().getControlPoints( ele );
|
||||
const segmentPoints = ele => ele.renderer().getSegmentPoints( ele );
|
||||
const sourceEndpoint = ele => ele.renderer().getSourceEndpoint( ele );
|
||||
const targetEndpoint = ele => ele.renderer().getTargetEndpoint( ele );
|
||||
const midpoint = ele => ele.renderer().getEdgeMidpoint( ele );
|
||||
|
||||
const pts = {
|
||||
controlPoints: { get: controlPoints, mult: true },
|
||||
segmentPoints: { get: segmentPoints, mult: true },
|
||||
sourceEndpoint: { get: sourceEndpoint },
|
||||
targetEndpoint: { get: targetEndpoint },
|
||||
midpoint: { get: midpoint }
|
||||
};
|
||||
|
||||
const renderedName = name => 'rendered' + name[0].toUpperCase() + name.substr(1);
|
||||
|
||||
export default Object.keys( pts ).reduce( ( obj, name ) => {
|
||||
let spec = pts[ name ];
|
||||
let rName = renderedName( name );
|
||||
|
||||
obj[ name ] = function(){ return ifEdge( this, spec.get ); };
|
||||
|
||||
if( spec.mult ){
|
||||
obj[ rName ] = function(){ return ifEdgeRenderedPositions( this, spec.get ); };
|
||||
} else {
|
||||
obj[ rName ] = function(){ return ifEdgeRenderedPosition( this, spec.get ); };
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, {} );
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import * as util from '../../util/index.mjs';
|
||||
import position from './position.mjs';
|
||||
import bounds from './bounds.mjs';
|
||||
import widthHeight from './width-height.mjs';
|
||||
import edgePoints from './edge-points.mjs';
|
||||
|
||||
export default util.assign( {}, position, bounds, widthHeight, edgePoints );
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
import define from '../../define/index.mjs';
|
||||
import * as is from '../../is.mjs';
|
||||
import * as math from '../../math.mjs';
|
||||
import * as util from '../../util/index.mjs';
|
||||
|
||||
let fn, elesfn;
|
||||
|
||||
let beforePositionSet = function( eles, newPos, silent ){
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[i];
|
||||
|
||||
if( !ele.locked() ){
|
||||
let oldPos = ele._private.position;
|
||||
|
||||
let delta = {
|
||||
x: newPos.x != null ? newPos.x - oldPos.x : 0,
|
||||
y: newPos.y != null ? newPos.y - oldPos.y : 0
|
||||
};
|
||||
|
||||
if( ele.isParent() && !(delta.x === 0 && delta.y === 0) ){
|
||||
ele.children().shift( delta, silent );
|
||||
}
|
||||
|
||||
ele.dirtyBoundingBoxCache();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let positionDef = {
|
||||
field: 'position',
|
||||
bindingEvent: 'position',
|
||||
allowBinding: true,
|
||||
allowSetting: true,
|
||||
settingEvent: 'position',
|
||||
settingTriggersEvent: true,
|
||||
triggerFnName: 'emitAndNotify',
|
||||
allowGetting: true,
|
||||
validKeys: [ 'x', 'y' ],
|
||||
beforeGet: function( ele ){
|
||||
ele.updateCompoundBounds();
|
||||
},
|
||||
beforeSet: function( eles, newPos ){
|
||||
beforePositionSet( eles, newPos, false );
|
||||
},
|
||||
onSet: function( eles ){
|
||||
eles.dirtyCompoundBoundsCache();
|
||||
},
|
||||
canSet: function( ele ){
|
||||
return !ele.locked();
|
||||
}
|
||||
};
|
||||
|
||||
fn = elesfn = ({
|
||||
|
||||
position: define.data( positionDef ),
|
||||
|
||||
// position but no notification to renderer
|
||||
silentPosition: define.data( util.assign( {}, positionDef, {
|
||||
allowBinding: false,
|
||||
allowSetting: true,
|
||||
settingTriggersEvent: false,
|
||||
allowGetting: false,
|
||||
beforeSet: function( eles, newPos ){
|
||||
beforePositionSet( eles, newPos, true );
|
||||
},
|
||||
onSet: function( eles ){
|
||||
eles.dirtyCompoundBoundsCache();
|
||||
}
|
||||
} ) ),
|
||||
|
||||
positions: function( pos, silent ){
|
||||
if( is.plainObject( pos ) ){
|
||||
if( silent ){
|
||||
this.silentPosition( pos );
|
||||
} else {
|
||||
this.position( pos );
|
||||
}
|
||||
|
||||
} else if( is.fn( pos ) ){
|
||||
let fn = pos;
|
||||
let cy = this.cy();
|
||||
|
||||
cy.startBatch();
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let pos;
|
||||
|
||||
if( ( pos = fn(ele, i) ) ){
|
||||
if( silent ){
|
||||
ele.silentPosition( pos );
|
||||
} else {
|
||||
ele.position( pos );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cy.endBatch();
|
||||
}
|
||||
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
silentPositions: function( pos ){
|
||||
return this.positions( pos, true );
|
||||
},
|
||||
|
||||
shift: function( dim, val, silent ){
|
||||
let delta;
|
||||
|
||||
if( is.plainObject( dim ) ){
|
||||
delta = {
|
||||
x: is.number(dim.x) ? dim.x : 0,
|
||||
y: is.number(dim.y) ? dim.y : 0
|
||||
};
|
||||
|
||||
silent = val;
|
||||
} else if( is.string( dim ) && is.number( val ) ){
|
||||
delta = { x: 0, y: 0 };
|
||||
|
||||
delta[ dim ] = val;
|
||||
}
|
||||
|
||||
if( delta != null ){
|
||||
let cy = this.cy();
|
||||
|
||||
cy.startBatch();
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
// exclude any node that is a descendant of the calling collection
|
||||
if (cy.hasCompoundNodes() && ele.isChild() && ele.ancestors().anySame(this)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pos = ele.position();
|
||||
let newPos = {
|
||||
x: pos.x + delta.x,
|
||||
y: pos.y + delta.y
|
||||
};
|
||||
|
||||
if( silent ){
|
||||
ele.silentPosition( newPos );
|
||||
} else {
|
||||
ele.position( newPos );
|
||||
}
|
||||
}
|
||||
|
||||
cy.endBatch();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
silentShift: function( dim, val ){
|
||||
if( is.plainObject( dim ) ){
|
||||
this.shift( dim, true );
|
||||
} else if( is.string( dim ) && is.number( val ) ){
|
||||
this.shift( dim, val, true );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// get/set the rendered (i.e. on screen) positon of the element
|
||||
renderedPosition: function( dim, val ){
|
||||
let ele = this[0];
|
||||
let cy = this.cy();
|
||||
let zoom = cy.zoom();
|
||||
let pan = cy.pan();
|
||||
let rpos = is.plainObject( dim ) ? dim : undefined;
|
||||
let setting = rpos !== undefined || ( val !== undefined && is.string( dim ) );
|
||||
|
||||
if( ele && ele.isNode() ){ // must have an element and must be a node to return position
|
||||
if( setting ){
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
|
||||
if( val !== undefined ){ // set one dimension
|
||||
ele.position( dim, ( val - pan[ dim ] ) / zoom );
|
||||
} else if( rpos !== undefined ){ // set whole position
|
||||
ele.position( math.renderedToModelPosition( rpos, zoom, pan ) );
|
||||
}
|
||||
}
|
||||
} else { // getting
|
||||
let pos = ele.position();
|
||||
rpos = math.modelToRenderedPosition( pos, zoom, pan );
|
||||
|
||||
if( dim === undefined ){ // then return the whole rendered position
|
||||
return rpos;
|
||||
} else { // then return the specified dimension
|
||||
return rpos[ dim ];
|
||||
}
|
||||
}
|
||||
} else if( !setting ){
|
||||
return undefined; // for empty collection case
|
||||
}
|
||||
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
// get/set the position relative to the parent
|
||||
relativePosition: function( dim, val ){
|
||||
let ele = this[0];
|
||||
let cy = this.cy();
|
||||
let ppos = is.plainObject( dim ) ? dim : undefined;
|
||||
let setting = ppos !== undefined || ( val !== undefined && is.string( dim ) );
|
||||
let hasCompoundNodes = cy.hasCompoundNodes();
|
||||
|
||||
if( ele && ele.isNode() ){ // must have an element and must be a node to return position
|
||||
if( setting ){
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let parent = hasCompoundNodes ? ele.parent() : null;
|
||||
let hasParent = parent && parent.length > 0;
|
||||
let relativeToParent = hasParent;
|
||||
|
||||
if( hasParent ){
|
||||
parent = parent[0];
|
||||
}
|
||||
|
||||
let origin = relativeToParent ? parent.position() : { x: 0, y: 0 };
|
||||
|
||||
if( val !== undefined ){ // set one dimension
|
||||
ele.position( dim, val + origin[ dim ] );
|
||||
} else if( ppos !== undefined ){ // set whole position
|
||||
ele.position({
|
||||
x: ppos.x + origin.x,
|
||||
y: ppos.y + origin.y
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} else { // getting
|
||||
let pos = ele.position();
|
||||
let parent = hasCompoundNodes ? ele.parent() : null;
|
||||
let hasParent = parent && parent.length > 0;
|
||||
let relativeToParent = hasParent;
|
||||
|
||||
if( hasParent ){
|
||||
parent = parent[0];
|
||||
}
|
||||
|
||||
let origin = relativeToParent ? parent.position() : { x: 0, y: 0 };
|
||||
|
||||
ppos = {
|
||||
x: pos.x - origin.x,
|
||||
y: pos.y - origin.y
|
||||
};
|
||||
|
||||
if( dim === undefined ){ // then return the whole rendered position
|
||||
return ppos;
|
||||
} else { // then return the specified dimension
|
||||
return ppos[ dim ];
|
||||
}
|
||||
}
|
||||
} else if( !setting ){
|
||||
return undefined; // for empty collection case
|
||||
}
|
||||
|
||||
return this; // chaining
|
||||
}
|
||||
});
|
||||
|
||||
// aliases
|
||||
fn.modelPosition = fn.point = fn.position;
|
||||
fn.modelPositions = fn.points = fn.positions;
|
||||
fn.renderedPoint = fn.renderedPosition;
|
||||
fn.relativePoint = fn.relativePosition;
|
||||
|
||||
export default elesfn;
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import * as util from '../../util/index.mjs';
|
||||
|
||||
let fn, elesfn;
|
||||
|
||||
fn = elesfn = {};
|
||||
|
||||
let defineDimFns = function( opts ){
|
||||
opts.uppercaseName = util.capitalize( opts.name );
|
||||
opts.autoName = 'auto' + opts.uppercaseName;
|
||||
opts.labelName = 'label' + opts.uppercaseName;
|
||||
opts.outerName = 'outer' + opts.uppercaseName;
|
||||
opts.uppercaseOuterName = util.capitalize( opts.outerName );
|
||||
|
||||
fn[ opts.name ] = function dimImpl(){
|
||||
let ele = this[0];
|
||||
let _p = ele._private;
|
||||
let cy = _p.cy;
|
||||
let styleEnabled = cy._private.styleEnabled;
|
||||
|
||||
if( ele ){
|
||||
if( styleEnabled ){
|
||||
if( ele.isParent() ){
|
||||
ele.updateCompoundBounds();
|
||||
|
||||
return _p[ opts.autoName ] || 0;
|
||||
}
|
||||
|
||||
let d = ele.pstyle( opts.name );
|
||||
|
||||
switch( d.strValue ){
|
||||
case 'label':
|
||||
ele.recalculateRenderedStyle();
|
||||
|
||||
return _p.rstyle[ opts.labelName ] || 0;
|
||||
|
||||
default:
|
||||
return d.pfValue;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn[ 'outer' + opts.uppercaseName ] = function outerDimImpl(){
|
||||
let ele = this[0];
|
||||
let _p = ele._private;
|
||||
let cy = _p.cy;
|
||||
let styleEnabled = cy._private.styleEnabled;
|
||||
|
||||
if( ele ){
|
||||
if( styleEnabled ){
|
||||
let dim = ele[ opts.name ]();
|
||||
|
||||
let borderPos = ele.pstyle( 'border-position' ).value;
|
||||
|
||||
let border;
|
||||
if(borderPos === 'center') {
|
||||
border = ele.pstyle( 'border-width' ).pfValue; // n.b. 1/2 each side
|
||||
} else if(borderPos === 'outside') {
|
||||
border = 2 * ele.pstyle( 'border-width' ).pfValue;
|
||||
} else { // 'inside'
|
||||
border = 0;
|
||||
}
|
||||
|
||||
let padding = 2 * ele.padding();
|
||||
|
||||
return dim + border + padding;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn[ 'rendered' + opts.uppercaseName ] = function renderedDimImpl(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
let d = ele[ opts.name ]();
|
||||
return d * this.cy().zoom();
|
||||
}
|
||||
};
|
||||
|
||||
fn[ 'rendered' + opts.uppercaseOuterName ] = function renderedOuterDimImpl(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
let od = ele[ opts.outerName ]();
|
||||
return od * this.cy().zoom();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
defineDimFns( {
|
||||
name: 'width'
|
||||
} );
|
||||
|
||||
defineDimFns( {
|
||||
name: 'height'
|
||||
} );
|
||||
|
||||
elesfn.padding = function(){
|
||||
let ele = this[0];
|
||||
let _p = ele._private;
|
||||
if( ele.isParent() ){
|
||||
ele.updateCompoundBounds();
|
||||
|
||||
if( _p.autoPadding !== undefined ){
|
||||
return _p.autoPadding;
|
||||
} else {
|
||||
return ele.pstyle('padding').pfValue;
|
||||
}
|
||||
} else {
|
||||
return ele.pstyle('padding').pfValue;
|
||||
}
|
||||
};
|
||||
|
||||
elesfn.paddedHeight = function(){
|
||||
let ele = this[0];
|
||||
|
||||
return ele.height() + (2 * ele.padding());
|
||||
};
|
||||
|
||||
elesfn.paddedWidth = function(){
|
||||
let ele = this[0];
|
||||
|
||||
return ele.width() + (2 * ele.padding());
|
||||
};
|
||||
|
||||
export default elesfn;
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import * as util from '../util/index.mjs';
|
||||
import * as is from '../is.mjs';
|
||||
import Set from '../set.mjs';
|
||||
|
||||
// represents a node or an edge
|
||||
let Element = function( cy, params, restore = true ){
|
||||
if( cy === undefined || params === undefined || !is.core( cy ) ){
|
||||
util.error( 'An element must have a core reference and parameters set' );
|
||||
return;
|
||||
}
|
||||
|
||||
let group = params.group;
|
||||
|
||||
// try to automatically infer the group if unspecified
|
||||
if( group == null ){
|
||||
if( params.data && params.data.source != null && params.data.target != null ){
|
||||
group = 'edges';
|
||||
} else {
|
||||
group = 'nodes';
|
||||
}
|
||||
}
|
||||
|
||||
// validate group
|
||||
if( group !== 'nodes' && group !== 'edges' ){
|
||||
util.error( 'An element must be of type `nodes` or `edges`; you specified `' + group + '`' );
|
||||
return;
|
||||
}
|
||||
|
||||
// make the element array-like, just like a collection
|
||||
this.length = 1;
|
||||
this[0] = this;
|
||||
|
||||
// NOTE: when something is added here, add also to ele.json()
|
||||
let _p = this._private = {
|
||||
cy: cy,
|
||||
single: true, // indicates this is an element
|
||||
data: params.data || {}, // data object
|
||||
position: params.position || { x: 0, y: 0 }, // (x, y) position pair
|
||||
autoWidth: undefined, // width and height of nodes calculated by the renderer when set to special 'auto' value
|
||||
autoHeight: undefined,
|
||||
autoPadding: undefined,
|
||||
compoundBoundsClean: false, // whether the compound dimensions need to be recalculated the next time dimensions are read
|
||||
listeners: [], // array of bound listeners
|
||||
group: group, // string; 'nodes' or 'edges'
|
||||
style: {}, // properties as set by the style
|
||||
rstyle: {}, // properties for style sent from the renderer to the core
|
||||
styleCxts: [], // applied style contexts from the styler
|
||||
styleKeys: {}, // per-group keys of style property values
|
||||
removed: true, // whether it's inside the vis; true if removed (set true here since we call restore)
|
||||
selected: params.selected ? true : false, // whether it's selected
|
||||
selectable: params.selectable === undefined ? true : ( params.selectable ? true : false ), // whether it's selectable
|
||||
locked: params.locked ? true : false, // whether the element is locked (cannot be moved)
|
||||
grabbed: false, // whether the element is grabbed by the mouse; renderer sets this privately
|
||||
grabbable: params.grabbable === undefined ? true : ( params.grabbable ? true : false ), // whether the element can be grabbed
|
||||
pannable: params.pannable === undefined ? (group === 'edges' ? true : false) : ( params.pannable ? true : false ), // whether the element has passthrough panning enabled
|
||||
active: false, // whether the element is active from user interaction
|
||||
classes: new Set(), // map ( className => true )
|
||||
animation: { // object for currently-running animations
|
||||
current: [],
|
||||
queue: []
|
||||
},
|
||||
rscratch: {}, // object in which the renderer can store information
|
||||
scratch: params.scratch || {}, // scratch objects
|
||||
edges: [], // array of connected edges
|
||||
children: [], // array of children
|
||||
parent: params.parent && params.parent.isNode() ? params.parent : null, // parent ref
|
||||
traversalCache: {}, // cache of output of traversal functions
|
||||
backgrounding: false, // whether background images are loading
|
||||
bbCache: null, // cache of the current bounding box
|
||||
bbCacheShift: { x: 0, y: 0 }, // shift applied to cached bb to be applied on next get
|
||||
bodyBounds: null, // bounds cache of element body, w/o overlay
|
||||
overlayBounds: null, // bounds cache of element body, including overlay
|
||||
labelBounds: { // bounds cache of labels
|
||||
all: null,
|
||||
source: null,
|
||||
target: null,
|
||||
main: null
|
||||
},
|
||||
arrowBounds: { // bounds cache of edge arrows
|
||||
source: null,
|
||||
target: null,
|
||||
'mid-source': null,
|
||||
'mid-target': null
|
||||
}
|
||||
};
|
||||
|
||||
if( _p.position.x == null ){ _p.position.x = 0; }
|
||||
if( _p.position.y == null ){ _p.position.y = 0; }
|
||||
|
||||
// renderedPosition overrides if specified
|
||||
if( params.renderedPosition ){
|
||||
let rpos = params.renderedPosition;
|
||||
let pan = cy.pan();
|
||||
let zoom = cy.zoom();
|
||||
|
||||
_p.position = {
|
||||
x: (rpos.x - pan.x) / zoom,
|
||||
y: (rpos.y - pan.y) / zoom
|
||||
};
|
||||
}
|
||||
|
||||
let classes = [];
|
||||
if( is.array( params.classes ) ){
|
||||
classes = params.classes;
|
||||
} else if( is.string( params.classes ) ){
|
||||
classes = params.classes.split( /\s+/ );
|
||||
}
|
||||
for( let i = 0, l = classes.length; i < l; i++ ){
|
||||
let cls = classes[ i ];
|
||||
if( !cls || cls === '' ){ continue; }
|
||||
|
||||
_p.classes.add(cls);
|
||||
}
|
||||
|
||||
this.createEmitter();
|
||||
|
||||
if( restore === undefined || restore ){
|
||||
this.restore();
|
||||
}
|
||||
|
||||
let bypass = params.style || params.css;
|
||||
if( bypass ){
|
||||
util.warn('Setting a `style` bypass at element creation should be done only when absolutely necessary. Try to use the stylesheet instead.');
|
||||
|
||||
this.style(bypass);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default Element;
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import Emitter from '../emitter.mjs';
|
||||
import define from '../define/index.mjs';
|
||||
import * as is from '../is.mjs';
|
||||
import Selector from '../selector/index.mjs';
|
||||
|
||||
let emitterOptions = {
|
||||
qualifierCompare: function( selector1, selector2 ){
|
||||
if( selector1 == null || selector2 == null ){
|
||||
return selector1 == null && selector2 == null;
|
||||
} else {
|
||||
return selector1.sameText( selector2 );
|
||||
}
|
||||
},
|
||||
eventMatches: function( ele, listener, eventObj ){
|
||||
let selector = listener.qualifier;
|
||||
|
||||
if( selector != null ){
|
||||
return ele !== eventObj.target && is.element( eventObj.target ) && selector.matches( eventObj.target );
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
addEventFields: function( ele, evt ){
|
||||
evt.cy = ele.cy();
|
||||
evt.target = ele;
|
||||
},
|
||||
callbackContext: function( ele, listener, eventObj ){
|
||||
return listener.qualifier != null ? eventObj.target : ele;
|
||||
},
|
||||
beforeEmit: function( context, listener/*, eventObj*/ ){
|
||||
if( listener.conf && listener.conf.once ){
|
||||
listener.conf.onceCollection.removeListener( listener.event, listener.qualifier, listener.callback );
|
||||
}
|
||||
},
|
||||
bubble: function(){
|
||||
return true;
|
||||
},
|
||||
parent: function( ele ){
|
||||
return ele.isChild() ? ele.parent() : ele.cy();
|
||||
}
|
||||
};
|
||||
|
||||
let argSelector = function( arg ){
|
||||
if( is.string(arg) ){
|
||||
return new Selector( arg );
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
};
|
||||
|
||||
let elesfn = ({
|
||||
createEmitter: function(){
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
let _p = ele._private;
|
||||
|
||||
if( !_p.emitter ){
|
||||
_p.emitter = new Emitter( emitterOptions, ele );
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
emitter: function(){
|
||||
return this._private.emitter;
|
||||
},
|
||||
|
||||
on: function( events, selector, callback ){
|
||||
let argSel = argSelector(selector);
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
ele.emitter().on( events, argSel, callback );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
removeListener: function( events, selector, callback ){
|
||||
let argSel = argSelector(selector);
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
ele.emitter().removeListener( events, argSel, callback );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
removeAllListeners: function(){
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
ele.emitter().removeAllListeners();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
one: function( events, selector, callback ){
|
||||
let argSel = argSelector(selector);
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
ele.emitter().one( events, argSel, callback );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
once: function( events, selector, callback ){
|
||||
let argSel = argSelector(selector);
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
ele.emitter().on( events, argSel, callback, {
|
||||
once: true,
|
||||
onceCollection: this
|
||||
} );
|
||||
}
|
||||
},
|
||||
|
||||
emit: function( events, extraParams ){
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
ele.emitter().emit( events, extraParams );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
emitAndNotify: function( event, extraParams ){ // for internal use only
|
||||
if( this.length === 0 ){ return; } // empty collections don't need to notify anything
|
||||
|
||||
// notify renderer
|
||||
this.cy().notify( event, this );
|
||||
|
||||
this.emit( event, extraParams );
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
define.eventAliasesOn( elesfn );
|
||||
|
||||
export default elesfn;
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
import * as is from '../is.mjs';
|
||||
import Selector from '../selector/index.mjs';
|
||||
|
||||
let elesfn = ({
|
||||
nodes: function( selector ){
|
||||
return this.filter( ele => ele.isNode() ).filter( selector );
|
||||
},
|
||||
|
||||
edges: function( selector ){
|
||||
return this.filter( ele => ele.isEdge() ).filter( selector );
|
||||
},
|
||||
|
||||
// internal helper to get nodes and edges as separate collections with single iteration over elements
|
||||
byGroup: function(){
|
||||
let nodes = this.spawn();
|
||||
let edges = this.spawn();
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
if( ele.isNode() ){
|
||||
nodes.push(ele);
|
||||
} else {
|
||||
edges.push(ele);
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
},
|
||||
|
||||
filter: function( filter, thisArg ){
|
||||
if( filter === undefined ){ // check this first b/c it's the most common/performant case
|
||||
return this;
|
||||
} else if( is.string( filter ) || is.elementOrCollection( filter ) ){
|
||||
return new Selector( filter ).filter( this );
|
||||
} else if( is.fn( filter ) ){
|
||||
let filterEles = this.spawn();
|
||||
let eles = this;
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
let include = thisArg ? filter.apply( thisArg, [ ele, i, eles ] ) : filter( ele, i, eles );
|
||||
|
||||
if( include ){
|
||||
filterEles.push( ele );
|
||||
}
|
||||
}
|
||||
|
||||
return filterEles;
|
||||
}
|
||||
|
||||
return this.spawn(); // if not handled by above, give 'em an empty collection
|
||||
},
|
||||
|
||||
not: function( toRemove ){
|
||||
if( !toRemove ){
|
||||
return this;
|
||||
} else {
|
||||
|
||||
if( is.string( toRemove ) ){
|
||||
toRemove = this.filter( toRemove );
|
||||
}
|
||||
|
||||
let elements = this.spawn();
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let element = this[ i ];
|
||||
|
||||
let remove = toRemove.has(element);
|
||||
if( !remove ){
|
||||
elements.push( element );
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
absoluteComplement: function(){
|
||||
let cy = this.cy();
|
||||
|
||||
return cy.mutableElements().not( this );
|
||||
},
|
||||
|
||||
intersect: function( other ){
|
||||
// if a selector is specified, then filter by it instead
|
||||
if( is.string( other ) ){
|
||||
let selector = other;
|
||||
return this.filter( selector );
|
||||
}
|
||||
|
||||
let elements = this.spawn();
|
||||
let col1 = this;
|
||||
let col2 = other;
|
||||
let col1Smaller = this.length < other.length;
|
||||
let colS = col1Smaller ? col1 : col2;
|
||||
let colL = col1Smaller ? col2 : col1;
|
||||
|
||||
for( let i = 0; i < colS.length; i++ ){
|
||||
let ele = colS[i];
|
||||
|
||||
if( colL.has(ele) ){
|
||||
elements.push(ele);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
|
||||
xor: function( other ){
|
||||
let cy = this._private.cy;
|
||||
|
||||
if( is.string( other ) ){
|
||||
other = cy.$( other );
|
||||
}
|
||||
|
||||
let elements = this.spawn();
|
||||
let col1 = this;
|
||||
let col2 = other;
|
||||
|
||||
let add = function( col, other ){
|
||||
for( let i = 0; i < col.length; i++ ){
|
||||
let ele = col[ i ];
|
||||
let id = ele._private.data.id;
|
||||
let inOther = other.hasElementWithId( id );
|
||||
|
||||
if( !inOther ){
|
||||
elements.push( ele );
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
add( col1, col2 );
|
||||
add( col2, col1 );
|
||||
|
||||
return elements;
|
||||
},
|
||||
|
||||
diff: function( other ){
|
||||
let cy = this._private.cy;
|
||||
|
||||
if( is.string( other ) ){
|
||||
other = cy.$( other );
|
||||
}
|
||||
|
||||
let left = this.spawn();
|
||||
let right = this.spawn();
|
||||
let both = this.spawn();
|
||||
let col1 = this;
|
||||
let col2 = other;
|
||||
|
||||
let add = function( col, other, retEles ){
|
||||
|
||||
for( let i = 0; i < col.length; i++ ){
|
||||
let ele = col[ i ];
|
||||
let id = ele._private.data.id;
|
||||
let inOther = other.hasElementWithId( id );
|
||||
|
||||
if( inOther ){
|
||||
both.merge( ele );
|
||||
} else {
|
||||
retEles.push( ele );
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
add( col1, col2, left );
|
||||
add( col2, col1, right );
|
||||
|
||||
return { left, right, both };
|
||||
},
|
||||
|
||||
add: function( toAdd ){
|
||||
let cy = this._private.cy;
|
||||
|
||||
if( !toAdd ){
|
||||
return this;
|
||||
}
|
||||
|
||||
if( is.string( toAdd ) ){
|
||||
let selector = toAdd;
|
||||
toAdd = cy.mutableElements().filter( selector );
|
||||
}
|
||||
|
||||
let elements = this.spawnSelf();
|
||||
|
||||
for( let i = 0; i < toAdd.length; i++ ){
|
||||
let ele = toAdd[i];
|
||||
|
||||
let add = !this.has(ele);
|
||||
if( add ){
|
||||
elements.push(ele);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
|
||||
// in place merge on calling collection
|
||||
merge: function( toAdd ){
|
||||
let _p = this._private;
|
||||
let cy = _p.cy;
|
||||
|
||||
if( !toAdd ){
|
||||
return this;
|
||||
}
|
||||
|
||||
if( toAdd && is.string( toAdd ) ){
|
||||
let selector = toAdd;
|
||||
toAdd = cy.mutableElements().filter( selector );
|
||||
}
|
||||
|
||||
let map = _p.map;
|
||||
|
||||
for( let i = 0; i < toAdd.length; i++ ){
|
||||
let toAddEle = toAdd[ i ];
|
||||
let id = toAddEle._private.data.id;
|
||||
let add = !map.has( id );
|
||||
|
||||
if( add ){
|
||||
let index = this.length++;
|
||||
|
||||
this[ index ] = toAddEle;
|
||||
|
||||
map.set( id, { ele: toAddEle, index: index } );
|
||||
}
|
||||
}
|
||||
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
unmergeAt: function( i ){
|
||||
let ele = this[i];
|
||||
let id = ele.id();
|
||||
let _p = this._private;
|
||||
let map = _p.map;
|
||||
|
||||
// remove ele
|
||||
this[ i ] = undefined;
|
||||
map.delete( id );
|
||||
|
||||
let unmergedLastEle = i === this.length - 1;
|
||||
|
||||
// replace empty spot with last ele in collection
|
||||
if( this.length > 1 && !unmergedLastEle ){
|
||||
let lastEleI = this.length - 1;
|
||||
let lastEle = this[ lastEleI ];
|
||||
let lastEleId = lastEle._private.data.id;
|
||||
|
||||
this[ lastEleI ] = undefined;
|
||||
this[ i ] = lastEle;
|
||||
map.set( lastEleId, { ele: lastEle, index: i } );
|
||||
}
|
||||
|
||||
// the collection is now 1 ele smaller
|
||||
this.length--;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// remove single ele in place in calling collection
|
||||
unmergeOne: function( ele ){
|
||||
ele = ele[0];
|
||||
|
||||
let _p = this._private;
|
||||
let id = ele._private.data.id;
|
||||
let map = _p.map;
|
||||
let entry = map.get( id );
|
||||
|
||||
if( !entry ){
|
||||
return this; // no need to remove
|
||||
}
|
||||
|
||||
let i = entry.index;
|
||||
|
||||
this.unmergeAt(i);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// remove eles in place on calling collection
|
||||
unmerge: function( toRemove ){
|
||||
let cy = this._private.cy;
|
||||
|
||||
if( !toRemove ){
|
||||
return this;
|
||||
}
|
||||
|
||||
if( toRemove && is.string( toRemove ) ){
|
||||
let selector = toRemove;
|
||||
toRemove = cy.mutableElements().filter( selector );
|
||||
}
|
||||
|
||||
for( let i = 0; i < toRemove.length; i++ ){
|
||||
this.unmergeOne( toRemove[ i ] );
|
||||
}
|
||||
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
unmergeBy: function( toRmFn ){
|
||||
for( let i = this.length - 1; i >= 0; i-- ){
|
||||
let ele = this[i];
|
||||
|
||||
if( toRmFn(ele) ){
|
||||
this.unmergeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
map: function( mapFn, thisArg ){
|
||||
let arr = [];
|
||||
let eles = this;
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
let ret = thisArg ? mapFn.apply( thisArg, [ ele, i, eles ] ) : mapFn( ele, i, eles );
|
||||
|
||||
arr.push( ret );
|
||||
}
|
||||
|
||||
return arr;
|
||||
},
|
||||
|
||||
reduce: function( fn, initialValue ){
|
||||
let val = initialValue;
|
||||
let eles = this;
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
val = fn( val, eles[i], i, eles );
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
|
||||
max: function( valFn, thisArg ){
|
||||
let max = -Infinity;
|
||||
let maxEle;
|
||||
let eles = this;
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
let val = thisArg ? valFn.apply( thisArg, [ ele, i, eles ] ) : valFn( ele, i, eles );
|
||||
|
||||
if( val > max ){
|
||||
max = val;
|
||||
maxEle = ele;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: max,
|
||||
ele: maxEle
|
||||
};
|
||||
},
|
||||
|
||||
min: function( valFn, thisArg ){
|
||||
let min = Infinity;
|
||||
let minEle;
|
||||
let eles = this;
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
let val = thisArg ? valFn.apply( thisArg, [ ele, i, eles ] ) : valFn( ele, i, eles );
|
||||
|
||||
if( val < min ){
|
||||
min = val;
|
||||
minEle = ele;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: min,
|
||||
ele: minEle
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// aliases
|
||||
let fn = elesfn;
|
||||
fn[ 'u' ] = fn[ '|' ] = fn[ '+' ] = fn.union = fn.or = fn.add;
|
||||
fn[ '\\' ] = fn[ '!' ] = fn[ '-' ] = fn.difference = fn.relativeComplement = fn.subtract = fn.not;
|
||||
fn[ 'n' ] = fn[ '&' ] = fn[ '.' ] = fn.and = fn.intersection = fn.intersect;
|
||||
fn[ '^' ] = fn[ '(+)' ] = fn[ '(-)' ] = fn.symmetricDifference = fn.symdiff = fn.xor;
|
||||
fn.fnFilter = fn.filterFn = fn.stdFilter = fn.filter;
|
||||
fn.complement = fn.abscomp = fn.absoluteComplement;
|
||||
|
||||
export default elesfn;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
let elesfn = ({
|
||||
isNode: function(){
|
||||
return this.group() === 'nodes';
|
||||
},
|
||||
|
||||
isEdge: function(){
|
||||
return this.group() === 'edges';
|
||||
},
|
||||
|
||||
isLoop: function(){
|
||||
return this.isEdge() && this.source()[0] === this.target()[0];
|
||||
},
|
||||
|
||||
isSimple: function(){
|
||||
return this.isEdge() && this.source()[0] !== this.target()[0];
|
||||
},
|
||||
|
||||
group: function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return ele._private.group;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default elesfn;
|
||||
+838
@@ -0,0 +1,838 @@
|
||||
import * as util from '../util/index.mjs';
|
||||
import * as is from '../is.mjs';
|
||||
import Map from '../map.mjs';
|
||||
import Set from '../set.mjs';
|
||||
|
||||
import Element from './element.mjs';
|
||||
import algorithms from './algorithms/index.mjs';
|
||||
import animation from './animation.mjs';
|
||||
import classNames from './class.mjs';
|
||||
import comparators from './comparators.mjs';
|
||||
import compounds from './compounds.mjs';
|
||||
import data from './data.mjs';
|
||||
import degree from './degree.mjs';
|
||||
import dimensions from './dimensions/index.mjs';
|
||||
import events from './events.mjs';
|
||||
import filter from './filter.mjs';
|
||||
import group from './group.mjs';
|
||||
import iteration from './iteration.mjs';
|
||||
import layout from './layout.mjs';
|
||||
import style from './style.mjs';
|
||||
import switchFunctions from './switch-functions.mjs';
|
||||
import traversing from './traversing.mjs';
|
||||
|
||||
// represents a set of nodes, edges, or both together
|
||||
let Collection = function( cy, elements, unique = false, removed = false ){
|
||||
if( cy === undefined ){
|
||||
util.error( 'A collection must have a reference to the core' );
|
||||
return;
|
||||
}
|
||||
|
||||
let map = new Map();
|
||||
let createdElements = false;
|
||||
|
||||
if( !elements ){
|
||||
elements = [];
|
||||
} else if( elements.length > 0 && is.plainObject( elements[0] ) && !is.element( elements[0] ) ){
|
||||
createdElements = true;
|
||||
|
||||
// make elements from json and restore all at once later
|
||||
let eles = [];
|
||||
let elesIds = new Set();
|
||||
|
||||
for( let i = 0, l = elements.length; i < l; i++ ){
|
||||
let json = elements[ i ];
|
||||
|
||||
if( json.data == null ){
|
||||
json.data = {};
|
||||
}
|
||||
|
||||
let data = json.data;
|
||||
|
||||
// make sure newly created elements have valid ids
|
||||
if( data.id == null ){
|
||||
data.id = util.uuid();
|
||||
} else if( cy.hasElementWithId( data.id ) || elesIds.has( data.id ) ){
|
||||
continue; // can't create element if prior id already exists
|
||||
}
|
||||
|
||||
let ele = new Element( cy, json, false );
|
||||
eles.push( ele );
|
||||
elesIds.add( data.id );
|
||||
}
|
||||
|
||||
elements = eles;
|
||||
}
|
||||
|
||||
this.length = 0;
|
||||
|
||||
for( let i = 0, l = elements.length; i < l; i++ ){
|
||||
let element = elements[i][0]; // [0] in case elements is an array of collections, rather than array of elements
|
||||
if( element == null ){ continue; }
|
||||
|
||||
let id = element._private.data.id;
|
||||
|
||||
if( !unique || !map.has(id) ){
|
||||
if( unique ){
|
||||
map.set( id, {
|
||||
index: this.length,
|
||||
ele: element
|
||||
} );
|
||||
}
|
||||
|
||||
this[ this.length ] = element;
|
||||
this.length++;
|
||||
}
|
||||
}
|
||||
|
||||
this._private = {
|
||||
eles: this,
|
||||
cy: cy,
|
||||
get map(){
|
||||
if( this.lazyMap == null ){
|
||||
this.rebuildMap();
|
||||
}
|
||||
|
||||
return this.lazyMap;
|
||||
},
|
||||
set map(m){
|
||||
this.lazyMap = m;
|
||||
},
|
||||
rebuildMap(){
|
||||
const m = this.lazyMap = new Map();
|
||||
const eles = this.eles;
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
const ele = eles[i];
|
||||
|
||||
m.set(ele.id(), { index: i, ele });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if( unique ){
|
||||
this._private.map = map;
|
||||
}
|
||||
|
||||
// restore the elements if we created them from json
|
||||
if( createdElements && !removed ){
|
||||
this.restore();
|
||||
}
|
||||
};
|
||||
|
||||
// Functions
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// keep the prototypes in sync (an element has the same functions as a collection)
|
||||
// and use elefn and elesfn as shorthands to the prototypes
|
||||
let elesfn = Element.prototype = Collection.prototype = Object.create(Array.prototype);
|
||||
|
||||
elesfn.instanceString = function(){
|
||||
return 'collection';
|
||||
};
|
||||
|
||||
elesfn.spawn = function( eles, unique ){
|
||||
return new Collection( this.cy(), eles, unique );
|
||||
};
|
||||
|
||||
elesfn.spawnSelf = function(){
|
||||
return this.spawn( this );
|
||||
};
|
||||
|
||||
elesfn.cy = function(){
|
||||
return this._private.cy;
|
||||
};
|
||||
|
||||
elesfn.renderer = function(){
|
||||
return this._private.cy.renderer();
|
||||
};
|
||||
|
||||
elesfn.element = function(){
|
||||
return this[0];
|
||||
};
|
||||
|
||||
elesfn.collection = function(){
|
||||
if( is.collection( this ) ){
|
||||
return this;
|
||||
} else { // an element
|
||||
return new Collection( this._private.cy, [ this ] );
|
||||
}
|
||||
};
|
||||
|
||||
elesfn.unique = function(){
|
||||
return new Collection( this._private.cy, this, true );
|
||||
};
|
||||
|
||||
elesfn.hasElementWithId = function( id ){
|
||||
id = '' + id; // id must be string
|
||||
|
||||
return this._private.map.has( id );
|
||||
};
|
||||
|
||||
elesfn.getElementById = function( id ){
|
||||
id = '' + id; // id must be string
|
||||
|
||||
let cy = this._private.cy;
|
||||
let entry = this._private.map.get( id );
|
||||
|
||||
return entry ? entry.ele : new Collection( cy ); // get ele or empty collection
|
||||
};
|
||||
|
||||
elesfn.$id = elesfn.getElementById;
|
||||
|
||||
elesfn.poolIndex = function(){
|
||||
let cy = this._private.cy;
|
||||
let eles = cy._private.elements;
|
||||
let id = this[0]._private.data.id;
|
||||
|
||||
return eles._private.map.get( id ).index;
|
||||
};
|
||||
|
||||
elesfn.indexOf = function( ele ){
|
||||
let id = ele[0]._private.data.id;
|
||||
|
||||
return this._private.map.get( id ).index;
|
||||
};
|
||||
|
||||
elesfn.indexOfId = function( id ){
|
||||
id = '' + id; // id must be string
|
||||
|
||||
return this._private.map.get( id ).index;
|
||||
};
|
||||
|
||||
elesfn.json = function( obj ){
|
||||
let ele = this.element();
|
||||
let cy = this.cy();
|
||||
|
||||
if( ele == null && obj ){ return this; } // can't set to no eles
|
||||
|
||||
if( ele == null ){ return undefined; } // can't get from no eles
|
||||
|
||||
let p = ele._private;
|
||||
|
||||
if( is.plainObject( obj ) ){ // set
|
||||
|
||||
cy.startBatch();
|
||||
|
||||
if( obj.data ){
|
||||
ele.data( obj.data );
|
||||
|
||||
let data = p.data;
|
||||
|
||||
if( ele.isEdge() ){ // source and target are immutable via data()
|
||||
let move = false;
|
||||
let spec = {};
|
||||
let src = obj.data.source;
|
||||
let tgt = obj.data.target;
|
||||
|
||||
if( src != null && src != data.source ){
|
||||
spec.source = '' + src; // id must be string
|
||||
move = true;
|
||||
}
|
||||
|
||||
if( tgt != null && tgt != data.target ){
|
||||
spec.target = '' + tgt; // id must be string
|
||||
move = true;
|
||||
}
|
||||
|
||||
if( move ){
|
||||
ele = ele.move(spec);
|
||||
}
|
||||
} else { // parent is immutable via data()
|
||||
let newParentValSpecd = 'parent' in obj.data;
|
||||
let parent = obj.data.parent;
|
||||
|
||||
if( newParentValSpecd && (parent != null || data.parent != null) && parent != data.parent ){
|
||||
if( parent === undefined ){ // can't set undefined imperatively, so use null
|
||||
parent = null;
|
||||
}
|
||||
|
||||
if( parent != null ){
|
||||
parent = '' + parent; // id must be string
|
||||
}
|
||||
|
||||
ele = ele.move({ parent });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( obj.position ){
|
||||
ele.position( obj.position );
|
||||
}
|
||||
|
||||
// ignore group -- immutable
|
||||
|
||||
let checkSwitch = function( k, trueFnName, falseFnName ){
|
||||
let obj_k = obj[ k ];
|
||||
|
||||
if( obj_k != null && obj_k !== p[ k ] ){
|
||||
if( obj_k ){
|
||||
ele[ trueFnName ]();
|
||||
} else {
|
||||
ele[ falseFnName ]();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkSwitch( 'removed', 'remove', 'restore' );
|
||||
|
||||
checkSwitch( 'selected', 'select', 'unselect' );
|
||||
|
||||
checkSwitch( 'selectable', 'selectify', 'unselectify' );
|
||||
|
||||
checkSwitch( 'locked', 'lock', 'unlock' );
|
||||
|
||||
checkSwitch( 'grabbable', 'grabify', 'ungrabify' );
|
||||
|
||||
checkSwitch( 'pannable', 'panify', 'unpanify' );
|
||||
|
||||
if( obj.classes != null ){
|
||||
ele.classes( obj.classes );
|
||||
}
|
||||
|
||||
cy.endBatch();
|
||||
|
||||
return this;
|
||||
|
||||
} else if( obj === undefined ){ // get
|
||||
|
||||
let json = {
|
||||
data: util.copy( p.data ),
|
||||
position: util.copy( p.position ),
|
||||
group: p.group,
|
||||
removed: p.removed,
|
||||
selected: p.selected,
|
||||
selectable: p.selectable,
|
||||
locked: p.locked,
|
||||
grabbable: p.grabbable,
|
||||
pannable: p.pannable,
|
||||
classes: null
|
||||
};
|
||||
|
||||
json.classes = '';
|
||||
|
||||
let i = 0;
|
||||
p.classes.forEach( cls => json.classes += ( i++ === 0 ? cls : ' ' + cls ) );
|
||||
|
||||
return json;
|
||||
}
|
||||
};
|
||||
|
||||
elesfn.jsons = function(){
|
||||
let jsons = [];
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let json = ele.json();
|
||||
|
||||
jsons.push( json );
|
||||
}
|
||||
|
||||
return jsons;
|
||||
};
|
||||
|
||||
elesfn.clone = function(){
|
||||
let cy = this.cy();
|
||||
let elesArr = [];
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let json = ele.json();
|
||||
let clone = new Element( cy, json, false ); // NB no restore
|
||||
|
||||
elesArr.push( clone );
|
||||
}
|
||||
|
||||
return new Collection( cy, elesArr );
|
||||
};
|
||||
elesfn.copy = elesfn.clone;
|
||||
|
||||
elesfn.restore = function( notifyRenderer = true, addToPool = true ){
|
||||
let self = this;
|
||||
let cy = self.cy();
|
||||
let cy_p = cy._private;
|
||||
|
||||
// create arrays of nodes and edges, since we need to
|
||||
// restore the nodes first
|
||||
let nodes = [];
|
||||
let edges = [];
|
||||
let elements;
|
||||
for( let i = 0, l = self.length; i < l; i++ ){
|
||||
let ele = self[ i ];
|
||||
|
||||
if( addToPool && !ele.removed() ){
|
||||
// don't need to handle this ele
|
||||
continue;
|
||||
}
|
||||
|
||||
// keep nodes first in the array and edges after
|
||||
if( ele.isNode() ){ // put to front of array if node
|
||||
nodes.push( ele );
|
||||
} else { // put to end of array if edge
|
||||
edges.push( ele );
|
||||
}
|
||||
}
|
||||
|
||||
elements = nodes.concat( edges );
|
||||
|
||||
let i;
|
||||
let removeFromElements = function(){
|
||||
elements.splice( i, 1 );
|
||||
i--;
|
||||
};
|
||||
|
||||
// now, restore each element
|
||||
for( i = 0; i < elements.length; i++ ){
|
||||
let ele = elements[ i ];
|
||||
|
||||
let _private = ele._private;
|
||||
let data = _private.data;
|
||||
|
||||
// the traversal cache should start fresh when ele is added
|
||||
ele.clearTraversalCache();
|
||||
|
||||
// set id and validate
|
||||
if( !addToPool && !_private.removed ){
|
||||
// already in graph, so nothing required
|
||||
|
||||
} else if( data.id === undefined ){
|
||||
data.id = util.uuid();
|
||||
|
||||
} else if( is.number( data.id ) ){
|
||||
data.id = '' + data.id; // now it's a string
|
||||
|
||||
} else if( is.emptyString( data.id ) || !is.string( data.id ) ){
|
||||
util.error( 'Can not create element with invalid string ID `' + data.id + '`' );
|
||||
|
||||
// can't create element if it has empty string as id or non-string id
|
||||
removeFromElements();
|
||||
continue;
|
||||
} else if( cy.hasElementWithId( data.id ) ){
|
||||
util.error( 'Can not create second element with ID `' + data.id + '`' );
|
||||
|
||||
// can't create element if one already has that id
|
||||
removeFromElements();
|
||||
continue;
|
||||
}
|
||||
|
||||
let id = data.id; // id is finalised, now let's keep a ref
|
||||
|
||||
if( ele.isNode() ){ // extra checks for nodes
|
||||
let pos = _private.position;
|
||||
|
||||
// make sure the nodes have a defined position
|
||||
|
||||
if( pos.x == null ){
|
||||
pos.x = 0;
|
||||
}
|
||||
|
||||
if( pos.y == null ){
|
||||
pos.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if( ele.isEdge() ){ // extra checks for edges
|
||||
|
||||
let edge = ele;
|
||||
let fields = [ 'source', 'target' ];
|
||||
let fieldsLength = fields.length;
|
||||
let badSourceOrTarget = false;
|
||||
for( let j = 0; j < fieldsLength; j++ ){
|
||||
|
||||
let field = fields[ j ];
|
||||
let val = data[ field ];
|
||||
|
||||
if( is.number( val ) ){
|
||||
val = data[ field ] = '' + data[ field ]; // now string
|
||||
}
|
||||
|
||||
if( val == null || val === '' ){
|
||||
// can't create if source or target is not defined properly
|
||||
util.error( 'Can not create edge `' + id + '` with unspecified ' + field );
|
||||
badSourceOrTarget = true;
|
||||
} else if( !cy.hasElementWithId( val ) ){
|
||||
// can't create edge if one of its nodes doesn't exist
|
||||
util.error( 'Can not create edge `' + id + '` with nonexistent ' + field + ' `' + val + '`' );
|
||||
badSourceOrTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
if( badSourceOrTarget ){ removeFromElements(); continue; } // can't create this
|
||||
|
||||
let src = cy.getElementById( data.source );
|
||||
let tgt = cy.getElementById( data.target );
|
||||
|
||||
// only one edge in node if loop
|
||||
if (src.same(tgt)) {
|
||||
src._private.edges.push( edge );
|
||||
} else {
|
||||
src._private.edges.push( edge );
|
||||
tgt._private.edges.push( edge );
|
||||
}
|
||||
|
||||
edge._private.source = src;
|
||||
edge._private.target = tgt;
|
||||
} // if is edge
|
||||
|
||||
// create mock ids / indexes maps for element so it can be used like collections
|
||||
_private.map = new Map();
|
||||
_private.map.set( id, { ele: ele, index: 0 } );
|
||||
|
||||
_private.removed = false;
|
||||
|
||||
if( addToPool ){
|
||||
cy.addToPool( ele );
|
||||
}
|
||||
} // for each element
|
||||
|
||||
// do compound node sanity checks
|
||||
for( let i = 0; i < nodes.length; i++ ){ // each node
|
||||
let node = nodes[ i ];
|
||||
let data = node._private.data;
|
||||
|
||||
if( is.number( data.parent ) ){ // then automake string
|
||||
data.parent = '' + data.parent;
|
||||
}
|
||||
|
||||
let parentId = data.parent;
|
||||
|
||||
let specifiedParent = parentId != null;
|
||||
|
||||
if( specifiedParent || node._private.parent ){
|
||||
|
||||
let parent = node._private.parent ? cy.collection().merge(node._private.parent) : cy.getElementById( parentId );
|
||||
|
||||
if( parent.empty() ){
|
||||
// non-existant parent; just remove it
|
||||
data.parent = undefined;
|
||||
} else if( parent[0].removed() ) {
|
||||
util.warn('Node added with missing parent, reference to parent removed');
|
||||
data.parent = undefined;
|
||||
node._private.parent = null;
|
||||
} else {
|
||||
let selfAsParent = false;
|
||||
let ancestor = parent;
|
||||
while( !ancestor.empty() ){
|
||||
if( node.same( ancestor ) ){
|
||||
// mark self as parent and remove from data
|
||||
selfAsParent = true;
|
||||
data.parent = undefined; // remove parent reference
|
||||
|
||||
// exit or we loop forever
|
||||
break;
|
||||
}
|
||||
|
||||
ancestor = ancestor.parent();
|
||||
}
|
||||
|
||||
if( !selfAsParent ){
|
||||
// connect with children
|
||||
parent[0]._private.children.push( node );
|
||||
node._private.parent = parent[0];
|
||||
|
||||
// let the core know we have a compound graph
|
||||
cy_p.hasCompoundNodes = true;
|
||||
}
|
||||
} // else
|
||||
} // if specified parent
|
||||
} // for each node
|
||||
|
||||
if( elements.length > 0 ){
|
||||
let restored = elements.length === self.length ? self : new Collection( cy, elements );
|
||||
|
||||
for( let i = 0; i < restored.length; i++ ){
|
||||
let ele = restored[i];
|
||||
|
||||
if( ele.isNode() ){ continue; }
|
||||
|
||||
// adding an edge invalidates the traversal caches for the parallel edges
|
||||
ele.parallelEdges().clearTraversalCache();
|
||||
|
||||
// adding an edge invalidates the traversal cache for the connected nodes
|
||||
ele.source().clearTraversalCache();
|
||||
ele.target().clearTraversalCache();
|
||||
}
|
||||
|
||||
let toUpdateStyle;
|
||||
|
||||
if( cy_p.hasCompoundNodes ){
|
||||
toUpdateStyle = cy.collection().merge( restored ).merge( restored.connectedNodes() ).merge( restored.parent() );
|
||||
} else {
|
||||
toUpdateStyle = restored;
|
||||
}
|
||||
|
||||
toUpdateStyle.dirtyCompoundBoundsCache().dirtyBoundingBoxCache().updateStyle( notifyRenderer );
|
||||
|
||||
if( notifyRenderer ){
|
||||
restored.emitAndNotify( 'add' );
|
||||
} else if( addToPool ){
|
||||
restored.emit( 'add' );
|
||||
}
|
||||
}
|
||||
|
||||
return self; // chainability
|
||||
};
|
||||
|
||||
elesfn.removed = function(){
|
||||
let ele = this[0];
|
||||
return ele && ele._private.removed;
|
||||
};
|
||||
|
||||
elesfn.inside = function(){
|
||||
let ele = this[0];
|
||||
return ele && !ele._private.removed;
|
||||
};
|
||||
|
||||
elesfn.remove = function( notifyRenderer = true, removeFromPool = true ){
|
||||
let self = this;
|
||||
let elesToRemove = [];
|
||||
let elesToRemoveIds = {};
|
||||
let cy = self._private.cy;
|
||||
|
||||
// add connected edges
|
||||
function addConnectedEdges( node ){
|
||||
let edges = node._private.edges;
|
||||
for( let i = 0; i < edges.length; i++ ){
|
||||
add( edges[ i ] );
|
||||
}
|
||||
}
|
||||
|
||||
// add descendant nodes
|
||||
function addChildren( node ){
|
||||
let children = node._private.children;
|
||||
|
||||
for( let i = 0; i < children.length; i++ ){
|
||||
add( children[ i ] );
|
||||
}
|
||||
}
|
||||
|
||||
function add( ele ){
|
||||
let alreadyAdded = elesToRemoveIds[ ele.id() ];
|
||||
if( (removeFromPool && ele.removed()) || alreadyAdded ){
|
||||
return;
|
||||
} else {
|
||||
elesToRemoveIds[ ele.id() ] = true;
|
||||
}
|
||||
|
||||
if( ele.isNode() ){
|
||||
elesToRemove.push( ele ); // nodes are removed last
|
||||
|
||||
addConnectedEdges( ele );
|
||||
addChildren( ele );
|
||||
} else {
|
||||
elesToRemove.unshift( ele ); // edges are removed first
|
||||
}
|
||||
}
|
||||
|
||||
// make the list of elements to remove
|
||||
// (may be removing more than specified due to connected edges etc)
|
||||
|
||||
for( let i = 0, l = self.length; i < l; i++ ){
|
||||
let ele = self[ i ];
|
||||
|
||||
add( ele );
|
||||
}
|
||||
|
||||
function removeEdgeRef( node, edge ){
|
||||
let connectedEdges = node._private.edges;
|
||||
|
||||
util.removeFromArray( connectedEdges, edge );
|
||||
|
||||
// removing an edges invalidates the traversal cache for its nodes
|
||||
node.clearTraversalCache();
|
||||
}
|
||||
|
||||
function removeParallelRef( pllEdge ){
|
||||
// removing an edge invalidates the traversal caches for the parallel edges
|
||||
pllEdge.clearTraversalCache();
|
||||
}
|
||||
|
||||
let alteredParents = [];
|
||||
alteredParents.ids = {};
|
||||
|
||||
function removeChildRef( parent, ele ){
|
||||
ele = ele[0];
|
||||
parent = parent[0];
|
||||
|
||||
let children = parent._private.children;
|
||||
let pid = parent.id();
|
||||
|
||||
util.removeFromArray( children, ele ); // remove parent => child ref
|
||||
|
||||
ele._private.parent = null; // remove child => parent ref
|
||||
|
||||
if( !alteredParents.ids[ pid ] ){
|
||||
alteredParents.ids[ pid ] = true;
|
||||
alteredParents.push( parent );
|
||||
}
|
||||
}
|
||||
|
||||
self.dirtyCompoundBoundsCache();
|
||||
|
||||
if( removeFromPool ){
|
||||
cy.removeFromPool( elesToRemove ); // remove from core pool
|
||||
}
|
||||
|
||||
for( let i = 0; i < elesToRemove.length; i++ ){
|
||||
let ele = elesToRemove[ i ];
|
||||
|
||||
if( ele.isEdge() ){ // remove references to this edge in its connected nodes
|
||||
let src = ele.source()[0];
|
||||
let tgt = ele.target()[0];
|
||||
|
||||
removeEdgeRef( src, ele );
|
||||
removeEdgeRef( tgt, ele );
|
||||
|
||||
let pllEdges = ele.parallelEdges();
|
||||
|
||||
for( let j = 0; j < pllEdges.length; j++ ){
|
||||
let pllEdge = pllEdges[j];
|
||||
|
||||
removeParallelRef(pllEdge);
|
||||
|
||||
if( pllEdge.isBundledBezier() ){
|
||||
pllEdge.dirtyBoundingBoxCache();
|
||||
}
|
||||
}
|
||||
|
||||
} else { // remove reference to parent
|
||||
let parent = ele.parent();
|
||||
|
||||
if( parent.length !== 0 ){
|
||||
removeChildRef( parent, ele );
|
||||
}
|
||||
}
|
||||
|
||||
if( removeFromPool ){
|
||||
// mark as removed
|
||||
ele._private.removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check to see if we have a compound graph or not
|
||||
let elesStillInside = cy._private.elements;
|
||||
cy._private.hasCompoundNodes = false;
|
||||
for( let i = 0; i < elesStillInside.length; i++ ){
|
||||
let ele = elesStillInside[ i ];
|
||||
|
||||
if( ele.isParent() ){
|
||||
cy._private.hasCompoundNodes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let removedElements = new Collection( this.cy(), elesToRemove );
|
||||
|
||||
if( removedElements.size() > 0 ){
|
||||
// must manually notify since trigger won't do this automatically once removed
|
||||
|
||||
if( notifyRenderer ){
|
||||
removedElements.emitAndNotify('remove');
|
||||
} else if( removeFromPool ){
|
||||
removedElements.emit('remove');
|
||||
}
|
||||
}
|
||||
|
||||
// the parents who were modified by the removal need their style updated
|
||||
for( let i = 0; i < alteredParents.length; i++ ){
|
||||
let ele = alteredParents[ i ];
|
||||
|
||||
if( !removeFromPool || !ele.removed() ){
|
||||
ele.updateStyle();
|
||||
}
|
||||
}
|
||||
|
||||
return removedElements;
|
||||
};
|
||||
|
||||
elesfn.move = function( struct ){
|
||||
let cy = this._private.cy;
|
||||
let eles = this;
|
||||
|
||||
// just clean up refs, caches, etc. in the same way as when removing and then restoring
|
||||
// (our calls to remove/restore do not remove from the graph or make events)
|
||||
let notifyRenderer = false;
|
||||
let modifyPool = false;
|
||||
|
||||
let toString = id => id == null ? id : '' + id; // id must be string
|
||||
|
||||
if( struct.source !== undefined || struct.target !== undefined ){
|
||||
let srcId = toString(struct.source);
|
||||
let tgtId = toString(struct.target);
|
||||
let srcExists = srcId != null && cy.hasElementWithId( srcId );
|
||||
let tgtExists = tgtId != null && cy.hasElementWithId( tgtId );
|
||||
|
||||
if( srcExists || tgtExists ){
|
||||
cy.batch(() => { // avoid duplicate style updates
|
||||
eles.remove( notifyRenderer, modifyPool ); // clean up refs etc.
|
||||
eles.emitAndNotify('moveout');
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[i];
|
||||
let data = ele._private.data;
|
||||
|
||||
if( ele.isEdge() ){
|
||||
if( srcExists ){ data.source = srcId; }
|
||||
|
||||
if( tgtExists ){ data.target = tgtId; }
|
||||
}
|
||||
}
|
||||
|
||||
eles.restore( notifyRenderer, modifyPool ); // make new refs, style, etc.
|
||||
});
|
||||
|
||||
eles.emitAndNotify('move');
|
||||
}
|
||||
|
||||
} else if( struct.parent !== undefined ){ // move node to new parent
|
||||
let parentId = toString(struct.parent);
|
||||
let parentExists = parentId === null || cy.hasElementWithId( parentId );
|
||||
|
||||
if( parentExists ){
|
||||
let pidToAssign = parentId === null ? undefined : parentId;
|
||||
|
||||
cy.batch(() => { // avoid duplicate style updates
|
||||
let updated = eles.remove( notifyRenderer, modifyPool ); // clean up refs etc.
|
||||
updated.emitAndNotify('moveout');
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[i];
|
||||
let data = ele._private.data;
|
||||
|
||||
if( ele.isNode() ){
|
||||
data.parent = pidToAssign;
|
||||
}
|
||||
}
|
||||
|
||||
updated.restore( notifyRenderer, modifyPool ); // make new refs, style, etc.
|
||||
});
|
||||
|
||||
eles.emitAndNotify('move');
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
[
|
||||
algorithms,
|
||||
animation,
|
||||
classNames,
|
||||
comparators,
|
||||
compounds,
|
||||
data,
|
||||
degree,
|
||||
dimensions,
|
||||
events,
|
||||
filter,
|
||||
group,
|
||||
iteration,
|
||||
layout,
|
||||
style,
|
||||
switchFunctions,
|
||||
traversing
|
||||
].forEach( function( props ){
|
||||
util.extend( elesfn, props );
|
||||
} );
|
||||
|
||||
export default Collection;
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import * as is from '../is.mjs' ;
|
||||
import zIndexSort from './zsort.mjs' ;
|
||||
import * as util from '../util/index.mjs';
|
||||
|
||||
let elesfn = ({
|
||||
forEach: function( fn, thisArg ){
|
||||
if( is.fn( fn ) ){
|
||||
let N = this.length;
|
||||
|
||||
for( let i = 0; i < N; i++ ){
|
||||
let ele = this[ i ];
|
||||
let ret = thisArg ? fn.apply( thisArg, [ ele, i, this ] ) : fn( ele, i, this );
|
||||
|
||||
if( ret === false ){ break; } // exit each early on return false
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
toArray: function(){
|
||||
let array = [];
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
array.push( this[ i ] );
|
||||
}
|
||||
|
||||
return array;
|
||||
},
|
||||
|
||||
slice: function( start, end ){
|
||||
let array = [];
|
||||
let thisSize = this.length;
|
||||
|
||||
if( end == null ){
|
||||
end = thisSize;
|
||||
}
|
||||
|
||||
if( start == null ){
|
||||
start = 0;
|
||||
}
|
||||
|
||||
if( start < 0 ){
|
||||
start = thisSize + start;
|
||||
}
|
||||
|
||||
if( end < 0 ){
|
||||
end = thisSize + end;
|
||||
}
|
||||
|
||||
for( let i = start; i >= 0 && i < end && i < thisSize; i++ ){
|
||||
array.push( this[ i ] );
|
||||
}
|
||||
|
||||
return this.spawn( array );
|
||||
},
|
||||
|
||||
size: function(){
|
||||
return this.length;
|
||||
},
|
||||
|
||||
eq: function( i ){
|
||||
return this[ i ] || this.spawn();
|
||||
},
|
||||
|
||||
first: function(){
|
||||
return this[0] || this.spawn();
|
||||
},
|
||||
|
||||
last: function(){
|
||||
return this[ this.length - 1 ] || this.spawn();
|
||||
},
|
||||
|
||||
empty: function(){
|
||||
return this.length === 0;
|
||||
},
|
||||
|
||||
nonempty: function(){
|
||||
return !this.empty();
|
||||
},
|
||||
|
||||
sort: function( sortFn ){
|
||||
if( !is.fn( sortFn ) ){
|
||||
return this;
|
||||
}
|
||||
|
||||
let sorted = this.toArray().sort( sortFn );
|
||||
|
||||
return this.spawn( sorted );
|
||||
},
|
||||
|
||||
sortByZIndex: function(){
|
||||
return this.sort( zIndexSort );
|
||||
},
|
||||
|
||||
zDepth: function(){
|
||||
let ele = this[0];
|
||||
if( !ele ){ return undefined; }
|
||||
|
||||
// let cy = ele.cy();
|
||||
let _p = ele._private;
|
||||
let group = _p.group;
|
||||
|
||||
if( group === 'nodes' ){
|
||||
let depth = _p.data.parent ? ele.parents().size() : 0;
|
||||
|
||||
if( !ele.isParent() ){
|
||||
return util.MAX_INT - 1; // childless nodes always on top
|
||||
}
|
||||
|
||||
return depth;
|
||||
} else {
|
||||
let src = _p.source;
|
||||
let tgt = _p.target;
|
||||
let srcDepth = src.zDepth();
|
||||
let tgtDepth = tgt.zDepth();
|
||||
|
||||
return Math.max( srcDepth, tgtDepth, 0 ); // depth of deepest parent
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
elesfn.each = elesfn.forEach;
|
||||
|
||||
const defineSymbolIterator = () => {
|
||||
const typeofUndef = typeof undefined;
|
||||
const isIteratorSupported = typeof Symbol != typeofUndef && typeof Symbol.iterator != typeofUndef;
|
||||
|
||||
if (isIteratorSupported) {
|
||||
elesfn[Symbol.iterator] = function() {
|
||||
let entry = { value: undefined, done: false };
|
||||
let i = 0;
|
||||
let length = this.length;
|
||||
|
||||
return {
|
||||
next: () => {
|
||||
if ( i < length ) {
|
||||
entry.value = this[i++];
|
||||
} else {
|
||||
entry.value = undefined;
|
||||
entry.done = true;
|
||||
}
|
||||
|
||||
return entry;
|
||||
},
|
||||
[Symbol.iterator]: function() {
|
||||
return this;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
defineSymbolIterator();
|
||||
|
||||
export default elesfn;
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import * as util from '../util/index.mjs';
|
||||
import Promise from '../promise.mjs';
|
||||
import * as math from '../math.mjs';
|
||||
|
||||
const getLayoutDimensionOptions = util.defaults({
|
||||
nodeDimensionsIncludeLabels: false
|
||||
});
|
||||
|
||||
let elesfn = ({
|
||||
// Calculates and returns node dimensions { x, y } based on options given
|
||||
layoutDimensions: function( options ){
|
||||
options = getLayoutDimensionOptions( options );
|
||||
|
||||
let dims;
|
||||
|
||||
if( !this.takesUpSpace() ){
|
||||
dims = { w: 0, h: 0 };
|
||||
} else if( options.nodeDimensionsIncludeLabels ){
|
||||
let bbDim = this.boundingBox();
|
||||
|
||||
dims = {
|
||||
w: bbDim.w,
|
||||
h: bbDim.h
|
||||
};
|
||||
} else {
|
||||
dims = {
|
||||
w: this.outerWidth(),
|
||||
h: this.outerHeight()
|
||||
};
|
||||
}
|
||||
|
||||
// sanitise the dimensions for external layouts (avoid division by zero)
|
||||
if( dims.w === 0 || dims.h === 0 ){
|
||||
dims.w = dims.h = 1;
|
||||
}
|
||||
|
||||
return dims;
|
||||
},
|
||||
|
||||
// using standard layout options, apply position function (w/ or w/o animation)
|
||||
layoutPositions: function( layout, options, fn ){
|
||||
let nodes = this.nodes().filter(n => !n.isParent());
|
||||
let cy = this.cy();
|
||||
let layoutEles = options.eles; // nodes & edges
|
||||
let getMemoizeKey = node => node.id();
|
||||
let fnMem = util.memoize( fn, getMemoizeKey ); // memoized version of position function
|
||||
|
||||
layout.emit( { type: 'layoutstart', layout: layout } );
|
||||
|
||||
layout.animations = [];
|
||||
|
||||
let calculateSpacing = function( spacing, nodesBb, pos ){
|
||||
let center = {
|
||||
x: nodesBb.x1 + nodesBb.w / 2,
|
||||
y: nodesBb.y1 + nodesBb.h / 2
|
||||
};
|
||||
|
||||
let spacingVector = { // scale from center of bounding box (not necessarily 0,0)
|
||||
x: (pos.x - center.x) * spacing,
|
||||
y: (pos.y - center.y) * spacing
|
||||
};
|
||||
|
||||
return {
|
||||
x: center.x + spacingVector.x,
|
||||
y: center.y + spacingVector.y
|
||||
};
|
||||
};
|
||||
|
||||
let useSpacingFactor = options.spacingFactor && options.spacingFactor !== 1;
|
||||
|
||||
let spacingBb = function(){
|
||||
if( !useSpacingFactor ){ return null; }
|
||||
|
||||
let bb = math.makeBoundingBox();
|
||||
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
let node = nodes[i];
|
||||
let pos = fnMem( node, i );
|
||||
|
||||
math.expandBoundingBoxByPoint( bb, pos.x, pos.y );
|
||||
}
|
||||
|
||||
return bb;
|
||||
};
|
||||
|
||||
let bb = spacingBb();
|
||||
|
||||
let getFinalPos = util.memoize( function( node, i ){
|
||||
let newPos = fnMem( node, i );
|
||||
|
||||
if( useSpacingFactor ){
|
||||
let spacing = Math.abs( options.spacingFactor );
|
||||
|
||||
newPos = calculateSpacing( spacing, bb, newPos );
|
||||
}
|
||||
|
||||
if( options.transform != null ){
|
||||
newPos = options.transform( node, newPos );
|
||||
}
|
||||
|
||||
return newPos;
|
||||
}, getMemoizeKey );
|
||||
|
||||
if( options.animate ){
|
||||
for( let i = 0; i < nodes.length; i++ ){
|
||||
let node = nodes[ i ];
|
||||
let newPos = getFinalPos( node, i );
|
||||
let animateNode = options.animateFilter == null || options.animateFilter( node, i );
|
||||
|
||||
if( animateNode ){
|
||||
let ani = node.animation( {
|
||||
position: newPos,
|
||||
duration: options.animationDuration,
|
||||
easing: options.animationEasing
|
||||
} );
|
||||
|
||||
layout.animations.push( ani );
|
||||
} else {
|
||||
node.position( newPos );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if( options.fit ){
|
||||
let fitAni = cy.animation({
|
||||
fit: {
|
||||
boundingBox: layoutEles.boundingBoxAt( getFinalPos ),
|
||||
padding: options.padding
|
||||
},
|
||||
duration: options.animationDuration,
|
||||
easing: options.animationEasing
|
||||
});
|
||||
|
||||
layout.animations.push( fitAni );
|
||||
} else if( options.zoom !== undefined && options.pan !== undefined ){
|
||||
let zoomPanAni = cy.animation({
|
||||
zoom: options.zoom,
|
||||
pan: options.pan,
|
||||
duration: options.animationDuration,
|
||||
easing: options.animationEasing
|
||||
});
|
||||
|
||||
layout.animations.push( zoomPanAni );
|
||||
}
|
||||
|
||||
layout.animations.forEach(ani => ani.play());
|
||||
|
||||
layout.one( 'layoutready', options.ready );
|
||||
layout.emit( { type: 'layoutready', layout: layout } );
|
||||
|
||||
Promise.all( layout.animations.map(function( ani ){
|
||||
return ani.promise();
|
||||
}) ).then(function(){
|
||||
layout.one( 'layoutstop', options.stop );
|
||||
layout.emit( { type: 'layoutstop', layout: layout } );
|
||||
});
|
||||
} else {
|
||||
|
||||
nodes.positions( getFinalPos );
|
||||
|
||||
if( options.fit ){
|
||||
cy.fit( options.eles, options.padding );
|
||||
}
|
||||
|
||||
if( options.zoom != null ){
|
||||
cy.zoom( options.zoom );
|
||||
}
|
||||
|
||||
if( options.pan ){
|
||||
cy.pan( options.pan );
|
||||
}
|
||||
|
||||
layout.one( 'layoutready', options.ready );
|
||||
layout.emit( { type: 'layoutready', layout: layout } );
|
||||
|
||||
layout.one( 'layoutstop', options.stop );
|
||||
layout.emit( { type: 'layoutstop', layout: layout } );
|
||||
}
|
||||
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
layout: function( options ){
|
||||
let cy = this.cy();
|
||||
|
||||
return cy.makeLayout( util.extend( {}, options, {
|
||||
eles: this
|
||||
} ) );
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// aliases:
|
||||
elesfn.createLayout = elesfn.makeLayout = elesfn.layout;
|
||||
|
||||
export default elesfn;
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
import * as is from '../is.mjs';
|
||||
import * as util from '../util/index.mjs';
|
||||
|
||||
function styleCache( key, fn, ele ){
|
||||
var _p = ele._private;
|
||||
var cache = _p.styleCache = _p.styleCache || [];
|
||||
var val;
|
||||
|
||||
if( (val = cache[key]) != null ){
|
||||
return val;
|
||||
} else {
|
||||
val = cache[key] = fn( ele );
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
function cacheStyleFunction( key, fn ){
|
||||
key = util.hashString( key );
|
||||
|
||||
return function cachedStyleFunction( ele ){
|
||||
return styleCache( key, fn, ele );
|
||||
};
|
||||
}
|
||||
|
||||
function cachePrototypeStyleFunction( key, fn ){
|
||||
key = util.hashString( key );
|
||||
|
||||
let selfFn = ele => fn.call( ele );
|
||||
|
||||
return function cachedPrototypeStyleFunction(){
|
||||
var ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return styleCache( key, selfFn, ele );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let elesfn = ({
|
||||
|
||||
recalculateRenderedStyle: function( useCache ){
|
||||
let cy = this.cy();
|
||||
let renderer = cy.renderer();
|
||||
let styleEnabled = cy.styleEnabled();
|
||||
|
||||
if( renderer && styleEnabled ){
|
||||
renderer.recalculateRenderedStyle( this, useCache );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
dirtyStyleCache: function(){
|
||||
let cy = this.cy();
|
||||
let dirty = ele => ele._private.styleCache = null;
|
||||
|
||||
if( cy.hasCompoundNodes() ){
|
||||
let eles;
|
||||
|
||||
eles = this.spawnSelf()
|
||||
.merge( this.descendants() )
|
||||
.merge( this.parents() )
|
||||
;
|
||||
|
||||
eles.merge( eles.connectedEdges() );
|
||||
|
||||
eles.forEach( dirty );
|
||||
} else {
|
||||
this.forEach( ele => {
|
||||
dirty( ele );
|
||||
|
||||
ele.connectedEdges().forEach( dirty );
|
||||
} );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// fully updates (recalculates) the style for the elements
|
||||
updateStyle: function( notifyRenderer ){
|
||||
let cy = this._private.cy;
|
||||
|
||||
if( !cy.styleEnabled() ){ return this; }
|
||||
|
||||
if( cy.batching() ){
|
||||
let bEles = cy._private.batchStyleEles;
|
||||
|
||||
bEles.merge( this );
|
||||
|
||||
return this; // chaining and exit early when batching
|
||||
}
|
||||
|
||||
let hasCompounds = cy.hasCompoundNodes();
|
||||
let updatedEles = this;
|
||||
|
||||
notifyRenderer = notifyRenderer || notifyRenderer === undefined ? true : false;
|
||||
|
||||
if( hasCompounds ){ // then add everything up and down for compound selector checks
|
||||
updatedEles = this.spawnSelf().merge( this.descendants() ).merge( this.parents() );
|
||||
}
|
||||
|
||||
// let changedEles = style.apply( updatedEles );
|
||||
let changedEles = updatedEles;
|
||||
|
||||
if( notifyRenderer ){
|
||||
changedEles.emitAndNotify( 'style' ); // let renderer know we changed style
|
||||
} else {
|
||||
changedEles.emit( 'style' ); // just fire the event
|
||||
}
|
||||
|
||||
updatedEles.forEach(ele => ele._private.styleDirty = true);
|
||||
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
// private: clears dirty flag and recalculates style
|
||||
cleanStyle: function(){
|
||||
let cy = this.cy();
|
||||
|
||||
if( !cy.styleEnabled() ){ return; }
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[i];
|
||||
|
||||
if( ele._private.styleDirty ){
|
||||
// n.b. this flag should be set before apply() to avoid potential infinite recursion
|
||||
ele._private.styleDirty = false;
|
||||
|
||||
cy.style().apply(ele);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// get the internal parsed style object for the specified property
|
||||
parsedStyle: function( property, includeNonDefault = true ){
|
||||
let ele = this[0];
|
||||
let cy = ele.cy();
|
||||
|
||||
if( !cy.styleEnabled() ){ return; }
|
||||
|
||||
if( ele ){
|
||||
// this.cleanStyle();
|
||||
|
||||
// Inline the important part of cleanStyle(), for raw performance
|
||||
if( ele._private.styleDirty ){
|
||||
// n.b. this flag should be set before apply() to avoid potential infinite recursion
|
||||
ele._private.styleDirty = false;
|
||||
cy.style().apply(ele);
|
||||
}
|
||||
|
||||
let overriddenStyle = ele._private.style[ property ];
|
||||
|
||||
if( overriddenStyle != null ){
|
||||
return overriddenStyle;
|
||||
} else if( includeNonDefault ){
|
||||
return cy.style().getDefaultProperty( property );
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
numericStyle: function( property ){
|
||||
let ele = this[0];
|
||||
|
||||
if( !ele.cy().styleEnabled() ){ return; }
|
||||
|
||||
if( ele ){
|
||||
let pstyle = ele.pstyle( property );
|
||||
|
||||
return pstyle.pfValue !== undefined ? pstyle.pfValue : pstyle.value;
|
||||
}
|
||||
},
|
||||
|
||||
numericStyleUnits: function( property ){
|
||||
let ele = this[0];
|
||||
|
||||
if( !ele.cy().styleEnabled() ){ return; }
|
||||
|
||||
if( ele ){
|
||||
return ele.pstyle( property ).units;
|
||||
}
|
||||
},
|
||||
|
||||
// get the specified css property as a rendered value (i.e. on-screen value)
|
||||
// or get the whole rendered style if no property specified (NB doesn't allow setting)
|
||||
renderedStyle: function( property ){
|
||||
let cy = this.cy();
|
||||
if( !cy.styleEnabled() ){ return this; }
|
||||
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return cy.style().getRenderedStyle( ele, property );
|
||||
}
|
||||
},
|
||||
|
||||
// read the calculated css style of the element or override the style (via a bypass)
|
||||
style: function( name, value ){
|
||||
let cy = this.cy();
|
||||
|
||||
if( !cy.styleEnabled() ){ return this; }
|
||||
|
||||
let updateTransitions = false;
|
||||
let style = cy.style();
|
||||
|
||||
if( is.plainObject( name ) ){ // then extend the bypass
|
||||
let props = name;
|
||||
style.applyBypass( this, props, updateTransitions );
|
||||
|
||||
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
|
||||
|
||||
} else if( is.string( name ) ){
|
||||
|
||||
if( value === undefined ){ // then get the property from the style
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return style.getStylePropertyValue( ele, name );
|
||||
} else { // empty collection => can't get any value
|
||||
return;
|
||||
}
|
||||
|
||||
} else { // then set the bypass with the property value
|
||||
style.applyBypass( this, name, value, updateTransitions );
|
||||
|
||||
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
|
||||
}
|
||||
|
||||
} else if( name === undefined ){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return style.getRawStyle( ele );
|
||||
} else { // empty collection => can't get any value
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
removeStyle: function( names ){
|
||||
let cy = this.cy();
|
||||
|
||||
if( !cy.styleEnabled() ){ return this; }
|
||||
|
||||
let updateTransitions = false;
|
||||
let style = cy.style();
|
||||
let eles = this;
|
||||
|
||||
if( names === undefined ){
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
|
||||
style.removeAllBypasses( ele, updateTransitions );
|
||||
}
|
||||
} else {
|
||||
names = names.split( /\s+/ );
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
|
||||
style.removeBypasses( ele, names, updateTransitions );
|
||||
}
|
||||
}
|
||||
|
||||
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
|
||||
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
show: function(){
|
||||
this.css( 'display', 'element' );
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
hide: function(){
|
||||
this.css( 'display', 'none' );
|
||||
return this; // chaining
|
||||
},
|
||||
|
||||
effectiveOpacity: function(){
|
||||
let cy = this.cy();
|
||||
if( !cy.styleEnabled() ){ return 1; }
|
||||
|
||||
let hasCompoundNodes = cy.hasCompoundNodes();
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
let _p = ele._private;
|
||||
let parentOpacity = ele.pstyle( 'opacity' ).value;
|
||||
|
||||
if( !hasCompoundNodes ){ return parentOpacity; }
|
||||
|
||||
let parents = !_p.data.parent ? null : ele.parents();
|
||||
|
||||
if( parents ){
|
||||
for( let i = 0; i < parents.length; i++ ){
|
||||
let parent = parents[ i ];
|
||||
let opacity = parent.pstyle( 'opacity' ).value;
|
||||
|
||||
parentOpacity = opacity * parentOpacity;
|
||||
}
|
||||
}
|
||||
|
||||
return parentOpacity;
|
||||
}
|
||||
},
|
||||
|
||||
transparent: function(){
|
||||
let cy = this.cy();
|
||||
if( !cy.styleEnabled() ){ return false; }
|
||||
|
||||
let ele = this[0];
|
||||
let hasCompoundNodes = ele.cy().hasCompoundNodes();
|
||||
|
||||
if( ele ){
|
||||
if( !hasCompoundNodes ){
|
||||
return ele.pstyle( 'opacity' ).value === 0;
|
||||
} else {
|
||||
return ele.effectiveOpacity() === 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
backgrounding: function(){
|
||||
let cy = this.cy();
|
||||
if( !cy.styleEnabled() ){ return false; }
|
||||
|
||||
let ele = this[0];
|
||||
|
||||
return ele._private.backgrounding ? true : false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function checkCompound( ele, parentOk ){
|
||||
let _p = ele._private;
|
||||
let parents = _p.data.parent ? ele.parents() : null;
|
||||
|
||||
if( parents ){ for( let i = 0; i < parents.length; i++ ){
|
||||
let parent = parents[ i ];
|
||||
|
||||
if( !parentOk( parent ) ){ return false; }
|
||||
} }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function defineDerivedStateFunction( specs ){
|
||||
let ok = specs.ok;
|
||||
let edgeOkViaNode = specs.edgeOkViaNode || specs.ok;
|
||||
let parentOk = specs.parentOk || specs.ok;
|
||||
|
||||
return function(){
|
||||
let cy = this.cy();
|
||||
if( !cy.styleEnabled() ){ return true; }
|
||||
|
||||
let ele = this[0];
|
||||
let hasCompoundNodes = cy.hasCompoundNodes();
|
||||
|
||||
if( ele ){
|
||||
let _p = ele._private;
|
||||
|
||||
if( !ok( ele ) ){ return false; }
|
||||
|
||||
if( ele.isNode() ){
|
||||
return !hasCompoundNodes || checkCompound( ele, parentOk );
|
||||
} else {
|
||||
let src = _p.source;
|
||||
let tgt = _p.target;
|
||||
|
||||
return ( edgeOkViaNode(src) && (!hasCompoundNodes || checkCompound(src, edgeOkViaNode)) ) &&
|
||||
( src === tgt || ( edgeOkViaNode(tgt) && (!hasCompoundNodes || checkCompound(tgt, edgeOkViaNode)) ) );
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let eleTakesUpSpace = cacheStyleFunction( 'eleTakesUpSpace', function( ele ){
|
||||
return (
|
||||
ele.pstyle( 'display' ).value === 'element'
|
||||
&& ele.width() !== 0
|
||||
&& ( ele.isNode() ? ele.height() !== 0 : true )
|
||||
);
|
||||
} );
|
||||
|
||||
elesfn.takesUpSpace = cachePrototypeStyleFunction( 'takesUpSpace', defineDerivedStateFunction({
|
||||
ok: eleTakesUpSpace
|
||||
}) );
|
||||
|
||||
let eleInteractive = cacheStyleFunction( 'eleInteractive', function( ele ){
|
||||
return (
|
||||
ele.pstyle('events').value === 'yes'
|
||||
&& ele.pstyle('visibility').value === 'visible'
|
||||
&& eleTakesUpSpace( ele )
|
||||
);
|
||||
} );
|
||||
|
||||
let parentInteractive = cacheStyleFunction( 'parentInteractive', function( parent ){
|
||||
return (
|
||||
parent.pstyle('visibility').value === 'visible'
|
||||
&& eleTakesUpSpace( parent )
|
||||
);
|
||||
} );
|
||||
|
||||
elesfn.interactive = cachePrototypeStyleFunction( 'interactive', defineDerivedStateFunction({
|
||||
ok: eleInteractive,
|
||||
parentOk: parentInteractive,
|
||||
edgeOkViaNode: eleTakesUpSpace
|
||||
}) );
|
||||
|
||||
elesfn.noninteractive = function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return !ele.interactive();
|
||||
}
|
||||
};
|
||||
|
||||
let eleVisible = cacheStyleFunction( 'eleVisible', function( ele ){
|
||||
return (
|
||||
ele.pstyle( 'visibility' ).value === 'visible'
|
||||
&& ele.pstyle( 'opacity' ).pfValue !== 0
|
||||
&& eleTakesUpSpace( ele )
|
||||
);
|
||||
} );
|
||||
|
||||
let edgeVisibleViaNode = eleTakesUpSpace;
|
||||
|
||||
elesfn.visible = cachePrototypeStyleFunction( 'visible', defineDerivedStateFunction({
|
||||
ok: eleVisible,
|
||||
edgeOkViaNode: edgeVisibleViaNode
|
||||
}) );
|
||||
|
||||
elesfn.hidden = function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
return !ele.visible();
|
||||
}
|
||||
};
|
||||
|
||||
elesfn.isBundledBezier = cachePrototypeStyleFunction('isBundledBezier', function(){
|
||||
if( !this.cy().styleEnabled() ){ return false; }
|
||||
|
||||
return !this.removed() && this.pstyle('curve-style').value === 'bezier' && this.takesUpSpace();
|
||||
});
|
||||
|
||||
elesfn.bypass = elesfn.css = elesfn.style;
|
||||
elesfn.renderedCss = elesfn.renderedStyle;
|
||||
elesfn.removeBypass = elesfn.removeCss = elesfn.removeStyle;
|
||||
elesfn.pstyle = elesfn.parsedStyle;
|
||||
|
||||
export default elesfn;
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import * as is from '../is.mjs';
|
||||
|
||||
const elesfn = {};
|
||||
|
||||
function defineSwitchFunction( params ){
|
||||
return function(){
|
||||
let args = arguments;
|
||||
let changedEles = [];
|
||||
|
||||
// e.g. cy.nodes().select( data, handler )
|
||||
if( args.length === 2 ){
|
||||
let data = args[0];
|
||||
let handler = args[1];
|
||||
this.on( params.event, data, handler );
|
||||
}
|
||||
|
||||
// e.g. cy.nodes().select( handler )
|
||||
else if( args.length === 1 && is.fn(args[0]) ){
|
||||
let handler = args[0];
|
||||
this.on( params.event, handler );
|
||||
}
|
||||
|
||||
// e.g. cy.nodes().select()
|
||||
// e.g. (private) cy.nodes().select(['tapselect'])
|
||||
else if( args.length === 0 || (args.length === 1 && is.array(args[0])) ){
|
||||
let addlEvents = args.length === 1 ? args[0] : null;
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let able = !params.ableField || ele._private[ params.ableField ];
|
||||
let changed = ele._private[ params.field ] != params.value;
|
||||
|
||||
if( params.overrideAble ){
|
||||
let overrideAble = params.overrideAble( ele );
|
||||
|
||||
if( overrideAble !== undefined ){
|
||||
able = overrideAble;
|
||||
|
||||
if( !overrideAble ){ return this; } // to save cycles assume not able for all on override
|
||||
}
|
||||
}
|
||||
|
||||
if( able ){
|
||||
ele._private[ params.field ] = params.value;
|
||||
|
||||
if( changed ){
|
||||
changedEles.push( ele );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let changedColl = this.spawn( changedEles );
|
||||
changedColl.updateStyle(); // change of state => possible change of style
|
||||
changedColl.emit( params.event );
|
||||
|
||||
if( addlEvents ){
|
||||
changedColl.emit( addlEvents );
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
function defineSwitchSet( params ){
|
||||
elesfn[ params.field ] = function(){
|
||||
let ele = this[0];
|
||||
|
||||
if( ele ){
|
||||
if( params.overrideField ){
|
||||
let val = params.overrideField( ele );
|
||||
|
||||
if( val !== undefined ){
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
return ele._private[ params.field ];
|
||||
}
|
||||
};
|
||||
|
||||
elesfn[ params.on ] = defineSwitchFunction( {
|
||||
event: params.on,
|
||||
field: params.field,
|
||||
ableField: params.ableField,
|
||||
overrideAble: params.overrideAble,
|
||||
value: true
|
||||
} );
|
||||
|
||||
elesfn[ params.off ] = defineSwitchFunction( {
|
||||
event: params.off,
|
||||
field: params.field,
|
||||
ableField: params.ableField,
|
||||
overrideAble: params.overrideAble,
|
||||
value: false
|
||||
} );
|
||||
}
|
||||
|
||||
defineSwitchSet( {
|
||||
field: 'locked',
|
||||
overrideField: function( ele ){
|
||||
return ele.cy().autolock() ? true : undefined;
|
||||
},
|
||||
on: 'lock',
|
||||
off: 'unlock'
|
||||
} );
|
||||
|
||||
defineSwitchSet( {
|
||||
field: 'grabbable',
|
||||
overrideField: function( ele ){
|
||||
return ele.cy().autoungrabify() || ele.pannable() ? false : undefined;
|
||||
},
|
||||
on: 'grabify',
|
||||
off: 'ungrabify'
|
||||
} );
|
||||
|
||||
defineSwitchSet( {
|
||||
field: 'selected',
|
||||
ableField: 'selectable',
|
||||
overrideAble: function( ele ){
|
||||
return ele.cy().autounselectify() ? false : undefined;
|
||||
},
|
||||
on: 'select',
|
||||
off: 'unselect'
|
||||
} );
|
||||
|
||||
defineSwitchSet( {
|
||||
field: 'selectable',
|
||||
overrideField: function( ele ){
|
||||
return ele.cy().autounselectify() ? false : undefined;
|
||||
},
|
||||
on: 'selectify',
|
||||
off: 'unselectify'
|
||||
} );
|
||||
|
||||
elesfn.deselect = elesfn.unselect;
|
||||
|
||||
elesfn.grabbed = function(){
|
||||
let ele = this[0];
|
||||
if( ele ){
|
||||
return ele._private.grabbed;
|
||||
}
|
||||
};
|
||||
|
||||
defineSwitchSet( {
|
||||
field: 'active',
|
||||
on: 'activate',
|
||||
off: 'unactivate'
|
||||
} );
|
||||
|
||||
defineSwitchSet( {
|
||||
field: 'pannable',
|
||||
on: 'panify',
|
||||
off: 'unpanify'
|
||||
} );
|
||||
|
||||
elesfn.inactive = function(){
|
||||
let ele = this[0];
|
||||
if( ele ){
|
||||
return !ele._private.active;
|
||||
}
|
||||
};
|
||||
|
||||
export default elesfn;
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
import * as util from '../util/index.mjs';
|
||||
import * as is from '../is.mjs';
|
||||
import cache from './cache-traversal-call.mjs';
|
||||
|
||||
let elesfn = {};
|
||||
|
||||
// DAG functions
|
||||
////////////////
|
||||
|
||||
let defineDagExtremity = function( params ){
|
||||
return function dagExtremityImpl( selector ){
|
||||
let eles = this;
|
||||
let ret = [];
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
if( !ele.isNode() ){
|
||||
continue;
|
||||
}
|
||||
|
||||
let disqualified = false;
|
||||
let edges = ele.connectedEdges();
|
||||
|
||||
for( let j = 0; j < edges.length; j++ ){
|
||||
let edge = edges[j];
|
||||
let src = edge.source();
|
||||
let tgt = edge.target();
|
||||
|
||||
if(
|
||||
( params.noIncomingEdges && tgt === ele && src !== ele )
|
||||
|| ( params.noOutgoingEdges && src === ele && tgt !== ele )
|
||||
){
|
||||
disqualified = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( !disqualified ){
|
||||
ret.push( ele );
|
||||
}
|
||||
}
|
||||
|
||||
return this.spawn( ret, true ).filter( selector );
|
||||
};
|
||||
};
|
||||
|
||||
let defineDagOneHop = function( params ){
|
||||
return function( selector ){
|
||||
let eles = this;
|
||||
let oEles = [];
|
||||
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let ele = eles[ i ];
|
||||
|
||||
if( !ele.isNode() ){ continue; }
|
||||
|
||||
let edges = ele.connectedEdges();
|
||||
for( let j = 0; j < edges.length; j++ ){
|
||||
let edge = edges[ j ];
|
||||
let src = edge.source();
|
||||
let tgt = edge.target();
|
||||
|
||||
if( params.outgoing && src === ele ){
|
||||
oEles.push( edge );
|
||||
oEles.push( tgt );
|
||||
} else if( params.incoming && tgt === ele ){
|
||||
oEles.push( edge );
|
||||
oEles.push( src );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.spawn( oEles, true ).filter( selector );
|
||||
};
|
||||
};
|
||||
|
||||
let defineDagAllHops = function( params ){
|
||||
return function( selector ){
|
||||
let eles = this;
|
||||
let sEles = [];
|
||||
let sElesIds = {};
|
||||
|
||||
for( ;; ){
|
||||
let next = params.outgoing ? eles.outgoers() : eles.incomers();
|
||||
|
||||
if( next.length === 0 ){ break; } // done if none left
|
||||
|
||||
let newNext = false;
|
||||
for( let i = 0; i < next.length; i++ ){
|
||||
let n = next[ i ];
|
||||
let nid = n.id();
|
||||
|
||||
if( !sElesIds[ nid ] ){
|
||||
sElesIds[ nid ] = true;
|
||||
sEles.push( n );
|
||||
newNext = true;
|
||||
}
|
||||
}
|
||||
|
||||
if( !newNext ){ break; } // done if touched all outgoers already
|
||||
|
||||
eles = next;
|
||||
}
|
||||
|
||||
return this.spawn( sEles, true ).filter( selector );
|
||||
};
|
||||
};
|
||||
|
||||
elesfn.clearTraversalCache = function( ){
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
this[i]._private.traversalCache = null;
|
||||
}
|
||||
};
|
||||
|
||||
util.extend( elesfn, {
|
||||
// get the root nodes in the DAG
|
||||
roots: defineDagExtremity({ noIncomingEdges: true }),
|
||||
|
||||
// get the leaf nodes in the DAG
|
||||
leaves: defineDagExtremity({ noOutgoingEdges: true }),
|
||||
|
||||
// normally called children in graph theory
|
||||
// these nodes =edges=> outgoing nodes
|
||||
outgoers: cache( defineDagOneHop({ outgoing: true }) , 'outgoers' ),
|
||||
|
||||
// aka DAG descendants
|
||||
successors: defineDagAllHops({ outgoing: true }),
|
||||
|
||||
// normally called parents in graph theory
|
||||
// these nodes <=edges= incoming nodes
|
||||
incomers: cache( defineDagOneHop({ incoming: true }), 'incomers' ),
|
||||
|
||||
// aka DAG ancestors
|
||||
predecessors: defineDagAllHops({ incoming: true })
|
||||
} );
|
||||
|
||||
|
||||
// Neighbourhood functions
|
||||
//////////////////////////
|
||||
|
||||
util.extend( elesfn, {
|
||||
neighborhood: cache(function( selector ){
|
||||
let elements = [];
|
||||
let nodes = this.nodes();
|
||||
|
||||
for( let i = 0; i < nodes.length; i++ ){ // for all nodes
|
||||
let node = nodes[ i ];
|
||||
let connectedEdges = node.connectedEdges();
|
||||
|
||||
// for each connected edge, add the edge and the other node
|
||||
for( let j = 0; j < connectedEdges.length; j++ ){
|
||||
let edge = connectedEdges[ j ];
|
||||
let src = edge.source();
|
||||
let tgt = edge.target();
|
||||
let otherNode = node === src ? tgt : src;
|
||||
|
||||
// need check in case of loop
|
||||
if( otherNode.length > 0 ){
|
||||
elements.push( otherNode[0] ); // add node 1 hop away
|
||||
}
|
||||
|
||||
// add connected edge
|
||||
elements.push( edge[0] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ( this.spawn( elements, true ) ).filter( selector );
|
||||
}, 'neighborhood'),
|
||||
|
||||
closedNeighborhood: function( selector ){
|
||||
return this.neighborhood().add( this ).filter( selector );
|
||||
},
|
||||
|
||||
openNeighborhood: function( selector ){
|
||||
return this.neighborhood( selector );
|
||||
}
|
||||
} );
|
||||
|
||||
// aliases
|
||||
elesfn.neighbourhood = elesfn.neighborhood;
|
||||
elesfn.closedNeighbourhood = elesfn.closedNeighborhood;
|
||||
elesfn.openNeighbourhood = elesfn.openNeighborhood;
|
||||
|
||||
// Edge functions
|
||||
/////////////////
|
||||
|
||||
util.extend( elesfn, {
|
||||
source: cache(function sourceImpl( selector ){
|
||||
let ele = this[0];
|
||||
let src;
|
||||
|
||||
if( ele ){
|
||||
src = ele._private.source || ele.cy().collection();
|
||||
}
|
||||
|
||||
return src && selector ? src.filter( selector ) : src;
|
||||
}, 'source'),
|
||||
|
||||
target: cache(function targetImpl( selector ){
|
||||
let ele = this[0];
|
||||
let tgt;
|
||||
|
||||
if( ele ){
|
||||
tgt = ele._private.target || ele.cy().collection();
|
||||
}
|
||||
|
||||
return tgt && selector ? tgt.filter( selector ) : tgt;
|
||||
}, 'target'),
|
||||
|
||||
sources: defineSourceFunction( {
|
||||
attr: 'source'
|
||||
} ),
|
||||
|
||||
targets: defineSourceFunction( {
|
||||
attr: 'target'
|
||||
} )
|
||||
} );
|
||||
|
||||
function defineSourceFunction( params ){
|
||||
return function sourceImpl( selector ){
|
||||
let sources = [];
|
||||
|
||||
for( let i = 0; i < this.length; i++ ){
|
||||
let ele = this[ i ];
|
||||
let src = ele._private[ params.attr ];
|
||||
|
||||
if( src ){
|
||||
sources.push( src );
|
||||
}
|
||||
}
|
||||
|
||||
return this.spawn( sources, true ).filter( selector );
|
||||
};
|
||||
}
|
||||
|
||||
util.extend( elesfn, {
|
||||
edgesWith: cache( defineEdgesWithFunction(), 'edgesWith' ),
|
||||
|
||||
edgesTo: cache( defineEdgesWithFunction( {
|
||||
thisIsSrc: true
|
||||
} ), 'edgesTo' )
|
||||
} );
|
||||
|
||||
function defineEdgesWithFunction( params ){
|
||||
|
||||
return function edgesWithImpl( otherNodes ){
|
||||
let elements = [];
|
||||
let cy = this._private.cy;
|
||||
let p = params || {};
|
||||
|
||||
// get elements if a selector is specified
|
||||
if( is.string( otherNodes ) ){
|
||||
otherNodes = cy.$( otherNodes );
|
||||
}
|
||||
|
||||
for( let h = 0; h < otherNodes.length; h++ ){
|
||||
let edges = otherNodes[ h ]._private.edges;
|
||||
|
||||
for( let i = 0; i < edges.length; i++ ){
|
||||
let edge = edges[ i ];
|
||||
let edgeData = edge._private.data;
|
||||
let thisToOther = this.hasElementWithId( edgeData.source ) && otherNodes.hasElementWithId( edgeData.target );
|
||||
let otherToThis = otherNodes.hasElementWithId( edgeData.source ) && this.hasElementWithId( edgeData.target );
|
||||
let edgeConnectsThisAndOther = thisToOther || otherToThis;
|
||||
|
||||
if( !edgeConnectsThisAndOther ){ continue; }
|
||||
|
||||
if( p.thisIsSrc || p.thisIsTgt ){
|
||||
if( p.thisIsSrc && !thisToOther ){ continue; }
|
||||
|
||||
if( p.thisIsTgt && !otherToThis ){ continue; }
|
||||
}
|
||||
|
||||
elements.push( edge );
|
||||
}
|
||||
}
|
||||
|
||||
return this.spawn( elements, true );
|
||||
};
|
||||
}
|
||||
|
||||
util.extend( elesfn, {
|
||||
connectedEdges: cache(function( selector ){
|
||||
let retEles = [];
|
||||
|
||||
let eles = this;
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let node = eles[ i ];
|
||||
if( !node.isNode() ){ continue; }
|
||||
|
||||
let edges = node._private.edges;
|
||||
|
||||
for( let j = 0; j < edges.length; j++ ){
|
||||
let edge = edges[ j ];
|
||||
retEles.push( edge );
|
||||
}
|
||||
}
|
||||
|
||||
return this.spawn( retEles, true ).filter( selector );
|
||||
}, 'connectedEdges'),
|
||||
|
||||
connectedNodes: cache(function( selector ){
|
||||
let retEles = [];
|
||||
|
||||
let eles = this;
|
||||
for( let i = 0; i < eles.length; i++ ){
|
||||
let edge = eles[ i ];
|
||||
if( !edge.isEdge() ){ continue; }
|
||||
|
||||
retEles.push( edge.source()[0] );
|
||||
retEles.push( edge.target()[0] );
|
||||
}
|
||||
|
||||
return this.spawn( retEles, true ).filter( selector );
|
||||
}, 'connectedNodes'),
|
||||
|
||||
parallelEdges: cache( defineParallelEdgesFunction(), 'parallelEdges' ),
|
||||
|
||||
codirectedEdges: cache( defineParallelEdgesFunction( {
|
||||
codirected: true
|
||||
} ), 'codirectedEdges' )
|
||||
} );
|
||||
|
||||
function defineParallelEdgesFunction( params ){
|
||||
let defaults = {
|
||||
codirected: false
|
||||
};
|
||||
params = util.extend( {}, defaults, params );
|
||||
|
||||
return function parallelEdgesImpl( selector ){ // micro-optimised for renderer
|
||||
let elements = [];
|
||||
let edges = this.edges();
|
||||
let p = params;
|
||||
|
||||
// look at all the edges in the collection
|
||||
for( let i = 0; i < edges.length; i++ ){
|
||||
let edge1 = edges[ i ];
|
||||
let edge1_p = edge1._private;
|
||||
let src1 = edge1_p.source;
|
||||
let srcid1 = src1._private.data.id;
|
||||
let tgtid1 = edge1_p.data.target;
|
||||
let srcEdges1 = src1._private.edges;
|
||||
|
||||
// look at edges connected to the src node of this edge
|
||||
for( let j = 0; j < srcEdges1.length; j++ ){
|
||||
let edge2 = srcEdges1[ j ];
|
||||
let edge2data = edge2._private.data;
|
||||
let tgtid2 = edge2data.target;
|
||||
let srcid2 = edge2data.source;
|
||||
|
||||
let codirected = tgtid2 === tgtid1 && srcid2 === srcid1;
|
||||
let oppdirected = srcid1 === tgtid2 && tgtid1 === srcid2;
|
||||
|
||||
if( (p.codirected && codirected) || (!p.codirected && (codirected || oppdirected)) ){
|
||||
elements.push( edge2 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.spawn( elements, true ).filter( selector );
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Misc functions
|
||||
/////////////////
|
||||
|
||||
util.extend( elesfn, {
|
||||
components: function(root){
|
||||
let self = this;
|
||||
let cy = self.cy();
|
||||
let visited = cy.collection();
|
||||
let unvisited = root == null ? self.nodes() : root.nodes();
|
||||
let components = [];
|
||||
|
||||
if( root != null && unvisited.empty() ){ // root may contain only edges
|
||||
unvisited = root.sources(); // doesn't matter which node to use (undirected), so just use the source sides
|
||||
}
|
||||
|
||||
let visitInComponent = ( node, component ) => {
|
||||
visited.merge( node );
|
||||
unvisited.unmerge( node );
|
||||
component.merge( node );
|
||||
};
|
||||
|
||||
if( unvisited.empty() ){ return self.spawn(); }
|
||||
|
||||
do { // each iteration yields a component
|
||||
let cmpt = cy.collection();
|
||||
components.push( cmpt );
|
||||
|
||||
let root = unvisited[0];
|
||||
visitInComponent( root, cmpt );
|
||||
|
||||
self.bfs({
|
||||
directed: false,
|
||||
roots: root,
|
||||
visit: v => visitInComponent( v, cmpt )
|
||||
} );
|
||||
|
||||
cmpt.forEach(node => {
|
||||
node.connectedEdges().forEach(e => { // connectedEdges() usually cached
|
||||
if( self.has(e) && cmpt.has(e.source()) && cmpt.has(e.target()) ){ // has() is cheap
|
||||
cmpt.merge(e); // forEach() only considers nodes -- sets N at call time
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} while( unvisited.length > 0 );
|
||||
|
||||
return components;
|
||||
},
|
||||
|
||||
component: function(){
|
||||
let ele = this[0];
|
||||
|
||||
return ele.cy().mutableElements().components( ele )[0];
|
||||
}
|
||||
} );
|
||||
|
||||
elesfn.componentsOf = elesfn.components;
|
||||
|
||||
export default elesfn;
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Elements are drawn in a specific order based on compound depth (low to high), the element type (nodes above edges),
|
||||
* and z-index (low to high). These styles affect how this applies:
|
||||
*
|
||||
* z-compound-depth: May be `bottom | orphan | auto | top`. The first drawn is `bottom`, then `orphan` which is the
|
||||
* same depth as the root of the compound graph, followed by the default value `auto` which draws in order from
|
||||
* root to leaves of the compound graph. The last drawn is `top`.
|
||||
* z-index-compare: May be `auto | manual`. The default value is `auto` which always draws edges under nodes.
|
||||
* `manual` ignores this convention and draws based on the `z-index` value setting.
|
||||
* z-index: An integer value that affects the relative draw order of elements. In general, an element with a higher
|
||||
* `z-index` will be drawn on top of an element with a lower `z-index`.
|
||||
*/
|
||||
import * as util from '../util/index.mjs';
|
||||
|
||||
let zIndexSort = function( a, b ){
|
||||
let cy = a.cy();
|
||||
let hasCompoundNodes = cy.hasCompoundNodes();
|
||||
|
||||
function getDepth(ele){
|
||||
let style = ele.pstyle( 'z-compound-depth' );
|
||||
if ( style.value === 'auto' ){
|
||||
return hasCompoundNodes ? ele.zDepth() : 0;
|
||||
} else if ( style.value === 'bottom' ){
|
||||
return -1;
|
||||
} else if ( style.value === 'top' ){
|
||||
return util.MAX_INT;
|
||||
}
|
||||
// 'orphan'
|
||||
return 0;
|
||||
}
|
||||
let depthDiff = getDepth(a) - getDepth(b);
|
||||
if ( depthDiff !== 0 ){
|
||||
return depthDiff;
|
||||
}
|
||||
|
||||
function getEleDepth(ele){
|
||||
let style = ele.pstyle( 'z-index-compare' );
|
||||
if ( style.value === 'auto' ){
|
||||
return ele.isNode() ? 1 : 0;
|
||||
}
|
||||
// 'manual'
|
||||
return 0;
|
||||
}
|
||||
let eleDiff = getEleDepth(a) - getEleDepth(b);
|
||||
if ( eleDiff !== 0 ){
|
||||
return eleDiff;
|
||||
}
|
||||
|
||||
let zDiff = a.pstyle( 'z-index' ).value - b.pstyle( 'z-index' ).value;
|
||||
if ( zDiff !== 0 ){
|
||||
return zDiff;
|
||||
}
|
||||
// compare indices in the core (order added to graph w/ last on top)
|
||||
return a.poolIndex() - b.poolIndex();
|
||||
};
|
||||
|
||||
export default zIndexSort;
|
||||
Reference in New Issue
Block a user