Building a line graph with CSS clip-mask
This isn’t a substitute for d3 or chart.js. It is however, a surprisingly simple and effective way of creating a line graph.
I’ve used this clip-path
technique when I want to show a basic updating worm of data but don’t want to add an extra library.
I’m using a bunch of stubbed data here to illustrate, and JavaScript to render some of the UI but the crucial thing to understand is CSS. With a little calc()
and the use of clip-mask
you can easily create a line graph (or worm as they are sometimes known). The premise is simple, a clip-mask clips a rectangle with a series of X/Y coordinates. Each bringing the shape ‘in’ from it’s edges. The best way to understand how the syntax works is to take a look at Bennet Feely’s excellent clippy.
Here is a basic implementation:
Example
See the Pen Untitled by Ben Frain (@benfrain) on CodePen.
Specifics of the technique
And here is the clip-mask creating that line chart:
clip-path: polygon(
0% 60%,
20% 90%,
40% 43.33%,
60% 61.67%,
80% 23.33%,
100% 18.33%,
100% calc(18.33% - 1px),
80% calc(23.33% - 1px),
60% calc(61.67% - 1px),
40% calc(43.33% - 1px),
20% calc(90% - 1px),
0% calc(60% - 1px)
);
The key thing to achieve this effect is understanding that to create a ‘line’, we want to run once along our data to bring our mask in from the top, and then, assuming we want a line and not a filled shape (also possible), we want to bring our mask in from the bottom by going over the positions again, exactly the same in reverse, albeit 1px shorter, made easy with calc()
, in the vertical axis. You can see this clearly when looking at the mask above, where it is laid out one position at a time.
Implementation
How you manipulate your data to create this string is up to you. For this basic example, from a basic array of data shaped like this:
let values = [
{
time: 1,
value: 144,
},
{
time: 2,
value: 126,
},
// ... more
];
I have used a function like so:
function makeWorm() {
let duration = 150;
let generatePointX = (data) => (100 / duration) * data.time;
let generatePointY = (data) => ((data.value - 120) * 100) / (180 - 120);
let maskString = `clip-path: polygon(`;
values.map((data) => {
maskString += `${generatePointX(data).toFixed(2)}% ${(
100 - generatePointY(data)
).toFixed(2)}%, `;
});
values.toReversed().map((data, i, { length }) => {
maskString += `${generatePointX(data).toFixed(2)}% calc(${(
100 - generatePointY(data)
).toFixed(2)}% - 1px)`;
if (i + 1 !== length) {
maskString += `, `;
}
});
maskString += `);`;
return maskString;
}
worm.style = makeWorm();
There are a couple of hard coded values in there for the vertical points, based upon the values I have in my data. You would likely want to compute the bottom and top extremes of your chart based upon the highest and lowest values in your own data.
There are likely cleaner ways of generating the string too but this works; looping over the array of data, first one way for the ‘top’ positions of the mask, and then reversing through the array to generate the ‘bottom’ coordinates.
Summary
This way of creating a line chart is very basic but very light. Sure, we can do this kind of thing with the more suitable SVG but I found it a fun application of clip-mask
. It also isn’t ‘transition-able’, so if you were hoping of getting it cleanly animating in this manner, I’m afraid you’re out of luck.
Improvements? Examples of clip-mask graphs you’ve made yourself? Let me know in the comments below.