Getting d3.max value for grouped bar chart y domain

109 Views Asked by At

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.

2

There are 2 best solutions below

2
On BEST ANSWER

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.

5
On

let data = [{
    name: 'Item 1',
    cat1: 50,
    cat2: 102,
    cat3: 302
},
{
    name: 'Item 2',
    cat1: 79,
    cat2: 140,
    cat3: 330
},
{
    name: 'Item 3',
    cat1: 200,
    cat2: 180,
    cat3: 120      
},
{
    name: 'Item 4',
    cat1: 104,
    cat2: 80,
    cat3: 83    
},
{
    name: 'Item 5',
    cat1: 90,
    cat2: 320,
    cat3: 130       
},
{
    name: 'Item 6',
    cat1: 85,
    cat2: 114,
    cat3: 130   
}
]

let max = d3.max(data, ((d)=> (d.cat1,d.cat2,d.cat3)));
let result = data.find((d) => d.cat1 == max || d.cat2 == max || d.cat3 == max);
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>