12

我正在使用 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;节点 = []; 链接 = []; 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);
    

可以在网络可视化中看到输出示例

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

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

4

4 回答 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 + ")"); 
        }
于 2013-08-22T11:11:22.920 回答
2

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

从动态 json 数据更新力有向图上的链接

于 2013-08-20T03:48:29.363 回答
2

我最近尝试做同样的事情,这是我想出的解决方案。我所做的是用 加载第一批数据,links.php然后用 更新它们newlinks.php,两者都返回一个带有属性的对象列表的 JSONsenderreceiver. 在这个例子中,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/

这段代码非常有用,并启发了这里介绍的一些部分。

于 2013-08-21T15:59:50.903 回答
1

您实际上不需要将任何内容传递回服务器,只要在server-side,您就能够分辨出新的nodeslinks正在生成的内容。然后,不是重新加载整个 d3 脚本,而是加载它一次,然后在 . 中force.on("tick", function())进行 10 秒超时 AJAX 调用,以从服务器获取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.nodesforce.links中。

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

于 2013-08-14T11:45:42.357 回答