home *** CD-ROM | disk | FTP | other *** search
- '''
- GRAPH (line, bar, whatever, ...) objects
-
- Richard Jones <richard@fulcrum.com.au>
-
- This module defines several top-level components:
- LineGraph - draws datasets with a line between the datapoints
- XTicks - decoration that gives regular tick-marks for X axis
- XTickValues - adds display of tick-mark values to XTicks
- XTickDates - adds display of date tick-marks to XTicks
- YTicks - same as XTicks but for Y axis
- YTickValues - adds display of tick-mark values to YTicks
- FloatValues - floating-point number renderer for TickValues classes
- ByteValues - byte number renderer for TickValues classes
- PieGraph - draws a pie graph given mappings 'name':value
- PieLabel - adds labels to a pie graph
-
- Sample usage:
- >>> from PILGraph import *
- >>> graph = LineGraph('P', (400,200), 0)
- >>> x = range(11)
- >>> y = [3.5,3,1,3,25,10,6,7,8,9,13]
- >>> graph.add_dataset('test', x, y)
- >>> y = [3.5,3,-1,3,22,10,6,7,8,9,13]
- >>> graph.add_dataset('test2', x, y)
- >>> graph.add_decoration(XTickValues(FloatValues(), mod_steps=1))
- >>> graph.add_decoration(XLabel('Test Time Scale'))
- >>> graph.add_decoration(YTickValues(ByteValues(align='right'), mod_steps=1))
- >>> graph.add_decoration(YLabel('Test Foo Scale (bytes)'))
- >>> graph.render()
- >>> graph.image.show()
- >>>
-
- version history
- 0.1 alpha 1 - first public release
- 0.1 alpha 2 - some internal changes, added X date axis, some cleanup &
- bugfixes
- 0.1 alpha 3 - moved path-stuffing from ImageFont (now unchanged) to here
- 0.1 alpha 4 - added PieGraph and PieLabels, cleaned up a bit, changed
- the colour scheme
- 0.1 alpha 5 - fixed XTickDates tick generation for monthly vals
- 0.1 alpha 6 - submission of ideas and code from "Roger Burnham"
- <roger.burnham@gte.net> (BarGraph, ScatterGraph added)
- - added fill argument to graph decorations
- - number-rounding mis-alignment of text labels fixed
- - FloatValues algorithm changed
- 0.1 alpha 7 - changed BarGraph to fit the current implementation plan
-
- BUGS:
- - BarGraph stuffs up the last tick on the X axis for XTicks
-
- TODO:
- - pass fill colour down to decorations from top level
- '''
-
- __version__ = '0.1a7'
-
- import os, math, time, sys
- import Image, ImagePalette, ImageDraw, ImagePath, ImageFont
- import SimplePalette
- import blue
-
- # add any PILGraph-accompanied font directories to the search path
- import site
- if sys.path[0] == '': path = '.'
- else: path = sys.path[0]
- site.addsitedir(path)
-
- def roundup(value):
- ''' Roung a floating point number up to the next highest integer.
- '''
- if value%1:
- value = int(value - value%1)
- else:
- value = int(value)
- return value
-
- class Graph:
- colours = ('blue', 'red', 'green', 'cyan', 'purple', 'yellow',
- 'lightblue', 'lightred', 'lightgreen')
-
- def __init__(self, mode, size, fill='white'):
- self.mode = mode
- self.size = size
- self.fill = fill
- self.data = {}
- self.decorations = {}
-
- def render(self):
- ''' Generic graph rendering. This handles the layout of the components
- of the graph image. They include a call to render_graph()
- which must be defined in a subclass. Decorations are defined as
- being horizontal (left and right of the graph) or vertical (top
- and bottom of the graph).
-
- Layout of decorations, including alignment:
- Vertical decorations are aligned along the line that separates the
- graph from the leftmost horizontal decorations. Thus all we have to do
- is add up their widths.
- Horizontal decorations are aligned along the line that separates the
- graph from the bottom-most vertical decorations. This is therefore the
- greater value of:
- 1. the heights of the top decorations and the graph or,
- 2. the highest leftmost decoration.
- The intersection of the horizontal and vertical alignment lines is
- also the origin of the graph.
- '''
- # render the graph
- self.create_image(self.mode, self.size, self.fill)
-
- # analyse the data
- self.analyse_data()
-
- self.render_graph()
-
- # these variables are used to add up all the decorations - horizontals
- # go into width, verticals go into height
- width = height = 0
-
- # these variables are used to keep track of the maximum width of
- # vertical decorations and height of the horizontal decorations - just in
- # case one of the decorations is larger than the graph in that dimension
- max_width = max_height = 0
-
- # these variables hold the alignment lines described in the doc above
- vert_align = horiz_align = 0
-
- # loop - render the decorations into their own images and figure
- # constraints
- top, bottom, left, right = [], [], [], []
- tops, bottoms, lefts, rights = 0, 0, 0, 0
- sides = {'top': top, 'bottom': bottom, 'left': left, 'right': right}
- keys = self.decorations.keys()
- keys.sort()
- for key in keys:
- decoration = self.decorations[key]
- decoration.render(self)
-
- # add to appropriate side
- sides[decoration.type[0]].append(decoration)
-
- # update new width & height
- if decoration.type[0] == 'top':
- max_width = max(decoration.image.size[0], max_width)
- tops = tops + decoration.image.size[1]
- elif decoration.type[0] == 'bottom':
- max_width = max(decoration.image.size[0], max_width)
- bottoms = bottoms + decoration.image.size[1]
- elif decoration.type[0] == 'left':
- max_height = max(decoration.image.size[1], max_height)
- lefts = lefts + decoration.image.size[0]
- else:
- max_height = max(decoration.image.size[1], max_height)
- rights = rights + decoration.image.size[0]
-
- # finish off the lists of decorations
- top.reverse()
- #left.reverse()
-
- # save a ref to the graph
- graph = self.image
-
- # ok, now figure the numbers
- horiz_align = max(max_height, graph.size[1] + tops)
- vert_align = lefts
- width = lefts + max(max_width, graph.size[0] + rights)
- height = horiz_align + bottoms
-
- # now we have the decorations and the sizes, let's make the real image
- # and render it!
- self.create_image(self.mode, (width, height), self.fill)
-
- # place the GRAPH! (woohoo!)
- self.image.paste(graph, (vert_align, horiz_align-graph.size[1]))
-
- # top decorations
- y = horiz_align - graph.size[1]
- for decoration in top:
- y = y - decoration.image.size[1]
- self.image.paste(decoration.image, (vert_align, y))
-
- # now draw the left horitonztal decorations!
- x = vert_align
- for decoration in left:
- x = x - decoration.image.size[0]
- self.image.paste(decoration.image, (x,
- horiz_align-decoration.image.size[1]))
-
- x, y = vert_align+graph.size[0], horiz_align
- # place the rest
- for decoration in bottom:
- self.image.paste(decoration.image, (vert_align, y))
- y = y + decoration.image.size[1]
- for decoration in right:
- self.image.paste(decoration.image, (x,
- horiz_align-decoration.image.size[1]))
- x = x + decoration.image.size[0]
-
- def add_decoration(self, decoration):
- self.decorations[decoration.type] = decoration
-
-
- class LineGraph(Graph):
- ''' Draw one or more datasets as line graphs.
- '''
- def __init__(self, *args, **kw):
- apply(Graph.__init__, (self, )+args, kw)
- self.transform = (1,0,0,0,1,0)
-
- def create_image(self, mode, size, fill='white'):
- # TODO support more than just 'P' mode images
- self.image = Image.new(mode, size, fill)
- self.draw = ImageDraw.ImageDraw(self.image)
- self.rgb = SimplePalette.Palette()
- self.rgb.simple_palette()
- self.image.putpalette(self.rgb.getpalette())
-
- def analyse_data(self):
- # determine minimum and maximum values
- max_x = min_x = max_y = min_y = None
- for points in self.data.values():
- for x,y in points:
- if min_x is None or x < min_x:
- min_x = x
- if max_x is None or x > max_x:
- max_x = x
- if min_y is None or y < min_y:
- min_y = y
- if max_y is None or y > max_y:
- max_y = y
- # store for later
-
- self.max_x, self.min_x, self.max_y, self.min_y = max_x, min_x, max_y, min_y
-
- self.data_max_x, self.data_min_x = max_x, min_x
- self.data_max_y, self.data_min_y = max_y, min_y
-
- # bollocks, we want zero
- self.min_y = 0
- self.data_min_y = 0
-
- # now go look for decorations and modify the transform's conditions
- # accordingly
- for decoration in self.decorations.itervalues():
- if hasattr(decoration, 'analyse_data'):
- decoration.analyse_data(self)
-
- # now figure the affine transform
- # x = ax + by + c
- # y = dx + ey + f
- b = d = 0
- deltaX = self.max_x - self.min_x
- if deltaX == 0:
- deltaX = 1
- a = (self.size[0]-1)/(deltaX)
- deltaY = self.max_y - self.min_y
- if deltaY == 0:
- deltaY = 1
- e = -(self.size[1]-1)/(deltaY)
- c = -a * self.min_x
- f = self.size[1] - (e * self.min_y) -1
- self.transform = (a,b,c,d,e,f)
-
- def add_dataset(self, name, x, y=None):
- if y is not None:
- points=[]
- for p in range(len(x)):
- points.append((x[p],y[p]))
- else:
- points = x
- points = ImagePath.Path(points)
- self.data[name] = points
-
- def render_graph(self):
- keys = self.data.keys()
- keys.sort()
- for dataset in range(len(self.data)):
- # set the colour
- # TODO: wrap colours and/or use drawing styles
- if len(self.data) > 1:
- self.draw.setink(self.rgb[self.colours[dataset]])
- else:
- self.draw.setink(self.rgb.black)
-
- # get the data, transform and draw
- points = self.data[keys[dataset]]
- points.transform(self.transform)
- self.draw.line(points)
-
-
- class ScatterGraph(LineGraph):
- def render_graph(self):
- keys = self.data.keys()
- keys.sort()
- self.draw.setfill(1)
- for dataset in range(len(self.data)):
- # get the data, transform and draw
- points = self.data[keys[dataset]]
- points.transform(self.transform)
- self.draw.setink(self.rgb[self.colours[dataset]])
- delta = 1
- for point in points:
- # self.draw.point(point, self.rgb.red)
- rect = (point[0]-delta, point[1]-delta,point[0]+1, point[1]+1)
- self.draw.ellipse(rect)
-
- class BarGraph(LineGraph):
- def __init__(self, *args, **kw):
- apply(Graph.__init__, (self, )+args, kw)
- self.transform = (1,0,0,0,1,0)
-
- def create_image(self, mode, size, fill='white'):
- # TODO support more than just 'P' mode images
- self.image = Image.new(mode, size, fill)
- self.draw = ImageDraw.ImageDraw(self.image)
- self.rgb = SimplePalette.Palette()
- self.rgb.simple_palette()
- self.image.putpalette(self.rgb.getpalette())
-
- def analyse_data(self):
- # determine minimum and maximum values
- max_x = min_x = max_y = min_y = None
- for points in self.data.values():
- for x,y in points:
- if min_x is None or x < min_x:
- min_x = x
- if max_x is None or x > max_x:
- max_x = x
- if min_y is None or y < min_y:
- min_y = y
- if max_y is None or y > max_y:
- max_y = y
- # store for later
- self.max_x, self.min_x, self.max_y, self.min_y = max_x, min_x, max_y, min_y
- self.data_max_x, self.data_min_x = max_x, min_x
- self.data_max_y, self.data_min_y = max_y, min_y
-
- # bollocks, we want zero
- self.min_y = 0
- self.data_min_y = 0
-
- # bar width
- key = self.data.keys()[0]
- self.bar_width = self.size[0] / len(self.data[key])
-
- # now go look for decorations and modify the transform's conditions
- # accordingly
- for decoration in self.decorations.values():
- if hasattr(decoration, 'analyse_data'):
- decoration.analyse_data(self)
-
- # now figure the affine transform
- # x = ax + by + c
- # y = dx + ey + f
- b = d = 0
- deltaX = self.max_x - self.min_x
- if deltaX == 0:
- deltaX = 1
- a = (self.size[0]-1)/(deltaX)
- deltaY = self.max_y - self.min_y
- if deltaY == 0:
- deltaY = 1
- e = -(self.size[1]-1)/(deltaY)
- # shift by bar width
- c = -a * self.min_x + (roundup(self.bar_width)/2 - 1)
- f = self.size[1] - (e * self.min_y) - 1
- self.transform = (a,b,c,d,e,f)
-
- def render_graph(self):
- keys = self.data.keys()
- keys.sort()
- self.draw.setfill(1)
- width = self.bar_width/2
- base = ImagePath.Path([(0,self.min_y)])
- base.transform(self.transform)
- base = base[0][1]
- for dataset in range(len(self.data)):
- # get the data, transform and draw
- points = self.data[keys[dataset]]
- points.transform(self.transform)
- self.draw.setink(self.rgb[self.colours[dataset]])
- right_edge = points[0][0] + (points[1][0]-points[0][0])/2
- self.draw.rectangle((points[0][0]-width, base+1, right_edge, points[0][1]))
- for i in range(1, len(points)-1):
- point = points[i]
- left_edge = right_edge
- right_edge = point[0] + (points[i+1][0]-point[0])/2
- self.draw.rectangle((left_edge, base+1, right_edge, point[1]))
- left_edge = right_edge
- self.draw.rectangle((left_edge, base+1, points[-1][0]+width, points[-1][1]))
-
- def avg_point(l):
- # l er listi af punktum
- ysum = 0
- for x,y in l:
- ysum += y
- return (l[-1][0], float(ysum) / float(len(l)))
-
- class PriceGraph(ScatterGraph):
- def __init__(self, *args, **kw):
- apply(ScatterGraph.__init__, (self, )+args, kw)
-
- def analyse_data(self):
- apply(ScatterGraph.analyse_data, (self, ))
- self.moving_average_data = []
- data = self.data.values()[0]
- print len(data)
- for i in range(10,len(data)):
- x,y = avg_point(data[i-10:i])
- print i, x, y
- self.moving_average_data.append((x,y))
-
- def render_graph(self):
- apply(ScatterGraph.render_graph, (self, ))
- self.draw.setink(self.rgb.red)
- points = ImagePath.Path(self.moving_average_data)
- points.transform(self.transform)
- self.draw.line(points)
-
- class StacketBarGraph(BarGraph):
- """
- Implement a BarGraph where each column can be stacked with many values, each with
- different colour
- """
- def __init__(self, *args, **kw):
- apply(BarGraph.__init__, (self, )+args, kw)
-
-
- class QuantityGraph(BarGraph):
- """
- Implement BarGraph but keep a space bewteen columns
- """
- def __init__(self, *args, **kw):
- apply(BarGraph.__init__, (self, )+args, kw)
-
- def render_graph(self):
- keys = self.data.keys()
- keys.sort()
- self.draw.setfill(1)
- width = (self.bar_width/2) - 50
- base = ImagePath.Path([(0,self.min_y)])
- base.transform(self.transform)
- base = base[0][1]
- for dataset in range(len(self.data)):
- # get the data, transform and draw
- points = self.data[keys[dataset]]
- points.transform(self.transform)
- self.draw.setink(self.rgb.blue)#self.rgb[self.colours[dataset]])
- for p in points:
- self.draw.rectangle((p[0]-1,base+1,p[0],p[1]))
- #self.draw.line((p[0]-1,base+1,p[0]+1,p[1]))
-
- class Decoration:
- ''' Must be subclassed to provide a render function and a type
- attribute.
- The type attribute must be a 2 entry tuple consisting of:
- (side, order)
- 'side' is used to determine which side of the graph the Decoration is
- placed. This is either 'top', 'bottom', 'left' or 'right'. The
- decoration will be aligned with the left (for top/bottom) or top
- (for left/right)
- 'order' is used to sort the Decorations that appear on the same side.
- Lower numbers are closer to the graph.
- '''
- fonts = {}
- def __init__(self, mod_steps=0):
- self.image = None
- self.step = None
- self.mod_steps = mod_steps
- self.points = None
- self.skip = 1
-
- def set_mod_steps(self, yesno):
- self.mod_steps = yesno
-
- def create_image(self, size, graph, fill='white'):
- self.image = Image.new(graph.mode, size, graph.rgb[fill])
- self.image.putpalette(graph.rgb.getpalette())
- self.draw = ImageDraw.ImageDraw(self.image)
- self.draw.setink(graph.rgb.black)
-
- def get_font(self, font):
- if not self.fonts.has_key(font):
- fontfilename = os.path.join(os.path.join(blue.os.respath, "common\\pilfonts"), font+'.pil')
- #print fontfilename
- self.fonts[font] = ImageFont.load_path(fontfilename)
- # self.fonts[font] = ImageFont.load_path(respath + "pilfonts\\" + font+'.pil')
- return self.fonts[font]
-
-
- def determine_step(min_value, max_value, number=5):
- ''' figure a step value for ticks between min_value and max_value that will
- give around 'number' "nice" steps. Nice steps are multiples of 1,
- 2 and 5.
- '''
- # figure a good initial list of nice tick placements
- range_ = float(max_value-min_value)
- if range_ <= 0.2:
- # all the values are the same
- magnitude = 2
- range_ = 1
- else:
- magnitude = math.log(abs(range_))/math.log(10)
- magnitude = roundup(magnitude)-1
- scale = 10. ** magnitude
- nicevals = map(lambda i,s=scale: i*s, [1., 2., 5., 10.])
-
- # figure the target distance between values for 5 ticks on the scale
- if range_ <= 1:
- return 1
-
- sep = range_/number
- step = 0
- while not step:
- for x in range(len(nicevals)-1):
- # check to see if we're in this range of nice values
- if nicevals[x] <= sep <= nicevals[x+1]:
- if (sep-nicevals[x]) <= (nicevals[x+1]<sep):
- step = nicevals[x]
- else:
- step = nicevals[x+1]
- break
- else:
- # scale it
- scale = scale * 10
- nicevals = map(lambda i,s=scale: i*s, [1., 2., 5., 10.])
- return step
-
- class NumericTickPoints:
- ''' Determine the tick point values for a graph axis that ranges in
- value from min_value to max_value.
- If step is not None, then use it to figure the ticks rather than
- trying to figure a "reasonable" step value.
- If mod_steps is 1, then try to put the ticks at multiples of the
- step value rather than at positions some multiple from the starting
- value. If mod_steps is 0 then the ticks will always appear at the
- start and end of the axis.
- '''
- def determine_tick_points(self, min_value, max_value):
- # determine step if we aren't supplied one
- step = self.step
- if step is None:
- step = determine_step(min_value, max_value)
-
- if self.mod_steps == 1:
- # round up to smallest integer multiple of step geater than max_value and
- # greatest integer multiple of step less than min_value
- if max_value%step:
- max_value = step * (int(max_value/step) + 1)
- min_value = min_value - (min_value%step)
-
- value = min_value
- self.points = [min_value]
- while value < max_value:
- value = value + step
- self.points.append(value)
-
-
- class XTicks:
- ''' Decorate the X axis of a graph with tick marks
- '''
- type = ('bottom', 1)
-
- def analyse_data(self, graph):
- # determine the tick marks
- self.determine_tick_points(graph.data_min_x, graph.data_max_x)
-
- # adjust min and max for this axis
- graph.max_x = max(self.points[-1], graph.data_max_x)
- graph.min_x = min(self.points[0], graph.data_min_x)
-
- def render(self, graph):
- # Figure the width of the scale.
- scale_range = (self.points[-1] - self.points[0])
- graph_range = (graph.max_x - graph.min_x)
- scale_size = roundup(graph.size[0] * scale_range/graph_range)
-
- # create the tick mark image
- self.create_image((scale_size, 8), graph)
-
- # only transform the X axis
- self.transform = graph.transform[:3]+(0, 1, 0)
-
- # draw the side of the graph
- line = ImagePath.Path([(self.points[0], 0), (self.points[-1], 0)])
- line.transform(self.transform)
- self.draw.line(line)
-
- # now draw the tick marks
- for tick in self.points:
- tick = ImagePath.Path([(tick, 1), (tick, 5)])
- tick.transform(self.transform)
- self.draw.line(tick)
-
-
- class YTicks:
- ''' Decorate the Y axis of a graph with tick marks
- '''
- type = ('left', 1)
-
- def analyse_data(self, graph):
- # determine the tick marks
- self.determine_tick_points(graph.data_min_y, graph.data_max_y)
-
- # adjust min and max for this axis
- graph.max_y = max(self.points[-1], graph.data_max_y)
- graph.min_y = min(self.points[0], graph.data_min_y)
-
- def render(self, graph):
- # Figure the height of the scale.
- scale_range = (self.points[-1] - self.points[0])
- graph_range = (graph.max_y - graph.min_y)
- if graph_range == 0:
- #print "graph_range == 0, setting to 2"
- graph_range = 2
- scale_size = roundup(graph.size[1] * scale_range/graph_range)
- y_shift = roundup(graph.transform[5] + scale_size - graph.size[1]) + 1
-
- # create the tick mark image
- self.create_image((8, scale_size), graph)
-
- # only transform the Y axis
- self.transform = (1, 0, 0) + graph.transform[3:5] + (y_shift,)
-
- # draw the side of the graph
- line = ImagePath.Path([(7, self.points[0]), (7, self.points[-1])])
- line.transform(self.transform)
- self.draw.line(line)
-
- # now draw the tick marks
- for tick in self.points:
- tick = ImagePath.Path([(2, tick), (6, tick)])
- tick.transform(self.transform)
- self.draw.line(tick)
-
-
-
- class XTickValues(Decoration, XTicks, NumericTickPoints):
- ''' Decorate the X axis of a graph with basic number markings.
- '''
- def __init__(self, valueRenderer, mod_steps=0, font='6x13', ink='black',
- fill='white'):
- Decoration.__init__(self, mod_steps)
- self.valueRenderer = valueRenderer
- self.font = font
- self.ink = ink
- self.fill = fill
-
- def render(self, graph):
- # render ticks to determine marking numbers
- XTicks.render(self, graph)
-
- # extra image size to hold numbers
- font = self.get_font(self.font)
- label_size = font.getsize(self.valueRenderer.template)
- ticks = self.image
- scale_size = (ticks.size[0]+label_size[0], ticks.size[1]+1+label_size[1])
- self.create_image(scale_size, graph, self.fill)
- self.image.paste(ticks, (0,0))
-
- # figure where to draw the labels
- path = ImagePath.Path(map(lambda x,s=ticks.size[1]+1: (x, s), self.points))
- path.transform(self.transform)
-
- # init the drawing
- self.draw.setfont(font)
- self.draw.setink(graph.rgb[self.ink])
-
- # draw the values
- for tick in range(len(self.points)):
- if tick%self.skip != 0:
- continue
- coords = path[tick]
- # TODO: not sure why PIL rounds the values here differently...
- coords = map(int, map(round, coords))
- tick = self.points[tick]
- self.draw.text(coords, self.valueRenderer(tick))
-
-
- class XTickDates(Decoration, XTicks):
- ''' Decorate the X axis of a graph with basic number markings.
- '''
- def __init__(self, mod_steps=0, font='6x13', ink='black', fill='white'):
- Decoration.__init__(self, mod_steps)
- self.font = font
- self.ink = ink
- self.fill = fill
- self.years = self.months = self.days = self.hours = self.minutes = 0
- self.seconds = 0
-
- def determine_tick_points(self, min_value, max_value):
- ''' Determine the tick point values for a graph axis that ranges in
- dates from min_value to max_value which represent time.time() values.
- If mod_steps is 1, then try to put the ticks at multiples of the
- step value rather than at positions some multiple from the starting
- value. If mod_steps is 0 then the ticks will always appear at the
- start and end of the axis.
- '''
- # determine step if we aren't supplied one
- start_date = time.localtime(min_value)
- end_date = time.localtime(max_value)
-
- year_d = int(end_date[0]-start_date[0])
- mon_d = int(year_d*12 + (end_date[1]-start_date[1]))
- sec_d = int(max_value-min_value)
- min_d = int(sec_d/60)
- hour_d = int(min_d/60)
- day_d = int(hour_d/24)
-
- #print year_d,"years", mon_d, "months", day_d, "days", hour_d, "hours", min_d, "min", sec_d, "sec"
-
- points = []
- if year_d > 3:
- # steps with be 6 monthly
- self.years = 1
- # mark every 6 months, possibly with a label
- step = 1
- if 3 < year_d < 10: self.skip = 2
- elif 0 < year_d < 4: self.months = 1
- else: step = determine_step(start_date[0], end_date[0])
- for year in range(start_date[0], end_date[0]+step, step):
- points.append((year, 1, 1, 0, 0, 0, 0, 0, 0))
- # mark the month with a tick
- if year_d < 10:
- points.append((year, 7, 1, 0, 0, 0, 0, 0, 0))
- elif mon_d > 4:
- # steps will be monthly
- year = start_date[0]
- if year_d: self.years = 1
- self.months = 1
- month = start_date[1]
- for i in range(0, mon_d+2):
- points.append((year, month, 1, 0, 0, 0, 0, 0, 0))
- month = month + 1
- # overflow of month
- if month > 12:
- self.years = 1
- year = year + 1
- month = 1
- elif day_d > 4:
- # steps will be daily
- year = start_date[0]
- month = start_date[1]
- if year_d: self.years = 1
- if mon_d: self.months = 1
- self.days = 1
- self.skip = int(day_d/5)
- for day in range(start_date[2], start_date[2]+day_d+1):
- points.append((year, month, day, 0, 0, 0, 0, 0, 0))
- elif hour_d > 4:
- # steps will be hourly
- #f "hourly"
- self.hours = 1
- self.skip = int(hour_d/10)
- for hour in range(start_date[3], start_date[3]+hour_d+1):
- points.append(start_date[:3]+(hour, 0, 0, 0, 0, 0))
- elif min_d > 5:
- #print "every minute"
- # steps will be every minute
- self.minutes = 1
- self.skip = int(min_d/5)
- for minute in range(start_date[4], start_date[4]+min_d+1):
- points.append(start_date[:4]+(minute, 0, 0, 0, 0))
- elif end_date[5] != start_date[5]:
- # steps will be every second
- self.seconds = 1
- self.skip = int((end_date[5] - start_date[5])/5)
- for second in range(start_date[5], start_date[5]+sec_d+1):
- points.append(start_date[:5]+(second, 0, 0, 0))
-
- # make time values
-
- self.points = []
- for point in points:
- self.points.append(time.mktime(point))
-
- month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
- "Oct", "Nov", "Dec"]
-
- def render(self, graph):
- # render ticks to determine marking numbers
- XTicks.render(self, graph)
-
- # figure components of date to be displayed:
- # self.years, self.months, self.days, self.hours, self.minutes,
- # self.seconds
-
- # extra image size to hold numbers
- font = self.get_font(self.font)
-
- # figure length of label
- # TODO: add the time cases here and in the drawing
- if self.months and self.days:
- length = 5
- elif self.years:
- length = 4
- elif self.months:
- length = 3
- else:
- length = 2
- if self.years and self.months:
- height = 2
- else:
- height = 1
- label_size = font.getsize('M'*length)
- # label height in pixels (plus spacer)
- lab_p = (1 + label_size[1])
-
- ticks = self.image
- scale_size = (ticks.size[0] + label_size[0], ticks.size[1] + (lab_p*height))
- self.create_image(scale_size, graph, self.fill)
- self.image.paste(ticks, (0,0))
-
- # figure where to draw the labels
- path = ImagePath.Path(map(lambda x,s=ticks.size[1]+1: (x, s), self.points))
- path.transform(self.transform)
-
- # init the drawing
- self.draw.setfont(font)
- self.draw.setink(graph.rgb[self.ink])
-
- # draw the values
- prev_mon = prev_year = None
- for tick in range(len(self.points)):
- if self.skip and tick%self.skip != 0:
- continue
- # TODO: PIL doesn't seem to be able to handle some float values in paste()
- coords = (int(path[tick][0]), int(path[tick][1]))
- tick = time.localtime(self.points[tick])
- if self.days:
- text = '%d'%tick[2]
- if self.months and tick[1] != prev_mon:
- text = text + ' %s'%self.month_names[tick[1]-1]
- prev_mon = tick[1]
- self.draw.text(coords, text)
- if self.years and tick[0] != prev_year:
- self.draw.text((coords[0], coords[1]+lab_p), '%d'%tick[0])
- prev_year = tick[0]
- elif self.months:
- self.draw.text(coords, self.month_names[tick[1]-1])
- if self.years and tick[0] != prev_year:
- self.draw.text((coords[0], coords[1]+lab_p), '%d'%tick[0])
- prev_year = tick[0]
- elif self.years:
- self.draw.text(coords, '%d'%tick[0])
-
-
- class YTickValues(Decoration, YTicks, NumericTickPoints):
- ''' Decorate the X axis of a graph with basic number markings.
- '''
- def __init__(self, valueRenderer, mod_steps=0, font='6x13', ink='black',
- fill='white'):
- Decoration.__init__(self, mod_steps)
- self.valueRenderer = valueRenderer
- self.font = font
- self.ink = ink
- self.fill = fill
-
- def render(self, graph):
- # render ticks to determine marking numbers
- YTicks.render(self, graph)
-
- # extra image size to hold numbers
- font = self.get_font(self.font)
- label_size = font.getsize(self.valueRenderer.template)
- ticks = self.image
- scale_size = (label_size[0]+1+ticks.size[0], ticks.size[1]+label_size[1])
- self.create_image(scale_size, graph, self.fill)
- self.image.paste(ticks, (label_size[0]+1, label_size[1]))
-
- # figure where to draw the labels
- path = ImagePath.Path(map(lambda x: (0, x), self.points))
- path.transform(self.transform)
-
- # init the drawing
- self.draw.setfont(font)
- self.draw.setink(graph.rgb[self.ink])
-
- # draw the values
- for tick in range(len(self.points)):
- if tick%self.skip != 0:
- continue
- coords = path[tick]
- # TODO: not sure why PIL rounds the values here differently...
- coords = map(int, map(round, coords))
- tick = self.points[tick]
- self.draw.text(coords, self.valueRenderer(tick))
-
-
-
- class FloatValues:
- # 1.23e-45 (worst case...)
- template = 'MMMMMMMM'
-
- def __init__(self, align='left'):
- self.len_template = len(self.template)
- self.align = align
- self.fract = "%%.%df"
- self.e_fract = "%%.%dfe%d"
- self.e_integer = "%de%d"
- self.e_float = "%fe%d"
-
- def __call__(self, value):
- if value == 0:
- if self.align=='right':
- return " 0"
- else:
- return "0"
-
- mag = math.log(abs(value))/math.log(10)
-
- tl = self.len_template
- imag = int(mag)
- if value >= 0:
- if -6 <= mag < 6:
- s = "%*.*f" % (tl, tl-2-imag, value)
- else:
- exp = '%d' % int(mag)
- le = len(exp)
- val = value * 10.0**-int(mag)
- s = '%*.*fe%s' % (tl-2-le, tl-3-le, val, exp)
- else:
- if -5 <= mag < 5:
- s = "%*.*f" % (tl, tl-3-imag, value)
- else:
- exp = '%d' % int(mag)
- le = len(exp)
- val = value * 10.0**-int(mag)
- s = '%*.*fe%s' % (tl-3-le, tl-4-le, val, exp)
-
- while s[-1:] == '0':
- s = s[:-1]
- if s[-1:] == '.':
- s = s[:-1]
- s = s[:tl]
- if self.align=='left':
- return s
- else:
- return " "*(tl-len(s)) + s
-
-
-
- class ByteValues:
- ''' Values must be integer and will be scaled by 1024 and have letter
- suffixes when scaled.
- '''
- # 1024K
- template = 'MMMMM'
- scales = ' KMGT'
-
- def __init__(self, align='left'):
- self.align = align
-
- def __call__(self, value):
- if value == 0:
- if self.align=='right':
- return " 0"
- else:
- return "0"
-
- # figure how big our number is...
- magnitude = math.log(abs(value))/math.log(2)
- scale = int(magnitude/10)
-
- if scale:
- value = value / (1024**scale)
- if self.align=='right':
- if scale:
- return "%4d%s"%(value, self.scales[scale])
- else:
- return "%5d"%value
- else:
- return "%d%s"%(value, self.scales[scale])
-
-
- class Values:
- ''' Values must be integer, nothing will be done to it
- '''
- template = 'MMMMM'
-
- def __init__(self, align='left'):
- self.align = align
-
- def __call__(self, value):
- if self.align == 'right':
- return "%5d"%value
- else:
- return "%d"%value
-
-
- class XLabel(Decoration):
- ''' Decorate the X axis of a graph with a text label.
- '''
- type = ('bottom', 2)
- def __init__(self, text, font='6x13', ink='black', fill='white'):
- Decoration.__init__(self)
- self.text = text
- self.font = font
- self.ink = ink
- self.fill = fill
-
- def render(self, graph):
- # determine the tick marks
- font = self.get_font(self.font)
- label_size = font.getsize(self.text)
- self.create_image(label_size, graph, self.fill)
-
- # draw the label
- self.draw.setfont(font)
- self.draw.setink(graph.rgb[self.ink])
- self.draw.text((0,0), self.text)
-
-
-
- class YLabel(XLabel):
- ''' Decorate the Y axis of a graph with a text label. Same as X axis except
- for placement and rotation.
- '''
- type = ('left', 2)
- def render(self, graph):
- XLabel.render(self, graph)
- self.image = self.image.rotate(90)
-
-
- class Title(XLabel):
- ''' Decorate the top of a graph with a text label. Same as X axis except
- for placement.
- '''
- type = ('top', 1)
-
-
- class PieGraph(Graph):
- ''' Draw a pie graph based on the values in self.data.
-
- If there is a labels decoration, then the number of labels that
- will fit vertically beside the graph image.
- '''
- colours = ('lightblue', 'lightred', 'lightgreen', 'cyan', 'purple',
- 'yellow', 'red', 'blue', 'green')
- def __init__(self, *args, **kw):
- apply(Graph.__init__, (self, )+args, kw)
- self.threshold = kw.get('threshold', None)
- self.numlabels = kw.get('numlabels', None)
- self.labels = []
-
- def create_image(self, mode, size, fill='white'):
- # TODO support more than just 'P' mode images
- # XXX handle extra width for bars
- self.image = Image.new(mode, size, fill)
- self.draw = ImageDraw.ImageDraw(self.image)
- self.rgb = SimplePalette.Palette()
- self.rgb.simple_palette()
- self.image.putpalette(self.rgb.getpalette())
-
- def analyse_data(self):
- # determine the order of the labels
- labels = []
- self.total = 0
- for key, value in self.data.items():
- labels.append((value, key))
- self.total = self.total + value
- labels.sort()
- labels.reverse()
-
- # now make the real labels list
- self.labels = []
- for entry in labels:
- self.labels.append(entry[1])
-
- # now go look for decorations and modify the transform's conditions
- # accordingly
- for decoration in self.decorations.values():
- if hasattr(decoration, 'analyse_data'):
- decoration.analyse_data(self)
-
- def add_dataset(self, name, value=None):
- ''' call with either add_dataset({mapping of name:value}) or
- add_dataset(name, value)
- '''
- if value is not None:
- name = {name: value}
- self.data.update(name)
-
- def render_graph(self):
- width, height = self.image.size
- width, height = (width-1, height-1)
-
- total = 0
- self.draw.setfill(1)
-
- # draw the data set[s]
- start = 0.
- for entry in range(len(self.labels)):
- # get the data
- value = self.data[self.labels[entry]]
-
- # set the colour
- # TODO: wrap colours and/or use drawing styles
- self.draw.setink(self.rgb[self.colours[entry]])
-
- end = start + (360. * value)/self.total
- self.draw.pieslice((0,0,width,height), start, end)
- start = end
-
-
-
-
-
-
- class PieLabels(Decoration):
- ''' Decorate a Pie Chart with some labels
- '''
- type = ('right', 1)
- def __init__(self, font='6x13', ink='black', fill='white'):
- Decoration.__init__(self)
- self.font = font
- self.ink = ink
- self.fill = fill
-
- def analyse_data(self, graph):
- # we've got a list of labels, now we need to figure out how many we can
- # draw
- font = self.get_font(self.font)
- height = font.getsize('M')[1]
- numlabels = graph.image.size[1]/(height+1)
- if numlabels >= len(graph.labels):
- return
-
- # chop labels list
- graph.labels, other = graph.labels[:numlabels], graph.labels[numlabels:]
-
- # figure the value of other
- other=0
- for label in other:
- other = other + graph.data[label]
- graph.labels.append('other')
- graph.data['other'] = other
-
- def render(self, graph):
- # figure image size
- font = self.get_font(self.font)
- height = font.getsize('M')[1]
- im_width = 0
- for label in graph.labels:
- im_width = max(im_width, font.getsize(label)[0] + height + 1)
-
- # create the tick mark image
- numlabels = len(graph.labels)
- self.create_image((im_width, (height+2)*numlabels), graph, self.fill)
- self.draw.setfont(font)
-
- # now draw the labels
- y = 0
- for label in range(numlabels):
- # colour reference box
- self.draw.setink(graph.rgb[graph.colours[label]])
- self.draw.setfill(1)
- self.draw.rectangle((0, y, height-1, y+height-1))
-
- # now the label
- self.draw.setink(graph.rgb.black)
- self.draw.setfill(0)
- self.draw.text((height+2, y), graph.labels[label])
-
- y = y + height
-
-
-
-
- if __name__=='__main__':
- # start = time.mktime((1999,1,1,0,0,0,0,0,0))
- # end = time.mktime((1999,12,31,0,0,0,0,0,0))
- # y = [0]*365
- # import random
- # for i in range(365):
- # y[i] = y[i-1] + random.random() - 0.5
- # x = range(start, end, 60*60*24)
- x = range(10)
- y = range(-10, 10, 2)
- graph = BarGraph('P', (600,200), 0)
- graph.add_dataset('a', x, y)
- # graph.add_decoration(XTickDates())
- graph.add_decoration(XTickValues(FloatValues()))
- graph.add_decoration(XLabel('Test Time Scale'))
- graph.add_decoration(YTickValues(FloatValues(align='right')))
- graph.add_decoration(YLabel('Test Foo Scale'))
- graph.render()
- graph.image.show()
-
- # graph = PieGraph('P', (400,200), 0)
- # graph.add_dataset({'USA Commercial': 41, 'Non-Profit Making Organisations': 15, 'Netherlands': 23, 'USA Educational': 28, 'Network':
- # 65, 'Australia': 494, '143.227': 3})
- # graph.add_decoration(PieLabels())
- # graph.render()
-
- #graph.image.save('out.gif')
-
-