You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

D3力导向图中如何在tick函数中根据节点组设置对应属性?

D3力导向图中如何在tick函数中根据节点组设置对应属性?

嘿,我明白你遇到的困扰了——不同形状的节点需要不同的位置属性,直接链式调用attr('cx', ...)对矩形完全不起作用,而且尝试用each的时候还摸不准怎么获取到当前DOM节点对吧?别着急,咱们一步步来解决:

问题根源

首先你要知道:SVG的<rect>元素是用xy属性定义左上角位置的,而<circle>用的是cxcy定义中心位置;另外你之前在each里如果用了箭头函数this会绑定到外部的上下文,而不是当前的DOM节点,这就是为什么你调用this.setAttribute没效果的原因。

解决方案1:用each结合普通函数处理不同节点

each里的箭头函数改成普通函数,这样this就会指向当前的DOM节点,然后根据数据里的group判断节点类型,设置对应的属性:

function ticked() {
  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)

  // 改用普通函数,this指向当前DOM节点
  node.each(function(d) {
    if (d.group === 1) {
      // 矩形要居中,所以x/y要减去宽高的一半(16/2=8)
      d3.select(this)
        .attr('x', d.x - 8)
        .attr('y', d.y - 8);
    } else {
      d3.select(this)
        .attr('cx', d.x)
        .attr('cy', d.y);
    }
  });
}

解决方案2:用transform统一设置位置(更简洁)

其实还有个更省心的方法:不管是圆还是矩形,都用transformtranslate来设置位置,这样不用区分节点类型。只需要在创建节点的时候,把矩形的初始位置设为左上角在原点(x=-8y=-8),圆形保持默认,这样translate之后所有节点的中心都会对齐到力导向计算的x/y坐标:

修改节点创建代码:

const node = svg
  .append('g')
  .attr('stroke', '#fff')
  .attr('stroke-width', 1.5)
  .selectAll()
  .data(nodes)
  .join((enter) => {
    return enter.append((d) => {
      if (d.group === 1) {
        const rectElement = document.createElementNS(svgNS, 'rect')
        rectElement.setAttribute('width', 16)
        rectElement.setAttribute('height', 16)
        rectElement.setAttribute('fill', color(d.group))
        // 提前把矩形的左上角移到原点,方便后续translate居中
        rectElement.setAttribute('x', -8)
        rectElement.setAttribute('y', -8)
        return rectElement
      } else {
        const circleElement = document.createElementNS(svgNS, 'circle')
        circleElement.setAttribute('r', 16)
        circleElement.setAttribute('fill', color(d.group))
        return circleElement
      }
    })
  })

然后ticked函数就变得超级简单:

function ticked() {
  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)

  // 统一设置transform,不用区分节点类型
  node.attr('transform', (d) => `translate(${d.x}, ${d.y})`)
}

这个方法更符合D3的风格,代码也更简洁易维护~

完整修改后的代码

<style>
.graph {
  width: 1000px;
  height: 400px;
}
</style>
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg id="chart" class="graph"></svg>
<script>
const width = 1000
const height = 400
const svgNS = d3.namespace('svg:text').space

const node_data = Array.from({ length: 5 }, () => ({
  group: Math.floor(Math.random() * 3),
}))

const edge_data = Array.from({ length: 10 }, () => ({
  source: Math.floor(Math.random() * 5),
  target: Math.floor(Math.random() * 5),
  value: Math.floor(Math.random() * 10) + 1,
}))

const links = edge_data.map((d) => ({ ...d }))
const nodes = node_data.map((d, index) => ({ id: index, ...d }))

const color = d3.scaleOrdinal(d3.schemeCategory10)
const svg = d3.select('#chart')

const simulation = d3
  .forceSimulation(nodes)
  .force(
    'link',
    d3
      .forceLink(links)
      .id((d) => d.id)
      .distance((d) => 100)
  )
  .force('charge', d3.forceManyBody())
  .force('center', d3.forceCenter(width / 2, height / 2))
  .on('tick', ticked)

const link = svg
  .append('g')
  .attr('stroke', '#999')
  .attr('stroke-opacity', 0.6)
  .selectAll()
  .data(links)
  .join('line')
  .attr('stroke-width', (d) => Math.sqrt(d.value))

const node = svg
  .append('g')
  .attr('stroke', '#fff')
  .attr('stroke-width', 1.5)
  .selectAll()
  .data(nodes)
  .join((enter) => {
    return enter.append((d) => {
      if (d.group === 1) {
        const rectElement = document.createElementNS(svgNS, 'rect')
        rectElement.setAttribute('width', 16)
        rectElement.setAttribute('height', 16)
        rectElement.setAttribute('fill', color(d.group))
        rectElement.setAttribute('x', -8)
        rectElement.setAttribute('y', -8)
        return rectElement
      } else {
        const circleElement = document.createElementNS(svgNS, 'circle')
        circleElement.setAttribute('r', 16)
        circleElement.setAttribute('fill', color(d.group))
        return circleElement
      }
    })
  })

function ticked() {
  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', (d) => `translate(${d.x}, ${d.y})`)
}
</script>

备注:内容来源于stack exchange,提问作者Eric G

火山引擎 最新活动