Reputation: 7285
I am using d3 for a bar chart in my application and have a need to annotate each of the bars with a piece of text for the data value the bar represents.
I have this working so far like so:
layers = svg.selectAll('g.layer')
.data(stacked, function(d) {
return d.dataPointLegend;
})
.enter()
.append('g')
.attr('class', function(d) {
return d.dataPointLegend;
});
layers.selectAll('rect')
.data(function(d) {
return d.dataPointValues;
})
.enter()
.append('rect')
.attr('x', function(d) {
return x(d.pointKey);
})
.attr('width', x.rangeBand())
.attr('y', function(d) {
return y(d.y0 + d.pointValue);
})
.attr('height', function(d) {
return height - margin.bottom - margin.top - y(d.pointValue)
});
layers.selectAll('text')
.data(function(d) {
return d.dataPointValues;
})
.enter()
.append('text')
.text(function() {
return 'bla';
})
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function(d) {
return y(d.y0 + d.pointValue) - 5;
})
I'd actually only like to append a text element if a certain property exists in the data.
I have seen the datum
selection method in the docs and wondered if this is what I need, I'm unsure how I cancel an append call if the property I'm seeking is not present.
Thanks
Attempt 2
Ok So I have had another stab, this time using the each
function like so:
layers.selectAll('text')
.data(function(d) {
return d.dataPointValues;
})
.enter()
//This line needs to be my each function I think?
.append('text')
.each(function(d){
if(d.pointLabel) {
d3.select(this)
.text(function(d) {
return d.pointLabel;
})
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function(d) {
return y(d.y0 + d.pointValue) - 5;
})
.attr('class', 'data-value')
}
});
}
The problem I now have is that I get a text element added regardless of whether a pointLabel
property is present.
It feels like I'm close, I did wonder if I should be moving my append('text')
down into the each, but when I tried I got an error as d3 was not expecting that particular chain of calls.
Upvotes: 0
Views: 889
Reputation: 25177
How about using d3's data binding for this.... Rather than appending the text to layers.enter()
selection, do the following to the entire layers
selection, i.e. including the updating nodes:
labels = layers.selectAll('text')
.data(function(d) {
// d is the datum of the parent, and for this example
// let's assume that the presence of `pointLabel`
// indicates whether the label should be displayed or not.
// You could work in more refined logic for it if needed:
return d.pointLabel ? [d] : [];
})
// The result of the above is that if a label is needed, a binding will
// occur to a single element array containing `d`. Otherwise, it'll bind
// to an empty array. After that binding, using enter, update and exit,
// you get to add, update or even remove text (you might need removal if
// you're updating an existing view whose existing label needs to go away)
labels.enter()
.append("text")
labels
.text(function(d) { d.pointLabel })
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', ...)
labels.exit()
.remove()
The trick here (it's hardly a trick, but it's not a very common d3 use-case) is that it's either binding to a single element array [d]
or to a blank one []
, which is how you get to use the enter()
selection to only create labels where needed. And the benefit of this approach over non-data-binding appproaches is that this code can be called multiple times — whenever a d.pointLabel
changes or when the app's state change — and the labels' presense (or lack of presence, ie removal) will update accordingly.
Upvotes: 1