d3 idiom for appending grouped elements on enter()? - javascript

Is there a better idiom to append a <svg:g> grouped set of elements to a container on an enter() selection as part of a generic update pattern?
var cell = d3.select(this); // parent container
cell = cell
.selectAll('.plot').data([0]); // seems kludgy
cell
.enter().append('g').classed('plot',true); // append the wrapping <g> element
cell = cell
.selectAll("circle")
.data(_.values(slice));
cell
.enter().append("circle"); // general enter() -- create plot elements
cell.attr() // etc. general update--style plot elements
cell
.exit().remove();
Of course,
if ( cell.select('g.plot').empty() ) {
cell = cell.append('g').classed('plot', true);
}
instead of the first two statements would do it too, but this seems like a very common operation and the selectAll().data([0]) seems contrived--is there a more elegant d3 idiom?

For creating an element if necessary or selecting it otherwise, I would usually use a structure similar to your if block as opposed to using a data join with meaningless data.
Not only is it shorter code, but it means that you're not carrying around that extra data property on your element when it doesn't have any significance. It's also easier for other people to figure out what you're doing!
The only thing I would change is to actually save the selection that you're using for the .empty() test, since if it's not empty you'll be using it. (You could use another variable to save the this-selection, but d3.select(this) isn't exactly a high computation method call to repeat, and even then you'll only be repeating it once, when you first create the group.)
var plot = d3.select(this) // this is the parent container
.selectAll('g.plot'); //select the plot group if it exists
if ( plot.empty() )
plot = d3.select(this).append('g').classed('plot',true);
//create the plot group if necessary
var cell = plot.selectAll("circle") //now select/create data elements as usual
.data(_.values(slice));
cell
.enter().append("circle"); // general enter() -- create plot elements
cell.attr() // etc. general update--style plot elements
cell
.exit().remove();

Simply append a "g" for every new group of elements that you need.
var cell = d3.select(this)
.append("g")
.attr("class","plot")
.selectAll("circle")
.data(…);
cell.enter().append("circle")
.attr(…);
cell.exit().remove();
What doesn't work here?

Related

D3 exit selecting wrong data

I'm trying to build kind of real time graph using D3.js. Code is available at https://plnkr.co/edit/hrawv8CTBIsJf2QWTBMb?p=preview.
The source data represent user authentication results from different organizations. For each organization there is a name, ok count and fail count. The graph should be dynamically (getting the data in loop) updated based on data.
The code is based on https://bl.ocks.org/mbostock/3808234.
There are few problems and few things i'm not sure about.
exit function only selects the red bars based on data update:
// JOIN new data with old elements
// specify function for data matching - correct?
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
// EXIT old elements not present in new data
// this works somehow strange
// it does select all red boxes
boxes.exit().transition(t).remove();
Why does exit() select only red bars and not all? From my point of understanding the d3 documentation exit() should only select such elements that do not have any new data. Shouldn't that be all bars in case of infinite loop and constant data file?
This obviously breaks the graph quite a lot (see plunker). I need the exit to select only bars, which are not available in data file anymore. See example below.
initial state of data file:
inst_name,ok,fail
inst1,24,-1
inst2,23,-3
...
updated state of data file:
inst_name,ok,fail
inst1,26,-1
inst14,22,-4
...
The bars (both blue and red) for inst2 from intial state should be removed (and replaced by data of inst14) when the data is updated. Why is this not working?
I've read, that new data are matched against older using index. I've specified that inst_name should be used:
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
Is this necessary (I've used it everywhere when inserting data)?
Also the transition for removing the elements does not work. What is the problem?
I'm also not sure if specifying data is necessary when adding new bars:
var boxes = svg.selectAll(".box").data(data, function(d) {
return d.inst_name;
});
.....
// add new element in new data
svg.selectAll(".blue")
.data(data, function(d) { // is this necessary ?
return d.inst_name;
}) // use function for new data matching against inst_name, necessary?
.enter().append("rect")
.transition(t)
.attr("class", function(d) {
return "blue box "
})
.attr("x", function(d) {
return x(d.inst_name);
})
.attr("width", x.bandwidth())
.attr("y", function(d) {
return y(d.ok);
})
.attr("height", function(d) {
return height - y(d.ok + min);
})
Thanks for help.
EDIT
The underlying data get changed by script (this was not written clearly in the original post), so it can change independently of the graph state. The data should be only growing.
You've asked a lot of questions.
Why does exit() select only red bars and not all? From my point of understanding the d3 documentation exit() should only select such elements that do not have any new data. Shouldn't that be all bars in case of infinite loop and constant data file?
First, you build two sets of bars (blue [ok] and red [fail]). When you data bind these you give them the same key function, which identifies them by inst_name. You then do your data update, which now selects all the bars at once with:
svg.selectAll(".box")
You again data-bind with the same key function. Your data has 10 values in the array but you just selected 20 bars. The second 10 bars exit (the red ones) because to d3 they are not in your 10 data-points
The bars (both blue and red) for inst2 from intial state should be removed (and replaced by data of inst14) when the data is updated. Why is this not working?
I don't see that in your plunker, you are giving it the same data over and over.
Also the transition for removing the elements does not work. What is the problem?
You haven't given the transition anything to do. It'll run it, then at the end remove the rects. What you need is something for it to transition, like "height":
boxes.exit().transition(t).attr('height', 0).remove();
This will shrink them to 0 height.
So how do we clean up your code?
First, I would operate on g elements each one paired to an item in your data array. You then place both bars in the g that belong to that data point. Take a look here, I've started to clean-up your code (incomplete, though, hopefully it gets you going).

How to bind d3 generated HTML elements to scope?

I am generating nodes in d3 in an angular directive, and I would like the classes of the nodes to be bound dynamically to an element in my scope. Below is a general outline of what I would like to do:
app.directive('myDirective',function(){
return {
restrict: 'EA',
link: function(scope,element,attrs){
var node = d3.selectAll('.node')
.data(nodes)
.enter().append('circle')
.classed('selected',function(d){return d.id=scope.selected.id})
}
}
})
This is pseudocode, but essentially d3 is generating these nodes/circles, and I want the class of these circles to depend on the value of an element within the scope. So, if at any point in time I modify the scope.selected.id, it should affect the node's class. Using the approach shown above, however, does not work. I have tried modifying the scope.selected.id, but the classes of the nodes are unaffected.
How can I dynamically bind a d3 generated element to scope? I don't want to redraw nodes whenever the scope is modified. I simply want their classes to be binded to the scope.
Add a watch around scope.selected in your link function then wrap your d3 drawing code in a function and call that method when the watch fires.
(BTW a fiddle is always easier to deal with when asking questions)
You might want to read up on the General update pattern for D3 too http://bl.ocks.org/mbostock/3808218
It will both make life easier and your updates faster if you get to grips with the various stages a D3 documents lifecycle has.
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data);
// UPDATE
// Update old elements as needed.
text.attr("class", "update");
// ENTER
// Create new elements as needed.
text.enter().append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
// ENTER + UPDATE
// Appending to the enter selection expands the update selection to include
// entering elements; so, operations on the update selection after appending to
// the enter selection will apply to both entering and updating nodes.
text.text(function(d) { return d; });
// EXIT
// Remove old elements as needed.
text.exit().remove();
}

Append elements into different containers depending on conditions with D3

I have an array, looking as simple as
var labels = ['label 1', 'label2', ..., 'label n']
Now if I want to put them as visual elements in the chart, I can do like this:
var legendItem = legendArea
.selectAll('g')
.data(labels)
.enter()
.append('g')
.attr({class: 'legend-element'});
legendArea becomes a parent for all the labels now. But, I have a more complex scenario, where I need to put labels not in legendArea directly but create a wrapper g element first inside legendArea, which will then contain a set of labels, depending on some criteria that I get from each label.
As a result, I will have a number of g elements with a set of labels inside of them, one can have 5, another can have 8, any number, as they are not spread evenly.
What I see now, is I need to run a loop through all labels array elements, check if current labels conforms to criteria, create a new wrapper element if needed and then append. But this solution seems to be not D3-style, as in most cases it's possible to do functional style code with D3, not for..loop.
I suspect I can do something more custom here, something like:
var legendItem = legendArea
.selectAll('g')
.data(labels)
.enter()
// Do some unknown D3 magic here to create a new wrapper element and append the label to it.
.attr({class: 'legend-element'});
Please advise on how to do it in D3 fashion.
I would get a list of categories, use that as data for the g elements, and then add the rest with filters:
var cats = svg.selectAll("g").data(categories)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(10," + ((i+1) * 30) + ")"; });
cats.selectAll("text")
.data(function(d) { return data.filter(function(e) { return e.category == d; }); })
.enter().append("text")
.text(function(d) { return d.label; });
This works similar to nested selections, except that the nesting isn't in the data itself (although you can modify the data structures for that as well), but by referencing different data structures.
The first block of code simply adds a g element for each category. The second block of code does a nested selection (and is hence able to reference the data bound to the g element just before). In the .data() function, we take all the data and filter out the items that match the current category. The rest is bog-standard D3 again.
Complete demo here.

d3.js: size of parents = sum of size of children

I am building something quite similar to this. What I would love is to make every node either their size as defined in the json file, OR, if it has no size attribute but a children attribute in json, the sum of all of its children's sizes. How would one go about doing that? I have tried various methods but short of adding things up and hardcoding it in JSON, which is a bit lame, I haven't found anything that really would have worked ;( Any suggestions, hive mind?
If your data is a tree structure, you could use a Partition Layout to initialize positions and sizes of nodes. The d.value returned by partition for parent nodes is by default the sum of values for all children nodes, assuming you've properly set the value accessor function to return the data variable that you want to use for size for leaf nodes.
Although the standard display in partition examples is to have space-filling rectangles or arcs instead of nodes and links, it still has all the basic functionality of the other hierarchy layouts. So once you've run the layout on your root to generate your array of nodes, you can run the links function to calculate the links.
If you still want a force-based layout instead of a static tree, you can just pass in your nodes and links to the force layout and start it up.
In sum:
var root = /*root object read from JSON data*/;
var w,h; /*set appropriately */
var partition = d3.layout.partition()
.size([w,h])
.value(function(d){return d.size;})
.children(function(d){return d.children;})
//optional: this is the default, change as needed
.sort(null);
//optional: turns off sorting,
//the default is to sort children by descending size
var nodes = partition(root);
var links = partition.links(nodes);
initialize();
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([w,h])
/*and any other customization*/
.start();
svg.on("tick", update);
One thing to note. The x value created by partition layout is designed to be the corner of a rectangle instead of the centre of a circle. So if you position your nodes based on the original x value, you'll end up with parents off to the far left of their children. If you're running everything through a force-based layout afterwards, it will sort itself out eventually, but you can centre them from the beginning by setting d.x = d.x + d.dx/2 and d.y = d.y + d.dy/2 on all your nodes during initialization (e.g., using an .each() call in your enter() chain). And of course, use d.value to initialize your node size (with an appropriate scale).

Adding and Removing Nodes in D3js Force Graph

I am loading json from database and creating a json file which loads fine. Now I don't know which steps to take for making the nodes responsive in a Force-Directed Graph. I need to remove and add new nodes and their links.
force.nodes(json.nodes)
.links(json.links)
.start();
initNodes(json);
How can I make this more dynamic or update it without resetting the whole visualization?
I have seen this question a couple of times not being answered so I hope someone can post and give a guide.
Adding nodes/links to my D3 force graph was very confusing until I better understood the way I was adding the initial set of nodes.
Assuming a <g> is what you'd like to use for your nodes:
// Select the element where you'd like to create the force layout
var el = d3.select("#svg");
// This should not select anything
el.selectAll("g")
// Because it's being compared to the data in force.nodes()
.data(force.nodes())
// Calling `.enter()` below returns the difference in nodes between
// the current selection and force.nodes(). At this point, there are
// no nodes in the selection, so `.enter()` should return
// all of the nodes in force.nodes()
.enter()
// Create the nodes
.append("g")
.attr("id", d.name)
.classed("manipulateYourNewNode", true);
Now let's make that function that will add a node to the layout once the graph has been initialized!
newNodeData is an object with the data you'd like to use for your new node.
connectToMe is a string containing the unique id of a node you'd like to connect your new node to.
function createNode (newNodeData, connectToMe) {
force.nodes().push(newNodeData);
el.selectAll("g")
.data(force.nodes(), function(datum, index) { return index })
The function given as the optional second argument in .data() is run once for each node in the selection and again for each node in force.nodes(), matching them up based on the returned value. If no function is supplied, a fallback function is invoked, which returns the index (as above).
However, there's most likely going to be a dispute between the index of your new selection (I believe the order is random) and the order in force.nodes(). Instead you'll most likely need the function to return a property that is unique to each node.
This time, .enter() will only return the node you're trying to add as newData because no key was found for it by the second argument of .data().
.enter()
.insert("g", "#svg")
.attr("id", d.name)
.classed("manipulatYourNewNode", true);
createLink(connectToMe, newNodeData.name);
force.start();
}
The function createLink (defined below) creates a link between your new node and your node of choice.
Additionally, the d3js API states that force.start() should be called after updating the layout.
Note: Calling force.stop() at the very beginning of my function was a huge help for me when I was first trying to figure out how to add nodes and links to my graph.
function createLink (from, to) {
var source = d3.select( "g#" + from ).datum(),
target = d3.select( "g#" + to ).datum(),
newLink = {
source: source,
target: target,
value: 1
};
force.links().push(newLink);
The code below works under the assumptions that:
#links is the wrapper element that contains all of your link elements
Your links are represented as <line> elements:
d3.select("#links")
.selectAll("line")
.data(force.links())
.enter()
.append("line");
You can see an example of how to append new nodes and relationships here:
http://bl.ocks.org/2432083
Getting rid of nodes and relationships is slightly trickier, but you can see the process here:
http://bl.ocks.org/1095795

Categories

Resources