DEV Community

Cover image for Doughnut time
Ross Angus
Ross Angus

Posted on • Updated on

Doughnut time

Image by Andres Ayrton

I was asked to build some doughnut charts the other day and it reminded me of a technique I'd read about years ago which would be perfect. This is often what it's like for developers: you read a blog post about something and think "that sounds useful. I'll need to remember that, for some hypothetical project in the future." With experience, a developer's mind resembles that drawer you have in your house full of cables which somehow allow a VHS recorder to connect to your microwave.

The technique in question is called conic gradients and can be used to do stuff like this:

One word you'll hear bandied about if you hang around long enough with developers is "elegance". Just to be clear - you're extremely unlikely to hear this applied to developers. But you'll hear them mutter it under their breath, in relation to some code they approve of.

When we say "elegant", we generally mean simple, clean code which achieves its function with great economy. Imagine an artist standing in front of a canvas and, using a single brush stroke, describes the form of a horse galloping full pelt. That, but with the confusing mess of punctuation symbols which make up computer languages.

There are few places where elegant code counts more than on the web. There's no end of statistics reminding you of how many customers you can shed, if you ask them to watch your website load for a fraction of a second longer than your rival.

Developers strive to achieve this in many ways, but when building a single component, we sometimes aim to do so in a single html "tag". Going back to the example of our doughnut chart, we might ideally represent this in the HTML as this:

<p>75%</p>
Enter fullscreen mode Exit fullscreen mode

That <p> is short for "paragraph", by the way.

HTML (broadly speaking) is used to store the data of a single web page. The language we're going to use to describe how the doughnut chart looks is called CSS or Cascading Style Sheets. So the first thing we need to do is pass the value 75% to the CSS.

A quick diversion down semantics avenue

Semantics is a term which is often flashed around in conversation with much of the same attitude as Mulder and Scully produced their FBI badges. In the context of web development, it means ensuring that the data which is displayed is put in the correct context.

Charts.css is a much more comprehensive approach to charting using CSS. As such, their starting point is often the table. Tables are great for displaying data which has both a vertical and a horizontal relationship. For example:

Doughnuts around the World

Name Place of origin Notable features
Bomboloni Italy Fillings
Berliner Germany Sometimes filled with champagne
Jelebi Middle Eastern/Indian/North African Made from maida flour
Churros Spain and Portugal Rod-shaped, not ring-shaped
Sufganiyot Israeli Cooked in schmaltz
Youtiao Doughnuts China Twisted stick
Beignets France Made from choux or yeast pastry
An Doughnut Japan Can be filled with red bean paste
Oliebollen Holland Can be filled with raisins

Content shamelessly stolen from Crazy & Co.

The data I'm representing here is much simpler - a single category and a figure.

HTML has a tag for that! It's called figure and it's not just for pretty pictures. Inside a figure tag, you can add a figcaption tag which should explain what the figure means. For example:

<figure>
  <p>100%</p>
  <figcaption>This is how much I love CSS</figcaption>
</figure>
Enter fullscreen mode Exit fullscreen mode

Anyway, back to how we're going to make these doughnuts.

From this point on, it's no longer for the layperson

It's not possible to extract the contents of a tag using CSS, but we can pass values to the CSS as CSS variables on a style attribute:

<p style="--value: 75%;">75%</p>
Enter fullscreen mode Exit fullscreen mode

A basic conic gradient which represents 75% would be as follows:

p {
  background: conic-gradient(red 270deg, transparent 270deg);
}
Enter fullscreen mode Exit fullscreen mode

This is the shortened form of conic-gradient, where the browser makes the assumption that the gradient is centred within the element, starts at the 12 noon position, the first colour starts at 0 degrees and the second colour ends at 360 degrees. The repetition of 270deg means that both the start and end colour change at this point, which leaves a hard line.

Note how the size of the doughnut is defined in degrees. We could do some math(s) here to convert our 75% figure to degrees, but what's the point, when conic-gradient is just as happy with a percentage?

p {
  background: conic-gradient(red var(--value), transparent var(--value));
}
Enter fullscreen mode Exit fullscreen mode

Adding the doughnut hole

A conic gradient is just a background image, but we're going to exploit where this image starts, in order to create the hole. We can use the background-origin property to tweak where the background image starts from:

Note how in the first example, the background image appears under the border, then in the second example, the image starts from inside the border and in the final example, the background image stats from some distance away from the border. This gap is determined by the padding inside the element.

We can use this property in combination with adding multiple background on the same elements in order to create the doughnut hole. Here's how it works:

Each one of those charts uses the following style rule:

/* --barColour and --value are passed in from the HTML */
p {
  background:
   conic-gradient(var(--bg) 0, var(--bg) 0) padding-box,
   conic-gradient(var(--barColour) var(--value), transparent var(--value)) border-box;
}
Enter fullscreen mode Exit fullscreen mode

That first background represents the hole in the middle and note that it starts from the padding-box position. Unfortunately, this needs to include the background colour as a colour value (I've used a CSS variable above, to keep it DRY). So the hole in the centre is not truly transparent, it's instead a circle which sits on top.

This means this technique is not ideal for doughnut charts which need to sit on top of complicated background graphics.

The second background is the doughnut itself and starts from the border-box element (inside the border). This means the thickness of the doughnut ring can be controlled by adjusting the transparent border on the doughnut element. Specifically, this property:

p {
   border: solid 1em transparent;
}
Enter fullscreen mode Exit fullscreen mode

Future work

The original design I was working from held no more than one figure per doughnut chart. It's possible to add more than one band of colour to the doughnut chart, but doing so breaks the tight relationship between the data in the HTML and the doughnut chart itself. This is because each band in the doughnut chart is passed as a pair of CSS variables, one for colour, the other for the value.

As the conic-gradient rule needs to expect these variables in advance, you'd need to decided what the maximum number of these, when writing the original rule. For example:

p {
  background:
    conic-gradient(var(--bg) 0, var(--bg) 0) padding-box,
    conic-gradient(
      var(--barColour) var(--value),
      var(--barColour2, transparent) var(--value) var(--value2, 100%),
      var(--barColour3, transparent) var(--value2, 100%) var(--value3, 100%),
      var(--barColour4, transparent) var(--value3, 100%) var(--value4, 100%),
      var(--barColour5, transparent) var(--value4, 100%) var(--value5, 100%),
      var(--barColour6, transparent) var(--value5, 100%) var(--value6, 100%),
      transparent var(--value6, 100%) 100%
    ) border-box;
}
Enter fullscreen mode Exit fullscreen mode

Note that the var() function can provide a default value, if none is supplied by the markup. This allows the doughnut chart to work with as few as one value.

Top comments (0)