Aggiunta di nuovi nodi al layout diretto dalla forza


89

Prima domanda su Stack Overflow, quindi abbi pazienza! Sono nuovo di d3.js, ma sono rimasto costantemente stupito da ciò che gli altri sono in grado di ottenere con esso ... e quasi altrettanto stupito da quanto poco sono stato in grado di fare io stesso con esso! Chiaramente non sto cercando qualcosa, quindi spero che le anime gentili qui possano mostrarmi la luce.

La mia intenzione è creare una funzione javascript riutilizzabile che faccia semplicemente quanto segue:

  • Crea un grafico guidato dalla forza vuoto in un elemento DOM specificato
  • Consente di aggiungere ed eliminare nodi etichettati con immagini a quel grafico, specificando le connessioni tra di loro

Ho preso http://bl.ocks.org/950642 come punto di partenza, poiché questo è essenzialmente il tipo di layout che voglio essere in grado di creare:

inserisci qui la descrizione dell'immagine

Ecco come appare il mio codice:

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

Ogni volta che aggiungo un nuovo nodo, rietichetta tutti i nodi esistenti; questi si accumulano l'uno sull'altro e le cose iniziano a diventare brutte. Capisco perché questo è: perché quando chiamo la update()funzione funzione dopo aver aggiunto un nuovo nodo, fa un node.append(...)a all'intero set di dati. Non riesco a capire come farlo solo per il nodo che sto aggiungendo ... e posso solo apparentemente usare node.enter()per creare un singolo nuovo elemento, quindi non funziona per gli elementi aggiuntivi di cui ho bisogno per il nodo . Come posso risolvere questo problema?

Grazie per qualsiasi consiglio che sei in grado di dare su qualsiasi di questo problema!

Modificato perché ho risolto rapidamente una fonte di molti altri bug menzionati in precedenza

Risposte:


152

Dopo molte lunghe ore in cui non sono stato in grado di farlo funzionare, sono finalmente incappato in una demo che non credo sia collegata a nessuna documentazione: http://bl.ocks.org/1095795 :

inserisci qui la descrizione dell'immagine

Questa demo conteneva le chiavi che finalmente mi hanno aiutato a risolvere il problema.

L'aggiunta di più oggetti su una enter()può essere eseguita assegnando enter()a una variabile e quindi aggiungendola. Questo ha senso. La seconda parte critica è che gli array di nodi e link devono essere basati suforce() - altrimenti il ​​grafico e il modello andranno fuori sincronia quando i nodi vengono eliminati e aggiunti.

Questo perché se viene costruito un nuovo array, mancherà i seguenti attributi :

  • index - l'indice in base zero del nodo all'interno della matrice dei nodi.
  • x - la coordinata x della posizione corrente del nodo.
  • y - la coordinata y della posizione corrente del nodo.
  • px: la coordinata x della posizione del nodo precedente.
  • py - la coordinata y della posizione del nodo precedente.
  • fisso - un valore booleano che indica se la posizione del nodo è bloccata.
  • peso - il peso del nodo; il numero di link associati.

Questi attributi non sono strettamente necessari per la chiamata a force.nodes(), ma se non sono presenti, verrebbero inizializzati casualmente daforce.start() prima chiamata.

Se qualcuno è curioso, il codice funzionante è simile a questo:

<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
Utilizzare al force.start()posto di force.resume()quando vengono aggiunti nuovi dati era la chiave. Molte grazie!
Mouagip

Questo e spettacolare. Va bene se ha scalato automaticamente il livello di zoom (magari riducendo la carica fino a quando tutto si adatta?) In modo che tutto rientri nelle dimensioni della scatola in cui si trovava.
Rob Grant

1
+1 per l'esempio di codice pulito. Mi piace di più dell'esempio del signor Bostock perché mostra come incapsulare il comportamento in un oggetto. Molto bene. (Considera l'idea di aggiungerlo alla libreria di esempio D3?)
fearless_fool

Questo è bello! Sto imparando a usare forceGraph con d3 da un paio di giorni ormai, e questo è il modo più bello per farlo che ho visto. Grazie mille!
Lucas Azevedo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.