[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 )
>