Updating a layout.pack in d3.js - javascript

I am trying to wrap my mind around d3's pack layout (http://bl.ocks.org/4063530).
I have the basic layout working but I would like to update it with new data. i.e. collect new data, bind it to the current layout.pack and update accordingly (update/exit/enter).
My attempts are here (http://jsfiddle.net/emepyc/n4xk8/14/):
var bPack = function(vis) {
var pack = d3.layout.pack()
.size([400,400])
.value(function(d) {return d.time});
var node = vis.data([data])
.selectAll("g.node")
.data(pack.nodes)
.enter()
.append("g")
.attr("class", function(d) { return d.children ? "node" : "leaf node"; })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.append("circle")
.attr("r", function(d) { return d.r });
node.filter(function(d) { return !d.children; }).append("text")
.attr("text-anchor", "middle")
.attr("dy", ".3em")
.text(function(d) { return d.analysis_id });
bPack.update = function(new_data) {
console.log("UPDATE");
node
.data([new_data])
.selectAll("g.node")
.data(pack.nodes);
node
.transition()
.duration(1000)
.attr("class", function(d) { return d.children ? "node" : "leaf node" })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" });
node.selectAll("circle")
.data(new_data)
.transition()
.duration(1000)
.attr("r", function(d) { return d.r; });
};
Specific questions...
How do I bind the data? (since the data is not complex structure and not an array of data)
How can new nodes/leafs be added to the layout? And old ones removed?
Pointers to a working example would be greatly appreciated.

Working example is here.
Basically, there is code for initial load, where all circles, tooltips, etc. are created and positioned in initial places. As well, the layout (pack) is created.
Than, on each button press, new data is loaded into pack, and the pack is recalculated. That crucial code is here:
Here you bind (load) now data into pack layout: (in my example its random data, of course you'll have your data from json or code or similar):
pack.value(function(d) { return 1 +
Math.floor(Math.random()*501); });
Here the new layout is calculated:
pack.nodes(data);
After that, elements are transitioned to new positions, and its attributes are changed as you determine.
I just want to stress that I don't use enter/update/exit pattern or transformations (that you might see in others solutions), since I believe this introduces unnecessary complexity for examples like this.
Here are some pics with transition in action:
Start:
Transition:
End:

I had the same problem recently, and came across the General Update Pattern tutorials as well. These did not serve my purpose. I had a few hundred DOM elements in a graph (ForceLayout), and I was receiving REST data back with properties for each individual node. Refreshing by rebinding data led to reconstruction of the entire graph, as you said in response to mg1075's suggestion. It tooks minutes to finish updating the DOM in my case.
I ended up assign unique ids to elements that need updating later, and I cherry picked them with JQuery. My entire graph setup uses D3, but then my updates don't. This feels bad, but it works just fine. Instead of taking minutes from destroying and recreating most of my DOM, it takes something like 3 seconds (leaving out timing for REST calls). I don't see a reason that something like property updates could not be made possible in D3.
Perhaps if Mike Bostock added a remove() or proper subselection function to the enter() selection, we could follow a pure D3 pattern to do updates. While figuring this out I was trying to bind a subset of data, the data with the new properties added, and then subselecting to get at elements that need updating, but it didn't work, due to the limited and specific nature of the enter() selection.

Of relevance, if you have not already reviewed:
http://bl.ocks.org/3808218 - General Update Pattern, I
http://bl.ocks.org/3808221 - General Update Pattern, II
http://bl.ocks.org/3808234 - General Update Pattern, III
This sample fiddle has no transitions, but here is at least one approach for updating the data.
http://jsfiddle.net/jmKH6/
// VISUALIZATION
var svg = d3.select("#kk")
.append("svg")
.attr("width", 500)
.attr("height", 600)
.attr("class", "pack");
var g = svg.append("g")
.attr("transform", "translate(2,2)");
var pack = d3.layout.pack()
.size([400,400])
.value(function(d) {return d.time});
function update(data) {
var nodeStringLenth = d3.selectAll("g.node").toString().length;
if ( nodeStringLenth > 0) {
d3.selectAll("g.node")
.remove();
}
var node = g.data([data]).selectAll("g.node")
.data(pack.nodes);
node.enter()
.append("g")
.attr("class", function(d) { return d.children ? "node" : "leaf node"; })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.append("circle")
.attr("r", function(d) { return d.r });
node.filter(function(d) { return !d.children; }).append("text")
.attr("text-anchor", "middle")
.attr("dy", ".3em")
.text(function(d) { return d.analysis_id });
node
.exit()
.remove();
}
var myData = [data1, data2, data3];
update(data1);
setInterval(function() {
update( myData[Math.floor(Math.random() * myData.length)] ); // http://stackoverflow.com/questions/4550505/getting-random-value-from-an-array?lq=1
}, 1500);

Related

Children with n>1 parents in d3.js tree layout

d3.js tree layout is a great tool, but it only allows children to have a single parent by default. I would like to be able to let children have more than one parent. I am happy with the position of the nodes provided by the tree's default behaviour. All I want is to draw extra diagonal links between childless parents and existing children after the default tree has been calculated.
My script currently looks like this :
<script>
var h = 870,
w = 1200;
var dimcirc = [60,30,10,8];
var offsetL = 60;
var couleurs = [
"#a2a2ff",
"#87ff87",
"#ffc55c",
"#ff844d",
"#ffe452"];
var tree = d3.layout.tree()
.size([h-100, w-400])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 1.2); });
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y + offsetL, d.x]; });
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g");
d3.json("donnees.json", function(error, root) {
if (error) throw error;
var nodes = tree.nodes(root),
links = tree.links(nodes);
var link = svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal);
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", function(d){return "niv_" + d.depth.toString();})
.classed("node", true)
.attr("transform", function(d) { return "translate(" + (d.y + offsetL).toString() + "," + d.x +")"; })
// Draw node circles
node.append("circle")
.attr("fill", function(d){
console.log(d.couleur);
if(d.couleur!=null){
return couleurs[d.couleur];
}
else {return couleurs[4];}
})
.attr("r", function(d){
return dimcirc[d.depth];
});
node.append("text")
.attr("dy", "0.31em")
//.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d){
return "translate(" + (dimcirc[d.depth] + 10).toString() + ")"
})
.text(function(d) { return d.name; })
.call(wrap, 230);
});
// Wrap text
function wrap(text, width) {
// A function to help wrap svg text.
}
</script>
I have tried to copy the children of a parent node to a childless node using the root object, to no avail (the structure of the root object is quite obscure to me).
Any help appreciated.
PS : I am aware of a similar question (d3.js tree nodes with same parents), but the lack of answers didn't help me much or course.
EDIT
I've managed to get what I want by :
1) Identifying the nodes of interest : those without children that are to be connected to existing children, and the parent of the later.
2) Extracting the coordinates of the source (node without children) and target (children to be connected to this node) from the root object.
3) Creating the extra paths using the same diagonal as for the "standard" paths created by the tree.
Here's the code I've added to get the source and target for each path :
var link2 = [];
var loopLink2 = [{"a":0,"b":1, "c":0,"d":0},{"a":1,"b":1, "c":1,"d":0},{"a":3,"b":0, "c":2,"d":1}];
loopLink2.forEach(function(d){
var sourX = root.children[d.a].children[d.b].x;
var sourY = root.children[d.a].children[d.b].y;
root.children[d.c].children[d.d].children.forEach(function(d){
link2.push({"source":{"x":sourX,"y":sourY}, "target":{"x":d.x,"y":d.y}});
console.log(link2);
});
});
And here's where the actual creation of the paths takes place :
svg.selectAll(".link2")
.data(link2)
.enter().append("path")
.attr("class", "link2")
.attr("d", diagonal)
.attr("transform", function(d) { return "translate(0," + offsetT +")"; });
Someone has a nicer solution ?
I don't know whether this is exactly what you are looking for. But this might help.
Dagre
Dagre is a library that uses d3 to plot acyclic graphs. You can try modifying this. Here
is the github wiki.
Family Tree
Here you can find an excellent answer for creating a family tree , that can have multiple parents for one child. This might fulfill your requirement if the structure doesn't matter.

Creating array from html to display using D3 Wordcloud

I'm afraid this is a very basic Javascript question on building an array from HTML elements. I've had a search for answers, but I think there's important fundamentals that aren't clicking for me and I'm going in circles, so some outside, expert perspective would be appreciated!
We're using Moodle to run an online course, and in the introduction week, we want to collect some information on users with a database module, such as country, and then show them the group responses.
Moodle give a template with a placeholder [[data]], which you can wrap in html, in this case a div:
<div class="country">[[country]]</div>
The issue I'm having is getting the content of all these divs into an array. I found this method explained here, but my attempts to push/concat didn't seem to work:
var countryList = document.getElementsByClassName("country");
Array.prototype.forEach.call(countryList, function() {});
The reason I want it in an array is so I can push the content into an existing template using Jason Davies's d3.js powered word cloud. Here's a link to GitHub, and the very basic code for a simple cloud is copied below for reference.
Essentially I want to be able to make an array from HTML elements and combine it with the words array used below:
var fill = d3.scale.category20();
d3.layout.cloud().size([300, 300])
.words([
"Hello", "world", "normally", "you", "want", "more", "words",
"than", "this"
].map(function(d) {
return {
text: d,
size: 10 + Math.random() * 90
};
}))
.padding(5)
.rotate(function() {
return ~~(Math.random() * 2) * 90;
})
.font("Impact")
.fontSize(function(d) {
return d.size;
})
.on("end", draw)
.start();
function draw(words) {
d3.select("body").append("svg")
.attr("width", 300)
.attr("height", 300)
.append("g")
.attr("transform", "translate(150,150)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) {
return d.size + "px";
})
.style("font-family", "Impact")
.style("fill", function(d, i) {
return fill(i);
})
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) {
return d.text;
});
}
You should be able to get the array you want using something like:
var countryList = document.querySelectorAll('.country');
var countryNames = Array.prototype.map.call(countryList, function(c){return c.textContent});

Wordcloud update data/ working example of adding words to cloud?

I am still pretty new to d3.js and I am diving into a wordcloud example using the
d3-cloud repo : https://github.com/jasondavies/d3-cloud
The example which is in there works for me, I turned it into a function so I can call it when data updates:
wordCloud : function(parameters,elementid){
var p = JSON.parse(JSON.stringify(parameters));
var fill = d3.scale.category20();
if (d3.select(elementid).selectAll("svg")[0][0] == undefined){
var svg = d3.select(elementid).append("svg")
.attr("width", 500)
.attr("height", 500)
.append("g")
.attr("transform", "translate(300,300)");
}else var svg = d3.select(elementid).selectAll("svg")
.attr("width", 500)
.attr("height", 500)
.select("g")
.attr("transform", "translate(300,300)");
d3.layout.cloud().size([300, 300])
.words(p.data)
.padding(5)
.rotate(function(d) {return ~~(Math.random()) * p.cloud.maxrotation; })
.font("Impact")
.fontSize(function(d) { return d.size; })
.on("end", draw)
.start();
function draw(words) {
console.log(words)
console.log(words.length)
svg.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) {return d.size + "px"; })
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.text(function(d) {console.log("enter text " + d.text) ; return d.text; });
svg.selectAll("text")
.data(words).transition().duration(2000).attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + Math.random() * p.cloud.maxrotation + ")";
})
}
}
The code works for me.
element id = the elements you bind to
parameters = all parameters which i should be able to set, including data (parameters.data).
Except for the packaging the code wasn't altered much from the original:
https://github.com/jasondavies/d3-cloud/blob/master/examples/simple.html
However when I add a new word to the wordcloud (so when I update the data), the new word is not recognized. I have put log output on several places and apparently in the draw function the data is incorrect but before it is ok.
for example:
original: [{"text":"this","size":5},{"text":"is","size":10},{"text":"a","size":50},{"text":"sentence","size":15}]
(the code adds other properties but this is for simplicity of explanation)
I add: "testing" with a size of 5
correct would be
[{"text":"this","size":5},{"text":"is","size":10},{"text":"a","size":50},{"text":"sentence","size":15},{"text":"testing","size":5}]
but I get results like:
[{"text":"a","size":50},{"text":"testing","size":5},{"text":"this","size":5},{"text":"sentence","size":15}]
--> new word added , an older one removed (don't know why) and array was mixed up.
QUESTION:
Anybody have an idea what I am doing wrong?
or
Does anybody have a working example of a d3.js wordcloud which you can update with new words by means of lets say an input box?
I think u got the same problem with me. The size of all the words do not fit in your svg and d3.layout.cloud somehow remove the oversized word. Try to increase the width and height of your svg or decrease the size of your word. What I done is check whether the x,y,width and height of the word is it out of the box. If yes, decrease the size or increase the width and height. Correct me if i am wrong

Animating histogram with D3

I have some code on this jsFiddle here that generates a histogram for a data array called "values". That's all well and good.
When I want to update this histogram with a new data array, called "newData", things go wrong. I am trying to adhere to the enter(), update(), exit() D3 strategy (which I am obviously extremely new with). An animation does indeed occur, but as you can see by the fiddle, it just squishes everything into the upper right hand corner. Can someone point out what I am doing wrong in this segment of the code (the update)?
//Animations
d3.select('#new')
.on('click', function(d,i) {
var newHist = d3.layout.histogram().bins(x.ticks(bins))(newData);
var rect = svg.selectAll(".bar")
.data(values, function(d) { return d; });
// enter
rect.enter().insert("g", "g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + x(d) + "," + y(d) + ")"; });
rect.enter().append("rect")
.attr("x", 1)
.attr("width", w)
.attr("height", function(d) { return y(d); });
rect.enter().append("text")
.attr("dy", ".75em")
.attr("y", 6)
.attr("x", x(histogram[0].dx) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return formatCount(d); });
// update
svg.selectAll('.bar')
.data(newHist)
.transition()
.duration(3000)
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });
svg.selectAll("rect")
.data(newHist)
.transition()
.duration(3000)
.attr("height", function(d) { return height - y(d.y); });
svg.selectAll("text")
.data(newHist)
.transition()
.duration(3000)
.text(function(d) { return formatCount(d.y); });
// exit
rect.exit()
.remove();
});
The entirety of the code is on the JSFiddle linked above. Thanks!
Looking at the code above and the fiddle, a few things jump out at me:
(Line 85) You are still binding the original data
(Lines 105, 115) You are binding the data multiple times
(Line 99) You are still referencing the original histogram variable without updating it with the new data
You are declaring multiple bind/add/update/remove patterns for a single set of (changing) data
You're on the right track, but you need to differentiate between things that need to be updated when the data changes, and things that should not be updated/declared when the data changes. You should only have to declare the d3 pattern (bind, add, update, remove) once... it will work for updated datasets.
So, declare as much as you can outside the makeHist(values) function, and only have code that needs the changed data inside the function (this includes modifying a previously declared scale's domain and range). Then, the on click function can simply call the makeHist function again with the new data.
Here's a rough outline:
// generate data
// declare everything that can be static
// (add svg to dom, declare axes, etc)
// function that handles everything that new data should modify
function makeHist(values) {
// modify domains of axes, create histogram
// bind data
var rect = svg.selectAll('rect')
.data(histogram);
// add new elements
rect.enter().append('rect');
// update existing elements
rect.transition()
.duration(3000)
.attr('transform', '...');
// remove old elements
rect.exit().remove();
}
// generate initial histogram
makeHist(initialValues);
// handle on click event
d3.select('#new')
.on('click', function() {
makeHist(newData);
});
Here's a mostly working updated fiddle, it needs a little bit of cleanup, though:
http://jsfiddle.net/spanndemic/rf4cw/
Spoiler Alert: the two datasets aren't all that different

d3.js force-directed issues, significance of "d"?

I'm having a bit of trouble getting a something to work with D3.js. Namely, I'm trying to make a tree of nodes, using the basic code from http://bl.ocks.org/mbostock/1021953.
I switched it to load the data inline, as opposed to loading from file, because I'm using it with a Rails application and don't want to have repetitive information. I switched the line so that you could see the format of my data.
Anyways, here's the bulk of my code:
<%= javascript_tag do %>
var nodes = [{"title":"Duncan's Death","id":"265"},{"title":"Nature Thrown Off","id":"266"},{"title":"Cows Dead","id":"267"},{"title":"Weather Bad","id":"268"},{"title":"Lighting kills man","id":"269"},{"title":"Macbeth's Rise","id":"270"}];
var links = [{"source":"265","target":"266","weight":"1"},{"source":"266","target":"267","weight":"1"},{"source":"266","target":"268","weight":"1"},{"source":"268","target":"269","weight":"1"}];
var firstelement = +links[0].source;
links.forEach(function(l) {
l.source = +l.source;
l.source = l.source-firstelement;
l.target = +l.target
l.target = l.target-firstelement;
});
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-1000)
.linkDistance(300)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force
.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.weight); });
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "circle_node")
.attr("r", 50)
.style("fill", function(d) { return color(d.id); })
node.append("title")
.text(function(d) { return d.title; });
node.append("text")
.attr("x", function(d) { return d.x; } )
.attr("y", function(d) { return d.y; })
.text(function(d) { return d.title; });
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("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
});
<% end %>
This seems like it should work to me, but I can seem to manage it. The links work, but the nodes all remain in the top left corner. I've tried just entering the circles directly and appending the text to them (staying close to the source code I listed above,) but while the circles behave properly, it doesn't display the text. I'd like the title to be centered in the nodes.
More generally, I'm kind of confused by how this is working. What does "d" refer to within lines like
function(d) { return d.source.x; }
It seems to be declaring a function and calling it simultaneously. I know that it doesn't have to be specifically the character "d," (for instance, switching the "d" to an "a" seems to make no difference as long as it's done both in the declaration and within the function.) But what is it referring to? The data entered into the object that's being modified? For instance, if I wanted to print that out, (outside of the attribute,) how would I do it?
Sorry, I'm new to D3 (and fairly new to JavaScript in general,) so I have a feeling the answer is obvious, but I've been looking it up and through tutorials and I'm still lost. Thanks in advance.
First, there's a simple problem with your code that is causing all your nodes to stay in the top left corner. You are trying to position each node using the code:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
However, node is a selection of gs which do not take x and y attributes. Instead, you can move each node using translate transform, e.g.
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
Making this change should allow the nodes to move around.
Next, moving to your question about "d", I think the first thing you need to understand is what you can do with a selection of elements in D3. From the docs: a selection (such as nodes) "is an array of elements pulled from the current document." Once you have a selection of elements, you can apply operators to change the attributes or style of the elements. You can also bind data to each element.
In your case, you are binding data to a selection of gs (nodes):
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
You are then using attr to change the position of each node. However, instead of setting the x and y attributes of each element to the same value, you are passing attr an anonymous function that will return a (presumably different) position for each node:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
This behavior is also explained in the docs for attr:
Attribute values and such are specified as either constants or
functions; the latter are evaluated for each element.
Thus, d represents an individual element (Object) in nodes.
So going back to your code, on each tick two things are happening:
The position of each node (data) is being recalculated by force.
Each corresponding element is then being moved to its new location by the anonymous function you pass to force.on.

Categories

Resources