Ajout de nouveaux nœuds à la disposition dirigée par la force

89

Première question sur Stack Overflow, alors soyez patient! Je suis nouveau sur d3.js, mais j'ai toujours été étonné par ce que les autres sont capables d'accomplir avec lui ... et presque aussi étonné par le peu de progrès que j'ai pu faire moi-même! De toute évidence, je ne suis pas en train de faire quelque chose, alors j'espère que les bonnes âmes ici pourront me montrer la lumière.

Mon intention est de créer une fonction javascript réutilisable qui fait simplement ce qui suit:

  • Crée un graphique vide dirigé par la force dans un élément DOM spécifié
  • Vous permet d'ajouter et de supprimer des nœuds étiquetés porteurs d'images à ce graphique, en spécifiant les connexions entre eux

J'ai pris http://bl.ocks.org/950642 comme point de départ, car c'est essentiellement le type de mise en page que je veux pouvoir créer:

entrez la description de l'image ici

Voici à quoi ressemble mon code:

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

Chaque fois que j'ajoute un nouveau nœud, il ré-étiquette tous les nœuds existants; ceux-ci s'empilent les uns sur les autres et les choses commencent à devenir moche. Je comprends pourquoi c'est: parce que lorsque j'appelle la update()fonction de fonction lors de l'ajout d'un nouveau nœud, elle fait un node.append(...)sur l'ensemble de données. Je ne peux pas comprendre comment faire cela uniquement pour le nœud que j'ajoute ... et je ne peux apparemment l'utiliser que node.enter()pour créer un seul nouvel élément, donc cela ne fonctionne pas pour les éléments supplémentaires dont j'ai besoin liés au nœud . Comment puis-je réparer cela?

Merci pour tous les conseils que vous pouvez donner sur l'un de ces problèmes!

Modifié car j'ai rapidement corrigé une source de plusieurs autres bogues qui ont été mentionnés précédemment

nkoren
la source

Réponses:

152

Après de longues heures à ne pas pouvoir faire fonctionner cela, je suis finalement tombé sur une démo que je ne pense pas être liée à la documentation: http://bl.ocks.org/1095795 :

entrez la description de l'image ici

Cette démo contenait les clés qui m'ont finalement aidé à résoudre le problème.

L'ajout de plusieurs objets sur un enter()peut être effectué en affectant le enter()à une variable, puis en y ajoutant. C'est logique. La deuxième partie critique est que les tableaux de nœuds et de liens doivent être basés sur le force()- sinon le graphique et le modèle ne seront pas synchronisés lorsque les nœuds sont supprimés et ajoutés.

En effet, si un nouveau tableau est construit à la place, il lui manquera les attributs suivants :

  • index - l'index de base zéro du nœud dans le tableau de nœuds.
  • x - la coordonnée x de la position actuelle du nœud.
  • y - la coordonnée y de la position actuelle du nœud.
  • px - la coordonnée x de la position précédente du nœud.
  • py - la coordonnée y de la position précédente du nœud.
  • fixed - un booléen indiquant si la position du nœud est verrouillée.
  • poids - le poids du nœud; le nombre de liens associés.

Ces attributs ne sont pas strictement nécessaires pour l'appel à force.nodes(), mais s'ils ne sont pas présents, ils seront initialisés de manière aléatoireforce.start() lors du premier appel.

Si quelqu'un est curieux, le code de travail ressemble à ceci:

<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
la source
1
Utiliser force.start()plutôt que force.resume()lorsque de nouvelles données sont ajoutées était la clé. Merci beaucoup!
Mouagip
C'est génial. Soyez cool s'il a mis à l'échelle automatiquement le niveau de zoom (peut-être en réduisant la charge jusqu'à ce que tout rentre?) Afin que tout soit ajusté dans la taille de la boîte dans laquelle il était dessiné.
Rob Grant
1
+1 pour l'exemple de code propre. Je l'aime mieux que l'exemple de M. Bostock car il montre comment encapsuler un comportement dans un objet. Bien joué. (Pensez à l'ajouter à la bibliothèque d'exemples D3?)
peurless_fool
C'est beau! J'apprends à utiliser forceGraph avec d3 depuis quelques jours maintenant, et c'est la plus belle façon de le faire que j'ai vue. Merci beaucoup!
Lucas Azevedo