Plotting a Line Chart With Tooltips Using React and D3.js

Create stunning data visualizations in your app today

Urvashi
Better Programming

--

simple line graph
Image credit: Author

D3.js is a data visualization library that is used to create beautiful charts and visual representations out of data using HTML, CSS, and SVG. You can plot and choose from a wide variety of charts such as treemaps, pie charts, sunburst charts, stacked area charts, bar charts, box plots, line charts, multi-line charts, and many more. You can check out the gallery here.

Today, we will be creating a simple (but cute) line chart with tooltips in React using D3.

Prerequisites

  1. Create a new React app if you don’t have one already.
  2. Add D3 v5.16.0 as a dependency.

Let’s Get Started

First of all, let’s create a new component, and let’s put it in a file called LineChart.js.

<LineChart /> will accept three props — data (the data to plot on the chart), width, and height of the chart. We have added a useEffect Hook that will call our drawChart() function. This Hook will depend on data props since we want to redraw the chart every time the data changes.

We have rendered a <div> element with id set to container, which will contain our SVG elements.

Next, let’s have a look at the component that is going to render this line chart.

regenerateData() generates random data and sets the component data state on first mount and then every time the Change Data button is clicked. We pass the height, width, and data as props to <LineChart /> .

The data object contains a label (X-Axis) and the value (Y-Axis) as well as the tooltipContent that we want to show on hovering over the chart.

Plotting the Chart

We will now be adding the logic to draw the chart. Let us define some constants first inside the drawChart() function.

const margin = { top: 50, right: 50, bottom: 50, left: 50 };const yMinValue = d3.min(data, d => d.value);
const yMaxValue = d3.max(data, d => d.value);
const xMinValue = d3.min(data, d => d.label);
const xMaxValue = d3.max(data, d => d.label);

Here we are adding the SVG element as well as the tooltip element:

const svg = d3
.select('#container')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const tooltip = d3
.select('#container')
.append('div')
.attr('class', 'tooltip');

We select the #container element and append an SVG inside it and a g element to group other SVG elements, and translate it, leaving the left and top margins. We also append a div element which will contain our tooltipContent .

Let us define the axes scales and the line/path generator next:

const xScale = d3
.scaleLinear()
.domain([xMinValue, xMaxValue])
.range([0, width]);
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, yMaxValue]);
const line = d3
.line()
.x(d => xScale(d.label))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);

d3.scaleLinear() maps any given number within the given domain to the given range. These scales will help us find the positions/coordinates on the graph for each data item.

Now let’s draw the gridlines (you can skip this if you want to), the X-axis, and the Y-axis as well as the data line:

svg
.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(
d3.axisBottom(xScale)
.tickSize(-height)
.tickFormat(''),
);
svg
.append('g')
.attr('class', 'grid')
.call(
d3.axisLeft(yScale)
.tickSize(-width)
.tickFormat(''),
);
svg
.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom().scale(xScale).tickSize(15));
svg
.append('g')
.attr('class', 'y-axis')
.call(d3.axisLeft(yScale));
svg
.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', '#f6c3d0')
.attr('stroke-width', 4)
.attr('class', 'line')
.attr('d', line);

First, we append another group of SVG elements that will contain the gridlines along the Y-axis. We set the tickFormat as empty strings because we don’t want any labels drawn with them. The height of these gridlines is set equal to the height of the chart, but we add the so that they are drawn above the axisBottom and not below it. Similarly, we draw the gridlines along the X-axis.

Next, we draw the actual axes as well as append the line path. At this point, you have already created the line chart!

Adding the Tooltip

We will be adding a circle marker for the point we are hovering over and a tooltip box. Initially, the opacity of the tooltip will be set to 0 and the circle marker will also not be displayed unless some mouse event happens.

const focus = svg
.append('g')
.attr('class', 'focus')
.style('display', 'none');
focus.append('circle').attr('r', 5).attr('class', 'circle');const tooltip = d3
.select('#container')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);

Next, we will be appending a rect (which will not be visible, so we set the opacity to 0) over our chart to capture mouse events:

svg
.append('rect')
.attr('class', 'overlay')
.attr('width', width)
.attr('height', height)
.style('opacity', 0)
.on('mouseover', () => {
focus.style('display', null);
})
.on('mouseout', () => {
tooltip
.transition()
.duration(300)
.style('opacity', 0);
})
.on('mousemove', mousemove);
function mousemove(event) {
const bisect = d3.bisector(d => d.label).left;
const xPos = d3.mouse(this)[0];
const x0 = bisect(data, xScale.invert(xPos));
const d0 = data[x0];
focus.attr(
'transform',
`translate(${xScale(d0.label)},${yScale(d0.value)})`,
);
tooltip
.transition()
.duration(300)
.style('opacity', 0.9);
tooltip
.html(d0.tooltipContent || d0.label)
.style(
'transform',
`translate(${xScale(d0.label) + 30}px,${yScale(d0.value) - 30}px)`,
);
}

When we move the mouse over the chart, the mousemove() function will be responsible for finding out the position of the cursor, figuring out the nearest plot point, and translating the tooltip as well as the circle marker to the nearest point.

xScale.invert takes a number from the scale’s range (i.e., the width of the chart) and maps it to the scale’s domain (i.e., a number between the values on the X-axis). Remember scaleLinear() above invert is, well, the invert of it. bisect helps us in finding the nearest point to the left of this invert point.

Lastly, we want to make sure we wipe off the old chart before plotting the new chart with the new data. So add this to the top of your drawChart() function:

d3.select('#container')
.select('svg')
.remove();
d3.select('#container')
.select('.tooltip')
.remove();

That’s it! Our line chart is ready. Obviously, you will want to add some more styling to your chart, but I will leave that up to you.

You can find the code to the above on GitHub. Thanks for reading!

--

--