Dodawanie nowych węzłów do układu kierowanego siłą

89

Pierwsze pytanie na temat przepełnienia stosu, więc wytrzymaj ze mną! Jestem nowy w d3.js, ale konsekwentnie byłem zdumiony tym, co inni są w stanie z nim osiągnąć ... i prawie tak samo zdumiony, jak małe postępy udało mi się zrobić samemu! Najwyraźniej niczego nie narzekam, więc mam nadzieję, że życzliwe dusze mogą mi pokazać światło.

Moim zamiarem jest stworzenie funkcji javascript wielokrotnego użytku, która po prostu wykonuje następujące czynności:

  • Tworzy pusty wykres ukierunkowany na siłę w określonym elemencie DOM
  • Umożliwia dodawanie i usuwanie oznaczonych węzłów zawierających obrazy do tego wykresu, określając połączenia między nimi

Wziąłem http://bl.ocks.org/950642 za punkt wyjścia, ponieważ zasadniczo taki układ chcę móc stworzyć:

wprowadź opis obrazu tutaj

Oto jak wygląda mój kod:

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

Za każdym razem, gdy dodaję nowy węzeł, zmienia on etykiety wszystkich istniejących węzłów; te piętrzą się jedna na drugiej i rzeczy zaczynają być brzydkie. Rozumiem, dlaczego tak jest: ponieważ kiedy wywołuję update()funkcję funkcji po dodaniu nowego węzła, robi to node.append(...)z całym zestawem danych. Nie mogę dowiedzieć się, jak to zrobić tylko dla dodawanego węzła ... i mogę tylko pozornie użyć node.enter()do utworzenia pojedynczego nowego elementu, więc to nie działa dla dodatkowych elementów, których potrzebuję, związanych z węzłem . Jak mogę to naprawić?

Dziękuję za wszelkie wskazówki, których możesz udzielić w każdej z tych kwestii!

Edytowane, ponieważ szybko naprawiłem źródło kilku innych błędów, o których wspomniano wcześniej

nkoren
źródło

Odpowiedzi:

152

Po wielu godzinach, w których nie mogłem tego uruchomić, w końcu natknąłem się na wersję demonstracyjną, do której nie sądzę, aby była połączona z żadną dokumentacją: http://bl.ocks.org/1095795 :

wprowadź opis obrazu tutaj

To demo zawierało klucze, które ostatecznie pomogły mi rozwiązać problem.

Dodanie wielu obiektów do zmiennej enter()może być wykonane poprzez przypisanie enter()zmiennej do zmiennej, a następnie dołączenie do niej. To ma sens. Drugą krytyczną częścią jest to, że tablice węzłów i łączy muszą być oparte na force()- w przeciwnym razie wykres i model stracą synchronizację, gdy węzły zostaną usunięte i dodane.

Dzieje się tak, ponieważ jeśli zamiast tego zostanie utworzona nowa tablica, będzie ona pozbawiona następujących atrybutów :

  • index - liczony od zera indeks węzła w tablicy węzłów.
  • x - współrzędna x aktualnej pozycji węzła.
  • y - współrzędna y aktualnej pozycji węzła.
  • px - współrzędna x poprzedniej pozycji węzła.
  • py - współrzędna y poprzedniego położenia węzła.
  • naprawiono - wartość logiczna wskazująca, czy pozycja węzła jest zablokowana.
  • waga - waga węzła; liczba powiązanych linków.

Te atrybuty nie są ściśle potrzebne do wywołania force.nodes(), ale jeśli ich nie ma, zostaną losowo zainicjowane przez force.start()przy pierwszym wywołaniu.

Jeśli ktoś jest ciekawy, działający kod wygląda tak:

<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
źródło
1
Kluczem było użycie force.start()zamiast force.resume()dodawania nowych danych. Wielkie dzięki!
Mouagip
To jest niesamowite. Spokojnie, jeśli automatycznie skaluje poziom powiększenia (może zmniejszając ładunek, aż wszystko pasuje?), Aby wszystko pasowało do rozmiaru pudełka, które rysował.
Rob Grant
1
+1 dla przykładu czystego kodu. Podoba mi się bardziej niż przykład pana Bostocka, ponieważ pokazuje, jak ująć zachowanie w obiekcie. Dobra robota. (Rozważ dodanie go do przykładowej biblioteki D3?)
strachless_fool
To jest piękne! Uczę się, jak używać forceGraph z d3 od kilku dni i jest to najpiękniejszy sposób, jaki widziałem. Dziękuję bardzo!
Lucas Azevedo