Skip to main content

Axes & Legends

Essential patterns for creating axes, labels, and legends in D3.js visualizations.

Basic Axes

Creating Axes

const xScale = d3.scaleLinear().domain([0, 100]).range([0, width]);

const yScale = d3.scaleLinear().domain([0, 50]).range([height, 0]);

// Create axis generators
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);

// Add axes to SVG
svg
.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height})`)
.call(xAxis);

svg.append('g').attr('class', 'y-axis').call(yAxis);

Axis Positions

// Bottom axis (typical for X)
const xAxis = d3.axisBottom(xScale);

// Top axis
const xAxisTop = d3.axisTop(xScale);

// Left axis (typical for Y)
const yAxis = d3.axisLeft(yScale);

// Right axis
const yAxisRight = d3.axisRight(yScale);

Customizing Axes

Tick Formatting

// Number formatting
const yAxis = d3
.axisLeft(yScale)
.tickFormat(d3.format('.2f')) // 2 decimal places
.tickFormat(d3.format('$,.0f')) // Currency, no decimals
.tickFormat(d3.format('.1%')); // Percentage with 1 decimal

// Time formatting
const timeAxis = d3.axisBottom(timeScale).tickFormat(d3.timeFormat('%b %Y')); // "Jan 2024"

Tick Control

// Number of ticks
const xAxis = d3.axisBottom(xScale).ticks(10); // Approximately 10 ticks

// Specific tick values
const yAxis = d3.axisLeft(yScale).tickValues([0, 25, 50, 75, 100]); // Exact tick positions

// Tick size
const gridAxis = d3
.axisLeft(yScale)
.tickSize(-width) // Negative for grid lines
.tickSizeInner(6) // Inner tick length
.tickSizeOuter(0); // Outer tick length

Tick Styling

// Custom tick padding
const xAxis = d3.axisBottom(xScale).tickPadding(10); // Space between ticks and labels

// Remove tick lines but keep labels
const cleanAxis = d3.axisBottom(xScale).tickSize(0).tickPadding(15);

Grid Lines

Basic Grid

// X-axis grid lines
svg
.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(
d3.axisBottom(xScale).tickSize(-height).tickFormat('') // No labels
);

// Y-axis grid lines
svg
.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(yScale).tickSize(-width).tickFormat(''));

Styled Grid

// CSS for grid styling
.grid line {
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}

.grid path {
stroke-width: 0;
}

Custom Grid Function

function makeXGridlines(scale) {
return d3.axisBottom(scale).ticks(5).tickSize(-height).tickFormat('');
}

function makeYGridlines(scale) {
return d3.axisLeft(scale).ticks(5).tickSize(-width).tickFormat('');
}

// Add gridlines
svg
.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(makeXGridlines(xScale));

svg.append('g').attr('class', 'grid').call(makeYGridlines(yScale));

Axis Labels

Simple Labels

// X-axis label
svg
.append('text')
.attr('transform', `translate(${width / 2}, ${height + margin.bottom})`)
.style('text-anchor', 'middle')
.text('X Axis Label');

// Y-axis label
svg
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 0 - margin.left)
.attr('x', 0 - height / 2)
.attr('dy', '1em')
.style('text-anchor', 'middle')
.text('Y Axis Label');

Positioned Labels

// Bottom label
svg
.append('text')
.attr('class', 'axis-label')
.attr('x', width / 2)
.attr('y', height + 35)
.attr('text-anchor', 'middle')
.text('Time');

// Left label
svg
.append('text')
.attr('class', 'axis-label')
.attr('transform', 'rotate(-90)')
.attr('x', -height / 2)
.attr('y', -35)
.attr('text-anchor', 'middle')
.text('Value');

Time Axes

Time Scale Axis

const timeScale = d3
.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width]);

const timeAxis = d3
.axisBottom(timeScale)
.ticks(d3.timeMonth.every(1)) // Monthly ticks
.tickFormat(d3.timeFormat('%b %Y'));

// Multi-format time axis
const formatMillisecond = d3.timeFormat('.%L');
const formatSecond = d3.timeFormat(':%S');
const formatMinute = d3.timeFormat('%I:%M');
const formatHour = d3.timeFormat('%I %p');
const formatDay = d3.timeFormat('%a %d');
const formatWeek = d3.timeFormat('%b %d');
const formatMonth = d3.timeFormat('%B');
const formatYear = d3.timeFormat('%Y');

function multiFormat(date) {
return (
d3.timeSecond(date) < date
? formatMillisecond
: d3.timeMinute(date) < date
? formatSecond
: d3.timeHour(date) < date
? formatMinute
: d3.timeDay(date) < date
? formatHour
: d3.timeMonth(date) < date
? d3.timeWeek(date) < date
? formatDay
: formatWeek
: d3.timeYear(date) < date
? formatMonth
: formatYear
)(date);
}

const timeAxis2 = d3.axisBottom(timeScale).tickFormat(multiFormat);

Legends

Color Legend

function createColorLegend(colorScale, legendContainer, title) {
const legend = legendContainer
.append('g')
.attr('class', 'legend')
.attr('transform', 'translate(20,20)');

// Legend title
legend
.append('text')
.attr('class', 'legend-title')
.attr('x', 0)
.attr('y', -5)
.style('font-weight', 'bold')
.text(title);

// Legend items
const legendItems = legend
.selectAll('.legend-item')
.data(colorScale.domain())
.join('g')
.attr('class', 'legend-item')
.attr('transform', (d, i) => `translate(0, ${i * 20})`);

// Color rectangles
legendItems
.append('rect')
.attr('width', 15)
.attr('height', 15)
.attr('fill', d => colorScale(d));

// Text labels
legendItems
.append('text')
.attr('x', 20)
.attr('y', 12)
.text(d => d);
}

Size Legend

function createSizeLegend(sizeScale, legendContainer) {
const legendData = [
{ value: 10, label: 'Small' },
{ value: 30, label: 'Medium' },
{ value: 50, label: 'Large' },
];

const legend = legendContainer
.append('g')
.attr('class', 'size-legend')
.attr('transform', 'translate(20,20)');

const legendItems = legend
.selectAll('.legend-item')
.data(legendData)
.join('g')
.attr('class', 'legend-item')
.attr('transform', (d, i) => `translate(0, ${i * 40})`);

legendItems
.append('circle')
.attr('r', d => sizeScale(d.value))
.attr('fill', 'steelblue')
.attr('opacity', 0.7);

legendItems
.append('text')
.attr('x', 30)
.attr('y', 5)
.text(d => d.label);
}

Continuous Color Legend

function createContinuousLegend(colorScale, legendContainer) {
const legendWidth = 200;
const legendHeight = 10;

const legend = legendContainer
.append('g')
.attr('class', 'continuous-legend')
.attr('transform', 'translate(20,20)');

// Create gradient
const defs = legendContainer.select('defs').empty()
? legendContainer.append('defs')
: legendContainer.select('defs');

const gradient = defs.append('linearGradient').attr('id', 'legend-gradient');

const numStops = 10;
const domain = colorScale.domain();

gradient
.selectAll('stop')
.data(d3.range(numStops))
.join('stop')
.attr('offset', d => `${(d / (numStops - 1)) * 100}%`)
.attr('stop-color', d => {
const value = domain[0] + ((domain[1] - domain[0]) * d) / (numStops - 1);
return colorScale(value);
});

// Legend rectangle
legend
.append('rect')
.attr('width', legendWidth)
.attr('height', legendHeight)
.style('fill', 'url(#legend-gradient)');

// Legend scale
const legendScale = d3.scaleLinear().domain(domain).range([0, legendWidth]);

const legendAxis = d3.axisBottom(legendScale).ticks(5);

legend
.append('g')
.attr('transform', `translate(0, ${legendHeight})`)
.call(legendAxis);
}

Custom Axis Styling

Styled Axes with CSS

.axis {
font-family: Arial, sans-serif;
font-size: 12px;
}

.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}

.axis text {
fill: #333;
}

.x-axis path {
stroke: #ccc;
}

.y-axis path {
stroke: #ccc;
}

Rotated Axis Labels

// Rotate X-axis labels
svg
.select('.x-axis')
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-45)');

Conditional Axis Formatting

const yAxis = d3.axisLeft(yScale).tickFormat(d => {
if (d >= 1000000) return d3.format('.1s')(d); // 1M, 2M, etc.
if (d >= 1000) return d3.format('.1s')(d); // 1k, 2k, etc.
return d3.format('d')(d); // 1, 2, 3, etc.
});

Updating Axes

Animated Axis Updates

function updateAxis(newScale) {
const newAxis = d3.axisBottom(newScale);

svg.select('.x-axis').transition().duration(750).call(newAxis);
}

Dynamic Tick Count

function responsiveAxis(scale, containerWidth) {
const tickCount = Math.floor(containerWidth / 100); // Rough guideline

return d3.axisBottom(scale).ticks(Math.max(2, tickCount));
}