Radial Charts API (Part Three)
Posted on 04 August, 2020
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
importto work (there's limited documentation and nothing that suggested the correct name) so I eventually just useddir(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:
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 ...