Radial Charts API (Part Four)
Posted on 06 September, 2020
Continuing the development of a project to provide an API to draw ‘stacked doughnut’ charts (see Parts One, Two and Three for the story so far).
It's been a few weeks since I posted an offer for this project, I've heard nothing and the project deadline is very long-gone, so posting more of my code!
Functionally Mute
At the moment our code is written to run once, on a static set of data, when the code is called. We
need to instead turn it into a function which is called with a data parameter.
We also need to think about what the final API is going to look like; from the specification, the
intention is that we can POST to the API with some data; this generates an image and returns a
link to that image. So our function should save an image and return a link.
You could write this all another way, of course — have the
POST'simply' return a URL with parameter encoding, and have aGETto the parameter-encoded URL generate and return the image 'in memory'. This would require no disk storage (pro) but would require computation every time that a file was 'retrieved' (con). Without knowing the detailed use-case it's hard to tell which route would be most beneficial, and I've decided to follow the specification.
Since we need to save the file to disk and return a link, we need to generate a unique filename. For simplicity I'm going to use the time/date that the file is generated as a name (no other instructions given).
To change our current code into a function, we need to:
- remove (or in my case rename) the
datadictionary in the current code (since we're going to pass the data in as a parameter) - 'wrap' everything below the current code's
importstatements in adefto make a new function - add code to get the current time (to use as a filename)
- add code to generate a file path (which means working with OS to understand current path)
- change the
img.save()to use the new path, and thenreturna URL that points to that path
The new code here is around knowing time and file paths, for which we're going to need to add a couple of imports from the standard library:
import time
import os
The time module has a method time.time() which generates "the duration, in milliseconds, since
the Epoch'. For our purposes, that means that it will always generate a unique number, so that works
fine for naming files; we can use:
imgname = str(int(time.time())) + '.png'
to generate a new filename.
We also need to know where to save the file, for which we can use the os module's getcwd()
method to get the current working directory and combine that with the new imgname to save the file:
CWD = os.getcwd()
img.save(CWD + '/tmp/' + imgname)
And then finally we know we're going to need to get to that file, so we can return the imgname
(I'm not going to return the whole path, since we're going to need to add a router/webapp that can
deal with retrieving a file given it's name).
The full code looks like this (I've renamed my old data variable to d and moved it outside of
the function def, which means I can use it as a default data set when testing if necessary):
import time
import os
from PIL import Image, ImageDraw, ImageFont
from fonts.ttf import Roboto
from fonts.otf import FontAwesome5FreeSolid as FA
d = {
'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
}
def generateChart(data)
img = Image.new("RGB", (4000,4000), color="white")
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)
CWD = os.getcwd()
imgname = str(int(time.time())) + '.png'
img.save(CWD + '/tmp/' + imgname)
return imgname
Next Up
The majority of the hard work is now done; what's needed is a 'router' that can receive POST and
GET requests and react accordingly ...