How to create a custom SVG graph (no library)
Current tech
For my portfolio, I wanted to create a graph representing my GitHub contributions in another way than GitHub’s grid layout. I wanted to use libraries such as visx. However, visx at the time had major vulnerabilities coming from its dependencies and I was interested in understanding the concept behind such libraries.
Visx is using SVG only to graph. I find this approach very interesting, because using a Canvas, which is the case with over libraries requires client code. My goal was to have a fully rendered graph on page load without using any client code.
Visx also uses client code for responsiveness which I find very inconvenient.
What we are going to create
To keep things simple, we are going to create a simple graph with no y scale (even though it wouldn’t be difficult), no interaction on hover, and no animations. The curve will be smooth, and it would even be easier to create a point or line graph.
Understanding SVG
I am going to explain all concepts we will be needing here, but I recommend reading MDN page on SVG with examples, tutorials, and many concepts that will help you make any type of graph you want.
SVG elements
SVG code ressembles in all ways to html. This is using tags. All the content is encapsulated in a svg tag. So let’s start by writing our container.
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="200" height="100" stroke="blue" fill="transparent"/>
<circle cx="100" cy="50" r="40" stroke="red" fill="transparent" stroke-width="5"/>
</svg>
Here we can see the svg
tags surrounding some basic shapes. The viewBox
property here defines the width and height of the content without using the width and height property. This will be interesting for responsiveness later.
Inside these tags, all defined height, width, and coordinates in general depend only on the viewBox
property.
SVG comes with some primitives out of the box, such as the rect
, the circle
, and many more. For that, we refer to the MDN docs on SVG.
One interesting primitive will be the path
.
Path primitive
The path
primitive allows you to create any path you want with simple directives. It will be easier to understand if you have any experience with the Adobe Suite or Figma. Writing directives is a lot like using a pen on these softwares.
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
<path
d="
M 10 10
L 190 10
"
stroke="red"
/>
</svg>
Here are two simple directives that helps us draw a line. We start by moving the pen to coordinates (10, 10). Every directives should start by an M
directive. Also, any directive has an absolute version and a relative version. Uppercase mean absolute directives, so we are going to move to (10, 10) and then draw a line to (190, 10) (L
directive).
We can also use their lowercase version in order to have the same drawing (we have to start with uppercase M
anyway).
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
<path
d="
M 10 10
l 180 0
"
stroke="red"
/>
</svg>
The lowercase l
means that we move 180px on the x
axis from the point that we are.
Anchor points
If you already know what. anchor points are, it will be easier to understand. Anchor points are points allowing you to draw a curve. They have 2D coordinates and drag the line into a sort of bezier curve.
Anchor points are the 2 blue points and we can see that a curve following these points is then created. It uses the tangent of the line between the two points for the direction, and the line length for the “momentum” of the curve.
Curves
Now that we know what anchor points are, we can use the C
primitive. It is also declined in its relative version c
.
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
<path
d="
M 10 10
c 90 -10 90 90 190 80
"
stroke="red"
/>
</svg>
The c
directive specifies 3 coordinates. The first two are the two anchor points. The last one is the point to which we draw the curve.
Now, a graph is multiple curves that follows. In order to keep the anchor points consistent, we will use the the s
directive that only requires one anchor point and uses previous point for the other one.
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
<path
d="
M 0 50
s 50 -20, 100 -20
s 50 70, 100 70
"
stroke="red"
/>
</svg>
Here we have 3 points which are (0, 50), (100, 70), and (200, 0) which are connected by a curve. Because the first s
is not preceded by any curve, it only has one anchor point. For bot anchor point, we will to be at half the distance on x
and at the y
coordinate of the next point. This will create a curve with its tangent horizontal when at a given point. This is what will be used for the purpose of our graph.
Writing our graph SVG
For our graph, we will have 5 points which are equidistant. So they can be written as a list of number.
const points = [30, 50, 10, 80, 40]
Based on the last section, we will be using the s
primitive which helps us writing continuous curves using anchor points at half the distance on x
(here 25) and at the y
coordinate of the next point.
We have 5 points so the distance between each point is 50 for a 200 width, giving us this code.
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
<path
d="
M 0 70
s 25 -20, 50 -20
s 25 40, 50 40
s 25 -70, 50 -70
s 25 40, 50 40
"
stroke="red"
/>
</svg>
💡 Y coordinates are inverted. so $y = height - point$
Styling the graph
Now, we may want to have a better graph, we can change the color or simply add a color gradient. To add a fill we have to close the graph with V {graph height} H 0 Z
. V
draws a vertical line to the coordinate, H
an horizontal line and Z
closes the current path. so now we have :
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg">
<path
d="
M 0 70
s 25 -20, 50 -20
s 25 40, 50 40
s 25 -70, 50 -70
s 25 40, 50 40
V 100
H 0
Z
"
fill="url(#bg)"
/>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#0C66A8" stop-opacity="1"></stop><stop offset="100%" stop-color="#0C66A8" stop-opacity="0"></stop></linearGradient>
</defs>
</svg>
Automate graphing
The most interesting part will be automation as a stale graph isn’t very useful. So I created a simple helper function to create your graph.
const points = [30, 50, 10, 80, 40]
const height = 100
const width = 200
const precision = 2
console.log(getSVGPath(points, height, width, precision))
// M 0 70 s 25.00 -20, 50.00 -20 s 25.00 40, 50.00 40 s 25.00 -70, 50.00 -70 s 25.00 40, 50.00 40 V 100 H 0 Z
function getSVGPath(points, height, width, precision) {
const xSep = width / (points.length - 1)
const x = xSep.toFixed(precision)
const xAnchor = (xSep / 2).toFixed(precision)
let currentPoint = points[0]
const path = points.slice(1).map(p => {
const dy = currentPoint - p
currentPoint = p
return `s ${xAnchor} ${dy}, ${x} ${dy}`
})
return `M 0 ${height - points[0]} ${path.join(' ')} V ${height} H 0 Z`
}