Merge selection with groups - d3.js

I've worked out a consistent pattern for using the new selection merge which is brilliant for reusable charts where data and/or scales may change.
I've also been using the key function successfully.
However, I seem to get a problem when entering and appending a group with multiple elements. The data is successfully updated in the group but not the appended elements.
I've got round it by adding a fix (see below) but I'm sure it is something really obvious that needs to be changed to resolve it.
Any thoughts?
//define data group
var my_group = svg.selectAll(".data_group")
.data(my_data,function(d){return d.id});
//enter new groups
var enter = my_group.enter()
.append("g")
.attr("class","data_group");
//append items to group
enter.append("text").attr("class","group_item group_text")
enter.append("circle").attr("class","group_item group_circle");
//merge and remove
my_group.merge(enter);
my_group.exit().remove();
//fix added to reset changing data for bars.
d3.selectAll(".group_item").each(function(d){
d3.select(this)._groups[0][0].__data__ = d3.select(this)._groups[0][0].parentElement.__data__;
});
d3.selectAll(".group_text")
.... add properties to text - ie x,y,fill,text-anchor,text
d3.selectAll(".group_circle")
.... add properties to circle - ie cx,cy,fill,stroke,radius

There is absolutely no need for selecting the parent group, getting its data and rebinding it to the child elements, as the code in your question and the other answer do. This is bending over backwards. Also, do not delete/re-append elements, as suggested, which is not an idiomatic D3 approach.
The thing is simple: the new data is there for the children elements in the "enter" selection. You just need to use the parent's selection (with select()) to propagate them.
Here is a basic demo, using (most of) your code. The code generates from 1 to 5 data objects, with a random property called someProperty. You'll see that, using your each(), only the children elements in the "enter" selection are changed:
var svg = d3.select("svg");
d3.interval(function() {
var data = d3.range(1 + ~~(Math.random() * 4)).map(function(d) {
return {
id: "id" + d,
"someProperty": ~~(Math.random() * 100)
}
});
update(data);
}, 2000);
function update(my_data) {
var my_group = svg.selectAll(".data_group")
.data(my_data, function(d) {
return d.id
});
my_group.exit().remove();
var enter = my_group.enter()
.append("g")
.attr("class", "data_group");
enter.append("text").attr("class", "group_item group_text")
enter.append("circle").attr("class", "group_item group_circle");
my_group = my_group.merge(enter);
console.log("---")
d3.selectAll(".group_text").each(function(d) {
console.log(JSON.stringify(d))
});
}
.as-console-wrapper { max-height: 100% !important;}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
Now, if we use your parent's selection...
my_group.select(".group_text").each(function(d) {
console.log(d)
})
... you'll see that all properties are updated:
var svg = d3.select("svg");
d3.interval(function() {
var data = d3.range(1 + ~~(Math.random() * 4)).map(function(d) {
return {
id: "id" + d,
"someProperty": ~~(Math.random() * 100)
}
});
update(data);
}, 2000);
function update(my_data) {
var my_group = svg.selectAll(".data_group")
.data(my_data, function(d) {
return d.id
});
my_group.exit().remove();
var enter = my_group.enter()
.append("g")
.attr("class", "data_group");
enter.append("text").attr("class", "group_item group_text")
enter.append("circle").attr("class", "group_item group_circle");
my_group = my_group.merge(enter);
console.log("---")
my_group.select(".group_text").each(function(d) {
console.log(d)
})
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
Finally, in your now deleted answer you're using my_group.selectAll(). The problem is that selectAll() does not propagate the data.
Have a look at this table I made:
Method
select()
selectAll()
Selection
selects the first element that matches the selector string
selects all elements that match the selector string
Grouping
Does not affect grouping
Affects grouping
Data propagation
Propagates data
Doesn't propagate data
Pay attention to the propagates data versus doesn't propagate data.

The more d3 way of copying the data bound to the parent g elements
No need to add the fix
d3.selectAll(".group_text")
.datum(function () { return d3.select(this.parentNode).datum(); } )
// .... add properties to text - ie x,y,fill,text-anchor,text
d3.selectAll(".group_circle")
.datum(function () { return d3.select(this.parentNode).datum(); } )
// .... add properties to circle - ie cx,cy,fill,stroke,radius

Related

D3 - using enter() and exit() selections to update child elements

I have g.row elements containing g.cell elements, each containing a rect element. My nested data is bound to g.row and then g.cell. The rect elements access the data bound to g.cell.
At the moment my enter and exit selections add and remove g.cell. It would be more efficient to have them add and remove the rect elements, because g.cell has events bound to it that I need to reassign. But is this possible? I can't see how to get it to work.
I've managed to run cell.exit().selectAll("rect").remove(); which works fine. But cell.enter().selectAll("g.cell").append("rect"); throws an error ("[this code] is not a function"). While cell.enter().append("rect") doesn't append a rect.
Current code on g.cell:
var cell = row.selectAll("g.cell")
.data(function(d){
return d.value.filter(function(p){
if (p[1]=='') {
return horizNodesCopy.indexOf(p[0])!=-1;
} else {
return horizNodesCopy.indexOf(p[0]+' -- '+p[1])!=-1;
}
});
});
var cell2 = cell.enter().append("g")
.attr("class",function(d,i,j){ return "cell cell_"+i; })
.attr('transform',function(d,i,j){
if (d[1]=='') {
return 'translate('+ x(d[0]) +',0)';
} else {
return 'translate('+ x(d[0]+' -- '+d[1]) +',0)';
}
});
addRectangles(cell2,colorScale);
cell.exit().remove();
This feels like it's going to be something obvious :/

d3js update and maintain structure of visualization

Is it possible to (and if yes - how to fix render()) to perform properly updating divs after click on [x] with d3?
var id_names = {},
render = function (id) {
var names = id_names[id],
divs = d3.select("#filelist").selectAll("div").data(names),
denter = divs.enter().append("div");
denter.append("div")
.classed("txt", true);
denter.append("div")
.classed("del", true);
divs.select("div .txt")
.text(function (d, i) {return i + " :: " + d});
divs.select("div .del")
.text("[x]")
.on("click", function (d, i) {
// remove element from array
id_names[id].splice(i, 1)
render(id)
});
divs.exit().remove()
};
// {id: [name, nameN]}
id_names[11] = ["aaa", "bbb", "cc"]
render(11)
I think the problem is that you bind the data to divs, and you have more divs in the denters.
So if you add some class (or id) to the containing divs, you get the desired effect (I hope).
See here:
JSFiddle
Or here, the core changes:
divs = d3.select("#filelist").selectAll(".container")
.data(name),
denter = divs.enter().append("div")
.attr('class', 'container');
(A side note: I am not a fan of modifying in a function that is in an outside scope, as you are doing it with id_name here...). This may lead to very nasty side effects...)
Hope this helps!

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.

D3 Zoomable Treemap changing the children accessor

I am trying to use Mike Bostock's zoomable treemap http://bost.ocks.org/mike/treemap/ with one modification. Instead of using nested JSON data, I have have a simple mapping from parents to a list of children. I built a function, getChildren(root), that simply returns root's children, or null if root does not have any children.
I have tried replacing all instances of d.children() with getChildren(d) in the treemap javascript file, but it seems that it is not working properly.
The resulting page shows the orange bar as normal up top, but nothing else displays correctly (i.e. there are no rectangles underneath the orange bar, just empty gray space). All the text from the children is mashed up in the top left corner of the empty gray space, so it might be that coordinates are not being assigned correctly.
Any ideas??
Thanks!
It looks like there were a few issues here:
Your data structure doesn't seem to be referencing the child nodes:
var nMap = {};
nMap.papa = {};
nMap.papa["children"] = [];
nMap.papa["children"].push({
"name": "c1"
});
// snip
nMap.c1 = {
size: 5
};
Unless I'm missing something, your getChildren function gets the { name: "c1" } object but never looks up nMap.c1. I'm not exactly certain what your alternative data structure is trying to achieve, but it seems like the most obvious option is to use a flat map of nodes, with children referenced by id, like this:
var nMap = {};
nMap.c1 = {
name: "c1",
value: 5
};
nMap.c2 = {
name: "c2",
value: 5
};
nMap.c3 = {
name: "c3",
value: 5
};
nMap.papa = {
name: "papa",
children: ['c1', 'c2', 'c3']
};
With a structure like this, you can map to the real children in the getChildren function:
function getChildren(par){
var parName = par.name,
childNames = parName in nMap && nMap[parName].children;
if (childNames) {
// look up real nodes
return childNames.map(function(name) { return nMap[name]; });
}
}
Your children were using size instead of value to indicate weight, and the rest of the code expected value (so they all had weight 0).
Because you're using the "zoomable" treemap approach, which uses a specialized version of the treemap layout, you don't need to specify the .children accessor of the treemap layout. Instead, use your custom accessor in the the custom layout helper:
function layout(d) {
// get the children with your accessor
var children = getChildren(d);
if (children && children.length > 0) {
treemap.nodes({ children: children });
children.forEach(function(c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
}
Working fiddle here: http://jsfiddle.net/nrabinowitz/WpQCy/

apply several mouseover events to neighboring (connected) nodes

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];
}
}

Resources