Center text over curved links in a force-directed graph - javascript

This is a 2nd question that builds off of this a previous question of mine here - D3 Force Graph With Arrows and Curved Edges - shorten links so arrow doesnt overlap nodes - on how to shorten curved links for a d3 force graph.
My latest struggle involves centering the text placed on top of the links, actually over the links. Here is a reproducible example showing my issue (apologies for the long code. a lot was needed to create a reproducible example, although I am only working on a small bit of it currently):
const svg = d3.select('#mySVG')
const nodesG = svg.select("g.nodes")
const linksG = svg.select("g.links")
var graphs = {
"nodes": [{
"name": "Peter",
"label": "Person",
"id": 1
},
{
"name": "Michael",
"label": "Person",
"id": 2
},
{
"name": "Neo4j",
"label": "Database",
"id": 3
},
{
"name": "Graph Database",
"label": "Database",
"id": 4
}
],
"links": [{
"source": 1,
"target": 2,
"type": "KNOWS",
"since": 2010
},
{
"source": 1,
"target": 3,
"type": "FOUNDED"
},
{
"source": 2,
"target": 3,
"type": "WORKS_ON"
},
{
"source": 3,
"target": 4,
"type": "IS_A"
}
]
}
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 0)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 13)
.attr('markerHeight', 13)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(100, 100));
let linksData = graphs.links.map(link => {
var obj = link;
obj.source = link.source;
obj.target = link.target;
return obj;
})
const links = linksG
.selectAll("g")
.data(graphs.links)
.enter().append("g")
.attr("cursor", "pointer")
const linkLines = links
.append("path")
.attr('stroke', '#000000')
.attr('opacity', 0.75)
.attr("stroke-width", 1)
.attr("fill", "transparent")
.attr('marker-end', 'url(#arrowhead)');
const linkText = links
.append("text")
.attr("x", d => (d.source.x + (d.target.x - d.source.x) * 0.5))
.attr("y", d => (d.source.y + (d.target.y - d.source.y) * 0.5))
.attr('stroke', '#000000')
.attr("text-anchor", "middle")
.attr('opacity', 1)
.text((d,i) => `${i}`);
const nodes = nodesG
.selectAll("g")
.data(graphs.nodes)
.enter().append("g")
.attr("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const circles = nodes.append("circle")
.attr("r", 12)
.attr("fill", "000000")
nodes.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graphs.nodes)
.on("tick", ticked);
simulation.force("link", d3.forceLink().links(linksData)
.id((d, i) => d.id)
.distance(150));
function ticked() {
linkLines.attr("d", function(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
// recalculate and back off the distance
linkLines.attr("d", function(d) {
// length of current path
var pl = this.getTotalLength(),
// radius of circle plus backoff
r = (12) + 30,
// position close to where path intercepts circle
m = this.getPointAtLength(pl - r);
var dx = m.x - d.source.x,
dy = m.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
});
linkText
.attr("x", function(d) { return (d.source.x + (d.target.x - d.source.x) * 0.5); })
.attr("y", function(d) { return (d.source.y + (d.target.y - d.source.y) * 0.5); })
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="//d3js.org/d3.v4.min.js" type="text/javascript"></script>
</head>
<body>
<svg id="mySVG" width="500" height="500">
<g class="links" />
<g class="nodes" />
</svg>
I know that the issue with my code is with the setting of the x and y values for the linkText here:
linkText
.attr("x", function(d) { return (d.source.x + (d.target.x - d.source.x) * 0.5); })
.attr("y", function(d) { return (d.source.y + (d.target.y - d.source.y) * 0.5); })
...and also earlier in my code. I am not sure how to update these functions to account for the fact that the links are curved lines (not straight lines from node to node).
The larger force graph for my project has many more links and nodes, and positioning of the text over the center of the curved linkLines would be preferable.
Any help with this is appreciated!

There are several different ways to fix this. The two most obvious ones are:
Using getPointAtLength() to get the middle of the <path>, and positioning the texts there;
Using a <textPath> element.
In my solution I'll choose #2 mainly because, using a text path, the numbers can flip according the paths' orientation, some of them ending up upside down (I'm assuming that this is what you want).
So, we append the textPaths...
const linkText = links
.append("text")
.attr("dy", -4)
.append("textPath")
.attr("xlink:href", function(_, i) {
return "#path" + i
})
.attr("startOffset", "50%")
.text((d, i) => `${i}`);
... having given the paths unique ids:
.attr("id", function(_, i) {
return "path" + i
})
This is the code with those changes:
const svg = d3.select('#mySVG')
const nodesG = svg.select("g.nodes")
const linksG = svg.select("g.links")
var graphs = {
"nodes": [{
"name": "Peter",
"label": "Person",
"id": 1
},
{
"name": "Michael",
"label": "Person",
"id": 2
},
{
"name": "Neo4j",
"label": "Database",
"id": 3
},
{
"name": "Graph Database",
"label": "Database",
"id": 4
}
],
"links": [{
"source": 1,
"target": 2,
"type": "KNOWS",
"since": 2010
},
{
"source": 1,
"target": 3,
"type": "FOUNDED"
},
{
"source": 2,
"target": 3,
"type": "WORKS_ON"
},
{
"source": 3,
"target": 4,
"type": "IS_A"
}
]
}
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 0)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 13)
.attr('markerHeight', 13)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(100, 100));
let linksData = graphs.links.map(link => {
var obj = link;
obj.source = link.source;
obj.target = link.target;
return obj;
})
const links = linksG
.selectAll("g")
.data(graphs.links)
.enter().append("g")
.attr("cursor", "pointer")
const linkLines = links
.append("path")
.attr("id", function(_, i) {
return "path" + i
})
.attr('stroke', '#000000')
.attr('opacity', 0.75)
.attr("stroke-width", 1)
.attr("fill", "transparent")
.attr('marker-end', 'url(#arrowhead)');
const linkText = links
.append("text")
.attr("dy", -4)
.append("textPath")
.attr("xlink:href", function(_, i) {
return "#path" + i
})
.attr("startOffset", "50%")
.attr('stroke', '#000000')
.attr('opacity', 1)
.text((d, i) => `${i}`);
const nodes = nodesG
.selectAll("g")
.data(graphs.nodes)
.enter().append("g")
.attr("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const circles = nodes.append("circle")
.attr("r", 12)
.attr("fill", "000000")
nodes.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graphs.nodes)
.on("tick", ticked);
simulation.force("link", d3.forceLink().links(linksData)
.id((d, i) => d.id)
.distance(150));
function ticked() {
linkLines.attr("d", function(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
// recalculate and back off the distance
linkLines.attr("d", function(d) {
// length of current path
var pl = this.getTotalLength(),
// radius of circle plus backoff
r = (12) + 30,
// position close to where path intercepts circle
m = this.getPointAtLength(pl - r);
var dx = m.x - d.source.x,
dy = m.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
});
linkText
.attr("x", function(d) {
return (d.source.x + (d.target.x - d.source.x) * 0.5);
})
.attr("y", function(d) {
return (d.source.y + (d.target.y - d.source.y) * 0.5);
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="//d3js.org/d3.v4.min.js" type="text/javascript"></script>
</head>
<body>
<svg id="mySVG" width="500" height="500">
<g class="links" />
<g class="nodes" />
</svg>
On the other hand, if you don't want some of the texts upside down, use getPointAtLength() to get the middle of the path, which is the approach #1:
.attr("x", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length/2).x
})
.attr("y", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length/2).y
})
Here is the demo:
const svg = d3.select('#mySVG')
const nodesG = svg.select("g.nodes")
const linksG = svg.select("g.links")
var graphs = {
"nodes": [{
"name": "Peter",
"label": "Person",
"id": 1
},
{
"name": "Michael",
"label": "Person",
"id": 2
},
{
"name": "Neo4j",
"label": "Database",
"id": 3
},
{
"name": "Graph Database",
"label": "Database",
"id": 4
}
],
"links": [{
"source": 1,
"target": 2,
"type": "KNOWS",
"since": 2010
},
{
"source": 1,
"target": 3,
"type": "FOUNDED"
},
{
"source": 2,
"target": 3,
"type": "WORKS_ON"
},
{
"source": 3,
"target": 4,
"type": "IS_A"
}
]
}
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 0)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 13)
.attr('markerHeight', 13)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(100, 100));
let linksData = graphs.links.map(link => {
var obj = link;
obj.source = link.source;
obj.target = link.target;
return obj;
})
const links = linksG
.selectAll("g")
.data(graphs.links)
.enter().append("g")
.attr("cursor", "pointer")
const linkLines = links
.append("path")
.attr('stroke', '#000000')
.attr('opacity', 0.75)
.attr("stroke-width", 1)
.attr("fill", "transparent")
.attr('marker-end', 'url(#arrowhead)');
const linkText = links
.append("text")
.attr("x", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length / 2).x
})
.attr("y", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length / 2).y
})
.attr('stroke', '#000000')
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr('opacity', 1)
.text((d, i) => `${i}`);
const nodes = nodesG
.selectAll("g")
.data(graphs.nodes)
.enter().append("g")
.attr("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const circles = nodes.append("circle")
.attr("r", 12)
.attr("fill", "000000")
nodes.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graphs.nodes)
.on("tick", ticked);
simulation.force("link", d3.forceLink().links(linksData)
.id((d, i) => d.id)
.distance(150));
function ticked() {
linkLines.attr("d", function(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
// recalculate and back off the distance
linkLines.attr("d", function(d) {
// length of current path
var pl = this.getTotalLength(),
// radius of circle plus backoff
r = (12) + 30,
// position close to where path intercepts circle
m = this.getPointAtLength(pl - r);
var dx = m.x - d.source.x,
dy = m.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + m.x + "," + m.y;
});
linkText
.attr("x", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length / 2).x
})
.attr("y", function(d) {
const length = this.previousSibling.getTotalLength();
return this.previousSibling.getPointAtLength(length / 2).y
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="//d3js.org/d3.v4.min.js" type="text/javascript"></script>
</head>
<body>
<svg id="mySVG" width="500" height="500">
<g class="links" />
<g class="nodes" />
</svg>
Here I'm assuming that the <path> is the previous sibling of the <text>. If that's not the case in the real code, change this accordingly.

Related

D3 v7 merge() pattern removes wrong nodes & links

I struggle to properly redraw the provided D3 forced graph. As soon as I delete a node, which is NOT the last node, the binding/assignment between link and node is broken. The belonged linktext is either wrong or written on top of an existing.
What I am doing?
On node click I retrieve the node id, which I use to retrieve the correct link array index, by comparing them the source.id and to find the correct node array index position too. Further I use the results to finally splice the link and node array at the correct position.
So whats my problem?
For example I delete node number 3, which should delete the node array object at position 2, which is true. Further the link between 3 --- 1 should be removed from the link array object at position 1 as well, which is also true.
At the end I restart the data assignment and call the restart() function. Which should use the modified nodes and links array´s to merge and redraw the graph. It actually does redraw but the link text is wrong.. instead of node 3 node 5 was deleted.
Help.
var data = {
"nodes": [{
"id": 1
},
{
"id": 2,
},
{
"id": 3,
},
{
"id": 4,
},
{
"id": 5,
}
],
"links": [{
"source": 2,
"target": 1,
"text": "2 --- 1"
},
{
"source": 3,
"target": 1,
"text": "3 --- 1"
},
{
"source": 4,
"target": 1,
"text": "4 --- 1"
},
{
"source": 5,
"target": 1,
"text": "5 --- 1"
}
]
};
let nodes = data.nodes
let links = data.links
//Helper
let nodeToDelete
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add node", "remove node"])
.enter()
.append("button")
.attr("class", "buttons")
.text(function (d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-5000))
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
console.log("links_on_init", links)
console.log("nodes_on_init", nodes)
restart()
simulation
.nodes(nodes)
.on("tick", tick)
simulation
.force("link").links(links)
function tick() {
linkLine.attr("d", function (d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
})
node
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
buttons.on("click", function (_, d) {
if (d === "add node") {
const newObj = { "id": nodes.length + 1,}
const newLink = {"source": nodes.length + 1, "target": 1, "text": nodes.length + 1 + " --- " + "1"}
nodes.push(newObj)
links.push(newLink)
} else if (d === "remove node") {
if (nodeToDelete != undefined) {
let linkToDeleteIndex = links.findIndex(obj => obj.source.id === nodeToDelete.id )
let nodeToDeleteIndex = nodes.findIndex(obj => obj.id === nodeToDelete.id)
links.splice(linkToDeleteIndex, 1)
nodes.splice(nodeToDeleteIndex, 1)
console.log("links_after_removal", links)
console.log("nodes_after_removal", nodes)
}
}
restart()
})
function restart() {
// Update linkLines
linkLine = linksContainer.selectAll(".linkPath")
.data(links)
linkLine.exit().remove()
const linkLineEnter = linkLine.enter()
.append("path")
.attr("class", "linkPath")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
.attr("id", function (_, i) {
return "path" + i
})
linkLine = linkLineEnter.merge(linkLine)
// Update linkText
linkText = linksContainer.selectAll("linkLabel")
.data(links)
linkText.exit().remove()
const linkTextEnter = linkText.enter()
.append("text")
.attr("dy", -10)
.attr("class", "linkLabel")
.attr("id", function (d, i) { return "linkLabel" + i })
.attr("text-anchor", "middle")
.text("")
linkTextEnter.append("textPath")
.attr("xlink:href", function (_, i) {
return "#path" + i
})
.attr("startOffset", "50%")
.attr("opacity", 0.75)
.attr("cursor", "default")
.attr("class", "linkText")
.attr("color", "black")
.text(function (d) {
return d.text
})
linkText = linkTextEnter.merge(linkText)
// Update nodes
node = nodesContainer.selectAll(".nodes")
.data(nodes)
node.exit().remove()
const nodesEnter = node.enter()
.append("g")
.attr("class", "nodes")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodesEnter.selectAll("circle")
.data(d => [d])
.enter()
.append("circle")
.style("stroke", "blue")
.attr("r", 40)
.on("click", function(_, d) {
d3.selectAll("circle")
.attr("fill", "whitesmoke")
d3.select(this)
.attr("fill", "red")
nodeToDelete = d
})
nodesEnter.append("text")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 20)
.attr("fill", "black")
.attr("pointer-events", "none")
.text(function (d) {
return d.id
})
node = nodesEnter.merge(node)
// Update and restart the simulation.
simulation
.nodes(nodes);
simulation
.force("link")
.links(links)
simulation.restart().alpha(1)
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.nodes {
fill: whitesmoke;
}
.buttons {
margin: 0 1em 0 0;
}
<!DOCTYPE html>
<html lang="de">
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<meta charset="utf-8">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.3.js"></script>
<!-- D3 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
</head>
<body>
</body>
</html>
You should provide data keys when you do .data(data). So for example when you provide data to the nodes, you may pass key like this:
node = nodesContainer.selectAll(".nodes")
.data(nodes, node => node.id) // Pass node.id as a key so d3 knows which node is a new and which one is old.
You can read about this here - https://observablehq.com/#dnarvaez27/understanding-enter-exit-merge-key-function
Also you've forgot to place . before class name when you adding text labels to the graph, that is why text was duplicated )
Here fixed example, but since you are using array length as ids, there will be duplicates in ids for nodes and links, it will lead to unexpected results, I consider to use uniq ids rather then indexes.
var data = {
"nodes": [{
"id": 1
},
{
"id": 2,
},
{
"id": 3,
},
{
"id": 4,
},
{
"id": 5,
}
],
"links": [{
"source": 2,
"target": 1,
"text": "2 --- 1"
},
{
"source": 3,
"target": 1,
"text": "3 --- 1"
},
{
"source": 4,
"target": 1,
"text": "4 --- 1"
},
{
"source": 5,
"target": 1,
"text": "5 --- 1"
}
]
};
let nodes = data.nodes
let links = data.links
//Helper
let nodeToDelete
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add node", "remove node"])
.enter()
.append("button")
.attr("class", "buttons")
.text(function (d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-5000))
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
console.log("links_on_init", links)
console.log("nodes_on_init", nodes)
restart()
simulation
.nodes(nodes)
.on("tick", tick)
simulation
.force("link").links(links)
function tick() {
linkLine.attr("d", function (d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
})
node
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
buttons.on("click", function (_, d) {
if (d === "add node") {
const newObj = { "id": nodes.length + 1,}
const newLink = {"source": nodes.length + 1, "target": 1, "text": nodes.length + 1 + " --- " + "1"}
nodes.push(newObj)
links.push(newLink)
} else if (d === "remove node") {
if (nodeToDelete != undefined) {
let linkToDeleteIndex = links.findIndex(obj => obj.source.id === nodeToDelete.id )
let nodeToDeleteIndex = nodes.findIndex(obj => obj.id === nodeToDelete.id)
links.splice(linkToDeleteIndex, 1)
nodes.splice(nodeToDeleteIndex, 1)
console.log("links_after_removal", links)
console.log("nodes_after_removal", nodes)
}
}
restart()
})
function restart() {
// Update linkLines
linkLine = linksContainer.selectAll(".linkPath")
.data(links, link => link.text) // ADD DATA KEY FOR LINK
linkLine.exit().remove()
const linkLineEnter = linkLine.enter()
.append("path")
.attr("class", "linkPath")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
.attr("id", function (_, i) {
return "path" + i
})
linkLine = linkLineEnter.merge(linkLine)
// Update linkText
linkText = linksContainer.selectAll(".linkLabel") // FIXED ClassName
.data(links, link => link.text) // ADD DATA KEY FOR TEXT
linkText.exit().remove()
const linkTextEnter = linkText.enter()
.append("text")
.attr("dy", -10)
.attr("class", "linkLabel")
.attr("id", function (d, i) { return "linkLabel" + i })
.attr("text-anchor", "middle")
.text("")
linkTextEnter.append("textPath")
.attr("xlink:href", function (_, i) {
return "#path" + i
})
.attr("startOffset", "50%")
.attr("opacity", 0.75)
.attr("cursor", "default")
.attr("class", "linkText")
.attr("color", "black")
.text(function (d) {
return d.text
})
linkText = linkTextEnter.merge(linkText)
// Update nodes
node = nodesContainer.selectAll(".nodes")
.data(nodes, node => node.id) // ADD DATA KEY FOR NODE
node.exit().remove()
const nodesEnter = node.enter()
.append("g")
.attr("class", "nodes")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodesEnter.selectAll("circle")
.data(d => [d])
.enter()
.append("circle")
.style("stroke", "blue")
.attr("r", 40)
.on("click", function(_, d) {
d3.selectAll("circle")
.attr("fill", "whitesmoke")
d3.select(this)
.attr("fill", "red")
nodeToDelete = d
})
nodesEnter.append("text")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 20)
.attr("fill", "black")
.attr("pointer-events", "none")
.text(function (d) {
return d.id
})
node = nodesEnter.merge(node)
// Update and restart the simulation.
simulation
.nodes(nodes);
simulation
.force("link")
.links(links)
simulation.restart().alpha(1)
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.nodes {
fill: whitesmoke;
}
.buttons {
margin: 0 1em 0 0;
}
<!DOCTYPE html>
<html lang="de">
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<meta charset="utf-8">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.3.js"></script>
<!-- D3 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
</head>
<body>
</body>
</html>

D3 v3 nodes distance missing

I am pretty confused regarding the following behavior. The D3v3 forced graph below shows 4 nodes, with at least one link to another node. I dont know why, but the distance between the nodes is broken as I got more than 3 nodes. Even if distance and charge are set.
var width = window.innerWidth,
height = window.innerHeight;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().on("zoom", function() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + "scale(" + d3.event.scale + ")")
}))
.append("g")
var defs = svg.append("defs")
const arc = d3.svg.arc()
.innerRadius(80 + 10)
.outerRadius(80 + 10)
.startAngle(-Math.PI)
.endAngle(Math.PI)
defs.append("path")
.attr("id", "curvedLabelPath")
.attr("d", arc())
////////////////////////
// outer force layout
var outerData = {
"nodes": [
{ "id": "A" },
{ "id": "B" },
{ "id": "C" },
{ "id": "D" }
],
"links": [
{ "source": 0, "target": 1},
{ "source": 1, "target": 2},
{ "source": 2, "target": 0},
{ "source": 3, "target": 0}
]
};
var outerLayout = d3.layout.force()
.size([width, height])
.charge(-1000)
.gravity(0.85)
.distance(500)
.links(outerData.links)
.nodes(outerData.nodes)
.start();
var outerLinks = svg.selectAll("g")
.data(outerData.links)
.enter().append("g")
.attr("cursor", "pointer")
//.attr("class", "g.outerLink")
var outerLine = outerLinks
.append("path")
.attr("id", function (_, i) {
return "path" + i
})
.attr("stroke", "black")
.attr("opacity", 0.75)
.attr("stroke-width", 3)
.attr("fill", "transparent")
.attr("cursor", "default")
var outerNodes = svg.selectAll("g.outer")
.data(outerData.nodes, function (d) { return d.id; })
.enter()
.append("g")
.attr("class", "outer")
.attr("id", function (d) { return d.id; })
.call(outerLayout.drag()
.on("dragstart", function () {
d3.event.sourceEvent.stopPropagation();
})
)
.attr("cursor", "pointer")
outerNodes
.append("circle")
.style("fill", "whitesmoke")
.style("stroke", "black")
.style("stroke-width", 3)
.attr("r", 80);
outerNodes
.append("text")
.append("textPath")
.attr("href", "#curvedLabelPath")
.attr("text-anchor", "middle")
.attr("startOffset", "25%")
.attr("font-size", "30px")
.attr("cursor", "pointer")
.text(function (d) {
return d.id
})
outerLayout
.on("tick", function() {
outerLine.attr("d", function (d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
outerNodes
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; });
})
body {
background: whitesmoke,´;
overflow: hidden;
margin: 0px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v3 forces</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v3.js"></script>
</head>
</html>
I am aware force setup is different in v4 and higher, still regarding my opinion some features are handled better in v3. I would be glad if an D3 expert could either help or explain those behaviour.

D3 linkText appends instead updating existing value

Recently I migrated my graph to v6 where I replaced the enter(), remove(), exit() pattern with join(). Unfortunately, I can´t change the link text any longer. Usually, the text should switch between "blue" and "green" as soon as a link was clicked.
I know, that I append a new text object instead of updating the existing value. Which is the problem. It's visible in the browser inspector. But I can´t figure out how to adapt the code, as join() is not working at this point.
Any hints?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3 JOIN Test</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
<!-- import multiselection framework -->
<script src="https://d3js.org/d3-selection-multi.v1.js"></script>
</head>
<style>
body {
overflow: hidden;
margin: 0px;
}
.canvas {
background-color: rgb(220, 220, 220);
}
.link {
cursor: pointer;
stroke: rgb(0, 0, 0);
stroke-width: 4px;
}
.link:hover {
stroke: red;
}
</style>
<body>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 1,
},
{
"id": 2,
},
{
"id": 3,
}
],
"links": [
{
"source": 1,
"target": 2,
"type": "blue"
},
{
"source": 2,
"target": 3,
"type": "blue"
},
{
"source": 3,
"target": 1,
"type": "blue"
}
]
}
// declare initial variables
var svg = d3.select("svg")
width = window.innerWidth
height = window.innerHeight
// define cavnas area to draw everything
svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linksContainer = svg.append("g").attr("class", "linksContainer")
var nodesContainer = svg.append("g").attr("class", "nodesContainer")
// iniital force simulation
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(200))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("attraceForce", d3.forceManyBody().strength(70));
initialze()
function initialze() {
links = linksContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
.on("click", click)
linkPaths = linksContainer.selectAll(".linkPath")
.data(graph.links)
.join("path")
.style("pointer-events", "none")
.attrs({
"class": "linkPath",
"fill-opacity": 1,
"stroke-opacity": 1,
"id": function (d, i) { return "linkPath" + i }
})
.style("display", "block")
linkLabels = linksContainer.selectAll(".linkLabel")
.data(graph.links)
.join("text")
.style("pointer-events", "none")
.attrs({
"class": "linkLabel",
"id": function (d, i) { return "linkLabel" + i },
"font-size": 16,
"fill": "black"
})
.style("display", "block")
linkLabels
.append("textPath")
.attr('xlink:href', function (d, i) { return '#linkPath' + i })
.style("text-anchor", "middle")
.style("pointer-events", "none")
.attr("startOffset", "50%")
.text(function (d) { return d.type })
nodes = nodesContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("circle")
.attr("class", "node")
.attr("r", 30)
.attr("fill", "whitesmoke")
.attr("stroke", "white")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
function mouseEnter(event, d) {
d3.select(this).style("fill", "lightblue")
}
function mouseLeave(event, d) {
d3.select(this).style("fill", "whitesmoke")
}
function click(event, d) {
if (d.type == "blue") {
d.type = "green"
} else if (d.type == "green") {
d.tyoe = "blue"
}
initialze()
}
function close() {
contextMenuLink.classList.remove("active")
}
function ticked() {
links
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
nodes
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
linkPaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});
linkLabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();
rx = bbox.x + bbox.width / 2;
ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}
</script>
</body>
</html>
For those who face a similar problem. I played around a bit and got it done by adding an empty .text("") attribute to the ".linklabel" class. The correct version looks liks:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3 JOIN Test</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<style>
body {
overflow: hidden;
margin: 0px;
}
.canvas {
background-color: rgb(220, 220, 220);
}
.link {
cursor: pointer;
stroke: rgb(0, 0, 0);
stroke-width: 4px;
}
.link:hover {
stroke: red;
}
</style>
<body>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 1,
},
{
"id": 2,
},
{
"id": 3,
}
],
"links": [
{
"source": 1,
"target": 2,
"type": "blue"
},
{
"source": 2,
"target": 3,
"type": "blue"
},
{
"source": 3,
"target": 1,
"type": "blue"
}
]
}
// declare initial variables
var svg = d3.select("svg")
width = window.innerWidth
height = window.innerHeight
// define cavnas area to draw everything
svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linksContainer = svg.append("g").attr("class", "linksContainer")
var nodesContainer = svg.append("g").attr("class", "nodesContainer")
// iniital force simulation
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(200))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("attraceForce", d3.forceManyBody().strength(70));
initialze()
function initialze() {
links = linksContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
.on("click", click)
linkPaths = linksContainer.selectAll(".linkPath")
.data(graph.links)
.join("path")
.attr("class", "linkPath")
.attr("id", function (d, i) { return "linkPath" + i })
linkLabels = linksContainer.selectAll(".linkLabel")
.data(graph.links)
.join("text")
.attr("class", "linkLabel")
.attr("id", function (d, i) { return "linkLabel" + i })
.attr("font-size", 16)
.attr("fill", "black")
.text("")
linkLabels
.append("textPath")
.attr('xlink:href', function (d, i) { return '#linkPath' + i })
.style("text-anchor", "middle")
.attr("startOffset", "50%")
.text(function (d) { return d.type })
nodes = nodesContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("circle")
.attr("class", "node")
.attr("r", 30)
.attr("fill", "whitesmoke")
.attr("stroke", "white")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
function click(event, d) {
if (d.type == "blue") {
d.type = "green"
} else if (d.type == "green") {
d.type = "blue"
}
initialze()
}
function ticked() {
links
.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
nodes
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
linkPaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});
linkLabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();
rx = bbox.x + bbox.width / 2;
ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
}
</script>
</body>
</html>

How to calculate a modified Path for different size circle in Force directed arrow graph?

My question is further modification of Force directed graph from here. Here from every source to target it has a arrow at the target end this works fine however if we stick to the theory that all circle are with radius 5.
However in my case I have modified the example that all the nodes will be of different radius based on a parameter, so in such scenario the arrow hides behind the target as the radius is big so the path and arrow hides beside, Im not looking for a solution to bring path and arrow in front instead what I'm trying is to find the new point. In layman language I need to subtract the radius unit from the path so that I get a point on the outer circle.
Sample fiddle is HERE
I guess some modification to this code will be required here in tick function
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});
I tried to modify this code as HERE but not a good fix it behaving weird Can anyone please comment how can we calculate this point
This is a "classic" solution (based on this answer):
path.attr("d", function(d) {
diffX = d.target.x - d.source.x;
diffY = d.target.y - d.source.y;
pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
offsetX = (diffX * d.target.radius) / pathLength;
offsetY = (diffY * d.target.radius) / pathLength;
var dx = (d.target.x - offsetX) - d.source.x,
dy = (d.target.y - offsetY) - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
(d.target.x - offsetX) + "," +
(d.target.y - offsetY);
});
The only problem with this math is that the arc starts and ends in the straight line going from the source to the target. That gives the impression that the arrowhead is a little bit "to the right".
Here is the demo:
// get the data
var graph = {
"nodes": [{
"name": "Ilya I",
"group": 0
}, {
"name": "Betty B",
"group": 1
}, {
"name": "Andy N",
"group": 2
}, {
"name": "Harper P",
"group": 3
}, {
"name": "Holly V",
"group": 4
}, {
"name": "Elijah W",
"group": 5
}, {
"name": "Kalvin L",
"group": 6
}, {
"name": "Chris D",
"group": 7
}, {
"name": "Alexa U",
"group": 8
}],
"links": [{
"source": 2,
"target": 5,
"value": 1,
"type": "arrow"
}, {
"source": 2,
"target": 6,
"value": 3,
"type": "arrow"
}, {
"source": 2,
"target": 7,
"value": 1,
"type": "arrow"
}, {
"source": 2,
"target": 8,
"value": 1,
"type": "arrow"
}, {
"source": 3,
"target": 2,
"value": 1,
"type": "arrow"
}, {
"source": 3,
"target": 4,
"value": 1,
"type": "arrow"
}, {
"source": 3,
"target": 6,
"value": 2,
"type": "arrow"
}, {
"source": 3,
"target": 7,
"value": 1,
"type": "arrow"
}, {
"source": 3,
"target": 8,
"value": 3,
"type": "arrow"
}, {
"source": 5,
"target": 5,
"value": 1,
"type": "arrow"
}, {
"source": 6,
"target": 2,
"value": 1,
"type": "arrow"
}]
};
var nodecolor = d3.scale.category20();
var nodes = {};
// Compute the distinct nodes from the links.
var links = graph.links;
var width = 500,
height = 400;
var force = d3.layout.force()
.nodes(graph.nodes)
.links(links)
.size([width, height])
.linkDistance(function(d) {
return 1 / d.value * 250;
})
.charge(-500)
.on("tick", tick)
.start();
// Set the range
var v = d3.scale.linear().range([0, 100]);
// Scale the range of the data
v.domain([0, d3.max(links, function(d) {
return d.value;
})]);
// asign a type per value to encode opacity
links.forEach(function(link) {
if (v(link.value) <= 25) {
link.type = "twofive";
} else if (v(link.value) <= 50 && v(link.value) > 25) {
link.type = "fivezero";
} else if (v(link.value) <= 75 && v(link.value) > 50) {
link.type = "sevenfive";
} else if (v(link.value) <= 100 && v(link.value) > 75) {
link.type = "onezerozero";
}
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
.data(force.links())
.enter().append("svg:path")
.attr("class", function(d) {
return "link " + d.type;
})
.attr("marker-end", "url(#end)");
// define the nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
//.on("click", click)
//.on("dblclick", dblclick)
.call(force.drag);
// add the nodes
node.append("circle")
.attr("r", function(d) {
d.radius = d.group * 5;
return d.radius
})
.style("fill", function(d) {
return nodecolor(d.group);
});
// add the text
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
// add the curvy lines
function tick() {
path.attr("d", function(d) {
diffX = d.target.x - d.source.x;
diffY = d.target.y - d.source.y;
pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
offsetX = (diffX * d.target.radius) / pathLength;
offsetY = (diffY * d.target.radius) / pathLength;
var dx = (d.target.x - offsetX) - d.source.x,
dy = (d.target.y - offsetY) - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
(d.target.x - offsetX) + "," +
(d.target.y - offsetY);
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
// action to take on mouse click
function click() {
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 22)
.style("fill", "steelblue")
.style("stroke", "lightsteelblue")
.style("stroke-width", ".5px");
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16)
.style("fill", function(d) {
return nodecolor(d.group);
});
}
// action to take on mouse double click
function dblclick() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 6)
.style("fill", function(d) {
return nodecolor(d.group);
});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 12)
.style("stroke", "none")
.style("fill", "black")
.style("stroke", "none")
.style("font", "10px sans-serif");
}
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
path.link.twofive {
opacity: 0.25;
}
path.link.fivezero {
opacity: 0.50;
}
path.link.sevenfive {
opacity: 0.75;
}
path.link.onezerozero {
opacity: 1.0;
}
circle {
fill: #ccc;
stroke: #fff;
stroke-width: 1.5px;
}
text {
fill: #000;
pointer-events: none;
}
#content {
padding: 7px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
One way would be to show the marker in the middle
doing like this:
.attr("marker-mid", "url(#end)");
instead of
.attr("marker-end", "url(#end)");
working example here

D3 drawing a second donut chart from nested JSON

I am learning D3 on the fly today. Below is a sample set of code with a JSON data set. I am currently trying to create a second graph next to the main donut chart. This chart will be another donut chart of the thin_vols[] array which is nested in each object. If you see the commented code below, I am attempting to build the second chart with no such luck. Any advice or help is greatly appreciated. Thank you.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>D3 Donut Chart</title>
</head>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
.arc path {
stroke: #fff;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var sampleSet = {
"title":"DummyData",
"data":[
{
"origin":"Disk_Pool_1",
"volumeName":"Vol1",
"usage":15,
"type":"thick",
"thin_vols":[]
},
{
"origin":"Disk_Pool_1",
"volumeName":"Vol2",
"usage":25,
"type":"thick",
"thin_vols":[]
},
{
"origin":"Disk_Pool_1",
"volumeName":"Repo_06",
"usage":50,
"type":"thick",
"thin_vols":[
{
"origin":"Repo_06",
"volumeName":"thinVol1",
"usage":10,
"max":20,
"type":"thin"
},
{
"origin":"Repo_06",
"volumeName":"thinVol2",
"usage":10,
"max":30,
"type":"thin"
},
{
"origin":"Repo_06",
"volumeName":"freespace",
"usage":20,
"max":40,
"type":"freespace"
}]
}
]
};
var m = 10,
r = 100,
z = d3.scale.category20c();
var pie = d3.layout.pie()
.value(function(d) { return +d.usage; })
.sort(function(a, b) { return b.usage - a.usage; });
var arc = d3.svg.arc()
.innerRadius(r / 2)
.outerRadius(r);
//here
var disks = d3.nest()
.key(function(d) { return d.origin; })
.entries(sampleSet.data);
var svg = d3.select("body").selectAll("div")
.data(disks)
.enter().append("div")
.style("display", "inline-block")
.style("width", (r + m) * 2 + "px")
.style("height", (r + m) * 2 + "px")
.append("svg:svg")
.attr("width", (r + m) * 2)
.attr("height", (r + m) * 2)
.append("svg:g")
.attr("transform", "translate(" + (r + m) + "," + (r + m) + ")");
svg.append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
var g = svg.selectAll("g")
.data(function(d) { return pie(d.values); })
.enter().append("svg:g");
g.append("svg:path")
.attr("d", arc)
.style("fill", function(d) { return z(d.data.volumeName); })
.append("svg:title")
.text(function(d) { return d.data.volumeName + ": " + d.data.usage; });
g.filter(function(d) { return d.endAngle - d.startAngle > .2; }).append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")rotate(" + angle(d) + ")"; })
.text(function(d) { return d.data.volumeName + ": " + d.data.usage; });
var disks2 = d3.nest()
.key(function(d) { return d.origin; })
.entries(sampleSet.data.thin_vols);
var svg2 = d3.select("body").selectAll("div")
.data(disks2)
.enter().append("div")
.style("display", "inline-block")
.style("width", (r + m) * 2 + "px")
.style("height", (r + m) * 2 + "px")
.append("svg:svg")
.attr("width", (r + m) * 2)
.attr("height", (r + m) * 2)
.append("svg:g")
.attr("transform", "translate(" + (r + m) + "," + (r + m) + ")");
svg2.append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
var g2 = svg2.selectAll("g")
.data(function(d) { return pie(d.values); })
.enter().append("svg:g");
g2.append("svg:path")
.attr("d", arc)
.style("fill", function(d) { return z(d.data.volumeName); })
.append("svg:title")
.text(function(d) { return d.data.thinvolumeName + ": " + d.data.usage; });
g2.filter(function(d) { return d.endAngle - d.startAngle > .2; }).append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")rotate(" + angle(d) + ")"; })
.text(function(d) { return d.data.volumeName + ": " + d.data.usage; });
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
</script>
</body>
</html>
I was able to render the second chart by flattening the existing JSON. See the below source code.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>D3 Donut Chart</title>
</head>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
.arc path {
stroke: #fff;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var sampleSet = {
"title": "DummyData",
"data": [
{
"origin": "Disk_Pool_1",
"volumeName": "Vol1",
"usage": 15,
"type": "thick"
},
{
"origin": "Disk_Pool_1",
"volumeName": "Vol2",
"usage": 25,
"type": "thick"
},
{
"origin": "Disk_Pool_1",
"volumeName": "Repo_06",
"usage": 50,
"type": "thick"
},
{
"origin": "Repo_06",
"volumeName": "thinVol1",
"usage": 10,
"max": 20,
"type": "thin"
},
{
"origin": "Repo_06",
"volumeName": "thinVol2",
"usage": 10,
"max": 30,
"type": "thin"
},
{
"origin": "Repo_06",
"volumeName": "freespace",
"usage": 20,
"max": 40,
"type": "freespace"
}
]
};
var m = 10, r = 100, z = d3.scale.category20c();
var pie = d3.layout.pie()
.value(function (d) {
return +d.usage;
})
.sort(function (a, b) {
return b.usage - a.usage;
});
var arc = d3.svg.arc()
.innerRadius(r / 2)
.outerRadius(r);
var disks = d3.nest()
.key(function (d) {
return d.origin;
})
.entries(sampleSet.data);
var svg = d3.select("body").selectAll("div")
.data(disks)
.enter().append("div")
.style("display", "inline-block")
.style("width", (r + m) * 2 + "px")
.style("height", (r + m) * 2 + "px")
.append("svg:svg")
.attr("width", (r + m) * 2)
.attr("height", (r + m) * 2)
.append("svg:g")
.attr("transform", "translate(" + (r + m) + "," + (r + m) + ")");
svg.append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.key;
});
var g = svg.selectAll("g")
.data(function (d) {
return pie(d.values);
})
.enter().append("svg:g");
g.append("svg:path")
.attr("d", arc)
.style("fill", function (d) {
return z(d.data.volumeName);
})
.append("svg:title")
.text(function (d) {
return d.data.volumeName + ": " + d.data.usage;
});
g.filter(function (d) {
return d.endAngle - d.startAngle > .2;
}).append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("transform", function (d) {
return "translate(" + arc.centroid(d) + ")rotate(" + angle(d) + ")";
})
.text(function (d) {
return d.data.volumeName + ": " + d.data.usage;
});
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
</script>
</body>
</html>

Categories