LINE CHART — HOW TO SHOW DATA ON MOUSEOVER USING D3.JS

Rajeev Pandey
14 min readApr 17, 2021

--

Before starting this blog post, let’s learn a bit more about data visualization. If you’re familiar with data analysis, then you must have encountered data visualization. It is a key part of data analysis. In simple terms, Data visualization is when you take raw data and present it visually using graphs, charts, and maps. It enables humans to detect trends and patterns in large data sets without wasting time and also gives meaning to complicated datasets and helps businesses identify the areas that require improvement and attention. With the rise of big data upon us, we need to be able to interpret increasingly larger batches of data in a quicker and more interactive way. We can use BI tools like Tableau, PowerBi, Superset, etc or we can use freely available data visualization libraries like D3.js, Highcharts, etc. D3.js is the default standard in data visualization libraries. In this blog post, I’ll be discussing more on interactivity in D3.js, specifically mouse events and how can you subscribe to and use these events in D3.

This blog is a continuation of my previous blog post, if you haven’t checked it yet, I would highly recommend going through the below-mentioned blog post before applying this concept.

CREATING SIMPLE LINE CHARTS USING D3.JS — PART 01

This article will help you explore the following core concepts of D3:

  • How to add an axis label to a chart using D3.js?
  • How to show data on Mouseover in d3.js?
  • How to draw a vertical line on mouseover using d3.js?
  • Adding interactivity to visualization using Dynamic Tooltip?

STEP 1: EVENTS IN D3

D3 supports built-in events and custom events. DOM Events are fired to notify code of “interesting changes” that may affect code execution. These can arise from user interactions such as using a mouse or resizing a window, changes in the state of the underlying environment, and other causes. Each event is represented by an object that is based on the Event interface, and may have additional custom fields and/or functions to provide information about what happened

d3.selection.on(type[, listener[, capture]]);

Event Listeners listen for specific events being triggered on specific DOM elements.We can bind an event listener to any DOM element using the d3.selection.on() method. The on() method adds an event listener to all selected DOM elements. The first parameter is an event type as a string such as “click”, “mouseover” etc. The second parameter is a callback function that will be executed when an event occurs and the third optional parameter capture flag may be specified, which corresponds to the W3C useCapture flag.

STEP 2: D3.JS MOUSE EVENTS

Let’s dive into more details about events that happen when the mouse moves between elements. To add these interactive features to our visualizations, we will need to use events. When adding an event to any element or node we need to set two things: the event type and the listener function.

The event type is what scenario the element is checking for, such as when the mouse comes onto or goes off of the element (onmouseenter and onmouseout respectively). The listener is the function that is called whenever the event is triggered. For example, our listener function could change the size and color of an element whenever the mouse goes over the element.

You can find an interesting demonstration of events in the W3Schools Website

  • The mouseover event triggers when the mouse pointer enters the div element, and its child elements.
  • The mouseenter event is only triggered when the mouse pointer enters the div element.
  • The onmousemove event triggers every time the mouse pointer is moved over the div element.

We have enough chit chat about events and listeners , lets focus on our goal now.

STEP 3: ADD Y-AXIS LABEL

When displaying our visualizations on a webpage, it may help our readers if our visualizations are interactive. We have already created a line chart in our previous article, so we just need to display a tooltip on hover or click. But before we add a tooltip, we should modify the existing code and add an axis Label for our Y-Axis — Internet Usage(GB)

const yAxisGenerator = d3.axisLeft().scale(yScale);
const yAxis = bounds.append("g").call(yAxisGenerator);
const yAxisLabel = yAxis
.append("text")
.attr("class", "y-axis-label")
.attr("x", -dimensions.boundedHeight / 2)
.attr("y", -dimensions.margin.left + 110)
.html("Internet Usage (GB)");

STEP 4: SETUP AN INTERACTION

Let’s create a tooltip for our Line. We will use mouseover effects to add interactivity to an SVG. The first thing we need to know where an event is triggered. We can use d3.event that contains the fields event.pathX and event.pathY which tells us the position of the mouse when the event was triggered, but this position is in relation to the entire HTML document. So while it may help us in some situations, it won’t help us figure out where the mouse is within our visualizations.

Instead of catching hover events for individual elements, we want to display a tooltip whenever a user is hovering anywhere on the chart. Therefore, we’ll want an element that spans our entire bounds. To set up interactions, let’s create a rectangle that covers our bounds and add our mouse event listeners to it. Here, we don’t need to define our ’s x or y attributes because they both default to 0.

const listeningRect = bounds
.append("rect")
.attr("class", "listening-rect")
.attr("width", dimensions.boundedWidth)
.attr("height", dimensions.boundedHeight)
.on("mousemove", onMouseMove)
.on("mouseleave", onMouseLeave);

This is definitely not what we wanted but we can fix this by creating a styles.css Sheet. Cascading Style Sheets (CSS) is a markup language responsible for how your web pages will look like. It controls the colors, fonts, and layouts of your elements. The basic idea behind CSS is to separate the structure of a document from the presentation of the document. HTML is meant for structure. It was never intended for anything else. All those attributes you add to style your pages were added later. Let’s create another file within the Same Blog 1 folder and name it Styles.css and add the below code. Now Blog 1 folder should show three files 1. Chart.js 2. Index.html 3. Styles.css.The best way to make the process as easy as possible is to plan ahead. I know, it’s a lot to ask, but it really will pay off later.

Here’s how you can use external CSS.External style sheets requires you to add <link> tag in the <head> section of your HTML document.The whole point of such a writeup is to let you establish a universal structure for your documents.

  1. Open your HTML page and locate <head> opening tag.
  2. Put the following code (highlighted in red )right after the <head> tag

Index.html

<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="./styles.css"></link>
<title>My Line Chart</title>
</head>
<body>
<div id="wrapper" class="wrapper">
<div id="tooltip" class="tooltip">
<div class="tooltip-date">
<span id="date"></span>
</div>

<div class="tooltip-Internet">
Internet Usage: <span id="internet"></span>
</div>

</div>

</div>
<script src="./../../d3.v5.js"></script>
<script src="./chart.js"></script>
</body>
</html>

During our build step, the compiler would search through that styles.css file that we’ve created

Styles.css

An external stylesheet is a standalone .css file that is linked from a web page. The advantage of external stylesheets is that it can be created once and the rules applied to multiple web pages. Should you need to make widespread changes to your chart design, you can make a single change in the stylesheet and it will be applied to all linked pages, saving time and effort.

Note: In the CSS, a class selector is a name preceded by a full stop (“.”) and an ID selector is a name preceded by a hash character (“#”).

.listening-rect {
fill: transparent;
}

Without all the extra HTML for styling, the structure of your document is much more readable making it easier to update without breaking the document. All of your CSS can be moved to a separate file making it easier to update your styles as well.

STEP 5: DROP LINE FOR A USER INTERACTION

Adding a vertical line to a D3 chart, that follows the mouse pointer. As a first step, we will add a line so that whenever someone hovers , they can a vertical drop line.

const xAxisLine = bounds
.append("g")
.append("rect")
.attr("class", "dotted")
.attr("stroke-width", "1px")
.attr("width", ".5px")
.attr("height", dimensions.boundedHeight);

STEP 6: SET UP OUR TOOLTIP VARIABLE

We have already set up three separate Div on our Index.html for holding our tooltip information.

  • Div ID — tooltip — Rectangle that shows the tooltip
  • Div ID — tooltip-date — Rectangle that shows the Date information
  • Div ID — tooltip-internet — Rectangle that shows the internet Usage

In order to show the tooltip next to an actual data point, we need to know which point we’re closest to. First, we’ll need to figure out what date we’re hovering over — how do we convert an x position into a date? So far, we’ve only used our scales to convert from the data space (in this case, JavaScript date objects) to the pixel space.

Thankfully, d3 scales make this very simple! We can use the same xScale() we’ve used previously, which has a .invert() method. .invert() will convert our units backward, from the range to the domain.

Let’s pass the x position of our mouse (mousePosition[0]) to the .invert() method of our xScale().

Excerpt From: Nate Murray. “Fullstack Data Visualization with D3”

function onMouseMove() {
const mousePosition = d3.mouse(this);
const hoveredDate = xScale.invert(mousePosition[0]);

When we call d3.mouse(container) we pass in the node that we want to have our event position related to. d3.mouse will return back an array of the x and y positions of where the current event was triggered.

  • d3.mouse(container) — Returns the location of the current event relative to the specified container. A container is an HTML node. Uses the same event that is in d3.event, so the event must have been registered by selection.on.

STEP 7: FIND THE CLOSEST POINT

The d3.scan() function is a built-in function in D3.js which scans the array linearly and returns the index of the minimum element according to the specified comparator. The function returns undefined when there are no comparable elements in the array.

Syntax:

d3.scan(array, comparator)

Parameters: This function accepts two parameters which are mentioned above and described below:

  • array: This mandatory parameter contains an array of elements whose minimum value is to be calculated and the respective index is to be returned.
  • comparator: This parameter is an optional parameter which specifies how the minimum element is to be obtained.

Return value: The function returns a single integer value denoting the index of the minimum element in the array based on the specified comparator.

Let’s try it out. Create a function to find the distance between the hovered point and a data point. We don’t care if the point is before or after the hovered date, so we’ll use Math.abs() to convert that distance to an absolute distance.

const getDistanceFromHoveredDate = (d) =>
Math.abs(xAccessor(d) - hoveredDate);
const closestIndex = d3.scan(dataset,(a, b) =>
getDistanceFromHoveredDate(a) - getDistanceFromHoveredDate(b));

//Grab the data point at that index
const closestDataPoint = dataset[closestIndex];
const closestXValue = xAccessor(closestDataPoint);
const closestYValue = yAccessor(closestDataPoint);

STEP 8 : FORMATTING TOOLTIP

We have completed the tough part. The next task is to adjust the display of the Tooltip based on our needs. The solution is effectively to create a tooltip div within our code, then attach the appropriate mouseover, and mousemove event functions to each of our bar chart divs to appropriately show/hide our custom tooltip div. When the tooltip is shown, we can easily grab the data we want to actually display as the text.

Formatting numbers is one of those things you don’t normally think about until an ugly “0.300004” appears on your axis labels. Also, maybe you want to group thousands to improve readability, and use fixed precision, such as “$1,240.10”. Or, maybe you want to display only the significant digits of a particular number. D3 makes this easy using a standard number format. The format() function in D3.js is used to format the numbers in different styles available in D3. It is the alias for locale.format. You can refer to the below link for better clarity
1. https://observablehq.com/@d3/d3-format
2. http://bl.ocks.org/zanarmstrong/05c1e95bf7aa16c4768e
3. https://bl.ocks.org/zanarmstrong/raw/ca0adb7e426c12c06a95/
4. https://www.ibm.com/docs/en/cmofz/10.1.0?topic=SSQHWE_10.1.0/com.ibm.ondemand.mp.doc/arsa0257.htm

const formatDate = d3.timeFormat("%B %A %-d, %Y");
tooltip.select("#date").text(formatDate(closestXValue));
const formatInternetUsage = (d) => `${d3.format(".1f")(d)} GB`;
tooltip.select("#internet").html(formatInternetUsage(closestYValue));
const x = xScale(closestXValue) + dimensions.margin.left;
const y = yScale(closestYValue) + dimensions.margin.top;
//Grab the x and y position of our closest point,
//shift our tooltip, and hide/show our tooltip appropriately
tooltip.style(
"transform",
`translate(` + `calc( -50% + ${x}px),` + `calc(-100% + ${y}px)` + `)`
);
tooltip.style("opacity", 1); tooltipCircle
.attr("cx", xScale(closestXValue))
.attr("cy", yScale(closestYValue))
.style("opacity", 1);
xAxisLine.attr("x", xScale(closestXValue));
}
function onMouseLeave() {
tooltip.style("opacity", 0);
tooltipCircle.style("opacity", 0);
}
// Add a circle under our tooltip, right over the “hovered” point

const tooltip = d3.select("#tooltip");
const tooltipCircle = bounds
.append("circle")
.attr("class", "tooltip-circle")
.attr("r", 4)
.attr("stroke", "#af9358")
.attr("fill", "white")
.attr("stroke-width", 2)
.style("opacity", 0);

Note: We want to use .html() here, to ensure that our degrees symbol will be parsed correctly.

In many cases, and where possible, it really is best practice to dynamically manipulate classes via the className property since the ultimate appearance of all of the styling hooks can be controlled in a single stylesheet. The critical additions are the Const tooltip = … block where we’re creating our tooltip itself, which is just a div that is hidden by default and positioned “absolute” all the elements on the page (using a high z-index value). Special thanks to Amelia Wattenberger for all her guidance

styles.css.wrapper {
position: relative;
}
.y-axis-label {
fill: black;
font-size: 1.4em;
text-anchor: middle;
transform: rotate(-90deg);
}
.listening-rect {
fill: transparent;
}
body {
display: flex;
justify-content: center;
padding: 5em 1em;
font-family: sans-serif;
}
.dotted {
stroke: #5c5c5c;
stroke-dasharray: 1 1;
fill: none;
}
.tooltip {
opacity: 0;
position: absolute;
top: -14px;
left: 0;
padding: 0.6em 1em;
background: #fff;
text-align: center;
line-height: 1.4em;
font-size: 0.9em;
border: 1px solid #ddd;
z-index: 10;
transition: all 0.1s ease-out;
pointer-events: none;
}
.tooltip:before {
content: "";
position: absolute;
bottom: 0;
left: 50%;
width: 12px;
height: 12px;
background: white;
border: 1px solid #ddd;
border-top-color: transparent;
border-left-color: transparent;
transform: translate(-50%, 50%) rotate(45deg);
transform-origin: center center;
z-index: 10;
}
.tooltip-date {
margin-bottom: 0.2em;
font-weight: 600;
font-size: 1.1em;
line-height: 1.4em;
}
Index.html<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="./styles.css"></link>
<title>My Line Chart</title>
</head>
<body>
<div id="wrapper" class="wrapper">

<div id="tooltip" class="tooltip">
<div class="tooltip-date">
<span id="date"></span>
</div>
<div class="tooltip-Internet">
Internet Usage: <span id="internet"></span>
</div>
</div>

</div>
<script src="./../../d3.v5.js"></script>
<script src="./chart.js"></script>
</body>
</html>Chart.jsasync function drawLineChart() {
//1. Load your Dataset
const dataset = await d3.csv("./../../Internet Usage.csv");
//Check the sample values available in the dataset
//console.table(dataset[0]);
const yAccessor = (d) => d.InternetUsage;
const dateParser = d3.timeParse("%d/%m/%Y");
const xAccessor = (d) => dateParser(d["Bill Date"]);
// Note : Unlike "natural language" date parsers (including JavaScript's built-in parse),
// this method is strict: if the specified string does not exactly match the
// associated format specifier, this method returns null.
// For example, if the associated format is the full ISO 8601
// string "%Y-%m-%dT%H:%M:%SZ", then the string "2011-07-01T19:15:28Z"
// will be parsed correctly, but "2011-07-01T19:15:28", "2011-07-01 19:15:28"
// and "2011-07-01" will return null, despite being valid 8601 dates.
//Check the value of xAccessor function now
//console.log(xAccessor(dataset[0]));
// 2. Create a chart dimension by defining the size of the Wrapper and Margin let dimensions = {
width: window.innerWidth * 0.8,
height: 600,
margin: {
top: 115,
right: 20,
bottom: 40,
left: 160,
},
};
dimensions.boundedWidth =
dimensions.width - dimensions.margin.left - dimensions.margin.right;
dimensions.boundedHeight =
dimensions.height - dimensions.margin.top - dimensions.margin.bottom;
// 3. Draw Canvas const wrapper = d3
.select("#wrapper")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height);
//Log our new Wrapper Variable to the console to see what it looks like
//console.log(wrapper);
// 4. Create a Bounding Box const bounds = wrapper
.append("g")
.style(
"transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
);
// 5. Define Domain and Range for Scales const yScale = d3
.scaleLinear()
.domain(d3.extent(dataset, yAccessor))
.range([dimensions.boundedHeight, 0]);
// console.log(yScale(100));
const referenceBandPlacement = yScale(100);
const referenceBand = bounds
.append("rect")
.attr("x", 0)
.attr("width", dimensions.boundedWidth)
.attr("y", referenceBandPlacement)
.attr("height", dimensions.boundedHeight - referenceBandPlacement)
.attr("fill", "#ffece6");
const xScale = d3
.scaleTime()
.domain(d3.extent(dataset, xAccessor))
.range([0, dimensions.boundedWidth]);
//6. Convert a datapoints into X and Y value
// Note : d3.line() method will create a generator that converts
// a data points into a d string
// This will transform our datapoints with both the Accessor function
// and the scale to get the Scaled value in Pixel Space
const lineGenerator = d3
.line()
.x((d) => xScale(xAccessor(d)))
.y((d) => yScale(yAccessor(d)));
//.curve(d3.curveBasis);
// 7. Convert X and Y into Path const line = bounds
.append("path")
.attr("d", lineGenerator(dataset))
.attr("fill", "none")
.attr("stroke", "Red")
.attr("stroke-width", 2);
//8. Create X axis and Y axis
// Generate Y Axis
const yAxisGenerator = d3.axisLeft().scale(yScale);
const yAxis = bounds.append("g").call(yAxisGenerator);
const yAxisLabel = yAxis
.append("text")
.attr("class", "y-axis-label")
.attr("x", -dimensions.boundedHeight / 2)
.attr("y", -dimensions.margin.left + 110)
.html("Internet Usage (GB)");
//9. Generate X Axis
const xAxisGenerator = d3.axisBottom().scale(xScale);
const xAxis = bounds
.append("g")
.call(xAxisGenerator.tickFormat(d3.timeFormat("%b,%y")))
.style("transform", `translateY(${dimensions.boundedHeight}px)`);
//10. Add a Chart Header wrapper
.append("g")
.style("transform", `translate(${50}px,${15}px)`)
.append("text")
.attr("class", "title")
.attr("x", dimensions.width / 2)
.attr("y", dimensions.margin.top / 2)
.attr("text-anchor", "middle")
.text("My 2020 Internet Usage(in GB)")
.style("font-size", "36px")
.style("text-decoration", "underline");
// 11. Set up interactions const listeningRect = bounds
.append("rect")
.attr("class", "listening-rect")
.attr("width", dimensions.boundedWidth)
.attr("height", dimensions.boundedHeight)
.on("mousemove", onMouseMove)
.on("mouseleave", onMouseLeave);
const xAxisLine = bounds
.append("g")
.append("rect")
.attr("class", "dotted")
.attr("stroke-width", "1px")
.attr("width", ".5px")
.attr("height", dimensions.boundedHeight);
//.style("transform", `translate(${0}px,${-5}px)`);
function onMouseMove() {
const mousePosition = d3.mouse(this);
const hoveredDate = xScale.invert(mousePosition[0]);
const getDistanceFromHoveredDate = (d) =>
Math.abs(xAccessor(d) - hoveredDate);
const closestIndex = d3.scan(
dataset,
(a, b) => getDistanceFromHoveredDate(a) - getDistanceFromHoveredDate(b)
);
const closestDataPoint = dataset[closestIndex];
console.table(closestDataPoint);
const closestXValue = xAccessor(closestDataPoint);
const closestYValue = yAccessor(closestDataPoint);
const formatDate = d3.timeFormat("%B %A %-d, %Y");
tooltip.select("#date").text(formatDate(closestXValue));
const formatInternetUsage = (d) => `${d3.format(".1f")(d)} GB`;
tooltip.select("#internet").html(formatInternetUsage(closestYValue));
const x = xScale(closestXValue) + dimensions.margin.left;
const y = yScale(closestYValue) + dimensions.margin.top;
//Grab the x and y position of our closest point,
//shift our tooltip, and hide/show our tooltip appropriately
tooltip.style(
"transform",
`translate(` + `calc( -50% + ${x}px),` + `calc(-100% + ${y}px)` + `)`
);
tooltip.style("opacity", 1); tooltipCircle
.attr("cx", xScale(closestXValue))
.attr("cy", yScale(closestYValue))
.style("opacity", 1);
xAxisLine.attr("x", xScale(closestXValue));
}
function onMouseLeave() {
tooltip.style("opacity", 0);
tooltipCircle.style("opacity", 0);
}
// Add a circle under our tooltip, right over the “hovered” point
const tooltip = d3.select("#tooltip");
const tooltipCircle = bounds
.append("circle")
.attr("class", "tooltip-circle")
.attr("r", 4)
.attr("stroke", "#af9358")
.attr("fill", "white")
.attr("stroke-width", 2)
.style("opacity", 0);
}
drawLineChart();

CONCLUSION

In this tutorial you’ve been introduced to mouse events which you can use in D3 visualizations. Mouse events are raised/triggered as a result of different user interactions. It is advisable to become familiar with the different mouse events available and their intended usage so that you can add an extra dimension to your data visualizations.

We covered a lot of ground here, but the core principles are relatively simple. In my next blog post, I’ll be covering how to create multiple line charts and add a legend. Once you get past the initial discomfort of learning the underpinning of D3 you can choose among thousands of chart types in D3 from D3 example galleries and modify them to your own needs.

We tried to cover as much as we could for a newbie to get started with D3.js. Hope you like it. As always, We welcome feedback and constructive criticism. If you enjoyed this blog, we’d love for you to hit the share button so others might stumble upon it. Please hit the subscribe button as well if you’d like to be added to my once-weekly email list, and don’t forget to follow Vizartpandey on Instagram!

Also, here are a few hand-picked articles for you to read next:

--

--

Rajeev Pandey

I’m Rajeev, 3 X Tableau Zen Master , 5 X Tableau Ambassador, Tableau Featured Author, Data Evangelist from Singapore