{"id":273668,"date":"2018-08-13T06:57:13","date_gmt":"2018-08-13T13:57:13","guid":{"rendered":"http:\/\/css-tricks.com\/?p=273668"},"modified":"2018-09-26T07:36:15","modified_gmt":"2018-09-26T14:36:15","slug":"simple-interactive-pie-chart-with-css-variables-and-houdini-magic","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/simple-interactive-pie-chart-with-css-variables-and-houdini-magic\/","title":{"rendered":"Simple Interactive Pie Chart with CSS Variables and Houdini Magic"},"content":{"rendered":"
I got the idea for doing something of the kind when I stumbled across this interactive SVG pie chart<\/a>. While the SVG code is as compact as it gets (a single I managed to get the following result using a single HTML element for the chart and very little JavaScript. The future should completely eliminate the need for any JavaScript, but more on that later.<\/p>\n <\/p>\n Some of you may remember Lea Verou’s Missing Slice<\/a> talk—my solution is based on her technique. This article dissects how it all works, showing what we can do in terms of graceful degradation and other ways this technique can be put to use.<\/p>\n We use Pug to generate the HTML from a We make all our elements reside in a The above Pug code compiles to the following HTML:<\/p>\n Note that we also made sure only the first radio I normally don’t like putting styles in HTML but, in this particular case, it’s a very useful way to pass custom property values to the CSS and ensure that we only need to update things in one place if we need to change any of our data points—the Pug code. The CSS remains the same.<\/p>\n The trick is to set a unitless percentage We use this percentage for a Note that this requires native Update<\/strong>: Chrome 69+ now supports We now have a working skeleton of our demo—picking a different year via the radio buttons results in a different The next step is to actually display the current value and we do this via a pseudo-element. Unfortunately, number-valued CSS variables cannot be used for the value of the We’ve also tweaked the We don’t want abrupt changes between values, so we smooth things out with the help of a CSS Note that using Also note that explicitly setting This is all the JavaScript we need for this demo and, in the future, we shouldn’t even need this much as we’ll be able to register custom properties from the CSS<\/a>.<\/p>\n With that out of the way, we can add a And that’s it for the functionality! All done with one element, one custom variable, and a sprinkle of Houdini magic!<\/p>\n While our demo is functional, it looks anything but pretty at this point. So, let’s take care of that while we’re at it!<\/p>\n Since the presence of We also want to display the percentage value in the middle of the dark pie slice.<\/p>\n In order to do this, we first position it dead in the middle of the This means the top-left corner of Remember that Next, we See the Pen<\/a> by thebabydino (@thebabydino<\/a>) on CodePen<\/a>.<\/p>\n What we need to figure out is how much to rotate the Initially, the x<\/var>-axis is horizontal, pointing towards the right, so in order to have it in the desired direction, we first need to rotate it so that it points up and going along the starting edge of the slice. Then it needs to rotate clockwise by half a slice.<\/p>\n In order to get the axis to point up, we need to rotate it by See the Pen<\/a> by thebabydino (@thebabydino<\/a>) on CodePen<\/a>.<\/p>\n Next, we need to rotate it by half a slice.<\/p>\n See the Pen<\/a> by thebabydino (@thebabydino<\/a>) on CodePen<\/a>.<\/p>\n But how much is half a slice?<\/p>\n Well, we already know what percentage of the pie this slice represents: it’s our custom property, After the This means we apply the following The last translation is by a quarter of Now the value label is positioned where we want it to be:<\/p>\n However, there’s still one problem: we don’t want it to be rotated, as that can look really awkward and neck-bending at certain angles. In order to fix this, we revert the rotation at the end. To make things easier for ourselves, we store the rotation angle in a CSS variable that we call Much better!<\/p>\n We want to have the whole assembly in the middle of the screen, so we solve this with a neat little grid trick<\/a>:<\/p>\n Alright, this puts the entire The next step is to place the pie chart above the radio buttons. We do this with a flexbox layout on the …or more accurately, we’re styling the radio button labels<\/em> because the first thing that we do is hide the radio inputs:<\/p>\n Since this leaves us with some very ugly labels that are very hard to distinguish from one another, let’s give each one some We’ve also blown up the <circle><\/code> element!), using strokes for creating pie chart slices is problematic as we run into rendering issues<\/a> on Windows for Firefox and Edge. Plus, in 2018, we can accomplish a lot more with a lot less JavaScript!<\/p>\n
The HTML<\/h3>\n
data<\/code> object that contains unitless percentage values for the past three years:<\/p>\n- var data = { 2016: 20, 2017: 26, 2018: 29 }<\/code><\/pre>\n.wrap<\/code> element. Then, we loop through our data<\/code> object and, for each of its properties, we create a radio input<\/code> with a corresponding label<\/code>. After these, we add a .pie<\/code> element to the mix.<\/p>\n- var darr = [], val;\r\n\r\n.wrap\r\n - for(var p in data) {\r\n - if(!val) val = data[p];\r\n input(id=`o${p}` name='o' type='radio' checked=val == data[p])\r\n label(for=`o${p}`) #{p}\r\n - darr.push(`${data[p]}% for year ${p}`)\r\n - }\r\n .pie(aria-label=`Value as pie chart. ${darr.join(', ')}.` \r\n role='graphics-document group')<\/code><\/pre>\n<style><div class=\"wrap\">\r\n <input id=\"o2016\" name=\"o\" type=\"radio\" checked=\"checked\"\/>\r\n <label for=\"o2016\">2016<\/label>\r\n <input id=\"o2017\" name=\"o\" type=\"radio\"\/>\r\n <label for=\"o2017\">2017<\/label>\r\n <input id=\"o2018\" name=\"o\" type=\"radio\"\/>\r\n <label for=\"o2018\">2018<\/label>\r\n <div class=\"pie\" aria-label=\"Value as pie chart. 20% for year 2016, 26% for year 2017, 29% for year 2018.\" role=\"graphics-document group\"><\/div>\r\n<\/div><\/code><\/pre>\ninput<\/code> is checked.<\/p>\nPassing custom properties to the CSS<\/h3>\n
--p<\/code> on the .pie<\/code> element for every radio input<\/code> that might be checked:<\/p>\nstyle\r\n - for(var p in data) {\r\n | #o#{p}:checked ~ .pie { --p: #{data[p]} }\r\n - }<\/code><\/pre>\nconic-gradient()<\/code> on the .pie<\/code> element after making sure neither of its dimensions (including border<\/code> and padding<\/code>) are 0<\/code>:<\/p>\n$d: 20rem;\r\n\r\n.wrap { width: $d; }\r\n\r\n.pie {\r\n padding: 50%;\r\n background: conic-gradient(#ab3e5b calc(var(--p)*1%), #ef746f 0%);\r\n}<\/code><\/pre>\nconic-gradient()<\/code> support since the polyfill<\/a> doesn’t work with CSS variables. At the moment, this limits support to Blink browsers with the Experimental Web Platform features<\/strong> flag enabled, though things are bound to get better.<\/p>\n
conic-gradient()<\/code> natively without the flag as well.<\/p>\nconic-gradient()<\/code>!<\/p>\n
Displaying the value<\/h3>\n
content<\/code> property, so we get around this by using the counter()<\/code> hack.<\/p>\n.pie:after {\r\n counter-reset: p var(--p);\r\n content: counter(p) '%';\r\n}<\/code><\/pre>\ncolor<\/code> and font-size<\/code> properties so that our pseudo-element is a bit more visible:<\/p>\n
Smoothing things out<\/h3>\n
transition<\/code>. Before we can transition or animate the --p<\/code> variable, we need to register it in JavaScript:<\/p>\nCSS.registerProperty({\r\n name: '--p', \r\n syntax: '<integer>',\r\n initialValue: 0, \r\n inherits: true\r\n});<\/code><\/pre>\n<number><\/code> instead of <integer><\/code> causes the displayed value to go to 0<\/code> during the transition<\/code> as our counter needs an integer. Thanks to Lea Verou for helping me figure this out!<\/p>\ninherits<\/code> is mandatory. This wasn’t the case until recently.<\/p>\ntransition<\/code> on our .pie<\/code> element.<\/p>\n.pie {\r\n \/* same styles as before *\/\r\n transition: --p .5s;\r\n}<\/code><\/pre>\n
Prettifying touches<\/h3>\n
Making the pie… a pie!<\/h4>\n
:after<\/code> has increased the height<\/code> of its .pie<\/code> parent, we absolutely position it. And since we want our .pie<\/code> element to look more like an actual pie, we make it round with border-radius: 50%<\/code>.<\/p>\n
.pie<\/code> element. By default, the :after<\/code> pseudo-element is displayed after its parent’s content. Since .pie<\/code> has no content in this case, the top-left corner of the :after<\/code> pseudo-element is in the top-left corner of the parent’s content-box<\/code>. Here, the content-box<\/code> is a 0x0<\/code> box in the center of the padding-box<\/code>. Remember that we’ve set the padding<\/code> of .pie<\/code> to 50%<\/code>—a value that’s relative to the wrapper width<\/code> for both the horizontal and the vertical direction!<\/p>\n:after<\/code> is in the middle of its parent, so a translate(-50%, -50%)<\/code> on it shifts it to the left by half its own width<\/code> and up by half its own height<\/code>, making its own mid-point coincide with that of .pie<\/code>.<\/p>\n%<\/code>-valued translations are relative<\/a> to the dimensions of the element they’re applied on<\/em> along the corresponding axis. In other words, a %<\/code>-valued translation along the x<\/var>-axis is relative to the element’s width<\/code>, a %<\/code>-valued translation along the y<\/var>-axis is relative to its height<\/code> and a %<\/code>-valued translation along the z<\/var>-axis is relative to its depth, which is always 0<\/code> because all elements are flat two-dimensional boxes with 0<\/code> depth along the third axis.<\/p>\n
rotate<\/code> the value such that the positive half of its x<\/var>-axis splits the dark slice into two equal halves and then translate<\/code> it by half a pie radius along this now-rotated x<\/var>-axis.<\/p>\n:after<\/code> pseudo-element so that its x<\/var>-axis splits the dark slice into two equal halves. Let’s break that down!<\/p>\n-90deg<\/code>. The minus sign is due to the fact that positive values follow a clockwise direction and we’re going the other way.<\/p>\n--p<\/code>. If we divide that value by 100<\/code> and then multiply it by 360deg<\/code> (or 1turn<\/code>, it doesn’t matter what unit is used), we get the central angle of our dark slice.<\/p>\n-90deg<\/code> rotation, we need to rotate :after<\/code> by half this central angle in the clockwise (positive) direction.<\/p>\ntransform<\/code> chain:<\/p>\ntranslate(-50%, -50%) rotate(calc(.5*var(--p)\/100*1turn - 90deg)) translate(.25*$d);<\/code><\/pre>\n$d<\/code>, which is the wrapper width<\/code> and gives us the .pie<\/code> diameter as well. (Since the content-box<\/code> of .pie<\/code> is a 0x0<\/code> box, it has no border<\/code> and both its left and right padding<\/code> are 50%<\/code> of its wrapper parent width<\/code>.) The .pie<\/code> radius is half its diameter, meaning that half the radius is a quarter of the diameter ($d<\/code>).<\/p>\n
--a<\/code>:<\/p>\n--a: calc(.5*var(--p)\/100*1turn - 90deg);\r\ntransform: \r\n translate(-50%, -50%) \r\n rotate(var(--a)) \r\n translate(.25*$d) \r\n rotate(calc(-1*var(--a)));<\/code><\/pre>\n
Layout<\/h4>\n
body {\r\n display: grid;\r\n place-items: center center;\r\n margin: 0;\r\n min-height: 100vh\r\n}<\/code><\/pre>\n.wrap<\/code> element in the middle:<\/p>\n
.wrap<\/code> element:<\/p>\n.wrap {\r\n display: flex;\r\n flex-wrap: wrap-reverse;\r\n justify-content: center;\r\n width: $d;\r\n}<\/code><\/pre>\n
Styling the radio buttons<\/h4>\n
[type='radio'] {\r\n position: absolute;\r\n left: -100vw;\r\n}<\/code><\/pre>\n
margin<\/code> and padding<\/code> so they don’t look so crammed together, plus backgrounds so that their clickable areas are clearly highlighted. We can even add box and text shadows for some 3D effects. And, of course, we can create a separate case for when their corresponding inputs are :checked<\/code>.<\/p>\n$c: #ecf081 #b3cc57;\r\n\r\n[type='radio'] {\r\n \/* same as before *\/\r\n\r\n + label {\r\n margin: 3em .5em .5em;\r\n padding: .25em .5em;\r\n border-radius: 5px;\r\n box-shadow: 1px 1px nth($c, 2);\r\n background: nth($c, 1);\r\n font-size: 1.25em;\r\n text-shadow: 1px 1px #fff;\r\n cursor: pointer;\r\n }\r\n\t\r\n &:checked {\r\n + label {\r\n box-shadow: inset -1px -1px nth($c, 1);\r\n background: nth($c, 2);\r\n color: #fff;\r\n text-shadow: 1px 1px #000;\r\n }\r\n }\r\n}<\/code><\/pre>\nfont-size<\/code> a bit and set a border-radius<\/code> to smooth out the corners:<\/p>\n
Final prettifying touches<\/h4>\n