I made real charts with D3.js — here’s how it felt

I’m Kayla. I make web charts for real folks. Small teams. Local shops. A few teachers. I used D3.js this spring and summer on three real projects. It was fun, messy, and pretty powerful. You know what? I learned a lot. Turns out I'm not the only one—the folks in this candid D3 field report came away with very similar feelings.

Let me explain what worked, what didn’t, and show real code I used. Spoiler: another dev documented exactly what worked (and what absolutely didn't) when building charts with D3, and my notes line up.

Why I picked D3.js

  • I wanted full control over shapes, colors, axes, and little details.
  • I needed print-ready SVG.
  • I had to match brand colors, not just use a default theme.

I tried chart kits before. They were fast but stiff. Earlier, I'd tried a bunch of JavaScript chart libraries and ran into the same “fast-but-rigid” trade-off. With D3, I could make weird stuff like a stepped line with a glow. It took more time, but the charts looked “ours,” not “template.”
If you ever need a middle ground—quick setup without losing customization—consider the lightweight EJSChart toolkit that wraps common patterns while leaving room for creativity.

My first win: a simple bar chart for a school report

A school counselor asked me for a clean bar chart of attendance by weekday. No fancy flair. It had to paste into Google Docs and print without blur. SVG fit the bill.
If you’d like a deeper, code-heavy walkthrough of crafting bar charts, the guide at RisingStack’s “D3.js Tutorial: Bar Charts with JavaScript” is an excellent companion.

Data looked like this:

const data = [
  { day: "Mon", value: 23 },
  { day: "Tue", value: 31 },
  { day: "Wed", value: 28 },
  { day: "Thu", value: 35 },
  { day: "Fri", value: 19 }
];

Here’s the core code I used. I ran this in a tiny HTML page with a script tag, plus d3 from a CDN.

<svg id="chart" width="560" height="320"></svg>
<script>
const svg = d3.select("#chart");
const width = +svg.attr("width");
const height = +svg.attr("height");
const margin = { top: 20, right: 20, bottom: 40, left: 40 };
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;

const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

const x = d3.scaleBand()
  .domain(data.map(d => d.day))
  .range([0, w])
  .padding(0.1);

const y = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.value)]).nice()
  .range([h, 0]);

g.append("g")
  .attr("transform", `translate(0,${h})`)
  .call(d3.axisBottom(x));

g.append("g")
  .call(d3.axisLeft(y).ticks(5));

g.selectAll("rect")
  .data(data)
  .join("rect")
  .attr("x", d => x(d.day))
  .attr("y", d => y(d.value))
  .attr("width", x.bandwidth())
  .attr("height", d => h - y(d.value))
  .attr("fill", "#4f46e5"); // a calm indigo
</script>

It printed sharp. The counselor smiled. I went for a walk. Small wins matter.

The bakery job: a line chart with real dates

My neighbor runs a tiny bakery. Summer pie season went wild, and she wanted to track daily sales. I needed real dates, nice ticks, and a line that eased through the week.

const parse = d3.timeParse("%Y-%m-%d");
const sales = [
  { date: "2025-06-01", value: 120 },
  { date: "2025-06-02", value: 98  },
  { date: "2025-06-03", value: 143 },
  { date: "2025-06-04", value: 170 },
  { date: "2025-06-05", value: 132 }
].map(d => ({ date: parse(d.date), value: d.value }));

const svg = d3.select("#line").attr("width", 640).attr("height", 360);
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const w = +svg.attr("width") - margin.left - margin.right;
const h = +svg.attr("height") - margin.top - margin.bottom;
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

const x = d3.scaleTime()
  .domain(d3.extent(sales, d => d.date))
  .range([0, w]);

const y = d3.scaleLinear()
  .domain([0, d3.max(sales, d => d.value)]).nice()
  .range([h, 0]);

g.append("g").attr("transform", `translate(0,${h})`).call(d3.axisBottom(x).ticks(5));
g.append("g").call(d3.axisLeft(y).ticks(5));

const line = d3.line()
  .x(d => x(d.date))
  .y(d => y(d.value))
  .curve(d3.curveMonotoneX);

g.append("path")
  .datum(sales)
  .attr("fill", "none")
  .attr("stroke", "#10b981") // teal
  .attr("stroke-width", 2)
  .attr("d", line);

I added a thin grid later with light gray lines. It made the chart easier to read when printed for a county fair flyer. Silly detail, big difference.

A tricky one: scatter plot with tooltips

A teacher wanted to show study time vs. test score. She asked, “Can we hover and see the student name?” Yes. But tooltips can get weird. They jump. They clip. They hide under the chart. Mine did too. I fixed it with a simple absolute-positioned div.

“`html

.tooltip {
position: absolute;
pointer-events: none;
background: #111;
color: #fff;
padding: 6px 8px;
border-radius: 4px;
font-size: 12px;
opacity: 0;
transition: opacity .15s;
}

const points = [
{ name: “Ana”, hours: 2, score: 71 },
{ name: “Luis”, hours: 5, score: 88 },
{ name: “Maya”, hours: 3, score: 79 },
{ name: “Jin”, hours: 6, score: 92 }
];

const svg = d3.select(“#scatter”);
const tip = d3.select(“#tip”);
const margin = { top: 20, right: 20, bottom: 40, left: 40 };
const w = +svg.attr(“width”) – margin.left – margin.right;
const h = +svg.attr(“height”) – margin.top – margin.bottom;
const g = svg.append(“g”).attr(“transform”, `translate(${margin.left},${margin.top})`);

const x = d3.scaleLinear().domain([0, d3.max(points, d => d.hours)]).nice().range([0, w]);
const y = d3.scaleLinear().domain([60, d3.max(points, d => d.score)]).nice().range([h, 0]);

g.append(“g”).attr(“transform”, `translate(0,${h})`).call(d3.axisBottom(x));
g.append(“g”).call(d3.axisLeft(y));

const color = d3.scaleOrdinal(d3.schemeTableau10);

g.selectAll(“circle”)
.data(points)
.join(“circle”)
.attr(“cx”, d => x(d.hours))
.attr(“cy”, d => y(d.score))
.attr(“r”, 5)
.attr(“fill”, d => color(d.name))
.on(“mousemove”, (event, d) => {
const [px, py] = d3.pointer(event, svg.node());
tip.style(“left”, (px + 12) + “px”)
.style(“top”, (py + 12) + “px”)
.style(“opacity”, 1