mindparse
mindparse

Reputation: 7285

Conditionally render a text element if a certain property is present in the data

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

Answers (1)

meetamit
meetamit

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

Related Questions