web-dev-qa-db-fra.com

D3 mise en page dirigée de force avec cadre de sélection

Je suis nouveau sur D3 et j'ai du mal à définir les limites de ma mise en page dirigée par la force. J'ai réussi à rassembler (à partir d'exemples) ce que je voudrais, mais j'ai besoin que le graphique soit contenu. Dans la fonction tick, une transformation/traduction affichera correctement mon graphique, mais lorsque j'utilise cx et cy avec Math.max/min (voir le code commenté), les nœuds sont épinglés dans le coin gauche du hauttop tandis contenue correctement.

Voici ce que j'ai en dessous ... Qu'est-ce que je fais mal ??

var w=960, h=500, r=8,  z = d3.scale.category20();

var color = d3.scale.category20();

var force = d3.layout.force()
       .linkDistance( function(d) { return (d.value*180) } )
       .linkStrength( function(d) { return (1/(1+d.value)) } )
       .charge(-1000)
       //.gravity(.08)
       .size([w, h]);

var vis = d3.select("#chart").append("svg:svg")
       .attr("width", w)
       .attr("height", h)
       .append("svg:g")
       .attr("transform", "translate(" + w / 4 + "," + h / 3 + ")");

vis.append("svg:rect")
   .attr("width", w)
   .attr("height", h)
   .style("stroke", "#000");


d3.json("miserables.json", function(json) {

       var link = vis.selectAll("line.link")
               .data(json.links);

       link.enter().append("svg: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.source.x; })
               .attr("y2", function(d) { return d.source.y; })
               .style("stroke-width", function(d) { return (1/(1+d.value))*5 });

       var node = vis.selectAll("g.node")
               .data(json.nodes);

       var nodeEnter = node.enter().append("svg:g")
               .attr("class", "node")
               .on("mouseover", fade(.1))
               .on("mouseout", fade(1))
               .call(force.drag);

       nodeEnter.append("svg:circle")
               .attr("r", r)
               .style("fill", function(d) { return z(d.group); })
               .style("stroke", function(d) { return
d3.rgb(z(d.group)).darker(); });

       nodeEnter.append("svg:text")
               .attr("text-anchor", "middle")
               .attr("dy", ".35em")
               .text(function(d) { return d.name; });

       force
       .nodes(json.nodes)
       .links(json.links)
       .on("tick", tick)
       .start();

       function tick() {

       // This works
               node.attr("transform", function(d) { return "translate(" + d.x + ","
+ d.y + ")"; });

       // This contains the lines within the boundary, but the nodes are
stuck in the top left corner
               //node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(w
- r, d.x)); })
               //      .attr("cy", function(d) { return d.y = Math.max(r, Math.min(h -
r, d.y)); });

       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; });
       }

       var linkedByIndex = {};

   json.links.forEach(function(d) {
       linkedByIndex[d.source.index + "," + d.target.index] = 1;
   });

       function isConnected(a, b) {
       return linkedByIndex[a.index + "," + b.index] ||
linkedByIndex[b.index + "," + a.index] || a.index == b.index;
   }

       function fade(opacity) {
       return function(d) {
           node.style("stroke-opacity", function(o) {
                       thisOpacity = isConnected(d, o) ? 1 : opacity;
                       this.setAttribute('fill-opacity', thisOpacity);
               return thisOpacity;
                       });

                       link.style("stroke-opacity", opacity).style("stroke-opacity",
function(o) {
               return o.source === d || o.target === d ? 1 : opacity;
               });
       };
       }

});
36
Michael Wilson

Il y a un exemple de cadre englobant dans mon discussion sur les dispositions de force . L'intégration de position Verlet vous permet de définir des contraintes géométriques (telles que des boîtes englobantes et une détection de collision ) à l'intérieur de l'écouteur d'événements "tick"; Il suffit de déplacer les nœuds pour se conformer à la contrainte et la simulation s’adapte en conséquence.

Cela dit, la gravité est certainement un moyen plus souple de traiter ce problème, car elle permet aux utilisateurs de faire glisser le graphique en dehors du cadre de sélection pour que le graphique se rétablisse. En fonction de la taille du graphique et de la taille de la zone affichée, vous devez essayer différentes forces relatives de gravité et de charge (répulsion) pour adapter votre graphique.

61
mbostock

Une force personnalisée est aussi une solution possible. J'aime davantage cette approche car non seulement les nœuds affichés sont repositionnés, mais toute la simulation fonctionne avec la force de collage.

let simulation = d3.forceSimulation(nodes)
    ...
    .force("bounds", boxingForce);

// Custom force to put all nodes in a box
function boxingForce() {
    const radius = 500;

    for (let node of nodes) {
        // Of the positions exceed the box, set them to the boundary position.
        // You may want to include your nodes width to not overlap with the box.
        node.x = Math.max(-radius, Math.min(radius, node.x));
        node.y = Math.max(-radius, Math.min(radius, node.y));
    }
}
1
Bruno Zell

Le code commenté fonctionne sur le noeud qui est, d'après votre définition, un élément svg g(rouping) et ne fait pas fonctionner les attributs cx/cy. Sélectionnez l'élément de cercle à l'intérieur du noeud pour rendre ces attributs vivants:

node.select("circle") // select the circle element in that node
    .attr("cx", function(d) { return d.x = Math.max(r, Math.min(w - r, d.x)); })
    .attr("cy", function(d) { return d.y = Math.max(r, Math.min(h - r, d.y)); });
0
Marc