Progressive Enhancement and Data Visualizations

The following is a guest post by Chris Scott. Chris has written for us before - always on the cutting edge of things. This time Chris shows us something new in the form of a new charting technique his company offers. But it's based on something old: the fundamentals of the web itself.

This month my company, Factmint, released its suite of Charts. They're really cool for a number of reasons (not least their design) but one is of particular interest to web developers: they all transform simple HTML tables into complex, interactive data visualizations. This approach is a perfect example of Progressive Enhancement - for browser compatibility, accessibility and SEO you have a simple table, for modern browsers a beautiful graphic.

I wanted to explore the techniques we used in a bit more detail. So here goes...

A recap on Progressive Enhancement

There are a few core concepts of PE. Here's the list on Wikipedia:

  • basic content should be accessible to all web browsers
  • basic functionality should be accessible to all web browsers
  • sparse, semantic markup contains all content
  • enhanced layout is provided by externally linked CSS
  • enhanced behavior is provided by unobtrusive, externally linked JavaScript
  • end-user web browser preferences are respected

Basically, implement a simple, cross-browser, pure HTML solution. Once that's done you have a safe minimum-functionality page. Now, build upon that with CSS and JavaScript.

This article is going to look at using these concepts to produce data visualizations.

Data visualizations are backed by data

It's painfully obvious but worth stating: data visualizations are based upon some underlying data. That data doesn't need to be lost when building a graphic (as it would be in a raster image, for example). Neither does the data format have to be JSON, as most charting libraries use.

Going back to the first three of the "core concepts" of PE, the basic functionality should be some kind of markup encoding that data. An HTML table or list, for example.

A working example

To illustrate the idea, we are going to Progressively Enhance a timeline towards an SVG visualization. The data might be something like this:

  • 1969: UNICS
  • 1971: UNIX Time-Sharing System
  • 1978: BSD
  • 1980: XENIX OS
  • 1981: UNIX System III
  • 1982: SunOS
  • 1983: UNIX System V
  • 1986: GNU (Trix)
  • 1986: HP-UX
  • 1987: Minix
  • 1989: NeXTSTEP
  • 1989: SCO UNIX
  • 1990: Solaris
  • 1991: Linux
  • 1993: FreeBSD
  • 1995: OpenBSD
  • 1999: Mac OS X

Basic content

There are a number of ways that this data could be encoded. I’m going to go with a definition list - I think that is semantically accurate and will display well without much styling. Let’s start with the base (no-enhancement) case:

<dl class="timeline">
  <dt>1969</dt><dd>UNICS</dd>
  <dt>1971</dt><dd>UNIX Time-Sharing System</dd>
  <dt>1978</dt><dd>BSD</dd>
  <dt>1980</dt><dd>XENIX OS</dd>
  <dt>1981</dt><dd>UNIX System III</dd>
  <dt>1982</dt><dd>SunOS</dd>
  <dt>1983</dt><dd>UNIX System V</dd>
  <dt>1986</dt><dd>GNU (Trix)</dd>
               <dd>HP-UX</dd>
  <dt>1987</dt><dd>Minix</dd>
  <dt>1989</dt><dd>NeXTSTEP</dd>
               <dd>SCO UNIX</dd>
  <dt>1990</dt><dd>Solaris</dd>
  <dt>1991</dt><dd>Linux</dd>
  <dt>1993</dt><dd>FreeBSD</dd>
  <dt>1995</dt><dd>OpenBSD</dd>
  <dt>1999</dt><dd>Mac OS X</dd>
</dl>

That looks something like this:

Unstyled DL in Chrome

Okay, so it's not pretty, but it will be clear and accessible on all browsers and it should be helpful for search engines, too.

Enhancing the layout

Now, let’s use a stylesheet to improve the layout. This could be taken much further, but, for the purpose of this article, let’s just make some simple improvements:

html {
  font-family: sans-serif;
}

dl {
  padding-left: 2em;
  margin-left: 1em;
  border-left: 1px solid;
  
  dt:before {
    content: '-';
    position: absolute;
    margin-left: -2.05em;
  }
}

Now it renders as:

That definitely looks more like a timeline but there are some clear problems with it. Most importantly, the timeline points should be distributed based upon their relative date (so 1983 and 1986 aren't next to each other). Also, I’d like the timeline to run horizontally to avoid the need for scrolling (in a production case I'd check for the best orientation).

Enhancing behaviour

Now for the fun bit. We are going to use externally linked, unobtrusive JavaScript to render an SVG timeline and replace the definition list with that. The final visualization will look something like this:

Objective: SVG timeline enhanced by JavaScript

Unobtrusive JavaScript

We are going to be producing an SVG graphic with this script, so the most important thing we can do - to keep the script unobtrusive - is to check that the browser supports SVG:

function supportsSvg() {
  return document.implementation &&
    (
      document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#Shape', '1.0') ||
      document.implementation.hasFeature('SVG', '1.0')
    );
}

That should give pretty accurate feature detection. Alternatively you could go for Modernizr.

Now, we will check for SVG support before we do anything: if the browser supports SVG we'll draw a pretty visualization and hide the definition list, otherwise we will leave the list in place.

if (supportsSvg()) {
  var timeline = document.querySelector('.timeline');
  timeline.style.display = 'none'; // We don't need to show the list
  // draw the graphic...
}

Extract the data

The key principle to this approach of PE data visualizations is that the data format is the semantic markup, so let’s parse our data…

function getDataFromDefinitionList(definitionList) {
  var children = definitionList.children;
  
  var yearIndex = {};
  var data = [];
  var currentYear = null;
  
  for (var childIndex = 0; childIndex < children.length; childIndex++) {
    var child = children[childIndex];
    
    if (child.nodeName == 'DT') {
      currentYear = child.innerText;
    } else if (child.nodeName == 'DD' && currentYear !== null) {
      if (! yearIndex[currentYear]) {
        yearIndex[currentYear] = data.length;
        data.push({
          year: currentYear,
          values: []
        });
      }
      
      data[yearIndex[currentYear]].values.push(child.innerText);
    }
  }
  
  return data;
}

There's quite a lot going on there but the essence is simple: iterate over the children, use the DTs as the year and the DDs as the releases. The fact that definition lists allow more than one DD for each DT makes it slightly more complex, hence the lookup to add additional releases from the same year to the same entry in the data array.

The output from that will be something like:

[
  {
    "year": "1969",
    "values": ["UNICS"]
  }
  ...
]

It's really useful to have an array, like this, as opposed to an object map. It will be much easier to iterate over the entries later.

Prepare the canvas

To draw this visualization we are going to use SnapSVG. It's an SVG drawing library by Dmitry Baranovskiy, who also wrote Raphael.js. First, we will need to create an SVG element:

var SVG_NS = 'http://www.w3.org/2000/svg';

function createSvgElement() {
  var element = document.createElementNS(SVG_NS, 'svg');
  element.setAttribute('width', '100%');
  element.setAttribute('height', '250px');
  
  element.classList.add('timeline-visualization');
  
  return element;
}

Snap can then wrap the element. Something like this:

var element = createSvgElement();
var paper = Snap(element);

Drawing the timeline

Now for the fun part!

We're going to write a method that iterates over our data object and draws onto the SVG element. Those two things (the data and the SVG element) will be the arguments:

function drawTimeline(svgElement, data) {
  var paper = Snap(svgElement);
  data.forEach(function(datum) {
    // draw the entry
  });
}

The simplest timeline would just draw a dot for each:

function drawTimeline(svgElement, data) {
  var paper = Snap(svgElement);
  var distanceBetweenPoints = 50;
  var x = 0;
  data.forEach(function(datum) {
    paper.circle(x, 200, 4);
    var x += distanceBetweenPoints;
  });
}

That should give 17 evenly distributed dots. But our main objective was to space the dots properly, so let's do something a little more interesting:

function drawTimeline(svgElement, data) {
  var paper = Snap(svgElement);
  
  var canvasSize = paper.node.offsetWidth;
  
  var start = data[0].year;
  var end = data[data.length - 1].year;
  
  // add some padding
  start--;
  end++;
  
  var range = end - start;
  
  paper.line(0, 200, canvasSize, 200).attr({
    'stroke': 'black',
    'stroke-width': 2
  });
  
  data.forEach(function(datum) {
    var x = canvasSize * (datum.year - start) / range;
    
    paper.circle(x, 200, 6);
  });
}

Cool: now our dots are distributed and there's a line underneath them. No information yet, though, so let's add some labels.

function drawTimeline(svgElement, data) {
  var paper = Snap(svgElement);
  
  var canvasSize = paper.node.offsetWidth;
  
  var start = data[0].year;
  var end = data[data.length - 1].year;
  
  // add some padding
  start--;
  end++;
  
  var range = end - start;
  
  paper.line(0, 200, canvasSize, 200).attr({
    'stroke': 'black',
    'stroke-width': 2
  });
  
  data.forEach(function(datum) {
    var x = canvasSize * (datum.year - start) / range;
    
    paper.circle(x, 200, 6);
    
    paper.text(x, 230, datum.year).attr({
      'text-anchor': 'middle'
    });
    
    var averageIndex = (datum.values.length - 1) / 2;
    var xOffsetSize = 24;
    datum.values.forEach(function(value, index) {
      var offset = (index - averageIndex) * xOffsetSize;
      
      paper.text(x + offset, 180, value)
        .attr({
          'text-anchor': 'start'
        })
        .transform('r -45 ' + (x + offset) + ' 180');
    });
  });
}

Dealing with years that have more than one entry has a little more complexity, but nothing we can't handle: the datum.values.forEach loop was used to spread duplicates out horizontally, centered around the dot. A rotation has also been applied to stop the labels from overlapping (even though that may be considered bad practice as it adds Cognitive Load - a better solution would be to show key releases always and others on hover but that's not the point of the article).

Finally, let’s add a little style for the SVG elements:

svg.timeline-visualization {
  circle {
    fill: white;
    stroke: black;
    stroke-width: 2;
  }
}

Bringing it all together

Now we just have to stitch our components together in the if-statement:

if (supportsSvg()) {
  var timeline = document.querySelector('.timeline');
  
  timeline.style.display = 'none';
  
  var data = getDataFromDefinitionList(timeline);
  
  var svgElement = createSvgElement();
  timeline.parentNode.insertBefore(svgElement, timeline);
  
  drawTimeline(svgElement, data);
}

Here's the complete code in a Pen:

See the Pen gbYqRW by chrismichaelscott (@chrismichaelscott) on CodePen.

Summary

There are lots of benefits with this approach to data visualizations; the markup is semantic, it's SEO friendly, it's accessible to screen-readers and it is progressively enhanced from a simple element supported on most browsers.

There are some things to consider though. If you only have one set of static data, it's probably not worth the effort - just build an SVG by hand. If you have loads and loads of data this might not be the right approach either, as traversing the DOM tree may not be efficient enough.

If you are trying to do standard charts, like pies, doughnuts, bubble, lines, etc, it's definitely worth checking out Factmint Charts. They are really beautiful and we've put a lot of thought into their design.