javascript - 如何让所有的节点都围绕中心节点?

标签 javascript d3.js d3-force-directed

我正在尝试制作一个力导向图,其中子节点和孙节点绕着父节点旋转/绕行。同时父节点连接到它的子节点,每个子节点连接到他们的每个孙节点。

从视觉上看,它看起来像这样:

enter image description here

我试过干预默认的力定向图(herethere ),但似乎没有办法像我试图制作的视觉效果那样将它们整齐地排列在圆圈/轨道上。

我尝试查找 orbit code , 但它似乎需要一种完全不同的方法。

这是我的 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)

有什么想法吗?

最佳答案

新的 d3.forceRadial()

您需要的是 d3.forceRadial,在 D3 v4.11 中引入。根据API , d3.forceRadial(radius[, x][, y]) 将...

Create a new positioning force towards a circle of the specified radius centered at ⟨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.forceCollide 一起),看看 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')
  .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>

PS:我把第一个节点的level设置为0

关于javascript - 如何让所有的节点都围绕中心节点?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47187515/

相关文章:

javascript - D3.js 分组条形图多次绘制数据

javascript - 单击和双击 d3 力定向图中的节点

javascript - 复选框结构,如果选中单个子复选框,则应自动选中父复选框

javascript - 寻找嵌入式报告解决方案

javascript - 通过滚动将表格行粘贴到浏览器顶部

javascript - d3.js 图中的搜索功能

javascript - 将数据传递给 D3 函数 - Ext JS 4.2.2

javascript - 如何限制 d3 力有向图中最初的节点数量

javascript - 解释 Mike Bostock 节点解析循环

javascript - 迭代一个数组并只返回匹配的值?