Skip to content

Instantly share code, notes, and snippets.

@fabric-io-rodrigues
Created January 24, 2025 02:03
Show Gist options
  • Save fabric-io-rodrigues/f5304881e354bfc99f3623519567e4cc to your computer and use it in GitHub Desktop.
Save fabric-io-rodrigues/f5304881e354bfc99f3623519567e4cc to your computer and use it in GitHub Desktop.
Try curves from elk layout in cytoscapejs
//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
<!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