Skip to content
D3.js Charts — Practical Guide to Bar, Pie, Line & Scatter Plots

D3.js Charts — Practical Guide to Bar, Pie, Line & Scatter Plots

DodaTech Updated Jun 6, 2026 13 min read

Building charts from scratch with D3.js gives you total control over appearance, interactivity, and animation.

What You’ll Learn

By the end of this tutorial, you will be able to:

  • Build vertical and horizontal bar charts with axes
  • Create stacked and grouped bar charts
  • Design pie and donut charts with labels
  • Draw line charts with multiple series and smooth curves
  • Build scatter plots with color and size encoding
  • Add chart titles, axis labels, and legends

Why Building Charts from Scratch Matters

Pre-built chart libraries (Chart.js, Highcharts) are great for standard dashboards. But Doda Browser’s performance profiler and Durga Antivirus Pro’s threat timeline need custom visualizations — animated real-time bars, hover-linked scatter plots, precisely styled legends that match the product’s design system. When you build charts from scratch with D3.js, you own every pixel. No library constraint will stop you from implementing the exact visualization your users need.

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"]
    D:::current
    
    classDef current fill:#4CAF50,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: You should understand scales and axes from the Scales & Axes tutorial, and SVG basics from the D3.js SVG tutorial.

Chart Building Blocks — The Margin Convention

Every chart in D3.js uses the same pattern. Once you understand this, all chart types are variations on the same theme:

// Step 1: Define margins (reserve space for axes and labels)
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: Group for the chart area
var svg = d3.select("#chart")
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

Why margins? Without them, axes and labels get clipped at the SVG boundary. The margin convention shifts the entire chart inward, giving you space for axis labels, tick marks, and a chart title.

Vertical Bar Chart — The Classic

<!DOCTYPE html>
<html>
<head>
    <title>Vertical Bar Chart</title>
</head>
<body>
    <svg width="500" height="300" id="chart"></svg>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script>
        var data = [
            { label: "A", value: 45 },
            { label: "B", value: 80 },
            { label: "C", value: 60 },
            { label: "D", value: 95 },
            { label: "E", value: 35 }
        ];

        var margin = { top: 20, right: 20, bottom: 40, left: 50 };
        var w = 500 - margin.left - margin.right;
        var h = 300 - margin.top - margin.bottom;

        var svg = d3.select("#chart")
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        var x = d3.scaleBand()
            .domain(data.map(function(d) { return d.label; }))
            .range([0, w]).padding(0.3);

        var y = d3.scaleLinear()
            .domain([0, d3.max(data, function(d) { return d.value; })])
            .range([h, 0]);  // inverted!

        svg.selectAll("rect").data(data).enter().append("rect")
            .attr("x", function(d) { return x(d.label); })
            .attr("y", function(d) { return y(d.value); })
            .attr("width", x.bandwidth())
            .attr("height", function(d) { return h - y(d.value); })
            .attr("fill", "steelblue");

        svg.append("g")
            .attr("transform", "translate(0," + h + ")")
            .call(d3.axisBottom(x));
        svg.append("g").call(d3.axisLeft(y));
    </script>
</body>
</html>

Line by line explanation:

  1. scaleBand() maps categories to horizontal positions — each label gets a “band” of pixels.
  2. scaleLinear() with range([h, 0]) inverts the y-axis — high values go to the top.
  3. x.bandwidth() gives the computed width of each bar (accounting for padding).
  4. h - y(d.value) calculates bar height because SVG draws from top to bottom.

Horizontal Bar Chart

Horizontal bars are just vertical bars with swapped scales:

// x is now the value axis, y is the category axis
var y = d3.scaleBand()
    .domain(data.map(function(d) { return d.label; }))
    .range([0, h]).padding(0.3);

var x = d3.scaleLinear()
    .domain([0, d3.max(data, function(d) { return d.value; })])
    .range([0, w]);

svg.selectAll("rect").data(data).enter().append("rect")
    .attr("x", 0)
    .attr("y", function(d) { return y(d.label); })
    .attr("width", function(d) { return x(d.value); })
    .attr("height", y.bandwidth())
    .attr("fill", "steelblue");

svg.append("g").call(d3.axisLeft(y));
svg.append("g").attr("transform", "translate(0," + h + ")").call(d3.axisBottom(x));

When to use horizontal bars: When category labels are long (they’re easier to read horizontally), or when you have many categories (horizontal charts scroll better).

Stacked Bar Chart

Stacked bars show part-to-whole relationships across categories:

var data = [
    { month: "Jan", apples: 30, oranges: 20, bananas: 15 },
    { month: "Feb", apples: 45, oranges: 25, bananas: 20 },
    { month: "Mar", apples: 35, oranges: 30, bananas: 25 }
];

// d3.stack() transforms the data: each series gets [y0, y1] for each category
var series = d3.stack()
    .keys(["apples", "oranges", "bananas"])
    .offset(d3.stackOffsetNone)(data);

var x = d3.scaleBand()
    .domain(data.map(function(d) { return d.month; }))
    .range([0, w]).padding(0.2);

var y = d3.scaleLinear()
    .domain([0, d3.max(series, function(s) {
        return d3.max(s, function(d) { return d[1]; });
    })])
    .range([h, 0]);

var color = d3.scaleOrdinal()
    .domain(["apples", "oranges", "bananas"])
    .range(["#e6194b", "#ffe119", "#3cb44b"]);

// For each series (apples, oranges, bananas), draw rects
svg.selectAll("g.layer")
    .data(series).enter().append("g")
    .attr("fill", function(d) { return color(d.key); })
    .selectAll("rect")
    .data(function(d) { return d; })
    .enter().append("rect")
    .attr("x", function(d) { return x(d.data.month); })
    .attr("y", function(d) { return y(d[1]); })           // top of segment
    .attr("height", function(d) { return y(d[0]) - y(d[1]); }) // height
    .attr("width", x.bandwidth());

How d3.stack() works: It takes your data table and computes [y0, y1] for every segment. y0 is the bottom of the segment, y1 is the top. The first series (apples) has y0 = 0, the second (oranges) has y0 = apples_value, and so on.

Pie & Donut Charts

Pie charts show proportions. Donut charts are pie charts with a hole (and are generally preferred because the center can display a total value).

var data = [
    { label: "Apples", value: 45 },
    { label: "Oranges", value: 30 },
    { label: "Bananas", value: 20 },
    { label: "Grapes", value: 15 }
];

// Pie generator: computes start/end angles for each slice
var pie = d3.pie()
    .value(function(d) { return d.value; })
    .sort(null);  // keep input order (don't sort by value)

// Arc generator: renders arcs from angles
var arc = d3.arc()
    .innerRadius(0)         // 0 = pie, >0 = donut
    .outerRadius(100);

var labelArc = d3.arc()
    .innerRadius(130).outerRadius(130);  // for label positioning

var color = d3.scaleOrdinal(d3.schemeCategory10);

var svg = d3.select("#chart").append("g")
    .attr("transform", "translate(200, 150)");  // center of pie

var arcs = svg.selectAll("path")
    .data(pie(data)).enter().append("g").attr("class", "arc");

arcs.append("path")
    .attr("d", arc)
    .attr("fill", function(d) { return color(d.data.label); });

arcs.append("text")
    .attr("transform", function(d) { return "translate(" + labelArc.centroid(d) + ")"; })
    .attr("text-anchor", "middle")
    .text(function(d) { return d.data.label; });

Donut Chart

Change just one line:

var arc = d3.arc()
    .innerRadius(60)     // > 0 creates the donut hole
    .outerRadius(100);

Why donuts over pies: The center area can show a total or summary. Also, humans are better at comparing arc lengths (donut) than angles (pie).

Line Chart (Multiple Series)

Line charts show trends over time. With multiple series, you can compare trends side by side:

var data = [
    { date: new Date(2024,0,1), sales: 100, cost: 70 },
    { date: new Date(2024,1,1), sales: 130, cost: 85 },
    { date: new Date(2024,2,1), sales: 120, cost: 80 },
    { date: new Date(2024,3,1), sales: 160, cost: 95 }
];

var x = d3.scaleTime()
    .domain(d3.extent(data, function(d) { return d.date; }))
    .range([0, w]);

var y = d3.scaleLinear()
    .domain([0, d3.max(data, function(d) { return Math.max(d.sales, d.cost); })])
    .range([h, 0]);

// Create two line generators — one per series
var salesLine = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.sales); });

var costLine = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.cost); });

// Draw the lines
svg.append("path").datum(data).attr("d", salesLine)
    .attr("fill", "none").attr("stroke", "steelblue").attr("stroke-width", 2);

svg.append("path").datum(data).attr("d", costLine)
    .attr("fill", "none").attr("stroke", "tomato").attr("stroke-width", 2);

// Add dots at each data point
svg.selectAll(".sales-dot").data(data).enter().append("circle")
    .attr("cx", function(d) { return x(d.date); })
    .attr("cy", function(d) { return y(d.sales); })
    .attr("r", 4).attr("fill", "steelblue");

svg.selectAll(".cost-dot").data(data).enter().append("circle")
    .attr("cx", function(d) { return x(d.date); })
    .attr("cy", function(d) { return y(d.cost); })
    .attr("r", 4).attr("fill", "tomato");

Smooth Curves

Make lines smoother by adding a curve type:

var line = d3.line()
    .curve(d3.curveMonotoneX);  // smooth, no overshooting
Curve TypeAppearanceUse When
curveLinearStraight segments (default)Precise data points
curveMonotoneXSmooth, no overshootTime series
curveBasisBezier curvesSmooth flow
curveStepStep functionDiscrete changes
curveNaturalNatural cubic splineSmooth interpolation

Scatter Plot

Scatter plots reveal relationships between two variables:

var data = d3.range(50).map(function() {
    return {
        x: Math.random() * 100,
        y: Math.random() * 100,
        size: Math.random() * 20 + 5
    };
});

var x = d3.scaleLinear().domain([0, 100]).range([0, w]);
var y = d3.scaleLinear().domain([0, 100]).range([h, 0]);
var color = d3.scaleSequential(d3.interpolateViridis)
    .domain([0, 100]);

svg.selectAll("circle").data(data).enter().append("circle")
    .attr("cx", function(d) { return x(d.x); })
    .attr("cy", function(d) { return y(d.y); })
    .attr("r", function(d) { return d.size / 5; })
    .attr("fill", function(d) { return color(d.x); })
    .attr("opacity", 0.7);

Three visual dimensions: x position, y position, and color — all encoded from data. You could add a fourth (size) using the r attribute.

Chart Labels & Titles

A chart without labels is meaningless. Here’s how to add them:

// Chart title (centered above the chart)
svg.append("text")
    .attr("x", w / 2)
    .attr("y", -10)
    .attr("text-anchor", "middle")
    .style("font-size", "16px")
    .style("font-weight", "bold")
    .text("Monthly Sales Report");

// X-axis label
svg.append("text")
    .attr("x", w / 2)
    .attr("y", h + 35)
    .attr("text-anchor", "middle")
    .text("Month");

// Y-axis label (rotated 90 degrees)
svg.append("text")
    .attr("x", -h / 2)
    .attr("y", -40)
    .attr("text-anchor", "middle")
    .attr("transform", "rotate(-90)")
    .text("Revenue ($)");

Common Mistakes

1. Pie Chart Slices in Reversed Order

By default, d3.pie() sorts slices by value (largest first). This can make the order unpredictable. Use .sort(null) to preserve input order.

2. Forgetting to Invert the Y-Axis in Bar Charts

scaleLinear().range([h, 0]) — if you write [0, h], bars will extend downward from the top of the chart. This is the single most common D3.js mistake.

3. Labels Overlapping on Pie Charts

Small slices produce tiny arcs where labels don’t fit. Solutions: omit labels for small slices (if (d.value < threshold) return ""), use leader lines, or place labels outside the pie.

4. Using .data() Instead of .datum() for Path Generators

// WRONG — creates N paths with 1 point each
svg.selectAll("path").data([points]).enter().append("path").attr("d", line);

// RIGHT — creates 1 path with all points
svg.datum(points).append("path").attr("d", line);

5. Not Reserving Margin Space

If you place axis labels at y = h + 35 but didn’t set margin.bottom = 40, the label gets clipped by the SVG boundary. Always use the margin convention.

6. Stacked Bar Data Order

In d3.stack(), the order of .keys() determines the stacking order. Changing the key order changes which series appears at the bottom vs top. Be intentional.

Practice Questions

Question 1

Why does changing innerRadius from 0 to 60 transform a pie chart into a donut chart?

Answer: The innerRadius defines the radius of the hole in the center. A value of 0 means no hole (pie). A value > 0 creates a donut hole. The arc generator draws both the inner and outer edges of each slice.

Question 2

What does d3.stack() return, and why is it different from the original data?

Answer: d3.stack() returns an array of series, where each series contains arrays of [y0, y1] values. y0 is the bottom of each segment and y1 is the top. This stacked format is what allows bars to pile on top of each other.

Question 3

How do you create a horizontal bar chart?

Answer: Swap the x and y scales. Use scaleBand() for the y-axis (categories) and scaleLinear() for the x-axis (values). Bars start at x=0 and extend rightward to x(value).

Question 4

When should you add dots to a line chart?

Answer: Add dots when data points are sparse (fewer than ~20) to make individual values readable. For dense data (100+ points), dots create visual clutter and lines alone are cleaner.

Question 5

How would you make a scatter plot where circle size represents a third variable?

Answer: Use d3.scaleSqrt() (or scaleLinear) to map the third variable to radius values: .attr("r", function(d) { return sizeScale(d.thirdVar); }). Use scaleSqrt for areas (circle sizes should be proportional to area, not radius).

Challenge

Build a dashboard with three charts on one page: a bar chart showing monthly revenue, a donut chart showing product category breakdown, and a line chart showing the revenue trend. Use CSS grid or flexbox to arrange them. Bonus: add a hover effect that highlights matching data across charts.

FAQ

How do I add tooltips to D3 charts?
Append a <div> tooltip, show/hide it on mouseover/mouseout, and position it with the mouse event coordinates using d3.pointer(event).
What is the best way to animate chart transitions?
Use .transition().duration(750) on the entering elements. D3 interpolates attribute values automatically.
How do I handle responsive charts?
Use the viewBox attribute on the SVG: .attr("viewBox", "0 0 500 300").attr("preserveAspectRatio", "xMidYMid meet") and set width/height to 100%.
Can I add a legend automatically?
D3 has no built-in legend. Create one manually by appending colored rectangles and text labels in a corner of the chart.
What is d3.stack() used for?
d3.stack() transforms a matrix of values into stacked series for stacked bar charts or streamgraphs. Each series has [y0, y1] for the bottom and top of each segment.

Try It Yourself

Here’s a mini dashboard with bar, pie, and line charts all in one page:

<!DOCTYPE html>
<html>
<head>
    <title>Mini Dashboard — Try It Yourself</title>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        .dashboard { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: 900px; }
        .card { border: 1px solid #ddd; border-radius: 8px; padding: 15px; }
        .card h3 { margin: 0 0 10px 0; font-size: 14px; color: #666; }
        svg { width: 100%; height: auto; }
    </style>
</head>
<body>
    <h2>Sales Dashboard</h2>
    <div class="dashboard">
        <div class="card">
            <h3>Monthly Revenue</h3>
            <svg viewBox="0 0 400 200" id="barChart"></svg>
        </div>
        <div class="card">
            <h3>Product Mix</h3>
            <svg viewBox="0 0 400 200" id="pieChart"></svg>
        </div>
        <div class="card" style="grid-column: 1 / -1;">
            <h3>Revenue Trend</h3>
            <svg viewBox="0 0 800 200" id="lineChart"></svg>
        </div>
    </div>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script>
        var months = ["Jan","Feb","Mar","Apr","May","Jun"];
        var revenue = [12, 19, 15, 22, 18, 25];
        var products = [
            { label: "Widgets", value: 40 },
            { label: "Gadgets", value: 30 },
            { label: "Doohickeys", value: 20 },
            { label: "Thingamajigs", value: 10 }
        ];
        var trend = months.map(function(m, i) { return { month: m, value: revenue[i] }; });

        function barChart(selector, data, labels) {
            var svg = d3.select(selector), w = 400, h = 200, m = { t: 10, r: 10, b: 30, l: 30 };
            var inner = svg.append("g").attr("transform", "translate("+m.l+","+m.t+")");
            var iw = w-m.l-m.r, ih = h-m.t-m.b;
            var x = d3.scaleBand().domain(labels).range([0, iw]).padding(0.3);
            var y = d3.scaleLinear().domain([0, d3.max(data)]).range([ih, 0]);
            inner.selectAll("rect").data(data).enter().append("rect")
                .attr("x", function(d,i) { return x(labels[i]); })
                .attr("y", function(d) { return y(d); })
                .attr("width", x.bandwidth())
                .attr("height", function(d) { return ih - y(d); })
                .attr("fill", "steelblue");
            inner.append("g").attr("transform", "translate(0,"+ih+")").call(d3.axisBottom(x));
            inner.append("g").call(d3.axisLeft(y).ticks(5));
        }

        function pieChart(selector, data) {
            var svg = d3.select(selector).append("g").attr("transform", "translate(150, 100)");
            var pie = d3.pie().value(function(d) { return d.value; }).sort(null)(data);
            var arc = d3.arc().innerRadius(30).outerRadius(80);
            var color = d3.scaleOrdinal(d3.schemeSet3);
            svg.selectAll("path").data(pie).enter().append("path")
                .attr("d", arc).attr("fill", function(d) { return color(d.data.label); });
        }

        function lineChart(selector, data) {
            var svg = d3.select(selector), w = 800, h = 200, m = { t: 10, r: 10, b: 30, l: 40 };
            var inner = svg.append("g").attr("transform", "translate("+m.l+","+m.t+")");
            var iw = w-m.l-m.r, ih = h-m.t-m.b;
            var x = d3.scalePoint().domain(data.map(function(d) { return d.month; })).range([0, iw]);
            var y = d3.scaleLinear().domain([0, d3.max(data, function(d) { return d.value; })]).range([ih, 0]);
            var line = d3.line().x(function(d) { return x(d.month); }).y(function(d) { return y(d.value); });
            inner.append("path").datum(data).attr("d", line)
                .attr("fill", "none").attr("stroke", "#e91e63").attr("stroke-width", 2);
            inner.selectAll("circle").data(data).enter().append("circle")
                .attr("cx", function(d) { return x(d.month); })
                .attr("cy", function(d) { return y(d.value); })
                .attr("r", 4).attr("fill", "#e91e63");
            inner.append("g").attr("transform", "translate(0,"+ih+")").call(d3.axisBottom(x));
            inner.append("g").call(d3.axisLeft(y).ticks(5));
        }

        barChart("#barChart", revenue, months);
        pieChart("#pieChart", products);
        lineChart("#lineChart", trend);
    </script>
</body>
</html>

Try this: Modify the revenue array and watch all three charts update. Experiment with different color schemes from d3.schemeSet3.

What’s Next

You can now build the major chart types. Level up with interactivity and data:

TutorialWhat You’ll Learn
D3.js InteractionsAdd tooltips, transitions, drag, and zoom
D3.js Data HandlingLoad CSV, JSON, and aggregate data
D3.js AdvancedMaps, force graphs, and hierarchies

Related topics: JavaScript, SVG, Data Visualization, CSS, React


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 Charts 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