Додавання нових вузлів до макета, спрямованого на Force


89

Перше питання про переповнення стека, так що терпіть зі мною! Я новачок у d3.js, але постійно дивуюсь тому, що інші можуть досягти за допомогою нього ... і майже так само вражений тим, як мало я просунувся до цього сам! Очевидно, я щось не маркую, тому сподіваюся, що добрі душі тут можуть показати мені світло.

Я маю намір створити багаторазову функцію javascript, яка просто робить наступне:

  • Створює порожній графік, спрямований на зусилля, у вказаному елементі DOM
  • Дозволяє додавати та видаляти мічені вузли, що містять зображення, до цього графіка, вказуючи зв’язки між ними

Я взяв за відправну точку http://bl.ocks.org/950642 , оскільки, по суті, саме такий макет я хочу мати можливість створити:

введіть тут опис зображення

Ось як виглядає мій код:

<!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>

Кожного разу, коли я додаю новий вузол, він повторно позначає всі існуючі вузли; ці купи накладають один на одного, і речі починають ставати некрасивими. Я розумію, чому це так: адже, коли я викликаю update()функцію функції при додаванні нового вузла, вона робить a node.append(...)для всього набору даних. Я не можу зрозуміти, як це зробити лише для того вузла, який я додаю ... і я можу, мабуть, використовувати його node.enter()для створення одного нового елемента, тому це не працює для додаткових елементів, які мені потрібні, прив'язані до вузла . Як я можу це виправити?

Дякуємо за будь-які вказівки, які ви можете дати щодо будь-якого з цього питання!

Відредаговано, оскільки я швидко виправив джерело кількох інших помилок, про які раніше згадувалося

Відповіді:


152

Після багатьох довгих годин неможливості отримати цю роботу, я нарешті наткнувся на демонстраційне відео, яке, на мою думку, не пов’язане з жодною документацією: http://bl.ocks.org/1095795 :

введіть тут опис зображення

Ця демонстрація містила ключі, які нарешті допомогли мені вирішити проблему.

Додавання кількох об'єктів до an enter()можна зробити, присвоївши enter()змінну, а потім додавши до неї. Це має сенс. Друга критична частина полягає в тому, що масиви вузлів і посилань повинні базуватися на force()- інакше графік і модель не синхронізуються, коли вузли видаляються і додаються.

Це пов’язано з тим, що якщо замість цього будується новий масив, у нього не буде таких атрибутів :

  • index - нульовий індекс вузла в масиві вузлів.
  • x - координата x поточної позиції вузла.
  • y - координата y поточної позиції вузла.
  • px - координата х попередньої позиції вузла.
  • py - координата y попередньої позиції вузла.
  • фіксований - логічне значення, що вказує, чи заблоковано положення вузла.
  • вага - вага вузла; кількість пов'язаних посилань.

Ці атрибути не є суворо необхідними для виклику force.nodes(), але якщо їх немає, тоді вони будуть випадково ініціалізовані при force.start()першому виклику.

Якщо комусь цікаво, робочий код виглядає так:

<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
Ключовим було використання force.start()замість того, force.resume()коли додаються нові дані. Дуже дякую!
Mouagip

Це круто. Будьте круті, якщо він автоматично масштабує рівень масштабування (можливо, зменшує заряд, поки все підходить?), Щоб все відповідало розміру коробки, в яку він втягувався.
Роб Грант

1
+1 для прикладу чистого коду. Мені це подобається більше, ніж приклад містера Бостока, оскільки він показує, як інкапсулювати поведінку в об’єкті. Молодці. (Подумайте про додавання його до бібліотеки прикладів D3?)
fearless_fool

Це прекрасно! Я вже пару днів навчаюсь використовувати forceGraph з d3, і це найкрасивіший спосіб зробити це, який я бачив. Дуже дякую!
Лукас Азеведо,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.