D3 grouped bar chart doesn't spread along x scale

Viewed 125

I'm trying to create a grouped bar chart by city with 3 category, however it is only draws one group and all others are taking the same position so they are on top of each other. It doesn't take the whole width and doesn't spread along x scale. Something wrong with my x0 and x1 scales. If I understand correctly x0 is the beginning and x1 scale is the end position of rectangles in the chart? Can you explain what am I doing wrong here?

Here's a snippet of the problem:

const groupedBarChartData = {
  dataset: [
    {
      City: 'York',
      'Sales': 20000,
      'Returns': 25000,
      'Total': 22000,
    },
    {
      City: 'London',
      'Sales': 40000,
      'Returns': 43000,
      'Total': 45000,
    },
    {
      City: 'Manchester',
      'Sales': 40000,
      'Returns': 7000,
      'Total': 19999,
    }]}
  
  
// svg dimensions
const width = 1192;
const height = 610;
const colorScale = d3
  .scaleOrdinal()
  .range(['red', 'blue', 'green', 'orange']);
  
  
  const svg = d3.select('body').append('svg');

    // shape data
    const data = groupedBarChartData.dataset;

    // get categories
    const keys = Object.keys(data[0]).slice(1);

    // x axis
    const x0Scale = d3.scaleBand().rangeRound([0, width]);
    const x1Scale = d3.scaleBand();

    // set x0 - cities
    x0Scale.domain(data.map((d) => d['City']));

    // set x1  - categories
    x1Scale.domain(keys).rangeRound([0, x0Scale.bandwidth()]);

    // y axis
    const yScale = d3.scaleLinear().range([height, 0]);

    // equal to Math.round(data.map((x) => Math.max(...keys.map((el) => x[el]))).sort((a, b) => b - a)[0])
    yScale.domain([
      0,
      Math.round(d3.max(data, (d) => d3.max(keys, (key) => d[key]))),
    ]);

    // draw chart
    svg
      .selectAll('rect')
      .data(data)
      .enter()
      .selectAll('rect')
      .data((d) =>
        keys.map((key) => {
          return { key: key, value: d[key] };
        })
      )
      .enter()
      .append('rect')
      .attr('x', (d, i) => x1Scale(d.key))
      .attr('y', (d) => yScale(d.value))
      .attr('width', x1Scale.bandwidth() - 15)
      .attr('height', (d) => height - yScale(d.value))
      .attr('fill', (d) => colorScale(d.key));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

And the result it gives me:

rect position

1 Answers

This happens as you only are positioning the rects based on x1Scale, which positions based on categories only:

  .attr('x', (d, i) => x1Scale(d.key))

Nowhere do you position based on the city.

The most common solution is to use a parent g representing and positioning the parent city, and then go about positioning the bars representing categories:

    svg
      .selectAll('rect')
      .data(data)
      .enter()
      .append("g")
      .attr("transform", d=>`translate(${x0Scale(d.City)},0)`)
      .selectAll('rect')
      ... 

Which gives us:

const groupedBarChartData = {
  dataset: [
    {
      City: 'York',
      'Sales': 20000,
      'Returns': 25000,
      'Total': 22000,
    },
    {
      City: 'London',
      'Sales': 40000,
      'Returns': 43000,
      'Total': 45000,
    },
    {
      City: 'Manchester',
      'Sales': 40000,
      'Returns': 7000,
      'Total': 19999,
    }]}
  
  
// svg dimensions
const width = 1192;
const height = 610;
const colorScale = d3
  .scaleOrdinal()
  .range(['red', 'blue', 'green', 'orange']);
  
  
  const svg = d3.select('body').append('svg').attr("width",width).attr("height",height);

    // shape data
    const data = groupedBarChartData.dataset;

    // get categories
    const keys = Object.keys(data[0]).slice(1);

    // x axis
    const x0Scale = d3.scaleBand().rangeRound([0, width]);
    const x1Scale = d3.scaleBand();

    // set x0 - cities
    x0Scale.domain(data.map((d) => d['City']));

    // set x1  - categories
    x1Scale.domain(keys).rangeRound([0, x0Scale.bandwidth()]);

    // y axis
    const yScale = d3.scaleLinear().range([height, 0]);

    // equal to Math.round(data.map((x) => Math.max(...keys.map((el) => x[el]))).sort((a, b) => b - a)[0])
    yScale.domain([
      0,
      Math.round(d3.max(data, (d) => d3.max(keys, (key) => d[key]))),
    ]);

    // draw chart
    svg
      .selectAll('rect')
      .data(data)
      .enter()
      .append("g")
      .attr("transform", d=>`translate(${x0Scale(d.City)},0)`)
      .selectAll('rect')
      .data((d) =>
        keys.map((key) => {
          return { key: key, value: d[key]};
        })
      )
      .enter()
      .append('rect')
      .attr('x', (d, i) => x1Scale(d.key))
      .attr('y', (d) => yScale(d.value))
      .attr('width', x1Scale.bandwidth() - 15)
      .attr('height', (d) => height - yScale(d.value))
      .attr('fill', (d) => colorScale(d.key));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Alternatively, if you don't want nested elements, you could pass the city information to each bar's datum and use that to position the bars in conjunction with the category:

  .selectAll('rect')
  .data((d) =>
    keys.map((key) => {
      return { key: key, value: d[key], city: d.City };
    })
  )
  .enter()
  .append('rect')
  .attr('x', (d, i) => x1Scale(d.key)+x0Scale(d.city))
  ... 

const groupedBarChartData = {
  dataset: [
    {
      City: 'York',
      'Sales': 20000,
      'Returns': 25000,
      'Total': 22000,
    },
    {
      City: 'London',
      'Sales': 40000,
      'Returns': 43000,
      'Total': 45000,
    },
    {
      City: 'Manchester',
      'Sales': 40000,
      'Returns': 7000,
      'Total': 19999,
    }]}
  
  
// svg dimensions
const width = 1192;
const height = 610;
const colorScale = d3
  .scaleOrdinal()
  .range(['red', 'blue', 'green', 'orange']);
  
  
  const svg = d3.select('body').append('svg').attr("width",width).attr("height",height);

    // shape data
    const data = groupedBarChartData.dataset;

    // get categories
    const keys = Object.keys(data[0]).slice(1);

    // x axis
    const x0Scale = d3.scaleBand().rangeRound([0, width]);
    const x1Scale = d3.scaleBand();

    // set x0 - cities
    x0Scale.domain(data.map((d) => d['City']));

    // set x1  - categories
    x1Scale.domain(keys).rangeRound([0, x0Scale.bandwidth()]);

    // y axis
    const yScale = d3.scaleLinear().range([height, 0]);

    // equal to Math.round(data.map((x) => Math.max(...keys.map((el) => x[el]))).sort((a, b) => b - a)[0])
    yScale.domain([
      0,
      Math.round(d3.max(data, (d) => d3.max(keys, (key) => d[key]))),
    ]);

    // draw chart
    svg
      .selectAll('rect')
      .data(data)
      .enter()
      .selectAll('rect')
      .data((d) =>
        keys.map((key) => {
          return { key: key, value: d[key], city: d.City };
        })
      )
      .enter()
      .append('rect')
      .attr('x', (d, i) => x1Scale(d.key)+x0Scale(d.city))
      .attr('y', (d) => yScale(d.value))
      .attr('width', x1Scale.bandwidth() - 15)
      .attr('height', (d) => height - yScale(d.value))
      .attr('fill', (d) => colorScale(d.key));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Related