First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+165
View File
@@ -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;
@@ -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 };
@@ -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;
@@ -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
View File
@@ -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;
@@ -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;
@@ -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 );
}
}
@@ -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
View File
@@ -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;
@@ -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;
@@ -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
View File
@@ -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;
@@ -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
View File
@@ -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;
@@ -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
};
@@ -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
View File
@@ -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;
@@ -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
View File
@@ -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;
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
File diff suppressed because it is too large Load Diff
@@ -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
View File
@@ -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
View File
@@ -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;
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;