Step-by-Step Code Walkthrough
Step 1: Define the Data and Margins
const data = [
{ page: '/home', views: 1250 },
{ page: '/about', views: 430 },
{ page: '/products', views: 890 },
{ page: '/contact', views: 320 },
{ page: '/blog', views: 675 }
];
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const width = 600 - margin.left - margin.right; // 530
const height = 400 - margin.top - margin.bottom; // 340
The margin convention reserves space for axes and labels. The chart area is the inner rectangle.
Step 2: Create the SVG and Chart Group
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
// Shift everything by the margins
const g = svg.append('g')
.attr('transform',
`translate(${margin.left}, ${margin.top})`);
The <g> group acts as a local coordinate system. All coordinates inside it are relative to the chart area, not the SVG edges.
Step 3: Create the Scales
// X scale: page names → horizontal band positions
const xScale = d3.scaleBand()
.domain(data.map(d => d.page))
.range([0, width])
.padding(0.2);
// Y scale: pageview counts → vertical pixel positions
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.views)])
.nice() // round to 1300
.range([height, 0]); // inverted!
// Color scale: page names → distinct colors
const colorScale = d3.scaleOrdinal()
.domain(data.map(d => d.page))
.range(['#16a085', '#2980b9', '#8e44ad',
'#e67e22', '#e74c3c']);
.nice() rounds the domain max from 1250 to 1300, giving cleaner axis tick values. The y-range is inverted because SVG's y=0 is the top.
Step 4: Draw the Axes
// X axis at the bottom of the chart area
g.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
// Y axis on the left side
g.append('g')
.call(d3.axisLeft(yScale));
D3 generates all the tick marks, labels, and axis lines automatically from the scale's domain and range.
Step 5: Bind Data and Draw Bars
g.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => xScale(d.page))
.attr('y', d => yScale(d.views))
.attr('width', xScale.bandwidth())
.attr('height', d => height - yScale(d.views))
.attr('fill', d => colorScale(d.page));
This is the core D3 pattern. .data(data) binds the array; .join('rect') creates one <rect> per data item. Each attribute is a function of the bound datum d. The bar's y position is yScale(d.views) (top edge), and its height extends down to the baseline at height.
Step 6: Add Value Labels
g.selectAll('.bar-label')
.data(data)
.join('text')
.attr('class', 'bar-label')
.attr('x', d => xScale(d.page) + xScale.bandwidth() / 2)
.attr('y', d => yScale(d.views) - 5)
.attr('text-anchor', 'middle')
.attr('fill', d => colorScale(d.page))
.text(d => d.views.toLocaleString());
A second data join creates <text> elements, one per bar, positioned 5px above each bar's top edge.