Search code examples
d3.jsalignmentbar-chart

d3 bar chart align bars on ticks for differing number of elements


I have a bar chart which displays data1.json or data2.json on clicking a radio button.

data1.json has 9 element and data2.json has 4 elements.

Question 1: How can I set things up so that the bars will always center on the ticks regardless of the number of objects or bars in the data?

Question 2: How can I get the x-axis down to where you would expect it to be? I know this sounds trivial, but I've tried many ways.

Question 3: I would like to make sure the 'value' is a number using

newdata.foreach(function(d) {
      d.value = +d.value;
    })

but I don't seem to be able to iterate over the 'newdata' as I would expect.

This is on github here and a gh-pages demo here.

The index.html file

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  .bar {
    fill: steelblue;
  }
  .bar:hover {
    fill: brown;
  }
  .axis text {
    font: 10px sans-serif;
  }
  .axis path,
  .axis line {
    fill: none;
    stroke: #000;
    shape-rendering: crispEdges;
  }
</style>

<body>
  <form>
    <input type="radio" name="inputsrc" id="defaultInput" value="default" checked=""><label for="defaultInput">data1.json</label>
    <input type="radio" name="inputsrc" id="updateInput" value="post"><label for="updateInput">data2.json</label>
  </form>
  <svg id="d3newbie-chart" width="600" height="400">
  </svg>
  <script src="https://d3js.org/d3.v4.min.js"></script>

  <script type="text/javascript">
    var outerWidth = 600,
      outerHeight = 400;
    var margin = {
        top: 40,
        right: 30,
        bottom: 30,
        left: 80
      },
      width = outerWidth - margin.left - margin.right,
      height = outerHeight - margin.top - margin.bottom;
    var x = d3.scaleBand()
      .range([0, width]);
    var y = d3.scaleLinear()
      .range([height, 0]);
    var xAxis = d3.axisBottom(x);
    var yAxis = d3.axisLeft(y)
      .ticks(10, "%");
    var chart = d3.select("#d3newbie-chart")
      .attr("width", outerWidth)
      .attr("height", outerHeight)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    // x-axis
    chart.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

    function defaultFunction() {
      d3.json("data1.json", function(error, newdata) {
        if (error) throw error;
        data = newdata;
        // newdata.foreach(function(d) {
        //   d.value = +d.value;
        // })
        update();
      });
    }
    function updateFunction() {
      d3.json("data2.json", function(error, newdata) {
        if (error) throw error;
        data = newdata;
        update();
      });
    }
    function update(err, newdata) {
      y.domain([0, d3.max(data, function(d) {
        return d.value;
      })]);
      x.domain(data.map(function(d) {
        return d.name
      }));
      // x-axis
      chart.select(".x.axis").remove();
      chart.append("g")
        .attr("class", "x axis")
        .call(xAxis)
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Frequency");
      // y-axis
      chart.select(".y.axis").remove();
      chart.append("g")
        .attr("class", "y axis")
        .call(yAxis)
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Frequency");
      var bar = chart.selectAll(".bar")
        .data(data, function(d) {
          return d.name;
        });
      // new data:
      bar.enter().append("rect")
        .attr("class", "bar")
        .attr("x", function(d) {
          return x(d.name);
        })
        .attr("y", function(d) {
          return y(d.value);
        })
        .attr("height", function(d) {
          return height - y(d.value);
        })
        .attr("width", 20);
      // removed data:
      bar.exit().remove();
      // updated data:
      bar.transition()
        .duration(750)
        .attr("y", function(d) {
          return y(d.value);
        })
        .attr("height", function(d) {
          return height - y(d.value);
        });
    };
    document.getElementById("defaultInput")
      .onclick = defaultFunction;
    document.getElementById("updateInput")
      .onclick = updateFunction;
    defaultFunction();
  </script>
</body>

Solution

  • Updated code: https://jsfiddle.net/5j2nw238/2/

    Question 1: The width of the bars is determined by the width of the svg, as well as your given values for the scaleBand functions paddingInner, paddingOuter, and a few others.

    https://github.com/d3/d3-scale/blob/master/README.md#band-scales

    Take a look at the image describing these parameters. The point is to not hardcode the width of the bars like you did with .attr("width", 20);, and instead manipulate the scaleBand to find your desired look.

    Question 2:

    You did this correctly with the code

    chart.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);
    

    But then on update, you remove the axis and didn't apply the transform again. Instead of deleting the axis and recreating it, simply update the axis scale with the new domain, and do chart.select(".x.axis").call(xAxis). This falls in line with the d3 way of thinking in a data driven fashion. Change your data, and let D3 handle the DOM.

    Question 3:

    The function name is forEach, not foreach. Also, you say you want to make sure the value is a string, but putting the + operator before a variable converts it to a number, not a string.