Binding data appears to be working, but nothing is being appended - d3.js

This is a follow-up to this question which I asked last week (since then I've changed a lot of things though so don't pay toooo much attention to the code I posted).
My data is organised like this:
dataset = {
'name': 'total',
'groups': [
{
'name': 'foo',
'children': [ {c1},{c2}...], // key: value pairings
'totals': { key1: val1, ....} // where val1 = sum of key1 from all children
},
// and so on
]
}
What I'm doing in D3 is this:
Make a table for each group
Append a row for the group totals
Append th's for those keys not included in 'totals'
Append more th's for each item in 'totals'
Then there is a function which adds the child rows/cells when the parent table is clicked
Item 4 is where I'm having trouble. Below are two scenarios which I have tried:
// (1) This works
parentTotals.selectAll('th.totals')
.data(tbs) // tbs is a list of the key names in 'totals'
.enter()
.append('th')
.attr('class', 'totals')
.text(function(d) { return d; });
// (2) This does not work
parentTotals.selectAll('th.totals')
.data(function(d) { return d.totals; }) // inherits a child from dataset.groups
.enter()
.append('th')
.attr('class', 'totals')
.text(function(d, i) { return d[tbs[i]]; });
The reason why I believe that the data is being bound correctly in scenario 2 is that if I put a console.log(d.totals); before return d.totals; I get a lovely Object { key1: val1, .... } for each member in the group. So if the data is being bound, why aren't any cells being appended? Thank you.
== EDIT ==
With the information provided by Scott I have made it work. If anyone's interested, this is what I did:
parentTotals.selectAll('th.totals')
.data(function(d) {
return Object.keys(d.totals).map(function(k, i) {
return d.totals[tbs[i]];
})
})
.enter()
.append('th')
.attr('class', function(d, i) { return tbs[i]; })
.text(function(d, i) {
return (d/1000).toFixed(1);
});

totals is an Object, not an Array. D3's data bind is based on arrays. You can get an array of key names from an object using: Object.keys(d.totals);.

Related

Set different diameters to graph nodes in javascript d3

How can I set different diameters to graph nodes, depend on their 'grade' (by grade I mean root or children)?
For example, I have one source node and I want to set the diameter to a value. Its children will have another value.
This is what I've tried by now:
Here I build the links array:
reply.forEach(function (targetNode) {
links.push({
source: sourceNode, // the source is a string
target: targetNode // the target is an array of strings
});
});
And here I tried to give different diameters:
.attr("r", function (d) {
links.forEach(function (link) {
if (d === link.source) {
return 15;
} else return 6;
})
})
The result was a graph only with links, all the nodes disappeared.
Any ideas how can I resolve this?
I fixed it. If it will help anyone, I leave the code here:
function setDiameter() {
links.forEach(function (link) {
svg.selectAll("circle")
.attr("r", function (d) {
if (d === link.source) {
return 15;
} else return 6;
})
});
}

how to use .data() to append 2 children to parent w/o counting an existing child?

d3.select(this).selectAll("li")
.data(function(d) {
return [{"name": "tab1"},{"name": "tab2"}]
})
.enter()
.append(function(d, i) {
return document.createElement("li");
})
.html(function(d,i) {
return "Test1";
});
If there is already an "li" in "this", only one will be added ... how do I use .data() to add the 2 "li"s and keep the one that is already there?
The problem is that, when you write selectAll("li"), you are selecting the existent li and joining the data to them. You have to use another selection, for instance, selectAll(".newLi").
d3.select(this).selectAll(".newList")
.data(function(d) {
return [{"name": "tab1"},{"name": "tab2"}]
})
.enter()
.append(function(d, i) {
return document.createElement("li");
})
.html(function(d,i) {
return "Test1";
});
In this fiddle, click on the existent li ("this" and "that") to append the new elements: https://jsfiddle.net/gerardofurtado/8s2g77kf/
To finish, there is nothing wrong in using pure JS, but D3 makes your life easier... you can append simply using:
.append("li")
PS: This has been asked so many times... not even a "duplicate", maybe a "multiplicate".

How to create a one level nest in D3

I'm learning about nesting and have been looking at phoebe bright's explanations, where she writes:
var nested_data = d3.nest()
.key(function(d) { return d.status; })
.entries(csv_data);
gets this:
[
{
"key": "Complete",
"values": [
{
"id": "T-024",
"name": "Organisation list in directory",
"priority": "MUST",
},
{
When I try to do the same, in my console, if I can recreate it, looks like this:
Object
key: "1847"
values: Array [2]
0: Object
production: "A Mirror for Witches"
1: Object
production: "Sadlers Wells"
When I try to display the "Values" as text, all I get is [Object, object] in my html, where what I want is the production names.
How do I do this? I have tried nesting production as a key also, but this doesn't seem to work, and have also tried returning the index when returning the values, but can't get that to work either.
Any help I will really appreciate, thanks.
Here is my code
data.csv
year,production,company
1847,A Mirror for Witches
1847,Sadlers Wells
d3.csv("data.csv", function(csv_data){
var nested_data = d3.nest()
.key(function(d) { return d.year; })
.entries(csv_data)
console.log(nested_data);
var selection =
d3.select("body").selectAll("div")
.data(nested_data)
.enter()
selection.append("div")
.classed('classed', true)
.text(function(d){
return d.key;
});
d3.selectAll(".classed").append("div")
.text(function(d){
return d.values;
});
});
Here's a working plunk: http://plnkr.co/edit/0QuH8P9ujMdl0vWuQkrQ?p=preview
I added a few more lines of data to show it working properly.
The thing to do here is to add a second selection (I've called it production_selection) and bind data based off the first selection (year_selection): You use nested selections to show nested data.
First selection (show a div for each year, or key, in your nested data):
var year_selection = d3.select("#chart").selectAll("div")
.data(nested_data)
.enter().append("div")
...
Second selection (show all productions, or values, under that key):
var production_selection = year_selection.selectAll(".classed")
.data(function(d) { return d.values; })
.enter().append("div")
...
For the second selection, you just define the accessor function (d.values)

d3 adding data attribute conditionally

I'm creating a table with d3 to be used by the FooTable jquery plugin and this requires having some data- attributes in the header row. But not all columns have all the data attributes and wondering if there is a way to do this.
This approach sort of works, by adding all the possible data attributes and leaving some blank, but I'm sure it's not good practise.
var th = d3.select(selection).select("thead").selectAll("th")
.data(colspec)
.enter().append("th")
.text(function(d) { return d["data-name"]; })
.attr("data-class", function(d) {
if ("data-class" in d) {
return d["data-class"];
} else {
return "";
}
})
.attr("data-hide", function(d) {
if ("data-hide" in d) {
return d["data-hide"];
} else {
return "";
}
})
.attr("data-ignore", function(d) {
if ("data-ignore" in d) {
return d["data-ignore"];
} else {
return "";
}
})
etc.
colspec example:
[{"data-name": "username"}, {"data-name": "Date Joined", "data-hide": "true"}]
Currently getting:
<th data-class="" data-hide="true" data-ignore="" data-type="">Joined</th>
Want
<th data-hide="true" >Joined</th>
Any suggestions?
You don't need to call each() or filter()... The attr() function will do this for you internally. Just call it with a function instead of a value, and have that function return the desired value for each datum, or null if the attribute is not desired for a particular datum, like so:
...
.attr('data-class', function(d) {
return 'data-class' in d ? d['data-class'] : null;
});
If your function returns null, the attribute is not added. You can even combine several attributes into one call by providing a map of attr names to functions like so:
...
.attr({
'data-class': function(d) {
return 'data-class' in d ? d['data-class'] : null;
},
'data-hide': function(d) {
return 'data-hide' in d ? d['data-hide'] : null;
},
'data-ignore': function(d) {
return 'data-ignore' in d ? d['data-ignore'] : null;
}
});
or if you're like me and would rather not type so much, you can reduce the list of attribute names into the appropriate map:
...
.attr(['data-class', 'data-hide', 'data-ignore'].reduce(function(result, attr) {
result[attr] = function(d) {
return attr in d ? d[attr] : null;
}
return result;
}, {}));
Seems like a good candidate for .each():
var th = d3.select(selection).select("thead").selectAll("th")
.data(colspec)
.enter().append("th")
.text(function(d) { return d["data-name"]; })
// now address each item individually
.each(function(d) {
var header = d3.select(this);
// loop through the keys - this assumes no extra data
d3.keys(d).forEach(function(key) {
if (key != "data-name")
header.attr(key, d[key]);
});
});
I often use .each when having a per-item scope makes more sense than trying to figure out a bunch of attributes for each item.
For a short list of attributes, especially if you're worried about extra data in the objects, it's probably easier to loop through the desired keys instead of everything:
.each(function(d) {
var header = d3.select(this);
['data-class', 'data-hide', 'data-ignore'].forEach(function(key) {
if (key in d)
header.attr(key, d[key]);
});
});
You can use the .filter() function to only operate on the subset of the selection that you need to set attributes for, e.g.
var th = d3.select(selection).select("thead").selectAll("th")
.data(colspec)
.enter().append("th")
.text(function(d) { return d["data-name"]; });
th.filter(function(d) { return ("data-class" in d); })
.attr("data-class", function(d) {
return d["data-class"];
});
The most voted solution is perfect because .attr(a,b) works as conditional when b is null,
d3chain.attr('data-class', d=>'data-class' in d ? d['data-class'] : null );
but this solution is not geral, is not valid for other chaining methods, except using .each(), .filter or .call(). In general the most simple is call().
.call(condFunc,param)
Suppose that param is an global variable used as parameter in the condition, and that g is a global object used to return a value.
// inconditional
d3chain.attr(param, g[param])
// conditional case using globals
d3chain.call( s => { if (g[param]) s.attr(param,g[param]) })
// conditional case passing the parameter
d3chain.call( (s,p) => {
if (g[p]) s.attr(p, g[p])
}, param)
.each(d => condFunc)
Typical use:
d3chain.each( d=> {
if (param && d) d3.select(this).attr(d, g[param])
})
See #nrabinowitz answer for detailed example.
.filter(d=>condFunc).etc
Typical use:
d3chain.filter( d=> param in d ).attr(param, d=> g[param])
See #LarsKotthoff answer for detailed example.
A cleaner is to use filter
.filter(d => !!d["data-class"]) // filter only data with the "data-class" property
.attr("data-class", d => d["data-class"])

How to draw line charts with complex data structures in d3

I have the following complex data structure:
[
Object {id: 15, targets: Array[2]},
Object {id: 26, targets: Array[2]},
Object {id: 39, targets: Array[2]}
]
'targets' is an array of objects. Each of them has this shape:
Object {idTarget: "target1", events: Array[315]}
Object {idTarget: "target2", events: Array[310]}
'events' is an array with the real values to plot.
So, each element has this shape:
Object {timestamp: 1373241642, value: 1801.335}
Now, with this structured dataset, I would like to create an svg group 'g' for each external object (I am referring to 15, 26 and 39) and inside each group I want to create two lines, one for each target, using the values in 'events'.
Having a flat dataset it's easy to proceed in the drawing following the pattern: select + data + enter + append, but I am having trouble with this complex dataset.
For example I don't even know how to assign a key function to start.
svg.selectAll('.element')
.data(data, function(d){ return d.id + ?})
I would like to have this kind of keys '15target1', '15target2', '26target1', '26target2' and so on.
Do you recommend to simplify the dataset giving up the possibility of having neat groups or there is a workaround here that lets me easily draw what I want?
Thanks.
You want nested selections for this. Your code would look something like this.
var gs = d3.selectAll("g").data(data, function(d) { return d.id; });
gs.enter().append("g");
var line = d3.svg.line().x(function(d) { return xscale(d.timestamp); })
.y(function(d) { return yscale(d.value); });
gs.selectAll("path")
.data(function(d) { return d.targets; }, function(d) { return d.idTarget; })
.enter().append("path")
.attr("d", function(d) { return line(d.events); });

Resources