D3.js Scales & Axes Explained — Practical Step-by-Step Guide
Scales map data values (domain) to visual values (range). Axes render reference lines and tick labels. Color scales encode data as colors.
What You’ll Learn
By the end of this tutorial, you will be able to:
- Understand the concept of domain, range, and scale functions
- Create linear, ordinal, band, point, and time scales
- Render labeled axes with
d3.axisBottom(),d3.axisLeft(), and more - Use color scales for sequential, diverging, and categorical data
- Build a complete bar chart with axes from scratch
Why Scales & Axes Matter
Raw data is just numbers — 45, 80, 120. To turn those numbers into a bar chart, you need to decide how wide each bar is and how tall. That’s what scales do. When Durga Antivirus Pro displays a threat severity dashboard, it uses scales to map severity scores (0–100) to bar heights (0–300 pixels). Without scales, every chart would need manual pixel calculations. Axes make those scales readable — they’re the ruler that tells viewers what values the bars represent.
Learning Path
flowchart LR
A["D3.js Basics"] --> B["D3.js SVG"]
B --> C["D3.js Scales & Axes"]
C --> D["D3.js Charts"]
D --> E["D3.js Interactions"]
D --> F["D3.js Data Handling"]
E --> G["D3.js Advanced"]
F --> G
G --> H["D3.js Reference"]
C:::current
classDef current fill:#4CAF50,color:#fff,stroke:#333,stroke-width:2px
What is a Scale?
A scale is a function that converts one value to another. Think of it like a currency converter — you have an amount in dollars (data value) and you want to know how many euros (pixels) it is.
Domain (data values) ─→ Scale Function ─→ Range (pixel values)
[0, 100] [0, 400]
50 ──────────────────────────────→ 200Domain = the input values your data can take (e.g., 0 to 100). Range = the output values you want on screen (e.g., 0 to 400 pixels).
var xScale = d3.scaleLinear()
.domain([0, 100]) // data values from 0 to 100
.range([0, 400]); // pixel values from 0 to 400
xScale(50); // 200 — halfway through both
xScale(0); // 0 — leftmost pixel
xScale(100); // 400 — rightmost pixel
Why not just calculate manually? You could write x = value * 4 for a linear scale. But what about logarithmic scales? Time scales? Ordinal scales (categories)? And what happens when data changes — do you recalculate everything by hand? Scales handle all of this automatically.
When to Use Which Scale
| Data Type | Scale | Use Case | Example |
|---|---|---|---|
| Continuous numbers | scaleLinear | Bar chart heights, line chart y-axes | Revenue over time |
| Categories | scaleBand | Bar chart x-axes | Product names, months |
| Categories (points) | scalePoint | Line chart x-axes | Time labels with gaps |
| Dates | scaleTime | Time-series charts | Stock price over months |
| Orders of magnitude | scaleLog | Wealth distribution, earthquakes | Richter scale, income |
| Circle areas | scaleSqrt | Bubble charts | Population by country |
Linear Scales — The Workhorse
Linear scales are the most common. They map continuous numeric data to a continuous output range.
var scale = d3.scaleLinear()
.domain([0, 100]) // input range (min data value, max data value)
.range([0, 500]) // output range (min pixels, max pixels)
scale(50); // 250
scale.invert(250); // 50 (reverse mapping)
The invert method is gold: It lets you convert screen coordinates back to data values. This is how you make charts interactive — when a user clicks at pixel 250, invert(250) tells you the data value is 50.
Nice Domain — Making Scales Readable
Raw min/max values often produce ugly axis labels like 0, 3.7, 7.4, 11.1. The .nice() method rounds the domain to “nice” human-readable values:
scale.domain(d3.extent(data)); // [1.3, 98.7] — raw
scale.nice(); // [0, 100] — nice!
Bar Chart Y Scale — The Inversion Trick
Remember that SVG y increases downward? This is where it matters:
var yScale = d3.scaleLinear()
.domain([0, d3.max(data)]) // data values: 0 to max
.range([chartHeight, 0]); // SVG pixels: bottom to top
Why [chartHeight, 0] instead of [0, chartHeight]? In SVG, y=0 is the top of the canvas. A data value of 0 should appear at the bottom (y = chartHeight). A data value of max should appear at the top (y = 0). By swapping the range, the scale automatically inverts the y-axis for you.
Ordinal Scales — For Categories
Ordinal scales map discrete categories to discrete output values:
var scale = d3.scaleOrdinal()
.domain(["Apples", "Oranges", "Bananas"])
.range([0, 100, 200]);
scale("Apples"); // 0
scale("Bananas"); // 200
Band Scales — For Bar Charts
Band scales are ordinal scales that also tell you the width of each “band” (bar).
var xScale = d3.scaleBand()
.domain(["A", "B", "C", "D"]) // 4 categories
.range([0, 400]) // total width in pixels
.padding(0.2); // 20% gap between bars
xScale("B"); // 100 — where B's bar starts
xScale.bandwidth(); // 80 — width of each bar
Why bandwidth() matters: Without it, you’d need to calculate bar width manually: totalWidth / numBars * (1 - padding). The scale does this math for you.
Point Scales — For Line Charts
Point scales position items at points (no width), useful for line charts:
var xScale = d3.scalePoint()
.domain(["Jan", "Feb", "Mar", "Apr"])
.range([0, 400])
.padding(0.5);
xScale("Feb"); // pixel position
xScale.step(); // distance between points
Time Scales — For Dates
Time scales work with JavaScript Date objects:
var xScale = d3.scaleTime()
.domain([new Date(2023, 0, 1), new Date(2023, 11, 31)])
.range([0, 800]);
xScale(new Date(2023, 5, 15)); // pixel position for June 15
Why not just use linear with timestamps? You could — dates are just numbers (milliseconds since 1970). But scaleTime gives you nice date-formatted ticks automatically (months, days, hours) instead of raw timestamps.
Logarithmic Scales — For Wide-Ranging Data
When data spans multiple orders of magnitude (1 to 1,000,000), a linear scale compresses small values into invisibility.
var scale = d3.scaleLog()
.domain([1, 1000])
.range([0, 400]);
scale(10); // ~133 — even spacing in log space
scale(100); // ~266
When to use: Wealth distribution, earthquake magnitudes, audio frequencies — any data where the difference between 1→10 is as meaningful as 100→1000.
Axes — Making Scales Readable
Scales calculate positions. Axes draw the labels and tick marks that tell the viewer what those positions mean.
Axis Orientations
| Function | Ticks Appear | Use For |
|---|---|---|
d3.axisBottom(scale) | Below the axis line | X-axis |
d3.axisTop(scale) | Above the axis line | X-axis (inverted) |
d3.axisLeft(scale) | Left of the axis line | Y-axis |
d3.axisRight(scale) | Right of the axis line | Y-axis (secondary) |
Rendering an Axis
// Bottom axis — place at the bottom of the chart
svg.append("g")
.attr("transform", "translate(0, " + chartHeight + ")")
.call(d3.axisBottom(xScale));
// Left axis — place at the left edge
svg.append("g")
.call(d3.axisLeft(yScale));Why .call()? .call(axisGenerator) passes the selection (the <g>) to the axis function, which fills it with tick lines, labels, and the axis path.
Customizing Axes
var axis = d3.axisBottom(xScale)
.ticks(5) // suggest 5 ticks (D3 decides exact count)
.tickFormat(d3.format("$,.0f")) // format as currency: $1,200
.tickSize(6) // length of tick marks
.tickPadding(10); // space between label and tick
.tickValues([10, 30, 50]) // explicit tick positions (overrides automatic)
Formatting Tick Labels
D3 provides d3.format() for numbers and d3.timeFormat() for dates:
d3.format(",d") // integer: 1,234
d3.format("$,.2f") // currency: $1,234.56
d3.format(".0%") // percentage: 50%
d3.format(".2s") // SI prefix: 1.2K (for 1,200)
d3.timeFormat("%Y") // date year: 2023
d3.timeFormat("%b") // month name: Jan
Color Scales — Encoding Data as Color
Color scales add a third dimension to your charts. They’re especially useful when Durga Antivirus Pro shows threat heatmaps — red for critical, yellow for moderate, green for safe.
Sequential (Single Hue)
For continuous data where you want shades of one color:
var color = d3.scaleSequential()
.domain([0, 100])
.interpolator(d3.interpolateBlues);
color(50); // medium blue
Built-in interpolators: interpolateBlues, interpolateGreens, interpolateReds, interpolateGreys, interpolatePurples, interpolateOranges.
Sequential (Multi-Hue)
For more colorful continuous scales:
var color = d3.scaleSequential()
.domain([0, 100])
.interpolator(d3.interpolateViridis);
// Others: interpolatePlasma, interpolateTurbo,
// interpolateRainbow, interpolateCool, interpolateWarm
Viridis tip: Viridis is colorblind-friendly and perceptually uniform. Prefer it over Rainbow, which distorts perception of data.
Diverging — For Data With a Meaningful Midpoint
When data has a neutral midpoint (like zero, or average):
var color = d3.scaleDiverging()
.domain([-50, 0, 50])
.interpolator(d3.interpolateRdBu);
color(-50); // red (negative extreme)
color(0); // white (neutral)
color(50); // blue (positive extreme)
Real-world use: Budget variances (negative = overspend, neutral = on budget, positive = under budget), temperature anomalies, sentiment scores.
Categorical (Ordinal) — For Distinct Categories
var color = d3.scaleOrdinal()
.domain(["A", "B", "C", "D"])
.range(d3.schemeCategory10); // 10 distinct colors
// Other schemes: schemeAccent, schemeDark2, schemePaired,
// schemePastel1, schemePastel2, schemeSet1, schemeSet2, schemeSet3
Complete Example: Bar Chart with Axes
<!DOCTYPE html>
<html>
<head>
<title>Bar Chart with Axes</title>
</head>
<body>
<svg width="500" height="300" id="chart"></svg>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
var data = [
{ name: "Apples", value: 45 },
{ name: "Oranges", value: 80 },
{ name: "Bananas", value: 60 },
{ name: "Grapes", value: 95 },
{ name: "Pears", value: 35 }
];
// Step 1: Define margins (reserve space for axes)
var margin = { top: 20, right: 20, bottom: 40, left: 50 };
var width = 500, height = 300;
var innerWidth = width - margin.left - margin.right;
var innerHeight = height - margin.top - margin.bottom;
// Step 2: Create a group for the chart area (shifted by margins)
var svg = d3.select("#chart")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Step 3: Create scales
var xScale = d3.scaleBand()
.domain(data.map(function(d) { return d.name; }))
.range([0, innerWidth])
.padding(0.3);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return d.value; })])
.range([innerHeight, 0]); // inverted!
// Step 4: Color scale for bars
var color = d3.scaleOrdinal(d3.schemeCategory10);
// Step 5: Draw bars using scales
svg.selectAll("rect")
.data(data).enter().append("rect")
.attr("x", function(d) { return xScale(d.name); })
.attr("y", function(d) { return yScale(d.value); })
.attr("width", xScale.bandwidth())
.attr("height", function(d) { return innerHeight - yScale(d.value); })
.attr("fill", function(d) { return color(d.name); });
// Step 6: Render axes
svg.append("g")
.attr("transform", "translate(0, " + innerHeight + ")")
.call(d3.axisBottom(xScale));
svg.append("g")
.call(d3.axisLeft(yScale));
</script>
</body>
</html>How the margin convention works: The outer SVG is 500×300. We shift the content group by (margin.left, margin.top) = (50, 20). The inner chart area is 410×240. The bottom axis goes at y = innerHeight = 240. This ensures axes don’t get clipped.
Common Mistakes
1. Forgetting to Invert the Y-Scale Range
// WRONG — bars start at top, extend downward
d3.scaleLinear().domain([0, 100]).range([0, chartHeight]);
// RIGHT — bars start at bottom, extend upward
d3.scaleLinear().domain([0, 100]).range([chartHeight, 0]);When your bar chart appears upside down or bars extend from the top, check your y-scale range order.
2. Swapping Domain and Range
Domain = your data values (e.g., [0, 100]). Range = pixel values (e.g., [0, 400]). Beginners frequently swap them. Remember: domain describes what you have, range describes what you want.
3. Creating an Axis Generator but Not Rendering It
// This does nothing:
d3.axisBottom(xScale);
// This renders:
svg.append("g").call(d3.axisBottom(xScale));Axis generators are functions that need to be called on a DOM element via .call().
4. Not Accounting for Margins in Axis Position
If you don’t reserve margin space, the bottom axis (labels, ticks) gets clipped outside the SVG boundary. Always use the margin convention.
5. Using Discrete Colors for Continuous Data
// WRONG — ordinal for continuous data
d3.scaleOrdinal().domain([0, 100]).range(d3.schemeCategory10);
// RIGHT — sequential for continuous
d3.scaleSequential().domain([0, 100]).interpolator(d3.interpolateViridis);Ordinal scales treat each value as a distinct category. For continuous data, this creates misleading visual banding.
6. Setting Tick Count on Small Charts
axis.ticks(10) is a suggestion, not a command. On a 300px chart, D3 may show fewer ticks to avoid overlap. Use tickValues() for exact control.
Practice Questions
Question 1
What is the difference between scaleBand and scalePoint?
Answer: scaleBand divides the range into bands with a calculable width (via bandwidth()), ideal for bar charts. scalePoint maps discrete values to points with a step distance, ideal for line charts where you want evenly spaced markers.
Question 2
Why must the y-scale range be [chartHeight, 0] instead of [0, chartHeight]?
Answer: SVG’s y-axis increases downward. The data value 0 should appear at the bottom of the chart (y = chartHeight), and the maximum value should appear at the top (y = 0). Inverting the range achieves this mapping automatically.
Question 3
How would you format axis labels as percentages?
Answer: Use d3.axisBottom(xScale).tickFormat(d3.format(".0%")). This multiplies the value by 100 and appends a % sign.
Question 4
When should you use scaleLog instead of scaleLinear?
Answer: When data spans multiple orders of magnitude (e.g., 1 to 1,000,000) and the relative difference matters more than the absolute difference. Examples: population density, earthquake magnitudes, wealth distribution.
Question 5
What happens if domain and range have different lengths in scaleOrdinal?
Answer: The domain values cycle through the range. If you have 5 domain values but only 3 range values, the 4th domain value gets the 1st range value, the 5th gets the 2nd, and so on.
Challenge
Create a dual-axis chart with revenue (bar chart) and profit margin (line chart) over 6 months. Use scaleBand for the x-axis, scaleLinear for the y-axes, and render both axisLeft and axisRight. The revenue bars should use one y-scale and the profit line should use another.
FAQ
Try It Yourself
Copy this complete example into an HTML file and open it in Doda Browser:
<!DOCTYPE html>
<html>
<head>
<title>Multi-Scale Chart — Try It Yourself</title>
</head>
<body>
<svg width="600" height="350" id="chart"></svg>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
var data = [
{ date: new Date(2024, 0, 1), revenue: 12000, profit: 2400 },
{ date: new Date(2024, 1, 1), revenue: 15000, profit: 3200 },
{ date: new Date(2024, 2, 1), revenue: 13500, profit: 2800 },
{ date: new Date(2024, 3, 1), revenue: 17000, profit: 4000 },
{ date: new Date(2024, 4, 1), revenue: 16000, profit: 3800 },
{ date: new Date(2024, 5, 1), revenue: 19000, profit: 4500 }
];
var margin = { top: 30, right: 60, bottom: 40, left: 60 };
var width = 600, height = 350;
var innerW = width - margin.left - margin.right;
var innerH = height - margin.top - margin.bottom;
var svg = d3.select("#chart").append("g")
.attr("transform", "translate("+margin.left+","+margin.top+")");
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) { return d.date; }))
.range([0, innerW]);
var yRev = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return d.revenue; })])
.range([innerH, 0]);
var yProfit = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return d.profit; })])
.range([innerH, 0]);
var revLine = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return yRev(d.revenue); });
var profitLine = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return yProfit(d.profit); });
svg.append("path").datum(data).attr("d", revLine)
.attr("fill", "none").attr("stroke", "steelblue").attr("stroke-width", 2);
svg.append("path").datum(data).attr("d", profitLine)
.attr("fill", "none").attr("stroke", "orange").attr("stroke-width", 2);
svg.append("g").attr("transform", "translate(0,"+innerH+")")
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%b")));
svg.append("g").call(d3.axisLeft(yRev));
svg.append("g").attr("transform", "translate("+innerW+",0)")
.call(d3.axisRight(yProfit));
</script>
</body>
</html>Try this: Change the data values and watch the axes re-scale automatically. Add more data points and see how the time scale adjusts.
What’s Next
Now you can map data to pixels and label your charts:
| Tutorial | What You’ll Learn |
|---|---|
| D3.js Charts | Build bar, pie, line, and scatter charts |
| D3.js Data Handling | Load CSV, JSON, and aggregate data |
| D3.js Interactions | Add tooltips, transitions, and zoom |
Related topics: JavaScript, SVG, Data Visualization, CSS, HTML
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — bringing secure, high-performance software to your digital life.
What’s Next
Congratulations on completing this D3Js Scales tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro