| #!/usr/bin/env python |
| # |
| # TODO: |
| # - Task colors: |
| # - User-defined using config file. |
| # - Automagically chosen from color space. |
| # - Advanced algorithm (contact Hannes Pretorius). |
| # - Koos' specs: |
| # - Resources and tasks sorted in read-in order (default) |
| # or alphabetically (flag). |
| # - Have proper gnuplot behavior on windows/x11, eps/pdf, latex terminals. |
| # - Create and implement algorithm for critical path analysis. |
| # - Split generic stuff into a Gantt class, and specific stuff into the main. |
| # |
| # gantt.py ganttfile | gnuplot |
| |
| import itertools, sys, getopt |
| from ConfigParser import ConfigParser |
| |
| rectangleHeight = 0.8 #: Height of a rectangle in units. |
| |
| class Activity(object): |
| """ |
| Container for activity information. |
| |
| @ivar resource: Resource name. |
| @type resource: C{str} |
| |
| @ivar start: Start time of the activity. |
| @type start: C{float} |
| |
| @ivar stop: End time of the activity. |
| @type stop: C{float} |
| |
| @ivar task: Name of the task/activity being performed. |
| @type task: C{str} |
| """ |
| def __init__(self, resource, start, stop, task): |
| self.resource = resource |
| self.start = start |
| self.stop = stop |
| self.task = task |
| |
| class Rectangle(object): |
| """ |
| Container for rectangle information. |
| """ |
| def __init__(self, bottomleft, topright, fillcolor): |
| self.bottomleft = bottomleft |
| self.topright = topright |
| self.fillcolor = fillcolor |
| self.fillstyle = 'solid 0.8' |
| self.linewidth = 2 |
| |
| class ColorBook(object): |
| """ |
| Class managing colors. |
| |
| @ivar colors |
| @ivar palette |
| @ivar prefix |
| """ |
| def __init__(self, colorfname, tasks): |
| """ |
| Construct a ColorBook object. |
| |
| @param colorfname: Name of the color config file (if specified). |
| @type colorfname: C{str} or C{None} |
| |
| @param tasks: Existing task types. |
| @type tasks: C{list} of C{str} |
| """ |
| if colorfname: |
| values = self.load_config(colorfname, tasks) |
| else: |
| values = self.fixed(tasks) |
| |
| self.colors, self.palette, self.prefix = values |
| |
| |
| def load_config(self, colorfname, tasks): |
| """ |
| Read task colors from a configuration file. |
| """ |
| palettedef = 'model RGB' |
| colorprefix = 'rgb' |
| |
| # Read in task colors from configuration file |
| config = ConfigParser() |
| config.optionxform = str # makes option names case sensitive |
| config.readfp(open(colorfname, 'r')) |
| # Colors are RGB colornames |
| colors = dict(config.items('Colors')) |
| |
| # Raise KeyError if no color is specified for a task |
| nocolors = [t for t in tasks if not colors.has_key(t)] |
| if nocolors: |
| msg = 'Could not find task color for ' + ', '.join(nocolors) |
| raise KeyError(msg) |
| |
| return colors, palettedef, colorprefix |
| |
| def fixed(self, tasks): |
| """ |
| Pick colors from a pre-defined palette. |
| """ |
| # Set task colors |
| # SE colors |
| # (see http://w3.wtb.tue.nl/nl/organisatie/systems_engineering/\ |
| # info_for_se_students/how2make_a_poster/pictures/) |
| # Decrease the 0.8 values for less transparent colors. |
| se_palette = {"se_red": (1.0, 0.8, 0.8), |
| "se_pink": (1.0, 0.8, 1.0), |
| "se_violet": (0.8, 0.8, 1.0), |
| "se_blue": (0.8, 1.0, 1.0), |
| "se_green": (0.8, 1.0, 0.8), |
| "se_yellow": (1.0, 1.0, 0.8)} |
| se_gradient = ["se_red", "se_pink", "se_violet", |
| "se_blue", "se_green", "se_yellow"] |
| se_palettedef = '( ' + \ |
| ', '.join(('%d ' % n + |
| ' '.join((str(x) for x in se_palette[c])) |
| for n, c in enumerate(se_gradient))) + \ |
| ' )' |
| |
| palettedef = 'model RGB defined %s' % se_palettedef |
| colorprefix = 'palette frac' |
| # Colors are fractions from the palette defined |
| colors = dict((t, '%0.2f' % (float(n)/(len(tasks)-1))) |
| for n, t in enumerate(tasks)) |
| |
| return colors, palettedef, colorprefix |
| |
| class DummyClass(object): |
| """ |
| Dummy class for storing option values in. |
| """ |
| |
| |
| def make_rectangles(activities, resource_map, colors): |
| """ |
| Construct a collection of L{Rectangle} for all activities. |
| |
| @param activities: Activities being performed. |
| @type activities: C{iterable} of L{Activity} |
| |
| @param resource_map: Indices of all resources. |
| @type resource_map: C{dict} of C{str} to C{int} |
| |
| @param colors: Colors for all tasks. |
| @type colors: C{dict} of C{str} to C{str} |
| |
| @return: Collection of rectangles to draw. |
| @rtype: C{list} of L{Rectangle} |
| """ |
| rectangles = [] |
| for act in activities: |
| ypos = resource_map[act.resource] |
| bottomleft = (act.start, ypos - 0.5 * rectangleHeight) |
| topright = (act.stop, ypos + 0.5 * rectangleHeight) |
| fillcolor = colors[act.task] |
| rectangles.append(Rectangle(bottomleft, topright, fillcolor)) |
| |
| return rectangles |
| |
| |
| def load_ganttfile(ganttfile): |
| """ |
| Load the resource/task file. |
| |
| @param ganttfile: Name of the gantt file. |
| @type ganttfile: C{str} |
| |
| @return: Activities loaded from the file, collection of |
| (resource, start, end, task) activities. |
| @rtype: C{list} of L{Activity} |
| """ |
| activities = [] |
| for line in open(ganttfile, 'r').readlines(): |
| line = line.strip().split() |
| if len(line) == 0: |
| continue |
| resource = line[0] |
| start = float(line[1]) |
| stop = float(line[2]) |
| task = line[3] |
| activities.append(Activity(resource, start, stop, task)) |
| |
| return activities |
| |
| def make_unique_tasks_resources(alphasort, activities): |
| """ |
| Construct collections of unique task names and resource names. |
| |
| @param alphasort: Sort resources and tasks alphabetically. |
| @type alphasort: C{bool} |
| |
| @param activities: Activities to draw. |
| @type activities: C{list} of L{Activity} |
| |
| @return: Collections of task-types and resources. |
| @rtype: C{list} of C{str}, C{list} of C{str} |
| """ |
| # Create list with unique resources and tasks in activity order. |
| resources = [] |
| tasks = [] |
| for act in activities: |
| if act.resource not in resources: |
| resources.append(act.resource) |
| if act.task not in tasks: |
| tasks.append(act.task) |
| |
| # Sort such that resources and tasks appear in alphabetical order |
| if alphasort: |
| resources.sort() |
| tasks.sort() |
| |
| # Resources are read from top (y=max) to bottom (y=1) |
| resources.reverse() |
| |
| return tasks, resources |
| |
| |
| def generate_plotdata(activities, resources, tasks, rectangles, options, |
| resource_map, color_book): |
| """ |
| Generate Gnuplot lines. |
| """ |
| xmin = 0 |
| xmax = max(act.stop for act in activities) |
| ymin = 0 + (rectangleHeight / 2) |
| ymax = len(resources) + 1 - (rectangleHeight / 2) |
| xlabel = 'time' |
| ylabel = '' |
| title = options.plottitle |
| ytics = ''.join(['(', |
| ', '.join(('"%s" %d' % item) |
| for item in resource_map.iteritems()), |
| ')']) |
| # outside and 2 characters from the graph |
| key_position = 'outside width +2' |
| grid_tics = 'xtics' |
| |
| # Set plot dimensions |
| plot_dimensions = ['set xrange [%f:%f]' % (xmin, xmax), |
| 'set yrange [%f:%f]' % (ymin, ymax), |
| 'set autoscale x', # extends x axis to next tic mark |
| 'set xlabel "%s"' % xlabel, |
| 'set ylabel "%s"' % ylabel, |
| 'set title "%s"' % title, |
| 'set ytics %s' % ytics, |
| 'set key %s' % key_position, |
| 'set grid %s' % grid_tics, |
| 'set palette %s' % color_book.palette, |
| 'unset colorbox'] |
| |
| # Generate gnuplot rectangle objects |
| plot_rectangles = (' '.join(['set object %d rectangle' % n, |
| 'from %f, %0.1f' % r.bottomleft, |
| 'to %f, %0.1f' % r.topright, |
| 'fillcolor %s %s' % (color_book.prefix, |
| r.fillcolor), |
| 'fillstyle solid 0.8']) |
| for n, r in itertools.izip(itertools.count(1), rectangles)) |
| |
| # Generate gnuplot lines |
| plot_lines = ['plot ' + |
| ', \\\n\t'.join(' '.join(['-1', |
| 'title "%s"' % t, |
| 'with lines', |
| 'linecolor %s %s ' % (color_book.prefix, |
| color_book.colors[t]), |
| 'linewidth 6']) |
| for t in tasks)] |
| |
| return plot_dimensions, plot_rectangles, plot_lines |
| |
| def write_data(plot_dimensions, plot_rectangles, plot_lines, fname): |
| """ |
| Write plot data out to file or screen. |
| |
| @param fname: Name of the output file, if specified. |
| @type fname: C{str} (??) |
| """ |
| if fname: |
| g = open(fname, 'w') |
| g.write('\n'.join(itertools.chain(plot_dimensions, plot_rectangles, |
| plot_lines))) |
| g.close() |
| else: |
| print '\n'.join(itertools.chain(plot_dimensions, plot_rectangles, |
| plot_lines)) |
| |
| def fmt_opt(short, long, arg, text): |
| if arg: |
| return '-%s %s, --%s%s\t%s' % (short[:-1], arg, long, arg, text) |
| else: |
| return '-%s, --%s\t%s' % (short, long, text) |
| |
| def make_default_options(): |
| option_values = DummyClass() |
| option_values.outputfile = '' |
| option_values.colorfile = '' |
| option_values.alphasort = False |
| option_values.plottitle = '' |
| return option_values |
| |
| def process_options(): |
| """ |
| Handle option and command-line argument processing. |
| |
| @return: Options and gantt input filename. |
| @rtype: L{OptionParser} options, C{str} |
| """ |
| optdefs = [('o:', 'output=', 'FILE', 'Write output to FILE.'), |
| ('c:', 'color=', 'FILE', 'Use task colors (RGB) as defined in ' |
| 'configuration FILE (in RGB triplets,\n\t\t\t\tGnuplot ' |
| 'colornames, or hexadecimal representations.'), |
| ('a', 'alpha', '', '\t\tShow resources and tasks in ' |
| 'alphabetical order.'), |
| ('t:','title=', 'TITLE', 'Set plot title to TITLE (between ' |
| 'double quotes).'), |
| ('h', 'help', '', '\t\tShow online help.')] |
| short_opts = ''.join(opt[0] for opt in optdefs if opt[0]) |
| long_opts = [opt[1] for opt in optdefs if opt[1]] |
| usage_text = 'gantt.py [options] gantt-file\nwhere\n' + \ |
| '\n'.join(' ' + fmt_opt(*opt) for opt in optdefs) |
| |
| option_values = make_default_options() |
| |
| try: |
| opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts) |
| except getopt.GetoptError, err: |
| sys.stderr.write("gantt.py: %s\n" % err) |
| sys.exit(2) |
| |
| for opt, optval in opts: |
| if opt in ('-o', '--output'): |
| option_values.outputfile = optval |
| continue |
| if opt in ('-c', '--color'): |
| option_values.colorfile = optval |
| continue |
| if opt in ('-a', '--alphasort'): |
| option_values.alphasort = True |
| continue |
| if opt in ('-t', '--title'): |
| option_values.plottitle = optval |
| continue |
| if opt in ('-h', '--help'): |
| print usage_text |
| sys.exit(0) |
| |
| # Check if correct number of arguments is supplied |
| if len(args) != 1: |
| sys.stderr.write('gantty.py: incorrect number of arguments ' |
| '(task/resource file expected)\n') |
| sys.exit(1) |
| |
| return option_values, args[0] |
| |
| def compute(options, ganttfile): |
| activities = load_ganttfile(ganttfile) |
| tasks, resources = make_unique_tasks_resources(options.alphasort, |
| activities) |
| |
| # Assign indices to resources |
| resource_map = dict(itertools.izip(resources, itertools.count(1))) |
| |
| color_book = ColorBook(options.colorfile, tasks) |
| rectangles = make_rectangles(activities, resource_map, color_book.colors) |
| |
| plot_dims, plot_rects, plot_lines = \ |
| generate_plotdata(activities, resources, tasks, rectangles, |
| options, resource_map, color_book) |
| |
| write_data(plot_dims, plot_rects, plot_lines, options.outputfile) |
| |
| def run(): |
| options, ganttfile = process_options() |
| compute(options, ganttfile) |
| |
| |
| if __name__ == '__main__': |
| run() |
| |
| |