Skip to main content

Chart Types & Patterns

Common chart implementations and visualization patterns in D3.js.

Bar Chart

Vertical Bar Chart

const data = [10, 20, 30, 40, 50];
const width = 400;
const height = 200;
const margin = { top: 20, right: 20, bottom: 30, left: 40 };

const svg = d3
.select('body')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);

const g = svg
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);

const x = d3
.scaleBand()
.domain(data.map((d, i) => i))
.range([0, width])
.padding(0.1);

const y = d3
.scaleLinear()
.domain([0, d3.max(data)])
.range([height, 0]);

g.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', (d, i) => x(i))
.attr('y', d => y(d))
.attr('width', x.bandwidth())
.attr('height', d => height - y(d))
.attr('fill', 'steelblue');

Horizontal Bar Chart

const x = d3
.scaleLinear()
.domain([0, d3.max(data)])
.range([0, width]);

const y = d3
.scaleBand()
.domain(data.map((d, i) => i))
.range([0, height])
.padding(0.1);

g.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', 0)
.attr('y', (d, i) => y(i))
.attr('width', d => x(d))
.attr('height', y.bandwidth())
.attr('fill', 'steelblue');

Grouped Bar Chart

const data = [
{ category: 'A', value1: 20, value2: 35 },
{ category: 'B', value1: 30, value2: 25 },
{ category: 'C', value1: 25, value2: 40 },
];

const x0 = d3
.scaleBand()
.domain(data.map(d => d.category))
.range([0, width])
.paddingInner(0.1);

const x1 = d3
.scaleBand()
.domain(['value1', 'value2'])
.range([0, x0.bandwidth()])
.padding(0.05);

const y = d3
.scaleLinear()
.domain([0, d3.max(data, d => Math.max(d.value1, d.value2))])
.range([height, 0]);

const color = d3
.scaleOrdinal()
.domain(['value1', 'value2'])
.range(['#1f77b4', '#ff7f0e']);

const groups = g
.selectAll('.group')
.data(data)
.join('g')
.attr('class', 'group')
.attr('transform', d => `translate(${x0(d.category)},0)`);

groups
.selectAll('rect')
.data(d => ['value1', 'value2'].map(key => ({ key, value: d[key] })))
.join('rect')
.attr('x', d => x1(d.key))
.attr('y', d => y(d.value))
.attr('width', x1.bandwidth())
.attr('height', d => height - y(d.value))
.attr('fill', d => color(d.key));

Line Chart

Basic Line Chart

const data = [
{ x: 0, y: 10 },
{ x: 1, y: 20 },
{ x: 2, y: 15 },
{ x: 3, y: 30 },
{ x: 4, y: 25 },
];

const x = d3
.scaleLinear()
.domain(d3.extent(data, d => d.x))
.range([0, width]);

const y = d3
.scaleLinear()
.domain(d3.extent(data, d => d.y))
.range([height, 0]);

const line = d3
.line()
.x(d => x(d.x))
.y(d => y(d.y))
.curve(d3.curveLinear);

svg
.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2)
.attr('d', line);

// Add dots
svg
.selectAll('.dot')
.data(data)
.join('circle')
.attr('class', 'dot')
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y))
.attr('r', 4)
.attr('fill', 'steelblue');

Multi-line Chart

const data = [
{
series: 'A',
values: [
{ x: 0, y: 10 },
{ x: 1, y: 20 },
{ x: 2, y: 15 },
],
},
{
series: 'B',
values: [
{ x: 0, y: 15 },
{ x: 1, y: 25 },
{ x: 2, y: 20 },
],
},
];

const color = d3.scaleOrdinal(d3.schemeCategory10);

const line = d3
.line()
.x(d => x(d.x))
.y(d => y(d.y));

svg
.selectAll('.line')
.data(data)
.join('path')
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', d => color(d.series))
.attr('stroke-width', 2)
.attr('d', d => line(d.values));

Scatter Plot

Basic Scatter Plot

const data = [
{ x: 10, y: 20, size: 5 },
{ x: 20, y: 30, size: 8 },
{ x: 30, y: 25, size: 12 },
{ x: 40, y: 35, size: 6 },
];

const x = d3
.scaleLinear()
.domain(d3.extent(data, d => d.x))
.range([0, width]);

const y = d3
.scaleLinear()
.domain(d3.extent(data, d => d.y))
.range([height, 0]);

const sizeScale = d3
.scaleSqrt()
.domain(d3.extent(data, d => d.size))
.range([3, 15]);

svg
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y))
.attr('r', d => sizeScale(d.size))
.attr('fill', 'steelblue')
.attr('opacity', 0.7);

Area Chart

Basic Area Chart

const area = d3
.area()
.x(d => x(d.x))
.y0(height) // Baseline
.y1(d => y(d.y)) // Top line
.curve(d3.curveCardinal);

svg
.append('path')
.datum(data)
.attr('fill', 'lightsteelblue')
.attr('stroke', 'steelblue')
.attr('stroke-width', 2)
.attr('d', area);

Stacked Area Chart

const stack = d3.stack().keys(['value1', 'value2', 'value3']);

const stackedData = stack(data);

const area = d3
.area()
.x(d => x(d.data.x))
.y0(d => y(d[0]))
.y1(d => y(d[1]));

svg
.selectAll('.area')
.data(stackedData)
.join('path')
.attr('class', 'area')
.attr('fill', (d, i) => color(i))
.attr('d', area);

Pie Chart

Basic Pie Chart

const data = [10, 20, 30, 40];

const pie = d3
.pie()
.sort(null) // Don't sort
.value(d => d); // Value accessor

const arc = d3
.arc()
.innerRadius(0) // 0 for pie, >0 for donut
.outerRadius(100);

const arcs = svg
.selectAll('.arc')
.data(pie(data))
.join('g')
.attr('class', 'arc')
.attr('transform', 'translate(150,150)');

arcs
.append('path')
.attr('d', arc)
.attr('fill', (d, i) => color(i));

// Add labels
arcs
.append('text')
.attr('transform', d => `translate(${arc.centroid(d)})`)
.attr('text-anchor', 'middle')
.text(d => d.data);

Donut Chart

const arc = d3
.arc()
.innerRadius(50) // Inner radius for donut hole
.outerRadius(100);

// Add center text
svg
.append('text')
.attr('text-anchor', 'middle')
.attr('transform', 'translate(150,150)')
.style('font-size', '20px')
.text('Total');

Histogram

Basic Histogram

const data = d3.range(1000).map(d3.randomNormal(100, 15));

const x = d3.scaleLinear().domain(d3.extent(data)).range([0, width]);

const histogram = d3
.histogram()
.value(d => d)
.domain(x.domain())
.thresholds(x.ticks(20));

const bins = histogram(data);

const y = d3
.scaleLinear()
.domain([0, d3.max(bins, d => d.length)])
.range([height, 0]);

svg
.selectAll('rect')
.data(bins)
.join('rect')
.attr('x', d => x(d.x0) + 1)
.attr('width', d => Math.max(0, x(d.x1) - x(d.x0) - 1))
.attr('y', d => y(d.length))
.attr('height', d => height - y(d.length))
.attr('fill', 'steelblue');

Box Plot

Basic Box Plot

function createBoxPlot(data, x, scale) {
const q1 = d3.quantile(data.sort(d3.ascending), 0.25);
const median = d3.quantile(data, 0.5);
const q3 = d3.quantile(data, 0.75);
const iqr = q3 - q1;
const min = Math.max(d3.min(data), q1 - 1.5 * iqr);
const max = Math.min(d3.max(data), q3 + 1.5 * iqr);

const boxWidth = 40;

// Vertical lines (whiskers)
svg
.append('line')
.attr('x1', x)
.attr('x2', x)
.attr('y1', scale(min))
.attr('y2', scale(max))
.attr('stroke', 'black');

// Box
svg
.append('rect')
.attr('x', x - boxWidth / 2)
.attr('y', scale(q3))
.attr('width', boxWidth)
.attr('height', scale(q1) - scale(q3))
.attr('fill', 'lightblue')
.attr('stroke', 'black');

// Median line
svg
.append('line')
.attr('x1', x - boxWidth / 2)
.attr('x2', x + boxWidth / 2)
.attr('y1', scale(median))
.attr('y2', scale(median))
.attr('stroke', 'black')
.attr('stroke-width', 2);
}

Margin Convention

Standard Margin Setup

const margin = { top: 20, right: 20, bottom: 30, left: 50 };
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;

const svg = d3
.select('body')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);

const g = svg
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);

Responsive Charts

Responsive SVG

const container = d3.select("#chart");
const svg = container.append("svg")
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.attr("preserveAspectRatio", "xMidYMid meet")
.classed("svg-content-responsive", true);

// CSS
.svg-content-responsive {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}