D3.js and React Integration: Scales and Axes
D3 scales are functions that map data values to visual properties. A linear scale transforms numbers (e.g., 0–10000 revenue) into pixels (e.g., 0–400px width). D3 also provides time scales (for dates), categorical scales (for labels), and more exotic variants. When paired with React, D3 scales give you fine-grained control over chart appearance without sacrificing simplicity. This article teaches you to use D3 scales, build axes, and integrate them into React components.
What Are D3 Scales and Why Use Them?
A D3 scale is a function that translates a domain (data values) to a range (visual properties, usually pixels). Example:
import { scaleLinear } from "d3-scale";
const scale = scaleLinear()
.domain([0, 10000]) // data extent
.range([0, 400]); // pixel extent
console.log(scale(0)); // 0
console.log(scale(5000)); // 200
console.log(scale(10000)); // 400
D3 scales solve several problems: they interpolate values between domain and range, handle edge cases (values outside the domain), and work with diverse data types (numbers, dates, strings). Writing this logic manually is tedious and error-prone. D3 scales are battle-tested and used in production visualizations across the industry.
Common Scale Types
Linear scale: Maps continuous numbers to continuous numbers. Best for amounts, percentages, durations.
const revenue = scaleLinear()
.domain([0, 100000])
.range([0, 600]);
console.log(revenue(50000)); // 300
Time scale: Maps dates to pixels. Critical for time-series charts.
import { scaleTime } from "d3-scale";
const dateScale = scaleTime()
.domain([new Date("2026-01-01"), new Date("2026-12-31")])
.range([0, 600]);
console.log(dateScale(new Date("2026-06-15"))); // ~297 (halfway)
Band scale: Maps categorical values (labels) to bands of equal width. Ideal for bar charts.
import { scaleBand } from "d3-scale";
const categories = scaleBand()
.domain(["Q1", "Q2", "Q3", "Q4"])
.range([0, 400])
.padding(0.2); // 20% padding between bars
console.log(categories("Q1")); // 0
console.log(categories.bandwidth()); // ~95 (width of one bar)
console.log(categories("Q2")); // ~110
Ordinal scale: Maps any discrete values to discrete outputs. Less common; band is usually better.
| Scale | Domain | Range | Use Case |
|---|---|---|---|
| Linear | Numbers | Numbers | Amounts, scores |
| Time | Dates | Numbers | Time series |
| Band | Categories | Numbers | Bar charts |
| Log | Numbers | Numbers | Large ranges (1–1M) |
| Power | Numbers | Numbers | Exponential data |
| Quantize | Numbers | Discrete | Binned colors |
Building a D3-Powered Bar Chart with React
Here's a complete bar chart using D3 scales and React rendering:
import React from "react";
import { scaleBand, scaleLinear } from "d3-scale";
const BarChartD3 = ({ data }) => {
const margin = { top: 20, right: 20, bottom: 40, left: 60 };
const width = 600;
const height = 300;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Scales
const xScale = scaleBand()
.domain(data.map((d) => d.label))
.range([0, innerWidth])
.padding(0.1);
const yScale = scaleLinear()
.domain([0, Math.max(...data.map((d) => d.value))])
.range([innerHeight, 0]); // inverted: top = 0, bottom = innerHeight
return (
<svg width={width} height={height} style={{ border: "1px solid #ddd" }}>
{/* Plot area group */}
<g transform={`translate(${margin.left}, ${margin.top})`}>
{/* Grid lines */}
{yScale.ticks(5).map((tick, i) => (
<line
key={`grid-${i}`}
x1="0"
y1={yScale(tick)}
x2={innerWidth}
y2={yScale(tick)}
stroke="#e0e0e0"
strokeDasharray="4"
/>
))}
{/* Bars */}
{data.map((d, i) => (
<rect
key={i}
x={xScale(d.label)}
y={yScale(d.value)}
width={xScale.bandwidth()}
height={innerHeight - yScale(d.value)}
fill="steelblue"
opacity="0.8"
/>
))}
</g>
{/* X axis */}
<g transform={`translate(${margin.left}, ${height - margin.bottom})`}>
<line x1="0" y1="0" x2={innerWidth} y2="0" stroke="black" />
{xScale.domain().map((label, i) => (
<g key={i} transform={`translate(${xScale(label) + xScale.bandwidth() / 2}, 0)`}>
<line y1="0" y2="5" stroke="black" />
<text y="15" textAnchor="middle" fontSize="12">
{label}
</text>
</g>
))}
</g>
{/* Y axis */}
<g transform={`translate(${margin.left}, ${margin.top})`}>
<line x1="0" y1="0" x2="0" y2={innerHeight} stroke="black" />
{yScale.ticks(5).map((tick, i) => (
<g key={i} transform={`translate(0, ${yScale(tick)})`}>
<line x1="-5" x2="0" stroke="black" />
<text x="-10" textAnchor="end" fontSize="12" dy="0.32em">
{tick}
</text>
</g>
))}
</g>
</svg>
);
};
export default BarChartD3;
This chart is built entirely from D3 scales and React SVG. The scales handle all coordinate transformations; React renders the SVG.
Building Axes as Reusable Components
The axis code is repetitive. Extract it into reusable components:
// XAxis component for categorical data
const XAxisBand = ({ scale, innerHeight, margin }) => {
const height = margin.bottom;
return (
<g transform={`translate(${margin.left}, ${margin.top + innerHeight})`}>
<line x1="0" y1="0" x2={scale.range()[1]} y2="0" stroke="black" />
{scale.domain().map((label, i) => (
<g key={i} transform={`translate(${scale(label) + scale.bandwidth() / 2}, 0)`}>
<line y1="0" y2="5" stroke="black" />
<text y="20" textAnchor="middle" fontSize="12">
{label}
</text>
</g>
))}
<text
y={height - 5}
x={scale.range()[1] / 2}
textAnchor="middle"
fontSize="14"
fontWeight="bold"
>
Categories
</text>
</g>
);
};
// YAxis component for numeric data
const YAxisLinear = ({ scale, innerWidth, margin }) => {
const width = margin.left;
return (
<g transform={`translate(${margin.left}, ${margin.top})`}>
<line x1="0" y1="0" x2="0" y2={scale.range()[0]} stroke="black" />
{scale.ticks(5).map((tick, i) => (
<g key={i} transform={`translate(0, ${scale(tick)})`}>
<line x1="-5" x2="0" stroke="black" />
<text x="-10" textAnchor="end" fontSize="12" dy="0.32em">
{tick.toLocaleString()}
</text>
</g>
))}
<text
x={-scale.range()[0] / 2}
y={-width + 20}
textAnchor="middle"
fontSize="14"
fontWeight="bold"
transform="rotate(-90)"
>
Values
</text>
</g>
);
};
// Use in chart
export function AxisedChart({ data }) {
const margin = { top: 20, right: 20, bottom: 60, left: 60 };
const width = 600;
const height = 350;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = scaleBand()
.domain(data.map((d) => d.label))
.range([0, innerWidth])
.padding(0.1);
const yScale = scaleLinear()
.domain([0, Math.max(...data.map((d) => d.value))])
.range([innerHeight, 0]);
return (
<svg width={width} height={height}>
<g transform={`translate(${margin.left}, ${margin.top})`}>
{data.map((d, i) => (
<rect
key={i}
x={xScale(d.label)}
y={yScale(d.value)}
width={xScale.bandwidth()}
height={innerHeight - yScale(d.value)}
fill="teal"
/>
))}
</g>
<XAxisBand scale={xScale} innerHeight={innerHeight} margin={margin} />
<YAxisLinear scale={yScale} innerWidth={innerWidth} margin={margin} />
</svg>
);
}
Now axes are reusable components you can drop into any chart.
Time-Series Charts with scaleTime
Time series are common in dashboards. scaleTime handles date objects intelligently:
import { scaleTime } from "d3-scale";
const data = [
{ date: new Date("2026-01-01"), value: 100 },
{ date: new Date("2026-02-01"), value: 150 },
{ date: new Date("2026-03-01"), value: 120 }
];
const xScale = scaleTime()
.domain([data[0].date, data[data.length - 1].date])
.range([0, 600]);
const yScale = scaleLinear()
.domain([0, 200])
.range([300, 0]);
export function TimeSeries({ data }) {
return (
<svg width="700" height="350">
<g transform="translate(50, 20)">
{data.map((d, i) => (
<circle
key={i}
cx={xScale(d.date)}
cy={yScale(d.value)}
r="5"
fill="steelblue"
/>
))}
{/* Line path */}
<path
d={`M ${data.map((d) => `${xScale(d.date)},${yScale(d.value)}`).join(" L ")}`}
stroke="steelblue"
fill="none"
strokeWidth="2"
/>
</g>
{/* X axis with date labels */}
<g transform="translate(50, 320)">
{xScale.ticks(5).map((tick, i) => (
<g key={i} transform={`translate(${xScale(tick)}, 0)`}>
<line y1="0" y2="5" stroke="black" />
<text y="20" textAnchor="middle" fontSize="12">
{tick.toLocaleDateString()}
</text>
</g>
))}
</g>
</svg>
);
}
scaleTime().ticks() intelligently chooses tick positions (every day, week, month, year) based on the zoom level.
Key Takeaways
- D3 scales are pure functions mapping data domains to visual ranges, solving interpolation and edge cases.
- Linear scales for continuous numbers; time scales for dates; band scales for categories.
- Use
scale.ticks()to compute axis tick positions automatically. - Extract axis rendering into reusable components to keep charts maintainable.
- D3 scales work perfectly with React rendering; they're entirely data-driven.
Frequently Asked Questions
Can I use D3 scales without D3 itself?
Yes. d3-scale is a standalone module. You can install only the scales package: npm install d3-scale. No need for the full D3 library (which is 200 KB+).
How do I invert a scale (e.g., for color gradients)?
Use .range() with reversed values: scale.range([400, 0]) instead of [0, 400]. Or use .invert(pixel) to reverse-map a pixel back to a data value.
What's the difference between scale.nice() and scale.ticks()?
ticks() returns an array of recommended tick positions (e.g., [0, 25, 50, 75, 100]). nice() extends the domain slightly so ticks align nicely (e.g., domain [0, 99] becomes [0, 100]). Use both together for clean axes.
How do I handle negative numbers in a linear scale?
Pass negative values in the domain: scaleLinear().domain([-100, 100]).range([0, 400]). The scale handles sign changes transparently.
Can I use ordinal scales instead of band scales?
ordinal scales work but don't compute bar widths (bandwidth()). band scales are ordinal scales with bandwidth semantics, making them better for bar charts. Use band unless you specifically need ordinal behavior.