Lines

Chart types

The graphical shapes used in data visualizations largely define the type of chart. Points (little squares or circles) in a scatter plot, rectangles in a bar chart, lines (splines or polylines) in a line chart, circular or annular sectors in a pie or donut chart, areas in a area chart etc.

The d3-shape module provides a variety of shape generators. They generate SVG path data (for attribute d) based on the declared scales and the given data. A shape generator can also render to HTML <canvas>.

Line generator

d3.line() returns a line generator, i.e., a function that takes an array and returns SVG path data.


const width = 30;
const height = 30;

const svg = d3.select("svg")
  .attr("width", "100%")
  .attr("height", "100%")
  .attr("viewBox", `0 0 ${width} ${height}`)
  .attr("style", "width: 200px; height: auto;");
  
const lineGenerator = d3.line();
const pathData = lineGenerator([[10,10], [20,10], [20,20], [10,20], [10,10]]);

svg.append("path")
  .attr("fill", "none")
  .attr("stroke", "steelblue")
  .attr("stroke-width", 1)
  .attr("d", pathData); 

Result:

An x and y accessor can be defined for d3.line(). This way you can control what properties in the data you want to be the coordinates.


const scaleX = … ;
const scaleY = … ;

const lineGenerator = d3.line(d => scaleX(d.prop1), d => scaleY(d.prop2));
const pathData = lineGenerator([{prop1: 10, prop2: 10}, {prop1: 20, prop2: 10}, … ]);
Is equivalent to:

const scaleX = … ;
const scaleY = … ;

const lineGenerator = d3.line()
  .x(d => scaleX(d.prop1))
  .y(d => scaleY(d.prop2));	
const pathData = lineGenerator([{prop1: 10, prop2: 10}, {prop1: 20, prop2: 10}, … ]);

The next example shows a line chart of the annual average temperature in the Netherlands. The data point are created without using a shape generator. The connecting line is created using a d3 line generator. The creation of the red line is not included in the code below, see regression.


const width = 640;
const height = 400;
const margin = {
  top: 35,
  right: 20,
  bottom: 35,
  left: 35,
}

const svg = d3.select("svg")
  .attr("width", "100%")
  .attr("height", "100%")
  .attr("viewBox", `0 0 ${width} ${height}`)
  .attr("style", "width: 100%; height: auto;");

function drawChart(data) {

  const scaleX = d3.scaleUtc()
    .domain(d3.extent(data, d => d.year))
    .range([margin.left, width - margin.right])
    .nice();

  const scaleY = d3.scaleLinear()
    .domain([d3.min(data, d => d.annualAverage) - 1, d3.max(data, d => d.annualAverage)])
    .range([height - margin.bottom, margin.top])
    .nice();

  // Add the x-axis.
  svg.append("g")
    .attr("transform", `translate(0,${height - margin.bottom})`)
    .call(d3.axisBottom(scaleX).ticks(null, "%Y").tickSizeOuter(0));

  // Add the y-axis.
  svg.append("g")
    .attr("transform", `translate(${margin.left},0)`)
    .call(d3.axisLeft(scaleY))
    .call(g => g.select(".domain").remove())
    .call(g => g.append("text")
      .attr("x", -margin.left)
      .attr("y", 10)
      .attr("fill", "currentColor")
      .attr("text-anchor", "start")
      .text("↑ Temperature (°C)"))
    .call(g => g.selectAll(".tick line").clone() // see 1)
      .attr("x2", width - margin.left - margin.right)
      .attr("stroke-dasharray", "2,2")
      .attr("stroke-opacity", 0.2));				  

  // Add data points.				  
  svg.append("g")
    .attr("fill", "steelblue")
    .attr("stroke", "none")			
    .selectAll()
    .data(data)
    .join("circle")		  
      .attr("cx", (d) => scaleX(d.year))  
      .attr("cy", (d) => scaleY(d.annualAverage))
      .attr("r", 2.6)
      .append("title")
        .text(d => `${d.year.getFullYear()}: ${d.annualAverage}°C`);				  

  // Declare the line generator.
  const line = d3.line()
    .x(d => scaleX(d.year))
    .y(d => scaleY(d.annualAverage));				  

  // Append a path for the line.
  svg.append("path")
    .attr("fill", "none")
    .attr("stroke", "steelblue")
    .attr("stroke-width", 1)
    .attr("d", line(data));				  
};

// Fetch the CSV data file,
// and then call 'drawChart' to draw the chart.
// See 2) below for more information.
(async function() {
  try {
    const response = await d3.csv('data/annual-average-temperature.csv', d => {
      d.year = new Date(+d.year, 0, 1);
      d.annualAverage = +d.annualAverage;
      return d;
    })
    drawChart(response); // "response" is an array of objects, created by d3.csv.	
  }
  catch (error) {
    document.querySelector("#errorMessage").textContent = error; 
  }		  
})();

Result:

Data source: KNMI - Klimaatdashboard. Annual average temperature in De Bilt, the Netherlands.

Download the used CSV data file.

1) Draw the horizontal dashed lines by cloning the tick lines and extending them to the full width. The cloning is done by selection.clone() which inserts the clone of the selected elements immediately following the selected elements and returns a selection of the newly added clone.

2) The data are provided by a comma-separated values (CSV) text file. d3.csv interprets numbers in the CSV file as strings. You can pass an "accessor" function as the second argument that is called on each row of data to convert strings to numbers and convert the year to a JS date object.

The creation of the red line is not included in the code above, see regression.

.defined()

If an element in the input data array cannot be coerced to a number, the generation of the path data will stop. The path will end at this "undefined" element. With .defined(bool) you can check if an element is "defined". bool is a specified accessor function that will be invoked for each element in the input data array. If bool returns true (truthy), the point will be added to the path, If bool returns false (falsy), the element will be skipped, the current line segment will be ended, and a new line segment will be generated for the next defined point.

In the next example the accessor of .defined() only returns false if the data element is null or undefined and skips this element.


const width = 400;
const height = 100;

const svg = d3.select("svg")
  .attr("width", "100%")
  .attr("height", "100%")
  .attr("viewBox", `0 0 ${width} ${height}`)
  .attr("style", "width: 100%; height: auto;");

const data = [40, 30, undefined, 80, 50, 60, 40];

const scaleX = d3.scaleLinear().domain([0, data.length - 1]).range([0, width]);
const scaleY = d3.scaleLinear().domain([0, 80]).range([height, 0]);		  

const lineGenerator = d3.line()
  .x((d, i) => scaleX(i))
  .y(d => scaleY(d))
  .defined((d) => d != null);
const pathData = lineGenerator(data);

svg.append("path")
  .attr("fill", "none")
  .attr("stroke", "steelblue")
  .attr("stroke-width", 1)
  .attr("d", pathData);

Result:

.curve()

line.curve() sets a curve factory. The default curve factory (d3.curveLinear) connects points with straight line segments (a polyline), as shown in the previous examples.


const line = d3.line()
  .x((d) => x(d.date))
  .y((d) => y(d.value))
  .curve(d3.curveLinear);
… is equivalent to …

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

A line generator can also produce a spline or step function. The D3 specification on curves provides an overview of all the available spline types and step functions. Next example shows a few curves.


const width = 400;
const height = 100;
const margin = {
  top: 10,
  right: 10,
  bottom: 10,
  left: 10,
}

const svg = d3.select("svg")
  .attr("width", "100%")
  .attr("height", "100%")
  .attr("viewBox", `0 0 ${width} ${height}`)
  .attr("style", "width: 100%; height: auto;");

const data = [40, 30, 80, 50, 60, 40];

const scaleX = d3.scaleLinear()
  .domain([0, data.length - 1])
  .range([margin.left, width - margin.right]);
const scaleY = d3.scaleLinear()
  .domain([0, d3.max(data, d => d)])
  .range([height - margin.bottom, margin.top]);

svg.append("g")
  .attr("fill", "none")
  .attr("stroke", "currentColor")
  .attr("stroke-width", 1)		  
  .selectAll()
  .data(data)
  .join("circle")		  
    .attr("cx", (d, i) => scaleX(i))  
    .attr("cy", (d) => scaleY(d))
    .attr("r", 2);

const paths = svg.append("g")
  .attr("fill", "none")
  .attr("stroke-width", 1);			

const lineGenerator = d3.line()
  .x((d, i) => scaleX(i))
  .y(d => scaleY(d));	

// d3.curveCardinal (steel blue)		  
lineGenerator.curve(d3.curveCardinal);		  
paths.append("path")
  .attr("stroke", "steelblue")
  .attr("d", lineGenerator(data));

// d3.curveBasis (orange)		  
lineGenerator.curve(d3.curveBasis);
paths.append("path")
  .attr("stroke", "orange")
  .attr("d", lineGenerator(data));

// d3.curveStep (magenta)
lineGenerator.curve(d3.curveStep);
paths.append("path")
  .attr("stroke", "magenta")
  .attr("d", lineGenerator(data));

Result:

Render to <canvas>

With context in line.context(context) specified, the generated line is rendered to this context as a sequence of HTML <canvas> path method calls.


const canvas = document.querySelector("#myCanvas");
const context = canvas.getContext("2d");		

const width = canvas.getBoundingClientRect().width;
const height = 100;
const margin = {
  top: 10,
  right: 10,
  bottom: 10,
  left: 10,
}	

canvas.width = width;
canvas.height = height;

const data = [40, 30, 80, 50, 60, 40];

const scaleX = d3.scaleLinear()
  .domain([0, data.length - 1])
  .range([margin.left, width - margin.right]);
const scaleY = d3.scaleLinear()
  .domain([0, d3.max(data, d => d)])
  .range([height - margin.bottom, margin.top]);

const lineGenerator = d3.line()
  .x((d, i) => scaleX(i))
  .y(d => scaleY(d))
  .curve(d3.curveCardinal)
  .context(context);		  
context.strokeStyle = "tomato";
context.beginPath();
lineGenerator(data);
context.stroke();

Result:

Radial lines

d3.lineRadial is similar to d3.line. Instead of cartesian x and y accessors it uses polar angle and radius accessors. The angle is in radians and rotates in clockwise order, starting at the top (12 o'clock).


const width = 200;
const height = 200;

const svg = d3.select("svg")
  .attr("width", "100%")
  .attr("height", "100%")
  .attr("viewBox", `0 0 ${width} ${height}`)
  .attr("style", "width: 100%; height: auto;");

const data = Array.from({ length: 500 }, (v, i) => ({
  angle: i,
  radius: i,
}))

const scaleAngle = d3.scaleLinear()
  .domain(d3.extent(data, d => d.angle))		
  .range([0, 20 * Math.PI]);
const scaleRadius = d3.scaleLinear()
  .domain(d3.extent(data, d => d.radius))	
  .range([0, 100]);		  

const lineGenerator = d3.lineRadial()
  .angle(d => scaleAngle(d.angle))
  .radius(d => scaleRadius(d.radius))
const pathData = lineGenerator(data);

svg.append("path")
  .attr("transform", `translate(${width/2}, ${height/2})`) // origin transformed to center
  .attr("fill", "none")
  .attr("stroke", "steelblue")
  .attr("stroke-width", 1)
  .attr("d", pathData);

Result: