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>
Related
I tried to implement a 3D force directed graph in d3.js by the help of the following codes. But was unable to create the 3D network by adding the z-coordinates.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
fill: blue;
stroke: black;
stroke-width: 2px;
}
.node.visited {
fill: red;
}
.link {
stroke-width: 2px;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 640;
var height = 480;
var links = [{
source: 'Rohit',
target: 'Deep'
},
{
source: 'Deep',
target: 'Deepa'
},
{
source: 'Deepa',
target: 'Rohit'
},
];
var nodes = [{
"id": 1,
"desc": "Rohit",
"x": 121.0284957885742,
"y": 116.3165512084961,
"z": 59.36788940429688
},
{
"id": 2,
"desc": "Deep",
"x": 12.10284957885742,
"y": 116.3165512084961,
"z": 5.936788940429688
},
{
"id": 3,
"desc": "Deepa",
"x": 12.10284957885742,
"y": 11.63165512084961,
"z": 5.936788940429688
}
];
//adding to nodes
links.forEach(function(link) {
link.source = nodes[link.source] ||
(nodes[link.source] = {
name: link.source
});
link.target = nodes[link.target] ||
(nodes[link.target] = {
name: link.target
});
});
//adding svg to body
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
var defs = svg.append('defs');
var gradient = defs
.append('linearGradient')
.attr('id', 'svgGradient')
.attr('x1', '0%')
.attr('x2', '10%')
.attr('y1', '0%')
.attr('y2', '10%');
gradient
.append('stop')
.attr('class', 'start')
.attr('offset', '0%')
.attr('start-color', 'red')
.attr('start-opacity', 1);
gradient
.append('stop')
.attr('class', 'end')
.attr('offset', '100%')
.attr('stop-color', 'blue')
.attr('stop-opacity', 1);
var force = d3.layout.force()
.size([width, height])
.nodes(d3.values(nodes))
.links(links)
.on("tick", tick)
.linkDistance(300)
.start();
var link = svg.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link')
.attr('stroke', 'url(#svgGradient)');
var node = svg.selectAll('.node')
.data(force.nodes())
.enter().append('circle')
.attr('class', 'node')
.on("click", clicked)
.attr('r', width * 0.03)
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
.attr('cz', function(d){
return d.z;
});
//Code for clicking func.
function clicked(event, d) {
if (event.defaultPrevented) return; // dragged
d3.select(this).transition()
.style("fill", "black")
.attr("r", width * 0.2)
.transition()
.attr("r", width * 0.03)
.transition()
.style("fill", "blue")
//.attr("fill", d3.schemeCategory10[d.index % 10]);
}
//define the tick func.
function tick(e) {
node
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
.attr('cz', function(d) {
return d.z;
})
.call(force.drag);
link
.attr('x1', function(d) {
return d.source.x;
})
.attr('y1', function(d) {
return d.source.y;
})
.attr('z1', function(d) {
return d.source.z;
})
.attr('x2', function(d) {
return d.target.x;
})
.attr('y2', function(d) {
return d.target.y;
})
.attr('z2', function(d) {
return d.source.z;
})
}
</script>
</body>
The image that came was carrying two parts one graph where the nodes are taking (x-y) coordinates assigned to them. And then I had another graph where the nodes are taking all x-y-z coordinates.
Can anyone please help us by telling how can we add the z co-ordinates to code.
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>
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>
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>
I am working with d3 force diagrams at the moment, I am wanting to plot my child nodes around a parent node equally spaced, so for example if I have a parent node, and 4 linked child nodes, I would want each those node positioned at 90 degree intervals? Is that possible?
Here is my current force code,
app.force
.nodes(nodes)
.links(app.edges)
.on("tick", tick)
.start();
function tick(e) {
// console.log(link);
var k = 6 * e.alpha;
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; });
linkText
.attr("x", function(d) {
return ((d.source.x + d.target.x)/2);
})
.attr("y", function(d) {
return ((d.source.y + d.target.y)/2);
});
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node
.attr("cx", function(d) { return d.x })
.attr("cy", function(d) { return d.y })
}
app.force = d3.layout.force()
.charge(-300)
.linkDistance(85)
.size([width, height]);
//Where we will draw our visualisation
app.svg = d3.select(".visualisation").append("svg")
.attr('width', width)
.attr('height', height);
d3.layout.force() was not created with such customisation in mind. Of course you can set some parameters, but most of the positions are automatic calculated, and changing them can be very difficult (unless you create your own force function). Version 4.x is better in that matter, but not much.
In your specific case, you can set a very high (mathematically speaking, "very low", since it is negative) charge:
var force = d3.layout.force()
.charge(-3000)
But even doing that the angles are not exactly right angles, and they vary: if you click "run snippet" you can get an almost perfect cross, but if you click it again it's not that perfect the next time. And it will not work as expected if you have data with several levels of depth.
Here is a demo:
<script src="https://d3js.org/d3.v2.min.js?2.9.3"></script>
<style>
.link {
stroke: #aaa;
}
.node text {
stroke: #333;
cursor: pointer;
}
.node circle {
stroke: #fff;
stroke-width: 3px;
fill: #555;
}
</style>
<body>
<script>
var width = 400,
height = 300
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.distance(50)
.charge(-3000)
.size([width, height]);
var json = {
"nodes": [{
"name": "node1"
}, {
"name": "node2"
}, {
"name": "node3"
}, {
"name": "node4"
}, {
"name": "node5"
}],
"links": [{
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}, {
"source": 0,
"target": 3
}, {
"source": 0,
"target": 4
}]
};
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", 2);
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 8);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name
});
force.on("tick", function() {
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;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
</script>