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.
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.