D3.js Line & Area Charts Tutorial

Maryam Alavi
Name
Maryam Alavi

Updated on

In this guide, you'll learn how to create line and area charts with D3.js using small, reusable React components rendered directly on this page.

Simple Line Chart

import React, { useEffect, useRef } from 'react'
import * as d3 from 'd3'
 
function SimpleLineChart() {
  const containerRef = useRef(null)
  useEffect(() => {
    const data = [
      { month: 'Jan', value: 34 },
      { month: 'Feb', value: 28 },
      { month: 'Mar', value: 40 }
    ]
    const width = 720, height = 360
    const margin = { top: 24, right: 24, bottom: 48, left: 56 }
    const innerWidth = width - margin.left - margin.right
    const innerHeight = height - margin.top - margin.bottom
    const root = d3.select(containerRef.current)
    root.selectAll('*').remove()
    const svg = root.append('svg')
      .attr('viewBox', `0 0 ${width} ${height}`)
      .style('max-width', '100%')
      .style('height', 'auto')
    const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
    const x = d3.scalePoint().domain(data.map(d => d.month)).range([0, innerWidth]).padding(0.5)
    const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value) || 0]).nice().range([innerHeight, 0])
    g.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x))
    g.append('g').call(d3.axisLeft(y))
    const line = d3.line().x(d => x(d.month) || 0).y(d => y(d.value)).curve(d3.curveMonotoneX)
    g.append('path').datum(data).attr('fill', 'none').attr('stroke', '#3b82f6').attr('stroke-width', 2).attr('d', line)
  }, [])
  return <div ref={containerRef} />
}

Multi-Series Line Chart

import React, { useEffect, useRef } from 'react'
import * as d3 from 'd3'
 
function MultiSeriesLineChart() {
  const containerRef = useRef(null)
  useEffect(() => {
    const seriesKeys = ['Alpha', 'Beta']
    const data = [
      { month: 'Jan', Alpha: 20, Beta: 34 },
      { month: 'Feb', Alpha: 26, Beta: 30 }
    ]
    const width = 760, height = 380
    const margin = { top: 24, right: 24, bottom: 52, left: 56 }
    const innerWidth = width - margin.left - margin.right
    const innerHeight = height - margin.top - margin.bottom
    const root = d3.select(containerRef.current)
    root.selectAll('*').remove()
    const svg = root.append('svg').attr('viewBox', `0 0 ${width} ${height}`).style('max-width', '100%').style('height', 'auto')
    const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
    const x = d3.scalePoint().domain(data.map(d => d.month)).range([0, innerWidth]).padding(0.5)
    const y = d3.scaleLinear().domain([0, d3.max(data, d => d3.max(seriesKeys, k => d[k])) || 0]).nice().range([innerHeight, 0])
    const color = d3.scaleOrdinal().domain(seriesKeys).range(['#3b82f6', '#ef4444'])
    g.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x))
    g.append('g').call(d3.axisLeft(y))
    const line = d3.line().x(d => x(d.month) || 0).y(d => y(d.value)).curve(d3.curveMonotoneX)
    const series = seriesKeys.map(key => ({ key, values: data.map(d => ({ month: d.month, value: d[key] })) }))
    g.selectAll('path.series').data(series).join('path').attr('class', 'series').attr('fill', 'none').attr('stroke-width', 2).attr('stroke', d => color(d.key)).attr('d', d => line(d.values))
  }, [])
  return <div ref={containerRef} />
}

Area Chart

import React, { useEffect, useRef } from 'react'
import * as d3 from 'd3'
 
function AreaChart() {
  const containerRef = useRef(null)
  useEffect(() => {
    const data = [ { month: 'Jan', value: 12 }, { month: 'Feb', value: 18 } ]
    const width = 720, height = 360
    const margin = { top: 24, right: 24, bottom: 48, left: 56 }
    const innerWidth = width - margin.left - margin.right
    const innerHeight = height - margin.top - margin.bottom
    const root = d3.select(containerRef.current)
    root.selectAll('*').remove()
    const svg = root.append('svg').attr('viewBox', `0 0 ${width} ${height}`).style('max-width', '100%').style('height', 'auto')
    const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
    const x = d3.scalePoint().domain(data.map(d => d.month)).range([0, innerWidth]).padding(0.5)
    const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value) || 0]).nice().range([innerHeight, 0])
    const area = d3.area().x(d => x(d.month) || 0).y0(innerHeight).y1(d => y(d.value)).curve(d3.curveMonotoneX)
    g.append('path').datum(data).attr('fill', '#93c5fd').attr('opacity', 0.8).attr('d', area)
  }, [])
  return <div ref={containerRef} />
}

Tips

  • Use d3.scalePoint for evenly spaced categories along the x-axis.
  • Smooth curves with d3.curveMonotoneX for a cleaner look.
  • Combine area + line for emphasis on magnitude and trend.

When to Use Line vs. Area Charts

Line charts are ideal when the primary task is to compare trends over time or another ordered dimension. They highlight the direction and rate of change and make it easy to compare multiple series. Area charts communicate magnitude in addition to trend by filling the region under the line. Use area charts when the “amount” matters—cumulative totals, volume, or time spent—because the filled region provides a visual cue for how much is being accrued or consumed.

For multi-series data, multiple lines make comparisons straightforward. If the main story is how categories contribute to a whole over time, consider a stacked area chart. Stacking emphasizes the total and the relative contribution of each category. However, be aware that stacking can obscure individual series’ volatility. Keep separate lines if you need to analyze category-specific movements precisely.

Data Modeling, Time Parsing, and Missing Values

Real-world time series often arrive as strings (for example, 2024-09-01 or 09/01/2024). Convert them to Date objects with d3.timeParse and use d3.scaleTime for continuous axes. This ensures correct spacing between irregular timestamps (e.g., missing weekends or holidays). Missing values are common—sensor dropouts, API gaps, or maintenance windows. Decide whether to interpolate or to show gaps. When accuracy is critical, prefer gaps by using the line generator’s defined accessor (line.defined(d => d.value != null)) so the path breaks where data is absent. If you interpolate, document the method to avoid implying false precision.

Responsiveness and Layout Considerations

Use an SVG viewBox and a flexible width so charts scale with their containers. On small screens, increase bottom margins for label legibility or abbreviate tick labels (e.g., JanJ). Reduce the number of ticks with axis.ticks() or provide custom tick values to prevent clutter. If labels overlap, rotate them slightly or switch to quarterly markers. Always verify contrast and touch target sizes on mobile; ensure markers are large enough to tap and tooltips do not obscure important data.

Interactivity: Tooltips, Focus, and Brushing

Small, purposeful interactions dramatically improve readability:

  • Hover tooltips that reveal exact values and timestamps help users read precise figures without scanning the axis.
  • A focus marker (a vertical rule and highlighted point) that follows the pointer provides a single, clear read point across multiple lines.
  • Brushing (dragging to select a range) enables zooming into interesting intervals. Pair a brushed overview chart (mini-chart) with a detailed focus chart for an effective exploratory workflow.

Throttle pointer events on large datasets, and precompute nearest points (e.g., using bisectors) to keep interactions responsive.

Smoothing and Curves

Curves such as d3.curveMonotoneX produce smooth lines without overshoot, preserving monotonicity between points. Use them for aesthetics and readability, but avoid implying continuity or density that does not exist. Weekly or monthly data with gaps often benefits from curveLinear unless you clearly communicate smoothing. For highly volatile metrics, over-smoothing can hide anomalies—make deliberate, documented choices.

Performance Tips

Time series can be large. To keep charts snappy:

  • Downsample for rendering (e.g., largest-triangle-three-buckets) while preserving accurate aggregates for calculations.
  • Memoize scale domains and accessors when input data is unchanged to avoid unnecessary recomputation.
  • Minimize DOM elements; skip per-point markers when thousands of points are present or render points on canvas while keeping axes in SVG.

Common Pitfalls

  • Inconsistent time zones: Normalize timestamps (usually to UTC) before plotting.
  • Uneven sampling: If one series updates every minute and another hourly, visually differentiate or resample for fair comparisons.
  • Axis mismatch: Always label units and avoid mixing incompatible measures on the same axis. If you must use dual axes, indicate units clearly and use them sparingly.

FAQs

How do I format dates on the axis? Use d3.timeFormat with a scaleTime axis, for example: axisBottom(time).tickFormat(d3.timeFormat('%b %d')).

How can I show missing data as gaps? Use line.defined(d => d.value != null) so the path breaks across null/undefined points.

Can I combine bars with lines? Yes—overlay a line on a bar chart for targets vs. actuals or rates vs. counts. Ensure color and legend clarify roles.