While working on the new Kinsta admin I wanted to create a lightweight, pure SVG chart that would allow us to plot a multitude of info easily. After going through multiple iterations and hitting some implementation issues we arrived at the final version, pictured below.

A green-blue line chart showing information about data transfer
A chart showing data transfer statistics for an account.

The SVG Chart Data Array

The pictured SVG chart uses a simple array of data that looks something like this:

let graphData = [
  {
    value: 52001,
    label: "Tue",
    time: 1488370332
  },
  {
    value: 64112,
    label: "Wed",
    time: 1488370332
  },
  {
    value: 58125,
    label: "Thu",
    time: 1488456732
  },
  {
    value: 5291,
    label: "Fri",
    time: 1488543132
  },
  {
    value: 31992,
    label: "Sat",
    time: 1488629532
  },
  {
    value: 28871,
    label: "Sun",
    time: 1488715932
  },
  {
    value: 57090,
    label: "Mon",
    time: 1488802332
  },
  {
    value: 2452,
    label: "Tue",
    time: 1488888732
  },
  {
    value: 22123,
    label: "Wed",
    time: 1488975132
  },
]

Note that the array contains nine items, even though the SVG chart only shows 7. The first item is where the line originates from; the line then disappears towards the last point.

Everything you see in the image is generated from this data, including large total display, the current selection text and the time range in the top right.

Getting The Point Coordinates

We’ll need the coordinates of each point for a few things so let’s start by figuring them out. Let’s assume that we’re creating a 500px wide and 150px high SVG chart.

The X coordinates are a bit easier. We have 7 points spaced equidistantly which means that the distance between each point is 500/7 which is 71.43. The first point would usually be at 0, or at 71.43 on the X-axis. In this example, I wanted to lay the points in the middle of the interval. All we need to do is shift everything 71.43/2 or 35.715 to the left.

Sliding to the left will put the point to the left of the canvas’ beginning: exactly what we need; we don’t want to show the first and last points. The first point will be at -35.715, the last will be at 535.715 – off canvas.

The Y coordinates depend on the height of the canvas – which is 150px – and the range of the data we need to display. We can find that by subtracting the lowest value from our array from the highest. That would be 1552182 - 1024392 or 527,790.

So we need to “spread” 527,790 worth of data over 150px. Each pixel distance represents an interval of 527790/150 or 3,518.6. Let’s call this the step interval.

If you’re used to graphs in maths, the norm is the 0,0 point in the bottom left. In SVG the default origin point is in the top left. Using 0 for the Y coordinate would represent our highest value: 527,790.

Let’s get the Y coordinate for 1,231,341. First, we need to deduct the minimum value – 1,024,392 – from it. We are only interested in the part of the number that falls within our interval since in our graph space 1,024,392 will be the zero point. We then divide it by the step interval to see how many pixels we need to move on the Y axis. Finally, we deduct this from the height – 150 – since we are moving down on the axis instead of up. The end result is 150 - ( (1231341 - 1024392) / 3518.6 ) orĀ 91.1843. Let’s turn all that into some Javascript to get the X and Y coordinates for all our points.

function getValueArray( graphData ) {
    return graphData.map(( item ) => {
        return item.value;
    })
}

function calculateSVGData( graphData, width, height ) {
    var values = getValueArray( graphData );
    return getCoordinates( values, width, height )
}


function getCoordinates( values, width, height ) {
    var min = Math.floor(Math.min.apply( null, values ) * 0.95)
    var max = Math.ceil(Math.max.apply( null, values ) * 1.05 )

    var yRatio = ( max - min ) / height
    var xRatio = width / ( values.length - 2 )

    return values.map( function( value, i ) {
        var y = height - ( ( value - min ) / yRatio );
        var x = ( xRatio * i ) - ( xRatio / 2 )
        return [x,y]
    })
}

var svgData = calculateSVGData( graphData, 500, 150 );
console.log(svgData);

If you log the value of svgData to the console, using our previous array of objects as graphData you should see the correct coordinates.

Note that when finding the minimum and maximum I multiplied them by 0.95 and 1.05 respectively. The lowest point of the graph will be 5% lower than the lowest value, the highest point of the graph will be 5% higher than the highest value.

An array of X and Y values for our SVG chart
Our calculations result in an array containing the SVG chart coordinates

Plotting these points on our actual SVG chart is pretty simple now. We need to create the SVG root element, map through the coordinates array and create a circle element for each point.

<svg width="500" height="150" id="svgChart"></svg>
<script>
var svgChart = document.getElementById('svgChart');

svgData.map(function( coordinates ){
    var point = document.createElementNS(
      "http://www.w3.org/2000/svg", 
      "circle"
    );
    point.setAttribute("cx",coordinates[0]);
    point.setAttribute("cy",coordinates[1]);
    point.setAttribute("r", 4);
    point.setAttribute("fill", "#5CC0C0");
    point.setAttribute("stroke", "#fff");
    point.setAttribute("stroke-width", 2);
    svgChart.appendChild(point);
})
</script>

So far we have a visually bland, but perfectly functioning SVG chart. I’ve given the circles their final styles, a blue-green color with a 2px white stroke.

Blue points plotted on our SVG chart
Points plotted on an SVG chart

Plotting A Line

Drawing a line through these coordinates is trivial – as long as you need straight lines. Let’s get it done just for fun.

var lineData = ""
svgData.map(function( coordinates, i ){
  var command = i === 0 ? "M" : "L";
  lineData = lineData
    + " "
    + command
    + " "
    + coordinates[0]
    + ","
    + coordinates[1]
})

var line = document.createElementNS(
  "http://www.w3.org/2000/svg",
  "path"
);
line.setAttribute("d", lineData);
line.setAttribute("fill", "none");
line.setAttribute("stroke", "#5CC0C0");
line.setAttribute("stroke-width", 5);
svgChart.appendChild(line);

We loop through our data points once again to generate the d property of the path. The very first point is prefixed with M which – in SVG – means “Go to this point”. Subsequent points are prefixed with L which means: “Draw a line to this point”.

Make sure to place this code above the code that generates our dots. This will lay the dots over our line, resulting in a nice graph like so:

A straight line drawn through all the points on our SVG chart
A straight line drawn through all the points on our SVG chart

Creating a curved line through the points is deceptively difficult. I looked at the math behind it and while it can be tackled, it’s not something I’d like to worry about troubleshooting. It involves derivatives, trigonometry, bezier curve theory and other niceties.

Enter d3 a JavaScript library for visualizing data. All we will be using it for is calculating the curved path between our points. In this example I’m using the full d3 library, in production (React) we’re only using the part of it we need. First, link to the script in the header:

<script src="https://d3js.org/d3.v4.min.js"></script>

The next step is writing a function to generate the d property of the path and placing the path as usual.

function getLineSVG( data ) {
  var lineGenerator = d3.line()
    .x(function(d) { return d[0]; })
    .y(function(d) { return d[1]; })
    .curve(d3.curveCardinal)

  return lineGenerator(data)
}

var line = document.createElementNS(
  "http://www.w3.org/2000/svg",
  "path"
);
line.setAttribute("d", getLineSVG(svgData));
line.setAttribute("fill", "none");
line.setAttribute("stroke", "#5CC0C0");
line.setAttribute("stroke-width", 5);
svgChart.appendChild(line);

Most of the documentation for this can be found in d3-shape. The line generator finds the x and y coordinates from the data we pass to it. d3.curveCardinal is where the magic happens. If you look at the linked page you’ll see some others you can use like linear and monotone. The result is a calculated curved line, thanks d3!

A curved line drawn through all the points on our SVG chart
A curved line drawn through all the points on our SVG chart

Drawing The Area Under The Curve

It would look a lot nicer if the are under the curve was shaded. To achieve this we’ll need another path. Our current one is open ended and produces the line. The one we’re about to create will be almost exactly the same, we’ll just close it off to create an area.

We need to add two more points to it: 535.715, 150 – the bottom right under our last off-canvas point and -35.715, 150 – the bottom left, under our initial off-canvas point. Finally, we’ll add the z command which means “close the path”.

var areaPoints = getLineSVG(svgData);
areaPoints = areaPoints
  + ' L' + svgData[svgData.length - 1][0] + ", " + 150
  + ' L' + svgData[0][0] + ", " + 150
  + ' z'

var area = document.createElementNS(
  "http://www.w3.org/2000/svg",
  "path"
);
area.setAttribute("d", areaPoints );
area.setAttribute("fill", "#f6f6f6");
svgChart.appendChild(area);

This should be the first thing appended to the SVG element to make sure it will become the layer furthest back. We want both the line and the circles to be above it.

The shaded area under the line in our SVG chart
The shaded area under the line in our SVG chart

Handling Hover Interactions

Adding interactions is a lot easier in React, but I wanted to showcase it here so we’ll do a quick and dirty implementation. We’ll be creating three more rectangles for each of our points, and a clip path which we’ll use to mask some of them.

Adding Mouseover Areas

I thought it would be easiest to create rectangles that cover each interval completely but have a transparent fill. They won’t be visible, but we can detect hover events on them.

var rectWidth = 500 / ( svgData.length - 2 );
svgData.map(function( coordinates, i ){
    if( i === 0 || i >= svgData.length - 1 ) { return }
    var rect = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "rect"
    );
    rect.setAttribute("width", rectWidth);
    rect.setAttribute("height", 150);
    rect.setAttribute("x", rectWidth * (i - 1));
    rect.setAttribute("y", 0);
    rect.setAttribute("fill", "transparent");
    rect.setAttribute('class', 'hoverArea');
    svgChart.appendChild(rect);
})

These should be the very last elements we prepend to our SVG chart. We’re creating seven rectangles that cover our seven segments top to bottom, left to right.

We’ll be attaching an event handler to each one. Within the even handler we’ll detect the index of the element, we’ll use this to figure out what other rectangles we need to display/hide to create our interaction.

function indexInClass(node, myClass) {
    var className = node.className;
    var num = 0;
    for (var i = 0; i < myClass.length; i++) {
        if (myClass[i] === node) {
            return num;
        }
        num++;
    }

    return -1;
}

var hoverAreas = document.getElementsByClassName("hoverArea");

for (var i = 0; i < hoverAreas.length; i++) {
    hoverAreas[i].addEventListener('mouseover', function( event ) {
        setSelection( indexInClass(event.target, hoverAreas) );
    });
    hoverAreas[i].addEventListener('mouseout', function() {
        removeSelection( indexInClass(event.target, hoverAreas) );
    });
}

When the user hovers over one of these areas a function named setSelection() will be fired and will receive the index of the selection as its parameter. When the user leaves an element the removeSelection() function fired and receives the index as its parameter.

Creating The Current Item Highlight

The current display consists of two parts. The part of the rectangle under the line is shaded blue-green. The part of the rectangle above the line is shaded grey.

The grey rectangle is a full rectangle which is under everything else. It is hidden by default and is set to be displayed only on hover. The shaded part is a full colored rectangle that uses the area path as a clip path, so all parts of it outside the path will be hidden.

svgData.map(function( coordinates, i ){
    if( i === 0 || i >= svgData.length - 1 ) { return }
    var rect = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "rect"
    );
    rect.setAttribute("width", rectWidth);
    rect.setAttribute("height", 150);
    rect.setAttribute("x", rectWidth * (i - 1));
    rect.setAttribute("y", 0);
    rect.setAttribute("fill", "#f6f6f6");
    rect.style.display = 'none';
    rect.setAttribute('class', 'selectionBackground');
    svgChart.appendChild(rect);
})

The code above should be the first thing appended to the SVG element to make sure only the parts of the rectangle above the line will be visible. The next section should be added before the line, dots and hover areas.

svgData.map(function( coordinates, i ){
    if( i === 0 || i >= svgData.length - 1 ) { return }
    var rect = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "rect"
    );
    rect.setAttribute("width", rectWidth);
    rect.setAttribute("height", 150);
    rect.setAttribute("x", rectWidth * (i - 1));
    rect.setAttribute("y", 0);
    rect.setAttribute("fill", "#5CC0C0");
    rect.setAttribute("clip-path", "url(#graphClipPath)");
    rect.style.display = 'none';
    rect.setAttribute('class', 'selectionForeground');
    svgChart.appendChild(rect);
})

Putting It All Together

All that remains is defining the setSelection() and removeSelection() functions. Whenever a user is hovering over item i we need to show the .selectionForeground and .selectionBackground elements with the same index.

function removeSelection(i) {
    var selectionBackgrounds = document.getElementsByClassName("selectionBackground");
    var selectionForegrounds = document.getElementsByClassName("selectionForeground");
    selectionBackgrounds[i].style.display = 'none'
    selectionForegrounds[i].style.display = 'none'
}

function setSelection(i) {
    var selectionBackgrounds = document.getElementsByClassName("selectionBackground");
    var selectionForegrounds = document.getElementsByClassName("selectionForeground");
    selectionBackgrounds[i].style.display = 'block'
    selectionForegrounds[i].style.display = 'block'
}

Once you’ve put it all together you should see a nice SVG chart with hover interactions like the gif below.

SVG Chart animation showing hover interactions
SVG Chart animation showing hover interactions

Conclusion

That concludes the difficult parts of the exercise, everything else you see in the original image is simple HTML and CSS. If you’d like to see the full example, feel free to download the single example HTML file.

The original implementation of this is in React which is a lot easier to digest once you get the hang of it. Either way, despite the seeming complexity, creating an SVG chart is actually quite easy.

If you’ve built something nice using just SVG please do let us know in the comments below, I’d love to take a look!