Created
January 24, 2025 02:03
-
-
Save fabric-io-rodrigues/f5304881e354bfc99f3623519567e4cc to your computer and use it in GitHub Desktop.
Try curves from elk layout in cytoscapejs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//https://eclipse.dev/elk/reference/algorithms/org-eclipse-elk-layered.html | |
var data = [ | |
{ data: { id: 'D' } }, | |
{ data: { id: 'A' } }, | |
{ data: { id: 'B' } }, | |
{ data: { id: 'C' } }, | |
{ data: { id: 'E' } }, | |
{ data: { id: 'F' } }, | |
{ data: { id: 'B1' } }, | |
{ data: { id: 'C1' } }, | |
{ data: { id: 'E1' } }, | |
{ data: { id: 'F1' } }, | |
{ data: { id: 'E2' } }, | |
{ data: { id: 'F2' } }, | |
{ data: { id: 'DE1', source: 'D', target: 'E1' } }, | |
{ data: { id: 'E1F1', source: 'E1', target: 'F1' } }, | |
{ data: { id: 'DE2', source: 'D', target: 'E2' } }, | |
{ data: { id: 'E2F2', source: 'E2', target: 'F2' } }, | |
{ data: { id: 'BC', source: 'B', target: 'C' } }, | |
{ data: { id: 'CD', source: 'C', target: 'D' } }, | |
{ data: { id: 'DF', source: 'D', target: 'F' } }, | |
{ data: { id: 'DE', source: 'D', target: 'E' } }, | |
{ data: { id: 'EF', source: 'E', target: 'F' } }, | |
{ data: { id: 'AB1', source: 'A', target: 'B1' } }, | |
{ data: { id: 'B1C1', source: 'B1', target: 'C1' } }, | |
{ data: { id: 'C1D', source: 'C1', target: 'D' } }, | |
{ data: { id: 'B1E2', source: 'B1', target: 'E2' } } | |
]; | |
var elk = new ELK(); | |
var graph = { | |
id: "root", | |
layoutOptions: { | |
'elk.algorithm': 'layered', | |
'elk.layered.spacing.edgeNodeBetweenLayers': 20, | |
'elk.layered.spacing.edgeEdgeBetweenLayers': 10, | |
}, | |
children: data.filter( function( ele ){ return ele.data.source == null; } ).map( function( ele ){ | |
return { id: ele.data.id, width: 40, height: 40 }; | |
} ), | |
edges: data.filter( function( ele ){ return ele.data.source != null; } ).map( function( ele ){ | |
return { id: ele.data.id, sources: [ ele.data.source ], targets: [ ele.data.target ] }; | |
} ) | |
}; | |
elk.layout(graph) | |
.then(function() { | |
//nodes | |
cy.nodes().positions(function( node, i ){ | |
var layoutNode = graph.children.find( function( layoutNode ){ return layoutNode.id === node.id(); } ); | |
return { x: layoutNode.x, y: layoutNode.y }; | |
}); | |
//edges | |
cy.edges().forEach(function( edge, i ){ | |
var layoutEdge = graph.edges.find( function( layoutEdge ){ return layoutEdge.id === edge.id(); } ); | |
var points = []; | |
points.push(layoutEdge.sections[0].startPoint); | |
points.push(...layoutEdge.sections[0].bendPoints||[]); | |
points.push(layoutEdge.sections[0].endPoint); | |
edge.data('points', points); | |
}); | |
updateEdges(cy); | |
//fit | |
var padding = 200; | |
var bb = cy.elements().boundingBox(); | |
cy.fit( bb, padding ); | |
}) | |
.catch(console.error); | |
var cy = cytoscape({ | |
container: document.getElementById('cy'), | |
elements: data, | |
layout: { | |
name: 'preset' | |
}, | |
zoom: 0.8, | |
pan: { x: 50, y: 50 }, | |
minZoom: 0.2, | |
maxZoom: 5, | |
wheelSensitivity: 0.3, | |
pixelRatio: "auto", | |
style: [ | |
{ | |
selector: 'edge', | |
style: { | |
"width": "1.5", | |
"curve-style": "segments", | |
"segment-weights": "0", | |
"segment-distances": "0", | |
"edge-distances": "endpoints", | |
"source-endpoint": "90deg", | |
"target-endpoint": "-90deg", | |
"target-arrow-shape": "triangle", | |
}, | |
}, | |
{ | |
selector: "node", | |
style: { | |
'width': '40px', | |
'height': '40px', | |
'text-wrap': 'wrap', | |
"content": "data(id)", | |
'shape': 'circle', | |
"overlay-opacity": 0, | |
"text-margin-x": "0px", | |
"text-valign": "center", | |
"text-halign": "center", | |
"border-width": "2px", | |
}, | |
} | |
], | |
}); | |
function getDistWeight(sX, sY, tX, tY, PointX, PointY) { | |
var W, D; | |
// Calculando a distância e o peso | |
D = (PointY - sY + ((sX - PointX) * (sY - tY)) / (sX - tX)) / Math.sqrt(1 + Math.pow((sY - tY) / (sX - tX), 2)); | |
W = Math.sqrt(Math.pow(PointY - sY, 2) + Math.pow(PointX - sX, 2) - Math.pow(D, 2)); | |
var distAB = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)); | |
W = W / distAB; | |
// Checando a posição relativa do ponto em relação à linha | |
var delta1 = (tX - sX) * (PointY - sY) - (tY - sY) * (PointX - sX); | |
delta1 = delta1 >= 0 ? 1 : -1; | |
var delta2 = (tX - sX) * (PointX - sX) + (tY - sY) * (PointY - sY); | |
delta2 = delta2 >= 0 ? 1 : -1; | |
D = Math.abs(D) * delta1; | |
W = W * delta2; | |
return { ResultDistance: D, ResultWeight: W }; | |
} | |
function calculateClockwiseAngle(x1, y1, x2, y2) { | |
const deltaY = y2 - y1; | |
const deltaX = x2 - x1; | |
let thetaRadians = Math.atan2(deltaY, deltaX); | |
let thetaDegrees = thetaRadians * (180 / Math.PI); | |
thetaDegrees = 90 - thetaDegrees; | |
return thetaDegrees; | |
} | |
function adjustEdgeCurve(edge) { | |
if (edge.data && edge.data("points") && edge.data("points").length > 2) { | |
let points = edge.data("points"); | |
let firstPoint = points[0]; | |
let lastPoint = points[points.length - 1]; | |
//target | |
let nodeTarget = edge.target(); | |
let nodeTargetPosition = nodeTarget.position(); | |
let nodeTargetSize = { w: nodeTarget.width(), h: nodeTarget.height() }; | |
let lastPointAdjusted = { x: lastPoint.x - (nodeTargetSize.w / 2 - 5), y: lastPoint.y - (nodeTargetSize.h / 2 - 5) }; | |
let targetAngle = calculateClockwiseAngle(nodeTargetPosition.x, nodeTargetPosition.y, lastPointAdjusted.x, lastPointAdjusted.y); | |
if (targetAngle < 0) { | |
targetAngle = (180 - Math.abs(targetAngle)) * (-1); | |
} | |
edge.style("target-endpoint", `${targetAngle}deg`); | |
//source | |
let nodeSource = edge.source(); | |
let nodeSourcePosition = nodeSource.position(); | |
let nodeSourceSize = { w: nodeSource.width(), h: nodeSource.height() }; | |
let firstPointAdjusted = { x: firstPoint.x - (nodeSourceSize.w / 2 - 0), y: firstPoint.y - (nodeSourceSize.h / 2 - 0) }; | |
let sourceAngle = calculateClockwiseAngle(nodeSourcePosition.x, nodeSourcePosition.y, firstPointAdjusted.x, firstPointAdjusted.y); | |
if (sourceAngle > 0) { | |
sourceAngle = (360 - sourceAngle) % 360 - 180; | |
} | |
edge.style("source-endpoint", `${sourceAngle}deg`); | |
let newPoints = [points[0]]; | |
for (let i = 1; i < points.length - 1; i++) { | |
newPoints.push(points[i]); | |
} | |
newPoints.push(points[points.length - 1]); | |
let distances = []; | |
let weights = []; | |
let src = newPoints[0]; | |
let tgt = newPoints[newPoints.length - 1]; | |
for (let i = 0; i < newPoints.length - 2; i++) { | |
let srcEp = newPoints[i + 1]; | |
let point = getDistWeight(src.x, src.y, tgt.x, tgt.y, srcEp.x, srcEp.y); | |
distances.push(point.ResultDistance); | |
weights.push(point.ResultWeight); | |
} | |
edge.style("segment-distances", distances.join(" ")); | |
edge.style("segment-weights", weights.join(" ")); | |
} else { | |
edge.style("edge-distances", "node-position"); | |
edge.style("segment-distances", "0"); | |
edge.style("segment-weights", "0"); | |
} | |
} | |
function updateEdges(cy) { | |
cy.startBatch(); | |
cy.edges().forEach((edge) => adjustEdgeCurve(edge)); | |
cy.endBatch(); | |
} | |
cy.on("dragfree", () => { | |
updateEdges(cy); | |
}); | |
let spanCoord = document.createElement("span"); | |
spanCoord.style.position = "absolute"; | |
spanCoord.style.top = "20px"; | |
spanCoord.style.left = "10px"; | |
spanCoord.style.backgroundColor = "white"; | |
spanCoord.style.padding = "5px"; | |
spanCoord.style.border = "1px solid #ccc"; | |
spanCoord.style.cursor = "pointer"; | |
document.body.appendChild(spanCoord); | |
//show x,y position from cursor | |
cy.on("mousemove", (event) => { | |
let pos = event.position || event.renderedPosition; | |
spanCoord.textContent = `x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}`; | |
}); | |
//attempt to make curves in cytoscapejs edges, obtaining the elk layout. | |
//to avoid edge overlap, it is necessary to calculate the distance between the points and the weight of each point. | |
//by Fabricio Rodrigues 2025 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE > | |
<html> | |
<head> | |
<title>cytoscape with elk.js</title> | |
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1" /> | |
<link rel="stylesheet" href="style.css" /> | |
<script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/elk.bundled.js"></script> | |
</head> | |
<body> | |
<div id="cy"></div> | |
<script src="main.js"></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment