D3: Highlight connected/adjacent nodes [duplicate] - d3.js

I am working on a force directed graph in D3. I want to highlight the mouseover'd node, its links, and its child nodes by setting all of the other nodes and links to a lower opacity.
In this example, http://jsfiddle.net/xReHA/, I am able to fade out all of the links and nodes then fade in the connected links, but, so far, I haven't been able to elegantly fade in the connected nodes that are children of the currently mouseover'd node.
This is the key function from the code:
function fade(opacity) {
return function(d, i) {
//fade all elements
svg.selectAll("circle, line").style("opacity", opacity);
var associated_links = svg.selectAll("line").filter(function(d) {
return d.source.index == i || d.target.index == i;
}).each(function(dLink, iLink) {
//unfade links and nodes connected to the current node
d3.select(this).style("opacity", 1);
//THE FOLLOWING CAUSES: Uncaught TypeError: Cannot call method 'setProperty' of undefined
d3.select(dLink.source).style("opacity", 1);
d3.select(dLink.target).style("opacity", 1);
});
};
}
I am getting a Uncaught TypeError: Cannot call method 'setProperty' of undefined error when I try to set the opacity on an element I loaded from the source.target. I suspect this is not the right way to load that node as a d3 object, but I can't find another way to load it without iterating over all of the nodes again to find the ones that match the link's target or source. To keep the performance reasonable, I don't want to iterate over all the nodes more than necessary.
I took the example of fading the links from https://bl.ocks.org/mbostock/4062006:
However, that doesn't show how to alter the connected child nodes.
Any good suggestions on how to solve or improve this will be furiously upvoted :)

The error is because you are selecting the data objects (d.source and d.target) rather than the DOM elements associated with those data objects.
You've got the line highlighting working, but I would probably combine your code into a single iteration, like this:
link.style("opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
Highlighting the neighboring nodes is harder because what you need to know the neighbors for each node. This information isn't that easy to determine with your current data structures, since all you have as an array of nodes and an array of links. Forget the DOM for a second, and ask yourself how you would determine whether two nodes a and b are neighbors?
function neighboring(a, b) {
// ???
}
An expensive way to do that is to iterate over all of the links and see if there is a link that connects a and b:
function neighboring(a, b) {
return links.some(function(d) {
return (d.source === a && d.target === b)
|| (d.source === b && d.target === a);
});
}
(This assumes that links are undirected. If you only want to highlight forward-connected neighbors, then eliminate the second half of the OR.)
A more efficient way of computing this, if you have to do it frequently, is to have a map or a matrix which allows constant-time lookup to test whether a and b are neighbors. For example:
var linkedByIndex = {};
links.forEach(function(d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
Now you can say:
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
And thus, you can now iterate over the nodes and update their opacity correctly:
node.style("opacity", function(o) {
return neighboring(d, o) ? 1 : opacity;
});
(You may also want to special-case the mouseovered link itself, either by setting a self-link for every node in linkedByIndex, or by testing for d directly when computing the style, or by using a !important css :hover style.)
The last thing I would change in your code is to use fill-opacity and stroke-opacity rather than opacity, because these offer much better performance.

Related

Hide root node and edges in D3 v4 Tree Diagram

For the following Fiddle I'd like to hide the root node. Any help is appreciated, to view the code please view the fiddle.
I imagine I would do something like the following but I'm unsure of how/where to implement it:
if (d.depth > 0) {
...node is drawn
}
Image below:
Not drawing it is not "drawing it with zero opacity" or "hidden display". It's actually not appending the element.
Therefore, the simplest option is removing it from the data array. Just filter out the first node:
nodes = nodes.filter(function(d){
return d.depth != 0;
})
As 0 is falsy, this is the same of:
nodes = nodes.filter(function(d){
return d.depth;
})
And also filter out all links from it:
links = links.filter(function(d){
return d.depth != 1;
})
Here is your updated fiddle: https://jsfiddle.net/wa21csbc/
Also, since those elements are not painted anymore, you can move the dataviz to the left, thus occupying the empty SVG space. That space is there because we're filtering out the first node after d3.tree() calculated the positions.

Force-directed graph reusability failure?

first of all:
d3-version: "version": "3.5.17"
i have two differnet kinds of thematic maps, a proportional symbol map and a pseudo-demers cartogram with symbols (circles, same as in proportional symbol) as close to its origin as possible with collision detection and gravity.
However, i want to animate the circles from the proportional symbol map to the cartogram. In particular, i just want to trigger my force-directed graph for collision detection and gravity; this works fine for the first time. I provide an animation back from a cartogram to a proportional symbol map where each symbol just moves back to its centroid. Now, if i want to go back to the cartogram, the code fails and it says Uncaught TypeError: Cannot read property '1' of undefined, even though i trigger it he same time again as the first time. Is this some kind of bug in reusability of the force? or some kind of error on my side?
The problem can be tested here: https://particles-masterthesis.github.io/aggregation/; on the top left, you can switch between proportional symbol and cartogram.
the appropriate code i am using is the following:
case 'psm_cartogram':
let psm = this.currentViz;
information = {
data: psm.nodes,
symbols: psm.symbols
};
upcomingViz.obj = this.canvas.drawCartogram(
null,
this.currentViz.constructor.name === "Cartogram",
information,
() => {
this.currentViz.hide(false, true);
this.fadeOutParticles();
}
);
upcomingViz.type = 'cartogram';
resolve(upcomingViz);
break;
The key part is in the information object, where i share the same objects between the cartogram and the proportional symbol map
in the cartogram, i have the following important code:
```
this.force = this.baseMap._d3.layout.force()
.charge(0)
.gravity(0)
.size([this.width - this.symbolPadding, this.height - this.symbolPadding]);
this.nodes = keepInformation.data;
this.node = keepInformation.symbols;
this.force
.nodes(this.nodes)
.on("tick", this.tick.bind(this, 0.0099))
.start();
...
tick(gravity) {
this.node
.each(this.gravity(gravity))
.each(this.collide(0.25))
.attr("cx", d => { return d.x; })
.attr("cy", d => { return d.y; });
}
gravity(k) {
return d => {
d.x += (d.x0 - d.x) * k;
d.y += (d.y0 - d.y) * k;
};
}
//the collide function is not shown as it is a simple quadtree
```
if it helps in any way, the code is also available at https://github.com/particles-masterthesis/aggregation/src/js/visualization/map The main code is the transition-manager and the two types of maps.
i am thankful for any suggestions and support i can get, even if its a simple hint that i could check out.
PS:
These are two screenshots; the first one is important for the different logs of cartogram:132 which was a console.log(this.node) in the tick-function before doing any gravity etc. and the second one mentions the error.
for more understanding of the first log:
it starts by logging this.node in the tick function; afterwards a visualization change to psm got triggered (cartogram_psm) with a change back to cartogram later on and then the error appeared.
okay so i could figure out my problem:
transition chaining was incorrect in my way and so, the mysterious attributes on the object appeared;
rewriting all transitions with this method helped (https://stackoverflow.com/a/17101823/1472902)

D3.js Highlighting links on node selection

so first, I have data in the format the same as miserables.json below
https://bl.ocks.org/mbostock/4062045
I would like to be able to make the link connected to my node red when I click the node.
So the psuedo⁻code would be like
selectAll(.links)
if links.source=nodeID or links.target=nodeID
then links.color=red
But I have not be able to do it. My ultimate goal would be intergrate it with Arc diagram below
http://bl.ocks.org/sjengle/5431779
Your pseudocode is a good start. You can use filter to implement the if condition in a selection. Note that the .source and .target of links are edited by d3, they are no longer the id of the nodes, but the node themselves:
thisNode = nodeObject; // where nodeObject is the javascript object for the node, it's probably called "d" in your function.
d3.selectAll(".link")
.filter(function(d) {
return (d.source === thisNode) || (d.target === thisNode);
})
.style("stroke", "red")
Expanding on top of #laurent's answer, to "reset" color of links that potentially were painted red during a previous interaction:
thisNode = nodeObject; // where nodeObject is the javascript object for the node, it's probably called "d" in your function.
d3.selectAll(".link")
.style("stroke",function(d) {
return d.source === thisNode || d.target === thisNode ? "red" : "#888888";
})

Using data(...) in D3 force layout to update the graph

I am using the D3 force layout and adding code to allow the user to select individual nodes (here, select is not related to D3 select() but indicates the highlighted state of the node from the user's perspective). Only one node should be selected at one time. So, when the user clicks on a node which is not selected, I want to deselect any selected nodes.
I am using an attribute, selected, on the node and struggling to set that using the D3 data(...) or datum(...) method. In fact, I couldn't get it to work like that. What I have now seems kind of kludgy so I am hoping there is a cleaner way.
function deselectAll() {
var sc = d3.selectAll("circle")
.filter(function(d) {return (d.selected == "y")});
var circles = sc.data();
sc.transition()
.duration(50)
.style("stroke-width", "1px")
.style("stroke", "#3182bd");
if(null != circles && circles.length > 0) {
for(i=0; i<circles.length; i++) {
circles[i].selected = "n";
}
}
}
The problem is that adding .data("n") to the chain doesn't set 'selected' to "n". Is it possible to do that as part of the d3 chain?
Many selection (or transition) functions in d3 pass the current datum from each node to your defined functions. You can modify the contents of this datum at any time.
Using your example, if you want to set the nodes that have d.selected == "y" from "y" to "n" you can use your filtered selection like this:
sc.each(function(d) {
d.selected = "n";
});
This function will be invoked once for each node in the selection (each having "y" for selected as per your filter) so you can simply change the property value on the datum.

Is it possible to create pie charts with object consistency?

the pie chart update example on the bl.ocks site doesn't update the elements 'in place':
http://bl.ocks.org/j0hnsmith/5591116
function change() {
clearTimeout(timeout);
path = path.data(pie(dataset[this.value])); // update the data
// set the start and end angles to Math.PI * 2 so we can transition
// anticlockwise to the actual values later
path.enter().append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc(enterAntiClockwise))
.each(function (d) {
this._current = {
data: d.data,
value: d.value,
startAngle: enterAntiClockwise.startAngle,
endAngle: enterAntiClockwise.endAngle
};
}); // store the initial values
path.exit()
.transition()
.duration(750)
.attrTween('d', arcTweenOut)
.remove() // now remove the exiting arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
Instead, it just treats the new array of value as brand new data and resizes the chart accordingly.
I've created a fiddle demonstrating the issue very simply:
http://jsfiddle.net/u9GBq/23/
If you press 'add', it add a random int to the array: this works as intended.
If you press 'remove', the only element getting transitioned out is always the last element to have entered the pie. In short, it behaves like a LIFO stack.
The expected behaviour is for the relevant pie arc to get transitioned out instead.
Is it possible to apply object consistency to pies? I've also tried adding a key function (not demonstrated on the fiddle) but that just breaks (oddly enough it works fine with my stacked graphs).
Thank you.
The easiest solution to this problem is to set missing values to zero, rather than removing them entirely, as in Part III of the Pie Chart Update series of examples. Then you get object constancy for free: you have the same number of elements, in the same order, across updates.
Alternatively, if you want a data join as in Part IV, you have to tell D3 where the entering arcs should enter from, and where the exiting arcs should exit to. A reasonable strategy is to find the closest neighboring arc from the opposite data: for a given entering arc, find the closest neighboring arc in the old data (pre-transition); likewise for a given exiting arc, find the closest neighboring arc in the new data (post-transition).
To continue the example, say you’re showing sales of apples in different regions, and want to switch to show oranges. You could use the following key function to maintain object constancy:
function key(d) {
return d.data.region;
}
(This assumes you’re using d3.layout.pie, which wraps your original data and exposes it as d.data.)
Now say when you transition to oranges, you have the following old data and new data:
var data0 = path.data(), // retrieve the old data
data1 = pie(region.values); // compute the new data
For each entering arc at index i (where d is data1[i]), you can step sequentially through preceding data in data1, and see if you can find a match in data0:
var m = data0.length;
while (--i >= 0) {
var k = key(data1[i]);
for (var j = 0; j < m; ++j) {
if (key(data0[j]) === k) return data0[j]; // a match!
}
}
If you find a match, your entering arcs can start from the matching arc’s end angle. If you don’t find a preceding match, you can then look for a following matching arc instead. If there are no matches, then there’s no overlap between the two datasets, so you might enter the arcs from angle 0°, or do a crossfade. You can likewise apply this technique to exiting arcs.
Putting it all together, here’s Part V:
Ok, found the solution.
The trick was to pass the key this way:
path = path.data(pie(dataset), function (d) {return d.data}); // this is good
as opposed to not passing it, or passing it the wrong way:
path = path.data(pie(dataset, function (d) {return d.data})); // this is bad
And here's an updated fiddle with a working transition on the right arc! :)
http://jsfiddle.net/StephanTual/PA7WD/1/

Resources