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));
}