How to make all the nodes circle the center node?

I'm trying to make a force directed graph where the children and grandchildren nodes are circling/orbiting the parent. Meanwhile the parent node is connected to its children nodes, and each children nodes are connected to each of their grandchildren.

Visually, it would look something like this:

enter image description here

I've tried meddling with the default force directed graph (both here and there) but it seems like there is no way to order them neatly in circle/orbit like the visual I'm trying to make.

I tried looking up the orbit code, but it seems like it requires a completely different approach.

Here is my fiddle and 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)

Any ideas?

Answers:

Answer

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.

Answer

You will get something close by using forceLink().distance to set a fixed link length, and increasing the forceManyBody().strength, for example:

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))

Here's an updated fiddle

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

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.