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