home *** CD-ROM | disk | FTP | other *** search
/ Enter 2004 April / enter-2004-04.iso / files / EVE_1424_100181.exe / PILGraph.py < prev    next >
Encoding:
Python Source  |  2004-04-20  |  42.3 KB  |  1,198 lines

  1. '''
  2.  GRAPH (line, bar, whatever, ...) objects
  3.  
  4.  Richard Jones <richard@fulcrum.com.au>
  5.  
  6.  This module defines several top-level components:
  7.   LineGraph   - draws datasets with a line between the datapoints
  8.     XTicks      - decoration that gives regular tick-marks for X axis
  9.     XTickValues - adds display of tick-mark values to XTicks
  10.     XTickDates  - adds display of date tick-marks to XTicks
  11.     YTicks      - same as XTicks but for Y axis
  12.     YTickValues - adds display of tick-mark values to YTicks
  13.     FloatValues - floating-point number renderer for TickValues classes
  14.   ByteValues  - byte number renderer for TickValues classes
  15.   PieGraph    - draws a pie graph given mappings 'name':value
  16.   PieLabel    - adds labels to a pie graph
  17.  
  18. Sample usage:
  19. >>> from PILGraph import *
  20. >>> graph = LineGraph('P', (400,200), 0)
  21. >>> x = range(11)
  22. >>> y = [3.5,3,1,3,25,10,6,7,8,9,13]
  23. >>> graph.add_dataset('test', x, y)
  24. >>> y = [3.5,3,-1,3,22,10,6,7,8,9,13]
  25. >>> graph.add_dataset('test2', x, y)
  26. >>> graph.add_decoration(XTickValues(FloatValues(), mod_steps=1))
  27. >>> graph.add_decoration(XLabel('Test Time Scale'))
  28. >>> graph.add_decoration(YTickValues(ByteValues(align='right'), mod_steps=1))
  29. >>> graph.add_decoration(YLabel('Test Foo Scale (bytes)'))
  30. >>> graph.render()
  31. >>> graph.image.show()
  32. >>>
  33.  
  34.  version history
  35.    0.1 alpha 1 - first public release
  36.    0.1 alpha 2 - some internal changes, added X date axis, some cleanup &
  37.                    bugfixes
  38.    0.1 alpha 3 - moved path-stuffing from ImageFont (now unchanged) to here
  39.    0.1 alpha 4 - added PieGraph and PieLabels, cleaned up a bit, changed
  40.                                  the colour scheme
  41.    0.1 alpha 5 - fixed XTickDates tick generation for monthly vals
  42.      0.1 alpha 6 - submission of ideas and code from "Roger Burnham"
  43.                      <roger.burnham@gte.net> (BarGraph, ScatterGraph added)
  44.                              - added fill argument to graph decorations
  45.                              - number-rounding mis-alignment of text labels fixed
  46.                              - FloatValues algorithm changed
  47.      0.1 alpha 7 - changed BarGraph to fit the current implementation plan
  48.  
  49.     BUGS:
  50.         - BarGraph stuffs up the last tick on the X axis for XTicks
  51.  
  52.   TODO:
  53.       - pass fill colour down to decorations from top level
  54. '''
  55.  
  56. __version__ = '0.1a7'
  57.  
  58. import os, math, time, sys
  59. import Image, ImagePalette, ImageDraw, ImagePath, ImageFont
  60. import SimplePalette
  61. import blue
  62.  
  63. # add any PILGraph-accompanied font directories to the search path
  64. import site
  65. if sys.path[0] == '': path = '.'
  66. else: path = sys.path[0]
  67. site.addsitedir(path)
  68.  
  69. def roundup(value):
  70.     ''' Roung a floating point number up to the next highest integer.
  71.     '''
  72.     if value%1:
  73.         value = int(value - value%1)
  74.     else:
  75.         value = int(value)
  76.     return value
  77.  
  78. class Graph:
  79.     colours = ('blue', 'red',  'green', 'cyan', 'purple', 'yellow',
  80.         'lightblue', 'lightred', 'lightgreen')
  81.  
  82.     def __init__(self, mode, size, fill='white'):
  83.         self.mode = mode
  84.         self.size = size
  85.         self.fill = fill
  86.         self.data = {}
  87.         self.decorations = {}
  88.  
  89.     def render(self):
  90.         ''' Generic graph rendering. This handles the layout of the components
  91.                  of the graph image. They include a call to render_graph()
  92.                  which must be defined in a subclass. Decorations are defined as
  93.                  being horizontal (left and right of the graph) or vertical (top
  94.                  and bottom of the graph).
  95.  
  96.         Layout of decorations, including alignment:
  97.          Vertical decorations are aligned along the line that separates the
  98.           graph from the leftmost horizontal decorations. Thus all we have to do
  99.           is add up their widths.
  100.          Horizontal decorations are aligned along the line that separates the
  101.           graph from the bottom-most vertical decorations. This is therefore the
  102.           greater value of:
  103.            1. the heights of the top decorations and the graph or,
  104.            2. the highest leftmost decoration.
  105.          The intersection of the horizontal and vertical alignment lines is
  106.           also the origin of the graph.
  107.         '''
  108.         # render the graph
  109.         self.create_image(self.mode, self.size, self.fill)
  110.  
  111.         # analyse the data
  112.         self.analyse_data()
  113.  
  114.         self.render_graph()
  115.  
  116.         # these variables are used to add up all the decorations - horizontals
  117.         # go into width, verticals go into height
  118.         width = height = 0
  119.  
  120.         # these variables are used to keep track of the maximum width of
  121.         # vertical decorations and height of the horizontal decorations - just in
  122.         # case one of the decorations is larger than the graph in that dimension
  123.         max_width = max_height = 0
  124.  
  125.         # these variables hold the alignment lines described in the doc above
  126.         vert_align = horiz_align = 0
  127.  
  128.         # loop - render the decorations into their own images and figure
  129.         # constraints
  130.         top, bottom, left, right = [], [], [], []
  131.         tops, bottoms, lefts, rights = 0, 0, 0, 0
  132.         sides = {'top': top, 'bottom': bottom, 'left': left, 'right': right}
  133.         keys = self.decorations.keys()
  134.         keys.sort()
  135.         for key in keys:
  136.             decoration = self.decorations[key]
  137.             decoration.render(self)
  138.  
  139.             # add to appropriate side
  140.             sides[decoration.type[0]].append(decoration)
  141.  
  142.             # update new width & height
  143.             if decoration.type[0] == 'top':
  144.                 max_width = max(decoration.image.size[0], max_width)
  145.                 tops = tops + decoration.image.size[1]
  146.             elif decoration.type[0] == 'bottom':
  147.                 max_width = max(decoration.image.size[0], max_width)
  148.                 bottoms = bottoms + decoration.image.size[1]
  149.             elif decoration.type[0] == 'left':
  150.                 max_height = max(decoration.image.size[1], max_height)
  151.                 lefts = lefts + decoration.image.size[0]
  152.             else:
  153.                 max_height = max(decoration.image.size[1], max_height)
  154.                 rights = rights + decoration.image.size[0]
  155.  
  156.         # finish off the lists of decorations
  157.         top.reverse()
  158.         #left.reverse()
  159.  
  160.         # save a ref to the graph
  161.         graph = self.image
  162.  
  163.         # ok, now figure the numbers
  164.         horiz_align = max(max_height, graph.size[1] + tops)
  165.         vert_align = lefts
  166.         width = lefts + max(max_width, graph.size[0] + rights)
  167.         height = horiz_align + bottoms
  168.  
  169.         # now we have the decorations and the sizes, let's make the real image
  170.         # and render it!
  171.         self.create_image(self.mode, (width, height), self.fill)
  172.  
  173.         # place the GRAPH! (woohoo!)
  174.         self.image.paste(graph, (vert_align, horiz_align-graph.size[1]))
  175.  
  176.         # top decorations
  177.         y = horiz_align - graph.size[1]
  178.         for decoration in top:
  179.             y = y - decoration.image.size[1]
  180.             self.image.paste(decoration.image, (vert_align, y))
  181.  
  182.         # now draw the left horitonztal decorations!
  183.         x = vert_align
  184.         for decoration in left:
  185.             x = x - decoration.image.size[0]
  186.             self.image.paste(decoration.image, (x,
  187.                 horiz_align-decoration.image.size[1]))
  188.  
  189.         x, y = vert_align+graph.size[0], horiz_align
  190.         # place the rest
  191.         for decoration in bottom:
  192.             self.image.paste(decoration.image, (vert_align, y))
  193.             y = y + decoration.image.size[1]
  194.         for decoration in right:
  195.             self.image.paste(decoration.image, (x,
  196.                 horiz_align-decoration.image.size[1]))
  197.             x = x + decoration.image.size[0]
  198.  
  199.     def add_decoration(self, decoration):
  200.         self.decorations[decoration.type] = decoration
  201.  
  202.  
  203. class LineGraph(Graph):
  204.     ''' Draw one or more datasets as line graphs.
  205.     '''
  206.     def __init__(self, *args, **kw):
  207.         apply(Graph.__init__, (self, )+args, kw)
  208.         self.transform = (1,0,0,0,1,0)
  209.  
  210.     def create_image(self, mode, size, fill='white'):
  211.         # TODO support more than just 'P' mode images
  212.         self.image = Image.new(mode, size, fill)
  213.         self.draw = ImageDraw.ImageDraw(self.image)
  214.         self.rgb = SimplePalette.Palette()
  215.         self.rgb.simple_palette()
  216.         self.image.putpalette(self.rgb.getpalette())
  217.  
  218.     def analyse_data(self):
  219.         # determine minimum and maximum values
  220.         max_x = min_x = max_y = min_y = None
  221.         for points in self.data.values():
  222.             for x,y in points:
  223.                 if min_x is None or x < min_x:
  224.                     min_x = x
  225.                 if max_x is None or x > max_x:
  226.                     max_x = x
  227.                 if min_y is None or y < min_y:
  228.                     min_y = y
  229.                 if max_y is None or y > max_y:
  230.                     max_y = y
  231.         # store for later
  232.  
  233.         self.max_x, self.min_x, self.max_y, self.min_y = max_x, min_x, max_y, min_y
  234.  
  235.         self.data_max_x, self.data_min_x = max_x, min_x
  236.         self.data_max_y, self.data_min_y = max_y, min_y
  237.  
  238.         # bollocks, we want zero
  239.         self.min_y = 0
  240.         self.data_min_y = 0
  241.  
  242.         # now go look for decorations and modify the transform's conditions
  243.         # accordingly
  244.         for decoration in self.decorations.itervalues():
  245.             if hasattr(decoration, 'analyse_data'):
  246.                 decoration.analyse_data(self)
  247.  
  248.         # now figure the affine transform
  249.         #  x = ax + by + c
  250.         #  y = dx + ey + f
  251.         b = d = 0
  252.         deltaX = self.max_x - self.min_x
  253.         if deltaX == 0:
  254.             deltaX = 1
  255.         a = (self.size[0]-1)/(deltaX)
  256.         deltaY = self.max_y - self.min_y
  257.         if deltaY == 0:
  258.             deltaY = 1
  259.         e = -(self.size[1]-1)/(deltaY)
  260.         c = -a * self.min_x
  261.         f = self.size[1] - (e * self.min_y) -1
  262.         self.transform = (a,b,c,d,e,f)
  263.  
  264.     def add_dataset(self, name, x, y=None):
  265.         if y is not None:
  266.             points=[]
  267.             for p in range(len(x)):
  268.                 points.append((x[p],y[p]))
  269.         else:
  270.             points = x
  271.         points = ImagePath.Path(points)
  272.         self.data[name] = points
  273.  
  274.     def render_graph(self):
  275.         keys = self.data.keys()
  276.         keys.sort()
  277.         for dataset in range(len(self.data)):
  278.             # set the colour
  279.             # TODO: wrap colours and/or use drawing styles
  280.             if len(self.data) > 1:
  281.                 self.draw.setink(self.rgb[self.colours[dataset]])
  282.             else:
  283.                 self.draw.setink(self.rgb.black)
  284.  
  285.             # get the data, transform and draw
  286.             points = self.data[keys[dataset]]
  287.             points.transform(self.transform)
  288.             self.draw.line(points)
  289.  
  290.  
  291. class ScatterGraph(LineGraph):
  292.     def render_graph(self):
  293.         keys = self.data.keys()
  294.         keys.sort()
  295.         self.draw.setfill(1)
  296.         for dataset in range(len(self.data)):
  297.             # get the data, transform and draw
  298.             points = self.data[keys[dataset]]
  299.             points.transform(self.transform)
  300.             self.draw.setink(self.rgb[self.colours[dataset]])
  301.             delta = 1
  302.             for point in points:
  303.                 # self.draw.point(point, self.rgb.red)
  304.                 rect = (point[0]-delta, point[1]-delta,point[0]+1, point[1]+1)
  305.                 self.draw.ellipse(rect)
  306.  
  307. class BarGraph(LineGraph):
  308.     def __init__(self, *args, **kw):
  309.         apply(Graph.__init__, (self, )+args, kw)
  310.         self.transform = (1,0,0,0,1,0)
  311.  
  312.     def create_image(self, mode, size, fill='white'):
  313.         # TODO support more than just 'P' mode images
  314.         self.image = Image.new(mode, size, fill)
  315.         self.draw = ImageDraw.ImageDraw(self.image)
  316.         self.rgb = SimplePalette.Palette()
  317.         self.rgb.simple_palette()
  318.         self.image.putpalette(self.rgb.getpalette())
  319.  
  320.     def analyse_data(self):
  321.         # determine minimum and maximum values
  322.         max_x = min_x = max_y = min_y = None
  323.         for points in self.data.values():
  324.             for x,y in points:
  325.                 if min_x is None or x < min_x:
  326.                     min_x = x
  327.                 if max_x is None or x > max_x:
  328.                     max_x = x
  329.                 if min_y is None or y < min_y:
  330.                     min_y = y
  331.                 if max_y is None or y > max_y:
  332.                     max_y = y
  333.         # store for later
  334.         self.max_x, self.min_x, self.max_y, self.min_y = max_x, min_x, max_y, min_y
  335.         self.data_max_x, self.data_min_x = max_x, min_x
  336.         self.data_max_y, self.data_min_y = max_y, min_y
  337.  
  338.         # bollocks, we want zero
  339.         self.min_y = 0
  340.         self.data_min_y = 0
  341.  
  342.         # bar width
  343.         key = self.data.keys()[0]
  344.         self.bar_width = self.size[0] / len(self.data[key])
  345.  
  346.         # now go look for decorations and modify the transform's conditions
  347.         # accordingly
  348.         for decoration in self.decorations.values():
  349.             if hasattr(decoration, 'analyse_data'):
  350.                 decoration.analyse_data(self)
  351.  
  352.         # now figure the affine transform
  353.         #  x = ax + by + c
  354.         #  y = dx + ey + f
  355.         b = d = 0
  356.         deltaX = self.max_x - self.min_x
  357.         if deltaX == 0:
  358.             deltaX = 1
  359.         a = (self.size[0]-1)/(deltaX)
  360.         deltaY = self.max_y - self.min_y
  361.         if deltaY == 0:
  362.             deltaY = 1
  363.         e = -(self.size[1]-1)/(deltaY)
  364.         # shift by bar width
  365.         c = -a * self.min_x + (roundup(self.bar_width)/2 - 1)
  366.         f = self.size[1] - (e * self.min_y) - 1
  367.         self.transform = (a,b,c,d,e,f)
  368.  
  369.     def render_graph(self):
  370.         keys = self.data.keys()
  371.         keys.sort()
  372.         self.draw.setfill(1)
  373.         width = self.bar_width/2
  374.         base = ImagePath.Path([(0,self.min_y)])
  375.         base.transform(self.transform)
  376.         base = base[0][1]
  377.         for dataset in range(len(self.data)):
  378.             # get the data, transform and draw
  379.             points = self.data[keys[dataset]]
  380.             points.transform(self.transform)
  381.             self.draw.setink(self.rgb[self.colours[dataset]])
  382.             right_edge = points[0][0] + (points[1][0]-points[0][0])/2
  383.             self.draw.rectangle((points[0][0]-width, base+1, right_edge, points[0][1]))
  384.             for i in range(1, len(points)-1):
  385.                 point = points[i]
  386.                 left_edge = right_edge
  387.                 right_edge = point[0] + (points[i+1][0]-point[0])/2
  388.                 self.draw.rectangle((left_edge, base+1, right_edge, point[1]))
  389.             left_edge = right_edge
  390.             self.draw.rectangle((left_edge, base+1, points[-1][0]+width, points[-1][1]))
  391.  
  392. def avg_point(l):
  393.     # l er listi af punktum
  394.     ysum = 0
  395.     for x,y in l:
  396.         ysum += y
  397.     return (l[-1][0], float(ysum) / float(len(l)))
  398.  
  399. class PriceGraph(ScatterGraph):
  400.     def __init__(self, *args, **kw):
  401.         apply(ScatterGraph.__init__, (self, )+args, kw)
  402.  
  403.     def analyse_data(self):
  404.         apply(ScatterGraph.analyse_data, (self, ))
  405.         self.moving_average_data = []
  406.         data = self.data.values()[0]
  407.         print len(data)
  408.         for i in range(10,len(data)):
  409.             x,y = avg_point(data[i-10:i])
  410.             print i, x, y
  411.             self.moving_average_data.append((x,y))
  412.  
  413.     def render_graph(self):
  414.         apply(ScatterGraph.render_graph, (self, ))
  415.         self.draw.setink(self.rgb.red)
  416.         points = ImagePath.Path(self.moving_average_data)
  417.         points.transform(self.transform)
  418.         self.draw.line(points)
  419.  
  420. class StacketBarGraph(BarGraph):
  421.     """
  422.     Implement a BarGraph where each column can be stacked with many values, each with
  423.     different colour
  424.     """
  425.     def __init__(self, *args, **kw):
  426.         apply(BarGraph.__init__, (self, )+args, kw)
  427.  
  428.  
  429. class QuantityGraph(BarGraph):
  430.     """
  431.     Implement BarGraph but keep a space bewteen columns
  432.     """
  433.     def __init__(self, *args, **kw):
  434.         apply(BarGraph.__init__, (self, )+args, kw)
  435.  
  436.     def render_graph(self):
  437.         keys = self.data.keys()
  438.         keys.sort()
  439.         self.draw.setfill(1)
  440.         width = (self.bar_width/2) - 50
  441.         base = ImagePath.Path([(0,self.min_y)])
  442.         base.transform(self.transform)
  443.         base = base[0][1]
  444.         for dataset in range(len(self.data)):
  445.             # get the data, transform and draw
  446.             points = self.data[keys[dataset]]
  447.             points.transform(self.transform)
  448.             self.draw.setink(self.rgb.blue)#self.rgb[self.colours[dataset]])
  449.             for p in points:
  450.                 self.draw.rectangle((p[0]-1,base+1,p[0],p[1]))
  451.                 #self.draw.line((p[0]-1,base+1,p[0]+1,p[1]))
  452.  
  453. class Decoration:
  454.     ''' Must be subclassed to provide a render function and a type
  455.             attribute.
  456.             The type attribute must be a 2 entry tuple consisting of:
  457.              (side, order)
  458.             'side' is used to determine which side of the graph the Decoration is
  459.              placed. This is either 'top', 'bottom', 'left' or 'right'. The
  460.              decoration will be aligned with the left (for top/bottom) or top
  461.              (for left/right)
  462.             'order' is used to sort the Decorations that appear on the same side.
  463.              Lower numbers are closer to the graph.
  464.     '''
  465.     fonts = {}
  466.     def __init__(self, mod_steps=0):
  467.         self.image = None
  468.         self.step = None
  469.         self.mod_steps = mod_steps
  470.         self.points = None
  471.         self.skip = 1
  472.  
  473.     def set_mod_steps(self, yesno):
  474.         self.mod_steps = yesno
  475.  
  476.     def create_image(self, size, graph, fill='white'):
  477.         self.image = Image.new(graph.mode, size, graph.rgb[fill])
  478.         self.image.putpalette(graph.rgb.getpalette())
  479.         self.draw = ImageDraw.ImageDraw(self.image)
  480.         self.draw.setink(graph.rgb.black)
  481.  
  482.     def get_font(self, font):
  483.         if not self.fonts.has_key(font):
  484.             fontfilename = os.path.join(os.path.join(blue.os.respath, "common\\pilfonts"), font+'.pil')
  485.             #print fontfilename
  486.             self.fonts[font] = ImageFont.load_path(fontfilename)
  487. #            self.fonts[font] = ImageFont.load_path(respath + "pilfonts\\" + font+'.pil')
  488.         return self.fonts[font]
  489.  
  490.  
  491. def determine_step(min_value, max_value, number=5):
  492.     ''' figure a step value for ticks between min_value and max_value that will
  493.             give around 'number' "nice" steps. Nice steps are multiples of 1,
  494.             2 and 5.
  495.     '''
  496.     # figure a good initial list of nice tick placements
  497.     range_ = float(max_value-min_value)
  498.     if range_ <= 0.2:
  499.         # all the values are the same
  500.         magnitude = 2
  501.         range_ = 1
  502.     else:
  503.         magnitude = math.log(abs(range_))/math.log(10)
  504.         magnitude = roundup(magnitude)-1
  505.     scale = 10. ** magnitude
  506.     nicevals = map(lambda i,s=scale: i*s, [1., 2., 5., 10.])
  507.  
  508.     # figure the target distance between values for 5 ticks on the scale
  509.     if range_ <= 1:
  510.         return 1
  511.  
  512.     sep = range_/number
  513.     step = 0
  514.     while not step:
  515.         for x in range(len(nicevals)-1):
  516.             # check to see if we're in this range of nice values
  517.             if nicevals[x] <= sep <= nicevals[x+1]:
  518.                 if (sep-nicevals[x]) <= (nicevals[x+1]<sep):
  519.                     step = nicevals[x]
  520.                 else:
  521.                     step = nicevals[x+1]
  522.                 break
  523.         else:
  524.             # scale it
  525.             scale = scale * 10
  526.             nicevals = map(lambda i,s=scale: i*s, [1., 2., 5., 10.])
  527.     return step
  528.  
  529. class NumericTickPoints:
  530.     ''' Determine the tick point values for a graph axis that ranges in
  531.              value from min_value to max_value.
  532.             If step is not None, then use it to figure the ticks rather than
  533.              trying to figure a "reasonable" step value.
  534.             If mod_steps is 1, then try to put the ticks at multiples of the
  535.              step value rather than at positions some multiple from the starting
  536.              value. If mod_steps is 0 then the ticks will always appear at the
  537.              start and end of the axis.
  538.     '''
  539.     def determine_tick_points(self, min_value, max_value):
  540.         # determine step if we aren't supplied one
  541.         step = self.step
  542.         if step is None:
  543.             step = determine_step(min_value, max_value)
  544.  
  545.         if self.mod_steps == 1:
  546.             # round up to smallest integer multiple of step geater than max_value and
  547.             # greatest integer multiple of step less than min_value
  548.             if max_value%step:
  549.                 max_value = step * (int(max_value/step) + 1)
  550.             min_value = min_value - (min_value%step)
  551.  
  552.         value = min_value
  553.         self.points = [min_value]
  554.         while value < max_value:
  555.             value = value + step
  556.             self.points.append(value)
  557.  
  558.  
  559. class XTicks:
  560.     ''' Decorate the X axis of a graph with tick marks
  561.     '''
  562.     type = ('bottom', 1)
  563.  
  564.     def analyse_data(self, graph):
  565.         # determine the tick marks
  566.         self.determine_tick_points(graph.data_min_x, graph.data_max_x)
  567.  
  568.         # adjust min and max for this axis
  569.         graph.max_x = max(self.points[-1], graph.data_max_x)
  570.         graph.min_x = min(self.points[0], graph.data_min_x)
  571.  
  572.     def render(self, graph):
  573.         # Figure the width of the scale.
  574.         scale_range = (self.points[-1] - self.points[0])
  575.         graph_range = (graph.max_x - graph.min_x)
  576.         scale_size = roundup(graph.size[0] * scale_range/graph_range)
  577.  
  578.         # create the tick mark image
  579.         self.create_image((scale_size, 8), graph)
  580.  
  581.         # only transform the X axis
  582.         self.transform = graph.transform[:3]+(0, 1, 0)
  583.  
  584.         # draw the side of the graph
  585.         line = ImagePath.Path([(self.points[0], 0), (self.points[-1], 0)])
  586.         line.transform(self.transform)
  587.         self.draw.line(line)
  588.  
  589.         # now draw the tick marks
  590.         for tick in self.points:
  591.             tick = ImagePath.Path([(tick, 1), (tick, 5)])
  592.             tick.transform(self.transform)
  593.             self.draw.line(tick)
  594.  
  595.  
  596. class YTicks:
  597.     ''' Decorate the Y axis of a graph with tick marks
  598.     '''
  599.     type = ('left', 1)
  600.  
  601.     def analyse_data(self, graph):
  602.         # determine the tick marks
  603.         self.determine_tick_points(graph.data_min_y, graph.data_max_y)
  604.  
  605.         # adjust min and max for this axis
  606.         graph.max_y = max(self.points[-1], graph.data_max_y)
  607.         graph.min_y = min(self.points[0], graph.data_min_y)
  608.  
  609.     def render(self, graph):
  610.         # Figure the height of the scale.
  611.         scale_range = (self.points[-1] - self.points[0])
  612.         graph_range = (graph.max_y - graph.min_y)
  613.         if graph_range == 0:
  614.             #print "graph_range == 0, setting to 2"
  615.             graph_range = 2
  616.         scale_size = roundup(graph.size[1] * scale_range/graph_range)
  617.         y_shift = roundup(graph.transform[5] + scale_size - graph.size[1]) + 1
  618.  
  619.         # create the tick mark image
  620.         self.create_image((8, scale_size), graph)
  621.  
  622.         # only transform the Y axis
  623.         self.transform = (1, 0, 0) + graph.transform[3:5] + (y_shift,)
  624.  
  625.         # draw the side of the graph
  626.         line = ImagePath.Path([(7, self.points[0]), (7, self.points[-1])])
  627.         line.transform(self.transform)
  628.         self.draw.line(line)
  629.  
  630.         # now draw the tick marks
  631.         for tick in self.points:
  632.             tick = ImagePath.Path([(2, tick), (6, tick)])
  633.             tick.transform(self.transform)
  634.             self.draw.line(tick)
  635.  
  636.  
  637.  
  638. class XTickValues(Decoration, XTicks, NumericTickPoints):
  639.     ''' Decorate the X axis of a graph with basic number markings.
  640.     '''
  641.     def __init__(self, valueRenderer, mod_steps=0, font='6x13', ink='black',
  642.             fill='white'):
  643.         Decoration.__init__(self, mod_steps)
  644.         self.valueRenderer = valueRenderer
  645.         self.font = font
  646.         self.ink = ink
  647.         self.fill = fill
  648.  
  649.     def render(self, graph):
  650.         # render ticks to determine marking numbers
  651.         XTicks.render(self, graph)
  652.  
  653.         # extra image size to hold numbers
  654.         font = self.get_font(self.font)
  655.         label_size = font.getsize(self.valueRenderer.template)
  656.         ticks = self.image
  657.         scale_size = (ticks.size[0]+label_size[0], ticks.size[1]+1+label_size[1])
  658.         self.create_image(scale_size, graph, self.fill)
  659.         self.image.paste(ticks, (0,0))
  660.  
  661.         # figure where to draw the labels
  662.         path = ImagePath.Path(map(lambda x,s=ticks.size[1]+1: (x, s), self.points))
  663.         path.transform(self.transform)
  664.  
  665.         # init the drawing
  666.         self.draw.setfont(font)
  667.         self.draw.setink(graph.rgb[self.ink])
  668.  
  669.         # draw the values
  670.         for tick in range(len(self.points)):
  671.             if tick%self.skip != 0:
  672.                 continue
  673.             coords = path[tick]
  674.             # TODO: not sure why PIL rounds the values here differently...
  675.             coords = map(int, map(round, coords))
  676.             tick = self.points[tick]
  677.             self.draw.text(coords, self.valueRenderer(tick))
  678.  
  679.  
  680. class XTickDates(Decoration, XTicks):
  681.     ''' Decorate the X axis of a graph with basic number markings.
  682.     '''
  683.     def __init__(self, mod_steps=0, font='6x13', ink='black', fill='white'):
  684.         Decoration.__init__(self, mod_steps)
  685.         self.font = font
  686.         self.ink = ink
  687.         self.fill = fill
  688.         self.years = self.months = self.days = self.hours = self.minutes = 0
  689.         self.seconds = 0
  690.  
  691.     def determine_tick_points(self, min_value, max_value):
  692.         ''' Determine the tick point values for a graph axis that ranges in
  693.                  dates from min_value to max_value which represent time.time() values.
  694.                 If mod_steps is 1, then try to put the ticks at multiples of the
  695.                  step value rather than at positions some multiple from the starting
  696.                  value. If mod_steps is 0 then the ticks will always appear at the
  697.                  start and end of the axis.
  698.         '''
  699.         # determine step if we aren't supplied one
  700.         start_date = time.localtime(min_value)
  701.         end_date = time.localtime(max_value)
  702.  
  703.         year_d = int(end_date[0]-start_date[0])
  704.         mon_d = int(year_d*12 + (end_date[1]-start_date[1]))
  705.         sec_d = int(max_value-min_value)
  706.         min_d = int(sec_d/60)
  707.         hour_d = int(min_d/60)
  708.         day_d = int(hour_d/24)
  709.  
  710.         #print year_d,"years", mon_d, "months", day_d, "days", hour_d, "hours", min_d, "min", sec_d, "sec"
  711.  
  712.         points = []
  713.         if year_d > 3:
  714.             # steps with be 6 monthly
  715.             self.years = 1
  716.             # mark every 6 months, possibly with a label
  717.             step = 1
  718.             if 3 < year_d < 10: self.skip = 2
  719.             elif 0 < year_d < 4: self.months = 1
  720.             else: step = determine_step(start_date[0], end_date[0])
  721.             for year in range(start_date[0], end_date[0]+step, step):
  722.                 points.append((year, 1, 1, 0, 0, 0, 0, 0, 0))
  723.                 # mark the month with a tick
  724.                 if year_d < 10:
  725.                     points.append((year, 7, 1, 0, 0, 0, 0, 0, 0))
  726.         elif mon_d > 4:
  727.             # steps will be monthly
  728.             year = start_date[0]
  729.             if year_d: self.years = 1
  730.             self.months = 1
  731.             month = start_date[1]
  732.             for i in range(0, mon_d+2):
  733.                 points.append((year, month, 1, 0, 0, 0, 0, 0, 0))
  734.                 month = month + 1
  735.                 # overflow of month
  736.                 if month > 12:
  737.                     self.years = 1
  738.                     year = year + 1
  739.                     month = 1
  740.         elif day_d > 4:
  741.             # steps will be daily
  742.             year = start_date[0]
  743.             month = start_date[1]
  744.             if year_d: self.years = 1
  745.             if mon_d: self.months = 1
  746.             self.days = 1
  747.             self.skip = int(day_d/5)
  748.             for day in range(start_date[2], start_date[2]+day_d+1):
  749.                 points.append((year, month, day, 0, 0, 0, 0, 0, 0))
  750.         elif hour_d > 4:
  751.             # steps will be hourly
  752.             #f "hourly"
  753.             self.hours = 1
  754.             self.skip = int(hour_d/10)
  755.             for hour in range(start_date[3], start_date[3]+hour_d+1):
  756.                 points.append(start_date[:3]+(hour, 0, 0, 0, 0, 0))
  757.         elif min_d > 5:
  758.             #print "every minute"
  759.             # steps will be every minute
  760.             self.minutes = 1
  761.             self.skip = int(min_d/5)
  762.             for minute in range(start_date[4], start_date[4]+min_d+1):
  763.                 points.append(start_date[:4]+(minute, 0, 0, 0, 0))
  764.         elif end_date[5] != start_date[5]:
  765.             # steps will be every second
  766.             self.seconds = 1
  767.             self.skip = int((end_date[5] - start_date[5])/5)
  768.             for second in range(start_date[5], start_date[5]+sec_d+1):
  769.                 points.append(start_date[:5]+(second, 0, 0, 0))
  770.  
  771.         # make time values
  772.  
  773.         self.points = []
  774.         for point in points:
  775.             self.points.append(time.mktime(point))
  776.  
  777.     month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
  778.                         "Oct", "Nov", "Dec"]
  779.  
  780.     def render(self, graph):
  781.         # render ticks to determine marking numbers
  782.         XTicks.render(self, graph)
  783.  
  784.         # figure components of date to be displayed:
  785.         # self.years, self.months, self.days, self.hours, self.minutes,
  786.         # self.seconds
  787.  
  788.         # extra image size to hold numbers
  789.         font = self.get_font(self.font)
  790.  
  791.         # figure length of label
  792.         # TODO: add the time cases here and in the drawing
  793.         if self.months and self.days:
  794.             length = 5
  795.         elif self.years:
  796.             length = 4
  797.         elif self.months:
  798.             length = 3
  799.         else:
  800.             length = 2
  801.         if self.years and self.months:
  802.             height = 2
  803.         else:
  804.             height = 1
  805.         label_size = font.getsize('M'*length)
  806.         # label height in pixels (plus spacer)
  807.         lab_p = (1 + label_size[1])
  808.  
  809.         ticks = self.image
  810.         scale_size = (ticks.size[0] + label_size[0], ticks.size[1] + (lab_p*height))
  811.         self.create_image(scale_size, graph, self.fill)
  812.         self.image.paste(ticks, (0,0))
  813.  
  814.         # figure where to draw the labels
  815.         path = ImagePath.Path(map(lambda x,s=ticks.size[1]+1: (x, s), self.points))
  816.         path.transform(self.transform)
  817.  
  818.         # init the drawing
  819.         self.draw.setfont(font)
  820.         self.draw.setink(graph.rgb[self.ink])
  821.  
  822.         # draw the values
  823.         prev_mon = prev_year = None
  824.         for tick in range(len(self.points)):
  825.             if self.skip and tick%self.skip != 0:
  826.                 continue
  827.             # TODO: PIL doesn't seem to be able to handle some float values in paste()
  828.             coords = (int(path[tick][0]), int(path[tick][1]))
  829.             tick = time.localtime(self.points[tick])
  830.             if self.days:
  831.                 text = '%d'%tick[2]
  832.                 if self.months and tick[1] != prev_mon:
  833.                     text = text + ' %s'%self.month_names[tick[1]-1]
  834.                     prev_mon = tick[1]
  835.                 self.draw.text(coords, text)
  836.                 if self.years and tick[0] != prev_year:
  837.                     self.draw.text((coords[0], coords[1]+lab_p), '%d'%tick[0])
  838.                     prev_year = tick[0]
  839.             elif self.months:
  840.                 self.draw.text(coords, self.month_names[tick[1]-1])
  841.                 if self.years and tick[0] != prev_year:
  842.                     self.draw.text((coords[0], coords[1]+lab_p), '%d'%tick[0])
  843.                     prev_year = tick[0]
  844.             elif self.years:
  845.                 self.draw.text(coords, '%d'%tick[0])
  846.  
  847.  
  848. class YTickValues(Decoration, YTicks, NumericTickPoints):
  849.     ''' Decorate the X axis of a graph with basic number markings.
  850.     '''
  851.     def __init__(self, valueRenderer, mod_steps=0, font='6x13', ink='black',
  852.             fill='white'):
  853.         Decoration.__init__(self, mod_steps)
  854.         self.valueRenderer = valueRenderer
  855.         self.font = font
  856.         self.ink = ink
  857.         self.fill = fill
  858.  
  859.     def render(self, graph):
  860.         # render ticks to determine marking numbers
  861.         YTicks.render(self, graph)
  862.  
  863.         # extra image size to hold numbers
  864.         font = self.get_font(self.font)
  865.         label_size = font.getsize(self.valueRenderer.template)
  866.         ticks = self.image
  867.         scale_size = (label_size[0]+1+ticks.size[0], ticks.size[1]+label_size[1])
  868.         self.create_image(scale_size, graph, self.fill)
  869.         self.image.paste(ticks, (label_size[0]+1, label_size[1]))
  870.  
  871.         # figure where to draw the labels
  872.         path = ImagePath.Path(map(lambda x: (0, x), self.points))
  873.         path.transform(self.transform)
  874.  
  875.         # init the drawing
  876.         self.draw.setfont(font)
  877.         self.draw.setink(graph.rgb[self.ink])
  878.  
  879.         # draw the values
  880.         for tick in range(len(self.points)):
  881.             if tick%self.skip != 0:
  882.                 continue
  883.             coords = path[tick]
  884.             # TODO: not sure why PIL rounds the values here differently...
  885.             coords = map(int, map(round, coords))
  886.             tick = self.points[tick]
  887.             self.draw.text(coords, self.valueRenderer(tick))
  888.  
  889.  
  890.  
  891. class FloatValues:
  892.     #           1.23e-45  (worst case...)
  893.     template = 'MMMMMMMM'
  894.  
  895.     def __init__(self, align='left'):
  896.         self.len_template = len(self.template)
  897.         self.align = align
  898.         self.fract = "%%.%df"
  899.         self.e_fract = "%%.%dfe%d"
  900.         self.e_integer = "%de%d"
  901.         self.e_float = "%fe%d"
  902.  
  903.     def __call__(self, value):
  904.         if value == 0:
  905.             if self.align=='right':
  906.                 return "      0"
  907.             else:
  908.                 return "0"
  909.  
  910.         mag = math.log(abs(value))/math.log(10)
  911.  
  912.         tl = self.len_template
  913.         imag = int(mag)
  914.         if value >= 0:
  915.             if -6 <= mag < 6:
  916.                 s = "%*.*f" % (tl, tl-2-imag, value)
  917.             else:
  918.                 exp = '%d' % int(mag)
  919.                 le = len(exp)
  920.                 val = value * 10.0**-int(mag)
  921.                 s = '%*.*fe%s' % (tl-2-le, tl-3-le, val, exp)
  922.         else:
  923.             if -5 <= mag < 5:
  924.                 s = "%*.*f" % (tl, tl-3-imag, value)
  925.             else:
  926.                 exp = '%d' % int(mag)
  927.                 le = len(exp)
  928.                 val = value * 10.0**-int(mag)
  929.                 s = '%*.*fe%s' % (tl-3-le, tl-4-le, val, exp)
  930.  
  931.         while s[-1:] == '0':
  932.             s = s[:-1]
  933.         if s[-1:] == '.':
  934.             s = s[:-1]
  935.         s = s[:tl]
  936.         if self.align=='left':
  937.             return s
  938.         else:
  939.             return " "*(tl-len(s)) + s
  940.  
  941.  
  942.  
  943. class ByteValues:
  944.     ''' Values must be integer and will be scaled by 1024 and have letter
  945.             suffixes when scaled.
  946.     '''
  947.     #           1024K
  948.     template = 'MMMMM'
  949.     scales = ' KMGT'
  950.  
  951.     def __init__(self, align='left'):
  952.         self.align = align
  953.  
  954.     def __call__(self, value):
  955.         if value == 0:
  956.             if self.align=='right':
  957.                 return "    0"
  958.             else:
  959.                 return "0"
  960.  
  961.         # figure how big our number is...
  962.         magnitude = math.log(abs(value))/math.log(2)
  963.         scale = int(magnitude/10)
  964.  
  965.         if scale:
  966.             value = value / (1024**scale)
  967.         if self.align=='right':
  968.             if scale:
  969.                 return "%4d%s"%(value, self.scales[scale])
  970.             else:
  971.                 return "%5d"%value
  972.         else:
  973.             return "%d%s"%(value, self.scales[scale])
  974.  
  975.  
  976. class Values:
  977.     ''' Values must be integer, nothing will be done to it
  978.     '''
  979.     template = 'MMMMM'
  980.  
  981.     def __init__(self, align='left'):
  982.         self.align = align
  983.  
  984.     def __call__(self, value):
  985.         if self.align == 'right':
  986.             return "%5d"%value
  987.         else:
  988.             return "%d"%value
  989.  
  990.  
  991. class XLabel(Decoration):
  992.     ''' Decorate the X axis of a graph with a text label.
  993.     '''
  994.     type = ('bottom', 2)
  995.     def __init__(self, text, font='6x13', ink='black', fill='white'):
  996.         Decoration.__init__(self)
  997.         self.text = text
  998.         self.font = font
  999.         self.ink = ink
  1000.         self.fill = fill
  1001.  
  1002.     def render(self, graph):
  1003.         # determine the tick marks
  1004.         font = self.get_font(self.font)
  1005.         label_size = font.getsize(self.text)
  1006.         self.create_image(label_size, graph, self.fill)
  1007.  
  1008.         # draw the label
  1009.         self.draw.setfont(font)
  1010.         self.draw.setink(graph.rgb[self.ink])
  1011.         self.draw.text((0,0), self.text)
  1012.  
  1013.  
  1014.  
  1015. class YLabel(XLabel):
  1016.     ''' Decorate the Y axis of a graph with a text label. Same as X axis except
  1017.             for placement and rotation.
  1018.     '''
  1019.     type = ('left', 2)
  1020.     def render(self, graph):
  1021.         XLabel.render(self, graph)
  1022.         self.image = self.image.rotate(90)
  1023.  
  1024.  
  1025. class Title(XLabel):
  1026.     ''' Decorate the top of a graph with a text label. Same as X axis except
  1027.             for placement.
  1028.     '''
  1029.     type = ('top', 1)
  1030.  
  1031.  
  1032. class PieGraph(Graph):
  1033.     ''' Draw a pie graph based on the values in self.data.
  1034.  
  1035.             If there is a labels decoration, then the number of labels that
  1036.              will fit vertically beside the graph image.
  1037.     '''
  1038.     colours = ('lightblue', 'lightred', 'lightgreen', 'cyan', 'purple',
  1039.         'yellow', 'red', 'blue', 'green')
  1040.     def __init__(self, *args, **kw):
  1041.         apply(Graph.__init__, (self, )+args, kw)
  1042.         self.threshold = kw.get('threshold', None)
  1043.         self.numlabels = kw.get('numlabels', None)
  1044.         self.labels = []
  1045.  
  1046.     def create_image(self, mode, size, fill='white'):
  1047.         # TODO support more than just 'P' mode images
  1048.         # XXX handle extra width for bars
  1049.         self.image = Image.new(mode, size, fill)
  1050.         self.draw = ImageDraw.ImageDraw(self.image)
  1051.         self.rgb = SimplePalette.Palette()
  1052.         self.rgb.simple_palette()
  1053.         self.image.putpalette(self.rgb.getpalette())
  1054.  
  1055.     def analyse_data(self):
  1056.         # determine the order of the labels
  1057.         labels = []
  1058.         self.total = 0
  1059.         for key, value in self.data.items():
  1060.             labels.append((value, key))
  1061.             self.total = self.total + value
  1062.         labels.sort()
  1063.         labels.reverse()
  1064.  
  1065.         # now make the real labels list
  1066.         self.labels = []
  1067.         for entry in labels:
  1068.             self.labels.append(entry[1])
  1069.  
  1070.         # now go look for decorations and modify the transform's conditions
  1071.         # accordingly
  1072.         for decoration in self.decorations.values():
  1073.             if hasattr(decoration, 'analyse_data'):
  1074.                 decoration.analyse_data(self)
  1075.  
  1076.     def add_dataset(self, name, value=None):
  1077.         ''' call with either add_dataset({mapping of name:value}) or
  1078.                 add_dataset(name, value)
  1079.         '''
  1080.         if value is not None:
  1081.             name = {name: value}
  1082.         self.data.update(name)
  1083.  
  1084.     def render_graph(self):
  1085.         width, height = self.image.size
  1086.         width, height = (width-1, height-1)
  1087.  
  1088.         total = 0
  1089.         self.draw.setfill(1)
  1090.  
  1091.         # draw the data set[s]
  1092.         start = 0.
  1093.         for entry in range(len(self.labels)):
  1094.             # get the data
  1095.             value = self.data[self.labels[entry]]
  1096.  
  1097.             # set the colour
  1098.             # TODO: wrap colours and/or use drawing styles
  1099.             self.draw.setink(self.rgb[self.colours[entry]])
  1100.  
  1101.             end = start + (360. * value)/self.total
  1102.             self.draw.pieslice((0,0,width,height), start, end)
  1103.             start = end
  1104.  
  1105.  
  1106.  
  1107.  
  1108.  
  1109.  
  1110. class PieLabels(Decoration):
  1111.     ''' Decorate a Pie Chart with some labels
  1112.     '''
  1113.     type = ('right', 1)
  1114.     def __init__(self, font='6x13', ink='black', fill='white'):
  1115.         Decoration.__init__(self)
  1116.         self.font = font
  1117.         self.ink = ink
  1118.         self.fill = fill
  1119.  
  1120.     def analyse_data(self, graph):
  1121.         # we've got a list of labels, now we need to figure out how many we can
  1122.         # draw
  1123.         font = self.get_font(self.font)
  1124.         height = font.getsize('M')[1]
  1125.         numlabels = graph.image.size[1]/(height+1)
  1126.         if numlabels >= len(graph.labels):
  1127.             return
  1128.  
  1129.         # chop labels list
  1130.         graph.labels, other = graph.labels[:numlabels], graph.labels[numlabels:]
  1131.  
  1132.         # figure the value of other
  1133.         other=0
  1134.         for label in other:
  1135.             other = other + graph.data[label]
  1136.         graph.labels.append('other')
  1137.         graph.data['other'] = other
  1138.  
  1139.     def render(self, graph):
  1140.         # figure image size
  1141.         font = self.get_font(self.font)
  1142.         height = font.getsize('M')[1]
  1143.         im_width = 0
  1144.         for label in graph.labels:
  1145.             im_width = max(im_width, font.getsize(label)[0] + height + 1)
  1146.  
  1147.         # create the tick mark image
  1148.         numlabels = len(graph.labels)
  1149.         self.create_image((im_width, (height+2)*numlabels), graph, self.fill)
  1150.         self.draw.setfont(font)
  1151.  
  1152.         # now draw the labels
  1153.         y = 0
  1154.         for label in range(numlabels):
  1155.             # colour reference box
  1156.             self.draw.setink(graph.rgb[graph.colours[label]])
  1157.             self.draw.setfill(1)
  1158.             self.draw.rectangle((0, y, height-1, y+height-1))
  1159.  
  1160.             # now the label
  1161.             self.draw.setink(graph.rgb.black)
  1162.             self.draw.setfill(0)
  1163.             self.draw.text((height+2, y), graph.labels[label])
  1164.  
  1165.             y = y + height
  1166.  
  1167.  
  1168.  
  1169.  
  1170. if __name__=='__main__':
  1171. #   start = time.mktime((1999,1,1,0,0,0,0,0,0))
  1172. #   end = time.mktime((1999,12,31,0,0,0,0,0,0))
  1173. #   y = [0]*365
  1174. #   import random
  1175. #   for i in range(365):
  1176. #       y[i] = y[i-1] + random.random() - 0.5
  1177. #   x = range(start, end, 60*60*24)
  1178.     x = range(10)
  1179.     y = range(-10, 10, 2)
  1180.     graph = BarGraph('P', (600,200), 0)
  1181.     graph.add_dataset('a', x, y)
  1182. #   graph.add_decoration(XTickDates())
  1183.     graph.add_decoration(XTickValues(FloatValues()))
  1184.     graph.add_decoration(XLabel('Test Time Scale'))
  1185.     graph.add_decoration(YTickValues(FloatValues(align='right')))
  1186.     graph.add_decoration(YLabel('Test Foo Scale'))
  1187.     graph.render()
  1188.     graph.image.show()
  1189.  
  1190. #   graph = PieGraph('P', (400,200), 0)
  1191. #   graph.add_dataset({'USA Commercial': 41, 'Non-Profit Making Organisations': 15, 'Netherlands': 23, 'USA Educational': 28, 'Network':
  1192. #   65, 'Australia': 494, '143.227': 3})
  1193. #   graph.add_decoration(PieLabels())
  1194. #   graph.render()
  1195.  
  1196.     #graph.image.save('out.gif')
  1197.  
  1198.