D3:当父母被拖入武力定向树时,如何阻止子节点被吸引到中心?

发布于 2025-02-13 19:53:49 字数 1802 浏览 1 评论 0原文

我已经使用D3.js V7构建了一个实力定向的树布局。到目前为止,它的工作正常,除了我甚至在可观察的示例中看到的行为 https ://observablehq.com/collection/@d3/d3-force

这就是我到目前为止所拥有的。

行为是,当我将一个节点拖到边缘时,孩子们似乎被吸引到中心。像这样:

“在此处输入图像描述”

我想像这样的“负载平衡”的子节点:

这是我设置模拟的方式:

const simulation = d3
  .forceSimulation()
  .force(
    "link",
    d3
      .forceLink()
      .id((d) => d.id)
      .distance(100)
      .strength(1)
  )
  .force("charge", d3.forceManyBody().strength(-500))
  .force("x", d3.forceX())
  .force("y", d3.forceY())
  .alphaDecay(0.05);

  simulation.on("tick", () => {
    link
      .attr("x1", (d) => d.source.x)
      .attr("y1", (d) => d.source.y)
      .attr("x2", (d) => d.target.x)
      .attr("y2", (d) => d.target.y);
  
    node.attr("transform", function (d) {
      return "translate(" + d.x + "," + d.y + ")";
    });
  });

我为此创建了一个工作codepen 在这里让您与我在谈论的内容一起玩。

我尝试将节点和链接到不同的值更改,但它们并没有真正帮助。我还尝试过禁用Center力量,但是如果我这样做的话,模拟永远不会出现在屏幕上。

.force("center").strength(0)

有没有办法实现我想要的东西?任何帮助都将受到赞赏。

I have built a force-directed tree layout using d3.js v7. It works fine so far except for a behavior that I've seen even in the examples on Observable https://observablehq.com/collection/@d3/d3-force

This is what I have so far.

enter image description here

The behavior is that when I drag a node towards the edge, the children seem to be attracted towards the center. Like so:

enter image description here

I'd like, instead, for the child nodes of "Load Balancing" to appear like this:

enter image description here

Here is how I've set up the simulation:

const simulation = d3
  .forceSimulation()
  .force(
    "link",
    d3
      .forceLink()
      .id((d) => d.id)
      .distance(100)
      .strength(1)
  )
  .force("charge", d3.forceManyBody().strength(-500))
  .force("x", d3.forceX())
  .force("y", d3.forceY())
  .alphaDecay(0.05);

  simulation.on("tick", () => {
    link
      .attr("x1", (d) => d.source.x)
      .attr("y1", (d) => d.source.y)
      .attr("x2", (d) => d.target.x)
      .attr("y2", (d) => d.target.y);
  
    node.attr("transform", function (d) {
      return "translate(" + d.x + "," + d.y + ")";
    });
  });

I've created a working Codepen for this here for you to play around with what I'm talking about.

I've tried changing the node and link forces to different values but they didn't really help. I've also tried disabling the center force but then the simulation never appears on the screen if I do that.

.force("center").strength(0)

Is there a way to achieve what I'm looking for? Any help is appreciated.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

酒与心事 2025-02-20 19:53:51

控制其他节点行为的一种方法是将它们修复在拖动函数中。我在您的代码中添加了一个函数,以修复其他节点,并且不允许它们在拖动过程中移动。

const data = {
  name: "AWS",
  resourceType: "aws",
  children: [{
    name: "VPC",
    resourceType: "vpc",
    children: [{
      name: "Load Balancing",
      resourceType: "load-balancing",
      children: [{
          name: "Public subnet 1",
          resourceType: "subnet"
        },
        {
          name: "Public subnet 2",
          resourceType: "subnet"
        },
        {
          name: "Private subnet 1",
          resourceType: "subnet"
        },
        {
          name: "Private subnet 2",
          resourceType: "subnet"
        }
      ]
    }]
  }]
};

// Set up the canvas
const chartContainer = d3.select("#svgcontainer");
const containerWidth = Math.max(
  1200,
  chartContainer.node().getBoundingClientRect().width
);

const containerHeight = Math.max(
  600,
  chartContainer.node().getBoundingClientRect().height
);

const margin = {
    top: 24,
    right: 124,
    bottom: 24,
    left: 24
  },
  width = containerWidth - margin.left - margin.right,
  height = containerHeight - margin.top - margin.bottom;

const svg = chartContainer
  .append("svg")
  .attr("viewBox", [-width / 2, -height / 2, width, height]);

const gLinks = svg
  .append("g")
  .attr("stroke", "#999")
  .attr("stroke-opacity", 0.6);

// Prepare the data
const root = d3.hierarchy(data);
let node, link;

// Deeper descendants are collapsed by default
root.descendants().forEach((d, i) => {
  d.id = i;
  d._children = d.children;
  if (d.depth > 2) d.children = null;
});

// Set up the force simulation
const simulation = d3
  .forceSimulation()
  .force(
    "link",
    d3
    .forceLink()
    .id((d) => d.id)
    .distance(100)
    .strength(1)
  )
  .force("charge", d3.forceManyBody().strength(-500))
  .force("x", d3.forceX())
  .force("y", d3.forceY())
  .alphaDecay(0.05);

// Update =========================================================
function update() {
  const links = root.links();
  const nodes = root.descendants();

  // Set up nodes
  node = svg.selectAll(".node").data(nodes, function(d) {
    return d.id;
  });
  node.exit().remove();

  const nodeEnter = node
    .enter()
    .append("g")
    .attr("class", "node")
    .call(drag(simulation));

  nodeEnter
    .append("circle")
    .attr("fill", (d) => {
      return "#fff";
    })
    .attr("r", 16);

  nodeEnter
    .append("text")
    .attr("dx", 32)
    .attr("dy", ".35em")
    .text(function(d) {
      return d.data.name;
    });

  nodeEnter.on("click", onNodeClicked);
  node = nodeEnter.merge(node);

  // Set up links
  link = gLinks.selectAll(".link").data(links, function(d) {
    return d.target.id;
  });
  link.exit().remove();
  const linkEnter = link.enter().append("line").attr("class", "link");
  link = linkEnter.merge(link);

  simulation.nodes(nodes);
  simulation.force("link").links(links);
}

function drag(simulation) {
  function dragstarted(event) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    event.subject.fx = event.subject.x;
    event.subject.fy = event.subject.y;

    event.fx = event.subject.x;
    event.fy = event.subject.y;
  }

  function dragged(event) {
    event.subject.fx = event.x;
    event.subject.fy = event.y;
    event.fx = event.x;
    event.fy = event.y;
    fix_other_nodes(event.subject);
  }

  function dragended(event) {
    if (!event.active) simulation.alphaTarget(0);
    event.subject.fx = event.x;
    event.subject.fy = event.y;

  }

  function fix_other_nodes(this_node) {
    node.each(function(d) {
      if (this_node != d) {
        d.fx = d.x;
        d.fy = d.y;
      }
    });
  }

  return d3
    .drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended);
}

function onNodeClicked(event, d) {
  // Show details drawer
  // setIsDrawerOpen(true);
  if (d._children) {
    d.children = d._children;
    d._children = null;
  } else {
    d._children = d.children;
    d.children = null;
  }

  if (d.children) {
    d.children.forEach((n) => {
      n.fx = d.x;
      n.fy = d.y;

      // Needed to make the children radiate from the parent upon expanding.
      n.needToUnfix = true;
    });
  }

  update();

  simulation.restart();
}

update();

simulation.on("tick", () => {
  link
    .attr("x1", (d) => d.source.x)
    .attr("y1", (d) => d.source.y)
    .attr("x2", (d) => d.target.x)
    .attr("y2", (d) => d.target.y);

  node.attr("transform", function(d) {
    if (d.needToUnfix) {
      // This is used to make the expanding children radiate from the parent.
      d.x = d.fx;
      d.y = d.fy;

      d.fx = null;
      d.fy = null;
      d.needToUnfix = false;
      return "translate(" + d.x + "," + d.y + ")";
    }
    return "translate(" + d.x + "," + d.y + ")";
  });
});
/************************/

html,
body {
  height: 100%;
  background-color: #eee;
}

#root {
  height: 100%;
}

.App {
  height: 100%;
}

#svgcontainer {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.node {
  filter: drop-shadow(0 4px 4px rgb(0 0 0 / 0.4));
}

.node text {
  font: 12px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 2px;
}

.resource-icon {
  transition: all 200ms;
}

.box-details {
  opacity: 0;
  transition: all 200ms;
}

.box-details text {
  font: 8px sans-serif;
}

.reset-zoom-btn {
  opacity: 0;
  transition: all 200ms;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js" integrity="sha512-MefNfAGJ/pEy89xLOFs3V6pYPs6AmUhXJrRlydI/9wZuGrqxmrdQ80zKHUcyadAcpH67teDZcBeS6oMJLPtTqw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<div className="App">
  <div id="svgcontainer"></div>
</div>

One way to control the other nodes behavior is to fix them in the dragged function. I have added a function in your code, to fix other nodes and not allow them to move during drag.

const data = {
  name: "AWS",
  resourceType: "aws",
  children: [{
    name: "VPC",
    resourceType: "vpc",
    children: [{
      name: "Load Balancing",
      resourceType: "load-balancing",
      children: [{
          name: "Public subnet 1",
          resourceType: "subnet"
        },
        {
          name: "Public subnet 2",
          resourceType: "subnet"
        },
        {
          name: "Private subnet 1",
          resourceType: "subnet"
        },
        {
          name: "Private subnet 2",
          resourceType: "subnet"
        }
      ]
    }]
  }]
};

// Set up the canvas
const chartContainer = d3.select("#svgcontainer");
const containerWidth = Math.max(
  1200,
  chartContainer.node().getBoundingClientRect().width
);

const containerHeight = Math.max(
  600,
  chartContainer.node().getBoundingClientRect().height
);

const margin = {
    top: 24,
    right: 124,
    bottom: 24,
    left: 24
  },
  width = containerWidth - margin.left - margin.right,
  height = containerHeight - margin.top - margin.bottom;

const svg = chartContainer
  .append("svg")
  .attr("viewBox", [-width / 2, -height / 2, width, height]);

const gLinks = svg
  .append("g")
  .attr("stroke", "#999")
  .attr("stroke-opacity", 0.6);

// Prepare the data
const root = d3.hierarchy(data);
let node, link;

// Deeper descendants are collapsed by default
root.descendants().forEach((d, i) => {
  d.id = i;
  d._children = d.children;
  if (d.depth > 2) d.children = null;
});

// Set up the force simulation
const simulation = d3
  .forceSimulation()
  .force(
    "link",
    d3
    .forceLink()
    .id((d) => d.id)
    .distance(100)
    .strength(1)
  )
  .force("charge", d3.forceManyBody().strength(-500))
  .force("x", d3.forceX())
  .force("y", d3.forceY())
  .alphaDecay(0.05);

// Update =========================================================
function update() {
  const links = root.links();
  const nodes = root.descendants();

  // Set up nodes
  node = svg.selectAll(".node").data(nodes, function(d) {
    return d.id;
  });
  node.exit().remove();

  const nodeEnter = node
    .enter()
    .append("g")
    .attr("class", "node")
    .call(drag(simulation));

  nodeEnter
    .append("circle")
    .attr("fill", (d) => {
      return "#fff";
    })
    .attr("r", 16);

  nodeEnter
    .append("text")
    .attr("dx", 32)
    .attr("dy", ".35em")
    .text(function(d) {
      return d.data.name;
    });

  nodeEnter.on("click", onNodeClicked);
  node = nodeEnter.merge(node);

  // Set up links
  link = gLinks.selectAll(".link").data(links, function(d) {
    return d.target.id;
  });
  link.exit().remove();
  const linkEnter = link.enter().append("line").attr("class", "link");
  link = linkEnter.merge(link);

  simulation.nodes(nodes);
  simulation.force("link").links(links);
}

function drag(simulation) {
  function dragstarted(event) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    event.subject.fx = event.subject.x;
    event.subject.fy = event.subject.y;

    event.fx = event.subject.x;
    event.fy = event.subject.y;
  }

  function dragged(event) {
    event.subject.fx = event.x;
    event.subject.fy = event.y;
    event.fx = event.x;
    event.fy = event.y;
    fix_other_nodes(event.subject);
  }

  function dragended(event) {
    if (!event.active) simulation.alphaTarget(0);
    event.subject.fx = event.x;
    event.subject.fy = event.y;

  }

  function fix_other_nodes(this_node) {
    node.each(function(d) {
      if (this_node != d) {
        d.fx = d.x;
        d.fy = d.y;
      }
    });
  }

  return d3
    .drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended);
}

function onNodeClicked(event, d) {
  // Show details drawer
  // setIsDrawerOpen(true);
  if (d._children) {
    d.children = d._children;
    d._children = null;
  } else {
    d._children = d.children;
    d.children = null;
  }

  if (d.children) {
    d.children.forEach((n) => {
      n.fx = d.x;
      n.fy = d.y;

      // Needed to make the children radiate from the parent upon expanding.
      n.needToUnfix = true;
    });
  }

  update();

  simulation.restart();
}

update();

simulation.on("tick", () => {
  link
    .attr("x1", (d) => d.source.x)
    .attr("y1", (d) => d.source.y)
    .attr("x2", (d) => d.target.x)
    .attr("y2", (d) => d.target.y);

  node.attr("transform", function(d) {
    if (d.needToUnfix) {
      // This is used to make the expanding children radiate from the parent.
      d.x = d.fx;
      d.y = d.fy;

      d.fx = null;
      d.fy = null;
      d.needToUnfix = false;
      return "translate(" + d.x + "," + d.y + ")";
    }
    return "translate(" + d.x + "," + d.y + ")";
  });
});
/************************/

html,
body {
  height: 100%;
  background-color: #eee;
}

#root {
  height: 100%;
}

.App {
  height: 100%;
}

#svgcontainer {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.node {
  filter: drop-shadow(0 4px 4px rgb(0 0 0 / 0.4));
}

.node text {
  font: 12px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 2px;
}

.resource-icon {
  transition: all 200ms;
}

.box-details {
  opacity: 0;
  transition: all 200ms;
}

.box-details text {
  font: 8px sans-serif;
}

.reset-zoom-btn {
  opacity: 0;
  transition: all 200ms;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js" integrity="sha512-MefNfAGJ/pEy89xLOFs3V6pYPs6AmUhXJrRlydI/9wZuGrqxmrdQ80zKHUcyadAcpH67teDZcBeS6oMJLPtTqw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<div className="App">
  <div id="svgcontainer"></div>
</div>

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文