Making segmented circles and pie charts in CSS

Circles, made with CSS

Saturday 14th of May 2016

It's been possible to make circles, semi circles and quarter circles using only CSS ever since the border radius property came along. But what if you wanted to make a circle with more than four segments?

I've seen it done but only occasionally and in a very fixed way. If there was a way of generating a single segment of a circle based on a mathematical formula, would it be possible to write a CSS mixin to generate a circle containing any number of required segments? Short answer: yes.

But I'm getting a bit ahead of myself. First let's look at how we'd traditionally generate part of a circle with CSS. We apply a border radius to one of the corners of an element that either exceeds the width and height of the element, or set it to 100%, like this.

Border radius applied to one corner of a circle.
Applying border radius to the top left of this square element.

This works well, but it won't help us make a smaller segment. For that we need CSS transforms, specifically the skew transformation.

Skewing the applied border radius.
Skew the element to form a tighter segment.

Now we've got the segment we want, but the outside edge of our segment doesn't look like the outer edge of a circle anymore. The solution? Put another element inside the skewed element with the same border radius and hide the overflow.

Skewed element containing unskewed element.
Add in a new element. The overlapping orange part is the bit we're interested in.

In order to achieve this in CSS we have to apply the opposite skew to the inner element to what has been applied to the outer. We'd also take the background colour off the outer and apply it only to the inner, with the result that only the orange segment would be seen.

Now we have our first segment, but we want to have a full circle. Surely we can take a copy of this segment and just apply another transformation, a rotation, to achieve this? Almost, but not quite. If we rotate the outer element now, because of the skew already applied to it, really weird unhelpful things start to happen. The best solution is to create a new parent element and rotate that.

Rotating our segment.
Take the original element, position it absolutely within a tiny element, then rotate that element.

We've got a lot of elements in our circle by now, which is starting to feel a bit clunky, but I'll come back to that later. Right now we have to apply a little bit of cleverness to these new parent elements. They have to be a tiny fraction of the overall circle size and positioned absolutely in the very centre so that the rotation can happen cleanly, but the segments within them still have to be the size of the overall circle. If the rotated parents are 2% of the size of the circle and positioned 49% from the top and left of it, the segments inside need a width and height of 5000% to fit the outer circle.

Overflow also has to be handled carefully - the inner (yellow) segments have overflow hidden, but the more they're skewed the more they stick out of the circle, so we also need to apply overflow hidden to the overall circle. Responsiveness also needs to be considered. Our circle can either have a fixed width and height or we can position it absolutely within a fixed aspect ratio square element, using an adaptation of the responsive Youtube frame trick (the technique I've used in the examples below).

Bringing it all together

A little bit of fiddling with the numbers and we can generate an entire circle this way, with as many segments as we want. But that's going to get fiddly if we want circles with different numbers of segments in them - we'd end up rebuilding them each time. Instead, let's create a mixin that does the hard work for us. And here it is, with some live examples (we'll get into the detail below). First, some simple circles.

You'll notice if you hover the mouse over a segment it responds (what good is a circle if you can't interact with it?) again using a transformation, this time of scale, to produce a hover effect. The next logical step is to go from a circle to a ring.

This effect is achieved by adding a pseudo element to the main circle. Note also the difference between the second and third example above. In in the third example, the hover effect is changed so that each segment appears to move away from the circle on hover. To do this, each of the segments includes its own pseudo element that matches the parent circle's one, but this one scales with the rest of the segment on hover to produce this effect.

The code is extremely flexible and opens up a lot of possibilities. For example, a little bit of custom styling alongside the use of our mixin allows us to produce things like this.

The mixin

All of the examples above were created with the same Less CSS mixin. Here's the basic core of it. There are some additional classes not shown here for clarity, as well as some mixins (but their purpose is pretty obvious).


.gencircle(@numSegments,@donut: no,@donutSize: 50%,@donutStatic: no){@segmentAngle: 360 / @numSegments;@skew: 90 - @segmentAngle;position:absolute;top:10%;left:10%;width:80%;height:80%;.border-radius( 100% );.border-box();.segment {.transform(skewX(~"@{skew}deg"));.inner {.transform(skewX(~"-@{skew}deg"));&:hover {.transform(skewX(~"-@{skew}deg") scale(1.1));}}}/* generate the segment classes */@segmentIterations: 360;.segmentLoop(@segmentIndex) when (@segmentIndex > 0){@segmentNumber: @segmentIndex / @segmentAngle;.segment@{segmentNumber}{.transform( rotate(~"@{segmentIndex}deg") );}.segmentLoop(@segmentIndex - @segmentAngle);}.segmentLoop(0){}.segmentLoop(@segmentIterations);
}

The mixin can be applied to any element and requires only a single parameter to compile, the number of segments. The additional donut parameters refer to generating a ring circle as previously demonstrated. Set @donut to yes to make a ring circle, set @donutSize to the size you want it, and set @donutStatic to yes to have the segments lift out on hover rather than simply expand, as discussed above.

If you don't want any hover effect on your segment, simply add the class 'nohover' to the parent element in the markup.

Obviously you'll need to put in all the markup yourself, which looks like this.


<div class="yourcircleclass"><div class="segmentwrapper segment1"><div class="segment"><div class="inner"></div></div></div><div class="segmentwrapper segment2"><div class="segment"><div class="inner"></div></div></div><!-- add in each required segment as above,incrementing the segment class number as shown -->
</div>

Limitations and improvements

The formula used to generate the segments can't handle anything below four segments. The maths for skewing and rotation doesn't work below that point. Similarly, any number of segments higher than twelve starts to run into rendering issues, depending on the browser - the outer edge of the circle gets blurry.

Each circle currently uses three nested elements per segment, which is a lot of elements. It might be possible to reduce this number with increased use of pseudo elements but I've left it as it is currently to allow for more flexible reuse. For example, you may wish to use :before and :after to add your own styling, perhaps in the form of graphics or text attached to the outside of each segment.

The real world calls

After writing this article mainly out of curiousity I found myself in a situation where I actually needed to use it, so here are some additional notes.

Firstly, some limitations of the existing approach. My work required each segment to contain some text, but with the hover (expanding) effect in place, most browsers distorted the text during the transition, then rendered it normally once complete. It was a small problem but quite noticeable, and unfortunately most of the techniques likely to solve it (adding a translateZ into the transformation, browser specific font smoothing techniques) failed to work.

The solution I finally chose was to substitute a padding increase instead of a scale on hover, which simply moved the text rather than re-rendering it. I also made the text as big as possible (so the browser has plenty of pixels to work with) and the transition fast. It's not perfect but it's a better solution.

The other problem is that depending on your browser and window size you might or might not see gaps between your segments, which will show whatever colour is behind the circle. This might be good or bad, depending on your required outcome - in my case it didn't matter, but it might in yours. Unfortunately I don't have a solution for this - I think it's simply to do with how browsers antialias angled lines.

Pie charts

Turns out that the most likely reason you'd want segmented circles like this is to make a pie chart. Here's a new mixin for creating an arbitrary segment size (as opposed to the mixin above where all the segments are the same size).


.segmentmixin(@start,@size,@bg,@ts){@rotate: @start + 90;@skew2:@size - 90;@skew1:-@skew2;transform:rotate(~"@{rotate}deg");transition:all @ts ease;.segment {transform:skewX(~"@{skew1}deg");.inner {background:@bg;transform:skewX(~"@{skew2}deg");&:hover {transform:skewX(~"@{skew2}deg") scale(1.1);}}}
}

This will allow the creation of a pie chart with varying sized segments. Simply supply the start position of the segment (in degrees), the size of the segment (also in degrees), the required background colour and hover transition speed. Behold:

Unfortunately there are limitations to this approach. You can't make a segment bigger than 180 degrees (i.e. more than half a circle) because the skew collapses back on itself. Actually, you can't even make a segment bigger than 145 degrees for the same reason, particularly if you want to include the expanding hover effect.

The size problem can be overcome by not using the mixin for all segments but simply positioning a large circle overlaid with smaller segments (as shown). It's not a perfect solution though - you can't use the hover effect and depending on the browser and window size you can see the large circle between the other segments.

Moving forward

Having expanded this work to include pie charts I now can't see a good solution that I would be happy using in a production environment, except in specific circumstances that avoid the problems I've already mentioned. If you need a pie chart that works with the code above then by all means use it, but I offer no guarantees of success. Might be better to just use an image, or try an SVG if you need the interactivity.

All of the code for this mixin and the examples above can be found on my github here or if you just want to jump straight in with the mixins and incorporate it into your own Less it can be found in this file here. Hope you find it useful.

Related