Hinzufügen neuer Knoten zum erzwungenen Layout

89

Erste Frage zum Stapelüberlauf, also nimm sie mit! Ich bin neu bei d3.js, war aber immer wieder erstaunt darüber, was andere damit erreichen können ... und fast genauso erstaunt darüber, wie wenig Fortschritte ich selbst damit machen konnte! Klar, ich mache mir nichts vor, also hoffe ich, dass die freundlichen Seelen hier mir das Licht zeigen können.

Meine Absicht ist es, eine wiederverwendbare Javascript-Funktion zu erstellen, die einfach Folgendes bewirkt:

  • Erstellt ein leeres, kraftgerichtetes Diagramm in einem angegebenen DOM-Element
  • Ermöglicht das Hinzufügen und Löschen von beschrifteten, bildtragenden Knoten zu diesem Diagramm unter Angabe von Verbindungen zwischen diesen

Ich habe http://bl.ocks.org/950642 als Ausgangspunkt genommen, da dies im Wesentlichen die Art von Layout ist, die ich erstellen möchte:

Geben Sie hier die Bildbeschreibung ein

So sieht mein Code aus:

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

Jedes Mal, wenn ich einen neuen Knoten hinzufüge, werden alle vorhandenen Knoten neu beschriftet. Diese stapeln sich übereinander und die Dinge beginnen hässlich zu werden. Ich verstehe, warum dies so ist: Wenn ich die update()Funktionsfunktion beim Hinzufügen eines neuen Knotens aufrufe, wird dies node.append(...)für den gesamten Datensatz ausgeführt. Ich kann nicht herausfinden, wie dies nur für den Knotennode.enter() geschieht, den ich hinzufüge ... und ich kann anscheinend nur ein einzelnes neues Element erstellen, sodass dies für die zusätzlichen Elemente, die ich an den Knoten binden muss, nicht funktioniert . Wie kann ich das beheben?

Vielen Dank für jede Anleitung, die Sie zu diesem Thema geben können!

Bearbeitet, weil ich schnell eine Quelle für mehrere andere Fehler behoben habe, die zuvor erwähnt wurden

nkoren
quelle

Antworten:

152

Nach vielen langen Stunden, in denen ich nicht in der Lage war, dies zum Laufen zu bringen, stieß ich schließlich auf eine Demo, von der ich glaube, dass sie mit keiner der Dokumentationen verknüpft ist: http://bl.ocks.org/1095795 :

Geben Sie hier die Bildbeschreibung ein

Diese Demo enthielt die Schlüssel, die mir schließlich geholfen haben, das Problem zu lösen.

Das Hinzufügen mehrerer Objekte zu einem enter()kann erfolgen, indem Sie das enter()einer Variablen zuweisen und dann an dieses anhängen. Das macht Sinn. Der zweite wichtige Teil ist, dass die Knoten- und Link-Arrays auf dem basieren müssen force()- andernfalls werden das Diagramm und das Modell nicht mehr synchronisiert, wenn Knoten gelöscht und hinzugefügt werden.

Dies liegt daran, dass beim Erstellen eines neuen Arrays die folgenden Attribute fehlen :

  • index - Der auf Null basierende Index des Knotens innerhalb des Knotenarrays.
  • x - die x-Koordinate der aktuellen Knotenposition.
  • y - die y-Koordinate der aktuellen Knotenposition.
  • px - die x-Koordinate der vorherigen Knotenposition.
  • py - die y-Koordinate der vorherigen Knotenposition.
  • fest - ein Boolescher Wert, der angibt, ob die Knotenposition gesperrt ist.
  • Gewicht - das Knotengewicht; die Anzahl der zugeordneten Links.

Diese Attribute werden für den Aufruf von nicht unbedingt benötigt. force.nodes()Wenn diese jedoch nicht vorhanden sind, werden sie beim ersten Aufruf zufällig von initialisiert force.start().

Wenn jemand neugierig ist, sieht der Arbeitscode folgendermaßen aus:

<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>
nkoren
quelle
1
Die Verwendung force.start()anstelle des force.resume()Hinzufügens neuer Daten war der Schlüssel. Vielen Dank!
Mouagip
Das ist fantastisch. Seien Sie cool, wenn die
Rob Grant
1
+1 für das saubere Codebeispiel. Ich mag es besser als Mr. Bostocks Beispiel, weil es zeigt, wie man Verhalten in ein Objekt einkapselt. Gut gemacht. (Erwägen Sie, es der D3-Beispielbibliothek hinzuzufügen?)
anxless_fool
Das ist schön! Ich lerne seit ein paar Tagen, wie man forceGraph mit d3 benutzt, und dies ist der schönste Weg, den ich gesehen habe. Vielen Dank!
Lucas Azevedo