Диагностическое обновление D3 Force Directed Graph ajax

Я использую d3.js и jquery с фреймворком PHP (на основе структуры yii), чтобы создать диаграмму динамической силы, предназначенную для представления текущего состояния хостов и служб в сети, которые мы контролируем с помощью Nagios.

На графике показаны команды root → hostgroups → hosts → . Я создал функцию на стороне сервера, чтобы вернуть объект JSON в следующем формате

{
    "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"
        }
    ]
}

Узлы содержат идентификатор объекта, который используется в ссылках и цвете для отображения состояния node (OK = зеленый, WARNING = желтый и т.д.). Ссылки имеют идентификаторы исходного объекта и идентификаторы целевого объекта для узлы. Узлы и ссылки могут меняться по мере добавления или удаления новых узлов из системы мониторинга.

У меня есть следующий код, который устанавливает исходный SVG, а затем каждые 10 секунд

  • Извлекает текущий объект JSON
  • Создает карту ссылок
  • Выбирает текущие узлы и ссылки и связывает их с данными JSON.
  • Ввод ссылок добавлен и удаляются ссылки.
  • обновленные и добавленные узлы изменят свой цвет заливки и всплывающая подсказка с добавленным именем.
  • Сила запущена

    $. ajaxSetup ({cache: false});   width = 960,   высота = 500;    node= [];   link = [];   force = d3.layout.force()       .charge(-1000)       .linkDistance(1)       .size([ширина, высота]);

    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);
    

Пример вывода можно увидеть в Сетевая визуализация

Все вышеизложенное работает правильно, за исключением того, что каждый раз, когда петли кода заставляют визуализацию перезапускаться, а все узлы откатываются до тех пор, пока они не опустится. Мне нужно, чтобы все текущие элементы оставались такими, какими они есть, но все новые узлы и ссылки добавляются к визуализации и доступны для просмотра и перетаскивания и т.д.

Если кто-то может помочь, я буду вечно благодарен.

Ответ 1

Мне удалось найти решение проблемы, используя смесь всех приведенных выше рекомендаций, ниже приведен код, который я использовал

    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 + ")"); 
        }

Ответ 3

Недавно я попытался сделать то же самое, вот решение, с которым я столкнулся. Я загружаю первую партию данных с links.php, а затем обновляю их с помощью newlinks.php, оба возвращают JSON со списком объектов с атрибутами sender и receiver. В этом примере newlinks каждый раз возвращает нового отправителя, и я устанавливаю приемник как случайно выбранный старый node.

$.post("links.php", function(data) {
// Functions as an "initializer", loads the first data
// Then newlinks.php will add more data to this first batch (see below)
var w = 1400,
    h = 1400;

var svg = d3.select("#networkviz")
            .append("svg")
            .attr("width", w)
            .attr("height", h);

var links = [];
var nodes = [];

var force = d3.layout.force()
                     .nodes(nodes)
                     .links(links)
                     .size([w, h])
                     .linkDistance(50)
                     .charge(-50)
                     .on("tick", tick);

svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");

var linkSVG = svg.select(".links").selectAll(".link"),
    nodeSVG = svg.select(".nodes").selectAll(".node");

handleData(data);
update();

// This is the server call
var interval = 5; // set the frequency of server calls (in seconds)
setInterval(function() {
    var currentDate = new Date();
    var beforeDate = new Date(currentDate.setSeconds(currentDate.getSeconds()-interval));
    $.post("newlinks.php", {begin: beforeDate, end: new Date()}, function(newlinks) {
        // newlinks.php returns a JSON file with my new transactions (the one that happened between now and 5 seconds ago)
        if (newlinks.length != 0) { // If nothing happened, then I don't need to do anything, the graph will stay as it was
            // here I decide to add any new node and never remove any of the old ones
            // so eventually my graph will grow extra large, but that up to you to decide what you want to do with your nodes
            newlinks = JSON.parse(newlinks);
            // Adds a node to a randomly selected node (completely useless, but a good example)
            var r = getRandomInt(0, nodes.length-1);
            newlinks[0].receiver = nodes[r].id;
            handleData(newlinks);
            update();
        }
    });
}, interval*1000);

function update() {
    // enter, update and exit
    force.start();

    linkSVG = linkSVG.data(force.links(), function(d) { return d.source.id+"-"+d.target.id; });
    linkSVG.enter().append("line").attr("class", "link").attr("stroke", "#ccc").attr("stroke-width", 2);
    linkSVG.exit().remove();

    var r = d3.scale.sqrt().domain(d3.extent(force.nodes(), function(d) {return d.weight; })).range([5, 20]);
    var c = d3.scale.sqrt().domain(d3.extent(force.nodes(), function(d) {return d.weight; })).range([0, 270]);

    nodeSVG = nodeSVG.data(force.nodes(), function(d) { return d.id; });
    nodeSVG.enter()
           .append("circle")
           .attr("class", "node")
    // Color of the nodes depends on their weight
    nodeSVG.attr("r", function(d) { return r(d.weight); })
           .attr("fill", function(d) {
               return "hsl("+c(d.weight)+", 83%, 60%)";
           });
    nodeSVG.exit().remove();    
}

function handleData(data) {
    // This is where you create nodes and links from the data you receive
    // In my implementation I have a list of transactions with a sender and a receiver that I use as id
    // You'll have to customize that part depending on your data
    for (var i = 0, c = data.length; i<c; i++) {
        var sender = {id: data[i].sender};
        var receiver = {id: data[i].receiver};
        sender = addNode(sender);
        receiver = addNode(receiver);
        addLink({source: sender, target: receiver});
    }
}

// Checks whether node already exists in nodes or not
function addNode(node) {
    var i = nodes.map(function(d) { return d.id; }).indexOf(node.id);
    if (i == -1) {
        nodes.push(node);
        return node;
    } else {
        return nodes[i];
    }
}

// Checks whether link already exists in links or not
function addLink(link) {
    if (links.map(function(d) { return d.source.id+"-"+d.target.id; }).indexOf(link.source.id+"-"+link.target.id) == -1
        && links.map(function(d) { return d.target.id+"-"+d.source.id; }).indexOf(link.source.id+"-"+link.target.id) == -1)
        links.push(link);
}

function tick() {
    linkSVG.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;});
    nodeSVG.attr("cx", function(d) {return d.x})
            .attr("cy", function(d) {return d.y});
}

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}
}, "json");

Это очень конкретная реализация, поэтому вы должны заполнить отверстия там, где это необходимо, в зависимости от вывода вашего сервера. Но я считаю, что D3-основа правильная и то, что вы ищете:) Вот JSFiddle, чтобы играть с ним: http://jsfiddle.net/bTyh5/2/

Этот код был действительно полезен и вдохновил некоторые из представленных здесь деталей.

Ответ 4

На самом деле вам не нужно передавать что-либо обратно на сервер, если серверная сторона, вы можете сказать, что генерируются новые nodes и links. Затем вместо перезагрузки всего d3 script вы загружаете его один раз, а в force.on("tick", function()) вы делаете свой вызов AJAX с интервалом в 10 секунд, чтобы перейти от сервера к новому data, который вы хотите добавить, будь то nodes или links.

Например, представьте, что вы изначально имеете этот JSON на своем сервере:

[
    {
        "nodes": [
            {
                "name": "MaaS",
                "object_id": 0
            },
            {
                "name": "Convergence",
                "object_id": "531",
                "colour": "#999900"
            }
        ]
    },
    {
        "links": [
            {
                "source": 0,
                "target": "531"
            }
        ]
    }
]

Вы можете получить его с вашего сервера с помощью AJAX и проанализировать его с помощью json = $.parseJSON(json);.

Затем напишите timeout, чтобы вместо выполнения всей функции, имеющейся в success, запускается только после вычисления макета. Затем снова на success проанализируйте новый JSON, полученный с сервера, и добавьте the_new_ nodes и links к уже существующим force.nodes и force.links соответственно.

Обратите внимание, что я не тестировал это, и я не уверен, как он будет работать и/или выполнять, но я думаю, что общий подход возможен.