Chris
Chris

Reputation: 303

how to automatically resize d3.js graph to include axis

I have a bit of a problem building a bar chart. I'm learning d3.js for the first time and being someone who always worked with PHP/MySQL, I haven't had to learn javascript. As a result, I'm struggling a bit.

My question is more conceptual in nature. If, let's say in a bar chart, the Y axis is contained in a g element and the bars are contained in another one, how can I ensure that my axis takes a dyanmic width based on the data presented?

I managed to generate a bar chart and it works great, but the padding is a fixed number (let's say 50px). it works great now, because my numbers go from 0 to 50, so everything fits. What happens if I get trillions instead? The width of the axis will change, yet my padding remains 50px, which means it will clip my content.

What is the "convention" when it comes to this? Any tricks?

Thanks

Upvotes: 0

Views: 2240

Answers (1)

Mark
Mark

Reputation: 108512

One trick you might use here is what I like to call the "double-render". You essentially draw the axis first (before the rest of the plot) and get the width of the greatest tick label. The, You can draw the plot conventionally with that value as the margin. This trick is especially useful for string "category" labels, but will work for numbers as well.

Here's a commented example. Run it multiple times to see how it refits the axis:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  .bar {
    fill: steelblue;
  }
  
  .bar:hover {
    fill: brown;
  }
  
  .axis--x path {
    display: none;
  }
</style>
<svg width="300" height="300"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//chancejs.com/chance.min.js"></script>
<script>


  // set up some random data
  // pick a random max value to render on the yaxis
  var maxVal = chance.integer({
      min: 1,
      max: chance.pickone([1e1, 1e5, 1e10])
    }),
    // generate some fake data
    data = [{
      x: chance.word(),
      y: chance.floating({
        min: 0,
        max: maxVal
      })
    }, {
      x: chance.word(),
      y: chance.floating({
        min: 0,
        max: maxVal
      })
    }, {
      x: chance.word(),
      y: chance.floating({
        min: 0,
        max: maxVal
      })
    }, {
      x: chance.word(),
      y: chance.floating({
        min: 0,
        max: maxVal
      })
    }];

  // create svg and set up a y scale, the height value doesn't matter
  var svg = d3.select("svg"),
    y = d3.scaleLinear().rangeRound([100, 0]);

  // set domain
  y.domain([0, d3.max(data, function(d) {
    return d.y;
  })]);

  // draw fake axis
  var yAxis = svg.append("g")
    .attr("class", "axis axis--y")
    .call(d3.axisLeft(y));

  // determine max width of text label
  var mW = 0;
  yAxis.selectAll(".tick>text").each(function(d) {
    var w = this.getBBox().width;
    if (w > mW) mW = w;
  });

  // remove fake yaxis
  yAxis.remove();

  // draw plot normally
  var margin = {
      top: 20,
      right: 20,
      bottom: 30,
      left: mW + 10 // max with + padding fudge
    },
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom;

  var g = svg.append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  // reset to actual height
  y.range([height, 0]);

  var x = d3.scaleBand().rangeRound([0, width]).padding(0.1);

  x.domain(data.map(function(d) {
    return d.x;
  }));

  g.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x));

  g.append("g")
    .attr("class", "axis axis--y")
    .call(d3.axisLeft(y));

  g.selectAll(".bar")
    .data(data)
    .enter().append("rect")
    .attr("class", "bar")
    .attr("x", function(d) {
      return x(d.x);
    })
    .attr("y", function(d) {
      return y(d.y);
    })
    .attr("width", x.bandwidth())
    .attr("height", function(d) {
      return height - y(d.y);
    });
</script>

Upvotes: 6

Related Questions