Radial Charts API (Part Three)

Continuing the development of a project to provide an API to draw ‘stacked doughnut’ charts (see Parts One and Two for the story so far).

A Font Of Knowledge

Now that we have our arcs being drawn and text labels being shown, the next thing to worry about is icons.

To draw icons we ideally want to have a set of pre-designed images — and wouldn't it be nice if they were wrapped up in a font package? Happily we can get just that by using Font Awesome — and even better, there's a Python package font-font-awesome which is also compatible with the fonts module that we previously installed.

pip3 install font-font-awesome

And then we need to add another import — note that this is from fonts.otf rather than fonts.ttf, since Font Awesome is an OpenType rather than TrueType font.

from fonts.otf import FontAwesome5FreeSolid as FA

Happy to admit that I really struggled to find out how to get this import to work (there's limited documentation and nothing that suggested the correct name) so I eventually just used dir(fonts.otf) which yielded the available names.

Next we need to add some more information to our 'fake JSON' data. At the moment I'm providing unicode character references (see this Font Awesome cheatsheet to get unicode for a required icon):

data = {
  'items': [
    {'percent': 82.0, 'color': 'purple', 'name': 'People', 'icon': '0xf0c0'},
    {'percent': 75.0, 'color': '#00788a', 'name': 'Business', 'icon': '0xf013'},
    {'percent': 80.0, 'color': 'gray', 'name': 'Future', 'icon': '0xf135'}
  ],
  'o_all' : 76.0
}

Let's Get Drawing

And then we need to work out how to draw the icons. In the example image that was provided for the project the icons were shown to the left of the text, and were vertically aligned. That means we need to know all of the 'normal text' positions before drawing any icons (so we can align them properly). So we need to change the for loop where we render the text a little:

minx = 1000
hs = []
for idx, item in enumerate(data['items']):
  s = f"{item['name']} {int(item['percent'])}%"
  w, h = font_main.getsize(s)
hs.append(h)
  x = 480 - w
  minx = min(minx, x)
  y = ( ( LW + SP ) * idx + SP ) + (LW - h)/2
  d.text((x,y), s, fill=item['color'], font=font_main)

And then we need to iterate through the items (again) to draw out icons (there's some finagling to be done to get the horizontal alignment between text and icons exactly right, but we'll park that for now).

I_SIZE = 45
font_icon = ImageFont.truetype(FA, I_SIZE)
for idx, item in enumerate(data['items']):
  s = chr(int(item['icon'], 16))
  w, h = font_icon.getsize(s)
  x = minx - w/2 - I_SIZE
  y = ( ( LW + SP ) * idx + SP ) + (LW - hs[idx])/2
  d.text((x,y), s, fill='gray', font=font_icon)

And when we run this code we get:

Radial chart with icons

And now we have icons!


Code So Far

from PIL import Image, ImageDraw, ImageFont
from fonts.ttf import Roboto
from fonts.otf import FontAwesome5FreeSolid as FA
img = Image.new("RGB", (4000,4000), color="white")

data = {
  'items': [
    {'percent': 82.0, 'color': 'purple', 'name': 'People', 'icon': '0xf0c0'},
    {'percent': 75.0, 'color': '#00788a', 'name': 'Business', 'icon': '0xf013'},
    {'percent': 80.0, 'color': 'gray', 'name': 'Future', 'icon': '0xf135'}
  ],
  'o_all' : 76.0
}
LW = 60
SP = 10
d = ImageDraw.Draw(img, mode="RGBA")

for idx, item in enumerate(data['items']):
  offset = 4 * ( ( LW + SP ) * idx + SP )
  box = (offset, offset, 4000 - offset, 4000 - offset)
  deg = int( 360 * item['percent']/100) - 90
  d.arc(box, -90, deg, fill=item['color'], width=4*LW)

img = img.resize((1000,1000), resample=Image.LANCZOS)
d = ImageDraw.Draw(img, mode="RGBA")
MF_SIZE = 45
font_main = ImageFont.truetype(Roboto, MF_SIZE)

minx = 1000
hs = []
for idx, item in enumerate(data['items']):
  s = f"{item['name']} {int(item['percent'])}%"
  w, h = font_main.getsize(s)
  hs.append(h)
  x = 480 - w
  minx = min(minx, x)
  y = ( ( LW + SP ) * idx + SP ) + (LW - h)/2
  d.text((x,y), s, fill=item['color'], font=font_main)

C_SIZE = 200
font_cent = ImageFont.truetype(Roboto, C_SIZE)
s = f"{int(data['o_all'])}%"
w, h = font_cent.getsize(s)
x = 500 - w/2
y = 500 - h/2
d.text((x,y), s, fill='purple', font=font_cent)

I_SIZE = 45
font_icon = ImageFont.truetype(FA, I_SIZE)
for idx, item in enumerate(data['items']):
  s = chr(int(item['icon'], 16))
  w, h = font_icon.getsize(s)
  x = minx - w/2 - I_SIZE
  y = ( ( LW + SP ) * idx + SP ) + (LW - hs[idx])/2
  d.text((x,y), s, fill='gray', font=font_icon)

img.save('example.png')

What Next?

So now that we have code to generate a chart, we need to think about a backend API to generate them to order ...