My code creates a graph and creates a pivot point on each node, if you double click them it'll fetch more data associated with that node and hopefully creates new links. Now here's the problem I'm running into:
I clicked on one of the outermost nodes but for some reason the new links were being attached to the first node (the blue one). Any idea why this is happening?
function draw_graph(graph) {
var color = d3.scaleOrdinal(d3.schemeCategory20);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
node,
link;
svg.append('defs').append('marker')
.attrs({
'id': 'arrowhead',
'viewBox': '-0 -5 10 10',
'refX': 13,
'refY': 0,
'orient': 'auto',
'markerWidth': 13,
'markerHeight': 13,
'xoverflow': 'visible'
})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(200).strength(1))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
update(graph.links, graph.nodes);
svg.selectAll('circle').on('dblclick', function () {
var pivot_id = ($(this).siblings('title').text())
console.log('pivoting on', pivot_id)
pivot_search(pivot_id)
});
function update(links, nodes) {
link = svg.selectAll(".link")
.data(links)
.enter()
.append("line")
.attr("class", "link")
.attr('marker-end', 'url(#arrowhead)')
edgepaths = svg.selectAll(".edgepath")
.data(links)
.enter()
.append('path')
.attrs({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'id': function (d, i) {
return 'edgepath' + i
}
})
.style("pointer-events", "none");
edgelabels = svg.selectAll(".edgelabel")
.data(links)
.enter()
.append('text')
.style("pointer-events", "none")
.attrs({
'class': 'edgelabel',
'id': function (d, i) {
return 'edgelabel' + i
},
'font-size': 10,
'fill': '#aaa'
});
node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
);
node.append("circle")
.attr("r", 5)
.attr("fill", function (d) {
return color(d.group);
})
node.append("title")
.text(function (d) {
return d.id;
});
node.append("text")
.attr("dy", -3)
.text(function (d) {
return d.label;
});
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function ticked() {
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 + ")";
});
edgepaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});
edgelabels.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(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
}
function pivot_search(entity_id) {
var json = {
'nodes': [],
'links': [],
}
get_entities({'id': entity_id})
.done(function (data) {
json.nodes.push({
'label': data['results'][0]['label'],
'id': data['results'][0]['id'],
'group': data['results'][0]['entity_type'],
})
get_entities({
'related_entities': entity_id,
'related_entities__entity_instance__entity_type__strong_entity': true,
'page_size': 500
})
.done(function (data) {
for (var i = 0; i < data['results'].length; i++) {
json.nodes.push({
'label': data['results'][i]['label'],
'id': data['results'][i]['id'],
'group': data['results'][i]['entity_type'],
})
json.links.push({
'source': entity_id,
'target': data['results'][i]['id'],
})
}
draw_graph(json)
})
})
}
EDIT: Upon further investigations seems like it's replacing the existing node links with the new data and creating new potentially duplicate nodes.
link = svg.selectAll('.link')
.data(links, function (d) {
return d.id;
})
.enter()
.append('line')
.attr('class', 'link')
.attr('marker-end', 'url(#arrowhead)')
edgepaths = svg.selectAll('.edgepath')
.data(links)
.enter()
.append('path')
.attrs({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'id': function (d, i) {
return 'edgepath' + i
}
})
.style('pointer-events', 'none');
node = svg.selectAll('.node')
.data(nodes, function (d) {
return d.id;
})
.enter()
.append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
);
I added an ID to help deal with node duplication, but now I have a problem with index root displacement.
It appears that the problem that you are having is due to the fact that your data might be duplicating within D3's data-join functionality. It is likely that the best way to solve the issue is to create a "UUID / GUID" for each node in your data before D3 binds it (see here for an example). Once you have done that, then you can bind the data and use the data-join's key-specify function (see here for an explanation) to tell D3 to use the UUID / GUID values you created for each object to guarantee consistency. From there, you should be able to handle the parent-child relationships easier.
Edit #1
Since that worked for the duplicate values, the next problem that you are likely having is because the reference to the "source" object is not set up the way that D3 would expect. In D3, the link's "source" property is a reference to the actual source object, where you are just providing the ID value (see here for the D3v4 docs reference). Try providing the reference to the actual source object within the array and that should fix it.
Edit #2
You are correct in that you are handling NEW data coming into the visualization, but I don't think that you're handling EXISTING or OLD (meaning, data points that are no longer relevant and the nodes / links need to be removed). In this case, try updating your code with the following example from Mike Bostock (the original creator of the D3.js library) here and then report back once that is done. It is possible that the new nodes that you are seeing are simply the older nodes that need to be removed since they no longer have the children tied to them, so D3js sees them as "new" or "existing" nodes that actually need to be removed.
Related
I am trying to add new nodes to a D3 graph dynamically by nodes.push({"id": item, "group": 1, "size": 30}); but when I do this there is a visual bug where there are duplicates. Anytime I update() I get a double of whatever was already there. Anyone have any advice? Would be appreciated.
var node;
var link;
var circles
var lables;
function update(){
node = svg.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(nodes)
.enter().append("g")
link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
circles = node.append("circle")
.attr("r", function(d) { return (d.size / 10) + 1})
.attr("fill", function(d) { return color(3); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", clicked);
lables = node.append("text")
.text(function(d) {
return d.id;
})
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function ticked() {
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 + ")";
})
}
Looking just at the nodes (the links are essentially the same issue), every time you update your data you:
Create a new parent g (svg.append("g"))
Select all the child g elements of that new parent g (.selectAll("g")). Since this new parent g has no children - you just made it, nothing is selected.
Bind data to the selection (.data(nodes))
Using the enter selection, append a g for each item in the data array (as there are no elements in the selection, everything is entered (the enter selection creates an element in the DOM for every item in the data array for which no corresponding element exists in the selection.)
Append a circle to each newly appended g. (.enter().append("g"))
Nowhere do you select the already existing nodes - these are just cast aside. They are ignored by the tick function because link and node refer to selections of newly created nodes and links. Neither do you remove the old links and nodes - so they just sit there for all eternity or until you close the browser.
The canonical solution is to:
Append structural elements once. I say structural in reference to the parent g elements: they aren't data dependent, they're organizational. They should be appended once outside of the update function.
Use the update function to manage (create, update, remove) elements that are dependent on the data: the nodes and links themselves. Anything that depends on the data needs to be modified in the update function, nothing else.
So we could append the parent g elements outside of the update function:
var nodeG = svg.append("g").attr("class", "nodes");
var linkG = svg.append("g").attr("class", "links");
Then in the update function we can use those selections to conduct the enter/update/exit cycle. This is slightly complicated in your case, and many others, because we have nodes represented by a g with child elements. Something like the following:
function update() {
var node = nodeG.selectAll("g")
.data(nodes)
// remove excess nodes.
node.exit().remove();
// enter new nodes as required:
var nodeEnter = node.enter().append("g")
.attr(...
// append circles to new nodes:
nodeEnter.append("circle")
.attr(...
// merge update and enter.
node = nodeEnter.merge(node);
// do enter/update/exit with lines.
var link = linkG.selectAll("line")
.attr(...
link.exit().remove();
var linkEnter = link.enter().append("line")
.attr(...
link = linkEnter.merge(link);
...
Which in your case may look like:
// Random data:
let graph = { nodes: [], links : [] }
function randomizeData(graph) {
// generate nodes:
let n = Math.floor(Math.random() * 10) + 6;
let newNodes = [];
for(let i = 0; i < n; i++) {
if (graph.nodes[i]) newNodes.push(graph.nodes[i]);
else newNodes.push({ id: i,
color: Math.floor(Math.random()*10),
size: Math.floor(Math.random() * 10 + 2),
x: (Math.random() * width),
y: (Math.random() * height)
})
}
// generate links
let newLinks = [];
let m = Math.floor(Math.random() * n) + 1;
for(let i = 0; i < m; i++) {
a = 0; b = 0;
while (a == b) {
a = Math.floor(Math.random() * n);
b = Math.floor(Math.random() * n);
}
newLinks.push({source: a, target: b})
if(i < newNodes.length - 2) newLinks.push({source: i, target: i+1})
}
return { nodes: newNodes, links: newLinks }
}
// On with main code:
// Set up the structure:
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
const color = d3.scaleOrdinal(d3.schemeCategory10);
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.004))
.force("charge", d3.forceManyBody())
// to attract nodes to center, use forceX and forceY:
.force("x", d3.forceX().x(width/2).strength(0.01))
.force("y", d3.forceY().y(height/2).strength(0.01));
const nodeG = svg.append("g").attr("class","nodes")
const linkG = svg.append("g").attr("class", "links")
graph = randomizeData(graph);
update();
// Two variables to hold our links and nodes - declared outside the update function so that the tick function can access them.
var links;
var nodes;
// Update based on data:
function update() {
// Select all nodes and bind data:
nodes = nodeG.selectAll("g")
.data(graph.nodes);
// Remove excess nodes:
nodes.exit()
.transition()
.attr("opacity",0)
.remove();
// Enter new nodes:
var newnodes = nodes.enter().append("g")
.attr("opacity", 0)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
// for effect:
newnodes.transition()
.attr("opacity",1)
.attr("class", "nodes")
newnodes.append("circle")
.attr("r", function(d) { return (d.size * 2) + 1})
.attr("fill", function(d) { return color(d.color); })
newnodes.append("text")
.text(function(d) { return d.id; })
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
newnodes.append("title")
.text(function(d) { return d.id; });
// Combine new nodes with old nodes:
nodes = newnodes.merge(nodes);
// Repeat but with links:
links = linkG.selectAll("line")
.data(graph.links)
// Remove excess links:
links.exit()
.transition()
.attr("opacity",0)
.remove();
// Add new links:
var newlinks = links.enter()
.append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
// for effect:
newlinks
.attr("opacity", 0)
.transition()
.attr("opacity",1)
// Combine new links with old:
links = newlinks.merge(links);
// Update the simualtion:
simulation
.nodes(graph.nodes) // the data array, not the selection of nodes.
.on("tick", ticked)
.force("link").links(graph.links)
simulation.alpha(1).restart();
}
function ticked() {
links // the selection of all 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 + ")";
})
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).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.alphaTarget(0);
d.fx = null;
d.fy = null;
}
d3.select("button")
.on("click", function() {
graph = randomizeData(graph);
update();
})
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<button> Update</button>
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
Note
I've updated the force paramaters a bit to use forceX and forceY: forces which attract the nodes to the center. The centering force only ensures the center of gravity is a specific value, not how close the nodes must be.
Alternative approaches:
Of course, you could just remove everything and append it each time: but this limits ability to transition from one dataset to the next and is generally not canonical.
If you only need to enter elements once (no elements need to be added or removed during updates) then you could avoid using the full enter/update/exit cycle and append once outside the update function, updating node/link attributes with new data on update rather than using the more involved enter/update/exit cycle in the snippet above.
Given the following layout:
<g>
... // many nodes
<g>
<circle></circle>
<text></text>
</g>
...
</g>
How would a correct update pattern look like in d3 v4?
What do I have to use as parameter in merge(?), how often do I have to call merge (only on node? node + circle + text ?)
I created an working example on fiddle: https://jsfiddle.net/cvvfsg97/6/
Code:
function update(items) {
node = nodeLayer.selectAll(".node")
.data(items, function(d) { return d.id; })
node = node.enter() // insert
.append("g")
.attr("class", "node");
node.append("circle") // insert
.attr("r", 2.5)
.attr('class', 'circle')
.merge(nodeLayer.selectAll('.node > circle')) // is this correct?! // merge
.attr('fill', 'red') // just for testing purposes
.exit().remove(); // exit
node.append("text") // insert
.attr("dy", 3)
.text(function(d) { return d.name; })
.merge(nodeLayer.selectAll('.node > text')) // is this correct?! // merge
.attr('fill', 'green') // just for testing purposes
.exit().remove();
node.merge(nodeLayer.selectAll('.node')) // is this correct?! // merge
.attr('class', 'anotherClass')
.exit().remove(); // does not work // exit
}
Could someone bring some clarity in terms of how to use enter(), merge(), exit() in groups?
I potentially like to do changes in every stage for every element.
Update: I simplified the example, I don't need links or a force-layout. My question is only about the update-pattern, not about forces. The updated jsfiddle does not have the force-layout.
You are over complicating the pattern. Here's your update function written properly:
function update(items) {
var node = nodeLayer.selectAll(".node") // bind the data, this is update
.data(items, function(d) {
return d.id;
});
node.exit().remove(); // exit, remove the g
nodeEnter = node.enter() // enter, append the g
.append("g")
.attr("class", "node");
nodeEnter.append("circle") // enter, append the circle on the g
.attr("r", 2.5)
.attr('class', 'circle')
.attr('fill', 'red');
nodeEnter.append("text") // enter, append the text on the g
.attr("dy", 3)
.text(function(d) {
return d.name;
})
.attr('fill', 'green');
node = nodeEnter.merge(node); // enter + update on the g
node.attr('transform', function(d){ // enter + update, position the g
return 'translate(' + d.x + ',' + d.y + ')';
});
node.select("text") // enter + update on subselection
.text(function(d) {
return d.name;
});
}
Here it is running with multiple calls:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var nodeLayer = d3.select('body')
.append('svg')
.attr('width',500)
.attr('height',500);
update([
{
id: 1,
name: 'A',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 2,
name: 'B',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 3,
name: 'C',
x: Math.random() * 500,
y: Math.random() * 500
}
]);
setTimeout(function(){
update([
{
id: 1,
name: 'A',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 4,
name: 'This is a new name...',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 3,
name: 'C',
x: Math.random() * 500,
y: Math.random() * 500
}
]);
}, 3000);
function update(items) {
var node = nodeLayer.selectAll(".node")
.data(items, function(d) {
return d.id;
});
node.exit().remove(); // exit, remove the g
nodeEnter = node.enter() // enter the g
.append("g")
.attr("class", "node");
nodeEnter.append("circle") // enter the circle on the g
.attr("r", 2.5)
.attr('class', 'circle')
.attr('fill', 'red');
nodeEnter.append("text") // enter the text on the g
.attr("dy", 3)
.attr('fill', 'green');
node = nodeEnter.merge(node); // enter + update
node.attr('transform', function(d){
return 'translate(' + d.x + ',' + d.y + ')';
});
node.select("text")
.text(function(d) {
return d.name;
});
}
</script>
</body>
</html>
I've done this recently in my code - I use select(subSelector) on the current selection that contains the elements. In your example I would change it as follows:
function update(items) {
var node = nodeLayer.selectAll(".node")
.data(items, function(d) { return d.id; })
var nodeEnter = node.enter()
.append("g")
.attr("class", "node");
nodeEnter.append("circle")
.attr("r", 2.5)
.attr('class', 'circle')
.merge(node.select('circle'))
.attr('fill', 'red');
nodeEnter.append("text") // insert
.attr("dy", 3)
.text(function(d) { return d.name; })
.merge(node.select('text'))
.attr('fill', 'green');
// You only need to call remove on the group, all the other exit().remove() calls are superfluous
node.exit().remove();
simulation
.nodes(items);
}
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
nodeLayer = svg.append('g'),
node;
var list = [];
var links = [];
var simulation = d3.forceSimulation(list)
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink(links).distance(200))
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);
function createNodes(index) {
links.push({
'source': 0,
'target': index
})
list.push({
"id": index,
"name": "server " + index
});
return list;
}
var iteration = 0;
update(createNodes(iteration)); // just simulating updates
d3.interval(function(timer) {
iteration++;
update(createNodes(iteration));
}, 1000); //<-- this was commented out incorrectly just now
function update(items) {
var dataJoin = nodeLayer.selectAll(".node")
.data(items, function(d) {
return d.id;
});
node = dataJoin.enter() // insert
.append("g")
.attr("class", "node");
node.append("circle") // insert
.attr("r", 2.5)
.attr('class', 'circle')
.merge(dataJoin) // not the class, the actual selected group you called enter() on
.select('.circle')
.style('fill', 'red') // just for testing purposes
.exit().remove(); // exit
node.append("text") // insert
.attr("dy", 3)
.attr('class', 'text')
.text(function(d) {
return d.name;
})
.merge(dataJoin)
.select('.text')
.style('fill', 'green') // fill is a style
dataJoin.exit().remove();
simulation.nodes(list);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
function ticked() {
node.attr("transform", function(d) {
var a = d.x || 0;
var b = d.y || 0;
return "translate(" + a + ", " + b + ")";
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.4.1/d3.min.js"></script>
<svg height="300" width="300"></svg>
there are bunch of bugs,
first of all 'update'(misnomer) should be called with the list of all the data nodes not just the change.
you might want to explore https://bl.ocks.org/ and copy how people are doing force directed graphs. you need links to have forces.
the idea behind merge is to compare the new list with the old list, which means you need to use dataJoin or the group that has the data/id attached.
I am not an expert, do take a look at all the examples of force-directed graphs and see how they update/merge. (there's more than 1 way to update/restart the graph)
I am trying to plot a network graph using networkD3 in R. I wanted to make some changes to the display so that the text labels (which appears when mouseover) can be easily read.
Please refer to the link here for an example. Note: Jump to the d3ForceNetwork plot.
As seen in the example, the labels are hard to read due to its colour and it often gets obstructed by the surrounding nodes. I have been messing around with the JS file and managed to change the text label color to black. However, having no knowledge of JS or CSS (I can't even tell the difference between the 2 actually), I have no idea how I can change the stack order such that the text labels will always be displayed above any other objects.
Can anyone advise me on how I can achieve the desired outcome?
Below is the full JS file:
HTMLWidgets.widget({
name: "forceNetwork",
type: "output",
initialize: function(el, width, height) {
d3.select(el).append("svg")
.attr("width", width)
.attr("height", height);
return d3.layout.force();
},
resize: function(el, width, height, force) {
d3.select(el).select("svg")
.attr("width", width)
.attr("height", height);
force.size([width, height]).resume();
},
renderValue: function(el, x, force) {
// Compute the node radius using the javascript math expression specified
function nodeSize(d) {
if(options.nodesize){
return eval(options.radiusCalculation);
}else{
return 6}
}
// alias options
var options = x.options;
// convert links and nodes data frames to d3 friendly format
var links = HTMLWidgets.dataframeToD3(x.links);
var nodes = HTMLWidgets.dataframeToD3(x.nodes);
// get the width and height
var width = el.offsetWidth;
var height = el.offsetHeight;
var color = eval(options.colourScale);
// set this up even if zoom = F
var zoom = d3.behavior.zoom();
// create d3 force layout
force
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(options.linkDistance)
.charge(options.charge)
.on("tick", tick)
.start();
// thanks http://plnkr.co/edit/cxLlvIlmo1Y6vJyPs6N9?p=preview
// http://stackoverflow.com/questions/22924253/adding-pan-zoom-to-d3js-force-directed
var drag = force.drag()
.on("dragstart", dragstart)
// allow force drag to work with pan/zoom drag
function dragstart(d) {
d3.event.sourceEvent.preventDefault();
d3.event.sourceEvent.stopPropagation();
}
// select the svg element and remove existing children
var svg = d3.select(el).select("svg");
svg.selectAll("*").remove();
// add two g layers; the first will be zoom target if zoom = T
// fine to have two g layers even if zoom = F
svg = svg
.append("g").attr("class","zoom-layer")
.append("g")
// add zooming if requested
if (options.zoom) {
function redraw() {
d3.select(el).select(".zoom-layer").attr("transform",
"translate(" + d3.event.translate + ")"+
" scale(" + d3.event.scale + ")");
}
zoom.on("zoom", redraw)
d3.select(el).select("svg")
.attr("pointer-events", "all")
.call(zoom);
} else {
zoom.on("zoom", null);
}
// draw links
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link")
.style("stroke", function(d) { return d.colour ; })
//.style("stroke", options.linkColour)
.style("opacity", options.opacity)
.style("stroke-width", eval("(" + options.linkWidth + ")"))
.on("mouseover", function(d) {
d3.select(this)
.style("opacity", 1);
})
.on("mouseout", function(d) {
d3.select(this)
.style("opacity", options.opacity);
});
// draw nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.style("fill", function(d) { return color(d.group); })
.style("opacity", options.opacity)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click)
.call(force.drag);
node.append("circle")
.attr("r", function(d){return nodeSize(d);})
.style("stroke", "#fff")
.style("opacity", options.opacity)
.style("stroke-width", "1.5px");
node.append("svg:text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name })
.style("font", options.fontSize + "px " + options.fontFamily)
.style("opacity", options.opacityNoHover)
.style("pointer-events", "none");
function tick() {
node.attr("transform", function(d) {
if(options.bounded){ // adds bounding box
d.x = Math.max(nodeSize(d), Math.min(width - nodeSize(d), d.x));
d.y = Math.max(nodeSize(d), Math.min(height - nodeSize(d), d.y));
}
return "translate(" + d.x + "," + 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; });
}
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d)+5;});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 13)
.style("stroke-width", ".5px")
.style("font", options.clickTextSize + "px ")
.style('fill', 'black')
.style('position','relative')
.style("opacity", 1);
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d);});
d3.select(this).select("text").transition()
.duration(1250)
.attr("x", 0)
.style("font", options.fontSize + "px ")
.style("opacity", options.opacityNoHover);
}
function click(d) {
return eval(options.clickAction)
}
// add legend option
if(options.legend){
var legendRectSize = 18;
var legendSpacing = 4;
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = legendRectSize;
var vert = i * height+4;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.style('fill', 'darkOrange')
.text(function(d) { return d; });
}
// make font-family consistent across all elements
d3.select(el).selectAll('text').style('font-family', options.fontFamily);
},
});
I suspect I need to make some changes to the code over here:
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d)+5;});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 13)
.style("stroke-width", ".5px")
.style("font", options.clickTextSize + "px ")
.style('fill', 'black')
.style("opacity", 1);
}
You need to resort the node groups holding the circles and text so the currently mouseover'ed one is the last in that group, and thus the last one drawn so it appears on top of the others. See the first answer here -->
Updating SVG Element Z-Index With D3
In your case, if your data doesn't have an id field you may have to use 'name' instead as below (adapted to use the mouseover function you've got):
function mouseover(d) {
d3.selectAll("g.node").sort(function (a, b) {
if (a.name != d.name) return -1; // a is not the hovered element, send "a" to the back
else return 1; // a is the hovered element, bring "a" to the front (by making it last)
});
// your code continues
The pain might be that you have to do this edit for every d3 graph generated by this R script, unless you can edit the R code/package itself. (or you could suggest it to the package author as an enhancement.)
I'm working on a force layout graph that displays relationships of writers. Since there are so many, I tried to implement zooming and dragging. Zooming works fine (with one exception), but when I drag a node it also drags the background. I tried following Mike Bostock's directions here and the StackOverflow question paired with it, but it still won't work. I based most of the code for the graph on this, which works beautifully, but since he used an older version of d3, his dragging breaks in the new version. (I can't just use the older version of d3 because I have some other parts of the graph not shown here that work only with the newer version.)
I think the problem has something to do with my grouping of SVG objects, but I also can't figure out what I'm doing wrong there. This also brings in the one zooming problem; when I zoom in or pan around, the legend also moves and zooms in. If there's an easy fix to make it stay still and sort of "hover" above the graph, that would be great.
I'm very new to coding, so I'm probably making really stupid mistakes, but any help would be appreciated.
Fiddle.
var graphData = {
nodes: [
{
id:0,
name:"Plotinus"
},
{
id:1,
name:"Iamblichus"
},
{
id:2,
name:"Porphyry"
}
],
links: [
{
relationship:"Teacher/student",
source:0,
target:1
},
{
relationship:"Enemies",
source:0,
target:2
},
{
relationship:"Family",
source:1,
target:2
}
]
};
var linkColor = d3.scale.category10(); //Sets the color for links
var drag = d3.behavior.drag()
.on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); })
.on("drag", function(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
});
var w = 300,
h = 300;
var vis = d3.select(".graph")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("pointer-events", "all")
.append('svg:g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('svg:g');
vis.append('svg:rect')
.attr('width', w)
.attr('height', h)
.attr('fill', 'rgba(1,1,1,0)');
function redraw() {
vis.attr("transform","translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); }
var force = d3.layout.force()
.gravity(.6)
.charge(-600)
.linkDistance( 60 )
.size([w, h]);
var svg = d3.select(".text").append("svg")
.attr("width", w)
.attr("height", h);
var link = vis.selectAll("line")
.data(graphData.links)
.enter().append("line")
.style("stroke", function(d) { return linkColor(d.relationship); })
.style("stroke-width", 1)
.attr("class", "connector");
var node = vis.selectAll("g.node")
.data(graphData.nodes)
.enter().append("svg:g")
.attr("class","node")
.call(force.drag);
node.append("svg:circle")
.attr("r", 10) //Adjusts size of nodes' radius
.style("fill", "#ccc");
node.append("svg:text")
.attr("text-anchor", "middle")
.attr("fill","black")
.style("pointer-events", "none")
.attr("font-size", "9px")
.attr("font-weight", "100")
.attr("font-family", "sans-serif")
.text( function(d) { return d.name;} );
// Adds the legend.
var legend = vis.selectAll(".legend")
.data(linkColor.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(-10," + i * 20 + ")"; });
legend.append("rect")
.attr("x", w - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", linkColor);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.attr("class", "legendText")
.style("text-anchor", "end")
.text(function(d) { return d; });
force
.nodes(graphData.nodes)
.links(graphData.links)
.on("tick", tick)
.start();
function tick() {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("transform", function(d) { return "translate(" + d.x + "," + 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; });
}
I think I figured it out.
I had to combine the instructions from here and here, which was sort of already answered in the answer I linked.
My old way, grabbed from the first example, looked like this:
var drag = d3.behavior.drag()
.on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); })
.on("drag", function(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
});
The problem was that I was focusing on d3.behavior.drag() instead of force.drag, which I think Stephen Thomas was trying to tell me. It should look like this:
//code code code//
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
//code code code//
var drag = force.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
You can use the drag() method of the force object instead of creating a separate drag behavior. Something like:
node.call(force.drag);
or, equivalently,
force.drag(node);
A complete example is available at http://bl.ocks.org/sathomas/a7b0062211af69981ff3
Here is what is working for me:
const zoom = d3.behavior.zoom()
.scaleExtent([.1, 10])
.on('zoom', zoomed);
const force = d3.layout.force()
.(...more stuff...);
const svg = d3.select('.some-parent-div')
.append('svg')
.attr('class', 'graph-container')
.call(zoom);
const mainGroup = svg.append('g');
var node = mainGroup.selectAll('.node');
node.enter()
.insert('g')
.attr('class', 'node')
.call(force.drag)
.on('mousedown', function(){
// line below is the key to make it work
d3.event.stopPropagation();
})
.(...more stuff...);
function zoomed(){
force.stop();
const canvasTranslate = zoom.translate();
mainGroup.attr('transform', 'translate('+canvasTranslate[0]+','+canvasTranslate[1]+')scale(' + zoom.scale() + ')');
force.resume();
}
With your code, the node can be dragged but when you drag a node other nodes will move too. I come up this to stop rest of nodes and just let you finished dragging then re-generated the whole graph
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("fixed", d.fixed = true);
}
function dragged(d) {
force.stop();
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
tick();
}
function dragended(d) {
force.resume();
}
I have some data I am trying to display with the D3 force layout. Apologies if this is a naive question, or if the terminology i employ in the question title is not accurate. I couldn't see an answer quite what i was looking for.
I made a fiddle with a sample showing what I am on about here :
http://jsfiddle.net/stevendwood/f3GJT/8/
In the example I have one node (0) which has lots of links. Another node (16) has a smaller amount of links, 0 and 16 are both connected to 15.
So what i would like is for 0 and 16 to be little clusters with their connected nodes appearing in a nice circle around them.
I vainly tried to customise the charge based on the number of links, but I think what i want to do is somehow make nodes more attracted to nodes they are connected to and less attracted to nodes that they are not connected to.
I would like something like this if possible :
var w = 500,
h = 500,
nodes = [],
links = [];
/* Fake up some data */
for (var i=0; i<20; i++) {
nodes.push({
name: ""+i
});
}
for (i=0; i<16; i++) {
links.push({
source: nodes[i],
target: nodes[0]
});
}
links.push({
source: nodes[16],
target: nodes[15]
});
for (i=17; i<20; i++) {
links.push({
source: nodes[i],
target: nodes[16]
});
}
var countLinks = function(n) {
var count = 0;
links.forEach(function(l) {
if (l.source === n || l.target === n) {
count++;
}
});
return count;
}
/////////////////////////////////////////////
var vis = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(nodes)
.links([])
.gravity(0.05)
.charge(function(d) {
return countLinks(d) * -50;
})
.linkDistance(300)
.size([w, h]);
var link = vis.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.attr("stroke", "#CCC")
.attr("fill", "none");
var node = vis.selectAll("circle.node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("svg:circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 14)
.style("fill", "#CCC")
.style("stroke", "#AAA")
.style("stroke-width", 1.5)
node.append("text").text(function(d) { return d.name; })
.attr("x", -6)
.attr("y", 6);
force.on("tick", function(e) {
node.attr("transform", function(d, i) {
return "translate(" + d.x + "," + 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();
Why did you leave out the links when declaring the force layout? If you add them back in, it looks much closer to what you wanted:
var force = d3.layout.force()
.nodes(nodes)
//.links([])
.links(links)
.gravity(0.1)
.charge(-400)
.linkDistance(75)
.size([w, h]);
http://jsfiddle.net/f3GJT/11/