d3 force diagrams node positions - javascript

I am working with d3 force diagrams at the moment, I am wanting to plot my child nodes around a parent node equally spaced, so for example if I have a parent node, and 4 linked child nodes, I would want each those node positioned at 90 degree intervals? Is that possible?
Here is my current force code,
app.force
.nodes(nodes)
.links(app.edges)
.on("tick", tick)
.start();
function tick(e) {
// console.log(link);
var k = 6 * e.alpha;
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
linkText
.attr("x", function(d) {
return ((d.source.x + d.target.x)/2);
})
.attr("y", function(d) {
return ((d.source.y + d.target.y)/2);
});
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node
.attr("cx", function(d) { return d.x })
.attr("cy", function(d) { return d.y })
}
app.force = d3.layout.force()
.charge(-300)
.linkDistance(85)
.size([width, height]);
//Where we will draw our visualisation
app.svg = d3.select(".visualisation").append("svg")
.attr('width', width)
.attr('height', height);

d3.layout.force() was not created with such customisation in mind. Of course you can set some parameters, but most of the positions are automatic calculated, and changing them can be very difficult (unless you create your own force function). Version 4.x is better in that matter, but not much.
In your specific case, you can set a very high (mathematically speaking, "very low", since it is negative) charge:
var force = d3.layout.force()
.charge(-3000)
But even doing that the angles are not exactly right angles, and they vary: if you click "run snippet" you can get an almost perfect cross, but if you click it again it's not that perfect the next time. And it will not work as expected if you have data with several levels of depth.
Here is a demo:
<script src="https://d3js.org/d3.v2.min.js?2.9.3"></script>
<style>
.link {
stroke: #aaa;
}
.node text {
stroke: #333;
cursor: pointer;
}
.node circle {
stroke: #fff;
stroke-width: 3px;
fill: #555;
}
</style>
<body>
<script>
var width = 400,
height = 300
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.distance(50)
.charge(-3000)
.size([width, height]);
var json = {
"nodes": [{
"name": "node1"
}, {
"name": "node2"
}, {
"name": "node3"
}, {
"name": "node4"
}, {
"name": "node5"
}],
"links": [{
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}, {
"source": 0,
"target": 3
}, {
"source": 0,
"target": 4
}]
};
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", 2);
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 8);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name
});
force.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
</script>

Related

Adding the z-coordinates to depict a 3D force-directed-graph

I tried to implement a 3D force directed graph in d3.js by the help of the following codes. But was unable to create the 3D network by adding the z-coordinates.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
fill: blue;
stroke: black;
stroke-width: 2px;
}
.node.visited {
fill: red;
}
.link {
stroke-width: 2px;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 640;
var height = 480;
var links = [{
source: 'Rohit',
target: 'Deep'
},
{
source: 'Deep',
target: 'Deepa'
},
{
source: 'Deepa',
target: 'Rohit'
},
];
var nodes = [{
"id": 1,
"desc": "Rohit",
"x": 121.0284957885742,
"y": 116.3165512084961,
"z": 59.36788940429688
},
{
"id": 2,
"desc": "Deep",
"x": 12.10284957885742,
"y": 116.3165512084961,
"z": 5.936788940429688
},
{
"id": 3,
"desc": "Deepa",
"x": 12.10284957885742,
"y": 11.63165512084961,
"z": 5.936788940429688
}
];
//adding to nodes
links.forEach(function(link) {
link.source = nodes[link.source] ||
(nodes[link.source] = {
name: link.source
});
link.target = nodes[link.target] ||
(nodes[link.target] = {
name: link.target
});
});
//adding svg to body
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
var defs = svg.append('defs');
var gradient = defs
.append('linearGradient')
.attr('id', 'svgGradient')
.attr('x1', '0%')
.attr('x2', '10%')
.attr('y1', '0%')
.attr('y2', '10%');
gradient
.append('stop')
.attr('class', 'start')
.attr('offset', '0%')
.attr('start-color', 'red')
.attr('start-opacity', 1);
gradient
.append('stop')
.attr('class', 'end')
.attr('offset', '100%')
.attr('stop-color', 'blue')
.attr('stop-opacity', 1);
var force = d3.layout.force()
.size([width, height])
.nodes(d3.values(nodes))
.links(links)
.on("tick", tick)
.linkDistance(300)
.start();
var link = svg.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link')
.attr('stroke', 'url(#svgGradient)');
var node = svg.selectAll('.node')
.data(force.nodes())
.enter().append('circle')
.attr('class', 'node')
.on("click", clicked)
.attr('r', width * 0.03)
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
.attr('cz', function(d){
return d.z;
});
//Code for clicking func.
function clicked(event, d) {
if (event.defaultPrevented) return; // dragged
d3.select(this).transition()
.style("fill", "black")
.attr("r", width * 0.2)
.transition()
.attr("r", width * 0.03)
.transition()
.style("fill", "blue")
//.attr("fill", d3.schemeCategory10[d.index % 10]);
}
//define the tick func.
function tick(e) {
node
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
.attr('cz', function(d) {
return d.z;
})
.call(force.drag);
link
.attr('x1', function(d) {
return d.source.x;
})
.attr('y1', function(d) {
return d.source.y;
})
.attr('z1', function(d) {
return d.source.z;
})
.attr('x2', function(d) {
return d.target.x;
})
.attr('y2', function(d) {
return d.target.y;
})
.attr('z2', function(d) {
return d.source.z;
})
}
</script>
</body>
The image that came was carrying two parts one graph where the nodes are taking (x-y) coordinates assigned to them. And then I had another graph where the nodes are taking all x-y-z coordinates.
Can anyone please help us by telling how can we add the z co-ordinates to code.

Force directed graph drawn out of the allocated SVG size

I'm trying to create force directed graph like this. It drew just fine when I was using the sample data. But when I use my own data, the nodes seem to be drawn out of the svg size.
Here is what I get:
And here is my code:
var nodes = createFDGNodes(stopsByLine);
var links = createFDGLinks(stopsByLine);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink()
.id(function(d) { return d.id; })
)
.force("charge", d3.forceManyBody()
.distanceMin(function(d) {return 1; })
)
.force("center", d3.forceCenter(960/2, 500/2));
const circleGroup = d3.select("div.transit-network")
.append("svg")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("class","fdg");
var color = d3.scaleOrdinal(d3.schemeCategory20);
var link = circleGroup.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke", "black")
.attr("stroke-width", 1);
var node = circleGroup.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 5)
.attr("class", function(d) {return "line-"+d.lineId+" stop-"+d.id;})
.attr("fill", function(d){
return color(d.lineId);
});
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
How could I make the graph so it is drawn within the allocated svg size?
The "correct" way to make your simulation fitting inside your allocated area is tweaking all the forces in the simulation, like forceManyBody, forceLink, forceCenter etc...
However, you can force the simulation (no pun intended) to fit in a given area. For instance, in the following demo, the simulation will be constrained in a small area of 100 x 100 pixels using this inside the tick function:
node.attr("transform", (d) => {
return "translate(" + (d.x < 10 ? dx = 10 : d.x > 90 ? d.x = 90 : d.x) +
"," + (d.y < 10 ? d.y = 10 : d.y > 90 ? d.y = 90 : d.y) + ")"
})
Here is the demo:
var width = 100;
var height = 100;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var nodes = [{
"id": "foo"
}, {
"id": "bar"
}, {
"id": "baz"
}, {
"id": "foobar"
}];
var edges = [{
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}, {
"source": 0,
"target": 3
}];
var simulation = d3.forceSimulation()
.force("link", d3.forceLink())
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(width / 2, height / 2));
var links = svg.selectAll("foo")
.data(edges)
.enter()
.append("line")
.style("stroke", "#ccc")
.style("stroke-width", 1);
var color = d3.scaleOrdinal(d3.schemeCategory20);
var node = svg.selectAll("foo")
.data(nodes)
.enter()
.append("g");
var nodeCircle = node.append("circle")
.attr("r", 5)
.attr("stroke", "gray")
.attr("stroke-width", "2px")
.attr("fill", "white");
simulation.nodes(nodes);
simulation.force("link")
.links(edges);
simulation.on("tick", function() {
node.attr("transform", (d) => {
return "translate(" + (d.x < 10 ? dx = 10 : d.x > 90 ? d.x = 90 : d.x) + "," + (d.y < 10 ? d.y = 10 : d.y > 90 ? d.y = 90 : d.y) + ")"
})
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;
})
});
svg{
background-color: lemonchiffon;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Mike Bostock's Bounded force layout also works and you can set the radius to match your nodes.
node.attr("cx", function(d) { return d.x = Math.max(radius, Math.min(width - radius, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(radius, Math.min(height - radius, d.y)); });
Just tweak with .strength function; e.g.
.force("charge", d3.forceManyBody().strength(-5) )

exit().remove() not working in my D3.js Force Layout Graph

I've created a simple D3 Force Layout graph. Please check it out in the JSFiddle here.
The graph is very basic - it features cities as nodes connected to nodes representing the ocuntry they are in. For simplicity, I've made only six nodes.
I've created a function called deleteNodeOnClick() and set it on the nodes like this
var nodeEnter = node.enter()
.append('g')
.attr('class', 'node')
.on("click", deleteNodeOnClick)
When you click on a node in the graph, that node gets removed from the data (actually for simplicity the first node gets removed from the data for now) however it does not get removed from the visual graph. You can look in the console and see that it is in fact removed from the data.
Why not? I am completely stumped.
The Code
var data = {
nodes: [{
name: "Canada"
}, {
name: "Montreal"
}, {
name: "Toronto"
}, {
name: "USA"
}, {
name: "New York"
}, {
name: "Los Angeles"
}],
links: [{
source: 0,
target: 1
}, {
source: 0,
target: 2
}, {
source: 3,
target: 4
}, {
source: 3,
target: 5
}, ]
};
var node;
var link;
var force;
var width = 400,
height = 400;
var svg = d3.select("body").append("svg")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight);
force = d3.layout.force()
.size([width, length])
.nodes(data.nodes)
.links(data.links)
.gravity(.1)
.alpha(0.01)
.charge(-400)
.friction(0.5)
.linkDistance(100)
.on('tick', forceLayoutTick);
var link = svg.selectAll(".link")
.data(data.links);
var linkEnter = link.enter()
.append('line')
.attr('class', 'link');
link.exit().remove();
node = svg.selectAll('.node')
.data(data.nodes, function(d){
return d.name;
});
node.exit().remove();
var nodeEnter = node.enter()
.append('g')
.attr('class', 'node')
.on("click", deleteNodeOnClick)
//.attr('r', 8)
//.attr('cx', function(d, i){ return (i+1)*(width/4); })
//.attr('cy', function(d, i){ return height/2; })
.call(force.drag);
nodeEnter
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 10)
.style("fill", "purple");
nodeEnter
.append("text")
.text(function(d) { return d.name })
.attr("class", "label")
.attr("dx", 0)
.attr("dy", ".35em");
force.start();
function forceLayoutTick(){
node.attr("transform", function(d) {
// Keep in bounding box
d.x = Math.max(10, Math.min(width - 10, d.x));
d.y = Math.max(10, Math.min(height - 10, 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 deleteNodeOnClick(d){
var dataBefore = JSON.parse(JSON.stringify(data.nodes));
// Just delete the first node, for demonstration purposes
data.nodes.splice(0, 1);
console.info("Node should be removed", dataBefore, data.nodes);
}
CSS
#graph {
width: 100%;
height: 100%;
}
#graph svg {
background-color: #CCC;
}
.link {
stroke-width: 2px;
stroke: black;
}
.node {
background-color: darkslategray;
stroke: #138;
width: 10px;
height: 10px;
stroke-width: 1.5px;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
.label {
display: block;
}
In D3, changing the data doesn't automagically change the SVG (or canvas, or HTML...) elements. You have to "repaint" your dataviz.
The good news is that you have (almost) all the selections. So, just to show you the general idea, I put all the rendering code inside a draw function, which is called on click:
function deleteNodeOnClick(d){
data.nodes = data.nodes.filter(function(e){
return e.name !== d.name;
});
draw();
}
Check the demo:
var data = {
nodes: [{
name: "Canada"
}, {
name: "Montreal"
}, {
name: "Toronto"
}, {
name: "USA"
}, {
name: "New York"
}, {
name: "Los Angeles"
}],
links: [{
source: 0,
target: 1
}, {
source: 0,
target: 2
}, {
source: 3,
target: 4
}, {
source: 3,
target: 5
}, ]
};
var node;
var link;
var force;
var width = 400,
height = 400;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
draw();
function draw() {
force = d3.layout.force()
.size([width, height])
.nodes(data.nodes)
.links(data.links)
.alpha(0.01)
.charge(-400)
.friction(0.5)
.linkDistance(100)
.on('tick', forceLayoutTick);
var link = svg.selectAll(".link")
.data(data.links);
var linkEnter = link.enter()
.append('line')
.attr('class', 'link');
link.exit().remove();
node = svg.selectAll('.node')
.data(data.nodes, function(d) {
return d.name;
});
node.exit().remove();
var nodeEnter = node.enter()
.append('g')
.attr('class', 'node')
.on("click", deleteNodeOnClick)
//.attr('r', 8)
//.attr('cx', function(d, i){ return (i+1)*(width/4); })
//.attr('cy', function(d, i){ return height/2; })
.call(force.drag);
nodeEnter
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 10)
.style("fill", "purple");
nodeEnter
.append("text")
.text(function(d) {
return d.name
})
.attr("class", "label")
.attr("dx", 0)
.attr("dy", ".35em");
force.start();
function forceLayoutTick() {
node.attr("transform", function(d) {
// Keep in bounding box
d.x = Math.max(10, Math.min(width - 10, d.x));
d.y = Math.max(10, Math.min(height - 10, 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 deleteNodeOnClick(d) {
data.nodes = data.nodes.filter(function(e) {
return e.name !== d.name;
});
draw();
}
#graph svg {
background-color: #CCC;
}
.link {
stroke-width: 2px;
stroke: black;
}
.node {
background-color: darkslategray;
stroke: #138;
width: 10px;
height: 10px;
stroke-width: 1.5px;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
.label {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>
Of course, as I said, that is just to give you the general idea: for instance, the click doesn't remove the links. But now you know how to put it to work.
Your function does not do anything to the graph: (just the data)
function deleteNodeOnClick(d){
var dataBefore = JSON.parse(JSON.stringify(data.nodes));
// Just delete the first node, for demonstration purposes
data.nodes.splice(0, 1);
console.info("Node should be removed", dataBefore, data.nodes);
}
re-render or remove from the graph...
Example, just to remove (specific) "dot"
function deleteNodeOnClick(d) {
var theElement = node.filter(function(da, i) {
if (i === d.index) {
console.log(da);
return true;
}
})
console.dir(theElement);
theElement.remove();
var dataBefore = JSON.parse(JSON.stringify(data.nodes));
// Just delete the first node, for demonstration purposes
data.nodes.splice(d.index, 1);
console.info("Node should be removed", dataBefore, data.nodes);
}
Note that does NOT remove the "line" connecting the OTHER node, I will leave that exercise up to you to do.
More compact version without all the logging etc.
function deleteNodeOnClick(d) {
d3.select(node[0][d.index]).remove();
data.nodes.splice(d.index, 1);
}
(added for OTHERS visiting this site)
For d3 version 4 this would be
function deleteNodeOnClick(d) {
d3.select(node._groups[0][d.index]).remove();
data.nodes.splice(d.index, 1);
}

How do I have a specific d3 node be an image in a force directed graph?

I have made a force directed graph which looks similar to this.
I would like the Nine Inch Nails node, in the centre, to be an image but the rest of the nodes to just be circles. Following this answer it seemed not too difficult but I just can't get my head around the combonation of svg, defs, patterns and d3.
My code is:
var simulation =
d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-50))
.force("collide", d3.forceCollide().radius(function (d) { return 15 - d.group}).strength(2).iterations(2))
.force("link", d3.forceLink().id(function(d, i) { return i;}).distance(20).strength(0.9))
.force("center", d3.forceCenter(width/2, height/2))
.force('X', d3.forceX(width/2).strength(0.15))
.force('Y', d3.forceY(height/2).strength(0.15));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
var defs = svg.append('svg:defs');
defs.append("svg:pattern")
.attr("id", "vit-icon")
.attr("width", 48)
.attr("height", 48)
.attr("patternUnits", "userSpaceOnUse")
.append("svg:image")
.attr("xlink:href", "http://placekitten.com/g/48/48")
.attr("width", 48)
.attr("height", 48)
.attr("x", width/2)
.attr("y", height/2)
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("id", function(d, i) { return 'c'+i})
.attr("r", radius)
.attr("fill", function(d) {
if(d.group==0) {return "url(#vit-icon)";}
else {return color(d.group); }
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
As I say, it seems straight forward in my mind. Basically what I think I'm trying to do is have the image in the svg pattern, then use an if statement to tell my root node to use the image url rather than fill with colour.
In dev tools I can see inspect the image and it shows the blue area it would take up and the node I want it to be associatated to has the 'url(#vit-icon)' as it's fill attribute. But it is not showing the image or any fill for that node.
What am I doing wrong? Or is this the complete wrong approach?
Thanks.
In your defs, just change:
.attr("x", width/2)
.attr("y", height/2)
To:
.attr("x", 0)
.attr("y", 0);
Here is a demo:
var nodes = [{
"id": 1,
}, {
"id": 2,
}, {
"id": 3,
}, {
"id": 4,
}, {
"id": 5,
}, {
"id": 6,
}, {
"id": 7,
}, {
"id": 8,
}];
var links = [{
source: 1,
target: 2
}, {
source: 1,
target: 3
}, {
source: 1,
target: 4
}, {
source: 2,
target: 5
}, {
source: 2,
target: 6
}, {
source: 1,
target: 7
}, {
source: 7,
target: 8
}];
var index = 10;
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
node,
link;
var defs = svg.append('svg:defs');
defs.append("svg:pattern")
.attr("id", "vit-icon")
.attr("width", 1)
.attr("height", 1)
.append("svg:image")
.attr("xlink:href", "http://66.media.tumblr.com/avatar_1c725152c551_128.png")
.attr("width", 48)
.attr("height", 48)
.attr("x", 0)
.attr("y", 0);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}).distance(100))
.force("collide", d3.forceCollide(50))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
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")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("circle")
.attr("r", d=> d.id === 1 ? 24 : 14)
.style("fill", function(d) {
if (d.id === 1) {
return "url(#vit-icon)";
} else {
return "teal"
}
})
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 + ")";
});
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart()
}
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 = undefined;
d.fy = undefined;
}
.link {
stroke: #aaa;
}
.node {
pointer-events: all;
stroke: none;
stroke-width: 40px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="300"></svg>

D3.js - Highlight chart elements when interacting with the legend & vice versa

I currently have a hover effect on each node, as well as a hover effect on each element of the legend.
I want to execute the effect when the user interacts with the legend so the relevant node on the chart is highlighted and visa versa. I also want to toggle the effect on and off.
So when the user clicks/hovers over a node the relevant legend item is highlighted in grey.
And when the user clicks/hovers over a legend item the relevant node is highlighted with a black stroke.
I have had a look at the following (Selectable Elements) but can't work out how to apply this to my code.
My understanding so far is that I need to apply a class (something like .selected) to the related element on mouseover & when toggled.
JSFiddle
//Nodes V2
var width = 300,
height = 300,
colors = d3.scale.category20b();
var force = d3.layout.force()
.gravity(.2)
.charge(-3000)
.size([width, height]);
//Svg Chart SVG Settings
var svg = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height);
var root = getData();
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
nodes.forEach(function(d, i) {
d.x = width/2 + i;
d.y = height/2 + 100 * d.depth;
});
root.fixed = true;
root.x = width / 2;
root.y = height / 2;
force.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll("line")
.data(links)
.enter()
.insert("svg:line")
.attr("class", "link");
var node = svg.selectAll("circle.node")
.data(nodes)
.enter()
.append("svg:circle")
.attr("r", function(d) { return d.size/200; })
//.attr('fill', function(d) { return d.color; }) // Use Data colors
.attr('fill', function(d, i) { return colors(i); }) // Use D3 colors
.attr("class", "node")
.call(force.drag)
.on('click', function(){
d3.select( function (d){
return i.li;
})
.style('background', '#000')
})
//Adding an event - mouseover/mouseout
.on('mouseover', function(d) {
d3.select(this)
.transition()//Set transition
.style('stroke', '#222222')
.attr("r", function(d) { return (d.size/200) + 2; })
})
.on('mouseout', function(d) {
d3.select(this)
.transition()
.style('stroke', '#bfbfbf')
.attr("r", function(d) { return (d.size/200) - 2; })
d3.select('ul')
});
//Add a legend
var legend = d3.select('#key').append('div')
.append('ul')
.attr('class', 'legend')
.selectAll('ul')
.data(nodes)
.enter().append('li')
.style('background', '#ffffff')
.text(function(d) { return d.name; })
.on('mouseover', function(d) {
d3.select(this)
.transition().duration(200)//Set transition
.style('background', '#ededed')
})
.on('mouseout', function(d) {
d3.select(this)
.transition().duration(500)//Set transition
.style('background', '#ffffff')
})
.append('svg')
.attr('width', 10)
.attr('height', 10)
.style('float', 'right')
.style('margin-top', 4)
.append('circle')
.attr("r", 5)
.attr('cx', 5)
.attr('cy', 5)
.style('fill', function(d, i) { return colors(i); });
//Add Ticks
force.on("tick", function(e) {
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; });
});
//Center Node
var userCenter = d3.select("svg").append("svg:circle")
.attr('class', 'user')
.attr('r', 30)
.attr('cx', width/2)
.attr('cy', height/2)
.style('fill', '#bfbfbf')
var label = d3.select('svg').append("text")
.text('USER')
.attr('x', width/2)
.attr('y', height/2)
.attr('text-anchor','middle')
.attr('transform', 'translate(0, 5)')
.style('font-size','12px')
.attr('fill','#666666')
//Fix Root
function flatten(root) {
var nodes = [];
function recurse(node, depth) {
if (node.children) {
node.children.forEach(function(child) {
recurse(child, depth + 1);
});
}
node.depth = depth;
nodes.push(node);
}
recurse(root, 1);
return nodes;
}
//Data
function getData() {
return {
"name": "flare",
"size": 0,
"children": [
{ "name": "Jobs", "size": 3743 },
{ "name": "Contact", "size": 3302 },
{ "name": "Dietary", "size": 2903 },
{ "name": "Bookings", "size": 4823 },
{ "name": "Menu", "size": 3002 },
{ "name": "Cards", "size": 3120 },
{ "name": "Newsletter", "size": 3302 }
]
};
}
You should use the same event(mouseover, mouseout) functions for the nodes and legend items.
//Adding an event - mouseover/mouseout
.on('mouseover', onMouseover)
.on('mouseout', onMouseout);
...
//Legend
.on('mouseover', onMouseover)
.on('mouseout', onMouseout)
Then, you use the data passed to the functions to select the correct elements to change the style.
function onMouseover(elemData) {
d3.select("svg").selectAll("circle")
.select( function(d) { return d===elemData?this:null;})
.transition()//Set transition
.style('stroke', '#222222')
.attr("r", function(d) { return (d.size/200) + 2; })
d3.select('#key').selectAll('li')
.select( function(d) { return d===elemData?this:null;})
.transition().duration(200)//Set transition
.style('background', '#ededed')
}
function onMouseout(elemData) {
d3.select("svg").selectAll("circle")
.select( function(d) { return d===elemData?this:null;})
.transition()
.style('stroke', '#bfbfbf')
.attr("r", function(d) { return (d.size/200) - 2; })
d3.select('#key').selectAll('li')
.select( function(d) { return d===elemData?this:null;})
.transition().duration(500)//Set transition
.style('background', '#ffffff')
}
Here is the updated JSFiddle

Categories