visjs / vis-network

:dizzy: Display dynamic, automatically organised, customizable network views.
https://visjs.github.io/vis-network/
Apache License 2.0
3k stars 367 forks source link

After opening clusters back again, time gap in stablisation #866

Open swatiwak opened 4 years ago

swatiwak commented 4 years ago

I am doing clustering and de-clustering. Initially I tried below example. De-clustering is seamless as there are less number of nodes and edges. Example - https://visjs.github.io/vis-network/examples/network/other/clusteringByZoom.html But when I tried it with my data which had approximately 100 nodes. I can see de-clustering happening in 2 steps. First the nodes are de-clustered and after a min they are stabilised which gives bad user experience.

Thomaash commented 4 years ago

Hi @swatiwak,

I tried the example with 153 nodes in quadtree arrangement and observed no gap (it went from clustered straight to stabilized). Could you provide an MWE, please?

Thanks.

swatiwak commented 4 years ago

Hi @Thomaash , I am using Contact Tracing database from neo4j.

Thomaash commented 4 years ago

I need something that I can just open and see the issue happen (e.g. something like https://jsfiddle.net/thomaash/uwor8n4v/).

swatiwak commented 4 years ago
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>

        #vis {
            /*width: 900px;*/
            height: 700px;
            background-color: #F8F9FB;
            border: 1px solid lightgray;
        }

        #loader {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 702px;
            background-color:transparent;
            -webkit-transition: all 0.5s ease;
            -moz-transition: all 0.5s ease;
            -ms-transition: all 0.5s ease;
            -o-transition: all 0.5s ease;
            transition: all 0.5s ease;
            opacity: 1;
            display: none;
        }

        #wrapper {
            position: relative;
            /*width: 900px;*/
            height: 700px;
        }

        /* Adding Spinner */
        .spinner {
            position: absolute;
            left: 50%;
            top: 50%;
            height:60px;
            width:60px;
            margin:0px auto;
            -webkit-animation: rotation .6s infinite linear;
            -moz-animation: rotation .6s infinite linear;
            -o-animation: rotation .6s infinite linear;
            animation: rotation .6s infinite linear;
            border-left:6px solid rgba(0,174,239,.15);
            border-right:6px solid rgba(0,174,239,.15);
            border-bottom:6px solid rgba(0,174,239,.15);
            border-top:6px solid rgba(0,174,239,.8);
            border-radius:100%;
        }

        @-webkit-keyframes rotation {
            from {-webkit-transform: rotate(0deg);}
            to {-webkit-transform: rotate(359deg);}
        }
        @-moz-keyframes rotation {
            from {-moz-transform: rotate(0deg);}
            to {-moz-transform: rotate(359deg);}
        }
        @-o-keyframes rotation {
            from {-o-transform: rotate(0deg);}
            to {-o-transform: rotate(359deg);}
        }
        @keyframes rotation {
            from {transform: rotate(0deg);}
            to {transform: rotate(359deg);}
        }
    </style>
</head>
<body onload="displayGraph()"> 
    <div id="wrapper">
        <div id="vis"></div>
        <div id="loader">
            <div class="spinner"></div>
        </div>
    </div>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js" charset="utf-8"></script>
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<script>
    let network;
    let clusterIndex = 0;
    let clusters = [];
    let lastClusterZoomLevel = 0;
    let clusterFactor = 0.9;
    let userName = "neo4j";
    let password = "department-daybreak-aircraft";
    let ipAddress = "54.237.28.79";
    let httpPort = "35514";
    let AUTHORIZATION = "Basic " + btoa(`${userName}:${password}`);
    /**
     * post ajax request on Neo4j REST API
     */
    function restPost(data) {
        return $.ajax({
            type: "POST",
            beforeSend: function (request) {
                if (AUTHORIZATION != undefined) {
                    request.setRequestHeader("Authorization", AUTHORIZATION);
                }
            },
            url: "http" + "://" + ipAddress + ":" + httpPort + "/db/data/transaction/commit",
            contentType: "application/json",
            data: JSON.stringify(data)
        });
    }

    /**
     * Function to call to display a new graph.
     */
    function displayGraph() {
        // Create the authorization header for the ajax request.
        // Show loading elements.
        $("#loader").show();

        var limit = 100;
        // Post Cypher query to return node and relations and return results as graph.
        restPost({
            "statements": [
                {
                    "statement": "MATCH (n1)-[r]->(n2) RETURN r, n1,n2 LIMIT " + limit,
                    "resultDataContents": ["graph"]
                }
            ]
        }).done(function (data) {
            // Parse results and convert it to vis.js compatible data.
            let graphData = parseGraphResultData(data);
            let nodes = convertNodes(graphData.nodes);
            let edges = convertEdges(graphData.edges);
            let visData = {
                nodes: nodes,
                edges: edges
            };

            displayVisJsData(visData);
        });
    }

    function displayVisJsData(data) {
        let container = document.getElementById('vis');

        let options = {
            nodes: {
                shape: 'circle',
                scaling: {
                    label: true
                },
                font: {
                    color: '#343434',
                    size: 20, // px
                },
            },
            edges: {
                arrows: 'to',
                /*smooth: {
                    type: 'continuous'
                },*/
                scaling: {
                    label: true,
                },
                /*font: {
                    color: '#343434',
                    size: 20, // px
                }*/
                //length: 10
            },
            interaction: {
                    //hover: true,
                    keyboard: {
                        enabled: true,
                        bindToWindow: false
                    },
                    navigationButtons: true,
                    //tooltipDelay: 1000000,
                    //hideEdgesOnDrag: true,
                    //zoomView: false
            },
            layout: { randomSeed: 8 }, 
            physics: {
                /*barnesHut: {
                    springLength: 80,
                    avoidOverlap:0.6,
                    gravitationalConstant: -2000,
                    springConstant: 0.001,
                },*/
                /*forceAtlas2Based: {
                    gravitationalConstant: -2000,
                    centralGravity: 0.3,
                    springLength: 230,
                    springConstant: 0.18,
                    avoidOverlap: 1.5
                },*/
                solver: 'forceAtlas2Based',
                maxVelocity: 146,
                timestep: 0.35,
                stabilization: {
                    enabled: true,
                    fit:true,
                    iterations: 500,
                    updateInterval: 50
                }
            },
            /*physics: {
                forceAtlas2Based: {
                    gravitationalConstant: -26,
                    centralGravity: 0.3,
                    springLength: 230,
                    springConstant: 0.18,
                    avoidOverlap: 1.5
                },
                maxVelocity: 146,
                solver: 'forceAtlas2Based',
                timestep: 0.35,
                stabilization: {
                    enabled: true,
                    iterations: 1000,
                    updateInterval: 50
                }
            }*/
        };

        // initialize the network!
        network = new vis.Network(container, data, options);

        // set the first initial zoom level
        network.once('initRedraw', function () {
            if (lastClusterZoomLevel === 0) {
                lastClusterZoomLevel = network.getScale();
            }
        });

        network.on("stabilizationProgress", function (params) {
        });

        network.once("stabilizationIterationsDone", function () {
            network.setOptions( { physics: false } );
            // really clean the dom element
            setTimeout(function () {
                $("#loader").hide();
            }, 500);
        });

        // we use the zoom event for our clustering
        network.on('zoom', function (params) {
            if (params.direction == '-') {
                if (params.scale < lastClusterZoomLevel * clusterFactor) {
                    makeClusters(params.scale);
                    lastClusterZoomLevel = params.scale;
                }
            }
            else {
                openClusters(params.scale);
            }
        });

        /*network.on("dragEnd", function (params) {
      for (var i = 0; i < params.nodes.length; i++) {
          var nodeId = params.nodes[i];
          nodes.update({id: nodeId, fixed: {x: true, y: true}});
      }
    });
    network.on('dragStart', function(params) {
      for (var i = 0; i < params.nodes.length; i++) {
          var nodeId = params.nodes[i];
          nodes.update({id: nodeId, fixed: {x: false, y: false}});
      }
    });*/

        // if we click on a node, we want to open it up!
        network.on("selectNode", function (params) {
            if (params.nodes.length == 1) {
                if (network.isCluster(params.nodes[0]) == true) {
                    network.openCluster(params.nodes[0])
                    network.stabilize(1000);
                }
            }
        });
    }

    // make the clusters
    function makeClusters(scale) {
        let clusterOptionsByData = {
            processProperties: function (clusterOptions, childNodes) {
                clusterIndex = clusterIndex + 1;
                var childrenCount = 0;
                for (var i = 0; i < childNodes.length; i++) {
                    childrenCount += childNodes[i].childrenCount || 1;
                }
                clusterOptions.childrenCount = childrenCount;
                clusterOptions.label = "# " + childrenCount + "";
                clusterOptions.font = { size: childrenCount + 30 }
                clusterOptions.id = 'cluster:' + clusterIndex;
                clusters.push({ id: 'cluster:' + clusterIndex, scale: scale });
                return clusterOptions;
            },
            clusterNodeProperties: { borderWidth: 3, shape: 'ellipse', font: { size: 30 } }
        }
        network.clusterOutliers(clusterOptionsByData);
        // since we use the scale as a unique identifier, we do NOT want to fit after the stabilization
        //network.setOptions({physics:{stabilization:{fit: false}}});
        //network.setOptions( { physics: false } );
        //network.stabilize(1000);
    }

    // open them back up!
    function openClusters(scale) {
        console.log("Open Cluster")
        let newClusters = [];
        let declustered = false;
        for (var i = 0; i < clusters.length; i++) {
            if (clusters[i].scale < scale) {
                network.openCluster(clusters[i].id);
                lastClusterZoomLevel = scale;
                declustered = true;
            }
            else {
                newClusters.push(clusters[i])
            }
        }
        clusters = newClusters;
        if (declustered === true){
            // since we use the scale as a unique identifier, we do NOT want to fit after the stabilization
            //network.setOptions({physics:{stabilization:{fit: false}}});
            //network.setOptions( { physics: false } );
            console.log("Open Stabilise")
            network.stabilize(1000);
        }
    }

    function parseGraphResultData(data) {
        let nodes = {}, edges = {};
        data.results[0].data.forEach(function (row) {
            row.graph.nodes.forEach(function (n) {
                if (!nodes.hasOwnProperty(n.id)) {
                    nodes[n.id] = n;
                }
            });

            row.graph.relationships.forEach(function (r) {
                if (!edges.hasOwnProperty(r.id)) {
                    edges[r.id] = r;
                }
            });
        });

        let nodesArray = [], edgesArray = [];

        for (var p in nodes) {
            if (nodes.hasOwnProperty(p)) {
                nodesArray.push(nodes[p]);
            }
        }

        for (var q in edges) {
            if (edges.hasOwnProperty(q)) {
                edgesArray.push(edges[q])
            }
        }

        return {nodes: nodesArray, edges: edgesArray};
    }

    function convertNodes(nodes) {
        let convertedNodes = [];

        nodes.forEach(function (node) {
            let nodeLabel = node.labels[0];
            let displayedLabel = nodeLabel + ("\n" + node.properties[node.properties.name ? 'name' : Object.keys(node.properties)[0]]).substr(0, 20);
            convertedNodes.push({
                id: node.id,
                label: displayedLabel,
                group: nodeLabel
            })
        });

        return convertedNodes;
    }

    function convertEdges(edges) {
        let convertedEdges = [];

        edges.forEach(function (edge) {
            convertedEdges.push({
                from: edge.startNode,
                to: edge.endNode,
                label: edge.type
            })
        });

        return convertedEdges;
    }

</script>
</body>
</html>

Above code is not working in jsfiddle as it has http ajax call.

Thomaash commented 4 years ago

I get CONNECTION_REFUSED from 54.237.28.79:35514. If it really is okay to publish credentials to this machine for everyone to use, you'll also have to configure it to accept connections from everywhere.

Ideally just hardcode the nodes and edges into the MWE.