D3.js Charts — Practical Guide to Bar, Pie, Line & Scatter Plots
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
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:
scaleBand()maps categories to horizontal positions — each label gets a “band” of pixels.scaleLinear()withrange([h, 0])inverts the y-axis — high values go to the top.x.bandwidth()gives the computed width of each bar (accounting for padding).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 Type | Appearance | Use When |
|---|---|---|
curveLinear | Straight segments (default) | Precise data points |
curveMonotoneX | Smooth, no overshoot | Time series |
curveBasis | Bezier curves | Smooth flow |
curveStep | Step function | Discrete changes |
curveNatural | Natural cubic spline | Smooth 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
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:
| Tutorial | What You’ll Learn |
|---|---|
| D3.js Interactions | Add tooltips, transitions, drag, and zoom |
| D3.js Data Handling | Load CSV, JSON, and aggregate data |
| D3.js Advanced | Maps, 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