88 votes

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

Première question sur Stack Overflow, alors soyez indulgent avec moi ! Je suis nouveau dans le domaine de d3.js, mais j'ai été constamment é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 été capable de faire avec lui moi-même ! Il est clair que je n'ai pas compris quelque chose, alors j'espère que les bonnes âmes ici présentes pourront me montrer la lumière.

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

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

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

enter image description here

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, tous les nœuds existants sont réétiquetés ; ils s'empilent les uns sur les autres et les choses commencent à se gâter. Je comprends pourquoi : parce que lorsque j'appelle la fonction update() lors de l'ajout d'un nouveau nœud, elle effectue un node.append(...) à l'ensemble des données. Je n'arrive pas à trouver comment faire pour seulement le noeud que j'ajoute ... et je ne peux apparemment utiliser que node.enter() pour créer un seul nouvel élément, ce qui ne fonctionne pas pour les éléments supplémentaires que je dois lier au nœud. Comment puis-je résoudre ce problème ?

Merci pour tout conseil que vous pourrez donner sur cette question !

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

150voto

nkoren Points 1137

Après de longues heures passées à ne pas réussir à le faire fonctionner, je suis finalement tombé sur une démo qui, je pense, n'est pas liée à la documentation : http://bl.ocks.org/1095795 :

enter image description here

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

L'ajout de plusieurs objets sur un enter() peut se faire en attribuant à l enter() à une variable, et ensuite l'ajouter à celle-ci. C'est logique. La deuxième partie critique est que les tableaux de nœuds et de liens doivent être basés sur la méthode force() -- sinon le graphe et le modèle seront désynchronisés au fur et à mesure que des nœuds seront supprimés et ajoutés.

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

  • index - l'index basé sur zéro du nœud dans le tableau des 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 du nœud précédent.
  • py - la coordonnée y de la position du nœud précédent.
  • fixe - un booléen indiquant si la position du nœud est verrouillée.
  • weight - le poids du nœud ; le nombre de liens associés.

Ces attributs ne sont pas strictement nécessaires pour l'appel à la fonction force.nodes() mais si ces derniers ne sont pas présents, ils seront alors au hasard initialisé par force.start() au 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>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X