D3 how to exclude links from Gooey effect - javascript

Problem: The Gooey effect applies to the links too. Which creates a teardrop shaped frame instead of an circle.
The snipped contains a dragged() function which allows the user to tear off node 1 from node 0. Further it is possible to connect node 1 with node 0 again with the help of dragging. The code isn´t clean at all, as its a playground only.
Goal: How can I exclude the links from the Gooey effect in a way, that all links are displayed correctly and still achieve a proper circled shape. The shape of the Gooey effect can be manipulated by changing the -5 to -40, unfortunately it will hide the links completely:
.attr("values", "1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 50 -5")
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v6 Playground</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);
}
.node {
cursor: pointer;
}
.node:hover {
stroke: red
}
.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
</style>
<body>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [
{
"source": 1,
"target": 0,
},
{
"source": 2,
"target": 0,
},
]
}
var width = window.innerWidth
var height = window.innerHeight
var 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")
.style("filter", "url(#gooey)")
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
var isSpliced = false;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(150))
.force("charge", d3.forceManyBody().strength(-50))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(30))
//###############################################
//######### SVG Filter for Gooey effect #########
//###############################################
var defs = svg.append("defs");
var filter = defs.append("filter").attr("id", "gooey");
filter.append("feGaussianBlur")
.attr("in", "SourceGraphic")
.attr("stdDeviation", "10")
.attr("result", "blur");
filter.append("feColorMatrix")
.attr("in", "blur")
.attr("mode", "matrix")
.attr("values", "1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 50 -5")
.attr("result", "gooey");
filter.append("feComposite")
.attr("in", "SourceGraphic")
.attr("in2", "gooey")
.attr("operator", "atop");
initialize()
//###############################################
//############## Initialization #################
//###############################################
function initialize() {
link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "white")
node.selectAll("text")
.data(d => [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
//###############################################
//############# Update Positions ################
//###############################################
function ticked() {
// update link positions
link
.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;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
//###############################################
//################ Drag Nodes ###################
//###############################################
var lineX;
var lineY;
var isPlugged = true;
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
lineX = d.x
lineY = d.y
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
var root = graph.nodes.find(element => (element.id === 0))
var distance = (root.x - d.x) * 2 + (root.y - d.y) * 2
if (isPlugged && d.id === 1) {
var indexOfLink = graph.links.findIndex(element => (element.source.id === d.id))
if (distance < 0) {
distance = -distance
}
if (distance > 1000) {
graph.links.splice(indexOfLink, 1)
isPlugged = false;
}
} else {
if (distance < 0) {
distance = -distance
}
if (distance < 20) {
graph.links.push({ source: d.id, target: root.id })
isPlugged = true;
}
}
initialize()
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
link.filter((a) => {
return a.source.id === d.id
}).style("stroke", "black")
}
</script>
</body>
</html>

Just apply the style to nodeContainer instead of svg - see comments below:
var 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 the style here
// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)
var linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
.style("filter", "url(#gooey)") // <---------- add the style here
Your code updated:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v6 Playground</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);
}
.node {
cursor: pointer;
}
.node:hover {
stroke: red
}
.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
</style>
<body>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [
{
"source": 1,
"target": 0,
},
{
"source": 2,
"target": 0,
},
]
}
var width = window.innerWidth
var height = window.innerHeight
var 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 linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
.style("filter", "url(#gooey)")
var isSpliced = false;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(150))
.force("charge", d3.forceManyBody().strength(-50))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(30))
//###############################################
//######### SVG Filter for Gooey effect #########
//###############################################
var defs = svg.append("defs");
var filter = defs.append("filter").attr("id", "gooey");
filter.append("feGaussianBlur")
.attr("in", "SourceGraphic")
.attr("stdDeviation", "10")
.attr("result", "blur");
filter.append("feColorMatrix")
.attr("in", "blur")
.attr("mode", "matrix")
.attr("values", "1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 50 -5")
.attr("result", "gooey");
filter.append("feComposite")
.attr("in", "SourceGraphic")
.attr("in2", "gooey")
.attr("operator", "atop");
initialize()
//###############################################
//############## Initialization #################
//###############################################
function initialize() {
link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "white")
node.selectAll("text")
.data(d => [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
//###############################################
//############# Update Positions ################
//###############################################
function ticked() {
// update link positions
link
.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;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
//###############################################
//################ Drag Nodes ###################
//###############################################
var lineX;
var lineY;
var isPlugged = true;
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
lineX = d.x
lineY = d.y
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
var root = graph.nodes.find(element => (element.id === 0))
var distance = (root.x - d.x) * 2 + (root.y - d.y) * 2
if (isPlugged && d.id === 1) {
var indexOfLink = graph.links.findIndex(element => (element.source.id === d.id))
if (distance < 0) {
distance = -distance
}
if (distance > 1000) {
graph.links.splice(indexOfLink, 1)
isPlugged = false;
}
} else {
if (distance < 0) {
distance = -distance
}
if (distance < 20) {
graph.links.push({ source: d.id, target: root.id })
isPlugged = true;
}
}
initialize()
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = undefined;
d.fy = undefined;
link.filter((a) => {
return a.source.id === d.id
}).style("stroke", "black")
}
</script>
</body>
</html>

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 recalculate arrowhead position based on node size

I am facing two visual problems with the given D3 forced graph.
The last inch of each link is still visible. The arrowhead should end exactly at the node edge, which is the case, but the red link underneath is visible. I could adjust the link stroke-width or the arrow size but actually do not want to change those attributes.
The radius of a node increases on a mouseenter event. If node size increases, the arrowhead is out of position and does not end on the node endge anymore. I assume I need to adjust the tick function, but do not find a proper starting point.
I appreciate any link, hint, help which gets me closer to the result.
var width = window.innerWidth,
height = window.innerHeight;
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")
////////////////////////
// outer force layout
var data = {
"nodes":[
{ "id": "A" },
{ "id": "B" },
{ "id": "C" },
],
"links": [
{ "source": "A", "target": "B"},
{ "source": "B", "target": "C"},
{ "source": "C", "target": "A"}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
svg.append("defs").append("marker")
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr("refX", 23.5)
.attr("refY", 0)
.attr('orient', 'auto')
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr("orient", "auto")
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
var links = svg.selectAll(".links")
.data(data.links)
.join("line")
.attr("stroke", "red")
.attr("stroke-width", 3)
.attr("marker-end", "url(#arrowhead)")
var nodes = svg.selectAll("g.outer")
.data(data.nodes, function (d) { return d.id; })
.enter()
.append("g")
.attr("class", "outer")
.attr("id", function (d) { return d.id; })
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
);
nodes
.append("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
.on("mouseenter", function() {
d3.select(this)
.transition()
.duration(250)
.attr("r", 40 * 1.3)
.attr("fill", "blue")
})
.on("mouseleave", function() {
d3.select(this)
.transition()
.duration(250)
.attr("r", 40)
.attr("fill", "lightgrey")
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
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", 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;
}
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>D3v7</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
</body>
</html>
solution
at the moment I can think of two solutions.
calculate each <path(.links)/> vector and subtract with extended circle radius
give offset to each <marker />
personally I've chosen the 2nd solution; it requires less computation and easier to understand.
1. give each links custom arrowhead
add separate arrowheads for each link because it seems individual offset is not supported by svg path.
// add custom arrowheads for each link
// to avoid exhaustive computations
svg.append("defs").selectAll('marker')
.data(data.links)
.join("marker")
.attr('id', d => `arrowhead-target-${d.target}`)
// ...
var links = svg.selectAll(".links")
.data(data.links)
.join("line")
.attr("stroke", "red")
.attr("stroke-width", 3)
.attr("marker-end", d => `url(#arrowhead-target-${d.target})`)
// add source and target so that it can be traced back
2. extend, subtract each individual marker's offset with mouseevent
the offset for <marker /> is applied with .refX, .refY attributes, as noted in MDN docs
nodes
.append("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
.on("mouseenter", function(ev, d) {
// trace back to custom arrowhead with given id
const arrowHead = d3.select(`#arrowhead-target-${d.id}`)
arrowHead.transition().duration(250).attr('refX', 40)
d3.select(this)
.transition()
.duration(250)
.attr("r", 40 * 1.3)
.attr("fill", "blue")
})
.on("mouseleave", function(ev, d) {
const arrowHead = d3.select(`#arrowhead-target-${d.id}`)
arrowHead.transition().duration(250).attr('refX', 23.5)
d3.select(this)
.transition()
.duration(250)
.attr("r", 40)
.attr("fill", "lightgrey")
})
Try run the code below. The size of extension and subtraction is purposefully exaggerated so that you can see the difference.
var width = window.innerWidth,
height = window.innerHeight;
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")
////////////////////////
// outer force layout
var data = {
"nodes":[
{ "id": "A" },
{ "id": "B" },
{ "id": "C" },
],
"links": [
{ "source": "A", "target": "B"},
{ "source": "B", "target": "C"},
{ "source": "C", "target": "A"}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
// add custom arrowheads for each link
// to avoid exhaustive computations
svg.append("defs").selectAll('marker')
.data(data.links)
.join("marker")
.attr('id', d => `arrowhead-target-${d.target}`)
.attr('viewBox', '-0 -5 10 10')
.attr("refX", 23.5)
.attr("refY", 0)
.attr('orient', 'auto')
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr("orient", "auto")
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
var links = svg.selectAll(".links")
.data(data.links)
.join("line")
.attr("stroke", "red")
.attr("stroke-width", 3)
.attr("marker-end", d => `url(#arrowhead-target-${d.target})`)
// add source and target so that it can be traced back
.attr('data-source', d => d.source)
.attr('data-target', d => d.target)
var nodes = svg.selectAll("g.outer")
.data(data.nodes, function (d) { return d.id; })
.enter()
.append("g")
.attr("class", "outer")
.attr("id", function (d) { return d.id; })
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
);
nodes
.append("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
.on("mouseenter", function(ev, d) {
// trace back to custom arrowhead with given id
const arrowHead = d3.select(`#arrowhead-target-${d.id}`)
arrowHead.transition().duration(250).attr('refX', 40)
d3.select(this)
.transition()
.duration(250)
.attr("r", 40 * 1.3)
.attr("fill", "blue")
})
.on("mouseleave", function(ev, d) {
const arrowHead = d3.select(`#arrowhead-target-${d.id}`)
arrowHead.transition().duration(250).attr('refX', 23.5)
d3.select(this)
.transition()
.duration(250)
.attr("r", 40)
.attr("fill", "lightgrey")
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
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", 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;
}
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>D3v7</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
</body>
</html>

D3 Stop interaction during ongoing transition

I noticed the Control Flow section in the D3 API and tried to apply those on my code. My final goal is to avoid any "mouseenter", "mouseleave", "click".. etc. interaction as long as the transition() is still ongoing.
The snippet displays 3 nodes which are swapping the color after an "mouseenter" event. The problem is, if I interrupt a ongoing transition with in "mouseenter" event the color changes back to white. How can I avoid such behaviour?
After my applying attempts I receive await is only valid in async functions and async generators as an error.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v6 Transition Example</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<style>
body {
background-color: rgb(220, 220, 220);
overflow: hidden;
margin: 0px;
}
.node {
stroke: white;
stroke-width: 2px;
cursor: pointer;
}
.node:hover {
stroke: red
}
.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
</style>
<body>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [
{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [
{
"source": 0,
"target": 1,
},
{
"source": 1,
"target": 2,
},
{
"source": 2,
"target": 0,
}
]
}
var width = window.innerWidth
var height = window.innerHeight
var 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 linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
var isRed = false;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(100))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(50))
initialize()
function initialize() {
link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
.on("mouseenter", mouseEnter)
node.selectAll("text")
.data(d=> [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
function mouseEnter(d) {
if (!isRed) {
d3.select(this)
.transition()
.duration(1500)
.style("fill", "red")
isRed = true
} else {
d3.select(this)
.transition()
.duration(1500)
.style("fill", "whitesmoke")
isRed = false
}
}
function ticked() {
// update link positions
link
.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;
});
// update node positions
node
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
</script>
</body>
</html>
There are several ways to do this, like using a flag. However, the most idiomatic D3 is just check if the element has an active transition, which is can be as simple as:
if(d3.active(this)) return;
Here is your code with that change:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v6 Transition Example</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<style>
body {
background-color: rgb(220, 220, 220);
overflow: hidden;
margin: 0px;
}
.node {
stroke: white;
stroke-width: 2px;
cursor: pointer;
}
.node:hover {
stroke: red
}
.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
</style>
<body>
<svg id="svg"> </svg>
<script>
var graph = {
"nodes": [{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [{
"source": 0,
"target": 1,
},
{
"source": 1,
"target": 2,
},
{
"source": 2,
"target": 0,
}
]
}
var width = window.innerWidth
var height = window.innerHeight
var 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 linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")
var isRed = false;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}).distance(100))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(50))
initialize()
function initialize() {
link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")
node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
.on("mouseenter", mouseEnter)
node.selectAll("text")
.data(d => [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links)
}
function mouseEnter(d) {
if (d3.active(this)) return;
if (!isRed) {
d3.select(this)
.transition()
.duration(1500)
.style("fill", "red")
isRed = true
} else {
d3.select(this)
.transition()
.duration(1500)
.style("fill", "whitesmoke")
isRed = false
}
}
function ticked() {
// update link positions
link
.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;
});
// update node positions
node
.attr("transform", function(d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
</script>
</body>
</html>

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>

Adding text to a circle

I want to add text outside the circle using d3.js but only text is not getting added,other code works fine. How to add text to circle?
//create somewhere to put the force directed graph
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var radius = 15;
var nodes_data = [{
"name": "Bacillus",
"sex": "F"
}, {
"name": "Candida",
"sex": "M"
}, {
"name": "Dorhea",
"sex": "M"
}, {
"name": "Pichia",
"sex": "F"
},
]
//Sample links data
//type: A for Ally, E for Enemy
var links_data = [{
"source": "Candida",
"target": "Bacillus",
"type": "A"
}, {
"source": "Dorhea",
"target": "Candida",
"type": "E"
}, {
"source": "Bacillus",
"target": "Dorhea",
"type": "A"
}, {
"source": "Bacillus",
"target": "Pichia",
"type": "C"
},
]
var linkElements = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke-width", 1)
.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", 18)
.attr("fill", getNodeColor)
var textElements = svg.append("g")
.attr("class", "texts")
.selectAll("text")
.data(nodes)
.enter().append("text")
.text(function(node) {
return node.name
})
.attr("font-size", 20)
.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
})
})
//set up the simulation
var simulation = d3.forceSimulation()
//add nodes
.nodes(nodes_data);
var link_force = d3.forceLink(links_data)
.id(function(d) {
return d.name;
});
var charge_force = d3.forceManyBody()
.strength(-2500);
var center_force = d3.forceCenter(width / 2, height / 2);
simulation
.force("charge_force", charge_force)
.force("center_force", center_force)
.force("links", link_force);
//add tick instructions:
simulation.on("tick", tickActions);
//draw lines for the links
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links_data)
.enter().append("line")
.attr("stroke-width", 2)
.style("stroke", linkColour);
//draw circles for the nodes
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes_data)
.enter()
.append("circle")
.attr("r", radius)
.attr("fill", circleColour);
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
var drag_handler = d3.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
drag_handler(node)
/** Functions **/
//Function to choose what color circle we have
//Let's return blue for males and red for females
function circleColour(d) {
if (d.sex == "M") {
return "blue";
} else {
return "pink";
}
}
//Function to choose the line colour and thickness
//If the link type is "A" return green
//If the link type is "E" return red
function linkColour(d) {
if (d.type == "A") {
return "green";
} else {
return "red";
}
}
//drag handler
//d is the node
function drag_start(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function drag_drag(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function drag_end(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function tickActions() {
//constrains the nodes to be within a box
node
.attr("cx", function(d) {
return d.x = Math.max(radius, Math.min(width - radius, d.x));
})
.attr("cy", function(d) {
return d.y = Math.max(radius, Math.min(height - radius, d.y));
});
link
.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;
});
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: black;
stroke-width: 0px;
}
svg {
border: 1px solid black;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="400"></svg>
The above code adds circles with colors and links between circle but i want to add text also outside the circle but when I apply code to add text its not adding text.how to solve this problem?
The code to add text is:
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")`enter code here`
.text(function(d) { return d.name; });
var drag_handler = d3.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);

Categories