The Front-End Testing of Data Visualizations

Clemens Anzmann
Empathy.co
Published in
9 min readSep 7, 2018

I’m a big fan of test-driven development and believe that if your application doesn’t have any tests, errors are inevitable as the application evolves. The front-end world has an exciting amount of test frameworks for all different types of testing. Just check out this amazing overview JavaScript testing by Vitali Zaidman.

Unfortunately, I have to admit that when working on data visualizations for the web, I never go much further than standard unit testing. This concerns me, because for data visualization in the browser, the position of certain DOM elements is directly linked with the value they represent. Width, height and positions of elements have to be precise, as dispositioned elements can invalidate the data and cause misinterpretation.

I often worry that this might one day happen to me: A sneaky bug causes visual elements to be misaligned, invalidating the data shown on the chart. If the chart itself still looks face-valid, this could take a while to be noticed. And unit tests alone won’t necessarily save me. When working on a complex visualization platform such as EmpathyBroker Insights app, I want to ensure I have full confidence in my tests when writing code.

How do I make sure my tests spot a bug in my chart?

To gain this confidence, I’ve tasked myself with investigating different front-end testing methods to test a simple data visualization I’ve created with d3.js. Not to test the d3 library and its drawing functions themselves, but rather to check that the final visualization project, it’s elements and interactions work as expected and display the correct information.

In order to do this, I created a simple chart and looked at all the different ways I could test it. Below is the simple chart I made. When launching, it shows the CTR trend for a fictional product and when hovering over the line, product details are displayed.

Normally, not having any axis and axis descriptions is very bad practise for a data visualization. But for this example I wanted to keep the code to a minimum.

Here’s what I discovered and what I learned when I tried to test this piece in different ways:

Unit Tests

Though unit tests alone won’t give me piece of mind, they are a fundamental part of my testing stack. Unit tests are the smallest pieces of tests in a project, testing specific functions for specific results. They have an exact purpose and don’t see the bigger picture. For example, to prevent my chart from being rendered when the data is invalid, I have a simple dataIsValid function, taking a data array and returning a boolean:

const dataIsValid = data => {
return Array.isArray(data) && data.length > 0;
};

This function can easily be tested with unit tests to make sure it works as expected:

describe('dataIsValid', () => {
it('recognises correct data', () => {
expect(dataIsValid([1, 2, 5])).toBeTruthy();
});
it('recognises missing data', () => {
expect(dataIsValid()).toBeFalsy();
});
it('recognises empty data', () => {
expect(dataIsValid([])).toBeFalsy();
});
});

This is a pretty standard testing method in any project.

Taking this further, here’s a function that renders a simple line chart:

import { line } from 'd3-shape';
import { scaleLinear } from 'd3-scale';
import { select } from 'd3-selection';
import { max, min } from 'd3-array';
const renderLine = (container, data) => {
if (!container) {
return;
}
if (!dataIsValid(data)) {
container.innerHTML = 'Missing or invalid data.';
return container;
}
const height = 200, width = 800, margin = 10; const xScale = scaleLinear()
.domain([0, data.length-1])
.range([0, width]);
const yScale = scaleLinear()
.domain([max(data), min(data)])
.range([margin, height - margin]);
const myLine = line()
.x((d, i) => xScale(i))
.y(d => yScale(d));
const svg = select(container)
.append('svg')
.attr('id', 'chart-svg')
.attr('width', width)
.attr('height', height);
const footer = select('#footer'); svg.append('path')
.attr('id', 'chart-line')
.datum(data)
.attr('fill', 'none')
.attr('stroke', '#b6c630')
.attr('stroke-width', 3)
.attr('d', myLine)
.on('mouseover', () => {
footer.style('display', 'block');
})
.on('mouseout', () => {
footer.style('display', 'none');
})
return container;
};

The function receives a DOM element and amends a line SVG element with certain attributes and mouse-events to it. In the end, it returns the entire element it was given. In a unit test, I can inspect the returned element and check that it contains the SVG elements and attributes I would expect:

describe('renderLine', () => {
it('does not render when data and container are missing', () => {
const resultContainer = renderLine(undefined, undefined);
expect(resultContainer).toBeUndefined();
});
it('does show message when data is empty', () => {
const resultContainer = renderLine(
document.createElement('div'),
[]
);
expect(resultContainer.innerHTML)
.toBe('Missing or invalid data');
});
it('does not render anything when container is missing', () => {
const resultContainer = renderLine(undefined, [1, 2, 5]);
expect(resultContainer).toBeUndefined();
});
it('does render expected line', () => {
const resultContainer = renderLine(
document.createElement('div'),
[1, 2, 5]
);
const resultSvg = resultContainer.childNodes[0];
expect(resultSvg.id).toBe('chart-svg');
expect(resultSvg.getAttribute('width')).toBe('800');
expect(resultSvg.getAttribute('height')).toBe('200');
const resultLine = resultSvg.childNodes[0];
expect(resultLine.id).toBe('chart-line');
expect(resultLine.getAttribute('d').length).toBeGreaterThan(0);
expect(resultLine.getAttribute('fill')).toBe('none');
expect(resultLine.getAttribute('stroke')).toBe('#b6c630');
expect(resultLine.getAttribute('stroke-width')).toBe('3');
});
});

All tests pass. So far so good!

Test output of the unit tests above executed with Karma on PhantomJS

Integration Tests

While unit tests are fundamental, it’s also necessary to test that all functions are connected with each other in the expected way and run together smoothly. That’s where integration tests come in. They care about the bigger picture and run a larger piece of code. They don’t care about specific functions but about the overall result. While unit tests run in isolation and mock external services, integration tests would query an API, database or use any other external service connected with the application. This is to ensure that all pieces works harmoniously together, rather than testing that individual functions work in isolation.

My main app.js entry-point file looks something like this:

import renderLine from './lineChart';
import data from './data';
const renderApp = () => {
renderLine(
document.getElementById('chart-container'),
data
);
};
const launchButton = document.getElementById('launch-button');
if (launchButton) {
launchButton.addEventListener('click', event => {
document.getElementById('main').style.display = 'block';
launchButton.style.display = 'none';
renderApp();
});
}

When clicking the button, the renderApp function gets called, displaying a line chart with external data in a specific container.

To test that all the moving parts are connected correctly, I’m calling this main renderApp function and then validate that the test-browser’s DOM looks as expected afterwards:

describe('renderApp', () => {
beforeEach(() => {
const myDiv = document.createElement('div');
myDiv.id = 'chart-container';
document.body.appendChild(myDiv);
});
afterEach(() => {
document.body.innerHTML = '';
});
it('does render chart expectedly' ,() => {
renderApp();
const resultSvg = document.getElementById('chart-svg');
const resultLine = document.getElementById('chart-line');
expect(resultSvg).toBeDefined();
expect(resultSvg.id).toBe('chart-svg');
expect(resultSvg.getAttribute('width')).toBe('800');
expect(resultSvg.getAttribute('height')).toBe('200');
expect(resultLine).toBeDefined();
expect(resultLine.id).toBe('chart-line');
expect(resultLine.getAttribute('d').length).toBeGreaterThan(0);
expect(resultLine.getAttribute('fill')).toBe('none');
expect(resultLine.getAttribute('stroke')).toBe('#b6c630');
expect(resultLine.getAttribute('stroke-width')).toBe('3');
});
});

If everything executes as it should, the tests pass:

Test output of the integration test above executed with Karma on PhantomJS

My example app is very basic and so this integration test seems pretty similar to the previous unit test. Differences to note are that in the previous unit test, we provided the container and data to the function and evaluated the functions return. In the integration test, we are evaluating the DOM of the test-browser and don’t provide test data but the actual data used by the project (which could come from a live database in a real world example).

There are many different ways of running integration tests on a project, depending on the project’s setup and framework. For me, the important part is that integration tests run the real thing, don’t use mocks, call high-level functions and check the overall results.

I’m using Jasmine for my unit and integration tests. A. Sharif wrote a great in-detail blog post especially about unit testing d3 code with Jasmine which you can fine here.

Other testing frameworks useful for this type of testing are Jest and AVA.

End-to-End

End-to-end tests load the final website in a browser and inspect the elements on the DOM and ensure that they are what they should be. This is not so different from what our earlier integration test does. But in addition to simply inspecting the present elements, end-to-end tests can interact with the browser elements, such as filling out forms and clicking buttons. I can then observe the resulting changes on the pages and ensure all user flows and interactions have the expected result. I used CasperJS for my end-to-end testing. This is how they look:

casper.test.begin('button is displayed', 1, function suite(test) {
casper.start('http://localhost:8080', function() {
test.assertAllVisible('#launch-button', 'launchbutton visible');
}).run(function() {
test.done();
});
});
casper.test.begin('Chart is shown on buttonclick', 3, function suite(test) {
casper.start('http://localhost:8080', function() {
this.click('#launch-button');
test.assertAllVisible('#chart-container', 'chartnode visible');
test.assertAllVisible('#chart-svg', 'chartsvg visible');
test.assertAllVisible('#chart-line', 'chartline visible');
}).run(function() {
test.done();
});
});
casper.test.begin('Footer displayed on line interaction', 1, function suite(test) {
casper.start('http://localhost:8080', function() {
this.click('#launch-button');
this.mouseEvent('mouseover', '#chart-line');
test.assertAllVisible('#footer', 'footer visible');
}).run(function() {
test.done();
});
});

I can test that the start screen shows the button, that clicking the button shows the chart and that hovering over the line will display the footer. If that is case, my tests pass:

Test output of the end to end test above run with CasperJS

Other very interesting end-to-end testing frameworks for JavaScript are Nightwatch.js and Cyrpress.io.

Visual Regression

Visual regression testing is a pretty nifty way of testing CSS and the overall layout of a page. It renders a page, takes screenshots of specified elements and then compares those screenshots to the screenshots taken before the changes were made. If there are differences, the test will fail and a comparison picture is created. At the beginning of a project, when the application’s UI is subject to a lot of change, it can be difficult to use. But when it comes to ensuring that a chart is consistently rendered the same way, this is a very useful tool. In particular for the problem of misaligned chart elements I mentioned at the beginning of this article, I believe visual regression testing can be real life saver.

For my visual regression tests, I’m using PhantomCSS, which is a plugin for CasperJS.

As stated previously, I am not interested in testing that d3 is drawing SVG paths correctly but I trust that the library works as expected. I want to ensure that chart elements are drawn in the correct position in relation to each other and only when the right actions are triggered.

This is what the test file looks like:

casper.test.begin( 'Line chart visual tests', function ( test ) {casper.start('http://localhost:8080').then(function () {
phantomcss.screenshot('body', 'overview screenshot');
phantomcss.screenshot('#launch-button', 'button screenshot');
});
casper.then(function () {
this.click('#launch-button');
phantomcss.screenshot('#main', 'chart screenshot');
phantomcss.screenshot('#title', 'title screenshot');
phantomcss.screenshot('#chart-line', 'line screenshot');
phantomcss.screenshot('#footer', 'footer screenshot');
});
casper.then(function () {
this.click('#launch-button');
this.mouseEvent('mouseover', '#chart-line');
phantomcss.screenshot('#main', 'chart screenshot');
phantomcss.screenshot('#title', 'title screenshot');
phantomcss.screenshot('#chart-line', 'line screenshot');
phantomcss.screenshot('#footer', 'footer screenshot');
});
casper.then(function () {
phantomcss.compareAll();
});
casper.run(function () {
casper.test.done();
});
});

Like in my earlier end-to-end tests, I can perform different interactions. But instead of analysing the DOM, PhantomCSS takes screenshots at every step. In the end, it compares the screenshots to the baseline screenshots of the very first run (if interface or chart layouts change, a new baseline screenshot has to be created first for the tests to pass again).

This is what a test run looks like:

Test output of the visual regression test above run with CasperJS

Let’s say because of a strange bug, the scale of the line chart changed slightly. Hard to spot for the human eye, but not for visual regression:

A picture of a failed PhantomCSS test run, overlaying the previous screenshot with a new screenshot where the scale of the line is wrong. Hence the two lines are slightly misaligned.

Obviously, visual regression testing of data visualizations is only possible when the data remains consistent. So when working with API data, it is important to test with consistent API query parameters in test queries and not to use changing live data.

Other interesting visual regression tools out there are Gemini, WebdriverCSS and Spectre, among many others.

Found you! (Image by Lucas DC)

After my analysis of this little project, I can conclude that data visualization projects are basically tested the in the same way all other front-end projects should be tested (data visualization projects aren’t fundamentally that different from any other web projects after all). However, if I have one big takeaway from this little undertaking, it is the usefulness of visual regression testing for data visualizations. This method of testing gives me a lot more confidence when working on a large complex data visualization project. I’m excited to investigate this type of testing further, to test the other available tools out there and to include them in our soon to be released EmpathyBroker Insights App.

--

--

Clemens Anzmann
Empathy.co

❤ code, music & especially data. Born in Aschaffenburg and working as a data visualisation engineer in London 📊