apply several mouseover events to neighboring (connected) nodes - d3.js

I have a network diagram (force-directed graph), a scatterplot, and a table that are all interconnected (see jsFiddle). I have the interconnections working the way I want them for mouseover events. I would like to modify my code so that when I mouseover a node in the network diagram, not only is the moused-over node highlighted (and its connections in the scatterplot and table), but its immediate neighbor nodes are also highlighted (as well as their connections in the scatterplot and table).
I looked at the information in Highlight selected node, its links, and its children in a D3 force directed graph for help. Somewhere along the way (not exactly sure where) I found an example of a function that helps define the connected nodes, isConnected().
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
I'd like to incorporate this function into my mouseover events, perhaps with an if() statement, so that I can do all of the "highlighting" that I want. But, I'm new to D3 and js and am not sure how to set it up.
Below is the snippet of code (from the jsFiddle) that I would like to modify. I would appreciate any suggestions or pointers to other examples.
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", function(d) { return "node " + d.name + " " + d.location; })
.call(force.drag)
.on("mouseover", function(d) {
// I would like to insert an if statement to do all of these things to the connected nodes
// if(isConnected(d, o)) {
d3.select(this).select("circle").style("stroke-width", 6);
d3.select(this).select("circle").style("stroke", "orange");
d3.select(this).select("text").style("font", "20px sans-serif");
d3.selectAll("rect." + d.location).style("stroke-width", 6);
d3.selectAll("rect." + d.location).style("stroke", "orange");
d3.selectAll("text." + d.location).style("font", "20px sans-serif");
d3.selectAll("tr." + d.name).style("background-color", "orange");
//}
})
.on("mouseout", function(d) {
// if(isConnected(d, o)) {
d3.select(this).select("circle").style("stroke-width", 1.5);
d3.select(this).select("circle").style("stroke", "gray");
d3.select(this).select("text").style("font", "12px sans-serif");
d3.selectAll("rect." + d.location).style("stroke-width", 1.5);
d3.selectAll("rect." + d.location).style("stroke", "gray");
d3.selectAll("text." + d.location).style("font", "12px sans-serif");
d3.selectAll("tr." + d.name).style("background-color", "white");
//}
});

In another scenario I would put my visual objects into a graph data structure and navigate that to efficiently update the appropriate items. But this is d3, but so we will do the same thing but instead of a graph data structure that we create we will use d3 selections (which can be like graphs but for this they will look a lot more like arrays). Algorithmically this approach will not be as efficient, but our graphs are small.
So working backwards I will want a selection that includes only the picked node's neighboring
nodes. I will do this by selecting all the circles and then using the d3 selection filter method to reduce that to only those circles that are neighbors.
Of course then I need the list of neighbors, but a few nice js array methods make short work of that. The final relevant code (in mouseover) is not even that long - but I've added a bunch of comments:
// Figure out the neighboring node id's with brute strength because the graph is small
var nodeNeighbors = graph.links.filter(function(link) {
// Filter the list of links to only those links that have our target
// node as a source or target
return link.source.index === d.index || link.target.index === d.index;})
.map(function(link) {
// Map the list of links to a simple array of the neighboring indices - this is
// technically not required but makes the code below simpler because we can use
// indexOf instead of iterating and searching ourselves.
return link.source.index === d.index ? link.target.index : link.source.index; });
// Reset all circles - we will do this in mouseout also
svg.selectAll('circle').style('stroke', 'gray');
// now we select the neighboring circles and apply whatever style we want.
// Note that we could also filter a selection of links in this way if we want to
// Highlight those as well
svg.selectAll('circle').filter(function(node) {
// I filter the selection of all circles to only those that hold a node with an
// index in my listg of neighbors
return nodeNeighbors.indexOf(node.index) > -1;
})
.style('stroke', 'orange');
You can also try the fiddle
I think the important d3 concept relevant here is that when you associate data with an element (usually using the data() or datum() methods on selections) then that data sticks with that element and any future selections will always use it.
To link other aspects you can pull those attributes in a similar way and link them through d3. For example for the location rectangles you could add to mouseover:
var nodeLocations = graph.links.filter(function(link) {
return link.source.index === d.index || link.target.index === d.index;})
.map(function(link) {
return link.source.index === d.index ? link.target.location : link.source.location; });
d3.selectAll("rect").filter(function(node) { return nodeLocations.indexOf(node.location) > -1; }) .style("stroke", "cyan");

This thing I built does that with the Ego Network feature:
https://gist.github.com/emeeks/4588962
Add a .on("mouseover", findEgo) to your nodes and the following should work, as long as you have some kind of identifying uid attribute, which you could generate when you load the nodes if one isn't handy. It's a bit of overkill, since it allows for n-degree ego networks, and creates an aggregated table for other network analysis functions, but the basic functionality will give you what you want and you or other users might find that aspect useful:
function findEgo(d) {
var computedEgoArray = findEgoNetwork(d.id, 1, false,"individual");
d3.selectAll("circle.node").style("fill", function(p) {return p.id == d.id ? "purple" : computedEgoArray.indexOf(p.id) > -1 ? "blue" : "pink"})
}
function findEgoNetwork(searchNode, egoNetworkDegree, isDirected, searchType) {
var egoNetwork = {};
for (x in nodes) {
if (nodes[x].id == searchNode || searchType == "aggregate") {
egoNetwork[nodes[x].id] = [nodes[x].id];
var z = 0;
while (z < egoNetworkDegree) {
var thisEgoRing = egoNetwork[nodes[x].id].slice(0);
for (y in links) {
if (thisEgoRing.indexOf(links[y].source.id) > -1 && thisEgoRing.indexOf(links[y].target.id) == -1) {
egoNetwork[nodes[x].id].push(links[y].target.id)
}
else if (isDirected == false && thisEgoRing.indexOf(links[y].source.id) == -1 && thisEgoRing.indexOf(links[y].target.id) > -1) {
egoNetwork[nodes[x].id].push(links[y].source.id)
}
}
z++;
}
}
}
if (searchType == "aggregate") {
//if it's checking the entire network, pass back the entire object of arrays
return egoNetwork;
}
else {
//Otherwise only give back the array that corresponds with the search node
return egoNetwork[searchNode];
}
}

Related

dc.js exclude the brushed area and highlight rest

I'm not data-viz expert or d3, I have found plenty of examples to how to build brushing and zoom for example Mike.
They all have shown how to filter to the brushed area but I want to achieve to reverse of that effect, how?
Can someone through me ideas how to achieve it?
I don't know why I assumed you meant a bar chart when you linked to an area chart. You can ignore the highlighting section and skip to filtering if you're interested in doing this with line charts. There is no highlighting of line chart, just the brush itself.
Highlighting the bars in reverse
This isn't all that hard, but it's somewhat messy because we replace an undocumented function in the chart. Like most things in dc.js, if there isn't an option, you can usually replace the functionality (or add or change stuff once the chart has rendered/drawn).
Here there's a specific, public function which fades the deselected areas. It's called fadeDeselectedArea. (Actually it both fades and un-fades when the chart is ordinal, but we'll ignore that part.)
The original function looks like this:
_chart.fadeDeselectedArea = function () {
var bars = _chart.chartBodyG().selectAll('rect.bar');
var extent = _chart.brush().extent();
if (_chart.isOrdinal()) {
if (_chart.hasFilter()) {
bars.classed(dc.constants.SELECTED_CLASS, function (d) {
return _chart.hasFilter(d.x);
});
bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
return !_chart.hasFilter(d.x);
});
} else {
bars.classed(dc.constants.SELECTED_CLASS, false);
bars.classed(dc.constants.DESELECTED_CLASS, false);
}
} else {
if (!_chart.brushIsEmpty(extent)) {
var start = extent[0];
var end = extent[1];
bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
return d.x < start || d.x >= end;
});
} else {
bars.classed(dc.constants.DESELECTED_CLASS, false);
}
}
};
source link
We'll ignore the ordinal part because that's only individual selection, not brushed selection. Here is the reverse of the second part:
spendHistChart.fadeDeselectedArea = function () {
var _chart = this;
var bars = _chart.chartBodyG().selectAll('rect.bar');
var extent = _chart.brush().extent();
// only covering the non-ordinal (ranged brush) case here...
if (!_chart.brushIsEmpty(extent)) {
var start = extent[0];
var end = extent[1];
bars.classed(dc.constants.DESELECTED_CLASS, function (d) {
return d.x >= start && d.x < end;
});
} else {
bars.classed(dc.constants.DESELECTED_CLASS, false);
}
};
Creating a variable _chart is just to keep the code the same as much as possible. You can see that d.x >= start && d.x < end is exactly the opposite of d.x < start || d.x >= end
Reversing the filtering
We'll need to add a filterHandler to the chart in order to reverse the filtering. Again, we'll base it off the default behavior, but here there's a legitimate customization point so we don't have to replace a function, just supply one:
spendHistChart.filterHandler(function(dimension, filters) {
if(filters.length === 0)
dimension.filter(null);
else {
// assume one RangedFilter but apply in reverse
// this is less efficient than filterRange but it shouldn't
// matter much unless the data is huge
var filter = filters[0];
dimension.filterFunction(function(d) {
return !filter.isFiltered(d);
})
}
});
Again, we cut out the cases we don't care about. There is no reason to be general about something that has a specific purpose and it will only cause maintenance problems. The only two cases we care about are no filter and one range filter.
Here the RangedFilter already supplies a filter function, so we can just call it and not (!) the result. This will be slightly less efficient than the filterRange but crossfilter has no native support for multiple ranges (or the inverse of a range).
That's it! Fiddle here: http://jsfiddle.net/gordonwoodhull/46snsbc2/8/

d3 append invisible if data is not same as previous

d3 newb is trying something else:
I want to add a date label to my bar chart only if the date is not the same as in the previous bar.
bar.append("text")
.attr("visibility", function(d,i){
if(d.Datum == data[i-1].Datum) return "hidden"})
.attr("x", padding)
.attr("y", barHeight-2)
.attr("fill", "Gainsboro")
.text(function(d){ return d.Datum})
So I thought I add a visibility to my text label. however I cannot access the previous date from the data-object... probably this is an easy one for someone not newbie as me...
data example (before CSV import)
Datum,Name,Kategorie,Betrag
01/10/15,,Lohn,1586.7
02/10/15,,lunch,-4.55
So assuming that after d3 parses the text file you are left with data like:
var data = [
{
Datum: "01/10/15",
Name: "",
Kategorie: "Lohn",
Betrag: 1586.7
},{
...
}
];
I'd pre-process the data to contain a bool about whether or not it is the first instance of that date:
data.forEach(function(d,i){
d.isFirstInstanceOfDate = (i === 0 || d.Dataum !== data[i-1].Datum);
});
Then assuming that bar is a selection of gs elements (which already contain a rect), I'd filter them and only append the text on the first instance:
bar
.filter(function(d){
return d.isFirstInstanceOfDate
})
.append('text')
...
The index starts from 0. By subtracting 1 from it the first time you get an error. You need to check whether i > 0 so that you don't do out of bounds.
For example:
.style('visibility', function (d, i) {
if (i > 0) {
if (d.datetime === data[i - 1].datetime) {
return 'hidden';
}
}
return 'visible';
});
Here are two sample fiddles:
http://jsfiddle.net/00drii/y079vw7e/
http://jsfiddle.net/00drii/dmhs0gza/

Append Circles to 1 Line in a D3 Multi-Line chart

I have a multi-line chart representing 8 different series of values, for a given date:
http://bl.ocks.org/eoiny/8548406
I have managed to filter out series1 and append circles for each data-point for series1 only, using:
var filtered = city
.filter(function(d){
return d.name == "series1"
})
filtered
.selectAll('circle')
.data(
function(d){return d.values}
)
.enter().append('circle')
.attr({
cx: function(d,i){
return x(d.date)
},
cy: function(d,i){
return y(d.pindex)
},
r: 5
})
However I am trying to append 4 circles to my series1 line, one for each of the following values only:
min value in series1,
max value in series1,
1st value in series1,
last value in series1.
I approached this problem by looking at the "filtered" array and I tried using something like this to catch the min & max values to start with:
.attr("visibility", function(d) {
if (d.pindex == d3.max(filtered, function(d) { return d.pindex; })) {return "visible"}
if (d.pindex == d3.min(filtered, function(d) { return d.pindex; })) {return "visible"}
else { return "hidden" }
;})
But I'm somehow getting muddled up by the fact that the data I need is in an object within the filtered array. I know that filtered should look like this:
[{
name: "series1",
values: [{date: "2005-01-01",
pindex: "100"},
{date: "2005-02-01"
pindex: "100.4"}, ...etc for all data points i.e. dates
]
}]
So I tried something like this:
d.pindex == d3.max(filtered, function(d) { return d.values.pindex; })
but I'm still getting a bit lost. Does anyone have any ideas?
In general, you probably want to filter your data rather than DOM elements. So instead of using city.filter you might use cities.filter to get the data array you're interested in. More importantly, you probably want to filter the data passed to the new circle selection, rather than creating all circles and then selectively showing or hiding them. I might try:
filtered
.selectAll('circle')
.data(function(d){
var points = d.values;
// create the array of desired points, starting with the easy ones
var circleData = [
// first
points[0],
// last
points[points.length - 1]
];
// now find min and max
function getValue(d) { return d.pindex; }
// d3.max returns the max value, *not* the object that contains it
var maxVal = d3.max(points, getValue);
// Note that you might have multiple points with the max. If you
// don't want them all, just take maxPoints[0]
var maxPoints = points.filter(function(d) { return d.pindex === maxVal; });
// same for min
var minVal = d3.min(points, getValue);
var minPoints = points.filter(function(d) { return d.pindex === minVal; });
// stick them all together
return circleData.concat(maxPoints).concat(minPoints);
})
.enter().append('circle')
// etc
Key points:
Filter your data, not your DOM. It's less expensive processing, easier to debug, and generally much easier to get your head around.
d3.min and d3.max don't return the object with the max value, they return the value itself, hence your TypeError.

Hiding text elements in D3 chord diagram

Similar to this example:
I have a function that highlights the chords linked to the selected group and hides all unrelated chords:
function fade() {
return function(d, i) {
svg.selectAll("path.chord")
.filter(function(d) {
return d.source.index != i && d.target.index != i;
})
.style("visibility", "hidden");
};
}
I was trying to extend this so that it not only hides the chords, but also the text labels for groups that are not connected to the currently selected group.
Is it possible to combine these two operations into a single function that I pass to .on("mouseover",[function])?
You can select with a logical OR like this:
svg.selectAll("path.chord, text.groupLabel")
see the documentation on selections for more details

d3js highlighting connected nodes

I want to list all the nodes that are connected to a node that I rollover with a mouse in a text field, been trying to use the filter function and it sort of does what I need since it affects the stroke width of the linked objects but I'd like to also output the node names too.
.on("mouseover", function(da) {
link.filter(function(db) { return da.group == db.groupColor; })
.transition()
.style("stroke-width", 8);
selectedText.text("Currently Selected: "+da.name+"is connecting to sensors: "+link.filter(function(db) { return da.group == db.groupColor; }) );
right now I am getting an output of "bike brian is connected to sensors: [object svgline element]"
but what I want is to be able to return all the names of the other nodes... does that make sense? so that it might say "bike brian is connected to sensors: accelerometer, gps, etc..."
Assuming your links are using standard network syntax, you could do the following:
var filteredLinks = link.filter(function(db) { return da.group == db.groupColor; });
var sensorList = '';
for (x in filteredLinks) {
if (filteredLinks[x]) {
filteredLinks[x].source.id == originalSensorID ? sensorList += filteredLinks[x].target.id : sensorList += filteredLinks[x].source.id
}
}
You'll probably want to add commas and spaces to the sensorList variable. It's a bit cumbersome, but I wasn't able to make the .each() method work with a filter, so it's the best I can come up with.

Resources