csvd3.jsgrouped-bar-chart

Getting d3.max value for grouped bar chart y domain


I've created a grouped bar chart and am trying to set the y domain to the largest bar value. Doing that rather than a static number that has to be changed depending on the data. I'm confused on how to use d3.max() with multiple columns in a CSV file. And I'm trying to make it so you don't have to use a specific column name, since that may be different in other CSV files. Right now the column names are cat1, cat2, and cat3. I'm using D3 v7

Data

name,cat1,cat2,cat3
Item 1,50,102,302
Item 2,79,140,330
Item 3,200,180,120
Item 4,104,80,83
Item 5,90,320,130
Item 6,85,114,130

I kind of understand how to use d3.max() but definitely not in a situation like this.

Here's how I'm currently using it. You'll see I have a 350 set in the y domain. And I'm simply trying console log the max value, for now. I realize I'm not doing this correctly but am not sure how to.

D3

const data = await d3.csv(src); 

  console.log(d3.max(data, d => {
    return d;
  }));
  // returns {name: 'Item 1', cat1: '50', cat2: '102', cat3: '302'}
  
  // Would like to use the max value
  // rather than 350
  y.domain([350, 0])
   .range([0, height]);

Thank you for your help.


Solution

  • const margin = { top: 20, right: 50, bottom: 50, left: 70 };
    const wrap = d3.select('#chart-wrap');
    let wrapWidth = parseInt(wrap.style('width'));
    let width = wrapWidth - margin.left - margin.right;
    const wrapHeight = parseInt(wrap.style('height'));
    const height = wrapHeight - margin.top - margin.bottom;
    let subgroup;
    const y = d3.scaleLinear();
    const x0 = d3.scaleBand();
    const x1 = d3.scaleBand();
    const src = 'https://assets.codepen.io/1329727/data-multi-demo.csv';
    const colors = d3.scaleOrdinal()
      .range(['#5626C4', '#E60576', '#2CCCC3', '#FACD3D', '#181818']);
    let tooltipChart;
    
    // Tooltip
    const tooltipMouseMove = (key, value, loc) => {
    
      tooltipChart
        .html(() => {
          return (
            `<div class="chart-tooltip-wrap">
              <p><strong>${key}</strong></p>
              <p>${value}</p>
             </div>
            `
           );
        })
        .style('visibility', 'visible')
        .style('left', `${d3.pointer(event)[0] + loc}px`)
        .style('top', `${d3.pointer(event)[1] + 20}px`)
    }
    
    const tooltipMouseOut = () => {
      tooltipChart
        .style('visibility', 'hidden');
    }
    
    const svg = wrap.append('svg')
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom);
      
    // SVG aria tags
    svg.append('title')
      .attr('id','chart-title')
      .html('Group, verical bar chart');
    
    svg.append('desc')
      .attr('id','chart-desc')
      .html('Displays grouped bar charts for different items.');
    
    svg.attr('aria-labelledby','chart-title chart-desc');
    
    tooltipChart = wrap.append('div')
        .attr('class','chart-tooltip')
        .style('visibility', 'hidden');
    
    const group = svg.append('g')
        .attr('class', 'group')
        .attr('transform', `translate(${margin.left}, ${margin.top})`);
          
    async function createGroupBars() {
      const data = await d3.csv(src); 
      
      // Keys and group keys
      const keys = data.columns.slice(1);
      const groupKey = data.columns[0];
      
      const values = [];
      data.forEach((el)=> {
        for(let i = 0; i < keys.length; i++) {
            values.push(+el[keys[i]])
        }
      })
      
      // Bar width
      const barWidth = (width / groupKey.length);
      
      const isSame = (data,keys) => {
           let output = false;
           for(let i=0; i<keys.length;i++) {
             if(data[keys[i]] == max){
               output = true;
               break;
             }
           }
        return output;
      }
      
     // Getting the maximum value from the keys
     let max = d3.max(values);
     let result = data.find((d) => {
       return isSame(d,keys);
     });
      
      // Scales
      y.domain([max, 0])
        .range([0, height]);
    
      x0.domain(data.map(d => { return d[groupKey]; }))
        .range([0, width])
        .paddingInner(0.3);
      
      if (width < 400) {
        x0.paddingInner(0.15);
      }
    
       x1.domain(keys)
        .range([0, x0.bandwidth()]);
     
      // X Axis
      const xAxis = group.append('g')
        .attr('class', 'x-axis')
        .attr('transform', `translate(0, ${height})`)
        .call(
          d3.axisBottom(x0)
          .tickSize(7)
        );
    
      xAxis.call(g => g.select('.domain').remove());
      
      // X axis labels 
      xAxis.selectAll('text')
          .attr('transform', 'translate(-10, 0) rotate(-45)')
          .style('text-anchor', 'end');
      
      // Y Axis
      group.append('g')
        .attr('class', 'y-axis')
        .call(
          d3.axisLeft(y)
            .tickSizeOuter(0)
            .tickSize(-width)
         );
      
        d3.selectAll('.y-axis text')
          .attr('x', -10)
      
       // Subgroups of rectangles
      subgroup = group.selectAll('.subgroup')
        .data(data)
        .join('g')
        .attr('class', 'subgroup')
        .attr('transform', (d) => {
          return `translate(${x0(d[groupKey])}, 0)`;
        })
        .attr('aria-label', d => {
          return `Values for ${d.name}`
        })
    
      // Rectangles
      subgroup
        .selectAll('rect')
        .data(d => {
          return keys.map(key => {
            return { key: key, value: d[key] }
          });
        })
        .join('rect')
        .attr('class', 'rect')
        .attr('y', d => y(d.value))
        .attr('x', d => { return x1(d.key); })
        .attr('height', (d) => { return height - y(d.value); })
        .attr('width', x1.bandwidth())
        .attr('fill', (d, i) => { return colors(d.key); })
        .attr('aria-label', d => {
          return `${d.key} bar`;
        })
        .on('mousemove', function (event, d, i) {
        
          // Get parent's translate x value
          const parent = d3.select(this.parentNode).attr('transform').slice(10);
          const loc = parseFloat(parent);
        
          // call tooltip function
          tooltipMouseMove(d.key, d.value, loc);
        })
        .on('mouseout', () => {
          tooltipMouseOut();
        });
    
      // Rectangle labels
      subgroup.selectAll('.bar-labels')
        .data(d => {
          return keys.map(key => {
            return { key: key, value: d[key] }
          });
        })
        .join('text')
        .attr('class', 'bar-labels')
        .attr('y', d => { return y(d.value) - 3 })
        .attr('x', d => { return x1(d.key) + 12; })
        .attr('text-anchor', 'middle')
        .style('fill', '#181818')
        .text(d => { return d.value });
      
       // Legend
      const createLegend = (parent, cat) => {
          parent.append('div')
            .attr('class', 'legend')
            .selectAll('div')
            .data(data.columns.slice(1))
            .enter()
            .append('div')
            .attr('class', 'legend-group')
            .html((d, i) => {
              return(`
                <div class="legend-box" style="background-color: ${colors(cat[i])};"></div>
                <p class="legend-label">${cat[i]}</p>
              `);
            });
          }
        createLegend(wrap, Object.keys(data[0]).slice(1));
    
        // Resize
        const resize = () => {
          wrapWidth = parseInt(wrap.style('width'));
          width = wrapWidth - margin.left - margin.right;
    
          x0.range([0, width])
            .paddingInner(0.3);
          
          if (width < 400) {
            x0.paddingInner(0.15);
          }
    
          x1.range([0, x0.bandwidth()]);
          
          svg.attr('width', width + margin.left + margin.right);
          
          subgroup.selectAll('.rect')
            .attr('x', d => { return x1(d.key); })
            .attr('width', x1.bandwidth());
          
          subgroup.selectAll('.bar-labels')
            .attr('x', d => { return x1(d.key) + 12; })
          
          group.select('.x-axis')
            .attr('transform', `translate(0, ${height})`)
            .call(
              d3.axisBottom(x0)
            );
          
          group.select('.y-axis')
            .call(
              d3.axisLeft(y)
                .tickSizeOuter(0)
                .tickSize(-width)
             );
          
          subgroup.attr('transform', (d) => {
            return `translate(${x0(d[groupKey])}, 0)`;
          });
        }
        
        d3.select(window).on('resize', resize);
      
    }
    createGroupBars();
    body {
      background-color: #f7f4e9;
      font-family: sans-serif;
    }
    
    .chart-section {
      margin: 2rem auto;
      padding: 1rem;
      width: calc(100% - 2rem);
      max-width: 700px;
    }
    .chart-section #chart-wrap {
      height: 400px;
      width: 100%;
      position: relative;
    }
    .chart-section #chart-wrap .chart-tooltip {
      margin-left: 10px;
      position: absolute;
      z-index: 10;
    }
    .chart-section #chart-wrap .chart-tooltip .chart-tooltip-wrap {
      background-color: #181818;
      border-radius: 10px;
      box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
      display: block;
      padding: 0.875rem;
    }
    .chart-section #chart-wrap .chart-tooltip .chart-tooltip-wrap p {
      color: #fff;
      font-size: 0.875rem;
      line-height: 1.75;
      margin: 0;
    }
    .chart-section #chart-wrap svg {
      margin: auto;
    }
    .chart-section #chart-wrap svg .y-axis .domain {
      display: none;
    }
    .chart-section #chart-wrap svg .y-axis .tick line {
      stroke: #aaa;
      stroke-dasharray: 3, 3;
    }
    .chart-section #chart-wrap svg .y-axis .tick:last-child line {
      stroke: #555;
      stroke-dasharray: 0;
    }
    .chart-section #chart-wrap svg .bar-labels {
      font-size: 0.875rem;
      display: block;
    }
    @media (max-width: 700px) {
      .chart-section #chart-wrap svg .bar-labels {
        display: none;
      }
    }
    .chart-section #chart-wrap svg .tick line {
      stroke: #555;
    }
    .chart-section #chart-wrap svg .tick text {
      font-size: 0.875rem;
    }
    .chart-section #chart-wrap .legend {
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      gap: 10px;
      justify-content: center;
      margin: 2rem auto;
      width: 100%;
    }
    .chart-section #chart-wrap .legend .legend-group {
      align-items: center;
      display: flex;
      flex-basis: 100px;
      flex-direction: row;
      gap: 8px;
      justify-content: flex-start;
    }
    .chart-section #chart-wrap .legend .legend-group .legend-box {
      height: 20px;
      margin: 0;
      width: 20px;
    }
    .chart-section #chart-wrap .legend .legend-group .legend-label {
      margin: 0;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
    <section class="chart-section">
      <div id="chart-wrap"></div>
    </section>

    Code Explanation

    I modified the code for the SVG within the container

    // increase bottom value
    const margin = { top: 20, right: 50, bottom: 20, left: 70 };
    // change zero translate y value to ${margin.top}
    const group = svg.append('g')
        .attr('class', 'group')
        .attr('transform', `translate(${margin.left}, ${margin.top})`)
    

    and get dynamically max value from csv file

    const values = [];
      data.forEach((el)=> {
        for(let i = 0; i < keys.length; i++) {
            values.push(+el[keys[i]])
        }
      })
      
      // Bar width
      const barWidth = (width / groupKey.length);
      
      const isSame = (data,keys) => {
           let output = false;
           for(let i=0; i<keys.length;i++) {
             if(data[keys[i]] == max){
               output = true;
               break;
             }
           }
        return output;
      }
      
     // Getting the maximum value from the keys
     let max = d3.max(values);
     let result = data.find((d) => {
       return isSame(d,keys);
     });
    
    

    Note : I don't focus on the time complexity of the code; I simply aim to solve the problem.