CSS Paint in Action: Bar Chart
In my previous article, we discovered CSS Paint API basics. Today, with this knowledge arsenal we are going to create data visualizations that could be used in production projects — bar chart. More than 65% of humans are visual learners. Visualization makes it easier to understand, compare, and analyze data. Using CSS Paint API, we can encapsulate all logic related to drawing charts on canvas and expose the high-level declarative interface.
The most uncomplicated graph we can create is a bar chart 📊. It is the set of rectangles, possibly with a different background color, and the size of each box represents its value compared to the others. Usually, the maximum dataset value is taken as 100% on the target axis. So in our example, we will use the range from zero to the maximum amount of the given dataset for values axis, as in 90% cases this is what we need to implement. On the secondary axis, we will linearly distribute our bars and separate them with the gap in pixels specified by the passed argument.
To create a custom painter we need to follow three easy steps: declare a custom paint class, register paint, and load worklet. So let’s start with the painter class definition:
So, for now, our
paint method does nothing 🤥, the only thing we declared is the static
inputProperties getter. This getter will return the list of CSS variable our painter relies on, that means that after each change of the value of these variables the browser will call the
Our first variable stands for the dataset and we will use it to draw the chart. My first thought was to use Custom Properties API from Houdini for them and use something like the
<color-stop> is the pair of percentage or length value and CSS color. Value and color are separated by spaces. The list of
<color-stop> value is used on the
linear-gradient function, to declare that we want to use a list of Typed OM values we need to add
+ at the end of type declaration –
<color-stop>+. Unfortunately, for now, doesn’t support lists and
Such an argument will cause
DOMException errors. After my initial idea failed, I decided to follow with a CSS variable with a special syntax. CSS variables are just strings that will be interpolated in the place that they are used.
Text “hello world” will be used as the value for
content property. That is completely equivalent to:
So I decided to use
color pairs delimited by a comma. Each number will represent the value for bar and color will be used for the background. This implementation has a big issue as we can’t use commas in color or value. I think it is ok for now to stay with that, as I wouldn’t want to complicate tutorial with
RexExp. Here how our dataset should look like:
After the input format decision has been made we can implement a helper method to parse it in our painter class:
Nothing special here, we pass our CSS variable input and transform it into the list of objects. Additionally, we added fallbacks for the value and color. Next, we will add the helper method to get the maximum value from the given dataset:
For the beginning let’s make vertical bars, other orientation support will be in our “To Do” list. We are going to implement the main —
Let’s take a look at this method line-by-line 🧐. First of all, we are trying to get the
--bar-gap variable and parse it as an integer. If there is no gap defined
props.get('--bar-gap') will return
null so we were providing a fallback value before calling
toString. Then we are parsing our dataset stored in the
--bar-map variable and getting the maximum value using helpers that we defined before. Then we are calculating how much height one value point should be by dividing the canvas height by the maximum value. After that, we are calculating the width of each bar. And finally, we are iterating our dataset and drawing rectangles for each of the values.
Now we are ready to register our paint:
Loading the worklet
So to have the ability to use our custom painter in the stylesheet, we need to load our worklet:
I called my file
paint.js and then loaded it using the
Math, but not
requestAnimationFrame. The idea behind Surma’s worklet embedding method is to write inline code inside the
script tag but with different a language type, to avoid its execution. Then we can convert it to
Blob objects and create an object URL from it. Here is the code example:
Using this trick, we avoid sending additional HTTP requests to load CSS Paint worklet.
After our painter is registered and loaded we can call it in the CSS:
--bar-gap variables and called the custom paint using
paint(bar-chart). We set a solid background color as well. Also, I have added a feature detection with
@supports rule. Using this if a browser doesn’t support the CSS Paint API will show an appropriate message to the user.
We have already created a bar chart MVP and now is the time to think about its improvements. Actually, there are a lot of points to improve, and I am not going to cover all of them. If you want to implement more feel free to fork my repository and do it, just don’t forget to share your ideas in the comments below and social networks (on Twitter please ping me).
There is a bug related to Typed OM that used as input properties in worklet. For the demos below
CSSUnitValue object without
value properties. The only way is to use
toString method on it. If you are facing any issues viewing demos, try to enable “Experimental Web Platform features” –
chrome://flags/#enable-experimental-web-platform-features to fix it.
So the first point I want to cover is to add the possibility to define offsets for our chart. And I’m going to use the
padding property for that. Here is the updated painter class:
Now we are using padding values to calculate bar width and height. Next, I want to add the possibility to change chart orientation. I will introduce the
--bar-placement variable with possible values: top, bottom, left and right. Here is the final code:
And the updated styles:
Change the input format
JSON.parse. Let’s look at stylesheet first:
So I defined a JSON array with data objects in my CSS! This rule looks strange but more verbose than the special string before. So now I can remove the
_parseData method from the painter class and use
requestAnimationFrame on mouse enter I animated the dataset from the initial value to the random data set and in opposite direction on mouse leave. Also, I’ve added the
--bar-max variable to set the range for the main axis from zero to this value.
After all, I’ve tried to go further and fetch the JSON file with the custom property of
<image> type. URL type doesn’t seem to work, it just got the string value from it. Such resource fetched exclusively when used as a value for properties accepting URLs. The image appears to work but doesn’t parse the JSON data as it has only mime types related to image formats in request headers.
requestAnimationFrame. I hope this was awesome, and as I still experimenting with Houdini APIs more exciting posts will come 😎. Let’s keep in touch 🤟!
Originally published at vitaliy-bobrov.github.io.