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"])
Related
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;
})
});
}
I am looking for how to create a rowchart in dc.js to show and filter items with multiple tags. I've summed up a few answers given on stack overflow, and now have a working code.
var data = [
{id:1, tags: [1,2,3]},
{id:2, tags: [3]},
{id:3, tags: [1]},
{id:4, tags: [2,3]},
{id:5, tags: [3]},
{id:6, tags: [1,2,3]},
{id:7, tags: [1,2]}];
var content=crossfilter(data);
var idDimension = content.dimension(function (d) { return d.id; });
var grid = dc.dataTable("#idgrid");
grid
.dimension(idDimension)
.group(function(d){ return "ITEMS" })
.columns([
function(d){return d.id+" : "; },
function(d){return d.tags;},
])
function reduceAdd(p, v) {
v.tags.forEach (function(val, idx) {
p[val] = (p[val] || 0) + 1; //increment counts
});
return p;
}
function reduceRemove(p, v) {
v.tags.forEach (function(val, idx) {
p[val] = (p[val] || 0) - 1; //decrement counts
});
return p;
}
function reduceInitial() {
return {};
}
var tags = content.dimension(function (d) { return d.tags });
var groupall = tags.groupAll();
var tagsGroup = groupall.reduce(reduceAdd, reduceRemove, reduceInitial).value();
tagsGroup.all = function() {
var newObject = [];
for (var key in this) {
if (this.hasOwnProperty(key) && key != "") {
newObject.push({
key: key,
value: this[key]
});
}
}
return newObject;
}
var tagsChart = dc.rowChart("#idtags")
tagsChart
.width(400)
.height(200)
.renderLabel(true)
.labelOffsetY(10)
.gap(2)
.group(tagsGroup)
.dimension(tags)
.elasticX(true)
.transitionDuration(1000)
.colors(d3.scale.category10())
.label(function (d) { return d.key })
.filterHandler (function (dimension, filters) {
var fm = filters.map(Number)
dimension.filter(null);
if (fm.length === 0)
dimension.filter(null);
else
dimension.filterFunction(function (d) {
for (var i=0; i < fm.length; i++) {
if (d.indexOf(fm[i]) <0) return false;
}
return true;
});
return filters;
}
)
.xAxis().ticks(5);
It can be seen on http://jsfiddle.net/ewm76uru/24/
Nevertheless, the rowchart is not updated when I filter by one tag. For example, on jsfiddle, if you select tag '1', it filters items 1,3,6 and 7. Fine. But the rowchart is not updated... I Should have tag '3' count lowered to 2 for example.
Is there a way to have the rowchart tags counts updated each time I filter by tags ?
Thanks.
After a long struggle, I think I have finally gathered a working solution.
As said on crossfilter documentation : "a grouping intersects the crossfilter's current filters, except for the associated dimension's filter"
So, the tags dimension is not filtered when tag selection is modified, and there is no flag or function to force this reset. Nevertheless, there is a workaround (given here : https://github.com/square/crossfilter/issues/146).
The idea is to duplicate the 'tags' dimension, and to use it as the filtered dimension :
var tags = content.dimension(function (d) { return d.tags });
// duplicate the dimension
var tags2 = content.dimension(function (d) { return d.tags });
var groupall = tags.groupAll();
...
tagsChart
.group(tagsGroup)
.dimension(tags2) // and use this duplicated dimension
as it can been seen here :
http://jsfiddle.net/ewm76uru/30/
I hope this will help.
I have a piechart I'm displaying using the following dimension -
var types = facts.dimension(function (d) {
if (d.types === 2)
return "Type 2";
else if (d.types === 3)
return "Type 3";
else
return "Other";
});
I would like to not return, ignore, all other types, so the pie chart would just display Type 2 and Type 3. I cannot seem to get this working though I'm sure it's simple. Can I do something within that dimension or do I need to filter before?
Thanks for any help.
Have you tried creating a new type and then creating a dimension on top of that?
facts.forEach(function(d) {
if (d.types == 2) {
d._type = "Type 2";
} else if (d.types == 3) {
d._type = "Type 3";
} else {
d._type = "Other";
}
});
var types = facts.dimension(dc.pluck('_type'))
You need to (1) Filter your data then (2) remove it from the bin. Run your data through ensure_group_bins(myGroup) you'll get the chart you're after
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.key != "unwanted" ;
});
}
};}
function ensure_group_bins(source_group, bins) {
return {
all:function () {
var result = source_group.all().slice(0), // copy original results (we mustn't modify them)
found = {};
result.forEach(function(d) {
found[d.key] = true;
});
bins.forEach(function(d) {
if(!found[d])
result.push({key: d, value: 0});
});
return result;
}
};};
dc.js Github
I'm trying to make the chord diagram in a reusable way. My problem is when transitioning between 2 data-sets, such one data-set contains zero value (means that one chord will be removed).
So here is the transition code that I have called to transition:
changeDataFunction = function(data,selection) { // thisfunction will be called to determine the next drawing function to be called: render() function for the first time of drawing or transition function for the second one.
if (!chord.matrix()) {
chord.matrix(data);
self.render();
} else {
var old = { //save the old value of the diagram for interpolate
groups: chord.groups(),
chords: chord.chords()
};
console.log(old);
chord.matrix(data);
transition(old,selection); //call the transition function
}
};
function transition(old,selection){
svg = d3.select(selection).selectAll("svg").data([chord]);
svg.enter().append("svg")
.classed("chorddiagram",true);
arcGroup = svg.select(".arcpath-group").selectAll(".arcpath")
.data(function(d){ return d.groups(); })
.enter().append("path")
.classed("arcpath",true)
.style("fill",function(d){ return color(d.index);})
.style("stroke",function(d) { return color (d.index);})
.attr("d",arc_svg)
.on("mouseover",fade(0.1))
.on("mouseout",fade(1));
arcGroup
.transition().duration(1500)
.attrTween("d",arcTween(arc_svg,old));
arcGroup.exit().remove();
chordGroup = svg.select(".chordpath-group").selectAll(".chordpath")
.data(function(d){ return d.chords(); })
.enter().append("path")
.classed("chordpath",true)
.attr("d",chord_svg)
.style("fill", function(d){ return color(d.target.index);});
chordGroup
.transition().duration(1500)
.style("fill",function(d){
return color(d.target.index);
})
.attrTween("d", chordTween(chord_svg, old));
chordGroup.exit().remove();
}
// Interpolate the arcs
function arcTween(arc_svg, old) {
return function(d,i) {
if (d) {
var i = d3.interpolate(old.groups[i], d);
};
return function(t) {
return arc_svg(i(t));
}
}
}
// Interpolate the chords
function chordTween(chord_svg, old) {
return function(d,i) { //I think the problem from inside this block of code. The chord doesn't exist then the bug occurred. But I'm not good at interpolate so I don't know how to fix this.
var i = d3.interpolate(old.chords[i], d);
return function(t) {
return chord_svg(i(t));
}
}
}
Thank you for your attention and helping!
So I have solved my own question above by using the arcTween and chordTween function of #AmeliaBR to replace my old one:
https://stackoverflow.com/a/21923560/3128209
Thank you so much!
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);.