Related
I'm trying to reproduce with d3.js the behaviour of a pull cord light switch.
You can see the code running here or below in the snippet (best to view full screen).
My question is how can I set the distance between nodes to be always the same (as it is in a real cord)?
The only link that should stretch is the one between the two green nodes
I tried to add more strength to the forces but doesn't look good.
//create somewhere to put the force directed graph
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
const nodes_data = [
{ name: "c1" },
{ name: "c2" },
{ name: "c3" },
{ name: "c4" },
{ name: "c5" },
{ name: "c6" },
];
const links_data = [
{ source: "c1", target: "c2" },
{ source: "c2", target: "c3" },
{ source: "c3", target: "c4" },
{ source: "c4", target: "c5" },
{ source: "c5", target: "c6" },
];
//set up the simulation
const simulation = d3.forceSimulation().nodes(nodes_data);
//add forces
simulation.force(
"manyBody",
d3.forceManyBody().distanceMin(20).distanceMax(21)
);
const link_force = d3.forceLink(links_data).distance(40).strength(1);
link_force.id(function (d) {
return d.name;
});
simulation.force("links", link_force);
simulation.force("centerx", d3.forceX(width / 2).strength(0.3));
simulation.force(
"centery",
d3
.forceY()
.y(function (d, i) {
return height / 10 + i * 35;
})
.strength(function (d, i) {
return 0.4;
})
);
//draw circles for the nodes
const node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes_data)
.enter()
.append("circle")
.attr("r", 10)
.attr("fill", "red")
.attr("draggable", "true");
const circles = d3.selectAll("circle")._groups[0];
const firstCircle = d3.select(circles[0]);
const secondCircle = d3.select(circles[1]);
const lastCircle = d3.select(circles[circles.length - 1]);
firstCircle.attr("fill", "green").text(function (d) {
d.fx = width / 2;
d.fy = height / 10;
console.log(d.fx, d.fy);
});
secondCircle.attr("fill", "green");
lastCircle.attr("fill", "blue");
//draw lines for the links
const link = svg
.append("g")
.attr("class", "links")
.selectAll("line")
.data(links_data)
.enter()
.append("line")
.attr("stroke-width", 2);
// The complete tickActions() function
function tickActions() {
//update circle positions each tick of the simulation
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
//update link positions
//simply tells one end of the line to follow one node around
//and the other end of the line to follow the other node around
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;
});
}
simulation.on("tick", tickActions);
const drag_handler = d3
.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
function drag_start(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function drag_drag(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function drag_end(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
d3.forceY().strength(0.1);
document.body.style.background == "black"
? (document.body.style.background = "white")
: (document.body.style.background = "black");
console.log(document.body.style.background == "black");
}
drag_handler(lastCircle);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width="400" height="400"></svg>
thanks
D3 isn't likely to create a perfect solution without modifying how the force layout works. Staying within the bounds of D3, I have a solution that achieves the desired result (with a minimal bit of elasticity, which may be acceptable).
As I noted in the comment, d3 is balancing a bunch of forces while the simulation runs. As a consequence, the resulting layout is a compromise between the forces. The solution I linked to in my comment gets links of a specified link by dialing down all the other forces as the simulation cools, allowing the other forces to influence the general layout, while the link distance force tweaks the result to ensure links are the proper length.
The same principle can be applied here, but without the benefit of multiple cycles to nudge the nodes to the precise location required.
First we declare all our forces, as usual:
var manybody = d3.forceManyBody().distanceMin(20).distanceMax(21);
var x = d3.forceX(width / 6).strength(0.3)
var y = d3.forceY().y(function (d, i) { return height / 10 + i * 35; })
.strength(0.4)
var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
.distance(35)
.id(function(d) { return d.name; })
.strength(1);
Then we apply them:
simulation
.force("centerx",x)
.force("centery",y)
.force("link", distance)
.force("many", manybody);
Then in the drag start function, we remove all forces except for the link distance function. We also up the alpha and eliminate alpha decay to allow the force to move the nodes as close as possible in a single tick to their intended place:
function drag_start(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
// Disable other forces:
simulation.force("centerx",null)
.force("centery",null)
.force("many",null);
// Juice the alpha:
simulation.alpha(1)
.alphaDecay(0)
}
At the end of the drag, we undo the changes we made on drag start by reapplying the forces, decreasing alpha, and increasing alpha decay:
function drag_end(event, d) {
// Reapply forces:
simulation.force("centerx",x)
.force("centery",y)
.force("many",manybody);
// De-juice the alpha:
simulation.alpha(0.2)
.alphaDecay(0.0228)
...
There are a few idiosyncrasies in the code as compared with canonical D3, but I've just implemented the changes from above:
//create somewhere to put the force directed graph
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
const nodes_data = [
{ name: "c1" },
{ name: "c2" },
{ name: "c3" },
{ name: "c4" },
{ name: "c5" },
{ name: "c6" },
];
const links_data = [
{ source: "c1", target: "c2" },
{ source: "c2", target: "c3" },
{ source: "c3", target: "c4" },
{ source: "c4", target: "c5" },
{ source: "c5", target: "c6" },
];
//set up the simulation
const simulation = d3.forceSimulation().nodes(nodes_data);
////////////////////////
// Changes start: (1/2)
// Set up forces:
var manybody = d3.forceManyBody().distanceMin(15).distanceMax(15);
var x = d3.forceX(width / 6).strength(0.3)
var y = d3.forceY().y(function (d, i) { return 0 + i * 35; })
.strength(0.4)
var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
.distance(35)
.id(function(d) { return d.name; })
.strength(1);
simulation
.force("centerx",x)
.force("centery",y)
.force("link", distance)
.force("many", manybody);
// End Changes (1/2)
/////////////////////////
//draw circles for the nodes
const node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes_data)
.enter()
.append("circle")
.attr("r", 10)
.attr("fill", "red")
.attr("draggable", "true");
const circles = d3.selectAll("circle")._groups[0];
const firstCircle = d3.select(circles[0]);
const secondCircle = d3.select(circles[1]);
const lastCircle = d3.select(circles[circles.length - 1]);
firstCircle.attr("fill", "green").text(function (d) {
d.fx = width / 6;
d.fy = 0;
console.log(d.fx, d.fy);
});
secondCircle.attr("fill", "green");
lastCircle.attr("fill", "blue");
//draw lines for the links
const link = svg
.append("g")
.attr("class", "links")
.selectAll("line")
.data(links_data)
.enter()
.append("line")
.attr("stroke-width", 2);
// The complete tickActions() function
function tickActions() {
//update circle positions each tick of the simulation
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
//update link positions
//simply tells one end of the line to follow one node around
//and the other end of the line to follow the other node around
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;
});
}
simulation.on("tick", tickActions);
const drag_handler = d3
.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
////////////////////////
// Start changes (2/2)
function drag_start(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
// Disable other forces:
simulation.force("centerx",null)
.force("centery",null)
.force("many",null);
// Juice the alpha:
simulation.alpha(1)
.alphaDecay(0)
}
function drag_drag(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function drag_end(event, d) {
// Reapply forces:
simulation.force("centerx",x)
.force("centery",y)
.force("many",manybody);
// De-juice the alpha:
simulation.alpha(0.2)
.alphaDecay(0.0228)
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
d3.forceY().strength(0.1);
document.body.style.background == "black"
? (document.body.style.background = "white")
: (document.body.style.background = "black");
}
// End changes (2/2)
////////////////////////
drag_handler(lastCircle);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width=400 height=200></svg>
Optional Addition
I haven't empirically tested it, but it appeared to make a slight improvement: the first parameter for simulation.force() is just a name, so that you can replace or remove individual forces, you could potentially apply a force several times if you applied with different names. In the case of link distance, this could nudge links a bit closer each tick:
var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
.distance(35)
.id(function(d) { return d.name; })
.strength(1);
simulation.force("a", distance);
simulation.force("b", distance);
simulation.force("c", distance);
I try the neo4j movie example in spring boot.( source code : https://github.com/neo4j-examples/movies-java-spring-data-neo4j)
Example screenshot is at http://my-neo4j-movies-app.herokuapp.com/
I draw graph every page load and clear and redraw in every movie search operation according to new data.
But the graph overflows from screen
or I pressed mouse down and dragged the graph to new position. when I released the button then graph came back to old position.
How can i specify the limit in the screen for graph?
Or when i press mouse down button how can graph keep this position?
Here my js;
$(document).ready(function(){
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = 1000 - margin.right - margin.left,
height = 1000 - margin.top - margin.bottom;
var svg = d3.select("#graph").append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox','0 0'+' '+Math.min(width,height)+' '+Math.min(width,height))
.attr('preserveAspectRatio','xMinYMin')
.append("g")
.attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")")
.attr("pointer-events", "all");
});
$(function () {
function search() {
//do something
//...
drawGraph();
return false;
}
function drawGraph() {
var query=$("#search").find("input[name=search]").val();
d3.selectAll("svg > *").remove();//clear old graph
var width = 1000, height = 1000;
var force = d3.layout.force()
.charge(-200).linkDistance(30).size([width, height]);
var svg = d3.select("#graph").selectAll("svg");
d3.json("/graph?movieName="+encodeURIComponent(query), function(error, graph) {
if (error) return;
force.nodes(graph.nodes).links(graph.links).start();
var link = svg.selectAll(".link")
.data(graph.links).enter()
.append("line").attr("class", "link");
var node = svg.selectAll(".node")
.data(graph.nodes).enter()
.append("circle")
.attr("class", function (d) { return "node "+d.label })
.attr("r", 10)
.call(force.drag);
// html title attribute
node.append("title")
.text(function (d) {
return d.titleName;
})
// force feed algo ticks
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
}
});
I have been stuck on this problem from the other day, but unfortunately haven't been able to reach a solution. I am trying to achieve a behavior shown here, but in a way to generally do this for various containers depending on properties of each node. I wanted to reach out and ask if there was any known way to do this generally.
Below is my JSFiddle is an example of what I currently have - a number of nodes assigned to a random group number and a barView function which separates these nodes dependent on their groups. I hope to have these nodes be confined within the dimensions of each of their respective bars, such that dragging these nodes cannot remove them from their box, but they can move within it (bouncing off each other). I would really appreciate your help in this.
For simplicity, I made the bars related to a 'total' field in each node (to show the bars in the SVG dimensions), but these would be related to the size in my implementation, similar to a volume.
I have been able to organize the x-positions of the nodes by using the following code, where position is based on the group:
simulation.force('x', d3.forceX().strength(1).x(function(d) {
return xscale(d.group); // xvariable
}));
With this code, I am unsure how to work within the dimensions of the rectangles, or maintain a boundary that circles can bounce within. I would appreciate your help on this!
Thank you so much!
My fiddle: http://jsfiddle.net/abf2er7z/2/
One possible solution is setting a new tick function, which uses Math.max and Math.min to get the boundaries of those rectangles:
simulation.on("tick", function() {
node.attr("cx", function(d) {
return d.x = Math.min(Math.max(xscale(d.group) - 20 + d.radius, d.x), xscale(d.group) + 20 - d.radius);
})
.attr("cy", function(d) {
return d.y = Math.min(Math.max(0.9 * height - heightMap[d.group] + d.radius, d.y), height - d.radius);
});
});
Here is the demo:
var width = 900,
height = 400;
var groupList = ['Group A', 'Group B', 'Group C', 'Group D'];
var data = d3.range(200).map(d => ({
id: d,
group: groupList[getRandomIntegerInRange(0, 3)],
size: getRandomIntegerInRange(1, 100),
total: getRandomIntegerInRange(1, 10)
}))
var svg = d3.select("body")
.append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.attr("preserveAspectRatio", "xMidYMid meet")
.attr('width', "100%")
.attr('height', height)
.attr('id', 'svg')
.append('g')
.attr('id', 'container')
.attr('transform', 'translate(' + 0 + ', ' + 0 + ')');
simulation = d3.forceSimulation();
data.forEach(function(d, i) {
d.radius = Math.sqrt(d['size']);
});
colorScale = d3.scaleOrdinal(d3.schemeCategory10);
node = svg.append("g")
.attr("class", "node")
.selectAll(".bubble")
.data(data, function(d) {
return d.id;
})
.enter().append("circle")
.attr('class', 'bubble')
.attr('r', function(d) {
return d.radius;
}) // INITIALIZED RADII TO 0 HERE
.attr("fill", function(d) {
// initially sets node colors
return colorScale(d.group);
})
.attr('stroke-width', 0.5)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d) {
if (!d3.event.active) {
simulation.alpha(.07).restart()
}
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alpha(0.07).restart()
d.fx = null;
d.fy = null;
// Update and restart the simulation.
simulation.nodes(data);
}
simulation
.nodes(data)
.force("x", d3.forceX().strength(0.1).x(width / 2))
.force("y", d3.forceY().strength(0.1).y(height / 2))
.force("collide", d3.forceCollide().strength(0.7).radius(function(d) {
return d.radius + 0.5;
}).iterations(2))
.on("tick", function() {
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
});
function barView() {
var buff = width * 0.12
var leftBuff = buff;
var rightBuff = width - buff;
var scale;
xscale = d3.scalePoint()
.padding(0.1)
.domain(groupList)
.range([leftBuff, rightBuff]);
// Save double computation below.
heightMap = {}
groupList.forEach(function(d) {
currVarTotal = data.filter(function(n) {
return n.group === d;
}).reduce(function(a, b) {
return a + +b.total;
}, 0);
heightMap[d] = currVarTotal;
})
var rects = svg.selectAll('.rect')
.data(groupList)
.enter()
.append('rect')
.attr('x', function(d) {
return xscale(d) - 20
})
.attr('y', function(d) {
return 0.9 * height - heightMap[d];
})
.attr('width', 40)
.attr('height', function(d) {
return heightMap[d];
})
.attr('fill', 'transparent')
.attr('stroke', function(d) {
return colorScale(d)
})
.attr('stroke-width', 2)
.attr('class', 'chartbars');
drawTheAxis(xscale);
simulation.force('x', d3.forceX().strength(1).x(function(d) {
return xscale(d.group); // xvariable
})).on("tick", function() {
node
.attr("cx", function(d) {
return d.x = Math.min(Math.max(xscale(d.group) - 20 + d.radius, d.x), xscale(d.group) + 20 - d.radius);
})
.attr("cy", function(d) {
return d.y = Math.min(Math.max(0.9 * height - heightMap[d.group] + d.radius, d.y), height - d.radius);
});
});
currHeights = {}
Object.keys(heightMap).forEach(d => {
currHeights[d] = 0.9 * height
});
// restart the simulation
simulation.alpha(0.07).restart();
function drawTheAxis(scale) {
var bottomBuffer = 0.9 * height;
// create axis objects
var xAxis = d3.axisBottom(xscale);
// Draw Axis
var gX = svg.append("g") // old: nodeG.append
.attr("class", "xaxis")
.attr('stroke-width', 2)
.attr("transform", "translate(0," + height + ")")
.attr('opacity', 0)
.call(xAxis)
.transition()
.duration(250)
.attr('opacity', 1)
.attr("transform", "translate(0," + bottomBuffer + ")");
}
}
function getRandomIntegerInRange(min, max) {
return Math.floor(Math.random() * (Math.floor(max) - Math.ceil(min) + 1)) + Math.ceil(min);
}
setTimeout(function() {
barView();
}, 1500);
<script src="https://d3js.org/d3.v5.min.js"></script>
Have in mind that this is not a final solution, but just a general guidance: the transition, the math (with those magic numbers) and the scales need to be improved.
I am working on a d3js side project where I am using the force layout to show nodes and lines to represent a graph. I have laid out the code such that the graph can be dynamically updated. I am doing this by:
Clearing force.nodes() then force.links()
Pushing all the nodes I want to add to force.nodes()
Updating the graph (join, enter, update, exit)
Pushing all the links I want with references to the nodes in force.nodes() to force.links
Updating the graph (join, enter, update, exit)
This works as long as I only have nodes to display, but as soon as I attempt to push a link to force.links then all hell breaks loose and the two nodes end up hiding in the top left corner.
Looking at the DOM I can see the following:
As you can see, the transform/translate parameters contain NaN values. So something blew up in my face but after two days of bug hunting I suspect that I am missing something here and need to take a cold shower.
I have stripped down my code to the smallest set that can reproduce the error. Please note from it that the nodes display fine until a link is pushed into force.links. No links are being drawn, only nodes, but the act of pushing a link where it belongs is disrupting the data in the nodes. This way of updating a graph should work as per the examples I have seen.
d3.json("data/fm.json", function(error, graph) {
if (error) throw error;
function chart(elementName) {
// look for the node in the d3 layout
var findNode = function(name) {
for (var i in nodes) {
if (nodes[i]["name"] === name) return nodes[i];
};
};
var width = 960, // default width
height = 450, // default height
color = d3.scale.category10(),
force = d3.layout.force(),
nodes = force.nodes(),
links = force.links(),
vis,
runOnceFlag = true;
vis = d3.select(elementName)
.append("svg:svg")
.attr("width", width)
.attr("height", height)
.attr("id", "svg")
.attr("pointer-events", "all")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("perserveAspectRatio", "xMinYMid")
.append('svg:g');
var update = function() {
var node = vis.selectAll("g.node")
.data(nodes, function (d) {
return d.name;
});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", 12)
.attr("id", function (d) {
return "Node;" + d.name;
})
.attr("class", "nodeStrokeClass")
.attr("fill", function(d) { return color(d.group); });
nodeEnter.append("svg:text")
.attr("class", "textClass")
.attr("x", 14)
.attr("y", ".31em")
.text(function (d) {
return d.name;
});
node.exit().remove();
force.on("tick", function () {
node.attr("transform", function (d) {
console.log(d);
return "translate(" + d.x + "," + d.y + ")";
});
});
// Restart the force layout.
force
.charge(-120)
.linkDistance( function(d) { return d.value * 10 } )
.size([width, height])
.start();
};
var a = graph.nodes[0];
var b = graph.nodes[1]
nodes.push(a);
update();
nodes.push(b);
update();
var c = {"source": findNode('a'), "target": findNode('b')}
// the line below causes the error
links.push(c);
update()
};
///
chart('body');
});
This is my data:
{
"nodes":[
{"name":"a", "group":1},
{"name":"b", "group":2},
{"name":"c", "group":3},
{"name":"d", "group":4},
{"name":"e", "group":5},
{"name":"f", "group":6},
{"name":"g", "group":7},
{"name":"h", "group":1},
{"name":"i", "group":2},
{"name":"j", "group":3},
{"name":"k", "group":4},
{"name":"l", "group":5},
{"name":"m", "group":6},
{"name":"n", "group":7}
],
"links":[
{"source":0,"target":1,"value":1},
{"source":2,"target":3,"value":1},
{"source":4,"target":5,"value":1},
{"source":7,"target":8,"value":1},
{"source":9,"target":10,"value":1},
{"source":11,"target":12,"value":1},
{"source":0,"target":5,"value":1},
{"source":1,"target":5,"value":1},
{"source":0,"target":6,"value":1},
{"source":1,"target":6,"value":1},
{"source":0,"target":7,"value":1},
{"source":1,"target":7,"value":1},
{"source":2,"target":8,"value":1},
{"source":3,"target":8,"value":1},
{"source":2,"target":9,"value":1},
{"source":3,"target":9,"value":1},
{"source":4,"target":11,"value":1},
{"source":5,"target":11,"value":1},
{"source":9,"target":12,"value":1},
{"source":10,"target":12,"value":1},
{"source":11,"target":13,"value":1},
{"source":12,"target":13,"value":1}
]
}
You have some basic problems with your code, see corrected concept below...
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<style>
.link {
stroke: #2E2E2E;
stroke-width: 2px;
}
.node {
stroke: #fff;
stroke-width: 2px;
}
.textClass {
stroke: #323232;
font-family: "Lucida Grande", "Droid Sans", Arial, Helvetica, sans-serif;
font-weight: normal;
stroke-width: .5;
font-size: 14px;
}
</style>
</head>
<body>
<!--<script src="d3 CB.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
d3.json("data.json", function (error, graph) {
if (error) throw error;
function chart(elementName) {
// look for the node in the d3 layout
var findNode = function (name) {
for (var i in nodes) {
if (nodes[i]["name"] === name) return nodes[i];
}
};
var width = 960, // default width
height = 450, // default height
color = d3.scale.category10(),
nodes = graph.nodes,
links = graph.links,
force = d3.layout.force()
.nodes(nodes)
.links([]),
vis,
runOnceFlag = true;
vis = d3.select(elementName)
.append("svg:svg")
.attr("width", width)
.attr("height", height)
.attr("id", "svg")
.attr("pointer-events", "all")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("perserveAspectRatio", "xMinYMid")
.append('svg:g');
var update = function () {
var link = vis.selectAll("line")
.data(force.links(), function (d) {
return d.source + "-" + d.target;
});
link.enter().insert("line", "g")
.attr("id", function (d) {
return d.source + "-" + d.target;
})
.attr("stroke-width", function (d) {
return d.value / 10;
})
.attr("class", "link")
.style("stroke", "red")
.transition().duration(5000).style("stroke", "black");
link.append("title")
.text(function (d) {
return d.value;
});
link.exit().remove();
var node = vis.selectAll("g.node")
.data(nodes, function (d) {
return d.name;
});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", 12)
.attr("id", function (d) {
return "Node;" + d.name;
})
.attr("class", "nodeStrokeClass")
.attr("fill", function (d) {
return color(d.group);
});
nodeEnter.append("svg:text")
.attr("class", "textClass")
.attr("x", 14)
.attr("y", ".31em")
.text(function (d) {
return d.name;
});
node.exit().remove();
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) {
console.log(d);
return "translate(" + d.x + "," + d.y + ")";
});
});
// Restart the force layout.
force
.charge(-120)
.linkDistance(function (d) {
return d.value * 100
})
.size([width, height])
.start();
};
update();
var c = {"source": findNode('a'), "target": findNode('b'), value: 1}
// the line below causes the error
window.setTimeout(function() {
force.links().push(c);
update()
},2000)
};
//
chart('body');
});
</script>
</body>
</html>
I have developed a force layout to represent relationships between social groups. Now I would like to get the nodes to be distributed in a circle with links joining them. What is the best way to do this?
The complete version of the code (without data) is here http://jsfiddle.net/PatriciaW/zZSJT/
(Why do I have to include code here too? Here is the main portion)
d3.json("/relationships?nocache=" + (new Date()).getTime(),function(error,members){
var links=members.organizations.map(function(members) {
return members.member;
});
var nodes = {};
links.forEach(function(link) {
link.source = nodes[link.xsource] || (nodes[link.xsource] = {source: link.xsource, name: link.xsource, category: link.categorysource, path: link.pathsource, desc: link.descsource, title: link.titlesource});
link.target = nodes[link.xtarget] || (nodes[link.xtarget] = {target: link.xtarget, name: link.xtarget, category: link.categorytarget, path: link.pathtarget, desc: link.desctarget, title: link.titletarget});
});
force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.charge(-120)
.linkDistance(function() {return (Math.random() * 200) + 100;})
.linkStrength(0.5)
.on("tick", tick)
.start();
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node_drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", dragmove)
.on("dragend", dragend);
var loading = svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment pleaseā¦");
function dragstart(d, i) {
force.stop() // stops the force auto positioning before you start dragging
}
function dragmove(d, i) {
d.px += d3.event.dx;
d.py += d3.event.dy;
d.x += d3.event.dx;
d.y += d3.event.dy;
tick(); // this is the key to make it work together with updating both px,py,x,y on d !
}
function dragend(d, i) {
d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
tick();
force.resume();
};
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", clickAlert)
.call(node_drag);
node.append("circle")
.attr("r", 8)
.style("fill", function(d) {
return categoryColour [d.category];
})
// add an image marker
node.append("image")
.attr("x",-8)
.attr("y",-8)
.attr("width", 16)
.attr("height", 16)
.attr("xlink:href", function(d) {
return categoryImage [d.category]
})
.on("click", clickAlert)
.style("cursor", "pointer")
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
// Use a timeout to allow the rest of the page to load first.
setTimeout(function() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
force.start();
for (var i = n * n; i > 0; --i) force.tick();
force.stop();
svg.selectAll("line")
.data(links)
.enter().append("line")
.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; });
svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 4.5);
loading.remove();
}, 10);
function tick() {
link
.attr("x1", function(d) {
return d.source.x + xadj; })
.attr("y1", function(d) {
return d.source.y + yadj; })
.attr("x2", function(d) {
return d.target.x +xadj; })
.attr("y2", function(d) {
return d.target.y +yadj; });
node
.attr("transform", function(d) {
return "translate(" + (d.x + xadj) + "," + (d.y + yadj) + ")";
});
};
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16);
d3.select(this).select("text")
.attr("font-size","34px")
.style("font-weight", "bold");
};
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 8);
d3.select(this).select("text")
.attr("font-size","12px")
.style("font-weight", "normal");
};
}) // end json
Here's someone else's solution:
This network graph uses the D3 force layout to draw nodes and links, but instead of using d3.force() to find the best node positions, we draw an invisible arc and evenly places nodes along the circumference.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
<style>
line.node-link, path.node-link {
fill: none;
stroke: black
}
circle.node {
fill: white;
stroke: black
}
circle.node+text {
text-anchor: middle;
}
text {
font-family: sans-serif;
pointer-events: none;
}
</style>
</head>
<body>
<script type="text/javascript">
// number of random nodes (gets crowded at >25 unless you change node diameter)
var num = 20;
// returns random int between 0 and num
function getRandomInt() {return Math.floor(Math.random() * (num));}
// nodes returns a [list] of {id: 1, fixed:true}
var nodes = d3.range(num).map(function(d) { return {id: d}; });
// links returns a [list] of {source: 0, target: 1} (values refer to indicies of nodes)
var links = d3.range(num).map(function(d) { return {source: getRandomInt(), target: getRandomInt()}; });
var width = 500,
height = 500;
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height]);
// evenly spaces nodes along arc
var circleCoord = function(node, index, num_nodes){
var circumference = circle.node().getTotalLength();
var pointAtLength = function(l){return circle.node().getPointAtLength(l)};
var sectionLength = (circumference)/num_nodes;
var position = sectionLength*index+sectionLength/2;
return pointAtLength(circumference-position)
}
// fades out lines that aren't connected to node d
var is_connected = function(d, opacity) {
lines.transition().style("stroke-opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// invisible circle for placing nodes
// it's actually two arcs so we can use the getPointAtLength() and getTotalLength() methods
var dim = width-80
var circle = svg.append("path")
.attr("d", "M 40, "+(dim/2+40)+" a "+dim/2+","+dim/2+" 0 1,0 "+dim+",0 a "+dim/2+","+dim/2+" 0 1,0 "+dim*-1+",0")
.style("fill", "#f5f5f5");
force.start();
// set coordinates for container nodes
nodes.forEach(function(n, i) {
var coord = circleCoord(n, i, nodes.length)
n.x = coord.x
n.y = coord.y
});
// use this one for straight line links...
// var lines = svg.selectAll("line.node-link")
// .data(links).enter().append("line")
// .attr("class", "node-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; });
// ...or use this one for curved line links
var lines = svg.selectAll("path.node-link")
.data(links).enter().append("path")
.attr("class", "node-link")
.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;
});
var gnodes = svg.selectAll('g.gnode')
.data(nodes).enter().append('g')
.attr("transform", function(d) {
return "translate("+d.x+","+d.y+")"
})
.classed('gnode', true);
var node = gnodes.append("circle")
.attr("r", 25)
.attr("class", "node")
.on("mouseenter", function(d) {
is_connected(d, 0.1)
node.transition().duration(100).attr("r", 25)
d3.select(this).transition().duration(100).attr("r", 30)
})
.on("mouseleave", function(d) {
node.transition().duration(100).attr("r", 25);
is_connected(d, 1);
});
var labels = gnodes.append("text")
.attr("dy", 4)
.text(function(d){return d.id})
</script>
</body>
</html>