Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

As adding an edge between 2 children node of group whole layout get disturbed #3327

Open
Amanyadav12345 opened this issue Jan 30, 2025 · 4 comments
Labels
bug A bug in the code of Cytoscape.js

Comments

@Amanyadav12345
Copy link

Image

Added image is of a layout which is getting changed when we add an edge from backend

Image
look at the first group node you can see the difference

this error is due to directional edges
as I exchanged the source and target of the edge the layout is coming perfect

{
"data": {
"textStyleName": "Link (8pt italic)",
"labels": null,
"overlayNames": null,
"overlays": null,
"currentOverlay": null,
"userEdge": false,
"actions": null,
"edgeStyleName": "Spine To LeafLink",
"isHidden": true,
"isHighlighted": false,
"APoint": "FaxxxxxxxxxxxxxxxxxxxxxxJwbGcpNRfG03",
"ZPoint": "FaxxxxxxxxxxxxxxxFdGcpNRfG03",
"ALabel": "Spine-Gxxxxxxxxxxxxxx.3.10",
"ZLabel": "Generated1xxxxxxxxxxxxxxxxxxxxxxxxxxxx1",
"linkType": "SpineToLeafLink",
"edgeStyleId": "Spine To Leaf Link",
"anchorIndexA": -1,
"anchorIndexZ": -1,
"linkSpeed": 0,
"notManaged": false,
"controlPoints": null,
"childCount": 0,
"edgeCollapsed": false,
"entityType": null,
"tooltips": [
{
"label": "Edge Type",
"value": "Logical Spine to Leaf Connection"
},
{
"label": "Spine",
"value": "Spxxxxxxxxxxxxxxxxxx3.10"
},
{
"label": "Leaf",
"value": "GXXXXXXXXXXXXXXX-----11.3.11"
}
],
"tableOverlays": null,
"id": "Fabricxxxxxxxxxxxxxxxxxx----------------------------dFdGcpNRfG11103",
"name": null,
"severity": 0,
"isGroup": false,
"target": "Port::rrrrrrrrrrrrrrrttttttttzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzttttttrryyyyyyyyyrrruussssssssuuuuuur",
"source": "---------------sdaff-----------------------------------------------cpNRfG03",
"default": {
"font-size": 10,
"color": 0,
"font-bold": "false",
"font-bolder": "false",
"font-lighter": "false",
"font-italic": "false",
"font-family": "SansSerif",
"line-style-dashed": false,
"line-dash-pattern": [
1,
1
],
"curve-style": "straight",
"width": 2,
"source-arrow-shape": "none",
"target-arrow-shape": "none",
"line-color": 0,
"text-wrap": "none",
"text-rotation": 0
}
},
"group": "edges"
},

this is the edge json

@Amanyadav12345 Amanyadav12345 added the bug A bug in the code of Cytoscape.js label Jan 30, 2025
@maxkfranz
Copy link
Member

@Amanyadav12345, would you post the required points in the issue template? In particular, we need a reproducible demo on Jsbin or similar.

@Amanyadav12345
Copy link
Author

Amanyadav12345 commented Mar 9, 2025

the issue is related to all the layout that support hierarchy we supposed to appear but all those things are related to the edges

for example you make a 3 layer structure in a directed form where you have your major spine on top level then you have 2 levels one leaf level and then port level both will be group nodes and will have 30- 30 nodes within them
now level one edges will go to level 2 and level 2 edges will go to level 3

and if there is some edges of level 3 which are giving signal to level 2 or multidirational transmission is happening then the layout of cytoscape changes

this was an example of my custom layout that I worked on with my custom data

<script>
import cytoscape from 'cytoscape';
import { onMount } from 'svelte';
import data from '../../src/data.json';

let cy;
let networkContainer;

// Transform the raw data into Cytoscape format
function transformData(rawData) {
    const elements = {
        nodes: [],
        edges: []
    };

    const nodeIds = new Set();
    if (Array.isArray(rawData.nodes)) {
        elements.nodes = rawData.nodes.map(node => {
            const nodeId = node.data.id || `node-${Math.random().toString(36).substr(2, 9)}`;
            nodeIds.add(nodeId);
            return {
                data: {
                    id: nodeId,
                    locked: true,
                    label: node.name || node.data.labels?.Default || nodeId,
                    type: node.data.type,
                    ipAddress: node.data.ipAddress,
                    severity: node.data.severity,
                    level: node.data.level,
                    parent: node.data.parent,
                    name : node.data.name
                },
                position: node.position || { x: 0, y: 0 }
            };
        });
    }

    if (Array.isArray(rawData.edges)) {
        elements.edges = rawData.edges.reduce((acc, edge) => {
            const source = edge.data.source ;
            const target = edge.data.target ;
            if (source && target && nodeIds.has(source) && nodeIds.has(target)) {
                acc.push({
                    data: {
                        id: edge.data.id || `edge-${Math.random().toString(36).substr(2, 9)}`,
                        source: source,
                        target: target,
                        label: edge.data.labels?.Default || '',
                        severity: edge.data.severity,
                        linkSpeed: edge.data.linkSpeed
                    }
                });
            }
            return acc;
        }, []);
    }

    console.log('Transformed elements:', elements);
    return elements;
}

function initializeCytoscape() {
    try {
        const elements = transformData(data);

        if (elements.nodes.length === 0) {
            console.error('No nodes found in the transformed data');
            return;
        }

        cy = cytoscape({
            container: networkContainer,
            elements: elements,
            style: [
                {
                    selector: 'node',
                    style: {
                        'label': 'data(label)',
                        'locked': 'true',
                        'width': 40,
                        'height': 40,
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-halign': 'center',
                        'font-size': '10px',
                        'text-wrap': 'wrap',
                        'text-max-width': '80px'
                    }
                },
                {
                    selector: 'node[type = "SWTCH"]',
                    style: {
                        'background-color': '#0074D9',
                        'shape': 'rectangle'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#999',
                        'target-arrow-color': '#999',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                        'label': 'data(label)',
                        'font-size': '8px',
                       
                    }
                    
                },
                {
                    selector: 'edge[severity = 6]',
                    style: {
                        'line-color': '#ff0000',
                        'target-arrow-color': '#ff0000'
                    }
                }
            ],
            layout: {
                name: 'grid',
                padding: 50
            }
        });

        cy.on('tap', 'node', function(evt) {
            const node = evt.target;
            console.log('Node tapped:', node.data());
        });
        

        cy.on('dblclick', 'node[type="group"]', (evt) => {
            const groupNode = evt.target;
            toggleGroupNode(groupNode);
        });
        
        adjustNodePositions();
        adjustEthNodesLayout();
        cy.nodes().forEach(node => {
            if (node.data('locked') === 'true') {
                const position = node.position();
                node.lock();
                // Ensure the node stays at its initial position
                node.position(position);
            }
        });
        cy.fit();
        groupNodesByParent(elements.nodes);
        const sourceIds = ['RedCell.Config.qqqqqqqqqsssPort::UOIxHtqapNR', 'RedCell.qqqqqqqqqqqConfig.Port::UOIxHtqapNRf03', 'RedCell.Conqqqqqqqqqfig.Port::weweeUOIxHtwwwwwwwwwwwqapNRfG03'];
        const targetIds = ['FabricMember:FabricMember::uWBJwbGcpNRfG03', 'FabricMember:FabricMember::uWBJwbGcpNRfG03', 'FabricMember:FabricMember::pWBJwbGcpNRfG03'];

        // Add proxy edges with a label and severity
        addProxyEdges(sourceIds, targetIds, 'Eth connection', 6);



    } catch (error) {
        console.error('Error initializing cytoscape:', error);
    }
}



function adjustNodePositions() {
    if (!cy) return;

    const nodes = cy.nodes();
    const nodesByLevel = new Map();
    
    // First pass: Group nodes with defined levels
    nodes.forEach(node => {
        const level = node.data('level');
        if (level !== undefined) {
            if (!nodesByLevel.has(level)) {
                nodesByLevel.set(level, []);
            }
            nodesByLevel.get(level).push(node);
        }
    });

    // Handle nodes without levels
    nodes.forEach(node => {
    if (node.data('level') === undefined) {
        // Check if the node is of type 'Server node'
        const nodeType = node.data('groupnodeLayout')?.nodetype;
        if (nodeType === 'Server node') {
            // If it's a "Server node", collapse at its own position
            const pos = node.position();
            if (pos) {
                if (!nodesByLevel.has(0)) {
                    nodesByLevel.set(0, []); // Ensure there's a level 0
                }
                nodesByLevel.get(0).push(node); // Place at level 0 (or desired level)
            }
        } else {
            // Get all descendants
            const descendants = node.successors().nodes();
            
            if (descendants.length > 0) {
                // Calculate average position of descendants
                let avgX = 0;
                let avgY = 0;
                let count = 0;
                
                descendants.forEach(descendant => {
                    // Only consider descendants that have positions
                    const pos = descendant.position();
                    if (pos) {
                        avgX += pos.x;
                        avgY += pos.y;
                        count++;
                    }
                });
                
                if (count > 0) {
                    avgX = avgX / count;
                    avgY = avgY / count;
                    
                    // Find the closest level to this Y position
                    const sortedLevels = Array.from(nodesByLevel.keys()).sort((a, b) => a - b);
                    const levelSpacing = cy.height() / (sortedLevels.length + 1);
                    
                    let closestLevel = Math.round(avgY / levelSpacing) - 1;
                    closestLevel = Math.max(0, Math.min(closestLevel, sortedLevels.length - 1));
                    
                    // Add node to that level
                    if (!nodesByLevel.has(closestLevel)) {
                        nodesByLevel.set(closestLevel, []);
                    }
                    nodesByLevel.get(closestLevel).push(node);
                }
            }
        }
    }
});

    const sortedLevels = Array.from(nodesByLevel.keys()).sort((a, b) => a - b);
    const totalHeight = cy.height();
    const levelSpacing = totalHeight / (sortedLevels.length + 1);

    sortedLevels.forEach((level, index) => {
        const nodesInLevel = nodesByLevel.get(level);
        nodesInLevel.sort((a, b) => a.data('label').localeCompare(b.data('label')));

        // Calculate x positions
        const totalWidth = cy.width();
        const spacingFactor = 3; // Adjust this value to increase spacing
        const nodeSpacing = (totalWidth / (nodesInLevel.length + 1)) * spacingFactor;
        
        // First pass: Set initial positions
        nodesInLevel.forEach((node, nodeIndex) => {
            const x = nodeSpacing * (nodeIndex + 1);
            const y = levelSpacing * (index + 1);
            node.position({ x: x, y: y });
        });

        // Second pass: Get existing y positions for this level
        const yPositions = new Map();
        nodesInLevel.forEach(node => {
            const currentY = node.position('y');
            yPositions.set(currentY, (yPositions.get(currentY) || 0) + 1);
        });

        // Find most common y position
        let mostCommonY = levelSpacing * (index + 1);
        let maxCount = 0;
        yPositions.forEach((count, y) => {
            if (count > maxCount) {
                maxCount = count;
                mostCommonY = y;
            }
        });

        // Third pass: Adjust all nodes to the most common y position
        nodesInLevel.forEach(node => {
            const currentX = node.position('x');
            node.position({ x: currentX, y: mostCommonY });
        });
    });
}
function adjustEthNodesLayout() {
    if (!cy) return;

    // Collect all Eth nodes
    const ethNodes = cy.nodes().filter(node => 
        node.data('name') === 'Eth1/26'
    );
    if (ethNodes.length === 0) return;

    // Group nodes by their parent
    const nodesByParent = {};
    ethNodes.forEach(node => {
        const parentNode = node.parent();
        if (!parentNode) return;

        const parentId = parentNode.id();
        if (!nodesByParent[parentId]) {
            nodesByParent[parentId] = {
                parentNode,
                nodes: []
            };
        }
        nodesByParent[parentId].nodes.push(node);
    });

    // Configuration for the grid layout
    const NODES_PER_ROW = 3;
    const HORIZONTAL_SPACING = 100;  // Space between nodes in a row
    const VERTICAL_SPACING = 100;   // Space between rows

    // Process each group of nodes by parent
    Object.values(nodesByParent).forEach(group => {
        const { parentNode, nodes } = group;

        // Get the parent's position
        const parentPosition = parentNode.position();

        // Sort nodes by name for consistent ordering
        const sortedNodes = nodes.sort((a, b) => 
            (a.data('name') || '').localeCompare(b.data('name') || '')
        );

        // Position each node in the grid, aligning the second node to the parent position
        sortedNodes.forEach((node, index) => {
            const row = Math.floor(index / NODES_PER_ROW);
            const col = index % NODES_PER_ROW;

            let x, y;

            // Align the second node to the parent position
            if (index === 1) {
                x = parentPosition.x;
                y = parentPosition.y;
            } else {
                // Adjust other nodes relative to the parent's position
                const centerCol = 1; // The column where the second node is aligned
                x = parentPosition.x + (col - centerCol) * HORIZONTAL_SPACING;
                y = parentPosition.y + (row * VERTICAL_SPACING);
            }

            // Animate the transition to the new position
            node.animate({
                position: { x, y },
                duration: 50,
               
            });
        });
    });

    // Optional: Fit the view to show all nodes with padding
    
}

function groupNodesByParent(nodes) {
    const parentChildMap = new Map();

    for (const node of nodes) {
        if (node.data.parent) {
            const parentId = node.data.parent;

            if (!parentChildMap.has(parentId)) {
                parentChildMap.set(parentId, []);
            }
            parentChildMap.get(parentId).push(node);
        }
    }

    const groupNodes = [];
    for (const [parentId, children] of parentChildMap) {
        const parentNode = nodes.find(node => node.data.id === parentId);

        const groupNode = {
            data: {
                id: `group-${parentId}`,
                label: parentNode.data.label,
                type: 'group',
                collapsed: false
            },
            position: { x: 0, y: 0 },
            group: 'nodes',
            children: children.map(child => child.id)
        };

        const childPositions = children.map(child => child.position);
        const minX = Math.min(...childPositions.map(pos => pos.x));
        const maxX = Math.max(...childPositions.map(pos => pos.x));
        const minY = Math.min(...childPositions.map(pos => pos.y));
        const maxY = Math.max(...childPositions.map(pos => pos.y));
        groupNode.position = {
            x: (minX + maxX) / 2,
            y: (minY + maxY) / 2
        };

        groupNodes.push(groupNode);
    }

    return [...nodes, ...groupNodes];
}
let proxyEdges = new Map();

function toggleGroupNode(groupNode) {
    const collapsed = groupNode.data('collapsed');

    if (collapsed) {
        // Expand: show all descendants and remove proxy edges
        groupNode.descendants().style('display', 'element'); // Show all descendants
        groupNode.data('collapsed', false);

        // Remove proxy edges for the group node
        if (proxyEdges.has(groupNode.id())) {
            proxyEdges.get(groupNode.id()).forEach(edge => cy.remove(edge));
            proxyEdges.delete(groupNode.id());
        }
    } else {
        // Collapse: hide all descendants and create proxy edges to represent their connections
        groupNode.descendants().style('display', 'none'); // Hide all descendants
        groupNode.data('collapsed', true);

        // Add proxy edges from the group node to all external connections of its descendants
        const newProxyEdges = [];
        groupNode.descendants().forEach(descendantNode => {
            descendantNode.connectedEdges().forEach(edge => {
                const otherNode =
                    edge.source().id() === descendantNode.id() ? edge.target() : edge.source();

                // Only add a proxy edge if the other node is external to the group
                if (!groupNode.descendants().includes(otherNode)) {
                    const proxyEdgeId = `proxy-${groupNode.id()}-${otherNode.id()}`;

                    // Avoid creating duplicate proxy edges
                    if (!cy.$id(proxyEdgeId).length) {
                        const proxyEdge = cy.add({
                            group: 'edges',
                            data: {
                                id: proxyEdgeId,
                                source: groupNode.id(),
                                target: otherNode.id(),
                                label: edge.data('label'),
                                severity: edge.data('severity')
                            },
                            style: {
                                'line-color': 'transparent', // Transparent line color
                                'target-arrow-color': 'transparent', // Transparent arrow color
                                'line-style': 'dashed', // Dashed line to indicate a proxy edge
                                'opacity': 0.2 // Lower opacity for transparency
                            }
                        });
                        newProxyEdges.push(proxyEdge);
                    }
                }
            });
        });

        // Store the proxy edges so they can be removed when expanded
        proxyEdges.set(groupNode.id(), newProxyEdges);
    }

    // Refresh layout to apply changes
    cy.layout({ name: 'preset' }).run();
}

function addProxyEdges(sourceIds, targetIds, edgeLabel = 'Proxy', severity = 0) {
    if (!cy) {
        console.error('Cytoscape instance is not initialized.');
        return;
    }

    sourceIds.forEach((sourceId) => {
        const sourceNode = cy.$id(sourceId);
        if (!sourceNode.length) {
            console.warn(`Source node with ID ${sourceId} does not exist.`);
            return;
        }

        targetIds.forEach((targetId) => {
            const targetNode = cy.$id(targetId);
            if (!targetNode.length) {
                console.warn(`Target node with ID ${targetId} does not exist.`);
                return;
            }

            // Construct a unique edge ID to avoid duplicates
            const proxyEdgeId = `proxy-${sourceId}-${targetId}`;

            if (!cy.$id(proxyEdgeId).length) {
                // Add the proxy edge
                cy.add({
                    group: 'edges',
                    data: {
                        id: proxyEdgeId,
                        source: sourceId,
                        target: targetId,
                        label: edgeLabel,
                        severity: severity
                    },
                    style: {
                        'line-color': 'transparent', // Transparent line color
                        'target-arrow-color': 'transparent', // Transparent arrow color
                        'line-style': 'solid', // Dashed line style for proxy edge
                        'opacity': 0.2 // Lower opacity for transparency
                    }
                });
            }
        });
    });
}



// function toggleGroupNode(groupNode) {
//     const collapsed = groupNode.data('collapsed');

//     if (collapsed) {
//         // Expand: show all descendants and remove proxy edges
//         groupNode.descendants().style('display', 'element');  // Show all descendants
//         groupNode.data('collapsed', false);

//         // Remove proxy edges for the group node
//         if (proxyEdges.has(groupNode.id())) {
//             proxyEdges.get(groupNode.id()).forEach(edge => cy.remove(edge));
//             proxyEdges.delete(groupNode.id());
//         }
//     } else {
//         // Collapse: hide all descendants and create proxy edges to represent their connections
//         groupNode.descendants().style('display', 'none');  // Hide all descendants
//         groupNode.data('collapsed', true);

//         // Add proxy edges from the group node to all external connections of its descendants
//         const newProxyEdges = [];
//         groupNode.descendants().forEach(descendantNode => {
//             descendantNode.connectedEdges().forEach(edge => {
//                 const otherNode = edge.source().id() === descendantNode.id() ? edge.target() : edge.source();

//                 // Only add a proxy edge if the other node is external to the group
//                 if (!groupNode.descendants().includes(otherNode)) {
//                     const proxyEdgeId = `proxy-${groupNode.id()}-${otherNode.id()}`;

//                     // Avoid creating duplicate proxy edges
//                     if (!cy.$id(proxyEdgeId).length) {
//                         const proxyEdge = cy.add({
//                             group: 'edges',
//                             data: {
//                                 id: proxyEdgeId,
//                                 source: groupNode.id(),
//                                 target: otherNode.id(),
//                                 label: edge.data('label'),
//                                 severity: edge.data('severity')
//                             },
//                             style: {
//                                     'line-color': 'transparent',  // Transparent line color
//                                     'target-arrow-color': 'transparent',  // Transparent arrow color
//                                     'line-style': 'dashed',  // Dashed line to indicate a proxy edge
//                                     'opacity': 0.2  // Lower opacity for transparency
//                                 }
//                                                         });
//                         newProxyEdges.push(proxyEdge);
//                     }
//                 }
//             });
//         });

//         // Store the proxy edges so they can be removed when expanded
//         proxyEdges.set(groupNode.id(), newProxyEdges);
//     }

//     // Refresh layout to apply changes
//     cy.layout({ name: 'preset' }).run();
// }

// Example usage on component mount or initialization
onMount(() => {
    initializeCytoscape();
});
    </script>
    
    <div bind:this={networkContainer} id="network"></div>
    
    <style>
        #network {
            width: 100%;
            height: 600px;
            border: 1px solid lightgray;
            background-color: #f8f8f8;
        }
    </style>


this component is was using with my data

@Amanyadav12345
Copy link
Author

will try to make you another demo for sure but for now I have this much to share

@maxkfranz
Copy link
Member

Look forward to the demo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A bug in the code of Cytoscape.js
Projects
None yet
Development

No branches or pull requests

2 participants