I loved Robin’s recent post, experimenting with CSS Grid for bar-charts. I’ve actually been using a similar approach on a client project, building a day-planner with CSS Grid. It’s a different use-case, but the same basic technique: using grid layouts to visualize data.
(I recommend reading Robin’s article first, since I’m building on top of his chart.)
Robin’s approach relies on a large Sass loop to generate 100 potential class-names, even though less than 12 are used in the final chart. In production we’ll want something more direct and performant, with better semantics, so I turned to definition lists and CSS Variables (aka Custom Properties) to build my charts.
Here’s the final result:
See the Pen Bar chart in CSS grid + variables by Miriam Suzanne (@mirisuzanne) on CodePen.
Let’s dig into it!
Robin was proposing a conceptual experiment, so he left out many real-life data and accessibility concerns. Since I’m aiming for (fictional) production code, I want to make sure it will be semantic and accessible. I borrowed the year-axis idea from a comment on Robin’s charts, and moved everything into a definition list. Each year is associated with a corresponding percentage in the list:
There are likely other ways to mark this up accessibly, but a dl seemed clean and clear to me – with all the data and associated pairs available as structured text. By default, this displays year/percentage pairs in a readable format. Now we have to make it beautiful.
I started from Robin’s grid, but my markup requires an extra row for the .date elements. I add that to the end of my grid-template-rows, and place my date/bar elements appropriately:
grid-template-rows: repeat(100, 1fr) 1.4rem;
/* fill the bottom row */
/* end before the bottom row */
Normally, I would use auto for that final row, but I needed an explicit height to make the background-grid work properly. Not not worth the trade-off, probably, but I was having fun.
Passing Data to CSS
At this point, CSS has no access to the relevant numbers for styling a chart. We have no hooks for setting individual bars to different heights. Robin’s solution involves individual class-names for every bar-value, with a Sass to loop to create custom classes for each value. That works, but we end up with a long list of classes we may never use. Is there a way to pass data into CSS more directly?
The most direct approach might be an inline style:
The start position is the full number of grid lines (one more than the number of rows, or 101 in this case), minus the total value of the given bar: 101 – 45 = 56. That works fine, but now our markup and CSS are tightly coupled. With CSS Variables, we can pass in raw data, and let the CSS decide how it is used:
In the CSS we can wire that up to grid-row-start:
We’ve replaced the class-name loop, and bloated 100-class output, with a single line of dynamic CSS. Variables also remove the danger normally associated with CSS-in-HTML. While an inline property like grid-row-start will be nearly impossible to override from a CSS file, the inline variable can simply be ignored by CSS. There are no specificity/cascade issues to worry about.
As a bonus, we can do more with the data than simply provide a grid-position – reusing it to style a fallback option, or even adjust the bar colors based on that same data:
background-image: linear-gradient(to right, green, yellow, orange, red);
background-size: 1600% 100%;
/* turn the start value into a percentage for position on the gradient */
background-position: calc(var(–start) * 1%) 0;
I started with a horizontal background gradient from green to yellow, orange, and then red. Then I used background-size to make the gradient much wider than the bar – at least 200% per color (800%). Larger gradient-widths will make the fade less visible, so I went with 1600% to keep it subtle. Finally, using calc() to convert our start position (1-100) into a percentage, I can adjust the background position left-or-right based on the value – showing a different color depending on the percentage.
The background grid is also generated using variables and background-gradients. Sadly, subpixel rounding makes it a bit unreliable, but you can play with the –line-every value to change the level of detail. Take a look around, and see what other improvements you can make!
Adding Scale [without Firefox]
Right now, we’re passing in a start position rather than a pure value (“56” for “45%”). That start position is based on an assumption that the overall scale is 100%. In order to make this a more flexible tool, I thought it would be fun to contain all the math, including the scale, inside CSS. Here’s what it would look like:
Then we can calculate the –start value in CSS, before applying it.
–start: calc(var(–scale) + 1 – var(–value));
With both the overall scale and individual values in CSS, we can manipulate either one individually. Change the scale to 200%, and watch the chart update accordingly:
See the Pen Bar Chart with Sale – no firefox by Miriam Suzanne (@mirisuzanne) on CodePen.
Both Chrome and Safari handle it beautifully, but Firefox seems unhappy about calc values in grid-positioning. I imagine they’ll get it fixed eventually. For now, we’ll just have to leave some calculations out of our CSS.
Sad, but we’ll get used to it. ?
There is much more we could do, providing fallbacks for older browsers – but I do think this is a viable option with potential to be accessible, semantic, performant, and beautiful. Thanks for starting that conversation, Robin!