mindparse
mindparse

Reputation: 7295

d3.js - calculating width of a previously drawn element

I'm creating a horizontal legend to appear above a bar chart I have created in with d3.js.

This is my first attempt and I struggling to figure how I position each item in the legend to take in account the width of the previous item.

So here is what I'm looking at so far:

enter image description here

And here is my code relevant to the legend rendering:

var legendRectSize = 18,
    legendSpacing = 4;

svg.selectAll('.legend')
.data(data.chartKeys)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
  //width = the chart area width
  var horz = width/2 + i*100; //This needs to add previously rendered elements width instead
  return 'translate(' + horz + ',' + 0 + ')';
});

svg.selectAll('.legend').append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.attr('class', function(d) {
  return d;
})
.style('stroke', 'black');

svg.selectAll('.legend').append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) {
  return d;
});

As you can see in the transform function I am just adding multiplying the index of each item by a factor of 100 to spread them horizontally, but really I could do with determining the width of the previously rendered element (so coloured box and text combined) and add this instead so the spacing between them is even, and nothing overlaps.

Is there a way in the transform function to get at the previous item(s), or is a different approach best?

Thanks

Upvotes: 1

Views: 850

Answers (1)

nrabinowitz
nrabinowitz

Reputation: 55678

If you want an inline-block style layout, you'll need to keep some kind of running offset based on the real text width. You can see an example here: http://jsfiddle.net/nrabinowitz/r7yodrm1/

The key part of this is keeping offset in the outer scope, then using .each to update the offset with the text width:

var offset = 0;
var legendBlockWidth = 15;
var textMargin = 3;

legend.enter().append('g')
  .attr('class', 'legend')
  .each(function(key, i) {
    var item = d3.select(this);

    var text = item.append('text')
      .text(key);

    // ... snip ...

    // Translate the group based on the running width

    item.attr('transform', function() {
      return 'translate(' + offset + ',0)';
    });

    // Update the offset
    offset += text.node().getBBox().width + legendBlockWidth + textMargin * 3;
  });

You could use a variation on this to use the max text width instead, which would give you even spacing with no collisions: http://jsfiddle.net/nrabinowitz/r7yodrm1/2/

var maxOffset = 0;

legend2.enter().append('g')
  .attr('class', 'legend2')
  .each(function(key, i) {
    var item = d3.select(this);

    var text = item.append('text')
      .text(key);

    // ... snip ...

    // Update the offset
    maxOffset = Math.max(
      maxOffset, 
      text.node().getBBox().width + legendBlockWidth + textMargin * 3
    );
  });

legend2.attr('transform', function(d, i) {
  return 'translate(' + (maxOffset * i) +',50)';
});

Upvotes: 4

Related Questions