Reputation: 5192
I've got this legend:
As you can see, each legend entry is the same width. Instead, I'd like each legend entry's width to vary based upon the width of the entry's symbol and text. Ultimately, I want the same distance between the ends of the leading entry's text and the start of the following entry's symbol. In other words, I'd like the same distance between 'OA' and the plus sign as between the 'OI' and the diamond and the 'RARC' and the square. I need this to be based on pixels (string lengths won't suffice). I've been trying all sorts of stuff, but haven't been successful.
Here's my code:
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangle-down"]];
this.svg.selectAll('.legend').remove() //remove remnants of previous legend so new legend has clean slate...eliminates overlays during resizing
var legend = this.svg.append('g')
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(' + (ScatterChart.Config.margins.left + (width * .008)) + ',' + (height += .40 * ScatterChart.Config.margins.bottom) + ')');
var legendRect = legend
.selectAll('g')
.data(legendData)
;
var labelLength = 0
var labelLengthPrevious = 0
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function (d, i) {
//labelLength = labelLengthPrevious //Need to figure out pixel lengths
//labelLengthPrevious += (d[0].length) + 50
//return 'translate(' + labelLength + ', ' + 0 + ' )'; // y is constant and x growing
return 'translate(' + (i * (.15 * width)) + ', ' + 0 + ' )'; // y is constant and x growing
})
;
legendRectE
.append('path')
.attr("d", d3.svg.symbol().type((d) => {
return d[2]
}
).size((d3.min([height, width]) * ScatterChart.Config.axisFontMultiplier) * (d3.min([height, width]) * ScatterChart.Config.symbolSizeMultiplier)))
.style("fill", function (d) {
return d[1];
})
.attr('stroke', 'black')
;
//This asserts legendRectE as a node...I think. I do this so I can use the width and height measurements of legendRectE.
var node: SVGElement = <SVGElement>legendRectE.node()
legendRectE
.append("text")
.attr("x", function (d) {
return node.getBoundingClientRect().width
})
.attr("y", function (d) {
return node.getBoundingClientRect().height / 2.25
})
.text(function (d) {
return d[0];
})
.style('font-size', function () { return d3.min([height, width]) * ScatterChart.Config.axisFontMultiplier + "px" })
;
I think the answer would have something to do with this line: return 'translate(' + (i * (.15 * width)) + ', ' + 0 + ' )'; // y is constant and x growing
. Right now, it just shifts to the right by multiplying the index by 15% of the chart's width. I figure I need to somehow substitute the width of the legendRectE
(or of legendRect
or legend
) in place of (I * (.15 * width))
. I can't figure out how to do that.
You can see that I use the following to get the width of legendRectE
later in the code: var node: SVGElement = <SVGElement>legendRectE.node()
, followed by node.getBoundingClientRect().width
.
node.getBoundingClientRect().width
gives me a width value where you see it being used now, but when I use this same approach to determine a value for the translate I mentioned, it chokes; and when I use legendRect
or legend
instead of legendRectE
I only get '0'.
I thought I'd be able to edit the transform function something like this:
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function (d, i) {
var node: SVGElement = <SVGElement>legendRectE.node()
return 'translate(' + node.getBoundingClientRect().width + ', ' + 0 + ' )'; // y is constant and x growing
})
;
Obviously, I was wrong. Any ideas/advice?
p.s. I'm using d3 v3.5.
Upvotes: 4
Views: 2140
Reputation: 38151
The challenge is that it is (as far as I know) difficult to determine the transform when appending elements initially as the widths are unknown. But you could go back and calculate the width of each legend entry after they are all appended and then reposition the legend entries accordingly.
The snippet below positions everything overtop of each other to start, then calculates the svg
width of each legend g
using getBBox
. Then, using d3.sum, calculates the width of each element that was appended before it (and thus should be to the left of it) and sets the translate value to the sum of those widths accordingly.
It can probably be cleaned up a bit probably, it's a little quick. If there is lag before the elements are positioned correctly, appending them transparently and then fading them in after they are positioned might be an elegant (visually, less so programatically) solution (or appending them initially outside of the view box).
d3v4:
var data = ['short text','much longer text','the longest text passage','short text'];
var svg = d3.select('body')
.append('svg')
.attr('width',800)
.attr('height',200);
var groups = svg.selectAll('g')
.data(data)
.enter()
.append('g');
var rect = groups.append('rect')
.attr('fill',function(d,i) { return d3.schemeCategory10[i];})
.attr('height',30)
.attr('width',30);
var text = groups.append('text')
.attr('y', 20)
.attr('x', 35)
.text(function(d) { return d; });
// Now space the groups out after they have been appended:
var padding = 10;
groups.attr('transform', function(d,i) {
return "translate("+(d3.sum(data, function(e,j) {
if (j < i) { return groups.nodes()[j].getBBox().width; } else return 0; }) + padding * i) + ",0)";
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
d3v3:
var data = ['short text','much longer text','the longest text passage','short text'];
var svg = d3.select('body')
.append('svg')
.attr('width',800)
.attr('height',200);
var groups = svg.selectAll('g')
.data(data)
.enter()
.append('g');
var color = ["orange","red","purple","green"];
var rect = groups.append('rect')
.attr('fill',function(d,i) { return color[i];})
.attr('height',30)
.attr('width',30);
var text = groups.append('text')
.attr('y', 20)
.attr('x', 35)
.text(function(d) { return d; });
// Now space the groups out after they have been appended:
var padding = 10;
groups.attr('transform', function(d,i) {
return "translate("+(d3.sum(data, function(e,j) {
if (j < i) { return groups[0][j].getBBox().width; } else return 0; }) + padding * i) + ",0)";
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Upvotes: 3