How to create a gradient brush chart using D3.js

Louise Moxy
7 min readOct 20, 2018

--

Summary:

This tutorial will go through how to add a gradient brush to an area chart in D3.js. We will add a gradient to the defs of an SVG, and apply the gradient as a fill to an area chart.

Prerequisites

Prerequisites are that you have some basic D3.js knowledge, if not then I would recommend checking out Scrimba for their free D3 course with interactive videos: https://scrimba.com/g/gd3js

So lets begin….

We have our basic area chart created with D3 in the codepen below:

This contains a set of fake data:

const data = [
{
year: 2000,
popularity: 50
},
{
year: 2001,
popularity: 150
}....
]

With this data set we use the popularity to calculate the y scale for our chart, and the year to calculate the x scale for our chart

// Create scales
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max(data, dataPoint => dataPoint.popularity)]);
const xScale = d3
.scaleLinear()
.range([0, width])
.domain(d3.extent(data, dataPoint => dataPoint.year));

Using these scales we generate a line using D3 area( );

const area = d3
.area()
.x(dataPoint => xScale(dataPoint.year))
.y0(height)
.y1(dataPoint => yScale(dataPoint.popularity));

And finally add a path with a fill it to our chart:

// Add area
grp
.append("path")
.attr("transform", `translate(${margin.left},0)`)
.datum(data)
.style("fill", "lightblue")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", strokeWidth)
.attr("d", area);

Creating SVG defs

Now we have our basic area chart we need to create a gradient, gradients can be placed into the SVG defs. Defs are used to define graphical objects in SVG for future use. Inside defs you can create things like patterns, symbols, and gradients.

<defs>
<linearGradient id="gradient01">
<stop offset="20%" stop-color="red" />
<stop offset="90%" stop-color="blue" />
</linearGradient>
</defs>

So from the example above, you can see that we create a defs tag, and inside it is a linearGradient with an id. The id can then be used to set a fill on an svg element using “url(#Gradient01)”.

There are two types of gradients, a linear gradient and a radial gradient, for a full explanation I would recommend checking MDN. https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Gradients which breaks down both gradients.

But in summary: the stops, are color stops which sets the position for the color to stop. In our example, our first stop is at 20%. Because we haven’t set a color at 0%, the gradient will start at ‘Red’ and continue for 20% it will then begin to fade to the next color ‘Blue’ which will be fully Blue at 90%. The larger the gap between these two gradients the smoother the gradient.

We are going to set two stops at the same point, so that there will be no fade/gradient between the two color stops.

As you can see from the second gradient, we have placed the colors stops on top of each other, which remove any gradient and fading.

<linearGradient id="gradient02">
<stop offset="40%" stop-color="red" />
<stop offset="40%" stop-color="blue" />
<stop offset="90%" stop-color="blue" />
<stop offset="90%" stop-color="red" />
</linearGradient>

So first lets create a D3 brush on our area chart

The D3 brush is a tool to select a region on our chart, this can be in one or two dimensions. In our example, we will be using it on one dimension. The brush is created by clicking and dragging on the chart to a selected region, once you let go, a box is shown overlaying the chart. The selection box can be dragged by clicking it and dragging, clicking just once outside the box, will clear the selection.

For more information on this feature, check out https://github.com/d3/d3-brush

First, we create a d3 brush by calling the brush function. I have called brushX(), as I only want to use the x-axis dimension, and then I pass it the extent which the brush should be applied to, which in our case is 0 to the full width and height of the chart.

const brush = d3.brushX().extent([[0, 0], [width, height]]);

Then we can add the brush by using the D3 call function.

chart
.append("g")
.attr("class", "brush")
.call(brush);

Next we need to add a gradient to our chart, by first creating a defs element, and then adding a linearGradient element inside of it.

// Add gradient defs to svg
const defs = svg.append("defs");
const gradient = defs.append("linearGradient").attr("id", "svgGradient");

Seems simple enough, and now we can add color stops to the gradient.

We start by adding a stop tag to our gradient and passing it the attributes we need, which in our case is an offset to set where the color will start and a stop-color, as you can see below.

gradient
.append("stop")
.attr("offset", "30%")
.attr("stop-color", "red");

To give the effect we want, we want to create a gradient which is transparent at the start and at the end and only has color in the middle. Then when the brush selects an area, we can update the offset values, to change where the color begins and ends.

We will need 4 stops in total, two to represent the start of the brush and two to represent the end of the brush. Both sets will have one being transparent and the other one being color, which will have the same offset value, giving the clear line rather than the gradient fading out.

Below is an example of how the gradient looks with transparency at either end.

I have created a gradient reset percentage which all color stops will start the offset at the gradient reset, therefore no gradient will be shown on the chart.

Then when the brush is used we can update the starting stops, and the ending stops.

const gradientResetPercentage = "50%";
const gradientColors = ["#84fab0", "#8fd3f4"];
gradient
.append("stop")
.attr("class", "start")
.attr("offset", gradientResetPercentage)
.attr("stop-color", "transparent");
gradient
.append("stop")
.attr("class", "start")
.attr("offset", gradientResetPercentage)
.attr("stop-color", gradientColors[0]);
gradient
.append("stop")
.attr("class", "end")
.attr("offset", gradientResetPercentage)
.attr("stop-color", gradientColors[1])
.attr("stop-opacity", 1);
gradient
.append("stop")
.attr("class", "end")
.attr("offset", gradientResetPercentage)
.attr("stop-color", "transparent");

Next, we can listen for when a brush selection has been made, D3 has made it simple to do this by using the ‘on’ event listener.

// Create brush
const brush = d3
.brushX()
.extent([[0, 0], [width, height]])
.on("brush", brushed);

Chaining to the end of our brush, we will add the event listener to the ‘brush’ event, this is triggered when the brush moves, such as a ‘mousemove’. So when the brush moves, we will execute our brushed function

// Update the gradient start and end points to match the selected brush area
function brushed() {
const selection = d3.event.selection;
const x1Percentage = selection[0] / width * 100;
const x2Percentage = selection[1] / width * 100;
d3.selectAll(".start").attr("offset", `${x1Percentage}%`);
d3.selectAll(".end").attr("offset", `${x2Percentage}%`);
}

Then using d3.event.selection, we will get the current brushed area in svg units.

For example, if the SVG brush is 100 units in width, and we brush from the start of the brush area to exactly halfway, the selection will return [0, 50].

With this selection, we need to convert it into a percentage so we can update our gradient.

Then we can update the offset attribute for our colorStops in our gradient.

And that is it! Well sort of…

So we currently have our standard blue brush overlay which we can remove with css.

.selection {
fill: none;
}

The default behaviour for the D3 brush is when the chart is clicked it removes the selection, so we need to update our gradient to reflect this as well. So if we add another event listener to our brush

const brush = d3
.brushX()
.extent([[0, 0], [width, height]])
.on("brush", brushed)
.on("start", resetGradient);

The start event is triggered at the start of a brush gesture, such as on mousedown. This means it will be triggered before our ‘brushed’ event, if the brush is clicked and the selection dragged, and also when the chart is just clicked with no selection.

So we will need to check if there is anything selected before we reset the gradient back to the default position.

Again using the d3.event.selection, if the selection is empty then we can reset the gradient, giving the effect of the brush being cleared.

function resetGradient() {
const selection = d3.event.selection;
// If the brush area is clicked but there is a selected area
// don't clear the gradient as it could be the brush area being moved
if(selection[1] - selection[0] === 0) {
d3.selectAll(".start").attr("offset", gradientResetPercentage);
d3.selectAll(".end").attr("offset", gradientResetPercentage);
}
}

So here is the final chart:

The gradient can have as many stops as you like so you can be more complex with the gradient.

Using the same idea you can create other effects, such as if you want the gradient to show outliers or warning points in the data, you can show this using a linear gradient. And using a clipping path controlled by the brush, in the same way, we did for the gradient, then your area chart can have more meaning and/or information.

Hopefully, this has been helpful, if I have made any mistakes or you have any questions/comments please leave a comment.

Thanks for reading 👋

--

--

Louise Moxy

An ethical designer & developer based in Torbay, UK. Learning and sharing javascript.