如何使所有节点围绕中心节点成圆形?

4

我正在尝试制作一个力导向图,其中子节点和孙子节点围绕/轨道父节点。同时,父节点连接到其子节点,每个子节点都连接到其各自的孙子节点。

从视觉上看,它应该类似于这样:

enter image description here

我尝试过修改默认的力导向图(这里那里),但似乎没有办法将它们整齐地排成圆形/轨道,就像我想要的可视化效果一样。
我尝试查找轨道代码,但似乎需要完全不同的方法。
这是我的fiddle和代码:https://jsfiddle.net/znqkcLhs/

function getNeighbors(node) {
  return links.reduce(function (neighbors, link) {
      if (link.target.id === node.id) {
        neighbors.push(link.source.id)
      } else if (link.source.id === node.id) {
        neighbors.push(link.target.id)
      }
      return neighbors
    },
    [node.id]
  )
}

function isNeighborLink(node, link) {
  return link.target.id === node.id || link.source.id === node.id
}


function getNodeColor(node, neighbors) {
  if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
    return node.level === 1 ? 'blue' : 'green'
  }

  return node.level === 1 ? 'red' : 'gray'
}


function getLinkColor(node, link) {
  return isNeighborLink(node, link) ? 'green' : '#E5E5E5'
}

function getTextColor(node, neighbors) {
  return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? 'green' : 'black'
}

var width = window.innerWidth
var height = window.innerHeight

var svg = d3.select('svg')
svg.attr('width', width).attr('height', height)

// simulation setup with all forces
var linkForce = d3
  .forceLink()
  .id(function (link) { return link.id })
  .strength(function (link) { return link.strength })

var simulation = d3
  .forceSimulation()
  .force('link', linkForce)
  .force('charge', d3.forceManyBody().strength(-120))
  .force('center', d3.forceCenter(width / 2, height / 2))

var dragDrop = d3.drag().on('start', function (node) {
  node.fx = node.x
  node.fy = node.y
}).on('drag', function (node) {
  simulation.alphaTarget(0.7).restart()
  node.fx = d3.event.x
  node.fy = d3.event.y
}).on('end', function (node) {
  if (!d3.event.active) {
    simulation.alphaTarget(0)
  }
  node.fx = null
  node.fy = null
})

function selectNode(selectedNode) {
  var neighbors = getNeighbors(selectedNode)

  // we modify the styles to highlight selected nodes
  nodeElements.attr('fill', function (node) { return getNodeColor(node, neighbors) })
  textElements.attr('fill', function (node) { return getTextColor(node, neighbors) })
  linkElements.attr('stroke', function (link) { return getLinkColor(selectedNode, link) })
}

var linkElements = svg.append("g")
  .attr("class", "links")
  .selectAll("line")
  .data(links)
  .enter().append("line")
    .attr("stroke-width", function(link) { return link.value; })
   .attr("stroke", "rgba(50, 50, 50, 0.2)")

var nodeElements = svg.append("g")
  .attr("class", "nodes")
  .selectAll("circle")
  .data(nodes)
  .enter().append("circle")
    .attr("r", 10)
    .attr("fill", getNodeColor)
    .call(dragDrop)
    .on('click', selectNode)

var textElements = svg.append("g")
  .attr("class", "texts")
  .selectAll("text")
  .data(nodes)
  .enter().append("text")
    .text(function (node) { return  node.label })
   .attr("font-size", 15)
   .attr("dx", 15)
    .attr("dy", 4)

simulation.nodes(nodes).on('tick', () => {
  nodeElements
    .attr('cx', function (node) { return node.x })
    .attr('cy', function (node) { return node.y })
  textElements
    .attr('x', function (node) { return node.x })
    .attr('y', function (node) { return node.y })
  linkElements
    .attr('x1', function (link) { return link.source.x })
    .attr('y1', function (link) { return link.source.y })
    .attr('x2', function (link) { return link.target.x })
    .attr('y2', function (link) { return link.target.y })
})

simulation.force("link").links(links)

任何想法?
2个回答

3

新的d3.forceRadial()

您需要使用D3 v4.11中引入的d3.forceRadial。根据APId3.forceRadial(radius[, x][, y])可以...

创建一个新的定位力,朝向以⟨x,y⟩为中心、指定半径的圆。

在您的情况下,我使用level来设置半径:

.force('radial', d3.forceRadial(function(d) {
    return d.level * 50
}, width / 2, height / 2))

如果只有节点,事情会变得更容易。但是,由于力图中有链接,您需要调整链接力度,直到获得所需的结果。

以下是使用d3.forceRadial的代码:

var nodes = [{
  id: "pusat",
  group: 0,
  label: "pusat",
  level: 0
}, {
  id: "dki",
  group: 1,
  label: "dki",
  level: 1
}, {
  id: "jaksel",
  group: 1,
  label: "jaksel",
  level: 3
}, {
  id: "jakpus",
  group: 1,
  label: "jakpus",
  level: 3
}, {
  id: "jabar",
  group: 2,
  label: "jabar",
  level: 1
}, {
  id: "sumedang",
  group: 2,
  label: "sumedang",
  level: 3
}, {
  id: "bekasi",
  group: 2,
  label: "bekasi",
  level: 3
}, {
  id: "bandung",
  group: 2,
  label: "bandung",
  level: 3
}, {
  id: "jatim",
  group: 3,
  label: "jatim",
  level: 1
}, {
  id: "malang",
  group: 3,
  label: "malang",
  level: 3
}, {
  id: "lamongan",
  group: 3,
  label: "lamongan",
  level: 3
}, {
  id: "diy",
  group: 4,
  label: "diy",
  level: 1
}, {
  id: "sleman",
  group: 4,
  label: "sleman",
  level: 3
}, {
  id: "jogja",
  group: 4,
  label: "jogja",
  level: 3
}, {
  id: "bali",
  group: 5,
  label: "bali",
  level: 1
}, {
  id: "bali1",
  group: 5,
  label: "bali1",
  level: 3
}, {
  id: "bali2",
  group: 5,
  label: "bali2",
  level: 3
}, {
  id: "ntt",
  group: 6,
  label: "ntt",
  level: 1
}, {
  id: "ntt1",
  group: 6,
  label: "ntt1",
  level: 3
}, {
  id: "ntt2",
  group: 6,
  label: "ntt2",
  level: 3
}]

var links = [{
    target: "pusat",
    source: "dki",
    strength: 0.2,
    value: 1
  }, {
    target: "pusat",
    source: "jabar",
    strength: 0.2,
    value: 3
  }, {
    target: "pusat",
    source: "jatim",
    strength: 0.2,
    value: 6
  }, {
    target: "pusat",
    source: "diy",
    strength: 0.2,
    value: 1
  }, {
    target: "pusat",
    source: "bali",
    strength: 0.2,
    value: 1
  }, {
    target: "pusat",
    source: "ntt",
    strength: 0.2,
    value: 1
  },

  //{ target: "pusat", source: "malang" , strength: 0.2, value:3 },
  //{ target: "pusat", source: "lamongan" , strength: 0.2, value:6 },

  {
    target: "dki",
    source: "jaksel",
    strength: 0.7,
    value: 2
  }, {
    target: "dki",
    source: "jakpus",
    strength: 0.7,
    value: 3
  }, {
    target: "jabar",
    source: "sumedang",
    strength: 0.7,
    value: 0.5
  }, {
    target: "jabar",
    source: "bekasi",
    strength: 0.7,
    value: 2
  }, {
    target: "jabar",
    source: "bandung",
    strength: 0.7,
    value: 2
  }, {
    target: "jatim",
    source: "malang",
    strength: 0.7,
    value: 3
  }, {
    target: "jatim",
    source: "lamongan",
    strength: 0.7,
    value: 1
  }, {
    target: "diy",
    source: "sleman",
    strength: 0.7,
    value: 3
  }, {
    target: "diy",
    source: "jogja",
    strength: 0.7,
    value: 1
  }, {
    target: "bali",
    source: "bali1",
    strength: 0.7,
    value: 1
  }, {
    target: "bali",
    source: "bali2",
    strength: 0.7,
    value: 1
  }, {
    target: "ntt",
    source: "ntt1",
    strength: 0.7,
    value: 1
  }, {
    target: "ntt",
    source: "ntt2",
    strength: 0.7,
    value: 1
  }
]

function getNeighbors(node) {
  return links.reduce(function(neighbors, link) {
    if (link.target.id === node.id) {
      neighbors.push(link.source.id)
    } else if (link.source.id === node.id) {
      neighbors.push(link.target.id)
    }
    return neighbors
  }, [node.id])
}

function isNeighborLink(node, link) {
  return link.target.id === node.id || link.source.id === node.id
}


function getNodeColor(node, neighbors) {
  if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
    return node.level === 1 ? 'blue' : 'green'
  }

  return node.level === 1 ? 'red' : 'gray'
}


function getLinkColor(node, link) {
  return isNeighborLink(node, link) ? 'green' : '#E5E5E5'
}

function getTextColor(node, neighbors) {
  return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? 'green' : 'black'
}

var width = window.innerWidth
var height = window.innerHeight

var svg = d3.select('svg')
svg.attr('width', width).attr('height', height);

var circles = svg.selectAll(null)
  .data([80,125])
  .enter()
  .append("circle")
  .attr("cx", width/2)
  .attr("cy", height/2)
  .attr("r", d=>d)
  .style("fill", "none")
  .style("stroke", "#ccc");

// simulation setup with all forces
var linkForce = d3
  .forceLink()
  .id(function(link) {
    return link.id 
  });

var simulation = d3
  .forceSimulation()
  .force('link', linkForce)
  .force('charge', d3.forceManyBody().strength(-120))
  .force('radial', d3.forceRadial(function(d) {
    return d.level * 50
  }, width / 2, height / 2))
  .force('center', d3.forceCenter(width / 2, height / 2))

var dragDrop = d3.drag().on('start', function(node) {
  node.fx = node.x
  node.fy = node.y
}).on('drag', function(node) {
  simulation.alphaTarget(0.7).restart()
  node.fx = d3.event.x
  node.fy = d3.event.y
}).on('end', function(node) {
  if (!d3.event.active) {
    simulation.alphaTarget(0)
  }
  node.fx = null
  node.fy = null
})

function selectNode(selectedNode) {
  var neighbors = getNeighbors(selectedNode)

  // we modify the styles to highlight selected nodes
  nodeElements.attr('fill', function(node) {
    return getNodeColor(node, neighbors) 
  })
  textElements.attr('fill', function(node) {
    return getTextColor(node, neighbors)
  })
  linkElements.attr('stroke', function(link) {
    return getLinkColor(selectedNode, link)
  })
}

var linkElements = svg.append("g")
  .attr("class", "links")
  .selectAll("line")
  .data(links)
  .enter().append("line")
  .attr("stroke-width", function(link) {
    return link.value;
  })
  .attr("stroke", "rgba(50, 50, 50, 0.2)")

var nodeElements = svg.append("g")
  .attr("class", "nodes")
  .selectAll("circle")
  .data(nodes)
  .enter().append("circle")
  .attr("r", 10)
  .attr("fill", getNodeColor)
  .call(dragDrop)
  .on('click', selectNode)

var textElements = svg.append("g")
  .attr("class", "texts")
  .selectAll("text")
  .data(nodes)
  .enter().append("text")
  .text(function(node) {
    return node.label
  })
  .attr("font-size", 15)
  .attr("dx", 15)
  .attr("dy", 4)

simulation.nodes(nodes).on('tick', () => {
  nodeElements
    .attr('cx', function(node) {
      return node.x
    })
    .attr('cy', function(node) {
      return node.y
    })
  textElements
    .attr('x', function(node) {
      return node.x
    })
    .attr('y', function(node) {
      return node.y
    })
  linkElements
    .attr('x1', function(link) {
      return link.source.x
    })
    .attr('y1', function(link) {
      return link.source.y
    })
    .attr('x2', function(link) {
      return link.target.x
    })
    .attr('y2', function(link) {
      return link.target.y
    })
})

simulation.force("link").links(links)
<svg width="960" height="600">
</svg>

<script src="https://d3js.org/d3.v4.min.js"></script>

正如我所说的,因为你有链接,事情变得更加复杂。观察 d3.forceRadial 如何创建一个漂亮的径向图案,如果你只有节点的话(这里还有 d3.forceCollide):

var nodes = [{
  id: "pusat",
  group: 0,
  label: "pusat",
  level: 0
}, {
  id: "dki",
  group: 1,
  label: "dki",
  level: 1
}, {
  id: "jaksel",
  group: 1,
  label: "jaksel",
  level: 3
}, {
  id: "jakpus",
  group: 1,
  label: "jakpus",
  level: 3
}, {
  id: "jabar",
  group: 2,
  label: "jabar",
  level: 1
}, {
  id: "sumedang",
  group: 2,
  label: "sumedang",
  level: 3
}, {
  id: "bekasi",
  group: 2,
  label: "bekasi",
  level: 3
}, {
  id: "bandung",
  group: 2,
  label: "bandung",
  level: 3
}, {
  id: "jatim",
  group: 3,
  label: "jatim",
  level: 1
}, {
  id: "malang",
  group: 3,
  label: "malang",
  level: 3
}, {
  id: "lamongan",
  group: 3,
  label: "lamongan",
  level: 3
}, {
  id: "diy",
  group: 4,
  label: "diy",
  level: 1
}, {
  id: "sleman",
  group: 4,
  label: "sleman",
  level: 3
}, {
  id: "jogja",
  group: 4,
  label: "jogja",
  level: 3
}, {
  id: "bali",
  group: 5,
  label: "bali",
  level: 1
}, {
  id: "bali1",
  group: 5,
  label: "bali1",
  level: 3
}, {
  id: "bali2",
  group: 5,
  label: "bali2",
  level: 3
}, {
  id: "ntt",
  group: 6,
  label: "ntt",
  level: 1
}, {
  id: "ntt1",
  group: 6,
  label: "ntt1",
  level: 3
}, {
  id: "ntt2",
  group: 6,
  label: "ntt2",
  level: 3
}]

var links = [{
    target: "pusat",
    source: "dki",
    strength: 0.2,
    value: 1
  }, {
    target: "pusat",
    source: "jabar",
    strength: 0.2,
    value: 3
  }, {
    target: "pusat",
    source: "jatim",
    strength: 0.2,
    value: 6
  }, {
    target: "pusat",
    source: "diy",
    strength: 0.2,
    value: 1
  }, {
    target: "pusat",
    source: "bali",
    strength: 0.2,
    value: 1
  }, {
    target: "pusat",
    source: "ntt",
    strength: 0.2,
    value: 1
  },

  //{ target: "pusat", source: "malang" , strength: 0.2, value:3 },
  //{ target: "pusat", source: "lamongan" , strength: 0.2, value:6 },

  {
    target: "dki",
    source: "jaksel",
    strength: 0.7,
    value: 2
  }, {
    target: "dki",
    source: "jakpus",
    strength: 0.7,
    value: 3
  }, {
    target: "jabar",
    source: "sumedang",
    strength: 0.7,
    value: 0.5
  }, {
    target: "jabar",
    source: "bekasi",
    strength: 0.7,
    value: 2
  }, {
    target: "jabar",
    source: "bandung",
    strength: 0.7,
    value: 2
  }, {
    target: "jatim",
    source: "malang",
    strength: 0.7,
    value: 3
  }, {
    target: "jatim",
    source: "lamongan",
    strength: 0.7,
    value: 1
  }, {
    target: "diy",
    source: "sleman",
    strength: 0.7,
    value: 3
  }, {
    target: "diy",
    source: "jogja",
    strength: 0.7,
    value: 1
  }, {
    target: "bali",
    source: "bali1",
    strength: 0.7,
    value: 1
  }, {
    target: "bali",
    source: "bali2",
    strength: 0.7,
    value: 1
  }, {
    target: "ntt",
    source: "ntt1",
    strength: 0.7,
    value: 1
  }, {
    target: "ntt",
    source: "ntt2",
    strength: 0.7,
    value: 1
  }
]

function getNeighbors(node) {
  return links.reduce(function(neighbors, link) {
    if (link.target.id === node.id) {
      neighbors.push(link.source.id)
    } else if (link.source.id === node.id) {
      neighbors.push(link.target.id)
    }
    return neighbors
  }, [node.id])
}

function isNeighborLink(node, link) {
  return link.target.id === node.id || link.source.id === node.id
}


function getNodeColor(node, neighbors) {
  if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
    return node.level === 1 ? 'blue' : 'green'
  }

  return node.level === 1 ? 'red' : 'gray'
}


function getLinkColor(node, link) {
  return isNeighborLink(node, link) ? 'green' : '#E5E5E5'
}

function getTextColor(node, neighbors) {
  return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? 'green' : 'black'
}

var width = window.innerWidth
var height = window.innerHeight

var svg = d3.select('svg')
  .attr('width', width).attr('height', height)
  .append("g")
  .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")

// simulation setup with all forces


var simulation = d3.forceSimulation()
  .force('radial', d3.forceRadial(function(d) {
    return d.level * 50
  }).strength(1))
  .force('collide', d3.forceCollide().radius(35));

var dragDrop = d3.drag().on('start', function(node) {
  node.fx = node.x
  node.fy = node.y
}).on('drag', function(node) {
  simulation.alphaTarget(0.7).restart()
  node.fx = d3.event.x
  node.fy = d3.event.y
}).on('end', function(node) {
  if (!d3.event.active) {
    simulation.alphaTarget(0)
  }
  node.fx = null
  node.fy = null
})

function selectNode(selectedNode) {
  var neighbors = getNeighbors(selectedNode)

  // we modify the styles to highlight selected nodes
  nodeElements.attr('fill', function(node) {
    return getNodeColor(node, neighbors)
  })
  textElements.attr('fill', function(node) {
    return getTextColor(node, neighbors)
  })
  linkElements.attr('stroke', function(link) {
    return getLinkColor(selectedNode, link)
  })
}

var linkElements = svg.append("g")
  .attr("class", "links")
  .selectAll("line")
  .data(links)
  .enter().append("line")
  .attr("stroke-width", function(link) {
    return link.value;
  })
  .attr("stroke", "rgba(50, 50, 50, 0.2)")

var nodeElements = svg.append("g")
  .attr("class", "nodes")
  .selectAll("circle")
  .data(nodes)
  .enter().append("circle")
  .attr("r", 10)
  .attr("fill", getNodeColor)
  .call(dragDrop)
  .on('click', selectNode)

var textElements = svg.append("g")
  .attr("class", "texts")
  .selectAll("text")
  .data(nodes)
  .enter().append("text")
  .text(function(node) {
    return node.label
  })
  .attr("font-size", 15)
  .attr("dx", 15)
  .attr("dy", 4)

simulation.nodes(nodes).on('tick', () => {
  nodeElements
    .attr('cx', function(node) {
      return node.x
    })
    .attr('cy', function(node) {
      return node.y
    })
  textElements
    .attr('x', function(node) {
      return node.x
    })
    .attr('y', function(node) {
      return node.y
    })

})
<svg width="600" height="500">
</svg>

<script src="https://d3js.org/d3.v4.min.js"></script>

提示:我将第一个节点的级别设置为0


非常感谢,这似乎是最好的解决方案。不过确保节点围绕中心旋转有点困难。是否有办法添加一些视觉提示,例如在d3 orbit中的背景圆? - deathlock
1
@deathlock 请再检查第一个代码片段。 - Gerardo Furtado

2

您可以使用forceLink().distance设置固定的链接长度,并增加forceManyBody().strength来获得类似的结果,例如:

var linkForce = d3
  .forceLink()
  .id(function (link) { return link.id })
  .distance(50)
  .strength(1)

var simulation = d3
  .forceSimulation()
  .force('link', linkForce)
  .force('charge', d3.forceManyBody().strength(-1000))
  .force('center', d3.forceCenter(width / 2, height / 2))

这是一个更新的代码片段

https://jsfiddle.net/znqkcLhs/1/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接