cytoscape
Version:
Graph theory (a.k.a. network) library for analysis and visualisation
440 lines (337 loc) • 11.1 kB
JavaScript
/* global Path2D */
import * as is from '../../../is';
import * as util from '../../../util';
let CRp = {};
CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){
let r = this;
let nodeWidth, nodeHeight;
let _p = node._private;
let rs = _p.rscratch;
let pos = node.position();
if( !is.number( pos.x ) || !is.number( pos.y ) ){
return; // can't draw node with undefined position
}
if( shouldDrawOpacity && !node.visible() ){ return; }
let eleOpacity = shouldDrawOpacity ? node.effectiveOpacity() : 1;
let usePaths = r.usePaths();
let path;
let pathCacheHit = false;
let padding = node.padding();
nodeWidth = node.width() + 2 * padding;
nodeHeight = node.height() + 2 * padding;
//
// setup shift
let bb;
if( shiftToOriginWithBb ){
bb = shiftToOriginWithBb;
context.translate( -bb.x1, -bb.y1 );
}
//
// load bg image
let bgImgProp = node.pstyle( 'background-image' );
let urls = bgImgProp.value;
let urlDefined = new Array( urls.length );
let image = new Array( urls.length );
let numImages = 0;
for( let i = 0; i < urls.length; i++ ){
let url = urls[i];
let defd = urlDefined[i] = url != null && url !== 'none';
if( defd ){
let bgImgCrossOrigin = node.cy().style().getIndexedStyle(node, 'background-image-crossorigin', 'value', i);
numImages++;
// get image, and if not loaded then ask to redraw when later loaded
image[i] = r.getCachedImage( url, bgImgCrossOrigin, function(){
_p.backgroundTimestamp = Date.now();
node.emitAndNotify('background');
} );
}
}
//
// setup styles
let darkness = node.pstyle('background-blacken').value;
let borderWidth = node.pstyle('border-width').pfValue;
let bgOpacity = node.pstyle('background-opacity').value * eleOpacity;
let borderColor = node.pstyle('border-color').value;
let borderStyle = node.pstyle('border-style').value;
let borderOpacity = node.pstyle('border-opacity').value * eleOpacity;
context.lineJoin = 'miter'; // so borders are square with the node shape
let setupShapeColor = ( bgOpy = bgOpacity ) => {
r.eleFillStyle( context, node, bgOpy );
};
let setupBorderColor = ( bdrOpy = borderOpacity ) => {
r.colorStrokeStyle( context, borderColor[0], borderColor[1], borderColor[2], bdrOpy );
};
//
// setup shape
let styleShape = node.pstyle('shape').strValue;
let shapePts = node.pstyle('shape-polygon-points').pfValue;
if( usePaths ){
context.translate( pos.x, pos.y );
let pathCache = r.nodePathCache = r.nodePathCache || [];
let key = util.hashStrings(
styleShape === 'polygon' ? styleShape + ',' + shapePts.join(',') : styleShape,
'' + nodeHeight,
'' + nodeWidth
);
let cachedPath = pathCache[ key ];
if( cachedPath != null ){
path = cachedPath;
pathCacheHit = true;
rs.pathCache = path;
} else {
path = new Path2D();
pathCache[ key ] = rs.pathCache = path;
}
}
let drawShape = () => {
if( !pathCacheHit ){
let npos = pos;
if( usePaths ){
npos = {
x: 0,
y: 0
};
}
r.nodeShapes[ r.getNodeShape( node ) ].draw(
( path || context ),
npos.x,
npos.y,
nodeWidth,
nodeHeight );
}
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
};
let drawImages = ( nodeOpacity = eleOpacity, inside = true ) => {
let prevBging = _p.backgrounding;
let totalCompleted = 0;
for( let i = 0; i < image.length; i++ ){
const bgContainment = node.cy().style().getIndexedStyle(node, 'background-image-containment', 'value', i);
if( inside && bgContainment === 'over' || !inside && bgContainment === 'inside' ){
totalCompleted++;
continue;
}
if( urlDefined[i] && image[i].complete && !image[i].error ){
totalCompleted++;
r.drawInscribedImage( context, image[i], node, i, nodeOpacity );
}
}
_p.backgrounding = !(totalCompleted === numImages);
if( prevBging !== _p.backgrounding ){ // update style b/c :backgrounding state changed
node.updateStyle( false );
}
};
let drawPie = ( redrawShape = false, pieOpacity = eleOpacity ) => {
if( r.hasPie( node ) ){
r.drawPie( context, node, pieOpacity );
// redraw/restore path if steps after pie need it
if( redrawShape ){
if( !usePaths ){
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight );
}
}
}
};
let darken = ( darkenOpacity = eleOpacity ) => {
let opacity = ( darkness > 0 ? darkness : -darkness ) * darkenOpacity;
let c = darkness > 0 ? 0 : 255;
if( darkness !== 0 ){
r.colorFillStyle( context, c, c, c, opacity );
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
}
};
let drawBorder = () => {
if( borderWidth > 0 ){
context.lineWidth = borderWidth;
context.lineCap = 'butt';
if( context.setLineDash ){ // for very outofdate browsers
switch( borderStyle ){
case 'dotted':
context.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
context.setLineDash( [ 4, 2 ] );
break;
case 'solid':
case 'double':
context.setLineDash( [ ] );
break;
}
}
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
if( borderStyle === 'double' ){
context.lineWidth = borderWidth / 3;
let gco = context.globalCompositeOperation;
context.globalCompositeOperation = 'destination-out';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
context.globalCompositeOperation = gco;
}
// reset in case we changed the border style
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};
let drawOverlay = () => {
if( shouldDrawOverlay ){
r.drawNodeOverlay( context, node, pos, nodeWidth, nodeHeight );
}
};
let drawUnderlay = () => {
if( shouldDrawOverlay ){
r.drawNodeUnderlay( context, node, pos, nodeWidth, nodeHeight );
}
};
let drawText = () => {
r.drawElementText( context, node, null, drawLabel );
};
let ghost = node.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = node.pstyle('ghost-offset-x').pfValue;
let gy = node.pstyle('ghost-offset-y').pfValue;
let ghostOpacity = node.pstyle('ghost-opacity').value;
let effGhostOpacity = ghostOpacity * eleOpacity;
context.translate( gx, gy );
setupShapeColor( ghostOpacity * bgOpacity );
drawShape();
drawImages( effGhostOpacity, true );
setupBorderColor( ghostOpacity * borderOpacity );
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawImages( effGhostOpacity, false );
darken( effGhostOpacity );
context.translate( -gx, -gy );
}
if( usePaths ){
context.translate( -pos.x, -pos.y );
}
drawUnderlay();
if( usePaths ){
context.translate( pos.x, pos.y );
}
setupShapeColor();
drawShape();
drawImages(eleOpacity, true);
setupBorderColor();
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawImages(eleOpacity, false);
darken();
if( usePaths ){
context.translate( -pos.x, -pos.y );
}
drawText();
drawOverlay();
//
// clean up shift
if( shiftToOriginWithBb ){
context.translate( bb.x1, bb.y1 );
}
};
const drawNodeOverlayUnderlay = function( overlayOrUnderlay ) {
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) {
throw new Error('Invalid state');
}
return function( context, node, pos, nodeWidth, nodeHeight ){
let r = this;
if( !node.visible() ){ return; }
let padding = node.pstyle( `${overlayOrUnderlay}-padding` ).pfValue;
let opacity = node.pstyle( `${overlayOrUnderlay}-opacity` ).value;
let color = node.pstyle( `${overlayOrUnderlay}-color` ).value;
var shape = node.pstyle( `${overlayOrUnderlay}-shape` ).value;
if( opacity > 0 ){
pos = pos || node.position();
if( nodeWidth == null || nodeHeight == null ){
let padding = node.padding();
nodeWidth = node.width() + 2 * padding;
nodeHeight = node.height() + 2 * padding;
}
r.colorFillStyle( context, color[0], color[1], color[2], opacity );
r.nodeShapes[shape].draw(
context,
pos.x,
pos.y,
nodeWidth + padding * 2,
nodeHeight + padding * 2
);
context.fill();
}
};
};
CRp.drawNodeOverlay = drawNodeOverlayUnderlay('overlay');
CRp.drawNodeUnderlay = drawNodeOverlayUnderlay('underlay');
// does the node have at least one pie piece?
CRp.hasPie = function( node ){
node = node[0]; // ensure ele ref
return node._private.hasPie;
};
CRp.drawPie = function( context, node, nodeOpacity, pos ){
node = node[0]; // ensure ele ref
pos = pos || node.position();
let cyStyle = node.cy().style();
let pieSize = node.pstyle( 'pie-size' );
let x = pos.x;
let y = pos.y;
let nodeW = node.width();
let nodeH = node.height();
let radius = Math.min( nodeW, nodeH ) / 2; // must fit in node
let lastPercent = 0; // what % to continue drawing pie slices from on [0, 1]
let usePaths = this.usePaths();
if( usePaths ){
x = 0;
y = 0;
}
if( pieSize.units === '%' ){
radius = radius * pieSize.pfValue;
} else if( pieSize.pfValue !== undefined ){
radius = pieSize.pfValue / 2;
}
for( let i = 1; i <= cyStyle.pieBackgroundN; i++ ){ // 1..N
let size = node.pstyle( 'pie-' + i + '-background-size' ).value;
let color = node.pstyle( 'pie-' + i + '-background-color' ).value;
let opacity = node.pstyle( 'pie-' + i + '-background-opacity' ).value * nodeOpacity;
let percent = size / 100; // map integer range [0, 100] to [0, 1]
// percent can't push beyond 1
if( percent + lastPercent > 1 ){
percent = 1 - lastPercent;
}
let angleStart = 1.5 * Math.PI + 2 * Math.PI * lastPercent; // start at 12 o'clock and go clockwise
let angleDelta = 2 * Math.PI * percent;
let angleEnd = angleStart + angleDelta;
// ignore if
// - zero size
// - we're already beyond the full circle
// - adding the current slice would go beyond the full circle
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){
continue;
}
context.beginPath();
context.moveTo( x, y );
context.arc( x, y, radius, angleStart, angleEnd );
context.closePath();
this.colorFillStyle( context, color[0], color[1], color[2], opacity );
context.fill();
lastPercent += percent;
}
};
export default CRp;