d3 get all nodes of tree - javascript

I try to keep this as short as I can:
In the snippet I have a Json which represents a tree. With the help of d3 I try to get all child nodes of the root as an Array. For that I use the function "nodes".
The Problem is that my children key is called "_children" instead of "children". I try to find a good solution to tell the nodes function to check "children" instead of "children". If I remove the "" of all children keys it works.
var json = {"_name":"root","_children":[{"_name":"Application","_children":[{"_name":"Application Heap","_children":[],"_color":"#0000ff", "MEMORY":20},{"_name":"Other","_children":[],"_color":"#000055","MEMORY":30},{"_name":"Statement Heap","_children":[],"_color":"","MEMORY":40}]}]};
console.log(json);
// tell d3 that my children key is "_children"
var treemap = d3.layout.treemap()
.children(function(d) { return d._children; })
.value(function(d) { return d.MEMORY; });
// With this line I try to get all child nodes of the root element
var nodes = treemap.nodes(json)
.filter(function(d) {return !d._children; });
console.log(json); // d3 sets the value and everything else correct
console.log(nodes); // for some reason I get an empty array
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

One possible solution (but not the one I want) is to rename the field recursive like that at first:
var renameKeys=function(obj){
if(obj._children){
obj.children=obj._children;
delete obj._children;
obj.children.forEach(renameKeys);
}
return obj;
};

Related

plotting graph symbols using two-level nested data in d3.js

I am trying to replicate this example of a multiline chart with dots. My data is basically the same, where I have an object with name and values in the first level, and then a couple of values in the second level, inside values. The length of the arrays inside values is 40.
Now, one requirement is that all the dots for all the paths are inside the same g group within the DOM. This is giving me a lot of trouble because I can't seem to figure out how to join the circles with the appropriate portion of the nested data.
The last thing I've tried is this:
var symbolsb = d3.select("#plot-b") // plot-b is the graph area group within the svg
.append("g")
.attr("id", "symbols-b");
symbolsb.selectAll("circle")
.data(games, function(d) {console.log(d.values) // games is my data object
return d.values})
.enter()
.append("circle")
.attr("class", "symbolsb")
.attr("cx", function(d,i) {console.log(d)
return x(d.values.date);})
.attr("cy", function(d,i) {return y_count(d.count);})
.attr("r", function(d,i) {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
console.log(parent)
if (i%3 === 1 && included_names.includes(datum[i].name)) {
return 8;}
else {return null;}})
.style("fill", function(d,i) {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
{return color(datum.name);}});
As I (incorrectly) understand the data() function, I thought that by returning d.values, the functions in cx, cy, and r would just see the array(s) that is inside d.values, but when log d to the console within the functions to define cx, cy, etc. I see again the full object games. Again, I though I should only get the values portion of the object.
I have been able to get a plot that looks like the result I want by loading the data and appending a g when defining symbolsb, but this creates a group for each set of circles.
I think the problem comes from my confusion of how nested objects are accessed by the data() function. So any help explaining that would be greatly appreciated.
It would be great if you could provide a live reproduction, for example in an Observable or VizHub notebook.
This line looks suspect
.data(games, function(d) {console.log(d.values) // games is my data object
return d.values})
The second argument to *selection*.data should be a 'key function', a function that returns a unique string identifier for each datum. Here you are giving an object (d.values) which will get stringified to [object Object] for each data point. This also explains why you're seeing the full games object when logging. I think it's safe here to just remove the second argument to .data():
.data(games)
This also doesn't look right
.attr("r", function(d,i) {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
console.log(parent)
if (i%3 === 1 && included_names.includes(datum[i].name)) {
return 8;}
else {
return null;
*emphasized text*}})
I'm not entirely sure what you're trying to do here. If you're trying to access the name of the data point you can just access it on the data point itself using .attr("r", function(d,i) { if (included_names.includes(d.name)) { return 8 } else { return 0} )

D3: How to conditionally bind SVG objects to data?

I have here an array of objects that I'm visualising using D3. I bind each object to a group element and append to that an SVG graphic that depends on some object property, roughly like this:
var iconGroups = zoomArea.selectAll("g.icons")
.data(resources)
.enter()
.append("g")
var icons = iconGroups.append(function(d){
if(d.type == "Apple"){
return appleIcon;
}else if(d.type == "Orange"){
return orangeIcon;
})
etc. Now I'd like to extend some of those icons with an additional line. I could add a line element for each data point and set them visible only where applicable, but since I want to add them only for say one out of a hundred data points, that seems inefficient. Is there a way to bind SVG lines to only those objects where d.type == "Apple"?
I would create separate selections for icons and lines, this way:
var iconGroups = zoomArea.selectAll('g.icons')
.data(resources);
iconGroups
.enter()
.append('g')
.classed('icons', true);
iconGroups.exit().remove();
var icons = iconGroups.selectAll('.icon').data(function(d) {return [d];});
icons
.enter()
.append(function(d) {
if(d.type === 'Apple'){
return appleIcon;
}else if(d.type === 'Orange'){
return orangeIcon;
}
}).classed('icon', true);
icons.exit().remove();
var lines = iconGroups.selectAll('.line').data(function(d) {
return d.type === 'Apple' ? [d] : [];
});
lines
.enter()
.append('line')
.classed('line', true);
lines.exit().remove();
.exit().remove() is added just because I add it always to be sure that updates work better. :)
Maybe the code is longer than .filter() but I use the following structure all the time and it's easier to scale it.
edit: apropos comment - If you need to pass indexes, you should pass them in binded data:
var iconGroups = zoomArea.selectAll('g.icons')
.data(resources.map(function(resource, index) {
return Object.create(resource, {index: index})
}));
(Object.create() was used just to not mutate the data, you can use _.clone, Object.assign() or just mutate it if it does not bother you)
then you can access it like:
lines.attr("x1", function(d){ console.log(d.index);})
You could add a class to the icons to be selected (e.g. appleIcon), and use that class in a selector to add the lines.
Use d3 filter.
selection.filter(selector)
Filters the selection, returning a new selection that contains only the elements for which the specified selector is true.
Reference: https://github.com/mbostock/d3/wiki/Selections#filter
Demo: http://bl.ocks.org/d3noob/8dc93bce7e7200ab487d

d3.js selection conditional rendering

Using the d3js join model, is it possible to do conditional rendering based on the data content?
I want to do something like this:
var nodes = svg.selectAll('.node').data(nodes);
var node = nodes.enter().insert('svg:g').attr('class', 'node');
// if node.hasDuration {
node.insert('svg:rect');
//} else {
node.insert('svg:circle');
//}
nodes.exit().remove();
There doesn't seem to be a way using the join model (enter/exit) to have conditional rendering. I can brute force it with selection.each() but that seems to defeat the purpose of the selection model.
You could use a filter:
var nodes = svg.selectAll('.node').data(nodes);
nodes.enter()
.insert('svg:g')
.attr('class', 'node');
nodes.filter(function(d,i){
return d.hasDuration;
}).append('svg:rect');
nodes.filter(function(d,i){
return !d.hasDuration;
}).append('svg:circle');
Example here.

updating a line graph in d3 is not working

i am trying to update a line graph and it is not throwing any error but it is also not updating the graph.
i am deleting a point and adding a new one with an incremented rate and incremented created_at date by a second(trying to follow http://bl.ocks.org/benjchristensen/1148374)
function redrawWithoutAnimation() {
for (var i in chart_data) {
linedata = chart_data[i];
//delete first element of array
linedata.points.reverse().shift();
//create a new point
rate = linedata.points[0].rate + 1;
created_at = linedata.points[0].created_at + 6000;
new_point = {};
new_point.rate = rate;
new_point.created_at = created_at;
linedata.points.push(new_point);
console.log(linedata);
}
// static update without animation
svg.selectAll("path")
.data([linedata.points]); // set the new data
line(linedata.points); // apply the new data values
}
redrawWithoutAnimation();
setInterval(function () {
redrawWithoutAnimation();
}, 8000);
here is my code
http://jsfiddle.net/yr2Nw/8/
Working fiddle: http://jsfiddle.net/reblace/GsaGb/1
There's a few issues here...
First, you were updating all the chart_data in the for loop, but outside the loop, you were only trying to update the line still stored in the linedata variable after loop execution. You should try to avoid having variables with greater scope than they need. It can lead to bugs like this one:
svg.selectAll("path").data([linedata.points]);
line(linedata.points);
You should instead use D3's data joining to rejoin the new data to all the paths at once declaratively like so:
linesGroup.selectAll("path")
.data(chart_data)
.attr("d", function(d){ return line(d.points); });
What that code's doing is it's selecting the paths and then joining each of them to the chart_data elements and then binding the appropriate line generator to the "d" attribute for the appropriate path.
Then, you need to update your x axis and y axis otherwise the plot will just shoot off the drawn area. This code is updating the domains and then rebinding the axes to the dom elements so they redraw:
xAxis.scale().domain([
d3.min(chart_data, function (c) { return d3.min(c.points, function (v) { return v.created_at; }); }),
d3.max(chart_data, function (c) { return d3.max(c.points, function (v) { return v.created_at; }); })
]);
yAxis.scale().domain([
0,
d3.max(chart_data, function (c) { return d3.max(c.points, function (v) { return v.rate; }); })
]);
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
There were a few other bugs I fixed them in the Fiddle. For example, you need to calculate the time for the new point based on the last element in the array, not the first, otherwise the line can't interpolate properly since its no longer a continuous function... and this is a bit more concise way to do your line updates:
for (var i=0; i<chart_data.length; i++) {
linedata = chart_data[i];
//delete first element of array
var removedPoint = linedata.points.shift();
//create a new point
var lastpoint = linedata.points[linedata.points.length-1];
var new_point = {
rate: removedPoint.rate,
created_at: lastpoint.created_at + 6000
};
linedata.points.push(new_point);
}
Also note that you shouldn't use the for(var in) loop for Arrays, that's for iterating over the properties in an object.
There's still some issues, but I think this should help get you over the hurdle you were stuck on. Anyways, it looks cool in action!
Fine fenac.. You facing so many problems since your data is not in good format for your requirements..
as per http://bl.ocks.org/benjchristensen/1148374 The x-axis data must be (data[] (data array))
Your data is something like this
[objects,object,object] where each object holds one element of xaxis value.. so the pushing and shifting is not possible..
try to change the format of the data (linedata.points) to an array (data[]) and try it out sure it works..
You just need to put all the values in linedata.points into an array data[] and use this data[] to animate your line..
Since yours the multiline.. you need to create 2D array and must pass them accordingly...
Cheers..
I updated your jsfiddle
setInterval(function () {
console.log(linedata.points);
var v = linedata.points.shift(); // remove the first element of the array
linedata.points.push(v); // add a new element to the array (we're just taking the number we just shifted off the front and appending to the end)
redrawWithoutAnimation();
}, 3000);
http://jsfiddle.net/yr2Nw/9/
But still it wont works till you do that work...
Personal Suggestion: First Try with single line graph then go with looping for multiline...

Setting the SVG basic shape on a node-by-node basis

(D3 beginner here)
I have the following snippet:
// myShape (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
myShape = myShape.data(nodes, function(d) { return d.nodeId; });
// update existing nodes (reflexive & selected visual states)
myShape.selectAll('circle')
.style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.nodeType)).brighter().toString() : colors(d.nodeType); })
.classed('reflexive', function(d) { return d.reflexive; });
// add new nodes
var g = myShape.enter().append('svg:g');
g.append('svg:circle')
.attr('r', 12)
But I would like to make this more flexible: instead of using only circles, I would like to use circles and polygons. This will be selected in a property in d:
var d = [
{ nodeId: 1, nodeType : 'type1' , shape: 'circle' },
{ nodeId: 2, nodeType : 'type2' , shape: 'triangle' },
];
Which means that, depending on d.shape. I must set 'svg:circle' or 'svg:polygon', and then set the radius (for the circle) or the points (for the polygons). I have tried to set the svg shape like this:
g.append(function (d) {
if (d.shape === 'circle' ) { return 'svg:circle'; }
else { return 'svg:polygon'; } } )
But this is not working: I am getting a:
Uncaught Error: NotFoundError: DOM Exception 8
It seems append does not accept a function? How can I set the svg shape on a node-by-node basis?
EDIT
I have prepared this jsbin to show what I want to do.
In recent versions of D3, you can append elements that are the results of function calls. That is, instead of passing a static name, you can pass a function that evaluates to the element to add.
It doesn't quite work the way you're using it -- it's not enough to return the name of the element from the function. Instead of
g.append(function (d) { return svgShape(d); })
where svgShape returns a string, you need to do something like
g.append(function(d) {
return document.createElementNS("http://www.w3.org/2000/svg", svgShape(d));
})
and the corresponding shape will be created. I've updated your jsbin here to demonstrate.
In your case, it might be easier to always append a path and vary the line generator, i.e. the d attribute value. That is, for a circle you would pass in a function that returns a circular path, for a polygon a function that returns the particular polygon path etc.

Categories