D3 Force Directed Graph ajax update

d9705996 picture d9705996 · Aug 14, 2013 · Viewed 7.6k times · Source

I am using d3.js and jquery with a PHP back-end (based on yii framework) to create a dynamic force directed graph to represent the current state of hosts and services on the network that we are monitoring using Nagios.

The graph shows root -> hostgroups -> hosts -> services. I have created a server side function to return a JSON object in the following format

{
    "nodes": [
        {
            "name": "MaaS",
            "object_id": 0
        },
        {
            "name": "Convergence",
            "object_id": "531",
            "colour": "#999900"
        },
        {
            "name": "maas-servers",
            "object_id": "719",
            "colour": "#999900"
        },
        {
            "name": "hrg-cube",
            "object_id": "400",
            "colour": "#660033"
        }
    ],
    "links": [
        {
            "source": 0,
            "target": "531"
        },
        {
            "source": 0,
            "target": "719"
        },
        {
            "source": "719",
            "target": "400"
        }
    ]
}

The nodes contain an object id which is used in the links and colour for displaying the state of the node (OK = green, WARNING = yellow, etc) The links has the source object ids and target object ids for the nodes. The nodes and links may change as new hosts are added or removed from the monitoring system

I have the following code which setups the initial SVG and then every 10 seconds

  1. Retrieves the current JSON object
  2. Creates map of the links
  3. Selects the current nodes and links and binds them to the JSON data
  4. Entering links are added and exiting links are removed
  5. updated and added nodes will change their fill colour and have a tooltip with their name added
  6. Force is started

    $.ajaxSetup({ cache: false }); width = 960, height = 500; node = []; link = []; force = d3.layout.force() .charge(-1000) .linkDistance(1) .size([width, height]);

    svg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height)
      .append("g");
    
    setInterval(function(){
        $.ajax({
            url: "<?php echo $url;?>",
            type: "post",
            async: false,
            datatype: "json",
            success: function(json, textStatus, XMLHttpRequest) 
            {
                json = $.parseJSON(json);
    
                var nodeMap = {};
                json.nodes.forEach(function(x) { nodeMap[x.object_id] = x; });
                json.links = json.links.map(function(x) {
                    return {
                        source: nodeMap[x.source],
                        target: nodeMap[x.target],
                    };
                });
    
                link = svg.selectAll("line")
                    .data(json.links);
    
                node = svg.selectAll("circle")
                    .data(json.nodes,function(d){return d.object_id})
    
                link.enter().append("line").attr("stroke-width",1).attr('stroke','#999');
                link.exit().remove();
    
                node.enter().append("circle").attr("r",5);
                node.exit().remove();
    
                node.attr("fill",function(d){return d.colour});
    
                node.append("title")
                  .text(function(d) { return d.name; });
    
                node.call(force.drag);
    
                force
                    .nodes(node.data())
                    .links(link.data()) 
                    .start()
    
                force.on("tick", function() {
    
                    link.attr("x1", function(d) { return d.source.x; })
                        .attr("y1", function(d) { return d.source.y; })
                        .attr("x2", function(d) { return d.target.x; })
                        .attr("y2", function(d) { return d.target.y; });
    
                    node.attr("cx", function(d) { return d.x = Math.max(5, Math.min(width - 5, d.x));  })
                        .attr("cy", function(d) { return d.y = Math.max(5, Math.min(height - 5, d.y)); });
    
                });
            }
        });
    },10000);
    

An example of the output can be seen at Network Visualization

All of the above works correctly with the exception that every time the code loops it causes the visualization to restart and the nodes all bounce about until they settle. What I need is for any current items to stay as they are but any new nodes and links are added to the visualisation and are clickable and draggable, etc.

If anyone can help I would be eternally grateful.

Answer

d9705996 picture d9705996 · Aug 22, 2013

I have managed to find a solution to the problem using a mixture of all the advice above, below is the code I have used

    var width = $(document).width();
    var height = $(document).height();

    var outer = d3.select("#chart")
        .append("svg:svg")
            .attr("width", width)
            .attr("height", height)
            .attr("pointer-events", "all");

    var vis = outer
        .append('svg:g')
            .call(d3.behavior.zoom().on("zoom", rescale))
            .on("dblclick.zoom", null)
        .append('svg:g')

        vis.append('svg:rect')
            .attr('width', width)
            .attr('height', height)
            .attr('fill', 'white');

        var force = d3.layout.force()
            .size([width, height])
            .nodes([]) // initialize with a single node
            .linkDistance(1)
            .charge(-500)
            .on("tick", tick);

        nodes = force.nodes(),
            links = force.links();

        var node = vis.selectAll(".node"),
            link = vis.selectAll(".link");

       redraw();

       setInterval(function(){
           $.ajax({
                url: "<?php echo $url;?>",
                type: "post",
                async: false,
                datatype: "json",
                success: function(json, textStatus, XMLHttpRequest) 
                {
                    var current_nodes = [];
                    var delete_nodes = [];
                    var json = $.parseJSON(json);

                    $.each(json.nodes, function (i,data){

                        result = $.grep(nodes, function(e){ return e.object_id == data.object_id; });
                        if (!result.length)
                        {
                            nodes.push(data);
                        }
                        else
                        {
                            pos = nodes.map(function(e) { return e.object_id; }).indexOf(data.object_id);
                            nodes[pos].colour = data.colour;
                        }
                        current_nodes.push(data.object_id);             
                    });

                    $.each(nodes,function(i,data){
                        if(current_nodes.indexOf(data.object_id) == -1)
                        {
                            delete_nodes.push(data.index);
                        }       
                    });
                    $.each(delete_nodes,function(i,data){
                        nodes.splice(data,1); 
                    });

                    var nodeMap = {};
                    nodes.forEach(function(x) { nodeMap[x.object_id] = x; });
                    links = json.links.map(function(x) {
                        return {
                            source: nodeMap[x.source],
                            target: nodeMap[x.target],
                            colour: x.colour,
                        };
                    });
                    redraw();
                }
            });
       },2000);


       function redraw()
       {
           node = node.data(nodes,function(d){ return d.object_id;});
           node.enter().insert("circle")
                .attr("r", 5)
           node.attr("fill", function(d){return d.colour})
           node.exit().remove();

           link = link.data(links);
           link.enter().append("line")
               .attr("stroke-width",1)
           link.attr('stroke',function(d){return d.colour});
           link.exit().remove();
           force.start();

       }

       function tick() {
          link.attr("x1", function(d) { return Math.round(d.source.x); })
              .attr("y1", function(d) { return Math.round(d.source.y); })
              .attr("x2", function(d) { return Math.round(d.target.x); })
              .attr("y2", function(d) { return Math.round(d.target.y); });

          node.attr("cx", function(d) { return Math.round(d.x); })
              .attr("cy", function(d) { return Math.round(d.y); });
        }

       function rescale() {
            trans=d3.event.translate;
            scale=d3.event.scale;

            vis.attr("transform",
                "translate(" + trans + ")"
                + " scale(" + scale + ")"); 
        }