web-dev-qa-db-fra.com

Ajout de nouveaux nœuds à la mise en page dirigée par la force

Première question sur Stack Overflow, alors restez avec moi! Je suis nouveau sur d3.js, mais j'ai toujours été étonné par ce que les autres peuvent accomplir avec lui ... et presque aussi étonné par le peu de progrès que j'ai pu faire avec moi-même! De toute évidence, je ne cherche pas quelque chose, alors j'espère que les bonnes âmes ici peuvent 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 vierge dirigé par la force dans un élément DOM spécifié
  • Vous permet d'ajouter et de supprimer des nœuds porteurs d'image étiquetés à 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:

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, il ré-étiquette tous les nœuds existants; ces tas les uns sur les autres et les choses commencent à devenir laides. Je comprends pourquoi: parce que lorsque j'appelle la fonction update() function lors de l'ajout d'un nouveau noeud, il fait node.append(...) à l'ensemble des données. Je ne peux pas comprendre comment faire cela pour uniquement le nœud que j'ajoute ... et je ne peux qu'apparemment utiliser node.enter() pour créer un seul nouvel élément, de sorte que 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 êtes en mesure de donner sur l'un de ces problèmes!

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

87
nkoren

Après de longues heures sans pouvoir faire fonctionner cela, je suis finalement tombé sur une démo qui, je pense, n'est liée à aucune de la documentation: http://bl.ocks.org/1095795 :

enter image description here

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

L'ajout de plusieurs objets sur une enter() peut être fait en affectant la 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 la force() - sinon le graphique et le modèle seront désynchronisés lorsque les 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 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 - 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 seraient au hasard initialisé par force.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>
150
nkoren