D3.js Bar Charts Tutorial: Simple, Grouped & Stacked

Maryam Alavi
Name
Maryam Alavi

Updated on

This tutorial shows how to create three essential bar chart variants in D3.js with React: a simple (single-series) bar chart, a grouped bar chart, and a stacked bar chart. Each example below renders live and includes a copyable code block.

Simple Bar Chart

The simple bar chart is a great starting point: one categorical axis and one quantitative axis.

import React, { useEffect, useRef } from 'react'
import * as d3 from 'd3'
 
function SimpleBarChart() {
  const containerRef = useRef(null)
  useEffect(() => {
    const root = d3.select(containerRef.current)
    root.selectAll('*').remove()
 
    const data = [
      { category: 'A', value: 34 },
      { category: 'B', value: 18 },
      { category: 'C', value: 51 },
      { category: 'D', value: 27 },
      { category: 'E', value: 42 }
    ]
 
    const width = 640, height = 360
    const margin = { top: 24, right: 24, bottom: 48, left: 56 }
    const innerWidth = width - margin.left - margin.right
    const innerHeight = height - margin.top - margin.bottom
 
    const svg = root
      .append('svg')
      .attr('viewBox', `0 0 ${width} ${height}`)
      .style('max-width', '100%')
      .style('height', 'auto')
 
    const g = svg
      .append('g')
      .attr('transform', `translate(${margin.left},${margin.top})`)
 
    const x = d3.scaleBand()
      .domain(data.map(d => d.category))
      .range([0, innerWidth])
      .padding(0.2)
    const y = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.value) || 0]).nice()
      .range([innerHeight, 0])
 
    g.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x))
    g.append('g').call(d3.axisLeft(y))
 
    g.selectAll('rect')
      .data(data)
      .join('rect')
      .attr('x', d => x(d.category) || 0)
      .attr('y', d => y(d.value))
      .attr('width', x.bandwidth())
      .attr('height', d => innerHeight - y(d.value))
      .attr('fill', '#3b82f6')
  }, [])
  return <div ref={containerRef} />
}

Grouped Bar Chart

Grouped bars compare multiple series side-by-side within each category.

import React, { useEffect, useRef } from 'react'
import * as d3 from 'd3'
 
function GroupedBarChart() {
  const containerRef = useRef(null)
  useEffect(() => {
    const seriesKeys = ['Apples', 'Bananas', 'Cherries']
    const data = [
      { group: '2019', Apples: 30, Bananas: 22, Cherries: 13 },
      { group: '2020', Apples: 35, Bananas: 28, Cherries: 18 },
      { group: '2021', Apples: 28, Bananas: 26, Cherries: 20 },
      { group: '2022', Apples: 40, Bananas: 31, Cherries: 24 }
    ]
 
    const width = 720, height = 380
    const margin = { top: 24, right: 24, bottom: 52, left: 56 }
    const innerWidth = width - margin.left - margin.right
    const innerHeight = height - margin.top - margin.bottom
 
    const root = d3.select(containerRef.current)
    root.selectAll('*').remove()
    const svg = root.append('svg')
      .attr('viewBox', `0 0 ${width} ${height}`)
      .style('max-width', '100%')
      .style('height', 'auto')
    const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
 
    const x0 = d3.scaleBand().domain(data.map(d => d.group)).range([0, innerWidth]).padding(0.2)
    const x1 = d3.scaleBand().domain(seriesKeys).range([0, x0.bandwidth()]).padding(0.1)
    const y = d3.scaleLinear().domain([0, d3.max(data, d => d3.max(seriesKeys, k => d[k])) || 0]).nice().range([innerHeight, 0])
    const color = d3.scaleOrdinal().domain(seriesKeys).range(['#3b82f6', '#10b981', '#f59e0b'])
 
    g.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x0))
    g.append('g').call(d3.axisLeft(y))
 
    const groupG = g.selectAll('g.series-group')
      .data(data)
      .join('g')
      .attr('class', 'series-group')
      .attr('transform', d => `translate(${x0(d.group)},0)`)
 
    groupG.selectAll('rect')
      .data(d => seriesKeys.map(key => ({ key, group: d.group, value: d[key] })))
      .join('rect')
      .attr('x', d => x1(d.key) || 0)
      .attr('y', d => y(d.value))
      .attr('width', x1.bandwidth())
      .attr('height', d => innerHeight - y(d.value))
      .attr('fill', d => color(d.key))
  }, [])
  return <div ref={containerRef} />
}

Stacked Bar Chart

Stacked bars show parts-of-a-whole within each category.

import React, { useEffect, useRef } from 'react'
import * as d3 from 'd3'
 
function StackedBarChart() {
  const containerRef = useRef(null)
  useEffect(() => {
    const seriesKeys = ['North', 'South', 'West']
    const data = [
      { quarter: 'Q1', North: 12, South: 18, West: 8 },
      { quarter: 'Q2', North: 16, South: 14, West: 12 },
      { quarter: 'Q3', North: 10, South: 20, West: 14 },
      { quarter: 'Q4', North: 18, South: 12, West: 16 }
    ]
 
    const width = 720, height = 380
    const margin = { top: 24, right: 24, bottom: 52, left: 56 }
    const innerWidth = width - margin.left - margin.right
    const innerHeight = height - margin.top - margin.bottom
 
    const root = d3.select(containerRef.current)
    root.selectAll('*').remove()
    const svg = root.append('svg')
      .attr('viewBox', `0 0 ${width} ${height}`)
      .style('max-width', '100%')
      .style('height', 'auto')
    const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
 
    const x = d3.scaleBand().domain(data.map(d => d.quarter)).range([0, innerWidth]).padding(0.2)
    const y = d3.scaleLinear().domain([0, d3.max(data, d => seriesKeys.reduce((s, k) => s + d[k], 0)) || 0]).nice().range([innerHeight, 0])
    const color = d3.scaleOrdinal().domain(seriesKeys).range(['#3b82f6', '#10b981', '#f59e0b'])
    const stacked = d3.stack().keys(seriesKeys)(data)
 
    g.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x))
    g.append('g').call(d3.axisLeft(y))
 
    g.selectAll('g.layer')
      .data(stacked)
      .join('g')
      .attr('class', 'layer')
      .attr('fill', d => color(d.key))
      .selectAll('rect')
      .data(d => d)
      .join('rect')
      .attr('x', d => x(d.data.quarter) || 0)
      .attr('y', d => y(d[1]))
      .attr('height', d => y(d[0]) - y(d[1]))
      .attr('width', x.bandwidth())
  }, [])
  return <div ref={containerRef} />
}

Tips and Best Practices

  • Use scales: Map your data to pixels using d3.scaleBand for categories and d3.scaleLinear for values.
  • Axes: d3.axisBottom and d3.axisLeft save time and add consistency.
  • Padding: For grouped bars, use two band scales (x0 for groups, x1 for series).
  • Colors: Use d3.scaleOrdinal for categorical series.
  • Accessibility: Add <title> to bars for quick tooltips and screen readers.