Home > Software design >  How to place the bars of a bar chart in the right positions of the xAxis using d3.js?
How to place the bars of a bar chart in the right positions of the xAxis using d3.js?

Time:12-31

I'm making a bar chart but I'm having problems to match the bar positions with the xAxis. They're not in the right place, for example, by hovering the bar above the 2010 mark, you can see it shows a 2007 value. How can I fix that?

 let url = "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json";

const padding = 50;
const height = 460;
const width = 940;
const barthickness = 2.909090909090909;

 var svg = d3.select('body')
    .append('svg')
    .attr('width', width)
    .attr('height', height);

var arr = [];
var years = [];

d3.json(url, function(data) {
        for (let i = 0; i < data.data.length; i  ) {
            arr[i] = data.data[i];
            years[i] = parseInt(data.data[i][0].slice(0,4));
        }  

     const yScale = d3.scaleLinear()
                      .domain([0, d3.max(arr, (d) => d[1])])
                      .range([height - padding, padding]);

    const xScale = d3.scaleLinear()
                       .domain([d3.min(years, d => d), d3.max(years, (d) => d)])
                       .range([padding, width - padding]);

const xAxis = d3.axisBottom(xScale);

const yAxis = d3.axisLeft(yScale);

    svg.append("g")
       .attr("transform", "translate(0,"   (height - padding)   ")")
       .call(xAxis);

   svg.append('g')
      .attr('transform', 'translate('   padding   ', 0)')
      .call(yAxis)

    svg.selectAll('rect')
        .data(arr)
        .enter()
        .append('rect')
        .attr('fill', 'blue')
        .attr('height', d => height - padding - yScale(d[1]))
        .attr('width', barthickness)
        .attr('x', (d, i) => padding   (3.2* i))
        .attr('y', d => yScale(d[1]))
        .append('title')
        .text((d, i) => years[i]   ': '   d[1])
    });
        <script src="https://d3js.org/d3.v4.min.js"></script>

CodePudding user response:

The problem is that you are not using your x-scale to position the bars. You are using padding (3.2* i) to set the x coordinate of the bars, which does not line up with your scale. Your chart is 840 pixels wide and has 275 bars, which would be ~3.055 pixels per bar. Your code is placing bars every 3.2 pixels, which is too far.

Typically with bar charts, rather than hard-coding a bar thickness, you use a band scale. You'll want to use your scales both in your axes and to position the bars.

Alternatively, since you are working with temporal data, you could also consider using an area chart instead of a bar chart.

Below I've provided two similarly looking charts for your data. One is a bar chart and the other an area chart.

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
  <div id="bar-chart"></div>
  <div id="area-chart"></div>

  <script>
    const url = 'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json';

    d3.json(url).then(json => {
      // convert the string into Date objects
      const parse = d3.timeParse('%Y-%m-%d');
      const data = json.data.map(d => ({ date: parse(d[0]), value: d[1] }));

      barchart(data);
      areachart(data);
    });

    function barchart(data) {
      // set up

      const margin = { top: 20, right: 20, bottom: 20, left: 30 };

      const width = 600 - margin.left - margin.right;
      const height = 300 - margin.top - margin.bottom;

      const svg = d3.select('#bar-chart')
        .append('svg')
          .attr('width', width   margin.left   margin.right)
          .attr('height', height   margin.top   margin.bottom)
        .append('g')
          .attr('transform', `translate(${margin.left},${margin.top})`);

      // scales

      const x = d3.scaleBand()
          .domain(data.map(d => d.date))
          .range([0, width]);

      const y = d3.scaleLinear()
          .domain([0, d3.max(data, d => d.value)])
          .range([height, 0]);

      // axes

      // by default, axes for band scales show tick marks for every bar
      // that would be too cluttered for this data, so we override this
      // by explicitly setting tickValues()
      const [minDate, maxDate] = d3.extent(data, d => d.date);
      const xAxis = d3.axisBottom(x)
          .tickSizeOuter(0)
          // only show the year in the tick labels
          .tickFormat(d3.timeFormat('%Y'))
          .tickValues(d3.timeTicks(minDate, maxDate, 10));

      const yAxis = d3.axisLeft(y)
          .tickSizeOuter(0)
          .ticks(10, '~s');

      svg.append('g')
          .attr('transform', `translate(0,${height})`)
          .call(xAxis);

      svg.append('g')
          .call(yAxis);

      // bars

      // function to convert Date into string showing the month and year
      const format = d3.timeFormat('%b %Y');

      svg.selectAll('rect')
        .data(data)
        .join('rect')
          .attr('x', d => x(d.date))
          .attr('width', d => x.bandwidth())
          .attr('y', d => y(d.value))
          .attr('height', d => height - y(d.value))
          .attr('fill', 'steelblue')
        .append('title')
          .text(d => `${format(d.date)}: ${d.value}`)
    }

    function areachart(data) {
      // set up

      const margin = { top: 20, right: 20, bottom: 20, left: 30 };

      const width = 600 - margin.left - margin.right;
      const height = 300 - margin.top - margin.bottom;

      const svg = d3.select('#area-chart')
        .append('svg')
          .attr('width', width   margin.left   margin.right)
          .attr('height', height   margin.top   margin.bottom)
        .append('g')
          .attr('transform', `translate(${margin.left},${margin.top})`);

      // scales

      const x = d3.scaleTime()
          .domain(d3.extent(data, d => d.date))
          .range([0, width]);

      const y = d3.scaleLinear()
          .domain([0, d3.max(data, d => d.value)])
          .range([height, 0]);

      // area generator

      const area = d3.area()
          .x(d => x(d.date))
          .y0(y(0))
          .y1(d => y(d.value))
          .curve(d3.curveStepAfter);

      // axes
      const xAxis = d3.axisBottom(x)
          .tickSizeOuter(0)
          // only show the year in the tick labels
          .tickFormat(d3.timeFormat('%Y'));

      const yAxis = d3.axisLeft(y)
          .tickSizeOuter(0)
          .ticks(10, '~s');

      svg.append('g')
          .attr('transform', `translate(0,${height})`)
          .call(xAxis);

      svg.append('g')
          .call(yAxis);

      // area

      svg.append('path')
          .attr('d', area(data))
          .attr('fill', 'steelblue')
    }

  </script>
</body>

</html>

  • Related