Thêm các nút mới vào bố cục theo hướng bắt buộc


89

Câu hỏi đầu tiên về Stack Overflow, vì vậy hãy chịu khó với tôi! Tôi mới làm quen với d3.js, nhưng đã luôn ngạc nhiên bởi những gì người khác có thể đạt được với nó ... và gần như ngạc nhiên trước cách mà tôi đã có thể tự mình làm được với nó! Rõ ràng là tôi không tìm kiếm thứ gì đó, vì vậy tôi hy vọng rằng những linh hồn tốt bụng ở đây có thể cho tôi thấy ánh sáng.

Ý định của tôi là tạo một hàm javascript có thể tái sử dụng, hàm này chỉ đơn giản thực hiện những việc sau:

  • Tạo một biểu đồ hướng lực trống trong phần tử DOM được chỉ định
  • Cho phép bạn thêm và xóa các nút mang hình ảnh, được gắn nhãn vào biểu đồ đó, chỉ định các kết nối giữa chúng

Tôi đã lấy http://bl.ocks.org/950642 làm điểm bắt đầu, vì về cơ bản đó là loại bố cục tôi muốn có thể tạo:

nhập mô tả hình ảnh ở đây

Đây là mã của tôi trông như thế nào:

<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="underscore-min.js"></script>
    <script type="text/javascript" src="d3.v2.min.js"></script>
    <style type="text/css">
        .link { stroke: #ccc; }
        .nodetext { pointer-events: none; font: 10px sans-serif; }
        body { width:100%; height:100%; margin:none; padding:none; }
        #graph { width:500px;height:500px; border:3px solid black;border-radius:12px; margin:auto; }
    </style>
</head>
<body>
<div id="graph"></div>
</body>
<script type="text/javascript">

function myGraph(el) {

    // Initialise the graph object
    var graph = this.graph = {
        "nodes":[{"name":"Cause"},{"name":"Effect"}],
        "links":[{"source":0,"target":1}]
    };

    // Add and remove elements on the graph object
    this.addNode = function (name) {
        graph["nodes"].push({"name":name});
        update();
    }

    this.removeNode = function (name) {
        graph["nodes"] = _.filter(graph["nodes"], function(node) {return (node["name"] != name)});
        graph["links"] = _.filter(graph["links"], function(link) {return ((link["source"]["name"] != name)&&(link["target"]["name"] != name))});
        update();
    }

    var findNode = function (name) {
        for (var i in graph["nodes"]) if (graph["nodes"][i]["name"] === name) return graph["nodes"][i];
    }

    this.addLink = function (source, target) {
        graph["links"].push({"source":findNode(source),"target":findNode(target)});
        update();
    }

    // set up the D3 visualisation in the specified element
    var w = $(el).innerWidth(),
        h = $(el).innerHeight();

    var vis = d3.select(el).append("svg:svg")
        .attr("width", w)
        .attr("height", h);

    var force = d3.layout.force()
        .nodes(graph.nodes)
        .links(graph.links)
        .gravity(.05)
        .distance(100)
        .charge(-100)
        .size([w, h]);

    var update = function () {

        var link = vis.selectAll("line.link")
            .data(graph.links);

        link.enter().insert("line")
            .attr("class", "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; });

        link.exit().remove();

        var node = vis.selectAll("g.node")
            .data(graph.nodes);

        node.enter().append("g")
            .attr("class", "node")
            .call(force.drag);

        node.append("image")
            .attr("class", "circle")
            .attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
            .attr("x", "-8px")
            .attr("y", "-8px")
            .attr("width", "16px")
            .attr("height", "16px");

        node.append("text")
            .attr("class", "nodetext")
            .attr("dx", 12)
            .attr("dy", ".35em")
            .text(function(d) { return d.name });

        node.exit().remove();

        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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
        });

        // Restart the force layout.
        force
          .nodes(graph.nodes)
          .links(graph.links)
          .start();
    }

    // Make it all go
    update();
}

graph = new myGraph("#graph");

// These are the sort of commands I want to be able to give the object.
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");

</script>
</html>

Mỗi khi tôi thêm một nút mới, nó sẽ gắn nhãn lại cho tất cả các nút hiện có; những thứ này chồng chất lên nhau và mọi thứ bắt đầu trở nên xấu xí. Tôi hiểu tại sao điều này là: bởi vì khi tôi gọi update()hàm hàm khi thêm một nút mới, nó sẽ thực hiện một node.append(...)đối với toàn bộ tập dữ liệu. Tôi không thể tìm ra cách thực hiện việc này cho chỉ nút mà tôi đang thêm ... và tôi dường như chỉ có thể sử dụng node.enter()để tạo một phần tử mới duy nhất, vì vậy điều đó không hoạt động đối với các phần tử bổ sung mà tôi cần liên kết với nút . Làm thế nào tôi có thể sửa lỗi này?

Cảm ơn bạn vì bất kỳ hướng dẫn nào mà bạn có thể cung cấp cho bất kỳ vấn đề nào trong số này!

Đã chỉnh sửa vì tôi đã nhanh chóng sửa một nguồn của một số lỗi khác đã được đề cập trước đó

Câu trả lời:


152

Sau nhiều giờ dài không thể làm cho việc này hoạt động, cuối cùng tôi tình cờ xem được một bản demo mà tôi không nghĩ là có liên kết với bất kỳ tài liệu nào: http://bl.ocks.org/1095795 :

nhập mô tả hình ảnh ở đây

Bản demo này chứa các khóa cuối cùng đã giúp tôi giải quyết vấn đề.

Việc thêm nhiều đối tượng trên một enter()có thể được thực hiện bằng cách gán biến enter()cho một biến, sau đó thêm vào đó. Điều này thật ý nghĩa. Phần quan trọng thứ hai là mảng nút và liên kết phải dựa trên force()- nếu không thì đồ thị và mô hình sẽ không đồng bộ khi các nút bị xóa và thêm vào.

Điều này là do nếu một mảng mới được xây dựng thay thế, nó sẽ thiếu những thứ sau thuộc tính :

  • index - chỉ số dựa trên 0 của nút trong mảng nút.
  • x - tọa độ x của vị trí nút hiện tại.
  • y - tọa độ y của vị trí nút hiện tại.
  • px - tọa độ x của vị trí nút trước đó.
  • py - tọa độ y của vị trí nút trước đó.
  • cố định - một boolean cho biết vị trí nút có bị khóa hay không.
  • weight - trọng lượng của nút; số lượng liên kết được liên kết.

Các thuộc tính này không hoàn toàn cần thiết cho lệnh gọi đến force.nodes(), nhưng nếu chúng không có mặt, thì chúng sẽ được khởi tạo ngẫu nhiên bởiforce.start() trong lần gọi đầu tiên.

Nếu ai đó tò mò, mã làm việc sẽ như thế này:

<script type="text/javascript">

function myGraph(el) {

    // Add and remove elements on the graph object
    this.addNode = function (id) {
        nodes.push({"id":id});
        update();
    }

    this.removeNode = function (id) {
        var i = 0;
        var n = findNode(id);
        while (i < links.length) {
            if ((links[i]['source'] === n)||(links[i]['target'] == n)) links.splice(i,1);
            else i++;
        }
        var index = findNodeIndex(id);
        if(index !== undefined) {
            nodes.splice(index, 1);
            update();
        }
    }

    this.addLink = function (sourceId, targetId) {
        var sourceNode = findNode(sourceId);
        var targetNode = findNode(targetId);

        if((sourceNode !== undefined) && (targetNode !== undefined)) {
            links.push({"source": sourceNode, "target": targetNode});
            update();
        }
    }

    var findNode = function (id) {
        for (var i=0; i < nodes.length; i++) {
            if (nodes[i].id === id)
                return nodes[i]
        };
    }

    var findNodeIndex = function (id) {
        for (var i=0; i < nodes.length; i++) {
            if (nodes[i].id === id)
                return i
        };
    }

    // set up the D3 visualisation in the specified element
    var w = $(el).innerWidth(),
        h = $(el).innerHeight();

    var vis = this.vis = d3.select(el).append("svg:svg")
        .attr("width", w)
        .attr("height", h);

    var force = d3.layout.force()
        .gravity(.05)
        .distance(100)
        .charge(-100)
        .size([w, h]);

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

    var update = function () {

        var link = vis.selectAll("line.link")
            .data(links, function(d) { return d.source.id + "-" + d.target.id; });

        link.enter().insert("line")
            .attr("class", "link");

        link.exit().remove();

        var node = vis.selectAll("g.node")
            .data(nodes, function(d) { return d.id;});

        var nodeEnter = node.enter().append("g")
            .attr("class", "node")
            .call(force.drag);

        nodeEnter.append("image")
            .attr("class", "circle")
            .attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
            .attr("x", "-8px")
            .attr("y", "-8px")
            .attr("width", "16px")
            .attr("height", "16px");

        nodeEnter.append("text")
            .attr("class", "nodetext")
            .attr("dx", 12)
            .attr("dy", ".35em")
            .text(function(d) {return d.id});

        node.exit().remove();

        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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
        });

        // Restart the force layout.
        force.start();
    }

    // Make it all go
    update();
}

graph = new myGraph("#graph");

// You can do this from the console as much as you like...
graph.addNode("Cause");
graph.addNode("Effect");
graph.addLink("Cause", "Effect");
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");

</script>

1
Sử dụng force.start()thay vì force.resume()khi dữ liệu mới được thêm vào là chìa khóa. Cảm ơn rất nhiều!
Mouagip

Điều này thật tuyệt. Được mát mẻ nếu nó autoscaled mức zoom (có thể giảm phí đến khi tất cả mọi thứ phù hợp?) Nên tất cả mọi thứ gắn trong kích thước của hộp nó được vẽ trong.
Rob Grant

1
+1 cho ví dụ về mã sạch. Tôi thích nó hơn ví dụ của ông Bostock vì nó chỉ ra cách đóng gói hành vi trong một đối tượng. Làm tốt. (Cân nhắc thêm nó vào thư viện ví dụ D3?)
Fearless_fool

Đẹp quá! Tôi đang học cách sử dụng forceGraph với d3 trong vài ngày nay và đây là cách hay nhất để làm điều đó mà tôi từng thấy. Cảm ơn bạn rất nhiều!
Lucas Azevedo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.