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
+260
View File
@@ -0,0 +1,260 @@
import * as util from './util/index.mjs';
import * as is from './is.mjs';
import Promise from './promise.mjs';
let Animation = function( target, opts, opts2 ){
let isCore = is.core(target);
let isEle = !isCore;
let _p = this._private = util.extend( {
duration: 1000
}, opts, opts2 );
_p.target = target;
_p.style = _p.style || _p.css;
_p.started = false;
_p.playing = false;
_p.hooked = false;
_p.applying = false;
_p.progress = 0;
_p.completes = [];
_p.frames = [];
if( _p.complete && is.fn( _p.complete ) ){
_p.completes.push( _p.complete );
}
if( isEle ){
let pos = target.position();
_p.startPosition = _p.startPosition || {
x: pos.x,
y: pos.y
};
_p.startStyle = _p.startStyle || target.cy().style().getAnimationStartStyle( target, _p.style );
}
if( isCore ){
let pan = target.pan();
_p.startPan = {
x: pan.x,
y: pan.y
};
_p.startZoom = target.zoom();
}
// for future timeline/animations impl
this.length = 1;
this[0] = this;
};
let anifn = Animation.prototype;
util.extend( anifn, {
instanceString: function(){ return 'animation'; },
hook: function(){
let _p = this._private;
if( !_p.hooked ){
// add to target's animation queue
let q;
let tAni = _p.target._private.animation;
if( _p.queue ){
q = tAni.queue;
} else {
q = tAni.current;
}
q.push( this );
// add to the animation loop pool
if( is.elementOrCollection( _p.target ) ){
_p.target.cy().addToAnimationPool( _p.target );
}
_p.hooked = true;
}
return this;
},
play: function(){
let _p = this._private;
// autorewind
if( _p.progress === 1 ){
_p.progress = 0;
}
_p.playing = true;
_p.started = false; // needs to be started by animation loop
_p.stopped = false;
this.hook();
// the animation loop will start the animation...
return this;
},
playing: function(){
return this._private.playing;
},
apply: function(){
let _p = this._private;
_p.applying = true;
_p.started = false; // needs to be started by animation loop
_p.stopped = false;
this.hook();
// the animation loop will apply the animation at this progress
return this;
},
applying: function(){
return this._private.applying;
},
pause: function(){
let _p = this._private;
_p.playing = false;
_p.started = false;
return this;
},
stop: function(){
let _p = this._private;
_p.playing = false;
_p.started = false;
_p.stopped = true; // to be removed from animation queues
return this;
},
rewind: function(){
return this.progress( 0 );
},
fastforward: function(){
return this.progress( 1 );
},
time: function( t ){
let _p = this._private;
if( t === undefined ){
return _p.progress * _p.duration;
} else {
return this.progress( t / _p.duration );
}
},
progress: function( p ){
let _p = this._private;
let wasPlaying = _p.playing;
if( p === undefined ){
return _p.progress;
} else {
if( wasPlaying ){
this.pause();
}
_p.progress = p;
_p.started = false;
if( wasPlaying ){
this.play();
}
}
return this;
},
completed: function(){
return this._private.progress === 1;
},
reverse: function(){
let _p = this._private;
let wasPlaying = _p.playing;
if( wasPlaying ){
this.pause();
}
_p.progress = 1 - _p.progress;
_p.started = false;
let swap = function( a, b ){
let _pa = _p[ a ];
if( _pa == null ){ return; }
_p[ a ] = _p[ b ];
_p[ b ] = _pa;
};
swap( 'zoom', 'startZoom' );
swap( 'pan', 'startPan' );
swap( 'position', 'startPosition' );
// swap styles
if( _p.style ){
for( let i = 0; i < _p.style.length; i++ ){
let prop = _p.style[ i ];
let name = prop.name;
let startStyleProp = _p.startStyle[ name ];
_p.startStyle[ name ] = prop;
_p.style[ i ] = startStyleProp;
}
}
if( wasPlaying ){
this.play();
}
return this;
},
promise: function( type ){
let _p = this._private;
let arr;
switch( type ){
case 'frame':
arr = _p.frames;
break;
default:
case 'complete':
case 'completed':
arr = _p.completes;
}
return new Promise( function( resolve, reject ){
arr.push( function(){
resolve();
} );
} );
}
} );
anifn.complete = anifn.completed;
anifn.run = anifn.play;
anifn.running = anifn.playing;
export default Animation;
+4
View File
@@ -0,0 +1,4 @@
// an entrypoint to use the raw source in cjs projects
// e.g. require('cytoscape/src/cjs') or setting an alias in your build tool of 'cytoscape':'cytoscape/src/cjs'
module.exports = require('./index.js').default;
+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;
+82
View File
@@ -0,0 +1,82 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
import Collection from '../collection/index.mjs';
import Element from '../collection/element.mjs';
let corefn = {
add: function( opts ){
let elements;
let cy = this;
// add the elements
if( is.elementOrCollection( opts ) ){
let eles = opts;
if( eles._private.cy === cy ){ // same instance => just restore
elements = eles.restore();
} else { // otherwise, copy from json
let jsons = [];
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
jsons.push( ele.json() );
}
elements = new Collection( cy, jsons );
}
}
// specify an array of options
else if( is.array( opts ) ){
let jsons = opts;
elements = new Collection( cy, jsons );
}
// specify via opts.nodes and opts.edges
else if( is.plainObject( opts ) && (is.array( opts.nodes ) || is.array( opts.edges )) ){
let elesByGroup = opts;
let jsons = [];
let grs = [ 'nodes', 'edges' ];
for( let i = 0, il = grs.length; i < il; i++ ){
let group = grs[ i ];
let elesArray = elesByGroup[ group ];
if( is.array( elesArray ) ){
for( let j = 0, jl = elesArray.length; j < jl; j++ ){
let json = util.extend( { group: group }, elesArray[ j ] );
jsons.push( json );
}
}
}
elements = new Collection( cy, jsons );
}
// specify options for one element
else {
let json = opts;
elements = (new Element( cy, json )).collection();
}
return elements;
},
remove: function( collection ){
if( is.elementOrCollection( collection ) ){
// already have right ref
} else if( is.string( collection ) ){
let selector = collection;
collection = this.$( selector );
}
return collection.remove();
}
};
export default corefn;
+158
View File
@@ -0,0 +1,158 @@
/* global Float32Array */
/*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License */
function generateCubicBezier(mX1, mY1, mX2, mY2) {
let NEWTON_ITERATIONS = 4,
NEWTON_MIN_SLOPE = 0.001,
SUBDIVISION_PRECISION = 0.0000001,
SUBDIVISION_MAX_ITERATIONS = 10,
kSplineTableSize = 11,
kSampleStepSize = 1.0 / (kSplineTableSize - 1.0),
float32ArraySupported = typeof Float32Array !== 'undefined';
/* Must contain four arguments. */
if (arguments.length !== 4) {
return false;
}
/* Arguments must be numbers. */
for (let i = 0; i < 4; ++i) {
if (typeof arguments[i] !== "number" || isNaN(arguments[i]) || !isFinite(arguments[i])) {
return false;
}
}
/* X values must be in the [0, 1] range. */
mX1 = Math.min(mX1, 1);
mX2 = Math.min(mX2, 1);
mX1 = Math.max(mX1, 0);
mX2 = Math.max(mX2, 0);
let mSampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize);
function A(aA1, aA2) {
return 1.0 - 3.0 * aA2 + 3.0 * aA1;
}
function B(aA1, aA2) {
return 3.0 * aA2 - 6.0 * aA1;
}
function C(aA1) {
return 3.0 * aA1;
}
function calcBezier(aT, aA1, aA2) {
return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
}
function getSlope(aT, aA1, aA2) {
return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
}
function newtonRaphsonIterate(aX, aGuessT) {
for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
let currentSlope = getSlope(aGuessT, mX1, mX2);
if (currentSlope === 0.0) {
return aGuessT;
}
let currentX = calcBezier(aGuessT, mX1, mX2) - aX;
aGuessT -= currentX / currentSlope;
}
return aGuessT;
}
function calcSampleValues() {
for (let i = 0; i < kSplineTableSize; ++i) {
mSampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2);
}
}
function binarySubdivide(aX, aA, aB) {
let currentX, currentT, i = 0;
do {
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) {
aB = currentT;
} else {
aA = currentT;
}
} while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
return currentT;
}
function getTForX(aX) {
let intervalStart = 0.0,
currentSample = 1,
lastSample = kSplineTableSize - 1;
for (; currentSample !== lastSample && mSampleValues[currentSample] <= aX; ++currentSample) {
intervalStart += kSampleStepSize;
}
--currentSample;
let dist = (aX - mSampleValues[currentSample]) / (mSampleValues[currentSample + 1] - mSampleValues[currentSample]),
guessForT = intervalStart + dist * kSampleStepSize,
initialSlope = getSlope(guessForT, mX1, mX2);
if (initialSlope >= NEWTON_MIN_SLOPE) {
return newtonRaphsonIterate(aX, guessForT);
} else if (initialSlope === 0.0) {
return guessForT;
} else {
return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize);
}
}
let _precomputed = false;
function precompute() {
_precomputed = true;
if (mX1 !== mY1 || mX2 !== mY2) {
calcSampleValues();
}
}
let f = function(aX) {
if (!_precomputed) {
precompute();
}
if (mX1 === mY1 && mX2 === mY2) {
return aX;
}
if (aX === 0) {
return 0;
}
if (aX === 1) {
return 1;
}
return calcBezier(getTForX(aX), mY1, mY2);
};
f.getControlPoints = function() {
return [{
x: mX1,
y: mY1
}, {
x: mX2,
y: mY2
}];
};
let str = "generateBezier(" + [mX1, mY1, mX2, mY2] + ")";
f.toString = function() {
return str;
};
return f;
}
export default generateCubicBezier;
+82
View File
@@ -0,0 +1,82 @@
import * as is from '../../is.mjs';
function getEasedValue( type, start, end, percent, easingFn ){
if( percent === 1 ){
return end;
}
if( start === end ){
return end;
}
let val = easingFn( start, end, percent );
if( type == null ){
return val;
}
if( type.roundValue || type.color ){
val = Math.round( val );
}
if( type.min !== undefined ){
val = Math.max( val, type.min );
}
if( type.max !== undefined ){
val = Math.min( val, type.max );
}
return val;
}
function getValue( prop, spec ){
if( prop.pfValue != null || prop.value != null ){
if( prop.pfValue != null && (spec == null || spec.type.units !== '%') ){
return prop.pfValue;
} else {
return prop.value;
}
} else {
return prop;
}
}
function ease( startProp, endProp, percent, easingFn, propSpec ){
let type = propSpec != null ? propSpec.type : null;
if( percent < 0 ){
percent = 0;
} else if( percent > 1 ){
percent = 1;
}
let start = getValue( startProp, propSpec );
let end = getValue( endProp, propSpec );
if( is.number( start ) && is.number( end ) ){
return getEasedValue( type, start, end, percent, easingFn );
} else if( is.array( start ) && is.array( end ) ){
let easedArr = [];
for( let i = 0; i < end.length; i++ ){
let si = start[ i ];
let ei = end[ i ];
if( si != null && ei != null ){
let val = getEasedValue( type, si, ei, percent, easingFn );
easedArr.push( val );
} else {
easedArr.push( ei );
}
}
return easedArr;
}
return undefined;
}
export default ease;
+76
View File
@@ -0,0 +1,76 @@
import generateCubicBezier from './cubic-bezier.mjs';
import generateSpringRK4 from './spring.mjs';
let cubicBezier = function( t1, p1, t2, p2 ){
let bezier = generateCubicBezier( t1, p1, t2, p2 );
return function( start, end, percent ){
return start + ( end - start ) * bezier( percent );
};
};
let easings = {
'linear': function( start, end, percent ){
return start + (end - start) * percent;
},
// default easings
'ease': cubicBezier( 0.25, 0.1, 0.25, 1 ),
'ease-in': cubicBezier( 0.42, 0, 1, 1 ),
'ease-out': cubicBezier( 0, 0, 0.58, 1 ),
'ease-in-out': cubicBezier( 0.42, 0, 0.58, 1 ),
// sine
'ease-in-sine': cubicBezier( 0.47, 0, 0.745, 0.715 ),
'ease-out-sine': cubicBezier( 0.39, 0.575, 0.565, 1 ),
'ease-in-out-sine': cubicBezier( 0.445, 0.05, 0.55, 0.95 ),
// quad
'ease-in-quad': cubicBezier( 0.55, 0.085, 0.68, 0.53 ),
'ease-out-quad': cubicBezier( 0.25, 0.46, 0.45, 0.94 ),
'ease-in-out-quad': cubicBezier( 0.455, 0.03, 0.515, 0.955 ),
// cubic
'ease-in-cubic': cubicBezier( 0.55, 0.055, 0.675, 0.19 ),
'ease-out-cubic': cubicBezier( 0.215, 0.61, 0.355, 1 ),
'ease-in-out-cubic': cubicBezier( 0.645, 0.045, 0.355, 1 ),
// quart
'ease-in-quart': cubicBezier( 0.895, 0.03, 0.685, 0.22 ),
'ease-out-quart': cubicBezier( 0.165, 0.84, 0.44, 1 ),
'ease-in-out-quart': cubicBezier( 0.77, 0, 0.175, 1 ),
// quint
'ease-in-quint': cubicBezier( 0.755, 0.05, 0.855, 0.06 ),
'ease-out-quint': cubicBezier( 0.23, 1, 0.32, 1 ),
'ease-in-out-quint': cubicBezier( 0.86, 0, 0.07, 1 ),
// expo
'ease-in-expo': cubicBezier( 0.95, 0.05, 0.795, 0.035 ),
'ease-out-expo': cubicBezier( 0.19, 1, 0.22, 1 ),
'ease-in-out-expo': cubicBezier( 1, 0, 0, 1 ),
// circ
'ease-in-circ': cubicBezier( 0.6, 0.04, 0.98, 0.335 ),
'ease-out-circ': cubicBezier( 0.075, 0.82, 0.165, 1 ),
'ease-in-out-circ': cubicBezier( 0.785, 0.135, 0.15, 0.86 ),
// user param easings...
'spring': function( tension, friction, duration ){
if( duration === 0 ){ // can't get a spring w/ duration 0
return easings.linear; // duration 0 => jump to end so impl doesn't matter
}
let spring = generateSpringRK4( tension, friction, duration );
return function( start, end, percent ){
return start + (end - start) * spring( percent );
};
},
'cubic-bezier': cubicBezier
};
export default easings;
+60
View File
@@ -0,0 +1,60 @@
import define from '../../define/index.mjs';
import * as util from '../../util/index.mjs';
import stepAll from './step-all.mjs';
let corefn = ({
// pull in animation functions
animate: define.animate(),
animation: define.animation(),
animated: define.animated(),
clearQueue: define.clearQueue(),
delay: define.delay(),
delayAnimation: define.delayAnimation(),
stop: define.stop(),
addToAnimationPool: function( eles ){
let cy = this;
if( !cy.styleEnabled() ){ return; } // save cycles when no style used
cy._private.aniEles.merge( eles );
},
stopAnimationLoop: function(){
this._private.animationsRunning = false;
},
startAnimationLoop: function(){
let cy = this;
cy._private.animationsRunning = true;
if( !cy.styleEnabled() ){ return; } // save cycles when no style used
// NB the animation loop will exec in headless environments if style enabled
// and explicit cy.destroy() is necessary to stop the loop
function headlessStep(){
if( !cy._private.animationsRunning ){ return; }
util.requestAnimationFrame( function animationStep( now ){
stepAll( now, cy );
headlessStep();
} );
}
let renderer = cy.renderer();
if( renderer && renderer.beforeRender ){ // let the renderer schedule animations
renderer.beforeRender( function rendererAnimationStep( willDraw, now ){
stepAll( now, cy );
}, renderer.beforeRenderPriorities.animations );
} else { // manage the animation loop ourselves
headlessStep(); // first call
}
}
});
export default corefn;
+88
View File
@@ -0,0 +1,88 @@
/*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License */
/* Given a tension, friction, and duration, a simulation at 60FPS will first run without a defined duration in order to calculate the full path. A second pass
then adjusts the time delta -- using the relation between actual time and duration -- to calculate the path for the duration-constrained animation. */
let generateSpringRK4 = (function(){
function springAccelerationForState( state ){
return (-state.tension * state.x) - (state.friction * state.v);
}
function springEvaluateStateWithDerivative( initialState, dt, derivative ){
let state = {
x: initialState.x + derivative.dx * dt,
v: initialState.v + derivative.dv * dt,
tension: initialState.tension,
friction: initialState.friction
};
return { dx: state.v, dv: springAccelerationForState( state ) };
}
function springIntegrateState( state, dt ){
let a = {
dx: state.v,
dv: springAccelerationForState( state )
},
b = springEvaluateStateWithDerivative( state, dt * 0.5, a ),
c = springEvaluateStateWithDerivative( state, dt * 0.5, b ),
d = springEvaluateStateWithDerivative( state, dt, c ),
dxdt = 1.0 / 6.0 * (a.dx + 2.0 * (b.dx + c.dx) + d.dx),
dvdt = 1.0 / 6.0 * (a.dv + 2.0 * (b.dv + c.dv) + d.dv);
state.x = state.x + dxdt * dt;
state.v = state.v + dvdt * dt;
return state;
}
return function springRK4Factory( tension, friction, duration ){
let initState = {
x: -1,
v: 0,
tension: null,
friction: null
},
path = [0],
time_lapsed = 0,
tolerance = 1 / 10000,
DT = 16 / 1000,
have_duration, dt, last_state;
tension = parseFloat( tension ) || 500;
friction = parseFloat( friction ) || 20;
duration = duration || null;
initState.tension = tension;
initState.friction = friction;
have_duration = duration !== null;
/* Calculate the actual time it takes for this animation to complete with the provided conditions. */
if( have_duration ){
/* Run the simulation without a duration. */
time_lapsed = springRK4Factory( tension, friction );
/* Compute the adjusted time delta. */
dt = time_lapsed / duration * DT;
} else {
dt = DT;
}
for(;;){
/* Next/step function .*/
last_state = springIntegrateState( last_state || initState, dt );
/* Store the position. */
path.push( 1 + last_state.x );
time_lapsed += 16;
/* If the change threshold is reached, break. */
if( !(Math.abs( last_state.x ) > tolerance && Math.abs( last_state.v ) > tolerance) ){
break;
}
}
/* If duration is not defined, return the actual time required for completing this animation. Otherwise, return a closure that holds the
computed path and returns a snapshot of the position according to a given percentComplete. */
return !have_duration ? time_lapsed : function( percentComplete ){ return path[ (percentComplete * (path.length - 1)) | 0 ]; };
};
}());
export default generateSpringRK4;
+8
View File
@@ -0,0 +1,8 @@
function startAnimation( self, ani, now, isCore ){
let ani_p = ani._private;
ani_p.started = true;
ani_p.startTime = now - ani_p.progress * ani_p.duration;
}
export default startAnimation;
+120
View File
@@ -0,0 +1,120 @@
import step from './step.mjs';
import startAnimation from './start.mjs';
function stepAll( now, cy ){
let eles = cy._private.aniEles;
let doneEles = [];
function stepOne( ele, isCore ){
let _p = ele._private;
let current = _p.animation.current;
let queue = _p.animation.queue;
let ranAnis = false;
// if nothing currently animating, get something from the queue
if( current.length === 0 ){
let next = queue.shift();
if( next ){
current.push( next );
}
}
let callbacks = function( callbacks ){
for( let j = callbacks.length - 1; j >= 0; j-- ){
let cb = callbacks[ j ];
cb();
}
callbacks.splice( 0, callbacks.length );
};
// step and remove if done
for( let i = current.length - 1; i >= 0; i-- ){
let ani = current[ i ];
let ani_p = ani._private;
if( ani_p.stopped ){
current.splice( i, 1 );
ani_p.hooked = false;
ani_p.playing = false;
ani_p.started = false;
callbacks( ani_p.frames );
continue;
}
if( !ani_p.playing && !ani_p.applying ){ continue; }
// an apply() while playing shouldn't do anything
if( ani_p.playing && ani_p.applying ){
ani_p.applying = false;
}
if( !ani_p.started ){
startAnimation( ele, ani, now, isCore );
}
step( ele, ani, now, isCore );
if( ani_p.applying ){
ani_p.applying = false;
}
callbacks( ani_p.frames );
if( ani_p.step != null ){
ani_p.step(now);
}
if( ani.completed() ){
current.splice( i, 1 );
ani_p.hooked = false;
ani_p.playing = false;
ani_p.started = false;
callbacks( ani_p.completes );
}
ranAnis = true;
}
if( !isCore && current.length === 0 && queue.length === 0 ){
doneEles.push( ele );
}
return ranAnis;
} // stepElement
// handle all eles
let ranEleAni = false;
for( let e = 0; e < eles.length; e++ ){
let ele = eles[ e ];
let handledThisEle = stepOne( ele );
ranEleAni = ranEleAni || handledThisEle;
} // each element
let ranCoreAni = stepOne( cy, true );
// notify renderer
if( ranEleAni || ranCoreAni ){
if( eles.length > 0 ){
cy.notify('draw', eles);
} else {
cy.notify('draw');
}
}
// remove elements from list of currently animating if its queues are empty
eles.unmerge( doneEles );
cy.emit('step');
} // stepAll
export default stepAll;
+162
View File
@@ -0,0 +1,162 @@
import easings from './easings.mjs';
import ease from './ease.mjs';
import * as is from '../../is.mjs';
import {bound} from '../../math.mjs';
function step( self, ani, now, isCore ){
let isEles = !isCore;
let _p = self._private;
let ani_p = ani._private;
let pEasing = ani_p.easing;
let startTime = ani_p.startTime;
let cy = isCore ? self : self.cy();
let style = cy.style();
if( !ani_p.easingImpl ){
if( pEasing == null ){ // use default
ani_p.easingImpl = easings[ 'linear' ];
} else { // then define w/ name
let easingVals;
if( is.string( pEasing ) ){
let easingProp = style.parse( 'transition-timing-function', pEasing );
easingVals = easingProp.value;
} else { // then assume preparsed array
easingVals = pEasing;
}
let name, args;
if( is.string( easingVals ) ){
name = easingVals;
args = [];
} else {
name = easingVals[1];
args = easingVals.slice( 2 ).map( function( n ){ return +n; } );
}
if( args.length > 0 ){ // create with args
if( name === 'spring' ){
args.push( ani_p.duration ); // need duration to generate spring
}
ani_p.easingImpl = easings[ name ].apply( null, args );
} else { // static impl by name
ani_p.easingImpl = easings[ name ];
}
}
}
let easing = ani_p.easingImpl;
let percent;
if( ani_p.duration === 0 ){
percent = 1;
} else {
percent = (now - startTime) / ani_p.duration;
}
if( ani_p.applying ){
percent = ani_p.progress;
}
if( percent < 0 ){
percent = 0;
} else if( percent > 1 ){
percent = 1;
}
if( ani_p.delay == null ){ // then update
let startPos = ani_p.startPosition;
let endPos = ani_p.position;
if( endPos && isEles && !self.locked() ){
let newPos = {};
if( valid( startPos.x, endPos.x ) ){
newPos.x = ease( startPos.x, endPos.x, percent, easing );
}
if( valid( startPos.y, endPos.y ) ){
newPos.y = ease( startPos.y, endPos.y, percent, easing );
}
self.position( newPos );
}
let startPan = ani_p.startPan;
let endPan = ani_p.pan;
let pan = _p.pan;
let animatingPan = endPan != null && isCore;
if( animatingPan ){
if( valid( startPan.x, endPan.x ) ){
pan.x = ease( startPan.x, endPan.x, percent, easing );
}
if( valid( startPan.y, endPan.y ) ){
pan.y = ease( startPan.y, endPan.y, percent, easing );
}
self.emit( 'pan' );
}
let startZoom = ani_p.startZoom;
let endZoom = ani_p.zoom;
let animatingZoom = endZoom != null && isCore;
if( animatingZoom ){
if( valid( startZoom, endZoom ) ){
_p.zoom = bound( _p.minZoom, ease( startZoom, endZoom, percent, easing ), _p.maxZoom );
}
self.emit( 'zoom' );
}
if( animatingPan || animatingZoom ){
self.emit( 'viewport' );
}
let props = ani_p.style;
if( props && props.length > 0 && isEles ){
for( let i = 0; i < props.length; i++ ){
let prop = props[ i ];
let name = prop.name;
let end = prop;
let start = ani_p.startStyle[ name ];
let propSpec = style.properties[ start.name ];
let easedVal = ease( start, end, percent, easing, propSpec );
style.overrideBypass( self, name, easedVal );
} // for props
self.emit('style');
} // if
}
ani_p.progress = percent;
return percent;
}
function valid( start, end ){
if( start == null || end == null ){
return false;
}
if( is.number( start ) && is.number( end ) ){
return true;
} else if( (start) && (end) ){
return true;
}
return false;
}
export default step;
+49
View File
@@ -0,0 +1,49 @@
import define from '../define/index.mjs';
const fn = {
data: define.data( {
field: 'data',
bindingEvent: 'data',
allowBinding: true,
allowSetting: true,
settingEvent: 'data',
settingTriggersEvent: true,
triggerFnName: 'trigger',
allowGetting: true,
updateStyle: true
} ),
removeData: define.removeData( {
field: 'data',
event: 'data',
triggerFnName: 'trigger',
triggerEvent: 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
} )
};
// aliases
fn.attr = fn.data;
fn.removeAttr = fn.removeData;
export default fn;
+102
View File
@@ -0,0 +1,102 @@
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( cy, listener, eventObj ){
let selector = listener.qualifier;
if( selector != null ){
return cy !== eventObj.target && is.element( eventObj.target ) && selector.matches( eventObj.target );
}
return true;
},
addEventFields: function( cy, evt ){
evt.cy = cy;
evt.target = cy;
},
callbackContext: function( cy, listener, eventObj ){
return listener.qualifier != null ? eventObj.target : cy;
}
};
let argSelector = function( arg ){
if( is.string(arg) ){
return new Selector( arg );
} else {
return arg;
}
};
let elesfn = ({
createEmitter: function(){
let _p = this._private;
if( !_p.emitter ){
_p.emitter = new Emitter( emitterOptions, this );
}
return this;
},
emitter: function(){
return this._private.emitter;
},
on: function( events, selector, callback ){
this.emitter().on( events, argSelector(selector), callback );
return this;
},
removeListener: function( events, selector, callback ){
this.emitter().removeListener( events, argSelector(selector), callback );
return this;
},
removeAllListeners: function(){
this.emitter().removeAllListeners();
return this;
},
one: function( events, selector, callback ){
this.emitter().one( events, argSelector(selector), callback );
return this;
},
once: function( events, selector, callback ){
this.emitter().one( events, argSelector(selector), callback );
return this;
},
emit: function( events, extraParams ){
this.emitter().emit( events, extraParams );
return this;
},
emitAndNotify: function( event, eles ){
this.emit( event );
this.notify( event, eles );
return this;
}
});
define.eventAliasesOn( elesfn );
export default elesfn;
+23
View File
@@ -0,0 +1,23 @@
let corefn = ({
png: function( options ){
let renderer = this._private.renderer;
options = options || {};
return renderer.png( options );
},
jpg: function( options ){
let renderer = this._private.renderer;
options = options || {};
options.bg = options.bg || '#fff';
return renderer.jpg( options );
}
});
corefn.jpeg = corefn.jpg;
export default corefn;
+522
View File
@@ -0,0 +1,522 @@
import window from '../window.mjs';
import * as util from '../util/index.mjs';
import Collection from '../collection/index.mjs';
import * as is from '../is.mjs';
import Promise from '../promise.mjs';
import addRemove from './add-remove.mjs';
import animation from './animation/index.mjs';
import events from './events.mjs';
import exportFormat from './export.mjs';
import layout from './layout.mjs';
import notification from './notification.mjs';
import renderer from './renderer.mjs';
import search from './search.mjs';
import style from './style.mjs';
import viewport from './viewport.mjs';
import data from './data.mjs';
let Core = function( opts ){
let cy = this;
opts = util.extend( {}, opts );
let container = opts.container;
// allow for passing a wrapped jquery object
// e.g. cytoscape({ container: $('#cy') })
if( container && !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
container = container[0];
}
let reg = container ? container._cyreg : null; // e.g. already registered some info (e.g. readies) via jquery
reg = reg || {};
if( reg && reg.cy ){
reg.cy.destroy();
reg = {}; // old instance => replace reg completely
}
let readies = reg.readies = reg.readies || [];
if( container ){ container._cyreg = reg; } // make sure container assoc'd reg points to this cy
reg.cy = cy;
let head = window !== undefined && container !== undefined && !opts.headless;
let options = opts;
options.layout = util.extend( { name: head ? 'grid' : 'null' }, options.layout );
options.renderer = util.extend( { name: head ? 'canvas' : 'null' }, options.renderer );
let defVal = function( def, val, altVal ){
if( val !== undefined ){
return val;
} else if( altVal !== undefined ){
return altVal;
} else {
return def;
}
};
let _p = this._private = {
container: container, // html dom ele container
ready: false, // whether ready has been triggered
options: options, // cached options
elements: new Collection( this ), // elements in the graph
listeners: [], // list of listeners
aniEles: new Collection( this ), // elements being animated
data: options.data || {}, // data for the core
scratch: {}, // scratch object for core
layout: null,
renderer: null,
destroyed: false, // whether destroy was called
notificationsEnabled: true, // whether notifications are sent to the renderer
minZoom: 1e-50,
maxZoom: 1e50,
zoomingEnabled: defVal( true, options.zoomingEnabled ),
userZoomingEnabled: defVal( true, options.userZoomingEnabled ),
panningEnabled: defVal( true, options.panningEnabled ),
userPanningEnabled: defVal( true, options.userPanningEnabled ),
boxSelectionEnabled: defVal( true, options.boxSelectionEnabled ),
autolock: defVal( false, options.autolock, options.autolockNodes ),
autoungrabify: defVal( false, options.autoungrabify, options.autoungrabifyNodes ),
autounselectify: defVal( false, options.autounselectify ),
styleEnabled: options.styleEnabled === undefined ? head : options.styleEnabled,
zoom: is.number( options.zoom ) ? options.zoom : 1,
pan: {
x: is.plainObject( options.pan ) && is.number( options.pan.x ) ? options.pan.x : 0,
y: is.plainObject( options.pan ) && is.number( options.pan.y ) ? options.pan.y : 0
},
animation: { // object for currently-running animations
current: [],
queue: []
},
hasCompoundNodes: false,
multiClickDebounceTime: defVal(250, options.multiClickDebounceTime)
};
this.createEmitter();
// set selection type
this.selectionType( options.selectionType );
// init zoom bounds
this.zoomRange({ min: options.minZoom, max: options.maxZoom });
let loadExtData = function( extData, next ){
let anyIsPromise = extData.some( is.promise );
if( anyIsPromise ){
return Promise.all( extData ).then( next ); // load all data asynchronously, then exec rest of init
} else {
next( extData ); // exec synchronously for convenience
}
};
// start with the default stylesheet so we have something before loading an external stylesheet
if( _p.styleEnabled ){
cy.setStyle([]);
}
// create the renderer
let rendererOptions = util.assign({}, options, options.renderer); // allow rendering hints in top level options
cy.initRenderer( rendererOptions );
let setElesAndLayout = function( elements, onload, ondone ){
cy.notifications( false );
// remove old elements
let oldEles = cy.mutableElements();
if( oldEles.length > 0 ){
oldEles.remove();
}
if( elements != null ){
if( is.plainObject( elements ) || is.array( elements ) ){
cy.add( elements );
}
}
cy.one( 'layoutready', function( e ){
cy.notifications( true );
cy.emit( e ); // we missed this event by turning notifications off, so pass it on
cy.one( 'load', onload );
cy.emitAndNotify( 'load' );
} ).one( 'layoutstop', function(){
cy.one( 'done', ondone );
cy.emit( 'done' );
} );
let layoutOpts = util.extend( {}, cy._private.options.layout );
layoutOpts.eles = cy.elements();
cy.layout( layoutOpts ).run();
};
loadExtData([ options.style, options.elements ], function( thens ){
let initStyle = thens[0];
let initEles = thens[1];
// init style
if( _p.styleEnabled ){
cy.style().append( initStyle );
}
// initial load
setElesAndLayout( initEles, function(){ // onready
cy.startAnimationLoop();
_p.ready = true;
// if a ready callback is specified as an option, the bind it
if( is.fn( options.ready ) ){
cy.on( 'ready', options.ready );
}
// bind all the ready handlers registered before creating this instance
for( let i = 0; i < readies.length; i++ ){
let fn = readies[ i ];
cy.on( 'ready', fn );
}
if( reg ){ reg.readies = []; } // clear b/c we've bound them all and don't want to keep it around in case a new core uses the same div etc
cy.emit( 'ready' );
}, options.done );
} );
};
let corefn = Core.prototype; // short alias
util.extend( corefn, {
instanceString: function(){
return 'core';
},
isReady: function(){
return this._private.ready;
},
destroyed: function(){
return this._private.destroyed;
},
ready: function( fn ){
if( this.isReady() ){
this.emitter().emit( 'ready', [], fn ); // just calls fn as though triggered via ready event
} else {
this.on( 'ready', fn );
}
return this;
},
destroy: function(){
let cy = this;
if( cy.destroyed() ) return;
cy.stopAnimationLoop();
cy.destroyRenderer();
this.emit( 'destroy' );
cy._private.destroyed = true;
return cy;
},
hasElementWithId: function( id ){
return this._private.elements.hasElementWithId( id );
},
getElementById: function( id ){
return this._private.elements.getElementById( id );
},
hasCompoundNodes: function(){
return this._private.hasCompoundNodes;
},
headless: function(){
return this._private.renderer.isHeadless();
},
styleEnabled: function(){
return this._private.styleEnabled;
},
addToPool: function( eles ){
this._private.elements.merge( eles );
return this; // chaining
},
removeFromPool: function( eles ){
this._private.elements.unmerge( eles );
return this;
},
container: function(){
return this._private.container || null;
},
window: function() {
let container = this._private.container;
if (container == null) return window;
let ownerDocument = this._private.container.ownerDocument;
if (ownerDocument === undefined || ownerDocument == null) {
return window;
}
return ownerDocument.defaultView || window;
},
mount: function( container ){
if( container == null ){ return; }
let cy = this;
let _p = cy._private;
let options = _p.options;
if( !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
container = container[0];
}
cy.stopAnimationLoop();
cy.destroyRenderer();
_p.container = container;
_p.styleEnabled = true;
cy.invalidateSize();
cy.initRenderer( util.assign({}, options, options.renderer, {
// allow custom renderer name to be re-used, otherwise use canvas
name: options.renderer.name === 'null' ? 'canvas' : options.renderer.name
}) );
cy.startAnimationLoop();
cy.style( options.style );
cy.emit( 'mount' );
return cy;
},
unmount: function(){
let cy = this;
cy.stopAnimationLoop();
cy.destroyRenderer();
cy.initRenderer( { name: 'null' } );
cy.emit( 'unmount' );
return cy;
},
options: function(){
return util.copy( this._private.options );
},
json: function( obj ){
let cy = this;
let _p = cy._private;
let eles = cy.mutableElements();
let getFreshRef = ele => cy.getElementById(ele.id());
if( is.plainObject( obj ) ){ // set
cy.startBatch();
if( obj.elements ){
let idInJson = {};
let updateEles = function( jsons, gr ){
let toAdd = [];
let toMod = [];
for( let i = 0; i < jsons.length; i++ ){
let json = jsons[ i ];
if( !json.data.id ){
util.warn( 'cy.json() cannot handle elements without an ID attribute' );
continue;
}
let id = '' + json.data.id; // id must be string
let ele = cy.getElementById( id );
idInJson[ id ] = true;
if( ele.length !== 0 ){ // existing element should be updated
toMod.push({ ele, json });
} else { // otherwise should be added
if( gr ){
json.group = gr;
toAdd.push( json );
} else {
toAdd.push( json );
}
}
}
cy.add( toAdd );
for( let i = 0; i < toMod.length; i++ ){
let { ele, json } = toMod[i];
ele.json(json);
}
};
if( is.array( obj.elements ) ){ // elements: []
updateEles( obj.elements );
} else { // elements: { nodes: [], edges: [] }
let grs = [ 'nodes', 'edges' ];
for( let i = 0; i < grs.length; i++ ){
let gr = grs[ i ];
let elements = obj.elements[ gr ];
if( is.array( elements ) ){
updateEles( elements, gr );
}
}
}
let parentsToRemove = cy.collection();
(eles
.filter(ele => !idInJson[ ele.id() ])
.forEach(ele => {
if ( ele.isParent() ) {
parentsToRemove.merge(ele);
} else {
ele.remove();
}
})
);
// so that children are not removed w/parent
parentsToRemove.forEach(ele => ele.children().move({ parent: null }));
// intermediate parents may be moved by prior line, so make sure we remove by fresh refs
parentsToRemove.forEach(ele => getFreshRef(ele).remove());
}
if( obj.style ){
cy.style( obj.style );
}
if( obj.zoom != null && obj.zoom !== _p.zoom ){
cy.zoom( obj.zoom );
}
if( obj.pan ){
if( obj.pan.x !== _p.pan.x || obj.pan.y !== _p.pan.y ){
cy.pan( obj.pan );
}
}
if( obj.data ){
cy.data( obj.data );
}
let fields = [
'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled',
'panningEnabled', 'userPanningEnabled',
'boxSelectionEnabled',
'autolock', 'autoungrabify', 'autounselectify',
'multiClickDebounceTime'
];
for( let i = 0; i < fields.length; i++ ){
let f = fields[ i ];
if( obj[ f ] != null ){
cy[ f ]( obj[ f ] );
}
}
cy.endBatch();
return this; // chaining
} else { // get
let flat = !!obj;
let json = {};
if( flat ){
json.elements = this.elements().map( ele => ele.json() );
} else {
json.elements = {};
eles.forEach( function( ele ){
let group = ele.group();
if( !json.elements[ group ] ){
json.elements[ group ] = [];
}
json.elements[ group ].push( ele.json() );
} );
}
if( this._private.styleEnabled ){
json.style = cy.style().json();
}
json.data = util.copy( cy.data() );
let options = _p.options;
json.zoomingEnabled = _p.zoomingEnabled;
json.userZoomingEnabled = _p.userZoomingEnabled;
json.zoom = _p.zoom;
json.minZoom = _p.minZoom;
json.maxZoom = _p.maxZoom;
json.panningEnabled = _p.panningEnabled;
json.userPanningEnabled = _p.userPanningEnabled;
json.pan = util.copy( _p.pan );
json.boxSelectionEnabled = _p.boxSelectionEnabled;
json.renderer = util.copy( options.renderer );
json.hideEdgesOnViewport = options.hideEdgesOnViewport;
json.textureOnViewport = options.textureOnViewport;
json.wheelSensitivity = options.wheelSensitivity;
json.motionBlur = options.motionBlur;
json.multiClickDebounceTime = options.multiClickDebounceTime;
return json;
}
}
} );
corefn.$id = corefn.getElementById;
[
addRemove,
animation,
events,
exportFormat,
layout,
notification,
renderer,
search,
style,
viewport,
data
].forEach( function( props ){
util.extend( corefn, props );
} );
export default Core;
+46
View File
@@ -0,0 +1,46 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
let corefn = ({
layout: function( options ){
let cy = this;
if( options == null ){
util.error( 'Layout options must be specified to make a layout' );
return;
}
if( options.name == null ){
util.error( 'A `name` must be specified to make a layout' );
return;
}
let name = options.name;
let Layout = cy.extension( 'layout', name );
if( Layout == null ){
util.error( 'No such layout `' + name + '` found. Did you forget to import it and `cytoscape.use()` it?' );
return;
}
let eles;
if( is.string( options.eles ) ){
eles = cy.$( options.eles );
} else {
eles = options.eles != null ? options.eles : cy.$();
}
let layout = new Layout( util.extend( {}, options, {
cy: cy,
eles: eles
} ) );
return layout;
}
});
corefn.createLayout = corefn.makeLayout = corefn.layout;
export default corefn;
+120
View File
@@ -0,0 +1,120 @@
let corefn = ({
notify: function( eventName, eventEles ){
let _p = this._private;
if( this.batching() ){
_p.batchNotifications = _p.batchNotifications || {};
let eles = _p.batchNotifications[ eventName ] = _p.batchNotifications[ eventName ] || this.collection();
if( eventEles != null ){
eles.merge( eventEles );
}
return; // notifications are disabled during batching
}
if( !_p.notificationsEnabled ){ return; } // exit on disabled
let renderer = this.renderer();
// exit if destroy() called on core or renderer in between frames #1499 #1528
if( this.destroyed() || !renderer ){ return; }
renderer.notify( eventName, eventEles );
},
notifications: function( bool ){
let p = this._private;
if( bool === undefined ){
return p.notificationsEnabled;
} else {
p.notificationsEnabled = bool ? true : false;
}
return this;
},
noNotifications: function( callback ){
this.notifications( false );
callback();
this.notifications( true );
},
batching: function(){
return this._private.batchCount > 0;
},
startBatch: function(){
let _p = this._private;
if( _p.batchCount == null ){
_p.batchCount = 0;
}
if( _p.batchCount === 0 ){
_p.batchStyleEles = this.collection();
_p.batchNotifications = {};
}
_p.batchCount++;
return this;
},
endBatch: function(){
let _p = this._private;
if( _p.batchCount === 0 ){ return this; }
_p.batchCount--;
if( _p.batchCount === 0 ){
// update style for dirty eles
_p.batchStyleEles.updateStyle();
let renderer = this.renderer();
// notify the renderer of queued eles and event types
Object.keys( _p.batchNotifications ).forEach( eventName => {
let eles = _p.batchNotifications[eventName];
if( eles.empty() ){
renderer.notify( eventName );
} else {
renderer.notify( eventName, eles );
}
} );
}
return this;
},
batch: function( callback ){
this.startBatch();
callback();
this.endBatch();
return this;
},
// for backwards compatibility
batchData: function( map ){
let cy = this;
return this.batch( function(){
let ids = Object.keys( map );
for( let i = 0; i < ids.length; i++ ){
let id = ids[i];
let data = map[ id ];
let ele = cy.getElementById( id );
ele.data( data );
}
} );
}
});
export default corefn;
+113
View File
@@ -0,0 +1,113 @@
import * as util from '../util/index.mjs';
let rendererDefaults = util.defaults({
hideEdgesOnViewport: false,
textureOnViewport: false,
motionBlur: false,
motionBlurOpacity: 0.05,
pixelRatio: undefined,
desktopTapThreshold: 4,
touchTapThreshold: 8,
wheelSensitivity: 1,
debug: false,
showFps: false,
// webgl options
webgl: false,
webglDebug: false,
webglDebugShowAtlases: false,
// defaults good for mobile
webglTexSize: 2048,
webglTexRows: 36,
webglTexRowsNodes: 18,
webglBatchSize: 2048,
webglTexPerBatch: 14,
webglBgColor: [255, 255, 255]
});
let corefn = ({
renderTo: function( context, zoom, pan, pxRatio ){
let r = this._private.renderer;
r.renderTo( context, zoom, pan, pxRatio );
return this;
},
renderer: function(){
return this._private.renderer;
},
forceRender: function(){
this.notify('draw');
return this;
},
resize: function(){
this.invalidateSize();
this.emitAndNotify('resize');
return this;
},
initRenderer: function( options ){
let cy = this;
let RendererProto = cy.extension( 'renderer', options.name );
if( RendererProto == null ){
util.error( `Can not initialise: No such renderer \`${options.name}\` found. Did you forget to import it and \`cytoscape.use()\` it?` );
return;
}
if( options.wheelSensitivity !== undefined ){
util.warn(`You have set a custom wheel sensitivity. This will make your app zoom unnaturally when using mainstream mice. You should change this value from the default only if you can guarantee that all your users will use the same hardware and OS configuration as your current machine.`);
}
let rOpts = rendererDefaults(options);
rOpts.cy = cy;
cy._private.renderer = new RendererProto( rOpts );
this.notify('init');
},
destroyRenderer: function(){
let cy = this;
cy.notify('destroy'); // destroy the renderer
let domEle = cy.container();
if( domEle ){
domEle._cyreg = null;
while( domEle.childNodes.length > 0 ){
domEle.removeChild( domEle.childNodes[0] );
}
}
cy._private.renderer = null; // to be extra safe, remove the ref
cy.mutableElements().forEach(function( ele ){
let _p = ele._private;
_p.rscratch = {};
_p.rstyle = {};
_p.animation.current = [];
_p.animation.queue = [];
});
},
onRender: function( fn ){
return this.on('render', fn);
},
offRender: function( fn ){
return this.off('render', fn);
}
});
corefn.invalidateDimensions = corefn.resize;
export default corefn;
+72
View File
@@ -0,0 +1,72 @@
import * as is from '../is.mjs';
import Collection from '../collection/index.mjs';
let corefn = ({
// get a collection
// - empty collection on no args
// - collection of elements in the graph on selector arg
// - guarantee a returned collection when elements or collection specified
collection: function( eles, opts ){
if( is.string( eles ) ){
return this.$( eles );
} else if( is.elementOrCollection( eles ) ){
return eles.collection();
} else if( is.array( eles ) ){
if (!opts) {
opts = {};
}
return new Collection( this, eles, opts.unique, opts.removed );
}
return new Collection( this );
},
nodes: function( selector ){
let nodes = this.$( function( ele ){
return ele.isNode();
} );
if( selector ){
return nodes.filter( selector );
}
return nodes;
},
edges: function( selector ){
let edges = this.$( function( ele ){
return ele.isEdge();
} );
if( selector ){
return edges.filter( selector );
}
return edges;
},
// search the graph like jQuery
$: function( selector ){
let eles = this._private.elements;
if( selector ){
return eles.filter( selector );
} else {
return eles.spawnSelf();
}
},
mutableElements: function(){
return this._private.elements;
}
});
// aliases
corefn.elements = corefn.filter = corefn.$;
export default corefn;
+41
View File
@@ -0,0 +1,41 @@
import * as is from '../is.mjs';
import Style from '../style/index.mjs';
let corefn = ({
style: function( newStyle ){
if( newStyle ){
let s = this.setStyle( newStyle );
s.update();
}
return this._private.style;
},
setStyle: function( style ){
let _p = this._private;
if( is.stylesheet( style ) ){
_p.style = style.generateStyle( this );
} else if( is.array( style ) ){
_p.style = Style.fromJson( this, style );
} else if( is.string( style ) ){
_p.style = Style.fromString( this, style );
} else {
_p.style = Style( this );
}
return _p.style;
},
// e.g. cy.data() changed => recalc ele mappers
updateStyle: function(){
this.mutableElements().updateStyle(); // just send to all eles
}
});
export default corefn;
+609
View File
@@ -0,0 +1,609 @@
import * as is from '../is.mjs';
import * as math from '../math.mjs';
let defaultSelectionType = 'single';
let corefn = ({
autolock: function( bool ){
if( bool !== undefined ){
this._private.autolock = bool ? true : false;
} else {
return this._private.autolock;
}
return this; // chaining
},
autoungrabify: function( bool ){
if( bool !== undefined ){
this._private.autoungrabify = bool ? true : false;
} else {
return this._private.autoungrabify;
}
return this; // chaining
},
autounselectify: function( bool ){
if( bool !== undefined ){
this._private.autounselectify = bool ? true : false;
} else {
return this._private.autounselectify;
}
return this; // chaining
},
selectionType: function( selType ){
let _p = this._private;
if( _p.selectionType == null ){
_p.selectionType = defaultSelectionType;
}
if( selType !== undefined ){
if( selType === 'additive' || selType === 'single' ){
_p.selectionType = selType;
}
} else {
return _p.selectionType;
}
return this;
},
panningEnabled: function( bool ){
if( bool !== undefined ){
this._private.panningEnabled = bool ? true : false;
} else {
return this._private.panningEnabled;
}
return this; // chaining
},
userPanningEnabled: function( bool ){
if( bool !== undefined ){
this._private.userPanningEnabled = bool ? true : false;
} else {
return this._private.userPanningEnabled;
}
return this; // chaining
},
zoomingEnabled: function( bool ){
if( bool !== undefined ){
this._private.zoomingEnabled = bool ? true : false;
} else {
return this._private.zoomingEnabled;
}
return this; // chaining
},
userZoomingEnabled: function( bool ){
if( bool !== undefined ){
this._private.userZoomingEnabled = bool ? true : false;
} else {
return this._private.userZoomingEnabled;
}
return this; // chaining
},
boxSelectionEnabled: function( bool ){
if( bool !== undefined ){
this._private.boxSelectionEnabled = bool ? true : false;
} else {
return this._private.boxSelectionEnabled;
}
return this; // chaining
},
pan: function(){
let args = arguments;
let pan = this._private.pan;
let dim, val, dims, x, y;
switch( args.length ){
case 0: // .pan()
return pan;
case 1:
if( is.string( args[0] ) ){ // .pan('x')
dim = args[0];
return pan[ dim ];
} else if( is.plainObject( args[0] ) ){ // .pan({ x: 0, y: 100 })
if( !this._private.panningEnabled ){
return this;
}
dims = args[0];
x = dims.x;
y = dims.y;
if( is.number( x ) ){
pan.x = x;
}
if( is.number( y ) ){
pan.y = y;
}
this.emit( 'pan viewport' );
}
break;
case 2: // .pan('x', 100)
if( !this._private.panningEnabled ){
return this;
}
dim = args[0];
val = args[1];
if( (dim === 'x' || dim === 'y') && is.number( val ) ){
pan[ dim ] = val;
}
this.emit( 'pan viewport' );
break;
default:
break; // invalid
}
this.notify('viewport');
return this; // chaining
},
panBy: function( arg0, arg1 ){
let args = arguments;
let pan = this._private.pan;
let dim, val, dims, x, y;
if( !this._private.panningEnabled ){
return this;
}
switch( args.length ){
case 1:
if( is.plainObject( arg0 ) ){ // .panBy({ x: 0, y: 100 })
dims = args[0];
x = dims.x;
y = dims.y;
if( is.number( x ) ){
pan.x += x;
}
if( is.number( y ) ){
pan.y += y;
}
this.emit( 'pan viewport' );
}
break;
case 2: // .panBy('x', 100)
dim = arg0;
val = arg1;
if( (dim === 'x' || dim === 'y') && is.number( val ) ){
pan[ dim ] += val;
}
this.emit( 'pan viewport' );
break;
default:
break; // invalid
}
this.notify('viewport');
return this; // chaining
},
gc: function() {
this.notify('gc');
},
fit: function( elements, padding ){
let viewportState = this.getFitViewport( elements, padding );
if( viewportState ){
let _p = this._private;
_p.zoom = viewportState.zoom;
_p.pan = viewportState.pan;
this.emit( 'pan zoom viewport' );
this.notify('viewport');
}
return this; // chaining
},
getFitViewport: function( elements, padding ){
if( is.number( elements ) && padding === undefined ){ // elements is optional
padding = elements;
elements = undefined;
}
if( !this._private.panningEnabled || !this._private.zoomingEnabled ){
return;
}
let bb;
if( is.string( elements ) ){
let sel = elements;
elements = this.$( sel );
} else if( is.boundingBox( elements ) ){ // assume bb
let bbe = elements;
bb = {
x1: bbe.x1,
y1: bbe.y1,
x2: bbe.x2,
y2: bbe.y2
};
bb.w = bb.x2 - bb.x1;
bb.h = bb.y2 - bb.y1;
} else if( !is.elementOrCollection( elements ) ){
elements = this.mutableElements();
}
if( is.elementOrCollection( elements ) && elements.empty() ){ return; } // can't fit to nothing
bb = bb || elements.boundingBox();
let w = this.width();
let h = this.height();
let zoom;
padding = is.number( padding ) ? padding : 0;
if( !isNaN( w ) && !isNaN( h ) && w > 0 && h > 0 && !isNaN( bb.w ) && !isNaN( bb.h ) && bb.w > 0 && bb.h > 0 ){
zoom = Math.min( (w - 2 * padding) / bb.w, (h - 2 * padding) / bb.h );
// crop zoom
zoom = zoom > this._private.maxZoom ? this._private.maxZoom : zoom;
zoom = zoom < this._private.minZoom ? this._private.minZoom : zoom;
let pan = { // now pan to middle
x: (w - zoom * ( bb.x1 + bb.x2 )) / 2,
y: (h - zoom * ( bb.y1 + bb.y2 )) / 2
};
return {
zoom: zoom,
pan: pan
};
}
return;
},
zoomRange: function( min, max ){
let _p = this._private;
if( max == null ){
let opts = min;
min = opts.min;
max = opts.max;
}
if( is.number( min ) && is.number( max ) && min <= max ){
_p.minZoom = min;
_p.maxZoom = max;
} else if( is.number( min ) && max === undefined && min <= _p.maxZoom ){
_p.minZoom = min;
} else if( is.number( max ) && min === undefined && max >= _p.minZoom ){
_p.maxZoom = max;
}
return this;
},
minZoom: function( zoom ){
if( zoom === undefined ){
return this._private.minZoom;
} else {
return this.zoomRange({ min: zoom });
}
},
maxZoom: function( zoom ){
if( zoom === undefined ){
return this._private.maxZoom;
} else {
return this.zoomRange({ max: zoom });
}
},
getZoomedViewport: function( params ){
let _p = this._private;
let currentPan = _p.pan;
let currentZoom = _p.zoom;
let pos; // in rendered px
let zoom;
let bail = false;
if( !_p.zoomingEnabled ){ // zooming disabled
bail = true;
}
if( is.number( params ) ){ // then set the zoom
zoom = params;
} else if( is.plainObject( params ) ){ // then zoom about a point
zoom = params.level;
if( params.position != null ){
pos = math.modelToRenderedPosition( params.position, currentZoom, currentPan );
} else if( params.renderedPosition != null ){
pos = params.renderedPosition;
}
if( pos != null && !_p.panningEnabled ){ // panning disabled
bail = true;
}
}
// crop zoom
zoom = zoom > _p.maxZoom ? _p.maxZoom : zoom;
zoom = zoom < _p.minZoom ? _p.minZoom : zoom;
// can't zoom with invalid params
if( bail || !is.number( zoom ) || zoom === currentZoom || ( pos != null && (!is.number( pos.x ) || !is.number( pos.y )) ) ){
return null;
}
if( pos != null ){ // set zoom about position
let pan1 = currentPan;
let zoom1 = currentZoom;
let zoom2 = zoom;
let pan2 = {
x: -zoom2 / zoom1 * (pos.x - pan1.x) + pos.x,
y: -zoom2 / zoom1 * (pos.y - pan1.y) + pos.y
};
return {
zoomed: true,
panned: true,
zoom: zoom2,
pan: pan2
};
} else { // just set the zoom
return {
zoomed: true,
panned: false,
zoom: zoom,
pan: currentPan
};
}
},
zoom: function( params ){
if( params === undefined ){ // get
return this._private.zoom;
} else { // set
let vp = this.getZoomedViewport( params );
let _p = this._private;
if( vp == null || !vp.zoomed ){ return this; }
_p.zoom = vp.zoom;
if( vp.panned ){
_p.pan.x = vp.pan.x;
_p.pan.y = vp.pan.y;
}
this.emit( 'zoom' + ( vp.panned ? ' pan' : '' ) + ' viewport' );
this.notify('viewport');
return this; // chaining
}
},
viewport: function( opts ){
let _p = this._private;
let zoomDefd = true;
let panDefd = true;
let events = []; // to trigger
let zoomFailed = false;
let panFailed = false;
if( !opts ){ return this; }
if( !is.number( opts.zoom ) ){ zoomDefd = false; }
if( !is.plainObject( opts.pan ) ){ panDefd = false; }
if( !zoomDefd && !panDefd ){ return this; }
if( zoomDefd ){
let z = opts.zoom;
if( z < _p.minZoom || z > _p.maxZoom || !_p.zoomingEnabled ){
zoomFailed = true;
} else {
_p.zoom = z;
events.push( 'zoom' );
}
}
if( panDefd && (!zoomFailed || !opts.cancelOnFailedZoom) && _p.panningEnabled ){
let p = opts.pan;
if( is.number( p.x ) ){
_p.pan.x = p.x;
panFailed = false;
}
if( is.number( p.y ) ){
_p.pan.y = p.y;
panFailed = false;
}
if( !panFailed ){
events.push( 'pan' );
}
}
if( events.length > 0 ){
events.push( 'viewport' );
this.emit( events.join( ' ' ) );
this.notify('viewport');
}
return this; // chaining
},
center: function( elements ){
let pan = this.getCenterPan( elements );
if( pan ){
this._private.pan = pan;
this.emit( 'pan viewport' );
this.notify('viewport');
}
return this; // chaining
},
getCenterPan: function( elements, zoom ){
if( !this._private.panningEnabled ){
return;
}
if( is.string( elements ) ){
let selector = elements;
elements = this.mutableElements().filter( selector );
} else if( !is.elementOrCollection( elements ) ){
elements = this.mutableElements();
}
if( elements.length === 0 ){ return; } // can't centre pan to nothing
let bb = elements.boundingBox();
let w = this.width();
let h = this.height();
zoom = zoom === undefined ? this._private.zoom : zoom;
let pan = { // middle
x: (w - zoom * ( bb.x1 + bb.x2 )) / 2,
y: (h - zoom * ( bb.y1 + bb.y2 )) / 2
};
return pan;
},
reset: function(){
if( !this._private.panningEnabled || !this._private.zoomingEnabled ){
return this;
}
this.viewport( {
pan: { x: 0, y: 0 },
zoom: 1
} );
return this; // chaining
},
invalidateSize: function(){
this._private.sizeCache = null;
},
size: function(){
let _p = this._private;
let container = _p.container;
let cy = this;
return ( _p.sizeCache = _p.sizeCache || ( container ? (function(){
let style = cy.window().getComputedStyle( container );
let val = function( name ){ return parseFloat( style.getPropertyValue( name ) ); };
return {
width: container.clientWidth - val('padding-left') - val('padding-right'),
height: container.clientHeight - val('padding-top') - val('padding-bottom')
};
})() : { // fallback if no container (not 0 b/c can be used for dividing etc)
width: 1,
height: 1
} ) );
},
width: function(){
return this.size().width;
},
height: function(){
return this.size().height;
},
extent: function(){
let pan = this._private.pan;
let zoom = this._private.zoom;
let rb = this.renderedExtent();
let b = {
x1: ( rb.x1 - pan.x ) / zoom,
x2: ( rb.x2 - pan.x ) / zoom,
y1: ( rb.y1 - pan.y ) / zoom,
y2: ( rb.y2 - pan.y ) / zoom
};
b.w = b.x2 - b.x1;
b.h = b.y2 - b.y1;
return b;
},
renderedExtent: function(){
let width = this.width();
let height = this.height();
return {
x1: 0,
y1: 0,
x2: width,
y2: height,
w: width,
h: height
};
},
multiClickDebounceTime: function ( int ){
if( int ) (this._private.multiClickDebounceTime = int);
else return this._private.multiClickDebounceTime;
return this; // chaining
}
});
// aliases
corefn.centre = corefn.center;
// backwards compatibility
corefn.autolockNodes = corefn.autolock;
corefn.autoungrabifyNodes = corefn.autoungrabify;
export default corefn;
+239
View File
@@ -0,0 +1,239 @@
import * as util from '../util/index.mjs';
import Animation from '../animation.mjs';
import * as math from '../math.mjs';
import * as is from '../is.mjs';
let define = {
animated: function(){
return function animatedImpl(){
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
let cy = this._private.cy || this;
if( !cy.styleEnabled() ){ return false; }
let ele = all[0];
if( ele ){
return ele._private.animation.current.length > 0;
}
};
}, // animated
clearQueue: function(){
return function clearQueueImpl(){
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
let cy = this._private.cy || this;
if( !cy.styleEnabled() ){ return this; }
for( let i = 0; i < all.length; i++ ){
let ele = all[ i ];
ele._private.animation.queue = [];
}
return this;
};
}, // clearQueue
delay: function(){
return function delayImpl( time, complete ){
let cy = this._private.cy || this;
if( !cy.styleEnabled() ){ return this; }
return this.animate( {
delay: time,
duration: time,
complete: complete
} );
};
}, // delay
delayAnimation: function(){
return function delayAnimationImpl( time, complete ){
let cy = this._private.cy || this;
if( !cy.styleEnabled() ){ return this; }
return this.animation( {
delay: time,
duration: time,
complete: complete
} );
};
}, // delay
animation: function(){
return function animationImpl( properties, params ){
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
let cy = this._private.cy || this;
let isCore = !selfIsArrayLike;
let isEles = !isCore;
if( !cy.styleEnabled() ){ return this; }
let style = cy.style();
properties = util.assign( {}, properties, params );
let propertiesEmpty = Object.keys( properties ).length === 0;
if( propertiesEmpty ){
return new Animation( all[0], properties ); // nothing to animate
}
if( properties.duration === undefined ){
properties.duration = 400;
}
switch( properties.duration ){
case 'slow':
properties.duration = 600;
break;
case 'fast':
properties.duration = 200;
break;
}
if( isEles ){
properties.style = style.getPropsList( properties.style || properties.css );
properties.css = undefined;
}
if( isEles && properties.renderedPosition != null ){
let rpos = properties.renderedPosition;
let pan = cy.pan();
let zoom = cy.zoom();
properties.position = math.renderedToModelPosition( rpos, zoom, pan );
}
// override pan w/ panBy if set
if( isCore && properties.panBy != null ){
let panBy = properties.panBy;
let cyPan = cy.pan();
properties.pan = {
x: cyPan.x + panBy.x,
y: cyPan.y + panBy.y
};
}
// override pan w/ center if set
let center = properties.center || properties.centre;
if( isCore && center != null ){
let centerPan = cy.getCenterPan( center.eles, properties.zoom );
if( centerPan != null ){
properties.pan = centerPan;
}
}
// override pan & zoom w/ fit if set
if( isCore && properties.fit != null ){
let fit = properties.fit;
let fitVp = cy.getFitViewport( fit.eles || fit.boundingBox, fit.padding );
if( fitVp != null ){
properties.pan = fitVp.pan;
properties.zoom = fitVp.zoom;
}
}
// override zoom (& potentially pan) w/ zoom obj if set
if( isCore && is.plainObject( properties.zoom ) ){
let vp = cy.getZoomedViewport( properties.zoom );
if( vp != null ){
if( vp.zoomed ){ properties.zoom = vp.zoom; }
if( vp.panned ){ properties.pan = vp.pan; }
} else {
properties.zoom = null; // an inavalid zoom (e.g. no delta) gets automatically destroyed
}
}
return new Animation( all[0], properties );
};
}, // animate
animate: function(){
return function animateImpl( properties, params ){
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
let cy = this._private.cy || this;
if( !cy.styleEnabled() ){ return this; }
if( params ){
properties = util.extend( {}, properties, params );
}
// manually hook and run the animation
for( let i = 0; i < all.length; i++ ){
let ele = all[ i ];
let queue = ele.animated() && (properties.queue === undefined || properties.queue);
let ani = ele.animation( properties, (queue ? { queue: true } : undefined) );
ani.play();
}
return this; // chaining
};
}, // animate
stop: function(){
return function stopImpl( clearQueue, jumpToEnd ){
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
let cy = this._private.cy || this;
if( !cy.styleEnabled() ){ return this; }
for( let i = 0; i < all.length; i++ ){
let ele = all[ i ];
let _p = ele._private;
let anis = _p.animation.current;
for( let j = 0; j < anis.length; j++ ){
let ani = anis[ j ];
let ani_p = ani._private;
if( jumpToEnd ){
// next iteration of the animation loop, the animation
// will go straight to the end and be removed
ani_p.duration = 0;
}
}
// clear the queue of future animations
if( clearQueue ){
_p.animation.queue = [];
}
if( !jumpToEnd ){
_p.animation.current = [];
}
}
// we have to notify (the animation loop doesn't do it for us on `stop`)
cy.notify('draw');
return this;
};
} // stop
}; // define
export default define;
+208
View File
@@ -0,0 +1,208 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import get from 'lodash/get.js';
import set from 'lodash/set.js';
import toPath from 'lodash/toPath.js';
let define = {
// access data field
data: function( params ){
let defaults = {
field: 'data',
bindingEvent: 'data',
allowBinding: false,
allowSetting: false,
allowGetting: false,
settingEvent: 'data',
settingTriggersEvent: false,
triggerFnName: 'trigger',
immutableKeys: {}, // key => true if immutable
updateStyle: false,
beforeGet: function( self ){},
beforeSet: function( self, obj ){},
onSet: function( self ){},
canSet: function( self ){ return true; }
};
params = util.extend( {}, defaults, params );
return function dataImpl( name, value ){
let p = params;
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
let single = selfIsArrayLike ? self[0] : self;
// .data('foo', ...)
if (is.string(name)) { // set or get property
let isPathLike = name.indexOf('.') !== -1; // there might be a normal field with a dot
let path = isPathLike && toPath(name);
// .data('foo')
if( p.allowGetting && value === undefined ){ // get
let ret;
if( single ){
p.beforeGet( single );
// check if it's path and a field with the same name doesn't exist
if (path && single._private[ p.field ][ name ] === undefined) {
ret = get(single._private[ p.field ], path);
} else {
ret = single._private[ p.field ][ name ];
}
}
return ret;
// .data('foo', 'bar')
} else if( p.allowSetting && value !== undefined ){ // set
let valid = !p.immutableKeys[ name ];
if( valid ){
let change = { [name]: value };
p.beforeSet( self, change );
for( let i = 0, l = all.length; i < l; i++ ){
let ele = all[i];
if( p.canSet( ele ) ){
if (path && single._private[ p.field ][ name ] === undefined) {
set(ele._private[ p.field ], path, value);
} else {
ele._private[ p.field ][ name ] = value;
}
}
}
// update mappers if asked
if( p.updateStyle ){ self.updateStyle(); }
// call onSet callback
p.onSet( self );
if( p.settingTriggersEvent ){
self[ p.triggerFnName ]( p.settingEvent );
}
}
}
// .data({ 'foo': 'bar' })
} else if( p.allowSetting && is.plainObject( name ) ){ // extend
let obj = name;
let k, v;
let keys = Object.keys( obj );
p.beforeSet( self, obj );
for( let i = 0; i < keys.length; i++ ){
k = keys[ i ];
v = obj[ k ];
let valid = !p.immutableKeys[ k ];
if( valid ){
for( let j = 0; j < all.length; j++ ){
let ele = all[j];
if( p.canSet( ele ) ){
ele._private[ p.field ][ k ] = v;
}
}
}
}
// update mappers if asked
if( p.updateStyle ){ self.updateStyle(); }
// call onSet callback
p.onSet( self );
if( p.settingTriggersEvent ){
self[ p.triggerFnName ]( p.settingEvent );
}
// .data(function(){ ... })
} else if( p.allowBinding && is.fn( name ) ){ // bind to event
let fn = name;
self.on( p.bindingEvent, fn );
// .data()
} else if( p.allowGetting && name === undefined ){ // get whole object
let ret;
if( single ){
p.beforeGet( single );
ret = single._private[ p.field ];
}
return ret;
}
return self; // maintain chainability
}; // function
}, // data
// remove data field
removeData: function( params ){
let defaults = {
field: 'data',
event: 'data',
triggerFnName: 'trigger',
triggerEvent: false,
immutableKeys: {} // key => true if immutable
};
params = util.extend( {}, defaults, params );
return function removeDataImpl( names ){
let p = params;
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
// .removeData('foo bar')
if( is.string( names ) ){ // then get the list of keys, and delete them
let keys = names.split( /\s+/ );
let l = keys.length;
for( let i = 0; i < l; i++ ){ // delete each non-empty key
let key = keys[ i ];
if( is.emptyString( key ) ){ continue; }
let valid = !p.immutableKeys[ key ]; // not valid if immutable
if( valid ){
for( let i_a = 0, l_a = all.length; i_a < l_a; i_a++ ){
all[ i_a ]._private[ p.field ][ key ] = undefined;
}
}
}
if( p.triggerEvent ){
self[ p.triggerFnName ]( p.event );
}
// .removeData()
} else if( names === undefined ){ // then delete all keys
for( let i_a = 0, l_a = all.length; i_a < l_a; i_a++ ){
let _privateFields = all[ i_a ]._private[ p.field ];
let keys = Object.keys( _privateFields );
for( let i = 0; i < keys.length; i++ ){
let key = keys[i];
let validKeyToDelete = !p.immutableKeys[ key ];
if( validKeyToDelete ){
_privateFields[ key ] = undefined;
}
}
}
if( p.triggerEvent ){
self[ p.triggerFnName ]( p.event );
}
}
return self; // maintain chaining
}; // function
}, // removeData
}; // define
export default define;
+34
View File
@@ -0,0 +1,34 @@
import Promise from '../promise.mjs';
let define = {
eventAliasesOn: function( proto ){
let p = proto;
p.addListener = p.listen = p.bind = p.on;
p.unlisten = p.unbind = p.off = p.removeListener;
p.trigger = p.emit;
// this is just a wrapper alias of .on()
p.pon = p.promiseOn = function( events, selector ){
let self = this;
let args = Array.prototype.slice.call( arguments, 0 );
return new Promise( function( resolve, reject ){
let callback = function( e ){
self.off.apply( self, offArgs );
resolve( e );
};
let onArgs = args.concat( [ callback ] );
let offArgs = onArgs.concat( [] );
self.on.apply( self, onArgs );
} );
};
},
}; // define
export default define;
+22
View File
@@ -0,0 +1,22 @@
// use this module to cherry pick functions into your prototype
// (useful for functions shared between the core and collections, for example)
// e.g.
// let foo = define.foo({ /* params... */ })
import * as util from '../util/index.mjs';
import animation from './animation.mjs';
import data from './data.mjs';
import events from './events.mjs';
let define = {};
[
animation,
data,
events
].forEach(function( m ){
util.assign( define, m );
});
export default define;
+240
View File
@@ -0,0 +1,240 @@
import * as util from './util/index.mjs';
import * as is from './is.mjs';
import Event from './event.mjs';
const eventRegex = /^([^.]+)(\.(?:[^.]+))?$/; // regex for matching event strings (e.g. "click.namespace")
const universalNamespace = '.*'; // matches as if no namespace specified and prevents users from unbinding accidentally
const defaults = {
qualifierCompare: function( q1, q2 ){
return q1 === q2;
},
eventMatches: function( /*context, listener, eventObj*/ ){
return true;
},
addEventFields: function( /*context, evt*/ ){
},
callbackContext: function( context/*, listener, eventObj*/ ){
return context;
},
beforeEmit: function(/* context, listener, eventObj */){
},
afterEmit: function(/* context, listener, eventObj */){
},
bubble: function( /*context*/ ){
return false;
},
parent: function( /*context*/ ){
return null;
},
context: null
};
let defaultsKeys = Object.keys( defaults );
let emptyOpts = {};
function Emitter( opts = emptyOpts, context ){
// micro-optimisation vs Object.assign() -- reduces Element instantiation time
for( let i = 0; i < defaultsKeys.length; i++ ){
let key = defaultsKeys[i];
this[key] = opts[key] || defaults[key];
}
this.context = context || this.context;
this.listeners = [];
this.emitting = 0;
}
let p = Emitter.prototype;
let forEachEvent = function( self, handler, events, qualifier, callback, conf, confOverrides ){
if( is.fn( qualifier ) ){
callback = qualifier;
qualifier = null;
}
if( confOverrides ){
if( conf == null ){
conf = confOverrides;
} else {
conf = util.assign( {}, conf, confOverrides );
}
}
let eventList = is.array(events) ? events : events.split(/\s+/);
for( let i = 0; i < eventList.length; i++ ){
let evt = eventList[i];
if( is.emptyString( evt ) ){ continue; }
let match = evt.match( eventRegex ); // type[.namespace]
if( match ){
let type = match[1];
let namespace = match[2] ? match[2] : null;
let ret = handler( self, evt, type, namespace, qualifier, callback, conf );
if( ret === false ){ break; } // allow exiting early
}
}
};
let makeEventObj = function( self, obj ){
self.addEventFields( self.context, obj );
return new Event( obj.type, obj );
};
let forEachEventObj = function( self, handler, events ){
if( is.event( events ) ){
handler( self, events );
return;
} else if( is.plainObject( events ) ){
handler( self, makeEventObj( self, events ) );
return;
}
let eventList = is.array(events) ? events : events.split(/\s+/);
for( let i = 0; i < eventList.length; i++ ){
let evt = eventList[i];
if( is.emptyString( evt ) ){ continue; }
let match = evt.match( eventRegex ); // type[.namespace]
if( match ){
let type = match[1];
let namespace = match[2] ? match[2] : null;
let eventObj = makeEventObj( self, {
type: type,
namespace: namespace,
target: self.context
} );
handler( self, eventObj );
}
}
};
p.on = p.addListener = function( events, qualifier, callback, conf, confOverrides ){
forEachEvent( this, function( self, event, type, namespace, qualifier, callback, conf ){
if( is.fn( callback ) ){
self.listeners.push( {
event: event, // full event string
callback: callback, // callback to run
type: type, // the event type (e.g. 'click')
namespace: namespace, // the event namespace (e.g. ".foo")
qualifier: qualifier, // a restriction on whether to match this emitter
conf: conf // additional configuration
} );
}
}, events, qualifier, callback, conf, confOverrides );
return this;
};
p.one = function( events, qualifier, callback, conf ){
return this.on( events, qualifier, callback, conf, { one: true } );
};
p.removeListener = p.off = function( events, qualifier, callback, conf ){
if( this.emitting !== 0 ){
this.listeners = util.copyArray( this.listeners );
}
let listeners = this.listeners;
for( let i = listeners.length - 1; i >= 0; i-- ){
let listener = listeners[i];
forEachEvent( this, function( self, event, type, namespace, qualifier, callback/*, conf*/ ){
if(
( listener.type === type || events === '*' ) &&
( (!namespace && listener.namespace !== '.*') || listener.namespace === namespace ) &&
( !qualifier || self.qualifierCompare( listener.qualifier, qualifier ) ) &&
( !callback || listener.callback === callback )
){
listeners.splice( i, 1 );
return false;
}
}, events, qualifier, callback, conf );
}
return this;
};
p.removeAllListeners = function(){
return this.removeListener('*');
};
p.emit = p.trigger = function( events, extraParams, manualCallback ){
let listeners = this.listeners;
let numListenersBeforeEmit = listeners.length;
this.emitting++;
if( !is.array( extraParams ) ){
extraParams = [ extraParams ];
}
forEachEventObj( this, function( self, eventObj ){
if( manualCallback != null ){
listeners = [{
event: eventObj.event,
type: eventObj.type,
namespace: eventObj.namespace,
callback: manualCallback
}];
numListenersBeforeEmit = listeners.length;
}
for( let i = 0; i < numListenersBeforeEmit; i++ ){
let listener = listeners[i];
if(
( listener.type === eventObj.type ) &&
( !listener.namespace || listener.namespace === eventObj.namespace || listener.namespace === universalNamespace ) &&
( self.eventMatches( self.context, listener, eventObj ) )
){
let args = [ eventObj ];
if( extraParams != null ){
util.push( args, extraParams );
}
self.beforeEmit( self.context, listener, eventObj );
if( listener.conf && listener.conf.one ){
self.listeners = self.listeners.filter( l => l !== listener );
}
let context = self.callbackContext( self.context, listener, eventObj );
let ret = listener.callback.apply( context, args );
self.afterEmit( self.context, listener, eventObj );
if( ret === false ){
eventObj.stopPropagation();
eventObj.preventDefault();
}
} // if listener matches
} // for listener
if( self.bubble( self.context ) && !eventObj.isPropagationStopped() ){
self.parent( self.context ).emit( eventObj, extraParams );
}
}, events );
this.emitting--;
return this;
};
export default Emitter;
+111
View File
@@ -0,0 +1,111 @@
/*!
Event object based on jQuery events, MIT license
https://jquery.org/license/
https://tldrlegal.com/license/mit-license
https://github.com/jquery/jquery/blob/master/src/event.js
*/
let Event = function( src, props ){
this.recycle( src, props );
};
function returnFalse(){
return false;
}
function returnTrue(){
return true;
}
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
Event.prototype = {
instanceString: function(){
return 'event';
},
recycle: function( src, props ){
this.isImmediatePropagationStopped = this.isPropagationStopped = this.isDefaultPrevented = returnFalse;
if( src != null && src.preventDefault ){ // Browser Event object
this.type = src.type;
// Events bubbling up the document may have been marked as prevented
// by a handler lower down the tree; reflect the correct value.
this.isDefaultPrevented = ( src.defaultPrevented ) ? returnTrue : returnFalse;
} else if( src != null && src.type ){ // Plain object containing all event details
props = src;
} else { // Event string
this.type = src;
}
// Put explicitly provided properties onto the event object
if( props != null ){
// more efficient to manually copy fields we use
this.originalEvent = props.originalEvent;
this.type = props.type != null ? props.type : this.type;
this.cy = props.cy;
this.target = props.target;
this.position = props.position;
this.renderedPosition = props.renderedPosition;
this.namespace = props.namespace;
this.layout = props.layout;
}
if( this.cy != null && this.position != null && this.renderedPosition == null ){
// create a rendered position based on the passed position
let pos = this.position;
let zoom = this.cy.zoom();
let pan = this.cy.pan();
this.renderedPosition = {
x: pos.x * zoom + pan.x,
y: pos.y * zoom + pan.y
};
}
// Create a timestamp if incoming event doesn't have one
this.timeStamp = src && src.timeStamp || Date.now();
},
preventDefault: function(){
this.isDefaultPrevented = returnTrue;
let e = this.originalEvent;
if( !e ){
return;
}
// if preventDefault exists run it on the original event
if( e.preventDefault ){
e.preventDefault();
}
},
stopPropagation: function(){
this.isPropagationStopped = returnTrue;
let e = this.originalEvent;
if( !e ){
return;
}
// if stopPropagation exists run it on the original event
if( e.stopPropagation ){
e.stopPropagation();
}
},
stopImmediatePropagation: function(){
this.isImmediatePropagationStopped = returnTrue;
this.stopPropagation();
},
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse,
isImmediatePropagationStopped: returnFalse
};
export default Event;
+246
View File
@@ -0,0 +1,246 @@
import * as util from'./util/index.mjs';
import define from './define/index.mjs';
import Collection from './collection/index.mjs';
import Core from './core/index.mjs';
import incExts from './extensions/index.mjs';
import * as is from './is.mjs';
import Emitter from './emitter.mjs';
// registered extensions to cytoscape, indexed by name
let extensions = {};
// registered modules for extensions, indexed by name
let modules = {};
function setExtension( type, name, registrant ){
let ext = registrant;
let overrideErr = function( field ){
util.warn( 'Can not register `' + name + '` for `' + type + '` since `' + field + '` already exists in the prototype and can not be overridden' );
};
if( type === 'core' ){
if( Core.prototype[ name ] ){
return overrideErr( name );
} else {
Core.prototype[ name ] = registrant;
}
} else if( type === 'collection' ){
if( Collection.prototype[ name ] ){
return overrideErr( name );
} else {
Collection.prototype[ name ] = registrant;
}
} else if( type === 'layout' ){
// fill in missing layout functions in the prototype
let Layout = function( options ){
this.options = options;
registrant.call( this, options );
// make sure layout has _private for use w/ std apis like .on()
if( !is.plainObject( this._private ) ){
this._private = {};
}
this._private.cy = options.cy;
this._private.listeners = [];
this.createEmitter();
};
let layoutProto = Layout.prototype = Object.create( registrant.prototype );
let optLayoutFns = [];
for( let i = 0; i < optLayoutFns.length; i++ ){
let fnName = optLayoutFns[ i ];
layoutProto[ fnName ] = layoutProto[ fnName ] || function(){ return this; };
}
// either .start() or .run() is defined, so autogen the other
if( layoutProto.start && !layoutProto.run ){
layoutProto.run = function(){ this.start(); return this; };
} else if( !layoutProto.start && layoutProto.run ){
layoutProto.start = function(){ this.run(); return this; };
}
let regStop = registrant.prototype.stop;
layoutProto.stop = function(){
let opts = this.options;
if( opts && opts.animate ){
let anis = this.animations;
if( anis ){
for( let i = 0; i < anis.length; i++ ){
anis[ i ].stop();
}
}
}
if( regStop ){
regStop.call( this );
} else {
this.emit( 'layoutstop' );
}
return this;
};
if( !layoutProto.destroy ){
layoutProto.destroy = function(){
return this;
};
}
layoutProto.cy = function(){
return this._private.cy;
};
let getCy = layout => layout._private.cy;
let emitterOpts = {
addEventFields: function( layout, evt ){
evt.layout = layout;
evt.cy = getCy(layout);
evt.target = layout;
},
bubble: function(){ return true; },
parent: function( layout ){ return getCy(layout); }
};
util.assign( layoutProto, {
createEmitter: function(){
this._private.emitter = new Emitter( emitterOpts, this );
return this;
},
emitter: function(){ return this._private.emitter; },
on: function( evt, cb ){ this.emitter().on( evt, cb ); return this; },
one: function( evt, cb ){ this.emitter().one( evt, cb ); return this; },
once: function( evt, cb ){ this.emitter().one( evt, cb ); return this; },
removeListener: function( evt, cb ){ this.emitter().removeListener( evt, cb ); return this; },
removeAllListeners: function(){ this.emitter().removeAllListeners(); return this; },
emit: function( evt, params ){ this.emitter().emit( evt, params ); return this; }
} );
define.eventAliasesOn( layoutProto );
ext = Layout; // replace with our wrapped layout
} else if( type === 'renderer' && name !== 'null' && name !== 'base' ){
// user registered renderers inherit from base
let BaseRenderer = getExtension( 'renderer', 'base' );
let bProto = BaseRenderer.prototype;
let RegistrantRenderer = registrant;
let rProto = registrant.prototype;
let Renderer = function(){
BaseRenderer.apply( this, arguments );
RegistrantRenderer.apply( this, arguments );
};
let proto = Renderer.prototype;
for( let pName in bProto ){
let pVal = bProto[ pName ];
let existsInR = rProto[ pName ] != null;
if( existsInR ){
return overrideErr( pName );
}
proto[ pName ] = pVal; // take impl from base
}
for( let pName in rProto ){
proto[ pName ] = rProto[ pName ]; // take impl from registrant
}
bProto.clientFunctions.forEach( function( name ){
proto[ name ] = proto[ name ] || function(){
util.error( 'Renderer does not implement `renderer.' + name + '()` on its prototype' );
};
} );
ext = Renderer;
} else if (type === '__proto__' || type === 'constructor' || type === 'prototype'){
// to avoid potential prototype pollution
return util.error( type + ' is an illegal type to be registered, possibly lead to prototype pollutions' );
}
return util.setMap( {
map: extensions,
keys: [ type, name ],
value: ext
} );
}
function getExtension( type, name ){
return util.getMap( {
map: extensions,
keys: [ type, name ]
} );
}
function setModule( type, name, moduleType, moduleName, registrant ){
return util.setMap( {
map: modules,
keys: [ type, name, moduleType, moduleName ],
value: registrant
} );
}
function getModule( type, name, moduleType, moduleName ){
return util.getMap( {
map: modules,
keys: [ type, name, moduleType, moduleName ]
} );
}
let extension = function(){
// e.g. extension('renderer', 'svg')
if( arguments.length === 2 ){
return getExtension.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', { ... })
else if( arguments.length === 3 ){
return setExtension.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', 'nodeShape', 'ellipse')
else if( arguments.length === 4 ){
return getModule.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', 'nodeShape', 'ellipse', { ... })
else if( arguments.length === 5 ){
return setModule.apply( null, arguments );
}
else {
util.error( 'Invalid extension access syntax' );
}
};
// allows a core instance to access extensions internally
Core.prototype.extension = extension;
// included extensions
incExts.forEach( function( group ){
group.extensions.forEach( function( ext ){
setExtension( group.type, ext.name, ext.impl );
} );
} );
export default extension;
+14
View File
@@ -0,0 +1,14 @@
import layout from './layout/index.mjs';
import renderer from './renderer/index.mjs';
export default [
{
type: 'layout',
extensions: layout
},
{
type: 'renderer',
extensions: renderer
}
];
+408
View File
@@ -0,0 +1,408 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
import * as is from '../../is.mjs';
/* eslint-disable no-unused-vars */
const defaults = {
fit: true, // whether to fit the viewport to the graph
directed: false, // whether the tree is directed downwards (or edges can point in any direction if false)
direction: 'downward', // determines the direction in which the tree structure is drawn. The possible values are 'downward', 'upward', 'rightward', or 'leftward'.
padding: 30, // padding on fit
circle: false, // put depths in concentric circles if true, put depths top down if false
grid: false, // whether to create an even grid into which the DAG is placed (circle:false only)
spacingFactor: 1.75, // positive spacing factor, larger => more space between nodes (N.B. n/a if causes overlap)
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
roots: undefined, // the roots of the trees
depthSort: undefined, // a sorting function to order nodes at equal depth. e.g. function(a, b){ return a.data('weight') - b.data('weight') }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled,
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
const deprecatedOptionDefaults = {
maximal: false, // whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only); setting acyclic to true sets maximal to true also
acyclic: false, // whether the tree is acyclic and thus a node could be shifted (due to the maximal option) multiple times without causing an infinite loop; setting to true sets maximal to true also; if you are uncertain whether a tree is acyclic, set to false to avoid potential infinite loops
};
/* eslint-enable */
const getInfo = ele => ele.scratch('breadthfirst');
const setInfo = (ele, obj) => ele.scratch('breadthfirst', obj);
function BreadthFirstLayout( options ){
this.options = util.extend( {}, defaults, deprecatedOptionDefaults, options );
}
BreadthFirstLayout.prototype.run = function(){
const options = this.options;
const cy = options.cy;
const eles = options.eles;
const nodes = eles.nodes().filter( n => n.isChildless() );
const graph = eles;
const directed = options.directed;
const maximal = options.acyclic || options.maximal || options.maximalAdjustments > 0; // maximalAdjustments for compat. w/ old code; also, setting acyclic to true sets maximal to true
const hasBoundingBox = !!options.boundingBox;
const bb = math.makeBoundingBox( hasBoundingBox ? options.boundingBox :
structuredClone(cy.extent()));
let roots;
if( is.elementOrCollection( options.roots ) ){
roots = options.roots;
} else if( is.array( options.roots ) ){
const rootsArray = [];
for( let i = 0; i < options.roots.length; i++ ){
const id = options.roots[ i ];
const ele = cy.getElementById( id );
rootsArray.push( ele );
}
roots = cy.collection( rootsArray );
} else if( is.string( options.roots ) ){
roots = cy.$( options.roots );
} else {
if( directed ){
roots = nodes.roots();
} else {
const components = eles.components();
roots = cy.collection();
for( let i = 0; i < components.length; i++ ){
const comp = components[i];
const maxDegree = comp.maxDegree( false );
const compRoots = comp.filter( function( ele ){
return ele.degree( false ) === maxDegree;
} );
roots = roots.add( compRoots );
}
}
}
const depths = [];
const foundByBfs = {};
const addToDepth = ( ele, d ) => {
if( depths[d] == null ){
depths[d] = [];
}
const i = depths[d].length;
depths[d].push( ele );
setInfo( ele, {
index: i,
depth: d
} );
};
const changeDepth = ( ele, newDepth ) => {
const { depth, index } = getInfo( ele );
depths[ depth ][ index ] = null;
// add only childless nodes
if (ele.isChildless()) addToDepth( ele, newDepth );
};
// find the depths of the nodes
graph.bfs( {
roots: roots,
directed: options.directed,
visit: function( node, edge, pNode, i, depth ){
const ele = node[0];
const id = ele.id();
// add only childless nodes
if (ele.isChildless()) addToDepth( ele, depth );
foundByBfs[ id ] = true;
}
} );
// check for nodes not found by bfs
const orphanNodes = [];
for( let i = 0; i < nodes.length; i++ ){
const ele = nodes[ i ];
if( foundByBfs[ ele.id() ] ){
continue;
} else {
orphanNodes.push( ele );
}
}
// assign the nodes a depth and index
const assignDepthsAt = function( i ){
const eles = depths[ i ];
for( let j = 0; j < eles.length; j++ ){
const ele = eles[ j ];
if( ele == null ){
eles.splice( j, 1 );
j--;
continue;
}
setInfo(ele, {
depth: i,
index: j
});
}
};
const adjustMaximally = function( ele, shifted ){
const eInfo = getInfo( ele );
const incomers = ele.incomers().filter( el => el.isNode() && eles.has(el) );
let maxDepth = -1;
const id = ele.id();
for( let k = 0; k < incomers.length; k++ ){
const incmr = incomers[k];
const iInfo = getInfo( incmr );
maxDepth = Math.max( maxDepth, iInfo.depth );
}
if( eInfo.depth <= maxDepth ){
if( !options.acyclic && shifted[id] ){
return null;
}
const newDepth = maxDepth + 1;
changeDepth( ele, newDepth );
shifted[id] = newDepth;
return true;
}
return false;
};
// for the directed case, try to make the edges all go down (i.e. depth i => depth i + 1)
if( directed && maximal ){
const Q = [];
const shifted = {};
const enqueue = n => Q.push(n);
const dequeue = () => Q.shift();
nodes.forEach( n => Q.push(n) );
while( Q.length > 0 ){
const ele = dequeue();
const didShift = adjustMaximally( ele, shifted );
if( didShift ){
ele.outgoers().filter( el => el.isNode() && eles.has(el) ).forEach( enqueue );
} else if( didShift === null ){
util.warn('Detected double maximal shift for node `' + ele.id() + '`. Bailing maximal adjustment due to cycle. Use `options.maximal: true` only on DAGs.');
break; // exit on failure
}
}
}
// find min distance we need to leave between nodes
let minDistance = 0;
if( options.avoidOverlap ){
for( let i = 0; i < nodes.length; i++ ){
const n = nodes[ i ];
const nbb = n.layoutDimensions( options );
const w = nbb.w;
const h = nbb.h;
minDistance = Math.max( minDistance, w, h );
}
}
// get the weighted percent for an element based on its connectivity to other levels
const cachedWeightedPercent = {};
const getWeightedPercent = function( ele ){
if( cachedWeightedPercent[ ele.id() ] ){
return cachedWeightedPercent[ ele.id() ];
}
const eleDepth = getInfo( ele ).depth;
const neighbors = ele.neighborhood();
let percent = 0;
let samples = 0;
for( let i = 0; i < neighbors.length; i++ ){
const neighbor = neighbors[ i ];
if( neighbor.isEdge() || neighbor.isParent() || !nodes.has( neighbor ) ){
continue;
}
const bf = getInfo( neighbor );
if (bf == null){ continue; }
const index = bf.index;
const depth = bf.depth;
// unassigned neighbours shouldn't affect the ordering
if( index == null || depth == null ){
continue;
}
const nDepth = depths[ depth ].length;
if( depth < eleDepth ){ // only get influenced by elements above
percent += index / nDepth;
samples++;
}
}
samples = Math.max( 1, samples );
percent = percent / samples;
if( samples === 0 ){ // put lone nodes at the start
percent = 0;
}
cachedWeightedPercent[ ele.id() ] = percent;
return percent;
};
// rearrange the indices in each depth level based on connectivity
let sortFn = function( a, b ){
const apct = getWeightedPercent( a );
const bpct = getWeightedPercent( b );
const diff = apct - bpct;
if( diff === 0 ){
return util.sort.ascending( a.id(), b.id() ); // make sure sort doesn't have don't-care comparisons
} else {
return diff;
}
};
if (options.depthSort !== undefined) {
sortFn = options.depthSort;
}
let depthsLen = depths.length;
// sort each level to make connected nodes closer
for( let i = 0; i < depthsLen; i++ ){
depths[ i ].sort( sortFn );
assignDepthsAt( i );
}
// assign orphan nodes to a new top-level depth
const orphanDepth = [];
for( let i = 0; i < orphanNodes.length; i++ ){
orphanDepth.push( orphanNodes[i] );
}
const assignDepths = function(){
for( let i = 0; i < depthsLen; i++ ){
assignDepthsAt( i );
}
};
// add a new top-level depth only when there are orphan nodes
if (orphanDepth.length) {
depths.unshift( orphanDepth );
depthsLen = depths.length;
assignDepths();
}
let biggestDepthSize = 0;
for( let i = 0; i < depthsLen; i++ ){
biggestDepthSize = Math.max( depths[ i ].length, biggestDepthSize );
}
const center = {
x: bb.x1 + bb.w / 2,
y: bb.y1 + bb.h / 2
};
// average node size
const aveNodeSize = nodes.reduce((acc, node) => ((box) => ({
w: acc.w === -1 ? box.w : (acc.w + box.w) / 2,
h: acc.h === -1 ? box.h : (acc.h + box.h) / 2,
}))(node.boundingBox({
includeLabels: options.nodeDimensionsIncludeLabels
})), { w: -1, h: -1 });
const distanceY = Math.max(
// only one depth
depthsLen === 1 ? 0 :
// inside a bounding box, no need for top & bottom padding
hasBoundingBox ? ((bb.h - options.padding * 2 - aveNodeSize.h) / (depthsLen - 1)) :
(bb.h - options.padding * 2 - aveNodeSize.h) / (depthsLen + 1),
minDistance );
const maxDepthSize = depths.reduce( (max, eles) => Math.max(max, eles.length), 0 );
const getPositionTopBottom = function( ele ){
const { depth, index } = getInfo( ele );
if ( options.circle ){
let radiusStepSize = Math.min( bb.w / 2 / depthsLen, bb.h / 2 / depthsLen );
radiusStepSize = Math.max( radiusStepSize, minDistance );
let radius = radiusStepSize * depth + radiusStepSize - (depthsLen > 0 && depths[0].length <= 3 ? radiusStepSize / 2 : 0);
const theta = 2 * Math.PI / depths[ depth ].length * index;
if( depth === 0 && depths[0].length === 1 ){
radius = 1;
}
return {
x: center.x + radius * Math.cos( theta ),
y: center.y + radius * Math.sin( theta )
};
} else {
const depthSize = depths[ depth ].length;
const distanceX = Math.max(
// only one depth
depthSize === 1 ? 0 :
// inside a bounding box, no need for left & right padding
hasBoundingBox ? ((bb.w - options.padding * 2 - aveNodeSize.w) / ((options.grid ? maxDepthSize : depthSize) - 1)):
(bb.w - options.padding * 2 - aveNodeSize.w) / ((options.grid ? maxDepthSize : depthSize) + 1),
minDistance );
const epos = {
x: center.x + (index + 1 - (depthSize + 1) / 2) * distanceX,
y: center.y + (depth + 1 - (depthsLen + 1) / 2) * distanceY
};
return epos;
}
};
const rotateDegrees = {
'downward': 0,
'leftward': 90,
'upward': 180,
'rightward': -90,
}
if (Object.keys(rotateDegrees).indexOf(options.direction) === -1) {
util.error(`Invalid direction '${options.direction}' specified for breadthfirst layout. Valid values are: ${Object.keys(rotateDegrees).join(', ')}`);
}
const getPosition = (ele) => util.rotatePosAndSkewByBox(getPositionTopBottom(ele), bb, rotateDegrees[options.direction]);
eles.nodes().layoutPositions( this, options, getPosition);
return this; // chaining
};
export default BreadthFirstLayout;
+105
View File
@@ -0,0 +1,105 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
import * as is from '../../is.mjs';
let defaults = {
fit: true, // whether to fit the viewport to the graph
padding: 30, // the padding on fit
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox and radius if not enough space
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
radius: undefined, // the radius of the circle
startAngle: 3 / 2 * Math.PI, // where nodes start in radians
sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function CircleLayout( options ){
this.options = util.extend( {}, defaults, options );
}
CircleLayout.prototype.run = function(){
let params = this.options;
let options = params;
let cy = params.cy;
let eles = options.eles;
let clockwise = options.counterclockwise !== undefined ? !options.counterclockwise : options.clockwise;
let nodes = eles.nodes().not( ':parent' );
if( options.sort ){
nodes = nodes.sort( options.sort );
}
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let center = {
x: bb.x1 + bb.w / 2,
y: bb.y1 + bb.h / 2
};
let sweep = options.sweep === undefined ? 2 * Math.PI - 2 * Math.PI / nodes.length : options.sweep;
let dTheta = sweep / ( Math.max( 1, nodes.length - 1 ) );
let r;
let minDistance = 0;
for( let i = 0; i < nodes.length; i++ ){
let n = nodes[ i ];
let nbb = n.layoutDimensions( options );
let w = nbb.w;
let h = nbb.h;
minDistance = Math.max( minDistance, w, h );
}
if( is.number( options.radius ) ){
r = options.radius;
} else if( nodes.length <= 1 ){
r = 0;
} else {
r = Math.min( bb.h, bb.w ) / 2 - minDistance;
}
// calculate the radius
if( nodes.length > 1 && options.avoidOverlap ){ // but only if more than one node (can't overlap)
minDistance *= 1.75; // just to have some nice spacing
let dcos = Math.cos( dTheta ) - Math.cos( 0 );
let dsin = Math.sin( dTheta ) - Math.sin( 0 );
let rMin = Math.sqrt( minDistance * minDistance / ( dcos * dcos + dsin * dsin ) ); // s.t. no nodes overlapping
r = Math.max( rMin, r );
}
let getPos = function( ele, i ){
let theta = options.startAngle + i * dTheta * ( clockwise ? 1 : -1 );
let rx = r * Math.cos( theta );
let ry = r * Math.sin( theta );
let pos = {
x: center.x + rx,
y: center.y + ry
};
return pos;
};
eles.nodes().layoutPositions( this, options, getPos );
return this; // chaining
};
export default CircleLayout;
+199
View File
@@ -0,0 +1,199 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
let defaults = {
fit: true, // whether to fit the viewport to the graph
padding: 30, // the padding on fit
startAngle: 3 / 2 * Math.PI, // where nodes start in radians
sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
equidistant: false, // whether levels have an equal radial distance betwen them, may cause bounding box overflow
minNodeSpacing: 10, // min spacing between outside of nodes (used for radius adjustment)
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
height: undefined, // height of layout area (overrides container height)
width: undefined, // width of layout area (overrides container width)
spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
concentric: function( node ){ // returns numeric value for each node, placing higher nodes in levels towards the centre
return node.degree();
},
levelWidth: function( nodes ){ // the variation of concentric values in each level
return nodes.maxDegree() / 4;
},
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function ConcentricLayout( options ){
this.options = util.extend( {}, defaults, options );
}
ConcentricLayout.prototype.run = function(){
let params = this.options;
let options = params;
let clockwise = options.counterclockwise !== undefined ? !options.counterclockwise : options.clockwise;
let cy = params.cy;
let eles = options.eles;
let nodes = eles.nodes().not( ':parent' );
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let center = {
x: bb.x1 + bb.w / 2,
y: bb.y1 + bb.h / 2
};
let nodeValues = []; // { node, value }
let maxNodeSize = 0;
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[ i ];
let value;
// calculate the node value
value = options.concentric( node );
nodeValues.push( {
value: value,
node: node
} );
// for style mapping
node._private.scratch.concentric = value;
}
// in case we used the `concentric` in style
nodes.updateStyle();
// calculate max size now based on potentially updated mappers
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[ i ];
let nbb = node.layoutDimensions( options );
maxNodeSize = Math.max( maxNodeSize, nbb.w, nbb.h );
}
// sort node values in descreasing order
nodeValues.sort( function( a, b ){
return b.value - a.value;
} );
let levelWidth = options.levelWidth( nodes );
// put the values into levels
let levels = [ [] ];
let currentLevel = levels[0];
for( let i = 0; i < nodeValues.length; i++ ){
let val = nodeValues[ i ];
if( currentLevel.length > 0 ){
let diff = Math.abs( currentLevel[0].value - val.value );
if( diff >= levelWidth ){
currentLevel = [];
levels.push( currentLevel );
}
}
currentLevel.push( val );
}
// create positions from levels
let minDist = maxNodeSize + options.minNodeSpacing; // min dist between nodes
if( !options.avoidOverlap ){ // then strictly constrain to bb
let firstLvlHasMulti = levels.length > 0 && levels[0].length > 1;
let maxR = ( Math.min( bb.w, bb.h ) / 2 - minDist );
let rStep = maxR / ( levels.length + firstLvlHasMulti ? 1 : 0 );
minDist = Math.min( minDist, rStep );
}
// find the metrics for each level
let r = 0;
for( let i = 0; i < levels.length; i++ ){
let level = levels[ i ];
let sweep = options.sweep === undefined ? 2 * Math.PI - 2 * Math.PI / level.length : options.sweep;
let dTheta = level.dTheta = sweep / ( Math.max( 1, level.length - 1 ) );
// calculate the radius
if( level.length > 1 && options.avoidOverlap ){ // but only if more than one node (can't overlap)
let dcos = Math.cos( dTheta ) - Math.cos( 0 );
let dsin = Math.sin( dTheta ) - Math.sin( 0 );
let rMin = Math.sqrt( minDist * minDist / ( dcos * dcos + dsin * dsin ) ); // s.t. no nodes overlapping
r = Math.max( rMin, r );
}
level.r = r;
r += minDist;
}
if( options.equidistant ){
let rDeltaMax = 0;
let r = 0;
for( let i = 0; i < levels.length; i++ ){
let level = levels[ i ];
let rDelta = level.r - r;
rDeltaMax = Math.max( rDeltaMax, rDelta );
}
r = 0;
for( let i = 0; i < levels.length; i++ ){
let level = levels[ i ];
if( i === 0 ){
r = level.r;
}
level.r = r;
r += rDeltaMax;
}
}
// calculate the node positions
let pos = {}; // id => position
for( let i = 0; i < levels.length; i++ ){
let level = levels[ i ];
let dTheta = level.dTheta;
let r = level.r;
for( let j = 0; j < level.length; j++ ){
let val = level[ j ];
let theta = options.startAngle + (clockwise ? 1 : -1) * dTheta * j;
let p = {
x: center.x + r * Math.cos( theta ),
y: center.y + r * Math.sin( theta )
};
pos[ val.node.id() ] = p;
}
}
// position the nodes
eles.nodes().layoutPositions( this, options, function( ele ){
let id = ele.id();
return pos[ id ];
} );
return this; // chaining
};
export default ConcentricLayout;
File diff suppressed because it is too large Load Diff
+247
View File
@@ -0,0 +1,247 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
let defaults = {
fit: true, // whether to fit the viewport to the graph
padding: 30, // padding used on fit
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
avoidOverlapPadding: 10, // extra spacing around nodes when avoidOverlap: true
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
condense: false, // uses all available space on false, uses minimal space on true
rows: undefined, // force num of rows in the grid
cols: undefined, // force num of columns in the grid
position: function( node ){}, // returns { row, col } for element
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function GridLayout( options ){
this.options = util.extend( {}, defaults, options );
}
GridLayout.prototype.run = function(){
let params = this.options;
let options = params;
let cy = params.cy;
let eles = options.eles;
let nodes = eles.nodes().not( ':parent' );
if( options.sort ){
nodes = nodes.sort( options.sort );
}
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
if( bb.h === 0 || bb.w === 0 ){
eles.nodes().layoutPositions( this, options, function( ele ){
return { x: bb.x1, y: bb.y1 };
} );
} else {
// width/height * splits^2 = cells where splits is number of times to split width
let cells = nodes.size();
let splits = Math.sqrt( cells * bb.h / bb.w );
let rows = Math.round( splits );
let cols = Math.round( bb.w / bb.h * splits );
let small = function( val ){
if( val == null ){
return Math.min( rows, cols );
} else {
let min = Math.min( rows, cols );
if( min == rows ){
rows = val;
} else {
cols = val;
}
}
};
let large = function( val ){
if( val == null ){
return Math.max( rows, cols );
} else {
let max = Math.max( rows, cols );
if( max == rows ){
rows = val;
} else {
cols = val;
}
}
};
let oRows = options.rows;
let oCols = options.cols != null ? options.cols : options.columns;
// if rows or columns were set in options, use those values
if( oRows != null && oCols != null ){
rows = oRows;
cols = oCols;
} else if( oRows != null && oCols == null ){
rows = oRows;
cols = Math.ceil( cells / rows );
} else if( oRows == null && oCols != null ){
cols = oCols;
rows = Math.ceil( cells / cols );
}
// otherwise use the automatic values and adjust accordingly
// if rounding was up, see if we can reduce rows or columns
else if( cols * rows > cells ){
let sm = small();
let lg = large();
// reducing the small side takes away the most cells, so try it first
if( (sm - 1) * lg >= cells ){
small( sm - 1 );
} else if( (lg - 1) * sm >= cells ){
large( lg - 1 );
}
} else {
// if rounding was too low, add rows or columns
while( cols * rows < cells ){
let sm = small();
let lg = large();
// try to add to larger side first (adds less in multiplication)
if( (lg + 1) * sm >= cells ){
large( lg + 1 );
} else {
small( sm + 1 );
}
}
}
let cellWidth = bb.w / cols;
let cellHeight = bb.h / rows;
if( options.condense ){
cellWidth = 0;
cellHeight = 0;
}
if( options.avoidOverlap ){
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[ i ];
let pos = node._private.position;
if( pos.x == null || pos.y == null ){ // for bb
pos.x = 0;
pos.y = 0;
}
let nbb = node.layoutDimensions( options );
let p = options.avoidOverlapPadding;
let w = nbb.w + p;
let h = nbb.h + p;
cellWidth = Math.max( cellWidth, w );
cellHeight = Math.max( cellHeight, h );
}
}
let cellUsed = {}; // e.g. 'c-0-2' => true
let used = function( row, col ){
return cellUsed[ 'c-' + row + '-' + col ] ? true : false;
};
let use = function( row, col ){
cellUsed[ 'c-' + row + '-' + col ] = true;
};
// to keep track of current cell position
let row = 0;
let col = 0;
let moveToNextCell = function(){
col++;
if( col >= cols ){
col = 0;
row++;
}
};
// get a cache of all the manual positions
let id2manPos = {};
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[ i ];
let rcPos = options.position( node );
if( rcPos && (rcPos.row !== undefined || rcPos.col !== undefined) ){ // must have at least row or col def'd
let pos = {
row: rcPos.row,
col: rcPos.col
};
if( pos.col === undefined ){ // find unused col
pos.col = 0;
while( used( pos.row, pos.col ) ){
pos.col++;
}
} else if( pos.row === undefined ){ // find unused row
pos.row = 0;
while( used( pos.row, pos.col ) ){
pos.row++;
}
}
id2manPos[ node.id() ] = pos;
use( pos.row, pos.col );
}
}
let getPos = function( element, i ){
let x, y;
if( element.locked() || element.isParent() ){
return false;
}
// see if we have a manual position set
let rcPos = id2manPos[ element.id() ];
if( rcPos ){
x = rcPos.col * cellWidth + cellWidth / 2 + bb.x1;
y = rcPos.row * cellHeight + cellHeight / 2 + bb.y1;
} else { // otherwise set automatically
while( used( row, col ) ){
moveToNextCell();
}
x = col * cellWidth + cellWidth / 2 + bb.x1;
y = row * cellHeight + cellHeight / 2 + bb.y1;
use( row, col );
moveToNextCell();
}
return { x: x, y: y };
};
nodes.layoutPositions( this, options, getPos );
}
return this; // chaining
};
export default GridLayout;
+19
View File
@@ -0,0 +1,19 @@
import breadthfirstLayout from './breadthfirst.mjs';
import circleLayout from './circle.mjs';
import concentricLayout from './concentric.mjs';
import coseLayout from './cose.mjs';
import gridLayout from './grid.mjs';
import nullLayout from './null.mjs';
import presetLayout from './preset.mjs';
import randomLayout from './random.mjs';
export default [
{ name: 'breadthfirst', impl: breadthfirstLayout },
{ name: 'circle', impl: circleLayout },
{ name: 'concentric',impl: concentricLayout },
{ name: 'cose', impl: coseLayout },
{ name: 'grid', impl: gridLayout },
{ name: 'null', impl: nullLayout },
{ name: 'preset', impl: presetLayout },
{ name: 'random', impl: randomLayout }
];
+53
View File
@@ -0,0 +1,53 @@
import * as util from '../../util/index.mjs';
// default layout options
let defaults = {
ready: function(){}, // on layoutready
stop: function(){} // on layoutstop
};
// constructor
// options : object containing layout options
function NullLayout( options ){
this.options = util.extend( {}, defaults, options );
}
// runs the layout
NullLayout.prototype.run = function(){
let options = this.options;
let eles = options.eles; // elements to consider in the layout
let layout = this;
// cy is automatically populated for us in the constructor
// (disable eslint for next line as this serves as example layout code to external developers)
// eslint-disable-next-line no-unused-vars
let cy = options.cy;
layout.emit( 'layoutstart' );
// puts all nodes at (0, 0)
// n.b. most layouts would use layoutPositions(), instead of positions() and manual events
eles.nodes().positions( function(){
return {
x: 0,
y: 0
};
} );
// trigger layoutready when each node has had its position set at least once
layout.one( 'layoutready', options.ready );
layout.emit( 'layoutready' );
// trigger layoutstop when the layout stops (e.g. finishes)
layout.one( 'layoutstop', options.stop );
layout.emit( 'layoutstop' );
return this; // chaining
};
// called on continuous layouts to stop them before they finish
NullLayout.prototype.stop = function(){
return this; // chaining
};
export default NullLayout;
+63
View File
@@ -0,0 +1,63 @@
import * as util from '../../util/index.mjs';
import * as is from '../../is.mjs';
import { copyPosition } from '../../math.mjs';
let defaults = {
positions: undefined, // map of (node id) => (position obj); or function(node){ return somPos; }
zoom: undefined, // the zoom level to set (prob want fit = false if set)
pan: undefined, // the pan level to set (prob want fit = false if set)
fit: true, // whether to fit to viewport
padding: 30, // padding on fit
spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function PresetLayout( options ){
this.options = util.extend( {}, defaults, options );
}
PresetLayout.prototype.run = function(){
let options = this.options;
let eles = options.eles;
let nodes = eles.nodes();
let posIsFn = is.fn( options.positions );
function getPosition( node ){
if( options.positions == null ){
return copyPosition( node.position() );
}
if( posIsFn ){
return options.positions( node );
}
let pos = options.positions[ node._private.data.id ];
if( pos == null ){
return null;
}
return pos;
}
nodes.layoutPositions( this, options, function( node, i ){
let position = getPosition( node );
if( node.locked() || position == null ){
return false;
}
return position;
} );
return this; // chaining
};
export default PresetLayout;
+43
View File
@@ -0,0 +1,43 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
let defaults = {
fit: true, // whether to fit to viewport
padding: 30, // fit padding
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function RandomLayout( options ){
this.options = util.extend( {}, defaults, options );
}
RandomLayout.prototype.run = function(){
let options = this.options;
let cy = options.cy;
let eles = options.eles;
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let getPos = function( node, i ){
return {
x: bb.x1 + Math.round( Math.random() * bb.w ),
y: bb.y1 + Math.round( Math.random() * bb.h )
};
};
eles.nodes().layoutPositions( this, options, getPos );
return this; // chaining
};
export default RandomLayout;
@@ -0,0 +1,344 @@
import * as math from '../../../math.mjs';
import * as is from '../../../is.mjs';
import * as util from '../../../util/index.mjs';
var BRp = {};
BRp.arrowShapeWidth = 0.3;
BRp.registerArrowShapes = function(){
var arrowShapes = this.arrowShapes = {};
var renderer = this;
// Contract for arrow shapes:
// 0, 0 is arrow tip
// (0, 1) is direction towards node
// (1, 0) is right
//
// functional api:
// collide: check x, y in shape
// roughCollide: called before collide, no false negatives
// draw: draw
// spacing: dist(arrowTip, nodeBoundary)
// gap: dist(edgeTip, nodeBoundary), edgeTip may != arrowTip
var bbCollide = function( x, y, size, angle, translation, edgeWidth, padding ){
var x1 = translation.x - size / 2 - padding;
var x2 = translation.x + size / 2 + padding;
var y1 = translation.y - size / 2 - padding;
var y2 = translation.y + size / 2 + padding;
var inside = (x1 <= x && x <= x2) && (y1 <= y && y <= y2);
return inside;
};
var transform = function( x, y, size, angle, translation ){
var xRotated = x * Math.cos( angle ) - y * Math.sin( angle );
var yRotated = x * Math.sin( angle ) + y * Math.cos( angle );
var xScaled = xRotated * size;
var yScaled = yRotated * size;
var xTranslated = xScaled + translation.x;
var yTranslated = yScaled + translation.y;
return {
x: xTranslated,
y: yTranslated
};
};
var transformPoints = function( pts, size, angle, translation ){
var retPts = [];
for( var i = 0; i < pts.length; i += 2 ){
var x = pts[ i ];
var y = pts[ i + 1];
retPts.push( transform( x, y, size, angle, translation ) );
}
return retPts;
};
var pointsToArr = function( pts ){
var ret = [];
for( var i = 0; i < pts.length; i++ ){
var p = pts[ i ];
ret.push( p.x, p.y );
}
return ret;
};
var standardGap = function( edge ) {
return edge.pstyle( 'width' ).pfValue * edge.pstyle( 'arrow-scale' ).pfValue * 2;
};
var defineArrowShape = function( name, defn ){
if( is.string( defn ) ){
defn = arrowShapes[ defn ];
}
arrowShapes[ name ] = util.extend( {
name: name,
points: [
-0.15, -0.3,
0.15, -0.3,
0.15, 0.3,
-0.15, 0.3
],
collide: function( x, y, size, angle, translation, padding ){
var points = pointsToArr( transformPoints( this.points, size + 2 * padding, angle, translation ) );
var inside = math.pointInsidePolygonPoints( x, y, points );
return inside;
},
roughCollide: bbCollide,
draw: function( context, size, angle, translation ){
var points = transformPoints( this.points, size, angle, translation );
renderer.arrowShapeImpl( 'polygon' )( context, points );
},
spacing: function( edge ){
return 0;
},
gap: standardGap
}, defn );
};
defineArrowShape( 'none', {
collide: util.falsify,
roughCollide: util.falsify,
draw: util.noop,
spacing: util.zeroify,
gap: util.zeroify
} );
defineArrowShape( 'triangle', {
points: [
-0.15, -0.3,
0, 0,
0.15, -0.3
]
} );
defineArrowShape( 'arrow', 'triangle' );
defineArrowShape( 'triangle-backcurve', {
points: arrowShapes[ 'triangle' ].points,
controlPoint: [ 0, -0.15 ],
roughCollide: bbCollide,
draw: function( context, size, angle, translation, edgeWidth ){
var ptsTrans = transformPoints( this.points, size, angle, translation );
var ctrlPt = this.controlPoint;
var ctrlPtTrans = transform( ctrlPt[0], ctrlPt[1], size, angle, translation );
renderer.arrowShapeImpl( this.name )( context, ptsTrans, ctrlPtTrans );
},
gap: function( edge ) {
return standardGap(edge) * 0.8;
}
} );
defineArrowShape( 'triangle-tee', {
points: [
0, 0,
0.15, -0.3,
-0.15, -0.3,
0, 0
],
pointsTee: [
-0.15, -0.4,
-0.15, -0.5,
0.15, -0.5,
0.15, -0.4
],
collide: function( x, y, size, angle, translation, edgeWidth, padding ){
var triPts = pointsToArr( transformPoints( this.points, size + 2 * padding, angle, translation ) );
var teePts = pointsToArr( transformPoints( this.pointsTee, size + 2 * padding, angle, translation ) );
var inside = math.pointInsidePolygonPoints( x, y, triPts ) || math.pointInsidePolygonPoints( x, y, teePts );
return inside;
},
draw: function( context, size, angle, translation, edgeWidth ){
var triPts = transformPoints( this.points, size, angle, translation );
var teePts = transformPoints( this.pointsTee, size, angle, translation );
renderer.arrowShapeImpl( this.name )( context, triPts, teePts );
}
} );
defineArrowShape( 'circle-triangle', {
radius: 0.15,
pointsTr: [0, -0.15, 0.15, -0.45, -0.15, -0.45, 0, -0.15],
collide: function collide(x, y, size, angle, translation, edgeWidth, padding) {
var t = translation;
var circleInside = Math.pow(t.x - x, 2) + Math.pow(t.y - y, 2) <= Math.pow((size + 2 * padding) * this.radius, 2);
var triPts = pointsToArr(transformPoints(this.points, size + 2 * padding, angle, translation));
return math.pointInsidePolygonPoints(x, y, triPts) || circleInside;
},
draw: function draw(context, size, angle, translation, edgeWidth) {
var triPts = transformPoints(this.pointsTr, size, angle, translation);
renderer.arrowShapeImpl(this.name)(context, triPts, translation.x, translation.y, this.radius * size);
},
spacing: function spacing(edge) {
return renderer.getArrowWidth(edge.pstyle('width').pfValue, edge.pstyle('arrow-scale').value) * this.radius;
}
} );
defineArrowShape( 'triangle-cross', {
points: [
0, 0,
0.15, -0.3,
-0.15, -0.3,
0, 0
],
baseCrossLinePts: [
-0.15, -0.4, // first half of the rectangle
-0.15, -0.4,
0.15, -0.4, // second half of the rectangle
0.15, -0.4
],
crossLinePts: function( size, edgeWidth ){
// shift points so that the distance between the cross points matches edge width
var p = this.baseCrossLinePts.slice();
var shiftFactor = edgeWidth / size;
var y0 = 3;
var y1 = 5;
p[y0] = p[y0] - shiftFactor;
p[y1] = p[y1] - shiftFactor;
return p;
},
collide: function( x, y, size, angle, translation, edgeWidth, padding ){
var triPts = pointsToArr( transformPoints( this.points, size + 2 * padding, angle, translation ) );
var teePts = pointsToArr( transformPoints( this.crossLinePts( size, edgeWidth ), size + 2 * padding, angle, translation ) );
var inside = math.pointInsidePolygonPoints( x, y, triPts ) || math.pointInsidePolygonPoints( x, y, teePts );
return inside;
},
draw: function( context, size, angle, translation, edgeWidth ){
var triPts = transformPoints( this.points, size, angle, translation );
var crossLinePts = transformPoints( this.crossLinePts( size, edgeWidth ), size, angle, translation );
renderer.arrowShapeImpl( this.name )( context, triPts, crossLinePts );
}
} );
defineArrowShape( 'vee', {
points: [
-0.15, -0.3,
0, 0,
0.15, -0.3,
0, -0.15
],
gap: function( edge ){
return standardGap(edge) * 0.525;
}
} );
defineArrowShape( 'circle', {
radius: 0.15,
collide: function( x, y, size, angle, translation, edgeWidth, padding ){
var t = translation;
var inside = ( Math.pow( t.x - x, 2 ) + Math.pow( t.y - y, 2 ) <= Math.pow( (size + 2 * padding) * this.radius, 2 ) );
return inside;
},
draw: function( context, size, angle, translation, edgeWidth ){
renderer.arrowShapeImpl( this.name )( context, translation.x, translation.y, this.radius * size );
},
spacing: function( edge ){
return renderer.getArrowWidth( edge.pstyle( 'width' ).pfValue, edge.pstyle( 'arrow-scale' ).value )
* this.radius;
}
} );
defineArrowShape( 'tee', {
points: [
-0.15, 0,
-0.15, -0.1,
0.15, -0.1,
0.15, 0
],
spacing: function( edge ){
return 1;
},
gap: function( edge ){
return 1;
}
} );
defineArrowShape( 'square', {
points: [
-0.15, 0.00,
0.15, 0.00,
0.15, -0.3,
-0.15, -0.3
]
} );
defineArrowShape( 'diamond', {
points: [
-0.15, -0.15,
0, -0.3,
0.15, -0.15,
0, 0
],
gap: function( edge ){
return edge.pstyle( 'width' ).pfValue * edge.pstyle( 'arrow-scale' ).value;
}
} );
defineArrowShape( 'chevron', {
points: [
0, 0,
-0.15, -0.15,
-0.1, -0.2,
0, -0.1,
0.1, -0.2,
0.15, -0.15
],
gap: function( edge ){
return 0.95 * edge.pstyle( 'width' ).pfValue * edge.pstyle( 'arrow-scale' ).value;
}
} );
};
export default BRp;
@@ -0,0 +1,580 @@
import * as math from '../../../../math.mjs';
import * as util from '../../../../util/index.mjs';
var BRp = {};
// Project mouse
BRp.projectIntoViewport = function( clientX, clientY ){
var cy = this.cy;
var offsets = this.findContainerClientCoords();
var offsetLeft = offsets[0];
var offsetTop = offsets[1];
var scale = offsets[4];
var pan = cy.pan();
var zoom = cy.zoom();
var x = ( (clientX - offsetLeft)/scale - pan.x ) / zoom;
var y = ( (clientY - offsetTop)/scale - pan.y ) / zoom;
return [ x, y ];
};
BRp.findContainerClientCoords = function(){
if( this.containerBB ){
return this.containerBB;
}
var container = this.container;
var rect = container.getBoundingClientRect();
var style = this.cy.window().getComputedStyle( container );
var styleValue = function( name ){ return parseFloat( style.getPropertyValue( name ) ); };
var padding = {
left: styleValue('padding-left'),
right: styleValue('padding-right'),
top: styleValue('padding-top'),
bottom: styleValue('padding-bottom')
};
var border = {
left: styleValue('border-left-width'),
right: styleValue('border-right-width'),
top: styleValue('border-top-width'),
bottom: styleValue('border-bottom-width')
};
var clientWidth = container.clientWidth;
var clientHeight = container.clientHeight;
var paddingHor = padding.left + padding.right;
var paddingVer = padding.top + padding.bottom;
var borderHor = border.left + border.right;
var scale = rect.width / ( clientWidth + borderHor );
var unscaledW = clientWidth - paddingHor;
var unscaledH = clientHeight - paddingVer;
var left = rect.left + padding.left + border.left;
var top = rect.top + padding.top + border.top;
return ( this.containerBB = [
left,
top,
unscaledW,
unscaledH,
scale
] );
};
BRp.invalidateContainerClientCoordsCache = function(){
this.containerBB = null;
};
BRp.findNearestElement = function( x, y, interactiveElementsOnly, isTouch ){
return this.findNearestElements( x, y, interactiveElementsOnly, isTouch )[0];
};
BRp.findNearestElements = function( x, y, interactiveElementsOnly, isTouch ){
var self = this;
var r = this;
var eles = r.getCachedZSortedEles();
var near = []; // 1 node max, 1 edge max
var zoom = r.cy.zoom();
var hasCompounds = r.cy.hasCompoundNodes();
var edgeThreshold = (isTouch ? 24 : 8) / zoom;
var nodeThreshold = (isTouch ? 8 : 2) / zoom;
var labelThreshold = (isTouch ? 8 : 2) / zoom;
var minSqDist = Infinity;
var nearEdge;
var nearNode;
if( interactiveElementsOnly ){
eles = eles.interactive;
}
function addEle( ele, sqDist ){
if( ele.isNode() ){
if( nearNode ){
return; // can't replace node
} else {
nearNode = ele;
near.push( ele );
}
}
if( ele.isEdge() && ( sqDist == null || sqDist < minSqDist ) ){
if( nearEdge ){ // then replace existing edge
// can replace only if same z-index
if(
nearEdge.pstyle('z-compound-depth').value === ele.pstyle('z-compound-depth').value
&& nearEdge.pstyle('z-compound-depth').value === ele.pstyle('z-compound-depth').value
){
for( var i = 0; i < near.length; i++ ){
if( near[i].isEdge() ){
near[i] = ele;
nearEdge = ele;
minSqDist = sqDist != null ? sqDist : minSqDist;
break;
}
}
}
} else {
near.push( ele );
nearEdge = ele;
minSqDist = sqDist != null ? sqDist : minSqDist;
}
}
}
function checkNode( node ){
var width = node.outerWidth() + 2 * nodeThreshold;
var height = node.outerHeight() + 2 * nodeThreshold;
var hw = width / 2;
var hh = height / 2;
var pos = node.position();
var cornerRadius = node.pstyle('corner-radius').value === 'auto' ? 'auto' : node.pstyle('corner-radius').pfValue;
var rs = node._private.rscratch;
if(
pos.x - hw <= x && x <= pos.x + hw // bb check x
&&
pos.y - hh <= y && y <= pos.y + hh // bb check y
){
var shape = r.nodeShapes[ self.getNodeShape( node ) ];
if(
shape.checkPoint( x, y, 0, width, height, pos.x, pos.y, cornerRadius, rs )
){
addEle( node, 0 );
return true;
}
}
}
function checkEdge( edge ){
var _p = edge._private;
var rs = _p.rscratch;
var styleWidth = edge.pstyle( 'width' ).pfValue;
var scale = edge.pstyle( 'arrow-scale' ).value;
var width = styleWidth / 2 + edgeThreshold; // more like a distance radius from centre
var widthSq = width * width;
var width2 = width * 2;
var src = _p.source;
var tgt = _p.target;
var sqDist;
if( rs.edgeType === 'segments' || rs.edgeType === 'straight' || rs.edgeType === 'haystack' ){
var pts = rs.allpts;
for( var i = 0; i + 3 < pts.length; i += 2 ){
if(
(math.inLineVicinity( x, y, pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3], width2 ))
&&
widthSq > ( sqDist = math.sqdistToFiniteLine( x, y, pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3] ) )
){
addEle( edge, sqDist );
return true;
}
}
} else if( rs.edgeType === 'bezier' || rs.edgeType === 'multibezier' || rs.edgeType === 'self' || rs.edgeType === 'compound' ){
var pts = rs.allpts;
for( var i = 0; i + 5 < rs.allpts.length; i += 4 ){
if(
(math.inBezierVicinity( x, y, pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3], pts[ i + 4], pts[ i + 5], width2 ))
&&
(widthSq > (sqDist = math.sqdistToQuadraticBezier( x, y, pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3], pts[ i + 4], pts[ i + 5] )) )
){
addEle( edge, sqDist );
return true;
}
}
}
// if we're close to the edge but didn't hit it, maybe we hit its arrows
var src = src || _p.source;
var tgt = tgt || _p.target;
var arSize = self.getArrowWidth( styleWidth, scale );
var arrows = [
{ name: 'source', x: rs.arrowStartX, y: rs.arrowStartY, angle: rs.srcArrowAngle },
{ name: 'target', x: rs.arrowEndX, y: rs.arrowEndY, angle: rs.tgtArrowAngle },
{ name: 'mid-source', x: rs.midX, y: rs.midY, angle: rs.midsrcArrowAngle },
{ name: 'mid-target', x: rs.midX, y: rs.midY, angle: rs.midtgtArrowAngle }
];
for( var i = 0; i < arrows.length; i++ ){
var ar = arrows[ i ];
var shape = r.arrowShapes[ edge.pstyle( ar.name + '-arrow-shape' ).value ];
var edgeWidth = edge.pstyle('width').pfValue;
if(
shape.roughCollide( x, y, arSize, ar.angle, { x: ar.x, y: ar.y }, edgeWidth, edgeThreshold )
&&
shape.collide( x, y, arSize, ar.angle, { x: ar.x, y: ar.y }, edgeWidth, edgeThreshold )
){
addEle( edge );
return true;
}
}
// for compound graphs, hitting edge may actually want a connected node instead (b/c edge may have greater z-index precedence)
if( hasCompounds && near.length > 0 ){
checkNode( src );
checkNode( tgt );
}
}
function preprop( obj, name, pre ){
return util.getPrefixedProperty( obj, name, pre );
}
function checkLabel( ele, prefix ){
var _p = ele._private;
var th = labelThreshold;
var prefixDash;
if( prefix ){
prefixDash = prefix + '-';
} else {
prefixDash = '';
}
ele.boundingBox();
var bb = _p.labelBounds[prefix || 'main'];
var text = ele.pstyle( prefixDash + 'label' ).value;
var eventsEnabled = ele.pstyle( 'text-events' ).strValue === 'yes';
if( !eventsEnabled || !text ){ return; }
var lx = preprop( _p.rscratch, 'labelX', prefix );
var ly = preprop( _p.rscratch, 'labelY', prefix );
var theta = preprop( _p.rscratch, 'labelAngle', prefix );
var ox = ele.pstyle(prefixDash + 'text-margin-x').pfValue;
let oy = ele.pstyle(prefixDash + 'text-margin-y').pfValue;
var lx1 = bb.x1 - th - ox; // (-ox, -oy) as bb already includes margin
var lx2 = bb.x2 + th - ox; // and rotation is about (lx, ly)
var ly1 = bb.y1 - th - oy;
var ly2 = bb.y2 + th - oy;
if( theta ){
var cos = Math.cos( theta );
var sin = Math.sin( theta );
var rotate = function( x, y ){
x = x - lx;
y = y - ly;
return {
x: x * cos - y * sin + lx,
y: x * sin + y * cos + ly
};
};
var px1y1 = rotate( lx1, ly1 );
var px1y2 = rotate( lx1, ly2 );
var px2y1 = rotate( lx2, ly1 );
var px2y2 = rotate( lx2, ly2 );
var points = [ // with the margin added after the rotation is applied
px1y1.x + ox, px1y1.y + oy,
px2y1.x + ox, px2y1.y + oy,
px2y2.x + ox, px2y2.y + oy,
px1y2.x + ox, px1y2.y + oy
];
if( math.pointInsidePolygonPoints( x, y, points ) ){
addEle( ele );
return true;
}
} else { // do a cheaper bb check
if( math.inBoundingBox( bb, x, y ) ){
addEle( ele );
return true;
}
}
}
for( var i = eles.length - 1; i >= 0; i-- ){ // reverse order for precedence
var ele = eles[ i ];
if( ele.isNode() ){
checkNode( ele ) || checkLabel( ele );
} else { // then edge
checkEdge( ele ) || checkLabel( ele ) || checkLabel( ele, 'source' ) || checkLabel( ele, 'target' );
}
}
return near;
};
// 'Give me everything from this box'
BRp.getAllInBox = function( x1, y1, x2, y2 ){
var eles = this.getCachedZSortedEles().interactive;
var zoom = this.cy.zoom();
var labelThreshold = 2 / zoom;
var box = [];
var x1c = Math.min( x1, x2 );
var x2c = Math.max( x1, x2 );
var y1c = Math.min( y1, y2 );
var y2c = Math.max( y1, y2 );
x1 = x1c;
x2 = x2c;
y1 = y1c;
y2 = y2c;
var boxBb = math.makeBoundingBox( {
x1: x1, y1: y1,
x2: x2, y2: y2
} );
var selectionBox = [
{ x: boxBb.x1, y: boxBb.y1 },
{ x: boxBb.x2, y: boxBb.y1 },
{ x: boxBb.x2, y: boxBb.y2 },
{ x: boxBb.x1, y: boxBb.y2 },
];
var boxEdges = [
[selectionBox[0], selectionBox[1]],
[selectionBox[1], selectionBox[2]],
[selectionBox[2], selectionBox[3]],
[selectionBox[3], selectionBox[0]]
];
function preprop(obj, name, pre) {
return util.getPrefixedProperty(obj, name, pre);
}
function getRotatedLabelBox(ele, prefix) {
var _p = ele._private;
var th = labelThreshold;
var prefixDash = prefix ? prefix + '-' : '';
ele.boundingBox();
var bb = _p.labelBounds[prefix || 'main'];
// If the bounding box is not available, return null.
// This indicates that the label box cannot be calculated, which is consistent
// with the expected behavior of this function. Returning null allows the caller
// to handle the absence of a bounding box explicitly.
if (!bb) {
return null;
}
var lx = preprop(_p.rscratch, 'labelX', prefix);
var ly = preprop(_p.rscratch, 'labelY', prefix);
var theta = preprop(_p.rscratch, 'labelAngle', prefix);
var ox = ele.pstyle(prefixDash + 'text-margin-x').pfValue;
var oy = ele.pstyle(prefixDash + 'text-margin-y').pfValue;
var lx1 = bb.x1 - th - ox;
var lx2 = bb.x2 + th - ox;
var ly1 = bb.y1 - th - oy;
var ly2 = bb.y2 + th - oy;
if (theta) {
var cos = Math.cos(theta);
var sin = Math.sin(theta);
var rotate = function (x, y) {
x = x - lx;
y = y - ly;
return {
x: x * cos - y * sin + lx,
y: x * sin + y * cos + ly,
};
};
return [rotate(lx1, ly1), rotate(lx2, ly1), rotate(lx2, ly2), rotate(lx1, ly2)];
} else {
return [
{ x: lx1, y: ly1 },
{ x: lx2, y: ly1 },
{ x: lx2, y: ly2 },
{ x: lx1, y: ly2 },
];
}
}
function doLinesIntersect(p1, p2, q1, q2) {
function ccw(a, b, c) {
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
}
return ccw(p1, q1, q2) !== ccw(p2, q1, q2) && ccw(p1, p2, q1) !== ccw(p1, p2, q2);
}
for( var e = 0; e < eles.length; e++ ){
var ele = eles[e];
if( ele.isNode() ){
var node = ele;
var textEvents = node.pstyle('text-events').strValue === 'yes';
var nodeBoxSelectMode = node.pstyle('box-selection').strValue;
var labelBoxSelectEnabled = node.pstyle('box-select-labels').strValue === 'yes';
if ( nodeBoxSelectMode === 'none' ) {
continue;
}
var includeLabels = (nodeBoxSelectMode === 'overlap' || labelBoxSelectEnabled) && textEvents;
var nodeBb = node.boundingBox({
includeNodes: true,
includeEdges: false,
includeLabels,
});
if ( nodeBoxSelectMode === 'contain' ) {
let selected = false;
if (labelBoxSelectEnabled && textEvents) {
const rotatedLabelBox = getRotatedLabelBox(node);
if (rotatedLabelBox && math.satPolygonIntersection(rotatedLabelBox, selectionBox)) {
box.push(node);
selected = true;
}
}
if (!selected && math.boundingBoxInBoundingBox(boxBb, nodeBb)) {
box.push(node);
}
} else if ( nodeBoxSelectMode === 'overlap' ) {
if (math.boundingBoxesIntersect(boxBb, nodeBb)) {
const nodeBodyBb = node.boundingBox({
includeNodes: true,
includeEdges: true,
includeLabels: false,
includeMainLabels: false,
includeSourceLabels: false,
includeTargetLabels: false
});
const nodeBodyCorners = [
{ x: nodeBodyBb.x1, y: nodeBodyBb.y1 },
{ x: nodeBodyBb.x2, y: nodeBodyBb.y1 },
{ x: nodeBodyBb.x2, y: nodeBodyBb.y2 },
{ x: nodeBodyBb.x1, y: nodeBodyBb.y2 },
];
// if node body intersects, no need to check label
if (math.satPolygonIntersection(nodeBodyCorners, selectionBox)) {
box.push(node);
} else {
// only check label if node body didn't intersect
const rotatedLabelBox = getRotatedLabelBox(node);
if (rotatedLabelBox && math.satPolygonIntersection(rotatedLabelBox, selectionBox)) {
box.push(node);
}
}
}
}
} else {
var edge = ele;
var _p = edge._private;
var rs = _p.rscratch;
var edgeBoxSelectMode = edge.pstyle('box-selection').strValue;
if ( edgeBoxSelectMode === 'none' ) {
continue;
}
if ( edgeBoxSelectMode === 'contain' ) {
if( rs.startX != null && rs.startY != null && !math.inBoundingBox( boxBb, rs.startX, rs.startY ) ){ continue; }
if( rs.endX != null && rs.endY != null && !math.inBoundingBox( boxBb, rs.endX, rs.endY ) ){ continue; }
if( rs.edgeType === 'bezier' || rs.edgeType === 'multibezier' || rs.edgeType === 'self' || rs.edgeType === 'compound' || rs.edgeType === 'segments' || rs.edgeType === 'haystack' ){
let pts = _p.rstyle.bezierPts || _p.rstyle.linePts || _p.rstyle.haystackPts;
let allInside = true;
for( var i = 0; i < pts.length; i++ ){
if( !math.pointInBoundingBox( boxBb, pts[ i ] ) ){
allInside = false;
break;
}
}
if( allInside ){
box.push( edge );
}
} else if( rs.edgeType === 'straight' ){
box.push( edge );
}
} else if ( edgeBoxSelectMode === 'overlap' ) {
let selected = false;
// Check: either endpoint inside box
if (
rs.startX != null && rs.startY != null &&
rs.endX != null && rs.endY != null &&
(math.inBoundingBox(boxBb, rs.startX, rs.startY) || math.inBoundingBox(boxBb, rs.endX, rs.endY))
) {
box.push(edge);
selected = true;
}
// Haystack fallback (only check if not already selected)
else if (!selected && rs.edgeType === 'haystack') {
const haystackPts = _p.rstyle.haystackPts;
for (let i = 0; i < haystackPts.length; i++) {
if (math.pointInBoundingBox(boxBb, haystackPts[i])) {
box.push(edge);
selected = true;
break;
}
}
}
// Segment intersection check (only if not already selected)
if (!selected) {
let pts = _p.rstyle.bezierPts || _p.rstyle.linePts || _p.rstyle.haystackPts;
// straight edges
if ((!pts || pts.length < 2) && rs.edgeType === 'straight') {
if (rs.startX != null && rs.startY != null && rs.endX != null && rs.endY != null) {
pts = [
{ x: rs.startX, y: rs.startY },
{ x: rs.endX, y: rs.endY }
];
}
}
if (!pts || pts.length < 2) continue;
for (let i = 0; i < pts.length - 1; i++) {
let segStart = pts[i];
let segEnd = pts[i + 1];
for (let b = 0; b < boxEdges.length; b++) {
let [boxStart, boxEnd] = boxEdges[b];
if (doLinesIntersect(segStart, segEnd, boxStart, boxEnd)) {
box.push(edge);
selected = true;
break;
}
}
if (selected) break;
}
}
}
}
}
return box;
};
export default BRp;
@@ -0,0 +1,181 @@
import * as math from '../../../../math.mjs';
var BRp = {};
BRp.calculateArrowAngles = function( edge ){
var rs = edge._private.rscratch;
var isHaystack = rs.edgeType === 'haystack';
var isBezier = rs.edgeType === 'bezier';
var isMultibezier = rs.edgeType === 'multibezier';
var isSegments = rs.edgeType === 'segments';
var isCompound = rs.edgeType === 'compound';
var isSelf = rs.edgeType === 'self';
// Displacement gives direction for arrowhead orientation
var dispX, dispY;
var startX, startY, endX, endY, midX, midY;
if( isHaystack ){
startX = rs.haystackPts[0];
startY = rs.haystackPts[1];
endX = rs.haystackPts[2];
endY = rs.haystackPts[3];
} else {
startX = rs.arrowStartX;
startY = rs.arrowStartY;
endX = rs.arrowEndX;
endY = rs.arrowEndY;
}
midX = rs.midX;
midY = rs.midY;
// source
//
if( isSegments ){
dispX = startX - rs.segpts[0];
dispY = startY - rs.segpts[1];
} else if( isMultibezier || isCompound || isSelf || isBezier ){
var pts = rs.allpts;
var bX = math.qbezierAt( pts[0], pts[2], pts[4], 0.1 );
var bY = math.qbezierAt( pts[1], pts[3], pts[5], 0.1 );
dispX = startX - bX;
dispY = startY - bY;
} else {
dispX = startX - midX;
dispY = startY - midY;
}
rs.srcArrowAngle = math.getAngleFromDisp( dispX, dispY );
// mid target
//
var midX = rs.midX;
var midY = rs.midY;
if( isHaystack ){
midX = ( startX + endX ) / 2;
midY = ( startY + endY ) / 2;
}
dispX = endX - startX;
dispY = endY - startY;
if( isSegments ){
var pts = rs.allpts;
if( pts.length / 2 % 2 === 0 ){
var i2 = pts.length / 2;
var i1 = i2 - 2;
dispX = ( pts[ i2 ] - pts[ i1 ] );
dispY = ( pts[ i2 + 1] - pts[ i1 + 1] );
} else if( rs.isRound ){
dispX = rs.midVector[1];
dispY = -rs.midVector[0];
} else {
var i2 = pts.length / 2 - 1;
var i1 = i2 - 2;
dispX = ( pts[ i2 ] - pts[ i1 ] );
dispY = ( pts[ i2 + 1] - pts[ i1 + 1] );
}
} else if( isMultibezier || isCompound || isSelf ){
var pts = rs.allpts;
var cpts = rs.ctrlpts;
var bp0x, bp0y;
var bp1x, bp1y;
if( cpts.length / 2 % 2 === 0 ){
var p0 = pts.length / 2 - 1; // startpt
var ic = p0 + 2;
var p1 = ic + 2;
bp0x = math.qbezierAt( pts[ p0 ], pts[ ic ], pts[ p1 ], 0.0 );
bp0y = math.qbezierAt( pts[ p0 + 1], pts[ ic + 1], pts[ p1 + 1], 0.0 );
bp1x = math.qbezierAt( pts[ p0 ], pts[ ic ], pts[ p1 ], 0.0001 );
bp1y = math.qbezierAt( pts[ p0 + 1], pts[ ic + 1], pts[ p1 + 1], 0.0001 );
} else {
var ic = pts.length / 2 - 1; // ctrpt
var p0 = ic - 2; // startpt
var p1 = ic + 2; // endpt
bp0x = math.qbezierAt( pts[ p0 ], pts[ ic ], pts[ p1 ], 0.4999 );
bp0y = math.qbezierAt( pts[ p0 + 1], pts[ ic + 1], pts[ p1 + 1], 0.4999 );
bp1x = math.qbezierAt( pts[ p0 ], pts[ ic ], pts[ p1 ], 0.5 );
bp1y = math.qbezierAt( pts[ p0 + 1], pts[ ic + 1], pts[ p1 + 1], 0.5 );
}
dispX = ( bp1x - bp0x );
dispY = ( bp1y - bp0y );
}
rs.midtgtArrowAngle = math.getAngleFromDisp( dispX, dispY );
rs.midDispX = dispX;
rs.midDispY = dispY;
// mid source
//
dispX *= -1;
dispY *= -1;
if( isSegments ){
var pts = rs.allpts;
if( pts.length / 2 % 2 === 0 ){
// already ok
} else if( !rs.isRound ){
var i2 = pts.length / 2 - 1;
var i3 = i2 + 2;
dispX = -( pts[ i3 ] - pts[ i2 ] );
dispY = -( pts[ i3 + 1] - pts[ i2 + 1] );
}
}
rs.midsrcArrowAngle = math.getAngleFromDisp( dispX, dispY );
// target
//
if( isSegments ){
dispX = endX - rs.segpts[ rs.segpts.length - 2 ];
dispY = endY - rs.segpts[ rs.segpts.length - 1 ];
} else if( isMultibezier || isCompound || isSelf || isBezier ){
var pts = rs.allpts;
var l = pts.length;
var bX = math.qbezierAt( pts[l-6], pts[l-4], pts[l-2], 0.9 );
var bY = math.qbezierAt( pts[l-5], pts[l-3], pts[l-1], 0.9 );
dispX = endX - bX;
dispY = endY - bY;
} else {
dispX = endX - midX;
dispY = endY - midY;
}
rs.tgtArrowAngle = math.getAngleFromDisp( dispX, dispY );
};
BRp.getArrowWidth = BRp.getArrowHeight = function( edgeWidth, scale ){
var cache = this.arrowWidthCache = this.arrowWidthCache || {};
var cachedVal = cache[ edgeWidth + ', ' + scale ];
if( cachedVal ){
return cachedVal;
}
cachedVal = Math.max( Math.pow( edgeWidth * 13.37, 0.9 ), 29 ) * scale;
cache[ edgeWidth + ', ' + scale ] = cachedVal;
return cachedVal;
};
export default BRp;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,356 @@
import * as math from '../../../../math.mjs';
import * as is from '../../../../is.mjs';
import {endsWith} from "../../../../util/index.mjs";
let BRp = {};
BRp.manualEndptToPx = function( node, prop ){
let r = this;
let npos = node.position();
let w = node.outerWidth();
let h = node.outerHeight();
let rs = node._private.rscratch;
if( prop.value.length === 2 ){
let p = [
prop.pfValue[0],
prop.pfValue[1]
];
if( prop.units[0] === '%' ){
p[0] = p[0] * w;
}
if( prop.units[1] === '%' ){
p[1] = p[1] * h;
}
p[0] += npos.x;
p[1] += npos.y;
return p;
} else {
let angle = prop.pfValue[0];
angle = -Math.PI / 2 + angle; // start at 12 o'clock
let l = 2 * Math.max( w, h );
let p = [
npos.x + Math.cos( angle ) * l,
npos.y + Math.sin( angle ) * l
];
return r.nodeShapes[ this.getNodeShape( node ) ].intersectLine(
npos.x, npos.y,
w, h,
p[0], p[1],
0, node.pstyle('corner-radius').value === 'auto' ? 'auto' : node.pstyle('corner-radius').pfValue, rs
);
}
};
BRp.findEndpoints = function( edge ){
let r = this;
let intersect;
let source = edge.source()[0];
let target = edge.target()[0];
let srcPos = source.position();
let tgtPos = target.position();
let tgtArShape = edge.pstyle( 'target-arrow-shape' ).value;
let srcArShape = edge.pstyle( 'source-arrow-shape' ).value;
let tgtDist = edge.pstyle( 'target-distance-from-node' ).pfValue;
let srcDist = edge.pstyle( 'source-distance-from-node' ).pfValue;
let srcRs = source._private.rscratch;
let tgtRs = target._private.rscratch;
let curveStyle = edge.pstyle('curve-style').value;
let rs = edge._private.rscratch;
let et = rs.edgeType;
let taxi = endsWith(curveStyle, 'taxi'); // Covers taxi and round-taxi
let self = et === 'self' || et === 'compound';
let bezier = et === 'bezier' || et === 'multibezier' || self;
let multi = et !== 'bezier';
let lines = et === 'straight' || et === 'segments';
let segments = et === 'segments';
let hasEndpts = bezier || multi || lines;
let overrideEndpts = self || taxi;
let srcManEndpt = edge.pstyle('source-endpoint');
let srcManEndptVal = overrideEndpts ? 'outside-to-node' : srcManEndpt.value;
let srcCornerRadius = source.pstyle('corner-radius').value === 'auto' ? 'auto' : source.pstyle('corner-radius').pfValue;
let tgtManEndpt = edge.pstyle('target-endpoint');
let tgtManEndptVal = overrideEndpts ? 'outside-to-node' : tgtManEndpt.value;
let tgtCornerRadius = target.pstyle('corner-radius').value === 'auto' ? 'auto' : target.pstyle('corner-radius').pfValue;
rs.srcManEndpt = srcManEndpt;
rs.tgtManEndpt = tgtManEndpt;
let p1; // last known point of edge on target side
let p2; // last known point of edge on source side
let p1_i; // point to intersect with target shape
let p2_i; // point to intersect with source shape
let tgtManEndptPt = (tgtManEndpt?.pfValue?.length === 2 ? tgtManEndpt.pfValue : null) ?? [0, 0];
let srcManEndptPt = (srcManEndpt?.pfValue?.length === 2 ? srcManEndpt.pfValue : null) ?? [0, 0];
if( bezier ){
let cpStart = [ rs.ctrlpts[0], rs.ctrlpts[1] ];
let cpEnd = multi ? [ rs.ctrlpts[ rs.ctrlpts.length - 2], rs.ctrlpts[ rs.ctrlpts.length - 1] ] : cpStart;
p1 = cpEnd;
p2 = cpStart;
} else if( lines ){
let srcArrowFromPt = !segments ? [
tgtPos.x + tgtManEndptPt[0],
tgtPos.y + tgtManEndptPt[1]
] : rs.segpts.slice( 0, 2 );
let tgtArrowFromPt = !segments ? [
srcPos.x + srcManEndptPt[0],
srcPos.y + srcManEndptPt[1]
] : rs.segpts.slice( rs.segpts.length - 2 );
p1 = tgtArrowFromPt;
p2 = srcArrowFromPt;
}
if( tgtManEndptVal === 'inside-to-node' ){
intersect = [ tgtPos.x, tgtPos.y ];
} else if( tgtManEndpt.units ){
intersect = this.manualEndptToPx( target, tgtManEndpt );
} else if( tgtManEndptVal === 'outside-to-line' ){
intersect = rs.tgtIntn; // use cached value from ctrlpt calc
} else {
if( tgtManEndptVal === 'outside-to-node' || tgtManEndptVal === 'outside-to-node-or-label' ){
p1_i = p1;
} else if( tgtManEndptVal === 'outside-to-line' || tgtManEndptVal === 'outside-to-line-or-label' ){
p1_i = [ srcPos.x, srcPos.y ];
}
intersect = r.nodeShapes[ this.getNodeShape( target ) ].intersectLine(
tgtPos.x,
tgtPos.y,
target.outerWidth(),
target.outerHeight(),
p1_i[0],
p1_i[1],
0, tgtCornerRadius, tgtRs
);
if( tgtManEndptVal === 'outside-to-node-or-label' || tgtManEndptVal === 'outside-to-line-or-label' ){
let trs = target._private.rscratch;
let lw = trs.labelWidth;
let lh = trs.labelHeight;
let lx = trs.labelX;
let ly = trs.labelY;
let lw2 = lw/2;
let lh2 = lh/2;
let va = target.pstyle('text-valign').value;
if( va === 'top' ){
ly -= lh2;
} else if( va === 'bottom' ){
ly += lh2;
}
let ha = target.pstyle('text-halign').value;
if( ha === 'left' ){
lx -= lw2;
} else if( ha === 'right' ){
lx += lw2;
}
let labelIntersect = math.polygonIntersectLine(p1_i[0], p1_i[1], [
lx - lw2, ly - lh2,
lx + lw2, ly - lh2,
lx + lw2, ly + lh2,
lx - lw2, ly + lh2
], tgtPos.x, tgtPos.y);
if( labelIntersect.length > 0 ){
let refPt = srcPos;
let intSqdist = math.sqdist( refPt, math.array2point(intersect) );
let labIntSqdist = math.sqdist( refPt, math.array2point(labelIntersect) );
let minSqDist = intSqdist;
if( labIntSqdist < intSqdist ){
intersect = labelIntersect;
minSqDist = labIntSqdist;
}
if( labelIntersect.length > 2 ){
let labInt2SqDist = math.sqdist( refPt, { x: labelIntersect[2], y: labelIntersect[3] } );
if( labInt2SqDist < minSqDist ){
intersect = [ labelIntersect[2], labelIntersect[3] ];
}
}
}
}
}
let arrowEnd = math.shortenIntersection(
intersect,
p1,
r.arrowShapes[ tgtArShape ].spacing( edge ) + tgtDist
);
let edgeEnd = math.shortenIntersection(
intersect,
p1,
r.arrowShapes[ tgtArShape ].gap( edge ) + tgtDist
);
rs.endX = edgeEnd[0];
rs.endY = edgeEnd[1];
rs.arrowEndX = arrowEnd[0];
rs.arrowEndY = arrowEnd[1];
if( srcManEndptVal === 'inside-to-node' ){
intersect = [ srcPos.x, srcPos.y ];
} else if( srcManEndpt.units ){
intersect = this.manualEndptToPx( source, srcManEndpt );
} else if( srcManEndptVal === 'outside-to-line' ){
intersect = rs.srcIntn; // use cached value from ctrlpt calc
} else {
if( srcManEndptVal === 'outside-to-node' || srcManEndptVal === 'outside-to-node-or-label' ){
p2_i = p2;
} else if( srcManEndptVal === 'outside-to-line' || srcManEndptVal === 'outside-to-line-or-label' ){
p2_i = [ tgtPos.x, tgtPos.y ];
}
intersect = r.nodeShapes[ this.getNodeShape( source ) ].intersectLine(
srcPos.x,
srcPos.y,
source.outerWidth(),
source.outerHeight(),
p2_i[0],
p2_i[1],
0, srcCornerRadius, srcRs
);
if( srcManEndptVal === 'outside-to-node-or-label' || srcManEndptVal === 'outside-to-line-or-label' ){
let srs = source._private.rscratch;
let lw = srs.labelWidth;
let lh = srs.labelHeight;
let lx = srs.labelX;
let ly = srs.labelY;
let lw2 = lw/2;
let lh2 = lh/2;
let va = source.pstyle('text-valign').value;
if( va === 'top' ){
ly -= lh2;
} else if( va === 'bottom' ){
ly += lh2;
}
let ha = source.pstyle('text-halign').value;
if( ha === 'left' ){
lx -= lw2;
} else if( ha === 'right' ){
lx += lw2;
}
let labelIntersect = math.polygonIntersectLine(p2_i[0], p2_i[1], [
lx - lw2, ly - lh2,
lx + lw2, ly - lh2,
lx + lw2, ly + lh2,
lx - lw2, ly + lh2
], srcPos.x, srcPos.y);
if( labelIntersect.length > 0 ){
let refPt = tgtPos;
let intSqdist = math.sqdist( refPt, math.array2point(intersect) );
let labIntSqdist = math.sqdist( refPt, math.array2point(labelIntersect) );
let minSqDist = intSqdist;
if( labIntSqdist < intSqdist ){
intersect = [ labelIntersect[0], labelIntersect[1] ];
minSqDist = labIntSqdist;
}
if( labelIntersect.length > 2 ){
let labInt2SqDist = math.sqdist( refPt, { x: labelIntersect[2], y: labelIntersect[3] } );
if( labInt2SqDist < minSqDist ){
intersect = [ labelIntersect[2], labelIntersect[3] ];
}
}
}
}
}
let arrowStart = math.shortenIntersection(
intersect,
p2,
r.arrowShapes[ srcArShape ].spacing( edge ) + srcDist
);
let edgeStart = math.shortenIntersection(
intersect,
p2,
r.arrowShapes[ srcArShape ].gap( edge ) + srcDist
);
rs.startX = edgeStart[0];
rs.startY = edgeStart[1];
rs.arrowStartX = arrowStart[0];
rs.arrowStartY = arrowStart[1];
if( hasEndpts ){
if( !is.number( rs.startX ) || !is.number( rs.startY ) || !is.number( rs.endX ) || !is.number( rs.endY ) ){
rs.badLine = true;
} else {
rs.badLine = false;
}
}
};
BRp.getSourceEndpoint = function( edge ){
let rs = edge[0]._private.rscratch;
this.recalculateRenderedStyle( edge );
switch( rs.edgeType ){
case 'haystack':
return {
x: rs.haystackPts[0],
y: rs.haystackPts[1]
};
default:
return {
x: rs.arrowStartX,
y: rs.arrowStartY
};
}
};
BRp.getTargetEndpoint = function( edge ){
let rs = edge[0]._private.rscratch;
this.recalculateRenderedStyle( edge );
switch( rs.edgeType ){
case 'haystack':
return {
x: rs.haystackPts[2],
y: rs.haystackPts[3]
};
default:
return {
x: rs.arrowEndX,
y: rs.arrowEndY
};
}
};
export default BRp;
@@ -0,0 +1,62 @@
import * as math from '../../../../math.mjs';
var BRp = {};
function pushBezierPts( r, edge, pts ){
var qbezierAt = function( p1, p2, p3, t ){ return math.qbezierAt( p1, p2, p3, t ); };
var _p = edge._private;
var bpts = _p.rstyle.bezierPts;
for( var i = 0; i < r.bezierProjPcts.length; i++ ){
var p = r.bezierProjPcts[i];
bpts.push( {
x: qbezierAt( pts[0], pts[2], pts[4], p ),
y: qbezierAt( pts[1], pts[3], pts[5], p )
} );
}
}
BRp.storeEdgeProjections = function( edge ){
var _p = edge._private;
var rs = _p.rscratch;
var et = rs.edgeType;
// clear the cached points state
_p.rstyle.bezierPts = null;
_p.rstyle.linePts = null;
_p.rstyle.haystackPts = null;
if( et === 'multibezier' || et === 'bezier' || et === 'self' || et === 'compound' ){
_p.rstyle.bezierPts = [];
for( var i = 0; i + 5 < rs.allpts.length; i += 4 ){
pushBezierPts( this, edge, rs.allpts.slice( i, i + 6 ) );
}
} else if( et === 'segments' ){
var lpts = _p.rstyle.linePts = [];
for( var i = 0; i + 1 < rs.allpts.length; i += 2 ){
lpts.push( {
x: rs.allpts[ i ],
y: rs.allpts[ i + 1]
} );
}
} else if( et === 'haystack' ){
var hpts = rs.haystackPts;
_p.rstyle.haystackPts = [
{ x: hpts[0], y: hpts[1] },
{ x: hpts[2], y: hpts[3] }
];
}
_p.rstyle.arrowWidth = this.getArrowWidth( edge.pstyle('width').pfValue, edge.pstyle( 'arrow-scale' ).value )
* this.arrowShapeWidth;
};
BRp.recalculateEdgeProjections = function( edges ){
this.findEdgeControlPoints( edges );
};
export default BRp;
@@ -0,0 +1,29 @@
import * as util from '../../../../util/index.mjs';
import coords from './coords.mjs';
import edgeArrows from './edge-arrows.mjs';
import edgeControlPoints from './edge-control-points.mjs';
import edgeEndpoints from './edge-endpoints.mjs';
import edgeProjection from './edge-projection.mjs';
import labels from './labels.mjs';
import nodes from './nodes.mjs';
import renderedStyle from './rendered-style.mjs';
import zOrdering from './z-ordering.mjs';
var BRp = {};
[
coords,
edgeArrows,
edgeControlPoints,
edgeEndpoints,
edgeProjection,
labels,
nodes,
renderedStyle,
zOrdering
].forEach(function( props ){
util.extend( BRp, props );
});
export default BRp;
@@ -0,0 +1,565 @@
import * as math from '../../../../math.mjs';
import * as is from '../../../../is.mjs';
import * as util from '../../../../util/index.mjs';
let BRp = {};
BRp.recalculateNodeLabelProjection = function( node ){
let content = node.pstyle( 'label' ).strValue;
if( is.emptyString(content) ){ return; }
let textX, textY;
let _p = node._private;
let nodeWidth = node.width();
let nodeHeight = node.height();
let padding = node.padding();
let nodePos = node.position();
let textHalign = node.pstyle( 'text-halign' ).strValue;
let textValign = node.pstyle( 'text-valign' ).strValue;
let rs = _p.rscratch;
let rstyle = _p.rstyle;
switch( textHalign ){
case 'left':
textX = nodePos.x - nodeWidth / 2 - padding;
break;
case 'right':
textX = nodePos.x + nodeWidth / 2 + padding;
break;
default: // e.g. center
textX = nodePos.x;
}
switch( textValign ){
case 'top':
textY = nodePos.y - nodeHeight / 2 - padding;
break;
case 'bottom':
textY = nodePos.y + nodeHeight / 2 + padding;
break;
default: // e.g. middle
textY = nodePos.y;
}
rs.labelX = textX;
rs.labelY = textY;
rstyle.labelX = textX;
rstyle.labelY = textY;
this.calculateLabelAngles( node );
this.applyLabelDimensions( node );
};
let lineAngleFromDelta = function( dx, dy ){
let angle = Math.atan( dy / dx );
if( dx === 0 && angle < 0 ){
angle = angle * -1;
}
return angle;
};
let lineAngle = function( p0, p1 ){
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
return lineAngleFromDelta( dx, dy );
};
let bezierAngle = function( p0, p1, p2, t ){
let t0 = math.bound( 0, t - 0.001, 1 );
let t1 = math.bound( 0, t + 0.001, 1 );
let lp0 = math.qbezierPtAt( p0, p1, p2, t0 );
let lp1 = math.qbezierPtAt( p0, p1, p2, t1 );
return lineAngle( lp0, lp1 );
};
BRp.recalculateEdgeLabelProjections = function( edge ){
let p;
let _p = edge._private;
let rs = _p.rscratch;
let r = this;
let content = {
mid: edge.pstyle('label').strValue,
source: edge.pstyle('source-label').strValue,
target: edge.pstyle('target-label').strValue
};
if( content.mid || content.source || content.target ){
// then we have to calculate...
} else {
return; // no labels => no calcs
}
// add center point to style so bounding box calculations can use it
//
p = {
x: rs.midX,
y: rs.midY
};
let setRs = function( propName, prefix, value ){
util.setPrefixedProperty( _p.rscratch, propName, prefix, value );
util.setPrefixedProperty( _p.rstyle, propName, prefix, value );
};
setRs( 'labelX', null, p.x );
setRs( 'labelY', null, p.y );
let midAngle = lineAngleFromDelta(rs.midDispX, rs.midDispY);
setRs( 'labelAutoAngle', null, midAngle );
let createControlPointInfo = function(){
if( createControlPointInfo.cache ){ return createControlPointInfo.cache; } // use cache so only 1x per edge
let ctrlpts = [];
// store each ctrlpt info init
for( let i = 0; i + 5 < rs.allpts.length; i += 4 ){
let p0 = { x: rs.allpts[i], y: rs.allpts[i+1] };
let p1 = { x: rs.allpts[i+2], y: rs.allpts[i+3] }; // ctrlpt
let p2 = { x: rs.allpts[i+4], y: rs.allpts[i+5] };
ctrlpts.push({
p0: p0,
p1: p1,
p2: p2,
startDist: 0,
length: 0,
segments: []
});
}
let bpts = _p.rstyle.bezierPts;
let nProjs = r.bezierProjPcts.length;
function addSegment( cp, p0, p1, t0, t1 ){
let length = math.dist( p0, p1 );
let prevSegment = cp.segments[ cp.segments.length - 1 ];
let segment = {
p0: p0,
p1: p1,
t0: t0,
t1: t1,
startDist: prevSegment ? prevSegment.startDist + prevSegment.length : 0,
length: length
};
cp.segments.push( segment );
cp.length += length;
}
// update each ctrlpt with segment info
for( let i = 0; i < ctrlpts.length; i++ ){
let cp = ctrlpts[i];
let prevCp = ctrlpts[i - 1];
if( prevCp ){
cp.startDist = prevCp.startDist + prevCp.length;
}
addSegment(
cp,
cp.p0, bpts[ i * nProjs ],
0, r.bezierProjPcts[ 0 ]
); // first
for( let j = 0; j < nProjs - 1; j++ ){
addSegment(
cp,
bpts[ i * nProjs + j ], bpts[ i * nProjs + j + 1 ],
r.bezierProjPcts[ j ], r.bezierProjPcts[ j + 1 ]
);
}
addSegment(
cp,
bpts[ i * nProjs + nProjs - 1 ], cp.p2,
r.bezierProjPcts[ nProjs - 1 ], 1
); // last
}
return ( createControlPointInfo.cache = ctrlpts );
};
let calculateEndProjection = function( prefix ){
let angle;
let isSrc = prefix === 'source';
if( !content[ prefix ] ){ return; }
let offset = edge.pstyle(prefix+'-text-offset').pfValue;
switch( rs.edgeType ){
case 'self':
case 'compound':
case 'bezier':
case 'multibezier': {
let cps = createControlPointInfo();
let selected;
let startDist = 0;
let totalDist = 0;
// find the segment we're on
for( let i = 0; i < cps.length; i++ ){
let cp = cps[ isSrc ? i : cps.length - 1 - i ];
for( let j = 0; j < cp.segments.length; j++ ){
let seg = cp.segments[ isSrc ? j : cp.segments.length - 1 - j ];
let lastSeg = i === cps.length - 1 && j === cp.segments.length - 1;
startDist = totalDist;
totalDist += seg.length;
if( totalDist >= offset || lastSeg ){
selected = { cp: cp, segment: seg };
break;
}
}
if( selected ){ break; }
}
let cp = selected.cp;
let seg = selected.segment;
let tSegment = ( offset - startDist ) / ( seg.length );
let segDt = seg.t1 - seg.t0;
let t = isSrc ? seg.t0 + segDt * tSegment : seg.t1 - segDt * tSegment;
t = math.bound( 0, t, 1 );
p = math.qbezierPtAt( cp.p0, cp.p1, cp.p2, t );
angle = bezierAngle( cp.p0, cp.p1, cp.p2, t, p );
break;
}
case 'straight':
case 'segments':
case 'haystack': {
let d = 0, di, d0;
let p0, p1;
let l = rs.allpts.length;
for( let i = 0; i + 3 < l; i += 2 ){
if( isSrc ){
p0 = { x: rs.allpts[i], y: rs.allpts[i+1] };
p1 = { x: rs.allpts[i+2], y: rs.allpts[i+3] };
} else {
p0 = { x: rs.allpts[l-2-i], y: rs.allpts[l-1-i] };
p1 = { x: rs.allpts[l-4-i], y: rs.allpts[l-3-i] };
}
di = math.dist( p0, p1 );
d0 = d;
d += di;
if( d >= offset ){ break; }
}
let pD = offset - d0;
let t = pD / di;
t = math.bound( 0, t, 1 );
p = math.lineAt( p0, p1, t );
angle = lineAngle( p0, p1 );
break;
}
}
setRs( 'labelX', prefix, p.x );
setRs( 'labelY', prefix, p.y );
setRs( 'labelAutoAngle', prefix, angle );
};
calculateEndProjection( 'source' );
calculateEndProjection( 'target' );
this.applyLabelDimensions( edge );
};
BRp.applyLabelDimensions = function( ele ){
this.applyPrefixedLabelDimensions( ele );
if( ele.isEdge() ){
this.applyPrefixedLabelDimensions( ele, 'source' );
this.applyPrefixedLabelDimensions( ele, 'target' );
}
};
BRp.applyPrefixedLabelDimensions = function( ele, prefix ){
let _p = ele._private;
let text = this.getLabelText( ele, prefix );
let cacheKey = util.hashString( text, ele._private.labelDimsKey );
// save recalc if the label is the same as before
if( util.getPrefixedProperty( _p.rscratch, 'prefixedLabelDimsKey', prefix ) === cacheKey ){
return; // then the label dimensions + text are the same
}
// save the key
util.setPrefixedProperty( _p.rscratch, 'prefixedLabelDimsKey', prefix, cacheKey );
let labelDims = this.calculateLabelDimensions( ele, text );
let lineHeight = ele.pstyle('line-height').pfValue;
let textWrap = ele.pstyle('text-wrap').strValue;
let lines = util.getPrefixedProperty( _p.rscratch, 'labelWrapCachedLines', prefix ) || [];
let numLines = textWrap !== 'wrap' ? 1 : Math.max(lines.length, 1);
let normPerLineHeight = labelDims.height / numLines;
let labelLineHeight = normPerLineHeight * lineHeight;
let width = labelDims.width;
let height = labelDims.height + (numLines - 1) * (lineHeight - 1) * normPerLineHeight;
util.setPrefixedProperty( _p.rstyle, 'labelWidth', prefix, width );
util.setPrefixedProperty( _p.rscratch, 'labelWidth', prefix, width );
util.setPrefixedProperty( _p.rstyle, 'labelHeight', prefix, height );
util.setPrefixedProperty( _p.rscratch, 'labelHeight', prefix, height );
util.setPrefixedProperty( _p.rscratch, 'labelLineHeight', prefix, labelLineHeight );
};
BRp.getLabelText = function( ele, prefix ){
let _p = ele._private;
let pfd = prefix ? prefix + '-' : '';
let text = ele.pstyle( pfd + 'label' ).strValue;
let textTransform = ele.pstyle( 'text-transform' ).value;
let rscratch = function( propName, value ){
if( value ){
util.setPrefixedProperty( _p.rscratch, propName, prefix, value );
return value;
} else {
return util.getPrefixedProperty( _p.rscratch, propName, prefix );
}
};
// for empty text, skip all processing
if( !text ){ return ''; }
if( textTransform == 'none' ){
// passthrough
} else if( textTransform == 'uppercase' ){
text = text.toUpperCase();
} else if( textTransform == 'lowercase' ){
text = text.toLowerCase();
}
let wrapStyle = ele.pstyle( 'text-wrap' ).value;
if( wrapStyle === 'wrap' ){
let labelKey = rscratch( 'labelKey' );
// save recalc if the label is the same as before
if( labelKey != null && rscratch( 'labelWrapKey' ) === labelKey ){
return rscratch( 'labelWrapCachedText' );
}
let zwsp = '\u200b';
let lines = text.split('\n');
let maxW = ele.pstyle('text-max-width').pfValue;
let overflow = ele.pstyle('text-overflow-wrap').value;
let overflowAny = overflow === 'anywhere';
let wrappedLines = [];
let separatorRegex = /[\s\u200b]+|$/g; // Include end of string to add last word
for( let l = 0; l < lines.length; l++ ){
let line = lines[ l ];
let lineDims = this.calculateLabelDimensions( ele, line );
let lineW = lineDims.width;
if( overflowAny ){
let processedLine = line.split('').join(zwsp);
line = processedLine;
}
if( lineW > maxW ){ // line is too long
let separatorMatches = line.matchAll(separatorRegex);
let subline = '';
let previousIndex = 0;
// Add fake match
for( let separatorMatch of separatorMatches ){
let wordSeparator = separatorMatch[ 0 ];
let word = line.substring( previousIndex, separatorMatch.index );
previousIndex = separatorMatch.index + wordSeparator.length;
let testLine = subline.length === 0 ? word : subline + word + wordSeparator;
let testDims = this.calculateLabelDimensions( ele, testLine );
let testW = testDims.width;
if( testW <= maxW ){ // word fits on current line
subline += word + wordSeparator;
} else { // word starts new line
if( subline ){
wrappedLines.push( subline );
}
subline = word + wordSeparator;
}
}
// if there's remaining text, put it in a wrapped line
if( !subline.match( /^[\s\u200b]+$/ ) ){
wrappedLines.push( subline );
}
} else { // line is already short enough
wrappedLines.push( line );
}
} // for
rscratch( 'labelWrapCachedLines', wrappedLines );
text = rscratch( 'labelWrapCachedText', wrappedLines.join( '\n' ) );
rscratch( 'labelWrapKey', labelKey );
} else if( wrapStyle === 'ellipsis' ){
let maxW = ele.pstyle( 'text-max-width' ).pfValue;
let ellipsized = '';
let ellipsis = '\u2026';
let incLastCh = false;
if (this.calculateLabelDimensions(ele, text).width < maxW) { // the label already fits
return text;
}
for( let i = 0; i < text.length; i++ ){
let widthWithNextCh = this.calculateLabelDimensions( ele, ellipsized + text[i] + ellipsis ).width;
if( widthWithNextCh > maxW ){ break; }
ellipsized += text[i];
if( i === text.length - 1 ){ incLastCh = true; }
}
if( !incLastCh ){
ellipsized += ellipsis;
}
return ellipsized;
} // if ellipsize
return text;
};
BRp.getLabelJustification = function(ele){
let justification = ele.pstyle('text-justification').strValue;
let textHalign = ele.pstyle('text-halign').strValue;
if( justification === 'auto' ){
if( ele.isNode() ){
switch( textHalign ){
case 'left':
return 'right';
case 'right':
return 'left';
default:
return 'center';
}
} else {
return 'center';
}
} else {
return justification;
}
};
BRp.calculateLabelDimensions = function( ele, text ){
let r = this;
var containerWindow = r.cy.window();
var document = containerWindow.document;
let padding = 0; // add padding around text dims, as the measurement isn't that accurate
let fStyle = ele.pstyle('font-style').strValue;
let size = ele.pstyle('font-size').pfValue;
let family = ele.pstyle('font-family').strValue;
let weight = ele.pstyle('font-weight').strValue;
let canvas = this.labelCalcCanvas;
let c2d = this.labelCalcCanvasContext;
if( !canvas ){
canvas = this.labelCalcCanvas = document.createElement('canvas');
c2d = this.labelCalcCanvasContext = canvas.getContext('2d');
let ds = canvas.style;
ds.position = 'absolute';
ds.left = '-9999px';
ds.top = '-9999px';
ds.zIndex = '-1';
ds.visibility = 'hidden';
ds.pointerEvents = 'none';
}
c2d.font = `${fStyle} ${weight} ${size}px ${family}`;
let width = 0;
let height = 0;
let lines = text.split('\n');
for( let i = 0; i < lines.length; i++ ){
let line = lines[i];
let metrics = c2d.measureText(line);
let w = Math.ceil(metrics.width);
let h = size;
width = Math.max(w, width);
height += h;
}
width += padding;
height += padding;
return {
width,
height
};
};
BRp.calculateLabelAngle = function( ele, prefix ){
let _p = ele._private;
let rs = _p.rscratch;
let isEdge = ele.isEdge();
let prefixDash = prefix ? prefix + '-' : '';
let rot = ele.pstyle( prefixDash + 'text-rotation' );
let rotStr = rot.strValue;
if( rotStr === 'none' ){
return 0;
} else if( isEdge && rotStr === 'autorotate' ){
return rs.labelAutoAngle;
} else if( rotStr === 'autorotate' ){
return 0;
} else {
return rot.pfValue;
}
};
BRp.calculateLabelAngles = function( ele ){
let r = this;
let isEdge = ele.isEdge();
let _p = ele._private;
let rs = _p.rscratch;
rs.labelAngle = r.calculateLabelAngle(ele);
if( isEdge ){
rs.sourceLabelAngle = r.calculateLabelAngle(ele, 'source');
rs.targetLabelAngle = r.calculateLabelAngle(ele, 'target');
}
};
export default BRp;
@@ -0,0 +1,45 @@
var BRp = {};
import { warn } from '../../../../util/index.mjs';
const TOO_SMALL_CUT_RECT = 28;
let warnedCutRect = false;
BRp.getNodeShape = function( node ){
var r = this;
var shape = node.pstyle( 'shape' ).value;
if( shape === 'cutrectangle' && (node.width() < TOO_SMALL_CUT_RECT || node.height() < TOO_SMALL_CUT_RECT) ){
if( !warnedCutRect ){
warn('The `cutrectangle` node shape can not be used at small sizes so `rectangle` is used instead');
warnedCutRect = true;
}
return 'rectangle';
}
if( node.isParent() ){
if( shape === 'rectangle'
|| shape === 'roundrectangle'
|| shape === 'round-rectangle'
|| shape === 'cutrectangle'
|| shape === 'cut-rectangle'
|| shape === 'barrel' ){
return shape;
} else {
return 'rectangle';
}
}
if( shape === 'polygon' ){
var points = node.pstyle( 'shape-polygon-points' ).value;
return r.nodeShapes.makePolygon( points ).name;
}
return shape;
};
export default BRp;
@@ -0,0 +1,164 @@
let BRp = {};
BRp.registerCalculationListeners = function(){
let cy = this.cy;
let elesToUpdate = cy.collection();
let r = this;
let enqueue = function( eles, dirtyStyleCaches = true ){
elesToUpdate.merge( eles );
if( dirtyStyleCaches ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
let _p = ele._private;
let rstyle = _p.rstyle;
rstyle.clean = false;
rstyle.cleanConnected = false;
}
}
};
r.binder( cy )
.on('bounds.* dirty.*', function onDirtyBounds( e ){
let ele = e.target;
enqueue( ele );
})
.on('style.* background.*', function onDirtyStyle( e ){
let ele = e.target;
enqueue( ele, false );
})
;
let updateEleCalcs = function( willDraw ){
if( willDraw ){
let fns = r.onUpdateEleCalcsFns;
// because we need to have up-to-date style (e.g. stylesheet mappers)
// before calculating rendered style (and pstyle might not be called yet)
elesToUpdate.cleanStyle();
for( let i = 0; i < elesToUpdate.length; i++ ){
let ele = elesToUpdate[i];
let rstyle = ele._private.rstyle;
if( ele.isNode() && !rstyle.cleanConnected ){
enqueue( ele.connectedEdges() );
rstyle.cleanConnected = true;
}
}
if( fns ){ for( let i = 0; i < fns.length; i++ ){
let fn = fns[i];
fn( willDraw, elesToUpdate );
} }
r.recalculateRenderedStyle( elesToUpdate );
elesToUpdate = cy.collection();
}
};
r.flushRenderedStyleQueue = function(){
updateEleCalcs(true);
};
r.beforeRender( updateEleCalcs, r.beforeRenderPriorities.eleCalcs );
};
BRp.onUpdateEleCalcs = function( fn ){
let fns = this.onUpdateEleCalcsFns = this.onUpdateEleCalcsFns || [];
fns.push( fn );
};
BRp.recalculateRenderedStyle = function( eles, useCache ){
let isCleanConnected = ele => ele._private.rstyle.cleanConnected;
if (eles.length === 0) { return; }
let edges = [];
let nodes = [];
// the renderer can't be used for calcs when destroyed, e.g. ele.boundingBox()
if( this.destroyed ){ return; }
// use cache by default for perf
if( useCache === undefined ){ useCache = true; }
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
let _p = ele._private;
let rstyle = _p.rstyle;
// an edge may be implicitly dirty b/c of one of its connected nodes
// (and a request for recalc may come in between frames)
if( ele.isEdge() && (!isCleanConnected(ele.source()) || !isCleanConnected(ele.target())) ){
rstyle.clean = false;
}
if (ele.isEdge() && ele.isBundledBezier()) {
if (ele.parallelEdges().some(ele => !ele._private.rstyle.clean && ele.isBundledBezier())) {
rstyle.clean = false;
}
}
// only update if dirty and in graph
if( (useCache && rstyle.clean) || ele.removed() ){ continue; }
// only update if not display: none
if( ele.pstyle('display').value === 'none' ){ continue; }
if( _p.group === 'nodes' ){
nodes.push( ele );
} else { // edges
edges.push( ele );
}
rstyle.clean = true;
}
// update node data from projections
for( let i = 0; i < nodes.length; i++ ){
let ele = nodes[i];
let _p = ele._private;
let rstyle = _p.rstyle;
let pos = ele.position();
this.recalculateNodeLabelProjection( ele );
rstyle.nodeX = pos.x;
rstyle.nodeY = pos.y;
rstyle.nodeW = ele.pstyle( 'width' ).pfValue;
rstyle.nodeH = ele.pstyle( 'height' ).pfValue;
}
this.recalculateEdgeProjections( edges );
// update edge data from projections
for( let i = 0; i < edges.length; i++ ){
let ele = edges[ i ];
let _p = ele._private;
let rstyle = _p.rstyle;
let rs = _p.rscratch;
// update rstyle positions
rstyle.srcX = rs.arrowStartX;
rstyle.srcY = rs.arrowStartY;
rstyle.tgtX = rs.arrowEndX;
rstyle.tgtY = rs.arrowEndY;
rstyle.midX = rs.midX;
rstyle.midY = rs.midY;
rstyle.labelAngle = rs.labelAngle;
rstyle.sourceLabelAngle = rs.sourceLabelAngle;
rstyle.targetLabelAngle = rs.targetLabelAngle;
}
};
export default BRp;
@@ -0,0 +1,61 @@
import zIndexSort from '../../../../collection/zsort.mjs';
var BRp = {};
BRp.updateCachedGrabbedEles = function(){
var eles = this.cachedZSortedEles;
if( !eles ){
// just let this be recalculated on the next z sort tick
return;
}
eles.drag = [];
eles.nondrag = [];
var grabTargets = [];
for( var i = 0; i < eles.length; i++ ){
var ele = eles[i];
var rs = ele._private.rscratch;
if( ele.grabbed() && !ele.isParent() ){
grabTargets.push( ele );
} else if( rs.inDragLayer ){
eles.drag.push( ele );
} else {
eles.nondrag.push( ele );
}
}
// put the grab target nodes last so it's on top of its neighbourhood
for( var i = 0; i < grabTargets.length; i++ ){
var ele = grabTargets[i];
eles.drag.push( ele );
}
};
BRp.invalidateCachedZSortedEles = function(){
this.cachedZSortedEles = null;
};
BRp.getCachedZSortedEles = function( forceRecalc ){
if( forceRecalc || !this.cachedZSortedEles ){
var eles = this.cy.mutableElements().toArray();
eles.sort( zIndexSort );
eles.interactive = eles.filter(ele => ele.interactive());
this.cachedZSortedEles = eles;
this.updateCachedGrabbedEles();
} else {
eles = this.cachedZSortedEles;
}
return eles;
};
export default BRp;
@@ -0,0 +1,18 @@
const setGrabState = function( ele, grabbed ){
let ele0 = ele[0];
if( !ele0 || ele0._private.grabbed === grabbed ){
return;
}
ele0._private.grabbed = grabbed;
ele.updateStyle( false );
};
export const setGrabbed = function( ele ){
setGrabState( ele, true );
};
export const setFreed = function( ele ){
setGrabState( ele, false );
};
+38
View File
@@ -0,0 +1,38 @@
var BRp = {};
BRp.getCachedImage = function( url, crossOrigin, onLoad ){
var r = this;
var imageCache = r.imageCache = r.imageCache || {};
var cache = imageCache[ url ];
if( cache ){
if( !cache.image.complete ){
cache.image.addEventListener('load', onLoad);
}
return cache.image;
} else {
cache = imageCache[ url ] = imageCache[ url ] || {};
var image = cache.image = new Image(); // eslint-disable-line no-undef
image.addEventListener('load', onLoad);
image.addEventListener('error', function(){ image.error = true; });
// #1582 safari doesn't load data uris with crossOrigin properly
// https://bugs.webkit.org/show_bug.cgi?id=123978
var dataUriPrefix = 'data:';
var isDataUri = url.substring( 0, dataUriPrefix.length ).toLowerCase() === dataUriPrefix;
if( !isDataUri ){
// if crossorigin is 'null'(stringified), then manually set it to null
crossOrigin = crossOrigin === 'null' ? null : crossOrigin;
image.crossOrigin = crossOrigin; // prevent tainted canvas
}
image.src = url;
return image;
}
};
export default BRp;
+224
View File
@@ -0,0 +1,224 @@
import * as util from '../../../util/index.mjs';
import * as is from '../../../is.mjs';
import arrowShapes from './arrow-shapes.mjs';
import coordEleMath from './coord-ele-math/index.mjs';
import images from './images.mjs';
import loadListeners from './load-listeners.mjs';
import nodeShapes from './node-shapes.mjs';
import redraw from './redraw.mjs';
var BaseRenderer = function( options ){ this.init( options ); };
var BR = BaseRenderer;
var BRp = BR.prototype;
BRp.clientFunctions = [ 'redrawHint', 'render', 'renderTo', 'matchCanvasSize', 'nodeShapeImpl', 'arrowShapeImpl' ];
BRp.init = function( options ){
var r = this;
r.options = options;
r.cy = options.cy;
var ctr = r.container = options.cy.container();
var containerWindow = r.cy.window();
// prepend a stylesheet in the head such that
if( containerWindow ){
var document = containerWindow.document;
var head = document.head;
var stylesheetId = '__________cytoscape_stylesheet';
var className = '__________cytoscape_container';
var stylesheetAlreadyExists = document.getElementById( stylesheetId ) != null;
if( ctr.className.indexOf( className ) < 0 ){
ctr.className = ( ctr.className || '' ) + ' ' + className;
}
if( !stylesheetAlreadyExists ){
var stylesheet = document.createElement('style');
stylesheet.id = stylesheetId;
stylesheet.textContent = '.'+className+' { position: relative; }';
head.insertBefore( stylesheet, head.children[0] ); // first so lowest priority
}
var computedStyle = containerWindow.getComputedStyle( ctr );
var position = computedStyle.getPropertyValue('position');
if( position === 'static' ){
util.warn('A Cytoscape container has style position:static and so can not use UI extensions properly');
}
}
r.selection = [ undefined, undefined, undefined, undefined, 0]; // Coordinates for selection box, plus enabled flag
r.bezierProjPcts = [ 0.05, 0.225, 0.4, 0.5, 0.6, 0.775, 0.95 ];
//--Pointer-related data
r.hoverData = {down: null, last: null,
downTime: null, triggerMode: null,
dragging: false,
initialPan: [ null, null ], capture: false};
r.dragData = {possibleDragElements: []};
r.touchData = {
start: null, capture: false,
// These 3 fields related to tap, taphold events
startPosition: [ null, null, null, null, null, null ],
singleTouchStartTime: null,
singleTouchMoved: true,
now: [ null, null, null, null, null, null ],
earlier: [ null, null, null, null, null, null ]
};
r.redraws = 0;
r.showFps = options.showFps;
r.debug = options.debug;
r.webgl = options.webgl;
r.hideEdgesOnViewport = options.hideEdgesOnViewport;
r.textureOnViewport = options.textureOnViewport;
r.wheelSensitivity = options.wheelSensitivity;
r.motionBlurEnabled = options.motionBlur; // on by default
r.forcedPixelRatio = is.number(options.pixelRatio) ? options.pixelRatio : null;
r.motionBlur = options.motionBlur; // for initial kick off
r.motionBlurOpacity = options.motionBlurOpacity;
r.motionBlurTransparency = 1 - r.motionBlurOpacity;
r.motionBlurPxRatio = 1;
r.mbPxRBlurry = 1; //0.8;
r.minMbLowQualFrames = 4;
r.fullQualityMb = false;
r.clearedForMotionBlur = [];
r.desktopTapThreshold = options.desktopTapThreshold;
r.desktopTapThreshold2 = options.desktopTapThreshold * options.desktopTapThreshold;
r.touchTapThreshold = options.touchTapThreshold;
r.touchTapThreshold2 = options.touchTapThreshold * options.touchTapThreshold;
r.tapholdDuration = 500;
r.bindings = [];
r.beforeRenderCallbacks = [];
r.beforeRenderPriorities = { // higher priority execs before lower one
animations: 400,
eleCalcs: 300,
eleTxrDeq: 200,
lyrTxrDeq: 150,
lyrTxrSkip: 100,
};
r.registerNodeShapes();
r.registerArrowShapes();
r.registerCalculationListeners();
};
BRp.notify = function( eventName, eles ) {
var r = this;
var cy = r.cy;
// the renderer can't be notified after it's destroyed
if( this.destroyed ){ return; }
if( eventName === 'init' ){
r.load();
return;
}
if( eventName === 'destroy' ){
r.destroy();
return;
}
if(
eventName === 'add'
|| eventName === 'remove'
|| (eventName === 'move' && cy.hasCompoundNodes())
|| eventName === 'load'
|| eventName === 'zorder'
|| eventName === 'mount'
){
r.invalidateCachedZSortedEles();
}
if( eventName === 'viewport' ){
r.redrawHint( 'select', true );
}
if( eventName === 'gc' ){
r.redrawHint( 'gc', true );
}
if( eventName === 'load' || eventName === 'resize' || eventName === 'mount' ){
r.invalidateContainerClientCoordsCache();
r.matchCanvasSize( r.container );
}
r.redrawHint( 'eles', true );
r.redrawHint( 'drag', true );
this.startRenderLoop();
this.redraw();
};
BRp.destroy = function(){
var r = this;
r.destroyed = true;
r.cy.stopAnimationLoop();
for( var i = 0; i < r.bindings.length; i++ ){
var binding = r.bindings[ i ];
var b = binding;
var tgt = b.target;
( tgt.off || tgt.removeEventListener ).apply( tgt, b.args );
}
r.bindings = [];
r.beforeRenderCallbacks = [];
r.onUpdateEleCalcsFns = [];
if( r.removeObserver ){
r.removeObserver.disconnect();
}
if( r.styleObserver ){
r.styleObserver.disconnect();
}
if( r.resizeObserver ){
r.resizeObserver.disconnect();
}
if( r.labelCalcDiv ){
try {
document.body.removeChild( r.labelCalcDiv ); // eslint-disable-line no-undef
} catch( e ){
// ie10 issue #1014
}
}
};
BRp.isHeadless = function(){
return false;
};
[
arrowShapes,
coordEleMath,
images,
loadListeners,
nodeShapes,
redraw
].forEach( function( props ){
util.extend( BRp, props );
} );
export default BR;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,658 @@
import * as math from '../../../math.mjs';
import * as round from "../../../round.mjs";
var BRp = {};
BRp.generatePolygon = function( name, points ){
return ( this.nodeShapes[ name ] = {
renderer: this,
name: name,
points: points,
draw: function( context, centerX, centerY, width, height, cornerRadius ){
this.renderer.nodeShapeImpl( 'polygon', context, centerX, centerY, width, height, this.points );
},
intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){
return math.polygonIntersectLine(
x, y,
this.points,
nodeX,
nodeY,
width / 2, height / 2,
padding )
;
},
checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){
return math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width, height, [0, -1], padding )
;
},
hasMiterBounds: name !== 'rectangle',
miterBounds: function( centerX, centerY, width, height, strokeWidth, strokePosition ){
return math.miterBox( this.points, centerX, centerY, width, height, strokeWidth, strokePosition );
}
} );
};
BRp.generateEllipse = function(){
return ( this.nodeShapes['ellipse'] = {
renderer: this,
name: 'ellipse',
draw: function( context, centerX, centerY, width, height, cornerRadius ){
this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height );
},
intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){
return math.intersectLineEllipse(
x, y,
nodeX,
nodeY,
width / 2 + padding,
height / 2 + padding )
;
},
checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){
return math.checkInEllipse( x, y, width, height, centerX, centerY, padding );
}
} );
};
BRp.generateRoundPolygon = function( name, points ){
return ( this.nodeShapes[ name ] = {
renderer: this,
name: name,
points: points,
getOrCreateCorners: function (centerX, centerY, width, height, cornerRadius, rs, field) {
if( rs[field] !== undefined && rs[field + '-cx'] === centerX && rs [field + '-cy'] === centerY ){
return rs[field];
}
rs[field] = new Array( points.length / 2 );
rs[field + '-cx'] = centerX;
rs[field + '-cy'] = centerY;
const halfW = width / 2;
const halfH = height / 2;
cornerRadius = cornerRadius === 'auto' ? math.getRoundPolygonRadius( width, height ) : cornerRadius;
const p = new Array( points.length / 2 );
for ( let i = 0; i < points.length / 2; i++ ){
p[i] = {
x: centerX + halfW * points[ i * 2 ],
y: centerY + halfH * points[ i * 2 + 1 ]
};
}
let i, p1, p2, p3, len = p.length;
p1 = p[ len - 1 ];
// for each point
for( i = 0; i < len; i++ ){
p2 = p[ (i) % len ];
p3 = p[ (i + 1) % len ];
rs[ field ][ i ] = round.getRoundCorner( p1, p2, p3, cornerRadius );
p1 = p2;
p2 = p3;
}
return rs[ field ];
},
draw: function( context, centerX, centerY, width, height, cornerRadius , rs){
this.renderer.nodeShapeImpl( 'round-polygon', context, centerX, centerY, width, height, this.points, this.getOrCreateCorners( centerX, centerY, width, height, cornerRadius, rs, 'drawCorners' ));
},
intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius, rs ){
return math.roundPolygonIntersectLine(
x, y,
this.points,
nodeX,
nodeY,
width, height,
padding, this.getOrCreateCorners( nodeX, nodeY, width, height, cornerRadius, rs, 'corners' ) )
;
},
checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius, rs ){
return math.pointInsideRoundPolygon( x, y, this.points,
centerX, centerY, width, height, this.getOrCreateCorners( centerX, centerY, width, height, cornerRadius, rs, 'corners' ) )
;
}
} );
};
BRp.generateRoundRectangle = function(){
return ( this.nodeShapes['round-rectangle'] = this.nodeShapes['roundrectangle'] = {
renderer: this,
name: 'round-rectangle',
points: math.generateUnitNgonPointsFitToSquare( 4, 0 ),
draw: function( context, centerX, centerY, width, height, cornerRadius ){
this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, this.points, cornerRadius );
},
intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){
return math.roundRectangleIntersectLine(
x, y,
nodeX,
nodeY,
width, height,
padding, cornerRadius )
;
},
checkPoint: function(
x, y, padding, width, height, centerX, centerY, cornerRadius ){
let halfWidth = width / 2;
let halfHeight = height / 2;
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( width, height ) : cornerRadius;
cornerRadius = Math.min(halfWidth, halfHeight, cornerRadius);
var diam = cornerRadius * 2;
// Check hBox
if( math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width, height - diam, [0, -1], padding ) ){
return true;
}
// Check vBox
if( math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width - diam, height, [0, -1], padding ) ){
return true;
}
// Check top left quarter circle
if( math.checkInEllipse( x, y,
diam, diam,
centerX - halfWidth + cornerRadius,
centerY - halfHeight + cornerRadius,
padding ) ){
return true;
}
// Check top right quarter circle
if( math.checkInEllipse( x, y,
diam, diam,
centerX + halfWidth - cornerRadius,
centerY - halfHeight + cornerRadius,
padding ) ){
return true;
}
// Check bottom right quarter circle
if( math.checkInEllipse( x, y,
diam, diam,
centerX + halfWidth - cornerRadius,
centerY + halfHeight - cornerRadius,
padding ) ){
return true;
}
// Check bottom left quarter circle
if( math.checkInEllipse( x, y,
diam, diam,
centerX - halfWidth + cornerRadius,
centerY + halfHeight - cornerRadius,
padding ) ){
return true;
}
return false;
}
} );
};
BRp.generateCutRectangle = function(){
return ( this.nodeShapes['cut-rectangle'] = this.nodeShapes['cutrectangle'] = {
renderer: this,
name: 'cut-rectangle',
cornerLength: math.getCutRectangleCornerLength(),
points: math.generateUnitNgonPointsFitToSquare( 4, 0 ),
draw: function( context, centerX, centerY, width, height, cornerRadius ){
this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, null, cornerRadius);
},
generateCutTrianglePts: function( width, height, centerX, centerY, cornerRadius ){
var cl = cornerRadius === 'auto' ? this.cornerLength : cornerRadius;
var hh = height / 2;
var hw = width / 2;
var xBegin = centerX - hw;
var xEnd = centerX + hw;
var yBegin = centerY - hh;
var yEnd = centerY + hh;
// points are in clockwise order, inner (imaginary) triangle pt on [4, 5]
return {
topLeft: [ xBegin, yBegin + cl, xBegin + cl, yBegin, xBegin + cl, yBegin + cl ],
topRight: [ xEnd - cl, yBegin, xEnd, yBegin + cl, xEnd - cl, yBegin + cl ],
bottomRight: [ xEnd, yEnd - cl, xEnd - cl, yEnd, xEnd - cl, yEnd - cl ],
bottomLeft: [ xBegin + cl, yEnd, xBegin, yEnd - cl, xBegin + cl, yEnd - cl ]
};
},
intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){
var cPts = this.generateCutTrianglePts( width + 2*padding, height+2*padding, nodeX, nodeY, cornerRadius );
var pts = [].concat.apply([],
[cPts.topLeft.splice(0, 4), cPts.topRight.splice(0, 4),
cPts.bottomRight.splice(0, 4), cPts.bottomLeft.splice(0, 4)
]);
return math.polygonIntersectLine( x, y, pts, nodeX, nodeY );
},
checkPoint: function( x, y, padding, width, height, centerX, centerY, cornerRadius ){
const cl = cornerRadius === 'auto' ? this.cornerLength : cornerRadius;
// Check hBox
if( math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width, height - 2 * cl, [0, -1], padding ) ){
return true;
}
// Check vBox
if( math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width - 2 * cl, height, [0, -1], padding ) ){
return true;
}
var cutTrianglePts = this.generateCutTrianglePts(width, height, centerX, centerY);
return math.pointInsidePolygonPoints( x, y, cutTrianglePts.topLeft)
|| math.pointInsidePolygonPoints( x, y, cutTrianglePts.topRight )
|| math.pointInsidePolygonPoints( x, y, cutTrianglePts.bottomRight )
|| math.pointInsidePolygonPoints( x, y, cutTrianglePts.bottomLeft );
}
} );
};
BRp.generateBarrel = function(){
return ( this.nodeShapes['barrel'] = {
renderer: this,
name: 'barrel',
points: math.generateUnitNgonPointsFitToSquare( 4, 0 ),
draw: function( context, centerX, centerY, width, height, cornerRadius ){
this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height );
},
intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){
// use two fixed t values for the bezier curve approximation
var t0 = 0.15;
var t1 = 0.5;
var t2 = 0.85;
var bPts = this.generateBarrelBezierPts( width + 2*padding, height + 2*padding, nodeX, nodeY );
var approximateBarrelCurvePts = pts => {
// approximate curve pts based on the two t values
var m0 = math.qbezierPtAt({x: pts[0], y: pts[1]}, {x: pts[2], y: pts[3]}, {x: pts[4], y: pts[5]}, t0);
var m1 = math.qbezierPtAt({x: pts[0], y: pts[1]}, {x: pts[2], y: pts[3]}, {x: pts[4], y: pts[5]}, t1);
var m2 = math.qbezierPtAt({x: pts[0], y: pts[1]}, {x: pts[2], y: pts[3]}, {x: pts[4], y: pts[5]}, t2);
return [
pts[0],pts[1],
m0.x, m0.y,
m1.x, m1.y,
m2.x, m2.y,
pts[4], pts[5]
];
};
var pts = [].concat(
approximateBarrelCurvePts(bPts.topLeft),
approximateBarrelCurvePts(bPts.topRight),
approximateBarrelCurvePts(bPts.bottomRight),
approximateBarrelCurvePts(bPts.bottomLeft)
);
return math.polygonIntersectLine( x, y, pts, nodeX, nodeY );
},
generateBarrelBezierPts: function( width, height, centerX, centerY ){
var hh = height / 2;
var hw = width / 2;
var xBegin = centerX - hw;
var xEnd = centerX + hw;
var yBegin = centerY - hh;
var yEnd = centerY + hh;
var curveConstants = math.getBarrelCurveConstants( width, height );
var hOffset = curveConstants.heightOffset;
var wOffset = curveConstants.widthOffset;
var ctrlPtXOffset = curveConstants.ctrlPtOffsetPct * width;
// points are in clockwise order, inner (imaginary) control pt on [4, 5]
var pts = {
topLeft: [ xBegin, yBegin + hOffset, xBegin + ctrlPtXOffset, yBegin, xBegin + wOffset, yBegin ],
topRight: [ xEnd - wOffset, yBegin, xEnd - ctrlPtXOffset, yBegin, xEnd, yBegin + hOffset ],
bottomRight: [ xEnd, yEnd - hOffset, xEnd - ctrlPtXOffset, yEnd, xEnd - wOffset, yEnd ],
bottomLeft: [ xBegin + wOffset, yEnd, xBegin + ctrlPtXOffset, yEnd, xBegin, yEnd - hOffset ]
};
pts.topLeft.isTop = true;
pts.topRight.isTop = true;
pts.bottomLeft.isBottom = true;
pts.bottomRight.isBottom = true;
return pts;
},
checkPoint: function(
x, y, padding, width, height, centerX, centerY, cornerRadius){
var curveConstants = math.getBarrelCurveConstants( width, height );
var hOffset = curveConstants.heightOffset;
var wOffset = curveConstants.widthOffset;
// Check hBox
if( math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width, height - 2 * hOffset, [0, -1], padding ) ){
return true;
}
// Check vBox
if( math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width - 2 * wOffset, height, [0, -1], padding ) ){
return true;
}
var barrelCurvePts = this.generateBarrelBezierPts( width, height, centerX, centerY );
var getCurveT = function (x, y, curvePts) {
var x0 = curvePts[ 4 ];
var x1 = curvePts[ 2 ];
var x2 = curvePts[ 0 ];
var y0 = curvePts[ 5 ];
// var y1 = curvePts[ 3 ];
var y2 = curvePts[ 1 ];
var xMin = Math.min( x0, x2 );
var xMax = Math.max( x0, x2 );
var yMin = Math.min( y0, y2 );
var yMax = Math.max( y0, y2 );
if( xMin <= x && x <= xMax && yMin <= y && y <= yMax ){
var coeff = math.bezierPtsToQuadCoeff( x0, x1, x2 );
var roots = math.solveQuadratic( coeff[0], coeff[1], coeff[2], x );
var validRoots = roots.filter(function( r ){
return 0 <= r && r <= 1;
});
if( validRoots.length > 0 ){
return validRoots[ 0 ];
}
}
return null;
};
var curveRegions = Object.keys( barrelCurvePts );
for( var i = 0; i < curveRegions.length; i++ ){
var corner = curveRegions[ i ];
var cornerPts = barrelCurvePts[ corner ];
var t = getCurveT( x, y, cornerPts );
if( t == null ){ continue; }
var y0 = cornerPts[ 5 ];
var y1 = cornerPts[ 3 ];
var y2 = cornerPts[ 1 ];
var bezY = math.qbezierAt( y0, y1, y2, t );
if( cornerPts.isTop && bezY <= y ){
return true;
}
if( cornerPts.isBottom && y <= bezY ){
return true;
}
}
return false;
}
} );
};
BRp.generateBottomRoundrectangle = function(){
return ( this.nodeShapes['bottom-round-rectangle'] = this.nodeShapes['bottomroundrectangle'] = {
renderer: this,
name: 'bottom-round-rectangle',
points: math.generateUnitNgonPointsFitToSquare( 4, 0 ),
draw: function( context, centerX, centerY, width, height, cornerRadius ){
this.renderer.nodeShapeImpl( this.name, context, centerX, centerY, width, height, this.points, cornerRadius );
},
intersectLine: function( nodeX, nodeY, width, height, x, y, padding, cornerRadius ){
var topStartX = nodeX - ( width / 2 + padding );
var topStartY = nodeY - ( height / 2 + padding );
var topEndY = topStartY;
var topEndX = nodeX + ( width / 2 + padding );
var topIntersections = math.finiteLinesIntersect(
x, y, nodeX, nodeY, topStartX, topStartY, topEndX, topEndY, false );
if( topIntersections.length > 0 ){
return topIntersections;
}
return math.roundRectangleIntersectLine(
x, y,
nodeX,
nodeY,
width, height,
padding, cornerRadius )
;
},
checkPoint: function(
x, y, padding, width, height, centerX, centerY, cornerRadius ){
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( width, height ) : cornerRadius;
var diam = 2 * cornerRadius;
// Check hBox
if( math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width, height - diam, [0, -1], padding ) ){
return true;
}
// Check vBox
if( math.pointInsidePolygon( x, y, this.points,
centerX, centerY, width - diam, height, [0, -1], padding ) ){
return true;
}
// check non-rounded top side
var outerWidth = ( ( width / 2 ) + 2 * padding );
var outerHeight = ( ( height / 2 ) + 2 * padding );
var points = [
centerX - outerWidth, centerY - outerHeight,
centerX - outerWidth, centerY,
centerX + outerWidth, centerY,
centerX + outerWidth, centerY - outerHeight
];
if( math.pointInsidePolygonPoints( x, y, points) ){
return true;
}
// Check bottom right quarter circle
if( math.checkInEllipse( x, y,
diam, diam,
centerX + width / 2 - cornerRadius,
centerY + height / 2 - cornerRadius,
padding ) ){
return true;
}
// Check bottom left quarter circle
if( math.checkInEllipse( x, y,
diam, diam,
centerX - width / 2 + cornerRadius,
centerY + height / 2 - cornerRadius,
padding ) ){
return true;
}
return false;
}
} );
};
BRp.registerNodeShapes = function(){
var nodeShapes = this.nodeShapes = {};
var renderer = this;
this.generateEllipse();
this.generatePolygon( 'triangle', math.generateUnitNgonPointsFitToSquare( 3, 0 ) );
this.generateRoundPolygon( 'round-triangle', math.generateUnitNgonPointsFitToSquare( 3, 0 ) );
this.generatePolygon( 'rectangle', math.generateUnitNgonPointsFitToSquare( 4, 0 ) );
nodeShapes[ 'square' ] = nodeShapes[ 'rectangle' ];
this.generateRoundRectangle();
this.generateCutRectangle();
this.generateBarrel();
this.generateBottomRoundrectangle();
{
const diamondPoints = [
0, 1,
1, 0,
0, -1,
-1, 0
];
this.generatePolygon( 'diamond', diamondPoints );
this.generateRoundPolygon( 'round-diamond', diamondPoints );
}
this.generatePolygon( 'pentagon', math.generateUnitNgonPointsFitToSquare( 5, 0 ) );
this.generateRoundPolygon( 'round-pentagon', math.generateUnitNgonPointsFitToSquare( 5, 0) );
this.generatePolygon( 'hexagon', math.generateUnitNgonPointsFitToSquare( 6, 0 ) );
this.generateRoundPolygon( 'round-hexagon', math.generateUnitNgonPointsFitToSquare( 6, 0) );
this.generatePolygon( 'heptagon', math.generateUnitNgonPointsFitToSquare( 7, 0 ) );
this.generateRoundPolygon( 'round-heptagon', math.generateUnitNgonPointsFitToSquare( 7, 0) );
this.generatePolygon( 'octagon', math.generateUnitNgonPointsFitToSquare( 8, 0 ) );
this.generateRoundPolygon( 'round-octagon', math.generateUnitNgonPointsFitToSquare( 8, 0) );
var star5Points = new Array( 20 );
{
var outerPoints = math.generateUnitNgonPoints( 5, 0 );
var innerPoints = math.generateUnitNgonPoints( 5, Math.PI / 5 );
// Outer radius is 1; inner radius of star is smaller
var innerRadius = 0.5 * (3 - Math.sqrt( 5 ));
innerRadius *= 1.57;
for( var i = 0;i < innerPoints.length / 2;i++ ){
innerPoints[ i * 2] *= innerRadius;
innerPoints[ i * 2 + 1] *= innerRadius;
}
for( var i = 0;i < 20 / 4;i++ ){
star5Points[ i * 4] = outerPoints[ i * 2];
star5Points[ i * 4 + 1] = outerPoints[ i * 2 + 1];
star5Points[ i * 4 + 2] = innerPoints[ i * 2];
star5Points[ i * 4 + 3] = innerPoints[ i * 2 + 1];
}
}
star5Points = math.fitPolygonToSquare( star5Points );
this.generatePolygon( 'star', star5Points );
this.generatePolygon( 'vee', [
-1, -1,
0, -0.333,
1, -1,
0, 1
] );
this.generatePolygon( 'rhomboid', [
-1, -1,
0.333, -1,
1, 1,
-0.333, 1
] );
this.generatePolygon( 'right-rhomboid', [
-0.333, -1,
1, -1,
0.333, 1,
-1, 1
] );
this.nodeShapes['concavehexagon'] = this.generatePolygon( 'concave-hexagon', [
-1, -0.95,
-0.75, 0,
-1, 0.95,
1, 0.95,
0.75, 0,
1, -0.95
] );
{
const tagPoints = [
-1, -1,
0.25, -1,
1, 0,
0.25,1,
-1, 1
];
this.generatePolygon( 'tag', tagPoints );
this.generateRoundPolygon( 'round-tag', tagPoints );
}
nodeShapes.makePolygon = function( points ){
// use caching on user-specified polygons so they are as fast as native shapes
var key = points.join( '$' );
var name = 'polygon-' + key;
var shape;
if( (shape = this[ name ]) ){ // got cached shape
return shape;
}
// create and cache new shape
return renderer.generatePolygon( name, points );
};
};
export default BRp;
+108
View File
@@ -0,0 +1,108 @@
import * as util from '../../../util/index.mjs';
var BRp = {};
BRp.timeToRender = function(){
return this.redrawTotalTime / this.redrawCount;
};
BRp.redraw = function( options ){
options = options || util.staticEmptyObject();
var r = this;
if( r.averageRedrawTime === undefined ){ r.averageRedrawTime = 0; }
if( r.lastRedrawTime === undefined ){ r.lastRedrawTime = 0; }
if( r.lastDrawTime === undefined ){ r.lastDrawTime = 0; }
r.requestedFrame = true;
r.renderOptions = options;
};
BRp.beforeRender = function( fn, priority ){
// the renderer can't add tick callbacks when destroyed
if( this.destroyed ){ return; }
if( priority == null ){
util.error('Priority is not optional for beforeRender');
}
var cbs = this.beforeRenderCallbacks;
cbs.push({ fn: fn, priority: priority });
// higher priority callbacks executed first
cbs.sort(function( a, b ){ return b.priority - a.priority; });
};
var beforeRenderCallbacks = function( r, willDraw, startTime ){
var cbs = r.beforeRenderCallbacks;
for( var i = 0; i < cbs.length; i++ ){
cbs[i].fn( willDraw, startTime );
}
};
BRp.startRenderLoop = function(){
var r = this;
var cy = r.cy;
if( r.renderLoopStarted ){
return;
} else {
r.renderLoopStarted = true;
}
var renderFn = function( requestTime ){
if( r.destroyed ){ return; }
if( cy.batching() ){
// mid-batch, none of these should run
// - pre frame hooks (calculations, texture caches, style, etc.)
// - any drawing
} else if( r.requestedFrame && !r.skipFrame ){
beforeRenderCallbacks( r, true, requestTime );
var startTime = util.performanceNow();
r.render( r.renderOptions );
var endTime = r.lastDrawTime = util.performanceNow();
if( r.averageRedrawTime === undefined ){
r.averageRedrawTime = endTime - startTime;
}
if( r.redrawCount === undefined ){
r.redrawCount = 0;
}
r.redrawCount++;
if( r.redrawTotalTime === undefined ){
r.redrawTotalTime = 0;
}
var duration = endTime - startTime;
r.redrawTotalTime += duration;
r.lastRedrawTime = duration;
// use a weighted average with a bias from the previous average so we don't spike so easily
r.averageRedrawTime = r.averageRedrawTime / 2 + duration / 2;
r.requestedFrame = false;
} else {
beforeRenderCallbacks( r, false, requestTime );
}
r.skipFrame = false;
util.requestAnimationFrame( renderFn );
};
util.requestAnimationFrame( renderFn );
};
export default BRp;
@@ -0,0 +1,87 @@
var CRp = {};
var impl;
function polygon( context, points ){
for( var i = 0; i < points.length; i++ ){
var pt = points[ i ];
context.lineTo( pt.x, pt.y );
}
}
function triangleBackcurve( context, points, controlPoint ){
var firstPt;
for( var i = 0; i < points.length; i++ ){
var pt = points[ i ];
if( i === 0 ){
firstPt = pt;
}
context.lineTo( pt.x, pt.y );
}
context.quadraticCurveTo( controlPoint.x, controlPoint.y, firstPt.x, firstPt.y );
}
function triangleTee( context, trianglePoints, teePoints ){
if( context.beginPath ){ context.beginPath(); }
var triPts = trianglePoints;
for( var i = 0; i < triPts.length; i++ ){
var pt = triPts[ i ];
context.lineTo( pt.x, pt.y );
}
var teePts = teePoints;
var firstTeePt = teePoints[0];
context.moveTo( firstTeePt.x, firstTeePt.y );
for( var i = 1; i < teePts.length; i++ ){
var pt = teePts[ i ];
context.lineTo( pt.x, pt.y );
}
if( context.closePath ){ context.closePath(); }
}
function circleTriangle(context, trianglePoints, rx, ry, r) {
if (context.beginPath) { context.beginPath(); }
context.arc(rx, ry, r, 0, Math.PI * 2, false);
var triPts = trianglePoints;
var firstTrPt = triPts[0];
context.moveTo(firstTrPt.x, firstTrPt.y);
for (var i = 0; i < triPts.length; i++) {
var pt = triPts[i];
context.lineTo(pt.x, pt.y);
}
if (context.closePath) {
context.closePath();
}
}
function circle( context, rx, ry, r ){
context.arc( rx, ry, r, 0, Math.PI * 2, false );
}
CRp.arrowShapeImpl = function( name ){
return ( impl || (impl = {
'polygon': polygon,
'triangle-backcurve': triangleBackcurve,
'triangle-tee': triangleTee,
'circle-triangle' : circleTriangle,
'triangle-cross': triangleTee,
'circle': circle
}) )[ name ];
};
export default CRp;
@@ -0,0 +1,431 @@
/* global Path2D */
import * as util from '../../../util/index.mjs';
import {drawPreparedRoundCorner} from "../../../round.mjs";
let CRp = {};
CRp.drawEdge = function( context, edge, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){
let r = this;
let rs = edge._private.rscratch;
if( shouldDrawOpacity && !edge.visible() ){ return; }
// if bezier ctrl pts can not be calculated, then die
if( rs.badLine || rs.allpts == null || isNaN(rs.allpts[0]) ){ // isNaN in case edge is impossible and browser bugs (e.g. safari)
return;
}
let bb;
if( shiftToOriginWithBb ){
bb = shiftToOriginWithBb;
context.translate( -bb.x1, -bb.y1 );
}
let opacity = shouldDrawOpacity ? edge.pstyle('opacity').value : 1;
let lineOpacity = shouldDrawOpacity ? edge.pstyle('line-opacity').value : 1;
let curveStyle = edge.pstyle('curve-style').value;
let lineStyle = edge.pstyle('line-style').value;
let edgeWidth = edge.pstyle('width').pfValue;
let lineCap = edge.pstyle('line-cap').value;
let lineOutlineWidth = edge.pstyle('line-outline-width').value;
let lineOutlineColor = edge.pstyle('line-outline-color').value;
let effectiveLineOpacity = opacity * lineOpacity;
// separate arrow opacity would require arrow-opacity property
let effectiveArrowOpacity = opacity * lineOpacity;
let drawLine = ( strokeOpacity = effectiveLineOpacity) => {
if (curveStyle === 'straight-triangle') {
r.eleStrokeStyle( context, edge, strokeOpacity );
r.drawEdgeTrianglePath(
edge,
context,
rs.allpts
);
} else {
context.lineWidth = edgeWidth;
context.lineCap = lineCap;
r.eleStrokeStyle( context, edge, strokeOpacity );
r.drawEdgePath(
edge,
context,
rs.allpts,
lineStyle
);
context.lineCap = 'butt'; // reset for other drawing functions
}
};
let drawLineOutline = ( strokeOpacity = effectiveLineOpacity) => {
context.lineWidth = edgeWidth + lineOutlineWidth;
context.lineCap = lineCap;
if (lineOutlineWidth > 0) {
r.colorStrokeStyle( context, lineOutlineColor[0], lineOutlineColor[1], lineOutlineColor[2], strokeOpacity );
} else {
// do not draw any lineOutline
context.lineCap = 'butt'; // reset for other drawing functions
return;
}
if (curveStyle === 'straight-triangle') {
r.drawEdgeTrianglePath(
edge,
context,
rs.allpts
);
} else {
r.drawEdgePath(
edge,
context,
rs.allpts,
lineStyle
);
context.lineCap = 'butt'; // reset for other drawing functions
}
};
let drawOverlay = () => {
if( !shouldDrawOverlay ){ return; }
r.drawEdgeOverlay( context, edge );
};
let drawUnderlay = () => {
if( !shouldDrawOverlay ){ return; }
r.drawEdgeUnderlay( context, edge );
};
let drawArrows = ( arrowOpacity = effectiveArrowOpacity) => {
r.drawArrowheads( context, edge, arrowOpacity );
};
let drawText = () => {
r.drawElementText( context, edge, null, drawLabel );
};
context.lineJoin = 'round';
let ghost = edge.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = edge.pstyle('ghost-offset-x').pfValue;
let gy = edge.pstyle('ghost-offset-y').pfValue;
let ghostOpacity = edge.pstyle('ghost-opacity').value;
let effectiveGhostOpacity = effectiveLineOpacity * ghostOpacity;
context.translate( gx, gy );
drawLine( effectiveGhostOpacity );
drawArrows( effectiveGhostOpacity );
context.translate( -gx, -gy );
} else {
drawLineOutline();
}
drawUnderlay();
drawLine();
drawArrows();
drawOverlay();
drawText();
if( shiftToOriginWithBb ){
context.translate( bb.x1, bb.y1 );
}
};
const drawEdgeOverlayUnderlay = function( overlayOrUnderlay ) {
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) {
throw new Error('Invalid state');
}
return function( context, edge ){
if( !edge.visible() ){ return; }
let opacity = edge.pstyle(`${overlayOrUnderlay}-opacity`).value;
if( opacity === 0 ){ return; }
let r = this;
let usePaths = r.usePaths();
let rs = edge._private.rscratch;
let padding = edge.pstyle(`${overlayOrUnderlay}-padding`).pfValue;
let width = 2 * padding;
let color = edge.pstyle(`${overlayOrUnderlay}-color`).value;
context.lineWidth = width;
if( rs.edgeType === 'self' && !usePaths ){
context.lineCap = 'butt';
} else {
context.lineCap = 'round';
}
r.colorStrokeStyle( context, color[0], color[1], color[2], opacity );
r.drawEdgePath(
edge,
context,
rs.allpts,
'solid'
);
};
};
CRp.drawEdgeOverlay = drawEdgeOverlayUnderlay('overlay');
CRp.drawEdgeUnderlay = drawEdgeOverlayUnderlay('underlay');
CRp.drawEdgePath = function( edge, context, pts, type ){
let rs = edge._private.rscratch;
let canvasCxt = context;
let path;
let pathCacheHit = false;
let usePaths = this.usePaths();
let lineDashPattern = edge.pstyle('line-dash-pattern').pfValue;
let lineDashOffset = edge.pstyle('line-dash-offset').pfValue;
if( usePaths ){
let pathCacheKey = pts.join( '$' );
let keyMatches = rs.pathCacheKey && rs.pathCacheKey === pathCacheKey;
if( keyMatches ){
path = context = rs.pathCache;
pathCacheHit = true;
} else {
path = context = new Path2D();
rs.pathCacheKey = pathCacheKey;
rs.pathCache = path;
}
}
if( canvasCxt.setLineDash ){ // for very outofdate browsers
switch( type ){
case 'dotted':
canvasCxt.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
canvasCxt.setLineDash( lineDashPattern );
canvasCxt.lineDashOffset = lineDashOffset;
break;
case 'solid':
canvasCxt.setLineDash( [ ] );
break;
}
}
if( !pathCacheHit && !rs.badLine ){
if( context.beginPath ){ context.beginPath(); }
context.moveTo( pts[0], pts[1] );
switch( rs.edgeType ){
case 'bezier':
case 'self':
case 'compound':
case 'multibezier':
for( let i = 2; i + 3 < pts.length; i += 4 ){
context.quadraticCurveTo( pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3] );
}
break;
case 'straight':
case 'haystack':
for( let i = 2; i + 1 < pts.length; i += 2 ) {
context.lineTo( pts[ i ], pts[ i + 1] );
}
break;
case 'segments':
if (rs.isRound) {
for( let corner of rs.roundCorners ){
drawPreparedRoundCorner(context, corner);
}
context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] );
} else {
for( let i = 2; i + 1 < pts.length; i += 2 ) {
context.lineTo( pts[ i ], pts[ i + 1] );
}
}
break;
}
}
context = canvasCxt;
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
// reset any line dashes
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
};
CRp.drawEdgeTrianglePath = function( edge, context, pts ){
// use line stroke style for triangle fill style
context.fillStyle = context.strokeStyle;
let edgeWidth = edge.pstyle('width').pfValue;
for( let i = 0; i + 1 < pts.length; i += 2 ){
const vector = [ pts[ i + 2 ] - pts[ i ], pts[ i + 3 ] - pts[ i + 1 ] ];
const length = Math.sqrt( vector[0] * vector[0] + vector[1] * vector[1] );
const normal = [ vector[1] / length, -vector[0] / length ];
const triangleHead = [ normal[0] * edgeWidth / 2, normal[1] * edgeWidth / 2 ];
context.beginPath();
context.moveTo( pts[ i ] - triangleHead[0], pts[ i + 1 ] - triangleHead[1] );
context.lineTo( pts[ i ] + triangleHead[0], pts[ i + 1 ] + triangleHead[1] );
context.lineTo( pts[ i + 2 ], pts[ i + 3 ] );
context.closePath();
context.fill();
}
};
CRp.drawArrowheads = function( context, edge, opacity ){
let rs = edge._private.rscratch;
let isHaystack = rs.edgeType === 'haystack';
if( !isHaystack ){
this.drawArrowhead( context, edge, 'source', rs.arrowStartX, rs.arrowStartY, rs.srcArrowAngle, opacity );
}
this.drawArrowhead( context, edge, 'mid-target', rs.midX, rs.midY, rs.midtgtArrowAngle, opacity );
this.drawArrowhead( context, edge, 'mid-source', rs.midX, rs.midY, rs.midsrcArrowAngle, opacity );
if( !isHaystack ){
this.drawArrowhead( context, edge, 'target', rs.arrowEndX, rs.arrowEndY, rs.tgtArrowAngle, opacity );
}
};
CRp.drawArrowhead = function( context, edge, prefix, x, y, angle, opacity ){
if( isNaN( x ) || x == null || isNaN( y ) || y == null || isNaN( angle ) || angle == null ){ return; }
let self = this;
let arrowShape = edge.pstyle( prefix + '-arrow-shape' ).value;
if( arrowShape === 'none' ) { return; }
let arrowClearFill = edge.pstyle( prefix + '-arrow-fill' ).value === 'hollow' ? 'both' : 'filled';
let arrowFill = edge.pstyle( prefix + '-arrow-fill' ).value;
let edgeWidth = edge.pstyle( 'width' ).pfValue;
let pArrowWidth = edge.pstyle( prefix + '-arrow-width' );
let arrowWidth = pArrowWidth.value === 'match-line' ? edgeWidth : pArrowWidth.pfValue;
if (pArrowWidth.units === '%') arrowWidth *= edgeWidth;
let edgeOpacity = edge.pstyle( 'opacity' ).value;
if( opacity === undefined ){
opacity = edgeOpacity;
}
let gco = context.globalCompositeOperation;
if( opacity !== 1 || arrowFill === 'hollow' ){ // then extra clear is needed
context.globalCompositeOperation = 'destination-out';
self.colorFillStyle( context, 255, 255, 255, 1 );
self.colorStrokeStyle( context, 255, 255, 255, 1 );
self.drawArrowShape( edge, context,
arrowClearFill, edgeWidth, arrowShape, arrowWidth, x, y, angle
);
context.globalCompositeOperation = gco;
} // otherwise, the opaque arrow clears it for free :)
let color = edge.pstyle( prefix + '-arrow-color' ).value;
self.colorFillStyle( context, color[0], color[1], color[2], opacity );
self.colorStrokeStyle( context, color[0], color[1], color[2], opacity );
self.drawArrowShape( edge, context,
arrowFill, edgeWidth, arrowShape, arrowWidth, x, y, angle
);
};
CRp.drawArrowShape = function( edge, context, fill, edgeWidth, shape, shapeWidth, x, y, angle ){
let r = this;
let usePaths = this.usePaths() && shape !== 'triangle-cross';
let pathCacheHit = false;
let path;
let canvasContext = context;
let translation = { x, y };
let scale = edge.pstyle( 'arrow-scale' ).value;
let size = this.getArrowWidth( edgeWidth, scale );
let shapeImpl = r.arrowShapes[ shape ];
if( usePaths ){
let cache = r.arrowPathCache = r.arrowPathCache || [];
let key = util.hashString(shape);
let cachedPath = cache[ key ];
if( cachedPath != null ){
path = context = cachedPath;
pathCacheHit = true;
} else {
path = context = new Path2D();
cache[ key ] = path;
}
}
if( !pathCacheHit ){
if( context.beginPath ){ context.beginPath(); }
if( usePaths ){ // store in the path cache with values easily manipulated later
shapeImpl.draw( context, 1, 0, { x: 0, y: 0 }, 1 );
} else {
shapeImpl.draw( context, size, angle, translation, edgeWidth );
}
if( context.closePath ){ context.closePath(); }
}
context = canvasContext;
if( usePaths ){ // set transform to arrow position/orientation
context.translate( x, y );
context.rotate( angle );
context.scale( size, size );
}
if( fill === 'filled' || fill === 'both' ){
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
}
if( fill === 'hollow' || fill === 'both' ){
context.lineWidth = shapeWidth / (usePaths ? size : 1);
context.lineJoin = 'miter';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
}
if( usePaths ){ // reset transform by applying inverse
context.scale( 1/size, 1/size );
context.rotate( -angle );
context.translate( -x, -y );
}
};
export default CRp;

Some files were not shown because too many files have changed in this diff Show More