How can I draw links between nodes with D3.js? - javascript

I am drawing a force directed graph with D3.js
I have my nodes working correctly but the lines are missing:
.
How can I show the lines?
var width = 300, height = 300
var nodes = [{}, {}, {}, {}, {}]
var links = [
{source: 0, target: 1},
{source: 0, target: 2},
{source: 0, target: 3},
{source: 3, target: 4},]
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.force('link', d3.forceLink().links(links))
.on('tick', ticked);
function ticked() {
var u = d3.select('svg')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', 5)
.attr('cx', function(d) {
return d.x
})
.attr('cy', function(d) {
return d.y
});}

Draw links with a <line> element:
const width = 100;
const height = 100
const nodes = [{}, {}, {}, {}, {}]
const links = [
{source: 0, target: 1},
{source: 0, target: 2},
{source: 0, target: 3},
{source: 3, target: 4}
];
const simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.force('link', d3.forceLink().links(links))
.on('tick', ticked);
function ticked() {
const svg = d3.select('svg');
svg.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', 5)
.attr('cx', d => d.x)
.attr('cy', d => d.y);
svg.selectAll('line')
.data(links)
.join('line')
.attr('x1', d => d.source.x)
.attr('x2', d => d.target.x)
.attr('y1', d => d.source.y)
.attr('y2', d => d.target.y)
.style('stroke', 'black')
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width="200" heihgt="250" />

Related

How to center a List inside a D3 circle

I have a List of items that are inside a circle. I am using hardcoded values for the alignment. I need it to be based off the central point of the circle and by the length of the array.
Need to get rid of these "yAxis: -40, yAxis: -40, yAxis: 0, yAxis: 20";
And also have some space between line items.
const w = 500,
h = 400,
r = 160;
const STREAMS = [{
label: 'Emissions',
isSelected: true,
yAxis: -40
}, {
label: 'Energy Produced',
isSelected: false,
yAxis: -20
}, {
label: 'Energy Consumed',
isSelected: false,
yAxis: 0
}, {
label: 'Intensity',
isSelected: false,
yAxis: 20
}]
const SUB_STREAMS = [{
value: 0.15,
label: 'Total',
isSelected: true
}, {
value: 0.2,
label: 'CO2',
isSelected: false
}, {
value: 0.25,
label: 'Methane',
isSelected: false
}, {
value: 0.30,
label: 'N2O',
isSelected: false
}, {
value: 0.35,
label: 'Other',
isSelected: false
}];
const svg = d3.select("#foo")
.append("svg")
.attr("width", w)
.attr("height", h);
const g = svg.append("g")
.attr("transform", "translate(" + [w / 2, h / 2] + ")");
g.append("circle")
.attr("r", r)
.style("fill", "none")
.style("stroke", "black");
const points = g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append("circle")
.attr('stroke', 'dodgerblue')
.attr('stroke-width', 1)
.style("fill", function(d) {
return d.isSelected ? 'dodgerblue' : 'white'
})
.attr("r", 12)
.attr("cx", function(d) {
return r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2)
})
.attr("cy", function(d) {
return r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2)
})
points.on("click", function(d) {
console.log(d)
})
g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append('text')
.style('cursor', 'pointer')
.style('fill', 'black')
.attr('text-anchor', 'right')
.attr('font-size', '1.3em')
.attr('dx', (d) => 14 + r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2))
.attr('dy', (d) => r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2))
.text((d) => d.label)
const text = g
.selectAll('path')
.data(STREAMS)
.enter()
.append("text")
.attr("text-anchor", "left")
.attr('font-size', '1em')
.attr("y", function(d, a) {
return d.yAxis - 5
})
.text((d) => d.label);
text.on("click", function(d) {
console.log(d)
})
var arc = d3.symbol().type(d3.symbolTriangle)
var line = g.selectAll('path')
.data(STREAMS)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', 'red')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', function(d) {
return `translate(-10,${d.yAxis - 5}) rotate(210)`;
});
text {
dominant-baseline: central;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="foo" />
One solution out of many is setting a padding...
const padding = 20
...and translating the container groups by their indices times that padding:
const groups = g
.selectAll('path')
.data(STREAMS)
.enter()
.append("g")
.attr("transform", (_, i) => "translate(0," +
(-padding * (STREAMS.length - 1) / 2 + i * padding) + ")");
Then, you append both texts and paths to those groups.
Here is your code with those changes:
const w = 500,
h = 400,
r = 160,
padding = 20;
const STREAMS = [{
label: 'Emissions',
isSelected: true
}, {
label: 'Energy Produced',
isSelected: false
}, {
label: 'Energy Consumed',
isSelected: false
}, {
label: 'Intensity',
isSelected: false
}]
const SUB_STREAMS = [{
value: 0.15,
label: 'Total',
isSelected: true
}, {
value: 0.2,
label: 'CO2',
isSelected: false
}, {
value: 0.25,
label: 'Methane',
isSelected: false
}, {
value: 0.30,
label: 'N2O',
isSelected: false
}, {
value: 0.35,
label: 'Other',
isSelected: false
}];
const svg = d3.select("#foo")
.append("svg")
.attr("width", w)
.attr("height", h);
const g = svg.append("g")
.attr("transform", "translate(" + [w / 2, h / 2] + ")");
g.append("circle")
.attr("r", r)
.style("fill", "none")
.style("stroke", "black");
const points = g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append("circle")
.attr('stroke', 'dodgerblue')
.attr('stroke-width', 1)
.style("fill", function(d) {
return d.isSelected ? 'dodgerblue' : 'white'
})
.attr("r", 12)
.attr("cx", function(d) {
return r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2)
})
.attr("cy", function(d) {
return r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2)
})
points.on("click", function(d) {
console.log(d)
})
g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append('text')
.style('cursor', 'pointer')
.style('fill', 'black')
.attr('text-anchor', 'right')
.attr('font-size', '1.3em')
.attr('dx', (d) => 14 + r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2))
.attr('dy', (d) => r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2))
.text((d) => d.label)
const groups = g
.selectAll('path')
.data(STREAMS)
.enter()
.append("g")
.attr("transform", (_, i) => "translate(0," + (-padding * (STREAMS.length - 1) / 2 + i * padding) + ")");
groups.append("text")
.attr('font-size', '1em')
.text((d) => d.label)
.on("click", function(d) {
console.log(d)
})
var arc = d3.symbol().type(d3.symbolTriangle)
groups.append('path')
.attr('d', arc)
.attr('fill', 'red')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', function(d) {
return "translate(-10,0) rotate(210)";
});
text {
dominant-baseline: central;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="foo" />
And here the same code, with a bigger data array, so you can see that it dynamically sets the positions according to the number of elements:
const w = 500,
h = 400,
r = 160,
padding = 20;
const STREAMS = [{
label: 'Emissions',
isSelected: true
}, {
label: 'Energy Produced',
isSelected: false
}, {
label: 'Energy Consumed',
isSelected: false
}, {
label: 'Intensity',
isSelected: false
}, {
label: 'Foo',
isSelected: false
}, {
label: 'Bar',
isSelected: false
}, {
label: 'Baz',
isSelected: false
}]
const SUB_STREAMS = [{
value: 0.15,
label: 'Total',
isSelected: true
}, {
value: 0.2,
label: 'CO2',
isSelected: false
}, {
value: 0.25,
label: 'Methane',
isSelected: false
}, {
value: 0.30,
label: 'N2O',
isSelected: false
}, {
value: 0.35,
label: 'Other',
isSelected: false
}];
const svg = d3.select("#foo")
.append("svg")
.attr("width", w)
.attr("height", h);
const g = svg.append("g")
.attr("transform", "translate(" + [w / 2, h / 2] + ")");
g.append("circle")
.attr("r", r)
.style("fill", "none")
.style("stroke", "black");
const points = g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append("circle")
.attr('stroke', 'dodgerblue')
.attr('stroke-width', 1)
.style("fill", function(d) {
return d.isSelected ? 'dodgerblue' : 'white'
})
.attr("r", 12)
.attr("cx", function(d) {
return r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2)
})
.attr("cy", function(d) {
return r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2)
})
points.on("click", function(d) {
console.log(d)
})
g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append('text')
.style('cursor', 'pointer')
.style('fill', 'black')
.attr('text-anchor', 'right')
.attr('font-size', '1.3em')
.attr('dx', (d) => 14 + r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2))
.attr('dy', (d) => r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2))
.text((d) => d.label)
const groups = g
.selectAll('path')
.data(STREAMS)
.enter()
.append("g")
.attr("transform", (_, i) => "translate(0," + (-padding * (STREAMS.length - 1) / 2 + i * padding) + ")");
groups.append("text")
.attr('font-size', '1em')
.text((d) => d.label)
.on("click", function(d) {
console.log(d)
})
var arc = d3.symbol().type(d3.symbolTriangle)
groups.append('path')
.attr('d', arc)
.attr('fill', 'red')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', function(d) {
return "translate(-10,0) rotate(210)";
});
text {
dominant-baseline: central;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="foo" />

d3.js Compressing links of a component

I am trying to select a set of nodes in a Force Directed Layout graph in d3, then to compress the component the nodes form. My idea was to make a force simulation, as shown below:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(function(d) {
return d.distance;
}).strength(0.5))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
Since it relies on distance, I thought finding and selecting the appropriate links in the graph's data and shrinking it, such as
graph_data.links[indx].distance = 0;
would compress it. When I think about it, I would have to refresh the graph in some way with this new data. However, that is not ideal as I do not want the graph to rebuild itself every time I select a component. Is there a way to change these distances without having to feed a redrawn graph newly modified data, such as selecting the link in the simulated graph directly rather than the passed data?
However, that is not ideal as I do not want the graph to rebuild itself every time I select a component
You don't really have to, just update the data and restart the simulation:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<body>
<svg height="500" width="500"></svg>
<script>
var svg = d3.select('svg'),
width = +svg.attr('width'),
height = +svg.attr('height');
var data = {
nodes: [
{ id: 'a' },
{ id: 'b' },
{ id: 'c' },
{ id: 'x' },
{ id: 'y' },
{ id: 'z' },
],
links: [
{ source: 'a', target: 'b', distance: 200 },
{ source: 'b', target: 'c', distance: 200 },
{ source: 'c', target: 'a', distance: 200 },
{ source: 'x', target: 'y', distance: 200 },
{ source: 'y', target: 'z', distance: 200 },
{ source: 'z', target: 'x', distance: 200 },
],
};
var simulation = d3
.forceSimulation()
.force(
'link',
d3
.forceLink()
.id((d) => d.id)
.distance(function (d) {
return d.distance;
})
.strength(0.5)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
var link = svg
.append('g')
.attr('class', 'links')
.selectAll('line')
.data(data.links)
.enter()
.append('line')
.attr('stroke', 'black');
var node = svg
.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(data.nodes)
.enter()
.append('circle')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('r', 20)
.on('click', function (e, d) {
link.data().forEach(function (l) {
if (l.source.id === d.id || l.target.id === d.id) {
l.distance = 0;
} else {
l.distance = 200;
}
});
// re-bind data
simulation.force('link').links(data.links);
// restart simulation
simulation.alpha(1).restart();
});
simulation.nodes(data.nodes).on('tick', ticked);
simulation.force('link').links(data.links);
function ticked() {
node.attr('cx', (d) => d.x).attr('cy', (d) => d.y);
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);
}
</script>
</body>
</html>

Wrong nodes connection in a directed graph with d3.js

I am developing a directed graph with nodes in static position using d3.js library.
Currently I am having 3 nodes, and I want to connect the two computers with the printer. The edges are set to different positions as seen in this image:
How can i make the lines connect the nodes based on the nodes position?
Here is the code I have so far.
var width = 640,
height = 400;
var nodes = [
{ x: 184.53020651496104, y: 0, id: 0, url:
"http://icons.iconarchive.com/icons/tpdkdesign.net/refresh-
cl/32/Hardware-My-Computer-3-icon.png"},
{ x: 100, y: 150, id: 1, url:
"http://icons.iconarchive.com/icons/tpdkdesign.net/refresh-
cl/32/Hardware-My-Computer-3-icon.png" },
{ x: width/3, y: height/2, id: 2, url:
"http://icons.iconarchive.com/icons/tpdkdesign.net/refresh-
cl/32/Hardware-Printer-Blue-icon.png" },
];
var links = [
{ source: 1, target: 2 },
{ source: 0, target: 2 }
];
var graph = d3.select('#graph');
var svg = graph.append('svg')
.attr('width', width)
.attr('height', height);
var force = d3.layout.force()
.size([width, height])
.nodes(nodes)
.links(links);
force.linkDistance(width/2);
var link = svg.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link');
var node = svg.selectAll('.node')
.data(nodes)
.enter().append("image")
.attr("xlink:href", d=> d.url)
.attr("x", d=> d.x)
.attr("y", d=> d.y)
.attr("width", 30)
.attr("height", 30)
.attr('class', 'node');
force.on('end', function() {
node.attr('r', width/25)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return 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; });
});
force.start();
Thank you in advance.

D3 draw sub circle

I'm new to D3 and I'm trying to draw something like this
What I have done so far is here:
https://jsfiddle.net/834wg3g9/
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.cover {
width: 400px;
height: 400px;
/* border: 1px solid; */
margin: auto;
margin-top: 100px;
}
</style>
<div class="cover">
<svg></svg>
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// let width = window.innerWidth
// let height = window.innerHeight
let width = 400
let height = 400
let svg = d3.select("svg")
svg.attr("width", width)
.attr("height", height)
// let nodes = [
// {'id': 0,'r': 50, 'color': '#E8837B'},
// {'id': 1,'r': 100, 'color': '#FBCB43'},
// {'id': 2,'r': 150, 'color': '#16A05D'},
// {'id': 3,'r': 200, 'color': '#4C8BF5'},
// ]
let nuts = [
{r: 15},
{r: 15},
{r: 15},
{r: 15},
{r: 15},
{r: 15},
]
let nodes = [
{'id': 3,'r': 400, 'color': '#E8837B'},
{'id': 1,'r': 330, 'color': '#FBCB43'},
{'id': 2,'r': 250, 'color': '#16A05D'},
{'id': 0,'r': 150, 'color': '#4C8BF5'},
]
let simulation = d3.forceSimulation()
.force('center', d3.forceCenter(width/2, height/2))
let simulation_2 = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(-30))
.force('center', d3.forceCenter(width/2, height/2))
let nodeElements = svg.append('g')
.selectAll('circle')
.data(nodes)
.enter().append('circle')
.attr("class", "node")
.attr('r', function(d) {
return d.r
})
.attr('fill', function(d) {
return d.color
})
let bottomLineElements = svg.append('g')
.selectAll('text')
.data(nodes)
.enter().append('text')
.text(function(d) {
return d.color
})
let nutElements = svg.append('g')
.selectAll('circle')
.data(nuts)
.enter().append('circle')
.attr('fill', 'white')
.attr('r', function(d) { return d.r })
simulation_2.nodes(nuts)
.on('tick', function() {
nutElements
.attr('cx', function (d) { return d.x })
.attr('cy', function (d) { return d.y })
})
simulation.nodes(nodes)
.on('tick', function() {
nodeElements
.attr('cx', function (d) { return 400 })
.attr('cy', function (d) { return 400 })
bottomLineElements
.attr('x', function (d) { return 20 + (d.id * 80) })
.attr('y', function (d) { return 400 })
})
</script>
Now I'm stuck at draw "New or moved" and "No change" part
How can append a bunch of sub circle to each area and make sure they don't display in wrong area
For example:
hold = [{}, {}, {}]
so 3 small circles will display in Hold area
Any advice, keywords to solve this
Thank you.

In d3 force directed, how do I change a nodes shape by clicking on it?

I have a force directed graph in d3, and want to be able to click on the circular nodes and have them turn into rectangles. Then if I click on a rectangle, I'd like it to revert to a circle.
I have looked at this and related questions on SO, but I think they are for earlier versions of D3, and do not work for me.
I can make it so the size and colour of my circles will change on click, and with the following code I can have the circle node replaced with a black rect, however it is not attached to the graph and is just a black square on the svg.
node.on("click", function(d,i) {
var size = 20;
d3.select(this).remove();
svg.append("rect")
.attr("x", d.x)
.attr("y", d.y)
.attr("height", size)
.attr("width", size)
.style("fill", function(d) {
return color( d.group);
});
})
Can anyone show me what I'm missing? I suspect rect is not being attached to the graph data but I am not familar enough with d3 to understand what I should change. Thank you.
It makes little sense when you say:
I have looked at this and related questions on SO, but I think they are for earlier versions of D3, and do not work for me.
It seems to me that nothing in that answer suggests that it won't work in D3 v4.x. It's worth mentioning that, in the answer (and the question) you linked, node is a group element, and therefore this refers to the group, not to the circle/rectangle.
Moving on, a possible solution (which doesn't involve removing and appending elements) is to simulate a circle with a rectangle:
node.append("rect")
.attr("width", 16)
.attr("height", 16)
.attr("rx", 8)
.attr("ry", 8)
And, inside the click function, changing rx and ry:
function click() {
if(d3.select(this).attr("rx") == 8){
d3.select(this).attr("rx", 0).attr("ry", 0);
} else {
d3.select(this).attr("rx", 8).attr("ry", 8);};
};
Here is a demo:
var nodes = [
{"id": 1},
{"id": 2},
{"id": 3},
{"id": 4},
{"id": 5},
{"id": 6},
{"id": 7},
{"id": 8},
{"id": 9},
{"id": 10},
{"id": 11},
{"id": 12}
];
var links = [
{source: 1, target: 8},
{source: 1, target: 3},
{source: 1, target: 4},
{source: 1, target: 9},
{source: 1, target: 10},
{source: 1, target: 11},
{source: 2, target: 5},
{source: 2, target: 6},
{source: 2, target: 7},
{source: 2, target: 12},
{source: 2, target: 4},
{source: 2, target: 8},
{source: 6, target: 7},
{source: 6, target: 8},
{source: 6, target: 9},
{source: 6, target: 5},
{source: 6, target: 3},
{source: 6, target: 9},
]
var index = 10;
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
node,
link;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("collide", d3.forceCollide(30))
.force("center", d3.forceCenter(width / 2, height / 2));
update();
function update() {
link = svg.selectAll(".link")
.data(links, function(d) { return d.target.id; })
link = link.enter()
.append("line")
.attr("class", "link");
node = svg.selectAll(".node")
.data(nodes, function(d) { return d.id; })
node = node.enter()
.append("g")
.attr("class", "node");
node.append("rect")
.attr("width", 16)
.attr("height", 16)
.attr("rx", 8)
.attr("ry", 8)
.attr("fill", "teal")
.on("click", click);
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function click() {
if(d3.select(this).attr("rx") == 8){d3.select(this).attr("rx", 0).attr("ry", 0);}
else{d3.select(this).attr("rx", 8).attr("ry", 8);};
}
function ticked() {
link
.attr("x1", function(d) { return d.source.x + 8; })
.attr("y1", function(d) { return d.source.y + 8; })
.attr("x2", function(d) { return d.target.x+ 8; })
.attr("y2", function(d) { return d.target.y+ 8; });
node
.attr("transform", function(d) { return "translate(" + d.x + ", " + d.y + ")"; });
}
.link {
stroke: #aaa;
}
.node {
stroke: none;
stroke-width: 40px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="300"></svg>

Categories