-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathvicariouschart.py
More file actions
402 lines (393 loc) Β· 18.1 KB
/
Copy pathvicariouschart.py
File metadata and controls
402 lines (393 loc) Β· 18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# import packages
from datetime import datetime
from PIL import ImageColor, ImageFont
import math
import time
import vicarioustext
def getShortTextOfNumber(n=0):
s=str(n)
ul = len(s) // 3
ut = ""
if ul == 0: return s
if ul == 1: ut = "K"
if ul == 2: ut = "M"
if ul == 3: ut = "B"
um = len(s) % 3
if um == 0: return "." + str(s[2:4]) + ut
if um == 1: return str(s[:1]) + "." + str(s[1:2]) + ut
if um == 2: return str(s[:2]) + "." + str(s[2:3]) + ut
def drawBarChart(draw, left=0, top=0, width=480, height=320, theList=[], fieldName=None,
showLabels=False, chartLabel="", grouping=1, valueColor="#ff8888",
showAverage=True, averageColor="#8888ff",
movingAverageDuration=0, movingAverageColor="#88ff88",
movingAverageWidth=3, borderColor="#888888",
showGridLines=False, gridLineColor="#444444",
forceHigh=None, forceLow=None
):
low, high, avg, _, _ = getFieldMinMaxAvgValues(theList, fieldName)
if forceHigh is not None: high = forceHigh
if forceLow is not None: low = forceLow
lowx, highx = -1, -1
lowy, highy = -1, -1
lowFound, highFound = False, False
minfloor = .8
ma = []
# chart area
chartLow = low * minfloor
chartHigh = high
chartLeft = left
chartTop = top
chartWidth = width
chartHeight = height
# grid lines needs space for labels
gridLegendWidth = 50
if showGridLines and (high - low > 0):
chartLeft += gridLegendWidth
chartWidth -= gridLegendWidth
difference = high - low
units = int(math.pow(10, len(str(int(difference))) - 1))
chartHigh = int(math.ceil(high / units) * units)
chartLow = int(math.floor(low / units) * units)
if chartHigh - (units//2) >= high: chartHigh -= (units//2)
if chartLow + (units//2) <= low: chartLow += (units//2)
chartDifference = (chartHigh - chartLow)
gridSteps = 5
gridStepSize = (float(chartDifference) / float(gridSteps))
gridSteps += 1
for g in range(0, gridSteps):
gridStepAmount = int(chartLow + (gridStepSize * g))
gridStepY = chartTop + int((1.0-(float(gridStepAmount-chartLow) / float(chartHigh-chartLow))) * chartHeight)
# dashed line
for gridStepX in range(chartLeft-(gridLegendWidth//2), chartLeft+chartWidth-1, 6):
draw.line(xy=[(gridStepX,gridStepY),(gridStepX+2,gridStepY)],fill=ImageColor.getrgb(gridLineColor),width=1)
# label it
gridlabelpos = "br" if g == 0 else "r"
gridlabelpos = "tr" if g == gridSteps - 1 else gridlabelpos
gridStepText = getShortTextOfNumber(gridStepAmount)
drawLabel(draw, gridStepText, 10, gridlabelpos, (gridLegendWidth*2)//3, gridStepY)
# maximum y allowed is the bottom minus 1
maxy = chartTop + chartHeight - 1
# draw average line
if showAverage:
avgy = chartTop if high == 0 else chartTop + int((1.0-(float(avg-chartLow) / float(chartHigh-chartLow))) * chartHeight)
draw.line(xy=[(chartLeft,avgy),(chartLeft+chartWidth-1,avgy)],fill=ImageColor.getrgb(averageColor),width=2)
# process each column
l = len(theList) #- 1
xwidth = math.ceil(chartWidth/l) # chartWidth // l
#xhalfwidth = (xwidth // 2) - 1 # for calculating bar width
#if xhalfwidth < 0: xhalfwidth = 0
xhalfwidthl = math.ceil(float(chartWidth/(l/grouping))/2) #xhalfwidth
xhalfwidthr = math.ceil(float(chartWidth/(l/grouping))/2) #xhalfwidth
if xhalfwidthl == 0 and xhalfwidthr == 0: xhalfwidthl = 1
index = 0
for i in range(0,l,grouping): #range(0,l+1,1): # range(l, 0, -1):
index += 1
# determine x coordinate for column
px = float(i) / float(l)
x = chartLeft + xhalfwidthl + int((chartWidth-xwidth) * px) + 1
# get current value
if grouping == 1:
# single value
item = theList[i]
if type(item) is dict and fieldName is not None:
curval = float(item[fieldName])
else:
curval = float(item)
else:
if i + grouping >= l: grouping = l - i
# highest of the range of values
curval = 0
for g in range (0,grouping):
item = theList[i+g]
if type(item) is dict and fieldName is not None:
curval = max(float(curval),float(item[fieldName]))
else:
curval = max(float(curval),float(item))
# determine y coordinate for column based on value
py = 1.0 if high <= 0 else float(curval-chartLow) / float(chartHigh-chartLow)
y = chartTop + int((1.0 - py) * chartHeight) - 1
y = chartTop if y < chartTop else y
y = maxy if y > maxy else y
if curval <= low: # gets right most low value to avoid conflict with avg label
lowx = x
lowy = y
lowFound = True
if curval >= high and not highFound: # earliest high value gets the label
highx = x
highy = y
highFound = True
# plot the value as a vertical bar
draw.rectangle(xy=[(x-xhalfwidthl,y),(x+xhalfwidthr,maxy)],fill=ImageColor.getrgb(valueColor),outline=ImageColor.getrgb(valueColor))
# process moving average
if movingAverageDuration > 0:
v = curval
ma = ma[-1 * movingAverageDuration:]
maold = v if len(ma) == 0 else sum(ma) / len(ma)
ma.append(v)
ma = ma[-1 * movingAverageDuration:]
manew = v if len(ma) == 0 else sum(ma) / len(ma)
if index > 1:
if high - low > 0:
maoldy = chartTop+((1.0-(float(maold-chartLow)/float(chartHigh-chartLow)))*chartHeight)//1
manewy = chartTop+((1.0-(float(manew-chartLow)/float(chartHigh-chartLow)))*chartHeight)//1
else:
maoldy = chartTop + (chartHeight//2)
manewy = maoldy
if manewy < chartTop + movingAverageWidth: manewy = chartTop + movingAverageWidth
if manewy > chartTop + chartHeight - movingAverageWidth: manewy = chartTop + chartHeight - movingAverageWidth
draw.line(xy=[(maoldx,maoldy),(x,manewy)],fill=ImageColor.getrgb(movingAverageColor),width=movingAverageWidth)
maoldx = x
# draw box around chart
draw.rectangle(xy=[(chartLeft,chartTop),(chartLeft+chartWidth-1,chartTop+chartHeight)],outline=ImageColor.getrgb(borderColor),width=1)
# draw labels
if showLabels:
if highFound and highx > 0 and highy > 0:
if highx - 100 < chartLeft and (chartLabel is not None and len(chartLabel)) > 0: highy = highy + 20
highpos = "tr" if highx + 150 > chartLeft + chartWidth else "tl"
highx = highx + 3 if highpos == "tl" else highx - 3
drawLabel(draw, f"HIGH:{high}", 10, highpos, highx, highy+2)
if lowFound and lowx > 0 and lowy > 0 and low < high:
lowpos = "br" if lowx + 150 > chartLeft + chartWidth else "bl"
lowx = lowx + 3 if lowpos == "bl" else lowx - 3
drawLabel(draw, f"LOW:{low}", 10, lowpos, lowx, lowy-2)
if showAverage and (avg < high) and (avg > low):
avgpos = "tl" if avgy + 20 < chartTop+chartHeight-1 else "bl"
avgx = chartLeft+2
avgy = avgy - 2 if avgpos == "bl" else avgy + 2
if avgy > highy:
drawLabel(draw, "AVG:" + str(avg), 10, avgpos, avgx, avgy)
if chartLabel is not None and len(chartLabel) > 0:
drawLabel(draw, chartLabel, 12, "tl", chartLeft+2, chartTop+2)
return chartHigh, chartLow, chartLeft, chartWidth
def drawDotChart(draw, left=0, top=0, width=480, height=320, list=[], fieldName=None,
valueColor="#2f3fc5", valueRadius=3,
movingAverageDuration=10, movingAverageColor="#40ff40", movingAverageWidth=3,
lowValueColor="#ffaa00", lowValueThreshold=-1
):
low, high, avg, _, _ = getFieldMinMaxAvgValues(list, fieldName)
ma = []
if fieldName is not None: fieldParts = fieldName.split(".")
index = 0
listLength = len(list)
for item in list:
index += 1
o = item
bOK = True
if type(o) is dict or fieldName is not None:
for fieldPart in fieldParts:
if type(o) is dict and fieldPart in o:
o = o[fieldPart]
else:
bOK = False
v = 0 if not bOK else int(o)
# assign color
dotColor = valueColor
if v < lowValueThreshold: dotColor = lowValueColor
# plot the dot
px = left + ((width * index) // listLength)
pp = .5
if high - low > 0: pp = float(v - low)/float(high - low)
py = top + ((1.0 - pp) * height)
px1 = px-1
px2 = px+1
py1 = py-1
py2 = py+1
if px1 < left: px1 = left
if px2 > left+width: px2 = left + width
if py1 < top: py1 = top
if py2 > top + height: py2 = top + height
draw.ellipse(xy=[(px1,py1),(px2,py2)],fill=ImageColor.getrgb(dotColor),outline=ImageColor.getrgb(dotColor),width=valueRadius)
# process moving average
if movingAverageDuration > 0:
ma = ma[-1 * movingAverageDuration:]
maold = v if len(ma) == 0 else sum(ma) / len(ma)
ma.append(v)
ma = ma[-1 * movingAverageDuration:]
manew = v if len(ma) == 0 else sum(ma) / len(ma)
if index > 1:
pp = .5
if high - low > 0: pp = float(maold - low)/float(high - low)
maoldy = top + ((1.0 - pp) * height)
pp = .5
if high - low > 0: pp = float(manew - low)/float(high - low)
manewy = top + ((1.0 - pp) * height)
if manewy < top + movingAverageWidth: manewy = top + movingAverageWidth
if manewy > top + height - movingAverageWidth: manewy = top + height - movingAverageWidth
draw.line(xy=[(maoldx,maoldy),(px,manewy)],fill=ImageColor.getrgb(movingAverageColor),width=movingAverageWidth)
maoldx = px
return low, high, avg
def drawStackedPercentageBarChart(draw, left=0, top=0, width=480, height=320,
thelist=[], fieldnames=[], showlabels=False, chartlabel="",
fieldlabels=[], grouping=1, backgroundColor="#000000",
dataColors=["#ffff00", "#0000ff", "#00ff00", "#808000",
"#ff0000", "#00ffff", "#800000", "#808080",
"#008000", "#800080", "#ff00ff", "#008080"],
borderColor="#888888"):
# check inputs
if len(fieldnames) > 8:
drawLabel(draw, "TOO MANY FIELDNAMES DEFINED", 12, "tl", left, top)
return
if len(fieldnames) == 0:
drawLabel(draw, "NO FIELDS PROVIDED FOR RENDERING", 12, "tl", left, top)
return
field0 = fieldnames[0]
totalprovided = len(field0) > 0
# determine chart range (if first field name isn't empty, it represents the total range)
chartlow, charthigh = None, 0
if totalprovided:
chartlow, charthigh, _, _, _ = getFieldMinMaxAvgValues(thelist, field0)
else:
for fieldname in fieldnames:
if len(fieldname) == 0:
continue
low, high, _, _, _ = getFieldMinMaxAvgValues(thelist, fieldname)
chartlow = low if chartlow is None or low < chartlow else chartlow
charthigh = high if high > charthigh else charthigh
# process each column from right to left
fieldtotals = {}
l = len(thelist)
barwidthF = (float(grouping)/float(l))*float(width)
groupNum = 0
for i in range(l-1, 0, (-1 * grouping)):
x2 = left + width - int(barwidthF * groupNum)
groupNum += 1
x1 = left + width - int(barwidthF * groupNum)
y2 = top + height - 1
# get total for this time point
timetotal = 0
for g in range(grouping):
item = thelist[i-g]
if not totalprovided:
# when first field is empty, we sum up from remaining fields
for fieldname in fieldnames:
if len(fieldname) == 0:
continue
fieldvalue, fieldvalueok = getNestedField(item, fieldname)
if fieldvalueok:
timetotal = timetotal + int(fieldvalue)
else:
# when first field is given, we use that to denote total being compared against
timetotal = timetotal + int(item[field0])
# then process each field
fieldnum = 0
for fieldname in fieldnames:
fieldnum += 1 # for legend and coloring
# skip field if empty
if len(fieldname) == 0:
continue
# skip field if on total
if totalprovided and fieldname == field0:
continue
# total for the grouping
grouptotal = 0
for g in range(grouping):
item = thelist[i-g]
itemvalue, itemvalueok = getNestedField(item, fieldname)
if itemvalueok:
grouptotal = grouptotal + int(itemvalue)
# with grouptotal for this field, determine height and coordinates for the bar
if grouptotal > 0 and timetotal > 0:
fieldtotals[fieldname] = grouptotal if fieldname not in fieldtotals else fieldtotals[fieldname] + grouptotal
valuepct = float(grouptotal) / float(timetotal)
valueheight = int(float(height) * valuepct)
y1 = y2 - valueheight
y1 = top if y1 < top or (fieldnum == len(fieldnames) and not totalprovided) else y1
# draw the bar
draw.rectangle(xy=[(x1,y1),(x2,y2)],fill=dataColors[fieldnum],width=1)
# draw value
if showlabels:
pcttext = str(int(valuepct * 100.0)) + "%"
vicarioustext.drawtoplefttext(draw, pcttext, 9, x1+1, y1+1, ImageColor.getrgb(backgroundColor))
# assign new bottom
y2 = y1
# draw box
draw.rectangle(xy=[(left,top),(left+width-1,top+height-1)],outline=ImageColor.getrgb(borderColor),width=1)
# draw labels
if showlabels:
if len(chartlabel) > 0:
drawLabel(draw, chartlabel, 12, "tl", left+2, top+2)
fieldnum = 0
legendx = left+10
legendy = top+height-10
legendsize = 10
for fieldname in fieldnames:
fieldnum += 1
if len(fieldname) == 0:
continue
if totalprovided and fieldname == field0:
continue
legendlabel = fieldname if fieldlabels[fieldnum-1] == "" else fieldlabels[fieldnum-1]
if len(legendlabel) > 0 and fieldname in fieldtotals and fieldtotals[fieldname] > 0:
legendlabel = " " + legendlabel
sw,sh,f = vicarioustext.gettextdimensions(draw, legendlabel, legendsize, False)
drawLabel(draw, legendlabel, legendsize, "bl", legendx, legendy)
draw.rectangle(xy=[(legendx,legendy-legendsize),(legendx+legendsize,legendy)],fill=dataColors[fieldnum],outline=ImageColor.getrgb(borderColor),width=1)
legendx += sw + 10
def drawLabel(draw, s="", fontsize=12, anchorposition="tl", anchorx=0, anchory=0, backgroundColor="#000000", textColor="#ffffff", borderColor="#888888"):
sw,sh,f = vicarioustext.gettextdimensions(draw, s, fontsize, False)
padding = 1
border = 1
nx = anchorx
ny = anchory
if anchorposition == "tl":
pass
elif anchorposition == "tr":
nx = anchorx - sw
elif anchorposition == "t":
nx = anchorx - (sw//2)
elif anchorposition == "bl":
ny = anchory - sh
elif anchorposition == "br":
nx = anchorx - sw
ny = anchory - sh
elif anchorposition == "b":
nx = anchorx - (sw//2)
ny = anchory - sh
elif anchorposition == "l":
ny = anchory - (sh//2)
elif anchorposition == "r":
nx = anchorx - sw
ny = anchory - (sh//2)
draw.rectangle(xy=[(nx-padding-border,ny-padding-border),(nx+sw+padding+border,ny+sh+padding+border)],fill=ImageColor.getrgb(backgroundColor),outline=ImageColor.getrgb(borderColor),width=1)
draw.text(xy=(nx,ny), text=s, font=f, fill=ImageColor.getrgb(textColor))
def getNestedField(thedict, thefield):
if thedict is None:
return None, False
fieldDelimiter = "."
if thefield.find(fieldDelimiter) > -1:
thefieldparts = thefield.split(fieldDelimiter)
nextpart = thefieldparts[0]
remainder = fieldDelimiter.join(thefieldparts[1:])
if nextpart in thedict:
return getNestedField(thedict[nextpart], remainder)
if thefield in thedict:
return thedict[thefield], True
else:
return None, False
def getFieldMinMaxAvgValues(thelist, thefield=None):
if len(thelist) == 0:
return -1, -1, -1, -1, -1
valuemin = None
valuemax = 0
valueminx0 = None
valueminx1 = None
total = 0
if thefield is not None:
thefieldparts = thefield.split(".")
for item in thelist:
o = item
bOK = True
if type(o) is dict or thefield is not None:
for thefieldpart in thefieldparts:
if type(o) is dict and thefieldpart in o:
o = o[thefieldpart]
else:
bOK = False
v = 0 if not bOK else o # int(o)
total += v
valuemin = v if valuemin is None or v < valuemin else valuemin
valuemax = v if v > valuemax else valuemax
valueminx0 = v if valueminx0 is None or v < valueminx0 and v > 0 else valueminx0
valueminx1 = v if valueminx1 is None or v < valueminx1 and v > 1 else valueminx1
valueavg = (float(total) / float(len(thelist)))
return valuemin, valuemax, valueavg, valueminx0, valueminx1