D3.js Practical Guide
Practical patterns for building data visualizations
D3.js Practical Guide: Data Visualization for Developers
When I first encountered D3.js, I wondered why anyone would choose it over simpler charting libraries like Chart.js. The API seemed unnecessarily complex for creating basic charts. After working with it on several projects, I understand why D3.js remains the go-to choice for custom data visualizations.
Why D3.js?
D3.js stands for Data-Driven Documents. The core concept is binding data to DOM elements so that when your data changes, the visual representation updates automatically.
// This is the fundamental D3.js pattern
const svg = d3.select("svg");
svg
  .selectAll("circle")
  .data(sampleData) // Bind data
  .enter() // Handle new data points
  .append("circle") // Create DOM elements
  .attr("r", (d) => d.radius); // Set attributes based on data
Understanding Selections
Important distinction: d3.select() is not the same as querySelector().
// ❌ This won't work for method chaining
const svg = document.querySelector("svg");
svg.selectAll("circle"); // Error!
// ✅ Use D3 selection objects
const svg = d3.select("svg");
svg.selectAll("circle"); // Works correctly
D3 selections aren’t just DOM elements - they’re special objects designed for data binding and manipulation.
Data Join Fundamentals
The data join is where D3.js gets confusing for newcomers. Let me break down what actually happens:
const circles = svg
  .selectAll("circle") // Select all circles (even if none exist)
  .data(dataset); // Bind data to selection
// At this point, three things can happen:
// 1. UPDATE: Existing elements that have matching data
// 2. ENTER: New data points that need new elements
// 3. EXIT: Existing elements that no longer have data
The Classic Pattern (Before .join())
Here’s how we used to handle data joins:
const circles = svg.selectAll("circle").data(dataset);
// Handle existing elements (UPDATE)
circles.attr("r", (d) => d.radius).attr("fill", (d) => d.color);
// Handle new data (ENTER)
const enterSelection = circles
  .enter()
  .append("circle")
  .attr("r", (d) => d.radius)
  .attr("cx", (d) => d.x)
  .attr("cy", (d) => d.y)
  .attr("fill", (d) => d.color);
// Combine for future updates
const merged = circles.merge(enterSelection);
// Handle removed data (EXIT)
circles.exit().remove();
The Modern Approach with .join()
D3 v5+ introduced .join() to simplify this pattern:
svg
  .selectAll("circle")
  .data(dataset)
  .join("circle") // Handles enter, update, and exit automatically
  .attr("r", (d) => d.radius)
  .attr("cx", (d) => d.x)
  .attr("cy", (d) => d.y)
  .attr("fill", (d) => d.color);
For custom behavior:
svg
  .selectAll("circle")
  .data(dataset)
  .join(
    (enter) =>
      enter
        .append("circle")
        .attr("fill", "green")
        .attr("r", 0)
        .transition()
        .attr("r", (d) => d.radius),
    (update) => update.attr("fill", "blue"),
    (exit) => exit.transition().attr("r", 0).remove()
  );
Scales: Converting Data to Visual Space
Scales translate your data values into visual dimensions. Think of them as conversion functions.
scaleLinear for Continuous Data
const xScale = d3
  .scaleLinear()
  .domain([0, 100]) // Input range (your data)
  .range([0, 500]); // Output range (pixels)
console.log(xScale(50)); // Returns 250
scaleBand for Categorical Data
const xScale = d3
  .scaleBand()
  .domain(["A", "B", "C", "D"])
  .range([0, 400])
  .padding(0.1);
console.log(xScale("B")); // Returns the x position for category B
console.log(xScale.bandwidth()); // Returns the width of each band
Building a Bar Chart: Step by Step
Here’s a complete example that demonstrates the core concepts:
// Setup
const margin = { top: 20, right: 30, bottom: 40, left: 40 };
const width = 800 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3
  .select("#chart")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom);
const g = svg
  .append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);
// Load and process data
d3.json("data.json").then((data) => {
  // Create scales
  const xScale = d3
    .scaleBand()
    .domain(data.map((d) => d.category))
    .range([0, width])
    .padding(0.1);
  const yScale = d3
    .scaleLinear()
    .domain([0, d3.max(data, (d) => d.value)])
    .range([height, 0]); // Note: reversed for SVG coordinate system
  // Create axes
  const xAxis = d3.axisBottom(xScale);
  const yAxis = d3.axisLeft(yScale);
  g.append("g").attr("transform", `translate(0,${height})`).call(xAxis);
  g.append("g").call(yAxis);
  // Create bars
  g.selectAll(".bar")
    .data(data)
    .join("rect")
    .attr("class", "bar")
    .attr("x", (d) => xScale(d.category))
    .attr("y", (d) => yScale(d.value))
    .attr("width", xScale.bandwidth())
    .attr("height", (d) => height - yScale(d.value))
    .attr("fill", "steelblue");
});
Common Pitfalls and Solutions
1. SVG Coordinate System
SVG coordinates start from the top-left corner. For charts, you typically want higher values to appear higher on screen:
// This is why we often reverse the range for y-scales
const yScale = d3.scaleLinear().domain([0, maxValue]).range([height, 0]); // height to 0, not 0 to height
2. Missing Required Attributes
Some SVG elements won’t render without specific attributes:
// Circles need r, cx, cy
circle.attr("r", 5).attr("cx", 10).attr("cy", 20);
// Rectangles need width and height
rect.attr("width", 50).attr("height", 30);
3. Data Type Issues
D3 scales expect consistent data types:
// Make sure numeric data is actually numeric
data.forEach((d) => {
  d.value = +d.value; // Convert string to number
});
Generators, Components, and Layouts
D3 provides utilities beyond basic data binding:
Generators
Create SVG path strings from data:
const line = d3
  .line()
  .x((d) => xScale(d.date))
  .y((d) => yScale(d.value))
  .curve(d3.curveMonotoneX);
svg
  .append("path")
  .datum(data)
  .attr("d", line)
  .attr("fill", "none")
  .attr("stroke", "steelblue");
Components
Pre-built interactive elements:
// Axes are components
const xAxis = d3.axisBottom(xScale).ticks(5).tickFormat(d3.timeFormat("%b"));
g.append("g").call(xAxis);
Layouts
Transform data for specific chart types:
const pie = d3.pie().value((d) => d.value);
const arc = d3.arc().innerRadius(0).outerRadius(100);
const pieData = pie(data);
// Now pieData has startAngle, endAngle properties for each slice
Best Practices
- Structure your code: Set up margins, scales, and basic structure before handling data
 - Use semantic grouping: Create 
<g>elements for logical chart components - Handle data loading errors: Always include error handling for data loading
 - Consider performance: For large datasets, consider canvas rendering or data aggregation
 - Make it responsive: Use viewBox and CSS for responsive charts
 
When to Use D3.js
Choose D3.js when you need:
- Custom visualizations that don’t fit standard chart types
 - Fine control over every visual element
 - Complex interactions and animations
 - Data-driven visual updates
 
Consider alternatives when:
- You need standard charts quickly (Chart.js, Recharts)
 - Your team isn’t comfortable with SVG and lower-level APIs
 - Simple visualizations are sufficient for your use case
 
D3.js has a learning curve, but the flexibility it provides is unmatched for custom data visualization needs.
Bar Graph Full code
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>D3.js Bar Chart - COVID Cases by Region</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 20px;
        background-color: #f5f5f5;
      }
      h2 {
        color: #333;
        text-align: center;
        margin-bottom: 30px;
      }
      .canvas {
        display: flex;
        justify-content: center;
        background-color: white;
        border-radius: 8px;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        padding: 20px;
      }
      /* Hover effect for rectangles - applies to any future graphs too */
      rect:hover {
        stroke: black;
        stroke-width: 2px;
        opacity: 0.8;
      }
      .x-axis text {
        font-size: 12px;
      }
      .y-axis text {
        font-size: 12px;
      }
      .text {
        pointer-events: none;
        font-weight: bold;
      }
    </style>
  </head>
  <body>
    <h2>확진자 현황 (COVID Cases by Region)</h2>
    <div class="canvas"></div>
    <script>
      // Chart dimensions and configuration
      const SVG_WIDTH = 1000;
      const SVG_HEIGHT = 800;
      const GRAPH_WIDTH = 800;
      const GRAPH_HEIGHT = 600;
      const padding = [70, 70];
      const RECT_WIDTH = 30;
      // Create SVG container
      const svg = d3
        .select(".canvas")
        .append("svg")
        .attr("width", SVG_WIDTH)
        .attr("height", SVG_HEIGHT);
      // Create main graph group with padding
      const graph = svg
        .append("g")
        .attr("transform", `translate(${padding[0]},${padding[1]})`);
      // Create axis groups
      const gx = graph
        .append("g")
        .attr("class", "x-axis")
        .attr("transform", `translate(0,${GRAPH_HEIGHT})`);
      const gy = graph.append("g").attr("class", "y-axis");
      // Sample data (replace with actual d3.json call)
      const sampleData = [
        { 지역이름: "서울", 확진자수: 1250 },
        { 지역이름: "부산", 확진자수: 890 },
        { 지역이름: "대구", 확진자수: 750 },
        { 지역이름: "인천", 확진자수: 630 },
        { 지역이름: "광주", 확진자수: 420 },
        { 지역이름: "대전", 확진자수: 380 },
        { 지역이름: "울산", 확진자수: 290 },
        { 지역이름: "세종", 확진자수: 150 },
      ];
      // Function to render the chart
      function renderChart(data) {
        // Create scales
        const xScale = d3
          .scaleBand()
          .domain(data.map((item) => item.지역이름))
          .range([0, GRAPH_WIDTH])
          .padding(0.1);
        // Y scale - higher values should appear higher (lower y coordinate)
        const yScale = d3
          .scaleLinear()
          .domain([0, d3.max(data, (d) => d.확진자수)])
          .range([GRAPH_HEIGHT, 0]);
        // Create axes
        const xAxis = d3.axisBottom(xScale);
        const yAxis = d3
          .axisLeft(yScale)
          .ticks(5)
          .tickFormat((d) => `${d}명`);
        // Render axes
        gx.call(xAxis);
        gy.call(yAxis);
        // Create bars
        graph
          .selectAll("rect")
          .data(data)
          .enter()
          .append("rect")
          .attr("x", (d) => xScale(d.지역이름))
          .attr("y", (d) => yScale(d.확진자수))
          .attr("height", (d) => GRAPH_HEIGHT - yScale(d.확진자수))
          .attr("width", xScale.bandwidth())
          .attr("fill", "hotpink")
          .attr("rx", 2); // Rounded corners
        // Add value labels on bars
        graph
          .selectAll(".text")
          .data(data)
          .enter()
          .append("text")
          .attr("x", (d) => xScale(d.지역이름) + xScale.bandwidth() / 2)
          .attr("y", (d) => yScale(d.확진자수) - 5)
          .text((d) => d.확진자수)
          .attr("class", "text")
          .style("font-size", "12px")
          .attr("text-anchor", "middle")
          .attr("fill", "#333");
        // Create line generator
        const line = d3
          .line()
          .x((d) => xScale(d.지역이름) + xScale.bandwidth() / 2)
          .y((d) => yScale(d.확진자수))
          .curve(d3.curveBasis);
        // Add trend line
        graph
          .append("path")
          .datum(data)
          .attr("fill", "none")
          .attr("stroke", "blue")
          .attr("stroke-width", "3px")
          .attr("d", line)
          .style("opacity", 0.8);
        // Add circles at data points
        graph
          .selectAll(".dot")
          .data(data)
          .enter()
          .append("circle")
          .attr("class", "dot")
          .attr("cx", (d) => xScale(d.지역이름) + xScale.bandwidth() / 2)
          .attr("cy", (d) => yScale(d.확진자수))
          .attr("r", 4)
          .attr("fill", "blue")
          .attr("stroke", "white")
          .attr("stroke-width", 2);
      }
      // Render chart with sample data
      renderChart(sampleData);
      // Uncomment below to use actual JSON data
      /*
        d3.json("data4.json")
            .then((data) => {
                // Remove header row if present
                [_, ...data] = [...data];
                console.log(data);
                renderChart(data);
            })
            .catch((err) => {
                console.error("Error loading data:", err);
                // Fallback to sample data
                renderChart(sampleData);
            });
        */
    </script>
  </body>
</html>