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:
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?
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
.