D3力导向图ajax更新

12

我正在使用d3.js和jquery与基于yii框架的PHP后端创建一个动态力导向图,以表示我们使用Nagios监视的网络上当前主机和服务的状态。

该图显示根->主机组->主机->服务。我已经创建了一个服务器端函数以返回以下格式的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"
        }
    ]
}

节点包含对象ID用于链接,颜色以显示节点的状态(OK = 绿色,WARNING = 黄色等)。链接具有节点的源对象ID和目标对象ID。随着监控系统中添加或删除新主机,节点和链接可能会发生变化。

我有以下代码,设置初始SVG,然后每10秒执行以下操作:

  1. 检索当前JSON对象
  2. 创建链接映射
  3. 选择当前节点和链接,并将其绑定到JSON数据
  4. 添加输入链接并移除退出链接
  5. 更新和添加的节点将更改其填充颜色,并添加具有名称的工具提示
  6. 启动力导向图

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

输出的一个示例可以在网络可视化中看到。

除了每次代码循环时会导致可视化重新启动并且节点都会反弹直到它们稳定之外,上述所有内容都是正确的。我需要的是任何当前项目保持不变,但是任何新节点和链接都将添加到可视化中,并且可以被单击并拖动等等。

如果有人能帮忙,我将不胜感激。


这是因为每次重新加载数据并重新计算布局。我认为,你应该找到一种方法在服务器端检查更改,并将其与更新内容连接起来,而不是每次重新加载新的JSON。例如,创建一个仅包含“新”节点和链接的JSON,并在调用force.on(“tick”,function())时将这些对象推入.nodes.links中。 - Joum
我真的希望有一种方法可以避免将当前可视化对象传回服务器,因为这会使整个解决方案变得更加复杂。我开始研究d3.js的原因是你只需要将数据传递给d3,它就会自动处理已进入和已退出的数据,省去了手动操作的麻烦。难道没有其他替代方法吗? - d9705996
实际上,我重新阅读了您的评论,d3.js 并不会在数据中计算输入和退出。它会根据您提供的数据计算任何您告诉它要计算的内容。如果您想更改使用的数据,则必须自己更改。 :) - Joum
啊,我在上一条评论中搞砸了...抱歉!你应该阅读一下这个链接:https://groups.google.com/forum/#!topic/d3-js/q8yz2OUMW8g,跟随链接,因为它们包含了宝贵的信息... - Joum
4个回答

6
我已经成功地使用上述所有建议的混合方法找到了问题的解决方案,下面是我使用的代码。
    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 + ")"); 
        }

1
我还添加了一个布尔变量,仅在添加或删除新节点时运行force.start(),以防止图形每个刻度都进行不必要的稳定。 - d9705996
你好!几年后:能否请您解释一下如何在上面的代码中从我的磁盘中输入JSON文件?谢谢 :-) - chameau13

2

这个例子比我目前的更好,但是当我使用这段代码并附加我的ajax调用时,动画不会从头开始重新启动,但即使节点或链接没有改变,它仍然会在更新时移动。对于我所需的内容来说,这是不可接受的,因为我需要图表在稳定后不移动,除非数据发生了一些变化,因为该图表将用作整体状态视图,并且将放置在墙板上,移动会吸引屏幕的注意力,因此只有在发生更改时才应该发生移动。 - d9705996

2

我最近尝试做相同的事情,这是我想出的解决方案。我使用links.php加载第一批数据,然后使用newlinks.php更新它们,两者都返回一个带有senderreceiver属性的对象列表的JSON。在这个例子中,每次newlinks都返回一个新的sender,而我将receiver设置为随机选择的旧节点。

$.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's 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/ 这段代码非常有用,启发了这里介绍的一些部分。

1
您实际上不需要把任何东西传回服务器,只要在服务器端能够确定正在生成哪些新的节点和链接即可。然后,不必重新加载整个d3脚本,您只需在“force.on(“tick”,function())”中加载一次,并进行10秒的超时AJAX调用以从服务器获取您想要追加的新数据,无论是节点还是链接。
例如,假设您最初在服务器上拥有此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_ nodeslinks添加到已存在的force.nodesforce.links中。
请注意,我没有测试过这个方法,也不确定它会如何工作和/或表现,但我认为一般的方法是可行的。

那有点说得通。你有没有一些例子可以展示如何比较当前节点和JSON中的节点,以找出两组节点/链接之间的差异? - d9705996
不好意思,不能。但是你可以将新数据输出到一个文件,而不是比较新旧数据。你只需要添加数据还是需要删除数据?这会使问题变得更加复杂... - Joum
数据需要被添加和删除,为了让事情变得更加复杂,当前节点可能具有不同的属性,因此可能需要重新渲染。 - d9705996
嗯,这让问题变得更加复杂了...我不认为我一个人能帮助你解决这个问题,尽管我对答案非常感兴趣。让我们再等几天看看是否有人可以提供帮助。同时,注意其他可能出现的评论。我会尽我所能推广这个问题。如果您认为有任何重要的细节需要重新表述问题,请务必编辑问题并添加它们。祝好运! - Joum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接