Diseño dirigido a la fuerza D3 con cuadro delimitador

Soy nuevo en D3 y tengo problemas para establecer los límites de mi diseño dirigido por fuerza. He logrado juntar (a partir de ejemplos) lo que me gustaría, pero necesito que se contenga el gráfico. En la función tick, una transformación / traducción mostrará mi gráfica correctamente, pero cuando uso cx y cy con Math.max / min (Ver código comentado), los nodos están fijados en la esquina superior izquierda mientras que las líneas están contenidas correctamente.

Esto es lo que tengo a continuación … ¿qué estoy haciendo 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(" + dx + "," + dy + ")"; }); // This contains the lines within the boundary, but the nodes are stuck in the top left corner //node.attr("cx", function(d) { return dx = Math.max(r, Math.min(w - r, dx)); }) // .attr("cy", function(d) { return dy = Math.max(r, Math.min(h - r, dy)); }); 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; }); }; } }); 

Hay un ejemplo de cuadro delimitador en mi charla sobre diseños de fuerza . La integración Verlet de posición le permite definir restricciones geométricas (como cuadros de límite y detección de colisión ) dentro del detector de eventos “tic”; simplemente mueva los nodos para cumplir con la restricción y la simulación se adaptará en consecuencia.

Dicho esto, la gravedad es definitivamente una forma más flexible de lidiar con este problema, ya que permite a los usuarios arrastrar el gráfico fuera del cuadro delimitador temporalmente y luego el gráfico se recuperará. Dependiendo del tamaño del gráfico y del tamaño del área mostrada, debe experimentar con distintas fuerzas relativas de gravedad y carga (repulsión) para que su gráfico encaje.

El código comentado funciona en un nodo que, a partir de su definición, es un elemento svg g (enrutamiento) y no opera los atributos cx / cy. Seleccione el elemento del círculo dentro del nodo para hacer que estos atributos cobren vida:

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

Una fuerza personalizada es una posible solución también. Me gusta más este acercamiento ya que no solo los nodos mostrados se reposicionan sino que toda la simulación funciona con la fuerza de delimitación.

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