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>
Related
How do I achieve this with D3? desired output
It's easy to have two layers of pie charts https://embed.plnkr.co/plunk/2p0zmp
Or to use d3 network with graph and nodes, http://using-d3js.com/05_08_links.html
but how could I overlay the concept of "nodes" and "links" onto these arcs of a piechart?
What kind of data structure is preferred?
{
nodes: [
{
layer: 1,
data: [
{name: A },
{name: B },
{name: C },
{name: D }
]
},
{
layer: 2,
data: [
{name: E },
{name: F },
{name: G }
]
}
],
links: [{ source: 'B', target: 'E'}, { source: 'D', target: 'F'}]
}
This gets pretty close with what you're looking for. You can use some additional arc generators and arc.centroid() to get the positions for the start and ends of the links. Then you can use a link generator to draw the links. One drawback of this is that the links can overlap the nodes.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
/*
set up
*/
const width = 700;
const height = 400;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g')
.attr('transform', `translate(${width / 2},${height})`);
/*
data
*/
const level1Nodes = [
{ name: 'A', value: 50, level: 1, color: 'RoyalBlue' },
{ name: 'B', value: 50, level: 1, color: 'DarkOrange' },
{ name: 'C', value: 50, level: 1, color: 'DarkOrange' },
{ name: 'D', value: 30, level: 1, color: 'Gold' }
];
const level2Nodes = [
{ name: 'E', value: 75, level: 2, color: 'RoyalBlue' },
{ name: 'F', value: 75, level: 2, color: 'DarkOrange' },
{ name: 'G', value: 30, level: 2, color: 'RoyalBlue' },
];
const links = [
{ source: 'B', target: 'E'},
{ source: 'D', target: 'F'}
];
/*
pie generator
*/
const pie = d3.pie()
.value(d => d.value)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.padAngle(Math.PI / 45);
// calculate the angles for the slices of the nodes
const slices = [
...pie(level1Nodes),
...pie(level2Nodes)
];
/*
arcs
*/
const level1InnerRadius = 130;
const level1OuterRadius = 200;
const level2InnerRadius = 270;
const level2OuterRadius = 340;
// for drawing the nodes
const level1Arc = d3.arc()
.innerRadius(level1InnerRadius)
.outerRadius(level1OuterRadius);
const level2Arc = d3.arc()
.innerRadius(level2InnerRadius)
.outerRadius(level2OuterRadius);
const levelToArc = new Map([
[1, level1Arc],
[2, level2Arc]
]);
// for positioning the links along the outside
// of the level 1 nodes and the inside of the
// level 2 nodes
const level1OuterArc = d3.arc()
.innerRadius(level1OuterRadius)
.outerRadius(level1OuterRadius);
const level2InnerArc = d3.arc()
.innerRadius(level2InnerRadius)
.outerRadius(level2InnerRadius);
/*
calculating position of links
*/
// Map from the name of a node to the data for its arc
const nameToSlice = d3.index(slices, d => d.data.name);
// get the start and end positions for each link
const linkPositions = links.map(({source, target}) => ({
source: level1OuterArc.centroid(nameToSlice.get(source)),
target: level2InnerArc.centroid(nameToSlice.get(target)),
}));
/*
drawing
*/
// nodes
g.append('g')
.selectAll('path')
.data(slices)
.join('path')
.attr('d', d => levelToArc.get(d.data.level)(d))
.attr('fill', d => d.data.color);
// node labels
const labelsGroup = g.append('g')
.attr('font-family', 'sans-serif')
.attr('font-weight', 'bold')
.attr('font-size', 30)
.selectAll('text')
.data(slices)
.join('text')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.attr('transform', d => `translate(${levelToArc.get(d.data.level).centroid(d)})`)
.text(d => d.data.name);
// links
g.append('g')
.selectAll('path')
.data(linkPositions)
.join('path')
.attr('d', d3.linkVertical())
.attr('fill', 'none')
.attr('stroke', 'DarkBlue')
.attr('stroke-width', 2);
// circles at the end of links
g.append('g')
.selectAll('circle')
.data(linkPositions.map(({source, target}) => [source, target]).flat())
.join('circle')
.attr('r', 5)
.attr('fill', 'DarkBlue')
.attr('transform', d => `translate(${d})`);
</script>
</body>
</html>
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" />
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.
I am a newbie in d3.js. I have tried to create a static architecture with 5 nodes and link them with each other according to preferences, the nodes should be organized like so:
At the beginning I set the position of the nodes and then create the links. Though, when the nodes get linked, the architecture changes and the result is the one displayed below:
Here is my code:
var width = 640,
height = 400;
var nodes = [
{ x: 60, y: 0, id: 0},
{ x: 150, y: height/4, id: 1},
{ x: 220, y: height/4, id: 2},
{ x: 340, y: height/4, id: 3},
{ x: 420, y: height/2, id: 4},
{ x: 480, y: height/2, id: 5}
];
var links = [
{ source: 1, target: 5 },
{ source: 0, target: 5 },
{ source: 2, target: 1 },
{ source: 3, target: 2 },
{ source: 4, target: 5 }
];
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 div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 1e-6);
var node = svg.selectAll('.node')
.data(nodes)
.enter().append("circle")
.attr("cx", d=> d.x)
.attr("cy", d=> d.y)
.attr('class', 'node')
.on("mouseover", function(d){
d3.select(this)
.transition()
.duration(500)
.style("cursor", "pointer")
div
.transition()
.duration(300)
.style("opacity", "1")
.style("display", "block")
console.log("label", d.label);
div
.html("IP: " + d.label + " x: " + d.x + " y: " + d.y)
.style("left", (d3.event.pageX ) + "px")
.style("top", (d3.event.pageY) + "px");
})
.on("mouseout", mouseout);
function mouseout() {
div.transition()
.duration(300)
.style("opacity", "0")
}
console.log("wait...");
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) { console.log("LINE x1-> ", d.source.x); return d.source.x; })
.attr('y1', function(d) { console.log("LINE y1-> ", d.source.y); return d.source.y; })
.attr('x2', function(d) { console.log("LINE x2-> ", d.source.x); return d.target.x; })
.attr('y2', function(d) { console.log("LINE y2-> ", d.source.y); return d.target.y; })
.attr("stroke-width", 2)
.attr("stroke","black");
});
force.start();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="graph"></div>
Could you please help me?
Thank you in advance.
A force layout offers some advantages that derive from its nature as a self organizing layout:
It places nodes and links automatically avoiding manual positioning of potentially thousands of elements
It organizes nodes and links based on assigned forces to an ideal spacing and layout
You have nodes to which you have already assigned positions, the two advantages listed above do not apply. You've already manually done the first item, and the second item will disturb and overwrite the positions you manually set.
We could fix the node positions, but if we do this with all nodes, it defeats the purpose of the force layout: to position nodes by simulating forces.
Instead, if you have the position of all nodes, we can skip the force and just append everything based on the data. The snippet below places the links first (so they are behind the nodes) using the index contained in d.source/d.target to access the specific node in the nodes array and get the appropriate x or y coordinate. The nodes are positioned normally.
It appears you have adjusted the code to use circles in your question though the screenshot uses images (as you also used in a previous question), I'll just use circles here. Based on the coordinates you've given some lines overlap. I modified the first node so that the y value wasn't 0 (which would have pushed half the circle off the svg)
var width = 640,
height = 400;
var nodes = [
{ x: 60, y: height/8, id: 0},
{ x: 150, y: height/4, id: 1},
{ x: 220, y: height/4, id: 2},
{ x: 340, y: height/4, id: 3},
{ x: 420, y: height/2, id: 4},
{ x: 480, y: height/2, id: 5}
];
var links = [
{ source: 1, target: 5 },
{ source: 0, target: 5 },
{ source: 2, target: 1 },
{ source: 3, target: 2 },
{ source: 4, target: 5 }
];
var graph = d3.select('#graph');
var svg = graph.append('svg')
.attr('width', width)
.attr('height', height);
// append links:
svg.selectAll()
.data(links)
.enter()
.append("line")
.attr("x1", function(d) { return nodes[d.source].x; })
.attr("y1", function(d) { return nodes[d.source].y; })
.attr("x2", function(d) { return nodes[d.target].x; })
.attr("y2", function(d) { return nodes[d.target].y; })
.attr("stroke-width", 2)
.attr("stroke","black");
// append nodes:
svg.selectAll()
.data(nodes)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 8);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="graph"></div>
I'm currently fiddling with polygons in d3 and would like to update individual polygons whilst the user is dragging a point. Drawing them initially works fine, but I can't get the update to work. The fiddle below contains my awful attempt at getting it to work:
https://jsfiddle.net/z4g5817z/9/
Relevant code:
const areas = [{
uid: 'ajf9v0',
points: [{
x: 52,
y: 92
},
{
x: 50,
y: 151
},
{
x: 123,
y: 149
},
{
x: 125,
y: 91
}
],
foo: 'bar',
// ...
},
{
uid: 'ufnf12',
points: [{
x: 350,
y: 250
},
{
x: 450,
y: 250
},
{
x: 450,
y: 275
},
{
x: 350,
y: 275
}
],
foo: 'baz',
// ...
}
];
const svg = d3.select('#root');
svg.attr('width', 500)
.attr('height', 500);
const areasGroup = svg.append('g')
.attr('class', 'areas');
function drawAreas(areas) {
console.log('Called draw');
const self = this;
const aGroup = areasGroup.selectAll('g.area')
.data(areas, (d) => {
console.log('Areas', d.points.map((d) => [d.x, d.y].join('#')).join('#'));
return d.points.map((d) => [d.x, d.y].join('#')).join('#');
});
areasGroup.exit().remove();
const areaGroups = aGroup.enter()
.append('g')
.attr('class', 'area');
//const areaPolygon = area.append('g')
// .attr('class', 'polygon');
//const areaPoints = area.append('g')
// .attr('class', 'points');
const polygon = areaGroups.selectAll('polygon')
.data((d) => {
console.log('Polygon data points', [d.points]);
return [d.points];
}, (d) => {
console.log('Polygon key', d.map((d) => [d.x, d.y].join('#')).join('#'));
return d.map((d) => [d.x, d.y].join('#')).join('#');
});
polygon.enter()
.append('polygon')
.merge(polygon)
.attr('points', (d) => {
console.log('Polygon points', d);
return d.map((d) => [d.x, d.y].join(',')).join(' ');
})
.attr('stroke', '#007bff')
.attr('stroke-width', 1)
.attr('fill', '#007bff')
.attr('fill-opacity', 0.25)
.on('click', this.handlePolygonSelection)
polygon.exit().remove();
const circles = areaGroups.selectAll('circle')
.data((d) => d.points, (d) => d.x + '#' + d.y);
circles.enter()
.append('circle')
.attr('r', 4)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('fill', '#007bff')
.on('click', (d, idx, j) => {
const parentArea = d3.select(j[idx].parentNode).datum().points;
const i = parentArea.findIndex((p) => p.x === d.x && p.y === d.y);
if (i === parentArea.length) {
parentArea.pop();
} else if (i === 0) {
parentArea.shift();
} else {
parentArea.splice(i, 1);
}
this.drawAreas(areas);
})
.call(d3.drag()
.on('start', function(d) {
d3.select(this).classed('active', true)
})
.on('drag', function(d) {
d3.select(this)
.attr('cx', d.x = d3.event.x)
.attr('cy', d.y = d3.event.y);
self.drawAreas(areas);
})
.on('end', function(d) {
d3.select(this).classed('active', false)
}));
circles.exit().remove();
}
this.drawAreas(areas);
Thank you to anybody who takes time to have a look, any and all help is appreciated.
So it looks like I found the issue: https://jsfiddle.net/z4g5817z/91/
Changing
const polygon = areaGroups.selectAll('polygon')
to
const polygon = areasGroup.selectAll('g.area').selectAll('polygon')
seems to have fixed it. I'm assuming this has to do with the areaGroups selection only handling enter events.
An alternative would be to keep it the way it is now and change
const areaGroups = aGroup.enter()
.append('g')
.attr('class', 'area');
to
const areaGroups = aGroup.enter()
.append('g')
.merge(aGroup)
.attr('class', 'area');
which will produce the same result, as the update event is now also handled appropriately.