14

I'm drawing line charts with d3 and it all works fine. However, I have to leave enough margin on the left of the chart area to fit whatever I think might be the widest y-axis text label. I'd like to adjust this space for each chart, depending on the widest label.

Initially I thought I could find the maximum y value, create a hidden text object, work out how wide that is, and use that value for the left margin when creating the chart. A bit nasty, but it gets me a value.

However, if the maximum y value is, say "1598.538" the top-most y-axis label might be "1500"... ie, a lot narrower.

So I guess I want to find the width of whatever will actually be the top-most label. But I can't think how to do that without drawing the chart and axis, measuring that width, and drawing it again for real. Which sounds nasty! Is there a non-nasty way to do this?

UPDATE

Here's part of my code, using Lars' suggestion, just to show where it fits in:

// I did have
// `.attr("transform", "translate(" + margin.left + "," + margin.top ")")`
// on the end of this line, but I've now moved that to the bottom.
var g = svg.select("g");

// Add line paths.
g.selectAll(".line").data(data)
    .enter()
    .append("path")
    .attr("d", line);

// Update the previously-created axes.
g.select(".axis-x")
    .attr("transform", "translate(0," + yScale.range()[0] + ")"))
    .call(xAxis);
g.select(".axis-y")
    .call(yAxis);

// Lars's suggestion for finding the maximum width of a y-axis label:
var maxw = 0;
d3.select(this).select('.axis-y').selectAll('text').each(function(){
  if (this.getBBox().width > maxw) maxw = this.getBBox().width;
});

// Now update inner dimensions of the chart.
g.attr("transform", "translate(" + (maxw + margin.left) + "," + margin.top + ")");
VividD
  • 10,456
  • 6
  • 64
  • 111
Phil Gyford
  • 13,432
  • 14
  • 81
  • 143

1 Answers1

6

You can put everything inside a g element and set transform based on the max width. Something along the lines of

var maxw = 0;
yAxisContainer.selectAll("text").each(function() {
    if(this.getBBox().width > maxw) maxw = this.getBBox().width;
});
graphContainer.attr("transform", "translate(" + maxw + ",0)");
Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
  • Excellent, thanks again Lars! I'll update my question with a bit more code example to show where I put your suggestion in relation to other parts of the chart-drawing code. – Phil Gyford Jun 14 '13 at 14:33
  • Having said that... while it leaves the perfect space for the left axis, it does so by moving the chart area to the right, which clips off the right-edge of the chart and x-axis. I'm not sure how to fix this - doesn't it come from the x-axis's scale's range, which should be set earlier? – Phil Gyford Jun 14 '13 at 15:35
  • You could also draw the y axis first, then measure and then set the x axis range accordingly and draw the rest of the graph. Having the elements of the y axis there already may interfere with selectors you have for drawing the rest of the graph (`.selectAll('path')`, `.selectAll('text')`) though, so be careful. – Lars Kotthoff Jun 14 '13 at 16:06
  • Thanks. I also want to draw the x-axis at a width that allows for the first and last labels on it, which stick off the left and right. But I need to draw the x-axis before I can get the labels' widths, which I need to set the range of the x-axis... it's all getting a bit chicken and egg :) – Phil Gyford Jun 14 '13 at 16:30
  • I've ended up drawing the x-axis, taking the measurements, then calling `.remove()` on it. Then drawing it again for real later. Not nice, but works for the moment. – Phil Gyford Jun 14 '13 at 17:21
  • 1
    It sounds like for what you want to do you really have to draw first, measure and redraw. – Lars Kotthoff Jun 14 '13 at 17:57
  • Yes, that's what I've settled on for now. It feels inelegant, but it works. Thanks for the help. – Phil Gyford Jun 15 '13 at 13:10
  • `maxw = Math.max(maxw, this.getBBox().width - parseInt(d3.select(this).attr('x')));` – MrYellow Sep 27 '21 at 04:19