blob: faf9e94f8d5f5a337de5d0cb583b6282be0eb37d [file] [log] [blame]
Ian Maxona70fba52016-02-18 13:52:36 -08001#!/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
17import itertools, sys, getopt
18from ConfigParser import ConfigParser
19
20rectangleHeight = 0.8 #: Height of a rectangle in units.
21
22class 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
44class 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
55class 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
134class DummyClass(object):
135 """
136 Dummy class for storing option values in.
137 """
138
139
140def 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
167def 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
191def 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
224def 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
278def 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
294def 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
300def 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
308def 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
362def 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
379def run():
380 options, ganttfile = process_options()
381 compute(options, ganttfile)
382
383
384if __name__ == '__main__':
385 run()
386
387