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: