如何让所有节点围绕中心节点?

时间:2017-11-08 18:53:50

标签: javascript d3.js d3-force-directed

我正在尝试制作一个力导向图,其中子孙节点正在绕着父母盘旋/轨道运行。同时,父节点连接到其子节点,并且每个子节点都连接到它们的每个孙子节点。

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

enter image description here

我已经尝试使用默认的力导向图(herethere)进行干预,但似乎没有办法像圆圈/轨道那样整齐地对它们进行排序。试图制作。

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

这是我的小提琴和代码: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 个答案:

答案 0 :(得分:3)

The new d3.forceRadial()

What you need is d3.forceRadial, introduced in D3 v4.11. According to the API, d3.forceRadial(radius[, x][, y]) will...

Create a new positioning force towards a circle of the specified radius centered at ⟨x,y⟩.

In your case, I'm using level to set the radius:

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

Things are easier when you have only nodes. However, since you have links in that force, you'll have to tweak the link force until you get the desired result.

Here is your code with 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>

As I said, because you have links, things are a bit more complicated. Look how d3.forceRadial creates a nice radial pattern if you had only nodes (here, together with 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>

PS: I set the level of the first node to 0.

答案 1 :(得分: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/