[Zope] graphics on the fly [long, with code]
ScherBi@BAM.com
ScherBi@BAM.com
Fri, 5 Nov 1999 07:34:00 -0500
Here is some code, for the community should they find it helpful, that works
with the python gd module to produce bar, line, and pie charts. I wrote it
about two years ago. It's not Zope specific. It's been useful for my
purposes, and I'm hoping there are some ideas in it worthy of inclusion in
some greater whole. The code should be quite self explanatory, and there's
a test included for each chart type. The gd module is, of course, required.
Enjoy,
Bill Scherer
#=========================== BEGIN CODE
=======================================
#!/usr/local/bin/python
"""
Python-GD Charting - William K Scherer 4/24/1998
scherbi@bam.com
Python-GD Charting provides for 2D charting (pie, line & bar).
The GD module is required. Value based sorting of input data is optional,
well as the insertion of mean, standard deviation, and median statistical
values. Charting range is automatically derived from the input data.
Negative values are acceptable.
Instantiation for all three subclasses is the same:
'chart = Chart.LineChart(data, stats, sort, chart, xaxis, yaxis, xsize,
ysize)'
...BarChart(...
...PieChart(...
where:
data - a list(or tuple) of lists(or tuples) of the form [('xItem1',
yvalue1), ('yItem1', yvalue1)]
data is the only required parameter
stats - any non zero number will enable statistics. Default is disabled.
sort - any number other than 1 wil disable sorting by value (data[0][1]).
Default is enabled.
chart title - The title for the chart. Defaults to 'Chart'.
xaxis - The x axis title. Defaults to 'X Axis'. Not used in PieChart.
yaxis - The y axis title. Defaults to 'Y Axis'. Not used in PieChart.
xsize - The x dimension, in pixels, of the final image. See note below.
ysize - The y dimension, in pixels, of the final image. See note below.
Note: xsize and ysize default to 550x440. Code to implement changing
these values
has not been done yet. In other words, don't mess with these unless
you're
prepared to do some coding!
See Test() for example usage. Running this module will execute Test() and
output three sample charts.
Feedback is welcomed. See address above.
License & Warranty info:
This code is free for anyone to use and modify as they see fit.
This code is provided 'as is', and as such there is no warranty of any
kind.
If this code causes you, your company, or anyone else harm of any
kind, there
is no one to sue over it. Use this code at your own discretion.
"""
import gd, time, math
class Chart:
""" Base class. Sets up colors and common features of image, common
functions. """
def __init__(self,data,Stat=0,Sort=1,charttitle='Chart',\
xtitle='X Axis',ytitle='Y Axis',xsize=550,ysize=440):
if Sort == 1:
self.data = self.ValueSort(data)
else:
self.data = data
if Stat != 0:
self.InsertMean()
self.InsertStDev()
self.InsertMedian()
## initialize image
self.image=gd.image((xsize, ysize))
## setup color space
# bgcolors
self.white = self.image.colorAllocate((255,255,255))
self.black = self.image.colorAllocate((0,0,0))
self.dkgray = self.image.colorAllocate((105,105,105))
self.gray = self.image.colorAllocate((190,190,190))
self.ltgray = self.image.colorAllocate((211,211,211))
self.vltgray = self.image.colorAllocate((235,235,235))
#fgcolors
self.MediumBlue = self.image.colorAllocate((0,0,180))
self.blue = self.image.colorAllocate((0,0,255))
self.green = self.image.colorAllocate((0,255,0))
self.ForestGreen = self.image.colorAllocate((34,139,34))
self.OliveDrab = self.image.colorAllocate((107,142,35))
self.DarkGoldenrod = self.image.colorAllocate((184,134,11))
self.SaddleBrown = self.image.colorAllocate((139,69,19))
self.sienna = self.image.colorAllocate((160,82,45))
self.chocolate = self.image.colorAllocate((210,105,30))
self.firebrick = self.image.colorAllocate((178,34,34))
self.brown = self.image.colorAllocate((165,42,42))
self.OrangeRed = self.image.colorAllocate((255,69,0))
self.red = self.image.colorAllocate((255,0,0))
self.maroon = self.image.colorAllocate((176,48,96))
self.RoyalBlue4 = self.image.colorAllocate((39,64,139))
self.SteelBlue4 = self.image.colorAllocate((54,100,139))
self.DeepSkyBlue4 = self.image.colorAllocate((0,104,139))
self.turquoise4 = self.image.colorAllocate((0,134,139))
self.cyan = self.image.colorAllocate((0,139,139))
self.SeaGreen = self.image.colorAllocate((46,139,87))
self.green = self.image.colorAllocate((0,255,0))
self.green3 = self.image.colorAllocate((0,205,0))
self.chartreuse = self.image.colorAllocate((102,205,0))
self.OliveDrab = self.image.colorAllocate((105,139,34))
self.yellow = self.image.colorAllocate((139,139,0))
self.gold = self.image.colorAllocate((139,117,0))
self.brown = self.image.colorAllocate((205,51,51))
self.salmon = self.image.colorAllocate((139,76,57))
self.orange = self.image.colorAllocate((139,90,0))
self.DarkOrange = self.image.colorAllocate((139,69,0))
self.coral = self.image.colorAllocate((139,62,47))
self.tomato = self.image.colorAllocate((205,79,57))
self.OrangeRed = self.image.colorAllocate((205,55,0))
self.red = self.image.colorAllocate((205,0,0))
self.DeepPink = self.image.colorAllocate((139,10,80))
self.HotPink = self.image.colorAllocate((139,58,98))
self.maroon = self.image.colorAllocate((139,28,98))
self.VioletRed = self.image.colorAllocate((139,34,82))
self.magenta = self.image.colorAllocate((139,0,139))
self.MediumOrchid = self.image.colorAllocate((122,55,139))
self.DarkOrchid = self.image.colorAllocate((104,34,139))
self.purple = self.image.colorAllocate((85,26,139))
## foreground color list
fgcolors = [self.MediumBlue,self.blue,self.SeaGreen,self.green,
self.ForestGreen,self.OliveDrab,self.DarkGoldenrod,self.SaddleBrown,
self.sienna,self.chocolate,self.firebrick,self.brown,self.OrangeRed,
self.red,self.maroon,self.RoyalBlue4,
self.SteelBlue4,self.DeepSkyBlue4,
self.turquoise4,self.cyan,self.SeaGreen,self.green,
self.green3,self.chartreuse,self.OliveDrab,self.yellow,self.gold,
self.brown,self.salmon,self.orange,
self.DarkOrange,self.coral,self.tomato,self.OrangeRed,self.red,
self.DeepPink,self.HotPink,self.maroon,self.VioletRed,self.magenta,
self.MediumOrchid,self.DarkOrchid,self.purple]
self.fgcolors = self.RandList(fgcolors)
# border rectangle
self.image.rectangle((0,0), (xsize,2),self.black,self.black)
self.image.rectangle((xsize-2,0), (xsize,ysize),
self.black,self.black)
self.image.rectangle((xsize,ysize-2), (0,xsize),
self.black,self.black)
self.image.rectangle((2,ysize-2), (0,0), self.black,self.black)
# chart client space dividers (assuming default size for now)
self.image.rectangle((0,40), (550,42), self.black,self.black) #
title bottom border
self.image.line((400,70), (550,70), self.black) # legend title
bottom border
self.image.line((475,71), (475,399), self.dkgray) # legend space
divider
self.image.rectangle((398,42), (400,438), self.black, self.black) #
legend left border
self.image.line((398,400), (550,400), self.black) # legend space
bottom border
if self.type == 'Bar' or self.type == 'Line':
self.image.line((40,400), (550,400), self.black) # x axis title
border
self.image.line((40,40), (40,400), self.black) # y axis title
border
self.image.line((0,440), (40,400), self.black) # lower left
corner line
# fills
self.image.fill((500,430), self.dkgray) # time stamp
self.image.fill((500,60), self.dkgray) # legend
self.image.fill((10,10), self.ltgray) # title#
if self.type == 'Bar' or self.type == 'Line':
self.image.fill((10,100), self.vltgray) # y axis
self.image.fill((100,430), self.vltgray) # x axis
## titles, lables, timestamp, vanity
self.image.string(gd.gdFontSmall, (403,403),
time.ctime(time.time()), self.white)
self.image.string(gd.gdFontSmall, (422,414), 'Python-GD Charting',
self.white)
self.image.string(gd.gdFontSmall, (437,425), 'WKS 1998',
self.white)
self.image.string(gd.gdFontMediumBold, (445,50), 'Legend',
self.white)
self.image.string(gd.gdFontGiant, (16,16), charttitle, self.black)
if self.type == 'Bar' or self.type == 'Line':
self.image.string(gd.gdFontMediumBold, (100,410), xtitle,
self.black)
self.image.stringUp(gd.gdFontMediumBold, (20,300), ytitle,
self.black)
def Legend(self):
xpos, ypos, ccount = 408, 77, 0
for each in self.data:
color = self.fgcolors[ccount]
if each[0] == 'Mean': color = self.black
elif each[0] == 'StDev': color = self.dkgray
elif each[0] == 'Median': color = self.gray
self.image.rectangle((xpos,ypos), (xpos+8, ypos+5), color,
color)
self.image.string(gd.gdFontSmall, (xpos + 12, ypos-3), each[0],
self.black)
ypos = ypos + 10
if ypos > 390:
if xpos == 485:
break
else:
xpos, ypos = 480, 76
ccount = ccount + 1
if ccount > len(self.fgcolors) - 1:
ccount = 0
def Output(self, file='./test.gif'):
self.image.interlace(1)
self.image.writeGif(file)
def RandList(self, List):
import whrandom
whrandom.seed(1,2,3)
myList = List[:]
newList = []
for i in List:
item=whrandom.choice(myList)
newList.append(item)
myList.remove(item)
return newList
def ValueSort(self, data):
newdata = []
for each in data:
tmp = []
tmp.append(each[1])
tmp.append(each[0])
newdata.append(tmp)
newdata.sort()
data = []
for each in newdata:
tmp = []
tmp.append(each[1])
tmp.append(each[0])
data.append(tmp)
return data
def InsertMean(self):
num = len(self.data)
sum = 0
for each in self.data:
sum = sum + each[1]
avg = sum / num
self.data.append('Mean', avg)
def InsertStDev(self):
m = self.data[-1:][0][1]
var = long(0)
for item in self.data:
n = long(item[1])
n = n - m
var = var + (n * n)
std = math.sqrt(var/float(len(self.data)-1))
self.data.append('StDev', std)
def InsertMedian(self):
x = map(None, self.data)
x.sort()
n = len(x)
if n % 2 :
median = x[n/2][1]
else:
n = n / 2
[a,b] = x[n-1:n+1]
median = (a[1]+b[1])/2.0
self.data.append('Median', median)
class BarChart(Chart):
def __init__(self,data,Stat=0,Sort=1,charttitle='Chart',\
xtitle='X Axis',ytitle='Y Axis',xsize=550,ysize=440):
self.type = 'Bar'
Chart.__init__(self,data,Stat,Sort,charttitle,xtitle,ytitle,xsize,ysize)
def Bounds(self):
self.xsize = len(self.data)
self.ysize = 0
self.ymin = 0
for each in self.data:
if each[1] > self.ysize: self.ysize = each[1]
if each[1] < self.ymin: self.ymin = each[1]
if self.ymin < 0:
self.ysize = self.ysize + abs(self.ymin)
try:
self.xdif = (300.0 / self.xsize)
except:
self.ydif = 1
try:
self.ydif = (300.0 / self.ysize)
except:
self.ydif = 1
try:
xgran = self.xsize / self.xdif
except:
xgran = 1
try:
ygran = int(pow((self.ysize / self.ydif), 0.5))
except:
ygran = 1
if xgran < 1: xgran = 1
if ygran < 1: ygran = 1
self.granularity = (ygran,xgran)
def Grid(self):
self.Origin = self.image.getOrigin()
self.image.origin((70,390), 1, -1)
ypoints, xpoints = self.ymin, 0
for y in range(0, self.ysize + self.granularity[0],
self.granularity[0]):
y = y * self.ydif
if y < 335:
self.image.line((0,y), (self.xsize * self.xdif + 5,y),
self.ltgray)
self.image.string(gd.gdFontTiny, (-27,y + 4), str(ypoints),
self.red)
ypoints = ypoints + self.granularity[0]
for x in range(0, self.xsize + self.granularity[1],
self.granularity[1]):
x = x * self.xdif
if x > 300: break
self.image.line((x,y+5), (x,0), self.gray)
xpoints = xpoints + self.granularity[1]
if self.ymin < 0:
yy = abs(self.ymin) * self.ydif
self.image.line((0,yy), (305,yy), self.red)
self.image.string(gd.gdFontSmall, (-8, yy+5), '0', self.red)
self.image.origin(self.Origin[0], self.Origin[1]) ## reset origin
def Plot(self):
self.image.origin((70,390), 1, -1)
x = ccount = 0
for a in self.data:
h = a[1] + abs(self.ymin)
color = self.fgcolors[ccount]
if a[0] == 'Mean': color = self.black
elif a[0] == 'StDev': color = self.dkgray
elif a[0] == 'Median': color = self.gray
#self.image.rectangle((x+2,0),(x+self.xdif-2,h*self.ydif),
color, color)
self.image.rectangle((x+3,abs(self.ymin) *
self.ydif),(x+self.xdif-3,h*self.ydif), color, color)
x = x + self.xdif
ccount = ccount + 1
if ccount > len(self.fgcolors) - 1:
ccount = 0
self.image.origin(self.Origin[0], self.Origin[1])
def MakeChart(self):
self.Legend()
self.Bounds()
self.Grid()
self.Plot()
class LineChart(BarChart):
def __init__(self,data,Stat=0,Sort=1,charttitle='Chart',\
xtitle='X Axis',ytitle='Y Axis',xsize=550,ysize=440):
self.type = 'Line'
self.LastPoint = None
Chart.__init__(self,data,Stat,Sort,charttitle,xtitle,ytitle,xsize,ysize)
def Plot(self):
self.image.origin((70,390), 1, -1)
x = ccount = 0
for a in self.data:
h = a[1] + abs(self.ymin)
color = self.fgcolors[ccount]
if a[0] == 'Mean': color = self.black
elif a[0] == 'StDev': color = self.dkgray
elif a[0] == 'Median': color = self.gray
point = (x,h*self.ydif)
self.image.arc(point, (4,4), 0, 360, color) ## plot the dot
## connect the dots
if self.LastPoint != None:
self.image.line(self.LastPoint, point, self.dkgray)
self.LastPoint = point
else:
self.LastPoint = point
x = x + self.xdif
ccount = ccount + 1
if ccount > len(self.fgcolors) - 1:
ccount = 0
self.image.origin(self.Origin[0], self.Origin[1])
class PieChart(Chart):
def __init__(self,data,Stat=0,Sort=1,charttitle='Chart',\
xtitle='X Axis',ytitle='Y Axis',xsize=550,ysize=440):
self.type = 'Pie'
Chart.__init__(self,data,Stat,Sort,charttitle,xtitle,ytitle,xsize,ysize)
def circle(self):
self.Origin = self.image.getOrigin()
self.image.origin((200,240), 1, -1)
self.image.arc((0,0), (330,330), 0, 360, self.black)
self.image.origin(self.Origin[0], self.Origin[1], self.Origin[2])
## reset origin
def wedge(self,Range,color):
circ = 1093
radius = 165
deg2rad = 0.0174532925199
self.Origin = self.image.getOrigin()
self.image.origin((200,240), 1, -1)
## first, find end point of range[0] and plot the line
try:
angle = 360.0 / (100.0/Range[0])
except ZeroDivisionError:
angle = 0
angle = angle * deg2rad
x1 = int(radius * math.cos(angle))
y1 = int(radius * math.sin(angle))
self.image.line((0,0), (x1,y1), self.black)
## then the second
try:
angle = 360.0 / (100.0/Range[1])
except ZeroDivisionError:
angle = 0
angle = angle * deg2rad
x2 = int(radius * math.cos(angle))
y2 = int(radius * math.sin(angle))
self.image.line((0,0), (x2,y2), self.black)
##then fill it
tmp = ((Range[1] - Range[0]) / 2) + Range[0]
try:
angle = 360.0 / (100.0/tmp)
except ZeroDivisionError:
angle = 0
angle = angle * deg2rad
x = int((radius*.80) * math.cos(angle))
y = int((radius*.80) * math.sin(angle))
self.image.fill((x,y), color)
# print percent value
percent = round(Range[1] - Range[0], 1)
pct_text = str(percent) + '%'
if percent >= 2.0: ## if the wedge is too small, move the text
outside the circle
x = int((radius*0.80) * math.cos(angle))
y = int((radius*0.80) * math.sin(angle))
self.image.string(gd.gdFontSmall, (x-10,y+5), pct_text,
self.black)
else:
x = int((radius*1.09) * math.cos(angle))
y = int((radius*1.09) * math.sin(angle))
self.image.string(gd.gdFontSmall, (x-10,y+5), pct_text,
self.black)
self.image.origin(self.Origin[0], self.Origin[1], self.Origin[2])
## reset origin
def plot(self):
ccount = 0
init = 0
for a in self.data:
Range = (init*100, (a[1] + init)*100)
self.wedge(Range, self.fgcolors[ccount])
init = init + a[1]
ccount = ccount + 1
if ccount > len(self.fgcolors) -1:
ccount = 4
def datacheck(self):
total = 0
total = float(total)
data2 = []
for a in self.data:
total = total + a[1]
for a in self.data:
a = a[0], a[1] / total
data2.append(a)
self.data = data2
def MakeChart(self):
self.circle()
self.datacheck()
self.Legend()
self.plot()
def Test():
data1 = [('Bob',30), ('Joe', 40), ('Ralph', 15),
('Mike', 60), ('Ed', 35), ('Bill',47.5),
('Frank', 75), ('Marlo', 25), ('Tarak', 90),
('Jon', 5), ('Jerry', 42.5), ('Pina', 110),
('Sara', 65), ('Lisa', 160), ('Megan', 45),
('Rascal', 115), ('Zippy', 10), ('Homer', 95)]
data2 = [('TCP/IP', 70), ('IPX', 19), ('VINES', 7), ('DECNET', 4)]
data3 = []
for a in range(32):
txt = 'Item' + str(a)
b = (a*a)
data3.append(txt, b)
data4 = [('A', 10), ('B', 5), ('C', 2), ('D', 0), ('E', -2), ('F', -5),
('G', -10)]
data5 = []
for a in range(-30, 31):
data5.append(str(a), a*a*a/500)
bc = BarChart(data1,1,1,"Bar Chart")
bc.MakeChart()
bc.Output("./bartest.gif")
pc = PieChart(data2, 0, 1, "Pie Chart")
pc.MakeChart()
pc.Output("./pietest.gif")
lc = LineChart(data5,0,0,"Line Chart")
lc.MakeChart()
lc.Output("./linetest.gif")
if __name__ == '__main__':
Test()
#====================== END CODE ================================
> -----Original Message-----
> From: Ulrich Wisser [mailto:u.wisser@luna-park.de]
> Sent: Thursday, November 04, 1999 11:43 AM
> To: zope@zope.org
> Subject: [Zope] graphics on the fly
>
>
> Hi,
>
> is there a Zope (or Python) Interface to GD? I need to
> make graphics as dynamic objects. The data will be
> stored in a SQL datbase and from there I will have to
> make a bar/pie/line chart out of it.
>
> I know how to do it in Perl (<- forgive me :).
> Is there an object or library to do it in Zope/Python?
>
> Thanks
>
> Ulli
> --
> ----------------- Die Website Effizienzer ------------------
> luna-park Bravo Sanchez, Vollmert, Wisser GbR
> Ulrich Wisser mailto:u.wisser@luna-park.de
> Alter Schlachthof, Immenburgstr. 20 Tel +49-228-9654055
> D-53121 Bonn X-Mozilla-Status: 00094057
> ------------------http://www.luna-park.de ------------------
>
>
>
> _______________________________________________
> Zope maillist - Zope@zope.org
> http://lists.zope.org/mailman/listinfo/zope
> No cross posts or HTML encoding!
> (Related lists -
> http://lists.zope.org/mailman/listinfo/zope-announce
> http://lists.zope.org/mailman/listinfo/zope-dev )
>