Ian Maxon | a70fba5 | 2016-02-18 13:52:36 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # TODO: |
| 4 | # - Task colors: |
| 5 | # - User-defined using config file. |
| 6 | # - Automagically chosen from color space. |
| 7 | # - Advanced algorithm (contact Hannes Pretorius). |
| 8 | # - Koos' specs: |
| 9 | # - Resources and tasks sorted in read-in order (default) |
| 10 | # or alphabetically (flag). |
| 11 | # - Have proper gnuplot behavior on windows/x11, eps/pdf, latex terminals. |
| 12 | # - Create and implement algorithm for critical path analysis. |
| 13 | # - Split generic stuff into a Gantt class, and specific stuff into the main. |
| 14 | # |
| 15 | # gantt.py ganttfile | gnuplot |
| 16 | |
| 17 | import itertools, sys, getopt |
| 18 | from ConfigParser import ConfigParser |
| 19 | |
| 20 | rectangleHeight = 0.8 #: Height of a rectangle in units. |
| 21 | |
| 22 | class Activity(object): |
| 23 | """ |
| 24 | Container for activity information. |
| 25 | |
| 26 | @ivar resource: Resource name. |
| 27 | @type resource: C{str} |
| 28 | |
| 29 | @ivar start: Start time of the activity. |
| 30 | @type start: C{float} |
| 31 | |
| 32 | @ivar stop: End time of the activity. |
| 33 | @type stop: C{float} |
| 34 | |
| 35 | @ivar task: Name of the task/activity being performed. |
| 36 | @type task: C{str} |
| 37 | """ |
| 38 | def __init__(self, resource, start, stop, task): |
| 39 | self.resource = resource |
| 40 | self.start = start |
| 41 | self.stop = stop |
| 42 | self.task = task |
| 43 | |
| 44 | class Rectangle(object): |
| 45 | """ |
| 46 | Container for rectangle information. |
| 47 | """ |
| 48 | def __init__(self, bottomleft, topright, fillcolor): |
| 49 | self.bottomleft = bottomleft |
| 50 | self.topright = topright |
| 51 | self.fillcolor = fillcolor |
| 52 | self.fillstyle = 'solid 0.8' |
| 53 | self.linewidth = 2 |
| 54 | |
| 55 | class ColorBook(object): |
| 56 | """ |
| 57 | Class managing colors. |
| 58 | |
| 59 | @ivar colors |
| 60 | @ivar palette |
| 61 | @ivar prefix |
| 62 | """ |
| 63 | def __init__(self, colorfname, tasks): |
| 64 | """ |
| 65 | Construct a ColorBook object. |
| 66 | |
| 67 | @param colorfname: Name of the color config file (if specified). |
| 68 | @type colorfname: C{str} or C{None} |
| 69 | |
| 70 | @param tasks: Existing task types. |
| 71 | @type tasks: C{list} of C{str} |
| 72 | """ |
| 73 | if colorfname: |
| 74 | values = self.load_config(colorfname, tasks) |
| 75 | else: |
| 76 | values = self.fixed(tasks) |
| 77 | |
| 78 | self.colors, self.palette, self.prefix = values |
| 79 | |
| 80 | |
| 81 | def load_config(self, colorfname, tasks): |
| 82 | """ |
| 83 | Read task colors from a configuration file. |
| 84 | """ |
| 85 | palettedef = 'model RGB' |
| 86 | colorprefix = 'rgb' |
| 87 | |
| 88 | # Read in task colors from configuration file |
| 89 | config = ConfigParser() |
| 90 | config.optionxform = str # makes option names case sensitive |
| 91 | config.readfp(open(colorfname, 'r')) |
| 92 | # Colors are RGB colornames |
| 93 | colors = dict(config.items('Colors')) |
| 94 | |
| 95 | # Raise KeyError if no color is specified for a task |
| 96 | nocolors = [t for t in tasks if not colors.has_key(t)] |
| 97 | if nocolors: |
| 98 | msg = 'Could not find task color for ' + ', '.join(nocolors) |
| 99 | raise KeyError(msg) |
| 100 | |
| 101 | return colors, palettedef, colorprefix |
| 102 | |
| 103 | def fixed(self, tasks): |
| 104 | """ |
| 105 | Pick colors from a pre-defined palette. |
| 106 | """ |
| 107 | # Set task colors |
| 108 | # SE colors |
| 109 | # (see http://w3.wtb.tue.nl/nl/organisatie/systems_engineering/\ |
| 110 | # info_for_se_students/how2make_a_poster/pictures/) |
| 111 | # Decrease the 0.8 values for less transparent colors. |
| 112 | se_palette = {"se_red": (1.0, 0.8, 0.8), |
| 113 | "se_pink": (1.0, 0.8, 1.0), |
| 114 | "se_violet": (0.8, 0.8, 1.0), |
| 115 | "se_blue": (0.8, 1.0, 1.0), |
| 116 | "se_green": (0.8, 1.0, 0.8), |
| 117 | "se_yellow": (1.0, 1.0, 0.8)} |
| 118 | se_gradient = ["se_red", "se_pink", "se_violet", |
| 119 | "se_blue", "se_green", "se_yellow"] |
| 120 | se_palettedef = '( ' + \ |
| 121 | ', '.join(('%d ' % n + |
| 122 | ' '.join((str(x) for x in se_palette[c])) |
| 123 | for n, c in enumerate(se_gradient))) + \ |
| 124 | ' )' |
| 125 | |
| 126 | palettedef = 'model RGB defined %s' % se_palettedef |
| 127 | colorprefix = 'palette frac' |
| 128 | # Colors are fractions from the palette defined |
| 129 | colors = dict((t, '%0.2f' % (float(n)/(len(tasks)-1))) |
| 130 | for n, t in enumerate(tasks)) |
| 131 | |
| 132 | return colors, palettedef, colorprefix |
| 133 | |
| 134 | class DummyClass(object): |
| 135 | """ |
| 136 | Dummy class for storing option values in. |
| 137 | """ |
| 138 | |
| 139 | |
| 140 | def make_rectangles(activities, resource_map, colors): |
| 141 | """ |
| 142 | Construct a collection of L{Rectangle} for all activities. |
| 143 | |
| 144 | @param activities: Activities being performed. |
| 145 | @type activities: C{iterable} of L{Activity} |
| 146 | |
| 147 | @param resource_map: Indices of all resources. |
| 148 | @type resource_map: C{dict} of C{str} to C{int} |
| 149 | |
| 150 | @param colors: Colors for all tasks. |
| 151 | @type colors: C{dict} of C{str} to C{str} |
| 152 | |
| 153 | @return: Collection of rectangles to draw. |
| 154 | @rtype: C{list} of L{Rectangle} |
| 155 | """ |
| 156 | rectangles = [] |
| 157 | for act in activities: |
| 158 | ypos = resource_map[act.resource] |
| 159 | bottomleft = (act.start, ypos - 0.5 * rectangleHeight) |
| 160 | topright = (act.stop, ypos + 0.5 * rectangleHeight) |
| 161 | fillcolor = colors[act.task] |
| 162 | rectangles.append(Rectangle(bottomleft, topright, fillcolor)) |
| 163 | |
| 164 | return rectangles |
| 165 | |
| 166 | |
| 167 | def load_ganttfile(ganttfile): |
| 168 | """ |
| 169 | Load the resource/task file. |
| 170 | |
| 171 | @param ganttfile: Name of the gantt file. |
| 172 | @type ganttfile: C{str} |
| 173 | |
| 174 | @return: Activities loaded from the file, collection of |
| 175 | (resource, start, end, task) activities. |
| 176 | @rtype: C{list} of L{Activity} |
| 177 | """ |
| 178 | activities = [] |
| 179 | for line in open(ganttfile, 'r').readlines(): |
| 180 | line = line.strip().split() |
| 181 | if len(line) == 0: |
| 182 | continue |
| 183 | resource = line[0] |
| 184 | start = float(line[1]) |
| 185 | stop = float(line[2]) |
| 186 | task = line[3] |
| 187 | activities.append(Activity(resource, start, stop, task)) |
| 188 | |
| 189 | return activities |
| 190 | |
| 191 | def make_unique_tasks_resources(alphasort, activities): |
| 192 | """ |
| 193 | Construct collections of unique task names and resource names. |
| 194 | |
| 195 | @param alphasort: Sort resources and tasks alphabetically. |
| 196 | @type alphasort: C{bool} |
| 197 | |
| 198 | @param activities: Activities to draw. |
| 199 | @type activities: C{list} of L{Activity} |
| 200 | |
| 201 | @return: Collections of task-types and resources. |
| 202 | @rtype: C{list} of C{str}, C{list} of C{str} |
| 203 | """ |
| 204 | # Create list with unique resources and tasks in activity order. |
| 205 | resources = [] |
| 206 | tasks = [] |
| 207 | for act in activities: |
| 208 | if act.resource not in resources: |
| 209 | resources.append(act.resource) |
| 210 | if act.task not in tasks: |
| 211 | tasks.append(act.task) |
| 212 | |
| 213 | # Sort such that resources and tasks appear in alphabetical order |
| 214 | if alphasort: |
| 215 | resources.sort() |
| 216 | tasks.sort() |
| 217 | |
| 218 | # Resources are read from top (y=max) to bottom (y=1) |
| 219 | resources.reverse() |
| 220 | |
| 221 | return tasks, resources |
| 222 | |
| 223 | |
| 224 | def generate_plotdata(activities, resources, tasks, rectangles, options, |
| 225 | resource_map, color_book): |
| 226 | """ |
| 227 | Generate Gnuplot lines. |
| 228 | """ |
| 229 | xmin = 0 |
| 230 | xmax = max(act.stop for act in activities) |
| 231 | ymin = 0 + (rectangleHeight / 2) |
| 232 | ymax = len(resources) + 1 - (rectangleHeight / 2) |
| 233 | xlabel = 'time' |
| 234 | ylabel = '' |
| 235 | title = options.plottitle |
| 236 | ytics = ''.join(['(', |
| 237 | ', '.join(('"%s" %d' % item) |
| 238 | for item in resource_map.iteritems()), |
| 239 | ')']) |
| 240 | # outside and 2 characters from the graph |
| 241 | key_position = 'outside width +2' |
| 242 | grid_tics = 'xtics' |
| 243 | |
| 244 | # Set plot dimensions |
| 245 | plot_dimensions = ['set xrange [%f:%f]' % (xmin, xmax), |
| 246 | 'set yrange [%f:%f]' % (ymin, ymax), |
| 247 | 'set autoscale x', # extends x axis to next tic mark |
| 248 | 'set xlabel "%s"' % xlabel, |
| 249 | 'set ylabel "%s"' % ylabel, |
| 250 | 'set title "%s"' % title, |
| 251 | 'set ytics %s' % ytics, |
| 252 | 'set key %s' % key_position, |
| 253 | 'set grid %s' % grid_tics, |
| 254 | 'set palette %s' % color_book.palette, |
| 255 | 'unset colorbox'] |
| 256 | |
| 257 | # Generate gnuplot rectangle objects |
| 258 | plot_rectangles = (' '.join(['set object %d rectangle' % n, |
| 259 | 'from %f, %0.1f' % r.bottomleft, |
| 260 | 'to %f, %0.1f' % r.topright, |
| 261 | 'fillcolor %s %s' % (color_book.prefix, |
| 262 | r.fillcolor), |
| 263 | 'fillstyle solid 0.8']) |
| 264 | for n, r in itertools.izip(itertools.count(1), rectangles)) |
| 265 | |
| 266 | # Generate gnuplot lines |
| 267 | plot_lines = ['plot ' + |
| 268 | ', \\\n\t'.join(' '.join(['-1', |
| 269 | 'title "%s"' % t, |
| 270 | 'with lines', |
| 271 | 'linecolor %s %s ' % (color_book.prefix, |
| 272 | color_book.colors[t]), |
| 273 | 'linewidth 6']) |
| 274 | for t in tasks)] |
| 275 | |
| 276 | return plot_dimensions, plot_rectangles, plot_lines |
| 277 | |
| 278 | def write_data(plot_dimensions, plot_rectangles, plot_lines, fname): |
| 279 | """ |
| 280 | Write plot data out to file or screen. |
| 281 | |
| 282 | @param fname: Name of the output file, if specified. |
| 283 | @type fname: C{str} (??) |
| 284 | """ |
| 285 | if fname: |
| 286 | g = open(fname, 'w') |
| 287 | g.write('\n'.join(itertools.chain(plot_dimensions, plot_rectangles, |
| 288 | plot_lines))) |
| 289 | g.close() |
| 290 | else: |
| 291 | print '\n'.join(itertools.chain(plot_dimensions, plot_rectangles, |
| 292 | plot_lines)) |
| 293 | |
| 294 | def fmt_opt(short, long, arg, text): |
| 295 | if arg: |
| 296 | return '-%s %s, --%s%s\t%s' % (short[:-1], arg, long, arg, text) |
| 297 | else: |
| 298 | return '-%s, --%s\t%s' % (short, long, text) |
| 299 | |
| 300 | def make_default_options(): |
| 301 | option_values = DummyClass() |
| 302 | option_values.outputfile = '' |
| 303 | option_values.colorfile = '' |
| 304 | option_values.alphasort = False |
| 305 | option_values.plottitle = '' |
| 306 | return option_values |
| 307 | |
| 308 | def process_options(): |
| 309 | """ |
| 310 | Handle option and command-line argument processing. |
| 311 | |
| 312 | @return: Options and gantt input filename. |
| 313 | @rtype: L{OptionParser} options, C{str} |
| 314 | """ |
| 315 | optdefs = [('o:', 'output=', 'FILE', 'Write output to FILE.'), |
| 316 | ('c:', 'color=', 'FILE', 'Use task colors (RGB) as defined in ' |
| 317 | 'configuration FILE (in RGB triplets,\n\t\t\t\tGnuplot ' |
| 318 | 'colornames, or hexadecimal representations.'), |
| 319 | ('a', 'alpha', '', '\t\tShow resources and tasks in ' |
| 320 | 'alphabetical order.'), |
| 321 | ('t:','title=', 'TITLE', 'Set plot title to TITLE (between ' |
| 322 | 'double quotes).'), |
| 323 | ('h', 'help', '', '\t\tShow online help.')] |
| 324 | short_opts = ''.join(opt[0] for opt in optdefs if opt[0]) |
| 325 | long_opts = [opt[1] for opt in optdefs if opt[1]] |
| 326 | usage_text = 'gantt.py [options] gantt-file\nwhere\n' + \ |
| 327 | '\n'.join(' ' + fmt_opt(*opt) for opt in optdefs) |
| 328 | |
| 329 | option_values = make_default_options() |
| 330 | |
| 331 | try: |
| 332 | opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts) |
| 333 | except getopt.GetoptError, err: |
| 334 | sys.stderr.write("gantt.py: %s\n" % err) |
| 335 | sys.exit(2) |
| 336 | |
| 337 | for opt, optval in opts: |
| 338 | if opt in ('-o', '--output'): |
| 339 | option_values.outputfile = optval |
| 340 | continue |
| 341 | if opt in ('-c', '--color'): |
| 342 | option_values.colorfile = optval |
| 343 | continue |
| 344 | if opt in ('-a', '--alphasort'): |
| 345 | option_values.alphasort = True |
| 346 | continue |
| 347 | if opt in ('-t', '--title'): |
| 348 | option_values.plottitle = optval |
| 349 | continue |
| 350 | if opt in ('-h', '--help'): |
| 351 | print usage_text |
| 352 | sys.exit(0) |
| 353 | |
| 354 | # Check if correct number of arguments is supplied |
| 355 | if len(args) != 1: |
| 356 | sys.stderr.write('gantty.py: incorrect number of arguments ' |
| 357 | '(task/resource file expected)\n') |
| 358 | sys.exit(1) |
| 359 | |
| 360 | return option_values, args[0] |
| 361 | |
| 362 | def compute(options, ganttfile): |
| 363 | activities = load_ganttfile(ganttfile) |
| 364 | tasks, resources = make_unique_tasks_resources(options.alphasort, |
| 365 | activities) |
| 366 | |
| 367 | # Assign indices to resources |
| 368 | resource_map = dict(itertools.izip(resources, itertools.count(1))) |
| 369 | |
| 370 | color_book = ColorBook(options.colorfile, tasks) |
| 371 | rectangles = make_rectangles(activities, resource_map, color_book.colors) |
| 372 | |
| 373 | plot_dims, plot_rects, plot_lines = \ |
| 374 | generate_plotdata(activities, resources, tasks, rectangles, |
| 375 | options, resource_map, color_book) |
| 376 | |
| 377 | write_data(plot_dims, plot_rects, plot_lines, options.outputfile) |
| 378 | |
| 379 | def run(): |
| 380 | options, ganttfile = process_options() |
| 381 | compute(options, ganttfile) |
| 382 | |
| 383 | |
| 384 | if __name__ == '__main__': |
| 385 | run() |
| 386 | |
| 387 | |