The Front-End Testing of Data Visualizations
By: Clemens Anzmann
Tags: D3js, Tdd, JavaScript, Data Visualization, Test Driven Development

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!