【问题标题】:D3 Force Directed Graph ajax updateD3 强制有向图 ajax 更新
【发布时间】:2013-08-16 04:45:35
【问题描述】:

我正在使用 d3.js 和 jquery 以及 PHP 后端(基于 yii 框架)来创建动态强制有向图来表示我们正在使用 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({ 缓存: false }); 宽度 = 960, 高度 = 500; 节点 = []; 链接 = []; 力 = 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);
    

输出示例见Network Visualization

上述所有方法都可以正常工作,但每次代码循环时都会导致可视化重新启动,并且节点都会反弹,直到它们稳定下来。我需要的是让任何当前项目保持原样,但任何新节点和链接都会添加到可视化中,并且可以点击和拖动等。

如果有人能提供帮助,我将永远感激不尽。

【问题讨论】:

  • 发生这种情况是因为您实际上每次都在重新加载数据并重新计算布局。我认为您应该找到一种方法来检查服务器端的更改,并找到一种方法将它们与更新时的内容连接起来,而不是每次都重新加载新的 JSON。例如,创建一个仅包含 new 节点和链接的 JSON,然后在调用 force.on("tick", function()) 时将这些对象推送到 .nodes.links
  • 我真的希望有一种方法可以避免将当前的可视化对象传回服务器,因为这会使整个解决方案变得更加复杂。我开始查看 d3.js 的原因是您将数据传递给 d3,它会计算出输入和退出数据的内容,从而使您不必手动执行此操作。没有替代方法吗?
  • 实际上,我正在重新阅读您的评论,而 d3.js 没有 弄清楚数据中输入和退出的内容。它使用您提供的数据计算您告诉它的任何内容。如果要更改正在使用的数据,则必须自己更改。 :)
  • Aaa我在最后一条评论中搞砸了......对不起!您应该阅读一下:groups.google.com/forum/#!topic/d3-js/q8yz2OUMW8g 请点击链接,因为它们有宝贵的信息...

标签: ajax json d3.js force-layout


【解决方案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 + ")"); 
        }

【讨论】:

  • 我还添加了一个布尔变量,该变量仅在添加或删除新节点时运行 force.start() 以防止在图表的每个刻度上进行不必要的设置
  • 嗨!几年后:您能否解释一下我如何在上面的代码中从我的磁盘中输入一个 json 文件?谢谢:-)
【解决方案2】:

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

$.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/

This code 非常有用,并启发了这里介绍的一些部分。

【讨论】:

    【解决方案3】:

    看看这个答案。您需要为您的节点提供一个唯一标识符,您似乎拥有该标识符。

    Updating links on a force directed graph from dynamic json data

    【讨论】:

    • 该示例比我目前的示例要好,但是当我使用此代码并附加我的 ajax 调用时,动画不会从头开始重新启动,但即使节点或链接它仍然会在更新时移动没有改变。这对于我的要求是不可接受的,因为我需要图表在解决时不要移动,除非数据有一些变化,因为图表将用作整体状态视图并将在墙板上,移动会引起注意屏幕因此只应在发生更改时出现。
    【解决方案4】:

    您实际上不需要将任何内容传回服务器,只要服务器端,您能够知道正在生成什么新的nodeslinks。然后,不是重新加载整个 d3 脚本,而是加载一次,然后在 force.on("tick", function()) 中进行 10 秒超时 AJAX 调用,以从服务器获取要附加的新 data,无论是 @987654325 @ 或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_ nodeslinks 分别添加到已经存在的 force.nodesforce.links 中。

    请注意,我没有对此进行测试,也不确定它将如何工作和/或执行,但我认为一般方法是可行的。

    【讨论】:

    • 那是有道理的。您是否有任何示例说明我如何比较当前节点和 JSON 中的节点以找出两组节点/链接之间的差异?
    • 不是真的,抱歉。但是不是比较新/旧,你不能把新数据输出到文件吗?您只需要添加数据还是还需要删除数据?这使问题变得更加复杂......
    • 需要添加和删除数据,使事情变得更加复杂,当前节点可能具有不同的属性,因此可能需要重新渲染
    • 嗯,这让问题变得更加复杂......我认为只有我一个人无法帮助你解决这个问题,尽管我对答案很感兴趣。让我们再等几天,看看有没有人可以帮忙。此外,请留意可能出现的其他 cmets。我会尽可能多地宣传这个问题。如果有任何您认为对改写问题很重要的细节,请务必编辑问题并添加它们。祝你好运!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-02
    • 2013-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多