Skip to content
D3.js Scales & Axes Explained — Practical Step-by-Step Guide

D3.js Scales & Axes Explained — Practical Step-by-Step Guide

DodaTech Updated Jun 6, 2026 13 min read

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
  
Prerequisites: Complete the D3.js SVG tutorial. You should understand SVG shapes and the coordinate system before learning scales.

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 ──────────────────────────────→ 200

Domain = 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 TypeScaleUse CaseExample
Continuous numbersscaleLinearBar chart heights, line chart y-axesRevenue over time
CategoriesscaleBandBar chart x-axesProduct names, months
Categories (points)scalePointLine chart x-axesTime labels with gaps
DatesscaleTimeTime-series chartsStock price over months
Orders of magnitudescaleLogWealth distribution, earthquakesRichter scale, income
Circle areasscaleSqrtBubble chartsPopulation 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

FunctionTicks AppearUse For
d3.axisBottom(scale)Below the axis lineX-axis
d3.axisTop(scale)Above the axis lineX-axis (inverted)
d3.axisLeft(scale)Left of the axis lineY-axis
d3.axisRight(scale)Right of the axis lineY-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

What is the difference between scaleBand and scalePoint?
scaleBand divides the range into bands (bars) with a width. scalePoint maps discrete values to points (for scatter/line charts) with a step distance.
How do I add units to axis labels?
Use tickFormat: d3.axisBottom(xScale).tickFormat(function(d) { return d + "%"; }).
Can I have multiple y-axes?
Yes, create two scales with different domains, render axisLeft and axisRight. This is common when comparing datasets with different units.
What does d3.schemeCategory10 contain?
10 distinct, colorblind-friendly colors: #1f77b4, #ff7f0e, #2ca02c, #d62728, #9467bd, #8c564b, #e377c2, #7f7f7f, #bcbd22, #17becf.
How do I make a diverging color scale?
Use d3.scaleDiverging() with a three-element domain [-max, 0, max] and an interpolator like d3.interpolateRdBu or d3.interpolatePiYG.

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:

TutorialWhat You’ll Learn
D3.js ChartsBuild bar, pie, line, and scatter charts
D3.js Data HandlingLoad CSV, JSON, and aggregate data
D3.js InteractionsAdd 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